degesch: add backlog/scrollback functionality

Finally!  I went with possibly the simplest solution, which is to
run less, instead of badly reimplementing its functionality.
This commit is contained in:
Přemysl Eric Janouch 2015-08-08 19:36:34 +02:00
parent 27f185e8aa
commit a28528d260
1 changed files with 200 additions and 23 deletions

223
degesch.c
View File

@ -1364,6 +1364,8 @@ struct app_context
bool awaiting_mirc_escape; ///< Awaiting a mIRC attribute escape
char char_buf[MB_LEN_MAX + 1]; ///< Buffered multibyte char
size_t char_buf_len; ///< How much of an MB char is buffered
bool running_backlog_helper; ///< Running a backlog helper
}
*g_ctx;
@ -1640,6 +1642,10 @@ static struct config_schema g_config_behaviour[] =
.type = CONFIG_ITEM_BOOLEAN,
.default_ = "off",
.on_change = on_config_logging_change },
{ .name = "backlog_helper",
.comment = "Shell command to display a buffer's history",
.type = CONFIG_ITEM_STRING,
.default_ = "\"LESSSECURE=1 less -M -R +G\"" },
{ .name = "save_on_quit",
.comment = "Save configuration before quitting",
.type = CONFIG_ITEM_BOOLEAN,
@ -2839,11 +2845,18 @@ log_formatter (struct app_context *ctx,
&& buffer == ctx->current_buffer->server->buffer))
can_leak = true;
if (buffer == ctx->current_buffer)
bool displayed = true;
if (ctx->running_backlog_helper)
// Another process is using the terminal
displayed = false;
else if (buffer == ctx->current_buffer)
buffer_line_display (ctx, line, false);
else if (!ctx->isolate_buffers && can_leak)
buffer_line_display (ctx, line, true);
else
displayed = false;
if (!displayed)
{
buffer->unseen_messages_count++;
if (flags & BUFFER_LINE_HIGHLIGHT)
@ -2957,6 +2970,8 @@ buffer_open_log_file (struct app_context *ctx, struct buffer *buffer)
if (!(buffer->log_file = fopen (path.str, "ab")))
log_global_error (ctx, "Couldn't open log file `#s': #s",
path.str, strerror (errno));
else
set_cloexec (fileno (buffer->log_file));
str_free (&path);
}
@ -8807,6 +8822,38 @@ make_completions (struct app_context *ctx, char *line, int start, int end)
// --- Common code for user actions --------------------------------------------
static void
suspend_terminal (struct app_context *ctx)
{
#ifdef HAVE_READLINE
rl_deprep_terminal ();
#elif defined (HAVE_EDITLINE)
el_set (ctx->input.editline, EL_PREP_TERM, 0);
#endif
input_hide (&ctx->input);
poller_fd_reset (&ctx->tty_event);
// TODO: also disable the date change timer
}
static void
resume_terminal (struct app_context *ctx)
{
#ifdef HAVE_READLINE
rl_prep_terminal (true);
#elif defined (HAVE_EDITLINE)
el_set (ctx->input.editline, EL_PREP_TERM, 1);
#endif
// In theory we could just print all unseen messages but this is safer
buffer_print_backlog (ctx, ctx->current_buffer);
// Now it's safe to process any user input
poller_fd_set (&ctx->tty_event, POLLIN);
input_show (&ctx->input);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
redraw_screen (struct app_context *ctx)
{
@ -8849,6 +8896,59 @@ await_mirc_escape (struct app_context *ctx)
ctx->char_buf_len = 0;
}
static void
exec_backlog_helper (const char *command, FILE *backlog)
{
dup2 (fileno (backlog), STDIN_FILENO);
// Put the child into a new foreground process group
hard_assert (setpgid (0, 0)!= -1);
hard_assert (tcsetpgrp (STDOUT_FILENO, getpgid (0)) != -1);
execl ("/bin/sh", "/bin/sh", "-c", command, NULL);
print_error ("%s: %s", "Failed to launch backlog helper", strerror (errno));
_exit (EXIT_FAILURE);
}
static void
display_backlog (struct app_context *ctx)
{
hard_assert (!ctx->running_backlog_helper);
FILE *backlog = tmpfile ();
set_cloexec (fileno (backlog));
// TODO: retrieve the lines with formatting
for (struct buffer_line *line = ctx->current_buffer->lines;
line; line = line->next)
buffer_line_write_to_log (ctx, line, backlog);
rewind (backlog);
suspend_terminal (ctx);
pid_t child = fork ();
if (child == 0)
exec_backlog_helper (get_config_string
(ctx->config.root, "behaviour.backlog_helper"), backlog);
if (child == -1)
{
int saved_errno = errno;
resume_terminal (ctx);
log_global_error (ctx, "#s: #s",
"Failed to launch backlog helper", strerror (saved_errno));
}
else
{
// Make sure the child has its own process group
(void) setpgid (child, child);
ctx->running_backlog_helper = true;
}
fclose (backlog);
}
static void
bind_common_keys (struct app_context *ctx)
{
@ -8866,6 +8966,8 @@ bind_common_keys (struct app_context *ctx)
input_bind (self, key_f5, "previous-buffer");
if (key_f6)
input_bind (self, key_f6, "next-buffer");
if (key_ppage)
input_bind (self, key_ppage, "display-backlog");
if (clear_screen)
input_bind_control (self, 'l', "redraw-screen");
@ -8906,6 +9008,17 @@ on_readline_next_buffer (int count, int key)
return 0;
}
static int
on_readline_display_backlog (int count, int key)
{
(void) count;
(void) key;
struct app_context *ctx = g_ctx;
display_backlog (ctx);
return 0;
}
static int
on_readline_redraw_screen (int count, int key)
{
@ -8998,6 +9111,7 @@ app_readline_init (void)
rl_add_defun ("previous-buffer", on_readline_previous_buffer, -1);
rl_add_defun ("next-buffer", on_readline_next_buffer, -1);
rl_add_defun ("goto-buffer", on_readline_goto_buffer, -1);
rl_add_defun ("display-backlog", on_readline_display_backlog, -1);
rl_add_defun ("redraw-screen", on_readline_redraw_screen, -1);
rl_add_defun ("insert-attribute", on_readline_insert_attribute, -1);
rl_add_defun ("send-line", on_readline_return, -1);
@ -9057,6 +9171,15 @@ on_editline_next_buffer (EditLine *editline, int key)
return CC_NORM;
}
static unsigned char
on_editline_display_backlog (EditLine *editline, int key)
{
(void) editline;
(void) key;
display_backlog (g_ctx);
}
static unsigned char
on_editline_redraw_screen (EditLine *editline, int key)
{
@ -9173,6 +9296,7 @@ app_editline_init (struct input *self)
{ "goto-buffer", "Go to buffer", on_editline_goto_buffer },
{ "previous-buffer", "Previous buffer", on_editline_previous_buffer },
{ "next-buffer", "Next buffer", on_editline_next_buffer },
{ "display-backlog", "Display backlog", on_editline_display_backlog },
{ "redraw-screen", "Redraw screen", on_editline_redraw_screen },
{ "insert-attribute", "mIRC formatting", on_editline_insert_attribute },
{ "send-line", "Send line", on_editline_return },
@ -9376,29 +9500,34 @@ static volatile sig_atomic_t g_termination_requested;
static volatile sig_atomic_t g_winch_received;
static void
sigterm_handler (int signum)
postpone_signal_handling (char id)
{
(void) signum;
g_termination_requested = true;
int original_errno = errno;
if (write (g_signal_pipe[1], "t", 1) == -1)
if (write (g_signal_pipe[1], &id, 1) == -1)
soft_assert (errno == EAGAIN);
errno = original_errno;
}
static void
sigwinch_handler (int signum)
signal_superhandler (int signum)
{
(void) signum;
g_winch_received = true;
int original_errno = errno;
if (write (g_signal_pipe[1], "w", 1) == -1)
soft_assert (errno == EAGAIN);
errno = original_errno;
switch (signum)
{
case SIGWINCH:
g_winch_received = true;
postpone_signal_handling ('w');
break;
case SIGINT:
case SIGTERM:
g_termination_requested = true;
postpone_signal_handling ('t');
break;
case SIGCHLD:
postpone_signal_handling ('c');
break;
default:
hard_assert (!"unhandled signal");
}
}
static void
@ -9418,28 +9547,76 @@ setup_signal_handlers (void)
signal (SIGPIPE, SIG_IGN);
// So that we can write to the terminal while we're running a backlog
// helper. This is also inherited by the child so that it doesn't stop
// when it calls tcsetpgrp().
signal (SIGTTOU, SIG_IGN);
struct sigaction sa;
sa.sa_flags = SA_RESTART;
sa.sa_handler = sigwinch_handler;
sa.sa_handler = signal_superhandler;
sigemptyset (&sa.sa_mask);
if (sigaction (SIGWINCH, &sa, NULL) == -1)
exit_fatal ("sigaction: %s", strerror (errno));
sa.sa_handler = sigterm_handler;
if (sigaction (SIGINT, &sa, NULL) == -1
|| sigaction (SIGTERM, &sa, NULL) == -1)
if (sigaction (SIGWINCH, &sa, NULL) == -1
|| sigaction (SIGINT, &sa, NULL) == -1
|| sigaction (SIGTERM, &sa, NULL) == -1
|| sigaction (SIGCHLD, &sa, NULL) == -1)
exit_fatal ("sigaction: %s", strerror (errno));
}
// --- I/O event handlers ------------------------------------------------------
static bool
try_reap_child (struct app_context *ctx)
{
int status;
pid_t zombie = waitpid (-1, &status, WNOHANG | WUNTRACED);
if (zombie == -1)
{
if (errno == ECHILD) return false;
if (errno == EINTR) return true;
exit_fatal ("%s: %s", "waitpid", strerror (errno));
}
if (!zombie)
return false;
if (!ctx->running_backlog_helper)
{
print_debug ("an unknown child has died");
return true;
}
if (WIFSTOPPED (status))
{
// We could also send SIGCONT but what's the point
kill (-zombie, SIGKILL);
return true;
}
ctx->running_backlog_helper = false;
hard_assert (tcsetpgrp (STDOUT_FILENO, getpgid (0)) != -1);
resume_terminal (ctx);
if (WIFSIGNALED (status))
log_global_error (ctx,
"Child died from signal #d", WTERMSIG (status));
else if (WIFEXITED (status) && WEXITSTATUS (status) != 0)
log_global_error (ctx,
"Child returned status #d", WEXITSTATUS (status));
return true;
}
static void
on_signal_pipe_readable (const struct pollfd *fd, struct app_context *ctx)
{
char dummy;
(void) read (fd->fd, &dummy, 1);
// Reap all dead children (since the signal pipe may overflow etc. we run
// waitpid() in a loop to return all the zombies it knows about).
while (try_reap_child (ctx))
;
if (g_termination_requested && !ctx->quitting)
// TODO: this way we don't send a QUIT message but just close the
// connection from our side and wait for a full close.