gestament 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +20 -5
  2. package/dist/displaySession.d.ts +2 -2
  3. package/dist/displaySession.d.ts.map +1 -1
  4. package/dist/element.d.ts +2 -2
  5. package/dist/errors.d.ts +2 -2
  6. package/dist/generated/packageMetadata.d.ts +4 -4
  7. package/dist/gestament-config.d.ts +2 -2
  8. package/dist/gestament-launcher-driver.cjs +91 -5
  9. package/dist/gestament-launcher-driver.cjs.map +1 -1
  10. package/dist/gestament-launcher-driver.d.ts +2 -2
  11. package/dist/gestament-launcher-driver.mjs +91 -5
  12. package/dist/gestament-launcher-driver.mjs.map +1 -1
  13. package/dist/gestament-tray-host.cjs +9 -1
  14. package/dist/gestament-tray-host.cjs.map +1 -1
  15. package/dist/gestament-tray-host.d.ts +2 -2
  16. package/dist/gestament-tray-host.mjs +9 -1
  17. package/dist/gestament-tray-host.mjs.map +1 -1
  18. package/dist/gestament-xvfb-pool-probe.cjs +1 -1
  19. package/dist/gestament-xvfb-pool-probe.d.ts +2 -2
  20. package/dist/gestament-xvfb-pool-probe.mjs +1 -1
  21. package/dist/gestament-xvfb-worker.d.ts +2 -2
  22. package/dist/gestament-xvfb.d.ts +2 -2
  23. package/dist/gestament.cjs +279 -0
  24. package/dist/gestament.cjs.map +1 -0
  25. package/dist/gestament.d.ts +29 -0
  26. package/dist/gestament.d.ts.map +1 -0
  27. package/dist/gestament.mjs +278 -0
  28. package/dist/gestament.mjs.map +1 -0
  29. package/dist/index.cjs +1 -1
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.mjs +1 -1
  32. package/dist/{launchGtkApp-BfELuV-H.cjs → launchGtkApp-CzYcrc9f.cjs} +693 -76
  33. package/dist/launchGtkApp-CzYcrc9f.cjs.map +1 -0
  34. package/dist/{launchGtkApp-Bst1BFbD.js → launchGtkApp-EI6OIpPI.js} +691 -74
  35. package/dist/launchGtkApp-EI6OIpPI.js.map +1 -0
  36. package/dist/launchGtkApp.d.ts +2 -2
  37. package/dist/launchGtkApp.d.ts.map +1 -1
  38. package/dist/launcherDriverProtocol.d.ts +26 -4
  39. package/dist/launcherDriverProtocol.d.ts.map +1 -1
  40. package/dist/{native-C6MsIBNF.js → native-DlCBBWlV.js} +2 -2
  41. package/dist/native-DlCBBWlV.js.map +1 -0
  42. package/dist/{native-BUWDWMBB.cjs → native-Kfm95Uxw.cjs} +6 -6
  43. package/dist/native-Kfm95Uxw.cjs.map +1 -0
  44. package/dist/native.d.ts +2 -2
  45. package/dist/output.d.ts +27 -0
  46. package/dist/output.d.ts.map +1 -0
  47. package/dist/packageMetadata-CewXH0iF.cjs +4 -0
  48. package/dist/packageMetadata-CewXH0iF.cjs.map +1 -0
  49. package/dist/packageMetadata-DjxyJCJm.js +5 -0
  50. package/dist/packageMetadata-DjxyJCJm.js.map +1 -0
  51. package/dist/prerequisites.d.ts +2 -2
  52. package/dist/testing.d.ts +2 -2
  53. package/dist/tray.d.ts +2 -2
  54. package/dist/types.d.ts +183 -3
  55. package/dist/types.d.ts.map +1 -1
  56. package/dist/wait.d.ts +2 -2
  57. package/package.json +8 -7
  58. package/prebuilds/linux-arm/gtk3/node.napi.armv7.glibc.node +0 -0
  59. package/prebuilds/linux-arm/gtk4/node.napi.armv7.glibc.node +0 -0
  60. package/prebuilds/linux-arm64/gtk3/node.napi.glibc.node +0 -0
  61. package/prebuilds/linux-arm64/gtk4/node.napi.glibc.node +0 -0
  62. package/prebuilds/linux-ia32/gtk3/node.napi.glibc.node +0 -0
  63. package/prebuilds/linux-ia32/gtk4/node.napi.glibc.node +0 -0
  64. package/prebuilds/linux-riscv64/gtk3/node.napi.glibc.node +0 -0
  65. package/prebuilds/linux-riscv64/gtk4/node.napi.glibc.node +0 -0
  66. package/prebuilds/linux-x64/gtk3/node.napi.glibc.node +0 -0
  67. package/prebuilds/linux-x64/gtk4/node.napi.glibc.node +0 -0
  68. package/dist/launchGtkApp-BfELuV-H.cjs.map +0 -1
  69. package/dist/launchGtkApp-Bst1BFbD.js.map +0 -1
  70. package/dist/native-BUWDWMBB.cjs.map +0 -1
  71. package/dist/native-C6MsIBNF.js.map +0 -1
@@ -7,7 +7,232 @@ import { tmpdir } from "node:os";
7
7
  import { join, resolve, dirname } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { a as appendPrerequisiteInstallHint } from "./prerequisites-JB0SKPVd.js";
