summaryrefslogblamecommitdiffstats
path: root/src/main.c
blob: 2dd433e9d58ca53ebf3dc735e7e2990d06696352 (plain) (tree)


























                                                                                            






                                                         



                                                                                                    
               












                                 
                

         

                                                                        








































                                                                                          
                             


















































                                                                                                                                     
                                                                                                                 




























                                                                                                                                                                                                                            



                                                            

























                                                                                                  
                                                                                                                                  



                                                                                    













                                                                                                                     
                     
                                       





















                                                                                
                                                                                                                











                                                                                                           
                                             















                                                                              




                                                                                                                























                                                                           

























                                                                            

































                                                                                         
                                                   
                                              
                                                




































                                                                      
                       



















                                                                                


                                            


















                                                                                                 

                                        




























































                                                                                                    








                                                                                      
                                




                                                                           
#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
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
    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;
} 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 );

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 );
        int minIdleTime = -1;
        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;
            // Min of all sessions
            if ( idleTime >= 0 && idleTime < minIdleTime ) {
                minIdleTime = idleTime;
            }
            // 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 ( ! nextAction.force && 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 );
            time_t deadline;
            int delta;
            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.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 || minIdleTime >= config.minIdle || nextAction.force ) ) {
            // 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;
                    nextAction.force = false;
                } 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 );
        // Might have set a new scheduled action
        if ( nextAction.deadline != 0 && ( count == 0 || minIdleTime >= config.minIdle || nextAction.force ) ) {
            int delta = nextAction.deadline - monoNOW;
            CAP_SLEEP( delta );
        }
        // 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;
}

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';
        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' },
    { "min-idle", required_argument, NULL, 'min' },
    { "cmd", required_argument, NULL, 'cmd' },
    { "send", required_argument, NULL, 'send' },
    { "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;
    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 ) );
        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;
}

void main_queueAction( enum Shutdown action, int delta )
{
    if ( delta < 0 || action < 0 || action >= SHUTDOWN_ENUM_END )
        return;
    time_t monoNOW = now();
    if ( nextAction.deadline == 0 || ( nextAction.deadline - monoNOW - 3 ) > delta ) {
        // Engage
        nextAction.deadline = monoNOW + delta;
        nextAction.disarmed = false;
        nextAction.force = true;
        nextAction.action = action;
        printf( "RPC: Scheduling reboot/poweroff in %d seconds\n", delta );
    }
}