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 index 0000000000..cbde295190 --- /dev/null +++ b/package/utils/ucode/patches/111-uloop-add-optional-setup-callback-to-process.patch @@ -0,0 +1,85 @@ +From: Felix Fietkau +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 +--- + +--- 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 index 0000000000..e3097f136c --- /dev/null +++ b/package/utils/ucode/patches/120-fs-add-dup2-function.patch @@ -0,0 +1,75 @@ +From: Felix Fietkau +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 +--- + +--- 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 index 0000000000..8566ad158b --- /dev/null +++ b/package/utils/ucode/patches/121-fs-add-read_nb-method-for-non-blocking-reads.patch @@ -0,0 +1,133 @@ +From: Felix Fietkau +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 +--- + +--- 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 },