10
- import { c as nativeElementInfo, d as nativeClick, e as nativeImageInfo, f as nativeCaptureBounds, g as nativeTableDeselectColumn, h as nativeTableSelectColumn, i as nativeTableDeselectRow, j as nativeTableSelectRow, k as nativeTableIsCellSelected, l as nativeTableIsColumnSelected, m as nativeTableIsRowSelected, o as nativeTableSelectedColumns, p as nativeTableSelectedRows, q as nativeTableColumnCount, r as nativeTableRowCount, s as nativeClearSelection, t as nativeSelectAllChildren, u as nativeDeselectChildAt, v as nativeSelectChildAt, w as nativeIsChildSelected, x as nativeSelectedChildAt, y as nativeSelectedChildCount, z as nativeChildCount, A as nativeChildAt, B as nativeValueInfo, C as nativeSetValue, D as nativeText, E as nativeSetText, F as nativeX11Info, G as nativeResizeHints, H as nativeBounds, I as nativeCapture, J as nativeTableCellAt, K as nativeFindAnyById, L as nativeTrayItems, M as nativeWindowCount, N as nativeWindowAt, a as nativeCaptureScreen, O as nativeFindById, P as nativeProcessAtspiReadiness } from "./native-C6MsIBNF.js";
10
+ import { c as nativeElementInfo, d as nativeClick, e as nativeImageInfo, f as nativeCaptureBounds, g as nativeTableDeselectColumn, h as nativeTableSelectColumn, i as nativeTableDeselectRow, j as nativeTableSelectRow, k as nativeTableIsCellSelected, l as nativeTableIsColumnSelected, m as nativeTableIsRowSelected, o as nativeTableSelectedColumns, p as nativeTableSelectedRows, q as nativeTableColumnCount, r as nativeTableRowCount, s as nativeClearSelection, t as nativeSelectAllChildren, u as nativeDeselectChildAt, v as nativeSelectChildAt, w as nativeIsChildSelected, x as nativeSelectedChildAt, y as nativeSelectedChildCount, z as nativeChildCount, A as nativeChildAt, B as nativeValueInfo, C as nativeSetValue, D as nativeText, E as nativeSetText, F as nativeX11Info, G as nativeResizeHints, H as nativeBounds, I as nativeCapture, J as nativeTableCellAt, K as nativeFindAnyById, L as nativeTrayItems, M as nativeWindowCount, N as nativeWindowAt, a as nativeCaptureScreen, O as nativeFindById, P as nativeProcessAtspiReadiness } from "./native-DlCBBWlV.js";
11
+ const normalizeOutputBufferBytes = (value, optionName = "outputBufferBytes") => {
12
+ if (value === void 0) {
13
+ return void 0;
14
+ }
15
+ if (!Number.isSafeInteger(value) || value < 0) {
16
+ throw createGtkInvalidArgumentError(
17
+ `${optionName} must be a non-negative safe integer.`
18
+ );
19
+ }
20
+ return value;
21
+ };
22
+ const createStreamState = () => ({
23
+ byteLength: 0,
24
+ chunks: [],
25
+ decoder: new TextDecoder(),
26
+ flushed: false,
27
+ truncated: false
28
+ });
29
+ const appendBoundedChunk = (state, chunk, maxBytes) => {
30
+ if (chunk.length === 0) {
31
+ return;
32
+ }
33
+ if (maxBytes === void 0) {
34
+ state.chunks.push(Buffer.from(chunk));
35
+ state.byteLength += chunk.length;
36
+ return;
37
+ }
38
+ if (maxBytes === 0) {
39
+ state.truncated = true;
40
+ return;
41
+ }
42
+ if (chunk.length >= maxBytes) {
43
+ state.chunks.splice(
44
+ 0,
45
+ state.chunks.length,
46
+ Buffer.from(chunk.subarray(chunk.length - maxBytes))
47
+ );
48
+ state.byteLength = maxBytes;
49
+ state.truncated = true;
50
+ return;
51
+ }
52
+ state.chunks.push(Buffer.from(chunk));
53
+ state.byteLength += chunk.length;
54
+ while (state.byteLength > maxBytes) {
55
+ const first = state.chunks[0];
56
+ if (first === void 0) {
57
+ state.byteLength = 0;
58
+ return;
59
+ }
60
+ const excess = state.byteLength - maxBytes;
61
+ if (first.length <= excess) {
62
+ state.chunks.shift();
63
+ state.byteLength -= first.length;
64
+ state.truncated = true;
65
+ continue;
66
+ }
67
+ state.chunks[0] = Buffer.from(first.subarray(excess));
68
+ state.byteLength -= excess;
69
+ state.truncated = true;
70
+ }
71
+ };
72
+ const streamText = (state) => Buffer.concat(state.chunks, state.byteLength).toString("utf8");
73
+ const createOutputEvent = (nextSequence, create, text) => {
74
+ if (text.length === 0) {
75
+ return void 0;
76
+ }
77
+ return create(nextSequence());
78
+ };
79
+ const createGtkAppOutputRecorder = (outputBufferBytes) => {
80
+ const maxBytes = normalizeOutputBufferBytes(outputBufferBytes);
81
+ const stdout = createStreamState();
82
+ const stderr = createStreamState();
83
+ let nextSequence = 0;
84
+ const streamState = (stream) => stream === "stdout" ? stdout : stderr;
85
+ const takeSequence = () => {
86
+ const sequence = nextSequence;
87
+ nextSequence += 1;
88
+ return sequence;
89
+ };
90
+ const createEvent = (stream, text) => createOutputEvent(
91
+ takeSequence,
92
+ (sequence) => ({
93
+ sequence,
94
+ stream,
95
+ text,
96
+ timestampMs: Date.now()
97
+ }),
98
+ text
99
+ );
100
+ return {
101
+ append: (stream, chunk) => {
102
+ const state = streamState(stream);
103
+ appendBoundedChunk(state, chunk, maxBytes);
104
+ return createEvent(stream, state.decoder.decode(chunk, { stream: true }));
105
+ },
106
+ flush: (stream) => {
107
+ const state = streamState(stream);
108
+ if (state.flushed) {
109
+ return void 0;
110
+ }
111
+ state.flushed = true;
112
+ return createEvent(stream, state.decoder.decode());
113
+ },
114
+ snapshot: (exitCode, exitSignal) => ({
115
+ exitCode,
116
+ exitSignal,
117
+ stderr: streamText(stderr),
118
+ stderrTruncated: stderr.truncated,
119
+ stdout: streamText(stdout),
120
+ stdoutTruncated: stdout.truncated
121
+ })
122
+ };
123
+ };
124
+ const systemOutputSources = [
125
+ "xvfb",
126
+ "launcher-driver",
127
+ "tray-host"
128
+ ];
129
+ const createSourceState = () => ({
130
+ stderr: createStreamState(),
131
+ stdout: createStreamState()
132
+ });
133
+ const createGtkSystemOutputRecorder = (systemOutputBufferBytes) => {
134
+ const maxBytes = normalizeOutputBufferBytes(
135
+ systemOutputBufferBytes,
136
+ "systemOutputBufferBytes"
137
+ );
138
+ const sources = /* @__PURE__ */ new Map();
139
+ let nextSequence = 0;
140
+ const takeSequence = () => {
141
+ const sequence = nextSequence;
142
+ nextSequence += 1;
143
+ return sequence;
144
+ };
145
+ const sourceState = (source) => {
146
+ const existing = sources.get(source);
147
+ if (existing !== void 0) {
148
+ return existing;
149
+ }
150
+ const state = createSourceState();
151
+ sources.set(source, state);
152
+ return state;
153
+ };
154
+ const streamState = (source, stream) => {
155
+ const state = sourceState(source);
156
+ return stream === "stdout" ? state.stdout : state.stderr;
157
+ };
158
+ const existingStreamState = (source, stream) => {
159
+ const state = sources.get(source);
160
+ if (state === void 0) {
161
+ return void 0;
162
+ }
163
+ return stream === "stdout" ? state.stdout : state.stderr;
164
+ };
165
+ const createEvent = (source, stream, text) => createOutputEvent(
166
+ takeSequence,
167
+ (sequence) => ({
168
+ sequence,
169
+ source,
170
+ stream,
171
+ text,
172
+ timestampMs: Date.now()
173
+ }),
174
+ text
175
+ );
176
+ const sourceSnapshot = (source, state) => ({
177
+ source,
178
+ stderr: streamText(state.stderr),
179
+ stderrTruncated: state.stderr.truncated,
180
+ stdout: streamText(state.stdout),
181
+ stdoutTruncated: state.stdout.truncated
182
+ });
183
+ return {
184
+ append: (source, stream, chunk) => {
185
+ const state = streamState(source, stream);
186
+ appendBoundedChunk(state, chunk, maxBytes);
187
+ return createEvent(
188
+ source,
189
+ stream,
190
+ state.decoder.decode(chunk, { stream: true })
191
+ );
192
+ },
193
+ flush: (source, stream) => {
194
+ const state = existingStreamState(source, stream);
195
+ if (state === void 0) {
196
+ return void 0;
197
+ }
198
+ if (state.flushed) {
199
+ return void 0;
200
+ }
201
+ state.flushed = true;
202
+ return createEvent(source, stream, state.decoder.decode());
203
+ },
204
+ snapshot: () => ({
205
+ sources: systemOutputSources.flatMap((source) => {
206
+ const state = sources.get(source);
207
+ return state === void 0 ? [] : [sourceSnapshot(source, state)];
208
+ })
209
+ })
210
+ };
211
+ };
212
+ const notifyGtkAppOutput = (callback, event) => {
213
+ if (callback === void 0 || event === void 0) {
214
+ return;
215
+ }
216
+ try {
217
+ callback(event);
218
+ } catch (error) {
219
+ queueMicrotask(() => {
220
+ throw error;
221
+ });
222
+ }
223
+ };
224
+ const notifyGtkSystemOutput = (callback, event) => {
225
+ if (callback === void 0 || event === void 0) {
226
+ return;
227
+ }
228
+ try {
229
+ callback(event);
230
+ } catch (error) {
231
+ queueMicrotask(() => {
232
+ throw error;
233
+ });
234
+ }
235
+ };
11
236
  const defaultDisplay = "xvfb";
