summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimon Rettberg2019-05-10 15:25:43 +0200
committerSimon Rettberg2019-05-10 15:25:43 +0200
commitf7cd45464fd7c037f6a60098ae77760998b3b4b6 (patch)
tree02cf10b2766fc5bb47e44d7253d4ec4eb4a674d3
downloadidle-daemon-f7cd45464fd7c037f6a60098ae77760998b3b4b6.tar.gz
idle-daemon-f7cd45464fd7c037f6a60098ae77760998b3b4b6.tar.xz
idle-daemon-f7cd45464fd7c037f6a60098ae77760998b3b4b6.zip
Initial commit
-rw-r--r--.gitignore6
-rw-r--r--CMakeLists.txt43
-rw-r--r--src/main.c498
-rw-r--r--src/main.h10
-rw-r--r--src/rpc.c132
-rw-r--r--src/rpc.h10
-rw-r--r--src/userlist.c232
-rw-r--r--src/userlist.h31
-rw-r--r--src/util.c187
-rw-r--r--src/util.h30
-rw-r--r--src/warn.c86
-rw-r--r--src/warn.h18
-rw-r--r--src/x11util.c405
-rw-r--r--src/x11util.h28
14 files changed, 1716 insertions, 0 deletions
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 <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;
+}
+
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 <time.h>
+
+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 <poll.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/un.h>
+#include <stdio.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdlib.h>
+
+#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 <stdlib.h>
+#include <utmp.h>
+#include <errno.h>
+#include <string.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <time.h>
+#include <ctype.h>
+#include <signal.h>
+
+#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 <time.h>
+#include <stdbool.h>
+
+#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 <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <unistd.h>
+#include <stdarg.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <sys/stat.h>
+#include <signal.h>
+#include <pwd.h>
+#include <poll.h>
+
+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 <time.h>
+#include <stdbool.h>
+
+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 <stdio.h>
+#include <stdlib.h>
+
+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 <X11/Xlib.h>
+#include <X11/Xatom.h>
+#include <X11/Xutil.h>
+#include <X11/extensions/dpms.h>
+#include <X11/extensions/scrnsaver.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <string.h>
+
+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 <stdbool.h>
+#include <time.h>
+
+#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