systemd: improve systemd-activate.rb script
- Pass arguments verbatim to the `systemctl` subprocess, obviating the need for shell escaping. - Use open3 for capturing subprocess output. - Fix printing of commands during dry run. - Simplify `X-RestartIfChanged` regular expression. 1. Use \s to match whitespace, \b to match a word boundary. 2. Rename variable to conform to Ruby's underscore naming conventions. - Remove no-op set operation. Specifically, 'no_restart' and 'to_stop' are disjunct since 1. After reloading the daemon with the new generation, units in 'to_stop' (i.e. units from the old gen that are missing in the new gen) are not registered anymore in the systemd daemon. 2. Hence, 'systemctl cat' returns no output for these units. 3. Because this output is needed to detect 'no_restart' units, 'no_restart' includes no units from 'to_stop'. So 'to_stop -= to_restart' is a no-op. - Only notify about units that would otherwise be restarted. That is, exclude units that are started but not restarted. - Previously, all inactive units, like short-running services, were handled as failed units. Now systemd activation doesn't fail for oneshot services like 'setxkbmap' while 'servicesStartTimeoutMs' is set. - Don't start unchanged oneshot services. PR #1110
This commit is contained in:
parent
d11803d7b4
commit
e2414c4a4f
|
@ -1,6 +1,5 @@
|
||||||
require 'set'
|
require 'set'
|
||||||
require 'open3'
|
require 'open3'
|
||||||
require 'shellwords'
|
|
||||||
|
|
||||||
@dry_run = ENV['DRY_RUN']
|
@dry_run = ENV['DRY_RUN']
|
||||||
@verbose = ENV['VERBOSE']
|
@verbose = ENV['VERBOSE']
|
||||||
|
@ -28,33 +27,36 @@ def setup_services(old_gen_path, new_gen_path, start_timeout_ms_string)
|
||||||
|
|
||||||
exit if old_services.empty? && new_services.empty?
|
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
|
# These services should be running when this script is finished
|
||||||
services_to_run = get_services_to_run(new_units_path)
|
services_to_run = all_services - unchanged_oneshots
|
||||||
maybe_changed_services = services_to_run & old_services
|
|
||||||
|
|
||||||
# Only stop active services, otherwise we might get a 'service not loaded' error
|
# Only stop active services, otherwise we might get a 'service not loaded' error
|
||||||
# for inactive services that were removed in the current generation.
|
# for inactive services that were removed in the current generation.
|
||||||
to_stop = get_active_units(old_services - new_services)
|
to_stop = get_active_units(old_services - new_services)
|
||||||
to_restart = get_changed_services(old_units_path, new_units_path, maybe_changed_services)
|
to_restart = changed_services
|
||||||
to_start = get_inactive_units(services_to_run - to_restart)
|
to_start = get_inactive_units(services_to_run - to_restart)
|
||||||
|
|
||||||
raise "daemon-reload failed" unless run_cmd('systemctl --user daemon-reload')
|
raise "daemon-reload failed" unless run_cmd('systemctl', '--user', 'daemon-reload')
|
||||||
|
|
||||||
# Exclude services that aren't allowed to be manually started or stopped
|
# 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)
|
no_manual_start, no_manual_stop, no_restart = get_restricted_units(to_stop + to_restart + to_start)
|
||||||
to_stop -= no_manual_stop + no_restart
|
notify_skipped_units(to_restart & no_restart)
|
||||||
|
to_stop -= no_manual_stop
|
||||||
to_restart -= no_manual_stop + no_manual_start + no_restart
|
to_restart -= no_manual_stop + no_manual_start + no_restart
|
||||||
to_start -= no_manual_start
|
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?
|
if to_stop.empty? && to_start.empty? && to_restart.empty?
|
||||||
print_service_msg("All services are already running", services_to_run)
|
print_service_msg("All services are already running", services_to_run)
|
||||||
else
|
else
|
||||||
puts "Setting up services" if @verbose
|
puts "Setting up services" if @verbose
|
||||||
systemctl('stop', to_stop)
|
systemctl_action('stop', to_stop)
|
||||||
systemctl('start', to_start)
|
systemctl_action('start', to_start)
|
||||||
systemctl('restart', to_restart)
|
systemctl_action('restart', to_restart)
|
||||||
started_services = to_start + to_restart
|
started_services = to_start + to_restart
|
||||||
if start_timeout_ms > 0 && !started_services.empty? && !@dry_run
|
if start_timeout_ms > 0 && !started_services.empty? && !@dry_run
|
||||||
failed = wait_and_get_failed_services(started_services, start_timeout_ms)
|
failed = wait_and_get_failed_services(started_services, start_timeout_ms)
|
||||||
|
@ -89,24 +91,24 @@ end
|
||||||
|
|
||||||
TargetDirRegexp = /^(.*\.target)\.wants$/
|
TargetDirRegexp = /^(.*\.target)\.wants$/
|
||||||
|
|
||||||
# @return all services wanted by active targets
|
# @return all units wanted by active targets
|
||||||
def get_services_to_run(units_dir)
|
def get_active_targets_units(units_dir)
|
||||||
return Set.new unless Dir.exists?(units_dir)
|
return Set.new unless Dir.exists?(units_dir)
|
||||||
targets = Dir.entries(units_dir).map { |entry| entry[TargetDirRegexp, 1] }.compact
|
targets = Dir.entries(units_dir).map { |entry| entry[TargetDirRegexp, 1] }.compact
|
||||||
active_targets = get_active_units(targets)
|
active_targets = get_active_units(targets)
|
||||||
services_to_run = active_targets.map do |target|
|
active_units = active_targets.map do |target|
|
||||||
get_service_files(File.join(units_dir, "#{target}.wants"))
|
get_service_files(File.join(units_dir, "#{target}.wants"))
|
||||||
end.flatten
|
end.flatten
|
||||||
Set.new(services_to_run)
|
Set.new(active_units)
|
||||||
end
|
end
|
||||||
|
|
||||||
# @return true on success
|
# @return true on success
|
||||||
def run_cmd(cmd)
|
def run_cmd(*cmd)
|
||||||
print_cmd cmd
|
print_cmd cmd
|
||||||
@dry_run || system(cmd)
|
@dry_run || system(*cmd)
|
||||||
end
|
end
|
||||||
|
|
||||||
def systemctl(cmd, services)
|
def systemctl_action(cmd, services)
|
||||||
return if services.empty?
|
return if services.empty?
|
||||||
|
|
||||||
verb = (cmd == 'stop') ? 'Stopping' : "#{cmd.capitalize}ing"
|
verb = (cmd == 'stop') ? 'Stopping' : "#{cmd.capitalize}ing"
|
||||||
|
@ -114,7 +116,7 @@ def systemctl(cmd, services)
|
||||||
|
|
||||||
cmd = ['systemctl', '--user', cmd, *services]
|
cmd = ['systemctl', '--user', cmd, *services]
|
||||||
if @dry_run
|
if @dry_run
|
||||||
puts cmd
|
puts cmd.join(' ')
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -131,32 +133,44 @@ def systemctl(cmd, services)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def systemctl(*cmd)
|
||||||
|
output, _ = Open3.capture2('systemctl', '--user', *cmd)
|
||||||
|
output
|
||||||
|
end
|
||||||
|
|
||||||
def print_cmd(cmd)
|
def print_cmd(cmd)
|
||||||
puts cmd if @verbose || @dry_run
|
puts [*cmd].join(' ') if @verbose || @dry_run
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_active_units(units)
|
def get_active_units(units)
|
||||||
get_units_by_activity(units, true)
|
filter_units(units) { |state| state == 'active' }
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_inactive_units(units)
|
def get_inactive_units(units)
|
||||||
get_units_by_activity(units, false)
|
filter_units(units) { |state| state != 'active' }
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_units_by_activity(units, active)
|
def get_failed_units(units)
|
||||||
|
filter_units(units) { |state| state == 'failed' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_units(units)
|
||||||
return [] if units.empty?
|
return [] if units.empty?
|
||||||
units = units.to_a
|
states = systemctl('is-active', *units).split
|
||||||
is_active = `systemctl --user is-active #{units.shelljoin}`.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|
|
units.select.with_index do |_, i|
|
||||||
(is_active[i] == 'active') == active
|
types[i] == 'Type=oneshot'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_restricted_units(units)
|
def get_restricted_units(units)
|
||||||
units = units.to_a
|
infos = systemctl('show', '-p', 'RefuseManualStart', '-p', 'RefuseManualStop', *units)
|
||||||
infos = `systemctl --user show -p RefuseManualStart -p RefuseManualStop #{units.shelljoin}`
|
|
||||||
.split("\n\n")
|
.split("\n\n")
|
||||||
no_restart = []
|
|
||||||
no_manual_start = []
|
no_manual_start = []
|
||||||
no_manual_stop = []
|
no_manual_stop = []
|
||||||
infos.zip(units).each do |info, unit|
|
infos.zip(units).each do |info, unit|
|
||||||
|
@ -164,14 +178,9 @@ def get_restricted_units(units)
|
||||||
no_manual_start << unit if no_start.end_with?('yes')
|
no_manual_start << unit if no_start.end_with?('yes')
|
||||||
no_manual_stop << unit if no_stop.end_with?('yes')
|
no_manual_stop << unit if no_stop.end_with?('yes')
|
||||||
end
|
end
|
||||||
# Regular expression that indicates that a service should not be
|
# Get units that should not be restarted even if a change has been detected.
|
||||||
# restarted even if a change has been detected.
|
no_restart_regexp = /^\s*X-RestartIfChanged\s*=\s*false\b/
|
||||||
restartRe = /^[ \t]*X-RestartIfChanged[ \t]*=[ \t]*false[ \t]*(?:#.*)?$/
|
no_restart = units.select { |unit| systemctl('cat', unit) =~ no_restart_regexp }
|
||||||
units.each do |unit|
|
|
||||||
if `systemctl --user cat #{unit.shellescape}` =~ restartRe
|
|
||||||
no_restart << unit
|
|
||||||
end
|
|
||||||
end
|
|
||||||
[no_manual_start, no_manual_stop, no_restart]
|
[no_manual_start, no_manual_stop, no_restart]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -180,13 +189,13 @@ def wait_and_get_failed_services(services, start_timeout_ms)
|
||||||
# Force the previous message to always be visible before sleeping
|
# Force the previous message to always be visible before sleeping
|
||||||
STDOUT.flush
|
STDOUT.flush
|
||||||
sleep(start_timeout_ms / 1000.0)
|
sleep(start_timeout_ms / 1000.0)
|
||||||
get_inactive_units(services)
|
get_failed_units(services)
|
||||||
end
|
end
|
||||||
|
|
||||||
def show_failed_services_status(services)
|
def show_failed_services_status(services)
|
||||||
puts
|
puts
|
||||||
services.each do |service|
|
services.each do |service|
|
||||||
run_cmd("systemctl --user status #{service.shellescape}")
|
run_cmd('systemctl', '--user', 'status', service)
|
||||||
puts
|
puts
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -200,4 +209,8 @@ def print_service_msg(msg, services)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def notify_skipped_units(no_restart)
|
||||||
|
puts "Not restarting: #{no_restart.join(' ')}" unless no_restart.empty?
|
||||||
|
end
|
||||||
|
|
||||||
setup_services(*ARGV)
|
setup_services(*ARGV)
|
||||||
|
|
Loading…
Reference in a new issue