diff --git a/modules/systemd-activate.rb b/modules/systemd-activate.rb index 12791ce3..31d06d8f 100644 --- a/modules/systemd-activate.rb +++ b/modules/systemd-activate.rb @@ -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)