#!/usr/bin/python3

# Copyright 2015 Enrico Zini <enrico@enricozini.org>
# License: GPL-3
#
# Tries to make the status of the login/seat system visible, to help with
# understanding and troubleshooting.
#
# Further reading:
# http://www.freedesktop.org/wiki/Software/systemd/multiseat/
# http://www.freedesktop.org/wiki/Software/systemd/logind/

import argparse
import logging
from collections import namedtuple
from collections import OrderedDict
import sys
import os
import time
import datetime
import grp
from subprocess import check_output
from subprocess import CalledProcessError
import dbus
from systemd import journal

log = logging.getLogger()
system_bus = None
journal_reader = None
args = None

class ColourCode: 
    BLUE = '\033[94m'
    GREEN = '\033[92m'
    YELLOW = '\033[93m'
    RED = '\033[91m'
    ENDC = '\033[0m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'

def colour_on(colour_code):
    if not args.nocolour:
        print(colour_code, end="")

def colour_off():
    if not args.nocolour:
        print(ColourCode.ENDC, end="")

def format_usec_since_epoch(usec):
    if usec == 0: return "--"
    return datetime.datetime.fromtimestamp(usec / 1000000).strftime('%Y-%m-%d %H:%M:%S')

def format_elapsed(seconds):
    if seconds > 86400:
        return "{}d".format(round(seconds / 86400))
    elif seconds > 3600:
        return "{}h".format(round(seconds / 3600))
    elif seconds > 60:
        return "{}m".format(round(seconds / 60))
    else:
        return "{}s".format(round(seconds))

# A seat consists of all hardware devices assigned to a specific workplace.
# It consists of at least one graphics device, and usually also includes
# keyboard, mouse. It can also include video cameras, sound cards and more.
# Seats are identified by seat names, which are short strings (<= 64chars),
# that start with the four characters "seat" followed by at least one more
# character from the range a-zA-Z0-9, "_" and "-". They are suitable for
# inclusion in file names. Seat names may or may not be stable and may be
# reused if a seat becomes available again.
class Seat:
    """
    Get and hold information about a seat
    """
    def __init__(self, id, path):
        self.id = id
        self.path = path

        # Get the seat object from the login manager
        self.dbus_obj = system_bus.get_object("org.freedesktop.login1", self.path)
        # Read the seat API properties of the object, using the D-Bus
        # properties API
        self.dbus_props = self.dbus_obj.GetAll("org.freedesktop.login1.Seat",
                                               dbus_interface="org.freedesktop.DBus.Properties")

        # Pythonize the d-bus properties

        # The Sessions array is an array of all current sessions of this seat, each
        # encoded in a 2-tuple consisting of the ID and the object path
        self.sessions = self.dbus_props["Sessions"]

        # ActiveSession encodes the currently active session if there is one. It is a
        # 2-tuple consisting of session id and object path
        self.active_session = self.dbus_props["ActiveSession"]

        # CanMultiSession encodes whether the session is multi-session capable
        self.can_multisession = bool(self.dbus_props["CanMultiSession"])

        # CanTTY whether it is suitable for text logins
        self.can_tty = bool(self.dbus_props["CanTTY"])

        # CanGraphical whether it is suitable for graphical sessions
        self.can_graphical = bool(self.dbus_props["CanGraphical"])

        # The IdleHint, IdleSinceHint, IdleSinceHintMonotonic properties encode
        # the idle state, similar to the one exposed on the Manager object, but
        # specific for this seat
        self.idle_hint = bool(self.dbus_props["IdleHint"])
        self.idle_since_hint = self.dbus_props["IdleSinceHint"]

    def log_summary(self, prefix=" - "):
        """
        Return a one-line summary of all the properties of this seat
        """
        desc = []

        # Format ID
        desc.append("id: {}".format(self.id))

        # Format list of available session, marking the active one with an
        # asterisk
        seat_sessions = []
        for sess_id, sess_path in self.sessions:
            if sess_id == self.active_session[0]:
                seat_sessions.append("*" + sess_id)
            else:
                seat_sessions.append(sess_id)
        if seat_sessions:
            desc.append("sessions: " + ",".join(seat_sessions))
        else:
            desc.append("no sessions")

        # Format capabilities
        if self.can_multisession:
            desc.append("multisession")
        else:
            desc.append("no multisession")

        if self.can_tty:
            desc.append("has tty")
        else:
            desc.append("no tty")

        if self.can_graphical:
            desc.append("graphical")
        else:
            desc.append("non graphical")

        # Format idle time
        desc.append("idle: {} since {}".format(self.idle_hint, format_usec_since_epoch(self.idle_since_hint)))

        log.info("%s%s", prefix, ", ".join(desc))


