Module | Ohai::Mixin::Command |
In: |
lib/ohai/mixin/command.rb
|
This is taken directly from Ara T Howard‘s Open4 library, and then modified to suit the needs of Ohai. 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!
# File lib/ohai/mixin/command.rb, line 130 130: def popen4(cmd, args={}, &b) 131: 132: ## Disable garbage collection to work around possible bug in MRI 133: # Ruby 1.8 suffers from intermittent segfaults believed to be due to GC while IO.select 134: # See OHAI-330 / CHEF-2916 / CHEF-1305 135: GC.disable 136: 137: # Waitlast - this is magic. 138: # 139: # Do we wait for the child process to die before we yield 140: # to the block, or after? That is the magic of waitlast. 141: # 142: # By default, we are waiting before we yield the block. 143: args[:waitlast] ||= false 144: 145: args[:user] ||= nil 146: unless args[:user].kind_of?(Integer) 147: args[:user] = Etc.getpwnam(args[:user]).uid if args[:user] 148: end 149: args[:group] ||= nil 150: unless args[:group].kind_of?(Integer) 151: args[:group] = Etc.getgrnam(args[:group]).gid if args[:group] 152: end 153: args[:environment] ||= {} 154: 155: # Default on C locale so parsing commands output can be done 156: # independently of the node's default locale. 157: # "LC_ALL" could be set to nil, in which case we also must ignore it. 158: unless args[:environment].has_key?("LC_ALL") 159: args[:environment]["LC_ALL"] = "C" 160: end 161: 162: pw, pr, pe, ps = IO.pipe, IO.pipe, IO.pipe, IO.pipe 163: 164: verbose = $VERBOSE 165: begin 166: $VERBOSE = nil 167: ps.last.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) 168: 169: cid = fork { 170: Process.setsid 171: 172: pw.last.close 173: STDIN.reopen pw.first 174: pw.first.close 175: 176: pr.first.close 177: STDOUT.reopen pr.last 178: pr.last.close 179: 180: pe.first.close 181: STDERR.reopen pe.last 182: pe.last.close 183: 184: STDOUT.sync = STDERR.sync = true 185: 186: if args[:group] 187: Process.egid = args[:group] 188: Process.gid = args[:group] 189: end 190: 191: if args[:user] 192: Process.euid = args[:user] 193: Process.uid = args[:user] 194: end 195: 196: args[:environment].each do |key,value| 197: ENV[key] = value 198: end 199: 200: if args[:umask] 201: umask = ((args[:umask].respond_to?(:oct) ? args[:umask].oct : args[:umask].to_i) & 007777) 202: File.umask(umask) 203: end 204: 205: begin 206: if cmd.kind_of?(Array) 207: exec(*cmd) 208: else 209: exec(cmd) 210: end 211: raise 'forty-two' 212: rescue Exception => e 213: Marshal.dump(e, ps.last) 214: ps.last.flush 215: end 216: ps.last.close unless (ps.last.closed?) 217: exit! 218: } 219: ensure 220: $VERBOSE = verbose 221: end 222: 223: [pw.first, pr.last, pe.last, ps.last].each{|fd| fd.close} 224: 225: begin 226: e = Marshal.load ps.first 227: raise(Exception === e ? e : "unknown failure!") 228: rescue EOFError # If we get an EOF error, then the exec was successful 229: 42 230: ensure 231: ps.first.close 232: end 233: 234: pw.last.sync = true 235: 236: pi = [pw.last, pr.first, pe.first] 237: 238: if b 239: begin 240: if args[:waitlast] 241: b[cid, *pi] 242: # send EOF so that if the child process is reading from STDIN 243: # it will actually finish up and exit 244: pi[0].close_write 245: Process.waitpid2(cid).last 246: else 247: # This took some doing. 248: # The trick here is to close STDIN 249: # Then set our end of the childs pipes to be O_NONBLOCK 250: # Then wait for the child to die, which means any IO it 251: # wants to do must be done - it's dead. If it isn't, 252: # it's because something totally skanky is happening, 253: # and we don't care. 254: o = StringIO.new 255: e = StringIO.new 256: 257: #pi[0].close 258: 259: stdout = pi[1] 260: stderr = pi[2] 261: 262: stdout.sync = true 263: stderr.sync = true 264: 265: stdout.fcntl(Fcntl::F_SETFL, pi[1].fcntl(Fcntl::F_GETFL) | Fcntl::O_NONBLOCK) 266: stderr.fcntl(Fcntl::F_SETFL, pi[2].fcntl(Fcntl::F_GETFL) | Fcntl::O_NONBLOCK) 267: 268: stdout_finished = false 269: stderr_finished = false 270: 271: results = nil 272: 273: while !stdout_finished || !stderr_finished 274: begin 275: channels_to_watch = [] 276: channels_to_watch << stdout if !stdout_finished 277: channels_to_watch << stderr if !stderr_finished 278: ready = IO.select(channels_to_watch, nil, nil, 1.0) 279: rescue Errno::EAGAIN 280: ensure 281: results = Process.waitpid2(cid, Process::WNOHANG) 282: if results 283: stdout_finished = true 284: stderr_finished = true 285: end 286: end 287: 288: if ready && ready.first.include?(stdout) 289: line = results ? stdout.gets(nil) : stdout.gets 290: if line 291: o.write(line) 292: else 293: stdout_finished = true 294: end 295: end 296: if ready && ready.first.include?(stderr) 297: line = results ? stderr.gets(nil) : stderr.gets 298: if line 299: e.write(line) 300: else 301: stderr_finished = true 302: end 303: end 304: end 305: results = Process.waitpid2(cid) unless results 306: o.rewind 307: e.rewind 308: 309: # **OHAI-275** 310: # The way we read from the pipes causes ruby to mark the strings 311: # as ASCII-8BIT (i.e., binary), but the content should be encoded 312: # as the default external encoding. For example, a command may 313: # return data encoded as UTF-8, but the strings will be marked as 314: # ASCII-8BIT. Later, when you attempt to print the values as 315: # UTF-8, Ruby will try to convert them and fail, raising an 316: # error. 317: # 318: # Ruby always marks strings as binary when read from IO in 319: # incomplete chunks, since you may have split the data within a 320: # multibyte char. In our case, we concat the chunks back 321: # together, so any multibyte chars will be reassembled. 322: # 323: # Note that all of this applies only to Ruby 1.9, which we check 324: # for by making sure that the Encoding class exists and strings 325: # have encoding methods. 326: if "".respond_to?(:force_encoding) && defined?(Encoding) 327: o.string.force_encoding(Encoding.default_external) 328: e.string.force_encoding(Encoding.default_external) 329: end 330: 331: b[cid, pi[0], o, e] 332: results.last 333: end 334: ensure 335: pi.each{|fd| fd.close unless fd.closed?} 336: end 337: else 338: [cid, pw.last, pr.first, pe.first] 339: end 340: rescue Errno::ENOENT 341: raise Ohai::Exceptions::Exec, "command #{cmd} doesn't exist or is not in the PATH" 342: ensure 343: # we disabled GC entering 344: GC.enable 345: end
# File lib/ohai/mixin/command.rb, line 32 32: def run_command(args={}) 33: if args.has_key?(:creates) 34: if File.exists?(args[:creates]) 35: Ohai::Log.debug("Skipping #{args[:command]} - creates #{args[:creates]} exists.") 36: return false 37: end 38: end 39: 40: stdout_string = nil 41: stderr_string = nil 42: 43: args[:cwd] ||= Dir.tmpdir 44: unless File.directory?(args[:cwd]) 45: raise Ohai::Exceptions::Exec, "#{args[:cwd]} does not exist or is not a directory" 46: end 47: 48: status = nil 49: Dir.chdir(args[:cwd]) do 50: status, stdout_string, stderr_string = run_command_backend(args[:command], args[:timeout]) 51: # systemu returns 42 when it hits unexpected errors 52: if status.exitstatus == 42 and stderr_string == "" 53: stderr_string = "Failed to run: #{args[:command]}, assuming command not found" 54: Ohai::Log.debug(stderr_string) 55: end 56: 57: if stdout_string 58: Ohai::Log.debug("---- Begin #{args[:command]} STDOUT ----") 59: Ohai::Log.debug(stdout_string.strip) 60: Ohai::Log.debug("---- End #{args[:command]} STDOUT ----") 61: end 62: if stderr_string 63: Ohai::Log.debug("---- Begin #{args[:command]} STDERR ----") 64: Ohai::Log.debug(stderr_string.strip) 65: Ohai::Log.debug("---- End #{args[:command]} STDERR ----") 66: end 67: 68: args[:returns] ||= 0 69: args[:no_status_check] ||= false 70: if status.exitstatus != args[:returns] and not args[:no_status_check] 71: raise Ohai::Exceptions::Exec, "#{args[:command_string]} returned #{status.exitstatus}, expected #{args[:returns]}" 72: else 73: Ohai::Log.debug("Ran #{args[:command_string]} (#{args[:command]}) returned #{status.exitstatus}") 74: end 75: end 76: return status, stdout_string, stderr_string 77: end
# File lib/ohai/mixin/command.rb, line 81 81: def run_command_unix(command, timeout) 82: stderr_string, stdout_string, status = "", "", nil 83: 84: exec_processing_block = lambda do |pid, stdin, stdout, stderr| 85: stdout_string, stderr_string = stdout.string.chomp, stderr.string.chomp 86: end 87: 88: if timeout 89: begin 90: Timeout.timeout(timeout) do 91: status = popen4(command, {}, &exec_processing_block) 92: end 93: rescue Timeout::Error => e 94: Chef::Log.error("#{command} exceeded timeout #{timeout}") 95: raise(e) 96: end 97: else 98: status = popen4(command, {}, &exec_processing_block) 99: end 100: return status, stdout_string, stderr_string 101: end
# File lib/ohai/mixin/command.rb, line 103 103: def run_command_windows(command, timeout) 104: if timeout 105: begin 106: systemu(command) 107: rescue SystemExit => e 108: raise 109: rescue Timeout::Error => e 110: Ohai::Log.error("#{command} exceeded timeout #{timeout}") 111: raise(e) 112: end 113: else 114: systemu(command) 115: end 116: end