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:
Erik Arvstedt 2020-03-23 07:49:44 +01:00 committed by Robert Helgesson
parent d11803d7b4
commit e2414c4a4f
No known key found for this signature in database
GPG key ID: 36BDAA14C2797E89

View file

@ -1,6 +1,5 @@
require 'set'
require 'open3'
require 'shellwords'
@dry_run = ENV['DRY_RUN']
@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?
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 = get_services_to_run(new_units_path)
maybe_changed_services = services_to_run & old_services
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 = 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)
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)
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_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)
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)
@ -89,24 +91,24 @@ end
TargetDirRegexp = /^(.*\.target)\.wants$/
# @return all services wanted by active targets
def get_services_to_run(units_dir)
# @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)
services_to_run = active_targets.map do |target|
active_units = active_targets.map do |target|
get_service_files(File.join(units_dir, "#{target}.wants"))
end.flatten
Set.new(services_to_run)
Set.new(active_units)
end
# @return true on success
def run_cmd(cmd)
def run_cmd(*cmd)
print_cmd cmd
@dry_run || system(cmd)
@dry_run || system(*cmd)
end
def systemctl(cmd, services)
def systemctl_action(cmd, services)
return if services.empty?
verb = (cmd == 'stop') ? 'Stopping' : "#{cmd.capitalize}ing"
@ -114,7 +116,7 @@ def systemctl(cmd, services)
cmd = ['systemctl', '--user', cmd, *services]
if @dry_run
puts cmd
puts cmd.join(' ')
return
end
@ -131,32 +133,44 @@ def systemctl(cmd, services)
end
end
def systemctl(*cmd)
output, _ = Open3.capture2('systemctl', '--user', *cmd)
output
end
def print_cmd(cmd)
puts cmd if @verbose || @dry_run
puts [*cmd].join(' ') if @verbose || @dry_run
end
def get_active_units(units)
get_units_by_activity(units, true)
filter_units(units) { |state| state == 'active' }
end
def get_inactive_units(units)
get_units_by_activity(units, false)
filter_units(units) { |state| state != 'active' }
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?
units = units.to_a
is_active = `systemctl --user is-active #{units.shelljoin}`.split
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|
(is_active[i] == 'active') == active
types[i] == 'Type=oneshot'
end
end
def get_restricted_units(units)
units = units.to_a
infos = `systemctl --user show -p RefuseManualStart -p RefuseManualStop #{units.shelljoin}`
infos = systemctl('show', '-p', 'RefuseManualStart', '-p', 'RefuseManualStop', *units)
.split("\n\n")
no_restart = []
no_manual_start = []
no_manual_stop = []
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_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
# 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
@ -180,13 +189,13 @@ def wait_and_get_failed_services(services, start_timeout_ms)
# Force the previous message to always be visible before sleeping
STDOUT.flush
sleep(start_timeout_ms / 1000.0)
get_inactive_units(services)
get_failed_units(services)
end
def show_failed_services_status(services)
puts
services.each do |service|
run_cmd("systemctl --user status #{service.shellescape}")
run_cmd('systemctl', '--user', 'status', service)
puts
end
end
@ -200,4 +209,8 @@ def print_service_msg(msg, services)
end
end
def notify_skipped_units(no_restart)
puts "Not restarting: #{no_restart.join(' ')}" unless no_restart.empty?
end
setup_services(*ARGV)