From e7649fbd8feb36f291375a0e7bba1bd36d0f193a Mon Sep 17 00:00:00 2001 From: Philip Chimento Date: Sat, 18 Jan 2025 16:40:20 -0800 Subject: [PATCH] console: Use readline callback-based API Changes gjs_console_readline() to use the readline callback-based API and process events from the default main context in between character inputs. This makes the console REPL non-blocking; for example, you can enter setTimeout(() => console.log('now'), 1000) and you'll get the log message after 1 second. Previously you would get it only after the REPL exited. Closes: #76 --- meson.build | 4 ++- modules/console.cpp | 78 +++++++++++++++++++++++++++++++++++++-------- 2 files changed, 67 insertions(+), 15 deletions(-) diff --git a/meson.build b/meson.build index 26453da2e..d09bf2145 100644 --- a/meson.build +++ b/meson.build @@ -478,7 +478,9 @@ if cairo_xlib.found() endif if build_readline - libgjs_dependencies += readline_deps + gio_unix = dependency('gio-unix-2.0', version: glib_required_version, + fallback: ['glib', 'libgiounix_dep']) + libgjs_dependencies += [readline_deps, gio_unix] endif libgjs_cpp_args = ['-DGJS_COMPILATION'] + directory_defines diff --git a/modules/console.cpp b/modules/console.cpp index 72d32f82c..920aab230 100644 --- a/modules/console.cpp +++ b/modules/console.cpp @@ -27,6 +27,10 @@ #include #include // for g_fprintf +#ifdef HAVE_READLINE_READLINE_H +# include +#endif + #include #include #include // for JS_EncodeStringToUTF8 @@ -44,6 +48,7 @@ #include #include #include // for JS_NewPlainObject +#include #include "gjs/atoms.h" #include "gjs/auto.h" @@ -58,6 +63,8 @@ namespace mozilla { union Utf8Unit; } +using mozilla::Maybe, mozilla::Nothing, mozilla::Some; + static void gjs_console_warning_reporter(JSContext*, JSErrorReport* report) { JS::PrintError(stderr, report, /* reportWarnings = */ true); } @@ -142,27 +149,72 @@ class AutoCatchCtrlC { sigjmp_buf AutoCatchCtrlC::jump_buffer; #endif // HAVE_SIGNAL_H -[[nodiscard]] static bool gjs_console_readline(char** bufp, const char* prompt, - const char* repl_history_path - [[maybe_unused]]) { #ifdef HAVE_READLINE_READLINE_H - char *line; - line = readline(prompt); - if (!line) +// Readline only has a global handler anyway, so may as well use global data +static Maybe rl_async_line{}; +static bool rl_async_done = true; + +static gboolean on_stdin_ready(GPollableInputStream* pollable, void*) { + while (g_pollable_input_stream_is_readable(pollable)) + rl_callback_read_char(); + return TRUE; // don't remove source +} + +static void on_readline_complete(char* line) { + rl_async_line = line ? Some(line) : Nothing(); + rl_async_done = true; + // This needs to be called from the callback handler, otherwise an extra + // prompt is displayed. + rl_callback_handler_remove(); +} +#endif + +[[nodiscard]] +static bool gjs_console_readline(std::string* bufp, const char* prompt, + const char* repl_history_path + [[maybe_unused]]) { +#ifdef HAVE_READLINE_READLINE_H + g_assert(rl_async_done && "should not attempt two parallel readline calls"); + + rl_callback_handler_install(prompt, on_readline_complete); + rl_async_done = false; + + Gjs::AutoUnref stdin_stream{ + g_unix_input_stream_new(fileno(rl_instream), /* close = */ false)}; + Gjs::AutoUnref stdin_source{g_pollable_input_stream_create_source( + G_POLLABLE_INPUT_STREAM(stdin_stream.get()), nullptr)}; + g_source_set_callback(stdin_source, G_SOURCE_FUNC(on_stdin_ready), nullptr, + nullptr); + + Gjs::AutoPointer + main_context{g_main_context_ref_thread_default()}; + unsigned tag = g_source_attach(stdin_source, main_context); + stdin_source.release(); + + while (!rl_async_done) { + // Yield to other things while waiting for input + while (g_main_context_pending(main_context)) + g_main_context_iteration(main_context, /* may_block = */ false); + } + + g_source_remove(tag); + + if (!rl_async_line) return false; - if (line[0] != '\0') { - add_history(line); + + *bufp = rl_async_line.extract(); + + if ((*bufp)[0] != '\0') { + add_history(bufp->c_str()); gjs_console_write_repl_history(repl_history_path); } - - *bufp = line; #else // !HAVE_READLINE_READLINE_H char line[256]; fprintf(stdout, "%s", prompt); fflush(stdout); if (!fgets(line, sizeof line, stdin)) return false; - *bufp = g_strdup(line); + *bufp = line; #endif // !HAVE_READLINE_READLINE_H return true; } @@ -235,7 +287,6 @@ gjs_console_interact(JSContext *context, JS::CallArgs argv = JS::CallArgsFromVp(argc, vp); volatile bool eof, exit_warning; // accessed after setjmp() JS::RootedObject global{context, JS::CurrentGlobalOrNull(context)}; - char* temp_buf; volatile int lineno; // accessed after setjmp() volatile int startline; // accessed after setjmp() GjsContextPrivate* gjs = GjsContextPrivate::from_cx(context); @@ -251,7 +302,6 @@ gjs_console_interact(JSContext *context, // Separate initialization from declaration because of possible overwriting // when siglongjmp() jumps into this function eof = exit_warning = false; - temp_buf = nullptr; lineno = 1; do { /* @@ -286,6 +336,7 @@ gjs_console_interact(JSContext *context, } #endif // HAVE_SIGNAL_H + std::string temp_buf; if (!gjs_console_readline(&temp_buf, startline == lineno ? "gjs> " : ".... ", gjs->repl_history_path())) { @@ -294,7 +345,6 @@ gjs_console_interact(JSContext *context, } buffer += temp_buf; buffer += "\n"; - g_free(temp_buf); lineno++; } while (!JS_Utf8BufferIsCompilableUnit(context, global, buffer.c_str(), buffer.size())); -- GitLab