diff options
Diffstat (limited to 'src/main.c')
-rw-r--r-- | src/main.c | 498 |
1 files changed, 498 insertions, 0 deletions
diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..f9960f5 --- /dev/null +++ b/src/main.c @@ -0,0 +1,498 @@ +#include "userlist.h" +#include "warn.h" +#include "x11util.h" +#include "util.h" +#include "main.h" +#include "rpc.h" + +#include <string.h> +#include <unistd.h> +#include <stdio.h> +#include <error.h> +#include <stdint.h> +#include <stdlib.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <pwd.h> +#include <getopt.h> + +#define CAP_SLEEP(newval) do { if ( (newval) < sleepTime ) sleepTime = (newval); } while (0) + +static time_t lastActivity; + +// Tracked user sessions +#define USERS (50) +static struct user *users; + +// List of reboot and shutdown times +enum Shutdown { + REBOOT = 0, + POWEROFF, + KEXEC, + SUSPEND, + SHUTDOWN_ENUM_END, +}; +static const char *shutdownActions[SHUTDOWN_ENUM_END] = { + [REBOOT] = "reboot", + [POWEROFF] = "poweroff", + [KEXEC] = "kexec", + [SUSPEND] = "suspend", +}; +#define TABMAX (20) +struct time { + int hour; + int minute; + enum Shutdown action; +}; +static struct { + time_t deadline; // MONOTONIC; 0 = none, or last one was disarmed and time elapsed past deadline + enum Shutdown action; + bool disarmed; +} nextAction; + +// Config via command line +static struct { + char *shutdownCommand; + struct time shutdown[TABMAX]; + int shutdowns; + int poweroffTimeout; + int logoutTimeout; + int suspendTimeout; + int saverTimeout; + int dpmsTimeout; + int gracePeriod; +} config; + +static void getXAuthority( struct user *user ); + +static bool parseCmdline( int argc, char **argv ); + +static void execShutdown( enum Shutdown action ); + +int main( int argc, char **argv ) +{ + int idx; + time_t startWall, startMono, endWall, endMono; + init_time(); + if ( ! parseCmdline( argc, argv ) ) + return 2; + // Always line buffered output + setvbuf( stdout, NULL, _IOLBF, -BUFSIZ ); + setvbuf( stderr, NULL, _IOLBF, -BUFSIZ ); + if ( _testmode ) { + fprintf( stderr, "Test mode only!\n" ); + } + users = calloc( USERS, sizeof(*users) ); + nextAction.deadline = 0; + startWall = time( NULL ); + startMono = now(); + // Calculate shortest possible sleep time without delaying any timeout related actions + int defaultSleep = 900; + if ( config.logoutTimeout > 0 && config.logoutTimeout < defaultSleep ) { + defaultSleep = config.logoutTimeout; + } + if ( config.saverTimeout > 0 && config.saverTimeout < defaultSleep ) { + defaultSleep = config.saverTimeout; + } + if ( config.dpmsTimeout > 0 && config.dpmsTimeout < defaultSleep ) { + defaultSleep = config.dpmsTimeout; + } + int listenFd = rpcOpen(); + if ( listenFd == -1 ) + return 1; + lastActivity = now(); + for ( ;; ) { + int sleepTime = defaultSleep; + const int count = getUserList( users, USERS ); + for ( idx = 0; idx < count; ++idx ) { + struct user * const usr = &users[idx]; + if ( !usr->mark ) { + fprintf( stderr, "This will never happen \\o/\n" ); + continue; + } + const time_t NOW = time( NULL ); + const time_t monoNOW = now(); + if ( *usr->display ) { + // User has an X11 session going, get actual idle time + struct x11state screen; + getXAuthority( usr ); + if ( !getX11IdleTimes( usr->xauth, usr->display, &screen ) ) { + // Failed, assume active to be safe + printf( "X11 query failed. Using NOW.\n "); + usr->lastActivity = NOW; + usr->lastActivityOffset = 0; + } else { + // Fiddle and fumble + usr->isLocked = ( screen.saverState == SAVER_LOCKED ); + if ( screen.lockTimestamp == 0 && ( NOW - usr->lockTime ) > 4 ) { + // Session is not locked, unconditionally update timestamp + if ( usr->lockTime != 0 ) { + printf( "%s #### Session got unlocked.\n", usr->display ); + } + usr->lockTime = 0; + usr->lockTimeOffset = 0; + usr->lastActivity = NOW - screen.idleSeconds; + usr->lastActivityOffset = 0; + } else if ( screen.lockTimestamp - usr->lockTime > 10 ) { // Allow for slight offset in case we manually set this + // Session is locked + printf( "%s #### Session got externally locked.\n", usr->display ); + usr->lockTime = screen.lockTimestamp; + usr->lockTimeOffset = 0; + if ( usr->lastActivity == 0 || usr->lockTime != 0 ) { + // We have no activity log yet, or quickly unlocked and relocked -- update idle time + usr->lastActivity = usr->lockTime; + usr->lastActivityOffset = 0; + } + } + const int idleTime = NOW - ( usr->lastActivity + usr->lastActivityOffset ); + if ( config.dpmsTimeout > 0 && screen.screenStandby != SCREEN_UNKNOWN ) { + int want = SCREEN_UNKNOWN; + if ( config.logoutTimeout > 0 && idleTime + 300 > config.logoutTimeout ) { + want = SCREEN_ON; + } else if ( ! nextAction.disarmed && nextAction.deadline != 0 && monoNOW - nextAction.deadline < 300 ) { + want = SCREEN_ON; + } else if ( idleTime > config.dpmsTimeout && screen.idleSeconds >= 60 ) { + want = SCREEN_OFF; + } + if ( want != SCREEN_UNKNOWN && screen.screenStandby != want ) { + printf( "#### Powering %s %s.\n", usr->display, want == SCREEN_ON ? "ON" : "OFF" ); + setScreenDpms( usr->xauth, usr->display, want ); + } + } + if ( config.saverTimeout > 0 ) { + int want = SAVER_OFF; + if ( config.gracePeriod >= 0 && idleTime > config.saverTimeout + config.gracePeriod + && ( screen.saverState == SAVER_OFF || screen.saverState == SAVER_ACTIVE ) ) { + want = SAVER_LOCKED; + } else if ( idleTime > config.saverTimeout && screen.saverState == SAVER_OFF ) { + want = SAVER_ACTIVE; + } else if ( screen.saverState == SAVER_OFF ) { + CAP_SLEEP( config.saverTimeout - idleTime ); + } + if ( want != SAVER_OFF ) { + enableScreenSaver( usr->xauth, usr->display, want ); + usr->lockTime = NOW; + usr->lockTimeOffset = 0; + if ( screen.screenStandby == SCREEN_OFF ) { + // Screen was off before, but our ugly vmware ungrab hack might + // have simulated input and woke up the screen - enable standby + // again right away + setScreenDpms( usr->xauth, usr->display, SCREEN_OFF ); + } + } + } + } // End X11 handling + } + //printf( "Have User %d: %s Locked %d (%+d) Idle %d (%+d) -- Leader: %d\n", idx, usr->user, (int)usr->lockTime, usr->lockTimeOffset, (int)usr->lastActivity, usr->lastActivityOffset, (int)usr->sessionLeader ); + const int idleTime = NOW - usr->lastActivity; + // See if we need to shorten sleep time for a pending session kill + // or warn the user about an imminent session kill + // or whether the screen saver is supposed to activate + if ( config.logoutTimeout > 0 ) { + // Idle logout is enabled + const int remaining = config.logoutTimeout - idleTime; + if ( remaining <= 2 ) { + killSession( usr ); + } else if ( remaining <= 65 ) { + warnUser( usr, WARN_LOGOUT, remaining ); + CAP_SLEEP( remaining ); + } else if ( remaining < 310 ) { + warnUser( usr, WARN_LOGOUT_LOW, remaining ); + CAP_SLEEP( remaining - ( ( remaining - 30 ) / 60 * 60 ) ); + } else { + CAP_SLEEP( remaining - 300 ); + } + usr->logoutTime = NOW + remaining; + } + const time_t laMono = monoNOW - idleTime; + if ( laMono > lastActivity ) { + lastActivity = laMono; + } + } // End loop over users + const time_t monoNOW = now(); + // See if any reboot or poweroff is due (if none is currently pending or still 5min+ away) + if ( config.shutdowns > 0 && ( nextAction.deadline == 0 || nextAction.deadline - monoNOW > 300 ) ) { + if ( nextAction.deadline != 0 && nextAction.deadline - monoNOW > 300 ) { + nextAction.deadline = 0; + } + const time_t NOW = time( NULL ); + struct tm tmNOW; + if ( localtime_r( &NOW, &tmNOW ) != NULL ) { + struct tm dtime; + time_t deadline; + int delta; + for ( idx = 0; idx < config.shutdowns; ++idx ) { + dtime = tmNOW; + dtime.tm_hour = config.shutdown[idx].hour; + dtime.tm_min = config.shutdown[idx].minute; + dtime.tm_sec = 0; + deadline = mktime( &dtime ); + if ( deadline + 6 < NOW ) { + // Try again tomorrow + dtime.tm_mday++; + deadline = mktime( &dtime ); + } + if ( deadline == -1 ) { + perror( "Cannot convert shutdown entry to time_t via mktime" ); + continue; + } + delta = deadline - NOW; + if ( delta >= -6 && ( nextAction.deadline == 0 || ( nextAction.deadline - monoNOW - 3 ) > delta ) ) { + // Engage + nextAction.deadline = monoNOW + ( delta < 0 ? 0 : delta ); + nextAction.disarmed = false; + nextAction.action = config.shutdown[idx].action; + if ( delta < 300 ) { + printf( "Scheduling reboot/poweroff in %d seconds\n", delta ); + } + CAP_SLEEP( delta ); + } + } + } + } + // Check unused idle time + if ( count == 0 ) { + if ( config.poweroffTimeout != 0 ) { + int delta = config.poweroffTimeout - ( monoNOW - lastActivity ); + if ( delta <= 0 ) { + execShutdown( POWEROFF ); + } else { + CAP_SLEEP( delta ); + } + } + if ( config.suspendTimeout != 0 ) { + int delta = config.suspendTimeout - ( monoNOW - lastActivity ); + if ( delta <= 0 ) { + execShutdown( SUSPEND ); + } else { + CAP_SLEEP( delta ); + } + } + } + if ( nextAction.deadline != 0 ) { + // Some action seems pending + const int remaining = nextAction.deadline - monoNOW; + if ( remaining < -60 ) { + if ( ! nextAction.disarmed ) { + fprintf( stderr, "Missed planned action!? Late by %d seconds. Ignored.\n", remaining ); + } + nextAction.deadline = 0; + } else if ( ! nextAction.disarmed ) { + if ( remaining <= 3 ) { + // Execute! + execShutdown( nextAction.action ); + nextAction.disarmed = true; + if ( ! _testmode ) { + } + } else if ( remaining < 310 ) { + enum Warning w = WARN_REBOOT; + if ( nextAction.action == POWEROFF ) { + w = WARN_POWEROFF; + } + for ( idx = 0; idx < count; ++idx ) { + warnUser( &users[idx], w, remaining ); + } + CAP_SLEEP( remaining - ( ( remaining - 30 ) / 60 * 60 ) ); + } else { + CAP_SLEEP( remaining ); + } + } + } + // Handle requests + rpcHandle( listenFd ); + // Sleep until next run + //printf( "Sleeping %d seconds\n ", sleepTime ); + rpcWait( listenFd, sleepTime > 5 ? sleepTime : 5 ); + // Detect time changes + endWall = time( NULL ); + endMono = now(); + // Adjust for clock changes + const int diff = ( endWall - startWall ) - ( endMono - startMono ); + if ( diff != 0 ) { + for ( idx = 0; idx < count; ++idx ) { + if ( users[idx].display[0] == '\0' ) + continue; + if ( users[idx].lockTime != 0 ) { + users[idx].lockTimeOffset += diff; + } + users[idx].lastActivityOffset += diff; + } + } + startWall = endWall; + startMono = endMono; + } + return 0; +} + +/** + * This doesn't really use any clever logic but rather assumes that + * it will be $HOME/.Xauthority + */ +static void getXAuthority( struct user *user ) +{ + if ( user->xauth[0] != '\0' ) + return; + char buffer[1000]; + struct passwd p, *ok; + if ( getpwnam_r( user->user, &p, buffer, sizeof(buffer), &ok ) != 0 || ok == NULL ) { + user->xauth[0] = '-'; + user->xauth[1] = '\0'; + return; + } + // Got valid data + snprintf( user->xauth, AUTHLEN, "%s/.Xauthority", p.pw_dir ); + struct stat foo; + if ( stat( user->xauth, &foo ) == -1 ) { + user->xauth[0] = '-'; + user->xauth[1] = '\0'; + } +} + +// cmdline options. Only support long for now. +static struct option long_options[] = { + { "reboot", required_argument, NULL, 'r' }, + { "poweroff", required_argument, NULL, 'p' }, + { "poweroff-timeout", required_argument, NULL, 'pot' }, + { "suspend-timeout", required_argument, NULL, 'sbt' }, + { "logout-timeout", required_argument, NULL, 'lot' }, + { "screensaver-timeout", required_argument, NULL, 'sst' }, + { "dpms-timeout", required_argument, NULL, 'dpms' }, + { "grace-period", required_argument, NULL, 'gp' }, + { "cmd", required_argument, NULL, 'cmd' }, + { "test", no_argument, NULL, 't' }, + { NULL, 0, NULL, 0 } +}; + +static bool parseTime( const char *str, struct time *result ) +{ + char *next; + result->hour = (int)strtol( str, &next, 10 ); + if ( result->hour < 0 || result->hour > 24 ) + return false; + result->hour %= 24; // Allow 24:00 + if ( *next != ':' ) + return false; + result->minute = (int)strtol( next + 1, &next, 10 ); + if ( result->minute < 0 || result->minute > 59 ) + return false; + if ( *next != '\0' ) { + fprintf( stderr, "Ignoring trailing garbage after minute\n" ); + } + return true; +} + +/** + * Parse command line and fill all the tables / vars etc. + * Return false if command line is unparsable. + */ +static bool parseCmdline( int argc, char **argv ) +{ + int ch; + config.shutdowns = 0; + config.logoutTimeout = 0; + config.poweroffTimeout = 0; + config.suspendTimeout = 0; + config.saverTimeout = 0; + config.dpmsTimeout = 0; + config.gracePeriod = -1; + config.shutdownCommand = NULL; + while ( ( ch = getopt_long( argc, argv, "", long_options, NULL ) ) != -1 ) { + switch ( ch ) { + case 'pot': + config.poweroffTimeout = atoi( optarg ); + break; + case 'sbt': + config.suspendTimeout = atoi( optarg ); + break; + case 'lot': + config.logoutTimeout = atoi( optarg ); + break; + case 'sst': + config.saverTimeout = atoi( optarg ); + break; + case 'dpms': + config.dpmsTimeout = atoi( optarg ); + break; + case 'gp': + config.gracePeriod = atoi( optarg ); + break; + case 'r': + case 'p': + if ( config.shutdowns < TABMAX ) { + if ( parseTime( optarg, &config.shutdown[config.shutdowns] ) ) { + config.shutdown[config.shutdowns].action = ( ch == 'r' ? REBOOT : POWEROFF ); + config.shutdowns++; + } else { + fprintf( stderr, "Could not parse shutdown time %s\n", optarg ); + } + } else { + fprintf( stderr, "Ignoring shutdown time %s: Table full\n", optarg ); + } + break; + case 't': + _testmode = 1; + break; + case 'cmd': + config.shutdownCommand = strdup( optarg ); + break; + default: + fprintf( stderr, "Unhandled command line option %d, aborting\n", ch ); + return false; + } + } + return true; +} + +static void execShutdown( enum Shutdown action ) +{ + if ( action < 0 && action >= SHUTDOWN_ENUM_END ) { + fprintf( stderr, "Invalid shutdown action %d\n", (int)action ); + return; + } + if ( _testmode ) { + printf( "[dryrun] Not execution shutdown(%d)\n", (int)action ); + return; + } + if ( config.shutdownCommand != NULL ) { + printf( "Executing shutdown via %s %s\n", config.shutdownCommand, shutdownActions[action] ); + run( true, config.shutdownCommand, shutdownActions[action], (char*)NULL ); + return; + } + // Builtin + printf( "Executing shutdown via builtin handler for %s\n", shutdownActions[action] ); + switch ( action ) { + case REBOOT: + case POWEROFF: + case SUSPEND: + run( true, "systemctl", shutdownActions[action], (char*)NULL ); + break; + case KEXEC: + run( true, "systemctl", "isolate", "kexec.target", (char*)NULL ); + break; + default: + fprintf( stderr, "BUG! Unhandled shutdown action %d\n", (int)action ); + break; + } +} + +void main_getStatus( const char **nextString, time_t *deadline ) +{ + if ( nextAction.deadline == 0 || nextAction.disarmed ) { + *deadline = 0; + return; + } + *deadline = ( nextAction.deadline - now() ) + time( NULL ); + *nextString = shutdownActions[nextAction.action]; +} + +struct user* main_getUser( const char *terminal ) +{ + for ( int i = 0; i < USERS && users[i].mark; ++i ) { + if ( strcmp( users[i].device, terminal ) == 0 + || strcmp( users[i].display, terminal ) == 0 ) { + return &users[i]; + } + } + return NULL; +} + |