Skip to content

Commit 62b39f2

Browse files
committed
feat: improve timeout handling
1 parent 6e86df0 commit 62b39f2

1 file changed

Lines changed: 46 additions & 44 deletions

File tree

tasks/patch_server.rb

Lines changed: 46 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -50,44 +50,38 @@ def history(dts, message, code, reboot, security, job)
5050
end
5151
end
5252

53-
def run_with_timeout(command, timeout, tick)
54-
output = ''
53+
def run_with_timeout(command, timeout)
54+
stdout = ''
55+
stderr = ''
56+
exit_code = nil
57+
wait_thr = nil
58+
5559
begin
56-
# Start task in another thread, which spawns a process
57-
stdin, stderrout, thread = Open3.popen2e(command)
58-
# Get the pid of the spawned process
59-
pid = thread[:pid]
60-
start = Time.now
61-
62-
while (Time.now - start) < timeout && thread.alive?
63-
# Wait up to `tick` seconds for output/error data
64-
Kernel.select([stderrout], nil, nil, tick)
65-
# Try to read the data
66-
begin
67-
output << stderrout.read_nonblock(BUFFER_SIZE)
68-
rescue IO::WaitReadable
69-
# A read would block, so loop around for another select
70-
sleep 1
71-
rescue EOFError
72-
# Command has completed, not really an error...
73-
break
60+
Timeout.timeout(timeout) do
61+
Open3.popen3(*command) do |stdin, out, err, thread|
62+
wait_thr = thread
63+
stdin.close
64+
65+
stdout = out.read
66+
stderr = err.read
67+
exit_code = wait_thr.value.exitstatus
7468
end
7569
end
76-
# Give Ruby time to clean up the other thread
77-
sleep 1
78-
79-
if thread.alive?
80-
# We need to kill the process, because killing the thread leaves
81-
# the process alive but detached, annoyingly enough.
82-
Process.kill('TERM', pid)
83-
err('403', 'os_patching/patching', "TIMEOUT AFTER #{timeout} seconds\n#{output}", start)
70+
rescue Timeout::Error
71+
if wait_thr
72+
begin
73+
Process.kill('TERM', wait_thr.pid)
74+
sleep 2
75+
Process.kill('KILL', wait_thr.pid)
76+
rescue Errno::ESRCH
77+
nil
78+
end
8479
end
85-
ensure
86-
stdin.close if stdin
87-
stderrout.close if stderrout
88-
status = thread.value.exitstatus
80+
81+
err('403', 'os_patching/patching', "TIMEOUT AFTER #{timeout} seconds", Time.now.iso8601)
8982
end
90-
[status, output]
83+
84+
[exit_code, stdout, stderr]
9185
end
9286

9387
# pending reboot detection function for windows
@@ -502,8 +496,8 @@ def gather_facts(log, starttime)
502496
log.info 'Running yum upgrade'
503497
log.debug "Timeout value set to : #{timeout}"
504498
yum_end = ''
505-
status, output = run_with_timeout("yum #{yum_params} #{securityflag} upgrade -y", timeout, 2)
506-
err(status, 'os_patching/yum', "yum upgrade returned non-zero (#{status}) : #{output}", starttime) if status != 0
499+
yum_exitcode, yum_output, yum_error = run_with_timeout("yum #{yum_params} #{securityflag} upgrade -y", timeout)
500+
err(yum_exitcode, 'os_patching/yum', "yum upgrade returned non-zero (#{yum_exitcode}) : #{yum_output}\n#{yum_error}", starttime) if yum_exitcode != 0
507501

