@opencode-ai/util 0.0.0-beta-18707 → 0.0.0-beta-18721
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/dist/cross-spawn-spawner.js +81 -46
- package/package.json +1 -1
|
@@ -192,19 +192,23 @@ const makeCrossSpawnSpawner = Effect.gen(function* () {
|
|
|
192
192
|
return Effect.as(Effect.forkScoped(Stream.run(cfg.stream, sink)), sink);
|
|
193
193
|
return Effect.succeed(sink);
|
|
194
194
|
});
|
|
195
|
-
const setupOutput = (command, proc, out, err) => {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
195
|
+
const setupOutput = (command, proc, out, err, stopOutput) => {
|
|
196
|
+
const capture = (readable, name) => {
|
|
197
|
+
if (!readable)
|
|
198
|
+
return Stream.empty;
|
|
199
|
+
return NodeStream.fromReadable({
|
|
200
|
+
evaluate: () => readable,
|
|
201
|
+
onError: (cause) => toPlatformError(`fromReadable(${name})`, toError(cause), command),
|
|
202
|
+
closeOnDone: false,
|
|
203
|
+
}).pipe(Stream.interruptWhen(Deferred.await(stopOutput)), Stream.ensuring(Effect.gen(function* () {
|
|
204
|
+
// Only the capture deadline transfers the reader back to the process scope.
|
|
205
|
+
if (yield* Deferred.isDone(stopOutput))
|
|
206
|
+
return;
|
|
207
|
+
readable.destroy();
|
|
208
|
+
})));
|
|
209
|
+
};
|
|
210
|
+
let stdout = capture(proc.stdout, "stdout");
|
|
211
|
+
let stderr = capture(proc.stderr, "stderr");
|
|
208
212
|
if (Sink.isSink(out.stream))
|
|
209
213
|
stdout = Stream.transduce(stdout, out.stream);
|
|
210
214
|
if (Sink.isSink(err.stream))
|
|
@@ -212,7 +216,8 @@ const makeCrossSpawnSpawner = Effect.gen(function* () {
|
|
|
212
216
|
return { stdout, stderr, all: Stream.merge(stdout, stderr) };
|
|
213
217
|
};
|
|
214
218
|
const launchProcess = (command, opts) => Effect.callback((resume) => {
|
|
215
|
-
const
|
|
219
|
+
const closed = Deferred.makeUnsafe();
|
|
220
|
+
const exited = Deferred.makeUnsafe();
|
|
216
221
|
const proc = launch(command.command, command.args, opts);
|
|
217
222
|
let end = false;
|
|
218
223
|
let exit;
|
|
@@ -221,15 +226,17 @@ const makeCrossSpawnSpawner = Effect.gen(function* () {
|
|
|
221
226
|
});
|
|
222
227
|
proc.on("exit", (...args) => {
|
|
223
228
|
exit = args;
|
|
229
|
+
Deferred.doneUnsafe(exited, Exit.succeed(args));
|
|
224
230
|
});
|
|
225
231
|
proc.on("close", (...args) => {
|
|
226
232
|
if (end)
|
|
227
233
|
return;
|
|
228
234
|
end = true;
|
|
229
|
-
Deferred.doneUnsafe(
|
|
235
|
+
Deferred.doneUnsafe(exited, Exit.succeed(exit ?? args));
|
|
236
|
+
Deferred.doneUnsafe(closed, Exit.succeed(exit ?? args));
|
|
230
237
|
});
|
|
231
238
|
proc.on("spawn", () => {
|
|
232
|
-
resume(Effect.succeed([proc,
|
|
239
|
+
resume(Effect.succeed([proc, closed, exited]));
|
|
233
240
|
});
|
|
234
241
|
return Effect.sync(() => {
|
|
235
242
|
proc.kill("SIGTERM");
|
|
@@ -237,8 +244,35 @@ const makeCrossSpawnSpawner = Effect.gen(function* () {
|
|
|
237
244
|
});
|
|
238
245
|
const spawn = Effect.fnUntraced(function* (command, opts) {
|
|
239
246
|
yield* Effect.logInfo("spawning process", { command: command.command, args: command.args, cwd: opts.cwd });
|
|
240
|
-
|
|
247
|
+
const [proc, closed, exited] = yield* launchProcess(command, opts);
|
|
248
|
+
const stopOutput = yield* Deferred.make();
|
|
249
|
+
// Register before process release so the deadline remains active during termination.
|
|
250
|
+
yield* Effect.forkScoped(Effect.gen(function* () {
|
|
251
|
+
yield* Deferred.await(exited);
|
|
252
|
+
// Particularly on Windows, some shell calls will cause detached descendants. Inherited stdio can hang the call.
|
|
253
|
+
// Almost every ecosystem has tried to fix this; there's no single "good" answer here.
|
|
254
|
+
// Calls that trigger this are e.g. dotnet build with warmed MSBuild processes, agent-browser, etc.
|
|
255
|
+
// One shared deadline covers stdout and stderr, then the output pump finishes its file.
|
|
256
|
+
if ((yield* Effect.timeoutOption(Deferred.await(closed), "1 second"))._tag === "Some")
|
|
257
|
+
return;
|
|
258
|
+
yield* Deferred.succeed(stopOutput, undefined);
|
|
259
|
+
discard(proc.stdout);
|
|
260
|
+
discard(proc.stderr);
|
|
261
|
+
}));
|
|
262
|
+
return [proc, closed, exited, stopOutput];
|
|
241
263
|
});
|
|
264
|
+
const discard = (readable) => {
|
|
265
|
+
if (!readable || readable.destroyed)
|
|
266
|
+
return;
|
|
267
|
+
// read() also drains while a backpressured Effect adapter still has a readable listener.
|
|
268
|
+
const drain = () => {
|
|
269
|
+
while (readable.read() !== null) { }
|
|
270
|
+
};
|
|
271
|
+
readable.on("readable", drain);
|
|
272
|
+
readable.once("error", () => { });
|
|
273
|
+
readable.once("close", () => readable.off("readable", drain));
|
|
274
|
+
drain();
|
|
275
|
+
};
|
|
242
276
|
const killGroup = (command, proc, signal) => {
|
|
243
277
|
if (globalThis.process.platform === "win32") {
|
|
244
278
|
return Effect.callback((resume) => {
|
|
@@ -261,23 +295,16 @@ const makeCrossSpawnSpawner = Effect.gen(function* () {
|
|
|
261
295
|
return Effect.void;
|
|
262
296
|
return Effect.fail(toPlatformError("kill", new Error("Failed to kill child process"), command));
|
|
263
297
|
});
|
|
264
|
-
const stop = (command, proc,
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
298
|
+
const stop = (command, proc, closed, stopOutput, opts) => {
|
|
299
|
+
const terminate = (signal) => Effect.catch(killGroup(command, proc, signal), () => killOne(command, proc, signal)).pipe(Effect.andThen(signal === "SIGKILL"
|
|
300
|
+
? Effect.raceFirst(Deferred.await(closed), Deferred.await(stopOutput))
|
|
301
|
+
: Deferred.await(closed)), Effect.asVoid);
|
|
302
|
+
const attempt = terminate(opts?.killSignal ?? "SIGTERM");
|
|
303
|
+
if (opts?.forceKillAfter === undefined)
|
|
268
304
|
return attempt;
|
|
269
305
|
return Effect.timeoutOrElse(attempt, {
|
|
270
306
|
duration: opts.forceKillAfter,
|
|
271
|
-
orElse: () =>
|
|
272
|
-
});
|
|
273
|
-
};
|
|
274
|
-
const timeout = (proc, command, opts) => (f) => {
|
|
275
|
-
const signal = opts?.killSignal ?? "SIGTERM";
|
|
276
|
-
if (Predicate.isUndefined(opts?.forceKillAfter))
|
|
277
|
-
return f(command, proc, signal);
|
|
278
|
-
return Effect.timeoutOrElse(f(command, proc, signal), {
|
|
279
|
-
duration: opts.forceKillAfter,
|
|
280
|
-
orElse: () => f(command, proc, "SIGKILL"),
|
|
307
|
+
orElse: () => terminate("SIGKILL"),
|
|
281
308
|
});
|
|
282
309
|
};
|
|
283
310
|
const source = (handle, from) => {
|
|
@@ -303,28 +330,34 @@ const makeCrossSpawnSpawner = Effect.gen(function* () {
|
|
|
303
330
|
const serr = stdio(command.options, "stderr");
|
|
304
331
|
const extra = fds(command.options);
|
|
305
332
|
const dir = yield* cwd(command.options);
|
|
306
|
-
const [proc,
|
|
333
|
+
const [proc, closed, exited, stopOutput] = yield* Effect.acquireRelease(spawn(command, {
|
|
307
334
|
cwd: dir,
|
|
308
335
|
env: env(command.options),
|
|
309
336
|
stdio: stdios(sin, sout, serr, extra),
|
|
310
337
|
detached: command.options.detached ?? process.platform !== "win32",
|
|
311
338
|
shell: command.options.shell,
|
|
312
339
|
windowsHide: process.platform === "win32",
|
|
313
|
-
}), Effect.fnUntraced(function* ([proc,
|
|
314
|
-
const done = yield* Deferred.isDone(
|
|
315
|
-
const kill = timeout(proc, command, command.options);
|
|
340
|
+
}), Effect.fnUntraced(function* ([proc, closed, exited, stopOutput]) {
|
|
341
|
+
const done = (yield* Deferred.isDone(closed)) || (yield* Deferred.isDone(stopOutput));
|
|
316
342
|
if (done) {
|
|
317
|
-
const [code] = yield* Deferred.await(
|
|
343
|
+
const [code] = yield* Deferred.await(exited);
|
|
318
344
|
if (process.platform === "win32")
|
|
319
|
-
return
|
|
320
|
-
if (code
|
|
321
|
-
return
|
|
322
|
-
|
|
345
|
+
return;
|
|
346
|
+
if (code === 0 || Predicate.isNull(code))
|
|
347
|
+
return;
|
|
348
|
+
if (command.options.forceKillAfter === undefined) {
|
|
349
|
+
return yield* Effect.ignore(killGroup(command, proc, command.options.killSignal ?? "SIGTERM"));
|
|
350
|
+
}
|
|
323
351
|
}
|
|
324
|
-
|
|
325
|
-
}))
|
|
352
|
+
yield* Effect.ignore(stop(command, proc, closed, stopOutput, command.options));
|
|
353
|
+
}, (effect, [proc, , , stopOutput]) => effect.pipe(Effect.ensuring(Effect.gen(function* () {
|
|
354
|
+
yield* Deferred.succeed(stopOutput, undefined);
|
|
355
|
+
proc.stdout?.destroy();
|
|
356
|
+
proc.stderr?.destroy();
|
|
357
|
+
})))));
|
|
358
|
+
const completion = Effect.raceFirst(Deferred.await(closed), Deferred.await(stopOutput).pipe(Effect.andThen(Deferred.await(exited))));
|
|
326
359
|
const fd = yield* setupFds(command, proc, extra);
|
|
327
|
-
const out = setupOutput(command, proc, sout, serr);
|
|
360
|
+
const out = setupOutput(command, proc, sout, serr, stopOutput);
|
|
328
361
|
let ref = true;
|
|
329
362
|
return makeHandle({
|
|
330
363
|
pid: ProcessId(proc.pid),
|
|
@@ -334,13 +367,15 @@ const makeCrossSpawnSpawner = Effect.gen(function* () {
|
|
|
334
367
|
all: out.all,
|
|
335
368
|
getInputFd: fd.getInputFd,
|
|
336
369
|
getOutputFd: fd.getOutputFd,
|
|
337
|
-
isRunning: Effect.
|
|
338
|
-
|
|
370
|
+
isRunning: Effect.gen(function* () {
|
|
371
|
+
return !(yield* Deferred.isDone(closed)) && !(yield* Deferred.isDone(stopOutput));
|
|
372
|
+
}),
|
|
373
|
+
exitCode: Effect.flatMap(completion, ([code, signal]) => {
|
|
339
374
|
if (Predicate.isNotNull(code))
|
|
340
375
|
return Effect.succeed(ExitCode(code));
|
|
341
376
|
return Effect.fail(toPlatformError("exitCode", new Error(`Process interrupted due to receipt of signal: '${signal}'`), command));
|
|
342
377
|
}),
|
|
343
|
-
kill: (opts) => stop(command, proc,
|
|
378
|
+
kill: (opts) => stop(command, proc, closed, stopOutput, opts),
|
|
344
379
|
unref: Effect.sync(() => {
|
|
345
380
|
if (ref) {
|
|
346
381
|
proc.unref();
|