1/**
2 * Service boundary for starting and controlling child processes.
3 *
4 * `ChildProcessSpawner` is the service used by `ChildProcess` commands to start
5 * operating-system processes. A spawner turns a command description into a
6 * handle that can write to stdin, read stdout and stderr, wait for exit, kill
7 * the process, and manage whether the process keeps its parent alive. Platform
8 * backends implement this service, while most application code uses the higher
9 * level `ChildProcess` module.
10 *
11 * @stability unstable
12 * @since 4.0.0
13 */
14import * as Brand from "../Brand.ts"
15import * as Context from "../Context.ts"
16import * as Effect from "../Effect.ts"
17import * as Inspectable from "../Inspectable.ts"
18import type * as PlatformError from "../PlatformError.ts"
19import type * as Scope from "../Scope.ts"
20import type * as Sink from "../Sink.ts"
21import * as Stream from "../Stream.ts"
22import type { Command, KillOptions } from "./ChildProcess.ts"
23
24/**
25 * Branded number representing the exit code reported by a child process.
26 *
27 * @stability unstable
28 * @category models
29 * @since 4.0.0
30 */
31export type ExitCode = Brand.Branded<number, "ExitCode">
32
33/**
34 * Constructs branded child process `ExitCode` values.
35 *
36 * @stability unstable
37 * @category constructors
38 * @since 4.0.0
39 */
40export const ExitCode: Brand.Constructor<ExitCode> = Brand.nominal<ExitCode>()
41
42/**
43 * Branded number representing the operating system process identifier of a
44 * child process.
45 *
46 * @stability unstable
47 * @category models
48 * @since 4.0.0
49 */
50export type ProcessId = Brand.Branded<number, "ProcessId">
51
52/**
53 * Constructs branded child process `ProcessId` values.
54 *
55 * @stability unstable
56 * @category constructors
57 * @since 4.0.0
58 */
59export const ProcessId: Brand.Constructor<ProcessId> = Brand.nominal<ProcessId>()
60
61/**
62 * An `Effect` that adds an unrefed child process back into the parent
63 * process's reference count.
64 *
65 * **Details**
66 *
67 * This value is returned by `ChildProcessHandle.unref` and can be run later to
68 * restore the default behavior where the child process keeps the parent
69 * process alive.
70 *
71 * @stability unstable
72 * @category models
73 * @since 4.0.0
74 */
75export type Reref = Effect.Effect<void, PlatformError.PlatformError>
76
77const HandleTypeId = "~effect/process/ChildProcessSpawner/ChildProcessHandle"
78
79/**
80 * A handle to a running child process.
81 *
82 * @stability unstable
83 * @category models
84 * @since 4.0.0
85 */
86export interface ChildProcessHandle {
87 readonly [HandleTypeId]: typeof HandleTypeId
88 /**
89 * The child process process identifier.
90 */
91 readonly pid: ProcessId
92 /**
93 * Waits for the child process to exit and returns the `ExitCode` of the
94 * command that was run.
95 */
96 readonly exitCode: Effect.Effect<ExitCode, PlatformError.PlatformError>
97 /**
98 * Returns `true` if the child process is still running, otherwise returns
99 * `false`.
100 */
101 readonly isRunning: Effect.Effect<boolean, PlatformError.PlatformError>
102 /**
103 * Kills the child process with the provided signal.
104 *
105 * **Details**
106 *
107 * If no signal option is provided, the signal defaults to `SIGTERM`.
108 */
109 readonly kill: (options?: KillOptions | undefined) => Effect.Effect<void, PlatformError.PlatformError>
110 /**
111 * The standard input sink for the child process.
112 */
113 readonly stdin: Sink.Sink<void, Uint8Array, never, PlatformError.PlatformError>
114 /**
115 * The standard output stream for the child process.
116 *
117 * **Gotchas**
118 *
119 * Using this stream alongside `all` may cause interleaving of output and
120 * unexpected results.
121 */
122 readonly stdout: Stream.Stream<Uint8Array, PlatformError.PlatformError>
123 /**
124 * The standard error stream for the child process.
125 *
126 * **Gotchas**
127 *
128 * Using this stream alongside `all` may cause interleaving of output and
129 * unexpected results.
130 */
131 readonly stderr: Stream.Stream<Uint8Array, PlatformError.PlatformError>
132 /**
133 * A stream which combines and interleaves all messages output by the child
134 * process `stdout` and `stderr` streams.
135 */
136 readonly all: Stream.Stream<Uint8Array, PlatformError.PlatformError>
137 /**
138 * Get an input `Sink` for writing to a file descriptor configured via
139 * `ChildProcessOptions.additionalFds`.
140 *
141 * **Details**
142 *
143 * If a file descriptor is accessed that was not configured, returns a drain
144 * `Sink`.
145 */
146 readonly getInputFd: (fd: number) => Sink.Sink<void, Uint8Array, never, PlatformError.PlatformError>
147 /**
148 * Get an output `Stream` for reading from a file descriptor configured via
149 * `ChildProcessOptions.additionalFds`.
150 *
151 * **Details**
152 *
153 * If a file descriptor is accessed that was not configured, returns an empty
154 * `Stream`.
155 */
156 readonly getOutputFd: (fd: number) => Stream.Stream<Uint8Array, PlatformError.PlatformError>
157 /**
158 * Allows the parent process to exit independently of this child process.
159 *
160 * **Details**
161 *
162 * Running this `Effect` removes this child process from the parent process's
163 * reference count, so the parent process is allowed to exit without waiting
164 * for the child process to finish.
165 *
166 * The returned `Reref` effect adds the child process back into the parent
167 * process's reference count when run, restoring the default behavior.
168 *
169 * **Gotchas**
170 *
171 * This is the only supported way to re-reference a child process after it
172 * has been unrefed.
173 *
174 * **Example** (Temporarily unreferencing a child process)
175 *
176 * ```ts import.meta.vitest
177 * import { Effect } from "effect"
178 * import type { ChildProcessSpawner } from "effect/process"
179 *
180 * let referenced = true
181 *
182 * const unref: ChildProcessSpawner.ChildProcessHandle["unref"] = Effect.sync(() => {
183 * referenced = false
184 * const reref: ChildProcessSpawner.Reref = Effect.sync(() => {
185 * referenced = true
186 * })
187 * return reref
188 * })
189 *
190 * const program = Effect.gen(function*() {
191 * const states = [] as Array<boolean>
192 * const reref = yield* unref
193 * states.push(referenced)
194 *
195 * yield* reref
196 * states.push(referenced)
197 * return states
198 * })
199 *
200 * Effect.runSync(program) // => [false, true]
201 * ```
202 */
203 readonly unref: Effect.Effect<Reref, PlatformError.PlatformError>
204}
205
206const HandleProto = {
207 [HandleTypeId]: HandleTypeId,
208 ...Inspectable.BaseProto,
209 toJSON(this: ChildProcessHandle) {
210 return { _id: "ChildProcessHandle", pid: this.pid }
211 }
212}
213
214/**
215 * Constructs a new `ChildProcessHandle`.
216 *
217 * @stability unstable
218 * @category constructors
219 * @since 4.0.0
220 */
221export const makeHandle = (params: Omit<ChildProcessHandle, typeof HandleTypeId>): ChildProcessHandle =>
222 Object.setPrototypeOf({ ...params }, HandleProto)
223
224/**
225 * Creates a `ChildProcessSpawner` service from a `spawn` function, deriving
226 * helpers for exit codes and output collection from that implementation.
227 *
228 * @stability unstable
229 * @category constructors
230 * @since 4.0.0
231 */
232export const make = (spawn: ChildProcessSpawner["Service"]["spawn"]): ChildProcessSpawner["Service"] => {
233 const streamString: ChildProcessSpawner["Service"]["streamLines"] = (command, options) =>
234 spawn(command).pipe(
235 Effect.map((handle) =>
236 Stream.decodeText(
237 options?.includeStderr === true ? handle.all : handle.stdout
238 )
239 ),
240 Stream.unwrap
241 )
242 const streamLines: ChildProcessSpawner["Service"]["streamLines"] = (command, options) =>
243 Stream.splitLines(streamString(command, options))
244
245 return ChildProcessSpawner.of({
246 spawn,
247 exitCode: (command) => Effect.scoped(Effect.flatMap(spawn(command), (handle) => handle.exitCode)),
248 streamString,
249 streamLines,
250 lines: (command, options) => Stream.runCollect(streamLines(command, options)),
251 string: (command, options) => Stream.mkString(streamString(command, options))
252 })
253}
254
255/**
256 * Service tag for child process spawning.
257 *
258 * @stability unstable
259 * @category services
260 * @since 4.0.0
261 */
262export class ChildProcessSpawner extends Context.Service<ChildProcessSpawner, {
263 /**
264 * Spawn a command and return a handle for interaction.
265 */
266 spawn(
267 command: Command
268 ): Effect.Effect<ChildProcessHandle, PlatformError.PlatformError, Scope.Scope>
269
270 /**
271 * Run a command and return its exit code.
272 */
273 exitCode(
274 command: Command
275 ): Effect.Effect<ExitCode, PlatformError.PlatformError>
276
277 /**
278 * Stream the output of a command as strings. Optionally include stderr output
279 * interleaved with stdout.
280 */
281 streamString(command: Command, options?: {
282 readonly includeStderr?: boolean | undefined
283 }): Stream.Stream<string, PlatformError.PlatformError>
284
285 /**
286 * Stream the output of a command as lines. Optionally include stderr output
287 * interleaved with stdout.
288 */
289 streamLines(command: Command, options?: {
290 readonly includeStderr?: boolean | undefined
291 }): Stream.Stream<string, PlatformError.PlatformError>
292
293 /**
294 * Run a command and return the lines of its output as an array of strings.
295 */
296 lines(command: Command, options?: {
297 readonly includeStderr?: boolean | undefined
298 }): Effect.Effect<Array<string>, PlatformError.PlatformError>
299
300 /**
301 * Run a command and return its output as a string.
302 */
303 string(command: Command, options?: {
304 readonly includeStderr?: boolean | undefined
305 }): Effect.Effect<string, PlatformError.PlatformError>
306}>()("effect/process/ChildProcessSpawner") {}