# A user (the way we know it on Unix) corresponds to the person using a computer.
# A single user can have opened multiple sessions at the same time. A user is
# identified by a numeric user id (UID) or a user name (string).
class User:
    """
    Get and hold information about a user
    """
    def __init__(self, id, name, path):
        self.id = id
        self.name = name
        self.path = path

        # Get the user object from the login manager
        self.dbus_obj = system_bus.get_object("org.freedesktop.login1", self.path)
        # Read the user API properties of the object, using the D-Bus
        # properties API
        self.dbus_props = self.dbus_obj.GetAll("org.freedesktop.login1.User",
                                               dbus_interface="org.freedesktop.DBus.Properties")

        # The UID and GID properties encode the Unix UID and primary GID of the
        # user.
        self.uid = self.dbus_props["UID"]
        self.gid = self.dbus_props["GID"]

        # The Name property encodes the user name.

        # Timestamp encodes the login time of the user in
        # usec since the epoch, in the CLOCK_REALTIME clock.
        self.timestamp = self.dbus_props["Timestamp"]

        # RuntimePath encodes the runtime path of the user, i.e.
        # $XDG_RUNTIME_DIR, for details see the XDG Basedir Specification.
        #
        # $XDG_RUNTIME_DIR defines the base directory relative to which
        # user-specific non-essential runtime files and other file objects
        # (such as sockets, named pipes, ...) should be stored. The directory
        # MUST be owned by the user, and he MUST be the only one having read
        # and write access to it. Its Unix access mode MUST be 0700.
        # (http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html)
        self.runtime_path = self.dbus_props["RuntimePath"]

        # Service contains the name of the user systemd service unit name of
        # this user. Each logged in user gets a user service unit assigned that
        # runs a user systemd instance. This is usually an instance of
        # user@.service.
        self.service = self.dbus_props["Service"]

        # Slice contains the name of the user systemd slice unit name of this
        # user. Each logged in user gets a private slice.
        self.slice = self.dbus_props["Slice"]

        # Display encodes which graphical session should be used as primary UI
        # display for the use. It is a structure encoding session ID and object
        # path of the session to use.
        self.display = self.dbus_props["Display"]

        # State encodes the user state, one of "offline", "lingering",
        # "online", "active", "closing". See sd_uid_get_state(3) for more
        # information about the states.
        #
        # The following states are currently known: "offline" (user not logged
        # in at all), "lingering" (user not logged in, but some user services
        # running), "online" (user logged in, but not active, i.e. has no
        # session in the foreground), "active" (user logged in, and has at
        # least one active session, i.e. one session in the foreground),
        # "closing" (user not logged in, and not lingering, but some processes
        # are still around). In the future additional states might be defined,
        # client code should be written to be robust in regards to additional
        # state strings being returned.
        # (http://www.freedesktop.org/software/systemd/man/sd_uid_get_state.html)
        self.state = self.dbus_props["State"]

        # Sessions is an array of structures encoding all current sessions of
        # the user. Each structure consists of ID and object path.
        self.sessions = self.dbus_props["Sessions"]

        # The IdleHint, IdleSinceHint, IdleSinceHintMonotonic properties encode
        # the idle hint state of the user, similar to the Manager's properties,
        # but specific for this user.
        self.idle_hint = bool(self.dbus_props["IdleHint"])
        self.idle_since_hint = self.dbus_props["IdleSinceHint"]

    def log_summary(self, prefix=" - "):
        desc = []

        desc.append("name: {}".format(self.name))
        desc.append("uid: {}".format(self.id))
        desc.append("gid: {}".format(self.gid))
        desc.append("display: {}".format(self.display[0]))
        desc.append("sessions: {}".format(",".join(x[0] for x in self.sessions)))
        log.info("%s%s", prefix, ", ".join(desc))

        # Replace prefix with just indentation
        prefix = " " * len(prefix)

        desc = []
        elapsed = time.time() - self.timestamp / 1000000
        desc.append("login time: " + format_elapsed(elapsed))
        desc.append("idle: {} since {}".format(self.idle_hint, format_usec_since_epoch(self.idle_since_hint)))
        log.info("%s%s", prefix, ", ".join(desc))

        desc = []
        desc.append("XDG_RUNTIME_DIR: {}".format(self.runtime_path))
        log.info("%s%s", prefix, ", ".join(desc))

        desc = []
        state_desc = {
            "offline": "user not logged in",
            "lingering": "user not logged in, but some user services running",
            "online": "user logged in, but not active, i.e. has no session in the foreground",
            "active": "user logged in, and has at least one active session, i.e. one session in the foreground",
            "closing": "user not logged in, and not lingering, but some processes are still around",
        }
        log.info("%sstate: %s (%s)", prefix, self.state, state_desc.get(self.state, "description unknown"))
        colour_on(ColourCode.YELLOW)
        log.info("%s$ loginctl user-status %s # user status", prefix, self.name)
        log.info("%s$ systemctl status %s # service status", prefix, self.service)
        log.info("%s$ systemctl status %s # slice status", prefix, self.slice)
        colour_off()

