diff --git a/docs/release-notes/rl-2411.md b/docs/release-notes/rl-2411.md index 85eae034f..bf21cb6c6 100644 --- a/docs/release-notes/rl-2411.md +++ b/docs/release-notes/rl-2411.md @@ -7,7 +7,19 @@ is therefore not final. This release has the following notable changes: -- No changes. +- The option + [systemd.user.startServices](#opt-systemd.user.startServices) now + defaults to `true`. This means that managed systemd services will be + automatically started and stopped as needed. + + If you encounter errors please set the option to `false` in your + configuration: + + ```nix + systemd.user.startServices = false + ``` + + Also, please report the error in the Home Manager issue tracker. ## State Version Changes {#sec-release-24.11-state-version-changes} diff --git a/modules/misc/news.nix b/modules/misc/news.nix index 655546ff3..bc54a2b4e 100644 --- a/modules/misc/news.nix +++ b/modules/misc/news.nix @@ -1724,6 +1724,23 @@ in { editor). ''; } + + { + time = "2024-09-17T05:45:39+00:00"; + condition = hostPlatform.isLinux; + message = '' + The option 'systemd.user.startServices' now defaults to 'true'. This + means that managed systemd services will be automatically started and + stopped as needed. + + If you encounter errors please set the option to 'false' in your + configuration: + + systemd.user.startServices = false + + Also, please report the error in the Home Manager issue tracker. + ''; + } ]; }; } diff --git a/modules/systemd-activate.rb b/modules/systemd-activate.rb deleted file mode 100644 index 31d06d8fc..000000000 --- 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 9d3777c65..3510e5175 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; + default = true; + type = types.bool; + apply = p: if p == "sd-switch" then true else p; description = '' Whether new or changed services that are wanted by active targets should be started. Additionally, stop obsolete services from the @@ -191,20 +190,14 @@ in { The alternatives are - `suggest` (or `false`) + `true` (the default) + : Use sd-switch, a tool that determines the necessary changes and + automatically apply them. + + `false` : Use a very simple shell script to print suggested {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. ''; }; @@ -327,10 +320,7 @@ in { 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