Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 134 additions & 41 deletions lib/uloop.c
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,17 @@ uc_uloop_vm_call(uc_vm_t *vm, bool mcall, size_t nargs)
{
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;
Expand Down Expand Up @@ -1176,6 +1184,93 @@ uc_uloop_process_cb(struct uloop_process *proc, int exitcode)
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.
*
Expand All @@ -1184,14 +1279,29 @@ uc_uloop_process_cb(struct uloop_process *proc, int exitcode)
* 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<string, *>} [env]
* Optional. A dictionary describing environment variables for the process.
Expand All @@ -1203,13 +1313,26 @@ uc_uloop_process_cb(struct uloop_process *proc, int exitcode)
*
* @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)
Expand All @@ -1219,55 +1342,25 @@ uc_uloop_process(uc_vm_t *vm, size_t nargs)
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;
Expand Down
26 changes: 26 additions & 0 deletions tests/custom/06_lib_uloop/12_callback_exit
Original file line number Diff line number Diff line change
@@ -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 --
133 changes: 133 additions & 0 deletions tests/custom/06_lib_uloop/13_process_function
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
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 SIGTERM ends the
child.

-- 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'];
});

uloop.timer(1, () => {
let child = uloop.process(() => {
sleep(300);
return ['/bin/true'];
}, null, null, (status) => {
printf('TERM: %d\n', status);
uloop.end();
});

uloop.timer(100, () => system(['kill', '-TERM', `${child.pid()}`]));
});

uloop.run();

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
TERM: -15
invalid: null
-- End --


Signals received by the child do not reach the signal callbacks of the
parent.

-- Testcase --
{%
import * as uloop from 'uloop';

if (!uloop.signal)
exit(77);

uloop.init();

let usr1_count = 0;
let usr1 = uloop.signal('SIGUSR1', () => usr1_count++);

uloop.timer(1, () => {
let child = uloop.process(() => {
sleep(300);
return ['/bin/true'];
}, null, null, (status) => {
printf('USR1: %d, parent callbacks: %d\n', status, usr1_count);
uloop.end();
});

uloop.timer(100, () => system(['kill', '-USR1', `${child.pid()}`]));
});

uloop.run();

usr1.delete();
uloop.done();
%}
-- End --

-- Expect stdout --
USR1: 0, parent callbacks: 0
-- End --
Loading