#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;
static int userCount;
// Tracked user sessions
#define USERS (50)
static struct user *users;
// List of reboot and shutdown times
static const char *shutdownActions[SHUTDOWN_ENUM_END] = {
[REBOOT] = "reboot",
[POWEROFF] = "poweroff",
[KEXEC] = "kexec",
[SUSPEND] = "suspend",
};
#define TABMAX (20)
static struct {
time_t deadline; // MONOTONIC; 0 = none, or last one was disarmed and time elapsed past deadline
time_t noUserDeadline; // In case no user is logged in, consider this deadline too
enum Shutdown action;
bool disarmed;
bool force;
} 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;
int minIdle;
bool killUserProcesses;
bool logoutIsShutdown;
} config;
static time_t combineTime( const time_t now, const struct time * time );
static void getXAuthority( struct user *user );
static bool parseCmdline( int argc, char **argv );
static void execShutdown( enum Shutdown action );
static void userLoggedOut(struct user* usr);
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 longest possible sleep time without delaying any timeout related actions
// But cap at 60 seconds
int defaultSleep = 60;
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 = rpc_open();
if ( listenFd == -1 )
return 1;
lastActivity = now();
for ( ;; ) {
int sleepTime = defaultSleep;
const int count = getUserList( users, USERS );
userCount = count;
for ( idx = 0; idx < count; ++idx ) {
struct user * const usr = &users[idx];
if ( !usr->online ) {
if ( !usr->lastOnline ) {
fprintf( stderr, "This will never happen \\o/\n" );
continue;
}
// User was logged in, but isn't anymore
userLoggedOut( usr );
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.saverState == SAVER_OFF
|| ( screen.saverState == SAVER_ACTIVE && usr->lockTime > 0 ) ) {
if ( usr->lockTime > 0 ) {
printf( "%s #### Session got unlocked (State: %d, LockTime: %u).\n",
usr->display, (int)screen.saverState, (unsigned int)usr->lockTime );
// Session is not locked, unconditionally update timestamp
usr->lockTime = 0;
usr->lockTimeOffset = 0;
}
usr->lastActivity = NOW - screen.idleSeconds;
usr->lastActivityOffset = 0;
} else if ( usr->isLocked && screen.lockTimestamp != 0 && screen.lockTimestamp - usr->lockTime > 10 ) { // Allow for slight offset in case we manually set this
// Session is locked
if ( usr->lastActivity == 0 || usr->lockTime != 0 ) {
// We have no activity log yet, or quickly unlocked and relocked -- update idle time
usr->lastActivity = screen.lockTimestamp;
usr->lastActivityOffset = 0;
printf( "%s #### Session got externally re-locked.\n", usr->display );
} else {
printf( "%s #### Session got externally locked.\n", usr->display );
}
usr->lockTime = screen.lockTimestamp;
usr->lockTimeOffset = 0;
}
// If this file exists, the user doesn't want screen saver or DPMS to trigger
const bool useSaver = ( config.saverTimeout > 0 || config.dpmsTimeout > 0 )
&& ( usr->saverFile[0] == '-' || access( usr->saverFile, F_OK ) == -1 );
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 && nextAction.deadline - monoNOW < 300 ) {
want = SCREEN_ON;
} else if ( useSaver && 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 ( useSaver && 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 ) {
// Kill session, or if enabled, shut down machine entirely, if this was the main session
if ( config.logoutIsShutdown && usr->display[0] == ':' && usr->display[1] == '0' ) {
execShutdown( POWEROFF );
} else {
killSession( usr );
}
} else if ( remaining <= 65 ) {
warn_showDefaultWarning( usr, WARN_LOGOUT, remaining );
CAP_SLEEP( remaining );
} else if ( remaining < 310 ) {
if ( remaining % 30 < 10 ) {
warn_showDefaultWarning( usr, WARN_LOGOUT_LOW, remaining );
}
CAP_SLEEP( ( remaining % 30 ) - 2 );
} else {
CAP_SLEEP( remaining - 305 );
}
usr->logoutTime = NOW + remaining;
}
// Update timestamp of last use of system
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 ( ! nextAction.force && config.shutdowns > 0 // Next action is not forced, and have shutdown schedule
// and no current action pending, or next action more than 5 mins away
&& ( nextAction.deadline == 0 || nextAction.deadline - monoNOW > 300 ) ) {
// next action is more than 5 mins away, reset for now
if ( nextAction.deadline != 0 && nextAction.deadline - monoNOW > 300 ) {
nextAction.deadline = 0;
}
const time_t NOW = time( NULL );
time_t deadline;
int delta;
// Loop through all scheduled actions and find the next one
for ( idx = 0; idx < config.shutdowns; ++idx ) {
deadline = combineTime( NOW, &config.shutdown[idx] );
if ( deadline == 0 )
continue;
delta = deadline - NOW;
if ( delta >= -6 && ( nextAction.deadline == 0 || ( nextAction.deadline - monoNOW - 3 ) > delta ) ) {
// Engage
nextAction.deadline = monoNOW + ( delta < 0 ? 0 : delta );
nextAction.noUserDeadline = nextAction.deadline;
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 && ( count == 0 || ( monoNOW - lastActivity ) >= config.minIdle || nextAction.force ) ) {
// Some action seems pending
int remaining;
// No user logged in, no user deadline is max. 300 seconds in the past -> use that value
if ( userCount == 0 && nextAction.noUserDeadline + 300 > monoNOW && nextAction.noUserDeadline < nextAction.deadline ) {
remaining = nextAction.noUserDeadline - monoNOW;
if ( remaining < 0 ) {
remaining = 0;
}
} else {
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;
nextAction.noUserDeadline = 0;
} else if ( ! nextAction.disarmed ) {
if ( remaining <= 1 ) {
// Execute!
execShutdown( nextAction.action );
nextAction.disarmed = true;
nextAction.force = false;
} else if ( remaining < 310 ) {
if ( remaining % 30 < 8 ) {
enum Warning w = WARN_REBOOT;
if ( nextAction.action == POWEROFF ) {
w = WARN_POWEROFF;
}
for ( idx = 0; idx < count; ++idx ) {
warn_showDefaultWarning( &users[idx], w, remaining );
}
}
CAP_SLEEP( ( remaining % 15 ) - 2 );
} else {
CAP_SLEEP( remaining - 305 );
}
}
}
do {
// Handle requests
const time_t oldDeadline = nextAction.deadline;
// Sleep until next run
int sn = sleepTime - ( endMono - startMono );
//printf( "Sleeping %d seconds\n ", sn );
if ( rpc_wait( listenFd, sn < 1 ? 1 : sn ) ) {
rpc_handle( listenFd );
if ( nextAction.deadline != oldDeadline ) {
// Might have set a new scheduled action, run main loop again soon
CAP_SLEEP( 1 );
} else {
// Otherwise, still update rather quickly, in case we get polled again
CAP_SLEEP( 5 );
}
}
// Detect time changes
endWall = time( NULL );
endMono = now();
// Sloppy way of preventing we loop too fast -- handle rpc callbacks but don't run main logic
} while ( sleepTime > ( endMono - startMono ) );
// Adjust for clock changes
const int diff = ( endWall - startWall ) - ( endMono - startMono );
if ( diff < -2 || diff > 2 ) {
printf( "Correcting time by %d seconds\n", diff );
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;
}
static time_t combineTime( const time_t now, const struct time * time )
{
struct tm dtime;
if ( localtime_r( &now, &dtime ) == NULL )
return 0;
time_t result;
dtime.tm_hour = time->hour;
dtime.tm_min = time->minute;
dtime.tm_sec = 0;
result = mktime( &dtime );
if ( result + 6 < now ) {
// Try again tomorrow
dtime.tm_mday++;
result = mktime( &dtime );
}
if ( result == -1 ) {
perror( "Cannot convert struct tm to time_t via mktime" );
return 0;
}
if ( result < now ) {
fprintf( stderr, "combineTime: Even +1 day seems in the past!?\n" );
return 0;
}
return result;
}
/**
* 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';
user->saverFile[0] = '-';
user->saverFile[1] = '\0';
return;
}
// Got valid data
snprintf( user->saverFile, SAVERLEN, "%s/.no-saver", p.pw_dir );
snprintf( user->xauth, AUTHLEN, "%s/.Xauthority", p.pw_dir );
if ( access( user->xauth, F_OK ) == -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' },
{ "min-idle", required_argument, NULL, 'min' },
{ "cmd", required_argument, NULL, 'cmd' },
{ "send", required_argument, NULL, 'send' },
{ "kill-user-processes", no_argument, NULL, 'kill' },
{ "logout-is-shutdown", no_argument, NULL, 'lis' },
{ "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;
config.minIdle = 0;
config.killUserProcesses = false;
config.logoutIsShutdown = false;
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 'min':
config.minIdle = 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;
case 'send':
exit( !rpc_send( optarg ) );
break;
case 'kill':
config.killUserProcesses = true;
break;
case 'lis':
config.logoutIsShutdown = true;
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, int *numUsers )
{
if ( nextAction.deadline == 0 || nextAction.disarmed ) {
*deadline = 0;
return;
}
*deadline = ( nextAction.deadline - now() ) + time( NULL );
*nextString = shutdownActions[nextAction.action];
*numUsers = userCount;
}
struct user* main_getUser( const char *terminal )
{
for ( int i = 0; i < userCount; ++i ) {
if ( strcmp( users[i].device, terminal ) == 0
|| strcmp( users[i].display, terminal ) == 0 ) {
return &users[i];
}
}
return NULL;
}
/**
* Called from RPC, only by root: Set the next action (but only
* if it's sooner than anything already pending)
*/
void main_queueAction( enum Shutdown action, int delta )
{
if ( delta < 0 || action < 0 || action >= SHUTDOWN_ENUM_END )
return;
time_t monoNOW = now();
int realDelta = delta;
if ( userCount != 0 ) {
int idleTime = monoNOW - lastActivity;
if ( idleTime < 3600 ) {
if ( delta < 306 ) {
delta = 306;
}
} else if ( idleTime < 7200 ) {
if ( delta < 66 ) {
delta = 66;
}
}
}
if ( nextAction.deadline == 0 || ( nextAction.deadline - monoNOW - 3 ) > delta ) {
// Engage
nextAction.deadline = monoNOW + delta;
nextAction.noUserDeadline = monoNOW + realDelta;
nextAction.disarmed = false;
nextAction.force = true;
nextAction.action = action;
printf( "RPC: Scheduling reboot/poweroff in %d seconds\n", delta );
}
}
void main_warnAll( const char *message )
{
for ( int idx = 0; idx < userCount; ++idx ) {
warn_showCustomWarning( &users[idx], "Warning", message );
}
}
static void userLoggedOut(struct user* usr)
{
if ( !config.killUserProcesses )
return;
for ( int i = 0; i < userCount; ++i ) {
if (users[i].online && strcmp(users[i].user, usr->user) == 0)
return; // Still an active session
}
struct passwd *u = getpwnam( usr->user );
if ( u == NULL || u->pw_uid < 1000 )
return; // Ignore system users
printf( "Killing remaining processes of %s\n", usr->user );
run( true, "pkill", "-u", usr->user, (char*)NULL );
}