-
Notifications
You must be signed in to change notification settings - Fork 81
/
pry-remote.rb
380 lines (310 loc) · 9.61 KB
/
pry-remote.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
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
293
294
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
350
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
require 'pry'
require 'slop'
require 'drb'
require 'readline'
require 'open3'
module PryRemote
DefaultHost = ENV['PRY_REMOTE_DEFAULT_HOST'] || "127.0.0.1"
DefaultPort = ENV['PRY_REMOTE_DEFAULT_PORT'] || 9876
# A class to represent an input object created from DRb. This is used because
# Pry checks for arity to know if a prompt should be passed to the object.
#
# @attr [#readline] input Object to proxy
InputProxy = Struct.new :input do
# Reads a line from the input
def readline(prompt)
case readline_arity
when 1 then input.readline(prompt)
else input.readline
end
end
def completion_proc=(val)
input.completion_proc = val
end
def readline_arity
input.method_missing(:method, :readline).arity
rescue NameError
0
end
end
# Class used to wrap inputs so that they can be sent through DRb.
#
# This is to ensure the input is used locally and not reconstructed on the
# server by DRb.
class IOUndumpedProxy
include DRb::DRbUndumped
def initialize(obj)
@obj = obj
end
def completion_proc=(val)
if @obj.respond_to? :completion_proc=
@obj.completion_proc = proc { |*args, &block| val.call(*args, &block) }
end
end
def completion_proc
@obj.completion_proc if @obj.respond_to? :completion_proc
end
def readline(prompt)
if Readline == @obj
@obj.readline(prompt, true)
elsif @obj.method(:readline).arity == 1
@obj.readline(prompt)
else
$stdout.print prompt
@obj.readline
end
end
def puts(*lines)
@obj.puts(*lines)
end
def print(*objs)
@obj.print(*objs)
end
def printf(*args)
@obj.printf(*args)
end
def write(data)
@obj.write data
end
def <<(data)
@obj << data
self
end
# Some versions of Pry expect $stdout or its output objects to respond to
# this message.
def tty?
false
end
end
# Ensure that system (shell command) output is redirected for remote session.
System = proc do |output, cmd, _|
status = nil
Open3.popen3 cmd do |stdin, stdout, stderr, wait_thr|
stdin.close # Send EOF to the process
until stdout.eof? and stderr.eof?
if res = IO.select([stdout, stderr])
res[0].each do |io|
next if io.eof?
output.write io.read_nonblock(1024)
end
end
end
status = wait_thr.value
end
unless status.success?
output.puts "Error while executing command: #{cmd}"
end
end
ClientEditor = proc do |initial_content, line|
# Hack to use Pry::Editor
Pry::Editor.new(Pry.new).edit_tempfile_with_content(initial_content, line)
end
# A client is used to retrieve information from the client program.
Client = Struct.new(:input, :output, :thread, :stdout, :stderr,
:editor) do
# Waits until both an input and output are set
def wait
sleep 0.01 until input and output and thread
end
# Tells the client the session is terminated
def kill
thread.run
end
# @return [InputProxy] Proxy for the input
def input_proxy
InputProxy.new input
end
end
class Server
def self.run(object, host = DefaultHost, port = DefaultPort, options = {})
new(object, host, port, options).run
end
def initialize(object, host = DefaultHost, port = DefaultPort, options = {})
@host = host
@port = port
@object = object
@options = options
@client = PryRemote::Client.new
DRb.start_service uri, @client
end
# Code that has to be called for Pry-remote to work properly
def setup
@hooks = Pry::Hooks.new
@hooks.add_hook :before_eval, :pry_remote_capture do
capture_output
end
@hooks.add_hook :after_eval, :pry_remote_uncapture do
uncapture_output
end
# Before Pry starts, save the pager config.
# We want to disable this because the pager won't do anything useful in
# this case (it will run on the server).
Pry.config.pager, @old_pager = false, Pry.config.pager
# As above, but for system config
Pry.config.system, @old_system = PryRemote::System, Pry.config.system
Pry.config.editor, @old_editor = editor_proc, Pry.config.editor
end
# Code that has to be called after setup to return to the initial state
def teardown
# Reset config
Pry.config.editor = @old_editor
Pry.config.pager = @old_pager
Pry.config.system = @old_system
puts "[pry-remote] Remote session terminated"
begin
@client.kill
rescue DRb::DRbConnError
puts "[pry-remote] Continuing to stop service"
ensure
puts "[pry-remote] Ensure stop service"
DRb.stop_service
end
end
# Captures $stdout and $stderr if so requested by the client.
def capture_output
@old_stdout, $stdout = if @client.stdout
[$stdout, @client.stdout]
else
[$stdout, $stdout]
end
@old_stderr, $stderr = if @client.stderr
[$stderr, @client.stderr]
else
[$stderr, $stderr]
end
end
# Resets $stdout and $stderr to their previous values.
def uncapture_output
$stdout = @old_stdout
$stderr = @old_stderr
end
def editor_proc
proc do |file, line|
File.write(file, @client.editor.call(File.read(file), line))
end
end
# Actually runs pry-remote
def run
puts "[pry-remote] Waiting for client on #{uri}"
@client.wait
puts "[pry-remote] Client received, starting remote session"
setup
Pry.start(@object, @options.merge(:input => client.input_proxy,
:output => client.output,
:hooks => @hooks))
ensure
teardown
end
# @return Object to enter into
attr_reader :object
# @return [PryServer::Client] Client connecting to the pry-remote server
attr_reader :client
# @return [String] Host of the server
attr_reader :host
# @return [Integer] Port of the server
attr_reader :port
# @return [String] URI for DRb
def uri
"druby://#{host}:#{port}"
end
end
# Parses arguments and allows to start the client.
class CLI
def initialize(args = ARGV)
params = Slop.parse args, :help => true do
banner "#$PROGRAM_NAME [OPTIONS]"
on :s, :server=, "Host of the server (#{DefaultHost})", :argument => :optional,
:default => DefaultHost
on :p, :port=, "Port of the server (#{DefaultPort})", :argument => :optional,
:as => Integer, :default => DefaultPort
on :w, :wait, "Wait for the pry server to come up",
:default => false
on :r, :persist, "Persist the client to wait for the pry server to come up each time",
:default => false
on :c, :capture, "Captures $stdout and $stderr from the server (true)",
:default => true
on :f, "Disables loading of .pryrc and its plugins, requires, and command history "
end
exit if params.help?
@host = params[:server]
@port = params[:port]
@wait = params[:wait]
@persist = params[:persist]
@capture = params[:capture]
Pry.initial_session_setup unless params[:f]
end
# @return [String] Host of the server
attr_reader :host
# @return [Integer] Port of the server
attr_reader :port
# @return [String] URI for DRb
def uri
"druby://#{host}:#{port}"
end
attr_reader :wait
attr_reader :persist
attr_reader :capture
alias wait? wait
alias persist? persist
alias capture? capture
def run
while true
connect
break unless persist?
end
end
# Connects to the server
#
# @param [IO] input Object holding input for pry-remote
# @param [IO] output Object pry-debug will send its output to
def connect(input = Pry.config.input, output = Pry.config.output)
local_ip = UDPSocket.open {|s| s.connect(@host, 1); s.addr.last}
DRb.start_service "druby://#{local_ip}:0"
client = DRbObject.new(nil, uri)
cleanup(client)
input = IOUndumpedProxy.new(input)
output = IOUndumpedProxy.new(output)
begin
client.input = input
client.output = output
rescue DRb::DRbConnError => ex
if wait? || persist?
sleep 1
retry
else
raise ex
end
end
if capture?
client.stdout = $stdout
client.stderr = $stderr
end
client.editor = ClientEditor
client.thread = Thread.current
sleep
DRb.stop_service
end
# Clean up the client
def cleanup(client)
begin
# The method we are calling here doesn't matter.
# This is a hack to close the connection of DRb.
client.cleanup
rescue DRb::DRbConnError, NoMethodError
end
end
end
end
class Object
# Starts a remote Pry session
#
# @param [String] host Host of the server
# @param [Integer] port Port of the server
# @param [Hash] options Options to be passed to Pry.start
def remote_pry(host = PryRemote::DefaultHost, port = PryRemote::DefaultPort, options = {})
PryRemote::Server.new(self, host, port, options).run
end
# a handy alias as many people may think the method is named after the gem
# (pry-remote)
alias pry_remote remote_pry
end