diff options
Diffstat (limited to 'driver/xscreensaver-systemd.c')
-rw-r--r-- | driver/xscreensaver-systemd.c | 1090 |
1 files changed, 920 insertions, 170 deletions
diff --git a/driver/xscreensaver-systemd.c b/driver/xscreensaver-systemd.c index a46ed4d..d06174a 100644 --- a/driver/xscreensaver-systemd.c +++ b/driver/xscreensaver-systemd.c @@ -1,4 +1,5 @@ -/* xscreensaver-systemd, Copyright (c) 2019 Martin Lucina <martin@lucina.net> +/* xscreensaver-systemd, Copyright (c) 2019-2021 + * Martin Lucina <martin@lucina.net> and Jamie Zawinski <jwz@jwz.org> * * ISC License * @@ -16,217 +17,966 @@ * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * - * This is a small utility providing systemd integration for XScreenSaver. * - * When run from ~/.xsession or equivalent, this will: + * This utility provides systemd integration for XScreenSaver. + * It does two things: * - * - Lock the screen before the system goes to sleep (using - * xscreensaver-command -suspend). + * - When the system is about to go to sleep (e.g., laptop lid closing) + * it locks the screen *before* the system goes to sleep, by running + * "xscreensaver-command -suspend". And then when the system wakes + * up again, it runs "xscreensaver-command -deactivate" to force the + * unlock dialog to appear immediately. * - * - Ensure the XScreenSaver password dialog is shown after the system - * is resumed (using xscreensaver-command -deactivate). + * - When another process on the system makes asks for the screen saver + * to be inhibited (e.g. because a video is playing) this program + * periodically runs "xscreensaver-command -deactivate" to keep the + * display un-blanked. It does this until the other program asks for + * it to stop. * - * This is implemented using the recommended way to do these things - * nowadays, namely inhibitor locks. sd-bus is used for DBUS communication, - * so the only dependency is libsystemd (which you already have if you - * want this). + * For this to work at all, you must prevent Gnome and KDE from usurping + * the "org.freedesktop.ScreenSaver" messages, or else this program can't + * receive them. The "xscreensaver" man page contains the (complicated) + * installation instructions. + * + * Background: + * + * For decades, the traditional way for a video player to temporarily + * inhibit the screen saver was to have a heartbeat command that ran + * "xscreensaver-command -deactivate" once a minute while the video was + * playing, and ceased when the video was paused or stopped. The reason + * to do it as a heartbeat rather than a toggle is so that the player + * fails SAFE -- if the player exits abnormally, the heart stops beating, + * and screen saving and locking resumes. + * + * These days, the popular apps do this by using systemd. The design of + * the systemd method easily and trivially allows an app to inhibit the + * screen saver, crash, and then never un-inhibit it, so now your screen + * will never blank again. + * + * Furthermore, since the systemd method uses cookies to ensure that only + * the app that sent "inhibit" can send the matching "uninhibit", simply + * re-launching the crashed video player does not fix the problem. + * + * "Did IQs just drop sharply while I was away?" -- Ellen Ripley + * + * We can sometimes detect that the inhibiting app has exited abnormally + * by using "tracking peers" but I'm not sure how reliable that is. + * + * Furthermore, we can't listen for these "inhibit blanking" requests + * if some other program is already listening for them -- which Gnome and + * KDE do by default, even if their screen savers are otherwise disabled. + * That makes it far more complicated for the user to install XScreenSaver + * in such a way that "xscreensaver-systemd" can even launch at all. + * + * To recap: because the existing video players decided to delete the + * single line of code that they already had -- the heartbeat call to + * "xscreensaver-command -deactivate" -- we had to respond by adding a + * THOUSAND LINES of complicated code that talks to a server that may + * not be running, and that may not allow us to connect, and that may + * not work properly anyway, and that makes installation hellaciously + * difficult and confusing for the end user. + * + * This is what is laughingly referred to as "progress". + * + * So here's what we're dealing with now, with the various apps that + * you might use to play video on Linux at the end of 2020: + * + * + ***************************************************************************** + * + * Firefox (version 78.5) + * + * When playing media, Firefox will send "inhibit" to one of these + * targets: "org.freedesktop.ScreenSaver" or "org.gnome.SessionManager". + * + * However, Firefox decides which, if any, of those to use at launch time, + * and does not revisit that decision. So if xscreensaver-systemd has not + * been launched before Firefox, it won't work. Fortunately, in most use + * cases, xscreensaver will have been launched earlier in the startup + * sequence than the web browser. + * + * If you close the tab or exit while playing, Firefox sends "uninhibit". + * + * Critical Firefox Bug: + * + * If Firefox crashes or is killed while playing, it never sends + * "uninhibit", leaving the screen saver permanently inhibited. Once + * that happens, the only way to un-fuck things is to kill and restart + * the "xscreensaver-systemd" program. + * + * Annoying Firefox Bug: + * + * Firefox sends an "inhibit" message when it is merely playing audio. + * That's horrible. Playing audio should prevent your machine from going + * to sleep, but it should NOT prevent your screen from blanking or + * locking. + * + * However at least it sends it with the reason "audio-playing" instead + * of "video-playing", meaning we can (and do) special-case Firefox and + * ignore that one. + * + * + ***************************************************************************** + * + * Chrome (version 87) + * + * Sends "inhibit" to "org.freedesktop.ScreenSaver" (though it uses a + * a different object path than Firefox does). Unlike Firefox, Chrome + * does not send an "inhibit" message when only audio is playing. + * + * Critical Chrome Bug: + * + * If Chrome crashes or is killed while playing, it never sends + * "uninhibit", leaving the screen saver permanently inhibited. + * + * + ***************************************************************************** + * + * Chromium (version 78, Raspbian 10.4) + * + * Does not use "org.freedesktop.ScreenSaver" or "xdg-screensaver". + * It appears to make no attempt to inhibit the screen saver while + * video is playing. + * + * + ***************************************************************************** + * + * Chromium (version 84.0.4147.141, Raspbian 10.6) + * + * Sends "inhibit" to "org.freedesktop.ScreenSaver" (though it uses a + * a different object path than Firefox does). Unlike Firefox, Chrome + * does not send an "inhibit" message when only audio is playing. + * + * If you close the tab or exit while playing, Chromium sends "uninhibit". + * + * Critical Chromium Bug: + * + * If Chromium crashes or is killed while playing, it never sends + * "uninhibit", leaving the screen saver permanently inhibited. + * + * Annoying Chromium Bug: + * + * Like Firefox, Chromium sends an "inhibit" message when it is merely + * playing audio. Unlike Firefox, it sends exactly the same "reason" + * string as it does when playing video, so we can't tell them apart. + * + * + ***************************************************************************** + * + * MPV (version 0.29.1) + * + * While playing, it runs "xdg-screensaver reset" every 10 seconds as a + * heartbeat. That program is a super-complicated shell script that will + * eventually run "xscreensaver-command -reset". So MPV talks to the + * xscreensaver daemon directly rather than going through systemd. + * That's fine. + * + * On Debian 10.4 and 10.6, MPV does not have a dependency on the + * "xdg-utils" package, so "xdg-screensaver" might not be installed. + * Oddly, Chromium *does* have a dependency on "xdg-utils", even though + * Chromium doesn't run "xdg-screensaver". + * + * The source code suggests that MPlayer and MPV call XResetScreenSaver() + * as well, but only affects the X11 server's built-in screen saver, not + * a userspace screen locker like xscreensaver. + * + * They also call XScreenSaverSuspend() which is part of the MIT + * SCREEN-SAVER server extension. XScreenSaver does make use of that + * extension because it is worse than useless. See the commentary at + * the top of xscreensaver.c for details. + * + * Annoying MPV Bug: + * + * Like Firefox and Chromium, MPV inhibits screen blanking when only + * audio is playing. + * + * + ***************************************************************************** + * + * MPlayer (version mplayer-gui 2:1.3.0) + * + * I can't get this thing to play video at all. It only plays the audio + * of MP4 files, so I can't guess what it might or might not do with video. + * It appears to make no attempt to inhibit the screen saver. + * + * + ***************************************************************************** + * + * VLC (version 3.0.11-0+deb10u1+rpt3) + * + * VLC sends "inhibit" to "org.freedesktop.ScreenSaver" when playing + * video. It does not send "inhibit" when playing audio only, and it + * sends "uninhibit" under all the right circumstances. + * + * NOTE: that's what I saw when I tested it on Raspbian 10.6. However, + * the version that came with Raspbian 10.4 -- which also called itself + * "VLC 3.0.11" -- did not send "uninhibit" when using the window + * manager's "close" button! Or when killed with "kill". + * + * NOTE ALSO: The VLC source code suggests that under some circumstances + * it might be talking to these instead: "org.freedesktop.ScreenSaver", + * "org.freedesktop.PowerManagement.Inhibit", "org.mate.SessionManager", + * and/or "org.gnome.SessionManager". It also contains code to run + * "xdg-screensaver reset" as a heartbeat. I can't tell how it decides + * which system to use. I have never seen it run "xdg-screensaver". + * + * + ***************************************************************************** + * + * Zoom + * + * I'm told that the proprietary Zoom executable for Linux sends "inhibit" + * to "org.freedesktop.ScreenSaver", but I don't have any further details. + * + ***************************************************************************** + * + * TO DO: + * + * - What precisely does the standalone Zoom executable do on Linux? + * There doesn't seem to be a Raspbian build, so I can't test it. + * + * - xscreensaver_method_uninhibit() does not actually send a reply, are + * we doing the right thing when registering it? + * + * - Currently this code is only listening to "org.freedesktop.ScreenSaver". + * Perhaps it should listen to "org.mate.SessionManager" and + * "org.gnome.SessionManager"? Where are those documented? + * + * - Do we need to call sd_bus_release_name() explicitly on exit? + * + * - Run under valgrind to check for any memory leaks. + * + * - Apparently the two different desktops have managed to come up with + * *three* different ways for dbus clients to ask the question, "is the + * screen currently blanked?" We should probably also respond to these: + * + * qdbus org.freedesktop.ScreenSaver /ScreenSaver org.freedesktop.ScreenSaver.GetActive + * qdbus org.kde.screensaver /ScreenSaver org.freedesktop.ScreenSaver.GetActive + * qdbus org.gnome.ScreenSaver /ScreenSaver org.gnome.ScreenSaver.GetActive + * + * + * + * TESTING: + * + * To call the D-BUS methods manually, you can use "busctl": + * + * busctl --user call org.freedesktop.ScreenSaver \ + * /ScreenSaver org.freedesktop.ScreenSaver \ + * Inhibit ss test-application test-reason + * + * This will hand out a cookie, which you can pass back to UnInhibit: + * + * u 1792821391 + * + * busctl --user call org.freedesktop.ScreenSaver \ + * /ScreenSaver org.freedesktop.ScreenSaver \ + * UnInhibit u 1792821391 * * https://github.com/mato/xscreensaver-systemd */ -#include <assert.h> -#include <err.h> +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#define _GNU_SOURCE +#include <stdio.h> +#include <poll.h> #include <errno.h> +#include <stdint.h> #include <stdlib.h> +#include <string.h> #include <sys/wait.h> #include <unistd.h> +#include <time.h> +#include <sys/time.h> +#include <sys/types.h> +#include <signal.h> +#include <X11/Xlib.h> #include <systemd/sd-bus.h> +#include "version.h" +#include "blurb.h" +#include "yarandom.h" +#include "queue.h" + +static char *screensaver_version; + +#define DBUS_CLIENT_NAME "org.jwz.XScreenSaver" +#define DBUS_SD_SERVICE_NAME "org.freedesktop.login1" +#define DBUS_SD_OBJECT_PATH "/org/freedesktop/login1" +#define DBUS_SD_INTERFACE "org.freedesktop.login1.Manager" +#define DBUS_SD_METHOD "Inhibit" +#define DBUS_SD_METHOD_ARGS "ssss" +#define DBUS_SD_METHOD_WHAT "sleep" +#define DBUS_SD_METHOD_WHO "xscreensaver" +#define DBUS_SD_METHOD_WHY "lock screen on suspend" +#define DBUS_SD_METHOD_MODE "delay" + +#define DBUS_SD_MATCH "type='signal'," \ + "interface='" DBUS_SD_INTERFACE "'," \ + "member='PrepareForSleep'" + +#define DBUS_FDO_NAME "org.freedesktop.ScreenSaver" +#define DBUS_FDO_OBJECT_PATH "/ScreenSaver" /* Firefox */ +#define DBUS_FDO_OBJECT_PATH_2 "/org/freedesktop/ScreenSaver" /* Chrome */ +#define DBUS_FDO_INTERFACE "org.freedesktop.ScreenSaver" + +#define HEARTBEAT_INTERVAL 50 /* seconds */ + +#undef countof +#define countof(x) (sizeof((x))/sizeof((*x))) + + struct handler_ctx { - sd_bus *bus; - sd_bus_message *lock; + sd_bus *system_bus; + sd_bus_message *lock_message; + int lock_fd; + int is_inhibited; + sd_bus_track *track; +}; + +static struct handler_ctx global_ctx = { NULL, NULL, -1, 0, NULL }; + +SLIST_HEAD(inhibit_head, inhibit_entry) inhibit_head = + SLIST_HEAD_INITIALIZER(inhibit_head); + +struct inhibit_entry { + uint32_t cookie; + time_t start_time; + char *appname; + char *peer; + SLIST_ENTRY(inhibit_entry) entries; }; -static struct handler_ctx global_ctx = { NULL, NULL }; -static int handler(sd_bus_message *m, void *arg, - sd_bus_error *ret_error) + +static void +xscreensaver_command (const char *cmd) { - struct handler_ctx *ctx = arg; - int before_sleep; - int rc; - sd_bus_error error = SD_BUS_ERROR_NULL; - sd_bus_message *reply = NULL; - int fd; - - rc = sd_bus_message_read(m, "b", &before_sleep); - if (rc < 0) { - warnx("Failed to read message: %s", strerror(-rc)); - return 0; - } + char buf[1024]; + int rc; + sprintf (buf, "xscreensaver-command %.100s -%.100s", + (verbose_p ? "-verbose" : "-quiet"), + cmd); + if (verbose_p) + fprintf (stderr, "%s: exec: %s\n", blurb(), buf); + rc = system (buf); + if (rc == -1) + fprintf (stderr, "%s: exec failed: %s\n", blurb(), buf); + else if (WEXITSTATUS(rc) != 0) + fprintf (stderr, "%s: exec: \"%s\" exited with status %d\n", + blurb(), buf, WEXITSTATUS(rc)); +} - /* Use the scheme described at - * https://www.freedesktop.org/wiki/Software/systemd/inhibit/ - * under "Taking Delay Locks". - */ - if (before_sleep) { - rc = system("xscreensaver-command -suspend"); - if (rc == -1) { - warnx("Failed to run xscreensaver-command"); - } - else if (WEXITSTATUS(rc) != 0) { - warnx("xscreensaver-command failed with %d", WEXITSTATUS(rc)); - } - if (ctx->lock) { - /* - * This will release the lock, since we hold the only ref to the - * message, and sd_bus_message_unref() will close the underlying - * fd. - */ - sd_bus_message_unref(ctx->lock); - ctx->lock = NULL; - } - else { - warnx("Warning: ctx->lock is NULL, this should not happen?"); - } +static int +xscreensaver_register_sleep_lock (struct handler_ctx *ctx) +{ + sd_bus_error error = SD_BUS_ERROR_NULL; + sd_bus_message *reply = NULL; + int fd = -1; + int rc = sd_bus_call_method (ctx->system_bus, + DBUS_SD_SERVICE_NAME, DBUS_SD_OBJECT_PATH, + DBUS_SD_INTERFACE, DBUS_SD_METHOD, + &error, &reply, + DBUS_SD_METHOD_ARGS, + DBUS_SD_METHOD_WHAT, DBUS_SD_METHOD_WHO, + DBUS_SD_METHOD_WHY, DBUS_SD_METHOD_MODE); + if (rc < 0) { + fprintf (stderr, "%s: inhibit sleep failed: %s\n", + blurb(), error.message); + goto DONE; + } + + /* Save the lock fd and explicitly take a ref to the lock message. */ + rc = sd_bus_message_read (reply, "h", &fd); + if (rc < 0 || fd < 0) { + fprintf (stderr, "%s: inhibit sleep failed: no lock fd: %s\n", + blurb(), strerror(-rc)); + goto DONE; + } + sd_bus_message_ref(reply); + ctx->lock_message = reply; + ctx->lock_fd = fd; + + DONE: + sd_bus_error_free (&error); + + return rc; +} + + +/* Called when DBUS_SD_INTERFACE sends a "PrepareForSleep" signal. + The event is sent twice: before sleep, and after. + */ +static int +xscreensaver_systemd_handler (sd_bus_message *m, void *arg, + sd_bus_error *ret_error) +{ + struct handler_ctx *ctx = arg; + int before_sleep; + int rc; + + rc = sd_bus_message_read (m, "b", &before_sleep); + if (rc < 0) { + fprintf (stderr, "%s: message read failed: %s\n", + blurb(), strerror(-rc)); + return 1; /* >= 0 means success */ + } + + /* Use the scheme described at + https://www.freedesktop.org/wiki/Software/systemd/inhibit/ + under "Taking Delay Locks". + */ + + if (before_sleep) { + /* Tell xscreensaver that we are suspending, and to lock if desired. */ + xscreensaver_command ("suspend"); + + if (ctx->lock_message) { + /* Release the lock, meaning we are done and it's ok to sleep now. + Don't rely on unref'ing the message to close the fd, do that + explicitly here. + */ + close(ctx->lock_fd); + sd_bus_message_unref (ctx->lock_message); + ctx->lock_message = NULL; + ctx->lock_fd = -1; + } else { + fprintf (stderr, "%s: no context lock\n", blurb()); } - else { - rc = system("xscreensaver-command -deactivate"); - if (rc == -1) { - warnx("Failed to run xscreensaver-command"); - } - else if (WEXITSTATUS(rc) != 0) { - warnx("xscreensaver-command exited with %d", WEXITSTATUS(rc)); - } + } else { + /* Tell xscreensaver to present the unlock dialog right now. */ + xscreensaver_command ("deactivate"); - rc = sd_bus_call_method(ctx->bus, - "org.freedesktop.login1", - "/org/freedesktop/login1", - "org.freedesktop.login1.Manager", - "Inhibit", - &error, - &reply, - "ssss", - "sleep", - "xscreensaver", - "lock screen on suspend", - "delay"); - if (rc < 0) { - warnx("Failed to call Inhibit(): %s", error.message); - goto out; - } - /* - * Verify that the reply actually contains a lock fd. - */ - rc = sd_bus_message_read(reply, "h", &fd); + /* We woke from sleep, so we need to re-register for the next sleep. */ + rc = xscreensaver_register_sleep_lock (ctx); + if (rc < 0) + fprintf (stderr, "%s: could not re-register sleep lock\n", blurb()); + } + + return 1; /* >= 0 means success */ +} + + +/* Called from the vtable when another process sends a request to systemd + to inhibit the screen saver. We return to them a cookie which they must + present with their "uninhibit" request. + */ +static int +xscreensaver_method_inhibit (sd_bus_message *m, void *arg, + sd_bus_error *ret_error) +{ + struct handler_ctx *ctx = arg; + const char *application_name = 0, *inhibit_reason = 0; + struct inhibit_entry *entry = 0; + const char *s; + const char *sender; + + int rc = sd_bus_message_read(m, "ss", &application_name, &inhibit_reason); + if (rc < 0) { + fprintf (stderr, "%s: failed to parse method call: %s\n", + blurb(), strerror(-rc)); + return rc; + } + + if (!application_name || !*application_name) { + fprintf (stderr, "%s: no app name in method call\n", blurb()); + return -1; + } + + if (!inhibit_reason || !*inhibit_reason) { + fprintf (stderr, "%s: no reason in method call from \"%s\"\n", + blurb(), application_name); + return -1; + } + + sender = sd_bus_message_get_sender (m); + + /* Omit directory (Chrome does this shit) */ + s = strrchr (application_name, '/'); + if (s && s[1]) application_name = s+1; + + if (strcasestr (inhibit_reason, "audio") && + !strcasestr (inhibit_reason, "video")) { + /* Firefox 78 sends an inhibit when playing audio only, with reason + "audio-playing". This is horrible. Ignore it. (But perhaps it + would be better to accept it, issue them a cookie, and then just + ignore that entry?) */ + if (verbose_p) + fprintf (stderr, "%s: inhibited by \"%s\" (%s) with \"%s\", ignored\n", + blurb(), application_name, sender, inhibit_reason); + return -1; + } + + /* Tell the global tracker object to monitor when this peer exits. */ + rc = sd_bus_track_add_name(ctx->track, sender); + if (rc < 0) { + fprintf (stderr, "%s: failed to track peer \"%s\": %s\n", + blurb(), sender, strerror(-rc)); + sender = NULL; + } + + entry = malloc(sizeof (struct inhibit_entry)); + entry->cookie = ya_random(); + entry->appname = strdup(application_name); + entry->peer = sender ? strdup(sender) : NULL; + entry->start_time = time ((time_t *)0); + SLIST_INSERT_HEAD(&inhibit_head, entry, entries); + ctx->is_inhibited++; + if (verbose_p) + fprintf (stderr, "%s: inhibited by \"%s\" (%s) with \"%s\"" + " -> cookie %08X\n", + blurb(), application_name, sender, inhibit_reason, entry->cookie); + + return sd_bus_reply_method_return (m, "u", entry->cookie); +} + + +/* Called from the vtable when another process sends a request to systemd + to uninhibit the screen saver. The cookie must match an earlier "inhibit" + request. + */ +static int +xscreensaver_method_uninhibit (sd_bus_message *m, void *arg, + sd_bus_error *ret_error) +{ + struct handler_ctx *ctx = arg; + uint32_t cookie; + struct inhibit_entry *entry; + int found = 0; + const char *sender; + + int rc = sd_bus_message_read (m, "u", &cookie); + if (rc < 0) { + fprintf (stderr, "%s: failed to parse method call: %s\n", + blurb(), strerror(-rc)); + return rc; + } + + sender = sd_bus_message_get_sender (m); + + SLIST_FOREACH(entry, &inhibit_head, entries) { + if (entry->cookie == cookie) { + if (verbose_p) + fprintf (stderr, "%s: uninhibited by \"%s\" (%s) with cookie %08X\n", + blurb(), entry->appname, sender, cookie); + SLIST_REMOVE (&inhibit_head, entry, inhibit_entry, entries); + if (entry->appname) free (entry->appname); + if (entry->peer) { + rc = sd_bus_track_remove_name(ctx->track, entry->peer); if (rc < 0) { - warnx("Failed to read message: %s", strerror(-rc)); - goto out; + fprintf (stderr, "%s: failed to stop tracking peer \"%s\": %s\n", + blurb(), entry->peer, strerror(-rc)); } - assert(fd >= 0); - ctx->lock = reply; - -out: - sd_bus_error_free(&error); + free(entry->peer); + } + free(entry); + ctx->is_inhibited--; + if (ctx->is_inhibited < 0) + ctx->is_inhibited = 0; + found = 1; + break; } + } - return 0; + if (! found) + fprintf (stderr, "%s: uninhibit: no match for cookie %08X\n", + blurb(), cookie); + + return sd_bus_reply_method_return (m, ""); } -int main(int argc, char *argv[]) +/* + * This vtable defines the service interface we implement. + */ +static const sd_bus_vtable +xscreensaver_dbus_vtable[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_METHOD("Inhibit", "ss", "u", xscreensaver_method_inhibit, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD("UnInhibit", "u", "", xscreensaver_method_uninhibit, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_VTABLE_END +}; + + +/* The only reason this program connects to X at all is so that it dies + right away when the X server shuts down. Otherwise the process might + linger, still connected to systemd but unable to connect to xscreensaver. + */ +static Display * +open_dpy (void) { - sd_bus *bus = NULL, *user_bus = NULL; - sd_bus_slot *slot = NULL; - struct handler_ctx *ctx = &global_ctx; - sd_bus_error error = SD_BUS_ERROR_NULL; - sd_bus_message *reply = NULL; - int rc; - int fd; - const char *match = - "type='signal',interface='org.freedesktop.login1.Manager'" - ",member='PrepareForSleep'"; - - rc = sd_bus_open_user(&user_bus); - if (rc < 0) { - warnx("Failed to connect to user bus: %s", strerror(-rc)); - goto out; - } - rc = sd_bus_request_name(user_bus, "org.jwz.XScreenSaver", 0); - if (rc < 0) { - warnx("Failed to acquire well-known name: %s", strerror(-rc)); - warnx("Is another copy of xscreensaver-systemd running?"); - goto out; + Display *d; + const char *s = getenv("DISPLAY"); + if (!s || !*s) { + fprintf (stderr, "%s: $DISPLAY unset\n", progname); + exit (1); + } + + d = XOpenDisplay (s); + if (!d) { + fprintf (stderr, "%s: can't open display %s\n", progname, s); + exit (1); + } + + return d; +} + + +static pid_t +get_bus_name_pid (sd_bus *bus, const char *name) +{ + int rc; + sd_bus_creds *creds; + pid_t pid; + + rc = sd_bus_get_name_creds (bus, name, SD_BUS_CREDS_PID, &creds); + if (rc == 0) { + rc = sd_bus_creds_get_pid (creds, &pid); + sd_bus_creds_unref (creds); + if (rc == 0) + return pid; + } + + return -1; +} + + +/* This only works on Linux, but it's useful for the error message. + */ +static char * +process_name (pid_t pid) +{ + char fn[100], buf[100], *s; + FILE *fd = 0; + if (pid <= 0) goto FAIL; + /* "comm" truncates at 16 characters. "cmdline" has nulls between args. */ + sprintf (fn, "/proc/%lu/cmdline", (unsigned long) pid); + fd = fopen (fn, "r"); + if (!fd) goto FAIL; + if (!fgets (buf, sizeof(buf)-1, fd)) goto FAIL; + if (fclose (fd) != 0) goto FAIL; + s = strchr (buf, '\n'); + if (s) *s = 0; + return strdup (buf); + FAIL: + if (fd) fclose (fd); + return 0; +} + + +static int +xscreensaver_systemd_loop (void) +{ + sd_bus *system_bus = NULL, *user_bus = NULL; + struct handler_ctx *ctx = &global_ctx; + sd_bus_error error = SD_BUS_ERROR_NULL; + int rc; + time_t last_deactivate_time = 0; + Display *dpy = open_dpy(); + + /* 'user_bus' is where we receive messages from other programs sending + inhibit/uninhibit to org.freedesktop.ScreenSaver, etc. + */ + + rc = sd_bus_open_user (&user_bus); + if (rc < 0) { + fprintf (stderr, "%s: user bus connection failed: %s\n", + blurb(), strerror(-rc)); + goto FAIL; + } + + /* Create a single tracking object so that we can later ask it, + "is the peer with this name still around?" This is how we tell + that Firefox has exited without calling 'uninhibit'. + */ + rc = sd_bus_track_new (user_bus, + &global_ctx.track, + NULL, + NULL); + if (rc < 0) { + fprintf (stderr, "%s: cannot create user bus tracker: %s\n", + blurb(), strerror(-rc)); + goto FAIL; + } + + rc = sd_bus_add_object_vtable (user_bus, + NULL, + DBUS_FDO_OBJECT_PATH, + DBUS_FDO_INTERFACE, + xscreensaver_dbus_vtable, + &global_ctx); + if (rc < 0) { + fprintf (stderr, "%s: vtable registration failed: %s\n", + blurb(), strerror(-rc)); + goto FAIL; + } + + rc = sd_bus_add_object_vtable (user_bus, + NULL, + DBUS_FDO_OBJECT_PATH_2, + DBUS_FDO_INTERFACE, + xscreensaver_dbus_vtable, + &global_ctx); + if (rc < 0) { + fprintf (stderr, "%s: vtable registration failed: %s\n", + blurb(), strerror(-rc)); + goto FAIL; + } + + { + const char * const names[] = { DBUS_FDO_NAME, DBUS_CLIENT_NAME }; + int i = 0; + for (i = 0; i < countof(names); i++) { + rc = sd_bus_request_name (user_bus, names[i], 0); + if (rc < 0) { + pid_t pid = get_bus_name_pid (user_bus, names[i]); + if (pid != -1) { + char *pname = process_name (pid); + if (pname) { + fprintf (stderr, + "%s: connection failed: \"%s\" in use by pid %lu (%s)\n", + blurb(), names[i], (unsigned long) pid, pname); + free (pname); + } else { + fprintf (stderr, + "%s: connection failed: \"%s\" in use by pid %lu\n", + blurb(), names[i], (unsigned long) pid); + } + } else if (-rc == EEXIST || -rc == EALREADY) { + fprintf (stderr, "%s: connection failed: \"%s\" already in use\n", + blurb(), names[i]); + } else { + fprintf (stderr, "%s: connection failed for \"%s\": %s\n", + blurb(), names[i], strerror(-rc)); + } + goto FAIL; + } } + } - rc = sd_bus_open_system(&bus); - if (rc < 0) { - warnx("Failed to connect to system bus: %s", strerror(-rc)); - goto out; + /* 'system_bus' is where we hold a lock on org.freedesktop.login1, meaning + that the system will send us a PrepareForSleep message when the system is + about to suspend. + */ + + rc = sd_bus_open_system (&system_bus); + if (rc < 0) { + fprintf (stderr, "%s: system bus connection failed: %s\n", + blurb(), strerror(-rc)); + goto FAIL; + } + + /* Obtain a lock fd from the "Inhibit" method, so that we can delay + sleep when a "PrepareForSleep" signal is posted. */ + + ctx->system_bus = system_bus; + rc = xscreensaver_register_sleep_lock (ctx); + if (rc < 0) + goto FAIL; + + /* This is basically an event mask, saying that we are interested in + "PrepareForSleep", and to run our callback when that signal is thrown. + */ + rc = sd_bus_add_match (system_bus, NULL, DBUS_SD_MATCH, + xscreensaver_systemd_handler, + &global_ctx); + if (rc < 0) { + fprintf (stderr, "%s: add match failed: %s\n", blurb(), strerror(-rc)); + goto FAIL; + } + + if (verbose_p) + fprintf (stderr, "%s: connected\n", blurb()); + + + /* Run an event loop forever, and wait for our callback to run. + */ + while (1) { + struct pollfd fds[3]; + uint64_t poll_timeout, system_timeout, user_timeout; + struct inhibit_entry *entry; + + /* We MUST call sd_bus_process() on each bus at least once before calling + sd_bus_get_events(), so just always start the event loop by processing + all outstanding requests on both busses. */ + do { + rc = sd_bus_process (system_bus, NULL); + if (rc < 0) { + fprintf (stderr, "%s: failed to process system bus: %s\n", + blurb(), strerror(-rc)); + goto FAIL; + } + } while (rc > 0); + + do { + rc = sd_bus_process (user_bus, NULL); + if (rc < 0) { + fprintf (stderr, "%s: failed to process user bus: %s\n", + blurb(), strerror(-rc)); + goto FAIL; + } + } while (rc > 0); + + fds[0].fd = sd_bus_get_fd (system_bus); + fds[0].events = sd_bus_get_events (system_bus); + fds[0].revents = 0; + + fds[1].fd = sd_bus_get_fd (user_bus); + fds[1].events = sd_bus_get_events (user_bus); + fds[1].revents = 0; + + fds[2].fd = XConnectionNumber (dpy); + fds[2].events = POLLIN; + fds[2].revents = 0; + + + sd_bus_get_timeout (system_bus, &system_timeout); + sd_bus_get_timeout (user_bus, &user_timeout); + + if (system_timeout == 0 && user_timeout == 0) + poll_timeout = 0; + else if (system_timeout == UINT64_MAX && user_timeout == UINT64_MAX) + poll_timeout = -1; + else { + poll_timeout = (system_timeout < user_timeout + ? system_timeout : user_timeout); + poll_timeout /= 1000000; } - ctx->bus = bus; - - rc = sd_bus_call_method(bus, - "org.freedesktop.login1", - "/org/freedesktop/login1", - "org.freedesktop.login1.Manager", - "Inhibit", - &error, - &reply, - "ssss", - "sleep", - "xscreensaver", - "lock screen on suspend", - "delay"); - if (rc < 0) { - warnx("Failed to call Inhibit(): %s", error.message); - goto out; + + /* Prune any entries whose original sender has gone away: this happens + if a program inhibits, then exits without having called uninhibit. + That would have left us inhibited forever, even if the inhibiting + program was re-launched, since the new instance won't have the + same cookie. */ + SLIST_FOREACH (entry, &inhibit_head, entries) { + if (entry->peer && + !sd_bus_track_count_name (ctx->track, entry->peer)) { + if (verbose_p) + fprintf (stderr, + "%s: peer %s for inhibiting app \"%s\" has died:" + " uninhibiting %08X\n", + blurb(), + entry->peer, + entry->appname, + entry->cookie); + SLIST_REMOVE (&inhibit_head, entry, inhibit_entry, entries); + if (entry->appname) free (entry->appname); + free(entry->peer); + free (entry); + ctx->is_inhibited--; + if (ctx->is_inhibited < 0) + ctx->is_inhibited = 0; + } } - /* - * Verify that the reply actually contains a lock fd. + + + /* We want to wake up at least once every N seconds to de-activate + the screensaver if we have been inhibited. */ - rc = sd_bus_message_read(reply, "h", &fd); + if (poll_timeout > HEARTBEAT_INTERVAL * 1000) + poll_timeout = HEARTBEAT_INTERVAL * 1000; + + rc = poll (fds, 3, poll_timeout); if (rc < 0) { - warnx("Failed to read message: %s", strerror(-rc)); - goto out; + fprintf (stderr, "%s: poll failed: %s\n", blurb(), strerror(-rc)); + exit (EXIT_FAILURE); } - assert(fd >= 0); - ctx->lock = reply; - rc = sd_bus_add_match(bus, &slot, match, handler, &global_ctx); - if (rc < 0) { - warnx("Failed to add match: %s", strerror(-rc)); - goto out; + if (fds[2].revents & (POLLERR | POLLHUP | POLLNVAL)) { + fprintf (stderr, "%s: X connection closed\n", blurb()); + goto FAIL; } - for (;;) { - rc = sd_bus_process(bus, NULL); - if (rc < 0) { - warnx("Failed to process bus: %s", strerror(-rc)); - goto out; + if (ctx->is_inhibited) { + time_t now = time ((time_t *) 0); + if (now - last_deactivate_time >= HEARTBEAT_INTERVAL) { + if (verbose_p) { + SLIST_FOREACH (entry, &inhibit_head, entries) { + char ct[100]; + ctime_r (&entry->start_time, ct); + fprintf (stderr, "%s: inhibited by \"%s\" since %s", + blurb(), entry->appname, ct); + } } - if (rc > 0) - /* we processed a request, try to process another one, right-away */ - continue; + xscreensaver_command ("deactivate"); + last_deactivate_time = now; + } + } + } - /* Wait for the next request to process */ - rc = sd_bus_wait(bus, (uint64_t) -1); - if (rc < 0) { - warnx("Failed to wait on bus: %s", strerror(-rc)); - goto out; - } + FAIL: + + XCloseDisplay(dpy); + + if (system_bus) + sd_bus_flush_close_unref (system_bus); + + if (ctx->track) + sd_bus_track_unref (ctx->track); + + if (user_bus) + sd_bus_flush_close_unref (user_bus); + + sd_bus_error_free (&error); + + return EXIT_FAILURE; +} + + +static char *usage = "\n\ +usage: %s [-verbose]\n\ +\n\ +This program is launched by the xscreensaver daemon to monitor DBus.\n\ +It invokes 'xscreensaver-command' to tell the xscreensaver daemon to lock\n\ +the screen before the system suspends, e.g., when a laptop's lid is closed.\n\ +\n\ +It also responds to certain messages sent by media players allowing them to\n\ +request that the screen not be blanked during playback.\n\ +\n\ +From XScreenSaver %s, (c) 1991-%s Jamie Zawinski <jwz@jwz.org>.\n"; + + +#define USAGE() do { \ + fprintf (stderr, usage, progname, screensaver_version, year); exit (1); \ + } while(0) + + +int +main (int argc, char **argv) +{ + int i; + char *version = strdup (screensaver_id + 17); + char *year = strchr (version, '-'); + char *s = strchr (version, ' '); + *s = 0; + year = strchr (year+1, '-') + 1; + s = strchr (year, ')'); + *s = 0; + + screensaver_version = version; + + progname = argv[0]; + s = strrchr (progname, '/'); + if (s) progname = s+1; + + for (i = 1; i < argc; i++) + { + const char *s = argv [i]; + int L; + if (s[0] == '-' && s[1] == '-') s++; + L = strlen (s); + if (L < 2) USAGE (); + else if (!strncmp (s, "-verbose", L)) verbose_p = 1; + else if (!strncmp (s, "-quiet", L)) verbose_p = 0; + else USAGE (); } -out: - if (reply) - sd_bus_message_unref(reply); - if (slot) - sd_bus_slot_unref(slot); - if (bus) - sd_bus_flush_close_unref(bus); - if (user_bus) - sd_bus_flush_close_unref(user_bus); - sd_bus_error_free(&error); - - return EXIT_FAILURE; +# undef ya_rand_init + ya_rand_init (0); + + exit (xscreensaver_systemd_loop()); } |