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:
- 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}

View file

@ -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.
'';
}
];
};
}

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 {
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

View file

@ -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";
};
};

View file

@ -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";
}