parent
9c0fe3957b
commit
223e3c38a1
@ -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) |
@ -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 |
Loading…
Reference in new issue