diff --git a/src/modules/child_process.c b/src/modules/child_process.c index 1cd1bd6..45afe5e 100644 --- a/src/modules/child_process.c +++ b/src/modules/child_process.c @@ -1328,6 +1328,31 @@ static ant_value_t exec_file_promisified_call(ant_t *js, ant_value_t *args, int return promise; } +static ant_value_t exec_promisified_call(ant_t *js, ant_value_t *args, int nargs) { + ant_value_t original = js_get_slot(js_getcurrentfunc(js), SLOT_DATA); + if (!is_callable(original)) return js_mkerr(js, "exec promisify target is not callable"); + + ant_value_t call_result = sv_vm_call( + js->vm, js, original, js_getthis(js), + args, nargs, NULL, false + ); + + if (vtype(call_result) == T_PROMISE) return call_result; + if (is_err(call_result) || js->thrown_exists) { + ant_value_t promise = js_mkpromise(js); + ant_value_t ex = js->thrown_exists ? js->thrown_value : call_result; + js->thrown_exists = false; + js->thrown_value = js_mkundef(); + js->thrown_stack = js_mkundef(); + js_reject_promise(js, promise, ex); + return promise; + } + + ant_value_t promise = js_mkpromise(js); + js_resolve_promise(js, promise, call_result); + return promise; +} + static ant_value_t builtin_execFile(ant_t *js, ant_value_t *args, int nargs) { ant_value_t argv = js_mkundef(); ant_value_t options = js_mkundef(); @@ -1815,6 +1840,11 @@ ant_value_t child_process_library(ant_t *js) { ant_value_t exec_file_fn = js_heavy_mkfun(js, builtin_execFile, js_mkundef()); child_process_init_constructor(js); + js_set_symbol(js, exec_fn, + "nodejs.util.promisify.custom", + js_heavy_mkfun(js, exec_promisified_call, exec_fn) + ); + js_set_symbol(js, exec_file_fn, "nodejs.util.promisify.custom", js_heavy_mkfun(js, exec_file_promisified_call, exec_file_fn) diff --git a/src/modules/readline.c b/src/modules/readline.c index 48fa160..b356869 100644 --- a/src/modules/readline.c +++ b/src/modules/readline.c @@ -58,6 +58,7 @@ typedef struct rl_interface { ant_value_t completer; ant_value_t js_obj; char *prompt; + char *active_prompt; char *line_buffer; int line_pos; int line_len; @@ -94,6 +95,23 @@ static rl_interface_t *interfaces = NULL; static ant_value_t g_rl_async_iter_proto = 0; static ant_value_t g_rl_interface_proto = 0; +static const char *rl_render_prompt(const rl_interface_t *iface) { + if (!iface) return ""; + return iface->active_prompt ? iface->active_prompt : iface->prompt; +} + +static void rl_set_active_prompt(rl_interface_t *iface, const char *prompt) { + if (!iface) return; + free(iface->active_prompt); + iface->active_prompt = prompt ? strdup(prompt) : NULL; +} + +static void rl_clear_active_prompt(rl_interface_t *iface) { + if (!iface) return; + free(iface->active_prompt); + iface->active_prompt = NULL; +} + static void rl_history_init(rl_history_t *hist, int capacity) { hist->capacity = capacity > 0 ? capacity : DEFAULT_HISTORY_SIZE; hist->lines = calloc(hist->capacity, sizeof(char*)); @@ -207,7 +225,7 @@ static int get_terminal_cols(void) { } static void move_cursor_to_line_start(rl_interface_t *iface, int cols) { - int prompt_len = (int)strlen(iface->prompt); + int prompt_len = (int)strlen(rl_render_prompt(iface)); int cursor_cols = prompt_len + iface->line_pos; int cursor_row = cursor_cols / cols; if (cursor_cols > 0 && cursor_cols % cols == 0) cursor_row--; @@ -222,7 +240,7 @@ static void move_cursor_to_line_start(rl_interface_t *iface, int cols) { static void clear_line_display(rl_interface_t *iface) { int cols = get_terminal_cols(); - int prompt_len = (int)strlen(iface->prompt); + int prompt_len = (int)strlen(rl_render_prompt(iface)); int line_cols = prompt_len + iface->line_len; int current_rows = line_cols > 0 ? (line_cols - 1) / cols + 1 : 1; int rows = iface->last_render_rows > current_rows ? iface->last_render_rows : current_rows; @@ -243,12 +261,13 @@ static void clear_line_display(rl_interface_t *iface) { static void refresh_line(rl_interface_t *iface) { char buf[MAX_LINE_LENGTH + 256]; int cols = get_terminal_cols(); + const char *prompt = rl_render_prompt(iface); clear_line_display(iface); - snprintf(buf, sizeof(buf), "%s%s", iface->prompt, iface->line_buffer); + snprintf(buf, sizeof(buf), "%s%s", prompt, iface->line_buffer); write_output(iface, buf); - int prompt_len = (int)strlen(iface->prompt); + int prompt_len = (int)strlen(prompt); int end_cols = prompt_len + iface->line_len; int end_row = end_cols > 0 ? end_cols / cols : 0; int cursor_cols = prompt_len + iface->line_pos; @@ -311,7 +330,7 @@ static void handle_char_input(rl_interface_t *iface, char c) { if (iface->line_pos == iface->line_len) { int cols = get_terminal_cols(); - int prompt_len = (int)strlen(iface->prompt); + int prompt_len = (int)strlen(rl_render_prompt(iface)); int total_cols = prompt_len + iface->line_len; int rows = total_cols > 0 ? total_cols / cols + 1 : 1; if (rows > iface->last_render_rows) iface->last_render_rows = rows; @@ -476,6 +495,7 @@ static void process_line(ant_t *js, rl_interface_t *iface) { rl_history_add(&iface->history, line, iface->remove_history_duplicates); emit_history_event(js, iface); + rl_clear_active_prompt(iface); ant_value_t line_val = js_mkstr(js, line, strlen(line)); emit_event(js, iface, "line", &line_val, 1); @@ -662,13 +682,12 @@ static ant_value_t rl_interface_prompt(ant_t *js, ant_value_t *args, int nargs) iface->line_len = 0; } - write_output(iface, iface->prompt); + write_output(iface, rl_render_prompt(iface)); if (iface->line_len > 0) { write_output(iface, iface->line_buffer); } start_reading(iface); - return js_mkundef(); } @@ -824,7 +843,8 @@ static ant_value_t rl_interface_question_callback(ant_t *js, ant_value_t *args, return js_mkerr(js, "callback must be a function"); } - write_output(iface, query); + rl_set_active_prompt(iface, query); + write_output(iface, rl_render_prompt(iface)); if (!rl_add_listener(js, iface, "line", args[1], true)) return js_mkerr(js, "listener must be a function"); return js_mkundef(); @@ -843,7 +863,8 @@ static ant_value_t rl_interface_question_promise(ant_t *js, ant_value_t *args, i ant_value_t promise = js_mkpromise(js); - write_output(iface, query); + rl_set_active_prompt(iface, query); + write_output(iface, rl_render_prompt(iface)); iface->pending_question_resolve = js_get(js, promise, "_resolve"); iface->pending_question_reject = js_get(js, promise, "_reject"); @@ -862,7 +883,7 @@ static ant_value_t rl_interface_get_cursor_pos(ant_t *js, ant_value_t *args, int return result; } - int prompt_len = (int)strlen(iface->prompt); + int prompt_len = (int)strlen(rl_render_prompt(iface)); int total_cols = prompt_len + iface->line_pos; int cols = 80; @@ -901,6 +922,7 @@ static void free_interface(rl_interface_t *iface) { HASH_DEL(interfaces, iface); free(iface->prompt); + free(iface->active_prompt); free(iface->line_buffer); rl_history_free(&iface->history); free(iface); @@ -1202,6 +1224,7 @@ static ant_value_t rl_create_interface(ant_t *js, ant_value_t *args, int nargs) iface->id = next_interface_id++; iface->prompt = strdup(DEFAULT_PROMPT); + iface->active_prompt = NULL; iface->line_buffer = calloc(MAX_LINE_LENGTH, 1); iface->line_pos = 0; iface->line_len = 0; diff --git a/tests/fixtures/readline_question_prompt_child.cjs b/tests/fixtures/readline_question_prompt_child.cjs new file mode 100644 index 0000000..70a2979 --- /dev/null +++ b/tests/fixtures/readline_question_prompt_child.cjs @@ -0,0 +1,11 @@ +const readline = require('node:readline'); + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +rl.question('Q: ', answer => { + console.log(`ANSWER ${JSON.stringify(answer)}`); + rl.close(); +}); diff --git a/tests/test_child_process_exec_promisify.cjs b/tests/test_child_process_exec_promisify.cjs new file mode 100644 index 0000000..76019e0 --- /dev/null +++ b/tests/test_child_process_exec_promisify.cjs @@ -0,0 +1,34 @@ +const { promisify } = require('util'); +const { exec } = require('child_process'); + +async function main() { + if (typeof promisify.custom !== 'symbol') { + throw new Error('expected util.promisify.custom to be a symbol'); + } + + if (typeof exec[promisify.custom] !== 'function') { + throw new Error('expected exec to define util.promisify.custom'); + } + + const execAsync = promisify(exec); + const result = await execAsync('printf "OUT"; printf "ERR" >&2'); + + if (!result || typeof result !== 'object' || Array.isArray(result)) { + throw new Error(`expected object result, got ${Object.prototype.toString.call(result)}`); + } + + if (result.stdout !== 'OUT') { + throw new Error(`expected stdout to be OUT, got ${JSON.stringify(result.stdout)}`); + } + + if (result.stderr !== 'ERR') { + throw new Error(`expected stderr to be ERR, got ${JSON.stringify(result.stderr)}`); + } + + console.log('util.promisify(exec) resolves { stdout, stderr }'); +} + +main().catch((err) => { + console.error(err && err.stack ? err.stack : String(err)); + process.exit(1); +}); diff --git a/tests/test_readline_question_prompt.cjs b/tests/test_readline_question_prompt.cjs new file mode 100644 index 0000000..cccb338 --- /dev/null +++ b/tests/test_readline_question_prompt.cjs @@ -0,0 +1,106 @@ +const { spawnSync } = require('child_process'); +const path = require('path'); + +function fail(message) { + throw new Error(message); +} + +function runInPty() { + const helper = path.join(__dirname, 'fixtures', 'readline_question_prompt_child.cjs'); + const script = ` +import os, select, signal, sys, time + +exec_path, helper = sys.argv[1], sys.argv[2] +pid, master = os.forkpty() + +if pid == 0: + os.execv(exec_path, [exec_path, helper]) + +buf = bytearray() +sent = False +exit_code = None +deadline = time.time() + 7.0 + +while time.time() < deadline: + done, status = os.waitpid(pid, os.WNOHANG) + if done == pid: + exit_code = os.waitstatus_to_exitcode(status) + break + + r, _, _ = select.select([master], [], [], 0.1) + if master not in r: + continue + + try: + chunk = os.read(master, 4096) + except OSError: + break + + if not chunk: + break + + buf.extend(chunk) + if (not sent) and b'Q: ' in buf: + os.write(master, b'ab\\x7fc\\r') + sent = True + +if exit_code is None: + os.kill(pid, signal.SIGKILL) + _, status = os.waitpid(pid, 0) + exit_code = os.waitstatus_to_exitcode(status) + +while True: + r, _, _ = select.select([master], [], [], 0.05) + if master not in r: + break + try: + chunk = os.read(master, 4096) + except OSError: + break + if not chunk: + break + buf.extend(chunk) + +sys.stdout.buffer.write(bytes(buf)) +sys.exit(exit_code) +`; + + if (process.platform === 'win32') { + console.log('skipping readline question prompt tty test on win32'); + process.exit(0); + } + + return spawnSync('python3', ['-c', script, process.execPath, helper], { + encoding: 'utf8', + timeout: 9000, + }); +} + +const result = runInPty(); + +if (result.error && result.error.code === 'ENOENT') { + console.log('skipping readline question prompt tty test because `python3` is unavailable'); + process.exit(0); +} + +if (result.error) throw result.error; + +const output = `${result.stdout || ''}${result.stderr || ''}`; + +if (result.status !== 0) { + fail(`child exited ${result.status}\n${output}`); +} + +if (!output.includes('Q: ')) { + fail(`expected question prompt in output\n${output}`); +} + +if (!output.includes('ANSWER "ac"')) { + fail(`expected edited answer to round-trip\n${output}`); +} + +if (output.includes('> ')) { + fail(`default interface prompt leaked into question redraw\n${output}`); +} + +console.log('readline question keeps the active prompt during redraw');