systemd: use sd-switch by default

Also remove the legacy Ruby alternative.

Fixes #5452
This commit is contained in:
Robert Helgesson 2024-10-24 07:54:56 +02:00
parent 5518f9d439
commit 9a9fef316a
Failed to generate hash of commit
6 changed files with 46 additions and 261 deletions

View file

@ -7,7 +7,15 @@ section is therefore not final.
This release has the following notable changes: 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} ## State Version Changes {#sec-release-25.05-state-version-changes}

View file

@ -1919,6 +1919,22 @@ in {
Cavalier is a GUI wrapper around the Cava audio visualizer. 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.
'';
}
]; ];
}; };
} }

View file

@ -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)

View file

@ -180,10 +180,9 @@ in {
}; };
startServices = mkOption { startServices = mkOption {
default = "suggest"; type = with types; either bool (enum [ "suggest" "sd-switch" ]);
type = with types; apply = p: if isBool p then p else p == "sd-switch";
either bool (enum [ "suggest" "legacy" "sd-switch" ]); default = true;
apply = p: if isBool p then if p then "sd-switch" else "suggest" else p;
description = '' description = ''
Whether new or changed services that are wanted by active targets Whether new or changed services that are wanted by active targets
should be started. Additionally, stop obsolete services from the should be started. Additionally, stop obsolete services from the
@ -196,12 +195,6 @@ in {
{command}`systemctl` commands to run. You will have to {command}`systemctl` commands to run. You will have to
manually run those commands after the switch. 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`) `sd-switch` (or `true`)
: Use sd-switch, a tool that determines the necessary changes and : Use sd-switch, a tool that determines the necessary changes and
automatically apply them. automatically apply them.
@ -297,12 +290,6 @@ in {
message = "This module is only available on Linux."; 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 [ xdg.configFile = mkMerge [
(lib.listToAttrs ((buildServices "service" cfg.services) (lib.listToAttrs ((buildServices "service" cfg.services)
++ (buildServices "slice" cfg.slices) ++ (buildServices "slice" cfg.slices)
@ -323,26 +310,23 @@ in {
# set and systemd commands will fail. We'll therefore have to # set and systemd commands will fail. We'll therefore have to
# set it ourselves in that case. # set it ourselves in that case.
home.activation.reloadSystemd = hm.dag.entryAfter [ "linkGeneration" ] (let home.activation.reloadSystemd = hm.dag.entryAfter [ "linkGeneration" ] (let
cmd = { suggestCmd = ''
suggest = '' bash ${./systemd-activate.sh} "''${oldGenPath=}" "$newGenPath"
bash ${./systemd-activate.sh} "''${oldGenPath=}" "$newGenPath" '';
'';
legacy = '' sdSwitchCmd = let
${pkgs.ruby}/bin/ruby ${./systemd-activate.rb} \ timeoutArg = if cfg.servicesStartTimeoutMs != 0 then
"''${oldGenPath=}" "$newGenPath" "${servicesStartTimeoutMs}" "--timeout " + servicesStartTimeoutMs
''; else
sd-switch = let "";
timeoutArg = if cfg.servicesStartTimeoutMs != 0 then in ''
"--timeout " + servicesStartTimeoutMs ${lib.getExe pkgs.sd-switch} \
else ''${DRY_RUN:+--dry-run} $VERBOSE_ARG ${timeoutArg} \
""; ''${oldUnitsDir:+--old-units $oldUnitsDir} \
in '' --new-units "$newUnitsDir"
${lib.getExe pkgs.sd-switch} \ '';
''${DRY_RUN:+--dry-run} $VERBOSE_ARG ${timeoutArg} \
''${oldUnitsDir:+--old-units $oldUnitsDir} \ systemdCmd = if cfg.startServices then sdSwitchCmd else suggestCmd;
--new-units "$newUnitsDir"
'';
};
# Make sure that we have an environment where we are likely to # Make sure that we have an environment where we are likely to
# successfully talk with systemd. # successfully talk with systemd.
@ -374,7 +358,7 @@ in {
newUnitsDir=${pkgs.emptyDirectory} newUnitsDir=${pkgs.emptyDirectory}
fi fi
${ensureSystemd} ${getAttr cfg.startServices cmd} ${ensureSystemd} ${systemdCmd}
unset newUnitsDir oldUnitsDir unset newUnitsDir oldUnitsDir
else else

View file

@ -21,9 +21,6 @@
home.file.test.text = "testfile"; home.file.test.text = "testfile";
# Enable a light-weight systemd service. # Enable a light-weight systemd service.
services.pueue.enable = true; services.pueue.enable = true;
# We focus on sd-switch since that hopefully will become the default in
# the future.
systemd.user.startServices = "sd-switch";
}; };
}; };

View file

@ -12,8 +12,4 @@
# Enable a light-weight systemd service. # Enable a light-weight systemd service.
services.pueue.enable = true; services.pueue.enable = true;
# We focus on sd-switch since that hopefully will become the default in the
# future.
systemd.user.startServices = "sd-switch";
} }