feat(core): Add spawnPath child-process API with timeout/abort/terminate support, export native types, and expand README
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tsrun',
|
||||
version: '1.5.0',
|
||||
version: '1.6.0',
|
||||
description: 'run typescript programs efficiently'
|
||||
}
|
||||
|
180
ts/index.ts
180
ts/index.ts
@@ -5,6 +5,64 @@ export interface IRunOptions {
|
||||
cwd?: string;
|
||||
}
|
||||
|
||||
export interface ISpawnOptions {
|
||||
/** Working directory for the child process */
|
||||
cwd?: string;
|
||||
|
||||
/** Environment variables (merged with parent's env) */
|
||||
env?: Record<string, string>;
|
||||
|
||||
/** Additional CLI arguments to pass to the script */
|
||||
args?: string[];
|
||||
|
||||
/**
|
||||
* Stdio configuration
|
||||
* - 'pipe': Create pipes for stdin/stdout/stderr (default)
|
||||
* - 'inherit': Use parent's stdio
|
||||
* - Array: Custom configuration per stream
|
||||
*/
|
||||
stdio?: 'pipe' | 'inherit' | ['pipe' | 'inherit' | 'ignore', 'pipe' | 'inherit' | 'ignore', 'pipe' | 'inherit' | 'ignore'];
|
||||
|
||||
/**
|
||||
* Optional timeout in milliseconds
|
||||
* If provided, process is automatically killed after timeout
|
||||
*/
|
||||
timeout?: number;
|
||||
|
||||
/**
|
||||
* AbortSignal for cancellation support
|
||||
* Allows external cancellation of the process
|
||||
*/
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export interface ITsrunChildProcess {
|
||||
/** Direct access to Node's ChildProcess object */
|
||||
childProcess: plugins.ChildProcess;
|
||||
|
||||
/** Readable stream for stdout (null if stdio is 'inherit') */
|
||||
stdout: plugins.Readable | null;
|
||||
|
||||
/** Readable stream for stderr (null if stdio is 'inherit') */
|
||||
stderr: plugins.Readable | null;
|
||||
|
||||
/** Promise that resolves with the exit code when process ends */
|
||||
exitCode: Promise<number>;
|
||||
|
||||
/**
|
||||
* Send signal to process
|
||||
* Returns true if signal was sent successfully
|
||||
*/
|
||||
kill(signal?: NodeJS.Signals): boolean;
|
||||
|
||||
/**
|
||||
* Gracefully terminate the process
|
||||
* Tries SIGTERM first, waits 5s, then SIGKILL if still running
|
||||
* Returns a promise that resolves when process is terminated
|
||||
*/
|
||||
terminate(): Promise<void>;
|
||||
}
|
||||
|
||||
export const runPath = async (pathArg: string, fromFileUrl?: string, options?: IRunOptions) => {
|
||||
pathArg = fromFileUrl
|
||||
? plugins.path.join(plugins.path.dirname(plugins.url.fileURLToPath(fromFileUrl)), pathArg)
|
||||
@@ -95,3 +153,125 @@ const runInChildProcess = async (pathArg: string | undefined, cwd: string): Prom
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const spawnPath = (
|
||||
filePath: string,
|
||||
fromFileUrl?: string | URL,
|
||||
options?: ISpawnOptions
|
||||
): ITsrunChildProcess => {
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
// 1. Resolve path (similar to runPath)
|
||||
const resolvedPath = fromFileUrl
|
||||
? plugins.path.join(
|
||||
plugins.path.dirname(
|
||||
plugins.url.fileURLToPath(
|
||||
typeof fromFileUrl === 'string' ? fromFileUrl : fromFileUrl.href
|
||||
)
|
||||
),
|
||||
filePath
|
||||
)
|
||||
: filePath;
|
||||
|
||||
// 2. Build spawn args
|
||||
const cliChildPath = plugins.path.join(__dirname, '../cli.child.js');
|
||||
const args = [
|
||||
...process.execArgv,
|
||||
cliChildPath,
|
||||
resolvedPath,
|
||||
...(options?.args || [])
|
||||
];
|
||||
|
||||
// 3. Build spawn options
|
||||
const spawnOptions = {
|
||||
cwd: options?.cwd || process.cwd(),
|
||||
env: { ...process.env, ...options?.env },
|
||||
stdio: options?.stdio || 'pipe',
|
||||
shell: false,
|
||||
windowsHide: false
|
||||
};
|
||||
|
||||
// 4. Spawn child process
|
||||
const child = spawn(process.execPath, args, spawnOptions);
|
||||
|
||||
// 5. Set up timeout if provided
|
||||
let timeoutId: NodeJS.Timeout | undefined;
|
||||
let timeoutTriggered = false;
|
||||
|
||||
if (options?.timeout) {
|
||||
timeoutId = setTimeout(() => {
|
||||
timeoutTriggered = true;
|
||||
child.kill('SIGTERM');
|
||||
}, options.timeout);
|
||||
}
|
||||
|
||||
// 6. Set up AbortSignal if provided
|
||||
let abortHandler: (() => void) | undefined;
|
||||
if (options?.signal) {
|
||||
abortHandler = () => {
|
||||
child.kill('SIGTERM');
|
||||
};
|
||||
options.signal.addEventListener('abort', abortHandler);
|
||||
}
|
||||
|
||||
// 7. Create exitCode promise
|
||||
const exitCodePromise = new Promise<number>((resolve, reject) => {
|
||||
child.on('close', (code: number | null, signal: NodeJS.Signals | null) => {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
if (abortHandler && options?.signal) {
|
||||
options.signal.removeEventListener('abort', abortHandler);
|
||||
}
|
||||
|
||||
if (timeoutTriggered) {
|
||||
reject(new Error(`Process killed: timeout of ${options?.timeout}ms exceeded`));
|
||||
} else if (options?.signal?.aborted) {
|
||||
reject(new Error('Process killed: aborted by signal'));
|
||||
} else if (signal) {
|
||||
reject(new Error(`Process killed with signal ${signal}`));
|
||||
} else {
|
||||
resolve(code || 0);
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (err: Error) => {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
if (abortHandler && options?.signal) {
|
||||
options.signal.removeEventListener('abort', abortHandler);
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
// 8. Implement terminate() method
|
||||
const terminate = async (): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
if (child.killed) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
child.kill('SIGTERM');
|
||||
|
||||
const killTimeout = setTimeout(() => {
|
||||
if (!child.killed) {
|
||||
child.kill('SIGKILL');
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
child.on('close', () => {
|
||||
clearTimeout(killTimeout);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 9. Return ITsrunChildProcess object
|
||||
return {
|
||||
childProcess: child,
|
||||
stdout: child.stdout,
|
||||
stderr: child.stderr,
|
||||
exitCode: exitCodePromise,
|
||||
kill: (signal?: NodeJS.Signals) => child.kill(signal),
|
||||
terminate
|
||||
};
|
||||
};
|
||||
|
@@ -1,8 +1,11 @@
|
||||
// node native
|
||||
import * as path from 'path';
|
||||
import * as url from 'url';
|
||||
import type { ChildProcess } from 'child_process';
|
||||
import type { Readable } from 'stream';
|
||||
|
||||
export { path, url };
|
||||
export type { ChildProcess, Readable };
|
||||
|
||||
// @pushrocks scope
|
||||
import * as smartfile from '@push.rocks/smartfile';
|
||||
|
Reference in New Issue
Block a user