diff --git a/.gitignore b/.gitignore
index 1ce9cc3..a6e6abd 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 988fbd4..1b20037 100644
--- a/SConstruct
+++ b/SConstruct
@@ -64,12 +64,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)
@@ -122,14 +125,18 @@ if 'package' in COMMAND_LINE_TARGETS:
# Specify components for the osx distribution pkg
client_home = '.'
client_root = client_home + '/build/pkg/root'
- pkg_files = [[str(client[0]), 'usr/local/bin/', 0o755],
- ['build/install/osx/fahclient.url',
+ pkg_files = [['build/install/osx/fahclient.url',
'Applications/Folding@home/fahclient.url', 0o644],
['build/install/osx/uninstall.url',
'Applications/Folding@home/uninstall.url', 0o644],
['build/install/osx/launchd.plist',
'Library/LaunchDaemons/' +
- 'org.foldingathome.fahclient.plist', 0o644]]
+ 'org.foldingathome.fahclient.plist', 0o644],
+ ['build/install/osx/fah-screen-agent.plist','Library/LaunchAgents/' +
+ 'org.foldingathome.fah-screen-agent.plist', 0o644]]
+ for tool in client + tools:
+ pkg_files += [[str(tool), 'usr/local/bin/', 0o755]]
+
pkg_components = [
{
# name is component pkg file name and name shown in installer
@@ -145,7 +152,7 @@ if 'package' in COMMAND_LINE_TARGETS:
# default build/pkg/root, as per cbang config pkg module
'root' : client_root,
# relative to root
- 'sign_tools' : ['usr/local/bin/fah-client'],
+ 'sign_tools' : ['usr/local/bin/*'],
'must_close_apps': [
'org.foldingathome.fahviewer',
'org.foldingathome.fahcontrol',
@@ -220,7 +227,6 @@ if 'package' in COMMAND_LINE_TARGETS:
pkg_background = 'fah-opacity-50.png',
pkg_customize = 'always',
pkg_target = pkg_target,
- pkg_arch = env.get('package_arch', 'x86_64'),
pkg_components = pkg_components,
)
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 94fdd17..7ccdba1 100755
--- a/install/osx/scripts/postinstall
+++ b/install/osx/scripts/postinstall
@@ -11,16 +11,28 @@ 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
+
# Don't launch GUI if CLI install
[ "$COMMAND_LINE_INSTALL" == "1" ] && exit 0
diff --git a/install/osx/scripts/preinstall b/install/osx/scripts/preinstall
index 71ae8df..ff07158 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 49f2985..1b59ab2 100644
--- a/src/fah/client/osx/OSXOSImpl.cpp
+++ b/src/fah/client/osx/OSXOSImpl.cpp
@@ -49,6 +49,8 @@
#include
#include
+#include
+
using namespace FAH::Client;
using namespace cb;
using namespace std;
@@ -66,6 +68,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) {
@@ -94,6 +99,17 @@ namespace {
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();
+ }
}
@@ -204,7 +220,7 @@ void OSXOSImpl::run() {
void OSXOSImpl::finishInit() {
- LOG_DEBUG(5, "OSXOSImpl::finishInit() on thread " << pthread_self() <<
+ LOG_DEBUG(5, "OSXOSImpl::finishInit() on thread " << pthread_self() <<
(pthread_main_np() ? " main" : ""));
// Init display power state if registration succeeded
@@ -222,8 +238,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;
event->activate();
@@ -483,6 +501,45 @@ 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();
@@ -496,16 +553,18 @@ bool OSXOSImpl::registerForDarwinNotifications() {
CFStringRef name =
CFStringCreateWithCString(0, key.c_str(), kCFStringEncodingUTF8);
- if (name) {
- CFNotificationCenterAddObserver(
- nc, (void *)this, ¬eQuitCB, name, 0,
- CFNotificationSuspensionBehaviorCoalesce);
- CFRelease(name);
+ if (!name) return false;
+ CFNotificationCenterAddObserver(nc, (void *)this, ¬eQuitCB,
+ name, 0, CFNotificationSuspensionBehaviorCoalesce);
+ CFRelease(name);
- return true;
- }
+ CFNotificationCenterAddObserver(nc, (void *)this, &screenIdleCB,
+ kScreenIdle, 0, CFNotificationSuspensionBehaviorCoalesce);
- return false;
+ CFNotificationCenterAddObserver(nc, (void *)this, &screenNotIdleCB,
+ kScreenNotIdle, 0, CFNotificationSuspensionBehaviorCoalesce);
+
+ return true;
}
diff --git a/src/fah/client/osx/OSXOSImpl.h b/src/fah/client/osx/OSXOSImpl.h
index 6dbad2d..87b2c89 100644
--- a/src/fah/client/osx/OSXOSImpl.h
+++ b/src/fah/client/osx/OSXOSImpl.h
@@ -46,10 +46,15 @@ namespace FAH {
static OSXOSImpl *singleton;
std::atomic systemIsIdle = false;
- bool screensaverIsActive = false;
- bool screenIsLocked = false;
+
bool loginwindowIsActive = false;
+ // these are notification received bools; both are needed
+ bool screenIdle = false;
+ bool screenNotIdle = false;
+ dispatch_time_t screenIdleExpiry = 0;
+ dispatch_time_t screenNotIdleExpiry = 0;
+
io_service_t displayWrangler = 0;
IONotificationPortRef displayNotePort = 0;
CFRunLoopSourceRef displayNoteSource = 0;
@@ -87,11 +92,15 @@ namespace FAH {
(void *context, io_service_t service, natural_t mtype, void *marg);
void finishInit();
void updateTimerFired(CFRunLoopTimerRef timer, void *info);
+ void noteScreenIdle();
+ void noteScreenNotIdle();
protected:
void initialize();
void addHeartbeatTimerToRunLoop(CFRunLoopRef loop);
bool registerForConsoleUserNotifications();
+ bool gotScreenIdleRecently();
+ bool gotScreenNotIdleRecently();
bool registerForDarwinNotifications();
bool registerForDisplayPowerNotifications();
bool registerForLaunchEvents();
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')