/* * Copyright (C) 1996-2000,2013 Michael R. Elkins * Copyright (C) 2020-2022 Kevin J. McCarthy * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #if HAVE_CONFIG_H # include "config.h" #endif #include "mutt.h" #include "mutt_menu.h" #include "send.h" #include "background.h" #include #include #include #include #include #include typedef struct background_process { pid_t pid; unsigned int finished; SEND_CONTEXT *sctx; struct background_process *next; } BACKGROUND_PROCESS; static BACKGROUND_PROCESS *ProcessList = NULL; static BACKGROUND_PROCESS *bg_process_new (pid_t pid, SEND_CONTEXT *sctx) { BACKGROUND_PROCESS *process; process = safe_calloc (1, sizeof(BACKGROUND_PROCESS)); process->pid = pid; process->sctx = sctx; return process; } static void bg_process_free (BACKGROUND_PROCESS **process) { if (!process || !*process) return; /* The SEND_CONTEXT is managed independently of the process. * Don't free it here */ FREE (process); /* __FREE_CHECKED__ */ } static void process_list_add (BACKGROUND_PROCESS *process) { process->next = ProcessList; ProcessList = process; BackgroundProcessCount++; } static void process_list_remove (BACKGROUND_PROCESS *process) { BACKGROUND_PROCESS *cur = ProcessList; BACKGROUND_PROCESS **plast = &ProcessList; while (cur) { if (cur == process) { *plast = cur->next; cur->next = NULL; BackgroundProcessCount--; break; } plast = &cur->next; cur = cur->next; } } int mutt_background_has_backgrounded (void) { return ProcessList ? 1 : 0; } /* Returns 0 if no processes were updated to finished. * 1 if one or more processes finished */ int mutt_background_process_waitpid (void) { BACKGROUND_PROCESS *process; pid_t pid; int has_finished = 0; if (!ProcessList) return 0; while ((pid = waitpid (-1, NULL, WNOHANG)) > 0) { process = ProcessList; while (process) { if (process->pid == pid) { process->finished = 1; has_finished = 1; break; } process = process->next; } } return has_finished; } static pid_t mutt_background_run (const char *cmd) { pid_t thepid; int fd; if (!cmd || !*cmd) return (0); /* must ignore SIGINT and SIGQUIT */ mutt_block_signals_system (); if ((thepid = fork ()) == 0) { /* give up controlling terminal */ setsid (); /* this ensures the child can't use stdin to take control of the * terminal */ #if defined(OPEN_MAX) for (fd = 0; fd < OPEN_MAX; fd++) close (fd); #elif defined(_POSIX_OPEN_MAX) for (fd = 0; fd < _POSIX_OPEN_MAX; fd++) close (fd); #else close (0); close (1); close (2); #endif mutt_unblock_signals_system (0); mutt_reset_child_signals (); execle (EXECSHELL, "sh", "-c", cmd, NULL, mutt_envlist ()); _exit (127); /* execl error */ } /* reset SIGINT, SIGQUIT and SIGCHLD */ mutt_unblock_signals_system (1); return (thepid); } static const struct mapping_t LandingHelp[] = { { N_("Exit"), OP_EXIT }, { N_("Redraw"), OP_REDRAW }, { N_("Help"), OP_HELP }, { NULL, 0 } }; /* Landing Page */ static void landing_redraw (MUTTMENU *menu) { int row, col; char key[SHORT_STRING]; BUFFER *messagebuf; size_t messagelen; menu_redraw (menu); if (MuttIndexWindow->rows < 2) return; messagebuf = mutt_buffer_pool_get (); /* L10N: Background Edit Landing Page message, first line. Displays while the editor is running. */ mutt_buffer_strcpy (messagebuf, _("Waiting for editor to exit")); messagelen = mutt_buffer_len (messagebuf); row = MuttIndexWindow->rows >= 10 ? 5 : 0; col = (MuttIndexWindow->cols > messagelen) ? ((MuttIndexWindow->cols - messagelen) / 2) : 0; mutt_window_mvaddstr (MuttIndexWindow, row, col, mutt_b2s (messagebuf)); *key = '\0'; if (!km_expand_key (key, sizeof(key), km_find_func (MENU_GENERIC, OP_EXIT))) strfcpy (key, "", sizeof(key)); /* L10N: Background Edit Landing Page message, second line. Displays while the editor is running. %s is the key binding for "", usually "q". */ mutt_buffer_printf (messagebuf, _("Type '%s' to background compose session."), key); messagelen = mutt_buffer_len (messagebuf); row = MuttIndexWindow->rows >= 10 ? 6 : 1; col = (MuttIndexWindow->cols > messagelen) ? ((MuttIndexWindow->cols - messagelen) / 2) : 0; mutt_window_mvaddstr (MuttIndexWindow, row, col, mutt_b2s (messagebuf)); mutt_buffer_pool_release (&messagebuf); mutt_curs_set (0); } /* Displays the "waiting for editor" page. * Returns: * 2 if the the menu is exited, leaving the process backgrounded * 0 when the waitpid() indicates the process has exited */ static int background_edit_landing_page (pid_t bg_pid) { int done = 0, rc = 0, op; short orig_timeout; pid_t wait_rc; MUTTMENU *menu; char helpstr[STRING]; menu = mutt_new_menu (MENU_GENERIC); menu->help = mutt_compile_help (helpstr, sizeof(helpstr), MENU_GENERIC, LandingHelp); menu->pagelen = 0; menu->title = _("Waiting for editor to exit"); mutt_push_current_menu (menu); /* Reduce timeout so we poll with bg_pid every second */ orig_timeout = Timeout; Timeout = 1; while (!done) { wait_rc = waitpid (bg_pid, NULL, WNOHANG); if ((wait_rc > 0) || ((wait_rc < 0) && (errno == ECHILD))) { rc = 0; break; } #if defined (USE_SLANG_CURSES) || defined (HAVE_RESIZETERM) if (SigWinch) { SigWinch = 0; mutt_resize_screen (); clearok (stdscr, TRUE); } #endif if (menu->redraw) landing_redraw (menu); op = km_dokey (MENU_GENERIC); switch (op) { case OP_HELP: mutt_help (MENU_GENERIC); menu->redraw = REDRAW_FULL; break; case OP_EXIT: rc = 2; done = 1; break; case OP_REDRAW: clearok (stdscr, TRUE); menu->redraw = REDRAW_FULL; break; } } Timeout = orig_timeout; mutt_pop_current_menu (menu); mutt_menuDestroy (&menu); return rc; } /* Runs editor in the background. * * After backgrounding the process, the background landing page will * be displayed. The user will have the opportunity to "quit" the * landing page, exiting back to the index. That will return 2 * (chosen for consistency with other backgrounding functions). * * If they leave the landing page up, it will detect when the editor finishes * and return 0, indicating the callers should continue processing * as if it were a foreground edit. * * Returns: * 2 - the edit was backgrounded * 0 - background edit completed. * -1 - an error occurred */ int mutt_background_edit_file (SEND_CONTEXT *sctx, const char *editor, const char *filename) { BUFFER *cmd; pid_t pid; int rc = -1; BACKGROUND_PROCESS *process; cmd = mutt_buffer_pool_get (); mutt_expand_file_fmt (cmd, editor, filename); pid = mutt_background_run (mutt_b2s (cmd)); if (pid <= 0) { mutt_error (_("Error running \"%s\"!"), mutt_b2s (cmd)); mutt_sleep (2); goto cleanup; } rc = background_edit_landing_page (pid); if (rc == 2) { process = bg_process_new (pid, sctx); process_list_add (process); } cleanup: mutt_buffer_pool_release (&cmd); return rc; } /* Background Compose Menu */ typedef struct entry { int num; BACKGROUND_PROCESS *process; } BG_ENTRY; static const struct mapping_t BgComposeHelp[] = { { N_("Exit"), OP_EXIT }, /* L10N: Background Compose Menu Help line: resume composing the mail */ { N_("Resume"), OP_GENERIC_SELECT_ENTRY }, { N_("Help"), OP_HELP }, { NULL, 0 } }; static const char *bg_format_str (char *dest, size_t destlen, size_t col, int cols, char op, const char *src, const char *fmt, const char *ifstring, const char *elsestring, void *data, format_flag flags) { BG_ENTRY *entry = (BG_ENTRY *)data; SEND_CONTEXT *sctx = entry->process->sctx; HEADER *hdr = sctx->msg; char tmp[SHORT_STRING]; char buf[LONG_STRING]; const char *msgid; int optional = (flags & MUTT_FORMAT_OPTIONAL); switch (op) { case 'i': msgid = sctx->cur_message_id; if (!msgid && sctx->tagged_message_ids) msgid = sctx->tagged_message_ids->data; if (!optional) mutt_format_s (dest, destlen, fmt, NONULL (msgid)); else if (!msgid) optional = 0; break; case 'n': snprintf (tmp, sizeof (tmp), "%%%sd", fmt); snprintf (dest, destlen, tmp, entry->num); break; case 'p': snprintf (tmp, sizeof (tmp), "%%%sd", fmt); snprintf (dest, destlen, tmp, entry->process->pid); break; case 'r': buf[0] = 0; rfc822_write_address(buf, sizeof(buf), hdr->env->to, 1); if (optional && buf[0] == '\0') optional = 0; mutt_format_s (dest, destlen, fmt, buf); break; case 'R': buf[0] = 0; rfc822_write_address(buf, sizeof(buf), hdr->env->cc, 1); if (optional && buf[0] == '\0') optional = 0; mutt_format_s (dest, destlen, fmt, buf); break; case 's': mutt_format_s (dest, destlen, fmt, NONULL (hdr->env->subject)); break; case 'S': if (!optional) { if (entry->process->finished) /* L10N: Background Compose menu flag that indicates the editor process has finished. */ mutt_format_s (dest, destlen, fmt, _("finished")); else /* L10N: Background Compose menu flag that indicates the editor process is still running. */ mutt_format_s (dest, destlen, fmt, _("running")); } else if (!entry->process->finished) optional = 0; break; } if (optional) mutt_FormatString (dest, destlen, col, cols, ifstring, bg_format_str, entry, flags); else if (flags & MUTT_FORMAT_OPTIONAL) mutt_FormatString (dest, destlen, col, cols, elsestring, bg_format_str, entry, flags); return (src); } static void make_bg_entry (char *s, size_t slen, MUTTMENU *m, int num) { BG_ENTRY *entry = &((BG_ENTRY *) m->data)[num]; mutt_FormatString (s, slen, 0, MuttIndexWindow->cols, NONULL (BackgroundFormat), bg_format_str, entry, MUTT_FORMAT_ARROWCURSOR); } static void update_bg_menu (MUTTMENU *menu) { if (SigChld) { SigChld = 0; if (mutt_background_process_waitpid ()) menu->redraw |= REDRAW_INDEX; } } static MUTTMENU *create_bg_menu (void) { MUTTMENU *menu = NULL; BACKGROUND_PROCESS *process; BG_ENTRY *entries = NULL; int num_entries = 0, i; char *helpstr; process = ProcessList; while (process) { num_entries++; process = process->next; } menu = mutt_new_menu (MENU_GENERIC); menu->make_entry = make_bg_entry; menu->custom_menu_update = update_bg_menu; /* L10N: Background Compose Menu title */ menu->title = _("Background Compose Menu"); helpstr = safe_malloc (STRING); menu->help = mutt_compile_help (helpstr, STRING, MENU_GENERIC, BgComposeHelp); menu->data = entries = safe_calloc (num_entries, sizeof(BG_ENTRY)); menu->max = num_entries; process = ProcessList; i = 0; while (process) { entries[i].num = i + 1; entries[i].process = process; process = process->next; i++; } mutt_push_current_menu (menu); return menu; } static void free_bg_menu (MUTTMENU **menu) { mutt_pop_current_menu (*menu); FREE (&(*menu)->data); FREE (&(*menu)->help); mutt_menuDestroy (menu); } void mutt_background_compose_menu (void) { MUTTMENU *menu; int done = 0, op; BG_ENTRY *entry; BACKGROUND_PROCESS *process; SEND_CONTEXT *sctx; char msg[SHORT_STRING]; if (!ProcessList) { /* L10N: Background Compose Menu: displayed if there are no background processes and the user tries to bring up the background compose menu */ mutt_message _("No backgrounded editing sessions."); return; } /* Force a rescan, just in case somehow the signal was missed. */ SigChld = 1; mutt_background_process_waitpid (); /* If there is only one process and it's finished, skip the menu */ if (!ProcessList->next && ProcessList->finished) { process = ProcessList; sctx = process->sctx; process_list_remove (process); bg_process_free (&process); mutt_send_message_resume (&sctx); return; } menu = create_bg_menu (); while (!done) { switch ((op = mutt_menuLoop (menu))) { case OP_EXIT: done = 1; break; case OP_GENERIC_SELECT_ENTRY: if (menu->data) { entry = (BG_ENTRY *)(menu->data) + menu->current; process = entry->process; sctx = process->sctx; if (!process->finished) { snprintf (msg, sizeof(msg), /* L10N: Background Compose menu: Confirms if an unfinished process is selected to continue. */ _("Process is still running. Really select?")); if (mutt_yesorno (msg, MUTT_NO) != MUTT_YES) break; mutt_message _("Waiting for editor to exit"); waitpid (process->pid, NULL, 0); mutt_clear_error (); } process_list_remove (process); bg_process_free (&process); mutt_send_message_resume (&sctx); if (!ProcessList) { done = 1; break; } free_bg_menu (&menu); menu = create_bg_menu (); } break; } } free_bg_menu (&menu); }