fix(editor): avoid Ctrl-Z hang on the editor child, add SIGUSR1 escape
All checks were successful
CI Code / Check spelling (pull_request) Successful in 18s
CI Code / Check coding style (pull_request) Successful in 32s
CI Code / Code Coverage (pull_request) Successful in 2m36s
CI Code / Linux (debian) (pull_request) Successful in 4m38s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m49s
CI Code / Linux (arch) (pull_request) Successful in 5m35s
All checks were successful
CI Code / Check spelling (pull_request) Successful in 18s
CI Code / Check coding style (pull_request) Successful in 32s
CI Code / Code Coverage (pull_request) Successful in 2m36s
CI Code / Linux (debian) (pull_request) Successful in 4m38s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m49s
CI Code / Linux (arch) (pull_request) Successful in 5m35s
The upstream signal reset in launch_editor set SIGTSTP back to SIG_DFL in
the child, letting Ctrl-Z (or vim's :stop) stop the editor. GLib's
g_child_watch uses waitpid(WNOHANG) without WUNTRACED, so a STOPPED
editor never wakes the watch and the app freezes until killed externally.
Other TUI chat clients carry this open problem for years (see
weechat/weechat#985, open since 2017); shells solve it with full job
control (setpgid + tcsetpgrp + WUNTRACED), which is heavy for our use
case.
Drop just the SIGTSTP reset. SIGINT and SIGPIPE are still set to SIG_DFL
— Neovim's libuv needs SIGINT-as-default to recognise an interactive
terminal (per upstream commit dd76f9087). SIGTSTP stays inherited as
SIG_IGN from profanity's _init, so Ctrl-Z and vim's :stop become no-ops
inside /editor. Losing vim's :stop during message composition is an
acceptable trade for not freezing the whole app.
As a universal escape hatch for any other "hung editor" scenario
(looping $EDITOR, corrupted tty, anything we cannot detect), install a
SIGUSR1 handler via g_unix_signal_add that sends SIGTERM to the editor
child via editor_emergency_kill(). The existing g_child_watch then
dispatches the normal exit callback, ui_resume runs, and the user is
back in profanity. Recovery from another terminal: `pkill -USR1
profanity`.
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
#include <string.h>
|
||||
|
||||
#include <glib.h>
|
||||
#include <glib-unix.h>
|
||||
|
||||
#include "profanity.h"
|
||||
#include "common.h"
|
||||
@@ -182,6 +183,16 @@ sigterm_handler(int sig)
|
||||
force_quit = TRUE;
|
||||
}
|
||||
|
||||
// Universal "I'm stuck in the editor" escape hatch. Dispatched in the main
|
||||
// loop by GLib (g_unix_signal_add), so it is safe to call into editor.c
|
||||
// directly. Trigger from another terminal: `pkill -USR1 profanity`.
|
||||
static gboolean
|
||||
_emergency_resume_handler(gpointer data)
|
||||
{
|
||||
editor_emergency_kill();
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
static void
|
||||
_init(char* log_level, char* config_file, char* log_file, char* theme_name)
|
||||
{
|
||||
@@ -193,6 +204,7 @@ _init(char* log_level, char* config_file, char* log_file, char* theme_name)
|
||||
signal(SIGWINCH, ui_sigwinch_handler);
|
||||
signal(SIGTERM, sigterm_handler);
|
||||
signal(SIGHUP, sigterm_handler);
|
||||
g_unix_signal_add(SIGUSR1, _emergency_resume_handler, NULL);
|
||||
if (pthread_mutex_init(&lock, NULL) != 0) {
|
||||
log_error("Mutex init failed");
|
||||
exit(1);
|
||||
|
||||
@@ -40,11 +40,13 @@ typedef struct EditorContext
|
||||
// editor that fights the first one for the terminal, then prematurely
|
||||
// ui_resume when either exits.
|
||||
static gboolean editor_active = FALSE;
|
||||
static pid_t editor_pid = 0;
|
||||
|
||||
static void
|
||||
_editor_exit_cb(GPid pid, gint status, gpointer data)
|
||||
{
|
||||
editor_active = FALSE;
|
||||
editor_pid = 0;
|
||||
|
||||
EditorContext* ctx = data;
|
||||
gchar* contents = NULL;
|
||||
@@ -157,9 +159,11 @@ launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* dat
|
||||
} else if (pid == 0) {
|
||||
// Child process: Inherits TTY from parent
|
||||
|
||||
// Reset signal handlers that profanity sets so the editor doesn't inherit them
|
||||
// SIGTSTP intentionally NOT reset: keeping SIG_IGN inherited via
|
||||
// fork+exec avoids hanging on a STOPPED editor (g_child_watch lacks
|
||||
// WUNTRACED). Cost: vim's :stop is a no-op in /editor. See
|
||||
// editor_emergency_kill for other hung-editor scenarios.
|
||||
signal(SIGINT, SIG_DFL);
|
||||
signal(SIGTSTP, SIG_DFL);
|
||||
signal(SIGPIPE, SIG_DFL);
|
||||
|
||||
if (editor_argv && editor_argv[0]) {
|
||||
@@ -169,7 +173,20 @@ launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* dat
|
||||
}
|
||||
|
||||
// Parent process: Watch the child asynchronously
|
||||
editor_pid = pid;
|
||||
g_child_watch_add((GPid)pid, _editor_exit_cb, ctx);
|
||||
g_strfreev(editor_argv);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
void
|
||||
editor_emergency_kill(void)
|
||||
{
|
||||
if (!editor_active || editor_pid <= 0) {
|
||||
return;
|
||||
}
|
||||
log_warning("[Editor] emergency resume: sending SIGTERM to editor pid %d", editor_pid);
|
||||
kill(editor_pid, SIGTERM);
|
||||
// g_child_watch dispatches _editor_exit_cb when the child exits, which
|
||||
// already restores the UI — no further action required here.
|
||||
}
|
||||
|
||||
@@ -16,4 +16,9 @@
|
||||
|
||||
gboolean launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* data), void* user_data);
|
||||
|
||||
// Universal escape hatch: if an editor session is active, send SIGTERM to the
|
||||
// child. The existing g_child_watch fires _editor_exit_cb on exit, which
|
||||
// already restores the UI. Wired to SIGUSR1 in profanity.c.
|
||||
void editor_emergency_kill(void);
|
||||
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user