char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somport process extension to typescriptb2fc926

main
15.5 KiB437 linesraw
1#[path = "supervision.rs"]
2pub(super) mod supervision;
3
4use super::sys;
5use avril_subprocess::{Options, Status, StdioSpec};
6use std::cell::RefCell;
7use std::collections::{BTreeMap, HashMap};
8use std::convert::Infallible;
9use std::io;
10use std::path::Path;
11use std::rc::Rc;
12use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
13use std::sync::{Arc, Mutex};
14use std::time::{Duration, Instant};
15
16use avril_caps::Capabilities;
17use avril_core::serde_v8::{JsBuffer, ToJsBuffer};
18use avril_core::{Extension, Op};
19use avril_host::environment::Environment;
20use serde::Serialize;
21use tokio::runtime::Handle;
22use tokio_util::sync::CancellationToken;
23
24use avril_ext_file::{Files, process_file};
25use avril_host::io::response;
26
27const MAX_ID: u64 = 9_007_199_254_740_991;
28
29fn invalid(message: &str) -> io::Error {
30    io::Error::new(io::ErrorKind::InvalidInput, message)
31}
32fn missing() -> io::Error {
33    io::Error::new(io::ErrorKind::NotConnected, "Subprocess handle is closed")
34}
35
36#[derive(Serialize)]
37#[serde(rename_all = "camelCase")]
38struct SpawnInfo {
39    id: u64,
40    pid: u32,
41    readable: Vec<u32>,
42    writable: Vec<u32>,
43    terminal: bool,
44}
45#[derive(Serialize)]
46struct SyncOutput {
47    status: Status,
48    stdout: Option<ToJsBuffer>,
49    stderr: Option<ToJsBuffer>,
50}
51
52struct Process {
53    native: Arc<sys::Process>,
54    /// Set while a thread is blocked inside run_sync on this process, so the
55    /// REPL can interrupt exactly the subprocess its evaluation is waiting on.
56    sync_wait: AtomicBool,
57    worker: bool,
58}
59impl std::ops::Deref for Process {
60    type Target = sys::Process;
61    fn deref(&self) -> &Self::Target {
62        &self.native
63    }
64}
65
66struct Registry {
67    runtime: Handle,
68    caps: Arc<Capabilities>,
69    files: Arc<Files>,
70    processes: Mutex<HashMap<u64, Arc<Process>>>,
71    next: AtomicU64,
72    owner: CancellationToken,
73    tree: Arc<supervision::Tree>,
74    worker: bool,
75    signal: super::SignalMode,
76    signals: Arc<super::signals::State>,
77}
78impl Registry {
79    fn get(&self, id: u64) -> io::Result<Arc<Process>> {
80        self.processes
81            .lock()
82            .unwrap()
83            .get(&id)
84            .cloned()
85            .ok_or_else(missing)
86    }
87    fn release(&self, id: u64) {
88        if let Some(process) = self.processes.lock().unwrap().remove(&id) {
89            process.request("dispose", None, false);
90        }
91    }
92    fn spawn(&self, options: Options, environment: &Environment) -> io::Result<SpawnInfo> {
93        if self.owner.is_cancelled() || self.tree.stopped.is_cancelled() || self.tree.is_closed() {
94            return Err(missing());
95        }
96        if self.worker && options.stdin.mode == "inherit" {
97            return Err(io::Error::new(
98                io::ErrorKind::Unsupported,
99                "Worker subprocesses cannot inherit host stdin",
100            ));
101        }
102        // Only run mode owns SIGINT; serve and the REPL leave it to their
103        // own handlers. SIGTERM fate belongs to whoever claims it, if anyone.
104        self.tree
105            .install(&self.runtime, matches!(self.signal, super::SignalMode::Run))?;
106        let id = self
107            .next
108            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| {
109                (id <= MAX_ID).then_some(id + 1)
110            })
111            .map_err(|_| io::Error::other("Subprocess handle space exhausted"))?;
112        let terminal = options.terminal.is_some();
113        let native = sys::Launcher {
114            runtime: self.runtime.clone(),
115            caps: self.caps.clone(),
116            owners: vec![self.owner.clone(), self.tree.stopped.clone()],
117        }
118        .spawn(
119            options,
120            environment
121                .to_object(&self.caps)
122                .into_iter()
123                .map(|(k, v)| (k.into(), v.into())),
124            None,
125            |spec, read, cwd| self.file(spec, read, cwd),
126        )?;
127        let process = Arc::new(Process {
128            native,
129            sync_wait: AtomicBool::new(false),
130            worker: self.worker,
131        });
132        let info = SpawnInfo {
133            id,
134            pid: process.pid,
135            terminal,
136            readable: process.outputs.keys().copied().collect(),
137            writable: process.inputs.keys().copied().collect(),
138        };
139        self.processes.lock().unwrap().insert(id, process.clone());
140        self.tree.register(&process);
141        Ok(info)
142    }
143    fn file(&self, spec: &StdioSpec, read: bool, cwd: &Path) -> io::Result<std::fs::File> {
144        if let Some(id) = spec.handle {
145            return self.files.duplicate_for_process(id, read, !read);
146        }
147        let path = cwd.join(
148            spec.file
149                .as_ref()
150                .ok_or_else(|| invalid("Missing stdio file"))?,
151        );
152        let flags = if read {
153            (true, false, false, false, false, false, None)
154        } else {
155            match spec.file_mode.as_deref() {
156                Some("append") => (false, true, true, false, true, false, None),
157                Some("truncate") => (false, true, false, true, true, false, None),
158                Some("create-new") => (false, true, false, false, false, true, None),
159                _ => return Err(invalid("Invalid stdio file mode")),
160            }
161        };
162        process_file(
163            self.caps.clone(),
164            path.to_str()
165                .ok_or_else(|| invalid("Stdio paths must be UTF-8"))?
166                .into(),
167            flags,
168        )
169    }
170}
171impl Drop for Registry {
172    fn drop(&mut self) {
173        // Release signal bindings first, so a dying isolate stops pinning
174        // listened signals process-wide before processes drain.
175        self.signals.shutdown();
176        let processes: Vec<_> = self
177            .processes
178            .get_mut()
179            .unwrap()
180            .drain()
181            .map(|(_, process)| process)
182            .collect();
183        for process in &processes {
184            process.request("owner", None, true);
185        }
186        let deadline = Instant::now() + Duration::from_secs(2);
187        for process in processes {
188            process.wait_blocking(deadline.saturating_duration_since(Instant::now()));
189        }
190    }
191}
192
193pub fn init(
194    runtime: Handle,
195    caps: Arc<Capabilities>,
196    environment: Rc<RefCell<Environment>>,
197    files: Arc<Files>,
198    owner: CancellationToken,
199    worker: bool,
200    signal: super::SignalMode,
201) -> Extension {
202    let tree = supervision::Tree::global();
203    let signals = super::signals::State::create(runtime.clone(), worker);
204    let registry = Arc::new(Registry {
205        runtime,
206        caps,
207        files,
208        processes: Mutex::new(HashMap::new()),
209        next: AtomicU64::new(1),
210        owner,
211        tree: tree.clone(),
212        worker,
213        signal,
214        signals: signals.clone(),
215    });
216    let mut ops = Vec::new();
217    ops.extend(super::signals::ops(&signals));
218    ops.push(Op::new("op_process_spawn", {
219        let registry = registry.clone();
220        let environment = environment.clone();
221        move |options: Options| {
222            Ok::<_, Infallible>(response(registry.spawn(options, &environment.borrow())))
223        }
224    }));
225    ops.push(Op::new("op_process_run_sync", {
226        let registry = registry.clone();
227        let environment = environment.clone();
228        move |(mut options, input): (Options, Option<JsBuffer>)| {
229            let generation = registry.tree.sync_generation();
230            // The blocking reader below drains both streams; backpressure keeps
231            // a burst from outrunning it into the unread limit.
232            options.backpressure = [(1, &options.stdout), (2, &options.stderr)]
233                .into_iter()
234                .filter(|(_, spec)| spec.mode == "piped")
235                .map(|(fd, _)| fd)
236                .collect();
237            let input = input.map(|bytes| bytes.to_vec());
238            let result = (|| {
239                let info = registry.spawn(options, &environment.borrow())?;
240                let process = registry.get(info.id)?;
241                process.sync_wait.store(true, Ordering::SeqCst);
242                if !registry.worker && registry.tree.sync_generation() != generation {
243                    process.request("terminated", None, false);
244                }
245                let output = async {
246                    let write = async {
247                        if let Some(pipe) = process.inputs.get(&0) {
248                            if let Some(bytes) = input
249                                && !bytes.is_empty()
250                                && let Err(error) = pipe.write(bytes).await
251                                && error.kind() != io::ErrorKind::BrokenPipe
252                            {
253                                process.request("io-error", Some(error.to_string()), false);
254                            }
255                            pipe.close().await;
256                        }
257                    };
258                    let reads = futures::future::join_all(process.outputs.iter().map(
259                        |(fd, pipe)| async move {
260                            let mut bytes = Vec::new();
261                            while let Some(chunk) = pipe.read().await {
262                                bytes.extend_from_slice(&chunk);
263                            }
264                            (*fd, bytes)
265                        },
266                    ));
267                    let (_, bytes) = futures::future::join(write, reads).await;
268                    let status = process.wait(true, None).await?.ok_or_else(missing)?;
269                    let mut bytes: BTreeMap<_, _> = bytes.into_iter().collect();
270                    Ok::<_, io::Error>(SyncOutput {
271                        status,
272                        stdout: bytes.remove(&1).map(ToJsBuffer::from),
273                        stderr: bytes.remove(&2).map(ToJsBuffer::from),
274                    })
275                };
276                // Sync ops may run inside a Tokio-driven JS event loop.
277                let _enter = registry.runtime.enter();
278                let output = avril_host::wait(output, || false).unwrap();
279                process.sync_wait.store(false, Ordering::SeqCst);
280                registry.release(info.id);
281                output
282            })();
283            Ok::<_, Infallible>(response(result))
284        }
285    }));
286    ops.push(Op::asynchronous_unreferenced("op_process_read", {
287        let registry = registry.clone();
288        move |(id, fd): (u64, u32)| {
289            let output = registry
290                .get(id)
291                .and_then(|process| process.outputs.get(&fd).cloned().ok_or_else(missing));
292            async move {
293                Ok::<_, Infallible>(response(match output {
294                    Ok(output) => Ok(output
295                        .read()
296                        .await
297                        .map(|bytes| ToJsBuffer::from(bytes.to_vec()))),
298                    Err(error) => Err(error),
299                }))
300            }
301        }
302    }));
303    ops.push(Op::asynchronous_unreferenced("op_process_write", {
304        let registry = registry.clone();
305        move |(id, fd, bytes): (u64, u32, JsBuffer)| {
306            let bytes = bytes.to_vec();
307            let input = registry
308                .get(id)
309                .and_then(|process| process.inputs.get(&fd).cloned().ok_or_else(missing));
310            async move {
311                Ok::<_, Infallible>(response(match input {
312                    Ok(input) => {
313                        let result = input.write(bytes).await;
314                        if result
315                            .as_ref()
316                            .is_err_and(|error| error.kind() == io::ErrorKind::BrokenPipe)
317                        {
318                            input.close().await;
319                        }
320                        result
321                    }
322                    Err(error) => Err(error),
323                }))
324            }
325        }
326    }));
327    ops.push(Op::asynchronous_unreferenced("op_process_end", {
328        let registry = registry.clone();
329        move |(id, fd): (u64, u32)| {
330            let input = registry
331                .get(id)
332                .and_then(|process| process.inputs.get(&fd).cloned().ok_or_else(missing));
333            async move {
334                Ok::<_, Infallible>(response(match input {
335                    Ok(input) => {
336                        input.close().await;
337                        Ok(())
338                    }
339                    Err(error) => Err(error),
340                }))
341            }
342        }
343    }));
344    ops.push(Op::new("op_process_cancel_read", {
345        let registry = registry.clone();
346        move |(id, fd): (u64, u32)| {
347            let result = registry.get(id).and_then(|process| {
348                process.outputs.get(&fd).ok_or_else(missing)?.cancel();
349                Ok(())
350            });
351            Ok::<_, Infallible>(response(result))
352        }
353    }));
354    ops.push(Op::asynchronous_unreferenced("op_process_wait", {
355        let registry = registry.clone();
356        move |(id, completed): (u64, bool)| {
357            let process = registry.get(id);
358            async move {
359                Ok::<_, Infallible>(response(match process {
360                    Ok(process) => process
361                        .wait(completed, None)
362                        .await
363                        .and_then(|status| status.ok_or_else(missing)),
364                    Err(error) => Err(error),
365                }))
366            }
367        }
368    }));
369    ops.push(Op::asynchronous("op_process_keepalive", {
370        let registry = registry.clone();
371        move |(id, epoch): (u64, u32)| {
372            let process = registry.get(id);
373            async move {
374                Ok::<_, Infallible>(response(match process {
375                    Ok(process) => process.wait(true, Some(epoch)).await.map(|_| ()),
376                    Err(error) => Err(error),
377                }))
378            }
379        }
380    }));
381    ops.push(Op::new("op_process_ref", {
382        let registry = registry.clone();
383        move |(id, _referenced): (u64, bool)| {
384            let result = registry.get(id).map(|process| process.invalidate_waiters());
385            Ok::<_, Infallible>(response(result))
386        }
387    }));
388    ops.push(Op::new("op_process_kill", {
389        let registry = registry.clone();
390        move |(id, signal): (u64, String)| {
391            Ok::<_, Infallible>(response(
392                registry.get(id).and_then(|process| process.kill(&signal)),
393            ))
394        }
395    }));
396    ops.push(Op::new("op_process_stop", {
397        let registry = registry.clone();
398        move |(id, reason): (u64, String)| {
399            let result = registry
400                .get(id)
401                .map(|process| process.request(&reason, None, false));
402            Ok::<_, Infallible>(response(result))
403        }
404    }));
405    ops.push(Op::new("op_process_release", {
406        let registry = registry.clone();
407        move |id: u64| {
408            registry.release(id);
409            Ok::<_, Infallible>(())
410        }
411    }));
412    ops.push(Op::new("op_process_resize", {
413        let registry = registry.clone();
414        move |(id, columns, rows): (u64, u16, u16)| {
415            let result = registry
416                .get(id)
417                .and_then(|process| process.resize(columns, rows));
418            Ok::<_, Infallible>(response(result))
419        }
420    }));
421    Extension {
422        name: "ext:process",
423        dependencies: &["ext:encoding", "ext:file", "ext:web", "ext:webstreams"],
424        ops,
425        modules: &[
426            (
427                "ext:process/main.js",
428                include_str!(concat!(env!("OUT_DIR"), "/process.js")),
429            ),
430            (
431                "ext:process/signals.js",
432                include_str!(concat!(env!("OUT_DIR"), "/signals.js")),
433            ),
434        ],
435        entry_point: None,
436    }
437}