diff --git a/docs/source/setup/installation.md b/docs/source/setup/installation.md index 44ef57e88..8b1f2b68c 100644 --- a/docs/source/setup/installation.md +++ b/docs/source/setup/installation.md @@ -13,9 +13,9 @@ choice. ## Supported platforms -Inko supports Linux, macOS, and FreeBSD (13.2 or newer). Inko might also work on -other platforms, but we only provide support for the listed platforms. Windows -isn't supported. +Inko supports Linux (4.11 or newer), macOS (10.15 or newer), and FreeBSD (13.2 +or newer). Inko might also work on other platforms, but we only provide support +for the listed platforms. Windows isn't supported. ## Requirements diff --git a/rt/src/process.rs b/rt/src/process.rs index 26bd90cfb..000d0e85d 100644 --- a/rt/src/process.rs +++ b/rt/src/process.rs @@ -746,19 +746,6 @@ impl ProcessPointer { self.as_ptr() as usize } - // TODO: remove - pub(crate) fn blocking(mut self, function: impl FnOnce() -> R) -> R { - // Safety: threads are stored in processes before running them. - let thread = unsafe { self.thread() }; - - thread.start_blocking(); - - let res = function(); - - thread.stop_blocking(self); - res - } - pub(crate) fn start_blocking(mut self) { // Safety: threads are stored in processes before running them. unsafe { self.thread() }.start_blocking(); diff --git a/rt/src/runtime.rs b/rt/src/runtime.rs index fc684eb65..d668f40c8 100644 --- a/rt/src/runtime.rs +++ b/rt/src/runtime.rs @@ -10,7 +10,6 @@ mod random; mod signal; mod socket; mod string; -mod sys; mod time; mod tls; @@ -25,8 +24,6 @@ use crate::stack::Stack; use crate::state::{MethodCounts, RcState, State}; use rustix::param::page_size; use std::ffi::CStr; -use std::io::{stdout, Write as _}; -use std::process::exit as rust_exit; use std::slice; use std::thread; @@ -87,7 +84,6 @@ pub unsafe extern "system" fn inko_runtime_start( method: NativeAsyncMethod, ) { (*runtime).start(class, method); - flush_stdout(); } #[no_mangle] @@ -107,17 +103,6 @@ pub unsafe extern "system" fn inko_runtime_stack_mask( !(total - 1) } -fn flush_stdout() { - // STDOUT is buffered by default, and not flushing it upon exit may result - // in parent processes not observing the output. - let _ = stdout().lock().flush(); -} - -pub(crate) fn exit(status: i32) -> ! { - flush_stdout(); - rust_exit(status); -} - /// An Inko runtime along with all its state. #[repr(C)] pub struct Runtime { diff --git a/rt/src/runtime/general.rs b/rt/src/runtime/general.rs index 3208bb8bd..5f558447e 100644 --- a/rt/src/runtime/general.rs +++ b/rt/src/runtime/general.rs @@ -1,6 +1,5 @@ use crate::mem::{header_of, ClassPointer}; use crate::process::ProcessPointer; -use crate::runtime::exit; use crate::runtime::process::panic; use std::alloc::handle_alloc_error; use std::io::Error; @@ -26,11 +25,6 @@ extern "C" { fn errno_location() -> *mut i32; } -#[no_mangle] -pub unsafe extern "system" fn inko_exit(status: i64) { - exit(status as i32); -} - #[no_mangle] pub unsafe extern "system" fn inko_reference_count_error( process: ProcessPointer, diff --git a/rt/src/runtime/helpers.rs b/rt/src/runtime/helpers.rs index 38b4f924e..5a4235842 100644 --- a/rt/src/runtime/helpers.rs +++ b/rt/src/runtime/helpers.rs @@ -4,22 +4,7 @@ use crate::process::ProcessPointer; use crate::scheduler::timeouts::Timeout; use crate::socket::Socket; use crate::state::State; -use std::io::{self, Read}; - -/// Reads a number of bytes from a buffer into a Vec. -pub(crate) fn read_into( - stream: &mut T, - output: &mut Vec, - size: i64, -) -> Result { - let read = if size > 0 { - stream.take(size as u64).read_to_end(output)? - } else { - stream.read_to_end(output)? - }; - - Ok(read as i64) -} +use std::io::{self}; pub(crate) fn poll( state: &State, diff --git a/rt/src/runtime/process.rs b/rt/src/runtime/process.rs index ee992fef0..f405638e7 100644 --- a/rt/src/runtime/process.rs +++ b/rt/src/runtime/process.rs @@ -5,12 +5,12 @@ use crate::process::{ ReceiveResult, RescheduleRights, SendResult, StackFrame, }; use crate::result::Result as InkoResult; -use crate::runtime::exit; use crate::scheduler::process::Action; use crate::scheduler::timeouts::Timeout; use crate::state::State; use std::cmp::max; use std::fmt::Write as _; +use std::process::exit; use std::str; use std::time::Duration; diff --git a/rt/src/runtime/sys.rs b/rt/src/runtime/sys.rs deleted file mode 100644 index 2e4255cbb..000000000 --- a/rt/src/runtime/sys.rs +++ /dev/null @@ -1,188 +0,0 @@ -use crate::mem::{ByteArray, String as InkoString}; -use crate::process::ProcessPointer; -use crate::result::Result as InkoResult; -use crate::runtime::helpers::read_into; -use std::io::Write; -use std::process::{Child, Command, Stdio}; -use std::slice; - -fn stdio_for(value: i64) -> Stdio { - match value { - 1 => Stdio::inherit(), - 2 => Stdio::piped(), - _ => Stdio::null(), - } -} - -#[no_mangle] -pub(crate) unsafe extern "system" fn inko_child_process_spawn( - process: ProcessPointer, - program: *const InkoString, - args: *const *const InkoString, - args_length: i64, - env: *const *const InkoString, - env_length: i64, - stdin: i64, - stdout: i64, - stderr: i64, - directory: *const InkoString, -) -> InkoResult { - let program = InkoString::read(program); - let args = slice::from_raw_parts(args, args_length as _); - let env = slice::from_raw_parts(env, env_length as _); - let directory = InkoString::read(directory); - let mut cmd = Command::new(program); - - for &ptr in args { - cmd.arg(InkoString::read(ptr as _)); - } - - for pair in env.chunks(2) { - let key = InkoString::read(pair[0] as _); - let val = InkoString::read(pair[1] as _); - - cmd.env(key, val); - } - - cmd.stdin(stdio_for(stdin)); - cmd.stdout(stdio_for(stdout)); - cmd.stderr(stdio_for(stderr)); - - if !directory.is_empty() { - cmd.current_dir(directory); - } - - process - .blocking(|| cmd.spawn()) - .map(InkoResult::ok_boxed) - .unwrap_or_else(InkoResult::io_error) -} - -#[no_mangle] -pub(crate) unsafe extern "system" fn inko_child_process_wait( - process: ProcessPointer, - child: *mut Child, -) -> InkoResult { - process - .blocking(|| (*child).wait()) - .map(|status| status.code().unwrap_or(0) as i64) - .map(|status| InkoResult::ok(status as _)) - .unwrap_or_else(InkoResult::io_error) -} - -#[no_mangle] -pub(crate) unsafe extern "system" fn inko_child_process_try_wait( - child: *mut Child, -) -> InkoResult { - let child = &mut *child; - - child - .try_wait() - .map(|status| { - InkoResult::ok({ - status.map(|s| s.code().unwrap_or(0)).unwrap_or(-1) as i64 - } as _) - }) - .unwrap_or_else(InkoResult::io_error) -} - -#[no_mangle] -pub(crate) unsafe extern "system" fn inko_child_process_stdout_read( - process: ProcessPointer, - child: *mut Child, - buffer: *mut ByteArray, - size: i64, -) -> InkoResult { - let child = &mut *child; - let buff = &mut (*buffer).value; - - child - .stdout - .as_mut() - .map(|stream| process.blocking(|| read_into(stream, buff, size))) - .unwrap_or(Ok(0)) - .map(|size| InkoResult::ok(size as _)) - .unwrap_or_else(InkoResult::io_error) -} - -#[no_mangle] -pub(crate) unsafe extern "system" fn inko_child_process_stderr_read( - process: ProcessPointer, - child: *mut Child, - buffer: *mut ByteArray, - size: i64, -) -> InkoResult { - let child = &mut *child; - let buff = &mut (*buffer).value; - - child - .stderr - .as_mut() - .map(|stream| process.blocking(|| read_into(stream, buff, size))) - .unwrap_or(Ok(0)) - .map(|size| InkoResult::ok(size as _)) - .unwrap_or_else(InkoResult::io_error) -} - -#[no_mangle] -pub(crate) unsafe extern "system" fn inko_child_process_stdin_write( - process: ProcessPointer, - child: *mut Child, - data: *mut u8, - size: i64, -) -> InkoResult { - let child = &mut *child; - let slice = std::slice::from_raw_parts(data, size as _); - - child - .stdin - .as_mut() - .map(|stream| process.blocking(|| stream.write(slice))) - .unwrap_or(Ok(0)) - .map(|size| InkoResult::ok(size as _)) - .unwrap_or_else(InkoResult::io_error) -} - -#[no_mangle] -pub(crate) unsafe extern "system" fn inko_child_process_stdin_flush( - process: ProcessPointer, - child: *mut Child, -) -> InkoResult { - let child = &mut *child; - - child - .stdin - .as_mut() - .map(|stream| process.blocking(|| stream.flush())) - .unwrap_or(Ok(())) - .map(|_| InkoResult::none()) - .unwrap_or_else(InkoResult::io_error) -} - -#[no_mangle] -pub(crate) unsafe extern "system" fn inko_child_process_stdout_close( - child: *mut Child, -) { - (*child).stdout.take(); -} - -#[no_mangle] -pub(crate) unsafe extern "system" fn inko_child_process_stderr_close( - child: *mut Child, -) { - (*child).stderr.take(); -} - -#[no_mangle] -pub(crate) unsafe extern "system" fn inko_child_process_stdin_close( - child: *mut Child, -) { - (*child).stdin.take(); -} - -#[no_mangle] -pub(crate) unsafe extern "system" fn inko_child_process_drop( - child: *mut Child, -) { - drop(Box::from_raw(child)); -} diff --git a/rt/src/rustls_platform_verifier/verification/apple.rs b/rt/src/rustls_platform_verifier/verification/apple.rs index 933c0f590..06d8c8f28 100644 --- a/rt/src/rustls_platform_verifier/verification/apple.rs +++ b/rt/src/rustls_platform_verifier/verification/apple.rs @@ -123,11 +123,19 @@ impl Verifier { // Safety: well, technically none, but due to the way the runtime uses // the verifier this should never misbehave. let process = unsafe { ProcessPointer::new(CURRENT_PROCESS.get()) }; - let trust_error = - match process.blocking(|| trust_evaluation.evaluate_with_error()) { - Ok(()) => return Ok(()), - Err(e) => e, - }; + + process.start_blocking(); + + let trust_error = match trust_evaluation.evaluate_with_error() { + Ok(()) => { + process.stop_blocking(); + return Ok(()); + } + Err(e) => { + process.stop_blocking(); + e + } + }; let err_code = trust_error.code(); diff --git a/std/src/std/alloc.inko b/std/src/std/alloc.inko new file mode 100644 index 000000000..eb170163f --- /dev/null +++ b/std/src/std/alloc.inko @@ -0,0 +1,34 @@ +import std.io (Error) +import std.libc + +# Allocates or resizes a chunk of raw memory such that it can fit `size` +# _elements_ (not bytes). +# +# This method is a thin wrapper around the `realloc(2)`. +# +# # Panics +# +# This method panics if `realloc()` returns `NULL` and the `size` argument +# _isn't_ zero. +fn resize[T](buffer: Pointer[T], size: Int) -> Pointer[T] { + let bytes = size * _INKO.size_of_type_parameter(T) + let ptr = libc.realloc(buffer as Pointer[UInt8], bytes) + + # In this case there's nothing we can do but abort. + if ptr as Int == 0 and size != 0 { + panic('std.alloc.resize() failed: ${Error.last_os_error}') + } + + ptr as Pointer[T] +} + +fn free[T](pointer: Pointer[T]) { + libc.free(pointer as Pointer[UInt8]) +} + +# Copies `size` _elements_ from the pointer `from` to the pointer `to`. +fn copy[T](from: Pointer[T], to: Pointer[T], size: Int) { + let bytes = size * _INKO.size_of_type_parameter(T) + + libc.memmove(to as Pointer[UInt8], from as Pointer[UInt8], bytes as UInt64) +} diff --git a/std/src/std/array.inko b/std/src/std/array.inko index 0fe6301e2..87ff80172 100644 --- a/std/src/std/array.inko +++ b/std/src/std/array.inko @@ -1,12 +1,13 @@ # An ordered, integer-indexed generic collection of values. +import std.alloc import std.clone (Clone) import std.cmp (Compare, Contains, Equal, Ordering, max, min) import std.drop (Drop) import std.fmt (Format, Formatter) import std.hash (Hash, Hasher) import std.iter (Iter, Stream) -import std.libc import std.option (Option) +import std.ptr import std.rand (Shuffle) # The capacity to use when resizing an array for the first time. @@ -112,10 +113,7 @@ class builtin Array[T] { fn pub static with_capacity(size: Int) -> Array[T] { if size < 0 { panic('The capacity must be greater than or equal to zero') } - let vsize = _INKO.size_of_type_parameter(T) - let buffer = libc.resize(0x0 as Pointer[T], size: size * vsize) - - Array(size: 0, capacity: size, buffer: buffer) + Array(size: 0, capacity: size, buffer: alloc.resize(0 as Pointer[T], size)) } # Returns an array filled with a certain amount of values. @@ -150,10 +148,8 @@ class builtin Array[T] { fn pub mut reserve(size: Int) { if @capacity - @size >= size { return } - let vsize = _INKO.size_of_type_parameter(T) - @capacity = max(@capacity * 2, @capacity + size) - @buffer = libc.resize(@buffer, @capacity * vsize) + @buffer = alloc.resize(@buffer, @capacity) } # Removes all values in the Array. @@ -248,13 +244,8 @@ class builtin Array[T] { let addr = address_of(index) let val = addr.0 - let vsize = _INKO.size_of_type_parameter(T) - libc.copy( - from: addr as Int + vsize as Pointer[T], - to: addr, - size: len - index - 1 * vsize, - ) + alloc.copy(from: ptr.add(addr, 1), to: addr, size: len - index - 1) @size = len - 1 val @@ -596,9 +587,8 @@ class builtin Array[T] { if index < @size { let from = address_of(index) let to = address_of(index + 1) - let vsize = _INKO.size_of_type_parameter(T) - libc.copy(from, to, size: @size - index * vsize) + alloc.copy(from, to, size: @size - index) } write_to(index, value) @@ -733,7 +723,7 @@ impl Array if T: mut { impl Drop for Array { fn mut drop { clear - libc.free(@buffer as Pointer[UInt8]) + alloc.free(@buffer) } } diff --git a/std/src/std/libc.inko b/std/src/std/libc.inko index 3e39c3439..aab557c0b 100644 --- a/std/src/std/libc.inko +++ b/std/src/std/libc.inko @@ -65,6 +65,8 @@ let O_RDONLY = sys.O_RDONLY let O_RDWR = sys.O_RDWR let O_TRUNC = sys.O_TRUNC let O_WRONLY = sys.O_WRONLY +let POSIX_SPAWN_SETSIGDEF = sys.POSIX_SPAWN_SETSIGDEF +let POSIX_SPAWN_SETSIGMASK = sys.POSIX_SPAWN_SETSIGMASK let SEEK_END = sys.SEEK_END let SEEK_SET = sys.SEEK_SET let SOL_SOCKET = sys.SOL_SOCKET @@ -76,6 +78,19 @@ let SO_REUSEADDR = sys.SO_REUSEADDR let SO_REUSEPORT = sys.SO_REUSEPORT let SO_SNDBUF = sys.SO_SNDBUF let TCP_NODELAY = sys.TCP_NODELAY +let WNOHANG = sys.WNOHANG + +class extern SigSet { + let @inner: sys.SigSet +} + +class extern PosixSpawnAttrs { + let @inner: sys.PosixSpawnAttrs +} + +class extern PosixSpawnFileActions { + let @inner: sys.PosixSpawnFileActions +} fn opendir(path: Pointer[UInt8]) -> Pointer[UInt8] { sys.opendir(path) @@ -123,36 +138,70 @@ fn extern isatty(fd: Int32) -> Int32 fn extern strlen(pointer: Pointer[UInt8]) -> UInt64 +fn extern posix_spawnp( + pid: Pointer[Int32], + file: Pointer[UInt8], + file_actions: Pointer[PosixSpawnFileActions], + attrp: Pointer[PosixSpawnAttrs], + argv: Pointer[UInt64], + envp: Pointer[UInt64], +) -> Int32 + +fn extern posix_spawn_file_actions_init( + actions: Pointer[PosixSpawnFileActions], +) -> Int32 + +fn extern posix_spawn_file_actions_destroy( + actions: Pointer[PosixSpawnFileActions], +) -> Int32 + +fn extern posix_spawn_file_actions_adddup2( + actions: Pointer[PosixSpawnFileActions], + fd: Int32, + new_fd: Int32, +) -> Int32 + +fn extern posix_spawn_file_actions_addchdir_np( + actions: mut Pointer[PosixSpawnFileActions], + path: Pointer[UInt8], +) -> Int32 + +fn extern posix_spawnattr_init(attr: mut Pointer[PosixSpawnAttrs]) -> Int32 + +fn extern posix_spawnattr_destroy(attr: mut Pointer[PosixSpawnAttrs]) -> Int32 + +fn extern posix_spawnattr_setflags( + attr: mut Pointer[PosixSpawnAttrs], + flags: Int16, +) -> Int32 + +fn extern posix_spawnattr_setsigdefault( + attr: mut Pointer[PosixSpawnAttrs], + mask: mut Pointer[SigSet], +) -> Int32 + +fn extern posix_spawnattr_setsigmask( + attr: mut Pointer[PosixSpawnAttrs], + mask: mut Pointer[SigSet], +) -> Int32 + +fn extern sigemptyset(set: mut Pointer[SigSet]) -> Int32 + +fn extern sigfillset(set: mut Pointer[SigSet]) -> Int32 + +fn extern waitpid(pid: Int32, status: Pointer[Int32], options: Int32) -> Int32 + fn extern realloc(pointer: Pointer[UInt8], size: Int) -> Pointer[UInt8] fn extern memmove( to: Pointer[UInt8], from: Pointer[UInt8], - size: Int, + size: UInt64, ) -> Pointer[UInt8] fn extern free(pointer: Pointer[UInt8]) -# A thin wrapper around `realloc()`. -# -# # Panics -# -# This method panics if `realloc()` returns `NULL` and the `size` argument -# _isn't_ zero. -fn resize[T](buffer: Pointer[T], size: Int) -> Pointer[T] { - let ptr = realloc(buffer as Pointer[UInt8], size) - - # In this case there's nothing we can do but abort. - if ptr as Int == 0 and size != 0 { - panic('std.libc.resize() failed: ${Error.last_os_error}') - } - - ptr as Pointer[T] -} - -fn copy[T](from: Pointer[T], to: Pointer[T], size: Int) { - memmove(to as Pointer[UInt8], from as Pointer[UInt8], size) -} +fn extern exit(status: Int32) -> Never # Returns the type of a directory entry. fn dirent_type(pointer: Pointer[sys.Dirent]) -> Int { @@ -163,3 +212,7 @@ fn dirent_type(pointer: Pointer[sys.Dirent]) -> Int { fn dirent_name(pointer: Pointer[sys.Dirent]) -> Pointer[UInt8] { sys.dirent_name(pointer) } + +fn pipes -> Result[(Int32, Int32), Error] { + sys.pipes +} diff --git a/std/src/std/libc/freebsd.inko b/std/src/std/libc/freebsd.inko index a9db12c4b..ddeab4169 100644 --- a/std/src/std/libc/freebsd.inko +++ b/std/src/std/libc/freebsd.inko @@ -1,3 +1,5 @@ +import std.io (Error) + let DT_DIR = 4 let DT_LNK = 10 let DT_REG = 8 @@ -50,6 +52,8 @@ let O_RDONLY = 0 let O_RDWR = 0x2 let O_TRUNC = 0x400 let O_WRONLY = 0x1 +let POSIX_SPAWN_SETSIGDEF = 0x10 +let POSIX_SPAWN_SETSIGMASK = 0x20 let SEEK_END = 2 let SEEK_SET = 0 let SOL_SOCKET = 0xFFFF @@ -65,6 +69,7 @@ let S_IFLNK = 0xA000 let S_IFMT = 0xF000 let S_IFREG = 0x8000 let TCP_NODELAY = 1 +let WNOHANG = 0x00000001 # FreeBSD doesn't define this constant, but we still define it here to make it # easier to handle platform differences. @@ -115,6 +120,26 @@ class extern StatBuf { let @st_spare9: Int64 } +class extern Pipes { + let @reader: Int32 + let @writer: Int32 +} + +class extern SigSet { + let @__val0: UInt32 + let @__val1: UInt32 + let @__val2: UInt32 + let @__val3: UInt32 +} + +class extern PosixSpawnAttrs { + let @inner: Pointer[UInt8] +} + +class extern PosixSpawnFileActions { + let @inner: Pointer[UInt8] +} + fn extern fchmod(fd: Int32, mode: UInt16) -> Int32 fn extern fstat(fd: Int32, buf: Pointer[StatBuf]) -> Int32 @@ -136,6 +161,8 @@ fn extern copy_file_range( flags: UInt32, ) -> Int64 +fn extern pipe2(pipes: Pointer[Pipes], flags: Int32) -> Int32 + fn flush(fd: Int32) -> Int32 { fsync(fd) } @@ -147,3 +174,13 @@ fn dirent_type(pointer: Pointer[Dirent]) -> Int { fn dirent_name(pointer: Pointer[Dirent]) -> Pointer[UInt8] { pointer as Int + 24 as Pointer[UInt8] } + +fn pipes -> Result[(Int32, Int32), Error] { + let pipes = Pipes() + + if pipe2(mut pipes, O_CLOEXEC as Int32) as Int != 0 { + throw Error.last_os_error + } + + Result.Ok((pipes.reader, pipes.writer)) +} diff --git a/std/src/std/libc/linux.inko b/std/src/std/libc/linux.inko index eb4a6208a..12bdb2f7c 100644 --- a/std/src/std/libc/linux.inko +++ b/std/src/std/libc/linux.inko @@ -1,8 +1,9 @@ +import std.io (Error) import std.libc.linux.amd64 (self as arch) if amd64 import std.libc.linux.arm64 (self as arch) if arm64 -# Generic libc constants (e.g. `errno` values). - +let AT_EMPTY_PATH = 0x1000 +let AT_FDCWD = -0x64 let DT_DIR = 4 let DT_LNK = 10 let DT_REG = 8 @@ -56,6 +57,8 @@ let O_RDONLY = 0 let O_RDWR = 0x2 let O_TRUNC = 0x200 let O_WRONLY = 0x1 +let POSIX_SPAWN_SETSIGDEF = 0x04 +let POSIX_SPAWN_SETSIGMASK = 0x08 let SEEK_END = 2 let SEEK_SET = 0 let SOL_SOCKET = 1 @@ -66,18 +69,14 @@ let SO_RCVBUF = 8 let SO_REUSEADDR = 2 let SO_REUSEPORT = 15 let SO_SNDBUF = 7 +let STATX_BASIC_STATS = 0x7FF +let STATX_BTIME = 0x800 let S_IFDIR = 0x4000 let S_IFLNK = 0xA000 let S_IFMT = 0xF000 let S_IFREG = 0x8000 let TCP_NODELAY = 1 - -# Constants specific to the statx() call. - -let AT_EMPTY_PATH = 0x1000 -let AT_FDCWD = -0x64 -let STATX_BASIC_STATS = 0x7FF -let STATX_BTIME = 0x800 +let WNOHANG = 0x00000001 class extern Dirent { let @d_ino: UInt64 @@ -130,6 +129,77 @@ class extern StatxTimestamp { let @__pad0: Int32 } +class extern SigSet { + let @__val0: UInt64 + let @__val1: UInt64 + let @__val2: UInt64 + let @__val3: UInt64 + let @__val4: UInt64 + let @__val5: UInt64 + let @__val6: UInt64 + let @__val7: UInt64 + let @__val8: UInt64 + let @__val9: UInt64 + let @__val10: UInt64 + let @__val11: UInt64 + let @__val12: UInt64 + let @__val13: UInt64 + let @__val14: UInt64 + let @__val15: UInt64 +} + +class extern PosixSpawnAttrs { + let @__flags: Int16 + let @__pgrp: Int32 + let @__sd: SigSet + let @__ss: SigSet + let @__sp: Int32 + let @__policy: Int32 + let @__cgroup: Int32 + let @__pad0: Int32 + let @__pad1: Int32 + let @__pad2: Int32 + let @__pad3: Int32 + let @__pad4: Int32 + let @__pad5: Int32 + let @__pad6: Int32 + let @__pad7: Int32 + let @__pad8: Int32 + let @__pad9: Int32 + let @__pad10: Int32 + let @__pad11: Int32 + let @__pad12: Int32 + let @__pad13: Int32 + let @__pad14: Int32 +} + +class extern PosixSpawnFileActions { + let @__allocated: Int32 + let @__used: Int32 + let @__actions: Pointer[UInt8] + let @__pad0: Int32 + let @__pad1: Int32 + let @__pad2: Int32 + let @__pad3: Int32 + let @__pad4: Int32 + let @__pad5: Int32 + let @__pad6: Int32 + let @__pad7: Int32 + let @__pad8: Int32 + let @__pad9: Int32 + let @__pad10: Int32 + let @__pad11: Int32 + let @__pad12: Int32 + let @__pad13: Int32 + let @__pad14: Int32 + let @__pad15: Int32 +} + +class extern Pipes { + let @reader: Int32 + let @writer: Int32 +} + fn extern fchmod(fd: Int32, mode: UInt16) -> Int32 fn extern syscall(number: Int32, ...) -> Int32 @@ -156,6 +226,8 @@ fn extern copy_file_range( flags: UInt32, ) -> Int64 +fn extern pipe2(pipes: Pointer[Pipes], flags: Int32) -> Int32 + fn flush(fd: Int32) -> Int32 { fsync(fd) } @@ -190,3 +262,13 @@ fn statx( ) as Int } + +fn pipes -> Result[(Int32, Int32), Error] { + let pipes = Pipes() + + if pipe2(mut pipes, O_CLOEXEC as Int32) as Int != 0 { + throw Error.last_os_error + } + + Result.Ok((pipes.reader, pipes.writer)) +} diff --git a/std/src/std/libc/mac.inko b/std/src/std/libc/mac.inko index 66a7185fe..5a18bdda2 100644 --- a/std/src/std/libc/mac.inko +++ b/std/src/std/libc/mac.inko @@ -1,3 +1,4 @@ +import std.io (Error) import std.libc.mac.amd64 (self as sys) if amd64 import std.libc.mac.arm64 (self as sys) if arm64 @@ -43,8 +44,10 @@ let ESPIPE = 29 let ETIME = 101 let ETIMEDOUT = 60 let EXDEV = 18 +let FD_CLOEXEC = 1 let F_BARRIERFSYNC = 85 let F_FULLFSYNC = 51 +let F_SETFD = 2 let IPPROTO_IP = 0 let IPPROTO_IPV6 = 41 let IPPROTO_TCP = 6 @@ -57,6 +60,8 @@ let O_RDONLY = 0 let O_RDWR = 0x2 let O_TRUNC = 0x400 let O_WRONLY = 0x1 +let POSIX_SPAWN_SETSIGDEF = 0x04 +let POSIX_SPAWN_SETSIGMASK = 0x08 let SEEK_END = 2 let SEEK_SET = 0 let SOL_SOCKET = 0xFFFF @@ -72,6 +77,7 @@ let S_IFLNK = 0xA000 let S_IFMT = 0xF000 let S_IFREG = 0x8000 let TCP_NODELAY = 1 +let WNOHANG = 0x00000001 # For macOS we need to use `SO_LINGER_SEC` to control the time in seconds # instead of ticks, and `SO_LINGER` itself isn't useful. @@ -111,6 +117,23 @@ class extern StatBuf { let @st_qspare1: Int64 } +class extern Pipes { + let @reader: Int32 + let @writer: Int32 +} + +class extern SigSet { + let @inner: UInt32 +} + +class extern PosixSpawnAttrs { + let @inner: Pointer[UInt8] +} + +class extern PosixSpawnFileActions { + let @inner: Pointer[UInt8] +} + fn extern chmod(path: Pointer[UInt8], mode: UInt16) -> Int32 fn extern fsync(fd: Int32) -> Int32 @@ -131,6 +154,8 @@ fn extern fcopyfile( flags: UInt32, ) -> Int32 +fn extern pipe(pipes: Pointer[Pipes]) -> Int32 + fn fstat(fd: Int32, buf: Pointer[StatBuf]) -> Int32 { sys.fstat(fd, buf) } @@ -181,3 +206,20 @@ fn dirent_type(pointer: Pointer[Dirent]) -> Int { fn dirent_name(pointer: Pointer[Dirent]) -> Pointer[UInt8] { pointer as Int + 21 as Pointer[UInt8] } + +fn pipes -> Result[(Int32, Int32), Error] { + let pipes = Pipes() + + if pipe(mut pipes) as Int != 0 { throw Error.last_os_error } + + # macOS has no pipe2() function, so we have to manually set the CLOEXEC flag. + if fcntl(pipes.reader, F_SETFD as Int32, FD_CLOEXEC as Int32) as Int != 0 { + throw Error.last_os_error + } + + if fcntl(pipes.writer, F_SETFD as Int32, FD_CLOEXEC as Int32) as Int != 0 { + throw Error.last_os_error + } + + Result.Ok((pipes.reader, pipes.writer)) +} diff --git a/std/src/std/ptr.inko b/std/src/std/ptr.inko index f958117b4..33e4c2bce 100644 --- a/std/src/std/ptr.inko +++ b/std/src/std/ptr.inko @@ -102,3 +102,8 @@ fn equal(left: Pointer[UInt8], right: Pointer[UInt8], size: Int) -> Bool { true } + +# Takes a pointer and increments it offset by `amount` _values_. +fn add[T](pointer: Pointer[T], amount: Int) -> Pointer[T] { + pointer as Int + (amount * _INKO.size_of_type_parameter(T)) as Pointer[T] +} diff --git a/std/src/std/sys.inko b/std/src/std/sys.inko index 2ec8fce0e..838bf5e19 100644 --- a/std/src/std/sys.inko +++ b/std/src/std/sys.inko @@ -1,75 +1,11 @@ # Types and methods for interacting with the underlying system. -import std.drop (Drop) +import std.drop (Drop, drop) +import std.env import std.fs.path (Path) import std.int (ToInt) -import std.io (Error, Read, Write, WriteInternal) +import std.io (Error, Read, Write, WriteInternal, start_blocking, stop_blocking) import std.string (ToString) - -class extern AnyResult { - let @tag: Int - let @value: UInt64 -} - -class extern IntResult { - let @tag: Int - let @value: Int -} - -fn extern inko_child_process_spawn( - process: Pointer[UInt8], - program: String, - args: Pointer[String], - args_size: Int, - env: Pointer[String], - env_size: Int, - stdin: Int, - stdout: Int, - stderr: Int, - directory: String, -) -> AnyResult - -fn extern inko_child_process_drop(child: Pointer[UInt8]) - -fn extern inko_child_process_stdout_close(child: Pointer[UInt8]) - -fn extern inko_child_process_stderr_close(child: Pointer[UInt8]) - -fn extern inko_child_process_stdin_close(child: Pointer[UInt8]) - -fn extern inko_child_process_stderr_read( - process: Pointer[UInt8], - child: Pointer[UInt8], - buffer: mut ByteArray, - size: Int, -) -> IntResult - -fn extern inko_child_process_stdout_read( - process: Pointer[UInt8], - child: Pointer[UInt8], - buffer: mut ByteArray, - size: Int, -) -> IntResult - -fn extern inko_child_process_stdin_flush( - process: Pointer[UInt8], - child: Pointer[UInt8], -) -> IntResult - -fn extern inko_child_process_stdin_write( - process: Pointer[UInt8], - child: Pointer[UInt8], - data: Pointer[UInt8], - size: Int, -) -> IntResult - -fn extern inko_child_process_try_wait(child: Pointer[UInt8]) -> IntResult - -fn extern inko_child_process_wait( - process: Pointer[UInt8], - child: Pointer[UInt8], -) -> IntResult - -fn extern inko_exit(status: Int) -> Never +import std.sys.unix.sys if unix # Returns the number of available CPU cores of the current system. # @@ -99,7 +35,7 @@ fn pub cpu_cores -> Int { # sys.exit(1) # ``` fn pub exit(status: Int) -> Never { - inko_exit(status) + sys.exit(status) } # A type that describes what to do with an input/output stream of a command. @@ -142,7 +78,10 @@ impl ToInt for Stream { # ```inko # import std.sys (Command, Stream) # -# Command.new('ls').stdout(Stream.Piped).spawn.get +# let cmd = Command.new('ls') +# +# cmd.stdout = Stream.Piped +# cmd.spawn.get # ``` # # We can also ignore a stream: @@ -150,7 +89,10 @@ impl ToInt for Stream { # ```inko # import std.sys (Command, Stream) # -# Command.new('ls').stderr(Stream.Null).spawn.get +# let cmd = Command.new('ls') +# +# cmd.stderr = Stream.Null +# cmd.spawn.get # ``` # # # Waiting for the child process @@ -162,50 +104,57 @@ impl ToInt for Stream { # ```inko # import std.sys (Command) # -# let child = Command.new('ls').get -# -# child.wait.get +# let child = Command.new('ls').spawn +# let status = child.wait.get # ``` # # There's also `ChildProcess.try_wait`, which returns immediately if the process -# is still running; instead of waiting for it to finish. +# is still running, instead of waiting for it to finish. # # The input and output streams are accessed using `ChildProcess.stdin`, # `ChildProcess.stdout`, and `ChildProcess.stderr`. For example, to read from # STDOUT: # # ```inko -# import std.sys (Command) +# import std.sys (Command, Stream) +# +# let cmd = Command.new('ls') # -# let child = Command.new('ls').get +# cmd.stdout = Stream.Piped +# +# let child = cmd.spawn.get +# let status = child.wait.get # let bytes = ByteArray.new # -# child.wait.get -# child.stdout.read_all(bytes).get +# match child.stdout { +# case Some(v) -> v.read_all(bytes).get +# case _ -> {} +# } # ``` class pub Command { # The path to the program to spawn. - let @program: String + let pub @program: String # What to do with the STDIN stream. - let @stdin: Stream + let pub @stdin: Stream # What to do with the STDOUT stream. - let @stdout: Stream + let pub @stdout: Stream # What to do with the STDERR stream. - let @stderr: Stream + let pub @stderr: Stream # The arguments to pass to the command. - let @arguments: Array[String] + let pub @arguments: Array[String] # The environment variables to pass to the command. # - # The order in which variables are passed isn't guaranteed. - let @variables: Map[String, String] + # This `Map` defaults to all the environment variables available at the time + # the program started. + let pub @variables: Map[String, String] # The working directory to use for the command. - let @directory: Option[String] + let pub @directory: Option[Path] # Creates a new `Command` that will run the given program. # @@ -240,153 +189,54 @@ class pub Command { stdout: Stream.Inherit, stderr: Stream.Inherit, arguments: [], - variables: Map.new, + variables: env.variables, directory: Option.None, ) } - # Returns the program to start. - fn pub program -> String { - @program + # Returns the working directory to use for the child process, if any. + fn pub mut directory -> Option[Path] { + @directory.clone } - # Sets the working directory of the command. + # Sets the working directory to use for the child process. # # # Examples # # ```inko # import std.sys (Command) # - # Command.new('ls').directory('/tmp') - # ``` - fn pub mut directory[T: ToString](path: ref T) { - @directory = Option.Some(path.to_string) - } - - # Returns the current working directory, if any was set. - fn pub current_directory -> ref Option[String] { - @directory - } - - # Adds a single argument to the command. - # - # # Examples - # - # ```inko - # import std.sys (Command) + # let cmd = Command.new('ls') # - # Command.new('ls').argument('/tmp') + # cmd.directory = '/'.to_path # ``` - fn pub mut argument(value: String) { - @arguments.push(value) + fn pub mut directory=(path: Path) { + @directory = Option.Some(path) } - # Adds multiple arguments to the command. - # - # # Examples - # - # ```inko - # import std.sys (Command) - # - # Command.new('ls').arguments(['/tmp', '/usr']) - # ``` - fn pub mut arguments(values: Array[String]) { - @arguments.append(values) - } - - # Returns the arguments added so far. - fn pub current_arguments -> ref Array[String] { - @arguments - } - - # Adds or updates an environment variable to the command. - # - # # Examples - # - # ```inko - # import std.sys (Command) - # - # Command.new('env').variable(name: 'FOO', value: 'bar') - # ``` - fn pub mut variable(name: String, value: String) { - @variables.set(name, value) - } - - # Adds or updates multiple environment variables to the command. + # Spawns a child process that runs the command. # # # Examples # # ```inko # import std.sys (Command) # - # let vars = Map.new - # - # vars['FOO'] = 'bar' - # - # Command.new('env').variables(vars) - # ``` - fn pub mut variables(values: Map[String, String]) { - @variables.merge(values) - } - - # Returns the variables added so far. - fn pub current_variables -> ref Map[String, String] { - @variables - } - - # Configures the STDIN stream. - fn pub mut stdin(stream: Stream) { - @stdin = stream - } - - # Configures the STDOUT stream. - fn pub mut stdout(stream: Stream) { - @stdout = stream - } - - # Configures the STDERR stream. - fn pub mut stderr(stream: Stream) { - @stderr = stream - } - - # Spawns a child process that runs the command. - # - # # Examples - # - # ```inko # let child = Command.new('ls').spawn.get # # child.wait.get # ``` fn pub spawn -> Result[ChildProcess, Error] { - let vars = [] - - @variables.iter.each(fn (entry) { - vars.push(entry.key) - vars.push(entry.value) - }) - - match - inko_child_process_spawn( - _INKO.process, - @program.to_string, - @arguments.to_pointer, - @arguments.size, - vars.to_pointer, - vars.size, - @stdin.to_int, - @stdout.to_int, - @stderr.to_int, - @directory.as_ref.or(ref ''), + sys + .spawn( + @program, + @arguments, + @variables, + @directory.as_ref.map(fn (v) { v.to_string }), + @stdin, + @stdout, + @stderr, ) - { - case { @tag = 0, @value = v } -> { - Result.Ok(ChildProcess(v as Pointer[UInt8])) - } - case { @tag = _, @value = e } -> { - Result.Error(Error.from_os_error(e as Int)) - } - } + .map(fn (v) { ChildProcess.new(v) }) } } @@ -420,30 +270,20 @@ impl ToInt for ExitStatus { } } -# The standard input stream. +# The standard input stream of a child process. class pub Stdin { - # The child process the stream is connected to. - let @process: ref ChildProcess - - fn pub static new(process: ref ChildProcess) -> Stdin { - Stdin(process) - } + let @fd: Int32 } impl Drop for Stdin { fn mut drop { - inko_child_process_stdin_close(@process.raw) + sys.close(@fd) } } impl WriteInternal for Stdin { fn mut write_internal(data: Pointer[UInt8], size: Int) -> Result[Int, Error] { - match - inko_child_process_stdin_write(_INKO.process, @process.raw, data, size) - { - case { @tag = 0, @value = n } -> Result.Ok(n) - case { @value = e } -> Result.Error(Error.from_os_error(e)) - } + sys.write(@fd, data, size) } } @@ -456,114 +296,108 @@ impl Write for Stdin { write_all_internal(string.to_pointer, string.size) } - fn pub mut flush -> Result[Nil, Error] { - match inko_child_process_stdin_flush(_INKO.process, @process.raw) { - case { @tag = 1, @value = _ } -> Result.Ok(nil) - case { @tag = _, @value = e } -> Result.Error(Error.from_os_error(e)) - } + fn pub mut flush -> Result[Nil, Never] { + Result.Ok(nil) } } -# The standard output stream. +# The standard output stream of a child process. class pub Stdout { - # The child process the stream is connected to. - let @process: ref ChildProcess - - fn pub static new(process: ref ChildProcess) -> Stdout { - Stdout(process) - } + let @fd: Int32 } impl Drop for Stdout { fn mut drop { - inko_child_process_stdout_close(@process.raw) + sys.close(@fd) } } impl Read for Stdout { fn pub mut read(into: mut ByteArray, size: Int) -> Result[Int, Error] { - match - inko_child_process_stdout_read(_INKO.process, @process.raw, into, size) - { - case { @tag = 0, @value = v } -> Result.Ok(v) - case { @tag = _, @value = e } -> Result.Error(Error.from_os_error(e)) - } + sys.read(@fd, into, size) } } -# The standard error output stream. +# The standard error stream of a child process. class pub Stderr { - # The child process the stream is connected to. - let @process: ref ChildProcess - - fn pub static new(process: ref ChildProcess) -> Stderr { - Stderr(process) - } + let @fd: Int32 } impl Drop for Stderr { fn mut drop { - inko_child_process_stderr_close(@process.raw) + sys.close(@fd) } } impl Read for Stderr { fn pub mut read(into: mut ByteArray, size: Int) -> Result[Int, Error] { - match - inko_child_process_stderr_read(_INKO.process, @process.raw, into, size) - { - case { @tag = 0, @value = v } -> Result.Ok(v) - case { @tag = _, @value = e } -> Result.Error(Error.from_os_error(e)) - } + sys.read(@fd, into, size) } } # A running or exited child OS process. class pub ChildProcess { - # A raw pointer to the OS process. - let @raw: Pointer[UInt8] + # The ID of the child process. + let @id: Int32 - # Returns a handle to the standard output stream. - fn pub stdout -> Stdout { - Stdout.new(self) - } + # A handle to the captured input stream of the child process. + let pub @stdin: Option[Stdin] - # Returns a handle to the standard error stream. - fn pub stderr -> Stderr { - Stderr.new(self) - } + # A handle to the captured output stream of the child process. + let pub @stdout: Option[Stdout] - # Returns a handle to the standard input stream. - fn pub stdin -> Stdin { - Stdin.new(self) + # A handle to the captured error stream of the child process. + let pub @stderr: Option[Stderr] + + fn static new(inner: sys.ChildProcess) -> ChildProcess { + let stdin = match inner.stdin { + case Some(v) -> Option.Some(Stdin(v)) + case _ -> Option.None + } + let stdout = match inner.stdout { + case Some(v) -> Option.Some(Stdout(v)) + case _ -> Option.None + } + let stderr = match inner.stderr { + case Some(v) -> Option.Some(Stderr(v)) + case _ -> Option.None + } + + ChildProcess(id: inner.id, stdin: stdin, stdout: stdout, stderr: stderr) } - # Waits for the process to terminate. + # Waits for the child process to finish running, and returns an `ExitStatus` + # containing the exit status. # - # The STDIN stream is closed before waiting. - fn pub wait -> Result[ExitStatus, Error] { - match inko_child_process_wait(_INKO.process, @raw) { - case { @tag = 0, @value = v } -> Result.Ok(ExitStatus.new(v)) - case { @tag = _, @value = e } -> Result.Error(Error.from_os_error(e)) - } + # The child's STDIN stream (if any) is closed before waiting, avoiding + # deadlocks caused by child processes waiting for input from the parent while + # the parent waits for the child to exit. + # + # Note that if you try to read from STDOUT or STDERR before calling + # `ChildProcess.wait` _without_ closing STDIN first, the parent process may + # still deadlock as the read might not return and thus prevent + # `ChildProcess.wait` from first closing STDIN. + # + # To prevent this from happening, always make sure STDIN is closed _before_ + # reading from STDOUT or STDERR _if_ the read happens _before_ a call to + # `ChildProcess.wait`. + fn pub mut wait -> Result[ExitStatus, Error] { + drop(@stdin := Option.None) + sys.wait(@id).map(fn (v) { ExitStatus(v) }) } - # Returns the exit status without blocking. + # Returns the exit status of the child process without blocking the calling + # process. # - # If the process is still running, a None is returned. + # If the process is still running, an `Option.None` is returned. If the + # process exited, an `Option.Some(ExitStatus)` is returned. # - # This method doesn't close the STDIN stream before waiting. + # This method doesn't close STDIN before waiting. fn pub try_wait -> Result[Option[ExitStatus], Error] { - match inko_child_process_try_wait(@raw) { - case { @tag = 0, @value = -1 } -> Result.Ok(Option.None) - case { @tag = 0, @value = v } -> Result.Ok(Option.Some(ExitStatus.new(v))) - case { @tag = _, @value = e } -> Result.Error(Error.from_os_error(e)) + match sys.try_wait(@id) { + case Ok(None) -> Result.Ok(Option.None) + case Ok(Some(n)) -> Result.Ok(Option.Some(ExitStatus(n))) + case Error(e) -> Result.Error(e) } } } - -impl Drop for ChildProcess { - fn mut drop { - inko_child_process_drop(@raw) - } -} diff --git a/std/src/std/sys/unix/sys.inko b/std/src/std/sys/unix/sys.inko new file mode 100644 index 000000000..e7406b9d0 --- /dev/null +++ b/std/src/std/sys/unix/sys.inko @@ -0,0 +1,265 @@ +import std.alloc +import std.drop (Drop) +import std.io (Error, start_blocking, stop_blocking) +import std.libc +import std.ptr +import std.sys (Stream) +import std.sys.unix.fs +import std.sys.unix.stdio + +class FileActions { + let @raw: libc.PosixSpawnFileActions + let @close: Array[Int32] + + fn static new -> Result[FileActions, Error] { + let actions = FileActions(raw: libc.PosixSpawnFileActions(), close: []) + + if libc.posix_spawn_file_actions_init(actions.raw) as Int != 0 { + throw Error.last_os_error + } + + Result.Ok(actions) + } + + fn mut directory=(path: String) { + libc.posix_spawn_file_actions_addchdir_np(@raw, path.to_pointer) + } + + fn mut redirect( + stream: ref Stream, + fd: Int32, + write: Bool, + ) -> Result[Option[Int32], Error] { + let res = match stream { + case Null -> { + let null = try fs.open_file( + '/dev/null', + read: write.false?, + write: write, + append: false, + truncate: false, + ) + + dup(null, fd) + Option.None + } + case Piped -> { + match try libc.pipes { + case (parent, child) if write -> { + dup(child, fd) + Option.Some(parent) + } + case (child, parent) -> { + dup(child, fd) + Option.Some(parent) + } + } + } + case Inherit -> Option.None + } + + Result.Ok(res) + } + + fn mut dup(source: Int32, target: Int32) { + libc.posix_spawn_file_actions_adddup2(@raw, source, target) + + # The child process gets a _copy_ of the file descriptor. This means we have + # to make sure to close them in the parent, otherwise reads/writes could + # block forever. + @close.push(source) + } +} + +impl Drop for FileActions { + fn mut drop { + libc.posix_spawn_file_actions_destroy(@raw) + + # Due to https://github.com/inko-lang/inko/issues/757 we can't use a closure + # here. + loop { + match @close.pop { + case Some(v) -> close(v) + case _ -> break + } + } + } +} + +class StringPointers { + let @raw: Pointer[UInt64] + + fn static new(size: Int) -> StringPointers { + let raw = alloc.resize(0 as Pointer[UInt64], size + 1) + + # The argv/envp arrays passed to posix_spawnp() must be NULL terminated. + ptr.add(raw, size).0 = 0 as UInt64 + StringPointers(raw) + } + + fn mut set(index: Int, value: String) { + ptr.add(@raw, index).0 = value.to_pointer as UInt64 + } +} + +impl Drop for StringPointers { + fn mut drop { + alloc.free(@raw) + } +} + +fn add_null(pointer: Pointer[UInt64], size: Int) { + let target = if size == 0 { + ptr.add(pointer, 1) + } else { + ptr.add(pointer, size - 1) + } + + target.0 = 0 as UInt64 +} + +# The WEXITSTATUS() macro as described in `wait(2)`. +fn exit_status(value: Int32) -> Int { + value as Int >> 8 & 0xFF +} + +# The WIFEXITED() macro as described in `wait(2)`. +fn exited?(value: Int32) -> Bool { + value as Int & 0x7F == 0 +} + +fn spawn( + program: String, + args: ref Array[String], + env: ref Map[String, String], + directory: Option[String], + stdin: ref Stream, + stdout: ref Stream, + stderr: ref Stream, +) -> Result[ChildProcess, Error] { + let argv = StringPointers.new(args.size + 1) + let envp = StringPointers.new(env.size) + + # The list of arguments starts with the program that's being executed, and is + # terminated by a NULL pointer. + argv.set(0, program) + args.iter.each_with_index(fn (idx, arg) { argv.set(idx + 1, arg) }) + + # Environment variables are exposed as a list of `KEY=VALUE` values, + # terminated by a NULL pointer. + # + # We MUST keep these pairs around until AFTER the program is started. + let pairs = [] + + env.iter.each_with_index(fn (idx, kv) { + let pair = '${kv.key}=${kv.value}' + + pairs.push(pair) + envp.set(idx, pair) + }) + + let actions = try FileActions.new + let attrs = libc.PosixSpawnAttrs() + + if libc.posix_spawnattr_init(mut attrs) as Int != 0 { + throw Error.last_os_error + } + + let signals = libc.SigSet() + + # Unmask all signals for the child process. This is needed because Inko + # threads mask all signals. + libc.sigemptyset(mut signals) + libc.posix_spawnattr_setsigmask(mut attrs, mut signals) + + # Reset the default behaviour for all the signals. + libc.sigfillset(mut signals) + libc.posix_spawnattr_setsigdefault(mut attrs, mut signals) + + libc.posix_spawnattr_setflags(mut attrs, libc.POSIX_SPAWN_SETSIGDEF as Int16) + libc.posix_spawnattr_setflags(mut attrs, libc.POSIX_SPAWN_SETSIGMASK as Int16) + + match directory { + case Some(v) -> actions.directory = v + case _ -> {} + } + + let in = try actions.redirect(stdin, stdio.stdin, write: false) + let out = try actions.redirect(stdout, stdio.stdout, write: true) + let err = try actions.redirect(stderr, stdio.stderr, write: true) + + start_blocking + + let pid = 0 as Int32 + let res = libc.posix_spawnp( + pid: mut pid, + file: program.to_pointer, + file_actions: actions.raw, + attrp: mut attrs, + argv: argv.raw, + envp: envp.raw, + ) + as Int + + stop_blocking + + if res != 0 { throw Error.from_os_error(res) } + + if libc.posix_spawnattr_destroy(mut attrs) as Int != 0 { + throw Error.last_os_error + } + + Result.Ok(ChildProcess(id: pid, stdin: in, stdout: out, stderr: err)) +} + +fn wait(pid: Int32) -> Result[Int, Error] { + let status = 0 as Int32 + + start_blocking + + let res = libc.waitpid(pid, mut status, 0 as Int32) as Int + let err = stop_blocking + + if res == -1 { throw Error.from_os_error(err) } + + stop_blocking + Result.Ok(exit_status(status)) +} + +fn try_wait(pid: Int32) -> Result[Option[Int], Error] { + let status = 0 as Int32 + let res = libc.waitpid(pid, mut status, libc.WNOHANG as Int32) as Int + + if res == -1 { throw Error.last_os_error } + + Result.Ok( + if exited?(status) { + Option.Some(exit_status(status)) + } else { + Option.None + }, + ) +} + +fn read(fd: Int32, into: mut ByteArray, size: Int) -> Result[Int, Error] { + fs.read_file(fd, into, size) +} + +fn write(fd: Int32, data: Pointer[UInt8], size: Int) -> Result[Int, Error] { + fs.write_file(fd, data, size) +} + +fn close(fd: Int32) { + fs.close_file(fd) +} + +fn exit(status: Int) -> Never { + libc.exit(status as Int32) +} + +class ChildProcess { + let @id: Int32 + let @stdin: Option[Int32] + let @stdout: Option[Int32] + let @stderr: Option[Int32] +} diff --git a/std/src/std/test.inko b/std/src/std/test.inko index 48c54d9f4..9d0660b37 100644 --- a/std/src/std/test.inko +++ b/std/src/std/test.inko @@ -390,21 +390,21 @@ class pub Process { fn static new(id: Int) -> Process { let cmd = Command.new(env.executable.get) - cmd.stdin(Stream.Piped) - cmd.stdout(Stream.Piped) - cmd.stderr(Stream.Piped) - cmd.variable(CHILD_VAR, id.to_string) + cmd.stdin = Stream.Piped + cmd.stdout = Stream.Piped + cmd.stderr = Stream.Piped + cmd.variables.set(CHILD_VAR, id.to_string) Process(cmd: cmd, stdin: '') } # Adds an argument to the process. fn pub mut argument(value: String) { - @cmd.argument(value) + @cmd.arguments.push(value) } # Adds or updates an environment variable to the process. fn pub mut variable(name: String, value: String) { - @cmd.variable(name, value) + @cmd.variables.set(name, value) } # Sets the data to write to STDIN. @@ -420,11 +420,11 @@ class pub Process { case Error(err) -> panic('Failed to spawn the child process: ${err}') } - let _ = child.stdin.write_string(@stdin) + let _ = (child.stdin := Option.None).get.write_string(@stdin) let stdout = ByteArray.new let stderr = ByteArray.new - let _ = child.stdout.read_all(stdout) - let _ = child.stderr.read_all(stderr) + let _ = child.stdout.as_mut.get.read_all(stdout) + let _ = child.stderr.as_mut.get.read_all(stderr) let status = match child.wait { case Ok(val) -> val case Error(err) -> panic('Failed to wait for the child process: ${err}') diff --git a/std/test/compiler/test_compile_time_variables.inko b/std/test/compiler/test_compile_time_variables.inko index 6eb89e136..1fbb22a5f 100644 --- a/std/test/compiler/test_compile_time_variables.inko +++ b/std/test/compiler/test_compile_time_variables.inko @@ -13,16 +13,16 @@ fn compile( ) -> Result[Nil, String] { let cmd = Command.new(compiler_path) - cmd.arguments(['build', input.to_string, '-o', output.to_string]) - cmd.directory(directory) - cmd.stdin(Stream.Null) - cmd.stderr(Stream.Piped) - cmd.stdout(Stream.Piped) + cmd.arguments = ['build', input.to_string, '-o', output.to_string] + cmd.directory = directory.clone + cmd.stdin = Stream.Null + cmd.stderr = Stream.Piped + cmd.stdout = Stream.Piped match define { case Some(v) -> { - cmd.argument('--define') - cmd.argument(v) + cmd.arguments.push('--define') + cmd.arguments.push(v) } case _ -> {} } @@ -42,8 +42,10 @@ fn compile( try child .stdout + .as_mut + .get .read_all(out) - .then(fn (_) { child.stderr.read_all(out) }) + .then(fn (_) { child.stderr.as_mut.get.read_all(out) }) .map_error(fn (e) { 'failed reading the output: ${e}' }) Result.Error(out.into_string) @@ -63,14 +65,13 @@ fn run(id: Int, define: Option[String]) -> Result[String, String] { let cmd = Command.new(output) - cmd.stdin(Stream.Null) - cmd.stderr(Stream.Piped) - cmd.stdout(Stream.Piped) + cmd.stdin = Stream.Null + cmd.stderr = Stream.Piped + cmd.stdout = Stream.Piped let child = try cmd.spawn.map_error(fn (e) { 'failed to spawn the executable: ${e}' }) - let status = try child.wait.map_error(fn (e) { 'the executable produced an error: ${e}' }) @@ -78,8 +79,10 @@ fn run(id: Int, define: Option[String]) -> Result[String, String] { try child .stdout + .as_mut + .get .read_all(out) - .then(fn (_) { child.stderr.read_all(out) }) + .then(fn (_) { child.stderr.as_mut.get.read_all(out) }) .map_error(fn (e) { 'failed reading the output: ${e}' }) let out = out.into_string diff --git a/std/test/compiler/test_diagnostics.inko b/std/test/compiler/test_diagnostics.inko index 8c90b4884..7ec5001da 100644 --- a/std/test/compiler/test_diagnostics.inko +++ b/std/test/compiler/test_diagnostics.inko @@ -67,11 +67,11 @@ fn check(compiler: String, name: String, file: Path) -> Array[Diagnostic] { let cmd = Command.new(compiler) let dir = file.directory - cmd.stdout(Stream.Null) - cmd.stdin(Stream.Null) - cmd.stderr(Stream.Piped) - cmd.directory(dir.clone) - cmd.arguments(['check', '--format=json', file.to_string]) + cmd.stdout = Stream.Null + cmd.stdin = Stream.Null + cmd.stderr = Stream.Piped + cmd.directory = dir.clone + cmd.arguments = ['check', '--format=json', file.to_string] # Given a test called `foo.inko`, if the directory `foo` exists we add it to # the include path. This way you can move separate files that are imported @@ -79,15 +79,15 @@ fn check(compiler: String, name: String, file: Path) -> Array[Diagnostic] { let extra_src = dir.join(name) if extra_src.directory? { - cmd.argument('--include') - cmd.argument(extra_src.to_string) + cmd.arguments.push('--include') + cmd.arguments.push(extra_src.to_string) } let child = cmd.spawn.or_panic('failed to start the compiler') let output = ByteArray.new child.wait.or_panic('failed to wait for the compiler') - child.stderr.read_all(output) + child.stderr.as_mut.get.read_all(output) match parse_output(dir.to_string, output) { case Ok(v) -> v diff --git a/std/test/compiler/test_fmt.inko b/std/test/compiler/test_fmt.inko index 91665d53a..dcc1a5a39 100644 --- a/std/test/compiler/test_fmt.inko +++ b/std/test/compiler/test_fmt.inko @@ -13,23 +13,27 @@ fn run( ) -> Result[String, String] { let cmd = Command.new(name) - cmd.arguments(arguments) - cmd.stdin(Stream.Piped) - cmd.stderr(Stream.Piped) - cmd.stdout(Stream.Piped) + cmd.arguments = arguments + cmd.stdin = Stream.Piped + cmd.stderr = Stream.Piped + cmd.stdout = Stream.Piped let child = try cmd.spawn.map_error(fn (e) { 'failed to spawn ${name}: ${e}' }) - try child.stdin.write_string(input).then(fn (_) { child.wait }).map_error( - fn (e) { '${name} failed: ${e}' }, - ) + try child.stdin.as_mut.get.write_string(input).map_error(fn (e) { + 'failed to write to STDIN: ${e}' + }) + + try child.wait.map_error(fn (e) { '${name} failed: ${e}' }) let out = ByteArray.new try child .stdout + .as_mut + .get .read_all(out) - .then(fn (_) { child.stderr.read_all(out) }) + .then(fn (_) { child.stderr.as_mut.get.read_all(out) }) .map_error(fn (e) { 'failed reading the output: ${e}' }) Result.Ok(out.into_string) } diff --git a/std/test/std/test_sys.inko b/std/test/std/test_sys.inko index 8041edaa3..b40acbc87 100644 --- a/std/test/std/test_sys.inko +++ b/std/test/std/test_sys.inko @@ -20,65 +20,62 @@ fn pub tests(t: mut Tests) { t.test('Command.program', fn (t) { t.equal(Command.new('ls').program, 'ls') }) - t.test('Command.directory', fn (t) { + t.ok('Command.directory', fn (t) { let cmd = Command.new('ls') - t.equal(cmd.current_directory, Option.None) - cmd.directory('/foo') - t.equal(cmd.current_directory, Option.Some('/foo')) - }) + t.equal(cmd.directory, Option.None) + cmd.directory = 'fixtures'.to_path + t.equal(cmd.directory, Option.Some('fixtures'.to_path)) - t.test('Command.argument', fn (t) { - let cmd = Command.new('ls') + # Make sure the command _actually_ uses the directory. + cmd.stdin = Stream.Null + cmd.stdout = Stream.Piped + cmd.stderr = Stream.Null - t.equal(cmd.current_arguments, []) - cmd.argument('foo') - t.equal(cmd.current_arguments, ['foo']) - }) + let child = try cmd.spawn + let bytes = ByteArray.new - t.test('Command.arguments', fn (t) { - let cmd = Command.new('ls') + child.stdout.as_mut.get.read_all(bytes) - t.equal(cmd.current_arguments, []) - cmd.arguments(['foo']) - t.equal(cmd.current_arguments, ['foo']) + t.true(bytes.into_string.contains?('hello.txt')) + try child.wait + Result.Ok(nil) }) - t.test('Command.variable', fn (t) { + t.test('Command.arguments', fn (t) { let cmd = Command.new('ls') - t.equal(cmd.current_variables, Map.new) - cmd.variable('TEST', 'foo') - t.equal(cmd.current_variables.get('TEST'), 'foo') + t.equal(cmd.arguments, []) + cmd.arguments.push('foo') + t.equal(cmd.arguments, ['foo']) }) t.test('Command.variables', fn (t) { let cmd = Command.new('ls') - let vars = Map.new - vars.set('TEST', 'foo') + t.equal(cmd.variables, env.variables) - t.equal(cmd.current_variables, Map.new) - cmd.variables(vars) - t.equal(cmd.current_variables.get('TEST'), 'foo') + cmd.variables.set('TEST', 'foo') + t.equal(cmd.variables.opt('TEST'), Option.Some('foo')) }) - t.test('Command.spawn with a valid command', fn (t) { + t.ok('Command.spawn with a valid command', fn (t) { let cmd = Command.new(compiler_path) - cmd.stdin(Stream.Null) - cmd.stderr(Stream.Null) - cmd.stdout(Stream.Piped) - cmd.argument('--help') + cmd.stdin = Stream.Null + cmd.stderr = Stream.Null + cmd.stdout = Stream.Piped + cmd.arguments.push('--help') - let child = cmd.spawn.get + let child = try cmd.spawn - child.wait.get + try child.wait let bytes = ByteArray.new - child.stdout.read_all(bytes).get + try child.stdout.as_mut.get.read_all(bytes) t.true(bytes.into_string.contains?('Usage: inko')) + Result.Ok(nil) }) t.test('Command.spawn with an invalid command', fn (t) {