aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
authorRobert Helgesson <robert@rycee.net>2020-07-07 00:35:28 +0200
committerRobert Helgesson <robert@rycee.net>2020-08-04 01:01:10 +0200
commit9c0fe3957bb91128d5d3599e289fe9344a293ece (patch)
treeb326bf3286587cf782d86cf47a3c376f8d6db244 /modules
parent152769aed96d4d6f005ab40daf03ec4f5102c763 (diff)
systemd: use sd-switch
This makes the systemd module use the sd-switch application to perform the unit switch during a generation activation. Since the closure of sd-switch is relatively lightweight we unconditionally pull it in as a dependency. We simultaneously remove the `systemd.user.startServices` option and perform the switch action automatically. PR #1388
Diffstat (limited to 'modules')
-rw-r--r--modules/misc/news.nix15
-rw-r--r--modules/systemd-activate.rb216
-rw-r--r--modules/systemd-activate.sh114
-rw-r--r--modules/systemd.nix33
4 files changed, 35 insertions, 343 deletions
diff --git a/modules/misc/news.nix b/modules/misc/news.nix
index 62f57a44cec..fae01869122 100644
--- a/modules/misc/news.nix
+++ b/modules/misc/news.nix
@@ -1619,6 +1619,21 @@ in
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.
+ '';
+ }
];
};
}
diff --git a/modules/systemd-activate.rb b/modules/systemd-activate.rb
deleted file mode 100644
index 31d06d8fc19..00000000000
--- 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-activate.sh b/modules/systemd-activate.sh
deleted file mode 100644
index 1c464693cfc..00000000000
--- a/modules/systemd-activate.sh
+++ /dev/null
@@ -1,114 +0,0 @@
-#!/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
diff --git a/modules/systemd.nix b/modules/systemd.nix
index dcb1a29570d..5acfabc7469 100644
--- a/modules/systemd.nix
+++ b/modules/systemd.nix
@@ -54,8 +54,6 @@ let
buildServices = style: serviceCfgs:
concatLists (mapAttrsToList (buildService style) serviceCfgs);
- servicesStartTimeoutMs = builtins.toString cfg.servicesStartTimeoutMs;
-
unitType = unitKind: with types;
let
primitive = either bool (either int str);
@@ -152,9 +150,11 @@ in
example = unitExample "Path";
};
+ # Keep for a while for backwards compatibility.
startServices = mkOption {
default = false;
type = types.bool;
+ visible = false;
description = ''
Start all services that are wanted by active targets.
Additionally, stop obsolete services from the previous
@@ -164,10 +164,10 @@ in
servicesStartTimeoutMs = mkOption {
default = 0;
- type = types.int;
+ type = types.ints.unsigned;
description = ''
- How long to wait for started services to fail until their
- start is considered successful.
+ How long to wait for started services to fail until their start is
+ considered successful. The value 0 indicates no timeout.
'';
};
@@ -203,6 +203,10 @@ in
"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
@@ -230,13 +234,17 @@ in
# set it ourselves in that case.
home.activation.reloadSystemD = hm.dag.entryAfter ["linkGeneration"] (
let
- autoReloadCmd = ''
- ${pkgs.ruby}/bin/ruby ${./systemd-activate.rb} \
- "''${oldGenPath=}" "$newGenPath" "${servicesStartTimeoutMs}"
- '';
+ timeoutArg =
+ if cfg.servicesStartTimeoutMs != 0 then
+ "--timeout " + toString cfg.servicesStartTimeoutMs
+ else
+ "";
- legacyReloadCmd = ''
- bash ${./systemd-activate.sh} "''${oldGenPath=}" "$newGenPath"
+ sdSwitchCmd = ''
+ ${pkgs.sd-switch}/bin/sd-switch \
+ ''${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)}";
@@ -254,8 +262,7 @@ in
fi
${ensureRuntimeDir} \
- PATH=${dirOf cfg.systemctlPath}:$PATH \
- ${if cfg.startServices then autoReloadCmd else legacyReloadCmd}
+ ${sdSwitchCmd}
else
echo "User systemd daemon not running. Skipping reload."
fi