@gtkx/vitest 0.21.0 → 1.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -34
- package/dist/headless-display.d.ts +31 -0
- package/dist/headless-display.d.ts.map +1 -0
- package/dist/headless-display.js +335 -0
- package/dist/headless-display.js.map +1 -0
- package/dist/headless-globals.d.ts +10 -0
- package/dist/headless-globals.d.ts.map +1 -0
- package/dist/headless-globals.js +13 -0
- package/dist/headless-globals.js.map +1 -0
- package/dist/index.d.ts +19 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +54 -1
- package/dist/index.js.map +1 -1
- package/dist/install-headless-shutdown.d.ts +3 -0
- package/dist/install-headless-shutdown.d.ts.map +1 -0
- package/dist/install-headless-shutdown.js +12 -0
- package/dist/install-headless-shutdown.js.map +1 -0
- package/dist/notification-service.d.ts +3 -0
- package/dist/notification-service.d.ts.map +1 -0
- package/dist/notification-service.js +48 -0
- package/dist/notification-service.js.map +1 -0
- package/dist/resolve-executable.d.ts +3 -0
- package/dist/resolve-executable.d.ts.map +1 -0
- package/dist/resolve-executable.js +26 -0
- package/dist/resolve-executable.js.map +1 -0
- package/dist/virtual-seat.d.ts +3 -0
- package/dist/virtual-seat.d.ts.map +1 -0
- package/dist/virtual-seat.js +155 -0
- package/dist/virtual-seat.js.map +1 -0
- package/dist/worker-preload.d.ts +2 -0
- package/dist/worker-preload.d.ts.map +1 -0
- package/dist/worker-preload.js +7 -0
- package/dist/worker-preload.js.map +1 -0
- package/dist/worker-setup.d.ts +2 -0
- package/dist/worker-setup.d.ts.map +1 -0
- package/dist/worker-setup.js +3 -0
- package/dist/worker-setup.js.map +1 -0
- package/package.json +14 -7
- package/src/dbus-native.d.ts +17 -0
- package/src/headless-display.ts +507 -0
- package/src/headless-globals.ts +22 -0
- package/src/index.ts +72 -1
- package/src/install-headless-shutdown.ts +15 -0
- package/src/notification-service.ts +57 -0
- package/src/resolve-executable.ts +32 -0
- package/src/virtual-seat.ts +214 -0
- package/src/worker-preload.ts +8 -0
- package/src/worker-setup.ts +3 -0
- package/dist/plugin.d.ts +0 -22
- package/dist/plugin.d.ts.map +0 -1
- package/dist/plugin.js +0 -36
- package/dist/plugin.js.map +0 -1
- package/dist/setup.d.ts +0 -2
- package/dist/setup.d.ts.map +0 -1
- package/dist/setup.js +0 -44
- package/dist/setup.js.map +0 -1
- package/src/plugin.ts +0 -41
- package/src/setup.ts +0 -52
package/README.md
CHANGED
|
@@ -1,70 +1,173 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
|
|
2
|
+
<img src="https://raw.githubusercontent.com/gtkx-org/gtkx/main/logo.svg" alt="GTKX" width="100" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<h1 align="center">GTKX</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
|
|
8
|
+
The React framework for Linux.<br />
|
|
9
|
+
Build GTK4 and Adwaita apps in TypeScript, with React components and hooks driving GNOME's own widgets. What you ship is a GNOME app.
|
|
9
10
|
</p>
|
|
10
11
|
|
|
11
12
|
<p align="center">
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
<a href="https://www.npmjs.com/package/create-gtkx"><img src="https://img.shields.io/npm/v/create-gtkx?color=cb3837&logo=npm&label=create-gtkx" alt="npm version" /></a>
|
|
14
|
+
<a href="https://www.npmjs.com/package/create-gtkx"><img src="https://img.shields.io/npm/dm/create-gtkx?color=cb3837&logo=npm&label=downloads" alt="npm downloads" /></a>
|
|
15
|
+
<img src="https://img.shields.io/badge/node-%E2%89%A524-339933?logo=node.js&logoColor=white" alt="Node >= 24" />
|
|
16
|
+
<a href="https://github.com/gtkx-org/gtkx/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MPL--2.0-blue.svg" alt="License: MPL-2.0" /></a>
|
|
17
|
+
<a href="https://github.com/gtkx-org/gtkx/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/gtkx-org/gtkx/ci.yml?branch=main&logo=github&label=CI" alt="CI status" /></a>
|
|
18
|
+
<img src="https://img.shields.io/badge/TypeScript-strict-3178c6?logo=typescript&logoColor=white" alt="TypeScript" />
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<a href="https://gtkx.dev">Homepage</a> ·
|
|
23
|
+
<a href="https://gtkx.dev/guide/why-gtkx">Documentation</a> ·
|
|
24
|
+
<a href="https://github.com/gtkx-org/gtkx/tree/main/examples">Examples</a> ·
|
|
25
|
+
<a href="https://github.com/gtkx-org/gtkx/blob/main/CONTRIBUTING.md">Contributing</a>
|
|
16
26
|
</p>
|
|
17
27
|
|
|
18
28
|
---
|
|
19
29
|
|
|
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
|
+
|
|
20
32
|
<p align="center">
|
|
21
|
-
|
|
33
|
+
<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." />
|
|
22
34
|
</p>
|
|
23
35
|
|
|
24
|
-
|
|
36
|
+
<p align="center">
|
|
37
|
+
<em>The Tasks app you build in the <a href="https://gtkx.dev/tutorial/">tutorial</a>.</em>
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
## Demo
|
|
41
|
+
|
|
42
|
+
The intrinsic elements in this snippet render GTK4 widgets, and ordinary React hooks and events drive them:
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
46
|
+
import { GtkApplication, GtkApplicationWindow, GtkBox, GtkButton, GtkLabel } from "@gtkx/jsx/gtk";
|
|
47
|
+
import { createRoot, quit } from "@gtkx/react";
|
|
48
|
+
import { useState } from "react";
|
|
49
|
+
|
|
50
|
+
const Counter = () => {
|
|
51
|
+
const [count, setCount] = useState(0);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<GtkApplicationWindow
|
|
55
|
+
title="Hello GTKX"
|
|
56
|
+
defaultWidth={400}
|
|
57
|
+
defaultHeight={300}
|
|
58
|
+
onCloseRequest={quit}
|
|
59
|
+
>
|
|
60
|
+
<GtkBox
|
|
61
|
+
orientation={Gtk.Orientation.VERTICAL}
|
|
62
|
+
spacing={20}
|
|
63
|
+
marginTop={40}
|
|
64
|
+
marginBottom={40}
|
|
65
|
+
marginStart={40}
|
|
66
|
+
marginEnd={40}
|
|
67
|
+
valign={Gtk.Align.CENTER}
|
|
68
|
+
halign={Gtk.Align.CENTER}
|
|
69
|
+
>
|
|
70
|
+
<GtkLabel cssClasses={["title-1"]}>Welcome to GTKX!</GtkLabel>
|
|
71
|
+
<GtkLabel cssClasses={["title-2"]}>{`Count: ${count}`}</GtkLabel>
|
|
72
|
+
<GtkButton
|
|
73
|
+
label="Increment"
|
|
74
|
+
onClicked={() => setCount((c) => c + 1)}
|
|
75
|
+
cssClasses={["suggested-action", "pill"]}
|
|
76
|
+
/>
|
|
77
|
+
</GtkBox>
|
|
78
|
+
</GtkApplicationWindow>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const App = () => (
|
|
83
|
+
<GtkApplication>
|
|
84
|
+
<Counter />
|
|
85
|
+
</GtkApplication>
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
createRoot().render(<App />);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
This is the [`hello-world`](https://github.com/gtkx-org/gtkx/tree/main/examples/hello-world) example, with `app.tsx` and `index.tsx` combined into a single snippet. `@gtkx/gi` and `@gtkx/jsx` are per-project bindings generated by the CLI, not packages you install from npm.
|
|
92
|
+
|
|
93
|
+
## Why GTKX
|
|
94
|
+
|
|
95
|
+
### A declarative layer for the GNOME stack
|
|
96
|
+
|
|
97
|
+
GTK4 is mature, and GtkBuilder XML can lay out a static interface, but nothing re-renders that interface when your application state changes, and nothing hot-reloads it as you work. GTKX adds that missing layer, and the tooling around it, on top of the stack you already know:
|
|
98
|
+
|
|
99
|
+
- a React reconciler that exposes every GObject as a JSX element,
|
|
100
|
+
- a CLI for scaffolding, development, and production builds,
|
|
101
|
+
- a dev server with Fast Refresh that patches your running UI in place,
|
|
102
|
+
- CSS-in-JS styling and high-level list, grid, and dialog components,
|
|
103
|
+
- a Testing Library-style API for querying and driving your widgets in tests,
|
|
104
|
+
- and a Model Context Protocol (MCP) server that exposes your live app to AI agents.
|
|
105
|
+
|
|
106
|
+
### The full GNOME API surface
|
|
107
|
+
|
|
108
|
+
React Native and similar frameworks hide the native toolkit so one API can run everywhere. GTKX exposes it: GTK4, Adwaita, and any other GObject-Introspection library on your system. Linux-only by design.
|
|
109
|
+
|
|
110
|
+
### Why Node.js, and why generated bindings
|
|
111
|
+
|
|
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 [why-gtkx guide](https://gtkx.dev/guide/why-gtkx) covers the comparison in full.
|
|
25
113
|
|
|
26
|
-
GTKX
|
|
114
|
+
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.
|
|
27
115
|
|
|
28
|
-
|
|
116
|
+
At runtime, the native Rust core calls straight into the system GTK4, Adwaita, and GLib libraries through libffi, without loading libgirepository at all.
|
|
29
117
|
|
|
30
|
-
|
|
31
|
-
- **Fully native Node.js environment** - Runs on vanilla Node.js, with the help of a Neon native module
|
|
32
|
-
- **TypeScript first** — Full type safety with auto-generated bindings
|
|
33
|
-
- **Rich GLib support** — Provides bindings for most modern GLib/GObject libraries, including Adwaita
|
|
34
|
-
- **HMR** — Fast refresh during development powered by Vite
|
|
35
|
-
- **CSS-in-JS styling** — Easy styling with GTK CSS powered by Emotion
|
|
36
|
-
- **Testing library** — Testing Library-inspired API for testing components and E2E
|
|
118
|
+
## Quick start
|
|
37
119
|
|
|
38
|
-
|
|
120
|
+
GTKX is Linux-only and needs Node.js 24 or later. See [Requirements](#requirements).
|
|
39
121
|
|
|
40
|
-
|
|
41
|
-
|
|
122
|
+
Scaffold a new app with the `create-gtkx` initializer:
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
npm create gtkx@rc
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The same command works with other package managers: `pnpm create gtkx@rc` or `yarn create gtkx@rc`.
|
|
129
|
+
|
|
130
|
+
Then run your new app:
|
|
131
|
+
|
|
132
|
+
```sh
|
|
42
133
|
cd my-app
|
|
43
134
|
npm run dev
|
|
44
135
|
```
|
|
45
136
|
|
|
46
|
-
|
|
137
|
+
To go further, follow the [tutorial](https://gtkx.dev/tutorial/).
|
|
47
138
|
|
|
48
|
-
|
|
139
|
+
## Documentation
|
|
49
140
|
|
|
50
|
-
|
|
51
|
-
- **[gtk-demo](./examples/gtk-demo)** — Full replica of the official GTK demo app
|
|
52
|
-
- **[tutorial](./examples/tutorial)** — Notes app from the tutorial with GSettings and Adwaita
|
|
53
|
-
- **[browser](./examples/browser)** — Simple browser using WebKitWebView
|
|
141
|
+
The documentation at **[gtkx.dev](https://gtkx.dev)** includes a step-by-step tutorial that builds a complete GNOME app, from scaffolding to packaging and shipping, plus guides and a full API reference.
|
|
54
142
|
|
|
55
|
-
|
|
143
|
+
**[Read the docs →](https://gtkx.dev/guide/why-gtkx)**
|
|
56
144
|
|
|
57
|
-
|
|
145
|
+
## Requirements
|
|
58
146
|
|
|
59
|
-
|
|
147
|
+
GTKX is Linux-only. You need:
|
|
60
148
|
|
|
61
|
-
|
|
149
|
+
- Linux with the GTK4 (4.20 or later), Adwaita (1.8 or later), and GLib development libraries
|
|
150
|
+
- Node.js 24 or later
|
|
62
151
|
|
|
63
|
-
|
|
152
|
+
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.
|
|
153
|
+
|
|
154
|
+
## Examples
|
|
155
|
+
|
|
156
|
+
Explore the [example apps](https://github.com/gtkx-org/gtkx/tree/main/examples):
|
|
157
|
+
|
|
158
|
+
- [`hello-world`](https://github.com/gtkx-org/gtkx/tree/main/examples/hello-world): the counter above.
|
|
159
|
+
- [`gtk-demo`](https://github.com/gtkx-org/gtkx/tree/main/examples/gtk-demo): a React port of the official GTK4 widget showcase, covering lists, dialogs, gestures, CSS, and OpenGL.
|
|
160
|
+
- [`browser`](https://github.com/gtkx-org/gtkx/tree/main/examples/browser): a WebKitWebView-based web browser.
|
|
161
|
+
- [`tutorial`](https://github.com/gtkx-org/gtkx/tree/main/examples/tutorial): the Tasks app the documentation builds.
|
|
162
|
+
|
|
163
|
+
## Status
|
|
164
|
+
|
|
165
|
+
GTKX 1.0 is at the release candidate stage.
|
|
166
|
+
|
|
167
|
+
## Contributing
|
|
64
168
|
|
|
65
|
-
- [
|
|
66
|
-
- [Issue Tracker](https://github.com/gtkx-org/gtkx/issues) — Bug reports and feature requests
|
|
169
|
+
Contributions are welcome. See [CONTRIBUTING.md](https://github.com/gtkx-org/gtkx/blob/main/CONTRIBUTING.md), the [Code of Conduct](https://github.com/gtkx-org/gtkx/blob/main/CODE_OF_CONDUCT.md), and the [security policy](https://github.com/gtkx-org/gtkx/blob/main/SECURITY.md). Building the repo needs Node.js 24 or later, pnpm, and a Rust toolchain.
|
|
67
170
|
|
|
68
171
|
## License
|
|
69
172
|
|
|
70
|
-
[MPL-2.0](
|
|
173
|
+
GTKX is licensed under [MPL-2.0](https://github.com/gtkx-org/gtkx/blob/main/LICENSE).
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wayland compositors that can back a headless display.
|
|
3
|
+
*/
|
|
4
|
+
type CompositorId = "sway" | "weston";
|
|
5
|
+
/**
|
|
6
|
+
* Settings for the per-worker headless Wayland display.
|
|
7
|
+
*/
|
|
8
|
+
type HeadlessOptions = {
|
|
9
|
+
/** Output resolution as a "WIDTHxHEIGHT" string, for example "1024x768". */
|
|
10
|
+
size: string;
|
|
11
|
+
/** Wayland compositor used to back the headless display. */
|
|
12
|
+
compositor: CompositorId;
|
|
13
|
+
};
|
|
14
|
+
declare const DEFAULT_HEADLESS_SIZE = "1024x768";
|
|
15
|
+
declare const STATIC_HEADLESS_ENV: {
|
|
16
|
+
GDK_BACKEND: string;
|
|
17
|
+
GDK_DISABLE: string;
|
|
18
|
+
GDK_DEBUG: string;
|
|
19
|
+
GSK_RENDERER: string;
|
|
20
|
+
GTK_A11Y: string;
|
|
21
|
+
LIBGL_ALWAYS_SOFTWARE: string;
|
|
22
|
+
GST_GL_WINDOW: string;
|
|
23
|
+
GSETTINGS_BACKEND: string;
|
|
24
|
+
ALSOFT_DRIVERS: string;
|
|
25
|
+
ALSOFT_LOGLEVEL: string;
|
|
26
|
+
};
|
|
27
|
+
declare const resolveHeadlessOptions: (provided: Partial<HeadlessOptions>) => HeadlessOptions;
|
|
28
|
+
declare const readHeadlessOptions: (params: URLSearchParams) => Partial<HeadlessOptions>;
|
|
29
|
+
declare const startHeadlessDisplay: (options: HeadlessOptions) => Promise<() => void>;
|
|
30
|
+
export { DEFAULT_HEADLESS_SIZE, STATIC_HEADLESS_ENV, resolveHeadlessOptions, readHeadlessOptions, startHeadlessDisplay, type CompositorId, type HeadlessOptions, };
|
|
31
|
+
//# sourceMappingURL=headless-display.d.ts.map
|
|
@@ -0,0 +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;AA6DzC,QAAA,MAAM,mBAAmB;;;;;;;;;;;CAWxB,CAAC;AAEF,QAAA,MAAM,sBAAsB,GAAI,UAAU,OAAO,CAAC,eAAe,CAAC,KAAG,eAGnE,CAAC;AAiDH,QAAA,MAAM,mBAAmB,GAAI,QAAQ,eAAe,KAAG,OAAO,CAAC,eAAe,CAe7E,CAAC;AA4OF,QAAA,MAAM,oBAAoB,GAAU,SAAS,eAAe,KAAG,OAAO,CAAC,MAAM,IAAI,CAiDhF,CAAC;AAEF,OAAO,EACH,qBAAqB,EACrB,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACpB,KAAK,YAAY,EACjB,KAAK,eAAe,GACvB,CAAC"}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { chmodSync, createWriteStream, existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { Socket } from "node:net";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { startNotificationService } from "./notification-service.js";
|
|
7
|
+
import { resolveExecutable } from "./resolve-executable.js";
|
|
8
|
+
import { startVirtualSeat } from "./virtual-seat.js";
|
|
9
|
+
const DEFAULT_HEADLESS_SIZE = "1024x768";
|
|
10
|
+
const DEFAULT_HEADLESS_COMPOSITOR = "sway";
|
|
11
|
+
const BUS_CONFIG_DOCTYPE = '<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" ' +
|
|
12
|
+
'"https://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">';
|
|
13
|
+
const PARENT_DEATH_SCRIPT = 'trap \'kill -9 "$child" 2>/dev/null\' TERM; "$@" & child=$!; wait "$child"';
|
|
14
|
+
const hasWestonFakeSeat = createWestonFakeSeatProbe();
|
|
15
|
+
const compositorRegistry = {
|
|
16
|
+
sway: {
|
|
17
|
+
socket: "wayland-1",
|
|
18
|
+
needsVirtualSeat: true,
|
|
19
|
+
env: {
|
|
20
|
+
WLR_BACKENDS: "headless",
|
|
21
|
+
WLR_RENDERER: "pixman",
|
|
22
|
+
WLR_RENDERER_ALLOW_SOFTWARE: "1",
|
|
23
|
+
WLR_LIBINPUT_NO_DEVICES: "1",
|
|
24
|
+
WLR_HEADLESS_OUTPUTS: "1",
|
|
25
|
+
},
|
|
26
|
+
start: (runtimeDir, width, height) => {
|
|
27
|
+
const configPath = join(runtimeDir, "sway.conf");
|
|
28
|
+
writeFileSync(configPath, [
|
|
29
|
+
"xwayland disable",
|
|
30
|
+
"default_border none",
|
|
31
|
+
"default_floating_border none",
|
|
32
|
+
`output HEADLESS-1 resolution ${width}x${height}`,
|
|
33
|
+
"output HEADLESS-1 bg #000000 solid_color",
|
|
34
|
+
'for_window [app_id=".*"] floating enable, border none',
|
|
35
|
+
'for_window [title=".*"] floating enable, border none',
|
|
36
|
+
"",
|
|
37
|
+
].join("\n"));
|
|
38
|
+
return spawnWithParentDeathSignal("sway", ["-c", configPath], ["ignore", "ignore", "pipe"]);
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
weston: {
|
|
42
|
+
socket: "wayland-0",
|
|
43
|
+
needsVirtualSeat: false,
|
|
44
|
+
env: {},
|
|
45
|
+
start: (_runtimeDir, width, height) => spawnWithParentDeathSignal("weston", [
|
|
46
|
+
"--backend=headless",
|
|
47
|
+
"--renderer=pixman",
|
|
48
|
+
...(hasWestonFakeSeat() ? ["--fake-seat"] : []),
|
|
49
|
+
`--width=${width}`,
|
|
50
|
+
`--height=${height}`,
|
|
51
|
+
"--socket=wayland-0",
|
|
52
|
+
], ["ignore", "ignore", "pipe"]),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
const STATIC_HEADLESS_ENV = {
|
|
56
|
+
GDK_BACKEND: "wayland",
|
|
57
|
+
GDK_DISABLE: "vulkan",
|
|
58
|
+
GDK_DEBUG: "no-vsync",
|
|
59
|
+
GSK_RENDERER: "cairo",
|
|
60
|
+
GTK_A11Y: "test",
|
|
61
|
+
LIBGL_ALWAYS_SOFTWARE: "1",
|
|
62
|
+
GST_GL_WINDOW: "none",
|
|
63
|
+
GSETTINGS_BACKEND: "memory",
|
|
64
|
+
ALSOFT_DRIVERS: "null",
|
|
65
|
+
ALSOFT_LOGLEVEL: "0",
|
|
66
|
+
};
|
|
67
|
+
const resolveHeadlessOptions = (provided) => ({
|
|
68
|
+
size: provided.size ?? DEFAULT_HEADLESS_SIZE,
|
|
69
|
+
compositor: provided.compositor ?? DEFAULT_HEADLESS_COMPOSITOR,
|
|
70
|
+
});
|
|
71
|
+
const applyEnv = (snapshot, values) => {
|
|
72
|
+
for (const [name, value] of Object.entries(values)) {
|
|
73
|
+
if (!Object.hasOwn(snapshot, name)) {
|
|
74
|
+
snapshot[name] = process.env[name];
|
|
75
|
+
}
|
|
76
|
+
process.env[name] = value;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const restoreEnv = (snapshot) => {
|
|
80
|
+
for (const [name, previous] of Object.entries(snapshot)) {
|
|
81
|
+
if (previous === undefined) {
|
|
82
|
+
Reflect.deleteProperty(process.env, name);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
process.env[name] = previous;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const spawnWithParentDeathSignal = (command, args, stdio) => {
|
|
90
|
+
const child = spawn(resolveExecutable("setpriv"), ["--pdeathsig", "SIGTERM", "sh", "-c", PARENT_DEATH_SCRIPT, "sh", command, ...args], { stdio });
|
|
91
|
+
child.unref();
|
|
92
|
+
return child;
|
|
93
|
+
};
|
|
94
|
+
function createWestonFakeSeatProbe() {
|
|
95
|
+
let isSupported;
|
|
96
|
+
return () => {
|
|
97
|
+
if (isSupported === undefined) {
|
|
98
|
+
const help = spawnSync(resolveExecutable("weston"), ["--help"], { encoding: "utf8" });
|
|
99
|
+
isSupported = `${help.stdout}${help.stderr}`.includes("--fake-seat");
|
|
100
|
+
}
|
|
101
|
+
return isSupported;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const isCompositorId = (value) => Object.hasOwn(compositorRegistry, value);
|
|
105
|
+
const readHeadlessOptions = (params) => {
|
|
106
|
+
const options = {};
|
|
107
|
+
const size = params.get("size");
|
|
108
|
+
if (size !== null) {
|
|
109
|
+
options.size = size;
|
|
110
|
+
}
|
|
111
|
+
const compositor = params.get("compositor");
|
|
112
|
+
if (compositor !== null && isCompositorId(compositor)) {
|
|
113
|
+
options.compositor = compositor;
|
|
114
|
+
}
|
|
115
|
+
return options;
|
|
116
|
+
};
|
|
117
|
+
const startCompositor = (runtimeDir, options, env) => {
|
|
118
|
+
const descriptor = compositorRegistry[options.compositor];
|
|
119
|
+
const [width = "", height = ""] = options.size.split("x", 2);
|
|
120
|
+
applyEnv(env, descriptor.env);
|
|
121
|
+
return {
|
|
122
|
+
child: descriptor.start(runtimeDir, width, height),
|
|
123
|
+
socket: descriptor.socket,
|
|
124
|
+
needsVirtualSeat: descriptor.needsVirtualSeat,
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
const noVirtualSeat = () => undefined;
|
|
128
|
+
const attachVirtualSeat = (compositor, socketPath) => compositor.needsVirtualSeat ? startVirtualSeat(socketPath) : Promise.resolve(noVirtualSeat);
|
|
129
|
+
const writeBusConfig = (busConfigPath, busSocketPath) => {
|
|
130
|
+
writeFileSync(busConfigPath, [
|
|
131
|
+
BUS_CONFIG_DOCTYPE,
|
|
132
|
+
"<busconfig>",
|
|
133
|
+
" <type>session</type>",
|
|
134
|
+
` <listen>unix:path=${busSocketPath}</listen>`,
|
|
135
|
+
" <auth>EXTERNAL</auth>",
|
|
136
|
+
' <policy context="default">',
|
|
137
|
+
' <allow send_destination="*" eavesdrop="true"/>',
|
|
138
|
+
' <allow eavesdrop="true"/>',
|
|
139
|
+
' <allow own="*"/>',
|
|
140
|
+
" </policy>",
|
|
141
|
+
"</busconfig>",
|
|
142
|
+
].join("\n"));
|
|
143
|
+
};
|
|
144
|
+
const captureStderr = (child) => {
|
|
145
|
+
const stderr = child?.stderr ?? null;
|
|
146
|
+
let log = "";
|
|
147
|
+
stderr?.setEncoding("utf8");
|
|
148
|
+
stderr?.on("data", (chunk) => {
|
|
149
|
+
log += chunk;
|
|
150
|
+
});
|
|
151
|
+
return {
|
|
152
|
+
read: () => log,
|
|
153
|
+
stop: () => {
|
|
154
|
+
stderr?.removeAllListeners("data");
|
|
155
|
+
stderr?.resume();
|
|
156
|
+
if (stderr instanceof Socket) {
|
|
157
|
+
stderr.unref();
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
const trackChild = (child, handlers) => {
|
|
163
|
+
child.on("exit", handlers.exit);
|
|
164
|
+
child.on("error", handlers.error);
|
|
165
|
+
return () => {
|
|
166
|
+
child.removeListener("exit", handlers.exit);
|
|
167
|
+
child.removeListener("error", handlers.error);
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
const exitedMessage = (label, path, code, signal) => `${label} exited (code ${String(code)}, signal ${signal ?? "null"}) before ${path} appeared`;
|
|
171
|
+
const runCleanups = (cleanups) => {
|
|
172
|
+
for (const cleanup of cleanups) {
|
|
173
|
+
cleanup();
|
|
174
|
+
}
|
|
175
|
+
cleanups.length = 0;
|
|
176
|
+
};
|
|
177
|
+
const pollForPath = (path, onFound) => setInterval(() => {
|
|
178
|
+
if (existsSync(path)) {
|
|
179
|
+
onFound();
|
|
180
|
+
}
|
|
181
|
+
}, 50);
|
|
182
|
+
const stderrSuffix = (child, stderr) => child ? `\n${stderr.read()}` : "";
|
|
183
|
+
const hasAlreadyAborted = (signal, onAbort, cleanups) => {
|
|
184
|
+
if (signal === undefined) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
if (signal.aborted) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
signal.addEventListener("abort", onAbort);
|
|
191
|
+
cleanups.push(() => {
|
|
192
|
+
signal.removeEventListener("abort", onAbort);
|
|
193
|
+
});
|
|
194
|
+
return false;
|
|
195
|
+
};
|
|
196
|
+
const watchForSocket = ({ path, options, resolve, reject }) => {
|
|
197
|
+
const { label, timeout = 15_000, child, signal } = options;
|
|
198
|
+
const stderr = captureStderr(child);
|
|
199
|
+
const cleanups = [stderr.stop];
|
|
200
|
+
const stop = () => {
|
|
201
|
+
runCleanups(cleanups);
|
|
202
|
+
};
|
|
203
|
+
const fail = (message) => {
|
|
204
|
+
stop();
|
|
205
|
+
reject(new Error(message));
|
|
206
|
+
};
|
|
207
|
+
const onExit = (code, terminationSignal) => {
|
|
208
|
+
fail(`${exitedMessage(label, path, code, terminationSignal)}\n${stderr.read()}`);
|
|
209
|
+
};
|
|
210
|
+
const onError = (cause) => {
|
|
211
|
+
fail(`${label} failed to spawn: ${cause.message}\n${stderr.read()}`);
|
|
212
|
+
};
|
|
213
|
+
const onAbort = () => {
|
|
214
|
+
fail(`${label} startup aborted before ${path} appeared`);
|
|
215
|
+
};
|
|
216
|
+
const poll = pollForPath(path, () => {
|
|
217
|
+
stop();
|
|
218
|
+
resolve();
|
|
219
|
+
});
|
|
220
|
+
const timer = setTimeout(() => {
|
|
221
|
+
fail(`${label} did not become available within ${String(timeout)}ms${stderrSuffix(child, stderr)}`);
|
|
222
|
+
}, timeout);
|
|
223
|
+
cleanups.push(() => {
|
|
224
|
+
clearInterval(poll);
|
|
225
|
+
clearTimeout(timer);
|
|
226
|
+
});
|
|
227
|
+
if (child) {
|
|
228
|
+
cleanups.push(trackChild(child, { exit: onExit, error: onError }));
|
|
229
|
+
}
|
|
230
|
+
if (hasAlreadyAborted(signal, onAbort, cleanups)) {
|
|
231
|
+
onAbort();
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const waitForSocket = (path, options) => new Promise((resolve, reject) => {
|
|
235
|
+
watchForSocket({ path, options, resolve, reject });
|
|
236
|
+
});
|
|
237
|
+
const captureCompositorStderr = (child, logPath) => {
|
|
238
|
+
const captured = [];
|
|
239
|
+
const stderr = child.stderr;
|
|
240
|
+
if (stderr !== null) {
|
|
241
|
+
stderr.setEncoding("utf8");
|
|
242
|
+
const logStream = createWriteStream(logPath);
|
|
243
|
+
logStream.on("error", () => undefined);
|
|
244
|
+
stderr.on("data", (chunk) => {
|
|
245
|
+
captured.push(chunk);
|
|
246
|
+
logStream.write(chunk);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return captured;
|
|
250
|
+
};
|
|
251
|
+
const compositorExitMessage = (code, signal, capturedStderr) => `[gtkx] the headless compositor exited (code ${String(code)}, signal ${signal ?? "null"}); ` +
|
|
252
|
+
`every Wayland client in this worker has been severed.\n${capturedStderr.join("")}`;
|
|
253
|
+
const watchCompositorExit = (child, capturedStderr) => {
|
|
254
|
+
const report = (code, signal) => {
|
|
255
|
+
process.stderr.write(compositorExitMessage(code, signal, capturedStderr));
|
|
256
|
+
};
|
|
257
|
+
child.on("exit", report);
|
|
258
|
+
return () => child.removeListener("exit", report);
|
|
259
|
+
};
|
|
260
|
+
const waitForDisplaySockets = async (sockets) => {
|
|
261
|
+
const { compositor, compositorSocketPath, busChild, busSocketPath } = sockets;
|
|
262
|
+
const abort = new AbortController();
|
|
263
|
+
try {
|
|
264
|
+
await Promise.all([
|
|
265
|
+
waitForSocket(compositorSocketPath, {
|
|
266
|
+
label: "Compositor",
|
|
267
|
+
child: compositor.child,
|
|
268
|
+
signal: abort.signal,
|
|
269
|
+
}),
|
|
270
|
+
waitForSocket(busSocketPath, { label: "D-Bus session bus", child: busChild, signal: abort.signal }),
|
|
271
|
+
]);
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
abort.abort();
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
const killSpawned = (children) => {
|
|
278
|
+
for (const child of children) {
|
|
279
|
+
child.kill("SIGTERM");
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
const makeTeardown = (stops) => {
|
|
283
|
+
let isTorndown = false;
|
|
284
|
+
return () => {
|
|
285
|
+
if (isTorndown) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
isTorndown = true;
|
|
289
|
+
runCleanups(stops);
|
|
290
|
+
};
|
|
291
|
+
};
|
|
292
|
+
const startHeadlessDisplay = async (options) => {
|
|
293
|
+
const env = {};
|
|
294
|
+
const runtimeDir = mkdtempSync(join(tmpdir(), "gtkx-xdg-"));
|
|
295
|
+
chmodSync(runtimeDir, 0o700);
|
|
296
|
+
const spawned = [];
|
|
297
|
+
const removeRuntime = () => {
|
|
298
|
+
restoreEnv(env);
|
|
299
|
+
rmSync(runtimeDir, { recursive: true, force: true });
|
|
300
|
+
};
|
|
301
|
+
try {
|
|
302
|
+
applyEnv(env, { XDG_RUNTIME_DIR: runtimeDir });
|
|
303
|
+
const busConfigPath = join(runtimeDir, "session.conf");
|
|
304
|
+
const busSocketPath = join(runtimeDir, "bus");
|
|
305
|
+
writeBusConfig(busConfigPath, busSocketPath);
|
|
306
|
+
const busChild = spawnWithParentDeathSignal("dbus-daemon", [`--config-file=${busConfigPath}`], ["ignore", "ignore", "pipe"]);
|
|
307
|
+
spawned.push(busChild);
|
|
308
|
+
applyEnv(env, { DBUS_SESSION_BUS_ADDRESS: `unix:path=${busSocketPath}` });
|
|
309
|
+
const compositor = startCompositor(runtimeDir, options, env);
|
|
310
|
+
spawned.push(compositor.child);
|
|
311
|
+
applyEnv(env, { WAYLAND_DISPLAY: compositor.socket });
|
|
312
|
+
const compositorSocketPath = join(runtimeDir, compositor.socket);
|
|
313
|
+
await waitForDisplaySockets({ compositor, compositorSocketPath, busChild, busSocketPath });
|
|
314
|
+
const stopVirtualSeat = await attachVirtualSeat(compositor, compositorSocketPath);
|
|
315
|
+
const stopNotifications = await startNotificationService(`unix:path=${busSocketPath}`);
|
|
316
|
+
const capturedStderr = captureCompositorStderr(compositor.child, join(runtimeDir, "compositor.stderr.log"));
|
|
317
|
+
const stopExitWatch = watchCompositorExit(compositor.child, capturedStderr);
|
|
318
|
+
return makeTeardown([
|
|
319
|
+
stopExitWatch,
|
|
320
|
+
() => {
|
|
321
|
+
killSpawned(spawned);
|
|
322
|
+
},
|
|
323
|
+
stopVirtualSeat,
|
|
324
|
+
stopNotifications,
|
|
325
|
+
removeRuntime,
|
|
326
|
+
]);
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
killSpawned(spawned);
|
|
330
|
+
removeRuntime();
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
export { DEFAULT_HEADLESS_SIZE, STATIC_HEADLESS_ENV, resolveHeadlessOptions, readHeadlessOptions, startHeadlessDisplay, };
|
|
335
|
+
//# sourceMappingURL=headless-display.js.map
|
|
@@ -0,0 +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"]}
|