12
237
  const defaultGSettings = "memory";
13
238
  const defaultTheme = "Adwaita";
@@ -20,6 +245,9 @@ const sessionStartupTimeoutMs = 3e4;
20
245
  const sessionReleaseTimeoutMs = 5e3;
21
246
  const xvfbStartupTimeoutMs = 1e4;
22
247
  const xvfbPoolProbeTimeoutMs = 5e3;
248
+ const xvfbPoolProbeRetryIntervalMs = 50;
249
+ const xvfbPoolProbePrefix = "gestament-xvfb-pool-probe: ";
250
+ const x11DisplayOpenFailureMessage = "Failed to open the X11 display. Ensure DISPLAY points to an X11 display.";
23
251
  const firstPooledDisplayNumber = 90;
24
252
  const lastPooledDisplayNumber = 590;
25
253
  const sessionOwnedEnvironmentKeys = [
@@ -33,6 +261,7 @@ const sessionOwnedEnvironmentKeys = [
33
261
  "GESTAMENT_XVFB_ACTIVE",
34
262
  "XDG_SESSION_TYPE"
35
263
  ];
264
+ let outputScopeCounter = 0;
36
265
  let socketCounter = 0;
37
266
  const leasedDisplayNumbers = /* @__PURE__ */ new Set();
38
267
  const idleXvfbByKey = /* @__PURE__ */ new Map();
@@ -49,6 +278,12 @@ const appendOutput$1 = (lines, chunk) => {
49
278
  lines.splice(0, lines.length - 40);
50
279
  }
51
280
  };
