Skip to content

Commit 68b5d16

Browse files
author
Oana Tanasoiu
committed
(maint) Avoid deadlock of Facter::Core::Execution.execute
1 parent 2bcf4a3 commit 68b5d16

5 files changed

Lines changed: 57 additions & 16 deletions

File tree

.github/workflows/acceptance_tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ jobs:
1717
os: [windows-2016, windows-2019, ubuntu-16.04, ubuntu-18.04, ubuntu-20.04, macos-10.15]
1818
runs-on: ${{ matrix.os }}
1919
env:
20+
BEAKER_debug: true
2021
FACTER_ROOT: facter
2122
SHA: latest
2223
RELEASE_STREAM: puppet7
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
test_name "Facter::Core::Execution doesn't kill process with long stderr message" do
2+
tag 'risk:high'
3+
4+
confine :except, :platform => /windows/
5+
6+
long_output = "This is a very long error message. " * 4096
7+
file_content = <<-EOM
8+
#!/bin/sh
9+
echo 'newfact=value_of_fact'
10+
1>&2 echo #{long_output}
11+
exit 1
12+
EOM
13+
14+
15+
agents.each do |agent|
16+
17+
external_dir = agent.tmpdir('external_dir')
18+
fact_file = File.join(external_dir, 'test.sh')
19+
create_remote_file(agent, fact_file, file_content)
20+
agent.chmod('+x', fact_file)
21+
22+
teardown do
23+
agent.rm_rf(external_dir)
24+
end
25+
26+
step "Facter: should resolve the external fact and print as warning script's stderr message" do
27+
on agent, facter('--external-dir', external_dir, 'newfact') do |facter_output|
28+
assert_match(/value_of_fact/, facter_output.stdout.chomp)
29+
assert_match(/WARN test.sh .*test.sh resulted with the following stderr message: This is a very long error message./, facter_output.stderr.chomp)
30+
end
31+
end
32+
end
33+
end
34+

acceptance/tests/custom_facts/time_limit_for_execute_command.rb

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
first_file_content = <<-EOM
55
Facter.add(:foo) do
66
setcode do
7-
Facter::Core::Execution.execute("sleep 3", {:limit => 2})
7+
Facter::Core::Execution.execute("sleep 3", {:limit => 2, :on_fail => :not_raise})
88
end
99
end
1010
EOM
1111

1212
second_file_content = <<-EOM
1313
Facter.add(:custom_fact) do
1414
setcode do
15-
Facter::Core::Execution.execute("sleep 2")
15+
Facter::Core::Execution.execute("sleep 2", {:limit => 1})
1616
end
1717
end
1818
EOM
@@ -31,16 +31,16 @@
3131
end
3232

3333
step "Facter: Logs that command of the first custom fact had timeout after setted time limit" do
34-
on agent, facter('--custom-dir', custom_dir, 'foo --debug') do |facter_output|
34+
on agent, facter('--custom-dir', custom_dir, 'foo --debug') do |output|
3535
assert_match(/DEBUG Facter::Core::Execution.*Timeout encounter after 2s, killing process with pid:/,
36-
facter_output.stderr.chomp)
36+
output.stderr.chomp)
3737
end
3838
end
3939

40-
step "Facter: Logs that command of the second custom fact had timeout after befault time limit" do
41-
on agent, facter('--custom-dir', custom_dir, 'custom_fact --debug') do |facter_output|
42-
assert_match(/DEBUG Facter::Core::Execution.*Timeout encounter after 1.5s, killing process with pid:/,
43-
facter_output.stderr.chomp)
40+
step "Facter: Logs an error stating that the command of the second custom fact had timeout" do
41+
on(agent, facter('--custom-dir', custom_dir, 'custom_fact --debug'), acceptable_exit_codes: 1) do |output|
42+
assert_match(/ERROR\s+.*Failed while executing '.*sleep 2': Timeout encounter after 1s, killing process/,
43+
output.stderr.chomp)
4444
end
4545
end
4646
end

lib/facter/custom_facts/core/execution/base.rb

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def builtin_command?(command)
9090
end
9191

9292
def execute_command(command, on_fail, logger = nil, time_limit = nil)
93-
time_limit ||= 1.5
93+
time_limit ||= 300
9494
begin
9595
# Set LC_ALL and LANG to force i18n to C for the duration of this exec;
9696
# this ensures that any code that parses the
@@ -100,18 +100,24 @@ def execute_command(command, on_fail, logger = nil, time_limit = nil)
100100
@log.debug("Executing command: #{command}")
101101
out, stderr = Open3.popen3(opts, command.to_s) do |_, stdout, stderr, wait_thr|
102102
pid = wait_thr.pid
103-
output = +''
104-
err = +''
103+
stdout_messages = +''
104+
stderr_messages = +''
105+
out_reader = Thread.new { stdout.read }
106+
err_reader = Thread.new { stderr.read }
105107
begin
106108
Timeout.timeout(time_limit) do
107-
output << stdout.read
108-
err << stderr.read
109+
stdout_messages << out_reader.value
110+
stderr_messages << err_reader.value
109111
end
110112
rescue Timeout::Error
111-
@log.debug("Timeout encounter after #{time_limit}s, killing process with pid: #{pid}")
113+
message = "Timeout encounter after #{time_limit}s, killing process with pid: #{pid}"
112114
Process.kill('KILL', pid)
115+
on_fail == :raise ? (raise StandardError, message) : @log.debug(message)
116+
ensure
117+
out_reader.kill
118+
err_reader.kill
113119
end
114-
[output, err]
120+
[stdout_messages, stderr_messages]
115121
end
116122
log_stderr(stderr, command, logger)
117123
rescue StandardError => e

lib/facter/resolvers/aix/os_level.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def post_resolve(fact_name)
1414
end
1515

1616
def read_oslevel(fact_name)
17-
output = Facter::Core::Execution.execute('/usr/bin/oslevel -s', { limit: 2, logger: log })
17+
output = Facter::Core::Execution.execute('/usr/bin/oslevel -s', logger: log )
1818
@fact_list[:build] = output unless output.empty?
1919
@fact_list[:kernel] = 'AIX'
2020

0 commit comments

Comments
 (0)