@gtkx/vitest 1.4.0 → 1.5.0
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.
- package/README.md +1 -1
- package/dist/headless-display.d.ts.map +1 -1
- package/dist/headless-display.js +61 -68
- package/dist/headless-display.js.map +1 -1
- package/package.json +3 -3
- package/src/headless-display.ts +87 -96
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ GTK4 is mature, and GtkBuilder XML can lay out a static interface, but nothing r
|
|
|
98
98
|
- a React reconciler that exposes every GObject as a JSX element,
|
|
99
99
|
- a CLI for scaffolding, development, and production builds,
|
|
100
100
|
- a dev server with Fast Refresh that patches your running UI in place,
|
|
101
|
-
- CSS-in-JS styling, React Spring animations, React Navigation stack, tab, drawer, and split view navigators, and high-level list, grid, and dialog components,
|
|
101
|
+
- CSS-in-JS styling, React Hook Form-powered Adwaita controls, gettext-backed react-i18next localization, React Spring animations, React Navigation stack, tab, drawer, and split view navigators, and high-level list, grid, and dialog components,
|
|
102
102
|
- a Testing Library-style API for querying and driving your widgets in tests,
|
|
103
103
|
- and a Model Context Protocol (MCP) server that exposes your live app to AI agents.
|
|
104
104
|
|
|
@@ -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;
|
|
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;AAuGF,QAAA,MAAM,mBAAmB;;;;;;;;;;;CAWxB,CAAC;AAEF,QAAA,MAAM,sBAAsB,GAAI,UAAU,OAAO,CAAC,eAAe,CAAC,KAAG,eAGnE,CAAC;AAqCH,QAAA,MAAM,mBAAmB,GAAI,QAAQ,eAAe,KAAG,OAAO,CAAC,eAAe,CAe7E,CAAC;AAsOF,QAAA,MAAM,oBAAoB,GAAU,SAAS,eAAe,KAAG,OAAO,CAAC,MAAM,IAAI,CAmDhF,CAAC;AAEF,OAAO,EACH,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACpB,KAAK,YAAY,EACjB,KAAK,eAAe,GACvB,CAAC"}
|
package/dist/headless-display.js
CHANGED
|
@@ -135,17 +135,42 @@ const writeBusConfig = (busConfigPath, busSocketPath) => {
|
|
|
135
135
|
"</busconfig>",
|
|
136
136
|
].join("\n"));
|
|
137
137
|
};
|
|
138
|
-
const
|
|
139
|
-
|
|
138
|
+
const exitedMessage = (label, path, code, signal) => `${label} exited (code ${String(code)}, signal ${signal ?? "null"}) before ${path} appeared`;
|
|
139
|
+
const monitorChild = (child, label, path) => {
|
|
140
|
+
const stderr = child.stderr;
|
|
141
|
+
const subscribers = new Set();
|
|
140
142
|
let log = "";
|
|
141
|
-
|
|
142
|
-
|
|
143
|
+
let failure;
|
|
144
|
+
const onData = (chunk) => {
|
|
143
145
|
log += chunk;
|
|
146
|
+
};
|
|
147
|
+
stderr?.setEncoding("utf8");
|
|
148
|
+
stderr?.on("data", onData);
|
|
149
|
+
const record = (cause) => {
|
|
150
|
+
failure ??= cause;
|
|
151
|
+
for (const notify of subscribers) {
|
|
152
|
+
notify(cause);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
child.on("close", (code, signal) => {
|
|
156
|
+
record(`${exitedMessage(label, path, code, signal)}\n${log}`);
|
|
157
|
+
});
|
|
158
|
+
child.on("error", (cause) => {
|
|
159
|
+
record(`${label} failed to spawn before ${path} appeared: ${cause.message}\n${log}`);
|
|
144
160
|
});
|
|
145
161
|
return {
|
|
162
|
+
label,
|
|
163
|
+
path,
|
|
146
164
|
read: () => log,
|
|
165
|
+
failure: () => failure,
|
|
166
|
+
subscribe: (notify) => {
|
|
167
|
+
subscribers.add(notify);
|
|
168
|
+
return () => {
|
|
169
|
+
subscribers.delete(notify);
|
|
170
|
+
};
|
|
171
|
+
},
|
|
147
172
|
stop: () => {
|
|
148
|
-
stderr?.
|
|
173
|
+
stderr?.removeListener("data", onData);
|
|
149
174
|
stderr?.resume();
|
|
150
175
|
if (stderr instanceof Socket) {
|
|
151
176
|
stderr.unref();
|
|
@@ -153,15 +178,6 @@ const captureStderr = (child) => {
|
|
|
153
178
|
},
|
|
154
179
|
};
|
|
155
180
|
};
|
|
156
|
-
const trackChild = (child, handlers) => {
|
|
157
|
-
child.on("close", handlers.exit);
|
|
158
|
-
child.on("error", handlers.error);
|
|
159
|
-
return () => {
|
|
160
|
-
child.removeListener("close", handlers.exit);
|
|
161
|
-
child.removeListener("error", handlers.error);
|
|
162
|
-
};
|
|
163
|
-
};
|
|
164
|
-
const exitedMessage = (label, path, code, signal) => `${label} exited (code ${String(code)}, signal ${signal ?? "null"}) before ${path} appeared`;
|
|
165
181
|
const runCleanups = (cleanups) => {
|
|
166
182
|
for (const cleanup of cleanups) {
|
|
167
183
|
cleanup();
|
|
@@ -173,60 +189,48 @@ const pollForPath = (path, onFound) => setInterval(() => {
|
|
|
173
189
|
onFound();
|
|
174
190
|
}
|
|
175
191
|
}, 50);
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
return true;
|
|
192
|
+
const firstFailure = (monitors) => {
|
|
193
|
+
for (const monitor of monitors) {
|
|
194
|
+
const failure = monitor.failure();
|
|
195
|
+
if (failure !== undefined) {
|
|
196
|
+
return failure;
|
|
197
|
+
}
|
|
183
198
|
}
|
|
184
|
-
|
|
185
|
-
cleanups.push(() => {
|
|
186
|
-
signal.removeEventListener("abort", onAbort);
|
|
187
|
-
});
|
|
188
|
-
return false;
|
|
199
|
+
return undefined;
|
|
189
200
|
};
|
|
190
|
-
const watchForSocket = ({
|
|
191
|
-
const {
|
|
192
|
-
const
|
|
193
|
-
const cleanups = [
|
|
201
|
+
const watchForSocket = ({ options, resolve, reject }) => {
|
|
202
|
+
const { monitor, guards = [], timeout = 15_000 } = options;
|
|
203
|
+
const watched = [monitor, ...guards];
|
|
204
|
+
const cleanups = [monitor.stop];
|
|
194
205
|
const stop = () => {
|
|
195
206
|
runCleanups(cleanups);
|
|
196
207
|
};
|
|
197
208
|
const fail = (message) => {
|
|
198
209
|
stop();
|
|
210
|
+
for (const guard of guards) {
|
|
211
|
+
guard.stop();
|
|
212
|
+
}
|
|
199
213
|
reject(new Error(message));
|
|
200
214
|
};
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const onAbort = () => {
|
|
208
|
-
fail(`${label} startup aborted before ${path} appeared`);
|
|
209
|
-
};
|
|
210
|
-
const poll = pollForPath(path, () => {
|
|
215
|
+
const alreadyFailed = firstFailure(watched);
|
|
216
|
+
if (alreadyFailed !== undefined) {
|
|
217
|
+
fail(alreadyFailed);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const poll = pollForPath(monitor.path, () => {
|
|
211
221
|
stop();
|
|
212
222
|
resolve();
|
|
213
223
|
});
|
|
214
224
|
const timer = setTimeout(() => {
|
|
215
|
-
fail(`${label} did not become available within ${String(timeout)}ms${
|
|
225
|
+
fail(`${monitor.label} did not become available within ${String(timeout)}ms\n${monitor.read()}`);
|
|
216
226
|
}, timeout);
|
|
217
227
|
cleanups.push(() => {
|
|
218
228
|
clearInterval(poll);
|
|
219
229
|
clearTimeout(timer);
|
|
220
|
-
});
|
|
221
|
-
if (child) {
|
|
222
|
-
cleanups.push(trackChild(child, { exit: onExit, error: onError }));
|
|
223
|
-
}
|
|
224
|
-
if (hasAlreadyAborted(signal, onAbort, cleanups)) {
|
|
225
|
-
onAbort();
|
|
226
|
-
}
|
|
230
|
+
}, ...watched.map((entry) => entry.subscribe(fail)));
|
|
227
231
|
};
|
|
228
|
-
const waitForSocket = (
|
|
229
|
-
watchForSocket({
|
|
232
|
+
const waitForSocket = (options) => new Promise((resolve, reject) => {
|
|
233
|
+
watchForSocket({ options, resolve, reject });
|
|
230
234
|
});
|
|
231
235
|
const captureCompositorStderr = (child, logPath) => {
|
|
232
236
|
const captured = [];
|
|
@@ -251,22 +255,9 @@ const watchCompositorExit = (child, capturedStderr) => {
|
|
|
251
255
|
child.on("exit", report);
|
|
252
256
|
return () => child.removeListener("exit", report);
|
|
253
257
|
};
|
|
254
|
-
const waitForDisplaySockets = async (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
try {
|
|
258
|
-
await Promise.all([
|
|
259
|
-
waitForSocket(compositorSocketPath, {
|
|
260
|
-
label: "Compositor",
|
|
261
|
-
child: compositor.child,
|
|
262
|
-
signal: abort.signal,
|
|
263
|
-
}),
|
|
264
|
-
waitForSocket(busSocketPath, { label: "D-Bus session bus", child: busChild, signal: abort.signal }),
|
|
265
|
-
]);
|
|
266
|
-
}
|
|
267
|
-
finally {
|
|
268
|
-
abort.abort();
|
|
269
|
-
}
|
|
258
|
+
const waitForDisplaySockets = async ({ compositorMonitor, busMonitor }) => {
|
|
259
|
+
await waitForSocket({ monitor: busMonitor, guards: [compositorMonitor] });
|
|
260
|
+
await waitForSocket({ monitor: compositorMonitor });
|
|
270
261
|
};
|
|
271
262
|
const killSpawned = (children) => {
|
|
272
263
|
for (const child of children) {
|
|
@@ -302,13 +293,15 @@ const startHeadlessDisplay = async (options) => {
|
|
|
302
293
|
});
|
|
303
294
|
busChild.unref();
|
|
304
295
|
spawned.push(busChild);
|
|
296
|
+
const busMonitor = monitorChild(busChild, "D-Bus session bus", busSocketPath);
|
|
305
297
|
applyEnv(env, { DBUS_SESSION_BUS_ADDRESS: `unix:path=${busSocketPath}` });
|
|
306
298
|
const compositor = startCompositor(runtimeDir, options, env);
|
|
307
299
|
compositor.child.unref();
|
|
308
300
|
spawned.push(compositor.child);
|
|
309
|
-
applyEnv(env, { WAYLAND_DISPLAY: compositor.socket });
|
|
310
301
|
const compositorSocketPath = join(runtimeDir, compositor.socket);
|
|
311
|
-
|
|
302
|
+
const compositorMonitor = monitorChild(compositor.child, "Compositor", compositorSocketPath);
|
|
303
|
+
applyEnv(env, { WAYLAND_DISPLAY: compositor.socket });
|
|
304
|
+
await waitForDisplaySockets({ compositorMonitor, busMonitor });
|
|
312
305
|
const stopVirtualSeat = await attachVirtualSeat(compositor, compositorSocketPath);
|
|
313
306
|
const stopNotifications = await startNotificationService(`unix:path=${busSocketPath}`);
|
|
314
307
|
const capturedStderr = captureCompositorStderr(compositor.child, join(runtimeDir, "compositor.stderr.log"));
|
|
@@ -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 } from \"@gtkx/utils\";\nimport { type ChildProcess, 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.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 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 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 spawnWithParentDeathSignal(\"sway\", [\"-c\", configPath], { stdio: [\"ignore\", \"ignore\", \"pipe\"] });\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\"] },\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\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 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(\"close\", handlers.exit);\n child.on(\"error\", handlers.error);\n\n return () => {\n child.removeListener(\"close\", 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(\"dbus-daemon\", [`--config-file=${busConfigPath}`], {\n stdio: [\"ignore\", \"ignore\", \"pipe\"],\n });\n\n busChild.unref();\n spawned.push(busChild);\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 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 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 } from \"@gtkx/utils\";\nimport { type ChildProcess, 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.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 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\";\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 spawnWithParentDeathSignal(\"sway\", [\"-c\", configPath], { stdio: [\"ignore\", \"ignore\", \"pipe\"] });\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\"] },\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\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): 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 ({ 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, { 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 });\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);\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 STATIC_HEADLESS_ENV,\n resolveHeadlessOptions,\n readHeadlessOptions,\n startHeadlessDisplay,\n type CompositorId,\n type HeadlessOptions,\n};\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtkx/vitest",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Vitest plugin for GTK apps with headless Wayland isolation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gtkx",
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@homebridge/dbus-native": "^0.7.9",
|
|
55
|
-
"@gtkx/config": "1.
|
|
56
|
-
"@gtkx/utils": "1.
|
|
55
|
+
"@gtkx/config": "1.5.0",
|
|
56
|
+
"@gtkx/utils": "1.5.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"vitest": "^4.1.11"
|
package/src/headless-display.ts
CHANGED
|
@@ -37,32 +37,27 @@ type SpawnedCompositor = {
|
|
|
37
37
|
requiresVirtualSeat: boolean;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
type
|
|
40
|
+
type ChildMonitor = {
|
|
41
41
|
label: string;
|
|
42
|
-
|
|
43
|
-
child?: ChildProcess;
|
|
44
|
-
signal?: AbortSignal;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
type StderrCapture = {
|
|
42
|
+
path: string;
|
|
48
43
|
read: () => string;
|
|
44
|
+
failure: () => string | undefined;
|
|
45
|
+
subscribe: (notify: (failure: string) => void) => () => void;
|
|
49
46
|
stop: () => void;
|
|
50
47
|
};
|
|
51
48
|
|
|
52
|
-
type
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
type WaitForSocketOptions = {
|
|
50
|
+
monitor: ChildMonitor;
|
|
51
|
+
guards?: ChildMonitor[];
|
|
52
|
+
timeout?: number;
|
|
55
53
|
};
|
|
56
54
|
|
|
57
55
|
type DisplaySockets = {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
busChild: ChildProcess;
|
|
61
|
-
busSocketPath: string;
|
|
56
|
+
compositorMonitor: ChildMonitor;
|
|
57
|
+
busMonitor: ChildMonitor;
|
|
62
58
|
};
|
|
63
59
|
|
|
64
60
|
type SocketWatch = {
|
|
65
|
-
path: string;
|
|
66
61
|
options: WaitForSocketOptions;
|
|
67
62
|
resolve: () => void;
|
|
68
63
|
reject: (error: Error) => void;
|
|
@@ -234,19 +229,52 @@ const writeBusConfig = (busConfigPath: string, busSocketPath: string): void => {
|
|
|
234
229
|
);
|
|
235
230
|
};
|
|
236
231
|
|
|
237
|
-
const
|
|
238
|
-
|
|
232
|
+
const exitedMessage = (label: string, path: string, code: number | null, signal: NodeJS.Signals | null): string =>
|
|
233
|
+
`${label} exited (code ${String(code)}, signal ${signal ?? "null"}) before ${path} appeared`;
|
|
234
|
+
|
|
235
|
+
const monitorChild = (child: ChildProcess, label: string, path: string): ChildMonitor => {
|
|
236
|
+
const stderr = child.stderr;
|
|
237
|
+
const subscribers: Set<(failure: string) => void> = new Set();
|
|
239
238
|
let log = "";
|
|
240
|
-
|
|
239
|
+
let failure: string | undefined;
|
|
241
240
|
|
|
242
|
-
|
|
241
|
+
const onData = (chunk: string): void => {
|
|
243
242
|
log += chunk;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
stderr?.setEncoding("utf8");
|
|
246
|
+
stderr?.on("data", onData);
|
|
247
|
+
|
|
248
|
+
const record = (cause: string): void => {
|
|
249
|
+
failure ??= cause;
|
|
250
|
+
|
|
251
|
+
for (const notify of subscribers) {
|
|
252
|
+
notify(cause);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
child.on("close", (code: number | null, signal: NodeJS.Signals | null) => {
|
|
257
|
+
record(`${exitedMessage(label, path, code, signal)}\n${log}`);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
child.on("error", (cause: Error) => {
|
|
261
|
+
record(`${label} failed to spawn before ${path} appeared: ${cause.message}\n${log}`);
|
|
244
262
|
});
|
|
245
263
|
|
|
246
264
|
return {
|
|
265
|
+
label,
|
|
266
|
+
path,
|
|
247
267
|
read: () => log,
|
|
268
|
+
failure: () => failure,
|
|
269
|
+
subscribe: (notify) => {
|
|
270
|
+
subscribers.add(notify);
|
|
271
|
+
|
|
272
|
+
return (): void => {
|
|
273
|
+
subscribers.delete(notify);
|
|
274
|
+
};
|
|
275
|
+
},
|
|
248
276
|
stop: () => {
|
|
249
|
-
stderr?.
|
|
277
|
+
stderr?.removeListener("data", onData);
|
|
250
278
|
stderr?.resume();
|
|
251
279
|
|
|
252
280
|
if (stderr instanceof Socket) {
|
|
@@ -256,19 +284,6 @@ const captureStderr = (child: ChildProcess | undefined): StderrCapture => {
|
|
|
256
284
|
};
|
|
257
285
|
};
|
|
258
286
|
|
|
259
|
-
const trackChild = (child: ChildProcess, handlers: ChildHandlers): (() => void) => {
|
|
260
|
-
child.on("close", handlers.exit);
|
|
261
|
-
child.on("error", handlers.error);
|
|
262
|
-
|
|
263
|
-
return () => {
|
|
264
|
-
child.removeListener("close", handlers.exit);
|
|
265
|
-
child.removeListener("error", handlers.error);
|
|
266
|
-
};
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
const exitedMessage = (label: string, path: string, code: number | null, signal: NodeJS.Signals | null): string =>
|
|
270
|
-
`${label} exited (code ${String(code)}, signal ${signal ?? "null"}) before ${path} appeared`;
|
|
271
|
-
|
|
272
287
|
const runCleanups = (cleanups: (() => void)[]): void => {
|
|
273
288
|
for (const cleanup of cleanups) {
|
|
274
289
|
cleanup();
|
|
@@ -284,31 +299,22 @@ const pollForPath = (path: string, onFound: () => void): NodeJS.Timeout =>
|
|
|
284
299
|
}
|
|
285
300
|
}, 50);
|
|
286
301
|
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const hasAlreadyAborted = (signal: AbortSignal | undefined, onAbort: () => void, cleanups: (() => void)[]): boolean => {
|
|
291
|
-
if (signal === undefined) {
|
|
292
|
-
return false;
|
|
293
|
-
}
|
|
302
|
+
const firstFailure = (monitors: ChildMonitor[]): string | undefined => {
|
|
303
|
+
for (const monitor of monitors) {
|
|
304
|
+
const failure = monitor.failure();
|
|
294
305
|
|
|
295
|
-
|
|
296
|
-
|
|
306
|
+
if (failure !== undefined) {
|
|
307
|
+
return failure;
|
|
308
|
+
}
|
|
297
309
|
}
|
|
298
310
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
cleanups.push(() => {
|
|
302
|
-
signal.removeEventListener("abort", onAbort);
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
return false;
|
|
311
|
+
return undefined;
|
|
306
312
|
};
|
|
307
313
|
|
|
308
|
-
const watchForSocket = ({
|
|
309
|
-
const {
|
|
310
|
-
const
|
|
311
|
-
const cleanups: (() => void)[] = [
|
|
314
|
+
const watchForSocket = ({ options, resolve, reject }: SocketWatch): void => {
|
|
315
|
+
const { monitor, guards = [], timeout = 15_000 } = options;
|
|
316
|
+
const watched = [monitor, ...guards];
|
|
317
|
+
const cleanups: (() => void)[] = [monitor.stop];
|
|
312
318
|
|
|
313
319
|
const stop = (): void => {
|
|
314
320
|
runCleanups(cleanups);
|
|
@@ -316,47 +322,43 @@ const watchForSocket = ({ path, options, resolve, reject }: SocketWatch): void =
|
|
|
316
322
|
|
|
317
323
|
const fail = (message: string): void => {
|
|
318
324
|
stop();
|
|
325
|
+
|
|
326
|
+
for (const guard of guards) {
|
|
327
|
+
guard.stop();
|
|
328
|
+
}
|
|
329
|
+
|
|
319
330
|
reject(new Error(message));
|
|
320
331
|
};
|
|
321
332
|
|
|
322
|
-
const
|
|
323
|
-
fail(`${exitedMessage(label, path, code, terminationSignal)}\n${stderr.read()}`);
|
|
324
|
-
};
|
|
333
|
+
const alreadyFailed = firstFailure(watched);
|
|
325
334
|
|
|
326
|
-
|
|
327
|
-
fail(
|
|
328
|
-
};
|
|
335
|
+
if (alreadyFailed !== undefined) {
|
|
336
|
+
fail(alreadyFailed);
|
|
329
337
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
};
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
333
340
|
|
|
334
|
-
const poll = pollForPath(path, () => {
|
|
341
|
+
const poll = pollForPath(monitor.path, () => {
|
|
335
342
|
stop();
|
|
336
343
|
resolve();
|
|
337
344
|
});
|
|
338
345
|
|
|
339
346
|
const timer = setTimeout(() => {
|
|
340
|
-
fail(`${label} did not become available within ${String(timeout)}ms${
|
|
347
|
+
fail(`${monitor.label} did not become available within ${String(timeout)}ms\n${monitor.read()}`);
|
|
341
348
|
}, timeout);
|
|
342
349
|
|
|
343
|
-
cleanups.push(
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (hasAlreadyAborted(signal, onAbort, cleanups)) {
|
|
353
|
-
onAbort();
|
|
354
|
-
}
|
|
350
|
+
cleanups.push(
|
|
351
|
+
() => {
|
|
352
|
+
clearInterval(poll);
|
|
353
|
+
clearTimeout(timer);
|
|
354
|
+
},
|
|
355
|
+
...watched.map((entry) => entry.subscribe(fail)),
|
|
356
|
+
);
|
|
355
357
|
};
|
|
356
358
|
|
|
357
|
-
const waitForSocket = (
|
|
359
|
+
const waitForSocket = (options: WaitForSocketOptions): Promise<void> =>
|
|
358
360
|
new Promise((resolve, reject) => {
|
|
359
|
-
watchForSocket({
|
|
361
|
+
watchForSocket({ options, resolve, reject });
|
|
360
362
|
});
|
|
361
363
|
|
|
362
364
|
const captureCompositorStderr = (child: ChildProcess, logPath: string): string[] => {
|
|
@@ -395,22 +397,9 @@ const watchCompositorExit = (child: ChildProcess, capturedStderr: string[]): (()
|
|
|
395
397
|
return () => child.removeListener("exit", report);
|
|
396
398
|
};
|
|
397
399
|
|
|
398
|
-
const waitForDisplaySockets = async (
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
try {
|
|
403
|
-
await Promise.all([
|
|
404
|
-
waitForSocket(compositorSocketPath, {
|
|
405
|
-
label: "Compositor",
|
|
406
|
-
child: compositor.child,
|
|
407
|
-
signal: abort.signal,
|
|
408
|
-
}),
|
|
409
|
-
waitForSocket(busSocketPath, { label: "D-Bus session bus", child: busChild, signal: abort.signal }),
|
|
410
|
-
]);
|
|
411
|
-
} finally {
|
|
412
|
-
abort.abort();
|
|
413
|
-
}
|
|
400
|
+
const waitForDisplaySockets = async ({ compositorMonitor, busMonitor }: DisplaySockets): Promise<void> => {
|
|
401
|
+
await waitForSocket({ monitor: busMonitor, guards: [compositorMonitor] });
|
|
402
|
+
await waitForSocket({ monitor: compositorMonitor });
|
|
414
403
|
};
|
|
415
404
|
|
|
416
405
|
const killSpawned = (children: ChildProcess[]): void => {
|
|
@@ -455,13 +444,15 @@ const startHeadlessDisplay = async (options: HeadlessOptions): Promise<() => voi
|
|
|
455
444
|
|
|
456
445
|
busChild.unref();
|
|
457
446
|
spawned.push(busChild);
|
|
447
|
+
const busMonitor = monitorChild(busChild, "D-Bus session bus", busSocketPath);
|
|
458
448
|
applyEnv(env, { DBUS_SESSION_BUS_ADDRESS: `unix:path=${busSocketPath}` });
|
|
459
449
|
const compositor = startCompositor(runtimeDir, options, env);
|
|
460
450
|
compositor.child.unref();
|
|
461
451
|
spawned.push(compositor.child);
|
|
462
|
-
applyEnv(env, { WAYLAND_DISPLAY: compositor.socket });
|
|
463
452
|
const compositorSocketPath = join(runtimeDir, compositor.socket);
|
|
464
|
-
|
|
453
|
+
const compositorMonitor = monitorChild(compositor.child, "Compositor", compositorSocketPath);
|
|
454
|
+
applyEnv(env, { WAYLAND_DISPLAY: compositor.socket });
|
|
455
|
+
await waitForDisplaySockets({ compositorMonitor, busMonitor });
|
|
465
456
|
const stopVirtualSeat = await attachVirtualSeat(compositor, compositorSocketPath);
|
|
466
457
|
const stopNotifications = await startNotificationService(`unix:path=${busSocketPath}`);
|
|
467
458
|
const capturedStderr = captureCompositorStderr(compositor.child, join(runtimeDir, "compositor.stderr.log"));
|