@gtkx/vitest 2.0.0-beta.3 → 2.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ declare const HEADLESS_RUNTIME_MARKER = ".gtkx-headless-runtime";
2
+ declare const createSwayConfig: (width: string, height: string) => string;
3
+ declare const createBusConfig: (busSocketPath: string) => string;
4
+ declare const createHeadlessRuntimeMarker: (runtimeDir: string) => string;
5
+ declare const isSwayConfig: (value: string) => boolean;
6
+ export { createBusConfig, createHeadlessRuntimeMarker, createSwayConfig, HEADLESS_RUNTIME_MARKER, isSwayConfig, };
7
+ //# sourceMappingURL=headless-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headless-config.d.ts","sourceRoot":"","sources":["../src/headless-config.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,uBAAuB,2BAA2B,CAAC;AAazD,QAAA,MAAM,gBAAgB,GAAI,OAAO,MAAM,EAAE,QAAQ,MAAM,KAAG,MAU1C,CAAC;AAEjB,QAAA,MAAM,eAAe,GAAI,eAAe,MAAM,KAAG,MAajC,CAAC;AAEjB,QAAA,MAAM,2BAA2B,GAAI,YAAY,MAAM,KAAG,MACc,CAAC;AAEzE,QAAA,MAAM,YAAY,GAAI,OAAO,MAAM,KAAG,OAKrC,CAAC;AAEF,OAAO,EACH,eAAe,EACf,2BAA2B,EAC3B,gBAAgB,EAChB,uBAAuB,EACvB,YAAY,GACf,CAAC"}
@@ -0,0 +1,44 @@
1
+ const BUS_CONFIG_DOCTYPE = '<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" ' +
2
+ '"https://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">';
3
+ const HEADLESS_RUNTIME_MARKER = ".gtkx-headless-runtime";
4
+ const SWAY_CONFIG_LINES = [
5
+ /^xwayland disable$/,
6
+ /^default_border none$/,
7
+ /^default_floating_border none$/,
8
+ /^output HEADLESS-1 resolution [1-9]\d*x[1-9]\d*$/,
9
+ /^output HEADLESS-1 bg #000000 solid_color$/,
10
+ /^for_window \[app_id="\.\*"\] floating enable, border none$/,
11
+ /^for_window \[title="\.\*"\] floating enable, border none$/,
12
+ /^$/,
13
+ ];
14
+ const createSwayConfig = (width, height) => [
15
+ "xwayland disable",
16
+ "default_border none",
17
+ "default_floating_border none",
18
+ `output HEADLESS-1 resolution ${width}x${height}`,
19
+ "output HEADLESS-1 bg #000000 solid_color",
20
+ 'for_window [app_id=".*"] floating enable, border none',
21
+ 'for_window [title=".*"] floating enable, border none',
22
+ "",
23
+ ].join("\n");
24
+ const createBusConfig = (busSocketPath) => [
25
+ BUS_CONFIG_DOCTYPE,
26
+ "<busconfig>",
27
+ " <type>session</type>",
28
+ ` <listen>unix:path=${busSocketPath}</listen>`,
29
+ " <auth>EXTERNAL</auth>",
30
+ ' <policy context="default">',
31
+ ' <allow send_destination="*" eavesdrop="true"/>',
32
+ ' <allow eavesdrop="true"/>',
33
+ ' <allow own="*"/>',
34
+ " </policy>",
35
+ "</busconfig>",
36
+ ].join("\n");
37
+ const createHeadlessRuntimeMarker = (runtimeDir) => ["gtkx-headless-runtime-v1", `runtime=${runtimeDir}`, ""].join("\n");
38
+ const isSwayConfig = (value) => {
39
+ const lines = value.split("\n");
40
+ return lines.length === SWAY_CONFIG_LINES.length &&
41
+ SWAY_CONFIG_LINES.every((pattern, index) => pattern.test(lines[index] ?? "invalid"));
42
+ };
43
+ export { createBusConfig, createHeadlessRuntimeMarker, createSwayConfig, HEADLESS_RUNTIME_MARKER, isSwayConfig, };
44
+ //# sourceMappingURL=headless-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headless-config.js","sourceRoot":"","sources":["../src/headless-config.ts"],"names":[],"mappings":"AAAA,MAAM,kBAAkB,GACpB,mFAAmF;IACnF,iEAAiE,CAAC;AAEtE,MAAM,uBAAuB,GAAG,wBAAwB,CAAC;AAEzD,MAAM,iBAAiB,GAAG;IACtB,oBAAoB;IACpB,uBAAuB;IACvB,gCAAgC;IAChC,kDAAkD;IAClD,4CAA4C;IAC5C,6DAA6D;IAC7D,4DAA4D;IAC5D,IAAI;CACP,CAAC;AAEF,MAAM,gBAAgB,GAAG,CAAC,KAAa,EAAE,MAAc,EAAU,EAAE,CAC/D;IACI,kBAAkB;IAClB,qBAAqB;IACrB,8BAA8B;IAC9B,gCAAgC,KAAK,IAAI,MAAM,EAAE;IACjD,0CAA0C;IAC1C,uDAAuD;IACvD,sDAAsD;IACtD,EAAE;CACL,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEjB,MAAM,eAAe,GAAG,CAAC,aAAqB,EAAU,EAAE,CACtD;IACI,kBAAkB;IAClB,aAAa;IACb,wBAAwB;IACxB,uBAAuB,aAAa,WAAW;IAC/C,yBAAyB;IACzB,8BAA8B;IAC9B,oDAAoD;IACpD,+BAA+B;IAC/B,sBAAsB;IACtB,aAAa;IACb,cAAc;CACjB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEjB,MAAM,2BAA2B,GAAG,CAAC,UAAkB,EAAU,EAAE,CAC/D,CAAC,0BAA0B,EAAE,WAAW,UAAU,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEzE,MAAM,YAAY,GAAG,CAAC,KAAa,EAAW,EAAE;IAC5C,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAEhC,OAAO,KAAK,CAAC,MAAM,KAAK,iBAAiB,CAAC,MAAM;QAC5C,iBAAiB,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC;AAC7F,CAAC,CAAC;AAEF,OAAO,EACH,eAAe,EACf,2BAA2B,EAC3B,gBAAgB,EAChB,uBAAuB,EACvB,YAAY,GACf,CAAC","sourcesContent":["const BUS_CONFIG_DOCTYPE =\n '<!DOCTYPE busconfig PUBLIC \"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN\" ' +\n '\"https://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd\">';\n\nconst HEADLESS_RUNTIME_MARKER = \".gtkx-headless-runtime\";\n\nconst SWAY_CONFIG_LINES = [\n /^xwayland disable$/,\n /^default_border none$/,\n /^default_floating_border none$/,\n /^output HEADLESS-1 resolution [1-9]\\d*x[1-9]\\d*$/,\n /^output HEADLESS-1 bg #000000 solid_color$/,\n /^for_window \\[app_id=\"\\.\\*\"\\] floating enable, border none$/,\n /^for_window \\[title=\"\\.\\*\"\\] floating enable, border none$/,\n /^$/,\n];\n\nconst createSwayConfig = (width: string, height: string): string =>\n [\n \"xwayland disable\",\n \"default_border none\",\n \"default_floating_border none\",\n `output HEADLESS-1 resolution ${width}x${height}`,\n \"output HEADLESS-1 bg #000000 solid_color\",\n 'for_window [app_id=\".*\"] floating enable, border none',\n 'for_window [title=\".*\"] floating enable, border none',\n \"\",\n ].join(\"\\n\");\n\nconst createBusConfig = (busSocketPath: string): string =>\n [\n BUS_CONFIG_DOCTYPE,\n \"<busconfig>\",\n \" <type>session</type>\",\n ` <listen>unix:path=${busSocketPath}</listen>`,\n \" <auth>EXTERNAL</auth>\",\n ' <policy context=\"default\">',\n ' <allow send_destination=\"*\" eavesdrop=\"true\"/>',\n ' <allow eavesdrop=\"true\"/>',\n ' <allow own=\"*\"/>',\n \" </policy>\",\n \"</busconfig>\",\n ].join(\"\\n\");\n\nconst createHeadlessRuntimeMarker = (runtimeDir: string): string =>\n [\"gtkx-headless-runtime-v1\", `runtime=${runtimeDir}`, \"\"].join(\"\\n\");\n\nconst isSwayConfig = (value: string): boolean => {\n const lines = value.split(\"\\n\");\n\n return lines.length === SWAY_CONFIG_LINES.length &&\n SWAY_CONFIG_LINES.every((pattern, index) => pattern.test(lines[index] ?? \"invalid\"));\n};\n\nexport {\n createBusConfig,\n createHeadlessRuntimeMarker,\n createSwayConfig,\n HEADLESS_RUNTIME_MARKER,\n isSwayConfig,\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"headless-display.d.ts","sourceRoot":"","sources":["../src/headless-display.ts"],"names":[],"mappings":"AASA;;GAEG;AACH,KAAK,YAAY,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEtC;;GAEG;AACH,KAAK,eAAe,GAAG;IACnB,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,UAAU,EAAE,YAAY,CAAC;CAC5B,CAAC;AAgHF,QAAA,MAAM,mBAAmB;;;;;;;;;;;CAWxB,CAAC;AAEF,QAAA,MAAM,sBAAsB,GAAI,UAAU,OAAO,CAAC,eAAe,CAAC,KAAG,eAWpE,CAAC;AAqCF,QAAA,MAAM,mBAAmB,GAAI,QAAQ,eAAe,KAAG,OAAO,CAAC,eAAe,CAe7E,CAAC;AAyPF,QAAA,MAAM,oBAAoB,GAAU,SAAS,eAAe,KAAG,OAAO,CAAC,MAAM,IAAI,CAsDhF,CAAC;AAEF,OAAO,EACH,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACpB,KAAK,YAAY,EACjB,KAAK,eAAe,GACvB,CAAC"}
1
+ {"version":3,"file":"headless-display.d.ts","sourceRoot":"","sources":["../src/headless-display.ts"],"names":[],"mappings":"AAeA;;GAEG;AACH,KAAK,YAAY,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEtC;;GAEG;AACH,KAAK,eAAe,GAAG;IACnB,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,UAAU,EAAE,YAAY,CAAC;CAC5B,CAAC;AAmGF,QAAA,MAAM,mBAAmB;;;;;;;;;;;CAWxB,CAAC;AAEF,QAAA,MAAM,sBAAsB,GAAI,UAAU,OAAO,CAAC,eAAe,CAAC,KAAG,eAWpE,CAAC;AAqCF,QAAA,MAAM,mBAAmB,GAAI,QAAQ,eAAe,KAAG,OAAO,CAAC,eAAe,CAe7E,CAAC;AA0PF,QAAA,MAAM,oBAAoB,GAAU,SAAS,eAAe,KAAG,OAAO,CAAC,MAAM,IAAI,CAqDhF,CAAC;AAEF,OAAO,EACH,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACpB,KAAK,YAAY,EACjB,KAAK,eAAe,GACvB,CAAC"}
@@ -4,13 +4,12 @@ import { chmodSync, closeSync, existsSync, mkdtempSync, openSync, rmSync, writeF
4
4
  import { Socket } from "node:net";
5
5
  import { tmpdir } from "node:os";
6
6
  import { join } from "node:path";
7
+ import { createBusConfig, createHeadlessRuntimeMarker, createSwayConfig, HEADLESS_RUNTIME_MARKER, } from "./headless-config.js";
7
8
  import { startNotificationService } from "./notification-service.js";
8
9
  import { startVirtualSeat } from "./virtual-seat.js";
9
10
  const DEFAULT_HEADLESS_SIZE = "1024x768";
10
11
  const DEFAULT_HEADLESS_COMPOSITOR = "sway";
11
12
  const HEADLESS_SIZE_PATTERN = /^[1-9]\d*x[1-9]\d*$/;
12
- const BUS_CONFIG_DOCTYPE = '<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" ' +
13
- '"https://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">';
14
13
  const hasWestonFakeSeat = createWestonFakeSeatProbe();
15
14
  const compositorRegistry = {
16
15
  sway: {
@@ -25,16 +24,7 @@ const compositorRegistry = {
25
24
  },
26
25
  start: (runtimeDir, width, height) => {
27
26
  const configPath = join(runtimeDir, "sway.conf");
28
- writeFileSync(configPath, [
29
- "xwayland disable",
30
- "default_border none",
31
- "default_floating_border none",
32
- `output HEADLESS-1 resolution ${width}x${height}`,
33
- "output HEADLESS-1 bg #000000 solid_color",
34
- 'for_window [app_id=".*"] floating enable, border none',
35
- 'for_window [title=".*"] floating enable, border none',
36
- "",
37
- ].join("\n"));
27
+ writeFileSync(configPath, createSwayConfig(width, height));
38
28
  return spawnWithParentDeathSupervisor("sway", ["-c", configPath], {
39
29
  stdio: ["ignore", "ignore", "pipe"],
40
30
  cleanupDirectory: runtimeDir,
@@ -131,19 +121,7 @@ const startCompositor = (runtimeDir, options, env) => {
131
121
  const noVirtualSeat = () => undefined;
132
122
  const attachVirtualSeat = (compositor, socketPath) => compositor.requiresVirtualSeat ? startVirtualSeat(socketPath) : Promise.resolve(noVirtualSeat);
133
123
  const writeBusConfig = (busConfigPath, busSocketPath) => {
134
- writeFileSync(busConfigPath, [
135
- BUS_CONFIG_DOCTYPE,
136
- "<busconfig>",
137
- " <type>session</type>",
138
- ` <listen>unix:path=${busSocketPath}</listen>`,
139
- " <auth>EXTERNAL</auth>",
140
- ' <policy context="default">',
141
- ' <allow send_destination="*" eavesdrop="true"/>',
142
- ' <allow eavesdrop="true"/>',
143
- ' <allow own="*"/>',
144
- " </policy>",
145
- "</busconfig>",
146
- ].join("\n"));
124
+ writeFileSync(busConfigPath, createBusConfig(busSocketPath));
147
125
  };
148
126
  const exitedMessage = (label, path, code, signal) => `${label} exited (code ${String(code)}, signal ${signal ?? "null"}) before ${path} appeared`;
149
127
  const monitorChild = (child, label, path) => {
@@ -300,10 +278,23 @@ const makeTeardown = (stops) => {
300
278
  runCleanups(stops);
301
279
  };
302
280
  };
281
+ const createHeadlessRuntimeDirectory = () => {
282
+ const runtimeDir = mkdtempSync(join(tmpdir(), "gtkx-xdg-"));
283
+ try {
284
+ chmodSync(runtimeDir, 0o700);
285
+ const markerPath = join(runtimeDir, HEADLESS_RUNTIME_MARKER);
286
+ writeFileSync(markerPath, createHeadlessRuntimeMarker(runtimeDir), { flag: "wx", mode: 0o600 });
287
+ chmodSync(markerPath, 0o600);
288
+ return runtimeDir;
289
+ }
290
+ catch (error) {
291
+ rmSync(runtimeDir, { recursive: true, force: true });
292
+ throw error;
293
+ }
294
+ };
303
295
  const startHeadlessDisplay = async (options) => {
304
296
  const env = {};
305
- const runtimeDir = mkdtempSync(join(tmpdir(), "gtkx-xdg-"));
306
- chmodSync(runtimeDir, 0o700);
297
+ const runtimeDir = createHeadlessRuntimeDirectory();
307
298
  const spawned = [];
308
299
  const removeRuntime = () => {
309
300
  restoreEnv(env);
@@ -1 +1 @@
1
- {"version":3,"file":"headless-display.js","sourceRoot":"","sources":["../src/headless-display.ts"],"names":[],"mappings":"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","sourcesContent":["import { resolveExecutable, spawnWithParentDeathSignal, spawnWithParentDeathSupervisor } from \"@gtkx/utils\";\nimport { type ChildProcess, spawnSync } from \"node:child_process\";\nimport { chmodSync, closeSync, existsSync, mkdtempSync, openSync, rmSync, writeFileSync, writeSync } from \"node:fs\";\nimport { Socket } from \"node:net\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { startNotificationService } from \"./notification-service.ts\";\nimport { startVirtualSeat } from \"./virtual-seat.ts\";\n\n/**\n * Wayland compositors that can back a headless display.\n */\ntype CompositorId = \"sway\" | \"weston\";\n\n/**\n * Settings for the per-worker headless Wayland display.\n */\ntype HeadlessOptions = {\n /** Output resolution as a \"WIDTHxHEIGHT\" string, for example \"1024x768\". */\n size: string;\n /** Wayland compositor used to back the headless display. */\n compositor: CompositorId;\n};\n\ntype EnvSnapshot = Record<string, string | undefined>;\n\ntype CompositorDescriptor = {\n socket: string;\n env: Record<string, string>;\n requiresVirtualSeat: boolean;\n start: (runtimeDir: string, width: string, height: string) => ChildProcess;\n};\n\ntype SpawnedCompositor = {\n child: ChildProcess;\n socket: string;\n requiresVirtualSeat: boolean;\n};\n\ntype ChildMonitor = {\n label: string;\n path: string;\n read: () => string;\n failure: () => string | undefined;\n subscribe: (notify: (failure: string) => void) => () => void;\n stop: () => void;\n};\n\ntype WaitForSocketOptions = {\n monitor: ChildMonitor;\n guards?: ChildMonitor[];\n timeout?: number;\n};\n\ntype DisplaySockets = {\n compositorMonitor: ChildMonitor;\n busMonitor: ChildMonitor;\n};\n\ntype CapturedStderr = {\n chunks: string[];\n stop: () => void;\n};\n\ntype SocketWatch = {\n options: WaitForSocketOptions;\n resolve: () => void;\n reject: (error: Error) => void;\n};\n\nconst DEFAULT_HEADLESS_SIZE = \"1024x768\";\nconst DEFAULT_HEADLESS_COMPOSITOR: CompositorId = \"sway\";\nconst HEADLESS_SIZE_PATTERN = /^[1-9]\\d*x[1-9]\\d*$/;\n\nconst BUS_CONFIG_DOCTYPE =\n '<!DOCTYPE busconfig PUBLIC \"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN\" ' +\n '\"https://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd\">';\n\nconst hasWestonFakeSeat = createWestonFakeSeatProbe();\n\nconst compositorRegistry: Record<CompositorId, CompositorDescriptor> = {\n sway: {\n socket: \"wayland-1\",\n requiresVirtualSeat: true,\n env: {\n WLR_BACKENDS: \"headless\",\n WLR_RENDERER: \"pixman\",\n WLR_RENDERER_ALLOW_SOFTWARE: \"1\",\n WLR_LIBINPUT_NO_DEVICES: \"1\",\n WLR_HEADLESS_OUTPUTS: \"1\",\n },\n start: (runtimeDir, width, height) => {\n const configPath = join(runtimeDir, \"sway.conf\");\n\n writeFileSync(\n configPath,\n [\n \"xwayland disable\",\n \"default_border none\",\n \"default_floating_border none\",\n `output HEADLESS-1 resolution ${width}x${height}`,\n \"output HEADLESS-1 bg #000000 solid_color\",\n 'for_window [app_id=\".*\"] floating enable, border none',\n 'for_window [title=\".*\"] floating enable, border none',\n \"\",\n ].join(\"\\n\"),\n );\n\n return spawnWithParentDeathSupervisor(\"sway\", [\"-c\", configPath], {\n stdio: [\"ignore\", \"ignore\", \"pipe\"],\n cleanupDirectory: runtimeDir,\n });\n },\n },\n weston: {\n socket: \"wayland-0\",\n requiresVirtualSeat: false,\n env: {},\n start: (_runtimeDir, width, height) =>\n spawnWithParentDeathSignal(\n \"weston\",\n [\n \"--backend=headless\",\n \"--renderer=pixman\",\n ...(hasWestonFakeSeat() ? [\"--fake-seat\"] : []),\n `--width=${width}`,\n `--height=${height}`,\n \"--socket=wayland-0\",\n ],\n { stdio: [\"ignore\", \"ignore\", \"pipe\"], cleanupDirectories: [_runtimeDir] },\n ),\n },\n};\n\nconst STATIC_HEADLESS_ENV = {\n GDK_BACKEND: \"wayland\",\n GDK_DISABLE: \"vulkan\",\n GDK_DEBUG: \"no-vsync\",\n GSK_RENDERER: \"cairo\",\n GTK_A11Y: \"test\",\n LIBGL_ALWAYS_SOFTWARE: \"1\",\n GST_GL_WINDOW: \"none\",\n GSETTINGS_BACKEND: \"memory\",\n ALSOFT_DRIVERS: \"null\",\n ALSOFT_LOGLEVEL: \"0\",\n};\n\nconst resolveHeadlessOptions = (provided: Partial<HeadlessOptions>): HeadlessOptions => {\n const size = provided.size ?? DEFAULT_HEADLESS_SIZE;\n\n if (!HEADLESS_SIZE_PATTERN.test(size)) {\n throw new Error(`Invalid headless display size: ${size}`);\n }\n\n return {\n size,\n compositor: provided.compositor ?? DEFAULT_HEADLESS_COMPOSITOR,\n };\n};\n\nconst applyEnv = (snapshot: EnvSnapshot, values: Record<string, string>): void => {\n for (const [name, value] of Object.entries(values)) {\n if (!Object.hasOwn(snapshot, name)) {\n snapshot[name] = process.env[name];\n }\n\n process.env[name] = value;\n }\n};\n\nconst restoreEnv = (snapshot: EnvSnapshot): void => {\n for (const [name, previous] of Object.entries(snapshot)) {\n if (previous === undefined) {\n Reflect.deleteProperty(process.env, name);\n } else {\n process.env[name] = previous;\n }\n }\n};\n\nfunction createWestonFakeSeatProbe(): () => boolean {\n let isSupported: boolean | undefined;\n\n return () => {\n if (isSupported === undefined) {\n const help = spawnSync(resolveExecutable(\"weston\"), [\"--help\"], { encoding: \"utf8\" });\n isSupported = `${help.stdout}${help.stderr}`.includes(\"--fake-seat\");\n }\n\n return isSupported;\n };\n}\n\nconst isCompositorId = (value: string): value is CompositorId => Object.hasOwn(compositorRegistry, value);\n\nconst readHeadlessOptions = (params: URLSearchParams): Partial<HeadlessOptions> => {\n const options: Partial<HeadlessOptions> = {};\n const size = params.get(\"size\");\n\n if (size !== null) {\n options.size = size;\n }\n\n const compositor = params.get(\"compositor\");\n\n if (compositor !== null && isCompositorId(compositor)) {\n options.compositor = compositor;\n }\n\n return options;\n};\n\nconst startCompositor = (runtimeDir: string, options: HeadlessOptions, env: EnvSnapshot): SpawnedCompositor => {\n const descriptor = compositorRegistry[options.compositor];\n const [width = \"\", height = \"\"] = options.size.split(\"x\", 2);\n applyEnv(env, descriptor.env);\n\n return {\n child: descriptor.start(runtimeDir, width, height),\n socket: descriptor.socket,\n requiresVirtualSeat: descriptor.requiresVirtualSeat,\n };\n};\n\nconst noVirtualSeat = (): void => undefined;\n\nconst attachVirtualSeat = (compositor: SpawnedCompositor, socketPath: string): Promise<() => void> =>\n compositor.requiresVirtualSeat ? startVirtualSeat(socketPath) : Promise.resolve(noVirtualSeat);\n\nconst writeBusConfig = (busConfigPath: string, busSocketPath: string): void => {\n writeFileSync(\n busConfigPath,\n [\n BUS_CONFIG_DOCTYPE,\n \"<busconfig>\",\n \" <type>session</type>\",\n ` <listen>unix:path=${busSocketPath}</listen>`,\n \" <auth>EXTERNAL</auth>\",\n ' <policy context=\"default\">',\n ' <allow send_destination=\"*\" eavesdrop=\"true\"/>',\n ' <allow eavesdrop=\"true\"/>',\n ' <allow own=\"*\"/>',\n \" </policy>\",\n \"</busconfig>\",\n ].join(\"\\n\"),\n );\n};\n\nconst exitedMessage = (label: string, path: string, code: number | null, signal: NodeJS.Signals | null): string =>\n `${label} exited (code ${String(code)}, signal ${signal ?? \"null\"}) before ${path} appeared`;\n\nconst monitorChild = (child: ChildProcess, label: string, path: string): ChildMonitor => {\n const stderr = child.stderr;\n const subscribers: Set<(failure: string) => void> = new Set();\n let log = \"\";\n let failure: string | undefined;\n\n const onData = (chunk: string): void => {\n log += chunk;\n };\n\n stderr?.setEncoding(\"utf8\");\n stderr?.on(\"data\", onData);\n\n const record = (cause: string): void => {\n failure ??= cause;\n\n for (const notify of subscribers) {\n notify(cause);\n }\n };\n\n child.on(\"close\", (code: number | null, signal: NodeJS.Signals | null) => {\n record(`${exitedMessage(label, path, code, signal)}\\n${log}`);\n });\n\n child.on(\"error\", (cause: Error) => {\n record(`${label} failed to spawn before ${path} appeared: ${cause.message}\\n${log}`);\n });\n\n return {\n label,\n path,\n read: () => log,\n failure: () => failure,\n subscribe: (notify) => {\n subscribers.add(notify);\n\n return (): void => {\n subscribers.delete(notify);\n };\n },\n stop: () => {\n stderr?.removeListener(\"data\", onData);\n stderr?.resume();\n\n if (stderr instanceof Socket) {\n stderr.unref();\n }\n },\n };\n};\n\nconst runCleanups = (cleanups: (() => void)[]): void => {\n for (const cleanup of cleanups) {\n cleanup();\n }\n\n cleanups.length = 0;\n};\n\nconst pollForPath = (path: string, onFound: () => void): NodeJS.Timeout =>\n setInterval(() => {\n if (existsSync(path)) {\n onFound();\n }\n }, 50);\n\nconst firstFailure = (monitors: ChildMonitor[]): string | undefined => {\n for (const monitor of monitors) {\n const failure = monitor.failure();\n\n if (failure !== undefined) {\n return failure;\n }\n }\n\n return undefined;\n};\n\nconst watchForSocket = ({ options, resolve, reject }: SocketWatch): void => {\n const { monitor, guards = [], timeout = 15_000 } = options;\n const watched = [monitor, ...guards];\n const cleanups: (() => void)[] = [monitor.stop];\n\n const stop = (): void => {\n runCleanups(cleanups);\n };\n\n const fail = (message: string): void => {\n stop();\n\n for (const guard of guards) {\n guard.stop();\n }\n\n reject(new Error(message));\n };\n\n const alreadyFailed = firstFailure(watched);\n\n if (alreadyFailed !== undefined) {\n fail(alreadyFailed);\n\n return;\n }\n\n const poll = pollForPath(monitor.path, () => {\n stop();\n resolve();\n });\n\n const timer = setTimeout(() => {\n fail(`${monitor.label} did not become available within ${String(timeout)}ms\\n${monitor.read()}`);\n }, timeout);\n\n cleanups.push(\n () => {\n clearInterval(poll);\n clearTimeout(timer);\n },\n ...watched.map((entry) => entry.subscribe(fail)),\n );\n};\n\nconst waitForSocket = (options: WaitForSocketOptions): Promise<void> =>\n new Promise((resolve, reject) => {\n watchForSocket({ options, resolve, reject });\n });\n\nconst captureCompositorStderr = (child: ChildProcess, logPath: string): CapturedStderr => {\n const captured: string[] = [];\n const stderr = child.stderr;\n\n if (stderr === null) {\n return { chunks: captured, stop: noVirtualSeat };\n }\n\n stderr.setEncoding(\"utf8\");\n const descriptor = openSync(logPath, \"a\");\n let isOpen = true;\n\n const onData = (chunk: string): void => {\n captured.push(chunk);\n\n try {\n writeSync(descriptor, chunk);\n } catch {\n return;\n }\n };\n\n stderr.on(\"data\", onData);\n\n return {\n chunks: captured,\n stop: () => {\n stderr.removeListener(\"data\", onData);\n\n if (isOpen) {\n isOpen = false;\n closeSync(descriptor);\n }\n },\n };\n};\n\nconst compositorExitMessage = (\n code: number | null,\n signal: NodeJS.Signals | null,\n capturedStderr: string[],\n): string =>\n `[gtkx] the headless compositor exited (code ${String(code)}, signal ${signal ?? \"null\"}); ` +\n `every Wayland client in this worker has been severed.\\n${capturedStderr.join(\"\")}`;\n\nconst watchCompositorExit = (child: ChildProcess, capturedStderr: string[]): (() => void) => {\n const report = (code: number | null, signal: NodeJS.Signals | null): void => {\n process.stderr.write(compositorExitMessage(code, signal, capturedStderr));\n };\n\n child.on(\"exit\", report);\n\n return () => child.removeListener(\"exit\", report);\n};\n\nconst waitForDisplaySockets = async ({ compositorMonitor, busMonitor }: DisplaySockets): Promise<void> => {\n await waitForSocket({ monitor: busMonitor, guards: [compositorMonitor] });\n await waitForSocket({ monitor: compositorMonitor });\n};\n\nconst killSpawned = (children: ChildProcess[]): void => {\n for (const child of children) {\n child.kill(\"SIGTERM\");\n }\n};\n\nconst makeTeardown = (stops: (() => void)[]): (() => void) => {\n let isTorndown = false;\n\n return (): void => {\n if (isTorndown) {\n return;\n }\n\n isTorndown = true;\n runCleanups(stops);\n };\n};\n\nconst startHeadlessDisplay = async (options: HeadlessOptions): Promise<() => void> => {\n const env: EnvSnapshot = {};\n const runtimeDir = mkdtempSync(join(tmpdir(), \"gtkx-xdg-\"));\n chmodSync(runtimeDir, 0o700);\n const spawned: ChildProcess[] = [];\n\n const removeRuntime = (): void => {\n restoreEnv(env);\n rmSync(runtimeDir, { recursive: true, force: true });\n };\n\n try {\n applyEnv(env, STATIC_HEADLESS_ENV);\n applyEnv(env, { XDG_RUNTIME_DIR: runtimeDir });\n const busConfigPath = join(runtimeDir, \"session.conf\");\n const busSocketPath = join(runtimeDir, \"bus\");\n writeBusConfig(busConfigPath, busSocketPath);\n\n const busChild = spawnWithParentDeathSignal(\"dbus-daemon\", [`--config-file=${busConfigPath}`], {\n stdio: [\"ignore\", \"ignore\", \"pipe\"],\n cleanupDirectories: [runtimeDir],\n });\n\n busChild.unref();\n spawned.push(busChild);\n const busMonitor = monitorChild(busChild, \"D-Bus session bus\", busSocketPath);\n applyEnv(env, { DBUS_SESSION_BUS_ADDRESS: `unix:path=${busSocketPath}` });\n const compositor = startCompositor(runtimeDir, options, env);\n compositor.child.unref();\n spawned.push(compositor.child);\n const compositorSocketPath = join(runtimeDir, compositor.socket);\n const compositorMonitor = monitorChild(compositor.child, \"Compositor\", compositorSocketPath);\n applyEnv(env, { WAYLAND_DISPLAY: compositor.socket });\n await waitForDisplaySockets({ compositorMonitor, busMonitor });\n const stopVirtualSeat = await attachVirtualSeat(compositor, compositorSocketPath);\n const stopNotifications = await startNotificationService(`unix:path=${busSocketPath}`);\n const capturedStderr = captureCompositorStderr(compositor.child, join(runtimeDir, \"compositor.stderr.log\"));\n const stopExitWatch = watchCompositorExit(compositor.child, capturedStderr.chunks);\n\n return makeTeardown([\n stopExitWatch,\n capturedStderr.stop,\n () => {\n killSpawned(spawned);\n },\n stopVirtualSeat,\n stopNotifications,\n removeRuntime,\n ]);\n } catch (error) {\n killSpawned(spawned);\n removeRuntime();\n throw error;\n }\n};\n\nexport {\n STATIC_HEADLESS_ENV,\n resolveHeadlessOptions,\n readHeadlessOptions,\n startHeadlessDisplay,\n type CompositorId,\n type HeadlessOptions,\n};\n"]}
1
+ {"version":3,"file":"headless-display.js","sourceRoot":"","sources":["../src/headless-display.ts"],"names":[],"mappings":"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","sourcesContent":["import { resolveExecutable, spawnWithParentDeathSignal, spawnWithParentDeathSupervisor } from \"@gtkx/utils\";\nimport { type ChildProcess, spawnSync } from \"node:child_process\";\nimport { chmodSync, closeSync, existsSync, mkdtempSync, openSync, rmSync, writeFileSync, writeSync } from \"node:fs\";\nimport { Socket } from \"node:net\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport {\n createBusConfig,\n createHeadlessRuntimeMarker,\n createSwayConfig,\n HEADLESS_RUNTIME_MARKER,\n} from \"./headless-config.ts\";\nimport { startNotificationService } from \"./notification-service.ts\";\nimport { startVirtualSeat } from \"./virtual-seat.ts\";\n\n/**\n * Wayland compositors that can back a headless display.\n */\ntype CompositorId = \"sway\" | \"weston\";\n\n/**\n * Settings for the per-worker headless Wayland display.\n */\ntype HeadlessOptions = {\n /** Output resolution as a \"WIDTHxHEIGHT\" string, for example \"1024x768\". */\n size: string;\n /** Wayland compositor used to back the headless display. */\n compositor: CompositorId;\n};\n\ntype EnvSnapshot = Record<string, string | undefined>;\n\ntype CompositorDescriptor = {\n socket: string;\n env: Record<string, string>;\n requiresVirtualSeat: boolean;\n start: (runtimeDir: string, width: string, height: string) => ChildProcess;\n};\n\ntype SpawnedCompositor = {\n child: ChildProcess;\n socket: string;\n requiresVirtualSeat: boolean;\n};\n\ntype ChildMonitor = {\n label: string;\n path: string;\n read: () => string;\n failure: () => string | undefined;\n subscribe: (notify: (failure: string) => void) => () => void;\n stop: () => void;\n};\n\ntype WaitForSocketOptions = {\n monitor: ChildMonitor;\n guards?: ChildMonitor[];\n timeout?: number;\n};\n\ntype DisplaySockets = {\n compositorMonitor: ChildMonitor;\n busMonitor: ChildMonitor;\n};\n\ntype CapturedStderr = {\n chunks: string[];\n stop: () => void;\n};\n\ntype SocketWatch = {\n options: WaitForSocketOptions;\n resolve: () => void;\n reject: (error: Error) => void;\n};\n\nconst DEFAULT_HEADLESS_SIZE = \"1024x768\";\nconst DEFAULT_HEADLESS_COMPOSITOR: CompositorId = \"sway\";\nconst HEADLESS_SIZE_PATTERN = /^[1-9]\\d*x[1-9]\\d*$/;\n\nconst hasWestonFakeSeat = createWestonFakeSeatProbe();\n\nconst compositorRegistry: Record<CompositorId, CompositorDescriptor> = {\n sway: {\n socket: \"wayland-1\",\n requiresVirtualSeat: true,\n env: {\n WLR_BACKENDS: \"headless\",\n WLR_RENDERER: \"pixman\",\n WLR_RENDERER_ALLOW_SOFTWARE: \"1\",\n WLR_LIBINPUT_NO_DEVICES: \"1\",\n WLR_HEADLESS_OUTPUTS: \"1\",\n },\n start: (runtimeDir, width, height) => {\n const configPath = join(runtimeDir, \"sway.conf\");\n\n writeFileSync(\n configPath,\n createSwayConfig(width, height),\n );\n\n return spawnWithParentDeathSupervisor(\"sway\", [\"-c\", configPath], {\n stdio: [\"ignore\", \"ignore\", \"pipe\"],\n cleanupDirectory: runtimeDir,\n });\n },\n },\n weston: {\n socket: \"wayland-0\",\n requiresVirtualSeat: false,\n env: {},\n start: (_runtimeDir, width, height) =>\n spawnWithParentDeathSignal(\n \"weston\",\n [\n \"--backend=headless\",\n \"--renderer=pixman\",\n ...(hasWestonFakeSeat() ? [\"--fake-seat\"] : []),\n `--width=${width}`,\n `--height=${height}`,\n \"--socket=wayland-0\",\n ],\n { stdio: [\"ignore\", \"ignore\", \"pipe\"], cleanupDirectories: [_runtimeDir] },\n ),\n },\n};\n\nconst STATIC_HEADLESS_ENV = {\n GDK_BACKEND: \"wayland\",\n GDK_DISABLE: \"vulkan\",\n GDK_DEBUG: \"no-vsync\",\n GSK_RENDERER: \"cairo\",\n GTK_A11Y: \"test\",\n LIBGL_ALWAYS_SOFTWARE: \"1\",\n GST_GL_WINDOW: \"none\",\n GSETTINGS_BACKEND: \"memory\",\n ALSOFT_DRIVERS: \"null\",\n ALSOFT_LOGLEVEL: \"0\",\n};\n\nconst resolveHeadlessOptions = (provided: Partial<HeadlessOptions>): HeadlessOptions => {\n const size = provided.size ?? DEFAULT_HEADLESS_SIZE;\n\n if (!HEADLESS_SIZE_PATTERN.test(size)) {\n throw new Error(`Invalid headless display size: ${size}`);\n }\n\n return {\n size,\n compositor: provided.compositor ?? DEFAULT_HEADLESS_COMPOSITOR,\n };\n};\n\nconst applyEnv = (snapshot: EnvSnapshot, values: Record<string, string>): void => {\n for (const [name, value] of Object.entries(values)) {\n if (!Object.hasOwn(snapshot, name)) {\n snapshot[name] = process.env[name];\n }\n\n process.env[name] = value;\n }\n};\n\nconst restoreEnv = (snapshot: EnvSnapshot): void => {\n for (const [name, previous] of Object.entries(snapshot)) {\n if (previous === undefined) {\n Reflect.deleteProperty(process.env, name);\n } else {\n process.env[name] = previous;\n }\n }\n};\n\nfunction createWestonFakeSeatProbe(): () => boolean {\n let isSupported: boolean | undefined;\n\n return () => {\n if (isSupported === undefined) {\n const help = spawnSync(resolveExecutable(\"weston\"), [\"--help\"], { encoding: \"utf8\" });\n isSupported = `${help.stdout}${help.stderr}`.includes(\"--fake-seat\");\n }\n\n return isSupported;\n };\n}\n\nconst isCompositorId = (value: string): value is CompositorId => Object.hasOwn(compositorRegistry, value);\n\nconst readHeadlessOptions = (params: URLSearchParams): Partial<HeadlessOptions> => {\n const options: Partial<HeadlessOptions> = {};\n const size = params.get(\"size\");\n\n if (size !== null) {\n options.size = size;\n }\n\n const compositor = params.get(\"compositor\");\n\n if (compositor !== null && isCompositorId(compositor)) {\n options.compositor = compositor;\n }\n\n return options;\n};\n\nconst startCompositor = (runtimeDir: string, options: HeadlessOptions, env: EnvSnapshot): SpawnedCompositor => {\n const descriptor = compositorRegistry[options.compositor];\n const [width = \"\", height = \"\"] = options.size.split(\"x\", 2);\n applyEnv(env, descriptor.env);\n\n return {\n child: descriptor.start(runtimeDir, width, height),\n socket: descriptor.socket,\n requiresVirtualSeat: descriptor.requiresVirtualSeat,\n };\n};\n\nconst noVirtualSeat = (): void => undefined;\n\nconst attachVirtualSeat = (compositor: SpawnedCompositor, socketPath: string): Promise<() => void> =>\n compositor.requiresVirtualSeat ? startVirtualSeat(socketPath) : Promise.resolve(noVirtualSeat);\n\nconst writeBusConfig = (busConfigPath: string, busSocketPath: string): void => {\n writeFileSync(busConfigPath, createBusConfig(busSocketPath));\n};\n\nconst exitedMessage = (label: string, path: string, code: number | null, signal: NodeJS.Signals | null): string =>\n `${label} exited (code ${String(code)}, signal ${signal ?? \"null\"}) before ${path} appeared`;\n\nconst monitorChild = (child: ChildProcess, label: string, path: string): ChildMonitor => {\n const stderr = child.stderr;\n const subscribers: Set<(failure: string) => void> = new Set();\n let log = \"\";\n let failure: string | undefined;\n\n const onData = (chunk: string): void => {\n log += chunk;\n };\n\n stderr?.setEncoding(\"utf8\");\n stderr?.on(\"data\", onData);\n\n const record = (cause: string): void => {\n failure ??= cause;\n\n for (const notify of subscribers) {\n notify(cause);\n }\n };\n\n child.on(\"close\", (code: number | null, signal: NodeJS.Signals | null) => {\n record(`${exitedMessage(label, path, code, signal)}\\n${log}`);\n });\n\n child.on(\"error\", (cause: Error) => {\n record(`${label} failed to spawn before ${path} appeared: ${cause.message}\\n${log}`);\n });\n\n return {\n label,\n path,\n read: () => log,\n failure: () => failure,\n subscribe: (notify) => {\n subscribers.add(notify);\n\n return (): void => {\n subscribers.delete(notify);\n };\n },\n stop: () => {\n stderr?.removeListener(\"data\", onData);\n stderr?.resume();\n\n if (stderr instanceof Socket) {\n stderr.unref();\n }\n },\n };\n};\n\nconst runCleanups = (cleanups: (() => void)[]): void => {\n for (const cleanup of cleanups) {\n cleanup();\n }\n\n cleanups.length = 0;\n};\n\nconst pollForPath = (path: string, onFound: () => void): NodeJS.Timeout =>\n setInterval(() => {\n if (existsSync(path)) {\n onFound();\n }\n }, 50);\n\nconst firstFailure = (monitors: ChildMonitor[]): string | undefined => {\n for (const monitor of monitors) {\n const failure = monitor.failure();\n\n if (failure !== undefined) {\n return failure;\n }\n }\n\n return undefined;\n};\n\nconst watchForSocket = ({ options, resolve, reject }: SocketWatch): void => {\n const { monitor, guards = [], timeout = 15_000 } = options;\n const watched = [monitor, ...guards];\n const cleanups: (() => void)[] = [monitor.stop];\n\n const stop = (): void => {\n runCleanups(cleanups);\n };\n\n const fail = (message: string): void => {\n stop();\n\n for (const guard of guards) {\n guard.stop();\n }\n\n reject(new Error(message));\n };\n\n const alreadyFailed = firstFailure(watched);\n\n if (alreadyFailed !== undefined) {\n fail(alreadyFailed);\n\n return;\n }\n\n const poll = pollForPath(monitor.path, () => {\n stop();\n resolve();\n });\n\n const timer = setTimeout(() => {\n fail(`${monitor.label} did not become available within ${String(timeout)}ms\\n${monitor.read()}`);\n }, timeout);\n\n cleanups.push(\n () => {\n clearInterval(poll);\n clearTimeout(timer);\n },\n ...watched.map((entry) => entry.subscribe(fail)),\n );\n};\n\nconst waitForSocket = (options: WaitForSocketOptions): Promise<void> =>\n new Promise((resolve, reject) => {\n watchForSocket({ options, resolve, reject });\n });\n\nconst captureCompositorStderr = (child: ChildProcess, logPath: string): CapturedStderr => {\n const captured: string[] = [];\n const stderr = child.stderr;\n\n if (stderr === null) {\n return { chunks: captured, stop: noVirtualSeat };\n }\n\n stderr.setEncoding(\"utf8\");\n const descriptor = openSync(logPath, \"a\");\n let isOpen = true;\n\n const onData = (chunk: string): void => {\n captured.push(chunk);\n\n try {\n writeSync(descriptor, chunk);\n } catch {\n return;\n }\n };\n\n stderr.on(\"data\", onData);\n\n return {\n chunks: captured,\n stop: () => {\n stderr.removeListener(\"data\", onData);\n\n if (isOpen) {\n isOpen = false;\n closeSync(descriptor);\n }\n },\n };\n};\n\nconst compositorExitMessage = (\n code: number | null,\n signal: NodeJS.Signals | null,\n capturedStderr: string[],\n): string =>\n `[gtkx] the headless compositor exited (code ${String(code)}, signal ${signal ?? \"null\"}); ` +\n `every Wayland client in this worker has been severed.\\n${capturedStderr.join(\"\")}`;\n\nconst watchCompositorExit = (child: ChildProcess, capturedStderr: string[]): (() => void) => {\n const report = (code: number | null, signal: NodeJS.Signals | null): void => {\n process.stderr.write(compositorExitMessage(code, signal, capturedStderr));\n };\n\n child.on(\"exit\", report);\n\n return () => child.removeListener(\"exit\", report);\n};\n\nconst waitForDisplaySockets = async ({ compositorMonitor, busMonitor }: DisplaySockets): Promise<void> => {\n await waitForSocket({ monitor: busMonitor, guards: [compositorMonitor] });\n await waitForSocket({ monitor: compositorMonitor });\n};\n\nconst killSpawned = (children: ChildProcess[]): void => {\n for (const child of children) {\n child.kill(\"SIGTERM\");\n }\n};\n\nconst makeTeardown = (stops: (() => void)[]): (() => void) => {\n let isTorndown = false;\n\n return (): void => {\n if (isTorndown) {\n return;\n }\n\n isTorndown = true;\n runCleanups(stops);\n };\n};\n\nconst createHeadlessRuntimeDirectory = (): string => {\n const runtimeDir = mkdtempSync(join(tmpdir(), \"gtkx-xdg-\"));\n\n try {\n chmodSync(runtimeDir, 0o700);\n const markerPath = join(runtimeDir, HEADLESS_RUNTIME_MARKER);\n writeFileSync(markerPath, createHeadlessRuntimeMarker(runtimeDir), { flag: \"wx\", mode: 0o600 });\n chmodSync(markerPath, 0o600);\n\n return runtimeDir;\n } catch (error) {\n rmSync(runtimeDir, { recursive: true, force: true });\n throw error;\n }\n};\n\nconst startHeadlessDisplay = async (options: HeadlessOptions): Promise<() => void> => {\n const env: EnvSnapshot = {};\n const runtimeDir = createHeadlessRuntimeDirectory();\n const spawned: ChildProcess[] = [];\n\n const removeRuntime = (): void => {\n restoreEnv(env);\n rmSync(runtimeDir, { recursive: true, force: true });\n };\n\n try {\n applyEnv(env, STATIC_HEADLESS_ENV);\n applyEnv(env, { XDG_RUNTIME_DIR: runtimeDir });\n const busConfigPath = join(runtimeDir, \"session.conf\");\n const busSocketPath = join(runtimeDir, \"bus\");\n writeBusConfig(busConfigPath, busSocketPath);\n\n const busChild = spawnWithParentDeathSignal(\"dbus-daemon\", [`--config-file=${busConfigPath}`], {\n stdio: [\"ignore\", \"ignore\", \"pipe\"],\n cleanupDirectories: [runtimeDir],\n });\n\n busChild.unref();\n spawned.push(busChild);\n const busMonitor = monitorChild(busChild, \"D-Bus session bus\", busSocketPath);\n applyEnv(env, { DBUS_SESSION_BUS_ADDRESS: `unix:path=${busSocketPath}` });\n const compositor = startCompositor(runtimeDir, options, env);\n compositor.child.unref();\n spawned.push(compositor.child);\n const compositorSocketPath = join(runtimeDir, compositor.socket);\n const compositorMonitor = monitorChild(compositor.child, \"Compositor\", compositorSocketPath);\n applyEnv(env, { WAYLAND_DISPLAY: compositor.socket });\n await waitForDisplaySockets({ compositorMonitor, busMonitor });\n const stopVirtualSeat = await attachVirtualSeat(compositor, compositorSocketPath);\n const stopNotifications = await startNotificationService(`unix:path=${busSocketPath}`);\n const capturedStderr = captureCompositorStderr(compositor.child, join(runtimeDir, \"compositor.stderr.log\"));\n const stopExitWatch = watchCompositorExit(compositor.child, capturedStderr.chunks);\n\n return makeTeardown([\n stopExitWatch,\n capturedStderr.stop,\n () => {\n killSpawned(spawned);\n },\n stopVirtualSeat,\n stopNotifications,\n removeRuntime,\n ]);\n } catch (error) {\n killSpawned(spawned);\n removeRuntime();\n throw error;\n }\n};\n\nexport {\n STATIC_HEADLESS_ENV,\n resolveHeadlessOptions,\n readHeadlessOptions,\n startHeadlessDisplay,\n type CompositorId,\n type HeadlessOptions,\n};\n"]}
@@ -1,2 +1,3 @@
1
1
  export { readHeadlessOptions, resolveHeadlessOptions, startHeadlessDisplay, STATIC_HEADLESS_ENV, type CompositorId, type HeadlessOptions, } from "./headless-display.js";
2
+ export { findStaleHeadlessDisplays, reapStaleHeadlessDisplays, type StaleHeadlessDisplay, } from "./reap-headless-displays.js";
2
3
  //# sourceMappingURL=headless.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"headless.d.ts","sourceRoot":"","sources":["../src/headless.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,EACpB,mBAAmB,EACnB,KAAK,YAAY,EACjB,KAAK,eAAe,GACvB,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"headless.d.ts","sourceRoot":"","sources":["../src/headless.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,EACpB,mBAAmB,EACnB,KAAK,YAAY,EACjB,KAAK,eAAe,GACvB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACH,yBAAyB,EACzB,yBAAyB,EACzB,KAAK,oBAAoB,GAC5B,MAAM,6BAA6B,CAAC"}
package/dist/headless.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { readHeadlessOptions, resolveHeadlessOptions, startHeadlessDisplay, STATIC_HEADLESS_ENV, } from "./headless-display.js";
2
+ export { findStaleHeadlessDisplays, reapStaleHeadlessDisplays, } from "./reap-headless-displays.js";
2
3
  //# sourceMappingURL=headless.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"headless.js","sourceRoot":"","sources":["../src/headless.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,EACpB,mBAAmB,GAGtB,MAAM,uBAAuB,CAAC","sourcesContent":["export {\n readHeadlessOptions,\n resolveHeadlessOptions,\n startHeadlessDisplay,\n STATIC_HEADLESS_ENV,\n type CompositorId,\n type HeadlessOptions,\n} from \"./headless-display.js\";\n"]}
1
+ {"version":3,"file":"headless.js","sourceRoot":"","sources":["../src/headless.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,EACpB,mBAAmB,GAGtB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACH,yBAAyB,EACzB,yBAAyB,GAE5B,MAAM,6BAA6B,CAAC","sourcesContent":["export {\n readHeadlessOptions,\n resolveHeadlessOptions,\n startHeadlessDisplay,\n STATIC_HEADLESS_ENV,\n type CompositorId,\n type HeadlessOptions,\n} from \"./headless-display.js\";\nexport {\n findStaleHeadlessDisplays,\n reapStaleHeadlessDisplays,\n type StaleHeadlessDisplay,\n} from \"./reap-headless-displays.js\";\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAM5C,OAAO,EAAE,KAAK,eAAe,EAAuB,MAAM,uBAAuB,CAAC;AAElF;;;GAGG;AACH,KAAK,aAAa,GAAG,OAAO,CAAC,eAAe,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;AAsBlG;;;;;;;GAOG;AACH,QAAA,MAAM,IAAI,GAAI,UAAS,aAAkB,KAAG,MA0B3C,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAChF,OAAO,EAAE,KAAK,aAAa,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAM5C,OAAO,EAAE,KAAK,eAAe,EAAuB,MAAM,uBAAuB,CAAC;AAGlF;;;GAGG;AACH,KAAK,aAAa,GAAG,OAAO,CAAC,eAAe,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;AAsBlG;;;;;;;GAOG;AACH,QAAA,MAAM,IAAI,GAAI,UAAS,aAAkB,KAAG,MA2B3C,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAChF,OAAO,EAAE,KAAK,aAAa,EAAE,CAAC"}
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { existsSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import { STATIC_HEADLESS_ENV } from "./headless-display.js";
7
+ import { reapStaleHeadlessDisplays } from "./reap-headless-displays.js";
7
8
  const GTKX_INLINE_DEPS = [/@gtkx\/(?!native)/, /[/\\]\.gtkx[/\\]/];
8
9
  const DEFAULT_TIMEOUT = 30_000;
9
10
  const workerPreloadUrl = () => {
@@ -28,6 +29,7 @@ const headlessPreloadSpecifier = (options) => {
28
29
  */
29
30
  const gtkx = (options = {}) => {
30
31
  assertSupportedNodeVersion();
32
+ reapStaleHeadlessDisplays();
31
33
  const { configFile, ...headlessOptions } = options;
32
34
  const loadConfig = createConfigLoader({ configFile });
33
35
  return createConfigPlugin({
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,0BAA0B,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AACvF,OAAO,kBAAkB,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAwB,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAQlF,MAAM,gBAAgB,GAAa,CAAC,mBAAmB,EAAE,kBAAkB,CAAC,CAAC;AAC7E,MAAM,eAAe,GAAG,MAAM,CAAC;AAE/B,MAAM,gBAAgB,GAAG,GAAQ,EAAE;IAC/B,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC;IAC/D,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,mBAAmB,CAAC,CAAC;IAE1G,OAAO,aAAa,CAAC,IAAI,CAAC,CAAC;AAC/B,CAAC,CAAC;AAEF,MAAM,wBAAwB,GAAG,CAAC,OAAiC,EAAU,EAAE;IAC3E,MAAM,GAAG,GAAG,gBAAgB,EAAE,CAAC;IAE/B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACjD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACrC,CAAC;IAED,OAAO,GAAG,CAAC,IAAI,CAAC;AACpB,CAAC,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,IAAI,GAAG,CAAC,UAAyB,EAAE,EAAU,EAAE;IACjD,0BAA0B,EAAE,CAAC;IAC7B,MAAM,EAAE,UAAU,EAAE,GAAG,eAAe,EAAE,GAAG,OAAO,CAAC;IACnD,MAAM,UAAU,GAAG,kBAAkB,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC;IAEtD,OAAO,kBAAkB,CAAC;QACtB,IAAI,EAAE,aAAa;QACnB,UAAU;QACV,MAAM,CAAC,MAAM;YACT,OAAO;gBACH,IAAI,EAAE;oBACF,OAAO,EAAE,IAAI;oBACb,QAAQ,EAAE,CAAC,mBAAmB,EAAE,UAAU,EAAE,wBAAwB,CAAC,eAAe,CAAC,CAAC;oBACtF,WAAW,EAAE,MAAM,CAAC,IAAI,EAAE,WAAW,IAAI,eAAe;oBACxD,WAAW,EAAE,MAAM,CAAC,IAAI,EAAE,WAAW,IAAI,eAAe;oBACxD,IAAI,EAAE,OAAO;oBACb,GAAG,EAAE,mBAAmB;oBACxB,MAAM,EAAE;wBACJ,IAAI,EAAE;4BACF,MAAM,EAAE,gBAAgB;yBAC3B;qBACJ;iBACJ;aACJ,CAAC;QACN,CAAC;KACJ,CAAC,CAAC;AACP,CAAC,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,OAAO,EAA2C,MAAM,uBAAuB,CAAC;AAChF,OAAO,EAAsB,CAAC","sourcesContent":["import type { Plugin } from \"vitest/config\";\nimport { assertSupportedNodeVersion, createConfigLoader } from \"@gtkx/config/internal\";\nimport createConfigPlugin from \"@gtkx/config/vite-plugin\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport { type HeadlessOptions, STATIC_HEADLESS_ENV } from \"./headless-display.ts\";\n\n/**\n * Options accepted by the GTKX Vitest plugin. Every headless display\n * setting is optional and falls back to a built-in default when omitted.\n */\ntype PluginOptions = Partial<HeadlessOptions> & Partial<Record<\"configFile\", string | undefined>>;\n\nconst GTKX_INLINE_DEPS: RegExp[] = [/@gtkx\\/(?!native)/, /[/\\\\]\\.gtkx[/\\\\]/];\nconst DEFAULT_TIMEOUT = 30_000;\n\nconst workerPreloadUrl = (): URL => {\n const sibling = join(import.meta.dirname, \"worker-preload.js\");\n const path = existsSync(sibling) ? sibling : join(import.meta.dirname, \"..\", \"dist\", \"worker-preload.js\");\n\n return pathToFileURL(path);\n};\n\nconst headlessPreloadSpecifier = (options: Partial<HeadlessOptions>): string => {\n const url = workerPreloadUrl();\n\n for (const [key, value] of Object.entries(options)) {\n url.searchParams.set(key, value);\n }\n\n return url.href;\n};\n\n/**\n * Vitest plugin that runs each test worker against its own isolated headless\n * Wayland display. It configures the forks pool, injects the worker preload and\n * setup files, and sets the environment needed for headless GTK4 rendering.\n *\n * @param options Headless display settings (size, compositor) forwarded to each worker.\n * @returns A Vitest config plugin.\n */\nconst gtkx = (options: PluginOptions = {}): Plugin => {\n assertSupportedNodeVersion();\n const { configFile, ...headlessOptions } = options;\n const loadConfig = createConfigLoader({ configFile });\n\n return createConfigPlugin({\n name: \"gtkx:vitest\",\n loadConfig,\n config(config) {\n return {\n test: {\n globals: true,\n execArgv: [\"--disable-sigusr1\", \"--import\", headlessPreloadSpecifier(headlessOptions)],\n testTimeout: config.test?.testTimeout ?? DEFAULT_TIMEOUT,\n hookTimeout: config.test?.hookTimeout ?? DEFAULT_TIMEOUT,\n pool: \"forks\",\n env: STATIC_HEADLESS_ENV,\n server: {\n deps: {\n inline: GTKX_INLINE_DEPS,\n },\n },\n },\n };\n },\n });\n};\n\nexport default gtkx;\nexport { type CompositorId, type HeadlessOptions } from \"./headless-display.ts\";\nexport { type PluginOptions };\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,0BAA0B,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AACvF,OAAO,kBAAkB,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAwB,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AAQxE,MAAM,gBAAgB,GAAa,CAAC,mBAAmB,EAAE,kBAAkB,CAAC,CAAC;AAC7E,MAAM,eAAe,GAAG,MAAM,CAAC;AAE/B,MAAM,gBAAgB,GAAG,GAAQ,EAAE;IAC/B,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC;IAC/D,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,mBAAmB,CAAC,CAAC;IAE1G,OAAO,aAAa,CAAC,IAAI,CAAC,CAAC;AAC/B,CAAC,CAAC;AAEF,MAAM,wBAAwB,GAAG,CAAC,OAAiC,EAAU,EAAE;IAC3E,MAAM,GAAG,GAAG,gBAAgB,EAAE,CAAC;IAE/B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACjD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACrC,CAAC;IAED,OAAO,GAAG,CAAC,IAAI,CAAC;AACpB,CAAC,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,IAAI,GAAG,CAAC,UAAyB,EAAE,EAAU,EAAE;IACjD,0BAA0B,EAAE,CAAC;IAC7B,yBAAyB,EAAE,CAAC;IAC5B,MAAM,EAAE,UAAU,EAAE,GAAG,eAAe,EAAE,GAAG,OAAO,CAAC;IACnD,MAAM,UAAU,GAAG,kBAAkB,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC;IAEtD,OAAO,kBAAkB,CAAC;QACtB,IAAI,EAAE,aAAa;QACnB,UAAU;QACV,MAAM,CAAC,MAAM;YACT,OAAO;gBACH,IAAI,EAAE;oBACF,OAAO,EAAE,IAAI;oBACb,QAAQ,EAAE,CAAC,mBAAmB,EAAE,UAAU,EAAE,wBAAwB,CAAC,eAAe,CAAC,CAAC;oBACtF,WAAW,EAAE,MAAM,CAAC,IAAI,EAAE,WAAW,IAAI,eAAe;oBACxD,WAAW,EAAE,MAAM,CAAC,IAAI,EAAE,WAAW,IAAI,eAAe;oBACxD,IAAI,EAAE,OAAO;oBACb,GAAG,EAAE,mBAAmB;oBACxB,MAAM,EAAE;wBACJ,IAAI,EAAE;4BACF,MAAM,EAAE,gBAAgB;yBAC3B;qBACJ;iBACJ;aACJ,CAAC;QACN,CAAC;KACJ,CAAC,CAAC;AACP,CAAC,CAAC;AAEF,eAAe,IAAI,CAAC;AACpB,OAAO,EAA2C,MAAM,uBAAuB,CAAC;AAChF,OAAO,EAAsB,CAAC","sourcesContent":["import type { Plugin } from \"vitest/config\";\nimport { assertSupportedNodeVersion, createConfigLoader } from \"@gtkx/config/internal\";\nimport createConfigPlugin from \"@gtkx/config/vite-plugin\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport { type HeadlessOptions, STATIC_HEADLESS_ENV } from \"./headless-display.ts\";\nimport { reapStaleHeadlessDisplays } from \"./reap-headless-displays.ts\";\n\n/**\n * Options accepted by the GTKX Vitest plugin. Every headless display\n * setting is optional and falls back to a built-in default when omitted.\n */\ntype PluginOptions = Partial<HeadlessOptions> & Partial<Record<\"configFile\", string | undefined>>;\n\nconst GTKX_INLINE_DEPS: RegExp[] = [/@gtkx\\/(?!native)/, /[/\\\\]\\.gtkx[/\\\\]/];\nconst DEFAULT_TIMEOUT = 30_000;\n\nconst workerPreloadUrl = (): URL => {\n const sibling = join(import.meta.dirname, \"worker-preload.js\");\n const path = existsSync(sibling) ? sibling : join(import.meta.dirname, \"..\", \"dist\", \"worker-preload.js\");\n\n return pathToFileURL(path);\n};\n\nconst headlessPreloadSpecifier = (options: Partial<HeadlessOptions>): string => {\n const url = workerPreloadUrl();\n\n for (const [key, value] of Object.entries(options)) {\n url.searchParams.set(key, value);\n }\n\n return url.href;\n};\n\n/**\n * Vitest plugin that runs each test worker against its own isolated headless\n * Wayland display. It configures the forks pool, injects the worker preload and\n * setup files, and sets the environment needed for headless GTK4 rendering.\n *\n * @param options Headless display settings (size, compositor) forwarded to each worker.\n * @returns A Vitest config plugin.\n */\nconst gtkx = (options: PluginOptions = {}): Plugin => {\n assertSupportedNodeVersion();\n reapStaleHeadlessDisplays();\n const { configFile, ...headlessOptions } = options;\n const loadConfig = createConfigLoader({ configFile });\n\n return createConfigPlugin({\n name: \"gtkx:vitest\",\n loadConfig,\n config(config) {\n return {\n test: {\n globals: true,\n execArgv: [\"--disable-sigusr1\", \"--import\", headlessPreloadSpecifier(headlessOptions)],\n testTimeout: config.test?.testTimeout ?? DEFAULT_TIMEOUT,\n hookTimeout: config.test?.hookTimeout ?? DEFAULT_TIMEOUT,\n pool: \"forks\",\n env: STATIC_HEADLESS_ENV,\n server: {\n deps: {\n inline: GTKX_INLINE_DEPS,\n },\n },\n },\n };\n },\n });\n};\n\nexport default gtkx;\nexport { type CompositorId, type HeadlessOptions } from \"./headless-display.ts\";\nexport { type PluginOptions };\n"]}
@@ -0,0 +1,9 @@
1
+ import { type CleanupDirectoryIdentity } from "@gtkx/utils";
2
+ type StaleHeadlessDisplay = {
3
+ runtimeDir: string;
4
+ cleanupDirectory: CleanupDirectoryIdentity;
5
+ };
6
+ declare const findStaleHeadlessDisplays: () => StaleHeadlessDisplay[];
7
+ declare const reapStaleHeadlessDisplays: (candidates?: readonly StaleHeadlessDisplay[]) => string[];
8
+ export { findStaleHeadlessDisplays, reapStaleHeadlessDisplays, type StaleHeadlessDisplay, };
9
+ //# sourceMappingURL=reap-headless-displays.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reap-headless-displays.d.ts","sourceRoot":"","sources":["../src/reap-headless-displays.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,wBAAwB,EAGhC,MAAM,aAAa,CAAC;AAWrB,KAAK,oBAAoB,GAAG;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,wBAAwB,CAAC;CAC9C,CAAC;AA8LF,QAAA,MAAM,yBAAyB,QAAO,oBAAoB,EAYzD,CAAC;AAkCF,QAAA,MAAM,yBAAyB,GAC3B,aAAY,SAAS,oBAAoB,EAAgC,KAC1E,MAAM,EAiBR,CAAC;AAEF,OAAO,EACH,yBAAyB,EACzB,yBAAyB,EACzB,KAAK,oBAAoB,GAC5B,CAAC"}
@@ -0,0 +1,190 @@
1
+ import { cleanupDirectoryIdentity, removeCleanupDirectory, } from "@gtkx/utils";
2
+ import { constants, lstatSync, readdirSync, readFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, dirname, join } from "node:path";
5
+ import { createBusConfig, createHeadlessRuntimeMarker, HEADLESS_RUNTIME_MARKER, isSwayConfig, } from "./headless-config.js";
6
+ const RUNTIME_DIRECTORY_PATTERN = /^gtkx-xdg-[A-Za-z0-9]{6}$/;
7
+ const STOPPED_PROCESS_STATES = new Set(["Z", "X", "x"]);
8
+ const CONFIG_SIZE_LIMIT = 2048;
9
+ const MINIMUM_STALE_AGE_MS = 5000;
10
+ const RUNTIME_ROOT = tmpdir();
11
+ const currentUserId = () => {
12
+ const getuid = process.getuid;
13
+ return getuid === undefined ? undefined : getuid();
14
+ };
15
+ const isUserOwned = (path, userId) => {
16
+ try {
17
+ return lstatSync(path).uid === userId;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ };
23
+ const isPrivateRuntimeDirectory = (runtimeDir, userId) => {
24
+ try {
25
+ const stat = lstatSync(runtimeDir);
26
+ return stat.isDirectory() &&
27
+ stat.uid === userId &&
28
+ (stat.mode & 0o777) === 0o700 &&
29
+ Date.now() - stat.mtimeMs >= MINIMUM_STALE_AGE_MS &&
30
+ RUNTIME_DIRECTORY_PATTERN.test(basename(runtimeDir)) &&
31
+ dirname(runtimeDir) === RUNTIME_ROOT;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ };
37
+ const readOwnedFile = (path, runtimeDir, userId, requiredMode) => {
38
+ try {
39
+ if (dirname(path) !== runtimeDir) {
40
+ return undefined;
41
+ }
42
+ const contents = readFileSync(path, {
43
+ encoding: "utf8",
44
+ flag: constants.O_RDONLY | constants.O_NOFOLLOW,
45
+ });
46
+ const stat = lstatSync(path);
47
+ return stat.isFile() &&
48
+ stat.uid === userId &&
49
+ stat.size < CONFIG_SIZE_LIMIT &&
50
+ (requiredMode === undefined || (stat.mode & 0o777) === requiredMode)
51
+ ? contents
52
+ : undefined;
53
+ }
54
+ catch {
55
+ return undefined;
56
+ }
57
+ };
58
+ const hasGeneratedRuntimeFiles = (runtimeDir, userId) => {
59
+ const sway = readOwnedFile(join(runtimeDir, "sway.conf"), runtimeDir, userId);
60
+ const bus = readOwnedFile(join(runtimeDir, "session.conf"), runtimeDir, userId);
61
+ const marker = readOwnedFile(join(runtimeDir, HEADLESS_RUNTIME_MARKER), runtimeDir, userId, 0o600);
62
+ return bus === createBusConfig(join(runtimeDir, "bus")) &&
63
+ (marker === createHeadlessRuntimeMarker(runtimeDir) || (sway !== undefined && isSwayConfig(sway)));
64
+ };
65
+ const readProcessArguments = (pid) => {
66
+ try {
67
+ const stat = readFileSync(`/proc/${String(pid)}/stat`, "utf8");
68
+ const state = stat.slice(stat.lastIndexOf(") ") + 2).split(" ", 1)[0];
69
+ if (state === undefined || STOPPED_PROCESS_STATES.has(state)) {
70
+ return undefined;
71
+ }
72
+ return readFileSync(`/proc/${String(pid)}/cmdline`)
73
+ .toString()
74
+ .split("\0")
75
+ .filter((argument) => argument.length > 0);
76
+ }
77
+ catch {
78
+ return undefined;
79
+ }
80
+ };
81
+ const runtimeFromArgument = (argument) => {
82
+ const path = argument.startsWith("--config-file=") ? argument.slice("--config-file=".length) : argument;
83
+ const name = basename(path);
84
+ if (name !== "sway.conf" && name !== "session.conf") {
85
+ return undefined;
86
+ }
87
+ const runtimeDir = dirname(path);
88
+ return RUNTIME_DIRECTORY_PATTERN.test(basename(runtimeDir)) && dirname(runtimeDir) === RUNTIME_ROOT
89
+ ? runtimeDir
90
+ : undefined;
91
+ };
92
+ const runtimeFromEnvironment = (pid) => {
93
+ try {
94
+ const prefix = "XDG_RUNTIME_DIR=";
95
+ const entry = readFileSync(`/proc/${String(pid)}/environ`, "utf8")
96
+ .split("\0")
97
+ .find((value) => value.startsWith(prefix));
98
+ const runtimeDir = entry?.slice(prefix.length);
99
+ return runtimeDir !== undefined &&
100
+ RUNTIME_DIRECTORY_PATTERN.test(basename(runtimeDir)) &&
101
+ dirname(runtimeDir) === RUNTIME_ROOT
102
+ ? runtimeDir
103
+ : undefined;
104
+ }
105
+ catch {
106
+ return undefined;
107
+ }
108
+ };
109
+ const isOwnedProcessId = (pid, userId) => Number.isSafeInteger(pid) && pid > 1 && isUserOwned(`/proc/${String(pid)}`, userId);
110
+ const addRuntimeArguments = (live, processArgs) => {
111
+ for (const argument of processArgs) {
112
+ const runtimeDir = runtimeFromArgument(argument);
113
+ if (runtimeDir !== undefined) {
114
+ live.add(runtimeDir);
115
+ }
116
+ }
117
+ };
118
+ const addProcessRuntimes = (live, pid) => {
119
+ const processArgs = readProcessArguments(pid);
120
+ if (processArgs === undefined) {
121
+ return;
122
+ }
123
+ addRuntimeArguments(live, processArgs);
124
+ const runtimeDir = runtimeFromEnvironment(pid);
125
+ if (runtimeDir !== undefined) {
126
+ live.add(runtimeDir);
127
+ }
128
+ };
129
+ const findLiveRuntimeDirectories = (userId) => {
130
+ const live = new Set();
131
+ const processEntries = readdirSync("/proc");
132
+ for (const entry of processEntries) {
133
+ const pid = Number(entry);
134
+ if (!isOwnedProcessId(pid, userId)) {
135
+ continue;
136
+ }
137
+ addProcessRuntimes(live, pid);
138
+ }
139
+ return live;
140
+ };
141
+ const classifyRuntimeDirectory = (name, userId, live) => {
142
+ const runtimeDir = join(RUNTIME_ROOT, name);
143
+ if (live.has(runtimeDir) ||
144
+ !isPrivateRuntimeDirectory(runtimeDir, userId) ||
145
+ !hasGeneratedRuntimeFiles(runtimeDir, userId)) {
146
+ return [];
147
+ }
148
+ const cleanupDirectory = cleanupDirectoryIdentity(runtimeDir);
149
+ return cleanupDirectory === undefined ? [] : [{ runtimeDir, cleanupDirectory }];
150
+ };
151
+ const findStaleHeadlessDisplays = () => {
152
+ const userId = currentUserId();
153
+ if (userId === undefined) {
154
+ return [];
155
+ }
156
+ const live = findLiveRuntimeDirectories(userId);
157
+ return readdirSync(RUNTIME_ROOT, { withFileTypes: true })
158
+ .filter((entry) => entry.isDirectory() && RUNTIME_DIRECTORY_PATTERN.test(entry.name))
159
+ .flatMap((entry) => classifyRuntimeDirectory(entry.name, userId, live));
160
+ };
161
+ const isSameCleanupDirectory = (left, right) => left?.device === right.device &&
162
+ left.inode === right.inode &&
163
+ left.userId === right.userId;
164
+ const isReapable = (candidate, userId, live) => !live.has(candidate.runtimeDir) &&
165
+ isSameCleanupDirectory(cleanupDirectoryIdentity(candidate.runtimeDir), candidate.cleanupDirectory) &&
166
+ isPrivateRuntimeDirectory(candidate.runtimeDir, userId) &&
167
+ hasGeneratedRuntimeFiles(candidate.runtimeDir, userId);
168
+ const didReapCandidate = (candidate, userId, live) => {
169
+ if (!isReapable(candidate, userId, live)) {
170
+ return false;
171
+ }
172
+ removeCleanupDirectory(candidate.cleanupDirectory);
173
+ return cleanupDirectoryIdentity(candidate.runtimeDir) === undefined;
174
+ };
175
+ const reapStaleHeadlessDisplays = (candidates = findStaleHeadlessDisplays()) => {
176
+ const userId = currentUserId();
177
+ if (userId === undefined) {
178
+ return [];
179
+ }
180
+ const live = findLiveRuntimeDirectories(userId);
181
+ const removed = [];
182
+ for (const candidate of candidates) {
183
+ if (didReapCandidate(candidate, userId, live)) {
184
+ removed.push(candidate.runtimeDir);
185
+ }
186
+ }
187
+ return removed;
188
+ };
189
+ export { findStaleHeadlessDisplays, reapStaleHeadlessDisplays, };
190
+ //# sourceMappingURL=reap-headless-displays.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reap-headless-displays.js","sourceRoot":"","sources":["../src/reap-headless-displays.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type CleanupDirectoryIdentity,\n cleanupDirectoryIdentity,\n removeCleanupDirectory,\n} from \"@gtkx/utils\";\nimport { constants, lstatSync, readdirSync, readFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { basename, dirname, join } from \"node:path\";\nimport {\n createBusConfig,\n createHeadlessRuntimeMarker,\n HEADLESS_RUNTIME_MARKER,\n isSwayConfig,\n} from \"./headless-config.ts\";\n\ntype StaleHeadlessDisplay = {\n runtimeDir: string;\n cleanupDirectory: CleanupDirectoryIdentity;\n};\n\nconst RUNTIME_DIRECTORY_PATTERN = /^gtkx-xdg-[A-Za-z0-9]{6}$/;\nconst STOPPED_PROCESS_STATES: Set<string> = new Set([\"Z\", \"X\", \"x\"]);\nconst CONFIG_SIZE_LIMIT = 2048;\nconst MINIMUM_STALE_AGE_MS = 5000;\nconst RUNTIME_ROOT = tmpdir();\n\nconst currentUserId = (): number | undefined => {\n const getuid = process.getuid;\n\n return getuid === undefined ? undefined : getuid();\n};\n\nconst isUserOwned = (path: string, userId: number): boolean => {\n try {\n return lstatSync(path).uid === userId;\n } catch {\n return false;\n }\n};\n\nconst isPrivateRuntimeDirectory = (runtimeDir: string, userId: number): boolean => {\n try {\n const stat = lstatSync(runtimeDir);\n\n return stat.isDirectory() &&\n stat.uid === userId &&\n (stat.mode & 0o777) === 0o700 &&\n Date.now() - stat.mtimeMs >= MINIMUM_STALE_AGE_MS &&\n RUNTIME_DIRECTORY_PATTERN.test(basename(runtimeDir)) &&\n dirname(runtimeDir) === RUNTIME_ROOT;\n } catch {\n return false;\n }\n};\n\nconst readOwnedFile = (\n path: string,\n runtimeDir: string,\n userId: number,\n requiredMode?: number,\n): string | undefined => {\n try {\n if (dirname(path) !== runtimeDir) {\n return undefined;\n }\n\n const contents = readFileSync(path, {\n encoding: \"utf8\",\n flag: constants.O_RDONLY | constants.O_NOFOLLOW,\n });\n const stat = lstatSync(path);\n\n return stat.isFile() &&\n stat.uid === userId &&\n stat.size < CONFIG_SIZE_LIMIT &&\n (requiredMode === undefined || (stat.mode & 0o777) === requiredMode)\n ? contents\n : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst hasGeneratedRuntimeFiles = (runtimeDir: string, userId: number): boolean => {\n const sway = readOwnedFile(join(runtimeDir, \"sway.conf\"), runtimeDir, userId);\n const bus = readOwnedFile(join(runtimeDir, \"session.conf\"), runtimeDir, userId);\n const marker = readOwnedFile(join(runtimeDir, HEADLESS_RUNTIME_MARKER), runtimeDir, userId, 0o600);\n\n return bus === createBusConfig(join(runtimeDir, \"bus\")) &&\n (marker === createHeadlessRuntimeMarker(runtimeDir) || (sway !== undefined && isSwayConfig(sway)));\n};\n\nconst readProcessArguments = (pid: number): string[] | undefined => {\n try {\n const stat = readFileSync(`/proc/${String(pid)}/stat`, \"utf8\");\n const state = stat.slice(stat.lastIndexOf(\") \") + 2).split(\" \", 1)[0];\n\n if (state === undefined || STOPPED_PROCESS_STATES.has(state)) {\n return undefined;\n }\n\n return readFileSync(`/proc/${String(pid)}/cmdline`)\n .toString()\n .split(\"\\0\")\n .filter((argument) => argument.length > 0);\n } catch {\n return undefined;\n }\n};\n\nconst runtimeFromArgument = (argument: string): string | undefined => {\n const path = argument.startsWith(\"--config-file=\") ? argument.slice(\"--config-file=\".length) : argument;\n const name = basename(path);\n\n if (name !== \"sway.conf\" && name !== \"session.conf\") {\n return undefined;\n }\n\n const runtimeDir = dirname(path);\n\n return RUNTIME_DIRECTORY_PATTERN.test(basename(runtimeDir)) && dirname(runtimeDir) === RUNTIME_ROOT\n ? runtimeDir\n : undefined;\n};\n\nconst runtimeFromEnvironment = (pid: number): string | undefined => {\n try {\n const prefix = \"XDG_RUNTIME_DIR=\";\n const entry = readFileSync(`/proc/${String(pid)}/environ`, \"utf8\")\n .split(\"\\0\")\n .find((value) => value.startsWith(prefix));\n const runtimeDir = entry?.slice(prefix.length);\n\n return runtimeDir !== undefined &&\n RUNTIME_DIRECTORY_PATTERN.test(basename(runtimeDir)) &&\n dirname(runtimeDir) === RUNTIME_ROOT\n ? runtimeDir\n : undefined;\n } catch {\n return undefined;\n }\n};\n\nconst isOwnedProcessId = (pid: number, userId: number): boolean =>\n Number.isSafeInteger(pid) && pid > 1 && isUserOwned(`/proc/${String(pid)}`, userId);\n\nconst addRuntimeArguments = (live: Set<string>, processArgs: string[]): void => {\n for (const argument of processArgs) {\n const runtimeDir = runtimeFromArgument(argument);\n\n if (runtimeDir !== undefined) {\n live.add(runtimeDir);\n }\n }\n};\n\nconst addProcessRuntimes = (live: Set<string>, pid: number): void => {\n const processArgs = readProcessArguments(pid);\n\n if (processArgs === undefined) {\n return;\n }\n\n addRuntimeArguments(live, processArgs);\n const runtimeDir = runtimeFromEnvironment(pid);\n\n if (runtimeDir !== undefined) {\n live.add(runtimeDir);\n }\n};\n\nconst findLiveRuntimeDirectories = (userId: number): Set<string> => {\n const live: Set<string> = new Set();\n const processEntries = readdirSync(\"/proc\");\n\n for (const entry of processEntries) {\n const pid = Number(entry);\n\n if (!isOwnedProcessId(pid, userId)) {\n continue;\n }\n\n addProcessRuntimes(live, pid);\n }\n\n return live;\n};\n\nconst classifyRuntimeDirectory = (\n name: string,\n userId: number,\n live: ReadonlySet<string>,\n): StaleHeadlessDisplay[] => {\n const runtimeDir = join(RUNTIME_ROOT, name);\n\n if (\n live.has(runtimeDir) ||\n !isPrivateRuntimeDirectory(runtimeDir, userId) ||\n !hasGeneratedRuntimeFiles(runtimeDir, userId)\n ) {\n return [];\n }\n\n const cleanupDirectory = cleanupDirectoryIdentity(runtimeDir);\n\n return cleanupDirectory === undefined ? [] : [{ runtimeDir, cleanupDirectory }];\n};\n\nconst findStaleHeadlessDisplays = (): StaleHeadlessDisplay[] => {\n const userId = currentUserId();\n\n if (userId === undefined) {\n return [];\n }\n\n const live = findLiveRuntimeDirectories(userId);\n\n return readdirSync(RUNTIME_ROOT, { withFileTypes: true })\n .filter((entry) => entry.isDirectory() && RUNTIME_DIRECTORY_PATTERN.test(entry.name))\n .flatMap((entry) => classifyRuntimeDirectory(entry.name, userId, live));\n};\n\nconst isSameCleanupDirectory = (\n left: CleanupDirectoryIdentity | undefined,\n right: CleanupDirectoryIdentity,\n): boolean =>\n left?.device === right.device &&\n left.inode === right.inode &&\n left.userId === right.userId;\n\nconst isReapable = (\n candidate: StaleHeadlessDisplay,\n userId: number,\n live: ReadonlySet<string>,\n): boolean =>\n !live.has(candidate.runtimeDir) &&\n isSameCleanupDirectory(cleanupDirectoryIdentity(candidate.runtimeDir), candidate.cleanupDirectory) &&\n isPrivateRuntimeDirectory(candidate.runtimeDir, userId) &&\n hasGeneratedRuntimeFiles(candidate.runtimeDir, userId);\n\nconst didReapCandidate = (\n candidate: StaleHeadlessDisplay,\n userId: number,\n live: ReadonlySet<string>,\n): boolean => {\n if (!isReapable(candidate, userId, live)) {\n return false;\n }\n\n removeCleanupDirectory(candidate.cleanupDirectory);\n\n return cleanupDirectoryIdentity(candidate.runtimeDir) === undefined;\n};\n\nconst reapStaleHeadlessDisplays = (\n candidates: readonly StaleHeadlessDisplay[] = findStaleHeadlessDisplays(),\n): string[] => {\n const userId = currentUserId();\n\n if (userId === undefined) {\n return [];\n }\n\n const live = findLiveRuntimeDirectories(userId);\n const removed: string[] = [];\n\n for (const candidate of candidates) {\n if (didReapCandidate(candidate, userId, live)) {\n removed.push(candidate.runtimeDir);\n }\n }\n\n return removed;\n};\n\nexport {\n findStaleHeadlessDisplays,\n reapStaleHeadlessDisplays,\n type StaleHeadlessDisplay,\n};\n"]}
@@ -1,5 +1,6 @@
1
- import { installGracefulShutdown } from "@gtkx/utils";
1
+ import { installGracefulShutdown, watchParentProcess } from "@gtkx/utils";
2
2
  import { readHeadlessOptions, resolveHeadlessOptions, startHeadlessDisplay } from "./headless-display.js";
3
+ watchParentProcess();
3
4
  const options = readHeadlessOptions(new URL(import.meta.url).searchParams);
4
5
  const teardown = await startHeadlessDisplay(resolveHeadlessOptions(options));
5
6
  process.on("exit", teardown);
@@ -1 +1 @@
1
- {"version":3,"file":"worker-preload.js","sourceRoot":"","sources":["../src/worker-preload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAE1G,MAAM,OAAO,GAAG,mBAAmB,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,YAAY,CAAC,CAAC;AAC3E,MAAM,QAAQ,GAAG,MAAM,oBAAoB,CAAC,sBAAsB,CAAC,OAAO,CAAC,CAAC,CAAC;AAE7E,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AAC7B,uBAAuB,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC","sourcesContent":["import { installGracefulShutdown } from \"@gtkx/utils\";\nimport { readHeadlessOptions, resolveHeadlessOptions, startHeadlessDisplay } from \"./headless-display.ts\";\n\nconst options = readHeadlessOptions(new URL(import.meta.url).searchParams);\nconst teardown = await startHeadlessDisplay(resolveHeadlessOptions(options));\n\nprocess.on(\"exit\", teardown);\ninstallGracefulShutdown({ onSignal: teardown });\n"]}
1
+ {"version":3,"file":"worker-preload.js","sourceRoot":"","sources":["../src/worker-preload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAC1E,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAE1G,kBAAkB,EAAE,CAAC;AAErB,MAAM,OAAO,GAAG,mBAAmB,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,YAAY,CAAC,CAAC;AAC3E,MAAM,QAAQ,GAAG,MAAM,oBAAoB,CAAC,sBAAsB,CAAC,OAAO,CAAC,CAAC,CAAC;AAE7E,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AAC7B,uBAAuB,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC","sourcesContent":["import { installGracefulShutdown, watchParentProcess } from \"@gtkx/utils\";\nimport { readHeadlessOptions, resolveHeadlessOptions, startHeadlessDisplay } from \"./headless-display.ts\";\n\nwatchParentProcess();\n\nconst options = readHeadlessOptions(new URL(import.meta.url).searchParams);\nconst teardown = await startHeadlessDisplay(resolveHeadlessOptions(options));\n\nprocess.on(\"exit\", teardown);\ninstallGracefulShutdown({ onSignal: teardown });\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtkx/vitest",
3
- "version": "2.0.0-beta.3",
3
+ "version": "2.0.0-beta.4",
4
4
  "description": "Vitest plugin for GTK apps with headless Wayland isolation.",
5
5
  "keywords": [
6
6
  "gtkx",
@@ -59,8 +59,8 @@
59
59
  "node": ">=26.7.0"
60
60
  },
61
61
  "dependencies": {
62
- "@gtkx/config": "2.0.0-beta.3",
63
- "@gtkx/utils": "2.0.0-beta.3",
62
+ "@gtkx/config": "2.0.0-beta.4",
63
+ "@gtkx/utils": "2.0.0-beta.4",
64
64
  "@homebridge/dbus-native": "^0.7.9"
65
65
  },
66
66
  "devDependencies": {
@@ -0,0 +1,61 @@
1
+ const BUS_CONFIG_DOCTYPE =
2
+ '<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" ' +
3
+ '"https://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">';
4
+
5
+ const HEADLESS_RUNTIME_MARKER = ".gtkx-headless-runtime";
6
+
7
+ const SWAY_CONFIG_LINES = [
8
+ /^xwayland disable$/,
9
+ /^default_border none$/,
10
+ /^default_floating_border none$/,
11
+ /^output HEADLESS-1 resolution [1-9]\d*x[1-9]\d*$/,
12
+ /^output HEADLESS-1 bg #000000 solid_color$/,
13
+ /^for_window \[app_id="\.\*"\] floating enable, border none$/,
14
+ /^for_window \[title="\.\*"\] floating enable, border none$/,
15
+ /^$/,
16
+ ];
17
+
18
+ const createSwayConfig = (width: string, height: string): string =>
19
+ [
20
+ "xwayland disable",
21
+ "default_border none",
22
+ "default_floating_border none",
23
+ `output HEADLESS-1 resolution ${width}x${height}`,
24
+ "output HEADLESS-1 bg #000000 solid_color",
25
+ 'for_window [app_id=".*"] floating enable, border none',
26
+ 'for_window [title=".*"] floating enable, border none',
27
+ "",
28
+ ].join("\n");
29
+
30
+ const createBusConfig = (busSocketPath: string): string =>
31
+ [
32
+ BUS_CONFIG_DOCTYPE,
33
+ "<busconfig>",
34
+ " <type>session</type>",
35
+ ` <listen>unix:path=${busSocketPath}</listen>`,
36
+ " <auth>EXTERNAL</auth>",
37
+ ' <policy context="default">',
38
+ ' <allow send_destination="*" eavesdrop="true"/>',
39
+ ' <allow eavesdrop="true"/>',
40
+ ' <allow own="*"/>',
41
+ " </policy>",
42
+ "</busconfig>",
43
+ ].join("\n");
44
+
45
+ const createHeadlessRuntimeMarker = (runtimeDir: string): string =>
46
+ ["gtkx-headless-runtime-v1", `runtime=${runtimeDir}`, ""].join("\n");
47
+
48
+ const isSwayConfig = (value: string): boolean => {
49
+ const lines = value.split("\n");
50
+
51
+ return lines.length === SWAY_CONFIG_LINES.length &&
52
+ SWAY_CONFIG_LINES.every((pattern, index) => pattern.test(lines[index] ?? "invalid"));
53
+ };
54
+
55
+ export {
56
+ createBusConfig,
57
+ createHeadlessRuntimeMarker,
58
+ createSwayConfig,
59
+ HEADLESS_RUNTIME_MARKER,
60
+ isSwayConfig,
61
+ };
@@ -4,6 +4,12 @@ import { chmodSync, closeSync, existsSync, mkdtempSync, openSync, rmSync, writeF
4
4
  import { Socket } from "node:net";
5
5
  import { tmpdir } from "node:os";
6
6
  import { join } from "node:path";
7
+ import {
8
+ createBusConfig,
9
+ createHeadlessRuntimeMarker,
10
+ createSwayConfig,
11
+ HEADLESS_RUNTIME_MARKER,
12
+ } from "./headless-config.ts";
7
13
  import { startNotificationService } from "./notification-service.ts";
8
14
  import { startVirtualSeat } from "./virtual-seat.ts";
9
15
 
@@ -72,10 +78,6 @@ const DEFAULT_HEADLESS_SIZE = "1024x768";
72
78
  const DEFAULT_HEADLESS_COMPOSITOR: CompositorId = "sway";
73
79
  const HEADLESS_SIZE_PATTERN = /^[1-9]\d*x[1-9]\d*$/;
74
80
 
75
- const BUS_CONFIG_DOCTYPE =
76
- '<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" ' +
77
- '"https://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">';
78
-
79
81
  const hasWestonFakeSeat = createWestonFakeSeatProbe();
80
82
 
81
83
  const compositorRegistry: Record<CompositorId, CompositorDescriptor> = {
@@ -94,16 +96,7 @@ const compositorRegistry: Record<CompositorId, CompositorDescriptor> = {
94
96
 
95
97
  writeFileSync(
96
98
  configPath,
97
- [
98
- "xwayland disable",
99
- "default_border none",
100
- "default_floating_border none",
101
- `output HEADLESS-1 resolution ${width}x${height}`,
102
- "output HEADLESS-1 bg #000000 solid_color",
103
- 'for_window [app_id=".*"] floating enable, border none',
104
- 'for_window [title=".*"] floating enable, border none',
105
- "",
106
- ].join("\n"),
99
+ createSwayConfig(width, height),
107
100
  );
108
101
 
109
102
  return spawnWithParentDeathSupervisor("sway", ["-c", configPath], {
@@ -228,22 +221,7 @@ const attachVirtualSeat = (compositor: SpawnedCompositor, socketPath: string): P
228
221
  compositor.requiresVirtualSeat ? startVirtualSeat(socketPath) : Promise.resolve(noVirtualSeat);
229
222
 
230
223
  const writeBusConfig = (busConfigPath: string, busSocketPath: string): void => {
231
- writeFileSync(
232
- busConfigPath,
233
- [
234
- BUS_CONFIG_DOCTYPE,
235
- "<busconfig>",
236
- " <type>session</type>",
237
- ` <listen>unix:path=${busSocketPath}</listen>`,
238
- " <auth>EXTERNAL</auth>",
239
- ' <policy context="default">',
240
- ' <allow send_destination="*" eavesdrop="true"/>',
241
- ' <allow eavesdrop="true"/>',
242
- ' <allow own="*"/>',
243
- " </policy>",
244
- "</busconfig>",
245
- ].join("\n"),
246
- );
224
+ writeFileSync(busConfigPath, createBusConfig(busSocketPath));
247
225
  };
248
226
 
249
227
  const exitedMessage = (label: string, path: string, code: number | null, signal: NodeJS.Signals | null): string =>
@@ -457,10 +435,25 @@ const makeTeardown = (stops: (() => void)[]): (() => void) => {
457
435
  };
458
436
  };
459
437
 
438
+ const createHeadlessRuntimeDirectory = (): string => {
439
+ const runtimeDir = mkdtempSync(join(tmpdir(), "gtkx-xdg-"));
440
+
441
+ try {
442
+ chmodSync(runtimeDir, 0o700);
443
+ const markerPath = join(runtimeDir, HEADLESS_RUNTIME_MARKER);
444
+ writeFileSync(markerPath, createHeadlessRuntimeMarker(runtimeDir), { flag: "wx", mode: 0o600 });
445
+ chmodSync(markerPath, 0o600);
446
+
447
+ return runtimeDir;
448
+ } catch (error) {
449
+ rmSync(runtimeDir, { recursive: true, force: true });
450
+ throw error;
451
+ }
452
+ };
453
+
460
454
  const startHeadlessDisplay = async (options: HeadlessOptions): Promise<() => void> => {
461
455
  const env: EnvSnapshot = {};
462
- const runtimeDir = mkdtempSync(join(tmpdir(), "gtkx-xdg-"));
463
- chmodSync(runtimeDir, 0o700);
456
+ const runtimeDir = createHeadlessRuntimeDirectory();
464
457
  const spawned: ChildProcess[] = [];
465
458
 
466
459
  const removeRuntime = (): void => {
package/src/headless.ts CHANGED
@@ -6,3 +6,8 @@ export {
6
6
  type CompositorId,
7
7
  type HeadlessOptions,
8
8
  } from "./headless-display.js";
9
+ export {
10
+ findStaleHeadlessDisplays,
11
+ reapStaleHeadlessDisplays,
12
+ type StaleHeadlessDisplay,
13
+ } from "./reap-headless-displays.js";
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import { existsSync } from "node:fs";
5
5
  import { join } from "node:path";
6
6
  import { pathToFileURL } from "node:url";
7
7
  import { type HeadlessOptions, STATIC_HEADLESS_ENV } from "./headless-display.ts";
8
+ import { reapStaleHeadlessDisplays } from "./reap-headless-displays.ts";
8
9
 
9
10
  /**
10
11
  * Options accepted by the GTKX Vitest plugin. Every headless display
@@ -42,6 +43,7 @@ const headlessPreloadSpecifier = (options: Partial<HeadlessOptions>): string =>
42
43
  */
43
44
  const gtkx = (options: PluginOptions = {}): Plugin => {
44
45
  assertSupportedNodeVersion();
46
+ reapStaleHeadlessDisplays();
45
47
  const { configFile, ...headlessOptions } = options;
46
48
  const loadConfig = createConfigLoader({ configFile });
47
49
 
@@ -0,0 +1,280 @@
1
+ import {
2
+ type CleanupDirectoryIdentity,
3
+ cleanupDirectoryIdentity,
4
+ removeCleanupDirectory,
5
+ } from "@gtkx/utils";
6
+ import { constants, lstatSync, readdirSync, readFileSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import { basename, dirname, join } from "node:path";
9
+ import {
10
+ createBusConfig,
11
+ createHeadlessRuntimeMarker,
12
+ HEADLESS_RUNTIME_MARKER,
13
+ isSwayConfig,
14
+ } from "./headless-config.ts";
15
+
16
+ type StaleHeadlessDisplay = {
17
+ runtimeDir: string;
18
+ cleanupDirectory: CleanupDirectoryIdentity;
19
+ };
20
+
21
+ const RUNTIME_DIRECTORY_PATTERN = /^gtkx-xdg-[A-Za-z0-9]{6}$/;
22
+ const STOPPED_PROCESS_STATES: Set<string> = new Set(["Z", "X", "x"]);
23
+ const CONFIG_SIZE_LIMIT = 2048;
24
+ const MINIMUM_STALE_AGE_MS = 5000;
25
+ const RUNTIME_ROOT = tmpdir();
26
+
27
+ const currentUserId = (): number | undefined => {
28
+ const getuid = process.getuid;
29
+
30
+ return getuid === undefined ? undefined : getuid();
31
+ };
32
+
33
+ const isUserOwned = (path: string, userId: number): boolean => {
34
+ try {
35
+ return lstatSync(path).uid === userId;
36
+ } catch {
37
+ return false;
38
+ }
39
+ };
40
+
41
+ const isPrivateRuntimeDirectory = (runtimeDir: string, userId: number): boolean => {
42
+ try {
43
+ const stat = lstatSync(runtimeDir);
44
+
45
+ return stat.isDirectory() &&
46
+ stat.uid === userId &&
47
+ (stat.mode & 0o777) === 0o700 &&
48
+ Date.now() - stat.mtimeMs >= MINIMUM_STALE_AGE_MS &&
49
+ RUNTIME_DIRECTORY_PATTERN.test(basename(runtimeDir)) &&
50
+ dirname(runtimeDir) === RUNTIME_ROOT;
51
+ } catch {
52
+ return false;
53
+ }
54
+ };
55
+
56
+ const readOwnedFile = (
57
+ path: string,
58
+ runtimeDir: string,
59
+ userId: number,
60
+ requiredMode?: number,
61
+ ): string | undefined => {
62
+ try {
63
+ if (dirname(path) !== runtimeDir) {
64
+ return undefined;
65
+ }
66
+
67
+ const contents = readFileSync(path, {
68
+ encoding: "utf8",
69
+ flag: constants.O_RDONLY | constants.O_NOFOLLOW,
70
+ });
71
+ const stat = lstatSync(path);
72
+
73
+ return stat.isFile() &&
74
+ stat.uid === userId &&
75
+ stat.size < CONFIG_SIZE_LIMIT &&
76
+ (requiredMode === undefined || (stat.mode & 0o777) === requiredMode)
77
+ ? contents
78
+ : undefined;
79
+ } catch {
80
+ return undefined;
81
+ }
82
+ };
83
+
84
+ const hasGeneratedRuntimeFiles = (runtimeDir: string, userId: number): boolean => {
85
+ const sway = readOwnedFile(join(runtimeDir, "sway.conf"), runtimeDir, userId);
86
+ const bus = readOwnedFile(join(runtimeDir, "session.conf"), runtimeDir, userId);
87
+ const marker = readOwnedFile(join(runtimeDir, HEADLESS_RUNTIME_MARKER), runtimeDir, userId, 0o600);
88
+
89
+ return bus === createBusConfig(join(runtimeDir, "bus")) &&
90
+ (marker === createHeadlessRuntimeMarker(runtimeDir) || (sway !== undefined && isSwayConfig(sway)));
91
+ };
92
+
93
+ const readProcessArguments = (pid: number): string[] | undefined => {
94
+ try {
95
+ const stat = readFileSync(`/proc/${String(pid)}/stat`, "utf8");
96
+ const state = stat.slice(stat.lastIndexOf(") ") + 2).split(" ", 1)[0];
97
+
98
+ if (state === undefined || STOPPED_PROCESS_STATES.has(state)) {
99
+ return undefined;
100
+ }
101
+
102
+ return readFileSync(`/proc/${String(pid)}/cmdline`)
103
+ .toString()
104
+ .split("\0")
105
+ .filter((argument) => argument.length > 0);
106
+ } catch {
107
+ return undefined;
108
+ }
109
+ };
110
+
111
+ const runtimeFromArgument = (argument: string): string | undefined => {
112
+ const path = argument.startsWith("--config-file=") ? argument.slice("--config-file=".length) : argument;
113
+ const name = basename(path);
114
+
115
+ if (name !== "sway.conf" && name !== "session.conf") {
116
+ return undefined;
117
+ }
118
+
119
+ const runtimeDir = dirname(path);
120
+
121
+ return RUNTIME_DIRECTORY_PATTERN.test(basename(runtimeDir)) && dirname(runtimeDir) === RUNTIME_ROOT
122
+ ? runtimeDir
123
+ : undefined;
124
+ };
125
+
126
+ const runtimeFromEnvironment = (pid: number): string | undefined => {
127
+ try {
128
+ const prefix = "XDG_RUNTIME_DIR=";
129
+ const entry = readFileSync(`/proc/${String(pid)}/environ`, "utf8")
130
+ .split("\0")
131
+ .find((value) => value.startsWith(prefix));
132
+ const runtimeDir = entry?.slice(prefix.length);
133
+
134
+ return runtimeDir !== undefined &&
135
+ RUNTIME_DIRECTORY_PATTERN.test(basename(runtimeDir)) &&
136
+ dirname(runtimeDir) === RUNTIME_ROOT
137
+ ? runtimeDir
138
+ : undefined;
139
+ } catch {
140
+ return undefined;
141
+ }
142
+ };
143
+
144
+ const isOwnedProcessId = (pid: number, userId: number): boolean =>
145
+ Number.isSafeInteger(pid) && pid > 1 && isUserOwned(`/proc/${String(pid)}`, userId);
146
+
147
+ const addRuntimeArguments = (live: Set<string>, processArgs: string[]): void => {
148
+ for (const argument of processArgs) {
149
+ const runtimeDir = runtimeFromArgument(argument);
150
+
151
+ if (runtimeDir !== undefined) {
152
+ live.add(runtimeDir);
153
+ }
154
+ }
155
+ };
156
+
157
+ const addProcessRuntimes = (live: Set<string>, pid: number): void => {
158
+ const processArgs = readProcessArguments(pid);
159
+
160
+ if (processArgs === undefined) {
161
+ return;
162
+ }
163
+
164
+ addRuntimeArguments(live, processArgs);
165
+ const runtimeDir = runtimeFromEnvironment(pid);
166
+
167
+ if (runtimeDir !== undefined) {
168
+ live.add(runtimeDir);
169
+ }
170
+ };
171
+
172
+ const findLiveRuntimeDirectories = (userId: number): Set<string> => {
173
+ const live: Set<string> = new Set();
174
+ const processEntries = readdirSync("/proc");
175
+
176
+ for (const entry of processEntries) {
177
+ const pid = Number(entry);
178
+
179
+ if (!isOwnedProcessId(pid, userId)) {
180
+ continue;
181
+ }
182
+
183
+ addProcessRuntimes(live, pid);
184
+ }
185
+
186
+ return live;
187
+ };
188
+
189
+ const classifyRuntimeDirectory = (
190
+ name: string,
191
+ userId: number,
192
+ live: ReadonlySet<string>,
193
+ ): StaleHeadlessDisplay[] => {
194
+ const runtimeDir = join(RUNTIME_ROOT, name);
195
+
196
+ if (
197
+ live.has(runtimeDir) ||
198
+ !isPrivateRuntimeDirectory(runtimeDir, userId) ||
199
+ !hasGeneratedRuntimeFiles(runtimeDir, userId)
200
+ ) {
201
+ return [];
202
+ }
203
+
204
+ const cleanupDirectory = cleanupDirectoryIdentity(runtimeDir);
205
+
206
+ return cleanupDirectory === undefined ? [] : [{ runtimeDir, cleanupDirectory }];
207
+ };
208
+
209
+ const findStaleHeadlessDisplays = (): StaleHeadlessDisplay[] => {
210
+ const userId = currentUserId();
211
+
212
+ if (userId === undefined) {
213
+ return [];
214
+ }
215
+
216
+ const live = findLiveRuntimeDirectories(userId);
217
+
218
+ return readdirSync(RUNTIME_ROOT, { withFileTypes: true })
219
+ .filter((entry) => entry.isDirectory() && RUNTIME_DIRECTORY_PATTERN.test(entry.name))
220
+ .flatMap((entry) => classifyRuntimeDirectory(entry.name, userId, live));
221
+ };
222
+
223
+ const isSameCleanupDirectory = (
224
+ left: CleanupDirectoryIdentity | undefined,
225
+ right: CleanupDirectoryIdentity,
226
+ ): boolean =>
227
+ left?.device === right.device &&
228
+ left.inode === right.inode &&
229
+ left.userId === right.userId;
230
+
231
+ const isReapable = (
232
+ candidate: StaleHeadlessDisplay,
233
+ userId: number,
234
+ live: ReadonlySet<string>,
235
+ ): boolean =>
236
+ !live.has(candidate.runtimeDir) &&
237
+ isSameCleanupDirectory(cleanupDirectoryIdentity(candidate.runtimeDir), candidate.cleanupDirectory) &&
238
+ isPrivateRuntimeDirectory(candidate.runtimeDir, userId) &&
239
+ hasGeneratedRuntimeFiles(candidate.runtimeDir, userId);
240
+
241
+ const didReapCandidate = (
242
+ candidate: StaleHeadlessDisplay,
243
+ userId: number,
244
+ live: ReadonlySet<string>,
245
+ ): boolean => {
246
+ if (!isReapable(candidate, userId, live)) {
247
+ return false;
248
+ }
249
+
250
+ removeCleanupDirectory(candidate.cleanupDirectory);
251
+
252
+ return cleanupDirectoryIdentity(candidate.runtimeDir) === undefined;
253
+ };
254
+
255
+ const reapStaleHeadlessDisplays = (
256
+ candidates: readonly StaleHeadlessDisplay[] = findStaleHeadlessDisplays(),
257
+ ): string[] => {
258
+ const userId = currentUserId();
259
+
260
+ if (userId === undefined) {
261
+ return [];
262
+ }
263
+
264
+ const live = findLiveRuntimeDirectories(userId);
265
+ const removed: string[] = [];
266
+
267
+ for (const candidate of candidates) {
268
+ if (didReapCandidate(candidate, userId, live)) {
269
+ removed.push(candidate.runtimeDir);
270
+ }
271
+ }
272
+
273
+ return removed;
274
+ };
275
+
276
+ export {
277
+ findStaleHeadlessDisplays,
278
+ reapStaleHeadlessDisplays,
279
+ type StaleHeadlessDisplay,
280
+ };
@@ -1,6 +1,8 @@
1
- import { installGracefulShutdown } from "@gtkx/utils";
1
+ import { installGracefulShutdown, watchParentProcess } from "@gtkx/utils";
2
2
  import { readHeadlessOptions, resolveHeadlessOptions, startHeadlessDisplay } from "./headless-display.ts";
3
3
 
4
+ watchParentProcess();
5
+
4
6
  const options = readHeadlessOptions(new URL(import.meta.url).searchParams);
5
7
  const teardown = await startHeadlessDisplay(resolveHeadlessOptions(options));
6
8