diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bcb27aa8..28d67c6f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for external refs and encoded refs in external terms - Introduce ports to represent native processes and added support for external ports and encoded ports in external terms - Added `atomvm:get_creation/0`, equivalent to `erts_internal:get_creation/0` +- Added `atomvm:subprocess/4` to perform pipe/fork/execve on POSIX platforms ### Fixed diff --git a/libs/eavmlib/src/atomvm.erl b/libs/eavmlib/src/atomvm.erl index a9a60df26..ba21145d9 100644 --- a/libs/eavmlib/src/atomvm.erl +++ b/libs/eavmlib/src/atomvm.erl @@ -44,7 +44,8 @@ posix_opendir/1, posix_closedir/1, posix_readdir/1, - get_creation/0 + get_creation/0, + subprocess/4 ]). -export_type([ @@ -341,3 +342,19 @@ posix_readdir(_Dir) -> -spec get_creation() -> non_neg_integer(). get_creation() -> erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Path path to the command to execute +%% @param Args arguments to pass to the command. First item is the name +%% of the command +%% @param Envp environment variables to pass to the command. +%% @param Options options to run execve. Should be `[stdout]' +%% @returns a tuple with the process id and a fd to the stdout of the process. +%% @doc Fork and execute a program using fork(2) and execve(2). Pipe stdout +%% so output of the program can be read with `atomvm:posix_read/2'. +%% @end +%%----------------------------------------------------------------------------- +-spec subprocess(Path :: iodata(), Args :: [iodata()], Env :: [iodata()], Options :: [stdout]) -> + {ok, non_neg_integer(), posix_fd()} | {error, posix_error()}. +subprocess(_Path, _Args, _Env, _Options) -> + erlang:nif_error(undefined). diff --git a/src/libAtomVM/CMakeLists.txt b/src/libAtomVM/CMakeLists.txt index 23739c466..42af53163 100644 --- a/src/libAtomVM/CMakeLists.txt +++ b/src/libAtomVM/CMakeLists.txt @@ -199,6 +199,9 @@ define_if_function_exists(libAtomVM closedir "dirent.h" PUBLIC HAVE_CLOSEDIR) define_if_function_exists(libAtomVM mkfifo "sys/stat.h" PRIVATE HAVE_MKFIFO) define_if_function_exists(libAtomVM readdir "dirent.h" PUBLIC HAVE_READDIR) define_if_function_exists(libAtomVM unlink "unistd.h" PRIVATE HAVE_UNLINK) +define_if_function_exists(libAtomVM execve "unistd.h" PRIVATE HAVE_EXECVE) +define_if_function_exists(libAtomVM closefrom "unistd.h" PRIVATE HAVE_CLOSEFROM) +define_if_symbol_exists(libAtomVM POSIX_SPAWN_CLOEXEC_DEFAULT "spawn.h" PRIVATE HAVE_POSIX_SPAWN_CLOEXEC_DEFAULT) define_if_symbol_exists(libAtomVM O_CLOEXEC "fcntl.h" PRIVATE HAVE_O_CLOEXEC) define_if_symbol_exists(libAtomVM O_DIRECTORY "fcntl.h" PRIVATE HAVE_O_DIRECTORY) define_if_symbol_exists(libAtomVM O_DSYNC "fcntl.h" PRIVATE HAVE_O_DSYNC) diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index f0bf5c90b..ea6e9cfef 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -851,8 +851,14 @@ DEFINE_MATH_NIF(tanh) //Handle optional nifs #if HAVE_OPEN && HAVE_CLOSE #define IF_HAVE_OPEN_CLOSE(expr) (expr) +#if HAVE_EXECVE +#define IF_HAVE_EXECVE(expr) (expr) +#else +#define IF_HAVE_EXECVE(expr) NULL +#endif #else #define IF_HAVE_OPEN_CLOSE(expr) NULL +#define IF_HAVE_EXECVE(expr) NULL #endif #if HAVE_MKFIFO #define IF_HAVE_MKFIFO(expr) (expr) diff --git a/src/libAtomVM/nifs.gperf b/src/libAtomVM/nifs.gperf index a4a3afe04..b0a1a1b33 100644 --- a/src/libAtomVM/nifs.gperf +++ b/src/libAtomVM/nifs.gperf @@ -148,6 +148,7 @@ atomvm:posix_write/2, IF_HAVE_OPEN_CLOSE(&atomvm_posix_write_nif) atomvm:posix_select_read/3, IF_HAVE_OPEN_CLOSE(&atomvm_posix_select_read_nif) atomvm:posix_select_write/3, IF_HAVE_OPEN_CLOSE(&atomvm_posix_select_write_nif) atomvm:posix_select_stop/1, IF_HAVE_OPEN_CLOSE(&atomvm_posix_select_stop_nif) +atomvm:subprocess/4, IF_HAVE_EXECVE(&atomvm_subprocess_nif) atomvm:posix_mkfifo/2, IF_HAVE_MKFIFO(&atomvm_posix_mkfifo_nif) atomvm:posix_unlink/1, IF_HAVE_UNLINK(&atomvm_posix_unlink_nif) atomvm:posix_clock_settime/2, IF_HAVE_CLOCK_SETTIME_OR_SETTIMEOFDAY(&atomvm_posix_clock_settime_nif) diff --git a/src/libAtomVM/posix_nifs.c b/src/libAtomVM/posix_nifs.c index 8aa98b852..0c10b33a6 100644 --- a/src/libAtomVM/posix_nifs.c +++ b/src/libAtomVM/posix_nifs.c @@ -47,6 +47,10 @@ #include #endif +#if HAVE_POSIX_SPAWN_CLOEXEC_DEFAULT +#include +#endif + #include "defaultatoms.h" #include "erl_nif_priv.h" #include "globalcontext.h" @@ -130,6 +134,24 @@ term posix_errno_to_term(int err, GlobalContext *glb) return term_from_int(err); } +static term error_tuple_maybe_gc(int err, Context *ctx) +{ + if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(2), MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term result = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(result, 0, ERROR_ATOM); + term_put_tuple_element(result, 1, posix_errno_to_term(err, ctx->global)); + + return result; +} + +static term errno_to_error_tuple_maybe_gc(Context *ctx) +{ + return error_tuple_maybe_gc(errno, ctx); +} + #if HAVE_OPEN && HAVE_CLOSE #define CLOSED_FD (-1) @@ -201,6 +223,18 @@ const ErlNifResourceTypeInit posix_fd_resource_type_init = { #define O_TRUNC_ATOM_STR ATOM_STR("\x8", "o_trunc") #define O_TTY_INIT_ATOM_STR ATOM_STR("\xA", "o_tty_init") +static term make_posix_fd_resource(Context *ctx, int fd) +{ + // Return a resource object + struct PosixFd *fd_obj = enif_alloc_resource(ctx->global->posix_fd_resource_type, sizeof(struct PosixFd)); + if (IS_NULL_PTR(fd_obj)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + fd_obj->fd = fd; + fd_obj->selecting_process_id = INVALID_PROCESS_ID; + return term_from_resource(fd_obj, &ctx->heap); +} + static term nif_atomvm_posix_open(Context *ctx, int argc, term argv[]) { GlobalContext *glb = ctx->global; @@ -304,17 +338,13 @@ static term nif_atomvm_posix_open(Context *ctx, int argc, term argv[]) term_put_tuple_element(result, 0, ERROR_ATOM); term_put_tuple_element(result, 1, posix_errno_to_term(errno, glb)); } else { - // Return a resource object - struct PosixFd *fd_obj = enif_alloc_resource(glb->posix_fd_resource_type, sizeof(struct PosixFd)); - if (IS_NULL_PTR(fd_obj)) { - RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } - fd_obj->fd = fd; - fd_obj->selecting_process_id = INVALID_PROCESS_ID; if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(2) + TERM_BOXED_RESOURCE_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - term obj = term_from_resource(fd_obj, &ctx->heap); + term obj = make_posix_fd_resource(ctx, fd); + if (term_is_invalid_term(obj)) { + return obj; + } result = term_alloc_tuple(2, &ctx->heap); term_put_tuple_element(result, 0, OK_ATOM); term_put_tuple_element(result, 1, obj); @@ -340,12 +370,7 @@ static term nif_atomvm_posix_close(Context *ctx, int argc, term argv[]) } if (UNLIKELY(close(fd_obj->fd) < 0)) { fd_obj->fd = CLOSED_FD; // even if bad things happen, do not close twice. - if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(2), MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { - RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } - result = term_alloc_tuple(2, &ctx->heap); - term_put_tuple_element(result, 0, ERROR_ATOM); - term_put_tuple_element(result, 1, posix_errno_to_term(errno, ctx->global)); + return errno_to_error_tuple_maybe_gc(ctx); } fd_obj->fd = CLOSED_FD; } @@ -373,13 +398,7 @@ static term nif_atomvm_posix_read(Context *ctx, int argc, term argv[]) int res = read(fd_obj->fd, (void *) term_binary_data(bin_term), count); if (UNLIKELY(res < 0)) { // Return an error. - if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(2), MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { - RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } - term ret = term_alloc_tuple(2, &ctx->heap); - term_put_tuple_element(ret, 0, ERROR_ATOM); - term_put_tuple_element(ret, 1, posix_errno_to_term(errno, glb)); - return ret; + return errno_to_error_tuple_maybe_gc(ctx); } if (res == 0) { return globalcontext_make_atom(glb, ATOM_STR("\x3", "eof")); @@ -488,6 +507,163 @@ static term nif_atomvm_posix_select_stop(Context *ctx, int argc, term argv[]) return OK_ATOM; } + +#if HAVE_EXECVE +static void free_string_list(char **list) +{ + char **ptr = list; + while (*ptr) { + char *str = *ptr; + free(str); + ptr++; + } + free(list); +} + +static char **parse_string_list(term list) +{ + if (!term_is_list(list)) { + return NULL; + } + int proper; + size_t result_len = term_list_length(list, &proper); + if (UNLIKELY(!proper)) { + return NULL; + } + // All items are initialized to NULL. + char **result_list = calloc(result_len + 1, sizeof(char *)); + if (IS_NULL_PTR(result_list)) { + return NULL; + } + term list_item = list; + int i = 0; + while (term_is_nonempty_list(list_item)) { + term item = term_get_list_head(list_item); + char *str = interop_term_to_string(item, &proper); + if (UNLIKELY(!proper)) { + free_string_list(result_list); + return NULL; + } + result_list[i++] = str; + list_item = term_get_list_tail(list_item); + } + return result_list; +} + +static term nif_atomvm_subprocess(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + int ok; + char *path = interop_term_to_string(argv[0], &ok); + if (UNLIKELY(!ok)) { + RAISE_ERROR(BADARG_ATOM); + } + char **args = parse_string_list(argv[1]); + if (IS_NULL_PTR(args)) { + free(path); + RAISE_ERROR(BADARG_ATOM); + } + char **envp = parse_string_list(argv[2]); + if (IS_NULL_PTR(envp)) { + free(path); + free_string_list(args); + RAISE_ERROR(BADARG_ATOM); + } + + int pstdout[2]; + int r = pipe(pstdout); + if (r < 0) { + free(path); + free_string_list(args); + free_string_list(envp); + return errno_to_error_tuple_maybe_gc(ctx); + } + pid_t pid; +#if HAVE_POSIX_SPAWN_CLOEXEC_DEFAULT + do { + posix_spawn_file_actions_t file_actions; + posix_spawnattr_t spawn_attrs; + if (UNLIKELY((r = posix_spawn_file_actions_init(&file_actions)) != 0)) { + break; + } + if (UNLIKELY((r = posix_spawn_file_actions_adddup2(&file_actions, pstdout[1], 1)) != 0)) { + break; + } + if (UNLIKELY((r = posix_spawnattr_init(&spawn_attrs)) != 0)) { + break; + } + if (UNLIKELY((r = posix_spawnattr_setflags(&spawn_attrs, HAVE_POSIX_SPAWN_CLOEXEC_DEFAULT)) != 0)) { + break; + } + if (UNLIKELY((r = posix_spawn(&pid, path, &file_actions, &spawn_attrs, args, envp)) != 0)) { + break; + } + if (UNLIKELY((r = posix_spawnattr_destroy(&spawn_attrs)) != 0)) { + break; + } + if (UNLIKELY((r = posix_spawn_file_actions_destroy(&file_actions)) != 0)) { + break; + } + } while (false); + if (UNLIKELY(r != 0)) { + free(path); + free_string_list(args); + free_string_list(envp); + close(pstdout[0]); + close(pstdout[1]); + return error_tuple_maybe_gc(r, ctx); + } +#else + r = fork(); + if (r < 0) { + int err = errno; + free(path); + free_string_list(args); + free_string_list(envp); + close(pstdout[0]); + close(pstdout[1]); + return error_tuple_maybe_gc(err, ctx); + } + if (r == 0) { + // child. + close(0); // close stdin of the child + close(pstdout[0]); // close read end of the pipe + dup2(pstdout[1], 1); // make stdout the write-end of the pipe +#if HAVE_CLOSEFROM + closefrom(2); +#else + int maxfd = sysconf(_SC_OPEN_MAX); + for (int fd = 3; fd < maxfd; fd++) + close(fd); +#endif + execve(path, args, envp); + exit(1); + } + pid = r; +#endif + // parent + close(pstdout[1]); // close write-end of the pipe + free(path); + free_string_list(args); + free_string_list(envp); + + if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(3) + TERM_BOXED_RESOURCE_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term stdout_term = make_posix_fd_resource(ctx, pstdout[0]); + if (term_is_invalid_term(stdout_term)) { + return stdout_term; + } + term result = term_alloc_tuple(3, &ctx->heap); + term_put_tuple_element(result, 0, OK_ATOM); + term_put_tuple_element(result, 1, term_from_int(pid)); + term_put_tuple_element(result, 2, stdout_term); + + return result; +} +#endif #endif #if HAVE_MKFIFO @@ -506,23 +682,15 @@ static term nif_atomvm_posix_mkfifo(Context *ctx, int argc, term argv[]) int mode = term_to_int(mode_term); - term result; int res = mkfifo(path, mode); free((void *) path); if (res < 0) { // Return an error. - if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(2), MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { - RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } - result = term_alloc_tuple(2, &ctx->heap); - term_put_tuple_element(result, 0, ERROR_ATOM); - term_put_tuple_element(result, 1, posix_errno_to_term(errno, ctx->global)); - } else { - result = OK_ATOM; + return errno_to_error_tuple_maybe_gc(ctx); } - return result; + return OK_ATOM; } #endif @@ -542,13 +710,7 @@ static term nif_atomvm_posix_unlink(Context *ctx, int argc, term argv[]) free((void *) path); if (res < 0) { // Return an error. - if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(2), MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { - RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } - term result = term_alloc_tuple(2, &ctx->heap); - term_put_tuple_element(result, 0, ERROR_ATOM); - term_put_tuple_element(result, 1, posix_errno_to_term(errno, ctx->global)); - return result; + return errno_to_error_tuple_maybe_gc(ctx); } return OK_ATOM; } @@ -629,19 +791,6 @@ const ErlNifResourceTypeInit posix_dir_resource_type_init = { .dtor = posix_dir_dtor }; -static term errno_to_error_tuple_maybe_gc(Context *ctx) -{ - if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(2), MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { - RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } - - term result = term_alloc_tuple(2, &ctx->heap); - term_put_tuple_element(result, 0, ERROR_ATOM); - term_put_tuple_element(result, 1, posix_errno_to_term(errno, ctx->global)); - - return result; -} - static term nif_atomvm_posix_opendir(Context *ctx, int argc, term argv[]) { UNUSED(argc); @@ -802,6 +951,12 @@ const struct Nif atomvm_posix_select_stop_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_atomvm_posix_select_stop }; +#if HAVE_EXECVE +const struct Nif atomvm_subprocess_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_atomvm_subprocess +}; +#endif #endif #if HAVE_MKFIFO const struct Nif atomvm_posix_mkfifo_nif = { diff --git a/src/libAtomVM/posix_nifs.h b/src/libAtomVM/posix_nifs.h index 4b425529e..f4eb826ed 100644 --- a/src/libAtomVM/posix_nifs.h +++ b/src/libAtomVM/posix_nifs.h @@ -43,6 +43,9 @@ extern const struct Nif atomvm_posix_write_nif; extern const struct Nif atomvm_posix_select_read_nif; extern const struct Nif atomvm_posix_select_write_nif; extern const struct Nif atomvm_posix_select_stop_nif; +#if HAVE_EXECVE +extern const struct Nif atomvm_subprocess_nif; +#endif #endif #if HAVE_MKFIFO extern const struct Nif atomvm_posix_mkfifo_nif; diff --git a/src/platforms/esp32/CMakeLists.txt b/src/platforms/esp32/CMakeLists.txt index 08fbfabd6..2683670dd 100644 --- a/src/platforms/esp32/CMakeLists.txt +++ b/src/platforms/esp32/CMakeLists.txt @@ -26,6 +26,8 @@ set(CMAKE_TRY_COMPILE_TARGET_TYPE "STATIC_LIBRARY") # mkfifo is defined in newlib header but not implemented set(HAVE_MKFIFO "" CACHE INTERNAL "Have symbol mkfifo" FORCE) +# Likewise with EXECVE +set(HAVE_EXECVE "" CACHE INTERNAL "Have symbol execve" FORCE) # Force HAVE_SOCKET # Automatically detecting it requires to put too many components include dirs # in CMAKE_REQUIRED_INCLUDES as lwip includes freetos and many esp system components diff --git a/src/platforms/esp32/test/CMakeLists.txt b/src/platforms/esp32/test/CMakeLists.txt index 14a2221f8..66739b33c 100644 --- a/src/platforms/esp32/test/CMakeLists.txt +++ b/src/platforms/esp32/test/CMakeLists.txt @@ -39,6 +39,8 @@ set(CMAKE_TRY_COMPILE_TARGET_TYPE "STATIC_LIBRARY") # mkfifo is defined in newlib header but not implemented set(HAVE_MKFIFO NO) set(HAVE_MKFIFO "" CACHE INTERNAL "Have symbol mkfifo" FORCE) +set(HAVE_EXECVE NO) +set(HAVE_EXECVE "" CACHE INTERNAL "Have symbol execve" FORCE) # Force HAVE_SOCKET # Automatically detecting it requires to put too many components include dirs # in CMAKE_REQUIRED_INCLUDES as lwip includes freetos and many esp system components diff --git a/src/platforms/rp2/CMakeLists.txt b/src/platforms/rp2/CMakeLists.txt index 7a275ed03..b88f136e1 100644 --- a/src/platforms/rp2/CMakeLists.txt +++ b/src/platforms/rp2/CMakeLists.txt @@ -52,6 +52,8 @@ set(CMAKE_TRY_COMPILE_TARGET_TYPE "STATIC_LIBRARY") set(HAVE_MKFIFO "" CACHE INTERNAL "Have symbol mkfifo" FORCE) # also avoid exposing unlink to silence warning that it will always fail set(HAVE_UNLINK "" CACHE INTERNAL "Have symbol unlink" FORCE) +# Likewise with EXECVE +set(HAVE_EXECVE "" CACHE INTERNAL "Have symbol execve" FORCE) # Options that make sense for this platform option(AVM_DISABLE_SMP "Disable SMP support." OFF) diff --git a/src/platforms/stm32/CMakeLists.txt b/src/platforms/stm32/CMakeLists.txt index ab5bb6e7e..eb0480c8e 100644 --- a/src/platforms/stm32/CMakeLists.txt +++ b/src/platforms/stm32/CMakeLists.txt @@ -95,6 +95,13 @@ if (NOT DEVICE) set(DEVICE stm32f407vgt6) endif () +# mkfifo may be defined in some newlib header but not implemented +set(HAVE_MKFIFO "" CACHE INTERNAL "Have symbol mkfifo" FORCE) +# we don't want unlink either +set(HAVE_UNLINK "" CACHE INTERNAL "Have symbol unlink" FORCE) +# nor EXECVE +set(HAVE_EXECVE "" CACHE INTERNAL "Have symbol execve" FORCE) + # Include auto-device configuration include(cmake/atomvm_dev_config.cmake) diff --git a/tests/libs/eavmlib/test_file.erl b/tests/libs/eavmlib/test_file.erl index ed6abf4e4..775ed6643 100644 --- a/tests/libs/eavmlib/test_file.erl +++ b/tests/libs/eavmlib/test_file.erl @@ -26,12 +26,14 @@ test() -> HasSelect = atomvm:platform() =/= emscripten, + HasExecve = atomvm:platform() =/= emscripten, ok = test_basic_file(), ok = test_fifo_select(HasSelect), ok = test_gc(HasSelect), ok = test_crash_no_leak(HasSelect), ok = test_select_with_gone_process(HasSelect), ok = test_select_with_listeners(HasSelect), + ok = test_subprocess(HasExecve), ok. test_basic_file() -> @@ -311,3 +313,26 @@ test_select_with_listeners(_HasSelect) -> after 200 -> fail end, ok. + +test_subprocess(false) -> + ok; +test_subprocess(true) -> + ok = test_subprocess_echo(), + ok = test_subprocess_env(), + ok. + +test_subprocess_echo() -> + {ok, _Pid, StdoutFd} = atomvm:subprocess("/bin/echo", ["echo"], [], [stdout]), + {ok, <<"\n">>} = atomvm:posix_read(StdoutFd, 10), + eof = atomvm:posix_read(StdoutFd, 10), + ok = atomvm:posix_close(StdoutFd), + ok. + +test_subprocess_env() -> + {ok, _Pid, StdoutFd} = atomvm:subprocess("/bin/sh", ["sh", "-c", "echo $FOO"], ["FOO=bar"], [ + stdout + ]), + {ok, <<"bar\n">>} = atomvm:posix_read(StdoutFd, 10), + eof = atomvm:posix_read(StdoutFd, 10), + ok = atomvm:posix_close(StdoutFd), + ok.