From 1d3bc3c013f9d4f9e39070c30840574d25b9bfb3 Mon Sep 17 00:00:00 2001 From: Gabriel Arazas Date: Wed, 24 Jan 2024 10:40:22 +0800 Subject: [PATCH] nixos/programs/sessiond: init --- modules/nixos/default.nix | 1 + modules/nixos/programs/sessiond/default.nix | 124 +++++++++ .../sessiond/submodules/component-type.nix | 228 +++++++++++++++++ .../sessiond/submodules/session-type.nix | 235 ++++++++++++++++++ 4 files changed, 588 insertions(+) create mode 100644 modules/nixos/programs/sessiond/default.nix create mode 100644 modules/nixos/programs/sessiond/submodules/component-type.nix create mode 100644 modules/nixos/programs/sessiond/submodules/session-type.nix diff --git a/modules/nixos/default.nix b/modules/nixos/default.nix index 3c97878c..d8f5cd74 100644 --- a/modules/nixos/default.nix +++ b/modules/nixos/default.nix @@ -5,6 +5,7 @@ ./programs/distrobox.nix ./programs/gnome-session ./programs/pop-launcher.nix + ./programs/sessiond ./programs/wezterm.nix ./services/archivebox.nix ./services/gallery-dl.nix diff --git a/modules/nixos/programs/sessiond/default.nix b/modules/nixos/programs/sessiond/default.nix new file mode 100644 index 00000000..6806aa0a --- /dev/null +++ b/modules/nixos/programs/sessiond/default.nix @@ -0,0 +1,124 @@ +{ config, lib, pkgs, utils, ... }: + +let + cfg = config.programs.sessiond; + + sessionPackages = lib.mapAttrsToList + (name: session: + let + displaySession = '' + [Desktop Entry] + Name=${session.fullName} + Comment=${session.description} + Exec="@out@/libexec/${name}-session" + Type=Application + DesktopNames=${lib.concatStringsSep ";" session.desktopNames}; + ''; + + sessionScript = '' + #!${pkgs.runtimeShell} + + ${lib.getExe' cfg.package "sessionctl"} run "${name}.target" + ''; + in + pkgs.runCommandLocal "${name}-desktop-session-files" + { + inherit displaySession sessionScript; + passAsFile = [ "displaySession" "sessionScript" ]; + passthru.providedSessions = [ name ]; + } + '' + SESSION_SCRIPT="$out/libexec/${name}-session" + install -Dm0755 "$sessionScriptPath" "$SESSION_SCRIPT" + substituteAllInPlace "$SESSION_SCRIPT" + + DISPLAY_SESSION_FILE="$out/share/xsessions/${name}.desktop" + install -Dm0644 "$displaySessionPath" "$DISPLAY_SESSION_FILE" + substituteAllInPlace "$DISPLAY_SESSION_FILE" + '' + ) + cfg.sessions; + + sessionSystemdUnits = lib.mapAttrsToList + (name: session: + let + inherit (utils.systemdUtils.lib) + pathToUnit serviceToUnit targetToUnit timerToUnit socketToUnit; + + sessionComponents = + lib.foldlAttrs + (acc: name: component: + acc // { + "${component.id}.service" = serviceToUnit component.id component.serviceUnit; + "${component.id}.target" = targetToUnit component.id component.targetUnit; + } // lib.optionalAttrs (component.socketUnit != null) { + "${component.id}.socket" = socketToUnit component.id component.socketUnit; + } // lib.optionalAttrs (component.timerUnit != null) { + "${component.id}.timer" = timerToUnit component.id component.timerUnit; + } // lib.optionalAttrs (component.pathUnit != null) { + "${component.id}.path" = pathToUnit component.id component.pathUnit; + }) + { } + session.components; + in + sessionComponents // { + "${name}.service" = serviceToUnit name session.serviceUnit; + "${name}.target" = targetToUnit name session.targetUnit; + } + ) + cfg.sessions; +in +{ + options.programs.sessiond = { + package = lib.mkOption { + type = lib.types.package; + default = pkgs.sessiond; + defaultText = "pkgs.sessiond"; + description = '' + The package containing sessiond executable and systemd units. This + module will use the `sessiond` executable for the generated session + script. + ''; + }; + + sessions = lib.mkOption { + type = with lib.types; attrsOf (submoduleWith { + specialArgs = { + inherit utils pkgs; + sessiondPkg = cfg.package; + }; + modules = [ ./submodules/session-type.nix ]; + }); + description = '' + A set of desktop sessions to be configured with sessiond. Each of the + attribute name will be used as the identifier of the desktop + environment. + + ::: {.tip} + While you can make identifiers in any way, it is + encouraged to stick to a naming scheme. Here's two common ways to name + a desktop environment. + + * Reverse DNS-like scheme (e.g., `com.example.MoseyBranch`). + * Kebab-case (e.g., `mosey-branch`). + ::: + ''; + default = { }; + }; + }; + + config = lib.mkIf (cfg.sessions != { }) { + environment.systemPackages = [ cfg.package ]; + + # Install all of the desktop session files. + services.xserver.displayManager.sessionPackages = sessionPackages; + + # Import those systemd units from sessiond as well. + systemd.packages = [ cfg.package ]; + systemd.user.units = lib.mkMerge sessionSystemdUnits; + + # We're disabling the upstream sessiond service since there can be multiple + # sessiond sessions here. + systemd.user.services.sessiond.enable = false; + }; +} diff --git a/modules/nixos/programs/sessiond/submodules/component-type.nix b/modules/nixos/programs/sessiond/submodules/component-type.nix new file mode 100644 index 00000000..f8ab4f9f --- /dev/null +++ b/modules/nixos/programs/sessiond/submodules/component-type.nix @@ -0,0 +1,228 @@ +{ name, config, lib, session, utils, ... }: { + options = { + description = lib.mkOption { + type = lib.types.nonEmptyStr; + description = "One-sentence description of the component."; + example = "Desktop widgets"; + }; + + # Most of the systemd config types are trying to eliminate as much of the + # NixOS systemd extensions as much as possible. For more details, see + # `config` attribute of the `sessionType`. + serviceUnit = lib.mkOption { + type = + let + inherit (utils.systemdUtils.lib) unitConfig serviceConfig; + inherit (utils.systemdUtils.unitOptions) commonUnitOptions serviceOptions; + in + lib.types.submodule [ + commonUnitOptions + serviceOptions + serviceConfig + unitConfig + ]; + description = '' + systemd service configuration to be generated. This should be + configured if the session is managed by systemd. + + :::{.note} + This has the same options as {option}`systemd.user.services.` + but without certain options from stage 2 counterparts such as + `reloadTriggers` and `restartTriggers`. + + By default, this module sets the service unit as part of the respective + target unit (i.e., `PartOf=$COMPONENTID.target`). + + On a typical case, you shouldn't mess with much of the dependency + ordering with the service unit. You should configure `targetUnit` for + that instead. + ::: + ''; + default = { }; + }; + + targetUnit = lib.mkOption { + type = + let + inherit (utils.systemdUtils.lib) unitConfig; + inherit (utils.systemdUtils.unitOptions) commonUnitOptions; + in + lib.types.submodule [ + commonUnitOptions + unitConfig + ]; + description = '' + systemd target configuration to be generated. This should be + configured if the session is managed by systemd. + + :::{.note} + This is generated by default alongside the service where it is + configured to be a part of the target unit. + + This has the same options as {option}`systemd.user.targets.` + but without certain options from stage 2 counterparts such as + `reloadTriggers` and `restartTriggers`. + ::: + ''; + default = { }; + }; + + timerUnit = lib.mkOption { + type = + let + inherit (utils.systemdUtils.unitOptions) timerOptions commonUnitOptions; + inherit (utils.systemdUtils.lib) unitConfig; + in + with lib.types; nullOr (submodule [ + commonUnitOptions + timerOptions + unitConfig + ]); + description = '' + An optional systemd timer configuration to be generated. This should + be configured if the session is managed by systemd. + + :::{.note} + This has the same options as {option}`systemd.user.timers.` + but without certain options from stage 2 counterparts such as + `reloadTriggers` and `restartTriggers`. + ::: + ''; + default = null; + }; + + socketUnit = lib.mkOption { + type = + let + inherit (utils.systemdUtils.unitOptions) socketOptions commonUnitOptions; + inherit (utils.systemdUtils.lib) unitConfig; + in + with lib.types; nullOr (submodule [ + commonUnitOptions + socketOptions + unitConfig + ]); + description = '' + An optional systemd socket configuration to be generated. This should + be configured if the session is managed by systemd. + + :::{.note} + This has the same options as {option}`systemd.user.sockets.` + but without certain options from stage 2 counterparts such as + `reloadTriggers` and `restartTriggers`. + ::: + ''; + default = null; + }; + + pathUnit = lib.mkOption { + type = + let + inherit (utils.systemdUtils.unitOptions) pathOptions commonUnitOptions; + inherit (utils.systemdUtils.lib) unitConfig; + in + with lib.types; nullOr (submodule [ + commonUnitOptions + pathOptions + unitConfig + ]); + description = '' + An optional systemd path configuration to be generated. This should + be configured if the session is managed by systemd. + + :::{.note} + This has the same options as {option}`systemd.user.paths.` + but without certain options from stage 2 counterparts such as + `reloadTriggers` and `restartTriggers`. + ::: + ''; + default = null; + }; + + id = lib.mkOption { + type = lib.types.str; + description = '' + The identifier of the component used in generating filenames for its + `.desktop` files and as part of systemd unit names. + ''; + default = "${session.name}.${name}"; + defaultText = "\${session-name}.\${name}"; + readOnly = true; + }; + }; + + config = { + /* + Setting some recommendation and requirements for sessiond components. + Note there are the missing directives that COULD include some sane + defaults here. + + * The `Unit.OnFailure=` and `Unit.OnFailureJobMode=` directives. Since + different components don't have the same priority and don't handle + failures the same way, we didn't set it here. This is on the user to + know how different desktop components interact with each other + especially if one of them failed. + + TODO: Is `Type=notify` a good default? + * `Service.Type=` is obviously not included since not all desktop + components are the same either. Some of them could a D-Bus service, + some of them are oneshots, etc. Not to mention, this is already implied + to be `Type=simple` by systemd anyways. + + * `Service.OOMScoreAdjust=` have different values for different + components so it isn't included. + + * Most sandboxing options. Aside from the fact we're dealing with a + systemd user unit, much of them are unnecessary and rarely needed (if + ever like `Service.PrivateTmp=`?) so we didn't set such defaults here. + + As you can tell, this module does not provide a framework for the user + to easily compose their own desktop environment. THIS MODULE ALREADY + DOES A LOT, ALRIGHT! CUT ME SOME SLACK! + + Take note that the default service configuration is leaning on the + desktop component being a simple type of service like how most NixOS + service modules are deployed. + */ + serviceUnit = { + description = lib.mkDefault config.description; + + # The typical workflow for service units to have them set as part of + # the respective target unit. + requisite = [ "${config.id}.target" ]; + before = [ "${config.id}.target" ]; + partOf = [ "${config.id}.target" ]; + + # Some sane service configuration for a desktop component. + serviceConfig = { + Slice = lib.mkDefault "session.slice"; + Restart = lib.mkDefault "on-failure"; + TimeoutStopSec = lib.mkDefault 5; + }; + + startLimitBurst = lib.mkDefault 3; + startLimitIntervalSec = lib.mkDefault 15; + + unitConfig = { + # We leave those up to the target units to start the services. + RefuseManualStart = lib.mkDefault true; + RefuseManualStop = lib.mkDefault true; + }; + }; + + /* + Similarly, there are things that COULD make it here but didn't for a + variety of reasons. + + * `Unit.PartOf=`, `Unit.Requisite=`, and the like since some components + require starting up earlier than the others. We could include it here + if we make it clear in the documentation or if it proves to be a + painful experience to configure this by a first-timer. For now, this is + on the user to know. + */ + targetUnit = { + wants = [ "${config.id}.service" ]; + description = lib.mkDefault config.description; + }; + }; +} diff --git a/modules/nixos/programs/sessiond/submodules/session-type.nix b/modules/nixos/programs/sessiond/submodules/session-type.nix new file mode 100644 index 00000000..263c9bea --- /dev/null +++ b/modules/nixos/programs/sessiond/submodules/session-type.nix @@ -0,0 +1,235 @@ +{ name, config, pkgs, lib, utils, sessiondPkg, ... }: + +let + # For an updated list, see `menu/menu-spec.xml` from + # https://gitlab.freedesktop.org/xdg/xdg-specs. + validDesktopNames = [ + "GNOME" + "GNOME-Classic" + "GNOME-Flashback" + "KDE" + "LXDE" + "LXQt" + "MATE" + "Razor" + "ROX" + "TDE" + "Unity" + "XFCE" + "EDE" + "Cinnamon" + "Pantheon" + "Budgie" + "Enlightenment" + "DDE" + "Endless" + "Old" + ]; + + # This is used both as the configuration format for sessiond.conf and its + # hooks. + settingsFormat = pkgs.formats.toml { }; + sessionSettingsFile = settingsFormat.generate "sessiond-conf-${name}" config.settings; +in +{ + options = { + fullName = lib.mkOption { + type = lib.types.nonEmptyStr; + description = "The display name of the desktop environment."; + default = name; + example = "Mosey Branch"; + }; + + desktopNames = lib.mkOption { + type = with lib.types; listOf nonEmptyStr; + description = '' + Names to be used as `DesktopNames=` entry of the session `.desktop` + file. Useful if you're creating a customized version of an already + existing desktop session. + + ::: {.note} + This module sanitizes the values by prepending the given names with + `X-` if they aren't part of the registered values from XDG spec. + ::: + ''; + default = [ config.fullName ]; + defaultText = "[ .fullName ]"; + apply = names: + builtins.map + (name: + if (lib.elem name validDesktopNames) || (lib.hasPrefix "X-" name) then + name + else + "X-${name}") + names; + example = [ "GNOME" "Garden" ]; + }; + + settings = lib.mkOption { + type = settingsFormat.type; + default = { }; + example = { + Idle = { + Inputs = [ "motion" "button-press" ]; + IdleSec = 60; + }; + + Lock = { + OnIdle = true; + OnSleep = true; + }; + + DPMS.Enable = true; + }; + description = '' + The settings associated with the sessiond session. For more + details, please see {manpage}`sessiond.conf(5)`. If not given, it + will use the default configuration from the compiled package. + ''; + }; + + description = lib.mkOption { + type = lib.types.nonEmptyStr; + description = '' + A one-sentence description of the desktop environment. + ''; + default = "${config.fullName} desktop environment"; + defaultText = lib.literalExpression "\${.fullName} desktop environment"; + example = "A desktop environment featuring a scrolling compositor."; + }; + + components = lib.mkOption { + type = with lib.types; attrsOf (submoduleWith { + specialArgs = { + inherit utils; + session = { + inherit (config) fullName desktopNames description; + inherit name; + }; + }; + modules = [ ./component-type.nix ]; + }); + description = '' + The individual components to be launched with the desktop session. + ''; + default = { }; + example = lib.literalExpression '' + { + } + ''; + }; + + targetUnit = lib.mkOption { + type = + let + inherit (utils.systemdUtils.lib) unitConfig; + inherit (utils.systemdUtils.unitOptions) commonUnitOptions; + in + lib.types.submodule [ + commonUnitOptions + unitConfig + ]; + description = '' + systemd target configuration to be generated for + `.target`. + + By default, the session target will have all of its components from + {option}`.requiredComponents` under `Wants=` directive. It + also assumes all of them have a target unit at + `''${requiredComponent}.target`. + + :::{.note} + This has the same options as {option}`systemd.user.targets.` + but without certain options from stage 2 counterparts such as + `reloadTriggers` and `restartTriggers`. + ::: + ''; + defaultText = '' + { + wants = ... # All of the required components as a target unit. + } + ''; + }; + + serviceUnit = lib.mkOption { + type = + let + inherit (utils.systemdUtils.lib) unitConfig serviceConfig; + inherit (utils.systemdUtils.unitOptions) commonUnitOptions serviceOptions; + in + lib.types.submodule [ + commonUnitOptions + serviceOptions + serviceConfig + unitConfig + ]; + description = '' + systemd service configuration to be generated for the sessiond session + itself. + + :::{.note} + This has the same options as {option}`systemd.user.services.` + but without certain options from stage 2 counterparts such as + `reloadTriggers` and `restartTriggers`. + + By default, this module sets the service unit as part of the respective + target unit (i.e., `PartOf=$COMPONENTID.target`). + + On a typical case, you shouldn't mess with much of the dependency + ordering with the service unit. You should configure `targetUnit` for + that instead. + ::: + ''; + }; + + extraArgs = lib.mkOption { + type = with lib.types; listOf str; + description = '' + A list of arguments from {program}`sessiond` to be added for the session + script. + ''; + example = lib.literalExpression '' + [ + "--hooksd=''${./config/sessiond/hooks.d}" + ] + ''; + }; + }; + + # Append the session argument. + config = { + extraArgs = lib.optional (config.settings != { }) "--config=${sessionSettingsFile}"; + + targetUnit = { + description = config.description; + requires = [ "${name}.service" ]; + wants = + let + componentTargetUnits = + lib.mapAttrsToList (_: component: "${component.id}.target") config.components; + in + componentTargetUnits; + }; + + serviceUnit = { + description = config.description; + partOf = [ "${name}.target" ]; + before = [ "${name}.target" ]; + requisite = [ "${name}.target" ]; + requires = [ "dbus.socket" ]; + after = [ "dbus.socket" ]; + + serviceConfig = { + Type = lib.mkForce "dbus"; + BusName = lib.mkForce "org.sessiond.session1"; + ExecStart = lib.mkForce "${lib.getExe' sessiondPkg "sessiond"} ${lib.concatStringsSep " " config.extraArgs}"; + Restart = "always"; + }; + + unitConfig = { + RefuseManualStart = true; + RefuseManualStop = true; + }; + }; + }; +}