PageRenderTime 50ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/background_process/background_process.rb

https://github.com/timcharper/background_process
Ruby | 116 lines | 69 code | 17 blank | 30 comment | 5 complexity | 28f20181ed623f6e541bb2eefc447ed5 MD5 | raw file
  1. class BackgroundProcess
  2. attr_reader :stdin, :stdout, :stderr, :pid
  3. # Initialize a BackgroundProcess task. Don't do this. Use BackgroundProcess.run or BackgroundProcess.run_pty instead
  4. def initialize(pid, stdin, stdout, stderr = nil)
  5. @pid, @stdin, @stdout, @stderr = pid, stdin, stdout, stderr
  6. ObjectSpace.define_finalizer(self) { kill }
  7. end
  8. # Run a command, connecting it's IO streams (stdin, sterr, stdout) via IO pipes,
  9. # which are not tty IO streams.
  10. #
  11. # Because of this, some programs (like ruby) will buffer their output and only
  12. # make it available when it's explicitely flushed (with IO#flush or when the
  13. # buffer gets full). This behavior can be overridden by setting the streams to
  14. # sync, like this:
  15. #
  16. # STDOUT.sync, STDERR.sync = true, true
  17. #
  18. # If you can't control the program and have it explicitly flush its output when it
  19. # should, or you can't tell the streams to run in sync mode, see
  20. # PTYBackgroundProcess.run for a workaround.
  21. def self.run(*command_with_args)
  22. command = sanitize_command(command_with_args)
  23. child_stdin, parent_stdin = IO::pipe
  24. parent_stdout, child_stdout = IO::pipe
  25. parent_stderr, child_stderr = IO::pipe
  26. pid = Kernel.fork do
  27. [parent_stdin, parent_stdout, parent_stderr].each { |io| io.close }
  28. STDIN.reopen(child_stdin)
  29. STDOUT.reopen(child_stdout)
  30. STDERR.reopen(child_stderr)
  31. [child_stdin, child_stdout, child_stderr].each { |io| io.close }
  32. exec command
  33. end
  34. [child_stdin, child_stdout, child_stderr].each { |io| io.close }
  35. parent_stdin.sync = true
  36. new(pid, parent_stdin, parent_stdout, parent_stderr)
  37. end
  38. # send a signal to the process. If the processes and running, do nothing.
  39. # Valid signals are those in Signal.list. Default is "TERM"
  40. def kill(signal = 'TERM')
  41. if running?
  42. Process.kill(Signal.list[signal], @pid)
  43. true
  44. end
  45. end
  46. # Sends the interrupt signal to the process. The equivalent of pressing control-C in it.
  47. def interrupt
  48. kill('INT')
  49. end
  50. # asks the operating system is the process still exists.
  51. def running?
  52. return false unless @pid
  53. Process.getpgid(@pid)
  54. true
  55. rescue Errno::ESRCH
  56. false
  57. end
  58. # waits for the process to finish. Freeze the process so it can rest in peace.
  59. # You should call this on every background job you create to avoid a flood of
  60. # zombie processes. (Processes won't go away until they are waited on)
  61. def wait(timeout = nil)
  62. @exit_status ||= Timeout.timeout(timeout) do
  63. Process.wait(@pid)
  64. $?
  65. end
  66. rescue Timeout::Error
  67. nil
  68. end
  69. # Waits for the process to terminate, and then returns the exit status
  70. def exitstatus
  71. wait && wait.exitstatus
  72. end
  73. # Calls block each time a line is available in the specified output buffer(s) and returns the first non-false value
  74. # By default, both stdout and stderr are monitored.
  75. #
  76. # Args:
  77. # * which: which streams to monitor. valid values are :stdout, :stderr, or :both.
  78. # * timeout: Total time in seconds to run detect for. If result not found within this time, abort and return nil. Pass nil for no timeout.
  79. # * &block: the block to call. If block takes two arguments, it will pass both the stream that received the input (an instance of IO, not the symbol), and the line read from the buffer.
  80. def detect(which = :both, timeout = nil, &block)
  81. streams = select_streams(which)
  82. BackgroundProcess::IOHelpers.detect(streams, timeout, &block)
  83. end
  84. protected
  85. # It's protected. What do you care? :P
  86. def self.sanitize_command(*args)
  87. command_and_args = args.flatten
  88. return command_and_args.first if command_and_args.length == 1
  89. command_and_args.map { |p| p.gsub(' ', '\ ') }.join(" ")
  90. end
  91. def select_streams(which)
  92. case which
  93. when :stdout then [stdout]
  94. when :stderr then [stderr]
  95. when :both then [stdout, stderr]
  96. else raise(ArgumentError, "invalid stream specification: #{which}")
  97. end.compact
  98. end
  99. end