# A session is defined by the time a user is logged in until he logs out. A
# session is bound to one or no seats (the latter for 'virtual' ssh logins).
# Multiple sessions can be attached to the same seat, but only one of them can
# be active, the others are in the background. A session is identified by a
# short string. systemd ensures that audit sessions are identical to systemd
# sessions, and uses the audit session id as session id in systemd (if auditing
# is enabled). The session identifier too shall be considered a short string
# (<= 64chars) consisting only of a-zA-Z0-9, "_" and "-", suitable for
# inclusion in a file name. Session IDs are unique on the local machine and are
# never reused as long as the machine is online.
class Session:
    """
    Get and hold information about a session
    """
    def __init__(self, id, uid, user, seat_id, path):
        # Id encodes the session ID.
        self.id = id
        # User encodes the user ID of the user this session belongs to. This is
        # a structure encoding the Unix UID and the object path.
        self.uid = uid
        # Name encodes the user name.
        self.user = user
        self.seat_id = seat_id
        self.path = path

        # Get the session object from the login manager
        self.dbus_obj = system_bus.get_object("org.freedesktop.login1", self.path)
        # Read the user API properties of the object, using the D-Bus
        # properties API
        self.dbus_props = self.dbus_obj.GetAll("org.freedesktop.login1.Session",
                                               dbus_interface="org.freedesktop.DBus.Properties")


        # Timestamp encodes the usec timestamp since the
        # epoch when the session was created, in the CLOCK_REALTIME clock.
        self.timestamp = self.dbus_props["Timestamp"]

        # Seat encodes the seat this session belongs to, if there is any. This
        # is a structure consisting of the ID and the seat object path.
        self.seat = self.dbus_props["Seat"]

        # TTY encodes the kernel TTY path of the session if this is a text
        # login. If not this is an empty string.
        self.tty = self.dbus_props["TTY"]

        # Display encodes the X11 display name if this is a graphical login. If
        # not this is an empty string.
        self.display = self.dbus_props["Display"]

        # Remote encodes whether the session is local or remote.
        self.remote = bool(self.dbus_props["Remote"])

        # RemoteHost and RemoteUser encode the remote host and user if this is
        # a remote session, or an empty string otherwise.
        self.remote_host = self.dbus_props["RemoteHost"]
        self.remote_user = self.dbus_props["RemoteUser"]

        # Service encodes the PAM service name that registered the session.
        self.service = self.dbus_props["Service"]

        # Scope contains the systemd scope unit name of this session.
        self.scope = self.dbus_props["Scope"]

        # Leader encodes the PID of the process that registered the session.
        self.leader = self.dbus_props["Leader"]

        # Audit encodes the Kernel Audit session ID of the session, if auditing is available.
        self.audit = self.dbus_props["Audit"]

        # Type encodes the session type. It's one of "unspecified" (for cron
        # PAM sessions and suchlike), "tty" (for text logins) or "x11" (for
        # graphical logins).
        self.type = self.dbus_props["Type"]

        # Class encodes the session class. It's one of "user" (for normal user
        # sessions), "greeter" (for display manager pseudo-sessions),
        # "lock-screen" (for display lock screens).
        self.session_class = self.dbus_props["Class"]

        # Active is a boolean that is true if the session is active, i.e.
        # currently in the foreground. This field is semi-redundant due to
        # State.
        self.active = bool(self.dbus_props["Active"])

        # State encodes the session state and one of "online", "active",
        # "closing". See sd_session_get_state(3) for more information about the
        # states.
        #
        # The following states are currently known: "online" (session logged
        # in, but session not active, i.e. not in the foreground), "active"
        # (session logged in and active, i.e. in the foreground), "closing"
        # (session nominally logged out, but some processes belonging to it are
        # still around). In the future additional states might be defined,
        # client code should be written to be robust in regards to additional
        # state strings being returned.
        # (http://stuff.onse.fi/man?program=sd_session_get_state&section=3)
        self.state = self.dbus_props["State"]

        # IdleHint, IdleSinceHint, IdleSinceHintMonotonic encapsulate the idle
        # hint state of this session, similar to how the respective properties
        # on the manager object do it for the whole system.
        self.idle_hint = bool(self.dbus_props["IdleHint"])
        self.idle_since_hint = self.dbus_props["IdleSinceHint"]

    def log_summary(self, prefix=" - "):
        desc = []
        desc.append("id: {}".format(self.id))
        desc.append("type: {}".format(self.type))
        desc.append("class: {}".format(self.session_class))
        desc.append("uid: {}".format(self.uid))
        desc.append("user: {}".format(self.user))
        desc.append("tty: {}".format(self.tty))
        desc.append("display: {}".format(self.display))
        log.info("%s%s", prefix, ", ".join(desc))

        # Replace prefix with just indentation
        prefix = " " * len(prefix)

        if self.remote:
            desc = []
            desc.append("remote host: {}".format(self.remote_host))
            desc.append("remote user: {}".format(self.remote_user))
            log.info("%s%s", prefix, ", ".join(desc))

        desc = []
        elapsed = time.time() - self.timestamp / 1000000
        desc.append("age: " + format_elapsed(elapsed))
        desc.append("idle: {} since {}".format(self.idle_hint, format_usec_since_epoch(self.idle_since_hint)))
        desc.append("leader pid: {}".format(self.leader))
        desc.append("audit: {}".format(self.audit))
        log.info("%s%s", prefix, ", ".join(desc))

        desc = []
        desc.append("active: {}".format(self.active))

        state_desc = {
            "online": "session logged in, but session not active, i.e. not in the foreground",
            "active": "session logged in and active, i.e. in the foreground",
            "closing": "session nominally logged out, but some processes belonging to it are still around",
        }
        desc.append("state: {} ({})".format(self.state, state_desc.get(self.state, "description unknown")))
        log.info("%s%s", prefix, ", ".join(desc))
        colour_on(ColourCode.YELLOW)
        log.info("%s$ loginctl session-status %s # session status", prefix, self.id)
        log.info("%s$ loginctl seat-status %s # seat status", prefix, self.seat_id)
        log.info("%s$ systemctl status %s # service status", prefix, self.service)
        log.info("%s$ systemctl status %s # scope status", prefix, self.scope)
        colour_off()


