# 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:
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"]
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 = "1", Name = "$pid")))]
Pid,
#[strum_discriminants(strum(props(Arity = "4", Name = "$chars_base64")))]
&Instruction::CallSetEnv |
&Instruction::CallUnsetEnv |
&Instruction::CallShell |
+ &Instruction::CallProcessCreate |
&Instruction::CallPid |
&Instruction::CallCharsBase64 |
&Instruction::CallDevourWhitespace |
&Instruction::ExecuteSetEnv |
&Instruction::ExecuteUnsetEnv |
&Instruction::ExecuteShell |
+ &Instruction::ExecuteProcessCreate |
&Instruction::ExecutePid |
&Instruction::ExecuteCharsBase64 |
&Instruction::ExecuteDevourWhitespace |
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;
TcpListener = 0b1000000,
HttpListener = 0b1000001,
HttpResponse = 0b1000010,
+ PipeWriter = 0b1000011,
Dropped = 0b1000100,
+ PipeReader = 0b1000101,
}
#[bitfield]
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::NullStream => {
unreachable!("NullStream is never arena allocated!");
}
; true
).
must_be_(list, Term) :- check_(error:ilist, list, Term).
+must_be_(list(Elem), Term) :-
+ must_be_(list, Term),
+ check_all(Elem, Term).
must_be_(type, Term) :- check_(error:type, type, Term).
must_be_(boolean, Term) :- check_(error:boolean, boolean, Term).
must_be_(pair, Term) :- check_(error:pair, pair, Term).
% We cannot use maplist(must_be(character), Cs), because library(lists)
% uses library(error), so importing it would create a cyclic dependency.
-all_characters([]).
-all_characters([C|Cs]) :-
- must_be(character, C),
- all_characters(Cs).
+check_all(_, []).
+check_all(Type, [Head| Tail]) :-
+ must_be(Type, Head),
+ check_all(Type, Tail).
+
+all_characters(Cs) :- check_all(character, Cs).
check_(Pred, Type, Term) :-
( var(Term) -> instantiation_error(must_be/2)
type(octet_chars).
type(chars).
type(list).
+type(list(Type)) :- type(Type).
type(var).
type(boolean).
type(term).
--- /dev/null
+:- module(process, [process_create/3]).
+
+:- use_module(library(error)).
+:- use_module(library(iso_ext)).
+:- use_module(library(lists), [append/3, member/2]).
+
+process_create(Exe, Args, Options) :-
+ must_be(chars, Exe),
+ must_be(list(chars), Args),
+ must_be(list, Options),
+ check_option(Sin, find_stdio(Sin, stdin, Options), valid_stdio, [std], Stdin),
+ check_option(Sout, find_stdio(Sout, stdout, Options), valid_stdio, [std], Stdout),
+ check_option(Serr, find_stdio(Serr, stderr, Options), valid_stdio, [std], Stderr),
+ check_option(Envs, find_env(Envs, Options), valid_env, [environment, []], EnvVars),
+ check_option(P, member(process(P), Options), valid_pid, _, Pid),
+ check_option(C, member(cwd(C), Options), valid_cwd, _, Cwd),
+ '$process_create'(Exe, Args, Stdin, Stdout, Stderr, EnvVars, Cwd, Pid).
+
+
+check_option(Template, Goal, Pred, Default, Choice) :-
+ findall(Template, Goal, Solutions),
+ check_option_(Solutions, Pred, Default, Choice).
+
+check_option_([] , Pred , Default , Default ) :- call(Pred, Default).
+check_option_([Choice] , Pred , _ , Choice ) :- call(Pred, Choice).
+check_option_([X1,X2|Xs], _ , _ , _ ) :- throw(error(duplicate_option, process_create/3, [X1, X2 | Xs])).
+
+find_stdio([std], Kind, Options ) :- Elem =.. [Kind, std], member(Elem, Options).
+find_stdio([null], Kind, Options ) :- Elem =.. [Kind, null], member(Elem, Options).
+find_stdio([pipe, Stream], Kind, Options) :- Elem =.. [Kind, pipe(Stream)], member(Elem, Options).
+find_stdio([file, Path], Kind, Options ) :- Elem =.. [Kind, file(Path)], member(Elem, Options).
+
+valid_stdio([std]).
+valid_stdio([null]).
+valid_stdio([pipe, Stream]) :- must_be(var, Stream).
+valid_stdio([file, Path]) :- must_be(chars, Path).
+
+find_env([env, E], Options) :- member(env(E), Options).
+find_env([environment, E], Options) :- member(environment(E), Options).
+
+valid_cwd(Cwd) :- must_be(chars, Cwd).
+
+valid_env([env, E]) :- valid_env_(E).
+valid_env([environment, E]) :- valid_env_(E).
+
+valid_env_([]).
+valid_env_([N=V|Es]) :-
+ must_be(chars, N),
+ must_be(chars, V),
+ valid_env_(Es).
+
+valid_pid(Pid) :- must_be(var, Pid).
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::CallPid => {
self.pid();
step_or_fail!(self, self.machine_st.p += 1);
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};
arena_allocated_impl_for_stream!(StandardErrorStream, StandardErrorStream);
arena_allocated_impl_for_stream!(CharReader<CallbackStream>, CallbackStream);
arena_allocated_impl_for_stream!(CharReader<InputChannelStream>, InputChannelStream);
+arena_allocated_impl_for_stream!(CharReader<PipeReader>, PipeReader);
+arena_allocated_impl_for_stream!(CharReader<PipeWriter>, PipeWriter);
#[derive(Debug, Copy, Clone)]
pub enum Stream {
StandardError(TypedArenaPtr<StandardErrorStream>),
Callback(TypedArenaPtr<CallbackStream>),
InputChannel(TypedArenaPtr<InputChannelStream>),
+ PipeReader(TypedArenaPtr<PipeReader>),
+ PipeWriter(TypedArenaPtr<PipeWriter>),
}
impl From<TypedArenaPtr<ReadlineStream>> for 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(),
}
}
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,
}
}
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,
}
}
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(_) => {}
}
}
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(_) => {}
}
}
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,
}
}
}
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,
| 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,
))),
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,
| 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,
))),
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(_) => {}
}
}
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(_) => {}
}
}
}
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,
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,
)),
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,
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,
)),
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")]
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,
)),
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,
}
}
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,
}
}
| 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")]
Stream::OutputFile(_)
| Stream::StandardError(_)
| Stream::StandardOutput(_)
- | Stream::Callback(_) => {
+ | Stream::Callback(_)
+ | Stream::PipeWriter(_) => {
atom!("write")
}
Stream::Null(_) => atom!(""),
))
}
+ 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();
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(_) => {
use std::net::{TcpListener, TcpStream};
use std::num::NonZeroU32;
use std::process;
+use std::process::Stdio;
#[cfg(feature = "http")]
use std::str::FromStr;
#[cfg(feature = "http")]
};
}
+ pub(crate) fn process_create(&mut self) -> CallResult {
+ fn stub_gen() -> Vec<FunctorElement> {
+ functor_stub(atom!("process_create"), 3)
+ }
+
+ let exe_r = self.deref_register(1);
+ let args_r = self.deref_register(2);
+ let stdin_r = self.deref_register(3);
+ let stdout_r = self.deref_register(4);
+ let stderr_r = self.deref_register(5);
+ let env_r = self.deref_register(6);
+ let cwd_r = self.deref_register(7);
+ let pid_r = self.deref_register(8);
+
+ let exe = self.machine_st.value_to_str_like(exe_r).unwrap();
+
+ let args = self
+ .machine_st
+ .try_from_list(args_r, stub_gen)
+ .unwrap()
+ .into_iter()
+ .map(|arg| {
+ self.machine_st
+ .value_to_str_like(arg)
+ .unwrap()
+ .as_str()
+ .to_string()
+ })
+ .collect::<Vec<_>>();
+
+ 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 env_names = self.machine_st.try_from_list(env_args[1], stub_gen)?;
+ let env_values = self.machine_st.try_from_list(env_args[2], stub_gen)?;
+
+ let envs = env_names
+ .into_iter()
+ .zip(env_values)
+ .map(|(name, value)| {
+ let name = self
+ .machine_st
+ .value_to_str_like(name)
+ .unwrap()
+ .as_str()
+ .to_string();
+ let value = self
+ .machine_st
+ .value_to_str_like(value)
+ .unwrap()
+ .as_str()
+ .to_string();
+ (name, value)
+ })
+ .collect::<Vec<_>>();
+
+ let cwd = self.machine_st.value_to_str_like(cwd_r);
+
+ let mut command = std::process::Command::new(&*exe.as_str());
+ command.args(args);
+
+ if let Some(cwd) = cwd {
+ 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) => {
+ self.machine_st
+ .unify_fixnum(Fixnum::build_with(child.id()), pid_r);
+ Ok(())
+ }
+ Err(_) => {
+ self.machine_st.fail = true;
+ Ok(())
+ }
+ }
+ }
+
+ fn handle_output_stream(&mut self, args: Vec<HeapCellValue>) -> Result<Stdio, MachineStub> {
+ Ok(match args[0].to_atom() {
+ Some(atom!("std")) => Stdio::inherit(),
+ Some(atom!("null")) => Stdio::null(),
+ Some(atom!("pipe")) => {
+ // TODO handler Err
+ let (reader, writer) = std::io::pipe().unwrap();
+
+ 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[2].as_var().unwrap(), stream.into());
+
+ Stdio::from(writer)
+ }
+ Some(atom!("file")) => {
+ let path = self.machine_st.value_to_str_like(args[1]).unwrap();
+
+ // TODO handler Err
+ let file = std::fs::File::open(&*path.as_str()).unwrap();
+ Stdio::from(file)
+ }
+ _ => {
+ panic!("Invalid stdin tag")
+ }
+ })
+ }
+
+ fn handle_input_stream(&mut self, args: Vec<HeapCellValue>) -> Result<Stdio, MachineStub> {
+ Ok(match args[0].to_atom() {
+ Some(atom!("std")) => Stdio::inherit(),
+ Some(atom!("null")) => Stdio::null(),
+ Some(atom!("pipe")) => {
+ // TODO handler Err
+ let (reader, writer) = std::io::pipe().unwrap();
+
+ 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))
+ .unwrap();
+
+ self.machine_st
+ .bind(args[2].as_var().unwrap(), stream.into());
+
+ Stdio::from(reader)
+ }
+ Some(atom!("file")) => {
+ let path = self.machine_st.value_to_str_like(args[1]).unwrap();
+
+ // TODO handler Err
+ let file = std::fs::File::open(&*path.as_str()).unwrap();
+ Stdio::from(file)
+ }
+ _ => {
+ panic!("Invalid stdin tag")
+ }
+ })
+ }
+
#[inline(always)]
pub(crate) fn chars_base64(&mut self) -> CallResult {
let padding = cell_as_atom!(self.deref_register(3));