#!/usr/bin/env python3
"""LinuxDoors GNOME 51 Session Manager -- a minimal, real implementation
of org.gnome.SessionManager, providing just enough of the real interface
for GNOME Shell's own system-menu "Log Out" action to actually work in
this hand-built headless session, which has no real gnome-session of its
own at all (see linuxdoors-gnome51-vnc). Interface copied verbatim from
gnome-shell's own data/dbus-interfaces/org.gnome.SessionManager.xml.
"""
import subprocess

import gi
gi.require_version("Gio", "2.0")
from gi.repository import Gio, GLib

INTERFACE_XML = """
<node>
  <interface name="org.gnome.SessionManager">
    <method name="Logout">
      <arg type="u" direction="in"/>
    </method>
    <method name="Shutdown"/>
    <method name="Reboot"/>
    <method name="Suspend"/>
    <method name="CanShutdown">
      <arg type="u" direction="out"/>
    </method>
    <method name="CanReboot">
      <arg type="u" direction="out"/>
    </method>
    <method name="CanSuspend">
      <arg type="u" direction="out"/>
    </method>
    <method name="IsInhibited">
      <arg type="u" direction="in"/>
      <arg type="b" direction="out"/>
    </method>
    <property name="SessionIsActive" type="b" access="read"/>
    <signal name="InhibitorAdded">
      <arg type="o" direction="out"/>
    </signal>
    <signal name="InhibitorRemoved">
      <arg type="o" direction="out"/>
    </signal>
  </interface>
</node>
"""

# org.gnome.SessionManager's real ActionAvailability enum -- 0 is
# UNAVAILABLE (confirmed against GnomeSession.ActionAvailability in
# gnome-shell's own js/misc/gnomeSession.js).
UNAVAILABLE = 0


def end_session():
    # A real logout: tear the whole headless session down rather than
    # restarting it the way linuxdoors-gnome51-resolution's resize does
    # -- logging out should mean logging back in fresh, not come
    # straight back up at the same size. setsid + detached, same
    # reasoning already documented in linuxdoors-gnome51-resolution and
    # cc-display-panel.c's on_gnome51_resolution_apply_cb, though this
    # process was never a GTK/Wayland client of Mutter to begin with
    # (plain GDBus, no display connection at all) -- it was never one
    # of the clients a compositor shutdown would wait on in the first
    # place, unlike those two.
    subprocess.Popen(
        ["/usr/bin/setsid", "/bin/sh", "-c",
         "systemctl --user stop gnome-shell51-vnc.service "
         "gnome-remote-desktop.service gsd-media-keys-manual"],
        start_new_session=True,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )


def handle_method_call(connection, sender, object_path, interface_name,
                        method_name, parameters, invocation):
    if method_name == "Logout":
        end_session()
        invocation.return_value(None)
    elif method_name in ("Shutdown", "Reboot", "Suspend"):
        # Never actually reachable via the real UI (CanShutdown/
        # CanReboot/CanSuspend below all report UNAVAILABLE, which
        # correctly hides these three menu items) -- treated as a plain
        # logout if somehow still called, rather than silently doing
        # nothing or erroring back at the caller.
        end_session()
        invocation.return_value(None)
    elif method_name == "CanShutdown":
        invocation.return_value(GLib.Variant("(u)", (UNAVAILABLE,)))
    elif method_name == "CanReboot":
        invocation.return_value(GLib.Variant("(u)", (UNAVAILABLE,)))
    elif method_name == "CanSuspend":
        invocation.return_value(GLib.Variant("(u)", (UNAVAILABLE,)))
    elif method_name == "IsInhibited":
        invocation.return_value(GLib.Variant("(b)", (False,)))
    else:
        invocation.return_dbus_error(
            "org.freedesktop.DBus.Error.UnknownMethod",
            f"Unknown method {method_name}",
        )


def handle_get_property(connection, sender, object_path, interface_name,
                         property_name):
    if property_name == "SessionIsActive":
        return GLib.Variant("b", True)
    return None


def on_bus_acquired(connection, name):
    node_info = Gio.DBusNodeInfo.new_for_xml(INTERFACE_XML)
    interface_info = node_info.interfaces[0]
    connection.register_object(
        "/org/gnome/SessionManager",
        interface_info,
        handle_method_call,
        handle_get_property,
        None,
    )


def main():
    Gio.bus_own_name(
        Gio.BusType.SESSION,
        "org.gnome.SessionManager",
        Gio.BusNameOwnerFlags.NONE,
        on_bus_acquired,
        None,
        None,
    )
    GLib.MainLoop().run()


if __name__ == "__main__":
    main()
