@gjsify/child_process 0.3.21 → 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,500 +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
-
95
- export interface SpawnSyncResult {
96
- pid: number;
97
- output: (Buffer | string | null)[];
98
- stdout: Buffer | string;
99
- stderr: Buffer | string;
100
- status: number | null;
101
- signal: string | null;
102
- error?: Error;
103
- }
104
-
105
- // GC guard — GJS garbage-collects objects with no JS references.
106
- // Keep strong references to active child processes to prevent their
107
- // Gio.Subprocess from being collected while async operations are pending.
108
- const _activeProcesses = new Set<ChildProcess>();
109
-
110
- /**
111
- * ChildProcess — EventEmitter wrapping Gio.Subprocess.
112
- */
113
- export class ChildProcess extends EventEmitter {
114
- pid?: number;
115
- exitCode: number | null = null;
116
- signalCode: string | null = null;
117
- killed = false;
118
- connected = false;
119
- stdin: Writable | null = null;
120
- stdout: Readable | null = null;
121
- stderr: Readable | null = null;
122
-
123
- private _subprocess: Gio.Subprocess | null = null;
124
-
125
- /** @internal Set the underlying Gio.Subprocess and extract PID. */
126
- _setSubprocess(proc: Gio.Subprocess): void {
127
- this._subprocess = proc;
128
- const pid = proc.get_identifier();
129
- if (pid) this.pid = parseInt(pid, 10);
130
- }
131
-
132
- /** Send a signal to the child process. */
133
- kill(signal?: string | number): boolean {
134
- if (!this._subprocess) return false;
135
- try {
136
- if (signal === 'SIGKILL' || signal === 9) {
137
- this._subprocess.force_exit();
138
- } else {
139
- this._subprocess.send_signal(typeof signal === 'number' ? signal : 15);
140
- }
141
- this.killed = true;
142
- return true;
143
- } catch {
144
- return false;
145
- }
146
- }
147
-
148
- ref(): this { return this; }
149
- unref(): this { return this; }
150
- }
151
-
152
- // Create a Gio.Subprocess with cwd and env support via SubprocessLauncher.
153
- function _spawnSubprocess(
154
- argv: string[],
155
- flags: Gio.SubprocessFlags,
156
- options?: { cwd?: string; env?: Record<string, string> }
157
- ): Gio.Subprocess {
158
- const launcher = new Gio.SubprocessLauncher({ flags });
159
- if (options?.cwd) {
160
- launcher.set_cwd(options.cwd);
161
- }
162
- if (options?.env) {
163
- for (const [key, value] of Object.entries(options.env)) {
164
- launcher.setenv(key, value, true);
165
- }
166
- }
167
- return launcher.spawnv(argv);
168
- }
169
-
170
- // Execute a command in a shell and buffer the output (sync).
171
- export function execSync(command: string, options?: ExecSyncOptions): Buffer | string {
172
- const encoding = options?.encoding;
173
- const input = options?.input;
174
-
175
- const flags = Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
176
- | (input ? Gio.SubprocessFlags.STDIN_PIPE : Gio.SubprocessFlags.NONE);
177
-
178
- const shell = typeof options?.shell === 'string' ? options.shell : '/bin/sh';
179
- const proc = _spawnSubprocess([shell, '-c', command], flags, options);
180
-
181
- const stdinBytes = input
182
- ? new GLib.Bytes(typeof input === 'string' ? new TextEncoder().encode(input) : input)
183
- : null;
184
-
185
- const [, stdoutBytes, stderrBytes] = proc.communicate(stdinBytes, null);
186
-
187
- const status = proc.get_exit_status();
188
- if (status !== 0) {
189
- const stderrStr = stderrBytes
190
- ? new TextDecoder().decode(gbytesToUint8Array(stderrBytes))
191
- : '';
192
- const error = new Error(`Command failed: ${command}\n${stderrStr}`) as ExecError;
193
- error.status = status;
194
- error.stderr = stderrStr;
195
- error.stdout = stdoutBytes
196
- ? new TextDecoder().decode(gbytesToUint8Array(stdoutBytes))
197
- : '';
198
- throw error;
199
- }
200
-
201
- if (!stdoutBytes) return encoding && encoding !== 'buffer' ? '' : Buffer.alloc(0);
202
- const data = gbytesToUint8Array(stdoutBytes);
203
- if (encoding && encoding !== 'buffer') {
204
- return new TextDecoder().decode(data);
205
- }
206
- return Buffer.from(data);
207
- }
208
-
209
- /**
210
- * Execute a command in a shell (async with callback).
211
- */
212
- function _exec(
213
- command: string,
214
- options?: ExecOptions | ((error: Error | null, stdout: string, stderr: string) => void),
215
- callback?: (error: Error | null, stdout: string, stderr: string) => void,
216
- ): ChildProcess {
217
- if (typeof options === 'function') {
218
- callback = options;
219
- options = {};
220
- }
221
- const opts = (options || {}) as ExecOptions;
222
- const child = new ChildProcess();
223
-
224
- const shell = typeof opts.shell === 'string' ? opts.shell : '/bin/sh';
225
- const flags = Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE;
226
-
227
- try {
228
- const proc = _spawnSubprocess([shell, '-c', command], flags, opts);
229
- child._setSubprocess(proc);
230
- ensureMainLoop();
231
-
232
- proc.communicate_utf8_async(null, null, (_source: Gio.Subprocess | null, result: Gio.AsyncResult) => {
233
- try {
234
- const [, stdout, stderr] = proc.communicate_utf8_finish(result);
235
- const exitStatus = proc.get_exit_status();
236
- child.exitCode = exitStatus;
237
-
238
- if (exitStatus !== 0) {
239
- const error = new Error(`Command failed: ${command}`) as ExecError;
240
- error.code = exitStatus;
241
- error.killed = child.killed;
242
- error.stdout = stdout || '';
243
- error.stderr = stderr || '';
244
- if (callback) callback(error, stdout || '', stderr || '');
245
- } else {
246
- if (callback) callback(null, stdout || '', stderr || '');
247
- }
248
-
249
- child.emit('close', exitStatus, null);
250
- child.emit('exit', exitStatus, null);
251
- } catch (err: unknown) {
252
- const error = err instanceof Error ? err : new Error(String(err));
253
- if (callback) callback(error, '', '');
254
- child.emit('error', error);
255
- }
256
- });
257
- } catch (err: unknown) {
258
- const error = err instanceof Error ? err : new Error(String(err));
259
- setTimeout(() => {
260
- if (callback) callback(error, '', '');
261
- child.emit('error', error);
262
- }, 0);
263
- }
264
-
265
- return child;
266
- }
267
-
268
- export { _exec as exec };
269
-
270
- /**
271
- * Execute a file directly without shell (async).
272
- */
273
- export function execFile(
274
- file: string,
275
- args?: string[] | ((error: Error | null, stdout: string, stderr: string) => void),
276
- options?: ExecOptions | ((error: Error | null, stdout: string, stderr: string) => void),
277
- callback?: (error: Error | null, stdout: string, stderr: string) => void,
278
- ): ChildProcess {
279
- let _args: string[] = [];
280
- let _opts: ExecOptions = {};
281
- let _callback: ((error: Error | null, stdout: string, stderr: string) => void) | undefined;
282
-
283
- if (typeof args === 'function') {
284
- _callback = args;
285
- } else if (Array.isArray(args)) {
286
- _args = args;
287
- if (typeof options === 'function') {
288
- _callback = options;
289
- } else {
290
- _opts = options || {};
291
- _callback = callback;
292
- }
293
- }
294
-
295
- const child = new ChildProcess();
296
- const flags = Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE;
297
-
298
- try {
299
- const proc = _spawnSubprocess([file, ..._args], flags, _opts);
300
- child._setSubprocess(proc);
301
- ensureMainLoop();
302
-
303
- proc.communicate_utf8_async(null, null, (_source: Gio.Subprocess | null, result: Gio.AsyncResult) => {
304
- try {
305
- const [, stdout, stderr] = proc.communicate_utf8_finish(result);
306
- const exitStatus = proc.get_exit_status();
307
- child.exitCode = exitStatus;
308
-
309
- if (exitStatus !== 0) {
310
- const error = new Error(`Command failed: ${file}`) as ExecError;
311
- error.code = exitStatus;
312
- error.stdout = stdout || '';
313
- error.stderr = stderr || '';
314
- if (_callback) _callback(error, stdout || '', stderr || '');
315
- } else {
316
- if (_callback) _callback(null, stdout || '', stderr || '');
317
- }
318
-
319
- child.emit('close', exitStatus, null);
320
- child.emit('exit', exitStatus, null);
321
- } catch (err: unknown) {
322
- const error = err instanceof Error ? err : new Error(String(err));
323
- if (_callback) _callback(error, '', '');
324
- child.emit('error', error);
325
- }
326
- });
327
- } catch (err: unknown) {
328
- const error = err instanceof Error ? err : new Error(String(err));
329
- setTimeout(() => {
330
- if (_callback) _callback(error, '', '');
331
- child.emit('error', error);
332
- }, 0);
333
- }
334
-
335
- return child;
336
- }
337
-
338
- /**
339
- * Execute a file directly without shell (sync).
340
- */
341
- export function execFileSync(file: string, args?: string[], options?: ExecSyncOptions): Buffer | string {
342
- const _args = args || [];
343
- const encoding = options?.encoding;
344
- const input = options?.input;
345
-
346
- const flags = Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
347
- | (input ? Gio.SubprocessFlags.STDIN_PIPE : Gio.SubprocessFlags.NONE);
348
-
349
- const proc = _spawnSubprocess([file, ..._args], flags, options);
350
-
351
- const stdinBytes = input
352
- ? new GLib.Bytes(typeof input === 'string' ? new TextEncoder().encode(input) : input)
353
- : null;
354
-
355
- const [, stdoutBytes, stderrBytes] = proc.communicate(stdinBytes, null);
356
-
357
- const status = proc.get_exit_status();
358
- if (status !== 0) {
359
- const stderrStr = stderrBytes
360
- ? new TextDecoder().decode(gbytesToUint8Array(stderrBytes))
361
- : '';
362
- const error = new Error(`Command failed: ${file} ${_args.join(' ')}`) as ExecError;
363
- error.status = status;
364
- error.stderr = stderrStr;
365
- throw error;
366
- }
367
-
368
- if (!stdoutBytes) return encoding && encoding !== 'buffer' ? '' : Buffer.alloc(0);
369
- const data = gbytesToUint8Array(stdoutBytes);
370
- if (encoding && encoding !== 'buffer') {
371
- return new TextDecoder().decode(data);
372
- }
373
- return Buffer.from(data);
374
- }
375
-
376
- /**
377
- * Spawn a new process (async, with event-based API).
378
- */
379
- export function spawn(command: string, args?: string[], options?: SpawnOptions): ChildProcess {
380
- const _args = args || [];
381
- const child = new ChildProcess();
382
- const useShell = options?.shell;
383
-
384
- let argv: string[];
385
- if (useShell) {
386
- const shell = typeof useShell === 'string' ? useShell : '/bin/sh';
387
- const fullCmd = [command, ..._args].join(' ');
388
- argv = [shell, '-c', fullCmd];
389
- } else {
390
- argv = [command, ..._args];
391
- }
392
-
393
- const flags = Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE | Gio.SubprocessFlags.STDIN_PIPE;
394
-
395
- try {
396
- const proc = _spawnSubprocess(argv, flags, options);
397
- child._setSubprocess(proc);
398
- _activeProcesses.add(child);
399
-
400
- const stdoutPipe = proc.get_stdout_pipe();
401
- if (stdoutPipe) child.stdout = new GioInputStreamReadable(stdoutPipe);
402
-
403
- const stderrPipe = proc.get_stderr_pipe();
404
- if (stderrPipe) child.stderr = new GioInputStreamReadable(stderrPipe);
405
-
406
- ensureMainLoop();
407
- proc.wait_async(null, (_source: Gio.Subprocess | null, result: Gio.AsyncResult) => {
408
- try {
409
- proc.wait_finish(result);
410
- const exitStatus = proc.get_if_exited() ? proc.get_exit_status() : null;
411
- const signal = proc.get_if_signaled() ? 'SIGTERM' : null;
412
- child.exitCode = exitStatus;
413
- child.signalCode = signal;
414
- child.emit('exit', exitStatus, signal);
415
- child.emit('close', exitStatus, signal);
416
- } catch (err: unknown) {
417
- child.emit('error', err instanceof Error ? err : new Error(String(err)));
418
- }
419
- _activeProcesses.delete(child);
420
- });
421
-
422
- deferEmit(child, 'spawn');
423
- } catch (err: unknown) {
424
- deferEmit(child, 'error', err instanceof Error ? err : new Error(String(err)));
425
- }
426
-
427
- return child;
428
- }
429
-
430
- /**
431
- * Spawn a new process (sync).
432
- */
433
- export function spawnSync(command: string, args?: string[], options?: ExecSyncOptions): SpawnSyncResult {
434
- const _args = args || [];
435
- const useShell = options?.shell;
436
- const input = options?.input;
437
-
438
- let argv: string[];
439
- if (useShell) {
440
- const shell = typeof useShell === 'string' ? useShell : '/bin/sh';
441
- const fullCmd = [command, ..._args].join(' ');
442
- argv = [shell, '-c', fullCmd];
443
- } else {
444
- argv = [command, ..._args];
445
- }
446
-
447
- const flags = Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
448
- | (input ? Gio.SubprocessFlags.STDIN_PIPE : Gio.SubprocessFlags.NONE);
449
-
450
- try {
451
- const proc = _spawnSubprocess(argv, flags, options);
452
- const pid = proc.get_identifier();
453
-
454
- const stdinBytes = input
455
- ? new GLib.Bytes(typeof input === 'string' ? new TextEncoder().encode(input) : input)
456
- : null;
457
-
458
- const [, stdoutBytes, stderrBytes] = proc.communicate(stdinBytes, null);
459
-
460
- const stdoutBuf = stdoutBytes ? Buffer.from(gbytesToUint8Array(stdoutBytes)) : Buffer.alloc(0);
461
- const stderrBuf = stderrBytes ? Buffer.from(gbytesToUint8Array(stderrBytes)) : Buffer.alloc(0);
462
-
463
- const encoding = options?.encoding;
464
- const stdoutData: Buffer | string = encoding && encoding !== 'buffer' ? new TextDecoder().decode(stdoutBuf) : stdoutBuf;
465
- const stderrData: Buffer | string = encoding && encoding !== 'buffer' ? new TextDecoder().decode(stderrBuf) : stderrBuf;
466
-
467
- const status = proc.get_if_exited() ? proc.get_exit_status() : null;
468
- const signal = proc.get_if_signaled() ? 'SIGTERM' : null;
469
-
470
- return {
471
- pid: pid ? parseInt(pid, 10) : 0,
472
- output: [null, stdoutData, stderrData],
473
- stdout: stdoutData,
474
- stderr: stderrData,
475
- status,
476
- signal,
477
- };
478
- } catch (err: unknown) {
479
- const empty: Buffer | string = options?.encoding && options.encoding !== 'buffer' ? '' : Buffer.alloc(0);
480
- return {
481
- pid: 0,
482
- output: [null, empty, empty],
483
- stdout: empty,
484
- stderr: empty,
485
- status: null,
486
- signal: null,
487
- error: err instanceof Error ? err : new Error(String(err)),
488
- };
489
- }
490
- }
491
-
492
- export default {
493
- ChildProcess,
494
- exec: _exec,
495
- execSync,
496
- execFile,
497
- execFileSync,
498
- spawn,
499
- spawnSync,
500
- };
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
- }