@gtkx/vitest 1.0.0-rc.4 → 1.1.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 +5 -5
- package/dist/headless-display.d.ts +1 -2
- package/dist/headless-display.d.ts.map +1 -1
- package/dist/headless-display.js +12 -14
- package/dist/headless-display.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -8
- package/dist/index.js.map +1 -1
- package/dist/worker-preload.js +2 -2
- package/dist/worker-preload.js.map +1 -1
- package/package.json +13 -3
- package/src/headless-display.ts +11 -25
- package/src/index.ts +4 -10
- package/src/worker-preload.ts +2 -2
- package/dist/headless-globals.d.ts +0 -10
- package/dist/headless-globals.d.ts.map +0 -1
- package/dist/headless-globals.js +0 -13
- package/dist/headless-globals.js.map +0 -1
- package/dist/install-headless-shutdown.d.ts +0 -3
- package/dist/install-headless-shutdown.d.ts.map +0 -1
- package/dist/install-headless-shutdown.js +0 -12
- package/dist/install-headless-shutdown.js.map +0 -1
- package/dist/resolve-executable.d.ts +0 -3
- package/dist/resolve-executable.d.ts.map +0 -1
- package/dist/resolve-executable.js +0 -26
- package/dist/resolve-executable.js.map +0 -1
- package/dist/worker-setup.d.ts +0 -2
- package/dist/worker-setup.d.ts.map +0 -1
- package/dist/worker-setup.js +0 -3
- package/dist/worker-setup.js.map +0 -1
- package/src/headless-globals.ts +0 -22
- package/src/install-headless-shutdown.ts +0 -15
- package/src/resolve-executable.ts +0 -32
- package/src/worker-setup.ts +0 -3
package/README.md
CHANGED
|
@@ -108,7 +108,7 @@ React Native and similar frameworks hide the native toolkit so one API can run e
|
|
|
108
108
|
|
|
109
109
|
### Why Node.js, and why generated bindings
|
|
110
110
|
|
|
111
|
-
GTKX runs on Node.js, which puts native modules, the npm ecosystem, and the tooling built for Node.js APIs within reach. GJS is GNOME's own runtime, built on SpiderMonkey rather than V8; node-gtk runs on Node.js but is lightly maintained, on the older nan/V8 ABI rather than N-API, and still centered on GTK3. The [Why GTKX guide](https://gtkx.dev/guide/why-gtkx) covers
|
|
111
|
+
GTKX runs on Node.js, which puts native modules, the npm ecosystem, and the tooling built for Node.js APIs within reach. GJS is GNOME's own runtime, built on SpiderMonkey rather than V8; node-gtk runs on Node.js but is lightly maintained, on the older nan/V8 ABI rather than N-API, and still centered on GTK3. The [Why GTKX guide](https://gtkx.dev/guide/why-gtkx) covers what running on Node.js means day to day.
|
|
112
112
|
|
|
113
113
|
GTKX generates the TypeScript types and the native FFI calls from the same GObject-Introspection data, so the types cannot drift from the calls they back. Codegen covers the whole GTK4 and Adwaita surface.
|
|
114
114
|
|
|
@@ -121,10 +121,10 @@ GTKX is Linux-only and needs Node.js 24 or later. See [Requirements](#requiremen
|
|
|
121
121
|
Scaffold a new app with the `create-gtkx` initializer:
|
|
122
122
|
|
|
123
123
|
```sh
|
|
124
|
-
npm create gtkx
|
|
124
|
+
npm create gtkx
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
-
The same command works with other package managers: `pnpm create gtkx
|
|
127
|
+
The same command works with other package managers: `pnpm create gtkx` or `yarn create gtkx`.
|
|
128
128
|
|
|
129
129
|
Then run your new app:
|
|
130
130
|
|
|
@@ -145,7 +145,7 @@ The documentation at **[gtkx.dev](https://gtkx.dev)** includes a step-by-step tu
|
|
|
145
145
|
|
|
146
146
|
GTKX is Linux-only. You need:
|
|
147
147
|
|
|
148
|
-
- Linux with the GTK4 (4.20 or later), Adwaita (1.8 or later)
|
|
148
|
+
- Linux with the GTK4 (4.20 or later) and GLib development libraries, plus Adwaita (1.8 or later) once your project binds `Adw-1`
|
|
149
149
|
- Node.js 24 or later
|
|
150
150
|
|
|
151
151
|
The `@gtkx/native` addon ships prebuilt for x64 and arm64 glibc Linux; other targets need to build it from the GTKX repository, which requires a Rust toolchain.
|
|
@@ -161,7 +161,7 @@ Explore the [example apps](https://github.com/gtkx-org/gtkx/tree/main/examples):
|
|
|
161
161
|
|
|
162
162
|
## Status
|
|
163
163
|
|
|
164
|
-
GTKX
|
|
164
|
+
GTKX is stable and ready for production use.
|
|
165
165
|
|
|
166
166
|
## Contributing
|
|
167
167
|
|
|
@@ -11,7 +11,6 @@ type HeadlessOptions = {
|
|
|
11
11
|
/** Wayland compositor used to back the headless display. */
|
|
12
12
|
compositor: CompositorId;
|
|
13
13
|
};
|
|
14
|
-
declare const DEFAULT_HEADLESS_SIZE = "1024x768";
|
|
15
14
|
declare const STATIC_HEADLESS_ENV: {
|
|
16
15
|
GDK_BACKEND: string;
|
|
17
16
|
GDK_DISABLE: string;
|
|
@@ -27,5 +26,5 @@ declare const STATIC_HEADLESS_ENV: {
|
|
|
27
26
|
declare const resolveHeadlessOptions: (provided: Partial<HeadlessOptions>) => HeadlessOptions;
|
|
28
27
|
declare const readHeadlessOptions: (params: URLSearchParams) => Partial<HeadlessOptions>;
|
|
29
28
|
declare const startHeadlessDisplay: (options: HeadlessOptions) => Promise<() => void>;
|
|
30
|
-
export {
|
|
29
|
+
export { STATIC_HEADLESS_ENV, resolveHeadlessOptions, readHeadlessOptions, startHeadlessDisplay, type CompositorId, type HeadlessOptions, };
|
|
31
30
|
//# sourceMappingURL=headless-display.d.ts.map
|
|
@@ -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;AA4GF,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;AA4OF,QAAA,MAAM,oBAAoB,GAAU,SAAS,eAAe,KAAG,OAAO,CAAC,MAAM,IAAI,CAiDhF,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
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveExecutable, spawnWithParentDeathSignal } from "@gtkx/utils";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
2
3
|
import { chmodSync, createWriteStream, existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { Socket } from "node:net";
|
|
4
5
|
import { tmpdir } from "node:os";
|
|
5
6
|
import { join } from "node:path";
|
|
6
7
|
import { startNotificationService } from "./notification-service.js";
|
|
7
|
-
import { resolveExecutable } from "./resolve-executable.js";
|
|
8
8
|
import { startVirtualSeat } from "./virtual-seat.js";
|
|
9
9
|
const DEFAULT_HEADLESS_SIZE = "1024x768";
|
|
10
10
|
const DEFAULT_HEADLESS_COMPOSITOR = "sway";
|
|
11
11
|
const BUS_CONFIG_DOCTYPE = '<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" ' +
|
|
12
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
13
|
const hasWestonFakeSeat = createWestonFakeSeatProbe();
|
|
15
14
|
const compositorRegistry = {
|
|
16
15
|
sway: {
|
|
@@ -35,7 +34,7 @@ const compositorRegistry = {
|
|
|
35
34
|
'for_window [title=".*"] floating enable, border none',
|
|
36
35
|
"",
|
|
37
36
|
].join("\n"));
|
|
38
|
-
return spawnWithParentDeathSignal("sway", ["-c", configPath], ["ignore", "ignore", "pipe"]);
|
|
37
|
+
return spawnWithParentDeathSignal("sway", ["-c", configPath], { stdio: ["ignore", "ignore", "pipe"] });
|
|
39
38
|
},
|
|
40
39
|
},
|
|
41
40
|
weston: {
|
|
@@ -49,7 +48,7 @@ const compositorRegistry = {
|
|
|
49
48
|
`--width=${width}`,
|
|
50
49
|
`--height=${height}`,
|
|
51
50
|
"--socket=wayland-0",
|
|
52
|
-
], ["ignore", "ignore", "pipe"]),
|
|
51
|
+
], { stdio: ["ignore", "ignore", "pipe"] }),
|
|
53
52
|
},
|
|
54
53
|
};
|
|
55
54
|
const STATIC_HEADLESS_ENV = {
|
|
@@ -86,11 +85,6 @@ const restoreEnv = (snapshot) => {
|
|
|
86
85
|
}
|
|
87
86
|
}
|
|
88
87
|
};
|
|
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
88
|
function createWestonFakeSeatProbe() {
|
|
95
89
|
let isSupported;
|
|
96
90
|
return () => {
|
|
@@ -160,10 +154,10 @@ const captureStderr = (child) => {
|
|
|
160
154
|
};
|
|
161
155
|
};
|
|
162
156
|
const trackChild = (child, handlers) => {
|
|
163
|
-
child.on("
|
|
157
|
+
child.on("close", handlers.exit);
|
|
164
158
|
child.on("error", handlers.error);
|
|
165
159
|
return () => {
|
|
166
|
-
child.removeListener("
|
|
160
|
+
child.removeListener("close", handlers.exit);
|
|
167
161
|
child.removeListener("error", handlers.error);
|
|
168
162
|
};
|
|
169
163
|
};
|
|
@@ -303,10 +297,14 @@ const startHeadlessDisplay = async (options) => {
|
|
|
303
297
|
const busConfigPath = join(runtimeDir, "session.conf");
|
|
304
298
|
const busSocketPath = join(runtimeDir, "bus");
|
|
305
299
|
writeBusConfig(busConfigPath, busSocketPath);
|
|
306
|
-
const busChild = spawnWithParentDeathSignal("dbus-daemon", [`--config-file=${busConfigPath}`],
|
|
300
|
+
const busChild = spawnWithParentDeathSignal("dbus-daemon", [`--config-file=${busConfigPath}`], {
|
|
301
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
302
|
+
});
|
|
303
|
+
busChild.unref();
|
|
307
304
|
spawned.push(busChild);
|
|
308
305
|
applyEnv(env, { DBUS_SESSION_BUS_ADDRESS: `unix:path=${busSocketPath}` });
|
|
309
306
|
const compositor = startCompositor(runtimeDir, options, env);
|
|
307
|
+
compositor.child.unref();
|
|
310
308
|
spawned.push(compositor.child);
|
|
311
309
|
applyEnv(env, { WAYLAND_DISPLAY: compositor.socket });
|
|
312
310
|
const compositorSocketPath = join(runtimeDir, compositor.socket);
|
|
@@ -331,5 +329,5 @@ const startHeadlessDisplay = async (options) => {
|
|
|
331
329
|
throw error;
|
|
332
330
|
}
|
|
333
331
|
};
|
|
334
|
-
export {
|
|
332
|
+
export { STATIC_HEADLESS_ENV, resolveHeadlessOptions, readHeadlessOptions, startHeadlessDisplay, };
|
|
335
333
|
//# 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, 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.ts\";\nimport { resolveExecutable } from \"./resolve-executable.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 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 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], [\"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 [\"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 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(\"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"]}
|
|
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"]}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAK5C,OAAO,EAAE,KAAK,eAAe,EAAuB,MAAM,uBAAuB,CAAC;AAElF;;;GAGG;AACH,KAAK,aAAa,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAK5C,OAAO,EAAE,KAAK,eAAe,EAAuB,MAAM,uBAAuB,CAAC;AAElF;;;GAGG;AACH,KAAK,aAAa,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;AAsB9C;;;;;;;GAOG;AACH,QAAA,MAAM,IAAI,GAAI,UAAS,aAAkB,KAAG,MAoBtC,CAAC;AAEP,eAAe,IAAI,CAAC;AACpB,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAChF,OAAO,EAAE,KAAK,aAAa,EAAE,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { join } from "node:path";
|
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { STATIC_HEADLESS_ENV } from "./headless-display.js";
|
|
6
6
|
const GTKX_INLINE_DEPS = [/@gtkx\/(?!native)/, /[/\\]\.gtkx[/\\]/];
|
|
7
|
+
const DEFAULT_TIMEOUT = 30_000;
|
|
7
8
|
const workerPreloadUrl = () => {
|
|
8
9
|
const sibling = join(import.meta.dirname, "worker-preload.js");
|
|
9
10
|
const path = existsSync(sibling) ? sibling : join(import.meta.dirname, "..", "dist", "worker-preload.js");
|
|
@@ -16,10 +17,6 @@ const headlessPreloadSpecifier = (options) => {
|
|
|
16
17
|
}
|
|
17
18
|
return url.href;
|
|
18
19
|
};
|
|
19
|
-
const workerSetupPath = () => {
|
|
20
|
-
const sibling = join(import.meta.dirname, "worker-setup.js");
|
|
21
|
-
return existsSync(sibling) ? sibling : join(import.meta.dirname, "..", "dist", "worker-setup.js");
|
|
22
|
-
};
|
|
23
20
|
/**
|
|
24
21
|
* Vitest plugin that runs each test worker against its own isolated headless
|
|
25
22
|
* Wayland display. It configures the forks pool, injects the worker preload and
|
|
@@ -30,14 +27,13 @@ const workerSetupPath = () => {
|
|
|
30
27
|
*/
|
|
31
28
|
const gtkx = (options = {}) => createConfigPlugin({
|
|
32
29
|
name: "gtkx:vitest",
|
|
33
|
-
config() {
|
|
30
|
+
config(config) {
|
|
34
31
|
return {
|
|
35
32
|
test: {
|
|
36
33
|
globals: true,
|
|
37
34
|
execArgv: ["--import", headlessPreloadSpecifier(options)],
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
hookTimeout: 30_000,
|
|
35
|
+
testTimeout: config.test?.testTimeout ?? DEFAULT_TIMEOUT,
|
|
36
|
+
hookTimeout: config.test?.hookTimeout ?? DEFAULT_TIMEOUT,
|
|
41
37
|
pool: "forks",
|
|
42
38
|
env: STATIC_HEADLESS_ENV,
|
|
43
39
|
server: {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,kBAAkB,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAwB,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAQlF,MAAM,gBAAgB,GAAa,CAAC,mBAAmB,EAAE,kBAAkB,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,kBAAkB,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAwB,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAQlF,MAAM,gBAAgB,GAAa,CAAC,mBAAmB,EAAE,kBAAkB,CAAC,CAAC;AAC7E,MAAM,eAAe,GAAG,MAAM,CAAC;AAE/B,MAAM,gBAAgB,GAAG,GAAQ,EAAE;IAC/B,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC;IAC/D,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,mBAAmB,CAAC,CAAC;IAE1G,OAAO,aAAa,CAAC,IAAI,CAAC,CAAC;AAC/B,CAAC,CAAC;AAEF,MAAM,wBAAwB,GAAG,CAAC,OAAsB,EAAU,EAAE;IAChE,MAAM,GAAG,GAAG,gBAAgB,EAAE,CAAC;IAE/B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACjD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACrC,CAAC;IAED,OAAO,GAAG,CAAC,IAAI,CAAC;AACpB,CAAC,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,IAAI,GAAG,CAAC,UAAyB,EAAE,EAAU,EAAE,CACjD,kBAAkB,CAAC;IACf,IAAI,EAAE,aAAa;IACnB,MAAM,CAAC,MAAM;QACT,OAAO;YACH,IAAI,EAAE;gBACF,OAAO,EAAE,IAAI;gBACb,QAAQ,EAAE,CAAC,UAAU,EAAE,wBAAwB,CAAC,OAAO,CAAC,CAAC;gBACzD,WAAW,EAAE,MAAM,CAAC,IAAI,EAAE,WAAW,IAAI,eAAe;gBACxD,WAAW,EAAE,MAAM,CAAC,IAAI,EAAE,WAAW,IAAI,eAAe;gBACxD,IAAI,EAAE,OAAO;gBACb,GAAG,EAAE,mBAAmB;gBACxB,MAAM,EAAE;oBACJ,IAAI,EAAE;wBACF,MAAM,EAAE,gBAAgB;qBAC3B;iBACJ;aACJ;SACJ,CAAC;IACN,CAAC;CACJ,CAAC,CAAC;AAEP,eAAe,IAAI,CAAC;AACpB,OAAO,EAA2C,MAAM,uBAAuB,CAAC;AAChF,OAAO,EAAsB,CAAC","sourcesContent":["import type { Plugin } from \"vitest/config\";\nimport createConfigPlugin from \"@gtkx/config/vite-plugin\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport { type HeadlessOptions, STATIC_HEADLESS_ENV } from \"./headless-display.ts\";\n\n/**\n * Options accepted by the GTKX Vitest plugin. Every headless display\n * setting is optional and falls back to a built-in default when omitted.\n */\ntype PluginOptions = Partial<HeadlessOptions>;\n\nconst GTKX_INLINE_DEPS: RegExp[] = [/@gtkx\\/(?!native)/, /[/\\\\]\\.gtkx[/\\\\]/];\nconst DEFAULT_TIMEOUT = 30_000;\n\nconst workerPreloadUrl = (): URL => {\n const sibling = join(import.meta.dirname, \"worker-preload.js\");\n const path = existsSync(sibling) ? sibling : join(import.meta.dirname, \"..\", \"dist\", \"worker-preload.js\");\n\n return pathToFileURL(path);\n};\n\nconst headlessPreloadSpecifier = (options: PluginOptions): string => {\n const url = workerPreloadUrl();\n\n for (const [key, value] of Object.entries(options)) {\n url.searchParams.set(key, value);\n }\n\n return url.href;\n};\n\n/**\n * Vitest plugin that runs each test worker against its own isolated headless\n * Wayland display. It configures the forks pool, injects the worker preload and\n * setup files, and sets the environment needed for headless GTK4 rendering.\n *\n * @param options Headless display settings (size, compositor) forwarded to each worker.\n * @returns A Vitest config plugin.\n */\nconst gtkx = (options: PluginOptions = {}): Plugin =>\n createConfigPlugin({\n name: \"gtkx:vitest\",\n config(config) {\n return {\n test: {\n globals: true,\n execArgv: [\"--import\", headlessPreloadSpecifier(options)],\n testTimeout: config.test?.testTimeout ?? DEFAULT_TIMEOUT,\n hookTimeout: config.test?.hookTimeout ?? DEFAULT_TIMEOUT,\n pool: \"forks\",\n env: STATIC_HEADLESS_ENV,\n server: {\n deps: {\n inline: GTKX_INLINE_DEPS,\n },\n },\n },\n };\n },\n });\n\nexport default gtkx;\nexport { type CompositorId, type HeadlessOptions } from \"./headless-display.ts\";\nexport { type PluginOptions };\n"]}
|
package/dist/worker-preload.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { installGracefulShutdown } from "@gtkx/utils";
|
|
1
2
|
import { readHeadlessOptions, resolveHeadlessOptions, startHeadlessDisplay } from "./headless-display.js";
|
|
2
|
-
import { setHeadlessTeardown } from "./headless-globals.js";
|
|
3
3
|
const options = readHeadlessOptions(new URL(import.meta.url).searchParams);
|
|
4
4
|
const teardown = await startHeadlessDisplay(resolveHeadlessOptions(options));
|
|
5
5
|
process.on("exit", teardown);
|
|
6
|
-
|
|
6
|
+
installGracefulShutdown({ onSignal: teardown });
|
|
7
7
|
//# sourceMappingURL=worker-preload.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker-preload.js","sourceRoot":"","sources":["../src/worker-preload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"worker-preload.js","sourceRoot":"","sources":["../src/worker-preload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAE1G,MAAM,OAAO,GAAG,mBAAmB,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,YAAY,CAAC,CAAC;AAC3E,MAAM,QAAQ,GAAG,MAAM,oBAAoB,CAAC,sBAAsB,CAAC,OAAO,CAAC,CAAC,CAAC;AAE7E,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AAC7B,uBAAuB,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC","sourcesContent":["import { installGracefulShutdown } from \"@gtkx/utils\";\nimport { readHeadlessOptions, resolveHeadlessOptions, startHeadlessDisplay } from \"./headless-display.ts\";\n\nconst options = readHeadlessOptions(new URL(import.meta.url).searchParams);\nconst teardown = await startHeadlessDisplay(resolveHeadlessOptions(options));\n\nprocess.on(\"exit\", teardown);\ninstallGracefulShutdown({ onSignal: teardown });\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtkx/vitest",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Vitest plugin for GTKX applications with headless Wayland display isolation",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gtkx",
|
|
@@ -37,13 +37,23 @@
|
|
|
37
37
|
"dist",
|
|
38
38
|
"src"
|
|
39
39
|
],
|
|
40
|
+
"nx": {
|
|
41
|
+
"targets": {
|
|
42
|
+
"lint": {
|
|
43
|
+
"options": {
|
|
44
|
+
"cwd": ".",
|
|
45
|
+
"command": "eslint {projectRoot}"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
40
50
|
"engines": {
|
|
41
51
|
"node": ">=24"
|
|
42
52
|
},
|
|
43
53
|
"dependencies": {
|
|
44
54
|
"@homebridge/dbus-native": "^0.7.8",
|
|
45
|
-
"@gtkx/
|
|
46
|
-
"@gtkx/
|
|
55
|
+
"@gtkx/config": "1.1.0",
|
|
56
|
+
"@gtkx/utils": "1.1.0"
|
|
47
57
|
},
|
|
48
58
|
"devDependencies": {
|
|
49
59
|
"vitest": "^4.1.10"
|
package/src/headless-display.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveExecutable, spawnWithParentDeathSignal } from "@gtkx/utils";
|
|
2
|
+
import { type ChildProcess, spawnSync } from "node:child_process";
|
|
2
3
|
import { chmodSync, createWriteStream, existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { Socket } from "node:net";
|
|
4
5
|
import { tmpdir } from "node:os";
|
|
5
6
|
import { join } from "node:path";
|
|
6
7
|
import { startNotificationService } from "./notification-service.ts";
|
|
7
|
-
import { resolveExecutable } from "./resolve-executable.ts";
|
|
8
8
|
import { startVirtualSeat } from "./virtual-seat.ts";
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -75,7 +75,6 @@ const BUS_CONFIG_DOCTYPE =
|
|
|
75
75
|
'<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" ' +
|
|
76
76
|
'"https://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">';
|
|
77
77
|
|
|
78
|
-
const PARENT_DEATH_SCRIPT = 'trap \'kill -9 "$child" 2>/dev/null\' TERM; "$@" & child=$!; wait "$child"';
|
|
79
78
|
const hasWestonFakeSeat = createWestonFakeSeatProbe();
|
|
80
79
|
|
|
81
80
|
const compositorRegistry: Record<CompositorId, CompositorDescriptor> = {
|
|
@@ -106,7 +105,7 @@ const compositorRegistry: Record<CompositorId, CompositorDescriptor> = {
|
|
|
106
105
|
].join("\n"),
|
|
107
106
|
);
|
|
108
107
|
|
|
109
|
-
return spawnWithParentDeathSignal("sway", ["-c", configPath], ["ignore", "ignore", "pipe"]);
|
|
108
|
+
return spawnWithParentDeathSignal("sway", ["-c", configPath], { stdio: ["ignore", "ignore", "pipe"] });
|
|
110
109
|
},
|
|
111
110
|
},
|
|
112
111
|
weston: {
|
|
@@ -124,7 +123,7 @@ const compositorRegistry: Record<CompositorId, CompositorDescriptor> = {
|
|
|
124
123
|
`--height=${height}`,
|
|
125
124
|
"--socket=wayland-0",
|
|
126
125
|
],
|
|
127
|
-
["ignore", "ignore", "pipe"],
|
|
126
|
+
{ stdio: ["ignore", "ignore", "pipe"] },
|
|
128
127
|
),
|
|
129
128
|
},
|
|
130
129
|
};
|
|
@@ -167,18 +166,6 @@ const restoreEnv = (snapshot: EnvSnapshot): void => {
|
|
|
167
166
|
}
|
|
168
167
|
};
|
|
169
168
|
|
|
170
|
-
const spawnWithParentDeathSignal = (command: string, args: string[], stdio: StdioOptions): ChildProcess => {
|
|
171
|
-
const child = spawn(
|
|
172
|
-
resolveExecutable("setpriv"),
|
|
173
|
-
["--pdeathsig", "SIGTERM", "sh", "-c", PARENT_DEATH_SCRIPT, "sh", command, ...args],
|
|
174
|
-
{ stdio },
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
child.unref();
|
|
178
|
-
|
|
179
|
-
return child;
|
|
180
|
-
};
|
|
181
|
-
|
|
182
169
|
function createWestonFakeSeatProbe(): () => boolean {
|
|
183
170
|
let isSupported: boolean | undefined;
|
|
184
171
|
|
|
@@ -270,11 +257,11 @@ const captureStderr = (child: ChildProcess | undefined): StderrCapture => {
|
|
|
270
257
|
};
|
|
271
258
|
|
|
272
259
|
const trackChild = (child: ChildProcess, handlers: ChildHandlers): (() => void) => {
|
|
273
|
-
child.on("
|
|
260
|
+
child.on("close", handlers.exit);
|
|
274
261
|
child.on("error", handlers.error);
|
|
275
262
|
|
|
276
263
|
return () => {
|
|
277
|
-
child.removeListener("
|
|
264
|
+
child.removeListener("close", handlers.exit);
|
|
278
265
|
child.removeListener("error", handlers.error);
|
|
279
266
|
};
|
|
280
267
|
};
|
|
@@ -462,15 +449,15 @@ const startHeadlessDisplay = async (options: HeadlessOptions): Promise<() => voi
|
|
|
462
449
|
const busSocketPath = join(runtimeDir, "bus");
|
|
463
450
|
writeBusConfig(busConfigPath, busSocketPath);
|
|
464
451
|
|
|
465
|
-
const busChild = spawnWithParentDeathSignal(
|
|
466
|
-
"
|
|
467
|
-
|
|
468
|
-
["ignore", "ignore", "pipe"],
|
|
469
|
-
);
|
|
452
|
+
const busChild = spawnWithParentDeathSignal("dbus-daemon", [`--config-file=${busConfigPath}`], {
|
|
453
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
454
|
+
});
|
|
470
455
|
|
|
456
|
+
busChild.unref();
|
|
471
457
|
spawned.push(busChild);
|
|
472
458
|
applyEnv(env, { DBUS_SESSION_BUS_ADDRESS: `unix:path=${busSocketPath}` });
|
|
473
459
|
const compositor = startCompositor(runtimeDir, options, env);
|
|
460
|
+
compositor.child.unref();
|
|
474
461
|
spawned.push(compositor.child);
|
|
475
462
|
applyEnv(env, { WAYLAND_DISPLAY: compositor.socket });
|
|
476
463
|
const compositorSocketPath = join(runtimeDir, compositor.socket);
|
|
@@ -497,7 +484,6 @@ const startHeadlessDisplay = async (options: HeadlessOptions): Promise<() => voi
|
|
|
497
484
|
};
|
|
498
485
|
|
|
499
486
|
export {
|
|
500
|
-
DEFAULT_HEADLESS_SIZE,
|
|
501
487
|
STATIC_HEADLESS_ENV,
|
|
502
488
|
resolveHeadlessOptions,
|
|
503
489
|
readHeadlessOptions,
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { type HeadlessOptions, STATIC_HEADLESS_ENV } from "./headless-display.ts
|
|
|
12
12
|
type PluginOptions = Partial<HeadlessOptions>;
|
|
13
13
|
|
|
14
14
|
const GTKX_INLINE_DEPS: RegExp[] = [/@gtkx\/(?!native)/, /[/\\]\.gtkx[/\\]/];
|
|
15
|
+
const DEFAULT_TIMEOUT = 30_000;
|
|
15
16
|
|
|
16
17
|
const workerPreloadUrl = (): URL => {
|
|
17
18
|
const sibling = join(import.meta.dirname, "worker-preload.js");
|
|
@@ -30,12 +31,6 @@ const headlessPreloadSpecifier = (options: PluginOptions): string => {
|
|
|
30
31
|
return url.href;
|
|
31
32
|
};
|
|
32
33
|
|
|
33
|
-
const workerSetupPath = (): string => {
|
|
34
|
-
const sibling = join(import.meta.dirname, "worker-setup.js");
|
|
35
|
-
|
|
36
|
-
return existsSync(sibling) ? sibling : join(import.meta.dirname, "..", "dist", "worker-setup.js");
|
|
37
|
-
};
|
|
38
|
-
|
|
39
34
|
/**
|
|
40
35
|
* Vitest plugin that runs each test worker against its own isolated headless
|
|
41
36
|
* Wayland display. It configures the forks pool, injects the worker preload and
|
|
@@ -47,14 +42,13 @@ const workerSetupPath = (): string => {
|
|
|
47
42
|
const gtkx = (options: PluginOptions = {}): Plugin =>
|
|
48
43
|
createConfigPlugin({
|
|
49
44
|
name: "gtkx:vitest",
|
|
50
|
-
config() {
|
|
45
|
+
config(config) {
|
|
51
46
|
return {
|
|
52
47
|
test: {
|
|
53
48
|
globals: true,
|
|
54
49
|
execArgv: ["--import", headlessPreloadSpecifier(options)],
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
hookTimeout: 30_000,
|
|
50
|
+
testTimeout: config.test?.testTimeout ?? DEFAULT_TIMEOUT,
|
|
51
|
+
hookTimeout: config.test?.hookTimeout ?? DEFAULT_TIMEOUT,
|
|
58
52
|
pool: "forks",
|
|
59
53
|
env: STATIC_HEADLESS_ENV,
|
|
60
54
|
server: {
|
package/src/worker-preload.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { installGracefulShutdown } from "@gtkx/utils";
|
|
1
2
|
import { readHeadlessOptions, resolveHeadlessOptions, startHeadlessDisplay } from "./headless-display.ts";
|
|
2
|
-
import { setHeadlessTeardown } from "./headless-globals.ts";
|
|
3
3
|
|
|
4
4
|
const options = readHeadlessOptions(new URL(import.meta.url).searchParams);
|
|
5
5
|
const teardown = await startHeadlessDisplay(resolveHeadlessOptions(options));
|
|
6
6
|
|
|
7
7
|
process.on("exit", teardown);
|
|
8
|
-
|
|
8
|
+
installGracefulShutdown({ onSignal: teardown });
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
declare global {
|
|
2
|
-
var gtkxHeadlessTeardown: (() => void) | undefined;
|
|
3
|
-
var isGtkxHeadlessShutdownInstalled: 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
|
|
@@ -1 +0,0 @@
|
|
|
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,+BAA+B,EAAE,OAAO,GAAG,SAAS,CAAC;CAC5D;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,OAA8D,CAAC;AAEvG,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,4BAA4B,EAAE,2BAA2B,EAAE,CAAC"}
|
package/dist/headless-globals.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
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("isGtkxHeadlessShutdownInstalled", installed);
|
|
10
|
-
};
|
|
11
|
-
const isHeadlessShutdownInstalled = () => globalThis.isGtkxHeadlessShutdownInstalled === true;
|
|
12
|
-
export { setHeadlessTeardown, headlessTeardown, setHeadlessShutdownInstalled, isHeadlessShutdownInstalled };
|
|
13
|
-
//# sourceMappingURL=headless-globals.js.map
|
|
@@ -1 +0,0 @@
|
|
|
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,iCAAiC,EAAE,SAAS,CAAC,CAAC;AAC/D,CAAC,CAAC;AAEF,MAAM,2BAA2B,GAAG,GAAY,EAAE,CAAC,UAAU,CAAC,+BAA+B,KAAK,IAAI,CAAC;AAEvG,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,4BAA4B,EAAE,2BAA2B,EAAE,CAAC","sourcesContent":["declare global {\n var gtkxHeadlessTeardown: (() => void) | undefined;\n var isGtkxHeadlessShutdownInstalled: 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(\"isGtkxHeadlessShutdownInstalled\", installed);\n};\n\nconst isHeadlessShutdownInstalled = (): boolean => globalThis.isGtkxHeadlessShutdownInstalled === true;\n\nexport { setHeadlessTeardown, headlessTeardown, setHeadlessShutdownInstalled, isHeadlessShutdownInstalled };\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"install-headless-shutdown.d.ts","sourceRoot":"","sources":["../src/install-headless-shutdown.ts"],"names":[],"mappings":"AAGA,QAAA,MAAM,uBAAuB,QAAO,IASnC,CAAC;AAEF,OAAO,EAAE,uBAAuB,EAAE,CAAC"}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { installGracefulShutdown } from "@gtkx/utils";
|
|
2
|
-
import { headlessTeardown, isHeadlessShutdownInstalled, setHeadlessShutdownInstalled } from "./headless-globals.js";
|
|
3
|
-
const installHeadlessShutdown = () => {
|
|
4
|
-
const teardown = headlessTeardown();
|
|
5
|
-
if (teardown === undefined || isHeadlessShutdownInstalled()) {
|
|
6
|
-
return;
|
|
7
|
-
}
|
|
8
|
-
setHeadlessShutdownInstalled(true);
|
|
9
|
-
installGracefulShutdown({ onSignal: teardown });
|
|
10
|
-
};
|
|
11
|
-
export { installHeadlessShutdown };
|
|
12
|
-
//# sourceMappingURL=install-headless-shutdown.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"install-headless-shutdown.js","sourceRoot":"","sources":["../src/install-headless-shutdown.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,2BAA2B,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAEpH,MAAM,uBAAuB,GAAG,GAAS,EAAE;IACvC,MAAM,QAAQ,GAAG,gBAAgB,EAAE,CAAC;IAEpC,IAAI,QAAQ,KAAK,SAAS,IAAI,2BAA2B,EAAE,EAAE,CAAC;QAC1D,OAAO;IACX,CAAC;IAED,4BAA4B,CAAC,IAAI,CAAC,CAAC;IACnC,uBAAuB,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;AACpD,CAAC,CAAC;AAEF,OAAO,EAAE,uBAAuB,EAAE,CAAC","sourcesContent":["import { installGracefulShutdown } from \"@gtkx/utils\";\nimport { headlessTeardown, isHeadlessShutdownInstalled, setHeadlessShutdownInstalled } from \"./headless-globals.ts\";\n\nconst installHeadlessShutdown = (): void => {\n const teardown = headlessTeardown();\n\n if (teardown === undefined || isHeadlessShutdownInstalled()) {\n return;\n }\n\n setHeadlessShutdownInstalled(true);\n installGracefulShutdown({ onSignal: teardown });\n};\n\nexport { installHeadlessShutdown };\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-executable.d.ts","sourceRoot":"","sources":["../src/resolve-executable.ts"],"names":[],"mappings":"AAaA,QAAA,MAAM,iBAAiB,GAAI,SAAS,MAAM,KAAG,MAgB5C,CAAC;AAEF,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { accessSync, constants } from "node:fs";
|
|
2
|
-
import { delimiter, isAbsolute, join } from "node:path";
|
|
3
|
-
const isExecutable = (path) => {
|
|
4
|
-
try {
|
|
5
|
-
accessSync(path, constants.X_OK);
|
|
6
|
-
return true;
|
|
7
|
-
}
|
|
8
|
-
catch {
|
|
9
|
-
return false;
|
|
10
|
-
}
|
|
11
|
-
};
|
|
12
|
-
const resolveExecutable = (command) => {
|
|
13
|
-
if (isAbsolute(command)) {
|
|
14
|
-
return command;
|
|
15
|
-
}
|
|
16
|
-
const searchPaths = (process.env.PATH ?? "").split(delimiter).filter((entry) => entry.length > 0);
|
|
17
|
-
for (const directory of searchPaths) {
|
|
18
|
-
const candidate = join(directory, command);
|
|
19
|
-
if (isExecutable(candidate)) {
|
|
20
|
-
return candidate;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
throw new Error(`Cannot find the "${command}" executable on PATH`);
|
|
24
|
-
};
|
|
25
|
-
export { resolveExecutable };
|
|
26
|
-
//# sourceMappingURL=resolve-executable.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-executable.js","sourceRoot":"","sources":["../src/resolve-executable.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAExD,MAAM,YAAY,GAAG,CAAC,IAAY,EAAW,EAAE;IAC3C,IAAI,CAAC;QACD,UAAU,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;QAEjC,OAAO,IAAI,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,KAAK,CAAC;IACjB,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,iBAAiB,GAAG,CAAC,OAAe,EAAU,EAAE;IAClD,IAAI,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACtB,OAAO,OAAO,CAAC;IACnB,CAAC;IAED,MAAM,WAAW,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAElG,KAAK,MAAM,SAAS,IAAI,WAAW,EAAE,CAAC;QAClC,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QAE3C,IAAI,YAAY,CAAC,SAAS,CAAC,EAAE,CAAC;YAC1B,OAAO,SAAS,CAAC;QACrB,CAAC;IACL,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,oBAAoB,OAAO,sBAAsB,CAAC,CAAC;AACvE,CAAC,CAAC;AAEF,OAAO,EAAE,iBAAiB,EAAE,CAAC","sourcesContent":["import { accessSync, constants } from \"node:fs\";\nimport { delimiter, isAbsolute, join } from \"node:path\";\n\nconst isExecutable = (path: string): boolean => {\n try {\n accessSync(path, constants.X_OK);\n\n return true;\n } catch {\n return false;\n }\n};\n\nconst resolveExecutable = (command: string): string => {\n if (isAbsolute(command)) {\n return command;\n }\n\n const searchPaths = (process.env.PATH ?? \"\").split(delimiter).filter((entry) => entry.length > 0);\n\n for (const directory of searchPaths) {\n const candidate = join(directory, command);\n\n if (isExecutable(candidate)) {\n return candidate;\n }\n }\n\n throw new Error(`Cannot find the \"${command}\" executable on PATH`);\n};\n\nexport { resolveExecutable };\n"]}
|
package/dist/worker-setup.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"worker-setup.d.ts","sourceRoot":"","sources":["../src/worker-setup.ts"],"names":[],"mappings":""}
|
package/dist/worker-setup.js
DELETED
package/dist/worker-setup.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"worker-setup.js","sourceRoot":"","sources":["../src/worker-setup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAEzE,uBAAuB,EAAE,CAAC","sourcesContent":["import { installHeadlessShutdown } from \"./install-headless-shutdown.ts\";\n\ninstallHeadlessShutdown();\n"]}
|
package/src/headless-globals.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
declare global {
|
|
2
|
-
var gtkxHeadlessTeardown: (() => void) | undefined;
|
|
3
|
-
var isGtkxHeadlessShutdownInstalled: boolean | undefined;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
const defineGlobal = (key: string, value: unknown): void => {
|
|
7
|
-
Object.defineProperty(globalThis, key, { value, configurable: true, writable: true });
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
const setHeadlessTeardown = (teardown: (() => void) | undefined): void => {
|
|
11
|
-
defineGlobal("gtkxHeadlessTeardown", teardown);
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const headlessTeardown = (): (() => void) | undefined => globalThis.gtkxHeadlessTeardown;
|
|
15
|
-
|
|
16
|
-
const setHeadlessShutdownInstalled = (installed: boolean | undefined): void => {
|
|
17
|
-
defineGlobal("isGtkxHeadlessShutdownInstalled", installed);
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const isHeadlessShutdownInstalled = (): boolean => globalThis.isGtkxHeadlessShutdownInstalled === true;
|
|
21
|
-
|
|
22
|
-
export { setHeadlessTeardown, headlessTeardown, setHeadlessShutdownInstalled, isHeadlessShutdownInstalled };
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { installGracefulShutdown } from "@gtkx/utils";
|
|
2
|
-
import { headlessTeardown, isHeadlessShutdownInstalled, setHeadlessShutdownInstalled } from "./headless-globals.ts";
|
|
3
|
-
|
|
4
|
-
const installHeadlessShutdown = (): void => {
|
|
5
|
-
const teardown = headlessTeardown();
|
|
6
|
-
|
|
7
|
-
if (teardown === undefined || isHeadlessShutdownInstalled()) {
|
|
8
|
-
return;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
setHeadlessShutdownInstalled(true);
|
|
12
|
-
installGracefulShutdown({ onSignal: teardown });
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export { installHeadlessShutdown };
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { accessSync, constants } from "node:fs";
|
|
2
|
-
import { delimiter, isAbsolute, join } from "node:path";
|
|
3
|
-
|
|
4
|
-
const isExecutable = (path: string): boolean => {
|
|
5
|
-
try {
|
|
6
|
-
accessSync(path, constants.X_OK);
|
|
7
|
-
|
|
8
|
-
return true;
|
|
9
|
-
} catch {
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const resolveExecutable = (command: string): string => {
|
|
15
|
-
if (isAbsolute(command)) {
|
|
16
|
-
return command;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const searchPaths = (process.env.PATH ?? "").split(delimiter).filter((entry) => entry.length > 0);
|
|
20
|
-
|
|
21
|
-
for (const directory of searchPaths) {
|
|
22
|
-
const candidate = join(directory, command);
|
|
23
|
-
|
|
24
|
-
if (isExecutable(candidate)) {
|
|
25
|
-
return candidate;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
throw new Error(`Cannot find the "${command}" executable on PATH`);
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export { resolveExecutable };
|
package/src/worker-setup.ts
DELETED