@gtkx/vitest 1.0.0-rc.1 → 1.0.0-rc.2

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.
Files changed (45) hide show
  1. package/dist/headless-display.d.ts +8 -7
  2. package/dist/headless-display.d.ts.map +1 -1
  3. package/dist/headless-display.js +221 -136
  4. package/dist/headless-display.js.map +1 -1
  5. package/dist/headless-globals.d.ts +10 -0
  6. package/dist/headless-globals.d.ts.map +1 -0
  7. package/dist/headless-globals.js +13 -0
  8. package/dist/headless-globals.js.map +1 -0
  9. package/dist/index.d.ts +4 -4
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +7 -6
  12. package/dist/index.js.map +1 -1
  13. package/dist/install-headless-shutdown.d.ts +3 -0
  14. package/dist/install-headless-shutdown.d.ts.map +1 -0
  15. package/dist/install-headless-shutdown.js +12 -0
  16. package/dist/install-headless-shutdown.js.map +1 -0
  17. package/dist/notification-service.d.ts +2 -1
  18. package/dist/notification-service.d.ts.map +1 -1
  19. package/dist/notification-service.js +17 -16
  20. package/dist/notification-service.js.map +1 -1
  21. package/dist/resolve-executable.d.ts +3 -0
  22. package/dist/resolve-executable.d.ts.map +1 -0
  23. package/dist/resolve-executable.js +26 -0
  24. package/dist/resolve-executable.js.map +1 -0
  25. package/dist/virtual-seat.d.ts +3 -0
  26. package/dist/virtual-seat.d.ts.map +1 -0
  27. package/dist/virtual-seat.js +155 -0
  28. package/dist/virtual-seat.js.map +1 -0
  29. package/dist/worker-preload.js +2 -1
  30. package/dist/worker-preload.js.map +1 -1
  31. package/dist/worker-setup.d.ts +1 -5
  32. package/dist/worker-setup.d.ts.map +1 -1
  33. package/dist/worker-setup.js +1 -8
  34. package/dist/worker-setup.js.map +1 -1
  35. package/package.json +4 -6
  36. package/src/dbus-native.d.ts +4 -4
  37. package/src/headless-display.ts +351 -174
  38. package/src/headless-globals.ts +22 -0
  39. package/src/index.ts +15 -12
  40. package/src/install-headless-shutdown.ts +15 -0
  41. package/src/notification-service.ts +22 -19
  42. package/src/resolve-executable.ts +32 -0
  43. package/src/virtual-seat.ts +214 -0
  44. package/src/worker-preload.ts +2 -1
  45. package/src/worker-setup.ts +1 -13
@@ -1,20 +1,18 @@
1
1
  /**
2
2
  * Wayland compositors that can back a headless display.
3
3
  */
4
- export type CompositorId = "sway" | "weston";
5
- export declare const DEFAULT_HEADLESS_SIZE = "1024x768";
4
+ type CompositorId = "sway" | "weston";
6
5
  /**
7
6
  * Settings for the per-worker headless Wayland display.
8
7
  */
