All checks were successful
CI Code / Check coding style (pull_request) Successful in 31s
CI Code / Check spelling (pull_request) Successful in 1m6s
CI Code / Linux (ubuntu) (pull_request) Successful in 9m59s
CI Code / Linux (debian) (pull_request) Successful in 10m16s
CI Code / Code Coverage (pull_request) Successful in 10m30s
CI Code / Linux (arch) (pull_request) Successful in 14m9s
The compose editor is spawned via fork+execvp and inherits profanity's environment, including LINES/COLUMNS. Those hold the terminal size captured when profanity started (set by readline/ncurses at init) and are never refreshed on resize. A curses-based editor (nano, vim, ...) honors LINES/COLUMNS over the real window, so it renders at the launch-time dimensions and leaves the rest of the window unused after the terminal has been resized. Build a copy of the environment without LINES/COLUMNS before forking and point the child's environ at it, so the editor's curses falls back to ioctl(TIOCGWINSZ) and tracks the current window. The env is assembled in the parent so the child only swaps the environ pointer, which is async-signal-safe -- unlike unsetenv() -- in the multithreaded fork->exec window. profanity itself is unaffected: it already reads the live size via ioctl in ui_resize(), and its own environment is intact.
228 lines
7.0 KiB
C
228 lines
7.0 KiB
C
/*
|
|
* editor.c
|
|
* vim: expandtab:ts=4:sts=4:sw=4
|
|
*
|
|
* Copyright (C) 2022 - 2026 Michael Vetter <jubalh@iodoru.org>
|
|
* Copyright (C) 2022 MarcoPolo PasTonMolo <marcopolopastonmolo@protonmail.com>
|
|
* Copyright (C) 2022 Steffen Jaeckel <jaeckel-floss@eyet-services.de>
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
|
|
*/
|
|
|
|
#include <glib.h>
|
|
#include <errno.h>
|
|
#include <stdio.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
#include <signal.h>
|
|
|
|
#include "config/files.h"
|
|
#include "config/preferences.h"
|
|
#include "editor.h"
|
|
#include "log.h"
|
|
#include "common.h"
|
|
#include "ncurses.h"
|
|
#include "ui/ui.h"
|
|
#include "xmpp/xmpp.h"
|
|
|
|
extern char** environ;
|
|
|
|
typedef struct EditorContext
|
|
{
|
|
gchar* filename;
|
|
void (*callback)(gchar* content, void* user_data);
|
|
void* user_data;
|
|
} EditorContext;
|
|
|
|
static gboolean editor_active = FALSE; // re-entrancy guard
|
|
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;
|
|
GError* error = NULL;
|
|
|
|
ui_resume();
|
|
ui_resize();
|
|
|
|
if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
|
|
if (g_file_get_contents(ctx->filename, &contents, NULL, &error)) {
|
|
g_strchomp(contents);
|
|
ctx->callback(contents, ctx->user_data);
|
|
g_free(contents);
|
|
} else {
|
|
log_error("[Editor] could not read from %s: %s", ctx->filename, error->message);
|
|
cons_show_error("Could not read edited content: %s", error->message);
|
|
g_error_free(error);
|
|
}
|
|
} 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) {
|
|
log_error("[Editor] error during file deletion of %s", ctx->filename);
|
|
}
|
|
|
|
g_free(ctx->filename);
|
|
g_free(ctx);
|
|
g_spawn_close_pid(pid);
|
|
}
|
|
|
|
// Returns a message via callback once the editor is closed.
|
|
// Returns true if an error occurred.
|
|
gboolean
|
|
launch_editor(gchar* initial_content, void (*callback)(gchar* content, void* data), void* user_data)
|
|
{
|
|
if (editor_active) {
|
|
cons_show_error("An editor session is already active.");
|
|
return TRUE;
|
|
}
|
|
|
|
auto_gchar gchar* filename = NULL;
|
|
auto_gerror GError* glib_error = NULL;
|
|
const char* jid = connection_get_barejid();
|
|
if (jid) {
|
|
filename = files_file_in_account_data_path(DIR_EDITOR, jid, "compose.md");
|
|
} else {
|
|
log_debug("[Editor] could not get JID");
|
|
auto_gchar gchar* data_dir = files_get_data_path(DIR_EDITOR);
|
|
if (!create_dir(data_dir)) {
|
|
cons_show_error("Could not create editor directory.");
|
|
return TRUE;
|
|
}
|
|
filename = g_strdup_printf("%s/compose.md", data_dir);
|
|
}
|
|
if (!filename) {
|
|
log_error("[Editor] something went wrong while creating compose file");
|
|
cons_show_error("Could not create compose file.");
|
|
return TRUE;
|
|
}
|
|
|
|
gsize messagelen = 0;
|
|
if (initial_content != NULL) {
|
|
messagelen = strlen(initial_content);
|
|
}
|
|
|
|
if (!g_file_set_contents(filename, initial_content, messagelen, &glib_error)) {
|
|
log_error("[Editor] could not write to %s: %s", filename, PROF_GERROR_MESSAGE(glib_error));
|
|
cons_show_error("Could not write to compose file: %s", PROF_GERROR_MESSAGE(glib_error));
|
|
return TRUE;
|
|
}
|
|
|
|
auto_gchar gchar* editor = prefs_get_string(PREF_COMPOSE_EDITOR);
|
|
gchar** editor_argv = NULL;
|
|
GError* error = NULL;
|
|
|
|
auto_gchar gchar* full_cmd = g_strdup_printf("%s %s", editor, filename);
|
|
if (!g_shell_parse_argv(full_cmd, NULL, &editor_argv, &error)) {
|
|
log_error("[Editor] Failed to parse editor command: %s", error->message);
|
|
cons_show_error("Failed to parse editor command: %s", error->message);
|
|
g_error_free(error);
|
|
return TRUE;
|
|
}
|
|
|
|
EditorContext* ctx = g_new0(EditorContext, 1);
|
|
ctx->filename = g_steal_pointer(&filename);
|
|
ctx->callback = callback;
|
|
ctx->user_data = user_data;
|
|
|
|
editor_active = TRUE;
|
|
ui_suspend();
|
|
|
|
// Force GLib to install its SIGCHLD handler before forking. Creating a
|
|
// GChildWatchSource for our own pid is the documented trigger; we never
|
|
// attach or dispatch it, just rely on the side effect.
|
|
GSource* sigchld_warmup = g_child_watch_source_new(getpid());
|
|
g_source_unref(sigchld_warmup);
|
|
|
|
// Build the editor's env without LINES/COLUMNS pre-fork, so the child only
|
|
// swaps environ (async-signal-safe) instead of calling unsetenv(). The
|
|
// editor's (n)curses then reads the live window via ioctl(TIOCGWINSZ).
|
|
gsize env_len = 0;
|
|
while (environ[env_len]) {
|
|
env_len++;
|
|
}
|
|
gchar** editor_env = g_new0(gchar*, env_len + 1);
|
|
gsize env_kept = 0;
|
|
for (gsize i = 0; i < env_len; i++) {
|
|
if (g_str_has_prefix(environ[i], "LINES=") || g_str_has_prefix(environ[i], "COLUMNS=")) {
|
|
continue;
|
|
}
|
|
editor_env[env_kept++] = environ[i];
|
|
}
|
|
|
|
pid_t pid = fork();
|
|
if (pid == -1) {
|
|
log_error("[Editor] Failed to fork: %s", strerror(errno));
|
|
editor_active = FALSE;
|
|
ui_resume();
|
|
ui_resize();
|
|
cons_show_error("Failed to start editor: %s", strerror(errno));
|
|
g_strfreev(editor_argv);
|
|
g_free(editor_env); // array only; strings are borrowed from environ
|
|
g_free(ctx->filename);
|
|
g_free(ctx);
|
|
return TRUE;
|
|
} else if (pid == 0) {
|
|
// Child process: Inherits TTY from parent
|
|
|
|
environ = editor_env; // live TIOCGWINSZ size, not the inherited LINES/COLUMNS
|
|
|
|
// 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]) {
|
|
execvp(editor_argv[0], editor_argv);
|
|
}
|
|
_exit(EXIT_FAILURE);
|
|
}
|
|
|
|
// Parent process: Watch the child asynchronously
|
|
editor_pid = pid;
|
|
g_child_watch_add((GPid)pid, _editor_exit_cb, ctx);
|
|
g_strfreev(editor_argv);
|
|
g_free(editor_env); // array only; strings are borrowed from environ
|
|
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));
|
|
}
|