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 let handle = args
99 .first()
100 .and_then(|a| a.as_io_handle())
101 .ok_or_else(|| "io.process_wait() requires a ChildProcess IoHandle".to_string())?;
102
103 let mut guard = handle
104 .resource
105 .lock()
106 .map_err(|_| "io.process_wait(): lock poisoned".to_string())?;
107 let resource = guard
108 .as_mut()
109 .ok_or_else(|| "io.process_wait(): handle is closed".to_string())?;
110
111 match resource {
112 IoResource::ChildProcess(child) => {
113 let status = child
114 .wait()
115 .map_err(|e| format!("io.process_wait(): {}", e))?;
116 Ok(ValueWord::from_i64(status.code().unwrap_or(-1) as i64))
117 }
118 _ => Err("io.process_wait(): handle is not a ChildProcess".to_string()),
119 }
120}
121
122pub fn io_process_kill(
126 args: &[ValueWord],
127 _ctx: &crate::module_exports::ModuleContext,
128) -> Result<ValueWord, String> {
129 let handle = args
130 .first()
131 .and_then(|a| a.as_io_handle())
132 .ok_or_else(|| "io.process_kill() requires a ChildProcess IoHandle".to_string())?;
133
134 let mut guard = handle
135 .resource
136 .lock()
137 .map_err(|_| "io.process_kill(): lock poisoned".to_string())?;
138 let resource = guard
139 .as_mut()
140 .ok_or_else(|| "io.process_kill(): handle is closed".to_string())?;
141
142 match resource {
143 IoResource::ChildProcess(child) => {
144 child
145 .kill()
146 .map_err(|e| format!("io.process_kill(): {}", e))?;
147 Ok(ValueWord::unit())
148 }
149 _ => Err("io.process_kill(): handle is not a ChildProcess".to_string()),
150 }
151}
152
153pub fn io_process_write(
158 args: &[ValueWord],
159 _ctx: &crate::module_exports::ModuleContext,
160) -> Result<ValueWord, String> {
161 let handle = args
162 .first()
163 .and_then(|a| a.as_io_handle())
164 .ok_or_else(|| "io.process_write() requires a ChildProcess IoHandle".to_string())?;
165
166 let data = args
167 .get(1)
168 .and_then(|a| a.as_str())
169 .ok_or_else(|| "io.process_write() requires a string as second argument".to_string())?;
170
171 let mut guard = handle
172 .resource
173 .lock()
174 .map_err(|_| "io.process_write(): lock poisoned".to_string())?;
175 let resource = guard
176 .as_mut()
177 .ok_or_else(|| "io.process_write(): handle is closed".to_string())?;
178
179 match resource {
180 IoResource::ChildProcess(child) => {
181 let stdin = child
182 .stdin
183 .as_mut()
184 .ok_or_else(|| "io.process_write(): stdin pipe not available".to_string())?;
185 let written = stdin
186 .write(data.as_bytes())
187 .map_err(|e| format!("io.process_write(): {}", e))?;
188 Ok(ValueWord::from_i64(written as i64))
189 }
190 IoResource::PipeWriter(stdin) => {
191 let written = stdin
192 .write(data.as_bytes())
193 .map_err(|e| format!("io.process_write(): {}", e))?;
194 Ok(ValueWord::from_i64(written as i64))
195 }
196 _ => Err("io.process_write(): handle is not a ChildProcess or PipeWriter".to_string()),
197 }
198}
199
200pub fn io_process_read(
204 args: &[ValueWord],
205 _ctx: &crate::module_exports::ModuleContext,
206) -> Result<ValueWord, String> {
207 let handle = args
208 .first()
209 .and_then(|a| a.as_io_handle())
210 .ok_or_else(|| "io.process_read() requires a ChildProcess IoHandle".to_string())?;
211
212 let n = args
213 .get(1)
214 .and_then(|a| a.as_number_coerce())
215 .unwrap_or(65536.0) as usize;
216
217 let mut guard = handle
218 .resource
219 .lock()
220 .map_err(|_| "io.process_read(): lock poisoned".to_string())?;
221 let resource = guard
222 .as_mut()
223 .ok_or_else(|| "io.process_read(): handle is closed".to_string())?;
224
225 match resource {
226 IoResource::ChildProcess(child) => {
227 let stdout = child
228 .stdout
229 .as_mut()
230 .ok_or_else(|| "io.process_read(): stdout pipe not available".to_string())?;
231 let mut buf = vec![0u8; n];
232 let bytes_read = stdout
233 .read(&mut buf)
234 .map_err(|e| format!("io.process_read(): {}", e))?;
235 buf.truncate(bytes_read);
236 let s = String::from_utf8(buf)
237 .map_err(|e| format!("io.process_read(): invalid UTF-8: {}", e))?;
238 Ok(ValueWord::from_string(Arc::new(s)))
239 }
240 IoResource::PipeReader(stdout) => {
241 let mut buf = vec![0u8; n];
242 let bytes_read = stdout
243 .read(&mut buf)
244 .map_err(|e| format!("io.process_read(): {}", e))?;
245 buf.truncate(bytes_read);
246 let s = String::from_utf8(buf)
247 .map_err(|e| format!("io.process_read(): invalid UTF-8: {}", e))?;
248 Ok(ValueWord::from_string(Arc::new(s)))
249 }
250 _ => Err("io.process_read(): handle is not a ChildProcess or PipeReader".to_string()),
251 }
252}
253
254pub fn io_process_read_err(
258 args: &[ValueWord],
259 _ctx: &crate::module_exports::ModuleContext,
260) -> Result<ValueWord, String> {
261 let handle = args
262 .first()
263 .and_then(|a| a.as_io_handle())
264 .ok_or_else(|| "io.process_read_err() requires a ChildProcess IoHandle".to_string())?;
265
266 let n = args
267 .get(1)
268 .and_then(|a| a.as_number_coerce())
269 .unwrap_or(65536.0) as usize;
270
271 let mut guard = handle
272 .resource
273 .lock()
274 .map_err(|_| "io.process_read_err(): lock poisoned".to_string())?;
275 let resource = guard
276 .as_mut()
277 .ok_or_else(|| "io.process_read_err(): handle is closed".to_string())?;
278
279 match resource {
280 IoResource::ChildProcess(child) => {
281 let stderr = child
282 .stderr
283 .as_mut()
284 .ok_or_else(|| "io.process_read_err(): stderr pipe not available".to_string())?;
285 let mut buf = vec![0u8; n];
286 let bytes_read = stderr
287 .read(&mut buf)
288 .map_err(|e| format!("io.process_read_err(): {}", e))?;
289 buf.truncate(bytes_read);
290 let s = String::from_utf8(buf)
291 .map_err(|e| format!("io.process_read_err(): invalid UTF-8: {}", e))?;
292 Ok(ValueWord::from_string(Arc::new(s)))
293 }
294 IoResource::PipeReaderErr(stderr) => {
295 let mut buf = vec![0u8; n];
296 let bytes_read = stderr
297 .read(&mut buf)
298 .map_err(|e| format!("io.process_read_err(): {}", e))?;
299 buf.truncate(bytes_read);
300 let s = String::from_utf8(buf)
301 .map_err(|e| format!("io.process_read_err(): invalid UTF-8: {}", e))?;
302 Ok(ValueWord::from_string(Arc::new(s)))
303 }
304 _ => {
305 Err("io.process_read_err(): handle is not a ChildProcess or PipeReaderErr".to_string())
306 }
307 }
308}
309
310pub fn io_process_read_line(
314 args: &[ValueWord],
315 _ctx: &crate::module_exports::ModuleContext,
316) -> Result<ValueWord, String> {
317 let handle = args
318 .first()
319 .and_then(|a| a.as_io_handle())
320 .ok_or_else(|| "io.process_read_line() requires a ChildProcess IoHandle".to_string())?;
321
322 let mut guard = handle
323 .resource
324 .lock()
325 .map_err(|_| "io.process_read_line(): lock poisoned".to_string())?;
326 let resource = guard
327 .as_mut()
328 .ok_or_else(|| "io.process_read_line(): handle is closed".to_string())?;
329
330 match resource {
331 IoResource::ChildProcess(child) => {
332 let stdout = child
333 .stdout
334 .as_mut()
335 .ok_or_else(|| "io.process_read_line(): stdout pipe not available".to_string())?;
336 let mut line = String::new();
337 BufReader::new(stdout)
338 .read_line(&mut line)
339 .map_err(|e| format!("io.process_read_line(): {}", e))?;
340 Ok(ValueWord::from_string(Arc::new(line)))
341 }
342 IoResource::PipeReader(stdout) => {
343 let mut line = String::new();
344 BufReader::new(stdout)
345 .read_line(&mut line)
346 .map_err(|e| format!("io.process_read_line(): {}", e))?;
347 Ok(ValueWord::from_string(Arc::new(line)))
348 }
349 _ => Err("io.process_read_line(): handle is not a ChildProcess or PipeReader".to_string()),
350 }
351}
352
353pub fn io_stdin(
357 _args: &[ValueWord],
358 _ctx: &crate::module_exports::ModuleContext,
359) -> Result<ValueWord, String> {
360 let file = std::fs::OpenOptions::new()
361 .read(true)
362 .open("/dev/stdin")
363 .map_err(|e| format!("io.stdin(): {}", e))?;
364 let handle = IoHandleData::new_file(file, "/dev/stdin".to_string(), "r".to_string());
365 Ok(ValueWord::from_io_handle(handle))
366}
367
368pub fn io_stdout(
372 _args: &[ValueWord],
373 _ctx: &crate::module_exports::ModuleContext,
374) -> Result<ValueWord, String> {
375 let file = std::fs::OpenOptions::new()
376 .write(true)
377 .open("/dev/stdout")
378 .map_err(|e| format!("io.stdout(): {}", e))?;
379 let handle = IoHandleData::new_file(file, "/dev/stdout".to_string(), "w".to_string());
380 Ok(ValueWord::from_io_handle(handle))
381}
382
383pub fn io_stderr(
387 _args: &[ValueWord],
388 _ctx: &crate::module_exports::ModuleContext,
389) -> Result<ValueWord, String> {
390 let file = std::fs::OpenOptions::new()
391 .write(true)
392 .open("/dev/stderr")
393 .map_err(|e| format!("io.stderr(): {}", e))?;
394 let handle = IoHandleData::new_file(file, "/dev/stderr".to_string(), "w".to_string());
395 Ok(ValueWord::from_io_handle(handle))
396}
397
398pub fn io_read_line(
403 args: &[ValueWord],
404 _ctx: &crate::module_exports::ModuleContext,
405) -> Result<ValueWord, String> {
406 if let Some(handle) = args.first().and_then(|a| a.as_io_handle()) {
408 let mut guard = handle
409 .resource
410 .lock()
411 .map_err(|_| "io.read_line(): lock poisoned".to_string())?;
412 let resource = guard
413 .as_mut()
414 .ok_or_else(|| "io.read_line(): handle is closed".to_string())?;
415
416 match resource {
417 IoResource::File(file) => {
418 let mut line = String::new();
419 BufReader::new(file)
420 .read_line(&mut line)
421 .map_err(|e| format!("io.read_line(): {}", e))?;
422 Ok(ValueWord::from_string(Arc::new(line)))
423 }
424 IoResource::ChildProcess(child) => {
425 let stdout = child
426 .stdout
427 .as_mut()
428 .ok_or_else(|| "io.read_line(): stdout pipe not available".to_string())?;
429 let mut line = String::new();
430 BufReader::new(stdout)
431 .read_line(&mut line)
432 .map_err(|e| format!("io.read_line(): {}", e))?;
433 Ok(ValueWord::from_string(Arc::new(line)))
434 }
435 IoResource::PipeReader(stdout) => {
436 let mut line = String::new();
437 BufReader::new(stdout)
438 .read_line(&mut line)
439 .map_err(|e| format!("io.read_line(): {}", e))?;
440 Ok(ValueWord::from_string(Arc::new(line)))
441 }
442 _ => Err("io.read_line(): handle does not support reading".to_string()),
443 }
444 } else {
445 let mut line = String::new();
447 std::io::stdin()
448 .read_line(&mut line)
449 .map_err(|e| format!("io.read_line(): {}", e))?;
450 Ok(ValueWord::from_string(Arc::new(line)))
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 fn test_ctx() -> crate::module_exports::ModuleContext<'static> {
459 let registry = Box::leak(Box::new(crate::type_schema::TypeSchemaRegistry::new()));
460 crate::module_exports::ModuleContext {
461 schemas: registry,
462 invoke_callable: None,
463 raw_invoker: None,
464 function_hashes: None,
465 vm_state: None,
466 granted_permissions: None,
467 scope_constraints: None,
468 set_pending_resume: None,
469 set_pending_frame_resume: None,
470 }
471 }
472
473 #[test]
474 fn test_exec_echo() {
475 let ctx = test_ctx();
476 let result = io_exec(
477 &[
478 ValueWord::from_string(Arc::new("echo".to_string())),
479 ValueWord::from_array(Arc::new(vec![ValueWord::from_string(Arc::new(
480 "hello world".to_string(),
481 ))])),
482 ],
483 &ctx,
484 )
485 .unwrap();
486 assert_eq!(result.type_name(), "object");
487 }
488
489 #[test]
490 fn test_exec_false() {
491 let ctx = test_ctx();
492 let result = io_exec(
493 &[ValueWord::from_string(Arc::new("false".to_string()))],
494 &ctx,
495 )
496 .unwrap();
497 assert_eq!(result.type_name(), "object");
498 }
499
500 #[test]
501 fn test_exec_nonexistent() {
502 let ctx = test_ctx();
503 let result = io_exec(
504 &[ValueWord::from_string(Arc::new(
505 "__nonexistent_command_xyz__".to_string(),
506 ))],
507 &ctx,
508 );
509 assert!(result.is_err());
510 }
511
512 #[test]
513 fn test_spawn_and_wait() {
514 let ctx = test_ctx();
515 let handle = io_spawn(
516 &[
517 ValueWord::from_string(Arc::new("echo".to_string())),
518 ValueWord::from_array(Arc::new(vec![ValueWord::from_string(Arc::new(
519 "test".to_string(),
520 ))])),
521 ],
522 &ctx,
523 )
524 .unwrap();
525 assert_eq!(handle.type_name(), "io_handle");
526
527 let code = io_process_wait(&[handle.clone()], &ctx).unwrap();
528 assert_eq!(code.as_number_coerce(), Some(0.0));
529 }
530
531 #[test]
532 fn test_spawn_read_stdout() {
533 let ctx = test_ctx();
534 let handle = io_spawn(
535 &[
536 ValueWord::from_string(Arc::new("echo".to_string())),
537 ValueWord::from_array(Arc::new(vec![ValueWord::from_string(Arc::new(
538 "hello from process".to_string(),
539 ))])),
540 ],
541 &ctx,
542 )
543 .unwrap();
544
545 io_process_wait(&[handle.clone()], &ctx).unwrap();
547
548 let output = io_process_read(&[handle.clone()], &ctx).unwrap();
549 let text = output.as_str().unwrap();
550 assert!(text.contains("hello from process"));
551
552 handle.as_io_handle().unwrap().close();
553 }
554
555 #[test]
556 fn test_spawn_write_stdin() {
557 let ctx = test_ctx();
558 let handle =
560 io_spawn(&[ValueWord::from_string(Arc::new("cat".to_string()))], &ctx).unwrap();
561
562 let written = io_process_write(
564 &[
565 handle.clone(),
566 ValueWord::from_string(Arc::new("input data".to_string())),
567 ],
568 &ctx,
569 )
570 .unwrap();
571 assert!(written.as_number_coerce().unwrap() > 0.0);
572
573 {
575 let h = handle.as_io_handle().unwrap();
576 let mut guard = h.resource.lock().unwrap();
577 if let Some(IoResource::ChildProcess(child)) = guard.as_mut() {
578 drop(child.stdin.take());
579 }
580 }
581
582 io_process_wait(&[handle.clone()], &ctx).unwrap();
584
585 let output = io_process_read(&[handle.clone()], &ctx).unwrap();
587 assert_eq!(output.as_str().unwrap(), "input data");
588
589 handle.as_io_handle().unwrap().close();
590 }
591
592 #[test]
593 fn test_spawn_kill() {
594 let ctx = test_ctx();
595 let handle = io_spawn(
597 &[
598 ValueWord::from_string(Arc::new("sleep".to_string())),
599 ValueWord::from_array(Arc::new(vec![ValueWord::from_string(Arc::new(
600 "60".to_string(),
601 ))])),
602 ],
603 &ctx,
604 )
605 .unwrap();
606
607 let result = io_process_kill(&[handle.clone()], &ctx);
609 assert!(result.is_ok());
610
611 let code = io_process_wait(&[handle.clone()], &ctx).unwrap();
613 let exit_code = code.as_number_coerce().unwrap() as i64;
615 assert!(exit_code != 0 || exit_code == -1);
616
617 handle.as_io_handle().unwrap().close();
618 }
619
620 #[test]
621 fn test_spawn_read_stderr() {
622 let ctx = test_ctx();
623 let handle = io_spawn(
625 &[
626 ValueWord::from_string(Arc::new("sh".to_string())),
627 ValueWord::from_array(Arc::new(vec![
628 ValueWord::from_string(Arc::new("-c".to_string())),
629 ValueWord::from_string(Arc::new("echo error_msg >&2".to_string())),
630 ])),
631 ],
632 &ctx,
633 )
634 .unwrap();
635
636 io_process_wait(&[handle.clone()], &ctx).unwrap();
637
638 let err_output = io_process_read_err(&[handle.clone()], &ctx).unwrap();
639 let text = err_output.as_str().unwrap();
640 assert!(text.contains("error_msg"));
641
642 handle.as_io_handle().unwrap().close();
643 }
644
645 #[test]
646 fn test_stdout_handle() {
647 let ctx = test_ctx();
648 let handle = io_stdout(&[], &ctx).unwrap();
649 assert_eq!(handle.type_name(), "io_handle");
650 let data = handle.as_io_handle().unwrap();
651 assert_eq!(data.path, "/dev/stdout");
652 assert_eq!(data.mode, "w");
653 data.close();
654 }
655
656 #[test]
657 fn test_stderr_handle() {
658 let ctx = test_ctx();
659 let handle = io_stderr(&[], &ctx).unwrap();
660 assert_eq!(handle.type_name(), "io_handle");
661 let data = handle.as_io_handle().unwrap();
662 assert_eq!(data.path, "/dev/stderr");
663 assert_eq!(data.mode, "w");
664 data.close();
665 }
666
667 #[test]
668 fn test_read_line_from_pipe() {
669 let ctx = test_ctx();
670 let handle = io_spawn(
672 &[
673 ValueWord::from_string(Arc::new("echo".to_string())),
674 ValueWord::from_array(Arc::new(vec![ValueWord::from_string(Arc::new(
675 "line output".to_string(),
676 ))])),
677 ],
678 &ctx,
679 )
680 .unwrap();
681
682 io_process_wait(&[handle.clone()], &ctx).unwrap();
683
684 let line = io_process_read_line(&[handle.clone()], &ctx).unwrap();
685 assert!(line.as_str().unwrap().contains("line output"));
686
687 handle.as_io_handle().unwrap().close();
688 }
689}