From 86d924eaac085bc7d55967067bb01ddd3dd19042 Mon Sep 17 00:00:00 2001 From: "jabber.developer2" Date: Fri, 29 May 2026 09:31:34 +0300 Subject: [PATCH] fix(editor): avoid Ctrl-Z hang on the editor child, add SIGUSR1 escape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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`. --- src/profanity.c | 12 ++++++++++++ src/tools/editor.c | 21 +++++++++++++++++++-- src/tools/editor.h | 5 +++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/profanity.c b/src/profanity.c index f841f158..9fccd4eb 100644 --- a/src/profanity.c +++ b/src/profanity.c @@ -20,6 +20,7 @@ #include #include +#include #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); diff --git a/src/tools/editor.c b/src/tools/editor.c index dc3bdeb8..7c58bddf 100644 --- a/src/tools/editor.c +++ b/src/tools/editor.c @@ -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. +} diff --git a/src/tools/editor.h b/src/tools/editor.h index a6ef0cef..74d05087 100644 --- a/src/tools/editor.h +++ b/src/tools/editor.h @@ -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