Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This reverts commit 9c0fe39.
- Loading branch information
Showing
5 changed files
with
344 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Oops, something went wrong.
223e3c3
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I liked this. Why revert it?
223e3c3
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because of #1388 (comment).