-
Notifications
You must be signed in to change notification settings - Fork 2.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
(PUP-6675) Use pipes instead of temporary files for Puppet exec #5844
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
test_name "tests that puppet correctly captures large and empty output." | ||
|
||
agents.each do |agent| | ||
testfile = agent.tmpfile('should_accept_large_output') | ||
|
||
# Generate >64KB file to exceed pipe buffer. | ||
lorem_ipsum = <<EOF | ||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna | ||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. | ||
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint | ||
occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. | ||
EOF | ||
create_remote_file(agent, testfile, lorem_ipsum*1024) | ||
|
||
apply_manifest_on(agent, "exec {'cat #{testfile}': path => ['/bin', '/usr/bin', 'C:/cygwin32/bin', 'C:/cygwin64/bin'], logoutput => true}") do | ||
fail_test "didn't seem to run the command" unless | ||
stdout.include? 'executed successfully' | ||
fail_test "didn't print output correctly" unless | ||
stdout.lines.select {|line| line =~ /\/returns:/}.count == 4097 | ||
end | ||
|
||
apply_manifest_on(agent, "exec {'echo': path => ['/bin', '/usr/bin', 'C:/cygwin32/bin', 'C:/cygwin64/bin'], logoutput => true}") do | ||
fail_test "didn't seem to run the command" unless | ||
stdout.include? 'executed successfully' | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -187,18 +187,31 @@ def self.execute(command, options = NoOptionsSpecified) | |
null_file = Puppet.features.microsoft_windows? ? 'NUL' : '/dev/null' | ||
|
||
begin | ||
# We close stdin/stdout/stderr immediately after execution as there no longer needed. | ||
# In most cases they could be closed later, but when stdout is the writer pipe we | ||
# must close it or we'll never reach eof on the reader. | ||
reader, writer = IO.pipe unless options[:squelch] | ||
stdin = Puppet::FileSystem.open(options[:stdinfile] || null_file, nil, 'r') | ||
stdout = options[:squelch] ? Puppet::FileSystem.open(null_file, nil, 'w') : Puppet::FileSystem::Uniquefile.new('puppet') | ||
stdout = options[:squelch] ? Puppet::FileSystem.open(null_file, nil, 'w') : writer | ||
stderr = options[:combine] ? stdout : Puppet::FileSystem.open(null_file, nil, 'w') | ||
|
||
exec_args = [command, options, stdin, stdout, stderr] | ||
output = '' | ||
|
||
if execution_stub = Puppet::Util::ExecutionStub.current_value | ||
return execution_stub.call(*exec_args) | ||
child_pid = execution_stub.call(*exec_args) | ||
[stdin, stdout, stderr].each {|io| io.close rescue nil} | ||
return child_pid | ||
elsif Puppet.features.posix? | ||
child_pid = nil | ||
begin | ||
child_pid = execute_posix(*exec_args) | ||
[stdin, stdout, stderr].each {|io| io.close rescue nil} | ||
unless options[:squelch] | ||
while !reader.eof? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe You can look at the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can also take a look at the PowerShell module which uses pipes - https://github.com/puppetlabs/puppetlabs-powershell/blob/master/lib/puppet_x/puppetlabs/powershell/powershell_manager.rb#L240-L307 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we're only using a single pipe, I think that's unnecessary. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Specifically, capture2 is using a thread for read while it also writes to stdin. We're not handling anything except reading from a single pipe, so only a single thread needed. |
||
output << reader.read | ||
end | ||
end | ||
exit_status = Process.waitpid2(child_pid).last.exitstatus | ||
child_pid = nil | ||
rescue Timeout::Error => e | ||
|
@@ -216,28 +229,28 @@ def self.execute(command, options = NoOptionsSpecified) | |
elsif Puppet.features.microsoft_windows? | ||
process_info = execute_windows(*exec_args) | ||
begin | ||
[stdin, stdout, stderr].each {|io| io.close rescue nil} | ||
unless options[:squelch] | ||
while !reader.eof? | ||
output << reader.read | ||
end | ||
end | ||
exit_status = Puppet::Util::Windows::Process.wait_process(process_info.process_handle) | ||
ensure | ||
FFI::WIN32.CloseHandle(process_info.process_handle) | ||
FFI::WIN32.CloseHandle(process_info.thread_handle) | ||
end | ||
end | ||
|
||
[stdin, stdout, stderr].each {|io| io.close rescue nil} | ||
|
||
# read output in if required | ||
unless options[:squelch] | ||
output = wait_for_output(stdout) | ||
Puppet.warning _("Could not get output") unless output | ||
end | ||
|
||
if options[:failonfail] and exit_status != 0 | ||
raise Puppet::ExecutionFailure, _("Execution of '%{str}' returned %{exit_status}: %{output}") % { str: command_str, exit_status: exit_status, output: output.strip } | ||
end | ||
ensure | ||
if !options[:squelch] && stdout | ||
# if we opened a temp file for stdout, we need to clean it up. | ||
stdout.close! | ||
# Make sure all handles are closed in case an exception was thrown attempting to execute. | ||
[stdin, stdout, stderr].each {|io| io.close rescue nil} | ||
if !options[:squelch] && reader | ||
# if we opened a pipe, we need to clean it up. | ||
reader.close | ||
end | ||
end | ||
|
||
|
@@ -325,35 +338,4 @@ def self.execute_windows(command, options, stdin, stdout, stderr) | |
end | ||
end | ||
private_class_method :execute_windows | ||
|
||
|
||
# This is private method. | ||
# @comment see call to private_class_method after method definition | ||
# @api private | ||
# | ||
def self.wait_for_output(stdout) | ||
# Make sure the file's actually been written. This is basically a race | ||
# condition, and is probably a horrible way to handle it, but, well, oh | ||
# well. | ||
# (If this method were treated as private / inaccessible from outside of this file, we shouldn't have to worry | ||
# about a race condition because all of the places that we call this from are preceded by a call to "waitpid2", | ||
# meaning that the processes responsible for writing the file have completed before we get here.) | ||
2.times do |try| | ||
if Puppet::FileSystem.exist?(stdout.path) | ||
stdout.open | ||
begin | ||
return stdout.read | ||
ensure | ||
stdout.close | ||
stdout.unlink | ||
end | ||
else | ||
time_to_sleep = try / 2.0 | ||
Puppet.warning _("Waiting for output; will sleep %{time_to_sleep} seconds") % { time_to_sleep: time_to_sleep } | ||
sleep(time_to_sleep) | ||
end | ||
end | ||
nil | ||
end | ||
private_class_method :wait_for_output | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Puppetserver installs an execution stub, e.g. when executing an autosigning policy. Will it work correctly if we close these descriptors? /cc @camlow325
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should only be closing them on the Puppet side, the forked process inherits the handles. I'm actually not sure we need to close them earlier than we did before, since nothing else about forking changed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already do close them in Puppet Server, although it seems like it would have better if we didn't have to. See this code.
If we add this here, we might get exceptions when the second set of close calls are made. That should be okay with the swallowing
rescue
, though, right? Not sure if anything would be written to the log when this happens?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rescue nil should handle that in both cases, and nothing should be logged. The execution doesn't change at all in this case, after execution_stub.call we still would have closed all the handles at old line 226.