diff --git a/Cargo.lock b/Cargo.lock index e6572b7..e6d58bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1013,7 +1013,7 @@ dependencies = [ ] [[package]] -name = "sessiond-dinit" +name = "sessiond-hooks" version = "0.1.0" dependencies = [ "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 84059f6..7b61f47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "sessiond-dinit" +name = "sessiond-hooks" version = "0.1.0" edition = "2024" diff --git a/README.md b/README.md index d44e647..37b4098 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,22 @@ -# sessiond-dinit +# sessiond-hooks -`sessiond-dinit` is a `sessiond` client that starts and tracks per-user -`dinit --user` service managers. +`sessiond-hooks` is a `sessiond` client that launches and supervises a per-user +session hook — an arbitrary service manager or daemon (`dinit --user`, +`runsvdir`, …) started for every login session. What to run is described by a +Dhall config function, so it is not tied to any particular init. + +Run several at once by defining multiple named instances (see below); each is an +independent process applying its own config. ## Installation -`sessiond-dinit` requires `sessiond` to be installed and enabled first. +`sessiond-hooks` requires `sessiond` to be installed and enabled first. On finix with flakes: ```nix { - inputs.sessiond-dinit.url = "git+https://tangled.org/r0chd.pl/sessiond-dinit"; + inputs.sessiond-hooks.url = "git+https://tangled.org/r0chd.pl/sessiond-hooks"; } ``` @@ -19,12 +24,12 @@ On finix with flakes: { inputs, pkgs, ... }: { imports = [ - inputs.sessiond-dinit.finixModules.default + inputs.sessiond-hooks.finixModules.default ]; - services.sessiond-dinit = { - enable = true; - package = inputs.sessiond-dinit.packages.${pkgs.system}.default; + services.sessiond-hooks.dinit = { + package = inputs.sessiond-hooks.packages.${pkgs.system}.default; + configFile = ./dinit.dhall; }; } ``` @@ -32,23 +37,35 @@ On finix with flakes: With npins: ```sh -npins add git --name sessiond-dinit https://tangled.org/r0chd.pl/sessiond-dinit --branch master +npins add git --name sessiond-hooks https://tangled.org/r0chd.pl/sessiond-hooks --branch master ``` ```nix { pkgs, ... }: let sources = import ./npins; - sessiond-dinit = import sources.sessiond-dinit.outPath { }; + sessiond-hooks = import sources.sessiond-hooks.outPath { }; in { imports = [ - sessiond-dinit.finixModules.default + sessiond-hooks.finixModules.default ]; - services.sessiond-dinit = { - enable = true; - package = sessiond-dinit.packages.sessiond-dinit; + services.sessiond-hooks.dinit = { + package = sessiond-hooks.packages.sessiond-hooks; + configFile = ./dinit.dhall; + }; +} +``` + +Defining an entry under `services.sessiond-hooks.` makes it live — there is +no separate `enable` toggle. Add more entries to launch more hooks: + +```nix +{ + services.sessiond-hooks = { + dinit = { configFile = ./dinit.dhall; }; + syncthing = { configFile = ./syncthing.dhall; }; }; } ``` @@ -59,14 +76,23 @@ To build it manually: cargo build --release ``` -The binary is written to `target/release/sessiond-dinit`. +The binary is written to `target/release/sessiond-hooks`. ## Usage ```sh -sessiond-dinit [--services-path PATH] +sessiond-hooks --config PATH [--log-target stderr|syslog] +``` + +`--config` points at a Dhall file that evaluates to a function + +```dhall +\(ctx : { uid : Natural, user : Text, home : Text, shell : Text }) + -> { init : Text, args : List Text } ``` -When `--services-path` is set, `dinit --user` reads services from that path. -Otherwise, `sessiond-dinit` first checks the user's `~/.config/dinit.d` and -falls back to `/etc/dinit.d/user`. +For each login session, `sessiond-hooks` applies this function to the session's +context and spawns `init` with `args`. The free-form `args` list means any +service manager works — `dinit --user --services-dir DIR`, `runsvdir DIR`, and so +on. A config may import the vendored Dhall Prelude via `./Prelude/package.dhall` +(placed next to the config); see `dhall/config.example.dhall`. diff --git a/default.nix b/default.nix index 9ae068e..ac8776a 100644 --- a/default.nix +++ b/default.nix @@ -23,7 +23,7 @@ in finixModules = rec { default = import ./nix/finix.nix; - sessiond-dinit = default; + sessiond-hooks = default; }; packages = rec { @@ -33,6 +33,6 @@ in rustc = rustToolchain; }; }; - sessiond-dinit = default; + sessiond-hooks = default; }; } diff --git a/dhall/config.example.dhall b/dhall/config.example.dhall index 65a2227..cda1f17 100644 --- a/dhall/config.example.dhall +++ b/dhall/config.example.dhall @@ -1,4 +1,4 @@ --- sessiond-dinit configuration. +-- sessiond-hooks configuration. -- -- Evaluates to a function from the per-session context to the init settings. -- The vendored Prelude is imported directly from the filesystem, relative to diff --git a/flake.nix b/flake.nix index d7daf2e..aedb444 100644 --- a/flake.nix +++ b/flake.nix @@ -65,7 +65,7 @@ }; packages = forAllSystems (pkgs: { - sessiond-dinit = pkgs.callPackage ./nix/package.nix { + sessiond-hooks = pkgs.callPackage ./nix/package.nix { rustPlatform = let rustToolchain = pkgs.rust-bin.stable.latest.default.override { @@ -81,12 +81,12 @@ rustc = rustToolchain; }; }; - default = self.packages.${pkgs.stdenv.hostPlatform.system}.sessiond-dinit; + default = self.packages.${pkgs.stdenv.hostPlatform.system}.sessiond-hooks; }); finixModules = { - sessiond-dinit = import ./nix/finix.nix; - default = self.finixModules.sessiond-dinit; + sessiond-hooks = import ./nix/finix.nix; + default = self.finixModules.sessiond-hooks; }; formatter = forAllSystems ( @@ -111,13 +111,13 @@ finix-vm = finix.lib.finixSystem { inherit (pkgs) lib; specialArgs = { - inherit (self.packages.${system}) sessiond-dinit; + inherit (self.packages.${system}) sessiond-hooks; sessiond = import sources.sessiond.outPath { inherit pkgs; }; }; modules = [ { nixpkgs = { inherit pkgs; }; } ./nix/finix-vm.nix - self.finixModules.sessiond-dinit + self.finixModules.sessiond-hooks "${finix.outPath}/modules/virtualisation/qemu.nix" ] ++ builtins.attrValues { diff --git a/nix/finix-vm.nix b/nix/finix-vm.nix index 0652c29..3ab5fde 100644 --- a/nix/finix-vm.nix +++ b/nix/finix-vm.nix @@ -1,7 +1,7 @@ { pkgs, sessiond, - sessiond-dinit, + sessiond-hooks, ... }: let @@ -12,10 +12,9 @@ in services = { sysklogd.enable = true; - sessiond-dinit = { - enable = true; - package = sessiond-dinit; - configFile = pkgs.writeText "sessiond-dinit-config.dhall" '' + sessiond-hooks.dinit = { + package = sessiond-hooks; + configFile = pkgs.writeText "sessiond-hooks-dinit.dhall" '' \(ctx : { uid : Natural, user : Text, home : Text, shell : Text }) -> { init = "${pkgs.dinit}/bin/dinit" , args = [ "--user", "--services-dir", "''${ctx.home}/.config/dinit.d" ] diff --git a/nix/finix.nix b/nix/finix.nix index 3cbcc66..bbeecf4 100644 --- a/nix/finix.nix +++ b/nix/finix.nix @@ -6,66 +6,83 @@ }: let inherit (lib) types; - cfg = config.services.sessiond-dinit; + cfg = config.services.sessiond-hooks; + # Assemble a directory where the config sits next to the Prelude, so the # config's `./Prelude/package.dhall` import resolves. The config is copied (the # daemon canonicalizes its path, which would follow a symlink away from the # Prelude sibling); the Prelude is symlinked as a whole directory. - configDir = pkgs.runCommandLocal "sessiond-dinit-config" { } '' - mkdir -p $out - ln -s ${cfg.preludePath} $out/Prelude - cp ${cfg.configFile} $out/config.dhall - ''; + configDirFor = + name: instance: + pkgs.runCommandLocal "sessiond-hooks-${name}-config" { } '' + mkdir -p $out + ln -s ${instance.preludePath} $out/Prelude + cp ${instance.configFile} $out/config.dhall + ''; + + instanceModule = + { config, ... }: + { + options = { + package = lib.mkPackageOption pkgs "sessiond-hooks" { }; + logLevel = lib.mkOption { + type = types.enum [ + "trace" + "debug" + "info" + "warn" + "error" + "off" + ]; + default = "info"; + description = "Maximum verbosity of this hook's log messages."; + }; + configFile = lib.mkOption { + type = types.path; + description = '' + Dhall configuration file: a function + `Context -> { init : Text, args : List Text }` where + `Context = { uid : Natural, user : Text, home : Text, shell : Text }`. + It may import the vendored Prelude via `./Prelude/package.dhall`. + ''; + }; + preludePath = lib.mkOption { + type = types.path; + default = "${config.package}/share/sessiond-hooks/Prelude"; + defaultText = lib.literalExpression ''"''${config.package}/share/sessiond-hooks/Prelude"''; + description = "Dhall Prelude directory symlinked next to the config as `./Prelude`."; + }; + }; + }; in { - options.services.sessiond-dinit = { - enable = lib.mkEnableOption "sessiond-dinit"; - package = lib.mkPackageOption pkgs "sessiond-dinit" { }; - logLevel = lib.mkOption { - type = types.enum [ - "trace" - "debug" - "info" - "warn" - "error" - "off" - ]; - default = "info"; - description = "Maximum verbosity of sessiond-dinit log messages."; - }; - configFile = lib.mkOption { - type = types.path; - description = '' - Dhall configuration file: a function - `Context -> { init : Text, servicesDir : Text }` where - `Context = { uid : Natural, user : Text, home : Text, shell : Text }`. - It may import the vendored Prelude via `./Prelude/package.dhall`. - ''; - }; - preludePath = lib.mkOption { - type = types.path; - default = "${cfg.package}/share/sessiond-dinit/Prelude"; - defaultText = lib.literalExpression ''"''${cfg.package}/share/sessiond-dinit/Prelude"''; - description = "Dhall Prelude directory symlinked next to the config as `./Prelude`."; - }; + options.services.sessiond-hooks = lib.mkOption { + type = types.attrsOf (types.submodule instanceModule); + default = { }; + description = '' + Per-user session hooks launched via sessiond. Each named entry spawns an + independent `sessiond-hooks` process (finit service `sessiond-hooks-`) + that applies its Dhall config to every login session. Defining an entry + makes it live; there is no separate enable toggle. + ''; }; - config = lib.mkIf cfg.enable { - environment.systemPackages = [ pkgs.dinit ]; - finit.services.sessiond-dinit = { - description = "sessiond-dinit per-user dinit launcher"; + config.finit.services = lib.mapAttrs' ( + name: instance: + lib.nameValuePair "sessiond-hooks-${name}" { + description = "sessiond per-user hook: ${name}"; conditions = [ "service/sessiond/ready" "service/syslogd/ready" ]; command = lib.escapeShellArgs [ - (lib.getExe cfg.package) + (lib.getExe instance.package) "--log-target" "syslog" "--config" - "${configDir}/config.dhall" + "${configDirFor name instance}/config.dhall" ]; - environment.LOG_LEVEL = cfg.logLevel; - }; - }; + environment.LOG_LEVEL = instance.logLevel; + } + ) cfg; } diff --git a/nix/package.nix b/nix/package.nix index df812bb..3cfcd92 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -20,16 +20,16 @@ rustPlatform.buildRustPackage { # Ship the vendored Dhall Prelude so the module can place it next to a config # file (configs import it via `./Prelude/package.dhall`). postInstall = '' - install -d $out/share/sessiond-dinit - cp -r dhall/Prelude $out/share/sessiond-dinit/Prelude + install -d $out/share/sessiond-hooks + cp -r dhall/Prelude $out/share/sessiond-hooks/Prelude ''; meta = { - description = "sessiond-dinit per-user dinit supervision"; - homepage = "https://tangled.org/r0chd.pl/sessiond-dinit"; + description = "sessiond-hooks per-user session hook supervision"; + homepage = "https://tangled.org/r0chd.pl/sessiond-hooks"; license = lib.licenses.gpl3; maintainers = builtins.attrValues { inherit (lib.maintainers) r0chd; }; platforms = lib.platforms.linux; - mainProgram = "sessiond-dinit"; + mainProgram = "sessiond-hooks"; }; } diff --git a/nix/shell.nix b/nix/shell.nix index 446100b..507ffa2 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -1,6 +1,6 @@ { pkgs, rustToolchain }: pkgs.mkShell { - name = "sessiond-dinit-devshell"; + name = "sessiond-hooks-devshell"; packages = builtins.attrValues { inherit rustToolchain diff --git a/src/main.rs b/src/main.rs index d256f20..d18836f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -44,7 +44,7 @@ struct Config { } #[derive(Parser)] -#[command(version, about = "Start and track per-user dinit service managers")] +#[command(version, about = "Launch and supervise a per-user session hook")] struct Cli { #[arg( long, @@ -97,11 +97,11 @@ struct ManagedSession { id: u32, uid: u32, object: session_management_session::SessionManagementSession, - dinit: Option, + hook: Option, envs: BTreeMap, } -struct SessiondDinit { +struct SessiondHooks { managed_sessions: Vec, sessions: Vec, config_path: path::PathBuf, @@ -151,7 +151,7 @@ fn is_regular_user(uid: u32) -> bool { shell != "/usr/sbin/nologin" && shell != "/sbin/nologin" && shell != "/bin/false" } -impl hyprwire::Dispatch for SessiondDinit { +impl hyprwire::Dispatch for SessiondHooks { fn event( &mut self, object: &session_core_manager::SessionCoreManager, @@ -169,7 +169,7 @@ impl hyprwire::Dispatch for SessiondDi log::info!("observed session {session_id}"); - let Some(session_object) = object.send_get_session::(session_id) + let Some(session_object) = object.send_get_session::(session_id) else { log::error!("failed to get session {session_id}: protocol returned no object"); return; @@ -190,7 +190,7 @@ impl hyprwire::Dispatch for SessiondDi } } -impl hyprwire::Dispatch for SessiondDinit { +impl hyprwire::Dispatch for SessiondHooks { fn event( &mut self, object: &session_management_session::SessionManagementSession, @@ -240,9 +240,9 @@ impl hyprwire::Dispatch fo return; }; - if session.dinit.is_some() { + if session.hook.is_some() { log::warn!( - "dinit is already managed for session {} uid {}; ignoring duplicate done", + "hook is already running for session {} uid {}; ignoring duplicate done", session.id, session.uid ); @@ -250,7 +250,7 @@ impl hyprwire::Dispatch fo } let Some(user) = uzers::get_user_by_uid(session.uid) else { - log::error!("cannot start dinit for unknown uid {}", session.uid); + log::error!("cannot start hook for unknown uid {}", session.uid); return; }; let ctx = SessionContext { @@ -305,7 +305,7 @@ impl hyprwire::Dispatch fo exec env "$@" "#, ) - .arg("dinit-session"); + .arg("sessiond-hook"); for (key, value) in envs.iter() { cmd.arg(format!("{key}={value}")); } @@ -326,8 +326,8 @@ impl hyprwire::Dispatch fo session.uid, envs.len() ); - let dinit = match cmd.spawn() { - Ok(dinit) => dinit, + let child = match cmd.spawn() { + Ok(child) => child, Err(err) => { log::error!( "failed to spawn {:?} for uid {}: {err}", @@ -340,11 +340,11 @@ impl hyprwire::Dispatch fo log::info!( "spawned {:?} pid {} for session {} uid {}", config.init, - dinit.id(), + child.id(), session.id, session.uid ); - session.dinit = Some(dinit); + session.hook = Some(child); } session_management_session::Event::Error { code, message } => { log::error!("session_management_session error {code:?}: {message}"); @@ -354,7 +354,7 @@ impl hyprwire::Dispatch fo } } -impl hyprwire::Dispatch for SessiondDinit { +impl hyprwire::Dispatch for SessiondHooks { fn event( &mut self, object: &session_core_session::SessionCoreSession, @@ -383,7 +383,7 @@ impl hyprwire::Dispatch for SessiondDi .iter() .any(|session| session.uid == uid) { - log::info!("uid {uid} already has a managed dinit session"); + log::info!("uid {uid} already has a managed hook session"); return; } @@ -407,7 +407,7 @@ impl hyprwire::Dispatch for SessiondDi id: 0, uid, object: session_object, - dinit: None, + hook: None, envs: BTreeMap::new(), }); } @@ -469,21 +469,21 @@ fn main() -> anyhow::Result<()> { match cli.log_target { LogTarget::Stderr => { env_logger::Builder::new() - .filter(Some("sessiond_dinit"), log_level) + .filter(Some("sessiond_hooks"), log_level) .init(); } LogTarget::Syslog => { syslog::init( syslog::Facility::LOG_DAEMON, log_level, - Some("sessiond-dinit"), + Some("sessiond-hooks"), )?; } } let mut event_loop = calloop::EventLoop::try_new()?; - let mut sessiond_dinit = SessiondDinit { + let mut sessiond_hooks = SessiondHooks { loop_handle: event_loop.handle(), managed_sessions: Vec::new(), sessions: Vec::new(), @@ -498,15 +498,15 @@ fn main() -> anyhow::Result<()> { socket.add_implementation::(); socket.add_implementation::(); - eq.wait_for_handshake(&mut sessiond_dinit)?; + eq.wait_for_handshake(&mut sessiond_hooks)?; - sessiond_dinit.session_manager = - Some(socket.bind::(&eq, &mut sessiond_dinit, 1)?); - sessiond_dinit.session_core_manager = Some( - socket.bind::(&eq, &mut sessiond_dinit, 1)?, + sessiond_hooks.session_manager = + Some(socket.bind::(&eq, &mut sessiond_hooks, 1)?); + sessiond_hooks.session_core_manager = Some( + socket.bind::(&eq, &mut sessiond_hooks, 1)?, ); - eq.roundtrip(&mut sessiond_dinit)?; + eq.roundtrip(&mut sessiond_hooks)?; let fd_wrapper = unsafe { generic::FdWrapper::new(socket.extract_loop_fd().as_raw_fd()) }; let source = generic::Generic::new( @@ -524,12 +524,12 @@ fn main() -> anyhow::Result<()> { Ok(calloop::PostAction::Continue) })?; - event_loop.run(None, &mut sessiond_dinit, |_| {})?; + event_loop.run(None, &mut sessiond_hooks, |_| {})?; Ok(()) } -hyprwire::delegate_noop!(SessiondDinit: session_manager::SessionManager); +hyprwire::delegate_noop!(SessiondHooks: session_manager::SessionManager); #[cfg(test)] mod tests {