diff --git a/package/utils/ucode/Makefile b/package/utils/ucode/Makefile index eb6abd6adb6..bc266b3e0db 100644 --- a/package/utils/ucode/Makefile +++ b/package/utils/ucode/Makefile @@ -11,14 +11,14 @@ PKG_NAME:=ucode PKG_RELEASE:=1 PKG_SOURCE_PROTO:=git -PKG_SOURCE_URL=https://github.com/jow-/ucode.git -PKG_SOURCE_DATE:=2026-07-09 -PKG_SOURCE_VERSION:=b885dd0fe1e974551fb1233ca5f3aa74d80b74d9 -PKG_MIRROR_HASH:=654ba9dc8714aa8caf75ef422962fb465c2b21c8fbe4518a47f9de391cce6e71 +PKG_SOURCE_URL=https://github.com/ucode-lang/ucode.git +PKG_SOURCE_DATE:=2026-10-18 +PKG_SOURCE_VERSION:=d2e8b8d2052778557ccca7e781d2ad423324961b +PKG_MIRROR_HASH:=d5a9c35c97abe4aca04f5fa970a5270d932066571ed684bb22094301978b8d58 PKG_MAINTAINER:=Jo-Philipp Wich PKG_LICENSE:=ISC -PKG_ABI_VERSION:=20230711 +PKG_ABI_VERSION:=20261009 PKG_BUILD_DEPENDS:=libmd HOST_BUILD_DEPENDS:=libjson-c/host @@ -54,7 +54,10 @@ CMAKE_HOST_OPTIONS += \ -DDEBUG_SUPPORT=ON \ -DLOG_SUPPORT=OFF \ -DZLIB_SUPPORT=ON \ - -DDIGEST_SUPPORT=OFF + -DDIGEST_SUPPORT=OFF \ + -DFFI_SUPPORT=OFF \ + -DNETADDR_SUPPORT=OFF \ + -DSERIAL_SUPPORT=OFF define Package/ucode/default @@ -137,6 +140,10 @@ $(eval $(call UcodeModule, \ debug, DEBUG_SUPPORT, +libubox +libucode, \ The debug plugin module provides runtime debugging and introspection facilities.)) +$(eval $(call UcodeModule, \ + ffi, FFI_SUPPORT, +libffi, \ + The ffi plugin module allows ucode scripts to call C functions in shared libraries through libffi.)) + $(eval $(call UcodeModule, \ fs, FS_SUPPORT, , \ The filesystem plugin module allows interaction with the local file system.)) @@ -149,6 +156,10 @@ $(eval $(call UcodeModule, \ math, MATH_SUPPORT, , \ The math plugin provides access to various procedures.)) +$(eval $(call UcodeModule, \ + netaddr, NETADDR_SUPPORT, , \ + The netaddr plugin provides IPv4, IPv6 and MAC address parsing and calculation.)) + $(eval $(call UcodeModule, \ nl80211, NL80211_SUPPORT, +libnl-tiny +libubox, \ The nl80211 plugin provides access to the Linux wireless 802.11 netlink API.)) @@ -161,6 +172,10 @@ $(eval $(call UcodeModule, \ rtnl, RTNL_SUPPORT, +libnl-tiny +libubox, \ The rtnl plugin provides access to the Linux routing netlink API.)) +$(eval $(call UcodeModule, \ + serial, SERIAL_SUPPORT, , \ + The serial plugin provides termios, modem control line and serial port configuration access.)) + $(eval $(call UcodeModule, \ socket, SOCKET_SUPPORT, , \ The socket plugin provides access to IPv4, IPv6, Unix and packet socket APIs.)) diff --git a/package/utils/ucode/patches/100-rtnl-return-true-for-an-acknowledgement-without-data.patch b/package/utils/ucode/patches/100-rtnl-return-true-for-an-acknowledgement-without-data.patch deleted file mode 100644 index 3963ec34a11..00000000000 --- a/package/utils/ucode/patches/100-rtnl-return-true-for-an-acknowledgement-without-data.patch +++ /dev/null @@ -1,64 +0,0 @@ -From: Felix Fietkau -Date: Wed, 30 Sep 2026 18:08:32 +0200 -Subject: [PATCH] rtnl: return true for an acknowledgement without data - -request() treats an ACK as the end of a reply and returns the collected -data. For a command that the kernel answers with a bare ACK, such as -RTM_SETLINK, that data is null, so a caller that tests the result for -truth sees a failure. - -Return true if an ACK ends a request without reply data. A dump that -ends without entries still returns null, and a refused request still -returns false. - -Document these results in the description of request(). - -Signed-off-by: Felix Fietkau ---- - ---- a/lib/rtnl.c -+++ b/lib/rtnl.c -@@ -3350,6 +3350,19 @@ cb_done(struct nl_msg *msg, void *arg) - } - - static int -+cb_done_ack(struct nl_msg *msg, void *arg) -+{ -+ request_state_t *s = arg; -+ -+ if (s->state == STATE_UNREPLIED) -+ s->res = ucv_boolean_new(true); -+ -+ s->state = STATE_REPLIED; -+ -+ return NL_STOP; -+} -+ -+static int - cb_error(struct sockaddr_nl *nla, struct nlmsgerr *err, void *arg) - { - request_state_t *s = arg; -@@ -3433,7 +3446,13 @@ static const struct { - * @param {number} flags - The netlink flags for the request - * @param {*} payload - The payload data for the request - * -- * @returns {?*} - The response data or null on error -+ * @returns {?(Object|Object[]|boolean)} - The response data, true for a -+ * successful acknowledgement without -+ * data, false when the kernel refuses -+ * the request or the reply cannot be -+ * received, or null for a dump -+ * without entries and when the -+ * request cannot be sent - * - * @example - * // Send a route request -@@ -3532,7 +3551,7 @@ uc_nl_request(uc_vm_t *vm, size_t nargs) - - nl_cb_set(cb, NL_CB_VALID, NL_CB_CUSTOM, cb_reply, &st); - nl_cb_set(cb, NL_CB_FINISH, NL_CB_CUSTOM, cb_done, &st); -- nl_cb_set(cb, NL_CB_ACK, NL_CB_CUSTOM, cb_done, &st); -+ nl_cb_set(cb, NL_CB_ACK, NL_CB_CUSTOM, cb_done_ack, &st); - nl_cb_err(cb, NL_CB_CUSTOM, cb_error, &st); - - nl_send_auto_complete(sock, msg); diff --git a/package/utils/ucode/patches/100-uloop-do-not-pass-exit-requests-to-the-exception-gua.patch b/package/utils/ucode/patches/100-uloop-do-not-pass-exit-requests-to-the-exception-gua.patch new file mode 100644 index 00000000000..87708075571 --- /dev/null +++ b/package/utils/ucode/patches/100-uloop-do-not-pass-exit-requests-to-the-exception-gua.patch @@ -0,0 +1,69 @@ +From: Felix Fietkau +Date: Thu, 15 Oct 2026 14:27:10 +0200 +Subject: [PATCH] uloop: do not pass exit() requests to the exception guard + +Since the exception guard was added, uc_uloop_vm_call() hands every +exception of a callback to the guard handler, also the EXCEPTION_EXIT +that exit() raises. The guard receives an "Exit" exception and the loop +keeps running. When run() returns, the script ends with code 0 instead +of the code given to exit(), because the VM already reset its call +frames for the exit request. + +End the loop and keep the exit request pending, as without a guard, so +that the script terminates with the requested code. + +Fixes: aaf712eb4fb4 ("uloop: add guard() function") +Signed-off-by: Felix Fietkau +--- + create mode 100644 tests/custom/06_lib_uloop/12_callback_exit + +--- a/lib/uloop.c ++++ b/lib/uloop.c +@@ -149,9 +149,17 @@ uc_uloop_vm_call(uc_vm_t *vm, bool mcall + { + uc_value_t *exh, *val; + +- if (uc_vm_call(vm, mcall, nargs) == EXCEPTION_NONE) ++ switch (uc_vm_call(vm, mcall, nargs)) { ++ case EXCEPTION_NONE: + return true; + ++ case EXCEPTION_EXIT: ++ goto error; ++ ++ default: ++ break; ++ } ++ + exh = uc_vm_registry_get(vm, "uloop.ex_handler"); + if (!ucv_is_callable(exh)) + goto error; +--- /dev/null ++++ b/tests/custom/06_lib_uloop/12_callback_exit +@@ -0,0 +1,26 @@ ++Calling exit() in a uloop callback ends the loop and terminates the script ++with the given code, also when an exception guard is set. The guard does not ++receive the exit request. ++ ++-- Testcase -- ++{% ++ import * as uloop from 'uloop'; ++ ++ uloop.init(); ++ ++ uloop.guard((e) => print(`guard: ${e.type}\n`)); ++ ++ uloop.timer(10, () => exit(4)); ++ uloop.timer(100, () => { ++ print("loop kept running\n"); ++ uloop.end(); ++ }); ++ ++ uloop.run(); ++ print("run() returned\n"); ++%} ++-- End -- ++ ++-- Expect exitcode -- ++4 ++-- End -- diff --git a/package/utils/ucode/patches/101-uloop-allow-a-function-to-prepare-the-child-in-proce.patch b/package/utils/ucode/patches/101-uloop-allow-a-function-to-prepare-the-child-in-proce.patch new file mode 100644 index 00000000000..3379816cd8e --- /dev/null +++ b/package/utils/ucode/patches/101-uloop-allow-a-function-to-prepare-the-child-in-proce.patch @@ -0,0 +1,354 @@ +From: Felix Fietkau +Date: Thu, 15 Oct 2026 14:29:11 +0200 +Subject: [PATCH] uloop: allow a function to prepare the child in process() + +A program started by uloop.process() can need preparatory work in the +child before exec(), for example to redirect a file descriptor. + +Accept a function instead of the executable path. The function runs in +the child after fork(), receives the argument vector and the +environment, and returns the complete argument vector to execute. +Changes to the environment object apply to the executed program. +exit() in the function sets the exit code of the child. If the function +throws an exception or does not return a non-empty array, the child +exits with code 255, the same as for a failed exec(). + +Release the event loop in the child before the function runs, like +uloop.task() does. The child otherwise shares the signal pipe of the +parent, so a signal sent to the child runs signal callbacks of the +parent, and SIGTERM does not end the child. + +Flush the output of the parent before fork(), since a flush in the child +writes the inherited buffer a second time. + +Signed-off-by: Felix Fietkau +--- + create mode 100644 tests/custom/06_lib_uloop/13_process_function + +--- a/lib/uloop.c ++++ b/lib/uloop.c +@@ -1184,6 +1184,93 @@ uc_uloop_process_cb(struct uloop_process + ucv_put(obj); + } + ++static char ** ++uc_uloop_process_argv(uc_vm_t *vm, uc_value_t *arg0, uc_value_t *list, ++ size_t first) ++{ ++ size_t len = ucv_array_length(list); ++ size_t count = len > first ? len - first : 0; ++ char **argp = calloc(count + 2, sizeof(char *)); ++ size_t i; ++ ++ if (!argp) ++ return NULL; ++ ++ argp[0] = ucv_to_string(vm, arg0); ++ ++ for (i = 0; i < count; i++) ++ argp[i + 1] = ucv_to_string(vm, ucv_array_get(list, first + i)); ++ ++ return argp; ++} ++ ++static char ** ++uc_uloop_process_envp(uc_vm_t *vm, uc_value_t *env_arg) ++{ ++ uc_stringbuf_t *buf; ++ char **envp; ++ size_t i = 0; ++ ++ if (!env_arg) ++ return environ; ++ ++ envp = calloc(ucv_object_length(env_arg) + 1, sizeof(char *)); ++ ++ if (!envp) ++ return NULL; ++ ++ ucv_object_foreach(env_arg, envk, envv) { ++ buf = xprintbuf_new(); ++ ++ ucv_stringbuf_printf(buf, "%s=", envk); ++ ucv_to_stringbuf(vm, buf, envv, false); ++ ++ envp[i++] = buf->buf; ++ ++ free(buf); ++ } ++ ++ return envp; ++} ++ ++static void ++uc_uloop_process_exec(uc_vm_t *vm, uc_value_t *executable, ++ uc_value_t *arguments, uc_value_t *env_arg) ++{ ++ uc_value_t *argv; ++ char **argp, **envp; ++ ++ uloop_done(); ++ ++ if (ucv_is_callable(executable)) { ++ uc_vm_stack_push(vm, ucv_get(executable)); ++ uc_vm_stack_push(vm, ucv_get(arguments)); ++ uc_vm_stack_push(vm, ucv_get(env_arg)); ++ ++ if (!uc_uloop_vm_call(vm, false, 2)) ++ _exit(vm->exception.type == EXCEPTION_EXIT ? vm->arg.s32 : -1); ++ ++ argv = uc_vm_stack_pop(vm); ++ ++ if (ucv_type(argv) != UC_ARRAY || !ucv_array_length(argv)) ++ _exit(-1); ++ ++ argp = uc_uloop_process_argv(vm, ucv_array_get(argv, 0), argv, 1); ++ } ++ else { ++ argp = uc_uloop_process_argv(vm, executable, arguments, 0); ++ } ++ ++ envp = uc_uloop_process_envp(vm, env_arg); ++ ++ if (!argp || !envp) ++ _exit(-1); ++ ++ execvpe(argp[0], (char * const *)argp, (char * const *)envp); ++ ++ _exit(-1); ++} ++ + /** + * Creates a process instance for executing external programs. + * +@@ -1192,14 +1279,29 @@ uc_uloop_process_cb(struct uloop_process + * vector, an optional dictionary describing environment variables, and a + * callback function to be invoked when the invoked process ends. + * ++ * When a function is given instead of the executable path, it is invoked in ++ * the child process after fork(), receiving the argument vector and the ++ * environment dictionary as parameters. It may perform any preparatory work, ++ * such as setting up file descriptors, and must return the complete argument ++ * vector to execute, with the executable path as first element. Changes made ++ * to the environment dictionary by the function apply to the environment of ++ * the executed program; the executable is searched in the `PATH` of the ++ * calling process. If the function calls `exit()`, the child process exits ++ * with the given code. If it throws an exception or does not return a ++ * non-empty array, the child process exits with code 255. ++ * ++ * The function runs outside of the event loop: signals received by the child ++ * process do not invoke the signal callbacks of the event loop. ++ * + * @function module:uloop#process + * +- * @param {string} executable +- * The path to the executable program. ++ * @param {string|Function} executable ++ * The path to the executable program, or a function returning the argument ++ * vector to execute. + * + * @param {string[]} [args] + * Optional. An array of strings representing the arguments passed to the +- * executable. ++ * executable, or to the function. + * + * @param {Object} [env] + * Optional. A dictionary describing environment variables for the process. +@@ -1211,13 +1313,26 @@ uc_uloop_process_cb(struct uloop_process + * + * @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. ++ * Returns `null` on invalid arguments or `fork()` failure. If the program ++ * cannot be executed, the callback receives the exit code 255. + * + * @example + * // Create a process instance for executing 'ls' command + * const myProcess = uloop.process("/bin/ls", ["-l", "/tmp"], null, (code) => { + * printf(`Process exited with code ${code}\n`); + * }); ++ * ++ * // Redirect stderr in the child process before executing 'ls' ++ * const myProcess = uloop.process((args, env) => { ++ * const fs = require('fs'); ++ * const errlog = fs.open('/tmp/error.log', 'w'); ++ * fs.dup2(errlog.fileno(), 2); ++ * errlog.close(); ++ * ++ * return ["/bin/ls", ...args]; ++ * }, ["-l", "/tmp"], null, (code) => { ++ * printf(`Process exited with code ${code}\n`); ++ * }); + */ + static uc_value_t * + uc_uloop_process(uc_vm_t *vm, size_t nargs) +@@ -1227,55 +1342,25 @@ uc_uloop_process(uc_vm_t *vm, size_t nar + uc_value_t *env_arg = uc_fn_arg(2); + uc_value_t *callback = uc_fn_arg(3); + uc_uloop_process_t *process; +- uc_stringbuf_t *buf; +- char **argp, **envp; + pid_t pid; +- size_t i; + +- if (ucv_type(executable) != UC_STRING || ++ if ((ucv_type(executable) != UC_STRING && !ucv_is_callable(executable)) || + (arguments && ucv_type(arguments) != UC_ARRAY) || + (env_arg && ucv_type(env_arg) != UC_OBJECT) || + !ucv_is_callable(callback)) { + err_return(EINVAL); + } + ++ /* the child may flush the inherited buffer again */ ++ fflush(vm->output); ++ + pid = fork(); + + if (pid == -1) + err_return(errno); + +- if (pid == 0) { +- argp = calloc(ucv_array_length(arguments) + 2, sizeof(char *)); +- envp = environ; +- +- if (env_arg) { +- envp = calloc(ucv_object_length(env_arg) + 1, sizeof(char *)); +- i = 0; +- ucv_object_foreach(env_arg, envk, envv) { +- buf = xprintbuf_new(); +- +- ucv_stringbuf_printf(buf, "%s=", envk); +- ucv_to_stringbuf(vm, buf, envv, false); +- +- envp[i++] = buf->buf; +- +- free(buf); +- } +- } +- +- if (!argp || !envp) +- _exit(-1); +- +- argp[0] = ucv_to_string(vm, executable); +- +- for (i = 0; i < ucv_array_length(arguments); i++) +- argp[i+1] = ucv_to_string(vm, ucv_array_get(arguments, i)); +- +- execvpe((const char *)ucv_string_get(executable), +- (char * const *)argp, (char * const *)envp); +- +- _exit(-1); +- } ++ if (pid == 0) ++ uc_uloop_process_exec(vm, executable, arguments, env_arg); + + process = uc_uloop_alloc(vm, "uloop.process", sizeof(*process), callback); + process->process.pid = pid; +--- /dev/null ++++ b/tests/custom/06_lib_uloop/13_process_function +@@ -0,0 +1,105 @@ ++When the first argument of process() is a function, it runs in the child ++process, receives the argument vector and the environment, and returns the ++argument vector to execute. Changes to the environment apply to the executed ++program. exit() in the function sets the exit code of the child. An exception ++or a return value that is not a non-empty array makes the child exit with code ++255. Buffered output of the parent is written once, and signals received by ++the child do not reach the signal callbacks of the parent. ++ ++-- Testcase -- ++{% ++ import * as uloop from 'uloop'; ++ import * as fs from 'fs'; ++ ++ uloop.init(); ++ ++ const tmpdir = fs.mkdtemp('/tmp/ucode-uloop-process-XXXXXX'); ++ const outfile = tmpdir + '/out'; ++ ++ function run(label, executable, args, env) { ++ let completed = false; ++ let child = uloop.process(executable, args, env, (status) => { ++ completed = true; ++ printf('%s: %d\n', label, status); ++ uloop.end(); ++ }); ++ ++ if (!child) ++ die('Unable to start child'); ++ ++ uloop.run(); ++ ++ if (!completed) ++ die('Missing process callback'); ++ } ++ ++ run('argv', (args, env) => { ++ let out = fs.open(outfile, 'w'); ++ fs.dup2(out.fileno(), 1); ++ out.close(); ++ ++ env.GREETING = 'changed'; ++ ++ return ['/bin/sh', '-c', 'echo "$0 $1 $GREETING"; exit 3', ...args]; ++ }, ['first', 'second'], { GREETING: 'initial' }); ++ ++ print(fs.readfile(outfile)); ++ fs.unlink(outfile); ++ fs.rmdir(tmpdir); ++ ++ run('exception', () => die('setup failed')); ++ run('null', () => null); ++ run('empty', () => []); ++ run('string', () => '/bin/true'); ++ run('exit', () => exit(3)); ++ ++ printf('buffered before fork\n'); ++ run('flush', () => { ++ fs.stdout.flush(); ++ return ['/bin/true']; ++ }); ++ ++ let usr1_count = 0; ++ let usr1 = uloop.signal('SIGUSR1', () => usr1_count++); ++ ++ function run_signal(signame) { ++ uloop.timer(1, () => { ++ let child = uloop.process(() => { ++ sleep(300); ++ return ['/bin/true']; ++ }, null, null, (status) => { ++ printf('%s: %d, parent callbacks: %d\n', signame, status, usr1_count); ++ uloop.end(); ++ }); ++ ++ uloop.timer(100, () => system(['kill', `-${signame}`, `${child.pid()}`])); ++ }); ++ ++ uloop.run(); ++ } ++ ++ run_signal('USR1'); ++ run_signal('TERM'); ++ ++ usr1.delete(); ++ ++ printf('invalid: %J\n', uloop.process(123, null, null, () => null)); ++ ++ uloop.done(); ++%} ++-- End -- ++ ++-- Expect stdout -- ++argv: 3 ++first second changed ++exception: 255 ++null: 255 ++empty: 255 ++string: 255 ++exit: 3 ++buffered before fork ++flush: 0 ++USR1: 0, parent callbacks: 0 ++TERM: -15, parent callbacks: 0 ++invalid: null ++-- End -- diff --git a/package/utils/ucode/patches/110-uloop-allow-reusing-the-existing-environment.patch b/package/utils/ucode/patches/110-uloop-allow-reusing-the-existing-environment.patch deleted file mode 100644 index 018410493dc..00000000000 --- a/package/utils/ucode/patches/110-uloop-allow-reusing-the-existing-environment.patch +++ /dev/null @@ -1,57 +0,0 @@ -From: Felix Fietkau -Date: Wed, 8 Oct 2025 19:11:46 +0200 -Subject: [PATCH] uloop: allow reusing the existing environment - -When passing null as environment argument, reuse existing environ. -This makes it possible to avoid having to duplicate and convert it -by passing the output of getenv(). - -Signed-off-by: Felix Fietkau ---- - ---- a/lib/uloop.c -+++ b/lib/uloop.c -@@ -1028,7 +1028,22 @@ uc_uloop_process(uc_vm_t *vm, size_t nar - - if (pid == 0) { - argp = calloc(ucv_array_length(arguments) + 2, sizeof(char *)); -- envp = calloc(ucv_object_length(env_arg) + 1, sizeof(char *)); -+ envp = environ; -+ -+ if (env_arg) { -+ envp = calloc(ucv_object_length(env_arg) + 1, sizeof(char *)); -+ i = 0; -+ ucv_object_foreach(env_arg, envk, envv) { -+ buf = xprintbuf_new(); -+ -+ ucv_stringbuf_printf(buf, "%s=", envk); -+ ucv_to_stringbuf(vm, buf, envv, false); -+ -+ envp[i++] = buf->buf; -+ -+ free(buf); -+ } -+ } - - if (!argp || !envp) - _exit(-1); -@@ -1038,19 +1053,6 @@ uc_uloop_process(uc_vm_t *vm, size_t nar - for (i = 0; i < ucv_array_length(arguments); i++) - argp[i+1] = ucv_to_string(vm, ucv_array_get(arguments, i)); - -- i = 0; -- -- ucv_object_foreach(env_arg, envk, envv) { -- buf = xprintbuf_new(); -- -- ucv_stringbuf_printf(buf, "%s=", envk); -- ucv_to_stringbuf(vm, buf, envv, false); -- -- envp[i++] = buf->buf; -- -- free(buf); -- } -- - execvpe((const char *)ucv_string_get(executable), - (char * const *)argp, (char * const *)envp); - 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 deleted file mode 100644 index e6accabe38a..00000000000 --- a/package/utils/ucode/patches/111-uloop-add-optional-setup-callback-to-process.patch +++ /dev/null @@ -1,85 +0,0 @@ -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 -@@ -973,8 +973,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 - * -@@ -991,6 +992,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. -@@ -1000,6 +1006,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) -@@ -1008,6 +1024,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; -@@ -1017,7 +1034,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); - } - -@@ -1027,6 +1045,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; -