281
+ const appendSystemOutput = (sink, source, stream, chunk) => {
282
+ sink?.append(source, stream, chunk);
283
+ };
284
+ const flushSystemOutput = (sink, source, stream) => {
285
+ sink?.flush(source, stream);
286
+ };
52
287
  const unrefHandle = (handle) => {
53
288
  const maybeRefHandle = handle;
54
289
  if (typeof maybeRefHandle.unref === "function") {
@@ -67,6 +302,41 @@ ${stdoutText}
67
302
  stderr:
68
303
  ${stderrText}`;
69
304
  };
305
+ const createXvfbProbeError = (message, retryable) => {
306
+ const error = createGtkOperationFailedError(message);
307
+ Object.defineProperty(error, "retryable", {
308
+ value: retryable
309
+ });
310
+ return error;
311
+ };
312
+ const parseXvfbProbeErrorPayload = (stderrText) => {
313
+ const lines = stderrText.trim().split("\n").reverse();
314
+ for (const line of lines) {
315
+ if (!line.startsWith(xvfbPoolProbePrefix)) {
316
+ continue;
317
+ }
318
+ try {
319
+ const value = JSON.parse(
320
+ line.slice(xvfbPoolProbePrefix.length)
321
+ );
322
+ if (!isRecord(value)) {
323
+ return void 0;
324
+ }
325
+ return {
326
+ code: typeof value.code === "string" ? value.code : void 0,
327
+ message: typeof value.message === "string" ? value.message : void 0
328
+ };
329
+ } catch {
330
+ return void 0;
331
+ }
332
+ }
333
+ return void 0;
334
+ };
335
+ const isRetryableXvfbProbeExit = (stderrText) => {
336
+ const payload = parseXvfbProbeErrorPayload(stderrText);
337
+ return payload?.code === "OPERATION_FAILED" && payload.message === x11DisplayOpenFailureMessage;
338
+ };
339
+ const isRetryableXvfbProbeError = (error) => isRecord(error) && typeof error.retryable === "boolean" && error.retryable;
70
340
  const getHostDisplayState = () => ({
71
341
  display: process.env.DISPLAY,
72
342
  waylandDisplay: process.env.WAYLAND_DISPLAY
@@ -92,6 +362,63 @@ const resolveDisplay = (display) => {
92
362
  );
93
363
  };
94
364
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
365
+ const createDriverEventKey = (channel, scopeId) => `${channel}
366
+ ${scopeId}`;
367
+ const createOutputScopeId = () => {
368
+ const scopeId = `output-${process.pid}-${outputScopeCounter}`;
369
+ outputScopeCounter += 1;
370
+ return scopeId;
371
+ };
372
+ const isDriverEventMessage = (message) => isRecord(message) && message.type === "event" && typeof message.channel === "string" && typeof message.scopeId === "string";
373
+ const resolveOutputBufferBytes = (launcherValue, launchValue) => {
374
+ const value = launchValue ?? launcherValue;
375
+ if (value === void 0) {
376
+ return null;
377
+ }
378
+ if (!Number.isSafeInteger(value) || value < 0) {
379
+ throw createGtkInvalidArgumentError(
380
+ "outputBufferBytes must be a non-negative safe integer."
381
+ );
382
+ }
383
+ return value;
384
+ };
385
+ const createSystemOutputSink = (recorder, callback) => ({
386
+ append: (source, stream, chunk) => {
387
+ notifyGtkSystemOutput(callback, recorder.append(source, stream, chunk));
388
+ },
389
+ flush: (source, stream) => {
390
+ notifyGtkSystemOutput(callback, recorder.flush(source, stream));
391
+ }
392
+ });
393
+ const isGtkSystemOutputSource = (value) => value === "xvfb" || value === "launcher-driver" || value === "tray-host";
394
+ const isGtkAppOutputStream = (value) => value === "stdout" || value === "stderr";
395
+ const isWireGtkSystemOutput = (value) => {
396
+ if (!isRecord(value)) {
397
+ return false;
398
+ }
399
+ if (!isGtkSystemOutputSource(value.source) || !isGtkAppOutputStream(value.stream)) {
400
+ return false;
401
+ }
402
+ if (value.type === "flush") {
403
+ return true;
404
+ }
405
+ return value.type === "chunk" && typeof value.chunkBase64 === "string";
406
+ };
407
+ const routeWireSystemOutput = (sink, value) => {
408
+ if (!isWireGtkSystemOutput(value)) {
409
+ return;
410
+ }
411
+ if (value.type === "flush") {
412
+ flushSystemOutput(sink, value.source, value.stream);
413
+ return;
414
+ }
415
+ appendSystemOutput(
416
+ sink,
417
+ value.source,
418
+ value.stream,
419
+ Buffer.from(value.chunkBase64, "base64")
420
+ );
421
+ };
95
422
  const resolvePoolLimit = (name, value, defaultValue) => {
96
423
  if (value === void 0) {
97
424
  return defaultValue;
@@ -239,6 +566,7 @@ const retainIdleXvfb = async (xvfb, limits) => {
239
566
  await terminateXvfb(xvfb);
240
567
  return;
241
568
  }
569
+ xvfb.systemOutputSink = void 0;
242
570
  xvfb.lastUsedAt = Date.now();
243
571
  pushArrayEntry(idleXvfbByKey, xvfb.key, xvfb);
244
572
  await trimIdleXvfbKey(xvfb.key, limits.maxIdlePerKey);
@@ -249,6 +577,8 @@ const retainIdleAllSession = async (session, limits) => {
249
577
  await session.session.terminate();
250
578
  return;
251
579
  }
580
+ session.session.setSystemOutputSink(void 0);
581
+ session.xvfb.systemOutputSink = void 0;
252
582
  session.lastUsedAt = Date.now();
253
583
  session.xvfb.lastUsedAt = session.lastUsedAt;
254
584
  pushArrayEntry(idleAllByKey, session.key, session);
@@ -461,7 +791,7 @@ const installPoolCleanup = () => {
461
791
  }
462
792
  });
463
793
  };
464
- const spawnDirectXvfb = async (screen) => {
794
+ const spawnDirectXvfb = async (screen, systemOutputSink) => {
465
795
  installPoolCleanup();
466
796
  for (let displayNumber = firstPooledDisplayNumber; displayNumber <= lastPooledDisplayNumber; displayNumber += 1) {
467
797
  if (!isDisplayNumberAvailable(displayNumber)) {
@@ -478,11 +808,30 @@ const spawnDirectXvfb = async (screen) => {
478
808
  stdio: ["ignore", "pipe", "pipe"]
479
809
  }
480
810
  );
811
+ const xvfb = {
812
+ child,
813
+ display: `:${displayNumber}`,
814
+ displayNumber,
815
+ key: screen,
816
+ lastUsedAt: Date.now(),
817
+ screen,
818
+ stderr,
819
+ stdout,
820
+ systemOutputSink
821
+ };
481
822
  child.stdout.on("data", (chunk) => {
482
823
  appendOutput$1(stdout, chunk);
824
+ appendSystemOutput(xvfb.systemOutputSink, "xvfb", "stdout", chunk);
483
825
  });
484
826
  child.stderr.on("data", (chunk) => {
485
827
  appendOutput$1(stderr, chunk);
828
+ appendSystemOutput(xvfb.systemOutputSink, "xvfb", "stderr", chunk);
829
+ });
830
+ child.stdout.once("end", () => {
831
+ flushSystemOutput(xvfb.systemOutputSink, "xvfb", "stdout");
832
+ });
833
+ child.stderr.once("end", () => {
834
+ flushSystemOutput(xvfb.systemOutputSink, "xvfb", "stderr");
486
835
  });
487
836
  child.unref();
488
837
  unrefHandle(child.stdout);
@@ -505,22 +854,10 @@ const spawnDirectXvfb = async (screen) => {
505
854
  });
506
855
  })
507
856
  ]);
508
- const xvfb = {
509
- child,
510
- display: `:${displayNumber}`,
511
- displayNumber,
512
- key: screen,
513
- lastUsedAt: Date.now(),
514
- screen,
515
- stderr,
516
- stdout
517
- };
518
857
  directXvfbs.add(xvfb);
519
858
  return xvfb;
520
859
  } catch (error) {
521
- killXvfbNow({
522
- child
523
- });
860
+ killXvfbNow(xvfb);
524
861
  leasedDisplayNumbers.delete(displayNumber);
525
862
  const message = error instanceof Error ? error.message : String(error);
526
863
  if (message.includes("ENOENT")) {
@@ -548,22 +885,24 @@ const terminateXvfb = async (xvfb) => {
548
885
  await delay(25);
549
886
  }
550
887
  }
888
+ xvfb.systemOutputSink = void 0;
551
889
  leasedDisplayNumbers.delete(xvfb.displayNumber);
552
890
  };
553
- const leaseXvfb = async (screen) => {
891
+ const leaseXvfb = async (screen, systemOutputSink) => {
554
892
  for (; ; ) {
555
893
  const idle = popArrayEntry(idleXvfbByKey, screen);
556
894
  if (idle === void 0) {
557
- return spawnDirectXvfb(screen);
895
+ return spawnDirectXvfb(screen, systemOutputSink);
558
896
  }
559
897
  if (idle.child.exitCode === null && idle.child.signalCode === null) {
560
898
  idle.lastUsedAt = Date.now();
899
+ idle.systemOutputSink = systemOutputSink;
561
900
  return idle;
562
901
  }
563
902
  await terminateXvfb(idle);
564
903
  }
565
904
  };
566
- const runXvfbProbe = (xvfb) => new Promise((resolveProbe, rejectProbe) => {
905
+ const runXvfbProbeOnce = (xvfb, timeoutMs) => new Promise((resolveProbe, rejectProbe) => {
567
906
  const probePath = resolveXvfbPoolProbePath();
568
907
  const stdout = [];
569
908
  const stderr = [];
@@ -583,12 +922,32 @@ const runXvfbProbe = (xvfb) => new Promise((resolveProbe, rejectProbe) => {
583
922
  env,
584
923
  stdio: ["ignore", "pipe", "pipe"]
585
924
  });
586
- const timeout = setTimeout(() => {
925
+ let settled = false;
926
+ let timeout;
927
+ const rejectOnce = (error) => {
928
+ if (settled) {
929
+ return;
930
+ }
931
+ settled = true;
932
+ if (timeout !== void 0) {
933
+ clearTimeout(timeout);
934
+ }
935
+ rejectProbe(error);
936
+ };
937
+ const resolveOnce = (result) => {
938
+ if (settled) {
939
+ return;
940
+ }
941
+ settled = true;
942
+ if (timeout !== void 0) {
943
+ clearTimeout(timeout);
944
+ }
945
+ resolveProbe(result);
946
+ };
947
+ timeout = setTimeout(() => {
587
948
  child.kill("SIGKILL");
588
- rejectProbe(
589
- createGtkOperationFailedError("Timed out probing Xvfb pool.")
590
- );
591
- }, xvfbPoolProbeTimeoutMs);
949
+ rejectOnce(createXvfbProbeError("Timed out probing Xvfb pool.", false));
950
+ }, timeoutMs);
592
951
  child.stdout.on("data", (chunk) => {
593
952
  appendOutput$1(stdout, chunk);
594
953
  });
@@ -596,42 +955,71 @@ const runXvfbProbe = (xvfb) => new Promise((resolveProbe, rejectProbe) => {
596
955
  appendOutput$1(stderr, chunk);
597
956
  });
598
957
  child.once("error", (error) => {
599
- clearTimeout(timeout);
600
- rejectProbe(error);
958
+ rejectOnce(error);
601
959
  });
602
960
  child.once("exit", (code, signal) => {
603
- clearTimeout(timeout);
604
961
  if (code !== 0) {
605
- rejectProbe(
606
- createGtkOperationFailedError(
962
+ const stderrText = stderr.join("");
963
+ rejectOnce(
964
+ createXvfbProbeError(
607
965
  `Xvfb pool probe failed: code=${String(code)}, signal=${String(
608
966
  signal
609
- )}` + formatOutputTail(stdout, stderr)
967
+ )}` + formatOutputTail(stdout, stderr),
968
+ isRetryableXvfbProbeExit(stderrText)
610
969
  )
611
970
  );
