λ

swayaway

a drop-in replacement for swayidle(1)

settings

screensaver.c

10151 bytes raw

#include "swayaway.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dbus/dbus.h>

#define SS_PATH      "/org/freedesktop/ScreenSaver"
#define SS_IFACE     "org.freedesktop.ScreenSaver"
#define GNOME_PATH   "/org/gnome/screen_saver"
#define GNOME_IFACE  "org.gnome.ScreenSaver"

// this sucks, yes, but who are you to judge
static const char introspect_xml[] =
    "<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\"\n"
    "\"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">\n"
    "<node>\n"
    "  <interface name=\"org.freedesktop.ScreenSaver\">\n"
    "    <method name=\"Inhibit\">\n"
    "      <arg name=\"application_name\" type=\"s\" direction=\"in\"/>\n"
    "      <arg name=\"reason\" type=\"s\" direction=\"in\"/>\n"
    "      <arg name=\"cookie\" type=\"u\" direction=\"out\"/>\n"
    "    </method>\n"
    "    <method name=\"Uninhibit\">\n"
    "      <arg name=\"cookie\" type=\"u\" direction=\"in\"/>\n"
    "    </method>\n"
    "    <method name=\"GetActive\">\n"
    "      <arg name=\"active\" type=\"b\" direction=\"out\"/>\n"
    "    </method>\n"
    "    <method name=\"GetActiveTime\">\n"
    "      <arg name=\"seconds\" type=\"u\" direction=\"out\"/>\n"
    "    </method>\n"
    "    <signal name=\"ActiveChanged\">\n"
    "      <arg name=\"active\" type=\"b\"/>\n"
    "    </signal>\n"
    "  </interface>\n"
    "  <interface name=\"org.gnome.ScreenSaver\">\n"
    "    <method name=\"Inhibit\">\n"
    "      <arg name=\"application_name\" type=\"s\" direction=\"in\"/>\n"
    "      <arg name=\"reason\" type=\"s\" direction=\"in\"/>\n"
    "      <arg name=\"cookie\" type=\"u\" direction=\"out\"/>\n"
    "    </method>\n"
    "    <method name=\"Uninhibit\">\n"
    "      <arg name=\"cookie\" type=\"u\" direction=\"in\"/>\n"
    "    </method>\n"
    "  </interface>\n"
    "  <interface name=\"org.freedesktop.DBus.Introspectable\">\n"
    "    <method name=\"Introspect\"><arg name=\"xml\" type=\"s\" direction=\"out\"/></method>\n"
    "  </interface>\n"
    "</node>\n";

/* last emitted ActiveChanged value; emit only on real transitions to avoid
 * flooding the bus with a signal per keystroke gap */
static bool ss_active = false;

static void recompute(void)
{
	state.screensaver_inhibited = (state.inhibitors != NULL);
	idle_recompute_inhibited();
}

static void remove_inhibitor(struct inhibitor *inh)
{
	struct inhibitor **p = &state.inhibitors;
	while (*p && *p != inh) {
		p = &(*p)->next;
	}
	if (*p == inh) {
		*p = inh->next;
	}
	free(inh->sender);
	free(inh->app);
	free(inh->reason);
	free(inh);
}

static struct inhibitor *find_cookie(uint32_t cookie)
{
	struct inhibitor *inh;
	for (inh = state.inhibitors; inh; inh = inh->next) {
		if (inh->cookie == cookie) {
			return inh;
		}
	}
	return NULL;
}

static void reply_simple(DBusConnection *conn, DBusMessage *msg,
                         int type, void *value)
{
	DBusMessage *reply = dbus_message_new_method_return(msg);
	if (!reply) {
		return;
	}
	dbus_message_append_args(reply, type, value, DBUS_TYPE_INVALID);
	dbus_connection_send(conn, reply, NULL);
	dbus_message_unref(reply);
	dbus_connection_flush(conn);
}

static void reply_error(DBusConnection *conn, DBusMessage *msg,
                        const char *text)
{
	DBusMessage *reply = dbus_message_new_error(
	                         msg, DBUS_ERROR_INVALID_ARGS, text);
	if (reply) {
		dbus_connection_send(conn, reply, NULL);
		dbus_message_unref(reply);
		dbus_connection_flush(conn);
	}
}

