/* * editor.c * vim: expandtab:ts=4:sts=4:sw=4 * * Copyright (C) 2022 - 2026 Michael Vetter * Copyright (C) 2022 MarcoPolo PasTonMolo * Copyright (C) 2022 Steffen Jaeckel * * SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception */ #include #include #include #include #include #include #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)); }