612
971
  return;
613
972
  }
614
973
  const output = stdout.join("").trim().split("\n").at(-1);
615
974
  if (output === void 0 || output.length === 0) {
616
- rejectProbe(
617
- createGtkOperationFailedError(
618
- "Xvfb pool probe did not return a result." + formatOutputTail(stdout, stderr)
975
+ rejectOnce(
976
+ createXvfbProbeError(
977
+ "Xvfb pool probe did not return a result." + formatOutputTail(stdout, stderr),
978
+ false
619
979
  )
620
980
  );
621
981
  return;
622
982
  }
623
983
  try {
624
- resolveProbe(JSON.parse(output));
984
+ resolveOnce(JSON.parse(output));
625
985
  } catch (error) {
626
986
  const message = error instanceof Error ? error.message : String(error);
627
- rejectProbe(
628
- createGtkOperationFailedError(
629
- `Xvfb pool probe returned invalid JSON: ${message}` + formatOutputTail(stdout, stderr)
987
+ rejectOnce(
988
+ createXvfbProbeError(
989
+ `Xvfb pool probe returned invalid JSON: ${message}` + formatOutputTail(stdout, stderr),
990
+ false
630
991
  )
631
992
  );
632
993
  }
633
994
  });
634
995
  });
996
+ const runXvfbProbe = async (xvfb) => {
997
+ const startedAt = Date.now();
998
+ let lastError;
999
+ while (Date.now() - startedAt < xvfbPoolProbeTimeoutMs) {
1000
+ const remainingTimeoutMs = Math.max(
1001
+ 1,
1002
+ xvfbPoolProbeTimeoutMs - (Date.now() - startedAt)
1003
+ );
1004
+ try {
1005
+ return await runXvfbProbeOnce(xvfb, remainingTimeoutMs);
1006
+ } catch (error) {
1007
+ lastError = error;
1008
+ if (!isRetryableXvfbProbeError(error)) {
1009
+ throw error;
1010
+ }
1011
+ }
1012
+ const remainingDelayMs = xvfbPoolProbeTimeoutMs - (Date.now() - startedAt);
1013
+ if (remainingDelayMs <= 0) {
1014
+ break;
1015
+ }
1016
+ await delay(Math.min(xvfbPoolProbeRetryIntervalMs, remainingDelayMs));
1017
+ }
1018
+ if (lastError instanceof Error) {
1019
+ throw lastError;
1020
+ }
1021
+ throw createGtkOperationFailedError("Timed out probing Xvfb pool.");
1022
+ };
635
1023
  const cleanCheckXvfb = async (xvfb, allowedMappedWindowCount) => {
636
1024
  try {
637
1025
  const probe = await runXvfbProbe(xvfb);
@@ -650,7 +1038,7 @@ const returnXvfbToPool = async (xvfb, limits) => {
650
1038
  };
651
1039
  const allPoolKey = (xvfb) => `${xvfb.screen}
652
1040
  ${xvfb.trayHost ? "tray" : "no-tray"}`;
653
- const spawnDriverProcess = (driverPath, socketPath, effective, xvfb) => {
1041
+ const spawnDriverProcess = (driverPath, socketPath, effective, xvfb, systemOutputSink) => {
654
1042
  const driverArgs = [
655
1043
  "--socket",
656
1044
  socketPath,
@@ -683,14 +1071,46 @@ const spawnDriverProcess = (driverPath, socketPath, effective, xvfb) => {
683
1071
  env,
684
1072
  stdio: ["ignore", "pipe", "pipe"]
685
1073
  });
1074
+ const processState = {
1075
+ child,
1076
+ commandLine: [command.bin, ...command.args].join(" "),
1077
+ stderr,
1078
+ stdout,
1079
+ systemOutputSink
1080
+ };
686
1081
  child.stdout.on("data", (chunk) => {
687
1082
  appendOutput$1(stdout, chunk);
1083
+ appendSystemOutput(
1084
+ processState.systemOutputSink,
1085
+ "launcher-driver",
1086
+ "stdout",
1087
+ chunk
1088
+ );
688
1089
  });
689
1090
  child.stderr.on("data", (chunk) => {
690
1091
  appendOutput$1(stderr, chunk);
1092
+ appendSystemOutput(
1093
+ processState.systemOutputSink,
1094
+ "launcher-driver",
1095
+ "stderr",
1096
+ chunk
1097
+ );
1098
+ });
1099
+ child.stdout.once("end", () => {
1100
+ flushSystemOutput(
1101
+ processState.systemOutputSink,
1102
+ "launcher-driver",
1103
+ "stdout"
1104
+ );
1105
+ });
1106
+ child.stderr.once("end", () => {
1107
+ flushSystemOutput(
1108
+ processState.systemOutputSink,
1109
+ "launcher-driver",
1110
+ "stderr"
1111
+ );
691
1112
  });
692
- const commandLine = [command.bin, ...command.args].join(" ");
693
- return { child, commandLine, stderr, stdout };
1113
+ return processState;
694
1114
  };
695
1115
  const listenOnSocket = (server, socketPath) => new Promise((resolveListen, rejectListen) => {
696
1116
  const rejectFromError = (error) => {
@@ -769,7 +1189,13 @@ const waitForDriverReady = (server, processState) => new Promise((resolveReady,
769
1189
  const line = input.slice(0, newlineIndex);
770
1190
  input = input.slice(newlineIndex + 1);
771
1191
  try {
772
- if (parseReadyMessage(line) !== void 0) {
1192
+ const message = JSON.parse(line);
1193
+ if (isDriverEventMessage(message)) {
1194
+ const event = message;
1195
+ if (event.channel === "system.output") {
1196
+ routeWireSystemOutput(processState.systemOutputSink, event.value);
1197
+ }
1198
+ } else if (isRecord(message) && message.type === "ready" && parseReadyMessage(line) !== void 0) {
773
1199
  if (settled) {
774
1200
  return;
775
1201
  }
@@ -847,6 +1273,7 @@ const decodeCapture = (capture) => ({
847
1273
  visibleBounds: capture.visibleBounds
848
1274
  });
849
1275
  const createDriverSession = (socket, bufferedInput, processState, tempDirectory, poolOptions) => {
1276
+ const eventHandlers = /* @__PURE__ */ new Map();
850
1277
  const pending = /* @__PURE__ */ new Map();
851
1278
  let input = bufferedInput;
852
1279
  let nextRequestId = 1;
@@ -866,8 +1293,34 @@ const createDriverSession = (socket, bufferedInput, processState, tempDirectory,
866
1293
  closed = true;
867
1294
  rejectPending(createGtkAppExitedError("Launcher driver has exited."));
868
1295
  };
1296
+ const handleEvent = (event) => {
1297
+ if (event.channel === "system.output") {
1298
+ routeWireSystemOutput(processState.systemOutputSink, event.value);
1299
+ return;
1300
+ }
1301
+ const handlers = eventHandlers.get(
1302
+ createDriverEventKey(event.channel, event.scopeId)
1303
+ );
1304
+ if (handlers === void 0) {
1305
+ return;
1306
+ }
1307
+ for (const handler of handlers) {
1308
+ try {
1309
+ handler(event.value);
1310
+ } catch (error) {
1311
+ queueMicrotask(() => {
1312
+ throw error;
1313
+ });
1314
+ }
1315
+ }
1316
+ };
869
1317
  const handleResponse = (line) => {
870
- const response = JSON.parse(line);
1318
+ const message = JSON.parse(line);
1319
+ if (isDriverEventMessage(message)) {
1320
+ handleEvent(message);
1321
+ return;
1322
+ }
1323
+ const response = message;
871
1324
  const entry = pending.get(response.id);
872
1325
  if (entry === void 0) {
873
1326
  return;
@@ -942,6 +1395,28 @@ const createDriverSession = (socket, bufferedInput, processState, tempDirectory,
942
1395
  });
943
1396
  });
944
1397
  };
1398
+ const subscribe = (channel, scopeId, handler) => {
1399
+ const key = createDriverEventKey(channel, scopeId);
1400
+ const handlers = eventHandlers.get(key) ?? /* @__PURE__ */ new Set();
1401
+ handlers.add(handler);
1402
+ eventHandlers.set(key, handlers);
1403
+ let disposed = false;
1404
+ return {
1405
+ dispose: () => {
1406
+ if (disposed) {
1407
+ return;
1408
+ }
1409
+ disposed = true;
1410
+ handlers.delete(handler);
1411
+ if (handlers.size === 0) {
1412
+ eventHandlers.delete(key);
1413
+ }
1414
+ }
1415
+ };
1416
+ };
1417
+ const setSystemOutputSink = (sink) => {
1418
+ processState.systemOutputSink = sink;
1419
+ };
945
1420
  const waitForExit = async () => {
946
1421
  const startedAt = Date.now();
947
1422
  while (processState.child.exitCode === null && processState.child.signalCode === null) {
@@ -1026,10 +1501,10 @@ const createDriverSession = (socket, bufferedInput, processState, tempDirectory,
1026
1501
  poolOptions.limits
1027
1502
  );
1028
1503
  };
1029
- session = { release, request, terminate };
1504
+ session = { release, request, setSystemOutputSink, subscribe, terminate };
1030
1505
  return session;
1031
1506
  };
1032
- const startFreshDriverSession = async (effective, xvfb, poolOptions) => {
1507
+ const startFreshDriverSession = async (effective, xvfb, poolOptions, systemOutputSink) => {
1033
1508
  const driverPath = resolveDriverPath();
1034
1509
  const tempDirectory = mkdtempSync(
1035
1510
  join(tmpdir(), `gestament-launcher-${process.pid}-${socketCounter}-`)
@@ -1041,7 +1516,13 @@ const startFreshDriverSession = async (effective, xvfb, poolOptions) => {
1041
1516
  try {
1042
1517
  await listenOnSocket(server, socketPath);
1043
1518
  server.unref();
1044
- processState = spawnDriverProcess(driverPath, socketPath, effective, xvfb);
1519
+ processState = spawnDriverProcess(
1520
+ driverPath,
1521
+ socketPath,
1522
+ effective,
1523
+ xvfb,
1524
+ systemOutputSink
1525
+ );
1045
1526
  const connection = await waitForDriverReady(server, processState);
1046
1527
  server.close();
1047
1528
  const resolvedPoolOptions = poolOptions.mode === "all" && xvfb !== void 0 ? {
@@ -1067,7 +1548,7 @@ const startFreshDriverSession = async (effective, xvfb, poolOptions) => {
1067
1548
  throw error;
1068
1549
  }
1069
1550
  };
1070
- const startDriverSession = async (options) => {
1551
+ const startDriverSession = async (options, systemOutputSink) => {
1071
1552
  const display = resolveDisplay(options.display);
1072
1553
  const xvfbOptions = resolveXvfbOptions(options);
1073
1554
  const effective = resolveEffectiveDisplay(display, xvfbOptions);
@@ -1075,7 +1556,8 @@ const startDriverSession = async (options) => {
1075
1556
  return startFreshDriverSession(
1076
1557
  effective,
1077
1558
  void 0,
1078
- emptyDriverSessionPoolOptions()
1559
+ emptyDriverSessionPoolOptions(),
1560
+ systemOutputSink
1079
1561
  );
1080
1562
  }
1081
1563
  const pool = effective.xvfb.pool;
@@ -1083,18 +1565,24 @@ const startDriverSession = async (options) => {
1083
1565
  return startFreshDriverSession(
1084
1566
  effective,
1085
1567
  void 0,
1086
- emptyDriverSessionPoolOptions()
1568
+ emptyDriverSessionPoolOptions(),
1569
+ systemOutputSink
1087
1570
  );
1088
1571
  }
1089
1572
  if (pool.type === "xvfb") {
1090
- const xvfb2 = await leaseXvfb(effective.xvfb.screen);
1091
- return startFreshDriverSession(effective, xvfb2, {
1092
- allKey: void 0,
1093
- allowedMappedWindowCount: 0,
1094
- limits: poolLimits(pool),
1095
- mode: "xvfb",
1096
- xvfb: xvfb2
1097
- });
1573
+ const xvfb2 = await leaseXvfb(effective.xvfb.screen, systemOutputSink);
1574
+ return startFreshDriverSession(
1575
+ effective,
1576
+ xvfb2,
1577
+ {
1578
+ allKey: void 0,
1579
+ allowedMappedWindowCount: 0,
1580
+ limits: poolLimits(pool),
1581
+ mode: "xvfb",
1582
+ xvfb: xvfb2
1583
+ },
1584
+ systemOutputSink
1585
+ );
1098
1586
  }
1099
1587
  const key = allPoolKey(effective.xvfb);
1100
1588
  for (; ; ) {
@@ -1105,20 +1593,27 @@ const startDriverSession = async (options) => {
1105
1593
  if (idle.xvfb.child.exitCode === null && idle.xvfb.child.signalCode === null) {
1106
1594
  idle.lastUsedAt = Date.now();
1107
1595
  idle.xvfb.lastUsedAt = idle.lastUsedAt;
1596
+ idle.xvfb.systemOutputSink = systemOutputSink;
1597
+ idle.session.setSystemOutputSink(systemOutputSink);
1108
1598
  return idle.session;
1109
1599
  }
1110
1600
  await idle.session.terminate().catch(() => void 0);
1111
1601
  }
1112
- const xvfb = await leaseXvfb(effective.xvfb.screen);
1113
- return startFreshDriverSession(effective, xvfb, {
1114
- allKey: key,
1115
- allowedMappedWindowCount: 0,
1116
- limits: poolLimits(pool),
1117
- mode: "all",
1118
- xvfb
1119
- });
1602
+ const xvfb = await leaseXvfb(effective.xvfb.screen, systemOutputSink);
1603
+ return startFreshDriverSession(
1604
+ effective,
1605
+ xvfb,
1606
+ {
1607
+ allKey: key,
1608
+ allowedMappedWindowCount: 0,
1609
+ limits: poolLimits(pool),
1610
+ mode: "all",
1611
+ xvfb
1612
+ },
1613
+ systemOutputSink
1614
+ );
1120
1615
  };
1121
- const createLaunchPayload = (options, args) => {
1616
+ const createLaunchPayload = (options, args, launchOptions, outputScopeId) => {
1122
1617
  const display = resolveDisplay(options.display);
1123
1618
  const xvfb = resolveXvfbOptions(options);
1124
1619
  const effective = resolveEffectiveDisplay(display, xvfb);
@@ -1126,6 +1621,11 @@ const createLaunchPayload = (options, args) => {
1126
1621
  appPath: options.appPath,
1127
1622
  args: [...options.args ?? [], ...args],
1128
1623
  env: resolveLauncherEnvironment(options, effective),
1624
+ outputBufferBytes: resolveOutputBufferBytes(
1625
+ options.outputBufferBytes,
1626
+ launchOptions?.outputBufferBytes
1627
+ ),
1628
+ outputScopeId,
1129
1629
  timeoutMs: options.timeoutMs ?? null
1130
1630
  };
1131
1631
  };
@@ -1139,7 +1639,7 @@ const createEnvironmentPayload = (options) => {
1139
1639
  };
1140
1640
  const elementRefToProxy = (session, ref) => ref === null ? void 0 : createProxyGtkElement(session, ref);
1141
1641
  const trayRefToProxy = (session, ref) => ref === null ? void 0 : createProxyGtkTrayItem(session, ref);
1142
- const createProxyGtkApp = (session, ref) => {
1642
+ const createProxyGtkApp = (session, ref, outputSubscription) => {
1143
1643
  let released = false;
1144
1644
  const assertNotReleased = () => {
1145
1645
  if (released) {
@@ -1158,6 +1658,7 @@ const createProxyGtkApp = (session, ref) => {
1158
1658
  return;
1159
1659
  }
1160
1660
  released = true;
1661
+ outputSubscription?.dispose();
1161
1662
  await session.request("app.release", { appId: ref.appId });
1162
1663
  };
1163
1664
  const app = {
@@ -1165,6 +1666,7 @@ const createProxyGtkApp = (session, ref) => {
1165
1666
  environment: async () => wireEnvironmentToGtkAppEnvironment(
1166
1667
  await appRequest("app.environment")
1167
1668
  ),
1669
+ output: () => appRequest("app.output"),
1168
1670
  findById: async (id) => elementRefToProxy(
1169
1671
  session,
1170
1672
  await appRequest("app.findById", { id })
@@ -1392,19 +1894,72 @@ const createProxyGtkTrayItem = (session, ref) => {
1392
1894
  };
1393
1895
  };
1394
1896
  const createDriverBackedGtkAppLauncher = (options) => {
1897
+ const outputSubscriptions = /* @__PURE__ */ new Set();
1898
+ let systemOutputRecorder = createGtkSystemOutputRecorder(
1899
+ normalizeOutputBufferBytes(
1900
+ options.systemOutputBufferBytes,
1901
+ "systemOutputBufferBytes"
1902
+ )
1903
+ );
1395
1904
  let sessionPromise;
1396
1905
  const ensureSession = () => {
1397
- sessionPromise ??= startDriverSession(options);
1906
+ if (sessionPromise === void 0) {
1907
+ systemOutputRecorder = createGtkSystemOutputRecorder(
1908
+ normalizeOutputBufferBytes(
1909
+ options.systemOutputBufferBytes,
1910
+ "systemOutputBufferBytes"
1911
+ )
1912
+ );
1913
+ sessionPromise = startDriverSession(
1914
+ options,
1915
+ createSystemOutputSink(systemOutputRecorder, options.onSystemOutput)
1916
+ );
1917
+ }
1398
1918
  return sessionPromise;
1399
1919
  };
1400
- const launch = async (args) => {
1401
- const payload = createLaunchPayload(options, args ?? []);
1920
+ const trackOutputSubscription = (subscription) => {
1921
+ let tracked;
1922
+ tracked = {
1923
+ dispose: () => {
1924
+ if (!outputSubscriptions.delete(tracked)) {
1925
+ return;
1926
+ }
1927
+ subscription.dispose();
1928
+ }
1929
+ };
1930
+ outputSubscriptions.add(tracked);
1931
+ return tracked;
1932
+ };
1933
+ const disposeOutputSubscriptions = () => {
1934
+ for (const subscription of [...outputSubscriptions]) {
1935
+ subscription.dispose();
1936
+ }
1937
+ };
1938
+ const launch = async (args, launchOptions) => {
1402
1939
  const session = await ensureSession();
1403
- const appRef = await session.request(
1404
- "launcher.launch",
1405
- payload
1940
+ const onOutput = launchOptions?.onOutput;
1941
+ const outputScopeId = onOutput === void 0 ? null : createOutputScopeId();
1942
+ const outputSubscription = outputScopeId === null ? void 0 : trackOutputSubscription(
1943
+ session.subscribe("app.output", outputScopeId, (value) => {
1944
+ onOutput?.(value);
1945
+ })
1946
+ );
1947
+ const payload = createLaunchPayload(
1948
+ options,
1949
+ args ?? [],
1950
+ launchOptions,
1951
+ outputScopeId
1406
1952
  );
1407
- return createProxyGtkApp(session, appRef);
1953
+ try {
1954
+ const appRef = await session.request(
1955
+ "launcher.launch",
1956
+ payload
1957
+ );
1958
+ return createProxyGtkApp(session, appRef, outputSubscription);
1959
+ } catch (error) {
1960
+ outputSubscription?.dispose();
1961
+ throw error;
1962
+ }
1408
1963
  };
1409
1964
  const environment = async () => {
1410
1965
  const payload = createEnvironmentPayload(options);
@@ -1416,6 +1971,7 @@ const createDriverBackedGtkAppLauncher = (options) => {
1416
1971
  )
1417
1972
  );
1418
1973
  };
1974
+ const systemOutput = () => Promise.resolve(systemOutputRecorder.snapshot());
1419
1975
  const release = async () => {
1420
1976
  const releasingSession = sessionPromise;
1421
1977
  sessionPromise = void 0;
@@ -1423,12 +1979,17 @@ const createDriverBackedGtkAppLauncher = (options) => {
1423
1979
  return;
1424
1980
  }
1425
1981
  const session = await releasingSession;
1426
- await session.release();
1982
+ try {
1983
+ await session.release();
1984
+ } finally {
1985
+ disposeOutputSubscriptions();
1986
+ }
1427
1987
  };
1428
1988
  return {
1429
1989
  environment,
1430
1990
  launch,
1431
1991
  release,
1992
+ systemOutput,
1432
1993
  [Symbol.asyncDispose]: release
1433
1994
  };
1434
1995
  };
@@ -2340,31 +2901,70 @@ const createGtkAppEnvironment = (baseEnv, overrides) => {
2340
2901
  const launchGtkApp = (appPath, args, options) => {
2341
2902
  const _args = args ?? [];
2342
2903
  const _timeoutMs = options?.timeoutMs ?? 1e4;
2904
+ const outputBufferBytes = normalizeOutputBufferBytes(
2905
+ options?.outputBufferBytes
2906
+ );
2907
+ const outputRecorder = createGtkAppOutputRecorder(outputBufferBytes);
2343
2908
  const appEnvironment = createGtkAppEnvironment(process.env, options?.env);
2344
2909
  const child = spawn(appPath, [..._args], {
2345
2910
  env: appEnvironment,
2346
2911
  stdio: "pipe"
2347
2912
  });
2913
+ let resolveClosed;
2914
+ const closedPromise = new Promise((resolve2) => {
2915
+ resolveClosed = resolve2;
2916
+ });
2348
2917
  const state = {
2349
2918
  atspiReadiness: "missing-bus-name",
2350
2919
  atspiReady: false,
2920
+ closed: false,
2921
+ closedPromise,
2351
2922
  exitCode: null,
2352
2923
  exitSignal: null,
2353
2924
  process: child,
2925
+ released: false,
2354
2926
  stderr: [],
2355
2927
  stdout: []
2356
2928
  };
2929
+ const notifyOutput = (stream, chunk) => {
2930
+ notifyGtkAppOutput(options?.onOutput, outputRecorder.append(stream, chunk));
2931
+ };
2932
+ const flushOutput = (stream) => {
2933
+ notifyGtkAppOutput(options?.onOutput, outputRecorder.flush(stream));
2934
+ };
2357
2935
  child.stdout.on("data", (chunk) => {
2358
2936
  appendOutput(state.stdout, chunk);
2937
+ notifyOutput("stdout", chunk);
2359
2938
  });
2360
2939
  child.stderr.on("data", (chunk) => {
2361
2940
  appendOutput(state.stderr, chunk);
2941
+ notifyOutput("stderr", chunk);
2942
+ });
2943
+ child.stdout.on("end", () => {
2944
+ flushOutput("stdout");
2945
+ });
2946
+ child.stderr.on("end", () => {
2947
+ flushOutput("stderr");
2362
2948
  });
2363
2949
  child.on("exit", (code, signal) => {
2364
2950
  state.exitCode = code;
2365
2951
  state.exitSignal = signal;
2366
2952
  });
2953
+ child.on("close", (code, signal) => {
2954
+ if (state.exitCode === null && state.exitSignal === null) {
2955
+ state.exitCode = code;
2956
+ state.exitSignal = signal;
2957
+ }
2958
+ flushOutput("stdout");
2959
+ flushOutput("stderr");
2960
+ state.closed = true;
2961
+ resolveClosed?.();
2962
+ });
2367
2963
  const release = async () => {
2964
+ if (state.released) {
2965
+ return;
2966
+ }
2967
+ state.released = true;
2368
2968
  if (state.exitCode !== null || state.exitSignal !== null) {
2369
2969
  return;
2370
2970
  }
@@ -2378,6 +2978,11 @@ const launchGtkApp = (appPath, args, options) => {
2378
2978
  await delay$1(25);
2379
2979
  }
2380
2980
  };
2981
+ const assertNotReleased = () => {
2982
+ if (state.released) {
2983
+ throw createGtkAppExitedError("GTK application has been released.");
2984
+ }
2985
+ };
2381
2986
  const findById = async (id) => {
2382
2987
  const startedAt = Date.now();
2383
2988
  const timeoutMs = effectiveWaitTimeoutMs(_timeoutMs);
@@ -2485,6 +3090,7 @@ const launchGtkApp = (appPath, args, options) => {
2485
3090
  };
2486
3091
  const app = {
2487
3092
  capture: async () => {
3093
+ assertNotReleased();
2488
3094
  assertProcessRunning(state, appPath);
2489
3095
  try {
2490
3096
  return nativeCaptureScreen();
@@ -2493,9 +3099,20 @@ const launchGtkApp = (appPath, args, options) => {
2493
3099
  }
2494
3100
  },
2495
3101
  environment: async () => {
3102
+ assertNotReleased();
2496
3103
  assertProcessRunning(state, appPath);
2497
3104
  return { ...appEnvironment };
2498
3105
  },
3106
+ output: async () => {
3107
+ assertNotReleased();
3108
+ if ((state.exitCode !== null || state.exitSignal !== null) && !state.closed) {
3109
+ await state.closedPromise;
3110
+ }
3111
+ return outputRecorder.snapshot(
3112
+ state.exitCode,
3113
+ state.exitSignal === null ? null : state.exitSignal
3114
+ );
3115
+ },
2499
3116
  findById,
2500
3117
  findByPath,
2501
3118
  getById,
@@ -2564,4 +3181,4 @@ export {
2564
3181
  createGtkAppEnvironment as c,
2565
3182
  launchGtkApp as l
2566
3183
  };
2567
- //# sourceMappingURL=launchGtkApp-Bst1BFbD.js.map
3184
+ //# sourceMappingURL=launchGtkApp-EI6OIpPI.js.map