class UnitObject:
    """
    Base class for all systemd Unit Objects
    """
    def __init__(self,
                 unit_name,
                 desc,
                 loaded,
                 state,
                 sub_state,
                 followed,
                 obj_path,
                 queued_job_id,
                 job_type,
                 job_obj_path):
        self.unit_name = unit_name
        self.desc = desc
        self.loaded = loaded
        self.state = state
        self.sub_state = sub_state
        self.followed = followed
        self.obj_path = obj_path
        self.queued_job_id = queued_job_id
        self.job_type = job_type
        self.job_obj_path = job_obj_path
        try:
            self.dbus_obj = system_bus.get_object("org.freedesktop.systemd1", self.obj_path)
            self.dbus_unit_props = self.dbus_obj.GetAll(
                    "org.freedesktop.systemd1.Unit",
                    dbus_interface="org.freedesktop.DBus.Properties")
        except dbus.exceptions.DBusException as e:
            if e.get_dbus_name() == "org.freedesktop.DBus.Error.UnknownInterface":
                log.warn("Failed to construct a UnitObject, %s does not support "
                        "the org.freedesktop.systemd1.Unit interface",
                        self.obj_path)
    
    def log_summary(self, prefix = " * "):
        desc = []
        desc.append("{}".format(self.unit_name))
        desc.append("({}".format(self.loaded))
        desc.append("{}".format(self.state))
        desc.append("{})".format(self.sub_state)) 
        if (self.sub_state in ["dead"]):
            colour_on(ColourCode.RED)
        log.info("%s%s", prefix, " ".join(desc))
        colour_off()


# Job objects encapsulate scheduled or running jobs. Each unit can have none
# or one jobs in the execution queue. Each job is attached to exactly one unit.
class Job(UnitObject):
    """
    Get and hold information about a systemd Job object
    """
    def __init__(self, unit, props):
        UnitObject.__init__(self, *unit)
        self.dbus_job_props = props


