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,21 +20,32 @@ 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 {
22
27
ExecuteCodeFunctionWithCallback ,
23
28
ExecuteCodeOptions ,
29
+ ExecuteCodeOptionsAsyncGet ,
24
30
ExecuteCodeOptionsWithCallback ,
25
31
ExecuteCodeOutput ,
26
32
} from "@cocalc/util/types/execute-code" ;
33
+ import { isExecuteCodeOptionsAsyncGet } from "./execute-code.test" ;
27
34
28
35
const log = getLogger ( "execute-code" ) ;
29
36
37
+ const asyncCache = new LRU < string , ExecuteCodeOutput > ( {
38
+ max : 100 ,
39
+ ttl : 1000 * 60 * 60 ,
40
+ ttlAutopurge : true ,
41
+ allowStale : true ,
42
+ updateAgeOnGet : true ,
43
+ updateAgeOnHas : true ,
44
+ } ) ;
45
+
30
46
// Async/await interface to executing code.
31
47
export async function executeCode (
32
- opts : ExecuteCodeOptions ,
48
+ opts : ExecuteCodeOptions | ExecuteCodeOptionsAsyncGet ,
33
49
) : Promise < ExecuteCodeOutput > {
34
50
return await callback_opts ( execute_code ) ( opts ) ;
35
51
}
@@ -50,8 +66,17 @@ export const execute_code: ExecuteCodeFunctionWithCallback = aggregate(
50
66
51
67
// actual implementation, without the aggregate wrapper
52
68
async function executeCodeNoAggregate (
53
- opts : ExecuteCodeOptions ,
69
+ opts : ExecuteCodeOptions | ExecuteCodeOptionsAsyncGet ,
54
70
) : Promise < ExecuteCodeOutput > {
71
+ if ( isExecuteCodeOptionsAsyncGet ( opts ) ) {
72
+ const cached = asyncCache . get ( opts . async_get ) ;
73
+ if ( cached != null ) {
74
+ return cached ;
75
+ } else {
76
+ throw new Error ( `Status or result of '${ opts . async_get } ' not found.` ) ;
77
+ }
78
+ }
79
+
55
80
if ( opts . args == null ) opts . args = [ ] ;
56
81
if ( opts . timeout == null ) opts . timeout = 10 ;
57
82
if ( opts . ulimit_timeout == null ) opts . ulimit_timeout = true ;
@@ -115,7 +140,48 @@ async function executeCodeNoAggregate(
115
140
await writeFile ( tempPath , cmd ) ;
116
141
await chmod ( tempPath , 0o700 ) ;
117
142
}
118
- return await callback ( doSpawn , { ...opts , origCommand } ) ;
143
+
144
+ if ( opts . async_exec ) {
145
+ // we return an ID, the caller can then use it to query the status
146
+ const async_limit = 1024 * 1024 ; // we limit how much we keep in memory, to avoid problems
147
+ opts . max_output = Math . min ( async_limit , opts . max_output ?? async_limit ) ;
148
+ const id = uuid ( ) ;
149
+ const start = new Date ( ) ;
150
+ const started = {
151
+ stdout : `Process started running at ${ start . toISOString ( ) } ` ,
152
+ stderr : "" ,
153
+ exit_code : start . getTime ( ) ,
154
+ async_id : id ,
155
+ } ;
156
+ asyncCache . set ( id , started ) ;
157
+
158
+ doSpawn ( { ...opts , origCommand } , ( err , result ) => {
159
+ const started = asyncCache . get ( id ) ?. exit_code ?? 0 ;
160
+ const info = { elapsed_s : ( Date . now ( ) - started ) / 1000 } ;
161
+ if ( err ) {
162
+ asyncCache . set ( id , {
163
+ stdout : "" ,
164
+ stderr : `${ err } ` ,
165
+ exit_code : 1 ,
166
+ ...info ,
167
+ } ) ;
168
+ } else if ( result != null ) {
169
+ asyncCache . set ( id , { ...result , ...info } ) ;
170
+ } else {
171
+ asyncCache . set ( id , {
172
+ stdout : "" ,
173
+ stderr : `No result` ,
174
+ exit_code : 1 ,
175
+ ...info ,
176
+ } ) ;
177
+ }
178
+ } ) ;
179
+
180
+ return started ;
181
+ } else {
182
+ // This is the blocking variant
183
+ return await callback ( doSpawn , { ...opts , origCommand } ) ;
184
+ }
119
185
} finally {
120
186
// clean up
121
187
if ( tempDir ) {
@@ -124,7 +190,10 @@ async function executeCodeNoAggregate(
124
190
}
125
191
}
126
192
127
- function doSpawn ( opts , cb ) {
193
+ function doSpawn (
194
+ opts ,
195
+ cb : ( err : string | undefined , result ?: ExecuteCodeOutput ) => void ,
196
+ ) {
128
197
const start_time = walltime ( ) ;
129
198
130
199
if ( opts . verbose ) {
@@ -138,7 +207,7 @@ function doSpawn(opts, cb) {
138
207
"seconds" ,
139
208
) ;
140
209
}
141
- const spawnOptions = {
210
+ const spawnOptions : SpawnOptionsWithoutStdio = {
142
211
detached : true , // so we can kill the entire process group if it times out
143
212
cwd : opts . path ,
144
213
...( opts . uid ? { uid : opts . uid } : undefined ) ,
@@ -150,8 +219,8 @@ function doSpawn(opts, cb) {
150
219
} ,
151
220
} ;
152
221
153
- let r ,
154
- ran_code = false ;
222
+ let r : ChildProcessWithoutNullStreams ;
223
+ let ran_code = false ;
155
224
try {
156
225
r = spawn ( opts . command , opts . args , spawnOptions ) ;
157
226
if ( r . stdout == null || r . stderr == null ) {
@@ -215,7 +284,7 @@ function doSpawn(opts, cb) {
215
284
} ) ;
216
285
217
286
r . on ( "exit" , ( code ) => {
218
- exit_code = code ;
287
+ exit_code = code != null ? code : undefined ;
219
288
finish ( ) ;
220
289
} ) ;
221
290
@@ -317,8 +386,10 @@ function doSpawn(opts, cb) {
317
386
) ;
318
387
}
319
388
try {
320
- killed = true ;
321
- process . kill ( - r . pid , "SIGKILL" ) ; // this should kill process group
389
+ killed = true ; // we set the kill flag in any case – i.e. process will no longer exist
390
+ if ( r . pid != null ) {
391
+ process . kill ( - r . pid , "SIGKILL" ) ; // this should kill process group
392
+ }
322
393
} catch ( err ) {
323
394
// Exceptions can happen, which left uncaught messes up calling code big time.
324
395
if ( opts . verbose ) {
0 commit comments