diff --git a/docs/release-notes/rl-2505.md b/docs/release-notes/rl-2505.md index dec0f213..5d47f2c6 100644 --- a/docs/release-notes/rl-2505.md +++ b/docs/release-notes/rl-2505.md @@ -7,7 +7,15 @@ section is therefore not final. This release has the following notable changes: -- No changes. +- The [](#opt-systemd.user.startServices) option now defaults to + `true`, meaning that services will automatically be restarted as + needed when activating a configuration. + + Further, the `"legacy"` alternative has been removed and will now + result in an evaluation error if used. + + The `"suggest"` alternative will remain for a while longer but may + also be deprecated for removal in the future. ## State Version Changes {#sec-release-25.05-state-version-changes} diff --git a/modules/misc/news.nix b/modules/misc/news.nix index d09176e5..4cfa0e64 100644 --- a/modules/misc/news.nix +++ b/modules/misc/news.nix @@ -1919,6 +1919,22 @@ in { Cavalier is a GUI wrapper around the Cava audio visualizer. ''; } + + { + time = "2025-01-01T15:31:15+00:00"; + condition = hostPlatform.isLinux; + message = '' + The 'systemd.user.startServices' option now defaults to 'true', + meaning that services will automatically be restarted as needed when + activating a configuration. + + Further, the "legacy" alternative has been removed and will now result + in an evaluation error if used. + + The "suggest" alternative will remain for a while longer but may also + be deprecated for removal in the future. + ''; + } ]; }; } diff --git a/modules/systemd-activate.rb b/modules/systemd-activate.rb deleted file mode 100644 index 31d06d8f..00000000 --- a/modules/systemd-activate.rb +++ /dev/null @@ -1,216 +0,0 @@ -require 'set' -require 'open3' - -@dry_run = ENV['DRY_RUN'] -@verbose = ENV['VERBOSE'] - -UnitsDir = 'home-files/.config/systemd/user' - -# 1. Stop all services from the old generation that are not present in the new generation. -# 2. Ensure all services from the new generation that are wanted by active targets are running: -# - Start services that are not already running. -# - Restart services whose unit config files have changed between generations. -# 3. If any services were (re)started, wait 'start_timeout_ms' and report services -# that failed to start. This helps debugging quickly failing services. -# -# Whenever service failures are detected, show the output of -# 'systemd --user status' for the affected services. -# -def setup_services(old_gen_path, new_gen_path, start_timeout_ms_string) - start_timeout_ms = start_timeout_ms_string.to_i - - old_units_path = File.join(old_gen_path, UnitsDir) unless old_gen_path.empty? - new_units_path = File.join(new_gen_path, UnitsDir) - - old_services = get_services(old_units_path) - new_services = get_services(new_units_path) - - exit if old_services.empty? && new_services.empty? - - all_services = get_active_targets_units(new_units_path) - maybe_changed = all_services & old_services - changed_services = get_changed_services(old_units_path, new_units_path, maybe_changed) - unchanged_oneshots = get_oneshot_services(maybe_changed - changed_services) - - # These services should be running when this script is finished - services_to_run = all_services - unchanged_oneshots - - # Only stop active services, otherwise we might get a 'service not loaded' error - # for inactive services that were removed in the current generation. - to_stop = get_active_units(old_services - new_services) - to_restart = changed_services - to_start = get_inactive_units(services_to_run - to_restart) - - raise "daemon-reload failed" unless run_cmd('systemctl', '--user', 'daemon-reload') - - # Exclude units that shouldn't be (re)started or stopped - no_manual_start, no_manual_stop, no_restart = get_restricted_units(to_stop + to_restart + to_start) - notify_skipped_units(to_restart & no_restart) - to_stop -= no_manual_stop - to_restart -= no_manual_stop + no_manual_start + no_restart - to_start -= no_manual_start - - if to_stop.empty? && to_start.empty? && to_restart.empty? - print_service_msg("All services are already running", services_to_run) - else - puts "Setting up services" if @verbose - systemctl_action('stop', to_stop) - systemctl_action('start', to_start) - systemctl_action('restart', to_restart) - started_services = to_start + to_restart - if start_timeout_ms > 0 && !started_services.empty? && !@dry_run - failed = wait_and_get_failed_services(started_services, start_timeout_ms) - if failed.empty? - print_service_msg("All services are running", services_to_run) - else - puts - puts "Error. These services failed to start:", failed - show_failed_services_status(failed) - exit 1 - end - end - end -end - -def get_services(dir) - services = get_service_files(dir) if dir && Dir.exists?(dir) - Set.new(services) -end - -def get_service_files(dir) - Dir.chdir(dir) { Dir['*[^@].{service,socket,timer}'] } -end - -def get_changed_services(dir_a, dir_b, services) - services.select do |service| - a = File.join(dir_a, service) - b = File.join(dir_b, service) - (File.size(a) != File.size(b)) || (File.read(a) != File.read(b)) - end -end - -TargetDirRegexp = /^(.*\.target)\.wants$/ - -# @return all units wanted by active targets -def get_active_targets_units(units_dir) - return Set.new unless Dir.exists?(units_dir) - targets = Dir.entries(units_dir).map { |entry| entry[TargetDirRegexp, 1] }.compact - active_targets = get_active_units(targets) - active_units = active_targets.map do |target| - get_service_files(File.join(units_dir, "#{target}.wants")) - end.flatten - Set.new(active_units) -end - -# @return true on success -def run_cmd(*cmd) - print_cmd cmd - @dry_run || system(*cmd) -end - -def systemctl_action(cmd, services) - return if services.empty? - - verb = (cmd == 'stop') ? 'Stopping' : "#{cmd.capitalize}ing" - puts "#{verb}: #{services.join(' ')}" - - cmd = ['systemctl', '--user', cmd, *services] - if @dry_run - puts cmd.join(' ') - return - end - - output, status = Open3.capture2e(*cmd) - print output - # Show status for failed services - unless status.success? - # Due to a bug in systemd, the '--user' argument is not always provided - output.scan(/systemctl (?:--user )?(status .*?)['"]/).flatten.each do |status_cmd| - puts - run_cmd("systemctl --user #{status_cmd}") - end - exit 1 - end -end - -def systemctl(*cmd) - output, _ = Open3.capture2('systemctl', '--user', *cmd) - output -end - -def print_cmd(cmd) - puts [*cmd].join(' ') if @verbose || @dry_run -end - -def get_active_units(units) - filter_units(units) { |state| state == 'active' } -end - -def get_inactive_units(units) - filter_units(units) { |state| state != 'active' } -end - -def get_failed_units(units) - filter_units(units) { |state| state == 'failed' } -end - -def filter_units(units) - return [] if units.empty? - states = systemctl('is-active', *units).split - units.select.with_index { |_, i| yield states[i] } -end - -def get_oneshot_services(units) - return [] if units.empty? - types = systemctl('show', '-p', 'Type', *units).split - units.select.with_index do |_, i| - types[i] == 'Type=oneshot' - end -end - -def get_restricted_units(units) - infos = systemctl('show', '-p', 'RefuseManualStart', '-p', 'RefuseManualStop', *units) - .split("\n\n") - no_manual_start = [] - no_manual_stop = [] - infos.zip(units).each do |info, unit| - no_start, no_stop = info.split("\n") - no_manual_start << unit if no_start.end_with?('yes') - no_manual_stop << unit if no_stop.end_with?('yes') - end - # Get units that should not be restarted even if a change has been detected. - no_restart_regexp = /^\s*X-RestartIfChanged\s*=\s*false\b/ - no_restart = units.select { |unit| systemctl('cat', unit) =~ no_restart_regexp } - [no_manual_start, no_manual_stop, no_restart] -end - -def wait_and_get_failed_services(services, start_timeout_ms) - puts "Waiting #{start_timeout_ms} ms for services to fail" - # Force the previous message to always be visible before sleeping - STDOUT.flush - sleep(start_timeout_ms / 1000.0) - get_failed_units(services) -end - -def show_failed_services_status(services) - puts - services.each do |service| - run_cmd('systemctl', '--user', 'status', service) - puts - end -end - -def print_service_msg(msg, services) - return if services.empty? - if @verbose - puts "#{msg}:", services.to_a - else - puts msg - end -end - -def notify_skipped_units(no_restart) - puts "Not restarting: #{no_restart.join(' ')}" unless no_restart.empty? -end - -setup_services(*ARGV) diff --git a/modules/systemd.nix b/modules/systemd.nix index 9d3777c6..3b8e0dbd 100644 --- a/modules/systemd.nix +++ b/modules/systemd.nix @@ -180,10 +180,9 @@ in { }; startServices = mkOption { - default = "suggest"; - type = with types; - either bool (enum [ "suggest" "legacy" "sd-switch" ]); - apply = p: if isBool p then if p then "sd-switch" else "suggest" else p; + type = with types; either bool (enum [ "suggest" "sd-switch" ]); + apply = p: if isBool p then p else p == "sd-switch"; + default = true; description = '' Whether new or changed services that are wanted by active targets should be started. Additionally, stop obsolete services from the @@ -196,12 +195,6 @@ in { {command}`systemctl` commands to run. You will have to manually run those commands after the switch. - `legacy` - : Use a Ruby script to, in a more robust fashion, determine the - necessary changes and automatically run the - {command}`systemctl` commands. Note, this alternative will soon - be removed. - `sd-switch` (or `true`) : Use sd-switch, a tool that determines the necessary changes and automatically apply them. @@ -297,12 +290,6 @@ in { message = "This module is only available on Linux."; }]; - warnings = lib.optional (cfg.startServices == "legacy") '' - Having 'systemd.user.startServices = "legacy"' is deprecated and will soon be removed. - - Please change to 'systemd.user.startServices = true' to use the new systemd unit switcher (sd-switch). - ''; - xdg.configFile = mkMerge [ (lib.listToAttrs ((buildServices "service" cfg.services) ++ (buildServices "slice" cfg.slices) @@ -323,26 +310,23 @@ in { # set and systemd commands will fail. We'll therefore have to # set it ourselves in that case. home.activation.reloadSystemd = hm.dag.entryAfter [ "linkGeneration" ] (let - cmd = { - suggest = '' - bash ${./systemd-activate.sh} "''${oldGenPath=}" "$newGenPath" - ''; - legacy = '' - ${pkgs.ruby}/bin/ruby ${./systemd-activate.rb} \ - "''${oldGenPath=}" "$newGenPath" "${servicesStartTimeoutMs}" - ''; - sd-switch = let - timeoutArg = if cfg.servicesStartTimeoutMs != 0 then - "--timeout " + servicesStartTimeoutMs - else - ""; - in '' - ${lib.getExe pkgs.sd-switch} \ - ''${DRY_RUN:+--dry-run} $VERBOSE_ARG ${timeoutArg} \ - ''${oldUnitsDir:+--old-units $oldUnitsDir} \ - --new-units "$newUnitsDir" - ''; - }; + suggestCmd = '' + bash ${./systemd-activate.sh} "''${oldGenPath=}" "$newGenPath" + ''; + + sdSwitchCmd = let + timeoutArg = if cfg.servicesStartTimeoutMs != 0 then + "--timeout " + servicesStartTimeoutMs + else + ""; + in '' + ${lib.getExe pkgs.sd-switch} \ + ''${DRY_RUN:+--dry-run} $VERBOSE_ARG ${timeoutArg} \ + ''${oldUnitsDir:+--old-units $oldUnitsDir} \ + --new-units "$newUnitsDir" + ''; + + systemdCmd = if cfg.startServices then sdSwitchCmd else suggestCmd; # Make sure that we have an environment where we are likely to # successfully talk with systemd. @@ -374,7 +358,7 @@ in { newUnitsDir=${pkgs.emptyDirectory} fi - ${ensureSystemd} ${getAttr cfg.startServices cmd} + ${ensureSystemd} ${systemdCmd} unset newUnitsDir oldUnitsDir else diff --git a/tests/integration/nixos/basics.nix b/tests/integration/nixos/basics.nix index 61c6b3e4..4b7cbcf8 100644 --- a/tests/integration/nixos/basics.nix +++ b/tests/integration/nixos/basics.nix @@ -21,9 +21,6 @@ home.file.test.text = "testfile"; # Enable a light-weight systemd service. services.pueue.enable = true; - # We focus on sd-switch since that hopefully will become the default in - # the future. - systemd.user.startServices = "sd-switch"; }; }; diff --git a/tests/integration/standalone/alice-home-next.nix b/tests/integration/standalone/alice-home-next.nix index 577214a4..416640ee 100644 --- a/tests/integration/standalone/alice-home-next.nix +++ b/tests/integration/standalone/alice-home-next.nix @@ -12,8 +12,4 @@ # Enable a light-weight systemd service. services.pueue.enable = true; - - # We focus on sd-switch since that hopefully will become the default in the - # future. - systemd.user.startServices = "sd-switch"; }