class Timer(UnitObject):
    """
    Get and hold information about a systemd Timer Unit Object:
        - a timer controlled and supervised by systemd, for timer-based activation
    """
    def __init__(self, unit, props):
        UnitObject.__init__(self, *unit)
        self.dbus_timer_props = props

    def log_summary(self, prefix = " - "):
        UnitObject.log_summary(self, prefix)
        desc = []
        desc.append("Description: \"{}\"".format(self.desc))
        if (self.state == "active"):
            next_elapse = self.dbus_timer_props['NextElapseUSecRealtime'] 
            next_elapse = format_usec_since_epoch(next_elapse) 
            desc.append("Next elapse: {}".format(next_elapse))
        for prop in desc:
            log.info("\t%s%s", prefix, prop)


class Socket(UnitObject):
    """
    Get and hold information about a systemd Socket Unit Object:
        - an IPC or network socket or a file system FIFO controlled and
        supervised by systemd, for socket-based activation
    """
    def __init__(self, unit, props):
        UnitObject.__init__(self, *unit)
        self.dbus_socket_props = props

    def log_summary(self, prefix = " - "):
        UnitObject.log_summary(self, prefix)
        desc = []
        if (self.dbus_socket_props['Accept']):
            desc.append(" Cxn: {}".format(self.dbus_socket_props['NConnections']))
        for prop in desc:
            log.info("\t%s%s", prefix, prop)


class Device(UnitObject):
    """
    Get and hold information about a systemd Device Unit Object:
        - a device unit as exposed in the sysfs/udev(7) device tree
    """
    def __init__(self, unit, props):
        UnitObject.__init__(self, *unit)
        self.dbus_device_props = props

    def log_summary(self, prefix = " - "):
        UnitObject.log_summary(self, prefix)
        desc = []
        desc.append("SysFSPath: {}".format(self.dbus_device_props['SysFSPath']))
        for prop in desc:
            log.info("\t%s%s", prefix, prop)


class Target(UnitObject):
    """
    Get and hold information about a systemd Target Unit Object:
        - a target unit of systemd, which is used for grouping units and as
        well-known synchronization points during start-up
    """
    def __init__(self, unit, props):
        UnitObject.__init__(self, *unit)
        # Target units have neither type-specific methods nor properties. 


class Mount(UnitObject):
    """
    Get and hold information about a systemd Mount Unit Object:
        - a file system mount point controlled and supervised by systemd
    """
    def __init__(self, unit, props):
        UnitObject.__init__(self, *unit)
        self.dbus_mount_props = props
    
    def log_summary(self, prefix = " - "):
        UnitObject.log_summary(self, prefix)
        desc = []
        for prop in ["Where", "What", "Type", "ControlGroup"]:
            desc.append(" {}: {}".format(
                prop, 
                self.dbus_mount_props[prop]))
        for line in desc:
            log.info("\t%s%s", prefix, line)


class Automount(UnitObject):
    """
    Get and hold information about a systemd Automount Unit Object:
        - a file system automount point controlled and supervised by systemd
    """
    def __init__(self, unit, props):
        UnitObject.__init__(self, *unit)
        self.dbus_automount_props = props
    
    def log_summary(self, prefix = " - "):
        UnitObject.log_summary(self, prefix)
        desc = []
        for prop in ["Where"]:
            desc.append(" {}: {}".format(
                prop, 
                self.dbus_automount_props[prop]))
        for line in desc:
            log.info("\t%s%s", prefix, line)


class Snapshot(UnitObject):
    """
    Get and hold information about a systemd Snapshot Unit Object:
        - Snapshot units are not configured via unit configuration files.
        Nonetheless they are named similar to filenames. A unit whose name
        ends in ".snapshot" refers to a dynamic snapshot of the
        systemd runtime state.

        - Snapshots are not configured on disk but created dynamically via
        systemctl snapshot
    """
    def __init__(self, unit, props):
        UnitObject.__init__(self, *unit)
        self.dbus_snapshot_props = props


class Swap(UnitObject):
    """
    Get and hold information about a systemd Swap Unit Object:
        - a swap device or file for memory paging
    """
    def __init__(self, unit, props):
        UnitObject.__init__(self, *unit)
        self.dbus_swap_props = props
    
    def log_summary(self, prefix = " - "):
        UnitObject.log_summary(self, prefix)
        desc = []
        for prop in ["What", "Slice", "ControlPID"]:
            desc.append(" {}: {}".format(
                prop, 
                self.dbus_swap_props[prop]))
        for line in desc:
            log.info("\t%s%s", prefix, line)


