summaryrefslogtreecommitdiffstats
path: root/driver/xscreensaver-systemd.c
diff options
context:
space:
mode:
Diffstat (limited to 'driver/xscreensaver-systemd.c')
-rw-r--r--driver/xscreensaver-systemd.c1090
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());
}