508502
if os['release']['major'].to_i > 5
509503
# Capture the yum job ID
@@ -569,6 +563,9 @@ def gather_facts(log, starttime)
569563
output(yum_return, reboot, security_only, 'Patching complete', pkg_hash, output, job, pinned_pkgs, starttime, log)
570564
log.info 'Patching complete'
571565
elsif os['family'] == 'Debian'
566+
log.info 'Running apt'
567+
log.debug "Timeout value set to : #{timeout}"
568+
572569
# Are we doing security only patching?
573570
if security_only == true
574571
pkg_list = os_patching['security_package_updates']
@@ -582,10 +579,16 @@ def gather_facts(log, starttime)
582579
log.debug "Running apt #{apt_mode}"
583580
deb_front = 'DEBIAN_FRONTEND=noninteractive'
584581
deb_opts = '-o Apt::Get::Purge=false -o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef --no-install-recommends'
585-
apt_std_out, status = run_with_timeout("#{deb_front} apt-get #{dpkg_params} -y #{deb_opts} #{apt_mode}", timeout, 2)
586-
err(status, 'os_patching/apt', apt_std_out, starttime) if status != 0
582+
# apt_std_out, stderr, status = Open3.capture3("#{deb_front} apt-get #{dpkg_params} -y #{deb_opts} #{apt_mode}")
583+
apt_exitcode, apt_out, apt_err = run_with_timeout("#{deb_front} apt-get #{dpkg_params} -y #{deb_opts} #{apt_mode}", timeout)
584+
585+
log.debug "apt output : #{apt_out}"
586+
log.debug "apt error output : #{apt_err}"
587+
log.debug "apt exit status : #{apt_exitcode}"
587588

588-
output('Success', reboot, security_only, 'Patching complete', pkg_list, apt_std_out, '', pinned_pkgs, starttime, log)
589+
err(apt_exitcode, 'os_patching/apt', "Error: #{apt_err}", starttime) if apt_exitcode != 0
590+
591+
output('Success', reboot, security_only, 'Patching complete', pkg_list, apt_out.split("\n"), '', pinned_pkgs, starttime, log)
589592
log.info 'Patching complete'
590593
elsif os['family'] == 'windows'
591594
# we're on windows
@@ -645,7 +648,6 @@ def gather_facts(log, starttime)
645648
# output results
646649
# def output(returncode, reboot, security, message, packages_updated, debug, job_id, pinned_packages, starttime)
647650
output('Success', reboot, security_only, 'Patching complete', update_titles, win_std_out.split("\n"), '', '', starttime, log)
648-
649651
elsif os['family'] == 'Suse'
650652
zypper_required_params = '--non-interactive --no-abbrev --quiet'
651653
zypper_cmd_params = '--auto-agree-with-licenses'
@@ -656,15 +658,15 @@ def gather_facts(log, starttime)
656658
if security_only == true
657659
pkg_list = os_patching['security_package_updates']
658660
log.info 'Running zypper patch'
659-
status, output = run_with_timeout("zypper #{zypper_required_params} #{zypper_params} patch -g security #{zypper_cmd_params}", timeout, 2)
660-
err(status, 'os_patching/zypper', "zypper patch returned non-zero (#{status}) : #{output}", starttime) if status != 0
661+
zypper_exitcode, zypper_output, zypper_error = run_with_timeout("zypper #{zypper_required_params} #{zypper_params} patch -g security #{zypper_cmd_params}", timeout)
662+
err(zypper_exitcode, 'os_patching/zypper', "zypper patch returned non-zero (#{zypper_exitcode}) : #{zypper_output}\n#{zypper_error}", starttime) if zypper_exitcode != 0
661663
else
662664
pkg_list = os_patching['package_updates']
663665
log.info 'Running zypper update'
664-
status, output = run_with_timeout("zypper #{zypper_required_params} #{zypper_params} update -t package #{zypper_cmd_params}", timeout, 2)
665-
err(status, 'os_patching/zypper', "zypper update returned non-zero (#{status}) : #{output}", starttime) if status != 0
666+
zypper_exitcode, zypper_output, zypper_error = run_with_timeout("zypper #{zypper_required_params} #{zypper_params} update -t package #{zypper_cmd_params}", timeout)
667+
err(zypper_exitcode, 'os_patching/zypper', "zypper update returned non-zero (#{zypper_exitcode}) : #{zypper_output}\n#{zypper_error}", starttime) if zypper_exitcode != 0
666668
end
667-
output('Success', reboot, security_only, 'Patching complete', pkg_list, output, '', pinned_pkgs, starttime, log)
669+
output('Success', reboot, security_only, 'Patching complete', pkg_list, zypper_output.split("\n"), '', pinned_pkgs, starttime, log)
668670
log.info 'Patching complete'
669671
log.debug "Timeout value set to : #{timeout}"
670672
else

0 commit comments

Comments
 (0)