/*
* Copyright (C) 1996-2000,2013 Michael R. Elkins <me@mutt.org>
* Copyright (C) 2020-2022 Kevin J. McCarthy <kevin@8t8.us>
*
* 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 <stdlib.h>
#include <signal.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include <errno.h>
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, "<exit>", sizeof(key));
/* L10N:
Background Edit Landing Page message, second line.
Displays while the editor is running.
%s is the key binding for "<exit>", 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);
}