All checks were successful
CI Code / Check spelling (pull_request) Successful in 17s
CI Code / Check coding style (pull_request) Successful in 32s
CI Code / Code Coverage (pull_request) Successful in 3m36s
CI Code / Linux (debian) (pull_request) Successful in 4m45s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m56s
CI Code / Linux (arch) (pull_request) Successful in 6m39s
Main loop now calls a single helper for cont/suspend/Ctrl-Z state instead of inlining the signal-handling block. Comment blocks in profanity.c / editor.c / inputwin.c shortened to trailing form.
206 lines
6.2 KiB
C
206 lines
6.2 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"
|
|
|
|
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);
|
|
|
|
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(ctx->filename);
|
|
g_free(ctx);
|
|
return TRUE;
|
|
} else if (pid == 0) {
|
|
// Child process: Inherits TTY from parent
|
|
|
|
// 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);
|
|
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));
|
|
}
|