class Path(UnitObject):
    """
    Get and hold information about a systemd Path Unit Object:
        - a path monitored by systemd, for path-based activation
    """
    def __init__(self, unit, props):
        UnitObject.__init__(self, *unit)
        self.dbus_path_props = props
    
    def log_summary(self, prefix = " - "):
        UnitObject.log_summary(self, prefix)
        desc = []
        for prop in ["Unit"]:
            desc.append(" {}: {}".format(
                prop, 
                self.dbus_path_props[prop]))
        for path in self.dbus_path_props["Paths"]:
            desc.append(" Path: {} @ {}".format(
                path[0], path[1]))
        for line in desc:
            log.info("\t%s%s", prefix, line)


class Slice(UnitObject):
    """
    Get and hold information about a systemd Slice Unit Object:
        - a concept for hierarchically managing resources of a group of
        processes. This management is performed by creating a node in the
        Linux Control Group (cgroup) tree. Units that manage processes
        (primarily scope and service units) may be assigned to a specific
        slice. For each slice, certain resource limits may be set that
        apply to all processes of all units contained in that slice.
        
        - Slices are organized hierarchically in a tree
    """
    def __init__(self, unit, props):
        UnitObject.__init__(self, *unit)
        self.dbus_slice_props = props

    def log_summary(self, prefix = " - "):
        UnitObject.log_summary(self, prefix)
        desc = []
        for prop in ["Slice", "ControlGroup"]:
            desc.append(" {}: {}".format(
                prop, 
                self.dbus_slice_props[prop]))
        for line in desc:
            log.info("\t%s%s", prefix, line)


class Scope(UnitObject):
    """
    Get and hold information about a systemd Scope Unit Object:
        - scope units manage externally created processes
    """
    def __init__(self, unit, props):
        UnitObject.__init__(self, *unit)
        self.dbus_scope_props = props
    
    def log_summary(self, prefix = " - "):
        UnitObject.log_summary(self, prefix)
        desc = []
        for prop in ["Slice", "ControlGroup", "Controller"]:
            desc.append(" {}: {}".format(
                prop, 
                self.dbus_scope_props[prop]))
        for line in desc:
            log.info("\t%s%s", prefix, line)


class Service(UnitObject):
    """
    Get and hold information about a systemd Service Unit Object:
        - a process controlled and supervised by systemd
    """
    def __init__(self, unit, props):
        UnitObject.__init__(self, *unit)
        self.dbus_service_props = props

    def failed(self):
        return self.state == "failed" or self.sub_state == "failed" or self.loaded == "error"

    def log_summary(self, prefix = " - "):
        if (self.failed()):
            colour_on(ColourCode.BOLD)
        UnitObject.log_summary(self, prefix)
        # Result encodes the execution result of the last run of the service:
        #  - 'success' - set if the unit didn't fail
        #  - 'resources' - not enough resources to fork and exec the service processes
        #  - 'timeout' - time-out occurred while executing a service operation
        #  - 'exit-code' - process exited with an unclean exit code
        #  - 'signal' - process exited with an uncaught signal
        #  - 'core-dump' - process exited uncleanly and dumped core
        #  - 'watchdog' - did not send out watchdog ping messages often enough
        #  - 'start-limit' - started too frequently (see StartLimitInterval/Burst)
        self.result = self.dbus_service_props["Result"]
        # Timestamp when state moved from deactivating → inactive/failed
        self.inactive_since = format_usec_since_epoch(self.dbus_unit_props["InactiveEnterTimestamp"]) 
        desc = []
        desc.append("result: {}".format(self.result))
        desc.append("since: {}".format(self.inactive_since))
        log.info("\t%s%s", prefix, " ".join(desc))

        # Log a warning if this user's not allowed to see journal entries
        sj_gid = grp.getgrnam('systemd-journal').gr_gid
        root_gid = grp.getgrnam('root').gr_gid
        if (root_gid not in os.getgroups() and sj_gid not in os.getgroups()):
            log.info("\t%s* Only root and systemd-journal group members "
                    "can see journal entries. *", prefix)

        # 'failed' indicates that it is inactive and the previous run was not successful
        # 'error' indicates that the configuration file failed to load
        if (self.failed()):
            colour_on(ColourCode.RED)
            # Log relevant journal entries (fails quietly if no permissions)
            journal_reader.flush_matches()
            journal_reader.add_match(UNIT=self.unit_name)
            journal_reader.add_disjunction()
            journal_reader.add_match(_SYSTEMD_UNIT=self.unit_name)
            for entry in journal_reader:
                if ('_SOURCE_REALTIME_TIMESTAMP' in entry):
                    timestamp = entry['_SOURCE_REALTIME_TIMESTAMP'].strftime("%Y-%m-%d %H:%M:%S")
                else:
                    timestamp = "--"
                frag = "{} {} {}[{}]: {}".format(
                        timestamp,
                        entry['_HOSTNAME'],
                        entry['_COMM'],
                        str(entry['_PID']),
                        entry['MESSAGE'])
                log.info("\t\t%s%s", prefix, frag)
                # Log the related Message Catalog entry if available
                if ('MESSAGE_ID' in entry):
                    msg_cat = journal_reader.get_catalog()
                    msg_cat = msg_cat.replace("\n\n", "\n")
                    msg_cat = msg_cat.replace("\n", "\n\t\t{}".format(prefix), msg_cat.count('\n') - 1)
                    msg_cat = msg_cat[:-1]
                    log.info("\t\t\t%s%s", prefix, msg_cat)
        colour_off()


