mirror of
https://github.com/nix-community/home-manager
synced 2024-11-04 18:29:45 +01:00
203 lines
6.2 KiB
Ruby
203 lines
6.2 KiB
Ruby
require 'set'
|
|
require 'open3'
|
|
require 'shellwords'
|
|
|
|
@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?
|
|
|
|
# These services should be running when this script is finished
|
|
services_to_run = get_services_to_run(new_units_path)
|
|
maybe_changed_services = services_to_run & old_services
|
|
|
|
# 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 = get_changed_services(old_units_path, new_units_path, maybe_changed_services)
|
|
to_start = get_inactive_units(services_to_run - to_restart)
|
|
|
|
raise "daemon-reload failed" unless run_cmd('systemctl --user daemon-reload')
|
|
|
|
# Exclude services that aren't allowed to be manually started or stopped
|
|
no_manual_start, no_manual_stop, no_restart = get_restricted_units(to_stop + to_restart + to_start)
|
|
to_stop -= no_manual_stop + no_restart
|
|
to_restart -= no_manual_stop + no_manual_start + no_restart
|
|
to_start -= no_manual_start
|
|
|
|
puts "Not restarting: #{no_restart.join(' ')}" unless no_restart.empty?
|
|
|
|
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('stop', to_stop)
|
|
systemctl('start', to_start)
|
|
systemctl('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}'] }
|
|
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 services wanted by active targets
|
|
def get_services_to_run(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)
|
|
services_to_run = active_targets.map do |target|
|
|
get_service_files(File.join(units_dir, "#{target}.wants"))
|
|
end.flatten
|
|
Set.new(services_to_run)
|
|
end
|
|
|
|
# @return true on success
|
|
def run_cmd(cmd)
|
|
print_cmd cmd
|
|
@dry_run || system(cmd)
|
|
end
|
|
|
|
def systemctl(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
|
|
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 print_cmd(cmd)
|
|
puts cmd if @verbose || @dry_run
|
|
end
|
|
|
|
def get_active_units(units)
|
|
get_units_by_activity(units, true)
|
|
end
|
|
|
|
def get_inactive_units(units)
|
|
get_units_by_activity(units, false)
|
|
end
|
|
|
|
def get_units_by_activity(units, active)
|
|
return [] if units.empty?
|
|
units = units.to_a
|
|
is_active = `systemctl --user is-active #{units.shelljoin}`.split
|
|
units.select.with_index do |_, i|
|
|
(is_active[i] == 'active') == active
|
|
end
|
|
end
|
|
|
|
def get_restricted_units(units)
|
|
units = units.to_a
|
|
infos = `systemctl --user show -p RefuseManualStart -p RefuseManualStop #{units.shelljoin}`
|
|
.split("\n\n")
|
|
no_restart = []
|
|
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
|
|
# Regular expression that indicates that a service should not be
|
|
# restarted even if a change has been detected.
|
|
restartRe = /^[ \t]*X-RestartIfChanged[ \t]*=[ \t]*false[ \t]*(?:#.*)?$/
|
|
units.each do |unit|
|
|
if `systemctl --user cat #{unit.shellescape}` =~ restartRe
|
|
no_restart << unit
|
|
end
|
|
end
|
|
[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_inactive_units(services)
|
|
end
|
|
|
|
def show_failed_services_status(services)
|
|
puts
|
|
services.each do |service|
|
|
run_cmd("systemctl --user status #{service.shellescape}")
|
|
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
|
|
|
|
setup_services(*ARGV)
|