6
6
// Execute code in a subprocess.
7
7
8
8
import { callback } from "awaiting" ;
9
- import { spawn } from "node:child_process" ;
9
+ import LRU from "lru-cache" ;
10
+ import {
11
+ ChildProcessWithoutNullStreams ,
12
+ spawn ,
13
+ SpawnOptionsWithoutStdio ,
14
+ } from "node:child_process" ;
10
15
import { chmod , mkdtemp , rm , writeFile } from "node:fs/promises" ;
11
16
import { tmpdir } from "node:os" ;
12
17
import { join } from "node:path" ;
@@ -15,7 +20,7 @@ import shellEscape from "shell-escape";
15
20
import getLogger from "@cocalc/backend/logger" ;
16
21
import { aggregate } from "@cocalc/util/aggregate" ;
17
22
import { callback_opts } from "@cocalc/util/async-utils" ;
18
- import { to_json , trunc , walltime } from "@cocalc/util/misc" ;
23
+ import { to_json , trunc , uuid , walltime } from "@cocalc/util/misc" ;
19
24
import { envForSpawn } from "./misc" ;
20
25
21
26
import type {
@@ -27,6 +32,15 @@ import type {
27
32
28
33
const log = getLogger ( "execute-code" ) ;
29
34
35
+ const asyncCache = new LRU < string , ExecuteCodeOutput > ( {
36
+ max : 100 ,
37
+ ttl : 1000 * 60 * 60 ,
38
+ ttlAutopurge : true ,
39
+ allowStale : true ,
40
+ updateAgeOnGet : true ,
41
+ updateAgeOnHas : true ,
42
+ } ) ;
43
+
30
44
// Async/await interface to executing code.
31
45
export async function executeCode (
32
46
opts : ExecuteCodeOptions ,
@@ -52,6 +66,13 @@ export const execute_code: ExecuteCodeFunctionWithCallback = aggregate(
52
66
async function executeCodeNoAggregate (
53
67
opts : ExecuteCodeOptions ,
54
68
) : Promise < ExecuteCodeOutput > {
69
+ if ( typeof opts . async_get === "string" ) {
70
+ const s = asyncCache . get ( opts . async_get ) ;
71
+ if ( s != null ) {
72
+ return s ;
73
+ }
74
+ }
75
+
55
76
if ( opts . args == null ) opts . args = [ ] ;
56
77
if ( opts . timeout == null ) opts . timeout = 10 ;
57
78
if ( opts . ulimit_timeout == null ) opts . ulimit_timeout = true ;
@@ -115,7 +136,32 @@ async function executeCodeNoAggregate(
115
136
await writeFile ( tempPath , cmd ) ;
116
137
await chmod ( tempPath , 0o700 ) ;
117
138
}
118
- return await callback ( doSpawn , { ...opts , origCommand } ) ;
139
+
140
+ if ( opts . async_exec ) {
141
+ // we return an ID, the caller can then use it to query the status
142
+ const async_limit = 1024 * 1024 ; // we limit how much we keep in memory, to avoid problems
143
+ opts . max_output = Math . min ( async_limit , opts . max_output ?? async_limit ) ;
144
+ const id = uuid ( ) ;
145
+ asyncCache . set ( id , {
146
+ stdout : `Process started running at ${ Date . now ( ) } ` ,
147
+ stderr : "" ,
148
+ exit_code : 0 ,
149
+ async_id : id ,
150
+ } ) ;
151
+
152
+ doSpawn ( { ...opts , origCommand } , ( err , result ) => {
153
+ if ( err ) {
154
+ asyncCache . set ( id , { stdout : "" , stderr : `${ err } ` , exit_code : 1 } ) ;
155
+ } else if ( result != null ) {
156
+ asyncCache . set ( id , result ) ;
157
+ } else {
158
+ asyncCache . set ( id , { stdout : "" , stderr : `No result` , exit_code : 1 } ) ;
159
+ }
160
+ } ) ;
161
+ } else {
162
+ // This is the blocking variant
163
+ return await callback ( doSpawn , { ...opts , origCommand } ) ;
164
+ }
119
165
} finally {
120
166
// clean up
121
167
if ( tempDir ) {
@@ -124,7 +170,10 @@ async function executeCodeNoAggregate(
124
170
}
125
171
}
126
172
127
- function doSpawn ( opts , cb ) {
173
+ function doSpawn (
174
+ opts ,
175
+ cb : ( err : string | undefined , result ?: ExecuteCodeOutput ) => void ,
176
+ ) {
128
177
const start_time = walltime ( ) ;
129
178
130
179
if ( opts . verbose ) {
@@ -138,7 +187,7 @@ function doSpawn(opts, cb) {
138
187
"seconds" ,
139
188
) ;
140
189
}
141
- const spawnOptions = {
190
+ const spawnOptions : SpawnOptionsWithoutStdio = {
142
191
detached : true , // so we can kill the entire process group if it times out
143
192
cwd : opts . path ,
144
193
...( opts . uid ? { uid : opts . uid } : undefined ) ,
@@ -150,11 +199,11 @@ function doSpawn(opts, cb) {
150
199
} ,
151
200
} ;
152
201
153
- let r ,
154
- ran_code = false ;
202
+ let r : ChildProcessWithoutNullStreams ;
203
+ let ran_code = false ;
155
204
try {
156
205
r = spawn ( opts . command , opts . args , spawnOptions ) ;
157
- if ( r . stdout == null || r . stderr == null ) {
206
+ if ( r . stdout == null || r . stderr == null || r . pid == null ) {
158
207
// The docs/examples at https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options
159
208
// suggest that r.stdout and r.stderr are always defined. However, this is
160
209
// definitely NOT the case in edge cases, as we have observed.
@@ -215,7 +264,7 @@ function doSpawn(opts, cb) {
215
264
} ) ;
216
265
217
266
r . on ( "exit" , ( code ) => {
218
- exit_code = code ;
267
+ exit_code = code != null ? code : undefined ;
219
268
finish ( ) ;
220
269
} ) ;
221
270
@@ -317,8 +366,10 @@ function doSpawn(opts, cb) {
317
366
) ;
318
367
}
319
368
try {
320
- killed = true ;
321
- process . kill ( - r . pid , "SIGKILL" ) ; // this should kill process group
369
+ killed = true ; // we set the kill flag in any case – i.e. process will no longer exist
370
+ if ( r . pid != null ) {
371
+ process . kill ( - r . pid , "SIGKILL" ) ; // this should kill process group
372
+ }
322
373
} catch ( err ) {
323
374
// Exceptions can happen, which left uncaught messes up calling code big time.
324
375
if ( opts . verbose ) {
0 commit comments