Compare commits

..

2 Commits

Author SHA1 Message Date
a04a8948e1 fix(chatwin): only fire plugins_post_chat_message_display on incoming
All checks were successful
CI Code / Check coding style (pull_request) Successful in 34s
CI Code / Check spelling (pull_request) Successful in 22s
CI Code / Linux (debian) (pull_request) Successful in 4m38s
CI Code / Code Coverage (pull_request) Successful in 3m12s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m34s
CI Code / Linux (arch) (pull_request) Successful in 6m57s
CI Code / Check spelling (push) Successful in 17s
CI Code / Check coding style (push) Successful in 31s
CI Code / Code Coverage (push) Successful in 3m45s
CI Code / Linux (debian) (push) Successful in 4m38s
CI Code / Linux (ubuntu) (push) Successful in 4m49s
CI Code / Linux (arch) (push) Successful in 6m46s
The trigger was wired into outgoing / outgoing-carbon / history paths
too, which made plugins like sounds.py play the "new message" sound
when opening a chat (history loads), sending a message yourself, or
receiving a carbon of your own sent message from another device.

Restrict the call to chatwin_incoming_msg — matches the semantic of
"a remote party sent a chat message and it was displayed".
2026-06-01 16:31:14 +03:00
c1093db090 fix(editor): prevent UI redraw conflicts during editor suspend/resume
All checks were successful
CI Code / Check spelling (push) Successful in 16s
CI Code / Check coding style (push) Successful in 31s
CI Code / Code Coverage (push) Successful in 3m36s
CI Code / Linux (debian) (push) Successful in 4m43s
CI Code / Linux (ubuntu) (push) Successful in 4m54s
CI Code / Linux (arch) (push) Successful in 6m46s
Guard against re-entrant editor launches, abort editor on Ctrl-Z,
and centralize suspend-aware redraws through prof_doupdate() to prevent
terminal corruption when SIGTSTP is received while editor is active.

Removes SIGUSR1 editor escape in favor of standard pkill <editor> recovery.
2026-06-01 12:21:34 +00:00
8 changed files with 93 additions and 54 deletions

View File

@@ -2638,7 +2638,8 @@ static const struct cmd_t command_defs[] = {
CMD_DESC(
"Spawn external editor to edit message. "
"After editing the inputline may appear empty. Press enter to send the text anyways. "
"Use /executable to set your favourite editor." )
"Use /executable to set your favourite editor. "
"Ctrl-Z in the editor aborts the session and returns to Profanity." )
},
{ CMD_PREAMBLE("/correct-editor",

View File

@@ -20,7 +20,6 @@
#include <string.h>
#include <glib.h>
#include <glib-unix.h>
#include "profanity.h"
#include "common.h"
@@ -71,6 +70,38 @@ static void _connect_default(const char* const account);
pthread_mutex_t lock;
static gboolean force_quit = FALSE;
static volatile sig_atomic_t suspend_requested = FALSE;
static volatile sig_atomic_t cont_requested = FALSE;
static void
_handle_suspend_signals(void)
{
if (cont_requested) {
cont_requested = FALSE;
if (!editor_is_active()) {
clearok(stdscr, TRUE);
ui_resize();
}
}
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);
}
}
}
void
prof_run(gchar* log_level, gchar* account_name, gchar* config_file, gchar* log_file, gchar* theme_name, gchar** commands)
{
@@ -94,6 +125,7 @@ prof_run(gchar* log_level, gchar* account_name, gchar* config_file, gchar* log_f
int waittime = 0;
while (cont && !force_quit) {
log_stderr_handler();
_handle_suspend_signals();
session_check_autoaway();
line = commands ? *commands : inp_readline();
@@ -183,14 +215,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)
static void
sigtstp_handler(int sig)
{
editor_emergency_kill();
return G_SOURCE_CONTINUE;
suspend_requested = TRUE;
}
static void
sigcont_handler(int sig)
{
cont_requested = TRUE;
}
static void
@@ -200,11 +234,11 @@ _init(char* log_level, char* config_file, char* log_file, char* theme_name)
// ignore SIGPIPE
signal(SIGPIPE, SIG_IGN);
signal(SIGINT, SIG_IGN);
signal(SIGTSTP, SIG_IGN);
signal(SIGTSTP, sigtstp_handler);
signal(SIGCONT, sigcont_handler);
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);

View File

@@ -32,14 +32,7 @@ typedef struct EditorContext
void* user_data;
} EditorContext;
// Set TRUE for the lifetime of an editor session (from launch_editor up to
// _editor_exit_cb / fork-error cleanup). The async editor model lost the old
// 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; // re-entrancy guard
static pid_t editor_pid = 0;
static void
@@ -65,8 +58,10 @@ _editor_exit_cb(GPid pid, gint status, gpointer data)
cons_show_error("Could not read edited content: %s", error->message);
g_error_free(error);
}
} else {
cons_show_error("Editor exited with error status %d", WEXITSTATUS(status));
} else if (WIFEXITED(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) {
@@ -159,11 +154,10 @@ launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* dat
} else if (pid == 0) {
// Child process: Inherits TTY from parent
// 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.
// 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(SIGTSTP, SIG_DFL);
signal(SIGPIPE, SIG_DFL);
if (editor_argv && editor_argv[0]) {
@@ -185,8 +179,27 @@ editor_emergency_kill(void)
if (!editor_active || editor_pid <= 0) {
return;
}
log_warning("[Editor] emergency resume: sending SIGTERM to editor pid %d", editor_pid);
log_warning("[Editor] aborting editor pid %d", editor_pid);
kill(editor_pid, SIGCONT); // wake if STOPPED; no-op otherwise
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.
// _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));
}

View File

@@ -16,9 +16,13 @@
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);
void editor_emergency_kill(void); // SIGCONT+SIGTERM the editor child
// 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

View File

@@ -399,8 +399,6 @@ chatwin_outgoing_msg(ProfChatWin* chatwin, const char* const message, const char
win_print_outgoing((ProfWin*)chatwin, enc_char, id, replace_id, display_message);
}
plugins_post_chat_message_display(myjid->barejid, myjid->resourcepart, display_message);
// Save last id and message for LMC
// Note: if the same message is to be corrected several times, the id of the original message is used in each case
// https://xmpp.org/extensions/xep-0308.html#rules
@@ -434,7 +432,6 @@ chatwin_outgoing_carbon(ProfChatWin* chatwin, ProfMessage* message)
win_print_outgoing(window, enc_char, message->id, message->replace_id, message->plain);
plugins_post_chat_message_display(message->from_jid->barejid, message->from_jid->resourcepart, message->plain);
message->plain = old_plain;
int num = wins_get_num(window);
@@ -561,7 +558,6 @@ _chatwin_history(ProfChatWin* chatwin, const char* const contact_barejid)
if (plugin_msg)
msg->plain = plugin_msg;
win_print_history((ProfWin*)chatwin, msg);
plugins_post_chat_message_display(msg->from_jid->barejid, msg->from_jid->resourcepart, msg->plain);
msg->plain = old_plain;
curr = g_slist_next(curr);
}
@@ -601,7 +597,6 @@ chatwin_db_history(ProfChatWin* chatwin, const gchar* start_time, const gchar* e
} else {
win_print_history((ProfWin*)chatwin, msg);
}
plugins_post_chat_message_display(msg->from_jid->barejid, msg->from_jid->resourcepart, msg->plain);
msg->plain = old_plain;
curr = g_slist_next(curr);
}

