Skip to main content

workflow_node/
child_process.rs

1use crate::node_sys::*;
2use crate::require;
3use js_sys::{Array, Object};
4use lazy_static::lazy_static;
5use wasm_bindgen::prelude::*;
6use workflow_log::log_info;
7
8lazy_static! {
9    static ref CP: Cp = require("child_process").unchecked_into();
10}
11
12#[wasm_bindgen]
13extern "C" {
14
15    /// Binding to the Node.js `child_process` module, used to spawn child
16    /// processes.
17    #[wasm_bindgen(extends = Object)]
18    #[derive(Clone)]
19    pub type Cp;
20
21    /// Spawns a child process running `cmd` with no arguments
22    /// (`child_process.spawn`).
23    #[wasm_bindgen(js_name = spawn, method)]
24    pub fn cp_spawn(this: &Cp, cmd: &str) -> ChildProcess;
25
26    /// Spawns a child process running `cmd` with the given `args`
27    /// (`child_process.spawn`).
28    #[wasm_bindgen(js_name = spawn, method)]
29    pub fn cp_spawn_with_args(this: &Cp, cmd: &str, args: &SpawnArgs) -> ChildProcess;
30
31    /// Spawns a child process running `cmd` with the given `args` and spawn
32    /// `options` (`child_process.spawn`).
33    #[wasm_bindgen(js_name = spawn, method)]
34    pub fn cp_spawn_with_args_and_options(
35        this: &Cp,
36        cmd: &str,
37        args: &SpawnArgs,
38        options: &SpawnOptions,
39    ) -> ChildProcess;
40
41    /// JavaScript array of command-line arguments passed to a spawned process.
42    #[wasm_bindgen(extends = Array, js_namespace = child_process)]
43    #[derive(Debug, Clone, PartialEq)]
44    pub type SpawnArgs;
45
46    /// JavaScript options object (cwd, env, stdio, …) for spawning a process.
47    #[wasm_bindgen(extends = Object, js_namespace = child_process)]
48    #[derive(Debug, Clone, PartialEq)]
49    pub type SpawnOptions;
50
51    /// Handle to a spawned child process (Node.js `ChildProcess`), an
52    /// [`EventEmitter`] exposing its streams and lifecycle.
53    #[wasm_bindgen(extends = EventEmitter, js_namespace = child_process)]
54    #[derive(Clone, Debug)]
55    pub type ChildProcess;
56
57    /// The process's exit code (valid once it has exited).
58    #[wasm_bindgen(method, getter)]
59    pub fn exit_code(this: &ChildProcess) -> u64;
60
61    /// The operating-system process identifier.
62    #[wasm_bindgen(method, getter)]
63    pub fn pid(this: &ChildProcess) -> u64;
64
65    /// The process's standard output stream.
66    #[wasm_bindgen(method, getter)]
67    pub fn stdout(this: &ChildProcess) -> ReadableStream;
68
69    /// The process's standard error stream.
70    #[wasm_bindgen(method, getter)]
71    pub fn stderr(this: &ChildProcess) -> ReadableStream;
72
73    /// The process's standard input stream.
74    #[wasm_bindgen(method, getter)]
75    pub fn stdin(this: &ChildProcess) -> WritableStream;
76
77    /// Sends the default termination signal to the process; returns `true` if
78    /// the signal was delivered successfully.
79    #[wasm_bindgen(method)]
80    pub fn kill(this: &ChildProcess) -> bool;
81
82    #[wasm_bindgen(method, js_name=kill)]
83    fn kill_with_signal_impl(this: &ChildProcess, signal: JsValue) -> bool;
84}
85
86unsafe impl Send for Cp {}
87unsafe impl Sync for Cp {}
88
89unsafe impl Send for ChildProcess {}
90unsafe impl Sync for ChildProcess {}
91
92unsafe impl Send for SpawnOptions {}
93unsafe impl Sync for SpawnOptions {}
94
95unsafe impl Send for SpawnArgs {}
96unsafe impl Sync for SpawnArgs {}
97
98/// Spawns a new child process running `cmd` with no arguments, returning a
99/// handle to the resulting [`ChildProcess`].
100#[inline(always)]
101pub fn spawn(cmd: &str) -> ChildProcess {
102    CP.cp_spawn(cmd)
103}
104
105/// Spawns a new child process running `cmd` with the given `args`, returning a
106/// handle to the resulting [`ChildProcess`].
107#[inline(always)]
108pub fn spawn_with_args(cmd: &str, args: &SpawnArgs) -> ChildProcess {
109    CP.cp_spawn_with_args(cmd, args)
110}
111
112/// Spawns a new child process running `cmd` with the given `args` and
113/// `options`, returning a handle to the resulting [`ChildProcess`].
114#[inline(always)]
115pub fn spawn_with_args_and_options(
116    cmd: &str,
117    args: &SpawnArgs,
118    options: &SpawnOptions,
119) -> ChildProcess {
120    CP.cp_spawn_with_args_and_options(cmd, args, options)
121}
122
123/// Signal to send to a child process when terminating it via
124/// [`ChildProcess::kill_with_signal`].
125#[derive(Debug)]
126pub enum KillSignal<'s> {
127    /// Send the default termination signal.
128    None,
129    /// Send `SIGKILL`, forcibly terminating the process.
130    SIGKILL,
131    /// Send `SIGTERM`, requesting a graceful termination.
132    SIGTERM,
133    /// Send a signal identified by name (e.g. `"SIGHUP"`).
134    Message(&'s str),
135    /// Send a signal identified by its numeric value.
136    Code(u32),
137}
138
139impl ChildProcess {
140    /// Sends a termination request to the child process using the given
141    /// [`KillSignal`], returning whether the signal was delivered.
142    pub fn kill_with_signal(self: &ChildProcess, signal: KillSignal) -> bool {
143        log_info!("kill_with_signal {:?}", signal);
144        match signal {
145            KillSignal::None => self.kill(),
146            KillSignal::SIGKILL => self.kill_with_signal_impl(JsValue::from("SIGKILL")),
147            KillSignal::SIGTERM => self.kill_with_signal_impl(JsValue::from("SIGTERM")),
148            KillSignal::Message(str) => self.kill_with_signal_impl(JsValue::from(str)),
149            KillSignal::Code(code) => self.kill_with_signal_impl(JsValue::from(code)),
150        }
151    }
152}
153
154impl From<Vec<&str>> for SpawnArgs {
155    fn from(list: Vec<&str>) -> Self {
156        let array = Array::new();
157        for (index, value) in list.iter().enumerate() {
158            array.set(index as u32, JsValue::from(*value));
159        }
160
161        #[allow(unused_mut)]
162        let mut args: Self = ::wasm_bindgen::JsCast::unchecked_into(array);
163        args
164    }
165}
166
167impl From<&[&str]> for SpawnArgs {
168    fn from(list: &[&str]) -> Self {
169        let array = Array::new();
170        for (index, value) in list.iter().enumerate() {
171            array.set(index as u32, JsValue::from(*value));
172        }
173
174        #[allow(unused_mut)]
175        let mut args: Self = ::wasm_bindgen::JsCast::unchecked_into(array);
176        args
177    }
178}
179
180impl From<&[String]> for SpawnArgs {
181    fn from(list: &[String]) -> Self {
182        let array = Array::new();
183        for (index, value) in list.iter().enumerate() {
184            array.set(index as u32, JsValue::from(value));
185        }
186
187        #[allow(unused_mut)]
188        let mut args: Self = ::wasm_bindgen::JsCast::unchecked_into(array);
189        args
190    }
191}
192
193impl Default for SpawnOptions {
194    fn default() -> Self {
195        Self::new()
196    }
197}
198
199impl SpawnOptions {
200    /// "Construct a new `SpawnOptions`.
201    ///
202    /// [NODEJS Documentation](https://nodejs.org/api/child_process.html#child_processspawncommand-args-options)
203    pub fn new() -> Self {
204        #[allow(unused_mut)]
205        let mut ret: Self = ::wasm_bindgen::JsCast::unchecked_into(Object::new());
206        ret
207    }
208
209    /// Sets an arbitrary option `key` to `value` on the underlying options
210    /// object, returning `self` for chaining.
211    pub fn set(&self, key: &str, value: JsValue) -> &Self {
212        let r = ::js_sys::Reflect::set(self.as_ref(), &JsValue::from(key), &value);
213        debug_assert!(
214            r.is_ok(),
215            "setting properties should never fail on our dictionary objects"
216        );
217        let _ = r;
218        self
219    }
220
221    /// Sets the current working directory of the child process.
222    pub fn cwd(&self, cwd: &str) -> &Self {
223        self.set("cwd", JsValue::from(cwd))
224    }
225
226    /// Sets the environment variables exposed to the child process.
227    pub fn env(&self, env: ProcessEnv) -> &Self {
228        self.set("env", JsValue::from(env))
229    }
230
231    /// Overrides the value sent to the child as `argv[0]` (the process name).
232    pub fn argv0(&self, argv0: &str) -> &Self {
233        self.set("argv0", JsValue::from(argv0))
234    }
235
236    /// Runs the child in its own process group, detached from the parent, when
237    /// `true`.
238    pub fn detached(&self, detached: bool) -> &Self {
239        self.set("detached", JsValue::from(detached))
240    }
241
242    /// Sets the user identity under which the child process is run.
243    pub fn uid(&self, uid: &str) -> &Self {
244        self.set("uid", JsValue::from(uid))
245    }
246
247    /// Sets the group identity under which the child process is run.
248    pub fn gid(&self, gid: &str) -> &Self {
249        self.set("gid", JsValue::from(gid))
250    }
251
252    /// Sets the serialization format used for messages exchanged with the
253    /// child process (e.g. `"json"` or `"advanced"`).
254    pub fn serialization(&self, serialization: &str) -> &Self {
255        self.set("serialization", JsValue::from(serialization))
256    }
257
258    /// Runs the command inside a shell when `true`, using the platform's
259    /// default shell.
260    pub fn shell(&self, shell: bool) -> &Self {
261        self.set("shell", JsValue::from(shell))
262    }
263
264    /// Runs the command inside the shell at the given path.
265    pub fn shell_str(&self, shell: &str) -> &Self {
266        self.set("shell", JsValue::from(shell))
267    }
268
269    /// Controls whether arguments are passed verbatim (without automatic
270    /// quoting/escaping) on Windows.
271    pub fn windows_verbatim_arguments(&self, args: bool) -> &Self {
272        self.set("windowsVerbatimArguments", JsValue::from(args))
273    }
274
275    /// Hides the subprocess console window that would normally be created on
276    /// Windows.
277    pub fn windows_hide(&self, windows_hide: bool) -> &Self {
278        self.set("windowsHide", JsValue::from(windows_hide))
279    }
280
281    /// Sets the maximum time, in milliseconds, the process is allowed to run
282    /// before it is killed.
283    pub fn timeout(&self, timeout: u32) -> &Self {
284        self.set("timeout", JsValue::from(timeout))
285    }
286
287    // TODO: AbortSignal
288
289    /// Sets the signal used to terminate the child when it is killed, as a
290    /// numeric signal value.
291    pub fn kill_signal(&self, signal: u32) -> &Self {
292        self.set("killSignal", JsValue::from(signal))
293    }
294
295    /// Sets the signal used to terminate the child when it is killed, as a
296    /// signal name (e.g. `"SIGTERM"`).
297    pub fn kill_signal_str(&self, signal: &str) -> &Self {
298        self.set("killSignal", JsValue::from(signal))
299    }
300
301    /// Sets the child's stdio configuration using a shorthand string
302    /// (e.g. `"pipe"`, `"inherit"`, `"ignore"`).
303    pub fn stdio(&self, stdio: &str) -> &Self {
304        self.set("stdio", JsValue::from(stdio))
305    }
306
307    /// Sets the child's stdio configuration from an array describing each
308    /// standard stream (stdin, stdout, stderr, and any extras) individually.
309    pub fn stdio_with_array(&self, array: js_sys::Array) -> &Self {
310        self.set("stdio", array.into())
311    }
312}