-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathposix.ts
More file actions
145 lines (129 loc) · 4.33 KB
/
Copy pathposix.ts
File metadata and controls
145 lines (129 loc) · 4.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import { spawn as spawnProcess } from "node:child_process";
import process from "node:process";
import { once } from "@effectionx/node/events";
import { fromReadable } from "@effectionx/node/stream";
import {
Err,
Ok,
type Result,
all,
createSignal,
resource,
spawn,
withResolvers,
} from "effection";
import type { CreateOSProcess, ExitStatus, Writable } from "./api.ts";
import { ExecError } from "./error.ts";
type ProcessResultValue = [number?, string?];
export const createPosixProcess: CreateOSProcess = (command, options) => {
return resource(function* (provide) {
let processResult = withResolvers<Result<ProcessResultValue>>();
// Killing all child processes started by this command is surprisingly
// tricky. If a process spawns another processes and we kill the parent,
// then the child process is NOT automatically killed. Instead we're using
// the `detached` option to force the child into its own process group,
// which all of its children in turn will inherit. By sending the signal to
// `-pid` rather than `pid`, we are sending it to the entire process group
// instead. This will send the signal to all processes started by the child
// process.
//
// More information here: https://unix.stackexchange.com/questions/14815/process-descendants
let childProcess = spawnProcess(command, options.arguments || [], {
detached: true,
shell: options.shell,
env: options.env,
cwd: options.cwd,
stdio: "pipe",
});
let { pid } = childProcess;
if (!childProcess.stdout || !childProcess.stderr) {
throw new Error("stdout and stderr must be available with stdio: pipe");
}
let io = {
stdout: yield* fromReadable(childProcess.stdout),
stderr: yield* fromReadable(childProcess.stderr),
stdoutDone: withResolvers<void>(),
stderrDone: withResolvers<void>(),
};
let stdout = createSignal<Uint8Array, void>();
let stderr = createSignal<Uint8Array, void>();
yield* spawn(function* () {
let next = yield* io.stdout.next();
while (!next.done) {
stdout.send(next.value);
next = yield* io.stdout.next();
}
stdout.close();
io.stdoutDone.resolve();
});
yield* spawn(function* () {
let next = yield* io.stderr.next();
while (!next.done) {
stderr.send(next.value);
next = yield* io.stderr.next();
}
stderr.close();
io.stderrDone.resolve();
});
let stdin: Writable<string> = {
send(data: string) {
childProcess.stdin.write(data);
},
};
const stdinErrorHandler = (err: Error & { code?: string }) => {
if (err.code === "EPIPE") {
console.warn(
`stdin EPIPE: child process (pid: ${childProcess.pid}) already exited. Writes to stdin are being discarded.`,
);
return;
}
processResult.resolve(Err(err));
};
childProcess.stdin.on("error", stdinErrorHandler);
yield* spawn(function* trapError() {
let [error] = yield* once<[Error]>(childProcess, "error");
processResult.resolve(Err(error));
});
yield* spawn(function* () {
let value = yield* once<ProcessResultValue>(childProcess, "close");
processResult.resolve(Ok(value));
});
function* join() {
let result = yield* processResult.operation;
if (result.ok) {
let [code, signal] = result.value;
return { command, options, code, signal } as ExitStatus;
}
throw result.error;
}
function* expect() {
let status: ExitStatus = yield* join();
if (status.code !== 0) {
throw new ExecError(status, command, options);
}
return status;
}
try {
yield* provide({
pid: pid as number,
stdin,
stdout,
stderr,
join,
expect,
});
} finally {
try {
if (typeof childProcess.pid === "undefined") {
// biome-ignore lint/correctness/noUnsafeFinally: Intentional error for missing PID
throw new Error("no pid for childProcess");
}
process.kill(-childProcess.pid, "SIGTERM");
yield* all([io.stdoutDone.operation, io.stderrDone.operation]);
} catch (_e) {
// do nothing, process is probably already dead
}
childProcess.stdin.off("error", stdinErrorHandler);
}
});
};