Inhibitor = namedtuple("Inhibitor", ("what", "who", "why", "mode", "uid", "pid"))


def enumerate_units():
    global system_bus
    sysd_obj = system_bus.get_object("org.freedesktop.systemd1", "/org/freedesktop/systemd1")
    sysd_iface = dbus.Interface(sysd_obj, dbus_interface="org.freedesktop.systemd1.Manager")
    units = {'Timer' : [], 'Socket' : [], 'Device' : [], 'Target' : [],
            'Mount' : [], 'Automount' : [], 'Snapshot' : [], 'Swap' : [],
            'Path' : [], 'Slice' : [], 'Scope' : [], 'Job' : [], 'Service' : []}
    units = OrderedDict(sorted(units.items()))
    for unit in sysd_iface.ListUnits():
        dbus_obj = system_bus.get_object("org.freedesktop.systemd1", unit[6])
        for unit_type in units:
            try:
                props = dbus_obj.GetAll("org.freedesktop.systemd1.{}".format(unit_type),
                        dbus_interface="org.freedesktop.DBus.Properties")
                units["{}".format(unit_type)].append(eval(unit_type)(unit, props))
                break
            except dbus.exceptions.DBusException as e: 
                pass
    for unit_type in units:
        log_summaries(unit_type, units[unit_type])

def log_summaries(unit_type, units):
    if not units:
        colour_on(ColourCode.BOLD)
        log.info("No %ss found.", unit_type)
        colour_off()
    else:
        colour_on(ColourCode.BOLD)
        log.info("%i %ss found:", len(units), unit_type)
        colour_off()
        for unit in units:
            unit.log_summary("\t - ")

