summaryrefslogtreecommitdiffstats
path: root/src/main.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.c')
-rw-r--r--src/main.c498
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;
+}
+