static void handle_inhibit(DBusConnection *conn, DBusMessage *msg)
{
	const char *app = NULL;
	const char *reason = NULL;
	DBusError err;
	dbus_error_init(&err);

	if (!dbus_message_get_args(msg, &err, DBUS_TYPE_STRING, &app,
	                           DBUS_TYPE_STRING, &reason, DBUS_TYPE_INVALID)) {
		reply_error(conn, msg, err.message ? err.message : "invalid arguments");
		dbus_error_free(&err);
		return;
	}

	uint32_t cookie = ++state.next_cookie;
	if (cookie == 0) {
		cookie = ++state.next_cookie;
	}

	struct inhibitor *inh = calloc(1, sizeof(*inh));
	if (!inh) {
		reply_error(conn, msg, "out of memory");
		return;
	}
	inh->cookie = cookie;
	const char *sender = dbus_message_get_sender(msg);
	inh->sender = sender ? strdup(sender) : strdup(":unknown");
	inh->app = strdup(app ? app : "");
	inh->reason = strdup(reason ? reason : "");
	inh->next = state.inhibitors;
	state.inhibitors = inh;

	sway_log("inhibit requested by %s (%s): %s",
	         inh->sender, inh->app, inh->reason);
	recompute();

	reply_simple(conn, msg, DBUS_TYPE_UINT32, &cookie);
}

static void handle_uninhibit(DBusConnection *conn, DBusMessage *msg)
{
	uint32_t cookie = 0;
	DBusError err;
	dbus_error_init(&err);

	if (!dbus_message_get_args(msg, &err, DBUS_TYPE_UINT32, &cookie,
	                           DBUS_TYPE_INVALID)) {
		reply_error(conn, msg, err.message ? err.message : "invalid arguments");
		dbus_error_free(&err);
		return;
	}

	struct inhibitor *inh = find_cookie(cookie);
	if (inh) {
		sway_log("uninhibit cookie %u", cookie);
		remove_inhibitor(inh);
		recompute();
	} else {
		sway_log_debug("uninhibit for unknown cookie %u", cookie);
	}
	reply_simple(conn, msg, DBUS_TYPE_INVALID, NULL);
}

static void handle_get_active(DBusConnection *conn, DBusMessage *msg)
{
	dbus_bool_t active = idle_any_timeout_fired();
	reply_simple(conn, msg, DBUS_TYPE_BOOLEAN, &active);
}

static void handle_get_active_time(DBusConnection *conn, DBusMessage *msg)
{
	uint32_t secs = 0;
	if (idle_any_timeout_fired() && state.is_idle) {
		secs = (uint32_t)((mono_ms() - state.idle_since_ms) / 1000);
	}
	reply_simple(conn, msg, DBUS_TYPE_UINT32, &secs);
}

static void handle_name_owner_changed(DBusConnection *conn, DBusMessage *msg)
{
	(void)conn;
	const char *name = NULL, *old_owner = NULL, *new_owner = NULL;
	DBusError err;
	dbus_error_init(&err);

	if (!dbus_message_get_args(msg, &err, DBUS_TYPE_STRING, &name,
	                           DBUS_TYPE_STRING, &old_owner, DBUS_TYPE_STRING, &new_owner,
	                           DBUS_TYPE_INVALID)) {
		dbus_error_free(&err);
		return;
	}

	// a connection died (new_owner empty): release everything it held
	if (name && *name && new_owner && *new_owner == '\0') {
		struct inhibitor *inh = state.inhibitors;
		while (inh) {
			struct inhibitor *next = inh->next;
			if (strcmp(inh->sender, name) == 0) {
				sway_log("client %s disconnected, releasing its inhibitors",
				         name);
				remove_inhibitor(inh);
			}
			inh = next;
		}
		recompute();
	}
}

