From 8f6c4afd2a82a9b9e53164ab05eefb7b7bd89e26 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 5 Sep 2024 03:18:43 +0200 Subject: [PATCH] WIP: implement std.sys in Inko --- rt/src/process.rs | 13 - rt/src/runtime/sys.rs | 24 +- .../verification/apple.rs | 18 +- std/src/std/alloc.inko | 34 ++ std/src/std/array.inko | 24 +- std/src/std/libc.inko | 83 +++- std/src/std/libc/freebsd.inko | 20 + std/src/std/libc/linux.inko | 100 ++++- std/src/std/libc/mac.inko | 28 ++ std/src/std/ptr.inko | 5 + std/src/std/sys.inko | 402 ++++++------------ std/src/std/sys/unix/sys.inko | 264 ++++++++++++ std/src/std/test.inko | 18 +- .../compiler/test_compile_time_variables.inko | 29 +- std/test/compiler/test_diagnostics.inko | 16 +- std/test/compiler/test_fmt.inko | 20 +- std/test/std/test_sys.inko | 46 +- 17 files changed, 708 insertions(+), 436 deletions(-) create mode 100644 std/src/std/alloc.inko create mode 100644 std/src/std/sys/unix/sys.inko 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/sys.rs b/rt/src/runtime/sys.rs index 2e4255cbb..8b4a6d8d5 100644 --- a/rt/src/runtime/sys.rs +++ b/rt/src/runtime/sys.rs @@ -1,5 +1,4 @@ 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; @@ -16,7 +15,6 @@ fn stdio_for(value: i64) -> Stdio { #[no_mangle] pub(crate) unsafe extern "system" fn inko_child_process_spawn( - process: ProcessPointer, program: *const InkoString, args: *const *const InkoString, args_length: i64, @@ -52,19 +50,15 @@ pub(crate) unsafe extern "system" fn inko_child_process_spawn( cmd.current_dir(directory); } - process - .blocking(|| cmd.spawn()) - .map(InkoResult::ok_boxed) - .unwrap_or_else(InkoResult::io_error) + 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()) + (*child) + .wait() .map(|status| status.code().unwrap_or(0) as i64) .map(|status| InkoResult::ok(status as _)) .unwrap_or_else(InkoResult::io_error) @@ -88,7 +82,6 @@ pub(crate) unsafe extern "system" fn inko_child_process_try_wait( #[no_mangle] pub(crate) unsafe extern "system" fn inko_child_process_stdout_read( - process: ProcessPointer, child: *mut Child, buffer: *mut ByteArray, size: i64, @@ -99,7 +92,7 @@ pub(crate) unsafe extern "system" fn inko_child_process_stdout_read( child .stdout .as_mut() - .map(|stream| process.blocking(|| read_into(stream, buff, size))) + .map(|stream| read_into(stream, buff, size)) .unwrap_or(Ok(0)) .map(|size| InkoResult::ok(size as _)) .unwrap_or_else(InkoResult::io_error) @@ -107,7 +100,6 @@ pub(crate) unsafe extern "system" fn inko_child_process_stdout_read( #[no_mangle] pub(crate) unsafe extern "system" fn inko_child_process_stderr_read( - process: ProcessPointer, child: *mut Child, buffer: *mut ByteArray, size: i64, @@ -118,7 +110,7 @@ pub(crate) unsafe extern "system" fn inko_child_process_stderr_read( child .stderr .as_mut() - .map(|stream| process.blocking(|| read_into(stream, buff, size))) + .map(|stream| read_into(stream, buff, size)) .unwrap_or(Ok(0)) .map(|size| InkoResult::ok(size as _)) .unwrap_or_else(InkoResult::io_error) @@ -126,7 +118,6 @@ pub(crate) unsafe extern "system" fn inko_child_process_stderr_read( #[no_mangle] pub(crate) unsafe extern "system" fn inko_child_process_stdin_write( - process: ProcessPointer, child: *mut Child, data: *mut u8, size: i64, @@ -137,7 +128,7 @@ pub(crate) unsafe extern "system" fn inko_child_process_stdin_write( child .stdin .as_mut() - .map(|stream| process.blocking(|| stream.write(slice))) + .map(|stream| stream.write(slice)) .unwrap_or(Ok(0)) .map(|size| InkoResult::ok(size as _)) .unwrap_or_else(InkoResult::io_error) @@ -145,7 +136,6 @@ pub(crate) unsafe extern "system" fn inko_child_process_stdin_write( #[no_mangle] pub(crate) unsafe extern "system" fn inko_child_process_stdin_flush( - process: ProcessPointer, child: *mut Child, ) -> InkoResult { let child = &mut *child; @@ -153,7 +143,7 @@ pub(crate) unsafe extern "system" fn inko_child_process_stdin_flush( child .stdin .as_mut() - .map(|stream| process.blocking(|| stream.flush())) + .map(|stream| stream.flush()) .unwrap_or(Ok(())) .map(|_| InkoResult::none()) .unwrap_or_else(InkoResult::io_error) 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..081139067 100644 --- a/std/src/std/libc.inko +++ b/std/src/std/libc.inko @@ -76,6 +76,7 @@ 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 fn opendir(path: Pointer[UInt8]) -> Pointer[UInt8] { sys.opendir(path) @@ -123,37 +124,71 @@ 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[sys.PosixSpawnFileActions], + attrp: Pointer[sys.PosixSpawnAttrs], + argv: Pointer[UInt64], + envp: Pointer[UInt64], +) -> Int32 + +fn extern posix_spawn_file_actions_init( + actions: Pointer[sys.PosixSpawnFileActions], +) -> Int32 + +fn extern posix_spawn_file_actions_destroy( + actions: Pointer[sys.PosixSpawnFileActions], +) -> Int32 + +fn extern posix_spawn_file_actions_adddup2( + actions: Pointer[sys.PosixSpawnFileActions], + fd: Int32, + new_fd: Int32, +) -> Int32 + +fn extern posix_spawn_file_actions_addchdir_np( + actions: mut Pointer[sys.PosixSpawnFileActions], + path: Pointer[UInt8], +) -> Int32 + +fn extern posix_spawnattr_init(attr: mut Pointer[sys.PosixSpawnAttrs]) -> Int32 + +fn extern posix_spawnattr_destroy( + attr: mut Pointer[sys.PosixSpawnAttrs], +) -> Int32 + +fn extern posix_spawnattr_setflags( + attr: mut Pointer[sys.PosixSpawnAttrs], + flags: Int16, +) -> Int32 + +fn extern posix_spawnattr_setsigdefault( + attr: mut Pointer[sys.PosixSpawnAttrs], + mask: mut Pointer[sys.SigSet], +) -> Int32 + +fn extern posix_spawnattr_setsigmask( + attr: mut Pointer[sys.PosixSpawnAttrs], + mask: mut Pointer[sys.SigSet], +) -> Int32 + +fn extern sigemptyset(set: mut Pointer[sys.SigSet]) -> Int32 + +fn extern sigfillset(set: mut Pointer[sys.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) -} - # Returns the type of a directory entry. fn dirent_type(pointer: Pointer[sys.Dirent]) -> Int { sys.dirent_type(pointer) @@ -163,3 +198,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..a77159dfd 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 @@ -65,6 +67,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 +118,11 @@ class extern StatBuf { let @st_spare9: Int64 } +class extern Pipes { + let @reader: Int32 + let @writer: Int32 +} + fn extern fchmod(fd: Int32, mode: UInt16) -> Int32 fn extern fstat(fd: Int32, buf: Pointer[StatBuf]) -> Int32 @@ -136,6 +144,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 +157,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..d753f217c 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 @@ -72,6 +75,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 +115,11 @@ class extern StatBuf { let @st_qspare1: Int64 } +class extern Pipes { + let @reader: Int32 + let @writer: Int32 +} + fn extern chmod(path: Pointer[UInt8], mode: UInt16) -> Int32 fn extern fsync(fd: Int32) -> Int32 @@ -131,6 +140,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 +192,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..3e98490e1 100644 --- a/std/src/std/sys.inko +++ b/std/src/std/sys.inko @@ -1,73 +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 +import std.sys.unix.sys if unix fn extern inko_exit(status: Int) -> Never @@ -142,7 +80,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 +91,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 +106,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 +191,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 +272,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 +298,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..22c189be1 --- /dev/null +++ b/std/src/std/sys/unix/sys.inko @@ -0,0 +1,264 @@ +import std.alloc +import std.drop (Drop) +import std.io (Error, start_blocking, stop_blocking) +import std.libc +import std.libc.linux (self as sys) if linux +import std.ptr +import std.sys (Stream) +import std.sys.unix.fs +import std.sys.unix.stdio + +class FileActions { + let @raw: sys.PosixSpawnFileActions + let @close: Array[Int32] + + fn static new -> Result[FileActions, Error] { + let actions = FileActions(raw: sys.PosixSpawnFileActions(), close: []) + + if libc.posix_spawn_file_actions_init(actions.raw) as Int != 0 { + throw Error.last_os_error + } + + Result.Ok(actions) + } + + # TODO: macOS only supports posix_spawn_file_actions_addchdir_np() since + # 10.15, so we need a way to handle that. + 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 = sys.PosixSpawnAttrs() + + if libc.posix_spawnattr_init(mut attrs) as Int != 0 { + throw Error.last_os_error + } + + let signals = sys.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, sys.POSIX_SPAWN_SETSIGDEF as Int16) + libc.posix_spawnattr_setflags(mut attrs, sys.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) +} + +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..734f843f0 100644 --- a/std/test/std/test_sys.inko +++ b/std/test/std/test_sys.inko @@ -23,53 +23,35 @@ fn pub tests(t: mut Tests) { t.test('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.test('Command.argument', fn (t) { - let cmd = Command.new('ls') - - t.equal(cmd.current_arguments, []) - cmd.argument('foo') - t.equal(cmd.current_arguments, ['foo']) + t.equal(cmd.directory, Option.None) + cmd.directory = '/foo'.to_path + t.equal(cmd.directory, Option.Some('/foo'.to_path)) }) t.test('Command.arguments', fn (t) { let cmd = Command.new('ls') - t.equal(cmd.current_arguments, []) - cmd.arguments(['foo']) - t.equal(cmd.current_arguments, ['foo']) - }) - - t.test('Command.variable', 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) { 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 @@ -77,7 +59,7 @@ fn pub tests(t: mut Tests) { let bytes = ByteArray.new - child.stdout.read_all(bytes).get + child.stdout.as_mut.get.read_all(bytes).get t.true(bytes.into_string.contains?('Usage: inko')) })