From 8b5a49d6b864790990befc3051578ee6ad1f39a0 Mon Sep 17 00:00:00 2001
From: Kevin Bernhagen <6762784+kbernhagen@users.noreply.github.com>
Date: Tue, 7 Sep 2021 18:02:54 -0700
Subject: [PATCH] Support osx idle on screensaver, screenlock
Add fah-screen-agent
Support idle/not idle notifications
Add tools.scons
---
.gitignore | 1 +
SConstruct | 17 +-
install/osx/fah-screen-agent.plist | 25 ++
install/osx/scripts/postinstall | 14 +-
install/osx/scripts/preinstall | 10 +
src/fah/client/osx/OSXOSImpl.cpp | 73 +++++-
src/fah/client/osx/OSXOSImpl.h | 12 +-
src/fah/screen-agent/defines.h | 39 +++
src/fah/screen-agent/screen-agent.c | 362 ++++++++++++++++++++++++++++
src/osx-screen-agent.scons | 27 +++
src/tools.scons | 19 ++
11 files changed, 589 insertions(+), 10 deletions(-)
create mode 100644 install/osx/fah-screen-agent.plist
create mode 100644 src/fah/screen-agent/defines.h
create mode 100644 src/fah/screen-agent/screen-agent.c
create mode 100644 src/osx-screen-agent.scons
create mode 100644 src/tools.scons
diff --git a/.gitignore b/.gitignore
index 7512d31..49b1211 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,7 @@
*.app
# Build byproducts
+/bin
/build
/package.txt
/package-description.txt
diff --git a/SConstruct b/SConstruct
index 749476f..1a6b952 100644
--- a/SConstruct
+++ b/SConstruct
@@ -67,12 +67,15 @@ if env['PLATFORM'] == 'win32' or int(env.get('cross_mingw', 0)):
duplicate = 0)
Default(hide_console)
+tools = SConscript('src/tools.scons', variant_dir = 'build', duplicate = 0)
+Default(tools)
+Depends(tools, client)
# Clean
-Clean(client, ['build', 'config.log'])
+Clean(client, ['bin', 'build', 'config.log'])
# Dist
docs = ['README.md', 'CHANGELOG.md', 'LICENSE']
-distfiles = docs + [client, 'images/fahlogo.png']
+distfiles = docs + client + tools + ['images/fahlogo.png']
if env['PLATFORM'] == 'posix':
distfiles.append('install/lin/fah-client.service')
if hide_console is not None: distfiles.append(hide_console)
@@ -116,7 +119,7 @@ if 'package' in COMMAND_LINE_TARGETS:
home = '.', # abs path or relative to PWD
pkg_scripts = 'build/install/osx/scripts', # relative to home
root = './build/pkg/root', # abs path or relative to PWD
- sign_tools = ['usr/local/bin/fah-client'], # relative to root
+ sign_tools = ['usr/local/bin/fah-*'], # relative to root
must_close_apps = [
'org.foldingathome.fahviewer',
'org.foldingathome.fahcontrol',
@@ -124,12 +127,14 @@ if 'package' in COMMAND_LINE_TARGETS:
'edu.stanford.folding.fahcontrol',
],
pkg_files = [
- [str(client[0]), 'usr/local/bin/', 0o755],
['scripts/fahctl', 'usr/local/bin/', 0o755],
['build/install/osx/fahclient.url',
'Applications/Folding@home/Folding@home.url', 0o644],
['build/install/osx/uninstall.url',
'Applications/Folding@home/uninstall.url', 0o644],
+ ['build/install/osx/fah-screen-agent.plist',
+ 'Library/LaunchAgents/' +
+ 'org.foldingathome.fah-screen-agent.plist', 0o644],
['build/install/osx/launchd.plist',
'Library/LaunchDaemons/' +
'org.foldingathome.fahclient.plist', 0o644]
@@ -137,6 +142,10 @@ if 'package' in COMMAND_LINE_TARGETS:
pre_sign_callback = seticon,
)]
+ pkg_files = pkg_components[0]['pkg_files']
+ for tool in client + tools:
+ pkg_files += [[str(tool), 'usr/local/bin/', 0o755]]
+
# min pkg target macos 10.13
pkg_target = env.get('osx_min_ver', '10.13')
ver = tuple([int(x) for x in pkg_target.split('.')])
diff --git a/install/osx/fah-screen-agent.plist b/install/osx/fah-screen-agent.plist
new file mode 100644
index 0000000..3ed8ac2
--- /dev/null
+++ b/install/osx/fah-screen-agent.plist
@@ -0,0 +1,25 @@
+
+
+
+
+ KeepAlive
+
+ SuccessfulExit
+
+
+ Label
+ org.foldingathome.fah-screen-agent
+ LimitLoadToSessionType
+ Aqua
+ LowPriorityIO
+
+ Program
+ /usr/local/bin/fah-screen-agent
+ RunAtLoad
+
+ StandardOutPath
+ /dev/null
+ Umask
+ 18
+
+
diff --git a/install/osx/scripts/postinstall b/install/osx/scripts/postinstall
index 357e565..6d44711 100755
--- a/install/osx/scripts/postinstall
+++ b/install/osx/scripts/postinstall
@@ -11,12 +11,24 @@ chmod -R u+rwX,go-w "$RUN_DIR"
chown -R nobody:nobody "$RUN_DIR"
PLIST=/Library/LaunchDaemons/org.foldingathome.fahclient.plist
+AGENT_PLIST="/Library/LaunchAgents/org.foldingathome.fah-screen-agent.plist"
"$SCRIPTS"/organize-credits.sh &
# Start service
-chmod 0644 "$PLIST"
+chmod 0644 "$PLIST" "$AGENT_PLIST"
launchctl load -w "$PLIST"
# start, in case RunAtLoad is false
launchctl start org.foldingathome.fahclient || true
+
+# restart any running agents in other sessions
+# note that this does not reload their launchd jobs
+killall -HUP fah-screen-agent || true
+
+# start agent in user's gui session
+conuser=$(/usr/bin/stat -f "%Su" /dev/console) || conuser="root"
+conuid=$(/usr/bin/id -u "$conuser") || conuid=0
+if [[ $conuid != 0 ]]; then
+ launchctl bootstrap gui/$conuid "$AGENT_PLIST" || true
+fi
diff --git a/install/osx/scripts/preinstall b/install/osx/scripts/preinstall
index 61aa4f3..babca8c 100755
--- a/install/osx/scripts/preinstall
+++ b/install/osx/scripts/preinstall
@@ -10,6 +10,7 @@ SCRIPTS="$(dirname "$0")"
OLD_LAUNCHD="/Library/LaunchDaemons/FAHClient.plist"
OLD_LAUNCHD2="/Library/LaunchDaemons/edu.stanford.folding.fahclient.plist"
NEW_LAUNCHD="/Library/LaunchDaemons/org.foldingathome.fahclient.plist"
+AGENT_PLIST="/Library/LaunchAgents/org.foldingathome.fah-screen-agent.plist"
if [ -f "$NEW_LAUNCHD" ]; then
launchctl unload -w "$NEW_LAUNCHD" || true
@@ -23,6 +24,15 @@ if [ -f "$OLD_LAUNCHD" ]; then
rm -f "$OLD_LAUNCHD" || true
fi
+if [ -f "$AGENT_PLIST" ]; then
+ # stop agent running in console user's session and unload job
+ conuser=$(/usr/bin/stat -f "%Su" /dev/console) || conuser="root"
+ conuid=$(/usr/bin/id -u "$conuser") || conuid=0
+ if [[ $conuid != 0 ]]; then
+ launchctl bootout gui/$conuid "$AGENT_PLIST" || true
+ fi
+fi
+
# Assuming upgrade, remove old stuff
F1="/Applications/FAHClient.url"
F2="/Applications/Folding@home/Web Control.url"
diff --git a/src/fah/client/osx/OSXOSImpl.cpp b/src/fah/client/osx/OSXOSImpl.cpp
index 9631f0b..57a1bc6 100644
--- a/src/fah/client/osx/OSXOSImpl.cpp
+++ b/src/fah/client/osx/OSXOSImpl.cpp
@@ -46,6 +46,8 @@
#include
#include
+#include
+
using namespace FAH::Client;
using namespace cb;
using namespace std;
@@ -60,6 +62,9 @@ enum {
namespace {
+ CFStringRef kScreenIdle = CFSTR(SCREEN_IDLE_NOTIFICATION);
+ CFStringRef kScreenNotIdle = CFSTR(SCREEN_NOT_IDLE_NOTIFICATION);
+
#pragma mark c callbacks
void consoleUserCB(SCDynamicStoreRef s, CFArrayRef keys, void *info) {
OSXOSImpl::instance().consoleUserChanged(s, keys, info);
@@ -85,6 +90,20 @@ namespace {
<< CFStringGetCStringPtr(name, kCFStringEncodingUTF8));
OSXOSImpl::instance().requestExit();
}
+
+
+ void screenIdleCB(CFNotificationCenterRef center, void *observer,
+ CFNotificationName name, const void *object,
+ CFDictionaryRef info) {
+ OSXOSImpl::instance().noteScreenIdle();
+ }
+
+
+ void screenNotIdleCB(CFNotificationCenterRef center, void *observer,
+ CFNotificationName name, const void *object,
+ CFDictionaryRef info) {
+ OSXOSImpl::instance().noteScreenNotIdle();
+ }
}
@@ -187,9 +206,10 @@ void OSXOSImpl::finishInit() {
void OSXOSImpl::updateSystemIdle() {
- bool shouldBeIdle = displayPower == kDisplayPowerOff || loginwindowIsActive ||
- screensaverIsActive || screenIsLocked;
-
+ bool shouldBeIdle;
+ if (gotScreenNotIdleRecently()) shouldBeIdle = false;
+ else shouldBeIdle = displayPower == kDisplayPowerOff || loginwindowIsActive ||
+ gotScreenIdleRecently();
if (shouldBeIdle == systemIsIdle) return;
systemIsIdle = shouldBeIdle;
@@ -415,10 +435,57 @@ bool OSXOSImpl::registerForConsoleUserNotifications() {
}
+void OSXOSImpl::noteScreenIdle() {
+ screenIdleExpiry =
+ dispatch_time(DISPATCH_TIME_NOW, SCREEN_NOTIFICATION_EXPIRES * NSEC_PER_SEC);
+ bool wasIdle = screenIdle;
+ screenIdle = true;
+ dispatch_after(screenIdleExpiry + NSEC_PER_SEC, dispatch_get_main_queue(), ^{
+ updateSystemIdle();
+ });
+ if (!wasIdle) delayedUpdateSystemIdle(5);
+}
+
+
+void OSXOSImpl::noteScreenNotIdle() {
+ screenNotIdleExpiry =
+ dispatch_time(DISPATCH_TIME_NOW, SCREEN_NOTIFICATION_EXPIRES * NSEC_PER_SEC);
+ screenNotIdle = true;
+ dispatch_after(screenNotIdleExpiry + NSEC_PER_SEC,dispatch_get_main_queue(),^{
+ updateSystemIdle();
+ });
+ updateSystemIdle();
+}
+
+
+bool OSXOSImpl::gotScreenIdleRecently() {
+ if (!screenIdle) return false;
+ if (dispatch_time(DISPATCH_TIME_NOW, 0) < screenIdleExpiry) return true;
+ screenIdle = false;
+ return false;
+}
+
+
+bool OSXOSImpl::gotScreenNotIdleRecently() {
+ if (!screenNotIdle) return false;
+ if (dispatch_time(DISPATCH_TIME_NOW, 0) < screenNotIdleExpiry) return true;
+ screenNotIdle = false;
+ return false;
+}
+
+
bool OSXOSImpl::registerForDarwinNotifications() {
CFNotificationCenterRef nc = CFNotificationCenterGetDarwinNotifyCenter();
if (!nc) return false;
+ CFNotificationCenterAddObserver(
+ nc, (void *)this, &screenIdleCB, kScreenIdle, 0,
+ CFNotificationSuspensionBehaviorCoalesce);
+
+ CFNotificationCenterAddObserver(
+ nc, (void *)this, &screenNotIdleCB, kScreenNotIdle, 0,
+ CFNotificationSuspensionBehaviorCoalesce);
+
string user = "nobody";
struct passwd *pwent = getpwuid(getuid());
if (pwent && pwent->pw_name) user = pwent->pw_name;
diff --git a/src/fah/client/osx/OSXOSImpl.h b/src/fah/client/osx/OSXOSImpl.h
index 95ea013..f6da907 100644
--- a/src/fah/client/osx/OSXOSImpl.h
+++ b/src/fah/client/osx/OSXOSImpl.h
@@ -47,8 +47,6 @@ namespace FAH {
static OSXOSImpl *singleton;
std::atomic systemIsIdle = false;
- bool screensaverIsActive = false;
- bool screenIsLocked = false;
bool loginwindowIsActive = false;
io_service_t displayWrangler = 0;
@@ -62,6 +60,12 @@ namespace FAH {
int displayPower = 0;
+ // these are notification received bools; both are needed
+ bool screenIdle = false;
+ bool screenNotIdle = false;
+ dispatch_time_t screenIdleExpiry = 0;
+ dispatch_time_t screenNotIdleExpiry = 0;
+
public:
OSXOSImpl(App &app);
~OSXOSImpl();
@@ -82,12 +86,16 @@ namespace FAH {
void displayPowerChanged
(void *context, io_service_t service, natural_t mtype, void *marg);
void finishInit();
+ void noteScreenIdle();
+ void noteScreenNotIdle();
protected:
void initialize();
void addHeartbeatTimerToRunLoop(CFRunLoopRef loop);
void deregisterForConsoleUserNotifications();
bool registerForConsoleUserNotifications();
+ bool gotScreenIdleRecently();
+ bool gotScreenNotIdleRecently();
bool registerForDarwinNotifications();
void deregisterForDisplayPowerNotifications();
bool registerForDisplayPowerNotifications();
diff --git a/src/fah/screen-agent/defines.h b/src/fah/screen-agent/defines.h
new file mode 100644
index 0000000..0ec2de0
--- /dev/null
+++ b/src/fah/screen-agent/defines.h
@@ -0,0 +1,39 @@
+/******************************************************************************\
+
+ This file is part of the Folding@home Client.
+
+ The fah-client runs Folding@home protein folding simulations.
+ Copyright (c) 2001-2023, foldingathome.org
+ All rights reserved.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+ For information regarding this software email:
+ Joseph Coffland
+ joseph@cauldrondevelopment.com
+
+\******************************************************************************/
+
+#pragma once
+
+// Defines shared with fah-client
+
+#define SCREEN_IDLE_NOTIFICATION "org.foldingathome.screen.idle"
+#define SCREEN_NOT_IDLE_NOTIFICATION "org.foldingathome.screen.notidle"
+
+#define SCREEN_NOTIFICATION_INTERVAL 60
+#define SCREEN_NOTIFICATION_LEEWAY 2
+#define SCREEN_NOTIFICATION_EXPIRES (\
+ SCREEN_NOTIFICATION_INTERVAL + 2 * SCREEN_NOTIFICATION_LEEWAY + 1)
diff --git a/src/fah/screen-agent/screen-agent.c b/src/fah/screen-agent/screen-agent.c
new file mode 100644
index 0000000..485d5d3
--- /dev/null
+++ b/src/fah/screen-agent/screen-agent.c
@@ -0,0 +1,362 @@
+/******************************************************************************\
+
+ This file is part of the Folding@home Client.
+
+ The fah-client runs Folding@home protein folding simulations.
+ Copyright (c) 2001-2023, foldingathome.org
+ All rights reserved.
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+ For information regarding this software email:
+ Joseph Coffland
+ joseph@cauldrondevelopment.com
+
+\******************************************************************************/
+
+// Observe screensaver start/stop, screen lock/unlock notifications.
+// Periodically post darwin notifications for screen idle/notidle.
+// These notifications are observed by fah-client, and expire
+// Agent will exit non-zero on SIGHUP, possibly to be restarted by launchd.
+
+#ifndef __APPLE__
+#error "This can only be compiled for macOS"
+#endif
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "defines.h"
+
+#define AGENT_LABEL "org.foldingathome.fah-screen-agent"
+
+#define STRINGIFY(x) XSTRINGIFY(x)
+#define XSTRINGIFY(x) #x
+#ifndef VERSION
+#define VERSION 0.0.0
+#endif
+static const char *version = STRINGIFY(VERSION);
+
+struct _DTimer {
+ dispatch_source_t ds;
+ dispatch_time_t start;
+ uint64_t interval;
+ uint64_t leeway;
+};
+typedef struct _DTimer DTimer;
+
+static DTimer timer;
+
+static dispatch_source_t sig_ds[NSIG] = {0};
+
+static int quit_sig = 0;
+static bool restart = false;
+static CFAbsoluteTime start_time = 0;
+static os_log_t logger = OS_LOG_DEFAULT;
+
+static bool screenIsLocked = false;
+static bool screensaverIsActive = false;
+static bool screenIdle = false;
+
+static CFNotificationCenterRef nc;
+static const char *observer = AGENT_LABEL;
+
+const char *kScreenIdle = SCREEN_IDLE_NOTIFICATION;
+const char *kScreenNotIdle = SCREEN_NOT_IDLE_NOTIFICATION;
+
+
+#if DEBUG
+static void print_time() {
+ time_t now = time(0);
+ struct tm sTm;
+ char buff[24];
+ gmtime_r(&now, &sTm);
+ strftime(buff, sizeof(buff), "%F %TZ", &sTm);
+ printf("%s ", buff);
+}
+
+#define LOGDEBUG(format, ...) do { \
+ if (isatty(STDOUT_FILENO)) { \
+ print_time(); \
+ printf(format, ## __VA_ARGS__); \
+ printf("\n"); \
+ } else os_log_debug(logger, format, ## __VA_ARGS__); \
+ } while(0)
+
+#else
+#define LOGDEBUG(format, ...)
+
+#endif
+
+
+static void notify() {
+ LOGDEBUG("%s %s", __FUNCTION__, screenIdle ? kScreenIdle : kScreenNotIdle);
+ notify_post(screenIdle ? kScreenIdle : kScreenNotIdle);
+}
+
+
+static void create_timer() {
+ // periodically repeat notifications, which expire in client
+ dispatch_queue_t q = dispatch_get_main_queue();
+ timer.ds = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, q);
+ timer.interval = SCREEN_NOTIFICATION_INTERVAL * NSEC_PER_SEC;
+ timer.leeway = SCREEN_NOTIFICATION_LEEWAY * NSEC_PER_SEC;
+ timer.start = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC / 4);
+ assert(timer.ds);
+ dispatch_source_set_timer(timer.ds,timer.start,timer.interval,timer.leeway);
+ dispatch_source_set_event_handler(timer.ds, ^{notify();});
+ dispatch_resume(timer.ds);
+}
+
+
+static void reset_timer() {
+ timer.start = dispatch_time(DISPATCH_TIME_NOW, timer.interval);
+ dispatch_source_set_timer(timer.ds,timer.start,timer.interval,timer.leeway);
+}
+
+
+static void update() {
+ LOGDEBUG("%s", __FUNCTION__);
+ bool shouldBeIdle = screenIsLocked || screensaverIsActive;
+ if (screenIdle == shouldBeIdle) return;
+ screenIdle = shouldBeIdle;
+ reset_timer();
+ notify();
+}
+
+
+void screenDidLock(CFNotificationCenterRef center, void *observer,
+ CFStringRef name, const void *object, CFDictionaryRef userInfo) {
+ LOGDEBUG("%s", __FUNCTION__);
+ screenIsLocked = true;
+ update();
+}
+
+
+void screenDidUnlock(CFNotificationCenterRef center, void *observer,
+ CFStringRef name, const void *object, CFDictionaryRef userInfo) {
+ LOGDEBUG("%s", __FUNCTION__);
+ screenIsLocked = false;
+ update();
+}
+
+
+void screensaverDidStart(CFNotificationCenterRef center, void *observer,
+ CFStringRef name, const void *object, CFDictionaryRef userInfo) {
+ LOGDEBUG("%s", __FUNCTION__);
+ screensaverIsActive = true;
+ update();
+}
+
+
+void screensaverDidStop(CFNotificationCenterRef center, void *observer,
+ CFStringRef name, const void *object, CFDictionaryRef userInfo) {
+ LOGDEBUG("%s", __FUNCTION__);
+ screensaverIsActive = false;
+ update();
+}
+
+
+struct note_item {
+ CFStringRef in;
+ CFNotificationCallback cb;
+};
+
+
+static const struct note_item notes[] = {
+ {CFSTR("com.apple.screensaver.didstart"), screensaverDidStart},
+ {CFSTR("com.apple.screensaver.didstop"), screensaverDidStop},
+ {CFSTR("com.apple.screenIsLocked"), screenDidLock},
+ {CFSTR("com.apple.screenIsUnlocked"), screenDidUnlock},
+ {NULL, NULL}
+};
+
+
+static void register_for_launch_events() {
+ // register for xpc launchd.plist LaunchEvents
+ // this MUST be done if launchd might have LaunchEvents for us
+ // we never un-register
+ dispatch_queue_t q = dispatch_get_main_queue();
+ xpc_set_event_stream_handler("com.apple.iokit.matching", q,
+ ^(xpc_object_t event) {
+ // DO NOTHING but consume event
+#if DEBUG
+ const char *ename = xpc_dictionary_get_string(event, XPC_EVENT_KEY_NAME);
+ LOGDEBUG("LaunchEvents IO: %s", ename);
+#endif
+ });
+
+ xpc_set_event_stream_handler("com.apple.notifyd.matching", q,
+ ^(xpc_object_t event) {
+ // DO NOTHING but consume event
+#if DEBUG
+ const char *nname = xpc_dictionary_get_string(event, "Notification");
+ LOGDEBUG("LaunchEvents notification: %s", nname);
+#endif
+ });
+}
+
+
+static void got_signal(int sig) {
+ // dispatch callback, NOT a signal handler
+ LOGDEBUG("%s %s", __FUNCTION__, sys_signame[sig]);
+ switch (sig) {
+ case SIGTERM:
+ quit_sig = sig;
+ // fallthrough
+ case SIGINT:
+ CFRunLoopStop(CFRunLoopGetMain());
+ break;
+ case SIGHUP:
+ restart = true;
+ CFRunLoopStop(CFRunLoopGetMain());
+ break;
+ default: break;
+ }
+}
+
+
+static void dummy_signal_handler(int sig) {}
+
+
+static void install_signal_handlers() {
+ sigset_t sigs;
+ sigemptyset(&sigs);
+
+ sigaddset(&sigs, SIGTERM);
+ sigaddset(&sigs, SIGINT);
+ sigaddset(&sigs, SIGHUP);
+
+ struct sigaction action = {0};
+ action.sa_handler = SIG_IGN;
+ sigaction(SIGPIPE, &action, NULL);
+
+ dispatch_queue_t q = dispatch_get_main_queue();
+ // dummy handler instead of SIG_IGN so syscalls can still be interrupted
+ action.sa_handler = dummy_signal_handler;
+ for(int s = 1; s < NSIG; s++) {
+ if (sigismember(&sigs, s)) {
+ sigaction(s, &action, NULL);
+ sig_ds[s] = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, s, 0, q);
+ assert(sig_ds[s]);
+ dispatch_source_set_event_handler(sig_ds[s], ^{got_signal(s);});
+ dispatch_resume(sig_ds[s]);
+ }
+ }
+}
+
+
+static void release_signal_handlers() {
+ for(int s = 1; s < NSIG; s++)
+ if (sig_ds[s]) dispatch_release(sig_ds[s]);
+}
+
+
+static void usage(int argc, const char * argv[]) {
+ printf("usage: %s [{start|stop}]\n", argv[0]);
+ printf(" start convenience for /bin/launchctl start %s\n", AGENT_LABEL);
+ printf(" stop convenience for /bin/launchctl stop %s\n", AGENT_LABEL);
+ printf(" -h,--help\n");
+ printf(" --version\n");
+}
+
+
+static void parse_args(int argc, const char * argv[]) {
+ if (getppid() != 1 && 1 < argc) {
+ const char *prog = "/bin/launchctl";
+ if (strcmp("start", argv[1]) == 0 || strcmp("stop", argv[1]) == 0) {
+ printf("%s %s %s\n", prog, argv[1], AGENT_LABEL);
+ execl(prog, prog, argv[1], AGENT_LABEL);
+ }
+ if (strcmp("--help", argv[1]) == 0 || strcmp("-h", argv[1]) == 0) {
+ usage(argc, argv);
+ } else if (strcmp("--version", argv[1]) == 0) {
+ printf("%s\n", version);
+ } else {
+ printf("error: unknown argument: %s\n", argv[1]);
+ usage(argc, argv);
+ exit(EX_USAGE);
+ }
+ exit(0);
+ }
+}
+
+
+static void init(int argc, const char * argv[]) {
+ start_time = CFAbsoluteTimeGetCurrent();
+#if DEBUG
+ logger = os_log_create(AGENT_LABEL, "main");
+#endif
+ nc = CFNotificationCenterGetDistributedCenter();
+
+ if (geteuid() == 0) {
+ fprintf(stderr, "error: %s cannot be run as root\n", argv[0]);
+ exit(1);
+ }
+ // FIXME: refuse to run if not GUI session
+
+ parse_args(argc, argv);
+
+ install_signal_handlers();
+ register_for_launch_events();
+
+ // TODO: maybe get initial values from CGSessionCopyCurrentDictionary
+ // https://stackoverflow.com/a/11511419
+ // this should always be correct for a fresh gui login
+ screenIsLocked = false;
+ screensaverIsActive = false;
+ screenIdle = false;
+
+ // set up notifications
+ for (int i = 0; notes[i].in; i++)
+ CFNotificationCenterAddObserver(nc, observer, notes[i].cb,
+ notes[i].in, NULL, CFNotificationSuspensionBehaviorCoalesce);
+
+ create_timer();
+}
+
+
+int main(int argc, const char * argv[]) {
+ init(argc, argv);
+ CFRunLoopRun();
+
+ // any critical cleanup would go here
+
+ // exit immediately if think launchd signaled us
+ if (quit_sig) exit(0);
+
+ // exit non-zero if want to restart (assume appropriate KeepAlive)
+ if (restart) exit(EX_TEMPFAIL);
+
+#if DEBUG
+ // non-critical cleanup
+ CFNotificationCenterRemoveEveryObserver(nc, observer);
+ dispatch_source_cancel(timer.ds);
+ dispatch_release(timer.ds);
+ release_signal_handlers();
+#endif
+
+ // run 10+ sec or launchd may think we crashed and relaunch us
+ if (getppid() == 1) {
+ CFTimeInterval run_time = CFAbsoluteTimeGetCurrent() - start_time;
+ if (run_time < 10.0) sleep(10 - (int)run_time);
+ }
+
+ return 0;
+}
diff --git a/src/osx-screen-agent.scons b/src/osx-screen-agent.scons
new file mode 100644
index 0000000..cccb795
--- /dev/null
+++ b/src/osx-screen-agent.scons
@@ -0,0 +1,27 @@
+Import('*')
+
+env = env.Clone()
+env.Replace(LIBS = [])
+env.Replace(LIBPATH = [])
+env.Replace(CPPPATH = [])
+env.Replace(FRAMEWORKS = ['CoreFoundation'])
+
+try:
+ env['LINKFLAGS'].remove('-stdlib=libc++')
+except: pass
+
+if not (env.GetOption('clean') or env.get('debug')):
+ try:
+ env.AppendUnique(CCFLAGS = ['-Os'])
+ env['CCFLAGS'].remove('-O3')
+ env['CCFLAGS'].remove('-funroll-loops')
+ except: pass
+
+version = env.get('PACKAGE_VERSION')
+if version: env.CBDefine('VERSION=' + version)
+
+src = Glob('fah/screen-agent/*.c')
+
+tool = env.Program('#/bin/fah-screen-agent', src)
+
+Return('tool')
diff --git a/src/tools.scons b/src/tools.scons
new file mode 100644
index 0000000..91f94f8
--- /dev/null
+++ b/src/tools.scons
@@ -0,0 +1,19 @@
+Import('*')
+
+tools_scons = Glob('all-*.scons')
+
+if env['PLATFORM'] == 'win32' or int(env.get('cross_mingw', 0)):
+ tools_scons += Glob('win-*.scons')
+elif env['PLATFORM'] == 'darwin':
+ tools_scons += Glob('osx-*.scons')
+else:
+ tools_scons += Glob('lin-*.scons')
+
+Export('env')
+
+tools = []
+for tscons in tools_scons:
+ tool = SConscript(tscons, variant_dir = '#/build', duplicate = 0)
+ if tool: tools += tool
+
+Return('tools')