about summary refs log tree commit homepage
path: root/lib/dtas/process.rb
blob: 35ca6a6858c8de225e9d91cb8190aed802deb176 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# -*- encoding: binary -*-
# :stopdoc:
# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
require 'shellwords'
require 'io/wait'
module DTAS::Process
  PIDS = {}

  def self.reaper
    begin
      pid, status = Process.waitpid2(-1, Process::WNOHANG)
      pid or return
      obj = PIDS.delete(pid)
      yield status, obj
    rescue Errno::ECHILD
      return
    end while true
  end

  # a convienient way to display commands so it's easy to
  # read, copy and paste to a shell
  def xs(cmd)
    cmd.map { |w| Shellwords.escape(w) }.join(' ')
  end

  # for long-running processes (sox/play/ecasound filters)
  def dtas_spawn(env, cmd, opts)
    opts = { close_others: true, pgroup: true }.merge!(opts)

    # stringify env, integer values are easier to type unquoted as strings
    env.each { |k,v| env[k] = v.to_s }

    pid = begin
      Process.spawn(env, cmd, opts)
    rescue Errno::EINTR # Ruby bug?
      retry
    end
    warn [ :spawn, pid, cmd ].inspect if $DEBUG
    @spawn_at = Time.now.to_f
    PIDS[pid] = self
    pid
  end

  # this is like backtick, but takes an array instead of a string
  # This will also raise on errors
  def qx(cmd, opts = {})
    r, w = IO.pipe
    opts = opts.merge(out: w)
    r.binmode
    if err = opts[:err]
      re, we = IO.pipe
      re.binmode
      opts[:err] = we
    end
    pid = begin
      Process.spawn(*cmd, opts)
    rescue Errno::EINTR # Ruby bug?
      retry
    end
    w.close
    if err
      we.close
      res = ""
      want = { r => res, re => err }
      begin
        readable = IO.select(want.keys) or next
        readable[0].each do |io|
          bytes = io.nread
          begin
            want[io] << io.read_nonblock(bytes > 0 ? bytes : 11)
          rescue Errno::EAGAIN
            # spurious wakeup, bytes may be zero
          rescue EOFError
            want.delete(io)
          end
        end
      end until want.empty?
      re.close
    else
      res = r.read
    end
    r.close
    _, status = Process.waitpid2(pid)
    return res if status.success?
    raise RuntimeError, "`#{xs(cmd)}' failed: #{status.inspect}"
  end
end