9
- export type HeadlessOptions = {
8
+ type HeadlessOptions = {
10
9
  /** Output resolution as a "WIDTHxHEIGHT" string, for example "1024x768". */
11
10
  size: string;
12
11
  /** Wayland compositor used to back the headless display. */
13
12
  compositor: CompositorId;
14
13
  };
15
- export declare const resolveHeadlessOptions: (provided: Partial<HeadlessOptions>) => HeadlessOptions;
16
- export declare const readHeadlessOptions: (params: URLSearchParams) => Partial<HeadlessOptions>;
17
- export declare const STATIC_HEADLESS_ENV: {
14
+ declare const DEFAULT_HEADLESS_SIZE = "1024x768";
15
+ declare const STATIC_HEADLESS_ENV: {
18
16
  GDK_BACKEND: string;
19
17
  GDK_DISABLE: string;
20
18
  GDK_DEBUG: string;
@@ -26,5 +24,8 @@ export declare const STATIC_HEADLESS_ENV: {
26
24
  ALSOFT_DRIVERS: string;
27
25
  ALSOFT_LOGLEVEL: string;
28
26
  };
29
- export declare const startHeadlessDisplay: (options: HeadlessOptions) => Promise<() => void>;
27
+ declare const resolveHeadlessOptions: (provided: Partial<HeadlessOptions>) => HeadlessOptions;
28
+ declare const readHeadlessOptions: (params: URLSearchParams) => Partial<HeadlessOptions>;
29
+ declare const startHeadlessDisplay: (options: HeadlessOptions) => Promise<() => void>;
30
+ export { DEFAULT_HEADLESS_SIZE, STATIC_HEADLESS_ENV, resolveHeadlessOptions, readHeadlessOptions, startHeadlessDisplay, type CompositorId, type HeadlessOptions, };
30
31
  //# sourceMappingURL=headless-display.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"headless-display.d.ts","sourceRoot":"","sources":["../src/headless-display.ts"],"names":[],"mappings":"AAOA;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,QAAQ,CAAC;AAE7C,eAAO,MAAM,qBAAqB,aAAa,CAAC;AAEhD;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC1B,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,UAAU,EAAE,YAAY,CAAC;CAC5B,CAAC;AAIF,eAAO,MAAM,sBAAsB,aAAc,OAAO,CAAC,eAAe,CAAC,KAAG,eAG1E,CAAC;AAyFH,eAAO,MAAM,mBAAmB,WAAY,eAAe,KAAG,OAAO,CAAC,eAAe,CAOpF,CAAC;AAkGF,eAAO,MAAM,mBAAmB;IAC5B,WAAW;IACX,WAAW;IACX,SAAS;IACT,YAAY;IACZ,QAAQ;IACR,qBAAqB;IACrB,aAAa;IACb,iBAAiB;IACjB,cAAc;IACd,eAAe;CAClB,CAAC;AAyCF,eAAO,MAAM,oBAAoB,YAAmB,eAAe,KAAG,OAAO,CAAC,MAAM,IAAI,CAsDvF,CAAC"}
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;AAgDF,QAAA,MAAM,qBAAqB,aAAa,CAAC;AA6DzC,QAAA,MAAM,mBAAmB;;;;;;;;;;;CAWxB,CAAC;AAEF,QAAA,MAAM,sBAAsB,GAAI,UAAU,OAAO,CAAC,eAAe,CAAC,KAAG,eAGnE,CAAC;AAiDH,QAAA,MAAM,mBAAmB,GAAI,QAAQ,eAAe,KAAG,OAAO,CAAC,eAAe,CAe7E,CAAC;AA4OF,QAAA,MAAM,oBAAoB,GAAU,SAAS,eAAe,KAAG,OAAO,CAAC,MAAM,IAAI,CAiDhF,CAAC;AAEF,OAAO,EACH,qBAAqB,EACrB,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACpB,KAAK,YAAY,EACjB,KAAK,eAAe,GACvB,CAAC"}
@@ -4,43 +4,18 @@ import { Socket } from "node:net";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { startNotificationService } from "./notification-service.js";
7
- export const DEFAULT_HEADLESS_SIZE = "1024x768";
8
- const DEFAULT_HEADLESS_COMPOSITOR = "weston";
9
- export const resolveHeadlessOptions = (provided) => ({
10
- size: provided.size ?? DEFAULT_HEADLESS_SIZE,
11
- compositor: provided.compositor ?? DEFAULT_HEADLESS_COMPOSITOR,
12
- });
13
- const applyEnv = (snapshot, values) => {
14
- for (const [name, value] of Object.entries(values)) {
15
- if (!(name in snapshot))
16
- snapshot[name] = process.env[name];
17
- process.env[name] = value;
18
- }
19
- };
20
- const restoreEnv = (snapshot) => {
21
- for (const [name, previous] of Object.entries(snapshot)) {
22
- if (previous === undefined)
23
- delete process.env[name];
24
- else
25
- process.env[name] = previous;
26
- }
27
- };
28
- const spawnWithParentDeathSignal = (command, args, stdio) => {
29
- const child = spawn("setpriv", ["--pdeathsig", "SIGKILL", command, ...args], { stdio });
30
- child.unref();
31
- return child;
32
- };
33
- let westonFakeSeatSupport;
34
- const westonSupportsFakeSeat = () => {
35
- if (westonFakeSeatSupport === undefined) {
36
- const help = spawnSync("weston", ["--help"], { encoding: "utf8" });
37
- westonFakeSeatSupport = `${help.stdout}${help.stderr}`.includes("--fake-seat");
38
- }
39
- return westonFakeSeatSupport;
40
- };
7
+ import { resolveExecutable } from "./resolve-executable.js";
8
+ import { startVirtualSeat } from "./virtual-seat.js";
9
+ const DEFAULT_HEADLESS_SIZE = "1024x768";
10
+ const DEFAULT_HEADLESS_COMPOSITOR = "sway";
11
+ const BUS_CONFIG_DOCTYPE = '<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" ' +
12
+ '"https://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">';
13
+ const PARENT_DEATH_SCRIPT = 'trap \'kill -9 "$child" 2>/dev/null\' TERM; "$@" & child=$!; wait "$child"';
14
+ const hasWestonFakeSeat = createWestonFakeSeatProbe();
41
15
  const compositorRegistry = {
42
16
  sway: {
43
17
  socket: "wayland-1",
18
+ needsVirtualSeat: true,
44
19
  env: {
45
20
  WLR_BACKENDS: "headless",
46
21
  WLR_RENDERER: "pixman",
@@ -65,119 +40,207 @@ const compositorRegistry = {
65
40
  },
66
41
  weston: {
67
42
  socket: "wayland-0",
43
+ needsVirtualSeat: false,
68
44
  env: {},
69
45
  start: (_runtimeDir, width, height) => spawnWithParentDeathSignal("weston", [
70
46
  "--backend=headless",
71
47
  "--renderer=pixman",
72
- ...(westonSupportsFakeSeat() ? ["--fake-seat"] : []),
48
+ ...(hasWestonFakeSeat() ? ["--fake-seat"] : []),
73
49
  `--width=${width}`,
74
50
  `--height=${height}`,
75
51
  "--socket=wayland-0",
76
52
  ], ["ignore", "ignore", "pipe"]),
77
53
  },
78
54
  };
55
+ const STATIC_HEADLESS_ENV = {
56
+ GDK_BACKEND: "wayland",
57
+ GDK_DISABLE: "vulkan",
58
+ GDK_DEBUG: "no-vsync",
59
+ GSK_RENDERER: "cairo",
60
+ GTK_A11Y: "test",
61
+ LIBGL_ALWAYS_SOFTWARE: "1",
62
+ GST_GL_WINDOW: "none",
63
+ GSETTINGS_BACKEND: "memory",
64
+ ALSOFT_DRIVERS: "null",
65
+ ALSOFT_LOGLEVEL: "0",
66
+ };
67
+ const resolveHeadlessOptions = (provided) => ({
68
+ size: provided.size ?? DEFAULT_HEADLESS_SIZE,
69
+ compositor: provided.compositor ?? DEFAULT_HEADLESS_COMPOSITOR,
70
+ });
71
+ const applyEnv = (snapshot, values) => {
72
+ for (const [name, value] of Object.entries(values)) {
73
+ if (!Object.hasOwn(snapshot, name)) {
74
+ snapshot[name] = process.env[name];
75
+ }
76
+ process.env[name] = value;
77
+ }
78
+ };
79
+ const restoreEnv = (snapshot) => {
80
+ for (const [name, previous] of Object.entries(snapshot)) {
81
+ if (previous === undefined) {
82
+ Reflect.deleteProperty(process.env, name);
83
+ }
84
+ else {
85
+ process.env[name] = previous;
86
+ }
87
+ }
88
+ };
89
+ const spawnWithParentDeathSignal = (command, args, stdio) => {
90
+ const child = spawn(resolveExecutable("setpriv"), ["--pdeathsig", "SIGTERM", "sh", "-c", PARENT_DEATH_SCRIPT, "sh", command, ...args], { stdio });
91
+ child.unref();
92
+ return child;
93
+ };
94
+ function createWestonFakeSeatProbe() {
95
+ let isSupported;
96
+ return () => {
97
+ if (isSupported === undefined) {
98
+ const help = spawnSync(resolveExecutable("weston"), ["--help"], { encoding: "utf8" });
99
+ isSupported = `${help.stdout}${help.stderr}`.includes("--fake-seat");
100
+ }
101
+ return isSupported;
102
+ };
103
+ }
79
104
  const isCompositorId = (value) => Object.hasOwn(compositorRegistry, value);
80
- export const readHeadlessOptions = (params) => {
105
+ const readHeadlessOptions = (params) => {
81
106
  const options = {};
82
107
  const size = params.get("size");
83
- if (size !== null)
108
+ if (size !== null) {
84
109
  options.size = size;
110
+ }
85
111
  const compositor = params.get("compositor");
86
- if (compositor !== null && isCompositorId(compositor))
112
+ if (compositor !== null && isCompositorId(compositor)) {
87
113
  options.compositor = compositor;
114
+ }
88
115
  return options;
89
116
  };
90
117
  const startCompositor = (runtimeDir, options, env) => {
91
118
  const descriptor = compositorRegistry[options.compositor];
92
- const [width = "", height = ""] = options.size.split("x");
119
+ const [width = "", height = ""] = options.size.split("x", 2);
93
120
  applyEnv(env, descriptor.env);
94
- return { child: descriptor.start(runtimeDir, width, height), socket: descriptor.socket };
121
+ return {
122
+ child: descriptor.start(runtimeDir, width, height),
123
+ socket: descriptor.socket,
124
+ needsVirtualSeat: descriptor.needsVirtualSeat,
125
+ };
95
126
  };
127
+ const noVirtualSeat = () => undefined;
128
+ const attachVirtualSeat = (compositor, socketPath) => compositor.needsVirtualSeat ? startVirtualSeat(socketPath) : Promise.resolve(noVirtualSeat);
96
129
  const writeBusConfig = (busConfigPath, busSocketPath) => {
97
- writeFileSync(busConfigPath, `<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
98
- <busconfig>
99
- <type>session</type>
100
- <listen>unix:path=${busSocketPath}</listen>
101
- <auth>EXTERNAL</auth>
102
- <policy context="default">
103
- <allow send_destination="*" eavesdrop="true"/>
104
- <allow eavesdrop="true"/>
105
- <allow own="*"/>
106
- </policy>
107
- </busconfig>`);
108
- };
109
- const waitForSocket = (path, { label, timeout = 15000, child, signal }) => new Promise((resolve, reject) => {
110
- let log = "";
130
+ writeFileSync(busConfigPath, [
131
+ BUS_CONFIG_DOCTYPE,
132
+ "<busconfig>",
133
+ " <type>session</type>",
134
+ ` <listen>unix:path=${busSocketPath}</listen>`,
135
+ " <auth>EXTERNAL</auth>",
136
+ ' <policy context="default">',
137
+ ' <allow send_destination="*" eavesdrop="true"/>',
138
+ ' <allow eavesdrop="true"/>',
139
+ ' <allow own="*"/>',
140
+ " </policy>",
141
+ "</busconfig>",
142
+ ].join("\n"));
143
+ };
144
+ const captureStderr = (child) => {
111
145
  const stderr = child?.stderr ?? null;
146
+ let log = "";
112
147
  stderr?.setEncoding("utf8");
113
148
  stderr?.on("data", (chunk) => {
114
149
  log += chunk;
115
150
  });
116
- let timer;
117
- let poll;
118
- const stopListening = () => {
119
- clearTimeout(timer);
120
- clearInterval(poll);
121
- child?.removeListener("exit", onExit);
122
- child?.removeListener("error", onError);
123
- signal?.removeEventListener("abort", onAbort);
124
- stderr?.removeAllListeners("data");
125
- stderr?.resume();
126
- if (stderr instanceof Socket)
127
- stderr.unref();
151
+ return {
152
+ read: () => log,
153
+ stop: () => {
154
+ stderr?.removeAllListeners("data");
155
+ stderr?.resume();
156
+ if (stderr instanceof Socket) {
157
+ stderr.unref();
158
+ }
159
+ },
160
+ };
161
+ };
162
+ const trackChild = (child, handlers) => {
163
+ child.on("exit", handlers.exit);
164
+ child.on("error", handlers.error);
165
+ return () => {
166
+ child.removeListener("exit", handlers.exit);
167
+ child.removeListener("error", handlers.error);
168
+ };
169
+ };
170
+ const exitedMessage = (label, path, code, signal) => `${label} exited (code ${String(code)}, signal ${signal ?? "null"}) before ${path} appeared`;
171
+ const runCleanups = (cleanups) => {
172
+ for (const cleanup of cleanups) {
173
+ cleanup();
174
+ }
175
+ cleanups.length = 0;
176
+ };
177
+ const pollForPath = (path, onFound) => setInterval(() => {
178
+ if (existsSync(path)) {
179
+ onFound();
180
+ }
181
+ }, 50);
182
+ const stderrSuffix = (child, stderr) => child ? `\n${stderr.read()}` : "";
183
+ const hasAlreadyAborted = (signal, onAbort, cleanups) => {
184
+ if (signal === undefined) {
185
+ return false;
186
+ }
187
+ if (signal.aborted) {
188
+ return true;
189
+ }
190
+ signal.addEventListener("abort", onAbort);
191
+ cleanups.push(() => {
192
+ signal.removeEventListener("abort", onAbort);
193
+ });
194
+ return false;
195
+ };
196
+ const watchForSocket = ({ path, options, resolve, reject }) => {
197
+ const { label, timeout = 15_000, child, signal } = options;
198
+ const stderr = captureStderr(child);
199
+ const cleanups = [stderr.stop];
200
+ const stop = () => {
201
+ runCleanups(cleanups);
202
+ };
203
+ const fail = (message) => {
204
+ stop();
205
+ reject(new Error(message));
128
206
  };
129
207
  const onExit = (code, terminationSignal) => {
130
- stopListening();
131
- reject(new Error(`${label} exited (code ${code ?? "null"}, signal ${terminationSignal ?? "null"}) before ${path} appeared\n${log}`));
208
+ fail(`${exitedMessage(label, path, code, terminationSignal)}\n${stderr.read()}`);
132
209
  };
133
210
  const onError = (cause) => {
134
- stopListening();
135
- reject(new Error(`${label} failed to spawn: ${cause.message}\n${log}`));
211
+ fail(`${label} failed to spawn: ${cause.message}\n${stderr.read()}`);
136
212
  };
137
213
  const onAbort = () => {
138
- stopListening();
139
- reject(new Error(`${label} startup aborted before ${path} appeared`));
214
+ fail(`${label} startup aborted before ${path} appeared`);
140
215
  };
141
- poll = setInterval(() => {
142
- if (existsSync(path)) {
143
- stopListening();
144
- resolve();
145
- }
146
- }, 50);
147
- timer = setTimeout(() => {
148
- stopListening();
149
- const suffix = child ? `\n${log}` : "";
150
- reject(new Error(`${label} did not become available within ${timeout}ms${suffix}`));
216
+ const poll = pollForPath(path, () => {
217
+ stop();
218
+ resolve();
219
+ });
220
+ const timer = setTimeout(() => {
221
+ fail(`${label} did not become available within ${String(timeout)}ms${stderrSuffix(child, stderr)}`);
151
222
  }, timeout);
152
- child?.on("exit", onExit);
153
- child?.on("error", onError);
154
- if (signal) {
155
- if (signal.aborted) {
156
- onAbort();
157
- return;
158
- }
159
- signal.addEventListener("abort", onAbort);
223
+ cleanups.push(() => {
224
+ clearInterval(poll);
225
+ clearTimeout(timer);
226
+ });
227
+ if (child) {
228
+ cleanups.push(trackChild(child, { exit: onExit, error: onError }));
229
+ }
230
+ if (hasAlreadyAborted(signal, onAbort, cleanups)) {
231
+ onAbort();
160
232
  }
161
- });
162
- export const STATIC_HEADLESS_ENV = {
163
- GDK_BACKEND: "wayland",
164
- GDK_DISABLE: "vulkan",
165
- GDK_DEBUG: "no-vsync",
166
- GSK_RENDERER: "cairo",
167
- GTK_A11Y: "test",
168
- LIBGL_ALWAYS_SOFTWARE: "1",
169
- GST_GL_WINDOW: "none",
170
- GSETTINGS_BACKEND: "memory",
171
- ALSOFT_DRIVERS: "null",
172
- ALSOFT_LOGLEVEL: "0",
173
233
  };
234
+ const waitForSocket = (path, options) => new Promise((resolve, reject) => {
235
+ watchForSocket({ path, options, resolve, reject });
236
+ });
174
237
  const captureCompositorStderr = (child, logPath) => {
175
238
  const captured = [];
176
239
  const stderr = child.stderr;
177
240
  if (stderr !== null) {
178
241
  stderr.setEncoding("utf8");
179
242
  const logStream = createWriteStream(logPath);
180
- logStream.on("error", () => { });
243
+ logStream.on("error", () => undefined);
181
244
  stderr.on("data", (chunk) => {
182
245
  captured.push(chunk);
183
246
  logStream.write(chunk);
@@ -185,24 +248,48 @@ const captureCompositorStderr = (child, logPath) => {
185
248
  }
186
249
  return captured;
187
250
  };
188
- const reportUnexpectedCompositorExit = (child, capturedStderr) => {
189
- if (child.exitCode === null && child.signalCode === null)
190
- return;
191
- process.stderr.write(`[gtkx] headless compositor died before teardown (code ${child.exitCode ?? "null"}, ` +
192
- `signal ${child.signalCode ?? "null"}); the worker's Wayland client was severed.\n${capturedStderr.join("")}`);
251
+ const compositorExitMessage = (code, signal, capturedStderr) => `[gtkx] the headless compositor exited (code ${String(code)}, signal ${signal ?? "null"}); ` +
252
+ `every Wayland client in this worker has been severed.\n${capturedStderr.join("")}`;
253
+ const watchCompositorExit = (child, capturedStderr) => {
254
+ const report = (code, signal) => {
255
+ process.stderr.write(compositorExitMessage(code, signal, capturedStderr));
256
+ };
257
+ child.on("exit", report);
258
+ return () => child.removeListener("exit", report);
259
+ };
260
+ const waitForDisplaySockets = async (sockets) => {
261
+ const { compositor, compositorSocketPath, busChild, busSocketPath } = sockets;
262
+ const abort = new AbortController();
263
+ try {
264
+ await Promise.all([
265
+ waitForSocket(compositorSocketPath, {
266
+ label: "Compositor",
267
+ child: compositor.child,
268
+ signal: abort.signal,
269
+ }),
270
+ waitForSocket(busSocketPath, { label: "D-Bus session bus", child: busChild, signal: abort.signal }),
271
+ ]);
272
+ }
273
+ finally {
274
+ abort.abort();
275
+ }
193
276
  };
194
- const makeTeardown = (compositor, capturedStderr, stopNotifications, removeRuntime) => {
195
- let torndown = false;
277
+ const killSpawned = (children) => {
278
+ for (const child of children) {
279
+ child.kill("SIGTERM");
280
+ }
281
+ };
282
+ const makeTeardown = (stops) => {
283
+ let isTorndown = false;
196
284
  return () => {
197
- if (torndown)
285
+ if (isTorndown) {
198
286
  return;
199
- torndown = true;
200
- reportUnexpectedCompositorExit(compositor, capturedStderr);
201
- stopNotifications();
202
- removeRuntime();
287
+ }
288
+ isTorndown = true;
289
+ runCleanups(stops);
203
290
  };
204
291
  };
205
- export const startHeadlessDisplay = async (options) => {
292
+ const startHeadlessDisplay = async (options) => {
206
293
  const env = {};
207
294
  const runtimeDir = mkdtempSync(join(tmpdir(), "gtkx-xdg-"));
208
295
  chmodSync(runtimeDir, 0o700);
@@ -222,29 +309,27 @@ export const startHeadlessDisplay = async (options) => {
222
309
  const compositor = startCompositor(runtimeDir, options, env);
223
310
  spawned.push(compositor.child);
224
311
  applyEnv(env, { WAYLAND_DISPLAY: compositor.socket });
225
- const abort = new AbortController();
226
- try {
227
- await Promise.all([
228
- waitForSocket(join(runtimeDir, compositor.socket), {
229
- label: "Compositor",
230
- child: compositor.child,
231
- signal: abort.signal,
232
- }),
233
- waitForSocket(busSocketPath, { label: "D-Bus session bus", child: busChild, signal: abort.signal }),
234
- ]);
235
- }
236
- finally {
237
- abort.abort();
238
- }
312
+ const compositorSocketPath = join(runtimeDir, compositor.socket);
313
+ await waitForDisplaySockets({ compositor, compositorSocketPath, busChild, busSocketPath });
314
+ const stopVirtualSeat = await attachVirtualSeat(compositor, compositorSocketPath);
239
315
  const stopNotifications = await startNotificationService(`unix:path=${busSocketPath}`);
240
- const capturedStderr = captureCompositorStderr(compositor.child, join(runtimeDir, "weston.stderr.log"));
241
- return makeTeardown(compositor.child, capturedStderr, stopNotifications, removeRuntime);
316
+ const capturedStderr = captureCompositorStderr(compositor.child, join(runtimeDir, "compositor.stderr.log"));
317
+ const stopExitWatch = watchCompositorExit(compositor.child, capturedStderr);
318
+ return makeTeardown([
319
+ stopExitWatch,
320
+ () => {
321
+ killSpawned(spawned);
322
+ },
323
+ stopVirtualSeat,
324
+ stopNotifications,
325
+ removeRuntime,
326
+ ]);
242
327
  }
243
- catch (cause) {
244
- for (const child of spawned)
245
- child.kill("SIGKILL");
328
+ catch (error) {
329
+ killSpawned(spawned);
246
330
  removeRuntime();
247
- throw cause;
331
+ throw error;
248
332
  }
249
333
  };
334
+ export { DEFAULT_HEADLESS_SIZE, STATIC_HEADLESS_ENV, resolveHeadlessOptions, readHeadlessOptions, startHeadlessDisplay, };
250
335
  //# sourceMappingURL=headless-display.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"headless-display.js","sourceRoot":"","sources":["../src/headless-display.ts"],"names":[],"mappings":"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","sourcesContent":["import { type ChildProcess, type StdioOptions, spawn, spawnSync } from \"node:child_process\";\nimport { chmodSync, createWriteStream, existsSync, mkdtempSync, rmSync, writeFileSync } from \"node:fs\";\nimport { Socket } from \"node:net\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { startNotificationService } from \"./notification-service.js\";\n\n/**\n * Wayland compositors that can back a headless display.\n */\nexport type CompositorId = \"sway\" | \"weston\";\n\nexport const DEFAULT_HEADLESS_SIZE = \"1024x768\";\n\n/**\n * Settings for the per-worker headless Wayland display.\n */\nexport type 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\nconst DEFAULT_HEADLESS_COMPOSITOR: CompositorId = \"weston\";\n\nexport const resolveHeadlessOptions = (provided: Partial<HeadlessOptions>): HeadlessOptions => ({\n size: provided.size ?? DEFAULT_HEADLESS_SIZE,\n compositor: provided.compositor ?? DEFAULT_HEADLESS_COMPOSITOR,\n});\n\ntype EnvSnapshot = { [name: string]: string | undefined };\n\nconst applyEnv = (snapshot: EnvSnapshot, values: { [name: string]: string }): void => {\n for (const [name, value] of Object.entries(values)) {\n if (!(name in snapshot)) snapshot[name] = process.env[name];\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) delete process.env[name];\n else process.env[name] = previous;\n }\n};\n\nconst spawnWithParentDeathSignal = (command: string, args: string[], stdio: StdioOptions): ChildProcess => {\n const child = spawn(\"setpriv\", [\"--pdeathsig\", \"SIGKILL\", command, ...args], { stdio });\n child.unref();\n return child;\n};\n\nlet westonFakeSeatSupport: boolean | undefined;\n\nconst westonSupportsFakeSeat = (): boolean => {\n if (westonFakeSeatSupport === undefined) {\n const help = spawnSync(\"weston\", [\"--help\"], { encoding: \"utf8\" });\n westonFakeSeatSupport = `${help.stdout}${help.stderr}`.includes(\"--fake-seat\");\n }\n return westonFakeSeatSupport;\n};\n\ntype CompositorDescriptor = {\n socket: string;\n env: { [name: string]: string };\n start: (runtimeDir: string, width: string, height: string) => ChildProcess;\n};\n\nconst compositorRegistry: { [K in CompositorId]: CompositorDescriptor } = {\n sway: {\n socket: \"wayland-1\",\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 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 return spawnWithParentDeathSignal(\"sway\", [\"-c\", configPath], [\"ignore\", \"ignore\", \"pipe\"]);\n },\n },\n weston: {\n socket: \"wayland-0\",\n env: {},\n start: (_runtimeDir, width, height) =>\n spawnWithParentDeathSignal(\n \"weston\",\n [\n \"--backend=headless\",\n \"--renderer=pixman\",\n ...(westonSupportsFakeSeat() ? [\"--fake-seat\"] : []),\n `--width=${width}`,\n `--height=${height}`,\n \"--socket=wayland-0\",\n ],\n [\"ignore\", \"ignore\", \"pipe\"],\n ),\n },\n};\n\nconst isCompositorId = (value: string): value is CompositorId => Object.hasOwn(compositorRegistry, value);\n\nexport const readHeadlessOptions = (params: URLSearchParams): Partial<HeadlessOptions> => {\n const options: Partial<HeadlessOptions> = {};\n const size = params.get(\"size\");\n if (size !== null) options.size = size;\n const compositor = params.get(\"compositor\");\n if (compositor !== null && isCompositorId(compositor)) options.compositor = compositor;\n return options;\n};\n\ntype SpawnedCompositor = {\n child: ChildProcess;\n socket: string;\n};\n\nconst startCompositor = (runtimeDir: string, options: HeadlessOptions, env: EnvSnapshot): SpawnedCompositor => {\n const descriptor = compositorRegistry[options.compositor];\n\n const [width = \"\", height = \"\"] = options.size.split(\"x\");\n applyEnv(env, descriptor.env);\n\n return { child: descriptor.start(runtimeDir, width, height), socket: descriptor.socket };\n};\n\nconst writeBusConfig = (busConfigPath: string, busSocketPath: string): void => {\n writeFileSync(\n busConfigPath,\n `<!DOCTYPE busconfig PUBLIC \"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN\" \"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd\">\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 );\n};\n\ntype WaitForSocketOptions = {\n label: string;\n timeout?: number;\n child?: ChildProcess;\n signal?: AbortSignal;\n};\n\nconst waitForSocket = (path: string, { label, timeout = 15000, child, signal }: WaitForSocketOptions): Promise<void> =>\n new Promise((resolve, reject) => {\n let log = \"\";\n const stderr = child?.stderr ?? null;\n stderr?.setEncoding(\"utf8\");\n stderr?.on(\"data\", (chunk: string) => {\n log += chunk;\n });\n let timer: ReturnType<typeof setTimeout>;\n let poll: ReturnType<typeof setInterval>;\n const stopListening = (): void => {\n clearTimeout(timer);\n clearInterval(poll);\n child?.removeListener(\"exit\", onExit);\n child?.removeListener(\"error\", onError);\n signal?.removeEventListener(\"abort\", onAbort);\n stderr?.removeAllListeners(\"data\");\n stderr?.resume();\n if (stderr instanceof Socket) stderr.unref();\n };\n const onExit = (code: number | null, terminationSignal: NodeJS.Signals | null): void => {\n stopListening();\n reject(\n new Error(\n `${label} exited (code ${code ?? \"null\"}, signal ${terminationSignal ?? \"null\"}) before ${path} appeared\\n${log}`,\n ),\n );\n };\n const onError = (cause: Error): void => {\n stopListening();\n reject(new Error(`${label} failed to spawn: ${cause.message}\\n${log}`));\n };\n const onAbort = (): void => {\n stopListening();\n reject(new Error(`${label} startup aborted before ${path} appeared`));\n };\n poll = setInterval(() => {\n if (existsSync(path)) {\n stopListening();\n resolve();\n }\n }, 50);\n timer = setTimeout(() => {\n stopListening();\n const suffix = child ? `\\n${log}` : \"\";\n reject(new Error(`${label} did not become available within ${timeout}ms${suffix}`));\n }, timeout);\n child?.on(\"exit\", onExit);\n child?.on(\"error\", onError);\n if (signal) {\n if (signal.aborted) {\n onAbort();\n return;\n }\n signal.addEventListener(\"abort\", onAbort);\n }\n });\n\nexport const 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 captureCompositorStderr = (child: ChildProcess, logPath: string): string[] => {\n const captured: string[] = [];\n const stderr = child.stderr;\n if (stderr !== null) {\n stderr.setEncoding(\"utf8\");\n const logStream = createWriteStream(logPath);\n logStream.on(\"error\", () => {});\n stderr.on(\"data\", (chunk: string) => {\n captured.push(chunk);\n logStream.write(chunk);\n });\n }\n return captured;\n};\n\nconst reportUnexpectedCompositorExit = (child: ChildProcess, capturedStderr: string[]): void => {\n if (child.exitCode === null && child.signalCode === null) return;\n process.stderr.write(\n `[gtkx] headless compositor died before teardown (code ${child.exitCode ?? \"null\"}, ` +\n `signal ${child.signalCode ?? \"null\"}); the worker's Wayland client was severed.\\n${capturedStderr.join(\"\")}`,\n );\n};\n\nconst makeTeardown = (\n compositor: ChildProcess,\n capturedStderr: string[],\n stopNotifications: () => void,\n removeRuntime: () => void,\n): (() => void) => {\n let torndown = false;\n return (): void => {\n if (torndown) return;\n torndown = true;\n reportUnexpectedCompositorExit(compositor, capturedStderr);\n stopNotifications();\n removeRuntime();\n };\n};\n\nexport const 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, { XDG_RUNTIME_DIR: runtimeDir });\n\n const busConfigPath = join(runtimeDir, \"session.conf\");\n const busSocketPath = join(runtimeDir, \"bus\");\n writeBusConfig(busConfigPath, busSocketPath);\n\n const busChild = spawnWithParentDeathSignal(\n \"dbus-daemon\",\n [`--config-file=${busConfigPath}`],\n [\"ignore\", \"ignore\", \"pipe\"],\n );\n spawned.push(busChild);\n applyEnv(env, { DBUS_SESSION_BUS_ADDRESS: `unix:path=${busSocketPath}` });\n\n const compositor = startCompositor(runtimeDir, options, env);\n spawned.push(compositor.child);\n\n applyEnv(env, { WAYLAND_DISPLAY: compositor.socket });\n\n const abort = new AbortController();\n try {\n await Promise.all([\n waitForSocket(join(runtimeDir, compositor.socket), {\n label: \"Compositor\",\n child: compositor.child,\n signal: abort.signal,\n }),\n waitForSocket(busSocketPath, { label: \"D-Bus session bus\", child: busChild, signal: abort.signal }),\n ]);\n } finally {\n abort.abort();\n }\n\n const stopNotifications = await startNotificationService(`unix:path=${busSocketPath}`);\n\n const capturedStderr = captureCompositorStderr(compositor.child, join(runtimeDir, \"weston.stderr.log\"));\n return makeTeardown(compositor.child, capturedStderr, stopNotifications, removeRuntime);\n } catch (cause) {\n for (const child of spawned) child.kill(\"SIGKILL\");\n removeRuntime();\n throw cause;\n }\n};\n"]}
1
+ {"version":3,"file":"headless-display.js","sourceRoot":"","sources":["../src/headless-display.ts"],"names":[],"mappings":"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","sourcesContent":["import { type ChildProcess, spawn, spawnSync, type StdioOptions } from \"node:child_process\";\nimport { chmodSync, createWriteStream, existsSync, mkdtempSync, rmSync, writeFileSync } from \"node:fs\";\nimport { Socket } from \"node:net\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { startNotificationService } from \"./notification-service.js\";\nimport { resolveExecutable } from \"./resolve-executable.js\";\nimport { startVirtualSeat } from \"./virtual-seat.js\";\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 needsVirtualSeat: boolean;\n start: (runtimeDir: string, width: string, height: string) => ChildProcess;\n};\n\ntype SpawnedCompositor = {\n child: ChildProcess;\n socket: string;\n needsVirtualSeat: boolean;\n};\n\ntype WaitForSocketOptions = {\n label: string;\n timeout?: number;\n child?: ChildProcess;\n signal?: AbortSignal;\n};\n\ntype StderrCapture = {\n read: () => string;\n stop: () => void;\n};\n\ntype ChildHandlers = {\n exit: (code: number | null, signal: NodeJS.Signals | null) => void;\n error: (cause: Error) => void;\n};\n\ntype DisplaySockets = {\n compositor: SpawnedCompositor;\n compositorSocketPath: string;\n busChild: ChildProcess;\n busSocketPath: string;\n};\n\ntype SocketWatch = {\n path: string;\n options: WaitForSocketOptions;\n resolve: () => void;\n reject: (error: Error) => void;\n};\n\nconst DEFAULT_HEADLESS_SIZE = \"1024x768\";\nconst DEFAULT_HEADLESS_COMPOSITOR: CompositorId = \"sway\";\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 PARENT_DEATH_SCRIPT = 'trap \\'kill -9 \"$child\" 2>/dev/null\\' TERM; \"$@\" & child=$!; wait \"$child\"';\nconst hasWestonFakeSeat = createWestonFakeSeatProbe();\n\nconst compositorRegistry: Record<CompositorId, CompositorDescriptor> = {\n sway: {\n socket: \"wayland-1\",\n needsVirtualSeat: 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 spawnWithParentDeathSignal(\"sway\", [\"-c\", configPath], [\"ignore\", \"ignore\", \"pipe\"]);\n },\n },\n weston: {\n socket: \"wayland-0\",\n needsVirtualSeat: 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 [\"ignore\", \"ignore\", \"pipe\"],\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 size: provided.size ?? DEFAULT_HEADLESS_SIZE,\n compositor: provided.compositor ?? DEFAULT_HEADLESS_COMPOSITOR,\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\nconst spawnWithParentDeathSignal = (command: string, args: string[], stdio: StdioOptions): ChildProcess => {\n const child = spawn(\n resolveExecutable(\"setpriv\"),\n [\"--pdeathsig\", \"SIGTERM\", \"sh\", \"-c\", PARENT_DEATH_SCRIPT, \"sh\", command, ...args],\n { stdio },\n );\n\n child.unref();\n\n return child;\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 needsVirtualSeat: descriptor.needsVirtualSeat,\n };\n};\n\nconst noVirtualSeat = (): void => undefined;\n\nconst attachVirtualSeat = (compositor: SpawnedCompositor, socketPath: string): Promise<() => void> =>\n compositor.needsVirtualSeat ? 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 captureStderr = (child: ChildProcess | undefined): StderrCapture => {\n const stderr = child?.stderr ?? null;\n let log = \"\";\n stderr?.setEncoding(\"utf8\");\n\n stderr?.on(\"data\", (chunk: string) => {\n log += chunk;\n });\n\n return {\n read: () => log,\n stop: () => {\n stderr?.removeAllListeners(\"data\");\n stderr?.resume();\n\n if (stderr instanceof Socket) {\n stderr.unref();\n }\n },\n };\n};\n\nconst trackChild = (child: ChildProcess, handlers: ChildHandlers): (() => void) => {\n child.on(\"exit\", handlers.exit);\n child.on(\"error\", handlers.error);\n\n return () => {\n child.removeListener(\"exit\", handlers.exit);\n child.removeListener(\"error\", handlers.error);\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 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 stderrSuffix = (child: ChildProcess | undefined, stderr: StderrCapture): string =>\n child ? `\\n${stderr.read()}` : \"\";\n\nconst hasAlreadyAborted = (signal: AbortSignal | undefined, onAbort: () => void, cleanups: (() => void)[]): boolean => {\n if (signal === undefined) {\n return false;\n }\n\n if (signal.aborted) {\n return true;\n }\n\n signal.addEventListener(\"abort\", onAbort);\n\n cleanups.push(() => {\n signal.removeEventListener(\"abort\", onAbort);\n });\n\n return false;\n};\n\nconst watchForSocket = ({ path, options, resolve, reject }: SocketWatch): void => {\n const { label, timeout = 15_000, child, signal } = options;\n const stderr = captureStderr(child);\n const cleanups: (() => void)[] = [stderr.stop];\n\n const stop = (): void => {\n runCleanups(cleanups);\n };\n\n const fail = (message: string): void => {\n stop();\n reject(new Error(message));\n };\n\n const onExit = (code: number | null, terminationSignal: NodeJS.Signals | null): void => {\n fail(`${exitedMessage(label, path, code, terminationSignal)}\\n${stderr.read()}`);\n };\n\n const onError = (cause: Error): void => {\n fail(`${label} failed to spawn: ${cause.message}\\n${stderr.read()}`);\n };\n\n const onAbort = (): void => {\n fail(`${label} startup aborted before ${path} appeared`);\n };\n\n const poll = pollForPath(path, () => {\n stop();\n resolve();\n });\n\n const timer = setTimeout(() => {\n fail(`${label} did not become available within ${String(timeout)}ms${stderrSuffix(child, stderr)}`);\n }, timeout);\n\n cleanups.push(() => {\n clearInterval(poll);\n clearTimeout(timer);\n });\n\n if (child) {\n cleanups.push(trackChild(child, { exit: onExit, error: onError }));\n }\n\n if (hasAlreadyAborted(signal, onAbort, cleanups)) {\n onAbort();\n }\n};\n\nconst waitForSocket = (path: string, options: WaitForSocketOptions): Promise<void> =>\n new Promise((resolve, reject) => {\n watchForSocket({ path, options, resolve, reject });\n });\n\nconst captureCompositorStderr = (child: ChildProcess, logPath: string): string[] => {\n const captured: string[] = [];\n const stderr = child.stderr;\n\n if (stderr !== null) {\n stderr.setEncoding(\"utf8\");\n const logStream = createWriteStream(logPath);\n logStream.on(\"error\", (): void => undefined);\n\n stderr.on(\"data\", (chunk: string) => {\n captured.push(chunk);\n logStream.write(chunk);\n });\n }\n\n return captured;\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 (sockets: DisplaySockets): Promise<void> => {\n const { compositor, compositorSocketPath, busChild, busSocketPath } = sockets;\n const abort = new AbortController();\n\n try {\n await Promise.all([\n waitForSocket(compositorSocketPath, {\n label: \"Compositor\",\n child: compositor.child,\n signal: abort.signal,\n }),\n waitForSocket(busSocketPath, { label: \"D-Bus session bus\", child: busChild, signal: abort.signal }),\n ]);\n } finally {\n abort.abort();\n }\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, { 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(\n \"dbus-daemon\",\n [`--config-file=${busConfigPath}`],\n [\"ignore\", \"ignore\", \"pipe\"],\n );\n\n spawned.push(busChild);\n applyEnv(env, { DBUS_SESSION_BUS_ADDRESS: `unix:path=${busSocketPath}` });\n const compositor = startCompositor(runtimeDir, options, env);\n spawned.push(compositor.child);\n applyEnv(env, { WAYLAND_DISPLAY: compositor.socket });\n const compositorSocketPath = join(runtimeDir, compositor.socket);\n await waitForDisplaySockets({ compositor, compositorSocketPath, busChild, busSocketPath });\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);\n\n return makeTeardown([\n stopExitWatch,\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 DEFAULT_HEADLESS_SIZE,\n STATIC_HEADLESS_ENV,\n resolveHeadlessOptions,\n readHeadlessOptions,\n startHeadlessDisplay,\n type CompositorId,\n type HeadlessOptions,\n};\n"]}
@@ -0,0 +1,10 @@
1
+ declare global {
2
+ var gtkxHeadlessTeardown: (() => void) | undefined;
3
+ var gtkxHeadlessShutdownInstalled: boolean | undefined;
4
+ }
5
+ declare const setHeadlessTeardown: (teardown: (() => void) | undefined) => void;
6
+ declare const headlessTeardown: () => (() => void) | undefined;
7
+ declare const setHeadlessShutdownInstalled: (installed: boolean | undefined) => void;
8
+ declare const isHeadlessShutdownInstalled: () => boolean;
9
+ export { setHeadlessTeardown, headlessTeardown, setHeadlessShutdownInstalled, isHeadlessShutdownInstalled };
10
+ //# sourceMappingURL=headless-globals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headless-globals.d.ts","sourceRoot":"","sources":["../src/headless-globals.ts"],"names":[],"mappings":"AAAA,OAAO,CAAC,MAAM,CAAC;IACX,IAAI,oBAAoB,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IACnD,IAAI,6BAA6B,EAAE,OAAO,GAAG,SAAS,CAAC;CAC1D;AAMD,QAAA,MAAM,mBAAmB,GAAI,UAAU,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,KAAG,IAEjE,CAAC;AAEF,QAAA,MAAM,gBAAgB,QAAO,CAAC,MAAM,IAAI,CAAC,GAAG,SAA4C,CAAC;AAEzF,QAAA,MAAM,4BAA4B,GAAI,WAAW,OAAO,GAAG,SAAS,KAAG,IAEtE,CAAC;AAEF,QAAA,MAAM,2BAA2B,QAAO,OAA4D,CAAC;AAErG,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,4BAA4B,EAAE,2BAA2B,EAAE,CAAC"}
@@ -0,0 +1,13 @@
1
+ const defineGlobal = (key, value) => {
2
+ Object.defineProperty(globalThis, key, { value, configurable: true, writable: true });
3
+ };
4
+ const setHeadlessTeardown = (teardown) => {
5
+ defineGlobal("gtkxHeadlessTeardown", teardown);
6
+ };
7
+ const headlessTeardown = () => globalThis.gtkxHeadlessTeardown;
8
+ const setHeadlessShutdownInstalled = (installed) => {
9
+ defineGlobal("gtkxHeadlessShutdownInstalled", installed);
10
+ };
11
+ const isHeadlessShutdownInstalled = () => globalThis.gtkxHeadlessShutdownInstalled === true;
12
+ export { setHeadlessTeardown, headlessTeardown, setHeadlessShutdownInstalled, isHeadlessShutdownInstalled };
13
+ //# sourceMappingURL=headless-globals.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headless-globals.js","sourceRoot":"","sources":["../src/headless-globals.ts"],"names":[],"mappings":"AAKA,MAAM,YAAY,GAAG,CAAC,GAAW,EAAE,KAAc,EAAQ,EAAE;IACvD,MAAM,CAAC,cAAc,CAAC,UAAU,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;AAC1F,CAAC,CAAC;AAEF,MAAM,mBAAmB,GAAG,CAAC,QAAkC,EAAQ,EAAE;IACrE,YAAY,CAAC,sBAAsB,EAAE,QAAQ,CAAC,CAAC;AACnD,CAAC,CAAC;AAEF,MAAM,gBAAgB,GAAG,GAA6B,EAAE,CAAC,UAAU,CAAC,oBAAoB,CAAC;AAEzF,MAAM,4BAA4B,GAAG,CAAC,SAA8B,EAAQ,EAAE;IAC1E,YAAY,CAAC,+BAA+B,EAAE,SAAS,CAAC,CAAC;AAC7D,CAAC,CAAC;AAEF,MAAM,2BAA2B,GAAG,GAAY,EAAE,CAAC,UAAU,CAAC,6BAA6B,KAAK,IAAI,CAAC;AAErG,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,4BAA4B,EAAE,2BAA2B,EAAE,CAAC","sourcesContent":["declare global {\n var gtkxHeadlessTeardown: (() => void) | undefined;\n var gtkxHeadlessShutdownInstalled: boolean | undefined;\n}\n\nconst defineGlobal = (key: string, value: unknown): void => {\n Object.defineProperty(globalThis, key, { value, configurable: true, writable: true });\n};\n\nconst setHeadlessTeardown = (teardown: (() => void) | undefined): void => {\n defineGlobal(\"gtkxHeadlessTeardown\", teardown);\n};\n\nconst headlessTeardown = (): (() => void) | undefined => globalThis.gtkxHeadlessTeardown;\n\nconst setHeadlessShutdownInstalled = (installed: boolean | undefined): void => {\n defineGlobal(\"gtkxHeadlessShutdownInstalled\", installed);\n};\n\nconst isHeadlessShutdownInstalled = (): boolean => globalThis.gtkxHeadlessShutdownInstalled === true;\n\nexport { setHeadlessTeardown, headlessTeardown, setHeadlessShutdownInstalled, isHeadlessShutdownInstalled };\n"]}