@gtkx/vitest 1.0.0-rc.3 → 1.0.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 +8 -9
- package/dist/headless-display.d.ts.map +1 -1
- package/dist/headless-display.js +15 -17
- package/dist/headless-display.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -5
- 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 +3 -3
- package/src/headless-display.ts +19 -32
- package/src/index.ts +7 -14
- package/src/worker-preload.ts +3 -3
- 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
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
The React framework for Linux.<br />
|
|
9
|
-
|
|
9
|
+
Write native Linux applications with React and TypeScript.
|
|
10
|
+
Built on the GNOME stack and standard web tooling.
|
|
10
11
|
</p>
|
|
11
12
|
|
|
12
13
|
<p align="center">
|
|
@@ -20,15 +21,13 @@
|
|
|
20
21
|
|
|
21
22
|
<p align="center">
|
|
22
23
|
<a href="https://gtkx.dev">Homepage</a> ·
|
|
23
|
-
<a href="https://gtkx.dev/guide/
|
|
24
|
+
<a href="https://gtkx.dev/guide/getting-started">Documentation</a> ·
|
|
24
25
|
<a href="https://github.com/gtkx-org/gtkx/tree/main/examples">Examples</a> ·
|
|
25
26
|
<a href="https://github.com/gtkx-org/gtkx/blob/main/CONTRIBUTING.md">Contributing</a>
|
|
26
27
|
</p>
|
|
27
28
|
|
|
28
29
|
---
|
|
29
30
|
|
|
30
|
-
GTKX generates fully typed bindings for the entire GTK4 and Adwaita surface directly from GObject-Introspection. On top of those bindings you get the React programming model: components and hooks driving GObject instances, with Fast Refresh while you develop.
|
|
31
|
-
|
|
32
31
|
<p align="center">
|
|
33
32
|
<img src="https://raw.githubusercontent.com/gtkx-org/gtkx/main/examples/tutorial/assets/screenshot.png" alt="The Tasks app: an Adwaita window with a sidebar of smart views and colored lists on the left, and a boxed task list on the right." />
|
|
34
33
|
</p>
|
|
@@ -109,7 +108,7 @@ React Native and similar frameworks hide the native toolkit so one API can run e
|
|
|
109
108
|
|
|
110
109
|
### Why Node.js, and why generated bindings
|
|
111
110
|
|
|
112
|
-
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 [
|
|
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.
|
|
113
112
|
|
|
114
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.
|
|
115
114
|
|
|
@@ -122,10 +121,10 @@ GTKX is Linux-only and needs Node.js 24 or later. See [Requirements](#requiremen
|
|
|
122
121
|
Scaffold a new app with the `create-gtkx` initializer:
|
|
123
122
|
|
|
124
123
|
```sh
|
|
125
|
-
npm create gtkx
|
|
124
|
+
npm create gtkx
|
|
126
125
|
```
|
|
127
126
|
|
|
128
|
-
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`.
|
|
129
128
|
|
|
130
129
|
Then run your new app:
|
|
131
130
|
|
|
@@ -146,7 +145,7 @@ The documentation at **[gtkx.dev](https://gtkx.dev)** includes a step-by-step tu
|
|
|
146
145
|
|
|
147
146
|
GTKX is Linux-only. You need:
|
|
148
147
|
|
|
149
|
-
- 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`
|
|
150
149
|
- Node.js 24 or later
|
|
151
150
|
|
|
152
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.
|
|
@@ -162,7 +161,7 @@ Explore the [example apps](https://github.com/gtkx-org/gtkx/tree/main/examples):
|
|
|
162
161
|
|
|
163
162
|
## Status
|
|
164
163
|
|
|
165
|
-
GTKX 1.0 is
|
|
164
|
+
GTKX 1.0 is released.
|
|
166
165
|
|
|
167
166
|
## Contributing
|
|
168
167
|
|
|
@@ -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;AAgDF,QAAA,MAAM,qBAAqB,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"headless-display.d.ts","sourceRoot":"","sources":["../src/headless-display.ts"],"names":[],"mappings":"AASA;;GAEG;AACH,KAAK,YAAY,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEtC;;GAEG;AACH,KAAK,eAAe,GAAG;IACnB,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,UAAU,EAAE,YAAY,CAAC;CAC5B,CAAC;AAgDF,QAAA,MAAM,qBAAqB,aAAa,CAAC;AA4DzC,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,qBAAqB,EACrB,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACpB,KAAK,YAAY,EACjB,KAAK,eAAe,GACvB,CAAC"}
|
package/dist/headless-display.js
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
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: {
|
|
17
16
|
socket: "wayland-1",
|
|
18
|
-
|
|
17
|
+
requiresVirtualSeat: true,
|
|
19
18
|
env: {
|
|
20
19
|
WLR_BACKENDS: "headless",
|
|
21
20
|
WLR_RENDERER: "pixman",
|
|
@@ -35,12 +34,12 @@ 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: {
|
|
42
41
|
socket: "wayland-0",
|
|
43
|
-
|
|
42
|
+
requiresVirtualSeat: false,
|
|
44
43
|
env: {},
|
|
45
44
|
start: (_runtimeDir, width, height) => spawnWithParentDeathSignal("weston", [
|
|
46
45
|
"--backend=headless",
|
|
@@ -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 () => {
|
|
@@ -121,11 +115,11 @@ const startCompositor = (runtimeDir, options, env) => {
|
|
|
121
115
|
return {
|
|
122
116
|
child: descriptor.start(runtimeDir, width, height),
|
|
123
117
|
socket: descriptor.socket,
|
|
124
|
-
|
|
118
|
+
requiresVirtualSeat: descriptor.requiresVirtualSeat,
|
|
125
119
|
};
|
|
126
120
|
};
|
|
127
121
|
const noVirtualSeat = () => undefined;
|
|
128
|
-
const attachVirtualSeat = (compositor, socketPath) => compositor.
|
|
122
|
+
const attachVirtualSeat = (compositor, socketPath) => compositor.requiresVirtualSeat ? startVirtualSeat(socketPath) : Promise.resolve(noVirtualSeat);
|
|
129
123
|
const writeBusConfig = (busConfigPath, busSocketPath) => {
|
|
130
124
|
writeFileSync(busConfigPath, [
|
|
131
125
|
BUS_CONFIG_DOCTYPE,
|
|
@@ -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);
|
|
@@ -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.js\";\nimport { resolveExecutable } from \"./resolve-executable.js\";\nimport { startVirtualSeat } from \"./virtual-seat.js\";\n\n/**\n * Wayland compositors that can back a headless display.\n */\ntype CompositorId = \"sway\" | \"weston\";\n\n/**\n * Settings for the per-worker headless Wayland display.\n */\ntype HeadlessOptions = {\n /** Output resolution as a \"WIDTHxHEIGHT\" string, for example \"1024x768\". */\n size: string;\n /** Wayland compositor used to back the headless display. */\n compositor: CompositorId;\n};\n\ntype EnvSnapshot = Record<string, string | undefined>;\n\ntype CompositorDescriptor = {\n socket: string;\n env: Record<string, string>;\n needsVirtualSeat: boolean;\n start: (runtimeDir: string, width: string, height: string) => ChildProcess;\n};\n\ntype SpawnedCompositor = {\n child: ChildProcess;\n socket: string;\n needsVirtualSeat: boolean;\n};\n\ntype WaitForSocketOptions = {\n label: string;\n timeout?: number;\n child?: ChildProcess;\n signal?: AbortSignal;\n};\n\ntype StderrCapture = {\n read: () => string;\n stop: () => void;\n};\n\ntype ChildHandlers = {\n exit: (code: number | null, signal: NodeJS.Signals | null) => void;\n error: (cause: Error) => void;\n};\n\ntype DisplaySockets = {\n compositor: SpawnedCompositor;\n compositorSocketPath: string;\n busChild: ChildProcess;\n busSocketPath: string;\n};\n\ntype SocketWatch = {\n path: string;\n options: WaitForSocketOptions;\n resolve: () => void;\n reject: (error: Error) => void;\n};\n\nconst DEFAULT_HEADLESS_SIZE = \"1024x768\";\nconst DEFAULT_HEADLESS_COMPOSITOR: CompositorId = \"sway\";\n\nconst BUS_CONFIG_DOCTYPE =\n '<!DOCTYPE busconfig PUBLIC \"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN\" ' +\n '\"https://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd\">';\n\nconst PARENT_DEATH_SCRIPT = 'trap \\'kill -9 \"$child\" 2>/dev/null\\' TERM; \"$@\" & child=$!; wait \"$child\"';\nconst hasWestonFakeSeat = createWestonFakeSeatProbe();\n\nconst compositorRegistry: Record<CompositorId, CompositorDescriptor> = {\n sway: {\n socket: \"wayland-1\",\n needsVirtualSeat: true,\n env: {\n WLR_BACKENDS: \"headless\",\n WLR_RENDERER: \"pixman\",\n WLR_RENDERER_ALLOW_SOFTWARE: \"1\",\n WLR_LIBINPUT_NO_DEVICES: \"1\",\n WLR_HEADLESS_OUTPUTS: \"1\",\n },\n start: (runtimeDir, width, height) => {\n const configPath = join(runtimeDir, \"sway.conf\");\n\n writeFileSync(\n configPath,\n [\n \"xwayland disable\",\n \"default_border none\",\n \"default_floating_border none\",\n `output HEADLESS-1 resolution ${width}x${height}`,\n \"output HEADLESS-1 bg #000000 solid_color\",\n 'for_window [app_id=\".*\"] floating enable, border none',\n 'for_window [title=\".*\"] floating enable, border none',\n \"\",\n ].join(\"\\n\"),\n );\n\n return spawnWithParentDeathSignal(\"sway\", [\"-c\", configPath], [\"ignore\", \"ignore\", \"pipe\"]);\n },\n },\n weston: {\n socket: \"wayland-0\",\n needsVirtualSeat: false,\n env: {},\n start: (_runtimeDir, width, height) =>\n spawnWithParentDeathSignal(\n \"weston\",\n [\n \"--backend=headless\",\n \"--renderer=pixman\",\n ...(hasWestonFakeSeat() ? [\"--fake-seat\"] : []),\n `--width=${width}`,\n `--height=${height}`,\n \"--socket=wayland-0\",\n ],\n [\"ignore\", \"ignore\", \"pipe\"],\n ),\n },\n};\n\nconst STATIC_HEADLESS_ENV = {\n GDK_BACKEND: \"wayland\",\n GDK_DISABLE: \"vulkan\",\n GDK_DEBUG: \"no-vsync\",\n GSK_RENDERER: \"cairo\",\n GTK_A11Y: \"test\",\n LIBGL_ALWAYS_SOFTWARE: \"1\",\n GST_GL_WINDOW: \"none\",\n GSETTINGS_BACKEND: \"memory\",\n ALSOFT_DRIVERS: \"null\",\n ALSOFT_LOGLEVEL: \"0\",\n};\n\nconst resolveHeadlessOptions = (provided: Partial<HeadlessOptions>): HeadlessOptions => ({\n size: provided.size ?? DEFAULT_HEADLESS_SIZE,\n compositor: provided.compositor ?? DEFAULT_HEADLESS_COMPOSITOR,\n});\n\nconst applyEnv = (snapshot: EnvSnapshot, values: Record<string, string>): void => {\n for (const [name, value] of Object.entries(values)) {\n if (!Object.hasOwn(snapshot, name)) {\n snapshot[name] = process.env[name];\n }\n\n process.env[name] = value;\n }\n};\n\nconst restoreEnv = (snapshot: EnvSnapshot): void => {\n for (const [name, previous] of Object.entries(snapshot)) {\n if (previous === undefined) {\n Reflect.deleteProperty(process.env, name);\n } else {\n process.env[name] = previous;\n }\n }\n};\n\nconst spawnWithParentDeathSignal = (command: string, args: string[], stdio: StdioOptions): ChildProcess => {\n const child = spawn(\n resolveExecutable(\"setpriv\"),\n [\"--pdeathsig\", \"SIGTERM\", \"sh\", \"-c\", PARENT_DEATH_SCRIPT, \"sh\", command, ...args],\n { stdio },\n );\n\n child.unref();\n\n return child;\n};\n\nfunction createWestonFakeSeatProbe(): () => boolean {\n let isSupported: boolean | undefined;\n\n return () => {\n if (isSupported === undefined) {\n const help = spawnSync(resolveExecutable(\"weston\"), [\"--help\"], { encoding: \"utf8\" });\n isSupported = `${help.stdout}${help.stderr}`.includes(\"--fake-seat\");\n }\n\n return isSupported;\n };\n}\n\nconst isCompositorId = (value: string): value is CompositorId => Object.hasOwn(compositorRegistry, value);\n\nconst readHeadlessOptions = (params: URLSearchParams): Partial<HeadlessOptions> => {\n const options: Partial<HeadlessOptions> = {};\n const size = params.get(\"size\");\n\n if (size !== null) {\n options.size = size;\n }\n\n const compositor = params.get(\"compositor\");\n\n if (compositor !== null && isCompositorId(compositor)) {\n options.compositor = compositor;\n }\n\n return options;\n};\n\nconst startCompositor = (runtimeDir: string, options: HeadlessOptions, env: EnvSnapshot): SpawnedCompositor => {\n const descriptor = compositorRegistry[options.compositor];\n const [width = \"\", height = \"\"] = options.size.split(\"x\", 2);\n applyEnv(env, descriptor.env);\n\n return {\n child: descriptor.start(runtimeDir, width, height),\n socket: descriptor.socket,\n needsVirtualSeat: descriptor.needsVirtualSeat,\n };\n};\n\nconst noVirtualSeat = (): void => undefined;\n\nconst attachVirtualSeat = (compositor: SpawnedCompositor, socketPath: string): Promise<() => void> =>\n compositor.needsVirtualSeat ? startVirtualSeat(socketPath) : Promise.resolve(noVirtualSeat);\n\nconst writeBusConfig = (busConfigPath: string, busSocketPath: string): void => {\n writeFileSync(\n busConfigPath,\n [\n BUS_CONFIG_DOCTYPE,\n \"<busconfig>\",\n \" <type>session</type>\",\n ` <listen>unix:path=${busSocketPath}</listen>`,\n \" <auth>EXTERNAL</auth>\",\n ' <policy context=\"default\">',\n ' <allow send_destination=\"*\" eavesdrop=\"true\"/>',\n ' <allow eavesdrop=\"true\"/>',\n ' <allow own=\"*\"/>',\n \" </policy>\",\n \"</busconfig>\",\n ].join(\"\\n\"),\n );\n};\n\nconst captureStderr = (child: ChildProcess | undefined): StderrCapture => {\n const stderr = child?.stderr ?? null;\n let log = \"\";\n stderr?.setEncoding(\"utf8\");\n\n stderr?.on(\"data\", (chunk: string) => {\n log += chunk;\n });\n\n return {\n read: () => log,\n stop: () => {\n stderr?.removeAllListeners(\"data\");\n stderr?.resume();\n\n if (stderr instanceof Socket) {\n stderr.unref();\n }\n },\n };\n};\n\nconst trackChild = (child: ChildProcess, handlers: ChildHandlers): (() => void) => {\n child.on(\"exit\", handlers.exit);\n child.on(\"error\", handlers.error);\n\n return () => {\n child.removeListener(\"exit\", handlers.exit);\n child.removeListener(\"error\", handlers.error);\n };\n};\n\nconst exitedMessage = (label: string, path: string, code: number | null, signal: NodeJS.Signals | null): string =>\n `${label} exited (code ${String(code)}, signal ${signal ?? \"null\"}) before ${path} appeared`;\n\nconst runCleanups = (cleanups: (() => void)[]): void => {\n for (const cleanup of cleanups) {\n cleanup();\n }\n\n cleanups.length = 0;\n};\n\nconst pollForPath = (path: string, onFound: () => void): NodeJS.Timeout =>\n setInterval(() => {\n if (existsSync(path)) {\n onFound();\n }\n }, 50);\n\nconst stderrSuffix = (child: ChildProcess | undefined, stderr: StderrCapture): string =>\n child ? `\\n${stderr.read()}` : \"\";\n\nconst hasAlreadyAborted = (signal: AbortSignal | undefined, onAbort: () => void, cleanups: (() => void)[]): boolean => {\n if (signal === undefined) {\n return false;\n }\n\n if (signal.aborted) {\n return true;\n }\n\n signal.addEventListener(\"abort\", onAbort);\n\n cleanups.push(() => {\n signal.removeEventListener(\"abort\", onAbort);\n });\n\n return false;\n};\n\nconst watchForSocket = ({ path, options, resolve, reject }: SocketWatch): void => {\n const { label, timeout = 15_000, child, signal } = options;\n const stderr = captureStderr(child);\n const cleanups: (() => void)[] = [stderr.stop];\n\n const stop = (): void => {\n runCleanups(cleanups);\n };\n\n const fail = (message: string): void => {\n stop();\n reject(new Error(message));\n };\n\n const onExit = (code: number | null, terminationSignal: NodeJS.Signals | null): void => {\n fail(`${exitedMessage(label, path, code, terminationSignal)}\\n${stderr.read()}`);\n };\n\n const onError = (cause: Error): void => {\n fail(`${label} failed to spawn: ${cause.message}\\n${stderr.read()}`);\n };\n\n const onAbort = (): void => {\n fail(`${label} startup aborted before ${path} appeared`);\n };\n\n const poll = pollForPath(path, () => {\n stop();\n resolve();\n });\n\n const timer = setTimeout(() => {\n fail(`${label} did not become available within ${String(timeout)}ms${stderrSuffix(child, stderr)}`);\n }, timeout);\n\n cleanups.push(() => {\n clearInterval(poll);\n clearTimeout(timer);\n });\n\n if (child) {\n cleanups.push(trackChild(child, { exit: onExit, error: onError }));\n }\n\n if (hasAlreadyAborted(signal, onAbort, cleanups)) {\n onAbort();\n }\n};\n\nconst waitForSocket = (path: string, options: WaitForSocketOptions): Promise<void> =>\n new Promise((resolve, reject) => {\n watchForSocket({ path, options, resolve, reject });\n });\n\nconst captureCompositorStderr = (child: ChildProcess, logPath: string): string[] => {\n const captured: string[] = [];\n const stderr = child.stderr;\n\n if (stderr !== null) {\n stderr.setEncoding(\"utf8\");\n const logStream = createWriteStream(logPath);\n logStream.on(\"error\", (): void => undefined);\n\n stderr.on(\"data\", (chunk: string) => {\n captured.push(chunk);\n logStream.write(chunk);\n });\n }\n\n return captured;\n};\n\nconst compositorExitMessage = (\n code: number | null,\n signal: NodeJS.Signals | null,\n capturedStderr: string[],\n): string =>\n `[gtkx] the headless compositor exited (code ${String(code)}, signal ${signal ?? \"null\"}); ` +\n `every Wayland client in this worker has been severed.\\n${capturedStderr.join(\"\")}`;\n\nconst watchCompositorExit = (child: ChildProcess, capturedStderr: string[]): (() => void) => {\n const report = (code: number | null, signal: NodeJS.Signals | null): void => {\n process.stderr.write(compositorExitMessage(code, signal, capturedStderr));\n };\n\n child.on(\"exit\", report);\n\n return () => child.removeListener(\"exit\", report);\n};\n\nconst waitForDisplaySockets = async (sockets: DisplaySockets): Promise<void> => {\n const { compositor, compositorSocketPath, busChild, busSocketPath } = sockets;\n const abort = new AbortController();\n\n try {\n await Promise.all([\n waitForSocket(compositorSocketPath, {\n label: \"Compositor\",\n child: compositor.child,\n signal: abort.signal,\n }),\n waitForSocket(busSocketPath, { label: \"D-Bus session bus\", child: busChild, signal: abort.signal }),\n ]);\n } finally {\n abort.abort();\n }\n};\n\nconst killSpawned = (children: ChildProcess[]): void => {\n for (const child of children) {\n child.kill(\"SIGTERM\");\n }\n};\n\nconst makeTeardown = (stops: (() => void)[]): (() => void) => {\n let isTorndown = false;\n\n return (): void => {\n if (isTorndown) {\n return;\n }\n\n isTorndown = true;\n runCleanups(stops);\n };\n};\n\nconst startHeadlessDisplay = async (options: HeadlessOptions): Promise<() => void> => {\n const env: EnvSnapshot = {};\n const runtimeDir = mkdtempSync(join(tmpdir(), \"gtkx-xdg-\"));\n chmodSync(runtimeDir, 0o700);\n const spawned: ChildProcess[] = [];\n\n const removeRuntime = (): void => {\n restoreEnv(env);\n rmSync(runtimeDir, { recursive: true, force: true });\n };\n\n try {\n applyEnv(env, { XDG_RUNTIME_DIR: runtimeDir });\n const busConfigPath = join(runtimeDir, \"session.conf\");\n const busSocketPath = join(runtimeDir, \"bus\");\n writeBusConfig(busConfigPath, busSocketPath);\n\n const busChild = spawnWithParentDeathSignal(\n \"dbus-daemon\",\n [`--config-file=${busConfigPath}`],\n [\"ignore\", \"ignore\", \"pipe\"],\n );\n\n spawned.push(busChild);\n applyEnv(env, { DBUS_SESSION_BUS_ADDRESS: `unix:path=${busSocketPath}` });\n const compositor = startCompositor(runtimeDir, options, env);\n spawned.push(compositor.child);\n applyEnv(env, { WAYLAND_DISPLAY: compositor.socket });\n const compositorSocketPath = join(runtimeDir, compositor.socket);\n await waitForDisplaySockets({ compositor, compositorSocketPath, busChild, busSocketPath });\n const stopVirtualSeat = await attachVirtualSeat(compositor, compositorSocketPath);\n const stopNotifications = await startNotificationService(`unix:path=${busSocketPath}`);\n const capturedStderr = captureCompositorStderr(compositor.child, join(runtimeDir, \"compositor.stderr.log\"));\n const stopExitWatch = watchCompositorExit(compositor.child, capturedStderr);\n\n return makeTeardown([\n stopExitWatch,\n () => {\n killSpawned(spawned);\n },\n stopVirtualSeat,\n stopNotifications,\n removeRuntime,\n ]);\n } catch (error) {\n killSpawned(spawned);\n removeRuntime();\n throw error;\n }\n};\n\nexport {\n DEFAULT_HEADLESS_SIZE,\n STATIC_HEADLESS_ENV,\n resolveHeadlessOptions,\n readHeadlessOptions,\n startHeadlessDisplay,\n type CompositorId,\n type HeadlessOptions,\n};\n"]}
|
|
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 DEFAULT_HEADLESS_SIZE,\n STATIC_HEADLESS_ENV,\n resolveHeadlessOptions,\n readHeadlessOptions,\n startHeadlessDisplay,\n type CompositorId,\n type HeadlessOptions,\n};\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { Plugin } from "vitest/config";
|
|
2
|
-
import { type HeadlessOptions } from "./headless-display.
|
|
2
|
+
import { type HeadlessOptions } from "./headless-display.ts";
|
|
3
3
|
/**
|
|
4
|
-
* Options accepted by the
|
|
4
|
+
* Options accepted by the GTKX Vitest plugin. Every headless display
|
|
5
5
|
* setting is optional and falls back to a built-in default when omitted.
|
|
6
6
|
*/
|
|
7
|
-
type
|
|
7
|
+
type PluginOptions = Partial<HeadlessOptions>;
|
|
8
8
|
/**
|
|
9
9
|
* Vitest plugin that runs each test worker against its own isolated headless
|
|
10
10
|
* Wayland display. It configures the forks pool, injects the worker preload and
|
|
@@ -13,8 +13,8 @@ type GtkxPluginOptions = Partial<HeadlessOptions>;
|
|
|
13
13
|
* @param options Headless display settings (size, compositor) forwarded to each worker.
|
|
14
14
|
* @returns A Vitest config plugin.
|
|
15
15
|
*/
|
|
16
|
-
declare const gtkx: (options?:
|
|
16
|
+
declare const gtkx: (options?: PluginOptions) => Plugin;
|
|
17
17
|
export default gtkx;
|
|
18
|
-
export { type CompositorId, type HeadlessOptions } from "./headless-display.
|
|
19
|
-
export { type
|
|
18
|
+
export { type CompositorId, type HeadlessOptions } from "./headless-display.ts";
|
|
19
|
+
export { type PluginOptions };
|
|
20
20
|
//# sourceMappingURL=index.d.ts.map
|
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,
|
|
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;AAqB9C;;;;;;;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
|
@@ -16,10 +16,6 @@ const headlessPreloadSpecifier = (options) => {
|
|
|
16
16
|
}
|
|
17
17
|
return url.href;
|
|
18
18
|
};
|
|
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
19
|
/**
|
|
24
20
|
* Vitest plugin that runs each test worker against its own isolated headless
|
|
25
21
|
* Wayland display. It configures the forks pool, injects the worker preload and
|
|
@@ -35,7 +31,6 @@ const gtkx = (options = {}) => createConfigPlugin({
|
|
|
35
31
|
test: {
|
|
36
32
|
globals: true,
|
|
37
33
|
execArgv: ["--import", headlessPreloadSpecifier(options)],
|
|
38
|
-
setupFiles: [workerSetupPath()],
|
|
39
34
|
testTimeout: 30_000,
|
|
40
35
|
hookTimeout: 30_000,
|
|
41
36
|
pool: "forks",
|
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;AAE7E,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,
|
|
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;AAE7E,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;QACF,OAAO;YACH,IAAI,EAAE;gBACF,OAAO,EAAE,IAAI;gBACb,QAAQ,EAAE,CAAC,UAAU,EAAE,wBAAwB,CAAC,OAAO,CAAC,CAAC;gBACzD,WAAW,EAAE,MAAM;gBACnB,WAAW,EAAE,MAAM;gBACnB,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[/\\\\]/];\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() {\n return {\n test: {\n globals: true,\n execArgv: [\"--import\", headlessPreloadSpecifier(options)],\n testTimeout: 30_000,\n hookTimeout: 30_000,\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.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Vitest plugin for GTKX applications with headless Wayland display isolation",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gtkx",
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@homebridge/dbus-native": "^0.7.8",
|
|
45
|
-
"@gtkx/config": "1.0.0
|
|
46
|
-
"@gtkx/utils": "1.0.0
|
|
45
|
+
"@gtkx/config": "1.0.0",
|
|
46
|
+
"@gtkx/utils": "1.0.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"vitest": "^4.1.10"
|
package/src/headless-display.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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
|
-
import { startNotificationService } from "./notification-service.
|
|
7
|
-
import {
|
|
8
|
-
import { startVirtualSeat } from "./virtual-seat.js";
|
|
7
|
+
import { startNotificationService } from "./notification-service.ts";
|
|
8
|
+
import { startVirtualSeat } from "./virtual-seat.ts";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Wayland compositors that can back a headless display.
|
|
@@ -27,14 +27,14 @@ type EnvSnapshot = Record<string, string | undefined>;
|
|
|
27
27
|
type CompositorDescriptor = {
|
|
28
28
|
socket: string;
|
|
29
29
|
env: Record<string, string>;
|
|
30
|
-
|
|
30
|
+
requiresVirtualSeat: boolean;
|
|
31
31
|
start: (runtimeDir: string, width: string, height: string) => ChildProcess;
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
type SpawnedCompositor = {
|
|
35
35
|
child: ChildProcess;
|
|
36
36
|
socket: string;
|
|
37
|
-
|
|
37
|
+
requiresVirtualSeat: boolean;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
type WaitForSocketOptions = {
|
|
@@ -75,13 +75,12 @@ 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> = {
|
|
82
81
|
sway: {
|
|
83
82
|
socket: "wayland-1",
|
|
84
|
-
|
|
83
|
+
requiresVirtualSeat: true,
|
|
85
84
|
env: {
|
|
86
85
|
WLR_BACKENDS: "headless",
|
|
87
86
|
WLR_RENDERER: "pixman",
|
|
@@ -106,12 +105,12 @@ 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: {
|
|
113
112
|
socket: "wayland-0",
|
|
114
|
-
|
|
113
|
+
requiresVirtualSeat: false,
|
|
115
114
|
env: {},
|
|
116
115
|
start: (_runtimeDir, width, height) =>
|
|
117
116
|
spawnWithParentDeathSignal(
|
|
@@ -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
|
|
|
@@ -219,14 +206,14 @@ const startCompositor = (runtimeDir: string, options: HeadlessOptions, env: EnvS
|
|
|
219
206
|
return {
|
|
220
207
|
child: descriptor.start(runtimeDir, width, height),
|
|
221
208
|
socket: descriptor.socket,
|
|
222
|
-
|
|
209
|
+
requiresVirtualSeat: descriptor.requiresVirtualSeat,
|
|
223
210
|
};
|
|
224
211
|
};
|
|
225
212
|
|
|
226
213
|
const noVirtualSeat = (): void => undefined;
|
|
227
214
|
|
|
228
215
|
const attachVirtualSeat = (compositor: SpawnedCompositor, socketPath: string): Promise<() => void> =>
|
|
229
|
-
compositor.
|
|
216
|
+
compositor.requiresVirtualSeat ? startVirtualSeat(socketPath) : Promise.resolve(noVirtualSeat);
|
|
230
217
|
|
|
231
218
|
const writeBusConfig = (busConfigPath: string, busSocketPath: string): void => {
|
|
232
219
|
writeFileSync(
|
|
@@ -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);
|
package/src/index.ts
CHANGED
|
@@ -3,13 +3,13 @@ import createConfigPlugin from "@gtkx/config/vite-plugin";
|
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
|
-
import { type HeadlessOptions, STATIC_HEADLESS_ENV } from "./headless-display.
|
|
6
|
+
import { type HeadlessOptions, STATIC_HEADLESS_ENV } from "./headless-display.ts";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Options accepted by the
|
|
9
|
+
* Options accepted by the GTKX Vitest plugin. Every headless display
|
|
10
10
|
* setting is optional and falls back to a built-in default when omitted.
|
|
11
11
|
*/
|
|
12
|
-
type
|
|
12
|
+
type PluginOptions = Partial<HeadlessOptions>;
|
|
13
13
|
|
|
14
14
|
const GTKX_INLINE_DEPS: RegExp[] = [/@gtkx\/(?!native)/, /[/\\]\.gtkx[/\\]/];
|
|
15
15
|
|
|
@@ -20,7 +20,7 @@ const workerPreloadUrl = (): URL => {
|
|
|
20
20
|
return pathToFileURL(path);
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
const headlessPreloadSpecifier = (options:
|
|
23
|
+
const headlessPreloadSpecifier = (options: PluginOptions): string => {
|
|
24
24
|
const url = workerPreloadUrl();
|
|
25
25
|
|
|
26
26
|
for (const [key, value] of Object.entries(options)) {
|
|
@@ -30,12 +30,6 @@ const headlessPreloadSpecifier = (options: GtkxPluginOptions): string => {
|
|
|
30
30
|
return url.href;
|
|
31
31
|
};
|
|
32
32
|
|
|
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
33
|
/**
|
|
40
34
|
* Vitest plugin that runs each test worker against its own isolated headless
|
|
41
35
|
* Wayland display. It configures the forks pool, injects the worker preload and
|
|
@@ -44,7 +38,7 @@ const workerSetupPath = (): string => {
|
|
|
44
38
|
* @param options Headless display settings (size, compositor) forwarded to each worker.
|
|
45
39
|
* @returns A Vitest config plugin.
|
|
46
40
|
*/
|
|
47
|
-
const gtkx = (options:
|
|
41
|
+
const gtkx = (options: PluginOptions = {}): Plugin =>
|
|
48
42
|
createConfigPlugin({
|
|
49
43
|
name: "gtkx:vitest",
|
|
50
44
|
config() {
|
|
@@ -52,7 +46,6 @@ const gtkx = (options: GtkxPluginOptions = {}): Plugin =>
|
|
|
52
46
|
test: {
|
|
53
47
|
globals: true,
|
|
54
48
|
execArgv: ["--import", headlessPreloadSpecifier(options)],
|
|
55
|
-
setupFiles: [workerSetupPath()],
|
|
56
49
|
testTimeout: 30_000,
|
|
57
50
|
hookTimeout: 30_000,
|
|
58
51
|
pool: "forks",
|
|
@@ -68,5 +61,5 @@ const gtkx = (options: GtkxPluginOptions = {}): Plugin =>
|
|
|
68
61
|
});
|
|
69
62
|
|
|
70
63
|
export default gtkx;
|
|
71
|
-
export { type CompositorId, type HeadlessOptions } from "./headless-display.
|
|
72
|
-
export { type
|
|
64
|
+
export { type CompositorId, type HeadlessOptions } from "./headless-display.ts";
|
|
65
|
+
export { type PluginOptions };
|
package/src/worker-preload.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { installGracefulShutdown } from "@gtkx/utils";
|
|
2
|
+
import { readHeadlessOptions, resolveHeadlessOptions, startHeadlessDisplay } from "./headless-display.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 gtkxHeadlessShutdownInstalled: boolean | undefined;
|
|
4
|
-
}
|
|
5
|
-
declare const setHeadlessTeardown: (teardown: (() => void) | undefined) => void;
|
|
6
|
-
declare const headlessTeardown: () => (() => void) | undefined;
|
|
7
|
-
declare const setHeadlessShutdownInstalled: (installed: boolean | undefined) => void;
|
|
8
|
-
declare const isHeadlessShutdownInstalled: () => boolean;
|
|
9
|
-
export { setHeadlessTeardown, headlessTeardown, setHeadlessShutdownInstalled, isHeadlessShutdownInstalled };
|
|
10
|
-
//# sourceMappingURL=headless-globals.d.ts.map
|
|
@@ -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,6BAA6B,EAAE,OAAO,GAAG,SAAS,CAAC;CAC1D;AAMD,QAAA,MAAM,mBAAmB,GAAI,UAAU,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,KAAG,IAEjE,CAAC;AAEF,QAAA,MAAM,gBAAgB,QAAO,CAAC,MAAM,IAAI,CAAC,GAAG,SAA4C,CAAC;AAEzF,QAAA,MAAM,4BAA4B,GAAI,WAAW,OAAO,GAAG,SAAS,KAAG,IAEtE,CAAC;AAEF,QAAA,MAAM,2BAA2B,QAAO,OAA4D,CAAC;AAErG,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,4BAA4B,EAAE,2BAA2B,EAAE,CAAC"}
|
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("gtkxHeadlessShutdownInstalled", installed);
|
|
10
|
-
};
|
|
11
|
-
const isHeadlessShutdownInstalled = () => globalThis.gtkxHeadlessShutdownInstalled === 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,+BAA+B,EAAE,SAAS,CAAC,CAAC;AAC7D,CAAC,CAAC;AAEF,MAAM,2BAA2B,GAAG,GAAY,EAAE,CAAC,UAAU,CAAC,6BAA6B,KAAK,IAAI,CAAC;AAErG,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,4BAA4B,EAAE,2BAA2B,EAAE,CAAC","sourcesContent":["declare global {\n var gtkxHeadlessTeardown: (() => void) | undefined;\n var gtkxHeadlessShutdownInstalled: boolean | undefined;\n}\n\nconst defineGlobal = (key: string, value: unknown): void => {\n Object.defineProperty(globalThis, key, { value, configurable: true, writable: true });\n};\n\nconst setHeadlessTeardown = (teardown: (() => void) | undefined): void => {\n defineGlobal(\"gtkxHeadlessTeardown\", teardown);\n};\n\nconst headlessTeardown = (): (() => void) | undefined => globalThis.gtkxHeadlessTeardown;\n\nconst setHeadlessShutdownInstalled = (installed: boolean | undefined): void => {\n defineGlobal(\"gtkxHeadlessShutdownInstalled\", installed);\n};\n\nconst isHeadlessShutdownInstalled = (): boolean => globalThis.gtkxHeadlessShutdownInstalled === true;\n\nexport { setHeadlessTeardown, headlessTeardown, setHeadlessShutdownInstalled, isHeadlessShutdownInstalled };\n"]}
|
|
@@ -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.js\";\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.js\";\n\ninstallHeadlessShutdown();\n"]}
|
package/src/headless-globals.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
declare global {
|
|
2
|
-
var gtkxHeadlessTeardown: (() => void) | undefined;
|
|
3
|
-
var gtkxHeadlessShutdownInstalled: 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("gtkxHeadlessShutdownInstalled", installed);
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const isHeadlessShutdownInstalled = (): boolean => globalThis.gtkxHeadlessShutdownInstalled === 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.js";
|
|
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