From f7cd45464fd7c037f6a60098ae77760998b3b4b6 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Fri, 10 May 2019 15:25:43 +0200 Subject: Initial commit --- .gitignore | 6 + CMakeLists.txt | 43 +++++ src/main.c | 498 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.h | 10 ++ src/rpc.c | 132 +++++++++++++++ src/rpc.h | 10 ++ src/userlist.c | 232 +++++++++++++++++++++++++++ src/userlist.h | 31 ++++ src/util.c | 187 ++++++++++++++++++++++ src/util.h | 30 ++++ src/warn.c | 86 ++++++++++ src/warn.h | 18 +++ src/x11util.c | 405 ++++++++++++++++++++++++++++++++++++++++++++++ src/x11util.h | 28 ++++ 14 files changed, 1716 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 src/main.c create mode 100644 src/main.h create mode 100644 src/rpc.c create mode 100644 src/rpc.h create mode 100644 src/userlist.c create mode 100644 src/userlist.h create mode 100644 src/util.c create mode 100644 src/util.h create mode 100644 src/warn.c create mode 100644 src/warn.h create mode 100644 src/x11util.c create mode 100644 src/x11util.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..74b5b05 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/.project +/.cproject +/build +/CMakeLists.txt.user +*.swp +*~ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..854e50a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,43 @@ +cmake_minimum_required(VERSION 2.8.12 FATAL_ERROR) + +# cmake -G"Eclipse CDT4 - Unix Makefiles" -D CMAKE_BUILD_TYPE=Debug .. + +# project name +project(idle-daemon C) + +option(CMAKE_BUILD_TYPE Release) +set(CMAKE_C_FLAGS "-D_GNU_SOURCE") +set(CMAKE_C_FLAGS_DEBUG "-O0 -g -Wall -Wextra -pedantic -Werror -Wno-multichar") +set(CMAKE_C_FLAGS_RELEASE "-O2 -Wno-multichar") + +file(GLOB_RECURSE IDLEDAEMON_SOURCES src/*.c) + +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR}/src + ${CMAKE_CURRENT_BINARY_DIR} + ${X11_INCLUDE_DIR} +) + +FIND_PACKAGE(X11 REQUIRED) + +IF(NOT X11_Xscreensaver_FOUND) + MESSAGE(FATAL_ERROR "Could not find X11 extension Xscreensaver") +ENDIF() +IF(NOT X11_dpms_FOUND) + MESSAGE(FATAL_ERROR "Could not find X11 extension dpms") +ENDIF() + + +# +# build idle-daemon +# +add_executable(idle-daemon + ${IDLEDAEMON_SOURCES} +) + +target_link_libraries(idle-daemon + ${X11_LIBRARIES} + ${X11_Xscreensaver_LIB} +) + +install(TARGETS idle-daemon RUNTIME DESTINATION sbin) 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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; +} + diff --git a/src/main.h b/src/main.h new file mode 100644 index 0000000..1ff924a --- /dev/null +++ b/src/main.h @@ -0,0 +1,10 @@ +#ifndef _MAIN_H_ +#define _MAIN_H_ + +#include + +void main_getStatus( const char **nextAction, time_t *deadline ); + +struct user* main_getUser( const char *terminal ); + +#endif diff --git a/src/rpc.c b/src/rpc.c new file mode 100644 index 0000000..e49547e --- /dev/null +++ b/src/rpc.c @@ -0,0 +1,132 @@ +#include "rpc.h" +#include "util.h" +#include "main.h" +#include "userlist.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define SOCKPATH "/run/idle-daemon" + +static void handleClient( int fd, struct ucred *user ); + +int rpcOpen() +{ + int fd = socket( AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0 ); + if ( fd == -1 ) { + perror( "Cannot create local RPC socket" ); + return -1; + } + struct sockaddr_un address = { + .sun_family = AF_UNIX, + .sun_path = SOCKPATH, + }; + unlink( SOCKPATH ); + if ( bind( fd, (struct sockaddr *)&address, sizeof(address) ) == -1 ) { + perror( "Could not bind RPC socket" ); + close( fd ); + return -1; + } + chmod( SOCKPATH, 0777 ); + if ( listen( fd, 10 ) == -1 ) { + perror( "Could not listen() on RPC socket" ); + close( fd ); + return -1; + } + return fd; +} + +void rpcWait( int listenFd, int seconds ) +{ + waitRead( listenFd, seconds * 1000 ); +} + +void rpcHandle( int listenFd ) +{ + int fd; + while ( ( fd = accept( listenFd, NULL, NULL ) ) != -1 ) { + // Determine who connected + socklen_t len; + struct ucred ucred; + len = sizeof(struct ucred); + if ( getsockopt( fd, SOL_SOCKET, SO_PEERCRED, &ucred, &len ) == -1 ) { + perror( "Could not get credentials of connection" ); + close( fd ); // TODO: Allow more for root (eg. cancel reboot/poweroff) + continue; + } + if ( ! doublefork() ) + continue; // Parent continues + handleClient( fd, &ucred ); + exit( 0 ); + } + if ( errno != EAGAIN && errno != EWOULDBLOCK ) { + perror( "accept() on RPC socket failed" ); + } +} + +static void handleClient( int fd, struct ucred *user ) +{ + //printf( "Credentials from SO_PEERCRED: pid=%ld, euid=%ld, egid=%ld\n", + // (long) ucred.pid, (long) ucred.uid, (long) ucred.gid ); + // Make socket blocking (should be default on linux after accept() but...) + (void)user; + int flags = fcntl( fd, F_GETFL, 0 ); + if ( flags != -1 ) { + fcntl( fd, F_SETFL, flags & ~O_NONBLOCK ); + } + // But set timeouts + struct timeval tv = { + .tv_sec = 2, + }; + setsockopt( fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv) ); + setsockopt( fd, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof(tv) ); + // Now read request + char buffer[1000]; + ssize_t len = read( fd, buffer, sizeof(buffer) - 1 ); + if ( len <= 0 ) + return; + buffer[len] = '\0'; + if ( strncmp( buffer, "get", 3 ) != 0 ) { + write( fd, "error", 5 ); + } else { + // Get request + FILE *s = fdopen( fd, "w" ); + if ( s == NULL ) { + perror( "Cannot wrap socket in stream" ); + return; + } + // 1) Global state + time_t deadline; + const char *name = "none"; + main_getStatus( &name, &deadline ); + fprintf( s, "[General]\n" + "nextAction=%s\n" + "nextActionTime=%lld\n", + name, (long long)deadline ); + // 2) Requested sessions + char *tok = strtok( buffer + 4, " \t\n\r" ); + while ( tok != NULL ) { + struct user *user = main_getUser( tok ); + if ( user != NULL ) { + fprintf( s, "[%s]\n" + "logoutTime=%lld\n" + "locked=%d\n", + tok, (long long)user->logoutTime, (int)user->isLocked ); + } + tok = strtok( NULL, " \t\n\r" ); + } + fflush( s ); + } + shutdown( fd, SHUT_WR ); + read( fd, buffer, sizeof(buffer) ); + close( fd ); +} + diff --git a/src/rpc.h b/src/rpc.h new file mode 100644 index 0000000..08e08cd --- /dev/null +++ b/src/rpc.h @@ -0,0 +1,10 @@ +#ifndef _RPC_H__ +#define _RPC_H__ + +int rpcOpen(); + +void rpcHandle( int listenFd ); + +void rpcWait( int listenFd, int seconds ); + +#endif diff --git a/src/userlist.c b/src/userlist.c new file mode 100644 index 0000000..6e1f3d2 --- /dev/null +++ b/src/userlist.c @@ -0,0 +1,232 @@ +#include "userlist.h" +#include "util.h" + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define TRUE 1 +#define FALSE 0 +#define MIN(a,b) ((a) < (b) ? (a) : (b)) + +static void getSessionData( struct user * user ); + +static void fixString( char *buffer, int maxLen ) +{ + while ( *buffer != '\0' && maxLen-- > 1 ) { + if ( *buffer == ' ' || *buffer == '\t' || *buffer == '\r' || *buffer == '\n' ) { + *buffer = '_'; + } + buffer++; + } + *buffer = '\0'; +} + +static time_t lastActivity( char *line ) { + struct stat s; + char dev[40]; + snprintf( dev, sizeof(dev), "/dev/%s", line ); + if ( stat( dev, &s ) != 0 || s.st_atime == 0 ) + return -1; + return s.st_atime; +} + +int getUserList( struct user *outbuf, int size ) +{ + int fh = open( "/run/utmp", O_RDONLY ); + if ( fh == -1 ) + return 0; + // Mark + int deadzone = 0; + for ( int i = 0; i < size; ++i ) { + if ( outbuf[i].user[0] != '\0' ) { + deadzone = i + 1; + } + outbuf[i].mark = false; + } + struct utmp buffer[100]; + ssize_t len = read( fh, buffer, sizeof(buffer) ); + int num = len / sizeof(struct utmp); + for ( int i = 0; i < num; ++i ) { + struct utmp *u = buffer + i; + if ( u->ut_type != USER_PROCESS ) + continue; // Not user session + if ( u->ut_user[0] == '\0' ) + continue; // Empty username!? + // Check if alive + if ( kill( u->ut_pid, 0 ) != 0 ) + continue; + // Make sure these contain anything + if ( u->ut_line[0] == '\0' ) { + snprintf( u->ut_line, UT_LINESIZE, "-" ); + } + if ( u->ut_host[0] == '\0' ) { + snprintf( u->ut_host, UT_HOSTSIZE, "-" ); + } + // No spaces, null terminated + fixString( u->ut_user, UT_NAMESIZE ); + fixString( u->ut_line, UT_LINESIZE ); + fixString( u->ut_host, UT_HOSTSIZE ); + // Find slot in outbuf (matching one, first free one if not found) + int use = -1; + for ( int j = 0; j < deadzone; ++j ) { + if ( outbuf[j].user[0] == '\0' ) { + if ( use == -1 ) { + use = j; + } + } else if ( outbuf[j].sessionLeader == u->ut_pid + && strcmp( outbuf[j].user, u->ut_user ) == 0 + && strcmp( outbuf[j].device, u->ut_line ) == 0 ) { + use = j; + break; + } + } + if ( use == -1 ) { // Not found and no free slot, expand if possible + if ( deadzone >= size ) // No more slots :-/ + continue; + use = deadzone++; + } + if ( outbuf[use].user[0] == '\0' ) { + // New entry, init + memset( &outbuf[use], 0, sizeof(outbuf[use]) ); + snprintf( outbuf[use].user, STRLEN, "%.*s", + (int)MIN( UT_NAMESIZE, STRLEN-1 ), u->ut_user ); + snprintf( outbuf[use].device, STRLEN, "%.*s", + (int)MIN( UT_LINESIZE, STRLEN-1 ), u->ut_line ); + if ( u->ut_host[0] == ':' && isdigit( u->ut_host[1] ) ) { + snprintf( outbuf[use].display, STRLEN, "%.*s", + (int)MIN( UT_HOSTSIZE, STRLEN-1 ), u->ut_host ); + } + outbuf[use].sessionLeader = u->ut_pid; + getSessionData( &outbuf[use] ); + printf( "New Session: '%s' on '%s', Display '%s', logind session '%s', login pid '%d', utmp pid '%d'.\n", outbuf[use].user, outbuf[use].device, outbuf[use].display, outbuf[use].sessionName, (int)outbuf[use].sessionHead, (int)outbuf[use].sessionLeader ); + } + outbuf[use].mark = true; + // Reset offset if timestamp changed + // but ONLY if this isn't a known X session + if ( outbuf[use].display[0] == '\0' ) { + const time_t la = lastActivity( outbuf[use].device ); + if ( outbuf[use].lastActivity != la ) { + outbuf[use].lastActivity = la; + outbuf[use].lastActivityOffset = 0; + } + } + } + close( fh ); + // Compact + for ( int i = 0; i < deadzone; ++i ) { + if ( outbuf[i].mark ) + continue; // In use + do { + deadzone--; + if ( outbuf[deadzone].mark ) { + outbuf[i] = outbuf[deadzone]; + outbuf[deadzone].user[0] = '\0'; + break; + } + } while ( deadzone > i ); + } + return deadzone; +} + +static void getSessionData( struct user * user ) +{ + char buffer[500]; + snprintf( buffer, sizeof(buffer), "/proc/%d/cgroup", (int)user->sessionLeader ); + FILE *fh = fopen( buffer, "r" ); + if ( fh == NULL ) { + fprintf( stderr, "getSessionData: Cannot open %s to get logind session name\n", buffer ); + return; + } + // Read from cgroup file + user->sessionName[0] = '\0'; + char sessionName[40] = ""; + while ( fgets( buffer, sizeof(buffer), fh ) != NULL ) { + const char *p = strstr( buffer, ":name=systemd:" ); + if ( p == NULL ) + continue; + p = strstr( p + 14, "/session-" ); + if ( p == NULL ) + continue; + copy( p + 9, sessionName, sizeof(sessionName), ".\n" ); + break; + } + fclose( fh ); + // Extract session leader pid from loginctl - it's often different from what utmp says + int fds[2]; + if ( pipe( fds ) == -1 ) { + perror( "getSessionData: pipe for loginctl failed" ); + return; + } + if ( doublefork() ) { + close( fds[0] ); + redirect( -1, fds[1] ); + run( false, "loginctl", "-p", "Name", "-p", "Class", "-p", "Leader", "show-session", sessionName, (char*)NULL ); + } + close( fds[1] ); + char lcName[100] = "", lcClass[100] = "", lcLeader[100] = ""; + if ( ! waitRead( fds[0], 2000 ) ) { + fprintf( stderr, "getSessionData: loginctl timed out.\n" ); + } else { + char output[4000] = "\n"; + char *pos = output + 1, *end = output + sizeof(output) - 1; + do { + ssize_t ret = read( fds[0], pos, end - pos ); + if ( ret == -1 ) { + if ( errno == EINTR ) + continue; + perror( "getSessionData: read() from loginctl failed" ); + break; + } + if ( ret == 0 ) + break; + pos += ret; + } while ( waitRead( fds[0], 50 ) && pos < end ); + *pos = '\0'; + // Find strings + pos = strstr( output, "\nName=" ); + if ( pos != NULL ) { + copy( pos + 6, lcName, sizeof(lcName), "\n \t" ); + } + pos = strstr( output, "\nClass=" ); + if ( pos != NULL ) { + copy( pos + 7, lcClass, sizeof(lcClass), "\n \t" ); + } + pos = strstr( output, "\nLeader=" ); + if ( pos != NULL ) { + copy( pos + 8, lcLeader, sizeof(lcLeader), "\n \t" ); + } + } + close( fds[0] ); + // Eval what we got - only honor output if it makes sense + if ( *lcName == '\0' ) { + fprintf( stderr, "getSessionData: Could not get user name for session %s\n", sessionName ); + return; + } + if ( strcmp( lcName, user->user ) != 0 ) { + fprintf( stderr, "getSessionData: Sanity check failed. Session %s belongs to %s, but expected %s.\n", sessionName, lcName, user->user ); + return; + } + if ( *lcClass != '\0' && strcmp( lcClass, "user" ) != 0 ) { + fprintf( stderr, "killSession: Sanity check failed. Session %s has Class=%s, but expected user.\n", sessionName, lcClass ); + return; + } + // All good + snprintf( user->sessionName, sizeof(user->sessionName), "%s", sessionName ); + user->sessionHead = atol( lcLeader ); + if ( user->sessionHead < 10 ) { + fprintf( stderr, "getSessionData: Nonsensical Leader=%s reported by loginctl for session %s.\n", lcLeader, sessionName ); + user->sessionHead = 0; + } +} + diff --git a/src/userlist.h b/src/userlist.h new file mode 100644 index 0000000..c0ffc2f --- /dev/null +++ b/src/userlist.h @@ -0,0 +1,31 @@ +#ifndef _USER_LIST_H_ +#define _USER_LIST_H_ + +#include +#include + +#define STRLEN (40) +#define AUTHLEN (128) + +struct user { + time_t lastActivity; + time_t lockTime; + time_t lastWarn; + time_t logoutTime; + pid_t sessionLeader; + pid_t sessionHead; + int lastWarnLevel; + int lastActivityOffset; + int lockTimeOffset; + bool mark; + bool isLocked; + char user[STRLEN]; + char device[STRLEN]; + char display[STRLEN]; + char sessionName[STRLEN]; + char xauth[AUTHLEN]; +}; + +int getUserList( struct user *outbuf, int numElements ); + +#endif diff --git a/src/util.c b/src/util.c new file mode 100644 index 0000000..e9d642c --- /dev/null +++ b/src/util.c @@ -0,0 +1,187 @@ +#include "util.h" +#include "userlist.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int _testmode = 0; + +// Base time for monotonic clock +static struct timespec basetime; + +time_t now() +{ + struct timespec retval; + if ( clock_gettime( CLOCK_MONOTONIC, &retval ) == -1 ) { + perror( "Cannot clock_gettime CLOCK_MONOTONIC" ); + abort(); + } + return (time_t)( retval.tv_sec - basetime.tv_sec ); +} + +void init_time() +{ + if ( clock_gettime( CLOCK_MONOTONIC, &basetime ) == -1 ) { + perror( "cannot clock_gettime CLOCK_MONOTONIC" ); + basetime.tv_sec = 0; // Hope it doesn't overflow + } +} + +/** + * Return false = parent, true = second child + */ +bool doublefork() +{ + const pid_t c1 = fork(); + if ( c1 == -1 ) { + perror( "doublefork: First fork failed" ); + return false; + } + if ( c1 != 0 ) { + // Parent (main program) + int ws; + while ( waitpid( c1, &ws, 0 ) == -1 && errno == EINTR ) {} + return false; + } + // This is executed by first child + signal( SIGCHLD, SIG_IGN ); + // Fork again + const pid_t c2 = fork(); + if ( c2 == -1 ) { + perror( "doublefork: Second fork failed" ); + abort(); + } + if ( c2 != 0 ) { + // Second parent (first child), exit immediately + exit( 0 ); + } + // Second child, this is what we want + signal( SIGCHLD, SIG_DFL ); + return true; +} + +bool waitRead( int fd, int ms ) +{ + struct pollfd pd = { .fd = fd, .events = POLLIN | POLLHUP | POLLRDHUP }; + return poll( &pd, 1, ms ) > 0 && ( pd.revents & POLLIN ) != 0; +} + +void copy( const char *src, char *dst, size_t len, const char *stop ) +{ + const char *end = src + len - 1; + while ( src < end && *src != '\0' && ( stop == NULL || strchr( stop, *src ) == NULL ) ) { + *dst++ = *src++; + } + *dst = '\0'; +} + +void killSession( const struct user * user ) +{ + if ( _testmode ) { + printf( "Not really killing session %s of user %s.\n", user->sessionName, user->user ); + return; + } + if ( ! doublefork() ) + return; + // Async do NOT use return from here on + if ( user->sessionName[0] != '\0' ) { + printf( "Terminating session %s of %s.\n", user->sessionName, user->user ); + run( true, "loginctl", "terminate-session", user->sessionName ); + sleep( 5 ); + if ( kill( user->sessionLeader, 0 ) == -1 + && ( user->sessionHead == 0 || kill( user->sessionHead, 0 ) == -1 ) ) + exit( 0 ); + } + if ( user->sessionHead != 0 && kill( user->sessionHead, SIGTERM ) == 0 ) { + printf( "Session %s seems not entirely dead, TERMing loginpid %d...\n", user->sessionName, (int)user->sessionHead ); + sleep( 2 ); + } + if ( ( killpg( user->sessionLeader, SIGTERM ) & kill( user->sessionLeader, SIGTERM ) ) == 0 ) { + printf( "Session %s seems not entirely dead, TERMing process group %d...\n", user->sessionName, (int)user->sessionLeader ); + sleep( 3 ); + } + // Maybe we need KILL now... + if ( user->sessionHead != 0 && kill( user->sessionHead, SIGKILL ) == 0 ) { + printf( "Session %s seems not entirely dead, KILLing %d...\n", user->sessionName, (int)user->sessionHead ); + } + exit( 0 ); +} + +void redirect( int newIn, int newOut ) +{ + if ( newIn > 2 ) { + dup2( newIn, STDIN_FILENO ); + close( newIn ); + } + if ( newOut > 2 ) { + dup2( newOut, STDOUT_FILENO ); + close( newOut ); + } +} + +void switchUserSafe( const char* user ) +{ + errno = 0; + struct passwd *u = getpwnam( user ); + if ( u == NULL ) { + if ( errno != 0 ) { + perror( "switchUserSafe: Cannot switch to user" ); + } else { + fprintf( stderr, "switchUserSafe: Cannot switch to user %s: User not known.\n", user ); + } + exit( 1 ); + } + chdir( "/" ); + if ( setgid( u->pw_gid ) != 0 ) { + perror( "switchUserSafe: setgid failed" ); + exit( 1 ); + } + if ( setuid( u->pw_uid ) != 0 ) { + perror( "switchUserSafe: setuid failed" ); + exit( 1 ); + } + if ( u->pw_dir == NULL ) { + unsetenv( "HOME" ); + } else { + setenv( "HOME", u->pw_dir, 1 ); + } + setenv( "USER", u->pw_name, 1 ); +} + +void run( bool detach, const char *file, ... ) +{ + if ( detach && ! doublefork() ) + return; + char *argv[100]; + argv[0] = strdup( file ); + argv[99] = NULL; + va_list ap; + va_start( ap, file ); + for ( int i = 1; i < 99; ++i ) { + argv[i] = va_arg( ap, char* ); + if ( argv[i] == NULL ) + break; + argv[i] = strdup( argv[i] ); + } + va_end( ap ); + printf( "Running: '%s'", file ); + for ( int i = 0; i < 100 && argv[i] != NULL; ++i ) { + printf( " '%s'", argv[i] ); + } + printf( "\n" ); + execvp( strdup( file ), argv ); + // Something went wrong... + perror( "run execvp failed" ); + exit( 1 ); +} + diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..2c3bc51 --- /dev/null +++ b/src/util.h @@ -0,0 +1,30 @@ +#ifndef _UTIL_H_ +#define _UTIL_H_ + +#include +#include + +extern int _testmode; + +struct user; + +// Get monotonic clock (seconds) +time_t now(); + +void init_time(); + +bool doublefork(); + +bool waitRead( int fd, int ms ); + +void copy( const char *src, char *dst, size_t len, const char *stop ); + +void killSession( const struct user * user ); + +void redirect( int newIn, int newOut ); + +void switchUserSafe( const char* user ); + +void run( bool detach, const char *file, ... ); + +#endif diff --git a/src/warn.c b/src/warn.c new file mode 100644 index 0000000..6dfd427 --- /dev/null +++ b/src/warn.c @@ -0,0 +1,86 @@ +#include "warn.h" +#include "util.h" +#include +#include + +struct action { + const char *message; + const char *title; + int level; +}; + +static struct action actions[WARN_ENUM_END] = { + [WARN_REBOOT] = { + .level = 50, + .title = "Neustart des Rechners", + .message = + "Dieser PC wird in %02d:%02d neu gestartet. Bitte speichern Sie Ihre Arbeit und beenden Sie die Sitzung.\n\n" + "This Computer will reboot in %02d:%02d. Please save your work and end the session." + }, + [WARN_POWEROFF] = { + .level = 50, + .title = "Herunterfahren des Rechners", + .message = + "Dieser PC wird in %02d:%02d heruntergefahren. Bitte speichern Sie Ihre Arbeit und beenden Sie die Sitzung.\n\n" + "This Computer will power down in %02d:%02d. Please save your work and end the session." + }, + [WARN_LOGOUT] = { + .level = 40, + .title = "Inaktivität", + .message = + "Diese Sitzung scheint inaktiv und wird bei weiterer Inaktivität in %02d:%02d beendet.\n\n" + "This session seems inactive and will be killed in %02d:%02d if no further activity is detected." + }, + [WARN_LOGOUT_LOW] = { + .level = 30, + .title = "Inaktivität", + .message = + "Diese Sitzung scheint inaktiv und wird bei weiterer Inaktivität in %02d:%02d beendet.\n\n" + "This session seems inactive and will be killed in %02d:%02d if no further activity is detected." + }, +}; + +void warnUser( struct user *usr, enum Warning what, int seconds ) +{ + const time_t NOW = now(); + if ( what < 0 || what >= WARN_ENUM_END ) + return; // Invalid! + struct action *warning = &actions[what]; + if ( usr->lastWarn + 30 > NOW && warning->level <= usr->lastWarnLevel ) + return; // Ignore + usr->lastWarn = NOW; + usr->lastWarnLevel = warning->level; + if ( seconds > 60 ) { + seconds = ( seconds / 10 ) * 10; + } + int minutes = seconds / 60; + seconds %= 60; + char buffer[1000]; + snprintf( buffer, sizeof(buffer), warning->message, minutes, seconds, minutes, seconds ); + // Text or X11 warning + if ( *usr->display ) { + // notify-send + if ( doublefork() ) { + switchUserSafe( usr->user ); + setenv( "DISPLAY", usr->display, 1 ); + if ( usr->xauth[0] != '\0' && usr->xauth[0] != '-' ) { + setenv( "XAUTHORITY", usr->xauth, 1 ); + } + run( false, "notify-send", "-t", "15000", warning->title, buffer, (char*)NULL ); + } + } else { + // write to device + char dev[100]; + snprintf( dev, sizeof(dev), "/dev/%s", usr->device ); + FILE *fh = fopen( dev, "w" ); + if ( fh == NULL ) + return; + fputs( "\n\n****************************\n", fh ); + fputs( warning->title, fh ); + fputs( "\n****************************\n", fh ); + fputs( buffer, fh ); + fputs( "\n****************************\n\n", fh ); + fclose( fh ); + } +} + diff --git a/src/warn.h b/src/warn.h new file mode 100644 index 0000000..c414378 --- /dev/null +++ b/src/warn.h @@ -0,0 +1,18 @@ +#ifndef _WARN_H_ +#define _WARN_H_ + +#include "userlist.h" + +enum Warning +{ + WARN_REBOOT = 0, + WARN_POWEROFF, + WARN_LOGOUT, + WARN_LOGOUT_LOW, + WARN_ENUM_END, +}; + +void warnUser( struct user *usr, enum Warning what, int seconds ); + +#endif + diff --git a/src/x11util.c b/src/x11util.c new file mode 100644 index 0000000..18e2fe4 --- /dev/null +++ b/src/x11util.c @@ -0,0 +1,405 @@ +#include "x11util.h" +#include "util.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static XErrorHandler old_handler = 0; + +static int runClient( const char *display, int fd ); + +static void getLockState( Display *dpy, struct x11state *state ); + +static Window getScreenSaverWindow( Display *dpy ); + +static Window find_screensaver_window( Display *dpy, char **version ); + +bool getX11IdleTimes( const char *xauth, const char *display, struct x11state *state ) +{ + int fds[2]; + if ( pipe( fds ) == -1 ) { + perror( "Could not create pipe for X11 stuff" ); + return false; + } + if ( doublefork() ) { + // The child + close( fds[0] ); + if ( xauth[0] != '\0' && strcmp( xauth, "-" ) != 0 ) { + setenv( "XAUTHORITY", xauth, 1 ); + } + int ret = runClient( display, fds[1] ); + close( fds[1] ); + exit( ret ); + } + // Parent -- wait + bool ok = false; + close( fds[1] ); + if ( ! waitRead( fds[0], 1000 ) ) { + fprintf( stderr, "X11 child didn't write to pipe in time\n" ); + } else { + // We should be able to read a reply + if ( read( fds[0], state, sizeof(*state) ) != sizeof(*state) ) { + fprintf( stderr, "X11 child wrote partial data\n" ); + } else { + ok = true; + } + } + close( fds[0] ); + return ok; +} + +static int printErrorHandler( Display *dpy, XErrorEvent *error ) +{ + char msg[500] = {0}; + + XGetErrorText( dpy, error->error_code, msg, sizeof(msg) ); + fprintf( stderr, "X11 Error %d: %s\n", (int)error->error_code, msg ); + + if( !old_handler ) { + abort(); + } + return ( *old_handler )( dpy, error ); +} + +static void setScreenDpmsFork( const char *xauth, const char *display, int mode ) +{ + Display *dpy; + int dummy; + + if ( xauth[0] != '\0' && strcmp( xauth, "-" ) != 0 ) { + setenv( "XAUTHORITY", xauth, 1 ); + } + dpy = XOpenDisplay( display ); + if ( dpy == NULL ) { + fprintf( stderr, "setScreenDpms: Cannot open display\n" ); + return; + } + if ( ! DPMSQueryExtension( dpy, &dummy, &dummy ) || ! DPMSCapable( dpy ) ) { + fprintf( stderr, "%s doesn't support DPMS.\n", display ); + return; + } + //DPMSInfo( dpy, &dummy, &onoff ); + old_handler = XSetErrorHandler( printErrorHandler ); + DPMSForceLevel( dpy, mode == SCREEN_ON ? DPMSModeOn : DPMSModeSuspend ); + XSync( dpy, False ); + XSetErrorHandler( old_handler ); + old_handler = 0; + XCloseDisplay( dpy ); +} + +void setScreenDpms( const char *xauth, const char *display, int mode ) +{ + if ( _testmode ) { + printf( "Testmode: Not setting screen to %d\n", mode ); + return; + } + if ( ! doublefork() ) + return; + sleep( 2 ); // Sleep a bit in case this was called right after enabling the screen saver + setScreenDpmsFork( xauth, display, mode ); + exit( 0 ); +} + +static void enableScreenSaverFork( const char *xauth, const char *display, const int state ) +{ + if ( xauth[0] != '\0' && strcmp( xauth, "-" ) != 0 ) { + setenv( "XAUTHORITY", xauth, 1 ); + } + Display *dpy; + Atom XA_SCREENSAVER; + Atom XA_ACTION; + + dpy = XOpenDisplay( display ); + if ( dpy == NULL ) { + fprintf( stderr, "enableScreenSaver: Cannot open display %s\n", display ); + return; + } + + Window window = getScreenSaverWindow( dpy ); + if( ! window ) + return; + + XA_SCREENSAVER = XInternAtom( dpy, "SCREENSAVER", False ); + if ( state == SAVER_LOCKED ) { + XA_ACTION = XInternAtom( dpy, "LOCK", False ); + } else { + XA_ACTION = XInternAtom( dpy, "ACTIVATE", False ); + } + /* + XEvent event = { + .xany.type = ClientMessage, + .xclient.display = dpy, + .xclient.window = window, + .xclient.message_type = XA_SCREENSAVER, + .xclient.format = 32, + .xclient.data.l[0] = (long) XA_ACTION, + }; + */ + XEvent event; + event.xany.type = ClientMessage; + event.xclient.display = dpy; + event.xclient.window = window; + event.xclient.message_type = XA_SCREENSAVER; + event.xclient.format = 32; + memset (&event.xclient.data, 0, sizeof(event.xclient.data)); + event.xclient.data.l[0] = (long) XA_ACTION; + event.xclient.data.l[1] = 0; + event.xclient.data.l[2] = 0; + old_handler = XSetErrorHandler( printErrorHandler ); + if ( ! XSendEvent( dpy, window, False, 0, &event ) ) { + fprintf( stderr, "enableScreenSaver: XSendEvent to window failed.\n" ); + } + XSync( dpy, False ); + XSetErrorHandler( old_handler ); + old_handler = 0; + XCloseDisplay( dpy ); +} + +void enableScreenSaver( const char *xauth, const char *display, const int state ) +{ + if ( state != SAVER_ACTIVE && state != SAVER_LOCKED ) { + fprintf( stderr, "enableScreenSaver: Invalid state %d requested.\n", state ); + return; + } + if ( _testmode ) { + printf( "Testmode: Not setting screen saver to %d\n", state ); + return; + } + if ( ! doublefork() ) + return; + enableScreenSaverFork( xauth, display, state ); + exit( 0 ); +} + +static int runClient( const char *display, int fd ) +{ + struct x11state buffer = {0}; + XScreenSaverInfo *ssi; + Display *dpy; + int dummy; + + dpy = XOpenDisplay( display ); + if ( dpy == NULL ) { + fprintf( stderr, "Cannot open display\n" ); + return 1; + } + if ( !XScreenSaverQueryExtension( dpy, &dummy, &dummy ) ) { + fprintf( stderr, "screen saver extension not supported\n" ); + return 1; + } + ssi = XScreenSaverAllocInfo(); + if ( ssi == NULL ) { + fprintf( stderr, "Cannot allocate screen saver info\n" ); + return 1; + } + if ( ! XScreenSaverQueryInfo(dpy, DefaultRootWindow( dpy ), ssi ) ) { + fprintf( stderr, "Cannot query screen saver info\n" ); + return 1; + } + + buffer.idleSeconds = ssi->idle / 1000; + XFree( ssi ); + getLockState( dpy, &buffer ); + if ( DPMSQueryExtension( dpy, &dummy, &dummy ) && DPMSCapable( dpy ) ) { + CARD16 state; + BOOL onoff; + if ( DPMSInfo( dpy, &state, &onoff ) && onoff ) { + if ( state == DPMSModeStandby || state == DPMSModeSuspend || state == DPMSModeOff ) { + buffer.screenStandby = SCREEN_OFF; + } else if ( state == DPMSModeOn ) { + buffer.screenStandby = SCREEN_ON; + } + } + } + + if ( write( fd, &buffer, sizeof(buffer) ) != sizeof(buffer) ) { + fprintf( stderr, "Error writing to pipe from child\n" ); + } + return 0; +} + +static Window getScreenSaverWindow( Display *dpy ) +{ + XWindowAttributes xgwa; + + char *v = NULL; + Window window = find_screensaver_window( dpy, &v ); + if( ! window ) { + return 0; + } + if( ! v || ! *v ) { + fprintf( stderr, "version property not set on window 0x%x?\n", + ( unsigned int ) window ); + return 0; + } + + /* Select for property change events, so that we can read the response. */ + XGetWindowAttributes( dpy, window, &xgwa ); + XSelectInput( dpy, window, xgwa.your_event_mask | PropertyChangeMask ); + + XClassHint hint = {0}; + XGetClassHint( dpy, window, &hint ); + if( ! hint.res_class ) { + fprintf( stderr, "class hints not set on window 0x%x?\n", + ( unsigned int ) window ); + return 0; + } + return window; +} + +static void getLockState( Display *dpy, struct x11state *state ) +{ + Atom XA_LOCK, XA_BLANK; + Atom XA_SCREENSAVER_STATUS; + XA_SCREENSAVER_STATUS = XInternAtom( dpy, "_SCREENSAVER_STATUS", False ); + XA_LOCK = XInternAtom( dpy, "LOCK", False ); + XA_BLANK = XInternAtom( dpy, "BLANK", False ); + + Window window = getScreenSaverWindow( dpy ); + if ( ! window ) + return; + + Atom type; + int format; + unsigned long nitems, bytesafter; + unsigned char *dataP = 0; + + if( XGetWindowProperty( dpy, + RootWindow( dpy, 0 ), + XA_SCREENSAVER_STATUS, + 0, 999, False, XA_INTEGER, + &type, &format, &nitems, &bytesafter, + &dataP ) + != Success ) { + fprintf( stderr, "Foof foof! No property\n" ); + return; + } + if ( ! type || dataP == NULL ) + return; + + Atom *data = ( Atom * ) dataP; + if( type != XA_INTEGER || nitems < 3 ) { + if( data ) { + free( data ); + } + fprintf( stderr, "bad status format on root window.\n" ); + return; + } + + Atom blanked = ( Atom ) data[0]; + time_t tt = ( time_t ) data[1]; + + if( tt <= ( time_t ) 666000000L ) { /* early 1991 */ + fprintf( stderr, "Bad lock time reported\n" ); + return; + } + + if( blanked == XA_LOCK ) { + // Got valid lock time + state->lockTimestamp = tt; + state->saverState = SAVER_LOCKED; + } else if ( blanked == XA_BLANK ) { + state->lockTimestamp = tt; + state->saverState = SAVER_ACTIVE; + } +} + +static Bool got_badwindow = False; +static int BadWindow_ehandler( Display *dpy, XErrorEvent *error ) +{ + if( error->error_code == BadWindow ) { + got_badwindow = True; + return 0; + } + if( !old_handler ) { + abort(); + } + return ( *old_handler )( dpy, error ); +} + +static Window find_screensaver_window( Display *dpy, char **version ) +{ + Window root = RootWindowOfScreen( DefaultScreenOfDisplay( dpy ) ); + Window root2, parent, *kids; + unsigned int nkids; + Atom XA_SCREENSAVER_VERSION; + XA_SCREENSAVER_VERSION = XInternAtom( dpy, "_SCREENSAVER_VERSION", False ); + + if( version ) { + *version = 0; + } + + if( ! XQueryTree( dpy, root, &root2, &parent, &kids, &nkids ) ) { + fprintf( stderr, "fssw: Cannot query tree\n" ); + return 0; + } + if( root != root2 ) { + fprintf( stderr, "fssw: root != root2\n" ); + return 0; + } + if( parent ) { + fprintf( stderr, "fssw: Got parent!\n" ); + return 0; + } + if( !( kids && nkids ) ) { + return 0; + } + for( unsigned int i = 0; i < nkids; i++ ) { + Atom type; + int format; + unsigned long nitems, bytesafter; + unsigned char *v; + int status; + + /* We're walking the list of root-level windows and trying to find + the one that has a particular property on it. We need to trap + BadWindows errors while doing this, because it's possible that + some random window might get deleted in the meantime. (That + window won't have been the one we're looking for.) + */ + XSync( dpy, False ); + if( old_handler ) { + abort(); + } + got_badwindow = False; + old_handler = XSetErrorHandler( BadWindow_ehandler ); + status = XGetWindowProperty( dpy, kids[i], + XA_SCREENSAVER_VERSION, + 0, 200, False, XA_STRING, + &type, &format, &nitems, &bytesafter, + &v ); + XSync( dpy, False ); + XSetErrorHandler( old_handler ); + old_handler = 0; + + if( got_badwindow ) { + status = BadWindow; + got_badwindow = False; + } + + if( status == Success && type != None ) { + Window ret = kids[i]; + if( version ) { + *version = ( char* )v; + } + XFree( kids ); + return ret; + } + } + + if( kids ) { + XFree( kids ); + } + return 0; +} + diff --git a/src/x11util.h b/src/x11util.h new file mode 100644 index 0000000..6389a7c --- /dev/null +++ b/src/x11util.h @@ -0,0 +1,28 @@ +#ifndef _X11_UTIL_H_ +#define _X11_UTIL_H_ + +#include +#include + +#define SAVER_OFF (0) +#define SAVER_ACTIVE (1) +#define SAVER_LOCKED (2) + +#define SCREEN_UNKNOWN (0) +#define SCREEN_ON (1) +#define SCREEN_OFF (2) + +struct x11state { + time_t lockTimestamp; + int idleSeconds; + int saverState; + int screenStandby; +}; + +bool getX11IdleTimes( const char *xauth, const char *display, struct x11state *state ); + +void setScreenDpms( const char *xauth, const char *display, int mode ); + +void enableScreenSaver( const char *xauth, const char *display, const int state ); + +#endif -- cgit v1.2.3-55-g7522