diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47f5050d..0b3071ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: # FIXME(issue #2138): run wasm tests, failing to run since https://github.com/mthom/scryer-prolog/pull/2137 removed wasm-pack - { os: ubuntu-22.04, rust-version: nightly, target: 'wasm32-unknown-unknown', publish: true, args: '--no-default-features' , test-args: '--no-run --no-default-features', use_swap: true } # Cargo.toml rust-version - - { os: ubuntu-22.04, rust-version: "1.85", target: 'x86_64-unknown-linux-gnu'} + - { os: ubuntu-22.04, rust-version: "1.87", target: 'x86_64-unknown-linux-gnu'} - { os: ubuntu-22.04, rust-version: beta, target: 'x86_64-unknown-linux-gnu'} - { os: ubuntu-22.04, rust-version: nightly, target: 'x86_64-unknown-linux-gnu', miri: true, components: "miri"} defaults: diff --git a/Cargo.toml b/Cargo.toml index 3087a3a2..67f65bc9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ keywords = ["prolog", "prolog-interpreter", "prolog-system"] categories = ["command-line-utilities"] build = "build/main.rs" # Remember to check CI -rust-version = "1.85" +rust-version = "1.87" [lib] crate-type = ["cdylib", "rlib"] diff --git a/build/instructions_template.rs b/build/instructions_template.rs index 31c329b9..1cf004b1 100644 --- a/build/instructions_template.rs +++ b/build/instructions_template.rs @@ -537,6 +537,16 @@ enum SystemClauseType { UnsetEnv, #[strum_discriminants(strum(props(Arity = "2", Name = "$shell")))] Shell, + #[strum_discriminants(strum(props(Arity = "8", Name = "$process_create")))] + ProcessCreate, + #[strum_discriminants(strum(props(Arity = "2", Name = "$process_id")))] + ProcessId, + #[strum_discriminants(strum(props(Arity = "3", Name = "$process_wait")))] + ProcessWait, + #[strum_discriminants(strum(props(Arity = "1", Name = "$process_kill")))] + ProcessKill, + #[strum_discriminants(strum(props(Arity = "1", Name = "$process_release")))] + ProcessRelease, #[strum_discriminants(strum(props(Arity = "1", Name = "$pid")))] Pid, #[strum_discriminants(strum(props(Arity = "4", Name = "$chars_base64")))] @@ -1825,6 +1835,11 @@ fn generate_instruction_preface() -> TokenStream { &Instruction::CallSetEnv | &Instruction::CallUnsetEnv | &Instruction::CallShell | + &Instruction::CallProcessCreate | + &Instruction::CallProcessId | + &Instruction::CallProcessWait | + &Instruction::CallProcessKill | + &Instruction::CallProcessRelease | &Instruction::CallPid | &Instruction::CallCharsBase64 | &Instruction::CallDevourWhitespace | @@ -2063,6 +2078,11 @@ fn generate_instruction_preface() -> TokenStream { &Instruction::ExecuteSetEnv | &Instruction::ExecuteUnsetEnv | &Instruction::ExecuteShell | + &Instruction::ExecuteProcessCreate | + &Instruction::ExecuteProcessId | + &Instruction::ExecuteProcessWait | + &Instruction::ExecuteProcessKill | + &Instruction::ExecuteProcessRelease | &Instruction::ExecutePid | &Instruction::ExecuteCharsBase64 | &Instruction::ExecuteDevourWhitespace | diff --git a/build/static_string_indexing.rs b/build/static_string_indexing.rs index a577a517..7309f1cf 100644 --- a/build/static_string_indexing.rs +++ b/build/static_string_indexing.rs @@ -194,6 +194,7 @@ pub fn index_static_strings(instruction_rs_path: &std::path::Path) -> TokenStrea macro_rules! atom { #((#static_str_keys) => { Atom { index: #indices } };)* + ($name:literal) => {compile_error!(concat!("unknown static atom ", $name))}; } pub static STATIC_ATOMS_MAP: phf::Map<&'static str, Atom> = phf::phf_map! { diff --git a/src/arena.rs b/src/arena.rs index 2a3fc8b9..ee9dda20 100644 --- a/src/arena.rs +++ b/src/arena.rs @@ -14,10 +14,13 @@ use ordered_float::OrderedFloat; use std::fmt; use std::fmt::Debug; use std::hash::{Hash, Hasher}; +use std::io::PipeReader; +use std::io::PipeWriter; use std::mem; use std::mem::ManuallyDrop; use std::net::TcpListener; use std::ops::{Deref, DerefMut}; +use std::process::Child; use std::ptr; use std::ptr::addr_of_mut; use std::ptr::NonNull; @@ -71,7 +74,10 @@ pub enum ArenaHeaderTag { TcpListener = 0b1000000, HttpListener = 0b1000001, HttpResponse = 0b1000010, + PipeWriter = 0b1000011, Dropped = 0b1000100, + PipeReader = 0b1001001, + ChildProcess = 0b1001010, } #[bitfield] @@ -387,6 +393,19 @@ impl ArenaAllocated for HttpResponse { } } +impl ArenaAllocated for Child { + type Payload = ManuallyDrop; + #[inline] + fn tag() -> ArenaHeaderTag { + ArenaHeaderTag::ChildProcess + } +} +impl AllocateInArena for Child { + fn arena_allocate(self, arena: &mut Arena) -> TypedArenaPtr { + Child::alloc(arena, ManuallyDrop::new(self)) + } +} + #[repr(C)] #[derive(Debug)] pub struct AllocSlab { @@ -546,6 +565,15 @@ unsafe fn drop_slab_in_place(value: NonNull, tag: ArenaHeaderTag) { ArenaHeaderTag::StandardErrorStream => { drop_typed_slab_in_place!(StandardErrorStream, value); } + ArenaHeaderTag::PipeReader => { + drop_typed_slab_in_place!(PipeReader, value); + } + ArenaHeaderTag::PipeWriter => { + drop_typed_slab_in_place!(PipeWriter, value); + } + ArenaHeaderTag::ChildProcess => { + drop_typed_slab_in_place!(Child, value); + } ArenaHeaderTag::NullStream => { unreachable!("NullStream is never arena allocated!"); } diff --git a/src/heap_print.rs b/src/heap_print.rs index 57fef895..2186c8ed 100644 --- a/src/heap_print.rs +++ b/src/heap_print.rs @@ -1789,6 +1789,23 @@ impl<'a, Outputter: HCValueOutputter> HCPrinter<'a, Outputter> { (ArenaHeaderTag::Dropped, _value) => { self.print_impromptu_atom(atom!("$dropped_value")); } + (ArenaHeaderTag::ChildProcess, process) => { + + let process_atom = atom!("$process"); + + if self.format_struct(max_depth, 1, process_atom) { + let atom = TokenOrRedirect::NumberFocus(max_depth, NumberFocus::Unfocused(Number::Fixnum(Fixnum::build_with(process.id()))), op); + + let process_root = self.state_stack.pop().unwrap(); + + self.state_stack.pop(); + self.state_stack.pop(); + + self.state_stack.push(atom); + self.state_stack.push(TokenOrRedirect::Open); + self.state_stack.push(process_root); + } + } _ => { } ); diff --git a/src/lib/process.pl b/src/lib/process.pl new file mode 100644 index 00000000..57c71426 --- /dev/null +++ b/src/lib/process.pl @@ -0,0 +1,224 @@ +:- module(process, [ + process_create/3, + process_id/2, + process_release/1, + process_wait/2, + process_wait/3, + process_kill/1 +]). + +:- use_module(library(error)). +:- use_module(library(iso_ext)). +:- use_module(library(lists), [member/2, maplist/2, maplist/3, append/2]). +:- use_module(library(reif), [tfilter/3, memberd_t/3]). + + +%% process_create(+Exe, +Args:list, +Options). +% +% Create a new process by executing the executable Exe and passing it the Arguments Args. +% +% Note: On windows please take note of [windows argument splitting](https://doc.rust-lang.org/std/process/index.html#windows-argument-splitting). +% +% Options is a list consisting of the following options: +% +% * `cwd(+Path)` Set the processes working directory to `Path` +% * `process(-Process)` `Process` will be assigned a process handle for the spawned process +% * `env(+List)` Don't inherit environment variables and set the variables defined in `List` +% * `environment(+List)` Inherit environment variables and set/override the variables defined in `List` +% * `stdin(Spec)`, `stdout(Spec)` or `stderr(Spec)` defines how to redirect the spawned processes io streams +% +% The elements of `List` in `env(List)`/`environment(List)` List must be string pairs using `=/2`. +% `env/1` and `environment/1` may not be both specified. +% +% The following stdio `Spec` are available: +% +% * `std` inherit the current processes original stdio streams (does currently not account for stdio being changed by `set_input` or `set_output`) +% * `file(+Path)` attach the strea to the file at `Path` +% * `null` discards writes and behaves as eof for read. Equivalent to using `file(/dev/null)` +% * `pipe(-Steam)` create a new pipe and assigne one end to the created process and the other end to `Stream` +% +% Specifying an option multiple times is an error, when an option is not specified the following defaults apply: +% +% - `cwd(".")` +% - `environment([])` +% - `stdin(std)`, `stdout(std)`, `stderr(std)` +% +process_create(Exe, Args, Options) :- call_with_error_context(process_create_(Exe, Args, Options), predicate-process_create/3). + +process_create_(Exe, Args, Options) :- + must_be(chars, Exe), + must_be(list, Args), + maplist(must_be(chars), Args), + must_be(list, Options), + check_options( + [ + option([stdin], valid_stdio, stdin(std), stdin(Stdin)), + option([stdout], valid_stdio, stdout(std), stdout(Stdout)), + option([stderr], valid_stdio, stderr(std), stderr(Stderr)), + option([env, environment], valid_env, environment([]), Env), + option([process], valid_uninit_process, process(_), process(Process)), + option([cwd], valid_cwd, cwd("."), cwd(Cwd)) + ], + Options, + process_create_option + ), + Stdin =.. Stdin1, + Stdout =.. Stdout1, + Stderr =.. Stderr1, + simplify_env(Env, Env1), + '$process_create'(Exe, Args, Stdin1, Stdout1, Stderr1, Env1, Cwd, Process). + +%% process_id(+Process, -Pid). +% +process_id(Process, Pid) :- call_with_error_context(process_id_(Process, Pid), predicate-process_id/2). + +process_id_(Process, Pid) :- + valid_process(Process), + must_be(var, Pid), + '$process_id'(Process, Pid). + +%% process_wait(+Process, Status). +% +% See `process_create/3` with `Options = []` +% +process_wait(Process, Status) :- call_with_error_context(process_wait(Process, Status, []), predicate-process_wait/2). + + +%% process_wait(+Process, Status, Options). +% +% Wait for the process behind the process handle `Process` to exit. +% +% When the process exits regulary `Status` will be unified with `exit(Exit)` where `Exit` is the processes exit code. +% When the process exits was killed `Status` will be unified with `killed(Signal)` where `Signal` is the signal number that killed the process. +% When the process doesn't exit before the timeout `Status` will be unified with `timeout`. +% +% `Options` is a a list of the following options +% +% * timeout(Timeout) supported values for `Timeout` are 0 or `infinite` +% +% Each options may be specified at most once, when an option is not specified the following defaults apply: +% +% - timeout(infinite) +% +process_wait(Process, Status, Options) :- call_with_error_context(process_wait_(Process, Status, Options), predicate-process_wait/3). + +process_wait_(Process, Status, Options) :- + valid_process(Process), + check_options( + [ + option([timeout], valid_timeout, timeout(infinite), timeout(Timeout)) + ], + Options, + process_wait_option + ), + '$process_wait'(Process, Exit, Timeout), + Exit = Status. + +valid_timeout(timeout(infinite)). +valid_timeout(timeout(0)). + + +%% process_kill(+Process). +% +% Kill the process using the process handle `Process`. +% On Unix this sends SIGKILL. +% +% Only works for processes spawned with `process_create/3` that have not yet been release with `process_release/1` +% +process_kill(Process) :- call_with_error_context(process_kill_(Process), predicate-process_kill/1). + +process_kill_(Process) :- + valid_process(Process), + '$process_kill'(Process). + +%% process_release(+Process) +% +% wait for the process to exit (if not already) and release process handle `Process` +% +% It's an error if `Process` is not a valid process handle +% +process_release(Process) :- call_with_error_context(process_release_(Process), predicate-process_release/1). + +process_release_(Process) :- + valid_process(Process), + process_wait(Process, _), + '$process_release'(Process). + + +must_be_known_options(Valid, Options, Domain) :- call_with_error_context(must_be_known_options_(Valid, [], Options, Domain),predicate-must_be_known_options/3). + +must_be_known_options_(_, _, [], _). +must_be_known_options_(Valid, Found, [X|XS], Domain) :- + ( functor(X, Option, 1) -> true + ; domain_error(Domain, X, []) + ) , + ( member(Option, Found) -> domain_error(non_duplicate_options, Option , []) + ; member(Option, Valid) -> true + ; domain_error(Domain, Option, []) + ), + must_be_known_options_(Valid, [Option | Found], XS, Domain). + +check_options(KnownOptions, Options, Domain) :- call_with_error_context(check_options_(KnownOptions, Options, Domain), predicate-check_options/3). + +check_options_(KnownOptions, Options, Domain) :- + maplist(option_names, KnownOptions, Namess), + append(Namess, Names), + must_be_known_options(Names, Options, Domain), + extract_options(KnownOptions, Options). + +option_names(option(Names,_,_,_), Names). + +extract_options(KnownOptions, Options) :- call_with_error_context(extract_options_(KnownOptions, Options), predicate-extract_options/2). + +extract_options_([], _). +extract_options_([X | XS], Options) :- + option(Kinds, Pred, Default, Choice) = X, + tfilter(find_option(Kinds), Options, Solutions), + ( Solutions = [] -> Choice = Default + ; Solutions = [Provided] -> functor(Pred, Name, Arity), ArityP1 is Arity+1, call_with_error_context(call(Pred, Provided),predicate-Name/ArityP1), Choice = Provided + ; domain_error(non_conflicting_options, Solutions, []) + ), + extract_options_(XS, Options). + +find_option(Names, Found, T) :- + functor(Found, Name, 1), + memberd_t(Name, Names, T). + +valid_stdio(IO) :- arg(1, IO, Arg), + ( var(Arg) -> instantiation_error([]) + ; valid_stdio_(Arg) -> true + ; domain_error(stdio_spec, Arg, []) + ). + +valid_stdio_(std). +valid_stdio_(null). +valid_stdio_(pipe(Stream)) :- must_be(var, Stream). +valid_stdio_(file(Path)) :- must_be(chars, Path). + +valid_env(env(E)) :- + must_be(list, E), + ( valid_env_(E) -> true + ; domain_error(process_create_option, env(E), []) + ). +valid_env(environment(E)) :- + must_be(list, E), + ( valid_env_(E) -> true + ; domain_error(process_create_option, environment(E), []) + ). + +valid_env_([]). +valid_env_([N=V|ES]) :- + must_be(chars, N), + must_be(chars, V), + valid_env_(ES). + +valid_uninit_process(process(Process)) :- must_be(var, Process). + +valid_process(Process) :- var(Process) -> instantiation_error([]) ; true. + +valid_cwd(cwd(Cwd)) :- must_be(chars, Cwd). + +simplify_env(E, [Kind, Envs1]) :- E =.. [Kind, Envs], simplify_env_(Envs, Envs1). + +simplify_env_([],[]). +simplify_env_([N=V|E],[[N, V]|E1]) :- simplify_env_(E, E1). diff --git a/src/machine/dispatch.rs b/src/machine/dispatch.rs index af34bcd7..8a8921e6 100644 --- a/src/machine/dispatch.rs +++ b/src/machine/dispatch.rs @@ -4787,6 +4787,46 @@ impl Machine { self.shell(); step_or_fail!(self, self.machine_st.p = self.machine_st.cp); } + &Instruction::CallProcessCreate => { + try_or_throw!(self.machine_st, self.process_create()); + step_or_fail!(self, self.machine_st.p += 1); + } + &Instruction::ExecuteProcessCreate => { + try_or_throw!(self.machine_st, self.process_create()); + step_or_fail!(self, self.machine_st.p = self.machine_st.cp); + } + &Instruction::CallProcessId => { + try_or_throw!(self.machine_st, self.process_id()); + step_or_fail!(self, self.machine_st.p += 1); + } + &Instruction::ExecuteProcessId => { + try_or_throw!(self.machine_st, self.process_id()); + step_or_fail!(self, self.machine_st.p = self.machine_st.cp); + } + &Instruction::CallProcessWait => { + try_or_throw!(self.machine_st, self.process_wait()); + step_or_fail!(self, self.machine_st.p += 1); + } + &Instruction::ExecuteProcessWait => { + try_or_throw!(self.machine_st, self.process_wait()); + step_or_fail!(self, self.machine_st.p = self.machine_st.cp); + } + &Instruction::CallProcessKill => { + try_or_throw!(self.machine_st, self.process_kill()); + step_or_fail!(self, self.machine_st.p += 1); + } + &Instruction::ExecuteProcessKill => { + try_or_throw!(self.machine_st, self.process_kill()); + step_or_fail!(self, self.machine_st.p = self.machine_st.cp); + } + &Instruction::CallProcessRelease => { + try_or_throw!(self.machine_st, self.process_release()); + step_or_fail!(self, self.machine_st.p += 1); + } + &Instruction::ExecuteProcessRelease => { + try_or_throw!(self.machine_st, self.process_release()); + step_or_fail!(self, self.machine_st.p = self.machine_st.cp); + } &Instruction::CallPid => { self.pid(); step_or_fail!(self, self.machine_st.p += 1); diff --git a/src/machine/machine_errors.rs b/src/machine/machine_errors.rs index 4551991e..c6836709 100644 --- a/src/machine/machine_errors.rs +++ b/src/machine/machine_errors.rs @@ -44,6 +44,7 @@ pub(crate) enum ValidType { // PredicateIndicator, // Variable TcpListener, + Process, } impl ValidType { @@ -67,6 +68,7 @@ impl ValidType { // ValidType::PredicateIndicator => atom!("predicate_indicator"), // ValidType::Variable => atom!("variable") ValidType::TcpListener => atom!("tcp_listener"), + ValidType::Process => atom!("process"), } } } @@ -405,6 +407,17 @@ impl MachineState { [atom_as_cell((atom!("stream"))), cell(culprit)] ); + MachineError { + stub, + location: None, + } + } + ExistenceError::Process(culprit) => { + let stub = functor!( + atom!("existence_error"), + [atom_as_cell((atom!("process"))), cell(culprit)] + ); + MachineError { stub, location: None, @@ -590,6 +603,15 @@ impl MachineState { } } + pub(super) fn unreachable_error(&self) -> MachineError { + let stub = functor!(atom!("system_error")); + + MachineError { + stub, + location: None, + } + } + #[cfg(feature = "ffi")] pub(super) fn ffi_error(&self, err: FFIError) -> MachineError { let error_atom = match err { @@ -1003,6 +1025,7 @@ pub enum ExistenceError { }, SourceSink(HeapCellValue), Stream(HeapCellValue), + Process(HeapCellValue), } #[derive(Debug)] diff --git a/src/machine/streams.rs b/src/machine/streams.rs index 25e9a75a..596b09de 100644 --- a/src/machine/streams.rs +++ b/src/machine/streams.rs @@ -23,6 +23,8 @@ use std::fmt::Debug; use std::fs::{File, OpenOptions}; use std::hash::Hash; use std::io; +use std::io::PipeReader; +use std::io::PipeWriter; use std::io::{Cursor, ErrorKind, Read, Seek, SeekFrom, Write}; use std::mem::ManuallyDrop; use std::net::{Shutdown, TcpStream}; @@ -588,6 +590,8 @@ arena_allocated_impl_for_stream!(StandardOutputStream, StandardOutputStream); arena_allocated_impl_for_stream!(StandardErrorStream, StandardErrorStream); arena_allocated_impl_for_stream!(CharReader, CallbackStream); arena_allocated_impl_for_stream!(CharReader, InputChannelStream); +arena_allocated_impl_for_stream!(CharReader, PipeReader); +arena_allocated_impl_for_stream!(CharReader, PipeWriter); #[derive(Debug, Copy, Clone)] pub enum Stream { @@ -608,6 +612,8 @@ pub enum Stream { StandardError(TypedArenaPtr), Callback(TypedArenaPtr), InputChannel(TypedArenaPtr), + PipeReader(TypedArenaPtr), + PipeWriter(TypedArenaPtr), } impl From> for Stream { @@ -688,6 +694,8 @@ impl Stream { ArenaHeaderTag::InputChannelStream => { Stream::InputChannel(unsafe { ptr.as_typed_ptr() }) } + ArenaHeaderTag::PipeReader => Stream::PipeReader(unsafe { ptr.as_typed_ptr() }), + ArenaHeaderTag::PipeWriter => Stream::PipeWriter(unsafe { ptr.as_typed_ptr() }), _ => unreachable!(), } } @@ -726,6 +734,8 @@ impl Stream { Stream::StandardError(ptr) => ptr.header_ptr(), Stream::Callback(ptr) => ptr.header_ptr(), Stream::InputChannel(ptr) => ptr.header_ptr(), + Stream::PipeReader(ptr) => ptr.header_ptr(), + Stream::PipeWriter(ptr) => ptr.header_ptr(), } } @@ -748,6 +758,8 @@ impl Stream { Stream::StandardError(ref ptr) => &ptr.options, Stream::Callback(ref ptr) => &ptr.options, Stream::InputChannel(ref ptr) => &ptr.options, + Stream::PipeReader(ref ptr) => &ptr.options, + Stream::PipeWriter(ref ptr) => &ptr.options, } } @@ -770,6 +782,8 @@ impl Stream { Stream::StandardError(ref mut ptr) => &mut ptr.options, Stream::Callback(ref mut ptr) => &mut ptr.options, Stream::InputChannel(ref mut ptr) => &mut ptr.options, + Stream::PipeReader(ref mut ptr) => &mut ptr.options, + Stream::PipeWriter(ref mut ptr) => &mut ptr.options, } } @@ -793,6 +807,8 @@ impl Stream { Stream::StandardError(ptr) => ptr.lines_read += incr_num_lines_read, Stream::Callback(ptr) => ptr.lines_read += incr_num_lines_read, Stream::InputChannel(ptr) => ptr.lines_read += incr_num_lines_read, + Stream::PipeReader(ptr) => ptr.lines_read += incr_num_lines_read, + Stream::PipeWriter(_) => {} } } @@ -816,6 +832,8 @@ impl Stream { Stream::StandardError(ptr) => ptr.lines_read = value, Stream::Callback(ptr) => ptr.lines_read = value, Stream::InputChannel(ptr) => ptr.lines_read = value, + Stream::PipeReader(ptr) => ptr.lines_read = value, + Stream::PipeWriter(_) => {} } } @@ -839,6 +857,8 @@ impl Stream { Stream::StandardError(ptr) => ptr.lines_read, Stream::Callback(ptr) => ptr.lines_read, Stream::InputChannel(ptr) => ptr.lines_read, + Stream::PipeReader(ptr) => ptr.lines_read, + Stream::PipeWriter(_) => 0, } } } @@ -856,6 +876,8 @@ impl CharRead for Stream { Stream::StaticString(src) => (*src).peek_char(), Stream::Byte(cursor) => (*cursor).peek_char(), Stream::InputChannel(cursor) => (*cursor).peek_char(), + Stream::PipeReader(cursor) => (*cursor).peek_char(), + #[cfg(feature = "http")] Stream::HttpWrite(_) => Some(Err(std::io::Error::new( ErrorKind::PermissionDenied, @@ -865,7 +887,8 @@ impl CharRead for Stream { | Stream::StandardError(_) | Stream::StandardOutput(_) | Stream::Null(_) - | Stream::Callback(_) => Some(Err(std::io::Error::new( + | Stream::Callback(_) + | Stream::PipeWriter(_) => Some(Err(std::io::Error::new( ErrorKind::PermissionDenied, StreamError::ReadFromOutputStream, ))), @@ -884,6 +907,7 @@ impl CharRead for Stream { Stream::StaticString(src) => (*src).read_char(), Stream::Byte(cursor) => (*cursor).read_char(), Stream::InputChannel(cursor) => (*cursor).read_char(), + Stream::PipeReader(cursor) => (*cursor).read_char(), #[cfg(feature = "http")] Stream::HttpWrite(_) => Some(Err(std::io::Error::new( ErrorKind::PermissionDenied, @@ -893,7 +917,8 @@ impl CharRead for Stream { | Stream::StandardError(_) | Stream::StandardOutput(_) | Stream::Null(_) - | Stream::Callback(_) => Some(Err(std::io::Error::new( + | Stream::Callback(_) + | Stream::PipeWriter(_) => Some(Err(std::io::Error::new( ErrorKind::PermissionDenied, StreamError::ReadFromOutputStream, ))), @@ -911,13 +936,15 @@ impl CharRead for Stream { Stream::Readline(rl_stream) => rl_stream.put_back_char(c), Stream::StaticString(src) => src.put_back_char(c), Stream::Byte(cursor) => cursor.put_back_char(c), + Stream::PipeReader(cursor) => cursor.put_back_char(c), #[cfg(feature = "http")] Stream::HttpWrite(_) => {} Stream::OutputFile(_) | Stream::StandardError(_) | Stream::StandardOutput(_) | Stream::Null(_) - | Stream::Callback(_) => {} + | Stream::Callback(_) + | Stream::PipeWriter(_) => {} Stream::InputChannel(_) => {} } } @@ -934,13 +961,15 @@ impl CharRead for Stream { Stream::StaticString(ref mut src) => src.consume(nread), Stream::Byte(ref mut cursor) => cursor.consume(nread), Stream::InputChannel(ref mut cursor) => cursor.consume(nread), + Stream::PipeReader(ref mut cursor) => cursor.consume(nread), #[cfg(feature = "http")] Stream::HttpWrite(_) => {} Stream::OutputFile(_) | Stream::StandardError(_) | Stream::StandardOutput(_) | Stream::Null(_) - | Stream::Callback(_) => {} + | Stream::Callback(_) + | Stream::PipeWriter(_) => {} } } } @@ -959,6 +988,7 @@ impl Read for Stream { Stream::StaticString(src) => (*src).read(buf), Stream::Byte(cursor) => (*cursor).read(buf), Stream::InputChannel(cursor) => (*cursor).read(buf), + Stream::PipeReader(cursor) => (*cursor).read(buf), #[cfg(feature = "http")] Stream::HttpWrite(_) => Err(std::io::Error::new( ErrorKind::PermissionDenied, @@ -967,7 +997,8 @@ impl Read for Stream { Stream::OutputFile(_) | Stream::StandardError(_) | Stream::StandardOutput(_) - | Stream::Callback(_) => Err(std::io::Error::new( + | Stream::Callback(_) + | Stream::PipeWriter(_) => Err(std::io::Error::new( ErrorKind::PermissionDenied, StreamError::ReadFromOutputStream, )), @@ -989,6 +1020,7 @@ impl Write for Stream { Stream::StandardError(stream) => stream.write(buf), #[cfg(feature = "http")] Stream::HttpWrite(ref mut stream) => stream.get_mut().write(buf), + Stream::PipeWriter(ref mut stream) => stream.get_mut().write(buf), #[cfg(feature = "http")] Stream::HttpRead(_) => Err(std::io::Error::new( ErrorKind::PermissionDenied, @@ -998,7 +1030,8 @@ impl Write for Stream { Stream::StaticString(_) | Stream::InputChannel(_) | Stream::Readline(_) - | Stream::InputFile(..) => Err(std::io::Error::new( + | Stream::InputFile(..) + | Stream::PipeReader(_) => Err(std::io::Error::new( ErrorKind::PermissionDenied, StreamError::WriteToInputStream, )), @@ -1015,6 +1048,7 @@ impl Write for Stream { Stream::Callback(ref mut callback_stream) => callback_stream.stream.get_mut().flush(), Stream::StandardError(stream) => stream.stream.flush(), Stream::StandardOutput(stream) => stream.stream.flush(), + Stream::PipeWriter(ref mut stream) => stream.stream.get_mut().flush(), #[cfg(feature = "http")] Stream::HttpWrite(ref mut stream) => stream.stream.get_mut().flush(), #[cfg(feature = "http")] @@ -1026,7 +1060,8 @@ impl Write for Stream { Stream::StaticString(_) | Stream::InputChannel(_) | Stream::Readline(_) - | Stream::InputFile(_) => Err(std::io::Error::new( + | Stream::InputFile(_) + | Stream::PipeReader(_) => Err(std::io::Error::new( ErrorKind::PermissionDenied, StreamError::FlushToInputStream, )), @@ -1192,6 +1227,8 @@ impl Stream { Stream::StandardError(stream) => stream.past_end_of_stream, Stream::Callback(stream) => stream.past_end_of_stream, Stream::InputChannel(stream) => stream.past_end_of_stream, + Stream::PipeReader(stream) => stream.past_end_of_stream, + Stream::PipeWriter(stream) => stream.past_end_of_stream, } } @@ -1220,6 +1257,8 @@ impl Stream { Stream::StandardError(stream) => stream.past_end_of_stream = value, Stream::Callback(stream) => stream.past_end_of_stream = value, Stream::InputChannel(stream) => stream.past_end_of_stream = value, + Stream::PipeReader(stream) => stream.past_end_of_stream = value, + Stream::PipeWriter(stream) => stream.past_end_of_stream = value, } } @@ -1330,7 +1369,8 @@ impl Stream { | Stream::InputChannel(_) | Stream::Readline(_) | Stream::StaticString(_) - | Stream::InputFile(..) => atom!("read"), + | Stream::InputFile(..) + | Stream::PipeReader(_) => atom!("read"), Stream::NamedTcp(..) => atom!("read_append"), Stream::OutputFile(file) if file.is_append => atom!("append"), #[cfg(feature = "http")] @@ -1338,7 +1378,8 @@ impl Stream { Stream::OutputFile(_) | Stream::StandardError(_) | Stream::StandardOutput(_) - | Stream::Callback(_) => { + | Stream::Callback(_) + | Stream::PipeWriter(_) => { atom!("write") } Stream::Null(_) => atom!(""), @@ -1372,6 +1413,20 @@ impl Stream { )) } + pub(crate) fn from_pipe_writer(writer: io::PipeWriter, arena: &mut Arena) -> Stream { + Stream::PipeWriter(arena_alloc!( + ManuallyDrop::new(StreamLayout::new(CharReader::new(writer))), + arena + )) + } + + pub(crate) fn from_pipe_reader(reader: io::PipeReader, arena: &mut Arena) -> Stream { + Stream::PipeReader(arena_alloc!( + ManuallyDrop::new(StreamLayout::new(CharReader::new(reader))), + arena + )) + } + #[inline] pub(crate) fn from_tcp_stream(address: Atom, tcp_stream: TcpStream, arena: &mut Arena) -> Self { tcp_stream.set_read_timeout(None).unwrap(); @@ -1512,6 +1567,16 @@ impl Stream { Ok(()) } + Stream::PipeReader(mut stream) => { + stream.drop_payload(); + Ok(()) + } + + Stream::PipeWriter(mut stream) => { + stream.drop_payload(); + Ok(()) + } + Stream::Null(_) => Ok(()), Stream::Readline(_) | Stream::StandardOutput(_) | Stream::StandardError(_) => { @@ -1538,6 +1603,7 @@ impl Stream { | Stream::Readline(_) | Stream::StaticString(_) | Stream::InputFile(..) + | Stream::PipeReader(_) | Stream::Null(_) => true, _ => false, } @@ -1556,6 +1622,7 @@ impl Stream { | Stream::Byte(_) | Stream::OutputFile(..) | Stream::Callback(_) + | Stream::PipeWriter(_) | Stream::Null(_) => true, _ => false, } diff --git a/src/machine/system_calls.rs b/src/machine/system_calls.rs index 8c0eee0a..3f367a39 100644 --- a/src/machine/system_calls.rs +++ b/src/machine/system_calls.rs @@ -56,6 +56,8 @@ use std::net::{SocketAddr, ToSocketAddrs}; use std::net::{TcpListener, TcpStream}; use std::num::NonZeroU32; use std::process; +use std::process::Child; +use std::process::Stdio; #[cfg(feature = "http")] use std::str::FromStr; #[cfg(feature = "http")] @@ -8393,6 +8395,431 @@ impl Machine { }; } + pub(crate) fn process_create(&mut self) -> CallResult { + fn stub_gen() -> Vec { + functor_stub(atom!("process_create"), 3) + } + + // String + let exe_r = self.deref_register(1); + // [String,...] + let args_r = self.deref_register(2); + // [std] | [null] | [pipe, Var] | [file, String] + let stdin_r = self.deref_register(3); + let stdout_r = self.deref_register(4); + let stderr_r = self.deref_register(5); + // [env | environment, [[String, String],...]] + let env_r = self.deref_register(6); + // String ("." for keep current cwd) + let cwd_r = self.deref_register(7); + // Var + let pid_r = self.deref_register(8); + + let exe = self + .machine_st + .value_to_str_like(exe_r) + .expect("invalid values should have been rejected on the prolog side"); + + let args = self + .machine_st + .try_from_list(args_r, stub_gen) + .expect("invalid values should have been rejected on the prolog side") + .into_iter() + .map(|arg| { + self.machine_st + .value_to_str_like(arg) + .expect("invalid values should have been rejected on the prolog side") + .as_str() + .to_string() + }) + .collect::>(); + + let stdin_args = self.machine_st.try_from_list(stdin_r, stub_gen)?; + let stdin = self.handle_input_stream(stdin_args)?; + + let stdout_args = self.machine_st.try_from_list(stdout_r, stub_gen)?; + let stdout = self.handle_output_stream(stdout_args)?; + + let stderr_args = self.machine_st.try_from_list(stderr_r, stub_gen)?; + let stderr = self.handle_output_stream(stderr_args)?; + + let env_args = self.machine_st.try_from_list(env_r, stub_gen)?; + + let clear_env = match env_args[0].to_atom() { + Some(atom!("env")) => true, + Some(atom!("environment")) => false, + _ => panic!("Invalid value for clear_env"), + }; + + let envs = self + .machine_st + .try_from_list(env_args[1], stub_gen)? + .into_iter() + .map(|entry| { + let entry = self.machine_st.try_from_list(entry, stub_gen)?; + let name = self + .machine_st + .value_to_str_like(entry[0]) + .expect("invalid values should have been rejected on the prolog side") + .as_str() + .to_string(); + let value = self + .machine_st + .value_to_str_like(entry[1]) + .expect("invalid values should have been rejected on the prolog side") + .as_str() + .to_string(); + Ok((name, value)) + }) + .collect::, MachineStub>>()?; + + let cwd = self + .machine_st + .value_to_str_like(cwd_r) + .expect("invalid values should have been rejected on the prolog side"); + + let mut command = std::process::Command::new(&*exe.as_str()); + command.args(args); + + if &*cwd.as_str() != "." { + command.current_dir(&*cwd.as_str()); + } + + if clear_env { + command.env_clear(); + } + + command + .envs(envs) + .stdin(stdin) + .stdout(stdout) + .stderr(stderr); + + match command.spawn() { + Ok(child) => { + let child_process_alloc: TypedArenaPtr = + arena_alloc!(child, &mut self.machine_st.arena); + + unify!( + self.machine_st, + pid_r, + typed_arena_ptr_as_cell!(child_process_alloc) + ); + + Ok(()) + } + Err(_) => { + let perm_error = self.machine_st.permission_error( + Permission::Create, + atom!("process"), + stub_gen(), + ); + Err(self.machine_st.error_form(perm_error, stub_gen())) + } + } + } + + fn handle_output_stream(&mut self, args: Vec) -> Result { + Ok(match args[0].to_atom() { + Some(atom!("std")) => Stdio::inherit(), + Some(atom!("null")) => Stdio::null(), + Some(atom!("pipe")) => { + let (reader, writer) = match std::io::pipe() { + Ok(pipe_pair) => pipe_pair, + Err(_) => { + return Err(self.machine_st.open_permission_error( + atom!("anonymous_pipe"), + atom!("process_create"), + 3, + )); + } + }; + + let stream = Stream::from_pipe_reader(reader, &mut self.machine_st.arena); + + self.indices + .add_stream(stream, atom!("process_create"), 3) + .map_err(|stub_gen| stub_gen(&mut self.machine_st))?; + + self.machine_st + .bind(args[1].as_var().unwrap(), stream.into()); + + Stdio::from(writer) + } + Some(atom!("file")) => { + let path = self.machine_st.value_to_str_like(args[1]).unwrap(); + + let file = match std::fs::File::open(&*path.as_str()) { + Ok(file) => file, + Err(_) => { + return Err(self.machine_st.open_permission_error( + args[1], + atom!("process_create"), + 3, + )); + } + }; + Stdio::from(file) + } + _ => { + panic!("Invalid stdout tag") + } + }) + } + + fn handle_input_stream(&mut self, args: Vec) -> Result { + Ok(match args[0].to_atom() { + Some(atom!("std")) => Stdio::inherit(), + Some(atom!("null")) => Stdio::null(), + Some(atom!("pipe")) => { + let (reader, writer) = match std::io::pipe() { + Ok(pipe_pair) => pipe_pair, + Err(_) => { + return Err(self.machine_st.open_permission_error( + atom!("anonymous_pipe"), + atom!("process_create"), + 3, + )); + } + }; + + let stream = Stream::from_pipe_writer(writer, &mut self.machine_st.arena); + + self.indices + .add_stream(stream, atom!("process_create"), 3) + .map_err(|stub_gen| stub_gen(&mut self.machine_st))?; + + self.machine_st + .bind(args[1].as_var().unwrap(), stream.into()); + + Stdio::from(reader) + } + Some(atom!("file")) => { + let path = self.machine_st.value_to_str_like(args[1]).unwrap(); + + let file = match std::fs::File::open(&*path.as_str()) { + Ok(file) => file, + Err(_) => { + return Err(self.machine_st.open_permission_error( + args[1], + atom!("process_create"), + 3, + )); + } + }; + Stdio::from(file) + } + _ => { + panic!("Invalid stdin tag") + } + }) + } + + pub(crate) fn process_id(&mut self) -> CallResult { + fn stub_gen() -> Vec { + functor_stub(atom!("process_id"), 2) + } + + // Process + let process_r = self.deref_register(1); + // Pid + let pid_r = self.deref_register(2); + + let Some(ptr) = process_r.to_untyped_arena_ptr() else { + let err = self.machine_st.type_error(ValidType::Process, process_r); + return Err(self.machine_st.error_form(err, stub_gen())); + }; + + let process = match_untyped_arena_ptr!(ptr, + (ArenaHeaderTag::ChildProcess, child_process) => { + child_process + } + (ArenaHeaderTag::Dropped, _dropped) => { + let err = self.machine_st.existence_error(ExistenceError::Process(process_r)); + return Err(self.machine_st.error_form(err, stub_gen())); + } + _ => { + let err = self.machine_st.type_error(ValidType::Process, process_r); + return Err(self.machine_st.error_form(err, stub_gen())); + } + ); + + self.machine_st.bind( + pid_r.as_var().unwrap(), + fixnum_as_cell!(Fixnum::build_with(process.id())), + ); + + Ok(()) + } + + pub(crate) fn process_wait(&mut self) -> CallResult { + fn stub_gen() -> Vec { + functor_stub(atom!("process_wait"), 3) + } + + // Process + let process_r = self.deref_register(1); + // Var | Status + let status_r = self.deref_register(2); + // timeout | 0 + let timeout_r = self.deref_register(3); + + let Some(ptr) = process_r.to_untyped_arena_ptr() else { + let err = self.machine_st.type_error(ValidType::Process, process_r); + return Err(self.machine_st.error_form(err, stub_gen())); + }; + + let mut process = match_untyped_arena_ptr!(ptr, + (ArenaHeaderTag::ChildProcess, child_process) => { + child_process + } + (ArenaHeaderTag::Dropped, _dropped) => { + let err = self.machine_st.existence_error(ExistenceError::Process(process_r)); + return Err(self.machine_st.error_form(err, stub_gen())); + } + _ => { + let err = self.machine_st.type_error(ValidType::Process, process_r); + return Err(self.machine_st.error_form(err, stub_gen())); + } + ); + + let status = if let Some(atom) = timeout_r.to_atom() { + match atom { + atom!("infinite") => process.wait().map(Some), + _ => { + panic!("Invalid Timeout value") + } + } + } else if let Some(timeout) = timeout_r.to_fixnum() { + if timeout.get_num() == 0 { + process.try_wait() + } else { + panic!("Invalid Timeout value") + } + } else { + panic!("Invalid Timeout value") + }; + + match status { + Ok(None) => { + unify!(self.machine_st, status_r, atom_as_cell!(atom!("timeout"))); + Ok(()) + } + Ok(Some(exit_status)) => { + if let Some(exit_code) = exit_status.code() { + let mut writer = + Heap::functor_writer(functor!(atom!("exit"), [fixnum(exit_code)])); + + match writer(&mut self.machine_st.heap) { + Ok(loc) => { + unify!(self.machine_st, status_r, loc); + } + Err(resource_err_loc) => { + self.machine_st.throw_resource_error(resource_err_loc); + } + } + Ok(()) + } else { + #[cfg(unix)] + { + use std::os::unix::process::ExitStatusExt; + + if let Some(signal) = ExitStatusExt::signal(&exit_status) { + let mut writer = + Heap::functor_writer(functor!(atom!("killed"), [fixnum(signal)])); + + match writer(&mut self.machine_st.heap) { + Ok(loc) => { + unify!(self.machine_st, status_r, loc); + } + Err(resource_err_loc) => { + self.machine_st.throw_resource_error(resource_err_loc); + } + } + Ok(()) + } else { + let err = self.machine_st.unreachable_error(); + Err(self.machine_st.error_form(err, stub_gen())) + } + } + #[cfg(not(unix))] + { + let err = self.machine_st.unreachable_error(); + Err(self.machine_st.error_form(err, stub_gen())) + } + } + } + Err(_) => { + let perm_error = self.machine_st.permission_error( + Permission::Modify, + atom!("process"), + stub_gen(), + ); + Err(self.machine_st.error_form(perm_error, stub_gen())) + } + } + } + + pub(crate) fn process_kill(&mut self) -> CallResult { + fn stub_gen() -> Vec { + functor_stub(atom!("process_kill"), 1) + } + + // Pid + let process_r = self.deref_register(1); + + let Some(ptr) = process_r.to_untyped_arena_ptr() else { + let err = self.machine_st.type_error(ValidType::Process, process_r); + return Err(self.machine_st.error_form(err, stub_gen())); + }; + + let mut process = match_untyped_arena_ptr!(ptr, + (ArenaHeaderTag::ChildProcess, child_process) => { + child_process + } + (ArenaHeaderTag::Dropped, _dropped) => { + let err = self.machine_st.existence_error(ExistenceError::Process(process_r)); + return Err(self.machine_st.error_form(err, stub_gen())); + } + _ => { + let err = self.machine_st.type_error(ValidType::Process, process_r); + return Err(self.machine_st.error_form(err, stub_gen())); + } + ); + + if process.kill().is_err() { + let perm_error = + self.machine_st + .permission_error(Permission::Modify, atom!("process"), stub_gen()); + return Err(self.machine_st.error_form(perm_error, stub_gen())); + } + Ok(()) + } + + pub(crate) fn process_release(&mut self) -> CallResult { + fn stub_gen() -> Vec { + functor_stub(atom!("process_release"), 1) + } + + let process = self.deref_register(1); + + if let Some(ptr) = process.to_untyped_arena_ptr() { + match_untyped_arena_ptr!(ptr, + (ArenaHeaderTag::ChildProcess, child_process) => { + child_process.drop_payload(); + + return Ok(()); + } + _ => { + } + ); + } + + let err = self.machine_st.type_error(ValidType::Process, process); + + Err(self.machine_st.error_form(err, stub_gen())) + } + #[inline(always)] pub(crate) fn chars_base64(&mut self) -> CallResult { let padding = cell_as_atom!(self.deref_register(3)); diff --git a/src/macros.rs b/src/macros.rs index e301832b..79470f18 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -218,6 +218,24 @@ macro_rules! match_untyped_arena_ptr_pat_body { #[allow(unused_braces)] $code }}; + ($ptr:ident, PipeReader, $listener:ident, $code:expr) => {{ + #[allow(unused_mut)] + let mut $listener = unsafe { $ptr.as_typed_ptr::() }; + #[allow(unused_braces)] + $code + }}; + ($ptr:ident, PipeWriter, $listener:ident, $code:expr) => {{ + #[allow(unused_mut)] + let mut $listener = unsafe { $ptr.as_typed_ptr::() }; + #[allow(unused_braces)] + $code + }}; + ($ptr:ident, ChildProcess, $listener:ident, $code:expr) => {{ + #[allow(unused_mut)] + let mut $listener = unsafe { $ptr.as_typed_ptr::() }; + #[allow(unused_braces)] + $code + }}; ($ptr:ident, $($tags:tt)|+, $s:ident, $code:expr) => {{ let $s = Stream::from_tag($ptr.get_tag(), $ptr); #[allow(unused_braces)] @@ -240,6 +258,8 @@ macro_rules! match_untyped_arena_ptr_pat { | ArenaHeaderTag::InputChannelStream | ArenaHeaderTag::StandardOutputStream | ArenaHeaderTag::StandardErrorStream + | ArenaHeaderTag::PipeReader + | ArenaHeaderTag::PipeWriter }; ($tag:ident) => { ArenaHeaderTag::$tag diff --git a/tests/scryer/cli/src_tests/all_modules.stdin b/tests/scryer/cli/src_tests/all_modules.stdin index c61818fe..24338cf0 100644 --- a/tests/scryer/cli/src_tests/all_modules.stdin +++ b/tests/scryer/cli/src_tests/all_modules.stdin @@ -27,6 +27,7 @@ use_module(library(ordsets)). use_module(library(os)). use_module(library(pairs)). use_module(library(pio)). +use_module(library(process)). use_module(library(queues)). use_module(library(random)). use_module(library(reif)). diff --git a/tests/scryer/cli/src_tests/all_modules.stdout b/tests/scryer/cli/src_tests/all_modules.stdout index 4e32a715..5ae49ac4 100644 --- a/tests/scryer/cli/src_tests/all_modules.stdout +++ b/tests/scryer/cli/src_tests/all_modules.stdout @@ -45,3 +45,4 @@ true. true. true. + true. diff --git a/tests/scryer/cli/src_tests/process.md b/tests/scryer/cli/src_tests/process.md new file mode 100644 index 00000000..fb0dccd8 --- /dev/null +++ b/tests/scryer/cli/src_tests/process.md @@ -0,0 +1,59 @@ +```trycmd +$ scryer-prolog -f --no-add-history -g 'use_module(library(process)), process_create("", [], [invalid, process(P)]), process_kill(P), halt' +use_module(library(process)),process_create([],[],[invalid,process(P)]),process_kill(P),halt causes: error(domain_error(process_create_option,invalid),[predicate-process_create/3,predicate-check_options/3,predicate-must_be_known_options/3]) + +``` + +```trycmd +$ scryer-prolog -f --no-add-history -g 'use_module(library(process)), process_create("", [], [invalid(_Var), process(P)]), process_kill(P), halt' +use_module(library(process)),process_create([],[],[invalid(_Var),process(P)]),process_kill(P),halt causes: error(domain_error(process_create_option,invalid),[predicate-process_create/3,predicate-check_options/3,predicate-must_be_known_options/3]) + +``` + +```trycmd +$ scryer-prolog -f --no-add-history -g 'use_module(library(process)), process_create("", [], [stdin(null), stdin(null), process(P)]), process_kill(P), halt' +use_module(library(process)),process_create([],[],[stdin(null),stdin(null),process(P)]),process_kill(P),halt causes: error(domain_error(non_duplicate_options,stdin),[predicate-process_create/3,predicate-check_options/3,predicate-must_be_known_options/3]) + +``` + +```trycmd +$ scryer-prolog -f --no-add-history -g 'use_module(library(process)), process_create("", [], [env([]), environment([]), process(P)]), process_kill(P), halt' +use_module(library(process)),process_create([],[],[env([]),environment([]),process(P)]),process_kill(P),halt causes: error(domain_error(non_conflicting_options,[env([]),environment([])]),[predicate-process_create/3,predicate-check_options/3,predicate-extract_options/2]) + +``` + +```trycmd +$ scryer-prolog -f --no-add-history -g 'use_module(library(process)), process_wait(pid, _Status, [invalid(_Var), timeout(0)]), halt' +use_module(library(process)),process_wait(pid,_Status,[invalid(_Var),timeout(0)]),halt causes: error(domain_error(process_wait_option,invalid),[predicate-process_wait/3,predicate-check_options/3,predicate-must_be_known_options/3]) + +``` + +```trycmd +$ scryer-prolog -f --no-add-history -g 'use_module(library(process)), process_create("", [], [stdin(invalid), process(P)]), process_kill(P), halt' +use_module(library(process)),process_create([],[],[stdin(invalid),process(P)]),process_kill(P),halt causes: error(domain_error(stdio_spec,invalid),[predicate-process_create/3,predicate-check_options/3,predicate-extract_options/2,predicate-valid_stdio/1]) + +``` + +```trycmd +$ scryer-prolog -f --no-add-history -g 'use_module(library(process)), process_wait(50, _Status), halt' +use_module(library(process)),process_wait(50,_Status),halt causes: error(type_error(process,50),[predicate-process_wait/2,predicate-process_wait/3|process_wait/3]) + +``` + +```trycmd +$ scryer-prolog -f --no-add-history -g 'use_module(library(process)), process_kill(50), halt' +use_module(library(process)),process_kill(50),halt causes: error(type_error(process,50),[predicate-process_kill/1|process_kill/1]) + +``` + +```trycmd +$ scryer-prolog -f --no-add-history -g 'use_module(library(process)), process_id(50,_Pid), halt' +use_module(library(process)),process_id(50,_Pid),halt causes: error(type_error(process,50),[predicate-process_id/2|process_id/2]) + +``` + +```trycmd +$ scryer-prolog -f --no-add-history -g 'use_module(library(process)), process_release(50), halt' +use_module(library(process)),process_release(50),halt causes: error(type_error(process,50),[predicate-process_release/1,predicate-process_wait/2,predicate-process_wait/3|process_wait/3]) + +``` diff --git a/tests/scryer/cli/unix/process.md b/tests/scryer/cli/unix/process.md new file mode 100644 index 00000000..6bb174e4 --- /dev/null +++ b/tests/scryer/cli/unix/process.md @@ -0,0 +1,14 @@ +```trycmd +$ scryer-prolog -f --no-add-history -g 'use_module(library(process)), process_create("false", [], [process(P)]), process_wait(P, exit(1)), halt' + +``` + +```trycmd +$ scryer-prolog -f --no-add-history -g 'use_module(library(process)), use_module(library(format)), process_create("sh", [], [process(P), stdout(null), stdin(pipe(S))]), format(S, "exit 1~n", []), process_wait(P, exit(1)), halt' + +``` + +```trycmd +$ scryer-prolog -f --no-add-history -g 'use_module(library(process)), process_create("sh", ["-c", "sleep 5"], [process(P), stdout(null)]), process_kill(P), process_wait(P, killed(9)), halt' + +``` \ No newline at end of file diff --git a/tests/scryer/cli/windows/process.md b/tests/scryer/cli/windows/process.md new file mode 100644 index 00000000..da27656b --- /dev/null +++ b/tests/scryer/cli/windows/process.md @@ -0,0 +1,9 @@ +```trycmd +$ scryer-prolog -f --no-add-history -g 'use_module(library(process)), process_create("cmd", ["/C", "exit", "1"], [process(P)]), process_wait(P, exit(1)), halt' + +``` + +```trycmd +$ scryer-prolog -f --no-add-history -g 'use_module(library(process)), use_module(library(format)), process_create("cmd", [], [process(P), stdout(null), stdin(pipe(S))]), format(S, "exit 1~n", []), process_wait(P, exit(1)), halt' + +``` \ No newline at end of file diff --git a/tests/scryer/main.rs b/tests/scryer/main.rs index a501bd42..c46ced24 100644 --- a/tests/scryer/main.rs +++ b/tests/scryer/main.rs @@ -19,9 +19,20 @@ mod src_tests; ignore = "miri isolation, unsupported operation: can't call foreign function" )] fn cli_tests() { - trycmd::TestCases::new() + let cases = trycmd::TestCases::new(); + cases .default_bin_name("scryer-prolog") .case("tests/scryer/cli/issues/*.toml") .case("tests/scryer/cli/src_tests/*.toml") .case("tests/scryer/cli/src_tests/*.md"); + + #[cfg(windows)] + { + cases.case("tests/scryer/cli/windows/*.md"); + } + + #[cfg(unix)] + { + cases.case("tests/scryer/cli/unix/*.md"); + } }