def main():
    global system_bus
    global journal_reader
    global args

    # Exit gracefully if systemd is not running
    try:
        check_output(["pidof", "systemd"])
    except CalledProcessError as e:
        log.error("Error: seat-inspect requires systemd to be running..")
        sys.exit(1)
    
    parser = argparse.ArgumentParser(description='Inspect the seat API.')
    parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
    parser.add_argument("--nocolour", "-nc", action="store_true", help="No colour")
    args = parser.parse_args()

    FORMAT = "%(message)s"
    if args.verbose:
        logging.basicConfig(level=logging.INFO, stream=sys.stdout, format=FORMAT)
    else:
        logging.basicConfig(level=logging.WARN, stream=sys.stdout, format=FORMAT)

    # The systemd journal could contain entries for other machines and/or previous
    # boots, but we only care about the last boot on this machine..
    journal_reader = journal.Reader()
    journal_reader.this_boot()
    journal_reader.this_machine()
    journal_reader.log_level(journal.LOG_INFO)
    
    try:
        # Get a private connection to the D-Bus daemon
        system_bus = dbus.bus.BusConnection(dbus.bus.BusConnection.TYPE_SYSTEM)
    except dbus.exceptions.DBusException as e:
        if e.get_dbus_name() == "org.freedesktop.DBus.Error.FileNotFound":
            log.warn("Cannot contact the system D-Bus server, is it running? The error is: %s", e.get_dbus_message())
        sys.exit(1)

    # Get a proxy to the system login object
    login_obj = system_bus.get_object("org.freedesktop.login1", "/org/freedesktop/login1")

    # Talk to it using the login Manager API
    login_iface = dbus.Interface(login_obj, dbus_interface="org.freedesktop.login1.Manager")
    # Also use the dbus Properties API to get a list of its properties
    login_props = login_obj.GetAll("org.freedesktop.login1.Manager", dbus_interface="org.freedesktop.DBus.Properties")

    # List available seats
    colour_on(ColourCode.BOLD)
    log.info("Available seats:")
    colour_off()
    seats = {}
    for seat in (Seat(*x) for x in login_iface.ListSeats()):
        seats[seat.path] = seat
        seat.log_summary()

    # List currently logged in users
    colour_on(ColourCode.BOLD)
    log.info("Users logged in:")
    colour_off()
    users = {}
    for user in (User(*x) for x in login_iface.ListUsers()):
        users[user.path] = user
        user.log_summary()

    # List existing sessions
    colour_on(ColourCode.BOLD)
    log.info("Existing sessions:")
    colour_off()
    sessions = {}
    for session in (Session(*x) for x in login_iface.ListSessions()):
        sessions[session.path] = session
        session.log_summary()

    cur_session = sessions.get(login_iface.GetSessionByPID(os.getpid()), None)
    colour_on(ColourCode.BOLD)
    log.info("Current session: %s", cur_session.id if cur_session else "unknown")
    colour_off()
    try:
        cur_user = users.get(login_iface.GetUserByPID(os.getpid()), None)
        colour_on(ColourCode.BOLD)
        log.info("Current user: %s", cur_user.name if cur_user else "unknown")
        colour_off()
    except dbus.exceptions.DBusException as e:
        if e.get_dbus_name() == "org.freedesktop.DBus.Error.AccessDenied":
            log.info("GetUserByPID not supported, you are probably running logind older than v208, no big deal.")
        else:
            log.warn("GetUserByPID failed: %s (%s)", e.get_dbus_name(), e.get_dbus_message())

    colour_on(ColourCode.BOLD)
    log.info("System capabilities:")
    colour_off()
    descs = {
        "na": "not supported by hardware, kernel or drivers",
        "yes": "supported, the current user may execute it without further authentication",
        "no": "supported, the current user is not allowed to execute it",
        "challenge": "supported, the current user can only execute it after authorization",
    }
    can_poweroff = login_iface.CanPowerOff()
    can_reboot = login_iface.CanReboot()
    can_suspend = login_iface.CanSuspend()
    can_hibernate = login_iface.CanHibernate()
    can_hybridsleep = login_iface.CanHybridSleep()
    log.info(" - power off: %s: %s", can_poweroff, descs[can_poweroff])
    log.info(" - reboot: %s: %s", can_reboot, descs[can_reboot])
    log.info(" - suspend: %s: %s", can_suspend, descs[can_suspend])
    log.info(" - hibernate: %s: %s", can_hibernate, descs[can_hibernate])
    log.info(" - hybrid sleep: %s: %s", can_hybridsleep, descs[can_hybridsleep])

    inhibitors = []
    for inhibitor in (Inhibitor(*x) for x in login_iface.ListInhibitors()):
        inhibitors.append(inhibitor)

    if inhibitors:
        colour_on(ColourCode.BOLD)
        log.info("Current inhibitor locks:")
        colour_off()
        for i in inhibitors:
            log.info(" - %s (%s) by %s (%s) uid %d pid %d", i.what, i.mode, i.who, i.why, i.uid, i.pid)
    else:
        log.info("No inhibitor locks currently present.")

    colour_on(ColourCode.BOLD)
    log.info("System idle: %s since %s", login_props.get("IdleHint", None), format_usec_since_epoch(login_props.get("IdleSinceHint", None)))
    log.info("Active block locks: %s", login_props.get("BlockInhibited", None))
    log.info("Active delay locks: %s", login_props.get("DelayInhibited", None))
    log.info("Preparing for shutdown: %s", login_props.get("PreparingForShutdown", None))
    log.info("Preparing for sleep: %s", login_props.get("PreparingForSleep", None))

    # Run troubleshooting checks
    # https://wiki.archlinux.org/index.php/General_troubleshooting#Session_permissions
    env_session_id = os.environ.get("XDG_SESSION_ID", None)
    if env_session_id is None:
        log.warn("XDG_SESSION_ID is not set: you may have a display manager or PAM setup with missing or incomplete session support")
    elif str(cur_session.id) != env_session_id:
        log.warn("XDG_SESSION_ID is %s but the login manager thinks that the current session id is %s", env_session_id, cur_session.id)
    elif cur_session.remote:
        log.warn("The current session is marked as remote (user %s host %s):"
                 " shutdown, suspend, network-manager and so on may not work without requiring a password",
                 cur_session.remote_user, cur_session.remote_host)
    elif not cur_session.active:
        log.warn("The current session state is not active (state: %s):"
                 " shutdown, suspend, network-manager and so on may not work without requiring a password",
                cur_session.state)
    else:
        log.info("The current session is local and active: shutdown, suspend, network-manager and so on"
                 " should be ok")
    colour_off()
    enumerate_units()
    journal_reader.close()
    sys.exit(0)

if __name__ == "__main__":
    main()
