mirror of
https://github.com/nix-community/home-manager
synced 2024-11-26 21:19:45 +01:00
parent
9c0fe3957b
commit
223e3c38a1
5 changed files with 344 additions and 39 deletions
|
@ -6,13 +6,10 @@ section is therefore not final.
|
||||||
|
|
||||||
[[sec-release-20.09-highlights]]
|
[[sec-release-20.09-highlights]]
|
||||||
=== Highlights
|
=== Highlights
|
||||||
:sd-switch-url: https://gitlab.com/rycee/sd-switch
|
|
||||||
|
|
||||||
This release has the following notable changes:
|
This release has the following notable changes:
|
||||||
|
|
||||||
* The systemd activation is now handled by {sd-switch-url}[sd-switch], a program that stops, starts, reloads, etc. systemd units as necessary to match the new Home Manager configuration.
|
* Nothing has happened.
|
||||||
+
|
|
||||||
Since sd-switch is relatively lightweight it is always used and the option `systemd.user.startServices` is therefore considered obsolete and can be removed from your configuration.
|
|
||||||
|
|
||||||
[[sec-release-20.09-state-version-changes]]
|
[[sec-release-20.09-state-version-changes]]
|
||||||
=== State Version Changes
|
=== State Version Changes
|
||||||
|
|
|
@ -1619,21 +1619,6 @@ in
|
||||||
A new module is available: 'services.dropbox'.
|
A new module is available: 'services.dropbox'.
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
time = "2020-08-03T22:34:42+00:00";
|
|
||||||
condition = hostPlatform.isLinux && (with config.systemd.user;
|
|
||||||
services != {} || sockets != {} || targets != {} || timers != {});
|
|
||||||
message = ''
|
|
||||||
The systemd activation is now handled by 'sd-switch', a program that
|
|
||||||
stops, starts, reloads, etc. systemd units as necessary to match the
|
|
||||||
new Home Manager configuration.
|
|
||||||
|
|
||||||
Since sd-switch is relatively lightweight it is always used and the
|
|
||||||
option 'systemd.user.startServices' is therefore considered obsolete
|
|
||||||
and can be removed from your configuration.
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
216
modules/systemd-activate.rb
Normal file
216
modules/systemd-activate.rb
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
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)
|
114
modules/systemd-activate.sh
Normal file
114
modules/systemd-activate.sh
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
function isStartable() {
|
||||||
|
local service="$1"
|
||||||
|
[[ $(systemctl --user show -p RefuseManualStart "$service") == *=no ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStoppable() {
|
||||||
|
if [[ -v oldGenPath ]] ; then
|
||||||
|
local service="$1"
|
||||||
|
[[ $(systemctl --user show -p RefuseManualStop "$service") == *=no ]]
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function systemdPostReload() {
|
||||||
|
local workDir
|
||||||
|
workDir="$(mktemp -d)"
|
||||||
|
|
||||||
|
if [[ -v oldGenPath ]] ; then
|
||||||
|
local oldUserServicePath="$oldGenPath/home-files/.config/systemd/user"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local newUserServicePath="$newGenPath/home-files/.config/systemd/user"
|
||||||
|
local oldServiceFiles="$workDir/old-files"
|
||||||
|
local newServiceFiles="$workDir/new-files"
|
||||||
|
local servicesDiffFile="$workDir/diff-files"
|
||||||
|
|
||||||
|
if [[ ! (-v oldUserServicePath && -d "$oldUserServicePath") \
|
||||||
|
&& ! -d "$newUserServicePath" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! (-v oldUserServicePath && -d "$oldUserServicePath") ]]; then
|
||||||
|
touch "$oldServiceFiles"
|
||||||
|
else
|
||||||
|
find "$oldUserServicePath" \
|
||||||
|
-maxdepth 1 -name '*.service' -exec basename '{}' ';' \
|
||||||
|
| sort \
|
||||||
|
> "$oldServiceFiles"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "$newUserServicePath" ]]; then
|
||||||
|
touch "$newServiceFiles"
|
||||||
|
else
|
||||||
|
find "$newUserServicePath" \
|
||||||
|
-maxdepth 1 -name '*.service' -exec basename '{}' ';' \
|
||||||
|
| sort \
|
||||||
|
> "$newServiceFiles"
|
||||||
|
fi
|
||||||
|
|
||||||
|
diff \
|
||||||
|
--new-line-format='+%L' \
|
||||||
|
--old-line-format='-%L' \
|
||||||
|
--unchanged-line-format=' %L' \
|
||||||
|
"$oldServiceFiles" "$newServiceFiles" \
|
||||||
|
> "$servicesDiffFile" || true
|
||||||
|
|
||||||
|
local -a maybeRestart=( $(grep '^ ' "$servicesDiffFile" | cut -c2-) )
|
||||||
|
local -a maybeStop=( $(grep '^-' "$servicesDiffFile" | cut -c2-) )
|
||||||
|
local -a maybeStart=( $(grep '^+' "$servicesDiffFile" | cut -c2-) )
|
||||||
|
local -a toRestart=( )
|
||||||
|
local -a toStop=( )
|
||||||
|
local -a toStart=( )
|
||||||
|
|
||||||
|
for f in "${maybeRestart[@]}" ; do
|
||||||
|
if isStoppable "$f" \
|
||||||
|
&& isStartable "$f" \
|
||||||
|
&& systemctl --quiet --user is-active "$f" \
|
||||||
|
&& ! cmp --quiet \
|
||||||
|
"$oldUserServicePath/$f" \
|
||||||
|
"$newUserServicePath/$f" ; then
|
||||||
|
toRestart+=("$f")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for f in "${maybeStop[@]}" ; do
|
||||||
|
if isStoppable "$f" ; then
|
||||||
|
toStop+=("$f")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for f in "${maybeStart[@]}" ; do
|
||||||
|
if isStartable "$f" ; then
|
||||||
|
toStart+=("$f")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
rm -r "$workDir"
|
||||||
|
|
||||||
|
local sugg=""
|
||||||
|
|
||||||
|
if [[ -n "${toRestart[@]}" ]] ; then
|
||||||
|
sugg="${sugg}systemctl --user restart ${toRestart[@]}\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${toStop[@]}" ]] ; then
|
||||||
|
sugg="${sugg}systemctl --user stop ${toStop[@]}\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${toStart[@]}" ]] ; then
|
||||||
|
sugg="${sugg}systemctl --user start ${toStart[@]}\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$sugg" ]] ; then
|
||||||
|
echo "Suggested commands:"
|
||||||
|
echo -n -e "$sugg"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
oldGenPath="$1"
|
||||||
|
newGenPath="$2"
|
||||||
|
|
||||||
|
$DRY_RUN_CMD systemctl --user daemon-reload
|
||||||
|
systemdPostReload
|
|
@ -54,6 +54,8 @@ let
|
||||||
buildServices = style: serviceCfgs:
|
buildServices = style: serviceCfgs:
|
||||||
concatLists (mapAttrsToList (buildService style) serviceCfgs);
|
concatLists (mapAttrsToList (buildService style) serviceCfgs);
|
||||||
|
|
||||||
|
servicesStartTimeoutMs = builtins.toString cfg.servicesStartTimeoutMs;
|
||||||
|
|
||||||
unitType = unitKind: with types;
|
unitType = unitKind: with types;
|
||||||
let
|
let
|
||||||
primitive = either bool (either int str);
|
primitive = either bool (either int str);
|
||||||
|
@ -150,11 +152,9 @@ in
|
||||||
example = unitExample "Path";
|
example = unitExample "Path";
|
||||||
};
|
};
|
||||||
|
|
||||||
# Keep for a while for backwards compatibility.
|
|
||||||
startServices = mkOption {
|
startServices = mkOption {
|
||||||
default = false;
|
default = false;
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
visible = false;
|
|
||||||
description = ''
|
description = ''
|
||||||
Start all services that are wanted by active targets.
|
Start all services that are wanted by active targets.
|
||||||
Additionally, stop obsolete services from the previous
|
Additionally, stop obsolete services from the previous
|
||||||
|
@ -164,10 +164,10 @@ in
|
||||||
|
|
||||||
servicesStartTimeoutMs = mkOption {
|
servicesStartTimeoutMs = mkOption {
|
||||||
default = 0;
|
default = 0;
|
||||||
type = types.ints.unsigned;
|
type = types.int;
|
||||||
description = ''
|
description = ''
|
||||||
How long to wait for started services to fail until their start is
|
How long to wait for started services to fail until their
|
||||||
considered successful. The value 0 indicates no timeout.
|
start is considered successful.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -203,10 +203,6 @@ in
|
||||||
"Must use Linux for modules that require systemd: " + names;
|
"Must use Linux for modules that require systemd: " + names;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
warnings = mkIf cfg.startServices [
|
|
||||||
"The option 'systemd.user.startServices' is obsolete and can be removed."
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# If we run under a Linux system we assume that systemd is
|
# If we run under a Linux system we assume that systemd is
|
||||||
|
@ -234,17 +230,13 @@ in
|
||||||
# set it ourselves in that case.
|
# set it ourselves in that case.
|
||||||
home.activation.reloadSystemD = hm.dag.entryAfter ["linkGeneration"] (
|
home.activation.reloadSystemD = hm.dag.entryAfter ["linkGeneration"] (
|
||||||
let
|
let
|
||||||
timeoutArg =
|
autoReloadCmd = ''
|
||||||
if cfg.servicesStartTimeoutMs != 0 then
|
${pkgs.ruby}/bin/ruby ${./systemd-activate.rb} \
|
||||||
"--timeout " + toString cfg.servicesStartTimeoutMs
|
"''${oldGenPath=}" "$newGenPath" "${servicesStartTimeoutMs}"
|
||||||
else
|
'';
|
||||||
"";
|
|
||||||
|
|
||||||
sdSwitchCmd = ''
|
legacyReloadCmd = ''
|
||||||
${pkgs.sd-switch}/bin/sd-switch \
|
bash ${./systemd-activate.sh} "''${oldGenPath=}" "$newGenPath"
|
||||||
''${DRY_RUN:+--dry-run} $VERBOSE_ARG ${timeoutArg} \
|
|
||||||
''${oldGenPath:+--old-units $oldGenPath/home-files/.config/systemd/user} \
|
|
||||||
--new-units $newGenPath/home-files/.config/systemd/user
|
|
||||||
'';
|
'';
|
||||||
|
|
||||||
ensureRuntimeDir = "XDG_RUNTIME_DIR=\${XDG_RUNTIME_DIR:-/run/user/$(id -u)}";
|
ensureRuntimeDir = "XDG_RUNTIME_DIR=\${XDG_RUNTIME_DIR:-/run/user/$(id -u)}";
|
||||||
|
@ -262,7 +254,8 @@ in
|
||||||
fi
|
fi
|
||||||
|
|
||||||
${ensureRuntimeDir} \
|
${ensureRuntimeDir} \
|
||||||
${sdSwitchCmd}
|
PATH=${dirOf cfg.systemctlPath}:$PATH \
|
||||||
|
${if cfg.startServices then autoReloadCmd else legacyReloadCmd}
|
||||||
else
|
else
|
||||||
echo "User systemd daemon not running. Skipping reload."
|
echo "User systemd daemon not running. Skipping reload."
|
||||||
fi
|
fi
|
||||||
|
|
Loading…
Reference in a new issue