fix(editor): Ctrl-Z aborts editor and returns to profanity
Some checks failed
CI Code / Check spelling (pull_request) Successful in 16s
CI Code / Check coding style (pull_request) Failing after 34s
CI Code / Code Coverage (pull_request) Successful in 3m1s
CI Code / Linux (debian) (pull_request) Successful in 4m43s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m55s
CI Code / Linux (arch) (pull_request) Successful in 6m55s
Some checks failed
CI Code / Check spelling (pull_request) Successful in 16s
CI Code / Check coding style (pull_request) Failing after 34s
CI Code / Code Coverage (pull_request) Successful in 3m1s
CI Code / Linux (debian) (pull_request) Successful in 4m43s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m55s
CI Code / Linux (arch) (pull_request) Successful in 6m55s
Replaces the SIGTSTP=SIG_IGN inheritance approach (was commit 8) with a detect-and-abort design that matches user expectation: Ctrl-Z inside the editor brings you back to profanity instead of leaving the session suspended. - src/tools/editor.c: SIGTSTP reset back to SIG_DFL in the child so vim's :stop / Ctrl-Z actually stop the editor (revert previous SIG_IGN inheritance). - src/tools/editor.c: new editor_check_stopped() — non-reaping waitpid poll for WIFSTOPPED. Lets prof_run detect Ctrl-Z / :stop / gdb-attach on the editor child. - src/tools/editor.c: editor_emergency_kill() now sends SIGCONT before SIGTERM so the abort works on a STOPPED child too. - src/tools/editor.c: _editor_exit_cb handles WIFSIGNALED cleanly with "Editor session cancelled." instead of printing garbage via WEXITSTATUS on a signaled child. - src/profanity.c: SIGTSTP / SIGCONT signal handlers set flags consumed by the main loop. Editor STOPPED -> editor_emergency_kill (returns to the chat with the pre-alt+c readline buffer intact). Plain Ctrl-Z with no editor -> mutt-style drop the process group to the shell. SIGCONT -> refresh curses with clearok + ui_resize. While editor is active, swallow suspend_requested (race: vim's kill(0,SIGTSTP) beats waitpid seeing STOPPED — next poll catches it). - src/tools/editor.h: declare editor_check_stopped and editor_is_active. Verbose comment blocks across the branch tightened to one to two lines.
This commit is contained in:
@@ -20,6 +20,7 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include <glib.h>
|
#include <glib.h>
|
||||||
|
#include <glib-unix.h>
|
||||||
|
|
||||||
#include "profanity.h"
|
#include "profanity.h"
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
@@ -70,6 +71,12 @@ static void _connect_default(const char* const account);
|
|||||||
pthread_mutex_t lock;
|
pthread_mutex_t lock;
|
||||||
static gboolean force_quit = FALSE;
|
static gboolean force_quit = FALSE;
|
||||||
|
|
||||||
|
// Signal-driven flags consumed by the main loop. SIGTSTP and a STOPPED editor
|
||||||
|
// child → drop the process group to the shell (mutt-style). SIGCONT →
|
||||||
|
// refresh curses on return.
|
||||||
|
static volatile sig_atomic_t suspend_requested = FALSE;
|
||||||
|
static volatile sig_atomic_t cont_requested = FALSE;
|
||||||
|
|
||||||
void
|
void
|
||||||
prof_run(gchar* log_level, gchar* account_name, gchar* config_file, gchar* log_file, gchar* theme_name, gchar** commands)
|
prof_run(gchar* log_level, gchar* account_name, gchar* config_file, gchar* log_file, gchar* theme_name, gchar** commands)
|
||||||
{
|
{
|
||||||
@@ -93,6 +100,36 @@ prof_run(gchar* log_level, gchar* account_name, gchar* config_file, gchar* log_f
|
|||||||
int waittime = 0;
|
int waittime = 0;
|
||||||
while (cont && !force_quit) {
|
while (cont && !force_quit) {
|
||||||
log_stderr_handler();
|
log_stderr_handler();
|
||||||
|
|
||||||
|
// Resume from fg after SIGSTOP. Vim handles its own refresh.
|
||||||
|
if (cont_requested) {
|
||||||
|
cont_requested = FALSE;
|
||||||
|
if (!editor_is_active()) {
|
||||||
|
clearok(stdscr, TRUE);
|
||||||
|
ui_resize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Editor STOPPED → abort. Plain Ctrl-Z → drop to shell. While editor
|
||||||
|
// is active, swallow suspend_requested (race: vim's kill(0,SIGTSTP)
|
||||||
|
// beats waitpid seeing STOPPED) — next poll catches it.
|
||||||
|
if (editor_check_stopped()) {
|
||||||
|
log_info("[Editor] stopped; aborting back to profanity");
|
||||||
|
suspend_requested = FALSE;
|
||||||
|
editor_emergency_kill();
|
||||||
|
} else if (suspend_requested) {
|
||||||
|
suspend_requested = FALSE;
|
||||||
|
if (editor_is_active()) {
|
||||||
|
log_info("[Editor] SIGTSTP from vim group; deferring to next poll");
|
||||||
|
} else {
|
||||||
|
log_info("[Suspend] dropping to shell");
|
||||||
|
if (!isendwin()) {
|
||||||
|
endwin();
|
||||||
|
}
|
||||||
|
kill(0, SIGSTOP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
session_check_autoaway();
|
session_check_autoaway();
|
||||||
|
|
||||||
line = commands ? *commands : inp_readline();
|
line = commands ? *commands : inp_readline();
|
||||||
@@ -182,6 +219,27 @@ sigterm_handler(int sig)
|
|||||||
force_quit = TRUE;
|
force_quit = TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SIGUSR1 escape for an alive-but-stuck editor (WUNTRACED can't catch it).
|
||||||
|
// Dispatched in the main loop. Trigger: `pkill -USR1 profanity`.
|
||||||
|
static gboolean
|
||||||
|
_emergency_resume_handler(gpointer data)
|
||||||
|
{
|
||||||
|
editor_emergency_kill();
|
||||||
|
return G_SOURCE_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
sigtstp_handler(int sig)
|
||||||
|
{
|
||||||
|
suspend_requested = TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
sigcont_handler(int sig)
|
||||||
|
{
|
||||||
|
cont_requested = TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
_init(char* log_level, char* config_file, char* log_file, char* theme_name)
|
_init(char* log_level, char* config_file, char* log_file, char* theme_name)
|
||||||
{
|
{
|
||||||
@@ -189,10 +247,12 @@ _init(char* log_level, char* config_file, char* log_file, char* theme_name)
|
|||||||
// ignore SIGPIPE
|
// ignore SIGPIPE
|
||||||
signal(SIGPIPE, SIG_IGN);
|
signal(SIGPIPE, SIG_IGN);
|
||||||
signal(SIGINT, SIG_IGN);
|
signal(SIGINT, SIG_IGN);
|
||||||
signal(SIGTSTP, SIG_IGN);
|
signal(SIGTSTP, sigtstp_handler);
|
||||||
|
signal(SIGCONT, sigcont_handler);
|
||||||
signal(SIGWINCH, ui_sigwinch_handler);
|
signal(SIGWINCH, ui_sigwinch_handler);
|
||||||
signal(SIGTERM, sigterm_handler);
|
signal(SIGTERM, sigterm_handler);
|
||||||
signal(SIGHUP, sigterm_handler);
|
signal(SIGHUP, sigterm_handler);
|
||||||
|
g_unix_signal_add(SIGUSR1, _emergency_resume_handler, NULL);
|
||||||
if (pthread_mutex_init(&lock, NULL) != 0) {
|
if (pthread_mutex_init(&lock, NULL) != 0) {
|
||||||
log_error("Mutex init failed");
|
log_error("Mutex init failed");
|
||||||
exit(1);
|
exit(1);
|
||||||
|
|||||||
@@ -32,19 +32,16 @@ typedef struct EditorContext
|
|||||||
void* user_data;
|
void* user_data;
|
||||||
} EditorContext;
|
} EditorContext;
|
||||||
|
|
||||||
// Set TRUE for the lifetime of an editor session (from launch_editor up to
|
// Re-entrancy guard (lost in the async-editor rewrite). Prevents a plugin
|
||||||
// _editor_exit_cb / fork-error cleanup). The async editor model lost the old
|
// from spawning a second editor that would fight the first for the terminal.
|
||||||
// editor_task.active check from the pthread implementation; without this
|
|
||||||
// guard a second launch_editor (e.g. via a plugin dispatched through
|
|
||||||
// g_main_context_iteration) would call ui_suspend again and fork a second
|
|
||||||
// editor that fights the first one for the terminal, then prematurely
|
|
||||||
// ui_resume when either exits.
|
|
||||||
static gboolean editor_active = FALSE;
|
static gboolean editor_active = FALSE;
|
||||||
|
static pid_t editor_pid = 0;
|
||||||
|
|
||||||
static void
|
static void
|
||||||
_editor_exit_cb(GPid pid, gint status, gpointer data)
|
_editor_exit_cb(GPid pid, gint status, gpointer data)
|
||||||
{
|
{
|
||||||
editor_active = FALSE;
|
editor_active = FALSE;
|
||||||
|
editor_pid = 0;
|
||||||
|
|
||||||
EditorContext* ctx = data;
|
EditorContext* ctx = data;
|
||||||
gchar* contents = NULL;
|
gchar* contents = NULL;
|
||||||
@@ -63,8 +60,10 @@ _editor_exit_cb(GPid pid, gint status, gpointer data)
|
|||||||
cons_show_error("Could not read edited content: %s", error->message);
|
cons_show_error("Could not read edited content: %s", error->message);
|
||||||
g_error_free(error);
|
g_error_free(error);
|
||||||
}
|
}
|
||||||
} else {
|
} else if (WIFEXITED(status)) {
|
||||||
cons_show_error("Editor exited with error status %d", WEXITSTATUS(status));
|
cons_show_error("Editor exited with status %d", WEXITSTATUS(status));
|
||||||
|
} else if (WIFSIGNALED(status)) {
|
||||||
|
cons_show("Editor session cancelled.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remove(ctx->filename) != 0) {
|
if (remove(ctx->filename) != 0) {
|
||||||
@@ -157,7 +156,8 @@ launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* dat
|
|||||||
} else if (pid == 0) {
|
} else if (pid == 0) {
|
||||||
// Child process: Inherits TTY from parent
|
// Child process: Inherits TTY from parent
|
||||||
|
|
||||||
// Reset signal handlers that profanity sets so the editor doesn't inherit them
|
// SIGTSTP=SIG_DFL lets vim's :stop / Ctrl-Z work; profanity catches
|
||||||
|
// the STOPPED state via editor_check_stopped() and drops to the shell.
|
||||||
signal(SIGINT, SIG_DFL);
|
signal(SIGINT, SIG_DFL);
|
||||||
signal(SIGTSTP, SIG_DFL);
|
signal(SIGTSTP, SIG_DFL);
|
||||||
signal(SIGPIPE, SIG_DFL);
|
signal(SIGPIPE, SIG_DFL);
|
||||||
@@ -169,7 +169,39 @@ launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* dat
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parent process: Watch the child asynchronously
|
// Parent process: Watch the child asynchronously
|
||||||
|
editor_pid = pid;
|
||||||
g_child_watch_add((GPid)pid, _editor_exit_cb, ctx);
|
g_child_watch_add((GPid)pid, _editor_exit_cb, ctx);
|
||||||
g_strfreev(editor_argv);
|
g_strfreev(editor_argv);
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
editor_emergency_kill(void)
|
||||||
|
{
|
||||||
|
if (!editor_active || editor_pid <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log_warning("[Editor] aborting editor pid %d", editor_pid);
|
||||||
|
kill(editor_pid, SIGCONT); // wake if STOPPED; no-op otherwise
|
||||||
|
kill(editor_pid, SIGTERM);
|
||||||
|
// _editor_exit_cb will restore the UI when the child reaps.
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean
|
||||||
|
editor_is_active(void)
|
||||||
|
{
|
||||||
|
return editor_active;
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean
|
||||||
|
editor_check_stopped(void)
|
||||||
|
{
|
||||||
|
// Poll for STOPPED without reaping (g_child_watch handles exit via its
|
||||||
|
// own WNOHANG). Lets prof_run drop the group to the shell on Ctrl-Z.
|
||||||
|
if (!editor_active || editor_pid <= 0) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
int status;
|
||||||
|
pid_t r = waitpid(editor_pid, &status, WNOHANG | WUNTRACED);
|
||||||
|
return (r == editor_pid && WIFSTOPPED(status));
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,4 +16,15 @@
|
|||||||
|
|
||||||
gboolean launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* data), void* user_data);
|
gboolean launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* data), void* user_data);
|
||||||
|
|
||||||
|
// SIGUSR1 escape: SIGTERM the editor child. Use when WUNTRACED can't catch
|
||||||
|
// it (alive but stuck). Wired in profanity.c.
|
||||||
|
void editor_emergency_kill(void);
|
||||||
|
|
||||||
|
// TRUE if editor child is STOPPED (Ctrl-Z / :stop / gdb-attach). Does not
|
||||||
|
// reap; g_child_watch keeps handling exit.
|
||||||
|
gboolean editor_check_stopped(void);
|
||||||
|
|
||||||
|
// Editor-session flag accessor. Used by prof_run on SIGCONT resume.
|
||||||
|
gboolean editor_is_active(void);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -64,10 +64,8 @@ static Display* display;
|
|||||||
|
|
||||||
static void _ui_draw_term_title(void);
|
static void _ui_draw_term_title(void);
|
||||||
|
|
||||||
// Skip drawing while an external editor is active (ui_suspended) or after the
|
// Fail-safe gate: authoritative flag (doesn't depend on isendwin staying
|
||||||
// screen has been closed (endwin). The flag is authoritative and does not rely
|
// TRUE), plus the regular endwin check for shutdown.
|
||||||
// on isendwin() staying TRUE, which a stray doupdate() during the session could
|
|
||||||
// otherwise clear.
|
|
||||||
static gboolean
|
static gboolean
|
||||||
_ui_redraw_suspended(void)
|
_ui_redraw_suspended(void)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -182,10 +182,8 @@ inp_readline(void)
|
|||||||
{
|
{
|
||||||
// UI is suspended
|
// UI is suspended
|
||||||
if (is_suspended || isendwin()) {
|
if (is_suspended || isendwin()) {
|
||||||
// Mirror the normal-path unlock/sleep/lock so worker threads
|
// Mirror the normal-path unlock so workers aren't starved
|
||||||
// (http_upload / http_download / ai_client) can acquire `lock`
|
// for the whole editor session.
|
||||||
// during the editor session — otherwise the main thread holds
|
|
||||||
// it continuously and every in-flight transfer freezes.
|
|
||||||
pthread_mutex_unlock(&lock);
|
pthread_mutex_unlock(&lock);
|
||||||
g_usleep(100000); // 100ms
|
g_usleep(100000); // 100ms
|
||||||
pthread_mutex_lock(&lock);
|
pthread_mutex_lock(&lock);
|
||||||
@@ -322,8 +320,8 @@ inp_get_line(void)
|
|||||||
while (!line) {
|
while (!line) {
|
||||||
line = inp_readline();
|
line = inp_readline();
|
||||||
ui_update();
|
ui_update();
|
||||||
// Let GLib sources (notably the editor child watch) dispatch so
|
// Let the editor child watch dispatch — otherwise a prompt raised
|
||||||
// ui_resume can run if the prompt was raised mid-editor-session.
|
// mid-editor-session deadlocks here.
|
||||||
while (g_main_context_iteration(NULL, FALSE))
|
while (g_main_context_iteration(NULL, FALSE))
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
@@ -354,8 +352,8 @@ inp_get_password(void)
|
|||||||
while (!password) {
|
while (!password) {
|
||||||
password = inp_readline();
|
password = inp_readline();
|
||||||
ui_update();
|
ui_update();
|
||||||
// Let GLib sources (notably the editor child watch) dispatch so
|
// Let the editor child watch dispatch — otherwise a prompt raised
|
||||||
// ui_resume can run if the prompt was raised mid-editor-session.
|
// mid-editor-session deadlocks here.
|
||||||
while (g_main_context_iteration(NULL, FALSE))
|
while (g_main_context_iteration(NULL, FALSE))
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,9 +43,8 @@ void ui_load_colours(void);
|
|||||||
void ui_update(void);
|
void ui_update(void);
|
||||||
void ui_redraw(void);
|
void ui_redraw(void);
|
||||||
void ui_resize(void);
|
void ui_resize(void);
|
||||||
// Centralised physical-paint primitive: flushes the virtual screen unless the
|
// doupdate() wrapper that skips when the UI is suspended. Use this instead
|
||||||
// UI is suspended (external editor active) or the screen has been closed. Use
|
// of raw doupdate() to avoid the per-site whitelist class of bug.
|
||||||
// this instead of raw doupdate() in any path reachable while suspended.
|
|
||||||
void prof_doupdate(void);
|
void prof_doupdate(void);
|
||||||
void ui_focus_win(ProfWin* window);
|
void ui_focus_win(ProfWin* window);
|
||||||
void ui_sigwinch_handler(int sig);
|
void ui_sigwinch_handler(int sig);
|
||||||
|
|||||||
Reference in New Issue
Block a user