Skip to content

Commit

Permalink
Add atomvm:subprocess/4 performing pipe/fork/execve`
Browse files Browse the repository at this point in the history
Signed-off-by: Paul Guyot <[email protected]>
  • Loading branch information
pguyot committed Dec 26, 2024
1 parent bd67862 commit 4c4f16f
Show file tree
Hide file tree
Showing 12 changed files with 216 additions and 44 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added support for external pids and encoded pids in external terms
- 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:subprocess/4` to perform pipe/fork/execve on POSIX platforms

## [0.6.6] - Unreleased

Expand Down
19 changes: 18 additions & 1 deletion libs/eavmlib/src/atomvm.erl
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
posix_clock_settime/2,
posix_opendir/1,
posix_closedir/1,
posix_readdir/1
posix_readdir/1,
subprocess/4
]).

-export_type([
Expand Down Expand Up @@ -335,3 +336,19 @@ posix_closedir(_Dir) ->
{ok, {dirent, Inode :: integer(), Name :: binary()}} | eof | {error, posix_error()}.
posix_readdir(_Dir) ->
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).
1 change: 1 addition & 0 deletions src/libAtomVM/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ 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_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)
Expand Down
6 changes: 6 additions & 0 deletions src/libAtomVM/nifs.c
Original file line number Diff line number Diff line change
Expand Up @@ -845,8 +845,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)
Expand Down
1 change: 1 addition & 0 deletions src/libAtomVM/nifs.gperf
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
191 changes: 148 additions & 43 deletions src/libAtomVM/posix_nifs.c
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,19 @@ term posix_errno_to_term(int err, GlobalContext *glb)
return term_from_int(err);
}

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;
}

#if HAVE_OPEN && HAVE_CLOSE
#define CLOSED_FD (-1)

Expand Down Expand Up @@ -340,12 +353,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;
}
Expand Down Expand Up @@ -373,13 +381,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"));
Expand Down Expand Up @@ -488,6 +490,130 @@ static term nif_atomvm_posix_select_stop(Context *ctx, int argc, term argv[])

return OK_ATOM;
}

#if HAVE_EXECVE
static char **parse_string_list(term list)
{
if (!term_is_list(list)) {
return NULL;
}
int ok;
size_t result_len = term_list_length(list, &ok);
if (UNLIKELY(!ok)) {
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, &ok);
if (UNLIKELY(!ok)) {
for (int j = 0; j < i; j++) {
free(result_list[j]);
}
free(result_list);
return NULL;
}
result_list[i++] = str;
list_item = term_get_list_tail(list_item);
if (str == NULL) {
// NULL pointer marks the end of the list, so we need to break
// here.
break;
}
}
return result_list;
}

static void free_string_list(char **list)
{
char **ptr = list;
while (*ptr) {
char *str = *ptr;
free(str);
ptr++;
}
free(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);
}
r = fork();
if (r < 0) {
free(path);
free_string_list(args);
free_string_list(envp);
close(pstdout[0]);
close(pstdout[1]);
return errno_to_error_tuple_maybe_gc(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
close(pstdout[1]); // close excess file descriptor
execve(path, args, envp);
exit(1);
}
// parent
close(pstdout[1]); // close write-end of the pipe
free(path);
free_string_list(args);
free_string_list(envp);

// Return a resource object
struct PosixFd *stdout_obj = enif_alloc_resource(ctx->global->posix_fd_resource_type, sizeof(struct PosixFd));
if (IS_NULL_PTR(stdout_obj)) {
RAISE_ERROR(OUT_OF_MEMORY_ATOM);
}
stdout_obj->fd = pstdout[0];
stdout_obj->selecting_process_id = INVALID_PROCESS_ID;

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 = term_from_resource(stdout_obj, &ctx->heap);
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(r));
term_put_tuple_element(result, 2, stdout_term);

return result;
}
#endif
#endif

#if HAVE_MKFIFO
Expand All @@ -506,23 +632,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

Expand All @@ -542,13 +660,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;
}
Expand Down Expand Up @@ -629,19 +741,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);
Expand Down Expand Up @@ -802,6 +901,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 = {
Expand Down
3 changes: 3 additions & 0 deletions src/libAtomVM/posix_nifs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/platforms/esp32/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/platforms/esp32/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/platforms/rp2/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions src/platforms/stm32/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading

0 comments on commit 4c4f16f

Please sign in to comment.