1use crate::marshal::{
20 register_typed_fn_0, register_typed_fn_1, register_typed_fn_2_full,
21};
22use crate::module_exports::{ModuleExports, ModuleParam};
23use crate::typed_module_exports::{ConcreteReturn, ConcreteType, TypedReturn};
24use shape_value::heap_value::{HeapValue, IoHandleData, IoResource};
25use std::io::{BufRead, BufReader, Read, Write};
26use std::process::{Command, Stdio};
27use std::sync::Arc;
28
29fn build_command_with_args(
41 cmd: &str,
42 args: Option<Vec<Arc<HeapValue>>>,
43 fn_name: &str,
44) -> Result<Command, String> {
45 let mut command = Command::new(cmd);
46 if let Some(arg_list) = args {
47 for elem in arg_list {
48 match &*elem {
49 HeapValue::String(s) => {
50 command.arg(&**s);
51 }
52 other => {
53 return Err(format!(
54 "{}: each element of args must be a string, got {}",
55 fn_name,
56 other.type_name()
57 ));
58 }
59 }
60 }
61 }
62 Ok(command)
63}
64
65pub fn register_process_io(module: &mut ModuleExports) {
69 register_typed_fn_2_full::<_, Arc<String>, Vec<Arc<HeapValue>>>(
71 module,
72 "spawn",
73 "Spawn a subprocess with piped stdin/stdout/stderr",
74 [
75 ModuleParam {
76 name: "cmd".to_string(),
77 type_name: "string".to_string(),
78 required: true,
79 description: "Command to run".to_string(),
80 ..Default::default()
81 },
82 ModuleParam {
83 name: "args".to_string(),
84 type_name: "Array<string>".to_string(),
85 required: false,
86 description: "Optional command arguments".to_string(),
87 default_snippet: Some("[]".to_string()),
88 ..Default::default()
89 },
90 ],
91 ConcreteType::IoHandle,
92 |cmd, args, ctx| {
93 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Process)?;
94 let cmd_s = cmd.as_str();
95 let mut command = build_command_with_args(cmd_s, Some(args), "io.spawn()")?;
96 command
97 .stdin(Stdio::piped())
98 .stdout(Stdio::piped())
99 .stderr(Stdio::piped());
100 let child = command
101 .spawn()
102 .map_err(|e| format!("io.spawn(\"{}\"): {}", cmd_s, e))?;
103 let handle = IoHandleData::new_child_process(child, cmd_s.to_string());
104 Ok(TypedReturn::Concrete(ConcreteReturn::IoHandle(Arc::new(
105 handle,
106 ))))
107 },
108 );
109
110 register_typed_fn_2_full::<_, Arc<String>, Vec<Arc<HeapValue>>>(
112 module,
113 "exec",
114 "Run a command to completion and capture its output",
115 [
116 ModuleParam {
117 name: "cmd".to_string(),
118 type_name: "string".to_string(),
119 required: true,
120 description: "Command to run".to_string(),
121 ..Default::default()
122 },
123 ModuleParam {
124 name: "args".to_string(),
125 type_name: "Array<string>".to_string(),
126 required: false,
127 description: "Optional command arguments".to_string(),
128 default_snippet: Some("[]".to_string()),
129 ..Default::default()
130 },
131 ],
132 ConcreteType::TypedObject,
133 |cmd, args, ctx| {
134 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Process)?;
135 let cmd_s = cmd.as_str();
136 let mut command = build_command_with_args(cmd_s, Some(args), "io.exec()")?;
137 let output = command
138 .output()
139 .map_err(|e| format!("io.exec(\"{}\"): {}", cmd_s, e))?;
140 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
141 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
142 let status = output.status.code().unwrap_or(-1) as i64;
143 Ok(TypedReturn::TypedObject(vec![
144 ("status".to_string(), ConcreteReturn::I64(status)),
145 ("stdout".to_string(), ConcreteReturn::String(stdout)),
146 ("stderr".to_string(), ConcreteReturn::String(stderr)),
147 ]))
148 },
149 );
150
151 register_typed_fn_1::<_, Arc<IoHandleData>>(
153 module,
154 "process_wait",
155 "Wait for a child process to exit and return its exit code",
156 "handle",
157 "IoHandle",
158 ConcreteType::Int,
159 |handle, ctx| {
160 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Process)?;
161 let mut guard = handle
162 .resource
163 .lock()
164 .map_err(|_| "io.process_wait(): lock poisoned".to_string())?;
165 let resource = guard
166 .as_mut()
167 .ok_or_else(|| "io.process_wait(): handle is closed".to_string())?;
168 match resource {
169 IoResource::ChildProcess(child) => {
170 let status = child
171 .wait()
172 .map_err(|e| format!("io.process_wait(): {}", e))?;
173 Ok(TypedReturn::Concrete(ConcreteReturn::I64(
174 status.code().unwrap_or(-1) as i64,
175 )))
176 }
177 _ => Err("io.process_wait(): handle is not a ChildProcess".to_string()),
178 }
179 },
180 );
181
182 register_typed_fn_1::<_, Arc<IoHandleData>>(
184 module,
185 "process_kill",
186 "Kill a running child process",
187 "handle",
188 "IoHandle",
189 ConcreteType::Unit,
190 |handle, ctx| {
191 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Process)?;
192 let mut guard = handle
193 .resource
194 .lock()
195 .map_err(|_| "io.process_kill(): lock poisoned".to_string())?;
196 let resource = guard
197 .as_mut()
198 .ok_or_else(|| "io.process_kill(): handle is closed".to_string())?;
199 match resource {
200 IoResource::ChildProcess(child) => {
201 child
202 .kill()
203 .map_err(|e| format!("io.process_kill(): {}", e))?;
204 Ok(TypedReturn::Concrete(ConcreteReturn::Unit))
205 }
206 _ => Err("io.process_kill(): handle is not a ChildProcess".to_string()),
207 }
208 },
209 );
210
211 register_typed_fn_2_full::<_, Arc<IoHandleData>, Arc<String>>(
213 module,
214 "process_write",
215 "Write to a child process's stdin, returning bytes written",
216 [
217 ModuleParam {
218 name: "handle".to_string(),
219 type_name: "IoHandle".to_string(),
220 required: true,
221 description: "ChildProcess or PipeWriter handle".to_string(),
222 ..Default::default()
223 },
224 ModuleParam {
225 name: "data".to_string(),
226 type_name: "string".to_string(),
227 required: true,
228 description: "Data to write".to_string(),
229 ..Default::default()
230 },
231 ],
232 ConcreteType::Int,
233 |handle, data, ctx| {
234 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Process)?;
235 let mut guard = handle
236 .resource
237 .lock()
238 .map_err(|_| "io.process_write(): lock poisoned".to_string())?;
239 let resource = guard
240 .as_mut()
241 .ok_or_else(|| "io.process_write(): handle is closed".to_string())?;
242 match resource {
243 IoResource::ChildProcess(child) => {
244 let stdin = child
245 .stdin
246 .as_mut()
247 .ok_or_else(|| "io.process_write(): stdin pipe not available".to_string())?;
248 let written = stdin
249 .write(data.as_bytes())
250 .map_err(|e| format!("io.process_write(): {}", e))?;
251 Ok(TypedReturn::Concrete(ConcreteReturn::I64(written as i64)))
252 }
253 IoResource::PipeWriter(stdin) => {
254 let written = stdin
255 .write(data.as_bytes())
256 .map_err(|e| format!("io.process_write(): {}", e))?;
257 Ok(TypedReturn::Concrete(ConcreteReturn::I64(written as i64)))
258 }
259 _ => Err(
260 "io.process_write(): handle is not a ChildProcess or PipeWriter".to_string(),
261 ),
262 }
263 },
264 );
265
266 register_typed_fn_2_full::<_, Arc<IoHandleData>, i64>(
268 module,
269 "process_read",
270 "Read up to n bytes from a child process's stdout",
271 [
272 ModuleParam {
273 name: "handle".to_string(),
274 type_name: "IoHandle".to_string(),
275 required: true,
276 description: "ChildProcess or PipeReader handle".to_string(),
277 ..Default::default()
278 },
279 ModuleParam {
280 name: "n".to_string(),
281 type_name: "int".to_string(),
282 required: false,
283 description: "Max bytes to read (default: 65536)".to_string(),
284 default_snippet: Some("65536".to_string()),
285 ..Default::default()
286 },
287 ],
288 ConcreteType::String,
289 |handle, n, ctx| {
290 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Process)?;
291 let buf_size = if n > 0 { n as usize } else { 65536 };
292 let mut guard = handle
293 .resource
294 .lock()
295 .map_err(|_| "io.process_read(): lock poisoned".to_string())?;
296 let resource = guard
297 .as_mut()
298 .ok_or_else(|| "io.process_read(): handle is closed".to_string())?;
299 let s = match resource {
300 IoResource::ChildProcess(child) => {
301 let stdout = child
302 .stdout
303 .as_mut()
304 .ok_or_else(|| "io.process_read(): stdout pipe not available".to_string())?;
305 let mut buf = vec![0u8; buf_size];
306 let bytes_read = stdout
307 .read(&mut buf)
308 .map_err(|e| format!("io.process_read(): {}", e))?;
309 buf.truncate(bytes_read);
310 String::from_utf8(buf)
311 .map_err(|e| format!("io.process_read(): invalid UTF-8: {}", e))?
312 }
313 IoResource::PipeReader(stdout) => {
314 let mut buf = vec![0u8; buf_size];
315 let bytes_read = stdout
316 .read(&mut buf)
317 .map_err(|e| format!("io.process_read(): {}", e))?;
318 buf.truncate(bytes_read);
319 String::from_utf8(buf)
320 .map_err(|e| format!("io.process_read(): invalid UTF-8: {}", e))?
321 }
322 _ => {
323 return Err(
324 "io.process_read(): handle is not a ChildProcess or PipeReader"
325 .to_string(),
326 );
327 }
328 };
329 Ok(TypedReturn::Concrete(ConcreteReturn::String(s)))
330 },
331 );
332
333 register_typed_fn_2_full::<_, Arc<IoHandleData>, i64>(
335 module,
336 "process_read_err",
337 "Read up to n bytes from a child process's stderr",
338 [
339 ModuleParam {
340 name: "handle".to_string(),
341 type_name: "IoHandle".to_string(),
342 required: true,
343 description: "ChildProcess or PipeReaderErr handle".to_string(),
344 ..Default::default()
345 },
346 ModuleParam {
347 name: "n".to_string(),
348 type_name: "int".to_string(),
349 required: false,
350 description: "Max bytes to read (default: 65536)".to_string(),
351 default_snippet: Some("65536".to_string()),
352 ..Default::default()
353 },
354 ],
355 ConcreteType::String,
356 |handle, n, ctx| {
357 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Process)?;
358 let buf_size = if n > 0 { n as usize } else { 65536 };
359 let mut guard = handle
360 .resource
361 .lock()
362 .map_err(|_| "io.process_read_err(): lock poisoned".to_string())?;
363 let resource = guard
364 .as_mut()
365 .ok_or_else(|| "io.process_read_err(): handle is closed".to_string())?;
366 let s = match resource {
367 IoResource::ChildProcess(child) => {
368 let stderr = child.stderr.as_mut().ok_or_else(|| {
369 "io.process_read_err(): stderr pipe not available".to_string()
370 })?;
371 let mut buf = vec![0u8; buf_size];
372 let bytes_read = stderr
373 .read(&mut buf)
374 .map_err(|e| format!("io.process_read_err(): {}", e))?;
375 buf.truncate(bytes_read);
376 String::from_utf8(buf)
377 .map_err(|e| format!("io.process_read_err(): invalid UTF-8: {}", e))?
378 }
379 IoResource::PipeReaderErr(stderr) => {
380 let mut buf = vec![0u8; buf_size];
381 let bytes_read = stderr
382 .read(&mut buf)
383 .map_err(|e| format!("io.process_read_err(): {}", e))?;
384 buf.truncate(bytes_read);
385 String::from_utf8(buf)
386 .map_err(|e| format!("io.process_read_err(): invalid UTF-8: {}", e))?
387 }
388 _ => {
389 return Err(
390 "io.process_read_err(): handle is not a ChildProcess or PipeReaderErr"
391 .to_string(),
392 );
393 }
394 };
395 Ok(TypedReturn::Concrete(ConcreteReturn::String(s)))
396 },
397 );
398
399 register_typed_fn_1::<_, Arc<IoHandleData>>(
401 module,
402 "process_read_line",
403 "Read a single line from a child process's stdout (including newline)",
404 "handle",
405 "IoHandle",
406 ConcreteType::String,
407 |handle, ctx| {
408 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Process)?;
409 let mut guard = handle
410 .resource
411 .lock()
412 .map_err(|_| "io.process_read_line(): lock poisoned".to_string())?;
413 let resource = guard
414 .as_mut()
415 .ok_or_else(|| "io.process_read_line(): handle is closed".to_string())?;
416 let line = match resource {
417 IoResource::ChildProcess(child) => {
418 let stdout = child.stdout.as_mut().ok_or_else(|| {
419 "io.process_read_line(): stdout pipe not available".to_string()
420 })?;
421 let mut line = String::new();
422 BufReader::new(stdout)
423 .read_line(&mut line)
424 .map_err(|e| format!("io.process_read_line(): {}", e))?;
425 line
426 }
427 IoResource::PipeReader(stdout) => {
428 let mut line = String::new();
429 BufReader::new(stdout)
430 .read_line(&mut line)
431 .map_err(|e| format!("io.process_read_line(): {}", e))?;
432 line
433 }
434 _ => {
435 return Err(
436 "io.process_read_line(): handle is not a ChildProcess or PipeReader"
437 .to_string(),
438 );
439 }
440 };
441 Ok(TypedReturn::Concrete(ConcreteReturn::String(line)))
442 },
443 );
444
445 register_typed_fn_0(
447 module,
448 "stdin",
449 "Return an IoHandle for the current process's standard input",
450 ConcreteType::IoHandle,
451 |ctx| {
452 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Process)?;
453 let file = std::fs::OpenOptions::new()
454 .read(true)
455 .open("/dev/stdin")
456 .map_err(|e| format!("io.stdin(): {}", e))?;
457 let handle =
458 IoHandleData::new_file(file, "/dev/stdin".to_string(), "r".to_string());
459 Ok(TypedReturn::Concrete(ConcreteReturn::IoHandle(Arc::new(
460 handle,
461 ))))
462 },
463 );
464
465 register_typed_fn_0(
467 module,
468 "stdout",
469 "Return an IoHandle for the current process's standard output",
470 ConcreteType::IoHandle,
471 |ctx| {
472 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Process)?;
473 let file = std::fs::OpenOptions::new()
474 .write(true)
475 .open("/dev/stdout")
476 .map_err(|e| format!("io.stdout(): {}", e))?;
477 let handle =
478 IoHandleData::new_file(file, "/dev/stdout".to_string(), "w".to_string());
479 Ok(TypedReturn::Concrete(ConcreteReturn::IoHandle(Arc::new(
480 handle,
481 ))))
482 },
483 );
484
485 register_typed_fn_0(
487 module,
488 "stderr",
489 "Return an IoHandle for the current process's standard error",
490 ConcreteType::IoHandle,
491 |ctx| {
492 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Process)?;
493 let file = std::fs::OpenOptions::new()
494 .write(true)
495 .open("/dev/stderr")
496 .map_err(|e| format!("io.stderr(): {}", e))?;
497 let handle =
498 IoHandleData::new_file(file, "/dev/stderr".to_string(), "w".to_string());
499 Ok(TypedReturn::Concrete(ConcreteReturn::IoHandle(Arc::new(
500 handle,
501 ))))
502 },
503 );
504
505 register_typed_fn_1::<_, Arc<IoHandleData>>(
515 module,
516 "read_line",
517 "Read a single line from an IoHandle (file or pipe)",
518 "handle",
519 "IoHandle",
520 ConcreteType::String,
521 |handle, ctx| {
522 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Process)?;
523 let mut guard = handle
524 .resource
525 .lock()
526 .map_err(|_| "io.read_line(): lock poisoned".to_string())?;
527 let resource = guard
528 .as_mut()
529 .ok_or_else(|| "io.read_line(): handle is closed".to_string())?;
530 let line = match resource {
531 IoResource::File(file) => {
532 let mut line = String::new();
533 BufReader::new(file)
534 .read_line(&mut line)
535 .map_err(|e| format!("io.read_line(): {}", e))?;
536 line
537 }
538 IoResource::ChildProcess(child) => {
539 let stdout = child.stdout.as_mut().ok_or_else(|| {
540 "io.read_line(): stdout pipe not available".to_string()
541 })?;
542 let mut line = String::new();
543 BufReader::new(stdout)
544 .read_line(&mut line)
545 .map_err(|e| format!("io.read_line(): {}", e))?;
546 line
547 }
548 IoResource::PipeReader(stdout) => {
549 let mut line = String::new();
550 BufReader::new(stdout)
551 .read_line(&mut line)
552 .map_err(|e| format!("io.read_line(): {}", e))?;
553 line
554 }
555 _ => {
556 return Err("io.read_line(): handle does not support reading".to_string());
557 }
558 };
559 Ok(TypedReturn::Concrete(ConcreteReturn::String(line)))
560 },
561 );
562}