diff --git a/CHANGELOG.md b/CHANGELOG.md index da897093..8572e550 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,11 @@ All notable changes to this project will be documented in this file. ## Unreleased - ??? +- Add `ev/to-stream` - Make `ffi/write` append to a buffer instead of insert at 0 by default. +- Add `os/getpid` to get the current process id. +- Add `:out` option to `os/spawn` to be able to redirect stderr to stdout with pipes. + Add `interrupt?` argument to `ev/deadline` to use VM interruptions. ## 1.38.0 - 2025-03-18 - Add `bundle/replace` diff --git a/README.md b/README.md index 5a37618d..72c25d40 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,21 @@ make install-jpm-git Find out more about the available make targets by running `make help`. +### Alpine Linux + +To build a statically-linked build of Janet, Alpine Linux + MUSL is a good combination. Janet can also +be built inside a docker container or similar in this manner. + +```sh +docker run -it --rm alpine /bin/ash +$ apk add make gcc musl-dev git +$ git clone https://github.com/janet-lang/janet.git +$ cd janet +$ make -j10 +$ make test +$ make install +``` + ### 32-bit Haiku 32-bit Haiku build instructions are the same as the UNIX-like build instructions, diff --git a/build_win.bat b/build_win.bat index 31cbd403..5d4f23d6 100644 --- a/build_win.bat +++ b/build_win.bat @@ -41,34 +41,34 @@ if not exist build\boot mkdir build\boot @rem Build the bootstrap interpreter for %%f in (src\core\*.c) do ( %JANET_COMPILE% /DJANET_BOOTSTRAP /Fobuild\boot\%%~nf.obj %%f - @if not errorlevel 0 goto :BUILDFAIL + @if errorlevel 1 goto :BUILDFAIL ) for %%f in (src\boot\*.c) do ( %JANET_COMPILE% /DJANET_BOOTSTRAP /Fobuild\boot\%%~nf.obj %%f - @if not errorlevel 0 goto :BUILDFAIL + @if errorlevel 1 goto :BUILDFAIL ) %JANET_LINK% /out:build\janet_boot.exe build\boot\*.obj -@if not errorlevel 0 goto :BUILDFAIL +@if errorlevel 1 goto :BUILDFAIL build\janet_boot . > build\c\janet.c -@if not errorlevel 0 goto :BUILDFAIL +@if errorlevel 1 goto :BUILDFAIL @rem Build the sources %JANET_COMPILE% /Fobuild\janet.obj build\c\janet.c -@if not errorlevel 0 goto :BUILDFAIL +@if errorlevel 1 goto :BUILDFAIL %JANET_COMPILE% /Fobuild\shell.obj src\mainclient\shell.c -@if not errorlevel 0 goto :BUILDFAIL +@if errorlevel 1 goto :BUILDFAIL @rem Build the resources rc /nologo /fobuild\janet_win.res janet_win.rc -@if not errorlevel 0 goto :BUILDFAIL +@if errorlevel 1 goto :BUILDFAIL @rem Link everything to main client %JANET_LINK% /out:janet.exe build\janet.obj build\shell.obj build\janet_win.res -@if not errorlevel 0 goto :BUILDFAIL +@if errorlevel 1 goto :BUILDFAIL @rem Build static library (libjanet.lib) %JANET_LINK_STATIC% /out:build\libjanet.lib build\janet.obj -@if not errorlevel 0 goto :BUILDFAIL +@if errorlevel 1 goto :BUILDFAIL echo === Successfully built janet.exe for Windows === echo === Run 'build_win test' to run tests. == @@ -102,7 +102,7 @@ exit /b 0 :TEST for %%f in (test/suite*.janet) do ( janet.exe test\%%f - @if not errorlevel 0 goto TESTFAIL + @if errorlevel 1 goto TESTFAIL ) exit /b 0 diff --git a/src/boot/boot.janet b/src/boot/boot.janet index c3e292ec..ce0e6012 100644 --- a/src/boot/boot.janet +++ b/src/boot/boot.janet @@ -1084,16 +1084,29 @@ (map-aggregator ,maptype ,res (,f x ;call-buffer))))))) (defn map - `Map a function over every value in a data structure and - return an array of the results.` + ``` + Map a function `f` over every value in a data structure `ind` + and return an array of results, but only if no `inds` are + provided. Multiple data structures can be handled if each + `inds` is a data structure and `f` is a function of arity + one more than the number of `inds`. The resulting array has + a length that is the shortest of `ind` and each of `inds`. + ``` [f ind & inds] (def res @[]) (map-template :map res f ind inds) res) (defn mapcat - ``Map a function over every element in an array or tuple and - use `array/concat` to concatenate the results.`` + ``` + Map a function `f` over every value in a data structure `ind` + and use `array/concat` to concatenate the results, but only if + no `inds` are provided. Multiple data structures can be handled + if each `inds` is a data structure and `f` is a function of + arity one more than the number of `inds`. Note that `f` is only + applied to values at indeces up to the largest index of the + shortest of `ind` and each of `inds`. + ``` [f ind & inds] (def res @[]) (map-template :mapcat res f ind inds) @@ -1110,18 +1123,30 @@ res) (defn count - ``Count the number of items in `ind` for which `(pred item)` - is true.`` + ``` + Count the number of values in a data structure `ind` for which + applying `pred` yields a truthy value, but only if no `inds` are + provided. Multiple data structures can be handled if each `inds` + is a data structure and `pred` is a function of arity one more + than the number of `inds`. Note that `pred` is only applied to + values at indeces up to the largest index of the shortest of + `ind` and each of `inds`. + ``` [pred ind & inds] (var res 0) (map-template :count res pred ind inds) res) (defn keep - ``Given a predicate `pred`, return a new array containing the truthy results - of applying `pred` to each element in the indexed collection `ind`. This is - different from `filter` which returns an array of the original elements where - the predicate is truthy.`` + ``` + Given a predicate `pred`, return a new array containing the + truthy results of applying `pred` to each value in the data + structure `ind`, but only if no `inds` are provided. Multiple + data structures can be handled if each `inds` is a data + structure and `pred` is a function of arity one more than the + number of `inds`. The resulting array has a length that is no + longer than the shortest of `ind` and each of `inds`. + ``` [pred ind & inds] (def res @[]) (map-template :keep res pred ind inds) @@ -2203,17 +2228,32 @@ ret) (defn all - ``Returns true if `(pred item)` is truthy for every item in `ind`. - Otherwise, returns the first falsey result encountered. - Returns true if `ind` is empty.`` + ``` + Returns true if applying `pred` to every value in a data + structure `ind` results in only truthy values, but only if no + `inds` are provided. Multiple data structures can be handled + if each `inds` is a data structure and `pred` is a function + of arity one more than the number of `inds`. Returns the first + falsey result encountered. Note that `pred` is only called as + many times as the length of the shortest of `ind` and each of + `inds`. If `ind` or any of `inds` are empty, returns true. + ``` [pred ind & inds] (var res true) (map-template :all res pred ind inds) res) (defn some - ``Returns nil if `(pred item)` is false or nil for every item in `ind`. - Otherwise, returns the first truthy result encountered.`` + ``` + Returns nil if applying `pred` to every value in a data + structure `ind` results in only falsey values, but only if no + `inds` are provided. Multiple data structures can be handled + if each `inds` is a data structure and `pred` is a function + of arity one more than the number of `inds`. Returns the first + truthy result encountered. Note that `pred` is only called as + many times as the length of the shortest of `ind` and each of + `inds`. If `ind` or any of `inds` are empty, returns nil. + ``` [pred ind & inds] (var res nil) (map-template :some res pred ind inds) @@ -4314,6 +4354,9 @@ (when check (do-hook module bundle-name :check man))) (print "installed " bundle-name) + (when (get man :has-bin-script) + (def binpath (string (dyn *syspath*) s "bin")) + (eprintf "executable scripts have been installed to %s" binpath)) bundle-name) (defn- bundle/pack @@ -4424,7 +4467,7 @@ (defn bundle/add "Add files and directories during a bundle install relative to `(dyn *syspath*)`. - Added paths will be recorded in the bundle manifest such that they are properly tracked + Added files and directories will be recorded in the bundle manifest such that they are properly tracked and removed during an upgrade or uninstall." [manifest src &opt dest chmod-mode] (default dest src) @@ -4447,6 +4490,7 @@ (default dest (last (string/split s src))) (default chmod-mode 8r755) (os/mkdir (string (dyn *syspath*) s "bin")) + (put manifest :has-bin-script true) (bundle/add-file manifest src (string "bin" s dest) chmod-mode)) (defn bundle/update-all diff --git a/src/core/ev.c b/src/core/ev.c index c6e99d6b..a39560a9 100644 --- a/src/core/ev.c +++ b/src/core/ev.c @@ -353,21 +353,22 @@ JanetStream *janet_stream(JanetHandle handle, uint32_t flags, const JanetMethod static void janet_stream_close_impl(JanetStream *stream) { stream->flags |= JANET_STREAM_CLOSED; + int canclose = !(stream->flags & JANET_STREAM_NOT_CLOSEABLE); #ifdef JANET_WINDOWS if (stream->handle != INVALID_HANDLE_VALUE) { #ifdef JANET_NET if (stream->flags & JANET_STREAM_SOCKET) { - closesocket((SOCKET) stream->handle); + if (canclose) closesocket((SOCKET) stream->handle); } else #endif { - CloseHandle(stream->handle); + if (canclose) CloseHandle(stream->handle); } stream->handle = INVALID_HANDLE_VALUE; } #else if (stream->handle != -1) { - close(stream->handle); + if (canclose) close(stream->handle); stream->handle = -1; #ifdef JANET_EV_POLL uint32_t i = stream->index; @@ -652,6 +653,12 @@ static VOID CALLBACK janet_timeout_stop(ULONG_PTR ptr) { UNREFERENCED_PARAMETER(ptr); ExitThread(0); } +#elif JANET_ANDROID +static void janet_timeout_stop(int sig_num) { + if (sig_num == SIGUSR1) { + pthread_exit(0); + } +} #endif static void janet_timeout_cb(JanetEVGenericMessage msg) { @@ -673,6 +680,14 @@ static DWORD WINAPI janet_timeout_body(LPVOID ptr) { } #else static void *janet_timeout_body(void *ptr) { +#ifdef JANET_ANDROID + struct sigaction action; + memset(&action, 0, sizeof(action)); + sigemptyset(&action.sa_mask); + action.sa_flags = 0; + action.sa_handler = &janet_timeout_stop; + sigaction(SIGUSR1, &action, NULL); +#endif JanetThreadedTimeout tto = *(JanetThreadedTimeout *)ptr; janet_free(ptr); struct timespec ts; @@ -1489,8 +1504,12 @@ JanetFiber *janet_loop1(void) { QueueUserAPC(janet_timeout_stop, to.worker, 0); WaitForSingleObject(to.worker, INFINITE); CloseHandle(to.worker); +#else +#ifdef JANET_ANDROID + pthread_kill(to.worker, SIGUSR1); #else pthread_cancel(to.worker); +#endif void *res; pthread_join(to.worker, &res); #endif @@ -3188,6 +3207,9 @@ JANET_CORE_FN(cfun_ev_deadline, to.is_error = 0; to.sched_id = to.fiber->sched_id; if (use_interrupt) { +#ifdef JANET_ANDROID + janet_sandbox_assert(JANET_SANDBOX_SIGNAL); +#endif JanetThreadedTimeout *tto = janet_malloc(sizeof(JanetThreadedTimeout)); if (NULL == tto) { JANET_OUT_OF_MEMORY; @@ -3465,6 +3487,39 @@ JANET_CORE_FN(janet_cfun_ev_all_tasks, return janet_wrap_array(array); } +JANET_CORE_FN(janet_cfun_to_stream, + "(ev/to-stream file)", + "Convert a core/file to a core/stream. On POSIX operating systems, this will mark " + "the underlying open file descriptor as non-blocking.") { + janet_fixarity(argc, 1); + int32_t flags = 0; + int32_t stream_flags = 0; + FILE *file = janet_getfile(argv, 0, &flags); + if (flags & JANET_FILE_READ) stream_flags |= JANET_STREAM_READABLE; + if (flags & JANET_FILE_WRITE) stream_flags |= JANET_STREAM_WRITABLE; + if (flags & JANET_FILE_NOT_CLOSEABLE) stream_flags |= JANET_STREAM_NOT_CLOSEABLE; + if (flags & JANET_FILE_CLOSED) janet_panic("file is closed"); +#ifdef JANET_WINDOWS + HANDLE handle = (HANDLE) _get_osfhandle(_fileno(file)); + HANDLE prochandle = GetCurrentProcess(); + HANDLE dupped_handle = INVALID_HANDLE_VALUE; + if (!DuplicateHandle(prochandle, handle, prochandle, &dupped_handle, 0, FALSE, DUPLICATE_SAME_ACCESS)) { + janet_panic("cannot duplicate handle to file"); + } + JanetStream *stream = janet_stream(dupped_handle, stream_flags, NULL); +#else + int handle = fileno(file); + int dupped_handle = 0; + int status = 0; + RETRY_EINTR(dupped_handle, dup(handle)); + if (status == -1) janet_panic(janet_strerror(errno)); + RETRY_EINTR(status, fcntl(dupped_handle, F_SETFL, O_NONBLOCK)); + if (status == -1) janet_panic(janet_strerror(errno)); + JanetStream *stream = janet_stream(dupped_handle, stream_flags, NULL); +#endif + return janet_wrap_abstract(stream); +} + void janet_lib_ev(JanetTable *env) { JanetRegExt ev_cfuns_ext[] = { JANET_CORE_REG("ev/give", cfun_channel_push), @@ -3496,6 +3551,7 @@ void janet_lib_ev(JanetTable *env) { JANET_CORE_REG("ev/release-rlock", janet_cfun_rwlock_read_release), JANET_CORE_REG("ev/release-wlock", janet_cfun_rwlock_write_release), JANET_CORE_REG("ev/to-file", janet_cfun_to_file), + JANET_CORE_REG("ev/to-stream", janet_cfun_to_stream), JANET_CORE_REG("ev/all-tasks", janet_cfun_ev_all_tasks), JANET_REG_END }; diff --git a/src/core/os.c b/src/core/os.c index fe1184e9..82a99aec 100644 --- a/src/core/os.c +++ b/src/core/os.c @@ -55,6 +55,7 @@ #include #include #include +#define JANET_SPAWN_CHDIR #else #include #include @@ -73,6 +74,20 @@ extern char **environ; #endif #endif +/* Detect availability of posix_spawn_file_actions_addchdir_np. Since + * this doesn't seem to follow any standard, just a common extension, we + * must enumerate supported systems for availability. Define JANET_SPAWN_NO_CHDIR + * to disable this. */ +#ifndef JANET_SPAWN_NO_CHDIR +#ifdef __GLIBC__ +#define JANET_SPAWN_CHDIR +#elif defined(JANET_APPLE) /* Some older versions may not work here. */ +#define JANET_SPAWN_CHDIR +#elif defined(__FreeBSD__) /* Not all BSDs work, for example openBSD doesn't seem to support this */ +#define JANET_SPAWN_CHDIR +#endif +#endif + /* Not POSIX, but all Unixes but Solaris have this function. */ #if defined(JANET_POSIX) && !defined(__sun) time_t timegm(struct tm *tm); @@ -813,6 +828,19 @@ JANET_CORE_FN(os_proc_close, #endif } +JANET_CORE_FN(os_proc_getpid, + "(os/getpid)", + "Get the process ID of the current process.") { + janet_sandbox_assert(JANET_SANDBOX_SUBPROCESS); + janet_fixarity(argc, 0); + (void) argv; +#ifdef JANET_WINDOWS + return janet_wrap_number((double) _getpid()); +#else + return janet_wrap_number((double) getpid()); +#endif +} + static void swap_handles(JanetHandle *handles) { JanetHandle temp = handles[0]; handles[0] = handles[1]; @@ -1137,6 +1165,7 @@ static Janet os_execute_impl(int32_t argc, Janet *argv, JanetExecuteMode mode) { JanetAbstract orig_in = NULL, orig_out = NULL, orig_err = NULL; JanetHandle new_in = JANET_HANDLE_NONE, new_out = JANET_HANDLE_NONE, new_err = JANET_HANDLE_NONE; JanetHandle pipe_in = JANET_HANDLE_NONE, pipe_out = JANET_HANDLE_NONE, pipe_err = JANET_HANDLE_NONE; + int stderr_is_stdout = 0; int pipe_errflag = 0; /* Track errors setting up pipes */ int pipe_owner_flags = (is_spawn && (flags & 0x8)) ? JANET_PROC_ALLOW_ZOMBIE : 0; @@ -1161,11 +1190,28 @@ static Janet os_execute_impl(int32_t argc, Janet *argv, JanetExecuteMode mode) { if (is_spawn && janet_keyeq(maybe_stderr, "pipe")) { new_err = make_pipes(&pipe_err, 0, &pipe_errflag); pipe_owner_flags |= JANET_PROC_OWNS_STDERR; + } else if (is_spawn && janet_keyeq(maybe_stderr, "out")) { + stderr_is_stdout = 1; } else if (!janet_checktype(maybe_stderr, JANET_NIL)) { new_err = janet_getjstream(&maybe_stderr, 0, &orig_err); } } + /* Optional working directory. Available for both os/execute and os/spawn. */ + const char *chdir_path = NULL; + if (argc > 2) { + JanetDictView tab = janet_getdictionary(argv, 2); + Janet workdir = janet_dictionary_get(tab.kvs, tab.cap, janet_ckeywordv("cd")); + if (janet_checktype(workdir, JANET_STRING)) { + chdir_path = (const char *) janet_unwrap_string(workdir); +#ifndef JANET_SPAWN_CHDIR + janet_panicf(":cd argument not supported on this system - %s", chdir_path); +#endif + } else if (!janet_checktype(workdir, JANET_NIL)) { + janet_panicf("expected string for :cd argumnet, got %v", workdir); + } + } + /* Clean up if any of the pipes have any issues */ if (pipe_errflag) { if (pipe_in != JANET_HANDLE_NONE) close_handle(pipe_in); @@ -1180,6 +1226,7 @@ static Janet os_execute_impl(int32_t argc, Janet *argv, JanetExecuteMode mode) { SECURITY_ATTRIBUTES saAttr; PROCESS_INFORMATION processInfo; STARTUPINFO startupInfo; + LPCSTR lpCurrentDirectory = NULL; memset(&saAttr, 0, sizeof(saAttr)); memset(&processInfo, 0, sizeof(processInfo)); memset(&startupInfo, 0, sizeof(startupInfo)); @@ -1196,6 +1243,10 @@ static Janet os_execute_impl(int32_t argc, Janet *argv, JanetExecuteMode mode) { } const char *path = (const char *) janet_unwrap_string(exargs.items[0]); + if (chdir_path != NULL) { + lpCurrentDirectory = chdir_path; + } + /* Do IO redirection */ if (pipe_in != JANET_HANDLE_NONE) { @@ -1203,7 +1254,7 @@ static Janet os_execute_impl(int32_t argc, Janet *argv, JanetExecuteMode mode) { } else if (new_in != JANET_HANDLE_NONE) { startupInfo.hStdInput = new_in; } else { - startupInfo.hStdInput = (HANDLE) _get_osfhandle(0); + startupInfo.hStdInput = (HANDLE) _get_osfhandle(_fileno(stdin)); } if (pipe_out != JANET_HANDLE_NONE) { @@ -1211,15 +1262,17 @@ static Janet os_execute_impl(int32_t argc, Janet *argv, JanetExecuteMode mode) { } else if (new_out != JANET_HANDLE_NONE) { startupInfo.hStdOutput = new_out; } else { - startupInfo.hStdOutput = (HANDLE) _get_osfhandle(1); + startupInfo.hStdOutput = (HANDLE) _get_osfhandle(_fileno(stdout)); } if (pipe_err != JANET_HANDLE_NONE) { startupInfo.hStdError = pipe_err; } else if (new_err != NULL) { startupInfo.hStdError = new_err; + } else if (stderr_is_stdout) { + startupInfo.hStdError = startupInfo.hStdOutput; } else { - startupInfo.hStdError = (HANDLE) _get_osfhandle(2); + startupInfo.hStdError = (HANDLE) _get_osfhandle(_fileno(stderr)); } int cp_failed = 0; @@ -1230,7 +1283,7 @@ static Janet os_execute_impl(int32_t argc, Janet *argv, JanetExecuteMode mode) { TRUE, /* handle inheritance */ 0, /* flags */ use_environ ? NULL : envp, /* pass in environment */ - NULL, /* use parents starting directory */ + lpCurrentDirectory, &startupInfo, &processInfo)) { cp_failed = 1; @@ -1287,6 +1340,15 @@ static Janet os_execute_impl(int32_t argc, Janet *argv, JanetExecuteMode mode) { /* Posix spawn setup */ posix_spawn_file_actions_t actions; posix_spawn_file_actions_init(&actions); +#ifdef JANET_SPAWN_CHDIR + if (chdir_path != NULL) { +#ifdef JANET_SPAWN_CHDIR_NO_NP + posix_spawn_file_actions_addchdir(&actions, chdir_path); +#else + posix_spawn_file_actions_addchdir_np(&actions, chdir_path); +#endif + } +#endif if (pipe_in != JANET_HANDLE_NONE) { posix_spawn_file_actions_adddup2(&actions, pipe_in, 0); posix_spawn_file_actions_addclose(&actions, pipe_in); @@ -1309,6 +1371,8 @@ static Janet os_execute_impl(int32_t argc, Janet *argv, JanetExecuteMode mode) { } else if (new_err != JANET_HANDLE_NONE && new_err != 2) { posix_spawn_file_actions_adddup2(&actions, new_err, 2); posix_spawn_file_actions_addclose(&actions, new_err); + } else if (stderr_is_stdout) { + posix_spawn_file_actions_adddup2(&actions, 1, 2); } pid_t pid; @@ -1414,7 +1478,8 @@ JANET_CORE_FN(os_spawn, "`:pipe` may fail if there are too many active file descriptors. The caller is " "responsible for closing pipes created by `:pipe` (either individually or using " "`os/proc-close`). Similar to `os/execute`, the caller is responsible for ensuring " - "pipes do not cause the program to block and deadlock.") { + "pipes do not cause the program to block and deadlock. As a special case, the stream passed to `:err` " + "can be the keyword `:out` to redirect stderr to stdout in the subprocess.") { return os_execute_impl(argc, argv, JANET_EXECUTE_SPAWN); } @@ -2797,6 +2862,7 @@ void janet_lib_os(JanetTable *env) { JANET_CORE_REG("os/proc-wait", os_proc_wait), JANET_CORE_REG("os/proc-kill", os_proc_kill), JANET_CORE_REG("os/proc-close", os_proc_close), + JANET_CORE_REG("os/getpid", os_proc_getpid), #endif /* high resolution timers */ diff --git a/src/include/janet.h b/src/include/janet.h index de82b5fb..672552d1 100644 --- a/src/include/janet.h +++ b/src/include/janet.h @@ -67,6 +67,11 @@ extern "C" { #define JANET_LINUX 1 #endif +/* Check for Android */ +#ifdef __ANDROID__ +#define JANET_ANDROID 1 +#endif + /* Check for Cygwin */ #if defined(__CYGWIN__) #define JANET_CYGWIN 1 @@ -591,7 +596,7 @@ typedef void *JanetAbstract; #define JANET_STREAM_WRITABLE 0x400 #define JANET_STREAM_ACCEPTABLE 0x800 #define JANET_STREAM_UDPSERVER 0x1000 -#define JANET_STREAM_BUFFERED 0x2000 +#define JANET_STREAM_NOT_CLOSEABLE 0x2000 #define JANET_STREAM_TOCLOSE 0x10000 typedef enum { diff --git a/test/suite-ev.janet b/test/suite-ev.janet index cf040e2c..b2d294a6 100644 --- a/test/suite-ev.janet +++ b/test/suite-ev.janet @@ -430,13 +430,7 @@ # Now do our telnet chat (def bob (assert (net/connect test-host test-port :stream))) (expect-read bob "Whats your name?\n") -(if (= :mingw (os/which)) - (net/write bob "bob") - (do - (def fbob (ev/to-file bob)) - (file/write fbob "bob") - (file/flush fbob) - (:close fbob))) +(net/write bob "bob") (expect-read bob "Welcome bob\n") (def alice (assert (net/connect test-host test-port))) (expect-read alice "Whats your name?\n") @@ -501,8 +495,10 @@ # soreuseport on unix domain sockets (compwhen (or (= :macos (os/which)) (= :linux (os/which))) (assert-no-error "unix-domain socket reuseaddr" - (let [s (net/listen :unix "./unix-domain-socket" :stream)] - (:close s)))) + (let [uds-path "./unix-domain-socket"] + (defer (os/rm uds-path) + (let [s (net/listen :unix uds-path :stream)] + (:close s)))))) # net/accept-loop level triggering (gccollect) @@ -554,4 +550,21 @@ (ev/deadline 0.01 nil f true) (assert-error "deadline expired" (resume f))) +# Use :err :stdout +(def- subproc-code '(do (eprint "hi") (eflush) (print "there") (flush))) +(defn ev/slurp + [f &opt buf] + (default buf @"") + (if (ev/read f 0x10000 buf) + (ev/slurp f buf) + buf)) +(def p (os/spawn [;run janet "-e" (string/format "%j" subproc-code)] :px {:out :pipe :err :out})) +(def [exit-code data] + (ev/gather + (os/proc-wait p) + (ev/slurp (p :out)))) +(def data (string/replace-all "\r" "" data)) +(assert (zero? exit-code) "subprocess ran") +(assert (= data "hi\nthere\n") "output is correct") + (end-suite)