From 0c39e7b158ff74a2ef4ed62a72b5b7a3c878ac7f Mon Sep 17 00:00:00 2001 From: fr33domlover Date: Thu, 26 Feb 2015 23:51:20 +0200 Subject: [PATCH] Add support for auto-away based on screen attach/detach --- build/Makefile | 2 +- cfg/systems/Linux.mk | 2 +- doc/toxic.conf.5 | 10 ++ doc/toxic.conf.5.asc | 9 + misc/toxic.conf.example | 6 + src/global_commands.c | 11 +- src/settings.c | 17 ++ src/settings.h | 9 + src/term_mplex.c | 364 ++++++++++++++++++++++++++++++++++++++++ src/term_mplex.h | 32 ++++ src/toxic.c | 27 ++- src/toxic.h | 3 + 12 files changed, 486 insertions(+), 6 deletions(-) create mode 100644 src/term_mplex.c create mode 100644 src/term_mplex.h diff --git a/build/Makefile b/build/Makefile index 9131137..2ebc5db 100644 --- a/build/Makefile +++ b/build/Makefile @@ -14,7 +14,7 @@ LDFLAGS = $(USER_LDFLAGS) OBJ = chat.o chat_commands.o configdir.o dns.o execute.o file_senders.o notify.o OBJ += friendlist.o global_commands.o groupchat.o line_info.o input.o help.o autocomplete.o OBJ += log.o misc_tools.o prompt.o settings.o toxic.o toxic_strings.o windows.o message_queue.o -OBJ += group_commands.o +OBJ += group_commands.o term_mplex.o # Check on wich system we are running UNAME_S = $(shell uname -s) diff --git a/cfg/systems/Linux.mk b/cfg/systems/Linux.mk index c8bf512..9a67614 100644 --- a/cfg/systems/Linux.mk +++ b/cfg/systems/Linux.mk @@ -1,4 +1,4 @@ # Specials options for linux systems CFLAGS += -LDFLAGS += -ldl -lresolv +LDFLAGS += -ldl -lresolv -lrt MANDIR = $(PREFIX)/share/man diff --git a/doc/toxic.conf.5 b/doc/toxic.conf.5 index 0ee128a..7adb194 100644 --- a/doc/toxic.conf.5 +++ b/doc/toxic.conf.5 @@ -137,6 +137,16 @@ Indicator for alert messages\&. .RS 4 Indicator for normal messages\&. .RE +.PP +\fBmplex_away\fR +.RS 4 +Set user status when attaching and detaching from GNU screen or tmux\&. true or false +.RE +.PP +\fBmplex_away_note\fR +.RS 4 +Status message to set when status is set to away due to screen/tmux detach\&. When attaching, the status message is set back to the original value\&. +.RE .RE .PP \fBaudio\fR diff --git a/doc/toxic.conf.5.asc b/doc/toxic.conf.5.asc index 5411bba..bfeb4fe 100644 --- a/doc/toxic.conf.5.asc +++ b/doc/toxic.conf.5.asc @@ -88,6 +88,15 @@ OPTIONS *line_normal*;; Indicator for normal messages. + *mplex_away*;; + Set user status when attaching and detaching from GNU screen or tmux. + true or false + + *mplex_away_note*;; + Status message to set when status is set to away due to screen/tmux + detach. When attaching, the status message is set back to the original + value. + *audio*:: Configuration related to audio devices. diff --git a/misc/toxic.conf.example b/misc/toxic.conf.example index ed2e24f..a4bf7fd 100644 --- a/misc/toxic.conf.example +++ b/misc/toxic.conf.example @@ -43,6 +43,12 @@ ui = { // Indicator for normal messages. line_normal="---"; + + // true to change status based on screen/tmux attach/detach, false to disable + mplex_away=true; + + // Status message to use when status set to away due to screen/tmux detach + mplex_away_note="Away from keyboard, be back soon!" }; audio = { diff --git a/src/global_commands.c b/src/global_commands.c index 93837b5..163db0c 100644 --- a/src/global_commands.c +++ b/src/global_commands.c @@ -528,12 +528,14 @@ void cmd_status(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[ bool have_note = false; const char *errmsg; + lock_status (); + if (argc >= 2) { have_note = true; } else if (argc < 1) { errmsg = "Require a status. Statuses are: online, busy and away."; line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, errmsg); - return; + goto finish; } char status[MAX_STR_SIZE]; @@ -551,7 +553,7 @@ void cmd_status(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[ else { errmsg = "Invalid status. Valid statuses are: online, busy and away."; line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, errmsg); - return; + goto finish; } tox_set_user_status(m, status_kind); @@ -560,7 +562,7 @@ void cmd_status(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[ if (have_note) { if (argv[2][0] != '\"') { line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Note must be enclosed in quotes."); - return; + goto finish; } /* remove opening and closing quotes */ @@ -571,4 +573,7 @@ void cmd_status(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[ prompt_update_statusmessage(prompt, m, msg); } + +finish: + unlock_status (); } diff --git a/src/settings.c b/src/settings.c index ca30f3e..3775e5b 100644 --- a/src/settings.c +++ b/src/settings.c @@ -62,6 +62,9 @@ static struct ui_strings { const char* line_quit; const char* line_alert; const char* line_normal; + + const char* mplex_away; + const char* mplex_away_note; } ui_strings = { "ui", "timestamps", @@ -79,6 +82,8 @@ static struct ui_strings { "line_quit", "line_alert", "line_normal", + "mplex_away", + "mplex_away_note", }; static void ui_defaults(struct user_settings* settings) @@ -99,6 +104,12 @@ static void ui_defaults(struct user_settings* settings) snprintf(settings->line_quit, LINE_HINT_MAX + 1, "%s", LINE_QUIT); snprintf(settings->line_alert, LINE_HINT_MAX + 1, "%s", LINE_ALERT); snprintf(settings->line_normal, LINE_HINT_MAX + 1, "%s", LINE_NORMAL); + + settings->mplex_away = MPLEX_ON; + snprintf (settings->mplex_away_note, + sizeof (settings->mplex_away_note), + "%s", + MPLEX_AWAY_NOTE); } static const struct keys_strings { @@ -310,6 +321,12 @@ int settings_load(struct user_settings *s, const char *patharg) if ( config_setting_lookup_string(setting, ui_strings.line_normal, &str) ) { snprintf(s->line_normal, sizeof(s->line_normal), "%s", str); } + + config_setting_lookup_bool (setting, ui_strings.mplex_away, &s->mplex_away); + + if (config_setting_lookup_string (setting, ui_strings.mplex_away_note, &str)) { + snprintf (s->mplex_away_note, sizeof (s->mplex_away_note), "%s", str); + } } /* paths */ diff --git a/src/settings.h b/src/settings.h index cf9b628..5403f0e 100644 --- a/src/settings.h +++ b/src/settings.h @@ -25,6 +25,8 @@ #include +#include + /* Represents line_* hints max strlen */ #define LINE_HINT_MAX 3 @@ -63,6 +65,9 @@ struct user_settings { int key_peer_list_down; int key_toggle_peerlist; + int mplex_away; /* boolean (1 for reaction to terminal attach/detach) */ + char mplex_away_note [TOX_MAX_STATUSMESSAGE_LENGTH]; + #ifdef AUDIO int audio_in_dev; int audio_out_dev; @@ -90,6 +95,9 @@ enum { SHOW_WELCOME_MSG_ON = 1, DFLT_HST_SIZE = 700, + + MPLEX_OFF = 0, + MPLEX_ON = 1, } settings_values; #define LINE_JOIN "-->" @@ -98,6 +106,7 @@ enum { #define LINE_NORMAL "---" #define TIMESTAMP_DEFAULT "%H:%M:%S" #define LOG_TIMESTAMP_DEFAULT "%Y/%m/%d [%H:%M:%S]" +#define MPLEX_AWAY_NOTE "Detached from screen" int settings_load(struct user_settings *s, const char *patharg); #endif /* #define SETTINGS_H */ diff --git a/src/term_mplex.c b/src/term_mplex.c new file mode 100644 index 0000000..3c9b34f --- /dev/null +++ b/src/term_mplex.c @@ -0,0 +1,364 @@ +/* term_mplex.c + * + * + * Copyright (C) 2015 Toxic All Rights Reserved. + * + * This file is part of Toxic. + * + * Toxic 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 3 of the License, or + * (at your option) any later version. + * + * Toxic 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 Toxic. If not, see . + * + */ + +#include /* PATH_MAX */ +#include /* fgets, popen, pclose */ +#include /* malloc, realloc, free, getenv */ +#include /* strlen, strcpy, strstr, strchr, strrchr, strcat, strncmp + */ + +#include +#include +#include +#include +#include +#include + +#include + +#include "global_commands.h" +#include "windows.h" +#include "term_mplex.h" +#include "toxic.h" +#include "settings.h" + +extern struct ToxWindow *prompt; +extern struct user_settings *user_settings; +extern struct Winthread Winthread; + +#if defined(PATH_MAX) && PATH_MAX > 512 +#define BUFFER_SIZE PATH_MAX +#else +#define BUFFER_SIZE 512 +#endif + +#define PATH_SEP_S "/" +#define PATH_SEP_C '/' + +typedef enum +{ + MPLEX_NONE, + MPLEX_SCREEN, + MPLEX_TMUX, +} mplex_status; + +/* used for: + - storing screen socket name + - storing tmux session number in string form */ +static char mplex_data [BUFFER_SIZE]; + +static char buffer [BUFFER_SIZE]; + +static mplex_status mplex = MPLEX_NONE; +static TOX_USERSTATUS prev_status = TOX_USERSTATUS_NONE; +static char prev_note [TOX_MAX_STATUSMESSAGE_LENGTH] = ""; +static Tox *tox = NULL; + +static char *read_into_dyn_buffer (FILE *stream) +{ + const char *input_ptr = NULL; + char *dyn_buffer = NULL; + int dyn_buffer_size = 1; /* account for the \0 */ + + while ((input_ptr = fgets (buffer, BUFFER_SIZE, stream)) != NULL) + { + int length = dyn_buffer_size + strlen (input_ptr); + if (dyn_buffer) + dyn_buffer = (char*) realloc (dyn_buffer, length); + else + dyn_buffer = (char*) malloc (length); + strcpy (dyn_buffer + dyn_buffer_size - 1, input_ptr); + dyn_buffer_size = length; + } + + return dyn_buffer; +} + +static char *extract_socket_path (const char *info) +{ + const char *search_str = " Socket"; + const char *pos = strstr (info, search_str); + char *end = NULL; + char* path = NULL; + + if (!pos) + return NULL; + + pos += strlen (search_str); + pos = strchr (pos, PATH_SEP_C); + if (!pos) + return NULL; + + end = strchr (pos, '\n'); + if (!end) + return NULL; + + *end = '\0'; + end = strrchr (pos, '.'); + if (!end) + return NULL; + + path = (char*) malloc (end - pos + 1); + *end = '\0'; + return strcpy (path, pos); +} + +static int detect_gnu_screen () +{ + FILE *session_info_stream = NULL; + char *socket_name = NULL, *socket_path = NULL; + char *dyn_buffer = NULL; + + socket_name = getenv ("STY"); + if (!socket_name) + goto nomplex; + + session_info_stream = popen ("env LC_ALL=C screen -ls", "r"); + if (!session_info_stream) + goto nomplex; + + dyn_buffer = read_into_dyn_buffer (session_info_stream); + if (!dyn_buffer) + goto nomplex; + + pclose (session_info_stream); + session_info_stream = NULL; + + socket_path = extract_socket_path (dyn_buffer); + if (!socket_path) + goto nomplex; + + free (dyn_buffer); + dyn_buffer = NULL; + strcpy (mplex_data, socket_path); + strcat (mplex_data, PATH_SEP_S); + strcat (mplex_data, socket_name); + free (socket_path); + socket_path = NULL; + + mplex = MPLEX_SCREEN; + return 1; + +nomplex: + if (session_info_stream) + pclose (session_info_stream); + if (dyn_buffer) + free (dyn_buffer); + return 0; +} + +static int detect_tmux () +{ + char *tmux_env = getenv ("TMUX"), *pos; + if (!tmux_env) + return 0; + + /* find second separator */ + pos = strrchr (tmux_env, ','); + if (!pos) + return 0; + + /* store the session number string for later use */ + strcpy (mplex_data, pos + 1); + mplex = MPLEX_TMUX; + return 1; +} + +/* Checks whether a terminal multiplexer (mplex) is present, and finds + its unix socket. + + GNU screen and tmux are supported. + + Returns 1 if present, 0 otherwise. This value can be used to determine + whether an auto-away detection timer is needed. + */ +static int detect_mplex () +{ + /* try screen, and if fails try tmux */ + return detect_gnu_screen () || detect_tmux (); +} + +/* Detects gnu screen session attached/detached by examining permissions of + the session's unix socket. + */ +static int gnu_screen_is_detached () +{ + if (mplex != MPLEX_SCREEN) + return 0; + + struct stat sb; + if (stat (mplex_data, &sb) != 0) + return 0; + + /* execution permission (x) means attached */ + return ! (sb.st_mode & S_IXUSR); +} + +/* Detects tmux attached/detached by getting session data and finding the + current session's entry. An attached entry ends with "(attached)". Example: + + $ tmux list-sessions + 0: 1 windows (created Mon Mar 2 21:48:29 2015) [80x23] (attached) + 1: 2 windows (created Mon Mar 2 21:48:43 2015) [80x23] + + In this example, session 0 is attached and session 1 is detached. +*/ +static int tmux_is_detached () +{ + if (mplex != MPLEX_TMUX) + return 0; + + FILE *session_info_stream = NULL; + char *dyn_buffer = NULL, *search_str = NULL; + char *entry_pos, *nl_pos, *attached_pos; + const int numstr_len = strlen (mplex_data); + + session_info_stream = popen ("env LC_ALL=C tmux list-sessions", "r"); + if (!session_info_stream) + goto fail; + + dyn_buffer = read_into_dyn_buffer (session_info_stream); + if (!dyn_buffer) + goto fail; + + pclose (session_info_stream); + session_info_stream = NULL; + + /* prepare search string, for finding the current session's entry */ + search_str = (char*) malloc (numstr_len + 4); + search_str[0] = '\n'; + strcpy (search_str + 1, mplex_data); + strcat (search_str, ": "); + + /* do the search */ + if (strncmp (dyn_buffer, search_str + 1, numstr_len + 2) == 0) + entry_pos = dyn_buffer; + else + entry_pos = strstr (dyn_buffer, search_str); + + if (! entry_pos) + goto fail; + + /* find the next \n and look for the "(attached)" before it */ + nl_pos = strchr (entry_pos + 1, '\n'); + attached_pos = strstr (entry_pos + 1, "(attached)\n"); + + free (search_str); + search_str = NULL; + + free (dyn_buffer); + dyn_buffer = NULL; + + return attached_pos == NULL || attached_pos > nl_pos; + +fail: + if (session_info_stream) + pclose (session_info_stream); + if (dyn_buffer) + free (dyn_buffer); + if (search_str) + free (search_str); + return 0; +} + +/* Checks whether there is a terminal multiplexer present, but in detached + state. Returns 1 if detached, 0 if attached or if there is no terminal + multiplexer. + + If detect_mplex_socket() failed to find a mplex, there is no need to call + this function. If it did find one, this function can be used to periodically + sample its state and update away status according to attached/detached state + of the mplex. + */ +static int mplex_is_detached () +{ + return gnu_screen_is_detached () || tmux_is_detached (); +} + +static void mplex_timer_handler (union sigval param) +{ + int detached; + TOX_USERSTATUS current_status, new_status; + const char *new_note; + + if (mplex == MPLEX_NONE) + return; + + detached = mplex_is_detached (); + current_status = tox_get_self_user_status (tox); + + if (current_status == TOX_USERSTATUS_AWAY && !detached) + { + new_status = prev_status; + new_note = prev_note; + } + else + if (current_status != TOX_USERSTATUS_AWAY && detached) + { + prev_status = current_status; + new_status = TOX_USERSTATUS_AWAY; + tox_get_self_status_message (tox, + (uint8_t*) prev_note, + sizeof (prev_note)); + new_note = user_settings->mplex_away_note; + } + else + return; + + char argv[3][MAX_STR_SIZE]; + strcpy (argv[0], "/status"); + strcpy (argv[1], (new_status == TOX_USERSTATUS_AWAY ? "away" : + new_status == TOX_USERSTATUS_BUSY ? "busy" : "online")); + argv[2][0] = '\"'; + strcpy (argv[2] + 1, new_note); + strcat (argv[2], "\""); + pthread_mutex_lock (&Winthread.lock); + cmd_status (prompt->chatwin->history, prompt, tox, 2, argv); + pthread_mutex_unlock (&Winthread.lock); +} + +void init_mplex_away_timer (Tox *m) +{ + struct sigevent sev; + timer_t timer_id; + struct itimerspec its; + + if (! detect_mplex ()) + return; + + if (! user_settings->mplex_away) + return; + + sev.sigev_notify = SIGEV_THREAD; + sev.sigev_notify_function = mplex_timer_handler; + sev.sigev_notify_attributes = NULL; + + if (timer_create (CLOCK_REALTIME, &sev, &timer_id) == -1) + return; + + its.it_interval.tv_sec = 5; + its.it_interval.tv_nsec = 0; + its.it_value = its.it_interval; + + timer_settime (timer_id, 0, &its, NULL); + tox = m; +} diff --git a/src/term_mplex.h b/src/term_mplex.h new file mode 100644 index 0000000..bccf5c9 --- /dev/null +++ b/src/term_mplex.h @@ -0,0 +1,32 @@ +/* term_mplex.h + * + * + * Copyright (C) 2015 Toxic All Rights Reserved. + * + * This file is part of Toxic. + * + * Toxic 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 3 of the License, or + * (at your option) any later version. + * + * Toxic 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 Toxic. If not, see . + * + */ + +#ifndef TERM_MPLEX_H +#define TERM_MPLEX_H + +/* Checks if Toxic runs inside a terminal multiplexer (GNU screen or tmux). If + yes, it initializes a timer which periodically checks the attached/detached + state of the terminal and updates away status accordingly. + */ +void init_mplex_away_timer (Tox *m); + +#endif /* #define TERM_MPLEX_H */ diff --git a/src/toxic.c b/src/toxic.c index 71035d3..1d7dc10 100644 --- a/src/toxic.c +++ b/src/toxic.c @@ -57,6 +57,7 @@ #include "device.h" #include "message_queue.h" #include "execute.h" +#include "term_mplex.h" #ifdef X11 #include "xtra.h" @@ -84,6 +85,23 @@ struct audio_thread audio_thread; struct arg_opts arg_opts; struct user_settings *user_settings = NULL; +/* mutex for access to status data, for sync between: + - user command /status from ncurses thread + - auto-away POSIX timer, which runs from a separate thread + after init, should be accessed only by cmd_status() + */ +static pthread_mutex_t status_lock; + +void lock_status () +{ + pthread_mutex_lock (&status_lock); +} + +void unlock_status () +{ + pthread_mutex_unlock (&status_lock); +} + #define MIN_PASSWORD_LEN 6 #define MAX_PASSWORD_LEN 64 @@ -1071,7 +1089,11 @@ int main(int argc, char *argv[]) /* thread for message queue */ if (pthread_create(&cqueue_thread.tid, NULL, thread_cqueue, (void *) m) != 0) exit_toxic_err("failed in main", FATALERR_THREAD_CREATE); - + + /* status access mutex */ + if (pthread_mutex_init (&status_lock, NULL) != 0) + exit_toxic_err("failed in main", FATALERR_MUTEX_INIT); + #ifdef AUDIO av = init_audio(prompt, m); @@ -1109,6 +1131,9 @@ int main(int argc, char *argv[]) snprintf(avatarstr, sizeof(avatarstr), "/avatar \"%s\"", user_settings->avatar_path); execute(prompt->chatwin->history, prompt, m, avatarstr, GLOBAL_COMMAND_MODE); + /* screen/tmux auto-away timer */ + init_mplex_away_timer (m); + uint64_t last_save = (uint64_t) time(NULL); uint64_t looptimer = last_save; useconds_t msleepval = 40000; diff --git a/src/toxic.h b/src/toxic.h index fc6b512..efe2cd3 100644 --- a/src/toxic.h +++ b/src/toxic.h @@ -90,6 +90,9 @@ typedef enum _FATAL_ERRS { Uncomment if necessary */ /* #define URXVT_FIX */ +void lock_status (); +void unlock_status (); + void exit_toxic_success(Tox *m); void exit_toxic_err(const char *errmsg, int errcode);