From a28528d260ea51883a3325be4f4682f4c2f66da8 Mon Sep 17 00:00:00 2001 From: Přemysl Janouch Date: Sat, 8 Aug 2015 19:36:34 +0200 Subject: degesch: add backlog/scrollback functionality Finally! I went with possibly the simplest solution, which is to run less, instead of badly reimplementing its functionality. --- degesch.c | 223 +++++++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 200 insertions(+), 23 deletions(-) diff --git a/degesch.c b/degesch.c index b3747ec..b7c9434 100644 --- a/degesch.c +++ b/degesch.c @@ -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. -- cgit v1.2.3-70-g09d2