@@ -5,10 +5,25 @@ import { normalizeExecutablePath } from './hooks/runtimes/utils.js';
55import { error , run } from './outputs.js' ;
66import { cliDebugPrint } from './utils/cliDebugPrint.js' ;
77
8- const spawnPromised = async ( cmd : string , args : string [ ] , opts : Options ) => {
8+ interface SpawnPromisedInternalOptions {
9+ /**
10+ * Signals that should be forwarded from the parent process to the spawned
11+ * child. When the CLI receives one of these signals it is re-sent to the
12+ * child so it can shut down cleanly instead of being orphaned when the CLI
13+ * exits.
14+ */
15+ forwardSignals ?: NodeJS . Signals [ ] ;
16+ }
17+
18+ const spawnPromised = async (
19+ cmd : string ,
20+ args : string [ ] ,
21+ opts : Options ,
22+ { forwardSignals } : SpawnPromisedInternalOptions = { } ,
23+ ) => {
924 const escapedCommand = normalizeExecutablePath ( cmd ) ;
1025
11- cliDebugPrint ( 'spawnPromised' , { escapedCommand, args, opts } ) ;
26+ cliDebugPrint ( 'spawnPromised' , { escapedCommand, args, opts, forwardSignals } ) ;
1227
1328 const childProcess = execa ( escapedCommand , args , {
1429 shell : true ,
@@ -21,23 +36,58 @@ const spawnPromised = async (cmd: string, args: string[], opts: Options) => {
2136 verbose : process . env . APIFY_CLI_DEBUG ? 'full' : undefined ,
2237 } ) ;
2338
24- return Result . fromAsync (
25- childProcess . catch ( ( execaError : ExecaError ) => {
26- throw new Error ( `${ cmd } exited with code ${ execaError . exitCode } ` , { cause : execaError } ) ;
27- } ) ,
28- ) as Promise < Result < Awaited < typeof childProcess > , Error & { cause : ExecaError } > > ;
39+ const cleanupSignalHandlers : ( ( ) => void ) [ ] = [ ] ;
40+
41+ if ( forwardSignals ?. length ) {
42+ for ( const signal of forwardSignals ) {
43+ const handler = ( ) => {
44+ childProcess . kill ( signal ) ;
45+ } ;
46+
47+ process . on ( signal , handler ) ;
48+ cleanupSignalHandlers . push ( ( ) => process . off ( signal , handler ) ) ;
49+ }
50+ }
51+
52+ try {
53+ return ( await Result . fromAsync (
54+ childProcess . catch ( ( execaError : ExecaError ) => {
55+ let message ;
56+
57+ if ( execaError . exitCode != null ) {
58+ message = `${ cmd } exited with code ${ execaError . exitCode } ` ;
59+ } else if ( execaError . signal ) {
60+ message = `${ cmd } exited due to signal ${ execaError . signal } ` ;
61+ } else {
62+ message = execaError . shortMessage ;
63+ }
64+
65+ throw new Error ( message , { cause : execaError } ) ;
66+ } ) ,
67+ ) ) as Result < Awaited < typeof childProcess > , Error & { cause : ExecaError } > ;
68+ } finally {
69+ for ( const cleanup of cleanupSignalHandlers ) {
70+ cleanup ( ) ;
71+ }
72+ }
2973} ;
3074
3175export interface ExecWithLogOptions {
3276 cmd : string ;
3377 args ?: string [ ] ;
3478 opts ?: Options ;
3579 overrideCommand ?: string ;
80+ /**
81+ * Signals to forward from the parent process to the spawned child. Use this
82+ * for long-running children (e.g. user scripts) so pressing Ctrl+C on the
83+ * CLI does not leave the child running in the background.
84+ */
85+ forwardSignals ?: NodeJS . Signals [ ] ;
3686}
3787
38- export async function execWithLog ( { cmd, args = [ ] , opts = { } , overrideCommand } : ExecWithLogOptions ) {
88+ export async function execWithLog ( { cmd, args = [ ] , opts = { } , overrideCommand, forwardSignals } : ExecWithLogOptions ) {
3989 run ( { message : `${ overrideCommand || cmd } ${ args . join ( ' ' ) } ` } ) ;
40- const result = await spawnPromised ( cmd , args , opts ) ;
90+ const result = await spawnPromised ( cmd , args , opts , { forwardSignals } ) ;
4191
4292 if ( result . isErr ( ) ) {
4393 const err = result . unwrapErr ( ) ;
0 commit comments