static DBusHandlerResult filter(DBusConnection *conn, DBusMessage *msg,
                                void *ud)
{
	(void)ud;

	if (dbus_message_is_method_call(msg, "org.freedesktop.DBus.Introspectable",
	                                "Introspect")) {
		const char *xml = introspect_xml;
		reply_simple(conn, msg, DBUS_TYPE_STRING, &xml);
		return DBUS_HANDLER_RESULT_HANDLED;
	}

	if (dbus_message_is_method_call(msg, SS_IFACE, "Inhibit") ||
	        dbus_message_is_method_call(msg, GNOME_IFACE, "Inhibit")) {
		handle_inhibit(conn, msg);
		return DBUS_HANDLER_RESULT_HANDLED;
	}
	if (dbus_message_is_method_call(msg, SS_IFACE, "Uninhibit") ||
	        dbus_message_is_method_call(msg, GNOME_IFACE, "Uninhibit")) {
		handle_uninhibit(conn, msg);
		return DBUS_HANDLER_RESULT_HANDLED;
	}
	if (dbus_message_is_method_call(msg, SS_IFACE, "GetActive")) {
		handle_get_active(conn, msg);
		return DBUS_HANDLER_RESULT_HANDLED;
	}
	if (dbus_message_is_method_call(msg, SS_IFACE, "GetActiveTime")) {
		handle_get_active_time(conn, msg);
		return DBUS_HANDLER_RESULT_HANDLED;
	}

	if (dbus_message_is_signal(msg, DBUS_INTERFACE_DBUS, "NameOwnerChanged")) {
		handle_name_owner_changed(conn, msg);
		return DBUS_HANDLER_RESULT_HANDLED;
	}

	return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}

/* Emit ActiveChanged only when the "screensaver active" state (at least one
 * timeout command fired) actually transitions. */
void screensaver_emit_active(void)
{
	if (!state.session_conn) {
		return;
	}
	bool now = idle_any_timeout_fired();
	if (now == ss_active) {
		return;
	}
	ss_active = now;

	DBusMessage *sig = dbus_message_new_signal(SS_PATH, SS_IFACE,
	                   "ActiveChanged");
	if (!sig) {
		return;
	}
	dbus_bool_t active = now;
	dbus_message_append_args(sig, DBUS_TYPE_BOOLEAN, &active,
	                         DBUS_TYPE_INVALID);
	dbus_connection_send(state.session_conn, sig, NULL);
	dbus_message_unref(sig);
	dbus_connection_flush(state.session_conn);
}

static void request_name(DBusConnection *conn, const char *name)
{
	DBusError err;
	dbus_error_init(&err);
	int ret = dbus_bus_request_name(conn, name,
	                                DBUS_NAME_FLAG_DO_NOT_QUEUE, &err);
	if (ret == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) {
		sway_log("exporting %s", name);
	} else {
		sway_log("could not acquire %s (%s); inhibitor service degraded",
		         name, err.message ? err.message : "unknown error");
	}
	dbus_error_free(&err);
}

void screensaver_init(void)
{
	DBusError err;
	dbus_error_init(&err);

	state.session_conn = dbus_bus_get(DBUS_BUS_SESSION, &err);
	if (state.session_conn == NULL) {
		sway_log("could not connect to the session bus: %s",
		         err.message ? err.message : "unknown error");
		dbus_error_free(&err);
		return;
	}
	dbus_error_free(&err);
	dbus_connection_set_exit_on_disconnect(state.session_conn, FALSE);
	dbus_connection_get_unix_fd(state.session_conn, &state.session_fd);

	dbus_connection_add_filter(state.session_conn, filter, NULL, NULL);

	DBusError match_err;
	dbus_error_init(&match_err);
	dbus_bus_add_match(state.session_conn,
	                   "type='signal',interface='org.freedesktop.DBus',"
	                   "member='NameOwnerChanged',path='/org/freedesktop/DBus'",
	                   &match_err);
	if (dbus_error_is_set(&match_err)) {
		sway_log("could not add NameOwnerChanged match: %s",
		         match_err.message);
	}
	dbus_error_free(&match_err);

	request_name(state.session_conn, SS_IFACE);
	request_name(state.session_conn, GNOME_IFACE);
}

bool screensaver_have_service(void)
{
	return state.session_conn != NULL;
}