ucode: add patches that make it easier to deal with non-blocking fds
authorFelix Fietkau <[email protected]>
Thu, 9 Oct 2025 07:56:10 +0000 (09:56 +0200)
committerFelix Fietkau <[email protected]>
Thu, 9 Oct 2025 07:57:30 +0000 (09:57 +0200)
This allows creating pipes for subprocesses to use as stdin/out/err
and polling them from a uloop process.

Signed-off-by: Felix Fietkau <[email protected]>
package/utils/ucode/patches/111-uloop-add-optional-setup-callback-to-process.patch [new file with mode: 0644]
package/utils/ucode/patches/120-fs-add-dup2-function.patch [new file with mode: 0644]
package/utils/ucode/patches/121-fs-add-read_nb-method-for-non-blocking-reads.patch [new file with mode: 0644]

diff --git a/package/utils/ucode/patches/111-uloop-add-optional-setup-callback-to-process.patch b/package/utils/ucode/patches/111-uloop-add-optional-setup-callback-to-process.patch
new file mode 100644 (file)
index 0000000..cbde295
--- /dev/null
@@ -0,0 +1,85 @@
+From: Felix Fietkau <[email protected]>
+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 <[email protected]>
+---
+
+--- 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;
diff --git a/package/utils/ucode/patches/120-fs-add-dup2-function.patch b/package/utils/ucode/patches/120-fs-add-dup2-function.patch
new file mode 100644 (file)
index 0000000..e3097f1
--- /dev/null
@@ -0,0 +1,75 @@
+From: Felix Fietkau <[email protected]>
+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 <[email protected]>
+---
+
+--- 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 },
diff --git a/package/utils/ucode/patches/121-fs-add-read_nb-method-for-non-blocking-reads.patch b/package/utils/ucode/patches/121-fs-add-read_nb-method-for-non-blocking-reads.patch
new file mode 100644 (file)
index 0000000..8566ad1
--- /dev/null
@@ -0,0 +1,133 @@
+From: Felix Fietkau <[email protected]>
+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 <[email protected]>
+---
+
+--- 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 },