Class: AngryShell::Shell

Inherits:
Object
  • Object
show all
Defined in:
lib/angry_shell.rb

Defined Under Namespace

Classes: IPCState, ShellResult

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*args, &block) ⇒ Shell

Returns a new instance of Shell.



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/angry_shell.rb', line 39

def initialize(*args,&block)
  @block = block
  @options = if Hash === args.last then args.pop else {} end

  case args.size
  when 0
    # no op
  when 1
    @options[:cmd] = args.first
  else
    @options[:cmd] = args
  end

  @options[:stream] = false unless @options.key?(:stream)
end

Instance Attribute Details

#optionsObject (readonly)

Returns the value of attribute options.



37
38
39
# File 'lib/angry_shell.rb', line 37

def options
  @options
end

Instance Method Details

#debug(*msg) ⇒ Object



33
34
35
# File 'lib/angry_shell.rb', line 33

def debug(*msg)
  puts "sh: #{msg * ' '}"
end

#executeObject



55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/angry_shell.rb', line 55

def execute
  error,out = nil,nil

  rv = popen4(options) {|pid,ipc|
    out   = ipc.stdout.read
    error = ipc.stderr.read
  }
  
  rv.stderr = error
  rv.stdout = out

  rv
end

#massaged_args(args) ⇒ Object



394
395
396
397
398
399
400
401
402
403
404
# File 'lib/angry_shell.rb', line 394

def massaged_args args
  args.dup.tap do |args_to_print|
    args_to_print[:environment] = e = args[:environment].dup

    %w{LC_ALL GEM_HOME GEM_PATH RUBYOPT BUNDLE_GEMFILE}.each {|env| e.delete(env)}

    args_to_print['cwd'] = args_to_print['cwd'].to_s if args_to_print['cwd']

    args_to_print.delete_if{|k,v| v.blank?}
  end
end

#ok?Boolean

runs the command, returning true if it returns success.

Returns:

  • (Boolean)


75
76
77
# File 'lib/angry_shell.rb', line 75

def ok?
  execute.ok?
end

#popen4(args = {}, &blk) ⇒ Object

This is taken from Chef and rewritten.

Chef’s preamble: 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!



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/angry_shell.rb', line 164

def popen4(args={}, &blk)
  popen4_normalise_args(args)
 
  
  # We pass and manipulate all IPC pipes around inside this object.
  ipc = IPCState.new

  verbose = $VERBOSE
  cid = begin
    $VERBOSE = nil
    ipc.before_fork!

    fork {
      popen4_proceed_as_child(args,ipc)
    }
  ensure
    $VERBOSE = verbose
  end

  popen4_proceed_as_parent(cid,args,ipc,&blk)
end

#popen4_normalise_args(args) ⇒ Object



351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
# File 'lib/angry_shell.rb', line 351

def popen4_normalise_args(args)
  # Do we wait for the child process to die before we yield
  # to the block, or after?
  #
  # By default, we are waiting before we yield the block.
  args[:stream] ||= 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

  unless TrueClass === args[:without_cleaning_bundler]
    args[:environment].update('RUBYOPT' => nil, 'BUNDLE_GEMFILE' => nil, 'GEM_HOME' => nil, 'GEM_PATH' => nil)
  end

  # `:as` - run the command as another user, via sudo,
  if user = args[:as]
    if (evars = args[:environment].reject {|k,v| v.nil?}.map {|k,v| "#{k}=#{v}"}) && !evars.empty?
      env = "env #{evars.join(' ')}"
    else
      env = ''
    end
    
    args[:cmd] = "sudo -H -u #{user} #{env} #{args[:cmd]}"
  end
end

#popen4_parent_exhaust_io(cid, args, ipc, &blk) ⇒ Object

Use select to read the entire contents of the pipes into StringIOs. This is the main change to come from Chef vs the original open4.



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/angry_shell.rb', line 222

def popen4_parent_exhaust_io(cid,args,ipc,&blk)
  output = StringIO.new
  error  = StringIO.new

  if args[:input]
    ipc.write.puts args[:input]
  end

  ipc.write.close

  stdout = ipc.read
  stderr = ipc.error

  stdout.sync = true
  stderr.sync = true

  stdout.fcntl(Fcntl::F_SETFL, stdout.fcntl(Fcntl::F_GETFL) | Fcntl::O_NONBLOCK)
  stderr.fcntl(Fcntl::F_SETFL, stderr.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
      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
        output.write(line)
      else
        stdout_finished = true
      end
    end

    if ready && ready.first.include?(stderr)
      line = results ? stderr.gets(nil) : stderr.gets
      if line
        error.write(line)
      else
        stderr_finished = true
      end
    end
  end

  results = Process.waitpid2(cid) unless results

  output.rewind
  error.rewind

  ipc.read = output
  ipc.error = error

  blk[cid, ipc]

  ShellResult.new(results.last, args)
end

#popen4_proceed_as_child(args, ipc) ⇒ Object



295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/angry_shell.rb', line 295

def popen4_proceed_as_child(args,ipc)
  ipc.child_after_fork!

  if args[:group]
    Process.egid = args[:group]
    Process.gid  = args[:group]
  end

  if args[:user]
    Process.euid = args[:user]
    Process.uid  = args[:user]
  end

  # Copy the specified environment across to the child's environment.
  # Keys with `nil` values are deleted from the environment.
  args[:environment].each do |key,value|
    if value.nil?
      ENV.delete(key.to_s)
    else
      ENV[key.to_s] = value
    end
  end

  if args[:umask]
    umask = ((args[:umask].respond_to?(:oct) ? args[:umask].oct : args[:umask].to_i) & 007777)
    File.umask(umask)
  end

  if args[:cwd]
    Dir.chdir args[:cwd]
  end

  begin
    cmd = args[:cmd]

    case cmd
    when Proc
      exit cmd.call.to_i
    when Array
      exec(*cmd)
    else
      exec(cmd)
    end

    raise 'forty-two' 
  rescue SystemExit
    exit $!.status
  rescue Object => e
    Marshal.dump(e, ipc.exception)
    ipc.exception.flush
  end

  ipc.exception.close unless (ipc.exception.closed?)
  exit!
end

#popen4_proceed_as_parent(cid, args, ipc, &blk) ⇒ Object



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/angry_shell.rb', line 187

def popen4_proceed_as_parent(cid,args,ipc,&blk)
  ipc.parent_after_fork!

  # The first thing a parent does after forking is look for an Marshalled exception on the exception pipe.
  begin
    e = Marshal.load ipc.exception
    raise(Exception === e ? e : "unknown failure!")
  rescue EOFError # If we get an EOF error, then the exec was successful
    42
  ensure
    ipc.exception.close
  end

  ipc.write.sync = true

  if block_given?
    begin
      if args[:stream]
        # hand the block the pipes inside ipc to manipulate manually
        yield(cid, ipc)
        ShellResult.new(Process.waitpid2(cid).last, args)
      else
        popen4_parent_exhaust_io(cid,args,ipc,&blk)
      end
    ensure
      ipc.close_all
    end
  else
    # Return the pipes. The User needs to clean up after themselves.
    [cid, ipc]
  end
end

#runObject

runs the command, raising if it doesn’t return success.



70
71
72
# File 'lib/angry_shell.rb', line 70

def run
  execute.ensure_ok!
end

#to_sObject

runs the command, returning its ‘stdout`. If the command doesn’t return success, return a blank string.



80
81
82
83
84
85
86
87
# File 'lib/angry_shell.rb', line 80

def to_s
  result = execute
  if result.ok?
    result.stdout.chomp
  else
    ''
  end
end