ucode: add patches that make it easier to deal with non-blocking fds

This allows creating pipes for subprocesses to use as stdin/out/err
and polling them from a uloop process.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
This commit is contained in:
Felix Fietkau
2025-10-09 09:56:10 +02:00
parent 7418c2d89b
commit ad6df8a3c8
3 changed files with 293 additions and 0 deletions

View File

@ -0,0 +1,85 @@
From: Felix Fietkau <nbd@nbd.name>
Date: Wed, 8 Oct 2025 22:06:46 +0200
Subject: [PATCH] uloop: add optional setup callback to process()
Add optional setup callback as 5th argument to uloop.process() that is
invoked in the child process after fork() but before exec().
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
--- a/lib/uloop.c
+++ b/lib/uloop.c
@@ -961,8 +961,9 @@ uc_uloop_process_cb(struct uloop_process
*
* This function creates a process instance for executing external programs.
* It takes the executable path string, an optional string array as the argument
- * vector, an optional dictionary describing environment variables, and a
- * callback function to be invoked when the invoked process ends.
+ * vector, an optional dictionary describing environment variables, a
+ * callback function to be invoked when the invoked process ends, and an optional
+ * setup callback to be invoked in the child process after fork().
*
* @function module:uloop#process
*
@@ -979,6 +980,11 @@ uc_uloop_process_cb(struct uloop_process
* @param {Function} callback
* The callback function to be invoked when the invoked process ends.
*
+ * @param {Function} [setup]
+ * Optional. A callback function to be invoked in the child process after fork()
+ * but before exec(). This can be used to set up file descriptors, change working
+ * directory, or perform other initialization.
+ *
* @returns {?module:uloop.process}
* Returns a process instance for executing external programs.
* Returns `null` on error, e.g. due to `exec()` failure or invalid arguments.
@@ -988,6 +994,16 @@ uc_uloop_process_cb(struct uloop_process
* const myProcess = uloop.process("/bin/ls", ["-l", "/tmp"], null, (code) => {
* printf(`Process exited with code ${code}\n`);
* });
+ *
+ * // With setup callback to redirect stderr
+ * const myProcess = uloop.process("/bin/ls", ["-l", "/tmp"], null, (code) => {
+ * printf(`Process exited with code ${code}\n`);
+ * }, () => {
+ * const fs = require('fs');
+ * const errlog = fs.open('/tmp/error.log', 'w');
+ * fs.dup2(errlog.fileno(), 2);
+ * errlog.close();
+ * });
*/
static uc_value_t *
uc_uloop_process(uc_vm_t *vm, size_t nargs)
@@ -996,6 +1012,7 @@ uc_uloop_process(uc_vm_t *vm, size_t nar
uc_value_t *arguments = uc_fn_arg(1);
uc_value_t *env_arg = uc_fn_arg(2);
uc_value_t *callback = uc_fn_arg(3);
+ uc_value_t *setup_cb = uc_fn_arg(4);
uc_uloop_process_t *process;
uc_stringbuf_t *buf;
char **argp, **envp;
@@ -1005,7 +1022,8 @@ uc_uloop_process(uc_vm_t *vm, size_t nar
if (ucv_type(executable) != UC_STRING ||
(arguments && ucv_type(arguments) != UC_ARRAY) ||
(env_arg && ucv_type(env_arg) != UC_OBJECT) ||
- !ucv_is_callable(callback)) {
+ !ucv_is_callable(callback) ||
+ (setup_cb && !ucv_is_callable(setup_cb))) {
err_return(EINVAL);
}
@@ -1015,6 +1033,13 @@ uc_uloop_process(uc_vm_t *vm, size_t nar
err_return(errno);
if (pid == 0) {
+ if (setup_cb) {
+ uc_vm_stack_push(vm, ucv_get(setup_cb));
+
+ if (uc_uloop_vm_call(vm, false, 0))
+ ucv_put(uc_vm_stack_pop(vm));
+ }
+
argp = calloc(ucv_array_length(arguments) + 2, sizeof(char *));
envp = environ;

View File

@ -0,0 +1,75 @@
From: Felix Fietkau <nbd@nbd.name>
Date: Wed, 8 Oct 2025 22:15:42 +0200
Subject: [PATCH] fs: add dup2() function
Add dup2() function to duplicate file descriptors, useful for redirecting
standard streams in child processes.
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
--- a/lib/fs.c
+++ b/lib/fs.c
@@ -1278,6 +1278,54 @@ uc_fs_fdopen(uc_vm_t *vm, size_t nargs)
return ucv_resource_create(vm, "fs.file", fp);
}
+/**
+ * Duplicates a file descriptor.
+ *
+ * This function duplicates the file descriptor `oldfd` to `newfd`. If `newfd`
+ * was previously open, it is silently closed before being reused.
+ *
+ * Returns `true` on success.
+ * Returns `null` on error.
+ *
+ * @function module:fs#dup2
+ *
+ * @param {number} oldfd
+ * The file descriptor to duplicate.
+ *
+ * @param {number} newfd
+ * The file descriptor number to duplicate to.
+ *
+ * @returns {?boolean}
+ *
+ * @example
+ * // Redirect stderr to a log file
+ * const logfile = open('/tmp/error.log', 'w');
+ * dup2(logfile.fileno(), 2);
+ * logfile.close();
+ */
+static uc_value_t *
+uc_fs_dup2(uc_vm_t *vm, size_t nargs)
+{
+ uc_value_t *oldfd_arg = uc_fn_arg(0);
+ uc_value_t *newfd_arg = uc_fn_arg(1);
+ int oldfd, newfd;
+
+ oldfd = get_fd(vm, oldfd_arg);
+
+ if (oldfd == -1)
+ err_return(errno ? errno : EBADF);
+
+ newfd = get_fd(vm, newfd_arg);
+
+ if (newfd == -1)
+ err_return(errno ? errno : EBADF);
+
+ if (dup2(oldfd, newfd) == -1)
+ err_return(errno);
+
+ return ucv_boolean_new(true);
+}
+
/**
* Represents a handle for interacting with a directory opened by `opendir()`.
@@ -2890,6 +2938,7 @@ static const uc_function_list_t global_f
{ "error", uc_fs_error },
{ "open", uc_fs_open },
{ "fdopen", uc_fs_fdopen },
+ { "dup2", uc_fs_dup2 },
{ "opendir", uc_fs_opendir },
{ "popen", uc_fs_popen },
{ "readlink", uc_fs_readlink },

View File

@ -0,0 +1,133 @@
From: Felix Fietkau <nbd@nbd.name>
Date: Wed, 8 Oct 2025 23:03:05 +0200
Subject: [PATCH] fs: add read_nb() method for non-blocking reads
Add file handle method for reading from non-blocking file descriptors.
Designed for use with uloop, bypasses stdio buffering, handles EAGAIN/EINTR.
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
--- a/lib/fs.c
+++ b/lib/fs.c
@@ -674,6 +674,112 @@ uc_fs_read(uc_vm_t *vm, size_t nargs)
}
/**
+ * Reads data from a non-blocking file descriptor.
+ *
+ * This function is designed for use with uloop file descriptor monitoring.
+ * When called from within a uloop handle callback (after ULOOP_READ event),
+ * it reads available data from the non-blocking file descriptor.
+ *
+ * Performs a single read() operation directly on the file descriptor,
+ * bypassing stdio buffering. Properly handles EAGAIN and EINTR errors.
+ *
+ * Returns a string containing the data read, up to the specified limit.
+ *
+ * Returns an empty string if no data is available (EAGAIN/EWOULDBLOCK).
+ *
+ * Returns `null` if an error occurred.
+ *
+ * @function module:fs.file#read_nb
+ *
+ * @param {number} [limit=4096]
+ * Maximum number of bytes to read. Defaults to 4096 if not specified.
+ *
+ * @returns {?string}
+ *
+ * @example
+ * import * as uloop from 'uloop';
+ * import { fdopen } from 'fs';
+ *
+ * uloop.init();
+ *
+ * let sock = connect_socket(...);
+ * let fp = fdopen(sock, "r");
+ *
+ * uloop.handle(fp, (events) => {
+ * if (events & uloop.ULOOP_READ) {
+ * let data = fp.read_nb();
+ * if (data === null) {
+ * print("Error reading\n");
+ * } else if (length(data) > 0) {
+ * print("Received: ", data, "\n");
+ * }
+ * }
+ * }, uloop.ULOOP_READ);
+ *
+ * uloop.run();
+ */
+static uc_value_t *
+uc_fs_read_nb(uc_vm_t *vm, size_t nargs)
+{
+ uc_value_t *limit_val = uc_fn_arg(0);
+ FILE **fp = uc_fn_this("fs.file");
+ char *buf = NULL;
+ ssize_t n_read;
+ size_t limit = 4096;
+ int fd;
+
+ if (!fp || !*fp)
+ err_return(EBADF);
+
+ if (limit_val) {
+ int64_t limit_arg;
+
+ if (ucv_type(limit_val) != UC_INTEGER)
+ err_return(EINVAL);
+
+ limit_arg = ucv_int64_get(limit_val);
+
+ if (limit_arg <= 0)
+ return NULL;
+
+ limit = (size_t)limit_arg;
+ }
+
+ fd = fileno(*fp);
+
+ if (fd == -1)
+ err_return(errno);
+
+ buf = malloc(limit);
+
+ if (!buf)
+ err_return(ENOMEM);
+
+ while (true) {
+ n_read = read(fd, buf, limit);
+
+ if (n_read >= 0)
+ break;
+
+ if (errno == EINTR)
+ continue;
+
+ if (errno == EAGAIN || errno == EWOULDBLOCK) {
+ free(buf);
+ return ucv_string_new_length("", 0);
+ }
+
+ free(buf);
+ err_return(errno);
+ }
+
+ uc_value_t *rv = ucv_string_new_length(buf, (size_t)n_read);
+ free(buf);
+
+ return rv;
+}
+
+/**
* Writes a chunk of data to the file handle.
*
* In case the given data is not a string, it is converted to a string before
@@ -2910,6 +3016,7 @@ static const uc_function_list_t proc_fns
static const uc_function_list_t file_fns[] = {
{ "read", uc_fs_read },
+ { "read_nb", uc_fs_read_nb },
{ "write", uc_fs_write },
{ "seek", uc_fs_seek },
{ "tell", uc_fs_tell },