From aaac61ba25080c0cae77b58c2dfa357161fa8455 Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Sun, 21 Jun 2026 11:27:13 +0200 Subject: [PATCH] Add `atomvm:posix_kill/2` Also stop leaking socat processes in tests Signed-off-by: Paul Guyot --- CHANGELOG.md | 1 + libs/eavmlib/src/atomvm.erl | 26 ++- src/libAtomVM/CMakeLists.txt | 1 + src/libAtomVM/nifs.c | 5 + src/libAtomVM/nifs.gperf | 1 + src/libAtomVM/posix_nifs.c | 50 ++++- src/libAtomVM/posix_nifs.h | 3 + src/platforms/esp32/CMakeLists.txt | 2 + src/platforms/esp32/test/CMakeLists.txt | 2 + src/platforms/rp2/CMakeLists.txt | 2 + src/platforms/stm32/CMakeLists.txt | 1 + tests/libs/eavmlib/CMakeLists.txt | 1 + tests/libs/eavmlib/test_subprocess.erl | 96 ++++++++ tests/libs/eavmlib/tests.erl | 1 + tests/libs/estdlib/test_serial_dist_socat.erl | 14 +- tests/libs/estdlib/test_uart.erl | 210 ++++++++++-------- 16 files changed, 321 insertions(+), 95 deletions(-) create mode 100644 tests/libs/eavmlib/test_subprocess.erl diff --git a/CHANGELOG.md b/CHANGELOG.md index dd6fbef48c..9e4c160323 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added `filename:dirname/1`, `filename:basename/1,2`, `filename:extension/1`, `filename:rootname/1,2` and `filename:join/2` - Added `init:get_arguments/0` +- Added `atomvm:posix_kill/2` to send a signal to a process, typically one started with `atomvm:subprocess/4` - Added Erlang distribution over serial (uart) - Added WASM32 JIT backend for Emscripten platform - Added `network:wifi_scan/0,1` to ESP32 network driver to scan available APs when in sta or sta+ap mode. diff --git a/libs/eavmlib/src/atomvm.erl b/libs/eavmlib/src/atomvm.erl index fb825cb65f..d5acf31224 100644 --- a/libs/eavmlib/src/atomvm.erl +++ b/libs/eavmlib/src/atomvm.erl @@ -56,7 +56,8 @@ posix_closedir/1, posix_readdir/1, get_creation/0, - subprocess/4 + subprocess/4, + posix_kill/2 ]). -export_type([ @@ -518,3 +519,26 @@ get_creation() -> {ok, non_neg_integer(), posix_fd()} | {error, posix_error()}. subprocess(_Path, _Args, _Env, _Options) -> erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param OsPid operating system process id, as returned by `subprocess/4' +%% @param Signal signal number to send, e.g. 15 for SIGTERM +%% @returns `ok' or an error tuple +%% @doc Send a signal to a process using kill(2). Typically used to +%% terminate a process started with `subprocess/4'. +%% +%% A return value of `ok' means kill(2) accepted the request; it does +%% not guarantee that the target terminated, as the signal may be +%% caught, blocked, or ignored. Signal `0' sends no signal and merely +%% performs a POSIX existence/permission check. +%% +%% `OsPid' follows kill(2) semantics: a positive value targets a single +%% process, `0' targets the caller's process group, `-1' targets every +%% process the caller may signal, and a value less than `-1' targets the +%% process group whose id is the absolute value. +%% @end +%%----------------------------------------------------------------------------- +-spec posix_kill(OsPid :: integer(), Signal :: non_neg_integer()) -> + ok | {error, posix_error()}. +posix_kill(_OsPid, _Signal) -> + erlang:nif_error(undefined). diff --git a/src/libAtomVM/CMakeLists.txt b/src/libAtomVM/CMakeLists.txt index eba7b1fba4..f7b2ef7e41 100644 --- a/src/libAtomVM/CMakeLists.txt +++ b/src/libAtomVM/CMakeLists.txt @@ -258,6 +258,7 @@ define_if_function_exists(libAtomVM fstat "sys/stat.h" PRIVATE HAVE_FSTAT) define_if_function_exists(libAtomVM unlink "unistd.h" PRIVATE HAVE_UNLINK) define_if_function_exists(libAtomVM rmdir "unistd.h" PRIVATE HAVE_RMDIR) define_if_function_exists(libAtomVM execve "unistd.h" PRIVATE HAVE_EXECVE) +define_if_function_exists(libAtomVM kill "signal.h" PRIVATE HAVE_KILL) define_if_function_exists(libAtomVM tcgetattr "termios.h" PRIVATE HAVE_TCGETATTR) define_if_function_exists(libAtomVM closefrom "unistd.h" PRIVATE HAVE_CLOSEFROM) define_if_function_exists(libAtomVM getcwd "unistd.h" PUBLIC HAVE_GETCWD) diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 2e3f70f7eb..e1926134c1 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -1033,6 +1033,11 @@ DEFINE_MATH_NIF(tanh) #define IF_HAVE_OPEN_CLOSE(expr) NULL #define IF_HAVE_EXECVE(expr) NULL #endif +#if HAVE_KILL +#define IF_HAVE_KILL(expr) (expr) +#else +#define IF_HAVE_KILL(expr) NULL +#endif #if HAVE_OPEN && HAVE_CLOSE && HAVE_LSEEK #define IF_HAVE_LSEEK(expr) (expr) #else diff --git a/src/libAtomVM/nifs.gperf b/src/libAtomVM/nifs.gperf index c7ce64cf7e..a7281dbc7e 100644 --- a/src/libAtomVM/nifs.gperf +++ b/src/libAtomVM/nifs.gperf @@ -190,6 +190,7 @@ 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_kill/2, IF_HAVE_KILL(&atomvm_posix_kill_nif) atomvm:posix_seek/3, IF_HAVE_LSEEK(&atomvm_posix_seek_nif) atomvm:posix_pread/3, IF_HAVE_PREAD(&atomvm_posix_pread_nif) atomvm:posix_pwrite/3, IF_HAVE_PWRITE(&atomvm_posix_pwrite_nif) diff --git a/src/libAtomVM/posix_nifs.c b/src/libAtomVM/posix_nifs.c index 7fde3982a6..13bbde4362 100644 --- a/src/libAtomVM/posix_nifs.c +++ b/src/libAtomVM/posix_nifs.c @@ -56,6 +56,11 @@ #include #endif +#if HAVE_KILL +#include +#include +#endif + #include "defaultatoms.h" #include "erl_nif_priv.h" #include "globalcontext.h" @@ -69,7 +74,7 @@ extern char **environ; term posix_errno_to_term(int err, GlobalContext *glb) { -#if HAVE_OPEN && HAVE_CLOSE || defined(HAVE_CLOCK_SETTIME) || defined(HAVE_SETTIMEOFDAY) +#if HAVE_OPEN && HAVE_CLOSE || defined(HAVE_CLOCK_SETTIME) || defined(HAVE_SETTIMEOFDAY) || HAVE_KILL // These are defined in SUSv1 term result; switch (err) { @@ -954,6 +959,43 @@ static term nif_atomvm_subprocess(Context *ctx, int argc, term argv[]) #endif #endif +#if HAVE_KILL +static bool term_is_valid_pid(term t) +{ + if (!term_is_int64(t)) { + return false; + } + // The value must round-trip through pid_t: a larger integer would be + // silently truncated and target a different (possibly own) process. + int64_t value = term_to_int64(t); + return ((int64_t) (pid_t) value) == value; +} + +static bool term_is_signal_number(term t) +{ + if (!term_is_int64(t)) { + return false; + } + int64_t value = term_to_int64(t); + return value >= 0 && value <= INT_MAX; +} + +static term nif_atomvm_posix_kill(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + VALIDATE_VALUE(argv[0], term_is_valid_pid); + VALIDATE_VALUE(argv[1], term_is_signal_number); + + pid_t pid = (pid_t) term_to_int64(argv[0]); + int signo = (int) term_to_int64(argv[1]); + + if (UNLIKELY(kill(pid, signo) != 0)) { + return errno_to_error_tuple_maybe_gc(ctx); + } + return OK_ATOM; +} +#endif + #if HAVE_MKFIFO static term nif_atomvm_posix_mkfifo(Context *ctx, int argc, term argv[]) { @@ -1836,6 +1878,12 @@ const struct Nif atomvm_subprocess_nif = { }; #endif #endif +#if HAVE_KILL +const struct Nif atomvm_posix_kill_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_atomvm_posix_kill +}; +#endif #if HAVE_OPEN && HAVE_CLOSE && HAVE_LSEEK const struct Nif atomvm_posix_seek_nif = { .base.type = NIFFunctionType, diff --git a/src/libAtomVM/posix_nifs.h b/src/libAtomVM/posix_nifs.h index 4b15d75f46..9f220c2991 100644 --- a/src/libAtomVM/posix_nifs.h +++ b/src/libAtomVM/posix_nifs.h @@ -47,6 +47,9 @@ extern const struct Nif atomvm_posix_select_stop_nif; extern const struct Nif atomvm_subprocess_nif; #endif #endif +#if HAVE_KILL +extern const struct Nif atomvm_posix_kill_nif; +#endif #if HAVE_OPEN && HAVE_CLOSE && HAVE_LSEEK extern const struct Nif atomvm_posix_seek_nif; #endif diff --git a/src/platforms/esp32/CMakeLists.txt b/src/platforms/esp32/CMakeLists.txt index 35fdcef0ff..2e2a86b2e6 100644 --- a/src/platforms/esp32/CMakeLists.txt +++ b/src/platforms/esp32/CMakeLists.txt @@ -28,6 +28,8 @@ set(CMAKE_TRY_COMPILE_TARGET_TYPE "STATIC_LIBRARY") set(HAVE_MKFIFO "" CACHE INTERNAL "Have symbol mkfifo" FORCE) # Likewise with EXECVE set(HAVE_EXECVE "" CACHE INTERNAL "Have symbol execve" FORCE) +# kill links to a newlib stub but there are no OS processes to signal +set(HAVE_KILL "" CACHE INTERNAL "Have symbol kill" 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 45bbe2c8d3..39ebc0095f 100644 --- a/src/platforms/esp32/test/CMakeLists.txt +++ b/src/platforms/esp32/test/CMakeLists.txt @@ -41,6 +41,8 @@ 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) +set(HAVE_KILL NO) +set(HAVE_KILL "" CACHE INTERNAL "Have symbol kill" 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 1f9859bd9a..53f60d04b2 100644 --- a/src/platforms/rp2/CMakeLists.txt +++ b/src/platforms/rp2/CMakeLists.txt @@ -54,6 +54,8 @@ set(HAVE_MKFIFO "" CACHE INTERNAL "Have symbol mkfifo" FORCE) set(HAVE_UNLINK "" CACHE INTERNAL "Have symbol unlink" FORCE) # Likewise with EXECVE set(HAVE_EXECVE "" CACHE INTERNAL "Have symbol execve" FORCE) +# kill is declared in newlib's signal.h but not linkable on bare-metal +set(HAVE_KILL "" CACHE INTERNAL "Have symbol kill" FORCE) # getcwd is defined in newlib header but not implemented set(HAVE_GETCWD "" CACHE INTERNAL "Have symbol getcwd" FORCE) # pread, pwrite, fsync, ftruncate, mkdir and rmdir are diff --git a/src/platforms/stm32/CMakeLists.txt b/src/platforms/stm32/CMakeLists.txt index d56861915d..b5b359f58e 100644 --- a/src/platforms/stm32/CMakeLists.txt +++ b/src/platforms/stm32/CMakeLists.txt @@ -103,6 +103,7 @@ endif () set(HAVE_MKFIFO "" CACHE INTERNAL "Have symbol mkfifo" FORCE) set(HAVE_UNLINK "" CACHE INTERNAL "Have symbol unlink" FORCE) set(HAVE_EXECVE "" CACHE INTERNAL "Have symbol execve" FORCE) +set(HAVE_KILL "" CACHE INTERNAL "Have symbol kill" FORCE) set(HAVE_OPEN "" CACHE INTERNAL "Have symbol open" FORCE) set(HAVE_CLOSE "" CACHE INTERNAL "Have symbol close" FORCE) set(HAVE_OPENDIR "" CACHE INTERNAL "Have symbol opendir" FORCE) diff --git a/tests/libs/eavmlib/CMakeLists.txt b/tests/libs/eavmlib/CMakeLists.txt index 92484b196d..3cc1d65f53 100644 --- a/tests/libs/eavmlib/CMakeLists.txt +++ b/tests/libs/eavmlib/CMakeLists.txt @@ -29,6 +29,7 @@ set(ERLANG_MODULES test_http_server test_mdns test_port + test_subprocess test_timer_manager ) diff --git a/tests/libs/eavmlib/test_subprocess.erl b/tests/libs/eavmlib/test_subprocess.erl new file mode 100644 index 0000000000..5d706e476f --- /dev/null +++ b/tests/libs/eavmlib/test_subprocess.erl @@ -0,0 +1,96 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Paul Guyot +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +-module(test_subprocess). + +-export([test/0]). + +test() -> + case erlang:system_info(machine) of + "ATOM" -> + test_atomvm(); + _ -> + %% atomvm:subprocess/posix_kill are only available on AtomVM + ok + end. + +test_atomvm() -> + case atomvm:platform() of + generic_unix -> + ok = test_posix_kill(), + ok = test_posix_kill_badarg(), + ok = test_posix_kill_esrch(), + ok; + _ -> + %% atomvm:subprocess/posix_kill are only available on generic_unix + ok + end. + +%% A subprocess that would outlive the test unless killed: posix_kill +%% terminates it, observed as EOF on its stdout pipe (the write end is +%% closed when the process dies). +test_posix_kill() -> + %% exec so the subprocess pid is sleep itself, not the wrapping shell: + %% killing the shell would not reliably reach sleep, which inherits the + %% stdout pipe and would keep read_until_eof/1 blocked. + {ok, OsPid, Fd} = atomvm:subprocess( + "/bin/sh", ["sh", "-c", "exec sleep 30"], undefined, [stdout] + ), + true = is_integer(OsPid), + %% SIGTERM + ok = atomvm:posix_kill(OsPid, 15), + eof = read_until_eof(Fd), + ok = atomvm:posix_close(Fd), + ok. + +test_posix_kill_badarg() -> + ok = expect_badarg(fun() -> atomvm:posix_kill(not_a_pid, 15) end), + ok = expect_badarg(fun() -> atomvm:posix_kill(1, not_a_signal) end), + %% Oversized signal must be rejected rather than truncated to a valid one. + ok = expect_badarg(fun() -> atomvm:posix_kill(1, 1 bsl 40) end), + ok. + +expect_badarg(Fun) -> + try Fun() of + _ -> fail + catch + error:badarg -> ok + end. + +%% Signalling a pid that cannot exist fails with esrch. +test_posix_kill_esrch() -> + {error, esrch} = atomvm:posix_kill(536870911, 0), + ok. + +read_until_eof(Fd) -> + case atomvm:posix_read(Fd, 64) of + eof -> + eof; + {ok, _Data} -> + read_until_eof(Fd); + {error, eagain} -> + ok = atomvm:posix_select_read(Fd, self(), undefined), + receive + {select, _FdRes, undefined, ready_input} -> ok + after 5000 -> + exit(posix_kill_eof_timeout) + end, + read_until_eof(Fd) + end. diff --git a/tests/libs/eavmlib/tests.erl b/tests/libs/eavmlib/tests.erl index 1eb41026ba..f5c978aa50 100644 --- a/tests/libs/eavmlib/tests.erl +++ b/tests/libs/eavmlib/tests.erl @@ -29,6 +29,7 @@ start() -> test_http_server, test_mdns, test_port, + test_subprocess, test_timer_manager, test_ahttp_client ]). diff --git a/tests/libs/estdlib/test_serial_dist_socat.erl b/tests/libs/estdlib/test_serial_dist_socat.erl index 23714f6044..db7efbb6e4 100644 --- a/tests/libs/estdlib/test_serial_dist_socat.erl +++ b/tests/libs/estdlib/test_serial_dist_socat.erl @@ -223,7 +223,7 @@ test_multi_port_ping(OsPid1, SocatFd1, PtyA1, PtyB, OsPid2, SocatFd2, PtyA2, Pty start_socat() -> {ok, OsPid, Fd} = atomvm:subprocess( "/bin/sh", - ["sh", "-c", "socat -d -d pty,raw,echo=0 pty,raw,echo=0 2>&1"], + ["sh", "-c", "exec socat -d -d pty,raw,echo=0 pty,raw,echo=0 2>&1"], undefined, [stdout] ), @@ -235,11 +235,13 @@ start_socat() -> {OsPid, Fd, PtyA, PtyB}. stop_socat(OsPid, Fd) -> - atomvm:posix_close(Fd), - {ok, _, KillFd} = atomvm:subprocess( - "/bin/kill", ["kill", integer_to_list(OsPid)], undefined, [stdout] - ), - atomvm:posix_close(KillFd). + %% SIGTERM; exec in start_socat makes OsPid socat itself. Tolerate esrch so + %% an already-exited socat does not mask the real test failure. + case atomvm:posix_kill(OsPid, 15) of + ok -> ok; + {error, esrch} -> ok + end, + atomvm:posix_close(Fd). start_peer(PtyPath, TestName) -> AvmBin = find_atomvm_binary(), diff --git a/tests/libs/estdlib/test_uart.erl b/tests/libs/estdlib/test_uart.erl index fd074ba1f0..7508df0712 100644 --- a/tests/libs/estdlib/test_uart.erl +++ b/tests/libs/estdlib/test_uart.erl @@ -75,31 +75,40 @@ has_socat() -> %% Verify that socat ptys actually support termios (fails under qemu-user) has_working_ptys() -> - {SocatFd, PtyA, _PtyB} = start_socat(), - Result = + with_socat(fun(PtyA, _PtyB) -> case atomvm:posix_open(PtyA, [o_rdwr, o_noctty]) of {ok, Fd} -> - case atomvm:posix_tcgetattr(Fd) of - {ok, _} -> - atomvm:posix_close(Fd), - true; - {error, _} -> - atomvm:posix_close(Fd), - false + try + case atomvm:posix_tcgetattr(Fd) of + {ok, _} -> true; + {error, _} -> false + end + after + atomvm:posix_close(Fd) end; {error, _} -> false - end, - stop_socat(SocatFd), - Result. + end + end). -%% Start socat and return {SocatFd, PtyA, PtyB} +%% Run Fun with a fresh socat pty pair, guaranteeing socat is killed even if +%% Fun crashes: a leaked socat holds both ptys and eventually exhausts the pool. +with_socat(Fun) -> + {SocatFd, PtyA, PtyB} = start_socat(), + try + Fun(PtyA, PtyB) + after + stop_socat(SocatFd) + end. + +%% Start socat and return {SocatHandle, PtyA, PtyB} %% socat -d -d pty,raw,echo=0 pty,raw,echo=0 %% outputs on stderr: "N PTY is /dev/ttysXXX" twice +%% exec so the subprocess pid is socat itself, not the wrapping shell. start_socat() -> - {ok, _Pid, Fd} = atomvm:subprocess( + {ok, OsPid, Fd} = atomvm:subprocess( "/bin/sh", - ["sh", "-c", "socat -d -d pty,raw,echo=0 pty,raw,echo=0 2>&1"], + ["sh", "-c", "exec socat -d -d pty,raw,echo=0 pty,raw,echo=0 2>&1"], undefined, [stdout] ), @@ -109,9 +118,18 @@ start_socat() -> receive after 200 -> ok end, - {Fd, PtyA, PtyB}. + {{OsPid, Fd}, PtyA, PtyB}. -stop_socat(Fd) -> +%% socat keeps running (holding both ptys) until killed: closing the +%% stdout pipe is not enough, and leaked socats eventually exhaust the +%% system pty pool. +stop_socat({OsPid, Fd}) -> + %% SIGTERM; tolerate esrch so an already-exited socat does not mask the + %% real test failure during cleanup. + case atomvm:posix_kill(OsPid, 15) of + ok -> ok; + {error, esrch} -> ok + end, atomvm:posix_close(Fd). %% Read a line like "... N PTY is /dev/ttysXXX" and extract the path @@ -148,90 +166,108 @@ extract_pty(Line) -> %%-------------------------------------------------------------------- test_posix_tcgetattr() -> - {SocatFd, PtyA, _PtyB} = start_socat(), - {ok, Fd} = atomvm:posix_open(PtyA, [o_rdwr, o_noctty]), - {ok, Tio} = atomvm:posix_tcgetattr(Fd), - true = is_map(Tio), - true = is_integer(maps:get(cflag, Tio)), - true = is_integer(maps:get(iflag, Tio)), - true = is_integer(maps:get(oflag, Tio)), - true = is_integer(maps:get(lflag, Tio)), - true = is_integer(maps:get(ispeed, Tio)), - true = is_integer(maps:get(ospeed, Tio)), - ok = atomvm:posix_close(Fd), - stop_socat(SocatFd), - ok. + with_socat(fun(PtyA, _PtyB) -> + {ok, Fd} = atomvm:posix_open(PtyA, [o_rdwr, o_noctty]), + try + {ok, Tio} = atomvm:posix_tcgetattr(Fd), + true = is_map(Tio), + true = is_integer(maps:get(cflag, Tio)), + true = is_integer(maps:get(iflag, Tio)), + true = is_integer(maps:get(oflag, Tio)), + true = is_integer(maps:get(lflag, Tio)), + true = is_integer(maps:get(ispeed, Tio)), + true = is_integer(maps:get(ospeed, Tio)), + ok + after + atomvm:posix_close(Fd) + end + end). test_posix_tcsetattr_raw() -> - {SocatFd, PtyA, _PtyB} = start_socat(), - {ok, Fd} = atomvm:posix_open(PtyA, [o_rdwr, o_noctty]), - %% Set raw mode + speed - ok = atomvm:posix_tcsetattr(Fd, tcsanow, #{ - raw => true, - ispeed => 115200, - ospeed => 115200 - }), - %% Verify speed was set - {ok, Tio} = atomvm:posix_tcgetattr(Fd), - 115200 = maps:get(ispeed, Tio), - 115200 = maps:get(ospeed, Tio), - ok = atomvm:posix_close(Fd), - stop_socat(SocatFd), - ok. + with_socat(fun(PtyA, _PtyB) -> + {ok, Fd} = atomvm:posix_open(PtyA, [o_rdwr, o_noctty]), + try + %% Set raw mode + speed + ok = atomvm:posix_tcsetattr(Fd, tcsanow, #{ + raw => true, + ispeed => 115200, + ospeed => 115200 + }), + %% Verify speed was set + {ok, Tio} = atomvm:posix_tcgetattr(Fd), + 115200 = maps:get(ispeed, Tio), + 115200 = maps:get(ospeed, Tio), + ok + after + atomvm:posix_close(Fd) + end + end). %%-------------------------------------------------------------------- %% UART HAL tests over socat pty pair %%-------------------------------------------------------------------- test_uart_roundtrip() -> - {SocatFd, PtyA, PtyB} = start_socat(), - UartA = uart:open([{peripheral, PtyA}, {speed, 115200}]), - UartB = uart:open([{peripheral, PtyB}, {speed, 115200}]), - ok = uart:write(UartA, <<"hello">>), - {ok, <<"hello">>} = uart:read(UartB, 2000), - ok = uart:write(UartB, <<"world">>), - {ok, <<"world">>} = uart:read(UartA, 2000), - uart:close(UartA), - uart:close(UartB), - stop_socat(SocatFd), - ok. + with_socat(fun(PtyA, PtyB) -> + UartA = uart:open([{peripheral, PtyA}, {speed, 115200}]), + UartB = uart:open([{peripheral, PtyB}, {speed, 115200}]), + try + ok = uart:write(UartA, <<"hello">>), + {ok, <<"hello">>} = uart:read(UartB, 2000), + ok = uart:write(UartB, <<"world">>), + {ok, <<"world">>} = uart:read(UartA, 2000), + ok + after + uart:close(UartA), + uart:close(UartB) + end + end). test_uart_read_timeout() -> - {SocatFd, PtyA, _PtyB} = start_socat(), - UartA = uart:open([{peripheral, PtyA}, {speed, 115200}]), - {error, timeout} = uart:read(UartA, 200), - uart:close(UartA), - stop_socat(SocatFd), - ok. + with_socat(fun(PtyA, _PtyB) -> + UartA = uart:open([{peripheral, PtyA}, {speed, 115200}]), + try + {error, timeout} = uart:read(UartA, 200), + ok + after + uart:close(UartA) + end + end). test_uart_bidirectional() -> - {SocatFd, PtyA, PtyB} = start_socat(), - UartA = uart:open([{peripheral, PtyA}, {speed, 115200}]), - UartB = uart:open([{peripheral, PtyB}, {speed, 115200}]), - %% Send from both sides simultaneously - ok = uart:write(UartA, <<"from_a">>), - ok = uart:write(UartB, <<"from_b">>), - {ok, <<"from_b">>} = uart:read(UartA, 2000), - {ok, <<"from_a">>} = uart:read(UartB, 2000), - uart:close(UartA), - uart:close(UartB), - stop_socat(SocatFd), - ok. + with_socat(fun(PtyA, PtyB) -> + UartA = uart:open([{peripheral, PtyA}, {speed, 115200}]), + UartB = uart:open([{peripheral, PtyB}, {speed, 115200}]), + try + %% Send from both sides simultaneously + ok = uart:write(UartA, <<"from_a">>), + ok = uart:write(UartB, <<"from_b">>), + {ok, <<"from_b">>} = uart:read(UartA, 2000), + {ok, <<"from_a">>} = uart:read(UartB, 2000), + ok + after + uart:close(UartA), + uart:close(UartB) + end + end). test_uart_large_payload() -> - {SocatFd, PtyA, PtyB} = start_socat(), - UartA = uart:open([{peripheral, PtyA}, {speed, 115200}]), - UartB = uart:open([{peripheral, PtyB}, {speed, 115200}]), - %% Send a payload larger than a single read buffer (256 bytes) - Payload = list_to_binary(lists:duplicate(500, $x)), - ok = uart:write(UartA, Payload), - %% May need multiple reads to get all data - Received = read_all(UartB, byte_size(Payload), 5000, <<>>), - Payload = Received, - uart:close(UartA), - uart:close(UartB), - stop_socat(SocatFd), - ok. + with_socat(fun(PtyA, PtyB) -> + UartA = uart:open([{peripheral, PtyA}, {speed, 115200}]), + UartB = uart:open([{peripheral, PtyB}, {speed, 115200}]), + try + %% Send a payload larger than a single read buffer (256 bytes) + Payload = list_to_binary(lists:duplicate(500, $x)), + ok = uart:write(UartA, Payload), + %% May need multiple reads to get all data + Received = read_all(UartB, byte_size(Payload), 5000, <<>>), + Payload = Received, + ok + after + uart:close(UartA), + uart:close(UartB) + end + end). %% Read until we have ExpectedSize bytes or timeout read_all(_Uart, ExpectedSize, _Timeout, Acc) when byte_size(Acc) >= ExpectedSize ->