From a4abb9953e97207f897ee4661eef8819611b6a54 Mon Sep 17 00:00:00 2001 From: Brandon Weaver Date: Sat, 31 May 2014 17:32:15 -0500 Subject: [PATCH 1/3] Update readme to be more descriptive --- README.md | 113 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 78 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 92d3b74..5b3b421 100644 --- a/README.md +++ b/README.md @@ -11,48 +11,91 @@ access the state of the running program from anywhere. Here's a program starting pry-remote: - require 'pry-remote' +```ruby +require 'pry-remote' - class Foo - def initialize(x, y) - binding.remote_pry - end - end +class Foo + def initialize(x, y) + binding.remote_pry + end +end - Foo.new 10, 20 +Foo.new 10, 20 +``` Running it will prompt you with a message telling you Pry is waiting for a program to connect itself to it: - [pry-remote] Waiting for client on drb://localhost:9876 +``` +[pry-remote] Waiting for client on drb://localhost:9876 +``` You can then connect yourself using ``pry-remote``: - $ pry-remote - From: example.rb @ line 7 in Foo#initialize: - 2: - 3: require 'pry-remote' - 4: - 5: class Foo - 6: def initialize(x, y) - => 7: binding.remote_pry - 8: end - 9: end - 10: - 11: Foo.new 10, 20 - pry(#):1> self - => # - pry(#):2> ls -l - Local variables: [ - [0] :_, - [1] :_dir_, - [2] :_file_, - [3] :_ex_, - [4] :_pry_, - [5] :_out_, - [6] :_in_, - [7] :x, - [8] :y - ] - pry(#):3> ^D +``` +$ pry-remote +From: example.rb @ line 7 in Foo#initialize: + 2: + 3: require 'pry-remote' + 4: + 5: class Foo + 6: def initialize(x, y) + => 7: binding.remote_pry + 8: end + 9: end + 10: + 11: Foo.new 10, 20 +pry(#):1> self +=> # +pry(#):2> ls -l +Local variables: [ + [0] :_, + [1] :_dir_, + [2] :_file_, + [3] :_ex_, + [4] :_pry_, + [5] :_out_, + [6] :_in_, + [7] :x, + [8] :y +] +pry(#):3> ^D +``` +# Command line options + +``` +[OPTIONS] + -s, --server Host of the server (127.0.0.1) + -p, --port Port of the server (9876) + -w, --wait Wait for the pry server to come up + -c, --capture Captures $stdout and $stderr from the server (true) + -f, Disables loading of .pryrc and its plugins, requires, and command history + -h, --help Display this help message. +``` + +# Connecting with an external client + +In order to connect with an external client, you first need to pass in the server address to the remote_pry: + +```ruby +require 'pry-remote' + +class Foo + def initialize(x, y) + binding.remote_pry(server_address, port_number) + end +end + +Foo.new 10, 20 +``` + +...where the server address is an externally accessible ip or hostname, such as 192.168.1.x + +To connect to this session, open a terminal on the client machine and enter: + +``` +pry-remote -s (server_ip) -p (port) -c -w +``` + +This will drop you into a new remote pry session. From 2f3d3393929fb219f607142114a3ab96f39c7649 Mon Sep 17 00:00:00 2001 From: Brandon Weaver Date: Sat, 31 May 2014 17:37:17 -0500 Subject: [PATCH 2/3] Revert "Update readme to be more descriptive" This reverts commit a4abb9953e97207f897ee4661eef8819611b6a54. --- README.md | 113 +++++++++++++++++------------------------------------- 1 file changed, 35 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 5b3b421..92d3b74 100644 --- a/README.md +++ b/README.md @@ -11,91 +11,48 @@ access the state of the running program from anywhere. Here's a program starting pry-remote: -```ruby -require 'pry-remote' + require 'pry-remote' -class Foo - def initialize(x, y) - binding.remote_pry - end -end + class Foo + def initialize(x, y) + binding.remote_pry + end + end -Foo.new 10, 20 -``` + Foo.new 10, 20 Running it will prompt you with a message telling you Pry is waiting for a program to connect itself to it: -``` -[pry-remote] Waiting for client on drb://localhost:9876 -``` + [pry-remote] Waiting for client on drb://localhost:9876 You can then connect yourself using ``pry-remote``: -``` -$ pry-remote -From: example.rb @ line 7 in Foo#initialize: - 2: - 3: require 'pry-remote' - 4: - 5: class Foo - 6: def initialize(x, y) - => 7: binding.remote_pry - 8: end - 9: end - 10: - 11: Foo.new 10, 20 -pry(#):1> self -=> # -pry(#):2> ls -l -Local variables: [ - [0] :_, - [1] :_dir_, - [2] :_file_, - [3] :_ex_, - [4] :_pry_, - [5] :_out_, - [6] :_in_, - [7] :x, - [8] :y -] -pry(#):3> ^D -``` + $ pry-remote + From: example.rb @ line 7 in Foo#initialize: + 2: + 3: require 'pry-remote' + 4: + 5: class Foo + 6: def initialize(x, y) + => 7: binding.remote_pry + 8: end + 9: end + 10: + 11: Foo.new 10, 20 + pry(#):1> self + => # + pry(#):2> ls -l + Local variables: [ + [0] :_, + [1] :_dir_, + [2] :_file_, + [3] :_ex_, + [4] :_pry_, + [5] :_out_, + [6] :_in_, + [7] :x, + [8] :y + ] + pry(#):3> ^D -# Command line options - -``` -[OPTIONS] - -s, --server Host of the server (127.0.0.1) - -p, --port Port of the server (9876) - -w, --wait Wait for the pry server to come up - -c, --capture Captures $stdout and $stderr from the server (true) - -f, Disables loading of .pryrc and its plugins, requires, and command history - -h, --help Display this help message. -``` - -# Connecting with an external client - -In order to connect with an external client, you first need to pass in the server address to the remote_pry: - -```ruby -require 'pry-remote' - -class Foo - def initialize(x, y) - binding.remote_pry(server_address, port_number) - end -end - -Foo.new 10, 20 -``` - -...where the server address is an externally accessible ip or hostname, such as 192.168.1.x - -To connect to this session, open a terminal on the client machine and enter: - -``` -pry-remote -s (server_ip) -p (port) -c -w -``` - -This will drop you into a new remote pry session. From a2d57b5a098547e3080b062dcd2b0f34c45d58ea Mon Sep 17 00:00:00 2001 From: Brandon Weaver Date: Sat, 31 May 2014 18:11:57 -0500 Subject: [PATCH 3/3] Seperated module for ease of maintainability --- lib/cli.rb | 105 +++++++++++++ lib/client.rb | 24 +++ lib/input_proxy.rb | 25 +++ lib/io_undumped_proxy.rb | 61 +++++++ lib/pry-remote.rb | 332 +-------------------------------------- lib/server.rb | 88 +++++++++++ lib/system.rb | 24 +++ 7 files changed, 334 insertions(+), 325 deletions(-) create mode 100644 lib/cli.rb create mode 100644 lib/client.rb create mode 100644 lib/input_proxy.rb create mode 100644 lib/io_undumped_proxy.rb create mode 100644 lib/server.rb create mode 100644 lib/system.rb diff --git a/lib/cli.rb b/lib/cli.rb new file mode 100644 index 0000000..d016b7b --- /dev/null +++ b/lib/cli.rb @@ -0,0 +1,105 @@ +module PryRemote + # 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, + a: 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.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 diff --git a/lib/client.rb b/lib/client.rb new file mode 100644 index 0000000..90c62d4 --- /dev/null +++ b/lib/client.rb @@ -0,0 +1,24 @@ +module PryRemote + # A client is used to retrieve information from the client program. + class Client + attr_accessor :input, :output, :thread, :stdout, :stderr + + def initialize + end + + # Waits until both an input and output are set + def wait + sleep 0.01 until input && output && 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 +end diff --git a/lib/input_proxy.rb b/lib/input_proxy.rb new file mode 100644 index 0000000..b23d40a --- /dev/null +++ b/lib/input_proxy.rb @@ -0,0 +1,25 @@ +module PryRemote + # 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 +end diff --git a/lib/io_undumped_proxy.rb b/lib/io_undumped_proxy.rb new file mode 100644 index 0000000..9a3f4ab --- /dev/null +++ b/lib/io_undumped_proxy.rb @@ -0,0 +1,61 @@ +module PryRemote + # 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 = val + 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 +end diff --git a/lib/pry-remote.rb b/lib/pry-remote.rb index e5ffb85..62bbbc8 100644 --- a/lib/pry-remote.rb +++ b/lib/pry-remote.rb @@ -7,333 +7,15 @@ module PryRemote DefaultHost = "127.0.0.1" DefaultPort = 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 = val - 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 - - # A client is used to retrieve information from the client program. - Client = Struct.new :input, :output, :thread, :stdout, :stderr 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 - - puts "[pry-remote] Waiting for client on #{uri}" - @client.wait - - puts "[pry-remote] Client received, starting remote session" - end - - # Code that has to be called for Pry-remote to work properly - def setup - # If client passed stdout and stderr, redirect actual messages there. - @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 - - # 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 - end - - # Code that has to be called after setup to return to the initial state - def teardown - # Reset output streams - $stdout = @old_stdout - $stderr = @old_stderr - - # Reset config - Pry.config.pager = @old_pager - - # Reset sysem - 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 - - # Actually runs pry-remote - def run - setup - - Pry.start(@object, @options.merge(:input => client.input_proxy, :output => client.output)) - 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.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 +require 'cli' +require 'client' +require 'input_proxy' +require 'io_undumped_proxy' +require 'server' +require 'system' + class Object # Starts a remote Pry session # diff --git a/lib/server.rb b/lib/server.rb new file mode 100644 index 0000000..d2d8c21 --- /dev/null +++ b/lib/server.rb @@ -0,0 +1,88 @@ +module PryRemote + 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 + + puts "[pry-remote] Waiting for client on #{uri}" + @client.wait + + puts "[pry-remote] Client received, starting remote session" + end + + # Code that has to be called for Pry-remote to work properly + def setup + # If client passed stdout and stderr, redirect actual messages there. + @old_stdout, $stdout = [$stdout, @client.stdout] if @client.stdout + @old_stderr, $stderr = [$stderr, @client.stderr] if @client.stderr + + # 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 + end + + # Code that has to be called after setup to return to the initial state + def teardown + # Reset output streams if they were changed + $stdout = @old_stdout if @old_stdout + $stderr = @old_stderr if @old_stderr + + # Reset config + Pry.config.pager = @old_pager + + # Reset sysem + 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 + + # Actually runs pry-remote + def run + setup + + Pry.start(@object, @options.merge(input: client.input_proxy, output: client.output)) + 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 +end diff --git a/lib/system.rb b/lib/system.rb new file mode 100644 index 0000000..aa979cf --- /dev/null +++ b/lib/system.rb @@ -0,0 +1,24 @@ +module PryRemote + # 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 +end