View File

@@ -64,10 +64,8 @@ static Display* display;
static void _ui_draw_term_title(void);
// Skip drawing while an external editor is active (ui_suspended) or after the
// screen has been closed (endwin). The flag is authoritative and does not rely
// on isendwin() staying TRUE, which a stray doupdate() during the session could
// otherwise clear.
// Fail-safe gate: authoritative flag (doesn't depend on isendwin staying
// TRUE), plus the regular endwin check for shutdown.
static gboolean
_ui_redraw_suspended(void)
{
@@ -200,6 +198,7 @@ void
ui_resume(void)
{
ui_suspended = FALSE;
clearok(stdscr, TRUE); // ncurses can't see vim's screen clobber
refresh();
inp_resume();
}

View File

@@ -182,10 +182,8 @@ inp_readline(void)
{
// UI is suspended
if (is_suspended || isendwin()) {
// Mirror the normal-path unlock/sleep/lock so worker threads
// (http_upload / http_download / ai_client) can acquire `lock`
// during the editor session — otherwise the main thread holds
// it continuously and every in-flight transfer freezes.
// Mirror the normal-path unlock so workers aren't starved
// for the whole editor session.
pthread_mutex_unlock(&lock);
g_usleep(100000); // 100ms
pthread_mutex_lock(&lock);
@@ -322,9 +320,7 @@ inp_get_line(void)
while (!line) {
line = inp_readline();
ui_update();
// Let GLib sources (notably the editor child watch) dispatch so
// ui_resume can run if the prompt was raised mid-editor-session.
while (g_main_context_iteration(NULL, FALSE))
while (g_main_context_iteration(NULL, FALSE)) // non-blocking; bounded by ready sources
;
}
status_bar_clear_prompt();
@@ -354,9 +350,7 @@ inp_get_password(void)
while (!password) {
password = inp_readline();
ui_update();
// Let GLib sources (notably the editor child watch) dispatch so
// ui_resume can run if the prompt was raised mid-editor-session.
while (g_main_context_iteration(NULL, FALSE))
while (g_main_context_iteration(NULL, FALSE)) // non-blocking; bounded by ready sources
;
}
get_password = FALSE;

View File

@@ -43,9 +43,8 @@ void ui_load_colours(void);
void ui_update(void);
void ui_redraw(void);
void ui_resize(void);
// Centralised physical-paint primitive: flushes the virtual screen unless the
// UI is suspended (external editor active) or the screen has been closed. Use
// this instead of raw doupdate() in any path reachable while suspended.
// doupdate() wrapper that skips when the UI is suspended. Use this instead
// of raw doupdate() to avoid the per-site whitelist class of bug.
void prof_doupdate(void);
void ui_focus_win(ProfWin* window);
void ui_sigwinch_handler(int sig);