forked from textmate/bundle-support.tmbundle
/
process.rb
238 lines (199 loc) · 6.45 KB
/
process.rb
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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
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
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# -----------------------
# TextMate::Process.run()
# -----------------------
# Method for opening processes under TextMate.
#
# # BASIC USAGE
#
# 1. out, err = TextMate::Process.run("svn", "commit", "-m", "A commit message")
#
# 'out' and 'err' are the what the process produced on stdout and stderr respectively.
#
# 2. TextMate::Process.run("svn", "commit", "-m", "A commit message") do |str, type|
# case type
# when :out
# STDOUT << str
# when :err
# STDERR << str
# end
# end
#
# The block will be called with the output of the process as it becomes available.
#
# 3. TextMate::Process.run("svn", "commit", "-m", "A commit message") do |str|
# STDOUT << str
# end
#
# Similar to 2, except that the type of the output is not passed.
#
# # OPTIONS
#
# The last (non block) argument to run() can be a Hash that will augment the behaviour.
# The available options are (with default values in parenthesis)…
#
# * :interactive_input (true)
#
# Inject the interactive input library into the program so that any requests for
# input present the user with a dialog to enter it.
#
# * :echo (false)
#
# If using interactive input, echo the user's input onto the output
# (has no effect if interactive input is off).
#
# * :granularity (:line)
#
# The size of the buffer to use to read the process output. The value :line
# indicates that output will be passed a line at a time. Any other non integer
# value will result in an unspecified buffer size being used.
#
# * :input (nil)
#
# A string to send to the stdin of the process.
#
# * :env (nil)
#
# A hash of environment variables to set for the process.
#
# NOTES
#
# The following is not valid Ruby…
#
# args = ["commit", "-m", "commit message"]
# TextMate::Process("svn", *args, :buffer => true)
#
# To get around this, arguments to run() are flattened. This allows the
# almost as good version…
#
# args = ["commit", "-m", "commit message"]
# TextMate::Process("svn", args, :buffer => true)
#
require ENV['TM_SUPPORT_PATH'] + '/lib/io'
require 'fcntl'
module PTree # Process Tree Construction
module_function
def build
list = %x{ps -axww -o "pid,ppid,command="}.sub(/^.*$\n/, '')
all_nodes = { }
all_nodes[0] = { :pid => 0, :cmd => 'System Startup', :children => [ ] }
list.each do |line|
abort "Syntax error: #{line}" unless line =~ /^\s*(\d+)\s+(\d+)\s+(.*)$/
all_nodes[$1.to_i] = { :pid => $1.to_i, :ppid => $2.to_i, :cmd => $3, :children => [ ] }
end
all_nodes.each do |pid, process|
next if pid == 0
abort "Inconsistent Process Tree: parent (#{process[:ppid]}) for pid #{pid} does not exist" unless all_nodes.has_key? process[:ppid]
all_nodes[process[:ppid]][:children] << process
end
all_nodes[0]
end
def find(tree, pid)
return tree if tree[:pid] == pid
tree[:children].each do |child|
res = find(child, pid)
return res unless res.nil?
end
nil
end
def traverse(tree, &block)
tree[:children].each { |child| traverse(child, &block) }
block.call(tree)
end
end
def pid_exists?(pid)
%x{ps >/dev/null -xp #{pid}}
$? == 0
end
def kill_and_wait(pid)
begin
Process.kill("INT", pid)
20.times { return unless pid_exists?(pid); sleep 0.02 }
Process.kill("TERM", pid)
20.times { return unless pid_exists?(pid); sleep 0.02 }
Process.kill("KILL", pid)
rescue
# process doesn't exist anymore
end
end
def setup_kill_handler(pid, &block)
Signal.trap("USR1") do
did_kill = false
PTree.traverse(PTree.find(PTree.build, pid)) do |node|
if !did_kill && pid_exists?(node[:pid])
block.call("^C: #{node[:cmd]} (pid: #{node[:pid]})\n", :err)
kill_and_wait(node[:pid])
did_kill = true
end
end
end
end
module TextMate
module Process
class << self
TM_INTERACTIVE_INPUT_DYLIB = ENV['TM_SUPPORT_PATH'] + '/lib/tm_interactive_input.dylib'
def run(*cmd, &block)
cmd.flatten!
options = {
:interactive_input => true,
:echo => false,
:granularity => :line,
:input => nil,
:env => nil,
:watch_fds => { },
}
options.merge! cmd.pop if cmd.last.is_a? Hash
io = []
3.times { io << ::IO::pipe }
# F_SETOWN = 6, ideally this would be under Fcntl::F_SETOWN
io[0][0].fcntl(6, ENV['TM_PID'].to_i) if ENV.has_key?('TM_PID') && File.exists?(TM_INTERACTIVE_INPUT_DYLIB)
pid = fork {
at_exit { exit! }
STDIN.reopen(io[0][0])
STDOUT.reopen(io[1][1])
STDERR.reopen(io[2][1])
io.flatten.each { |fd| fd.close }
options[:env].each { |k,v| ENV[k] = v } unless options[:env].nil?
if options[:interactive_input] and File.exists? TM_INTERACTIVE_INPUT_DYLIB
dil = ENV['DYLD_INSERT_LIBRARIES']
if dil.nil? or dil.empty?
ENV['DYLD_INSERT_LIBRARIES'] = TM_INTERACTIVE_INPUT_DYLIB
elsif not dil.include? TM_INTERACTIVE_INPUT_DYLIB
ENV['DYLD_INSERT_LIBRARIES'] = "#{TM_INTERACTIVE_INPUT_DYLIB}:#{dil}"
end
ENV['DYLD_FORCE_FLAT_NAMESPACE'] = "1"
ENV['TM_INTERACTIVE_INPUT'] = "AUTO" + ((options[:echo]) ? "|ECHO" : "")
end
exec(*cmd.compact)
}
[ io[0][0], io[1][1], io[2][1] ].each { |fd| fd.close }
if echo_fd = ENV['TM_INTERACTIVE_INPUT_ECHO_FD']
::IO.for_fd(echo_fd.to_i).close
ENV.delete('TM_INTERACTIVE_INPUT_ECHO_FD')
end
if options[:input].nil?
io[0][1].close
else
Thread.new { (io[0][1] << options[:input]).close }
end
out = ""
err = ""
block ||= proc { |str, fd|
case fd
when :out then out << str
when :err then err << str
end
}
previous_block_size = IO.blocksize
IO.blocksize = options[:granularity] if options[:granularity].is_a? Integer
previous_sync = IO.sync
IO.sync = true unless options[:granularity] == :line
setup_kill_handler(pid, &block)
IO.exhaust(options[:watch_fds].merge(:out => io[1][0], :err => io[2][0]), &block)
::Process.waitpid(pid)
IO.blocksize = previous_block_size
IO.sync = previous_sync
block_given? ? nil : [out,err]
end
end
end
end