File Manager
Viewing File: unix.rb
#
# Author:: Adam Jacob (<adam@chef.io>)
# Copyright:: Copyright 2008-2016, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
class Chef
module Mixin
module Command
module Unix
# This is taken directly from Ara T Howard's Open4 library, and then
# modified to suit the needs of Chef. Any bugs here are most likely
# my own, and not Ara's.
#
# The original appears in external/open4.rb in its unmodified form.
#
# Thanks Ara!
def popen4(cmd, args = {}, &b)
# Ruby 1.8 suffers from intermittent segfaults believed to be due to GC while IO.select
# See CHEF-2916 / CHEF-1305
GC.disable
# Waitlast - this is magic.
#
# Do we wait for the child process to die before we yield
# to the block, or after? That is the magic of waitlast.
#
# By default, we are waiting before we yield the block.
args[:waitlast] ||= false
args[:user] ||= nil
unless args[:user].kind_of?(Integer)
args[:user] = Etc.getpwnam(args[:user]).uid if args[:user]
end
args[:group] ||= nil
unless args[:group].kind_of?(Integer)
args[:group] = Etc.getgrnam(args[:group]).gid if args[:group]
end
args[:environment] ||= {}
# Default on C locale so parsing commands output can be done
# independently of the node's default locale.
# "LC_ALL" could be set to nil, in which case we also must ignore it.
unless args[:environment].has_key?("LC_ALL")
args[:environment]["LC_ALL"] = "C"
end
pw, pr, pe, ps = IO.pipe, IO.pipe, IO.pipe, IO.pipe
verbose = $VERBOSE
begin
$VERBOSE = nil
ps.last.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
cid = fork do
pw.last.close
STDIN.reopen pw.first
pw.first.close
pr.first.close
STDOUT.reopen pr.last
pr.last.close
pe.first.close
STDERR.reopen pe.last
pe.last.close
STDOUT.sync = STDERR.sync = true
if args[:group]
Process.egid = args[:group]
Process.gid = args[:group]
end
if args[:user]
Process.euid = args[:user]
Process.uid = args[:user]
end
args[:environment].each do |key, value|
ENV[key] = value
end
if args[:umask]
umask = ((args[:umask].respond_to?(:oct) ? args[:umask].oct : args[:umask].to_i) & 007777)
File.umask(umask)
end
begin
if cmd.kind_of?(Array)
Kernel.exec(*cmd)
else
Kernel.exec(cmd)
end
raise "forty-two"
rescue Exception => e
Marshal.dump(e, ps.last)
ps.last.flush
end
ps.last.close unless ps.last.closed?
exit!
end
ensure
$VERBOSE = verbose
end
[pw.first, pr.last, pe.last, ps.last].each { |fd| fd.close }
begin
e = Marshal.load ps.first
raise(Exception === e ? e : "unknown failure!")
rescue EOFError # If we get an EOF error, then the exec was successful
42
ensure
ps.first.close
end
pw.last.sync = true
pi = [pw.last, pr.first, pe.first]
if b
begin
if args[:waitlast]
b[cid, *pi]
# send EOF so that if the child process is reading from STDIN
# it will actually finish up and exit
pi[0].close_write
Process.waitpid2(cid).last
else
# This took some doing.
# The trick here is to close STDIN
# Then set our end of the childs pipes to be O_NONBLOCK
# Then wait for the child to die, which means any IO it
# wants to do must be done - it's dead. If it isn't,
# it's because something totally skanky is happening,
# and we don't care.
o = StringIO.new
e = StringIO.new
pi[0].close
stdout = pi[1]
stderr = pi[2]
stdout.sync = true
stderr.sync = true
stdout.fcntl(Fcntl::F_SETFL, pi[1].fcntl(Fcntl::F_GETFL) | Fcntl::O_NONBLOCK)
stderr.fcntl(Fcntl::F_SETFL, pi[2].fcntl(Fcntl::F_GETFL) | Fcntl::O_NONBLOCK)
stdout_finished = false
stderr_finished = false
results = nil
while !stdout_finished || !stderr_finished
begin
channels_to_watch = []
channels_to_watch << stdout if !stdout_finished
channels_to_watch << stderr if !stderr_finished
ready = IO.select(channels_to_watch, nil, nil, 1.0)
rescue Errno::EAGAIN
ensure
results = Process.waitpid2(cid, Process::WNOHANG)
if results
stdout_finished = true
stderr_finished = true
end
end
if ready && ready.first.include?(stdout)
line = results ? stdout.gets(nil) : stdout.gets
if line
o.write(line)
else
stdout_finished = true
end
end
if ready && ready.first.include?(stderr)
line = results ? stderr.gets(nil) : stderr.gets
if line
e.write(line)
else
stderr_finished = true
end
end
end
results = Process.waitpid2(cid) unless results
o.rewind
e.rewind
b[cid, pi[0], o, e]
results.last
end
ensure
pi.each { |fd| fd.close unless fd.closed? }
end
else
[cid, pw.last, pr.first, pe.first]
end
ensure
GC.enable
end
end
end
end
end