@gjsify/child_process 0.4.0 → 0.4.3

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/src/index.ts DELETED
@@ -1,537 +0,0 @@
1
- // Node.js child_process module for GJS
2
- // Uses Gio.Subprocess for process spawning
3
- // Reference: Node.js lib/child_process.js
4
-
5
- import Gio from '@girs/gio-2.0';
6
- import GLib from '@girs/glib-2.0';
7
- import { EventEmitter } from 'node:events';
8
- import { Buffer } from 'node:buffer';
9
- import { Readable } from 'node:stream';
10
- import type { Writable } from 'node:stream';
11
- import { gbytesToUint8Array, deferEmit, ensureMainLoop } from '@gjsify/utils';
12
-
13
- // Wraps a Gio.InputStream as a Node.js Readable so proc.stdout/stderr work
14
- // with standard stream consumers (.on('data', ...), pipe, async iteration).
15
- class GioInputStreamReadable extends Readable {
16
- private _stream: Gio.InputStream;
17
- private _cancellable = new Gio.Cancellable();
18
-
19
- constructor(stream: Gio.InputStream) {
20
- super();
21
- this._stream = stream;
22
- }
23
-
24
- override _read(size: number): void {
25
- this._stream.read_bytes_async(
26
- Math.max(size, 4096),
27
- GLib.PRIORITY_DEFAULT,
28
- this._cancellable,
29
- (_source, result) => {
30
- try {
31
- const gbytes = this._stream.read_bytes_finish(result);
32
- const data = gbytes.get_data();
33
- if (!data || data.length === 0) {
34
- this.push(null);
35
- } else {
36
- this.push(Buffer.from(data));
37
- }
38
- } catch (err) {
39
- if (!this._cancellable.is_cancelled()) {
40
- this.destroy(err as Error);
41
- }
42
- }
43
- },
44
- );
45
- }
46
-
47
- override _destroy(error: Error | null, callback: (err?: Error | null) => void): void {
48
- this._cancellable.cancel();
49
- callback(error);
50
- }
51
- }
52
-
53
- interface ExecError extends Error {
54
- status?: number;
55
- code?: number | string;
56
- killed?: boolean;
57
- stdout?: string;
58
- stderr?: string;
59
- }
60
-
61
- export interface ExecOptions {
62
- cwd?: string;
63
- env?: Record<string, string>;
64
- encoding?: BufferEncoding | 'buffer';
65
- shell?: string | boolean;
66
- timeout?: number;
67
- maxBuffer?: number;
68
- killSignal?: string | number;
69
- uid?: number;
70
- gid?: number;
71
- windowsHide?: boolean;
72
- }
73
-
74
- export interface ExecSyncOptions {
75
- cwd?: string;
76
- env?: Record<string, string>;
77
- encoding?: BufferEncoding | 'buffer';
78
- shell?: string | boolean;
79
- timeout?: number;
80
- maxBuffer?: number;
81
- killSignal?: string | number;
82
- stdio?: string | string[];
83
- input?: string | Buffer | Uint8Array;
84
- }
85
-
86
- export interface SpawnOptions {
87
- cwd?: string;
88
- env?: Record<string, string>;
89
- stdio?: string | string[];
90
- shell?: string | boolean;
91
- timeout?: number;
92
- killSignal?: string | number;
93
- /**
94
- * Allows aborting the child process via an `AbortController`. When the
95
- * signal fires, the child is killed with `killSignal` (default `SIGTERM`)
96
- * and an `error` event with `name: 'AbortError'` is emitted.
97
- * Reference: https://nodejs.org/api/child_process.html#child_processspawncommand-args-options
98
- */
99
- signal?: AbortSignal;
100
- }
101
-
102
- export interface SpawnSyncResult {
103
- pid: number;
104
- output: (Buffer | string | null)[];
105
- stdout: Buffer | string;
106
- stderr: Buffer | string;
107
- status: number | null;
108
- signal: string | null;
109
- error?: Error;
110
- }
111
-
112
- // GC guard — GJS garbage-collects objects with no JS references.
113
- // Keep strong references to active child processes to prevent their
114
- // Gio.Subprocess from being collected while async operations are pending.
115
- const _activeProcesses = new Set<ChildProcess>();
116
-
117
- /**
118
- * ChildProcess — EventEmitter wrapping Gio.Subprocess.
119
- */
120
- export class ChildProcess extends EventEmitter {
121
- pid?: number;
122
- exitCode: number | null = null;
123
- signalCode: string | null = null;
124
- killed = false;
125
- connected = false;
126
- stdin: Writable | null = null;
127
- stdout: Readable | null = null;
128
- stderr: Readable | null = null;
129
-
130
- private _subprocess: Gio.Subprocess | null = null;
131
-
132
- /** @internal Set the underlying Gio.Subprocess and extract PID. */
133
- _setSubprocess(proc: Gio.Subprocess): void {
134
- this._subprocess = proc;
135
- const pid = proc.get_identifier();
136
- if (pid) this.pid = parseInt(pid, 10);
137
- }
138
-
139
- /** Send a signal to the child process. */
140
- kill(signal?: string | number): boolean {
141
- if (!this._subprocess) return false;
142
- try {
143
- if (signal === 'SIGKILL' || signal === 9) {
144
- this._subprocess.force_exit();
145
- } else {
146
- this._subprocess.send_signal(typeof signal === 'number' ? signal : 15);
147
- }
148
- this.killed = true;
149
- return true;
150
- } catch {
151
- return false;
152
- }
153
- }
154
-
155
- ref(): this { return this; }
156
- unref(): this { return this; }
157
- }
158
-
159
- // Create a Gio.Subprocess with cwd and env support via SubprocessLauncher.
160
- function _spawnSubprocess(
161
- argv: string[],
162
- flags: Gio.SubprocessFlags,
163
- options?: { cwd?: string; env?: Record<string, string> }
164
- ): Gio.Subprocess {
165
- const launcher = new Gio.SubprocessLauncher({ flags });
166
- if (options?.cwd) {
167
- launcher.set_cwd(options.cwd);
168
- }
169
- if (options?.env) {
170
- for (const [key, value] of Object.entries(options.env)) {
171
- launcher.setenv(key, value, true);
172
- }
173
- }
174
- return launcher.spawnv(argv);
175
- }
176
-
177
- // Execute a command in a shell and buffer the output (sync).
178
- export function execSync(command: string, options?: ExecSyncOptions): Buffer | string {
179
- const encoding = options?.encoding;
180
- const input = options?.input;
181
-
182
- const flags = Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
183
- | (input ? Gio.SubprocessFlags.STDIN_PIPE : Gio.SubprocessFlags.NONE);
184
-
185
- const shell = typeof options?.shell === 'string' ? options.shell : '/bin/sh';
186
- const proc = _spawnSubprocess([shell, '-c', command], flags, options);
187
-
188
- const stdinBytes = input
189
- ? new GLib.Bytes(typeof input === 'string' ? new TextEncoder().encode(input) : input)
190
- : null;
191
-
192
- const [, stdoutBytes, stderrBytes] = proc.communicate(stdinBytes, null);
193
-
194
- const status = proc.get_exit_status();
195
- if (status !== 0) {
196
- const stderrStr = stderrBytes
197
- ? new TextDecoder().decode(gbytesToUint8Array(stderrBytes))
198
- : '';
199
- const error = new Error(`Command failed: ${command}\n${stderrStr}`) as ExecError;
200
- error.status = status;
201
- error.stderr = stderrStr;
202
- error.stdout = stdoutBytes
203
- ? new TextDecoder().decode(gbytesToUint8Array(stdoutBytes))
204
- : '';
205
- throw error;
206
- }
207
-
208
- if (!stdoutBytes) return encoding && encoding !== 'buffer' ? '' : Buffer.alloc(0);
209
- const data = gbytesToUint8Array(stdoutBytes);
210
- if (encoding && encoding !== 'buffer') {
211
- return new TextDecoder().decode(data);
212
- }
213
- return Buffer.from(data);
214
- }
215
-
216
- /**
217
- * Execute a command in a shell (async with callback).
218
- */
219
- function _exec(
220
- command: string,
221
- options?: ExecOptions | ((error: Error | null, stdout: string, stderr: string) => void),
222
- callback?: (error: Error | null, stdout: string, stderr: string) => void,
223
- ): ChildProcess {
224
- if (typeof options === 'function') {
225
- callback = options;
226
- options = {};
227
- }
228
- const opts = (options || {}) as ExecOptions;
229
- const child = new ChildProcess();
230
-
231
- const shell = typeof opts.shell === 'string' ? opts.shell : '/bin/sh';
232
- const flags = Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE;
233
-
234
- try {
235
- const proc = _spawnSubprocess([shell, '-c', command], flags, opts);
236
- child._setSubprocess(proc);
237
- ensureMainLoop();
238
-
239
- proc.communicate_utf8_async(null, null, (_source: Gio.Subprocess | null, result: Gio.AsyncResult) => {
240
- try {
241
- const [, stdout, stderr] = proc.communicate_utf8_finish(result);
242
- const exitStatus = proc.get_exit_status();
243
- child.exitCode = exitStatus;
244
-
245
- if (exitStatus !== 0) {
246
- const error = new Error(`Command failed: ${command}`) as ExecError;
247
- error.code = exitStatus;
248
- error.killed = child.killed;
249
- error.stdout = stdout || '';
250
- error.stderr = stderr || '';
251
- if (callback) callback(error, stdout || '', stderr || '');
252
- } else {
253
- if (callback) callback(null, stdout || '', stderr || '');
254
- }
255
-
256
- child.emit('close', exitStatus, null);
257
- child.emit('exit', exitStatus, null);
258
- } catch (err: unknown) {
259
- const error = err instanceof Error ? err : new Error(String(err));
260
- if (callback) callback(error, '', '');
261
- child.emit('error', error);
262
- }
263
- });
264
- } catch (err: unknown) {
265
- const error = err instanceof Error ? err : new Error(String(err));
266
- setTimeout(() => {
267
- if (callback) callback(error, '', '');
268
- child.emit('error', error);
269
- }, 0);
270
- }
271
-
272
- return child;
273
- }
274
-
275
- export { _exec as exec };
276
-
277
- /**
278
- * Execute a file directly without shell (async).
279
- */
280
- export function execFile(
281
- file: string,
282
- args?: string[] | ((error: Error | null, stdout: string, stderr: string) => void),
283
- options?: ExecOptions | ((error: Error | null, stdout: string, stderr: string) => void),
284
- callback?: (error: Error | null, stdout: string, stderr: string) => void,
285
- ): ChildProcess {
286
- let _args: string[] = [];
287
- let _opts: ExecOptions = {};
288
- let _callback: ((error: Error | null, stdout: string, stderr: string) => void) | undefined;
289
-
290
- if (typeof args === 'function') {
291
- _callback = args;
292
- } else if (Array.isArray(args)) {
293
- _args = args;
294
- if (typeof options === 'function') {
295
- _callback = options;
296
- } else {
297
- _opts = options || {};
298
- _callback = callback;
299
- }
300
- }
301
-
302
- const child = new ChildProcess();
303
- const flags = Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE;
304
-
305
- try {
306
- const proc = _spawnSubprocess([file, ..._args], flags, _opts);
307
- child._setSubprocess(proc);
308
- ensureMainLoop();
309
-
310
- proc.communicate_utf8_async(null, null, (_source: Gio.Subprocess | null, result: Gio.AsyncResult) => {
311
- try {
312
- const [, stdout, stderr] = proc.communicate_utf8_finish(result);
313
- const exitStatus = proc.get_exit_status();
314
- child.exitCode = exitStatus;
315
-
316
- if (exitStatus !== 0) {
317
- const error = new Error(`Command failed: ${file}`) as ExecError;
318
- error.code = exitStatus;
319
- error.stdout = stdout || '';
320
- error.stderr = stderr || '';
321
- if (_callback) _callback(error, stdout || '', stderr || '');
322
- } else {
323
- if (_callback) _callback(null, stdout || '', stderr || '');
324
- }
325
-
326
- child.emit('close', exitStatus, null);
327
- child.emit('exit', exitStatus, null);
328
- } catch (err: unknown) {
329
- const error = err instanceof Error ? err : new Error(String(err));
330
- if (_callback) _callback(error, '', '');
331
- child.emit('error', error);
332
- }
333
- });
334
- } catch (err: unknown) {
335
- const error = err instanceof Error ? err : new Error(String(err));
336
- setTimeout(() => {
337
- if (_callback) _callback(error, '', '');
338
- child.emit('error', error);
339
- }, 0);
340
- }
341
-
342
- return child;
343
- }
344
-
345
- /**
346
- * Execute a file directly without shell (sync).
347
- */
348
- export function execFileSync(file: string, args?: string[], options?: ExecSyncOptions): Buffer | string {
349
- const _args = args || [];
350
- const encoding = options?.encoding;
351
- const input = options?.input;
352
-
353
- const flags = Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
354
- | (input ? Gio.SubprocessFlags.STDIN_PIPE : Gio.SubprocessFlags.NONE);
355
-
356
- const proc = _spawnSubprocess([file, ..._args], flags, options);
357
-
358
- const stdinBytes = input
359
- ? new GLib.Bytes(typeof input === 'string' ? new TextEncoder().encode(input) : input)
360
- : null;
361
-
362
- const [, stdoutBytes, stderrBytes] = proc.communicate(stdinBytes, null);
363
-
364
- const status = proc.get_exit_status();
365
- if (status !== 0) {
366
- const stderrStr = stderrBytes
367
- ? new TextDecoder().decode(gbytesToUint8Array(stderrBytes))
368
- : '';
369
- const error = new Error(`Command failed: ${file} ${_args.join(' ')}`) as ExecError;
370
- error.status = status;
371
- error.stderr = stderrStr;
372
- throw error;
373
- }
374
-
375
- if (!stdoutBytes) return encoding && encoding !== 'buffer' ? '' : Buffer.alloc(0);
376
- const data = gbytesToUint8Array(stdoutBytes);
377
- if (encoding && encoding !== 'buffer') {
378
- return new TextDecoder().decode(data);
379
- }
380
- return Buffer.from(data);
381
- }
382
-
383
- /**
384
- * Spawn a new process (async, with event-based API).
385
- */
386
- export function spawn(command: string, args?: string[], options?: SpawnOptions): ChildProcess {
387
- const _args = args || [];
388
- const child = new ChildProcess();
389
- const useShell = options?.shell;
390
-
391
- let argv: string[];
392
- if (useShell) {
393
- const shell = typeof useShell === 'string' ? useShell : '/bin/sh';
394
- const fullCmd = [command, ..._args].join(' ');
395
- argv = [shell, '-c', fullCmd];
396
- } else {
397
- argv = [command, ..._args];
398
- }
399
-
400
- const flags = Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE | Gio.SubprocessFlags.STDIN_PIPE;
401
-
402
- try {
403
- const proc = _spawnSubprocess(argv, flags, options);
404
- child._setSubprocess(proc);
405
- _activeProcesses.add(child);
406
-
407
- const stdoutPipe = proc.get_stdout_pipe();
408
- if (stdoutPipe) child.stdout = new GioInputStreamReadable(stdoutPipe);
409
-
410
- const stderrPipe = proc.get_stderr_pipe();
411
- if (stderrPipe) child.stderr = new GioInputStreamReadable(stderrPipe);
412
-
413
- // AbortSignal wiring — the documented Node behavior is to kill the
414
- // child on abort (with `killSignal` if provided) and emit an `error`
415
- // event whose `name` is `'AbortError'`. We register `{ once: true }`
416
- // because the listener is no longer interesting after either abort
417
- // OR child-exit, and we explicitly remove it from the exit handler so
418
- // a late abort never fires after the child is already gone.
419
- const abortSignal = options?.signal;
420
- let onAbort: (() => void) | null = null;
421
- const emitAbortError = () => {
422
- const killSig = options?.killSignal ?? 'SIGTERM';
423
- child.kill(killSig);
424
- const err = new Error('The operation was aborted');
425
- err.name = 'AbortError';
426
- child.emit('error', err);
427
- };
428
- if (abortSignal) {
429
- if (abortSignal.aborted) {
430
- // Already aborted before spawn returned — kill + emit AbortError on
431
- // the next microtask so subscribers attached after `spawn()` returns
432
- // still receive the event (matches Node's documented behaviour).
433
- queueMicrotask(emitAbortError);
434
- } else {
435
- onAbort = emitAbortError;
436
- abortSignal.addEventListener('abort', onAbort, { once: true });
437
- }
438
- }
439
-
440
- ensureMainLoop();
441
- proc.wait_async(null, (_source: Gio.Subprocess | null, result: Gio.AsyncResult) => {
442
- try {
443
- proc.wait_finish(result);
444
- const exitStatus = proc.get_if_exited() ? proc.get_exit_status() : null;
445
- const signal = proc.get_if_signaled() ? 'SIGTERM' : null;
446
- child.exitCode = exitStatus;
447
- child.signalCode = signal;
448
- if (abortSignal && onAbort) {
449
- abortSignal.removeEventListener('abort', onAbort);
450
- }
451
- child.emit('exit', exitStatus, signal);
452
- child.emit('close', exitStatus, signal);
453
- } catch (err: unknown) {
454
- child.emit('error', err instanceof Error ? err : new Error(String(err)));
455
- }
456
- _activeProcesses.delete(child);
457
- });
458
-
459
- deferEmit(child, 'spawn');
460
- } catch (err: unknown) {
461
- deferEmit(child, 'error', err instanceof Error ? err : new Error(String(err)));
462
- }
463
-
464
- return child;
465
- }
466
-
467
- /**
468
- * Spawn a new process (sync).
469
- */
470
- export function spawnSync(command: string, args?: string[], options?: ExecSyncOptions): SpawnSyncResult {
471
- const _args = args || [];
472
- const useShell = options?.shell;
473
- const input = options?.input;
474
-
475
- let argv: string[];
476
- if (useShell) {
477
- const shell = typeof useShell === 'string' ? useShell : '/bin/sh';
478
- const fullCmd = [command, ..._args].join(' ');
479
- argv = [shell, '-c', fullCmd];
480
- } else {
481
- argv = [command, ..._args];
482
- }
483
-
484
- const flags = Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
485
- | (input ? Gio.SubprocessFlags.STDIN_PIPE : Gio.SubprocessFlags.NONE);
486
-
487
- try {
488
- const proc = _spawnSubprocess(argv, flags, options);
489
- const pid = proc.get_identifier();
490
-
491
- const stdinBytes = input
492
- ? new GLib.Bytes(typeof input === 'string' ? new TextEncoder().encode(input) : input)
493
- : null;
494
-
495
- const [, stdoutBytes, stderrBytes] = proc.communicate(stdinBytes, null);
496
-
497
- const stdoutBuf = stdoutBytes ? Buffer.from(gbytesToUint8Array(stdoutBytes)) : Buffer.alloc(0);
498
- const stderrBuf = stderrBytes ? Buffer.from(gbytesToUint8Array(stderrBytes)) : Buffer.alloc(0);
499
-
500
- const encoding = options?.encoding;
501
- const stdoutData: Buffer | string = encoding && encoding !== 'buffer' ? new TextDecoder().decode(stdoutBuf) : stdoutBuf;
502
- const stderrData: Buffer | string = encoding && encoding !== 'buffer' ? new TextDecoder().decode(stderrBuf) : stderrBuf;
503
-
504
- const status = proc.get_if_exited() ? proc.get_exit_status() : null;
505
- const signal = proc.get_if_signaled() ? 'SIGTERM' : null;
506
-
507
- return {
508
- pid: pid ? parseInt(pid, 10) : 0,
509
- output: [null, stdoutData, stderrData],
510
- stdout: stdoutData,
511
- stderr: stderrData,
512
- status,
513
- signal,
514
- };
515
- } catch (err: unknown) {
516
- const empty: Buffer | string = options?.encoding && options.encoding !== 'buffer' ? '' : Buffer.alloc(0);
517
- return {
518
- pid: 0,
519
- output: [null, empty, empty],
520
- stdout: empty,
521
- stderr: empty,
522
- status: null,
523
- signal: null,
524
- error: err instanceof Error ? err : new Error(String(err)),
525
- };
526
- }
527
- }
528
-
529
- export default {
530
- ChildProcess,
531
- exec: _exec,
532
- execSync,
533
- execFile,
534
- execFileSync,
535
- spawn,
536
- spawnSync,
537
- };
package/src/test.mts DELETED
@@ -1,6 +0,0 @@
1
-
2
- import { run } from '@gjsify/unit';
3
-
4
- import testSuiteChildProcess from './index.spec.js';
5
-
6
- run({ testSuiteChildProcess });
package/tsconfig.json DELETED
@@ -1,29 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "module": "ESNext",
4
- "target": "ESNext",
5
- "moduleResolution": "bundler",
6
- "types": [
7
- "node"
8
- ],
9
- "experimentalDecorators": true,
10
- "emitDeclarationOnly": true,
11
- "declaration": true,
12
- "allowImportingTsExtensions": true,
13
- "outDir": "lib",
14
- "rootDir": "src",
15
- "declarationDir": "lib/types",
16
- "composite": true,
17
- "skipLibCheck": true,
18
- "allowJs": true,
19
- "checkJs": false,
20
- "strict": false
21
- },
22
- "include": [
23
- "src/**/*.ts"
24
- ],
25
- "exclude": [
26
- "src/test.ts",
27
- "src/test.mts"
28
- ]
29
- }