mirror of
https://github.com/nix-community/home-manager
synced 2024-11-26 21:19:45 +01:00
systemd: enable start services by default
This enables the use of sd-switch to reload, start, and stop services as needed when switching configurations. This also entirely removes the Ruby based service helper. Fixes #5452
This commit is contained in:
parent
a9c9cc6e50
commit
d835fdc2d0
4 changed files with 39 additions and 236 deletions
|
@ -7,7 +7,19 @@ is therefore not final.
|
||||||
|
|
||||||
This release has the following notable changes:
|
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}
|
## State Version Changes {#sec-release-24.11-state-version-changes}
|
||||||
|
|
||||||
|
|
|
@ -1724,6 +1724,23 @@ in {
|
||||||
editor).
|
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.
|
||||||
|
'';
|
||||||
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
|
@ -180,10 +180,9 @@ in {
|
||||||
};
|
};
|
||||||
|
|
||||||
startServices = mkOption {
|
startServices = mkOption {
|
||||||
default = "suggest";
|
default = true;
|
||||||
type = with types;
|
type = types.bool;
|
||||||
either bool (enum [ "suggest" "legacy" "sd-switch" ]);
|
apply = p: if p == "sd-switch" then true else p;
|
||||||
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
|
||||||
|
@ -191,20 +190,14 @@ in {
|
||||||
|
|
||||||
The alternatives are
|
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
|
: Use a very simple shell script to print suggested
|
||||||
{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`)
|
|
||||||
: Use sd-switch, a tool that determines the necessary changes and
|
|
||||||
automatically apply them.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -327,10 +320,7 @@ in {
|
||||||
suggest = ''
|
suggest = ''
|
||||||
bash ${./systemd-activate.sh} "''${oldGenPath=}" "$newGenPath"
|
bash ${./systemd-activate.sh} "''${oldGenPath=}" "$newGenPath"
|
||||||
'';
|
'';
|
||||||
legacy = ''
|
|
||||||
${pkgs.ruby}/bin/ruby ${./systemd-activate.rb} \
|
|
||||||
"''${oldGenPath=}" "$newGenPath" "${servicesStartTimeoutMs}"
|
|
||||||
'';
|
|
||||||
sd-switch = let
|
sd-switch = let
|
||||||
timeoutArg = if cfg.servicesStartTimeoutMs != 0 then
|
timeoutArg = if cfg.servicesStartTimeoutMs != 0 then
|
||||||
"--timeout " + servicesStartTimeoutMs
|
"--timeout " + servicesStartTimeoutMs
|
||||||
|
|
Loading…
Reference in a new issue