1use shape_value::ValueWord;
7use shape_value::heap_value::{IoHandleData, IoResource};
8use std::io::{BufRead, BufReader, Read, Write};
9use std::process::{Command, Stdio};
10use std::sync::Arc;
11
12pub fn io_spawn(
17 args: &[ValueWord],
18 ctx: &crate::module_exports::ModuleContext,
19) -> Result<ValueWord, String> {
20 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Process)?;
21 let cmd = args
22 .first()
23 .and_then(|a| a.as_str())
24 .ok_or_else(|| "io.spawn() requires a command string".to_string())?;
25
26 let mut command = Command::new(cmd);
27
28 if let Some(view) = args.get(1).and_then(|a| a.as_any_array()) {
30 let arr = view.to_generic();
31 for arg in arr.iter() {
32 if let Some(s) = arg.as_str() {
33 command.arg(s);
34 }
35 }
36 }
37
38 command
39 .stdin(Stdio::piped())
40 .stdout(Stdio::piped())
41 .stderr(Stdio::piped());
42
43 let child = command
44 .spawn()
45 .map_err(|e| format!("io.spawn(\"{}\"): {}", cmd, e))?;
46
47 let handle = IoHandleData::new_child_process(child, cmd.to_string());
48 Ok(ValueWord::from_io_handle(handle))
49}
50
51pub fn io_exec(
55 args: &[ValueWord],
56 ctx: &crate::module_exports::ModuleContext,
57) -> Result<ValueWord, String> {
58 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Process)?;
59 let cmd = args
60 .first()
61 .and_then(|a| a.as_str())
62 .ok_or_else(|| "io.exec() requires a command string".to_string())?;
63
64 let mut command = Command::new(cmd);
65
66 if let Some(view) = args.get(1).and_then(|a| a.as_any_array()) {
67 let arr = view.to_generic();
68 for arg in arr.iter() {
69 if let Some(s) = arg.as_str() {
70 command.arg(s);
71 }
72 }
73 }
74
75 let output = command
76 .output()
77 .map_err(|e| format!("io.exec(\"{}\"): {}", cmd, e))?;
78
79 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
80 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
81 let status = output.status.code().unwrap_or(-1) as i64;
82
83 let pairs: Vec<(&str, ValueWord)> = vec![
84 ("status", ValueWord::from_i64(status)),
85 ("stdout", ValueWord::from_string(Arc::new(stdout))),
86 ("stderr", ValueWord::from_string(Arc::new(stderr))),
87 ];
88 Ok(crate::type_schema::typed_object_from_pairs(&pairs))
89}
90
91pub fn io_process_wait(
95 args: &[ValueWord],
96 ctx: &crate::module_exports::ModuleContext,
97) -> Result<ValueWord, String> {
98 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Process)?;
99 let handle = args
100 .first()
101 .and_then(|a| a.as_io_handle())
102 .ok_or_else(|| "io.process_wait() requires a ChildProcess IoHandle".to_string())?;
103
104 let mut guard = handle
105 .resource
106 .lock()
107 .map_err(|_| "io.process_wait(): lock poisoned".to_string())?;
108 let resource = guard
109 .as_mut()
110 .ok_or_else(|| "io.process_wait(): handle is closed".to_string())?;
111
112 match resource {
113 IoResource::ChildProcess(child) => {
114 let status = child
115 .wait()
116 .map_err(|e| format!("io.process_wait(): {}", e))?;
117 Ok(ValueWord::from_i64(status.code().unwrap_or(-1) as i64))
118 }
119 _ => Err("io.process_wait(): handle is not a ChildProcess".to_string()),
120 }
121}
122
123pub fn io_process_kill(
127 args: &[ValueWord],
128 ctx: &crate::module_exports::ModuleContext,
129) -> Result<ValueWord, String> {
130 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Process)?;
131 let handle = args
132 .first()
133 .and_then(|a| a.as_io_handle())
134 .ok_or_else(|| "io.process_kill() requires a ChildProcess IoHandle".to_string())?;
135
136 let mut guard = handle
137 .resource
138 .lock()
139 .map_err(|_| "io.process_kill(): lock poisoned".to_string())?;
140 let resource = guard
141 .as_mut()
142 .ok_or_else(|| "io.process_kill(): handle is closed".to_string())?;
143
144 match resource {
145 IoResource::ChildProcess(child) => {
146 child
147 .kill()
148 .map_err(|e| format!("io.process_kill(): {}", e))?;
149 Ok(ValueWord::unit())
150 }
151 _ => Err("io.process_kill(): handle is not a ChildProcess".to_string()),
152 }
153}
154
155pub fn io_process_write(
160 args: &[ValueWord],
161 ctx: &crate::module_exports::ModuleContext,
162) -> Result<ValueWord, String> {
163 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Process)?;
164 let handle = args
165 .first()
166 .and_then(|a| a.as_io_handle())
167 .ok_or_else(|| "io.process_write() requires a ChildProcess IoHandle".to_string())?;
168
169 let data = args
170 .get(1)
171 .and_then(|a| a.as_str())
172 .ok_or_else(|| "io.process_write() requires a string as second argument".to_string())?;
173
174 let mut guard = handle
175 .resource
176 .lock()
177 .map_err(|_| "io.process_write(): lock poisoned".to_string())?;
178 let resource = guard
179 .as_mut()
180 .ok_or_else(|| "io.process_write(): handle is closed".to_string())?;
181
182 match resource {
183 IoResource::ChildProcess(child) => {
184 let stdin = child
185 .stdin
186 .as_mut()
187 .ok_or_else(|| "io.process_write(): stdin pipe not available".to_string())?;
188 let written = stdin
189 .write(data.as_bytes())
190 .map_err(|e| format!("io.process_write(): {}", e))?;
191 Ok(ValueWord::from_i64(written as i64))
192 }
193 IoResource::PipeWriter(stdin) => {
194 let written = stdin
195 .write(data.as_bytes())
196 .map_err(|e| format!("io.process_write(): {}", e))?;
197 Ok(ValueWord::from_i64(written as i64))
198 }
199 _ => Err("io.process_write(): handle is not a ChildProcess or PipeWriter".to_string()),
200 }
201}
202
203pub fn io_process_read(
207 args: &[ValueWord],
208 ctx: &crate::module_exports::ModuleContext,
209) -> Result<ValueWord, String> {
210 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Process)?;
211 let handle = args
212 .first()
213 .and_then(|a| a.as_io_handle())
214 .ok_or_else(|| "io.process_read() requires a ChildProcess IoHandle".to_string())?;
215
216 let n = args
217 .get(1)
218 .and_then(|a| a.as_number_coerce())
219 .unwrap_or(65536.0) as usize;
220
221 let mut guard = handle
222 .resource
223 .lock()
224 .map_err(|_| "io.process_read(): lock poisoned".to_string())?;
225 let resource = guard
226 .as_mut()
227 .ok_or_else(|| "io.process_read(): handle is closed".to_string())?;
228
229 match resource {
230 IoResource::ChildProcess(child) => {
231 let stdout = child
232 .stdout
233 .as_mut()
234 .ok_or_else(|| "io.process_read(): stdout pipe not available".to_string())?;
235 let mut buf = vec![0u8; n];
236 let bytes_read = stdout
237 .read(&mut buf)
238 .map_err(|e| format!("io.process_read(): {}", e))?;
239 buf.truncate(bytes_read);
240 let s = String::from_utf8(buf)
241 .map_err(|e| format!("io.process_read(): invalid UTF-8: {}", e))?;
242 Ok(ValueWord::from_string(Arc::new(s)))
243 }
244 IoResource::PipeReader(stdout) => {
245 let mut buf = vec![0u8; n];
246 let bytes_read = stdout
247 .read(&mut buf)
248 .map_err(|e| format!("io.process_read(): {}", e))?;
249 buf.truncate(bytes_read);
250 let s = String::from_utf8(buf)
251 .map_err(|e| format!("io.process_read(): invalid UTF-8: {}", e))?;
252 Ok(ValueWord::from_string(Arc::new(s)))
253 }
254 _ => Err("io.process_read(): handle is not a ChildProcess or PipeReader".to_string()),
255 }
256}
257
258pub fn io_process_read_err(
262 args: &[ValueWord],
263 ctx: &crate::module_exports::ModuleContext,
264) -> Result<ValueWord, String> {
265 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Process)?;
266 let handle = args
267 .first()
268 .and_then(|a| a.as_io_handle())
269 .ok_or_else(|| "io.process_read_err() requires a ChildProcess IoHandle".to_string())?;
270
271 let n = args
272 .get(1)
273 .and_then(|a| a.as_number_coerce())
274 .unwrap_or(65536.0) as usize;
275
276 let mut guard = handle
277 .resource
278 .lock()
279 .map_err(|_| "io.process_read_err(): lock poisoned".to_string())?;
280 let resource = guard
281 .as_mut()
282 .ok_or_else(|| "io.process_read_err(): handle is closed".to_string())?;
283
284 match resource {
285 IoResource::ChildProcess(child) => {
286 let stderr = child
287 .stderr
288 .as_mut()
289 .ok_or_else(|| "io.process_read_err(): stderr pipe not available".to_string())?;
290 let mut buf = vec![0u8; n];
291 let bytes_read = stderr
292 .read(&mut buf)
293 .map_err(|e| format!("io.process_read_err(): {}", e))?;
294 buf.truncate(bytes_read);
295 let s = String::from_utf8(buf)
296 .map_err(|e| format!("io.process_read_err(): invalid UTF-8: {}", e))?;
297 Ok(ValueWord::from_string(Arc::new(s)))
298 }
299 IoResource::PipeReaderErr(stderr) => {
300 let mut buf = vec![0u8; n];
301 let bytes_read = stderr
302 .read(&mut buf)
303 .map_err(|e| format!("io.process_read_err(): {}", e))?;
304 buf.truncate(bytes_read);
305 let s = String::from_utf8(buf)
306 .map_err(|e| format!("io.process_read_err(): invalid UTF-8: {}", e))?;
307 Ok(ValueWord::from_string(Arc::new(s)))
308 }
309 _ => {
310 Err("io.process_read_err(): handle is not a ChildProcess or PipeReaderErr".to_string())
311 }
312 }
313}
314
315pub fn io_process_read_line(
319 args: &[ValueWord],
320 ctx: &crate::module_exports::ModuleContext,
321) -> Result<ValueWord, String> {
322 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Process)?;
323 let handle = args
324 .first()
325 .and_then(|a| a.as_io_handle())
326 .ok_or_else(|| "io.process_read_line() requires a ChildProcess IoHandle".to_string())?;
327
328 let mut guard = handle
329 .resource
330 .lock()
331 .map_err(|_| "io.process_read_line(): lock poisoned".to_string())?;
332 let resource = guard
333 .as_mut()
334 .ok_or_else(|| "io.process_read_line(): handle is closed".to_string())?;
335
336 match resource {
337 IoResource::ChildProcess(child) => {
338 let stdout = child
339 .stdout
340 .as_mut()
341 .ok_or_else(|| "io.process_read_line(): stdout pipe not available".to_string())?;
342 let mut line = String::new();
343 BufReader::new(stdout)
344 .read_line(&mut line)
345 .map_err(|e| format!("io.process_read_line(): {}", e))?;
346 Ok(ValueWord::from_string(Arc::new(line)))
347 }
348 IoResource::PipeReader(stdout) => {
349 let mut line = String::new();
350 BufReader::new(stdout)
351 .read_line(&mut line)
352 .map_err(|e| format!("io.process_read_line(): {}", e))?;
353 Ok(ValueWord::from_string(Arc::new(line)))
354 }
355 _ => Err("io.process_read_line(): handle is not a ChildProcess or PipeReader".to_string()),
356 }
357}
358
359pub fn io_stdin(
363 _args: &[ValueWord],
364 ctx: &crate::module_exports::ModuleContext,
365) -> Result<ValueWord, String> {
366 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Process)?;
367 let file = std::fs::OpenOptions::new()
368 .read(true)
369 .open("/dev/stdin")
370 .map_err(|e| format!("io.stdin(): {}", e))?;
371 let handle = IoHandleData::new_file(file, "/dev/stdin".to_string(), "r".to_string());
372 Ok(ValueWord::from_io_handle(handle))
373}
374
375pub fn io_stdout(
379 _args: &[ValueWord],
380 ctx: &crate::module_exports::ModuleContext,
381) -> Result<ValueWord, String> {
382 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Process)?;
383 let file = std::fs::OpenOptions::new()
384 .write(true)
385 .open("/dev/stdout")
386 .map_err(|e| format!("io.stdout(): {}", e))?;
387 let handle = IoHandleData::new_file(file, "/dev/stdout".to_string(), "w".to_string());
388 Ok(ValueWord::from_io_handle(handle))
389}
390
391pub fn io_stderr(
395 _args: &[ValueWord],
396 ctx: &crate::module_exports::ModuleContext,
397) -> Result<ValueWord, String> {
398 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Process)?;
399 let file = std::fs::OpenOptions::new()
400 .write(true)
401 .open("/dev/stderr")
402 .map_err(|e| format!("io.stderr(): {}", e))?;
403 let handle = IoHandleData::new_file(file, "/dev/stderr".to_string(), "w".to_string());
404 Ok(ValueWord::from_io_handle(handle))
405}
406
407pub fn io_read_line(
412 args: &[ValueWord],
413 ctx: &crate::module_exports::ModuleContext,
414) -> Result<ValueWord, String> {
415 crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Process)?;
416 if let Some(handle) = args.first().and_then(|a| a.as_io_handle()) {
418 let mut guard = handle
419 .resource
420 .lock()
421 .map_err(|_| "io.read_line(): lock poisoned".to_string())?;
422 let resource = guard
423 .as_mut()
424 .ok_or_else(|| "io.read_line(): handle is closed".to_string())?;
425
426 match resource {
427 IoResource::File(file) => {
428 let mut line = String::new();
429 BufReader::new(file)
430 .read_line(&mut line)
431 .map_err(|e| format!("io.read_line(): {}", e))?;
432 Ok(ValueWord::from_string(Arc::new(line)))
433 }
434 IoResource::ChildProcess(child) => {
435 let stdout = child
436 .stdout
437 .as_mut()
438 .ok_or_else(|| "io.read_line(): stdout pipe not available".to_string())?;
439 let mut line = String::new();
440 BufReader::new(stdout)
441 .read_line(&mut line)
442 .map_err(|e| format!("io.read_line(): {}", e))?;
443 Ok(ValueWord::from_string(Arc::new(line)))
444 }
445 IoResource::PipeReader(stdout) => {
446 let mut line = String::new();
447 BufReader::new(stdout)
448 .read_line(&mut line)
449 .map_err(|e| format!("io.read_line(): {}", e))?;
450 Ok(ValueWord::from_string(Arc::new(line)))
451 }
452 _ => Err("io.read_line(): handle does not support reading".to_string()),
453 }
454 } else {
455 let mut line = String::new();
457 std::io::stdin()
458 .read_line(&mut line)
459 .map_err(|e| format!("io.read_line(): {}", e))?;
460 Ok(ValueWord::from_string(Arc::new(line)))
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 fn test_ctx() -> crate::module_exports::ModuleContext<'static> {
469 let registry = Box::leak(Box::new(crate::type_schema::TypeSchemaRegistry::new()));
470 crate::module_exports::ModuleContext {
471 schemas: registry,
472 invoke_callable: None,
473 raw_invoker: None,
474 function_hashes: None,
475 vm_state: None,
476 granted_permissions: None,
477 scope_constraints: None,
478 set_pending_resume: None,
479 set_pending_frame_resume: None,
480 }
481 }
482
483 #[test]
484 fn test_exec_echo() {
485 let ctx = test_ctx();
486 let result = io_exec(
487 &[
488 ValueWord::from_string(Arc::new("echo".to_string())),
489 ValueWord::from_array(Arc::new(vec![ValueWord::from_string(Arc::new(
490 "hello world".to_string(),
491 ))])),
492 ],
493 &ctx,
494 )
495 .unwrap();
496 assert_eq!(result.type_name(), "object");
497 }
498
499 #[test]
500 fn test_exec_false() {
501 let ctx = test_ctx();
502 let result = io_exec(
503 &[ValueWord::from_string(Arc::new("false".to_string()))],
504 &ctx,
505 )
506 .unwrap();
507 assert_eq!(result.type_name(), "object");
508 }
509
510 #[test]
511 fn test_exec_nonexistent() {
512 let ctx = test_ctx();
513 let result = io_exec(
514 &[ValueWord::from_string(Arc::new(
515 "__nonexistent_command_xyz__".to_string(),
516 ))],
517 &ctx,
518 );
519 assert!(result.is_err());
520 }
521
522 #[test]
523 fn test_spawn_and_wait() {
524 let ctx = test_ctx();
525 let handle = io_spawn(
526 &[
527 ValueWord::from_string(Arc::new("echo".to_string())),
528 ValueWord::from_array(Arc::new(vec![ValueWord::from_string(Arc::new(
529 "test".to_string(),
530 ))])),
531 ],
532 &ctx,
533 )
534 .unwrap();
535 assert_eq!(handle.type_name(), "io_handle");
536
537 let code = io_process_wait(&[handle.clone()], &ctx).unwrap();
538 assert_eq!(code.as_number_coerce(), Some(0.0));
539 }
540
541 #[test]
542 fn test_spawn_read_stdout() {
543 let ctx = test_ctx();
544 let handle = io_spawn(
545 &[
546 ValueWord::from_string(Arc::new("echo".to_string())),
547 ValueWord::from_array(Arc::new(vec![ValueWord::from_string(Arc::new(
548 "hello from process".to_string(),
549 ))])),
550 ],
551 &ctx,
552 )
553 .unwrap();
554
555 io_process_wait(&[handle.clone()], &ctx).unwrap();
557
558 let output = io_process_read(&[handle.clone()], &ctx).unwrap();
559 let text = output.as_str().unwrap();
560 assert!(text.contains("hello from process"));
561
562 handle.as_io_handle().unwrap().close();
563 }
564
565 #[test]
566 fn test_spawn_write_stdin() {
567 let ctx = test_ctx();
568 let handle =
570 io_spawn(&[ValueWord::from_string(Arc::new("cat".to_string()))], &ctx).unwrap();
571
572 let written = io_process_write(
574 &[
575 handle.clone(),
576 ValueWord::from_string(Arc::new("input data".to_string())),
577 ],
578 &ctx,
579 )
580 .unwrap();
581 assert!(written.as_number_coerce().unwrap() > 0.0);
582
583 {
585 let h = handle.as_io_handle().unwrap();
586 let mut guard = h.resource.lock().unwrap();
587 if let Some(IoResource::ChildProcess(child)) = guard.as_mut() {
588 drop(child.stdin.take());
589 }
590 }
591
592 io_process_wait(&[handle.clone()], &ctx).unwrap();
594
595 let output = io_process_read(&[handle.clone()], &ctx).unwrap();
597 assert_eq!(output.as_str().unwrap(), "input data");
598
599 handle.as_io_handle().unwrap().close();
600 }
601
602 #[test]
603 fn test_spawn_kill() {
604 let ctx = test_ctx();
605 let handle = io_spawn(
607 &[
608 ValueWord::from_string(Arc::new("sleep".to_string())),
609 ValueWord::from_array(Arc::new(vec![ValueWord::from_string(Arc::new(
610 "60".to_string(),
611 ))])),
612 ],
613 &ctx,
614 )
615 .unwrap();
616
617 let result = io_process_kill(&[handle.clone()], &ctx);
619 assert!(result.is_ok());
620
621 let code = io_process_wait(&[handle.clone()], &ctx).unwrap();
623 let exit_code = code.as_number_coerce().unwrap() as i64;
625 assert!(exit_code != 0 || exit_code == -1);
626
627 handle.as_io_handle().unwrap().close();
628 }
629
630 #[test]
631 fn test_spawn_read_stderr() {
632 let ctx = test_ctx();
633 let handle = io_spawn(
635 &[
636 ValueWord::from_string(Arc::new("sh".to_string())),
637 ValueWord::from_array(Arc::new(vec![
638 ValueWord::from_string(Arc::new("-c".to_string())),
639 ValueWord::from_string(Arc::new("echo error_msg >&2".to_string())),
640 ])),
641 ],
642 &ctx,
643 )
644 .unwrap();
645
646 io_process_wait(&[handle.clone()], &ctx).unwrap();
647
648 let err_output = io_process_read_err(&[handle.clone()], &ctx).unwrap();
649 let text = err_output.as_str().unwrap();
650 assert!(text.contains("error_msg"));
651
652 handle.as_io_handle().unwrap().close();
653 }
654
655 #[test]
656 fn test_stdout_handle() {
657 let ctx = test_ctx();
658 let handle = io_stdout(&[], &ctx).unwrap();
659 assert_eq!(handle.type_name(), "io_handle");
660 let data = handle.as_io_handle().unwrap();
661 assert_eq!(data.path, "/dev/stdout");
662 assert_eq!(data.mode, "w");
663 data.close();
664 }
665
666 #[test]
667 fn test_stderr_handle() {
668 let ctx = test_ctx();
669 let handle = io_stderr(&[], &ctx).unwrap();
670 assert_eq!(handle.type_name(), "io_handle");
671 let data = handle.as_io_handle().unwrap();
672 assert_eq!(data.path, "/dev/stderr");
673 assert_eq!(data.mode, "w");
674 data.close();
675 }
676
677 #[test]
678 fn test_read_line_from_pipe() {
679 let ctx = test_ctx();
680 let handle = io_spawn(
682 &[
683 ValueWord::from_string(Arc::new("echo".to_string())),
684 ValueWord::from_array(Arc::new(vec![ValueWord::from_string(Arc::new(
685 "line output".to_string(),
686 ))])),
687 ],
688 &ctx,
689 )
690 .unwrap();
691
692 io_process_wait(&[handle.clone()], &ctx).unwrap();
693
694 let line = io_process_read_line(&[handle.clone()], &ctx).unwrap();
695 assert!(line.as_str().unwrap().contains("line output"));
696
697 handle.as_io_handle().unwrap().close();
698 }
699}