endoscope 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 90f6bdfe035c6da330ff7889c78313cdea6f654c
4
+ data.tar.gz: 866e177d694c6165eac866c813eeb962d3930151
5
+ SHA512:
6
+ metadata.gz: df4505b45883aa0583255228f1f21676531a8f5e4b8b41ecc0c115e5a0c81dad1e75fe5aadcda9451afe326a23a12e22ad880538caaade657c29ae2714adc9c5
7
+ data.tar.gz: c074d13f39ad2a9fdf5a0873449072ca081ad2474c6e985b9fe8c4dfea829ddfc3f082610b98eab6c313062d024b14316b03f1e52ce5453fdf1a687243044276
@@ -0,0 +1,23 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
23
+ vendor/bundle/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ services:
3
+ - redis-server
4
+ rvm:
5
+ - ruby-head
6
+ - 2.1.2
7
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in endoscope.gemspec
4
+ gemspec
5
+
6
+ gem "ripl"
7
+ gem "ripl-ripper"
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Mathieu Ravaux
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,115 @@
1
+ # Endoscope
2
+
3
+ [![Code Climate](https://codeclimate.com/github/preplay/endoscope.png)](https://codeclimate.com/github/preplay/endoscope)
4
+ [![Build Status](https://travis-ci.org/preplay/endoscope.svg?branch=master)](https://travis-ci.org/preplay/endoscope)
5
+
6
+ Remote shell for live interaction with Ruby processes
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'endoscope'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install endoscope
21
+
22
+ ## Usage
23
+
24
+ You need to start the endoscope agent when your application boots up, naming this process.
25
+
26
+ For example, in a Rails app this could go in `config/initializers/endoscope.rb`
27
+
28
+ ```ruby
29
+ require "endoscope"
30
+
31
+ process_name = if dyno = ENV['DYNO'] || ENV['PS']
32
+ Process.pid == 2 ? dyno : "#{dyno}-child-#{Process.pid}"
33
+ else
34
+ progname = $PROGRAM_NAME.gsub(Rails.root.to_s + '/', '')
35
+ "#{progname}:#{Process.pid}"
36
+ end
37
+
38
+ # using ENV['ENDOSCOPE_REDIS_URL'] and ENV['ENDOSCOPE_REDIS_NAMESPACE']
39
+ Endoscope::Agent.new(process_name).start
40
+ ```
41
+
42
+ You can also use a redis connection configuration as expected by the redis gem.
43
+
44
+ You can then use the Endoscope shell to interact with your app.
45
+ For example, on a running Heroku app comprising of web, sidekiq and
46
+ sidekiq-scheduler dynos:
47
+
48
+ ```
49
+ bundle exec endoscope
50
+ >> 40 + 2
51
+ 40 + 2
52
+ Sending command 40 + 2...
53
+
54
+ From web.1-child-6 :
55
+ 42
56
+
57
+ From web.2-child-9 :
58
+ 42
59
+
60
+ From web.1-child-9 :
61
+ 42
62
+
63
+ From worker.1 :
64
+ 42
65
+
66
+ From web.1-child-13 :
67
+ 42
68
+
69
+ From web.2-child-6 :
70
+ 42
71
+
72
+ From web.2-child-13 :
73
+ 42
74
+
75
+ From scheduler.1 :
76
+ 42
77
+
78
+ ```
79
+
80
+ You can direct your commands at select process types or processes via
81
+ the `use command`
82
+
83
+ ```
84
+ >> use web
85
+ Now adressing commands to processes listening for "web".
86
+ >> use worker.1
87
+ Now adressing commands to processes listening for "worker.1".
88
+ >> use all
89
+ Now adressing commands to processes listening for "all".
90
+ ```
91
+
92
+ ## Common uses
93
+ * retrieving garbage collection stats
94
+ * Taking thread dumps
95
+ * Re-open classes, redefining method to test changes / add logging without needing to commit and re-deploy
96
+ * adjust logging verbosity
97
+ * toggle features while debugging to narrow down the possible error causes
98
+
99
+
100
+ ## Notes
101
+
102
+ * The endoscope communication happens securely over Redis PubSub.
103
+ * The commands are evaluated in the top level binding of the instrumented program.
104
+ * stdout and stderr outputs are captured while the command is evaluated.
105
+ * Commands evaluation exceptions are caught and reported via the endoscope.
106
+ * Commands evaluation is protected by an execution timeout of 10 seconds.
107
+ * The agent starts a ruby thread with no overhead while not evaluating remote commands.
108
+
109
+ ## Contributing
110
+
111
+ 1. Fork it ( https://github.com/preplay/endoscope/fork )
112
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
113
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
114
+ 4. Push to the branch (`git push origin my-new-feature`)
115
+ 5. Create a new Pull Request
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/endoscope/cli"
4
+
5
+ if ARGV.count > 2
6
+ abort "endoscope [redis_url] [namespace] (or use ENDOSCOPE_REDIS_URL and/or ENDOSCOPE_REDIS_NAMESPACE ENV variables)"
7
+ end
8
+
9
+ redis_url, namespace = *ARGV
10
+ redis_url ||= ENV['ENDOSCOPE_REDIS_URL']
11
+ namespace ||= ENV['ENDOSCOPE_REDIS_NAMESPACE']
12
+ Endoscope::CLI.new({url: redis_url, namespace: namespace}).start
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/endoscope"
4
+
5
+ Endoscope::Agent.new("patient.1", {url: "redis://127.0.0.1:6379"}).start
6
+
7
+ module Patient
8
+ extend self
9
+
10
+ def live
11
+ say_hello
12
+ loop do
13
+ think
14
+ talk
15
+ sleep
16
+ end
17
+ end
18
+
19
+ def say_hello
20
+ puts "Hello from a sample instrumented process"
21
+ end
22
+
23
+ def think
24
+ 1000.times { 42 * 42 }
25
+ end
26
+
27
+ def talk
28
+ print "."
29
+ end
30
+
31
+ def sleep
32
+ Kernel.sleep 1
33
+ end
34
+ end
35
+
36
+ Patient.live
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'endoscope/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "endoscope"
8
+ spec.version = Endoscope::VERSION
9
+ spec.authors = ["Mathieu Ravaux"]
10
+ spec.email = ["mathieu.ravaux@gmail.com"]
11
+ spec.summary = "Remote shell for live interaction with Ruby processes"
12
+ spec.description = ""
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "redis"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.6"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "rspec"
26
+ end
@@ -0,0 +1,2 @@
1
+ require_relative "endoscope/version"
2
+ require_relative "endoscope/agent"
@@ -0,0 +1,93 @@
1
+ # TODO:
2
+ # * allow configuring the transport
3
+ # * allow configuring the namesapce
4
+
5
+ require_relative "transport"
6
+
7
+ require "timeout"
8
+ require "stringio"
9
+
10
+ module Endoscope
11
+ class Agent
12
+ ENDOSCOPE = "endoscope".freeze
13
+
14
+ attr_reader :dyno_name, :redis_options
15
+
16
+ def initialize(dyno_name, redis_options)
17
+ @dyno_name = dyno_name
18
+ @redis_options = redis_options || default_redis_options
19
+ end
20
+
21
+ def default_redis_options
22
+ {
23
+ url: ENV['ENDOSCOPE_REDIS_URL'] || 'redis://127.0.0.1:6379/',
24
+ namespace: ENV['ENDOSCOPE_REDIS_NAMESPACE']
25
+ }
26
+ end
27
+
28
+ def start
29
+ Thread.new(&method(:agent_listener))
30
+ end
31
+
32
+ def agent_listener
33
+ Thread.current[:name] = ENDOSCOPE
34
+ begin
35
+ wait_for_commands
36
+ rescue => e
37
+ puts e.inspect
38
+ puts e.backtrace.join("\n")
39
+ end
40
+ end
41
+
42
+ def wait_for_commands
43
+ transport = Transport.new(redis_options)
44
+ transport.wait_for_commands(dyno_name) do |command|
45
+ command_received(command)
46
+ end
47
+ rescue Transport::ConnectionError => error
48
+ puts "ns=endoscope at=wait_for_commands error=#{error} reconnect_in=1s"
49
+ sleep 1
50
+ retry
51
+ end
52
+
53
+ def command_received(command)
54
+ puts "ns=endoscope at=command_received"
55
+ to_eval = command.fetch('command')
56
+ result = evaluate(to_eval)
57
+ Transport.new(redis_options).publish_response(command, dyno_name, result)
58
+ end
59
+
60
+ EvalTimeout = Class.new(Timeout::Error)
61
+
62
+ def evaluate(ruby)
63
+ capture_streams do |out|
64
+ begin
65
+ Timeout.timeout(10, EvalTimeout) do
66
+ # rubocop:disable Eval
67
+ res = eval(ruby, TOPLEVEL_BINDING, 'remote_command')
68
+ # rubocop:enable Eval
69
+ out.puts res.inspect
70
+ end
71
+ rescue Exception => e
72
+ out.puts(e.inspect, *e.backtrace)
73
+ end
74
+ end
75
+ end
76
+
77
+ def capture_streams
78
+ $old_stdout = $stdout
79
+ $old_stderr = $stderr
80
+
81
+ out = StringIO.new
82
+ $stdout = out
83
+ $stderr = out
84
+ yield(out)
85
+
86
+ out.rewind
87
+ out.read
88
+ ensure
89
+ $stdout = $old_stdout
90
+ $stderr = $old_stderr
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,114 @@
1
+ require_relative "../endoscope"
2
+ require_relative "transport"
3
+
4
+ require "set"
5
+ require 'securerandom'
6
+
7
+ module Endoscope
8
+ class CLI
9
+ attr_accessor :dyno_selector, :issued, :transport_opts
10
+
11
+ def initialize(transport_opts=nil)
12
+ @transport_opts = transport_opts
13
+ end
14
+
15
+ def start(dyno_selector = 'all')
16
+ @dyno_selector = dyno_selector
17
+ @issued = Set.new
18
+ start_responses_printing_thread
19
+ transport
20
+ start_shell
21
+ end
22
+
23
+ def start_shell
24
+ begin
25
+ require "ripl"
26
+ require "ripl/ripper"
27
+ rescue LoadError => err
28
+ puts err.message
29
+ abort("\nYou need to run:\n\ngem install ripl ripl-ripper\n\nThen launch this command again.")
30
+ end
31
+
32
+ cli = self
33
+ Ripl::Shell.send(:define_method, :loop_eval) { |str| cli.eval_(str) }
34
+ Ripl::Shell.send(:define_method, :print_result) { |_result| }
35
+ Ripl.start( argv: [], irbrc: false, riplrc: false, ripper_prompt: ' | ')
36
+ end
37
+
38
+ def start_responses_printing_thread
39
+ @responses_thread = Thread.new(&method(:responses_printer))
40
+ end
41
+
42
+ def responses_printer
43
+ Thread.current[:name] = 'endoscope-responses-printing'
44
+ listen_to_command_responses
45
+ end
46
+
47
+ def listen_to_command_responses
48
+ transport = Transport.new(transport_opts)
49
+ transport.listen_to_responses do |response|
50
+ handle_response(response)
51
+ end
52
+ rescue Redis::TimeoutError => _
53
+ retry
54
+ rescue => err
55
+ puts err.inspect
56
+ puts err.backtrace.join("\n")
57
+ end
58
+
59
+ def handle_response(res)
60
+ #p res
61
+ #p issued
62
+ #p res['id']
63
+ return unless issued.include?(res['id'])
64
+ puts "From #{res['dyno_name']} :\n#{res['result']}\n\n"
65
+ $stdout.flush
66
+ end
67
+
68
+ def repl
69
+ catch(:break) do
70
+ puts "\n\n ---\nRemote console ready:\n\n"
71
+ $stdout.flush
72
+ loop { re($stdout) }
73
+ end
74
+ end
75
+
76
+ def re(out = $stdout)
77
+ command = read
78
+ eval_(command, out)
79
+ rescue Interrupt
80
+ throw(:break)
81
+ end
82
+
83
+ def eval_(command, out = $stdout)
84
+ case command
85
+ when 'exit'
86
+ throw(:break)
87
+ when /^use /
88
+ @dyno_selector = command.gsub('use ', '').strip
89
+ puts "Now adressing commands to processes listening for #{dyno_selector.inspect}."
90
+ else
91
+ send_command(command, out) unless command.nil? || command.strip == ''
92
+ end
93
+ end
94
+
95
+ def read
96
+ read = gets
97
+ read && read.chomp
98
+ end
99
+
100
+ def transport
101
+ @transport ||= begin
102
+ Transport.new(transport_opts)
103
+ end
104
+ end
105
+
106
+ def send_command(command, _out = $stdout)
107
+ puts "Sending command #{command}..."
108
+ command_id = SecureRandom.uuid
109
+ issued << command_id
110
+
111
+ transport.send_command(command_id, command, dyno_selector)
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,80 @@
1
+ require_relative "../endoscope"
2
+
3
+ require "redis"
4
+ require "json"
5
+
6
+ module Endoscope
7
+ class Transport
8
+ ConnectionError = Class.new(RuntimeError)
9
+
10
+ attr_reader :namespace, :redis_opts
11
+ def initialize(opts)
12
+ @namespace = opts.delete(:namespace) || "endoscope"
13
+ @redis_opts = opts
14
+ end
15
+
16
+ def wait_for_commands(dyno_name)
17
+ channels = command_channels(dyno_name)
18
+ connection.subscribe(*channels) do |on|
19
+ on.message do |_channel, message|
20
+ # puts "##{channel}: #{message}"
21
+ command = JSON.parse(message)
22
+ yield(command)
23
+ end
24
+ end
25
+ rescue Redis::BaseConnectionError => error
26
+ raise ConnectionError, error.message, error
27
+ end
28
+
29
+ def send_command(command_id, command, dyno_selector)
30
+ channel = requests_channel(dyno_selector)
31
+ connection.publish(channel, JSON.generate(
32
+ id: command_id,
33
+ command: command,
34
+ channel: channel
35
+ ))
36
+ end
37
+
38
+ def publish_response(command, dyno_name, result)
39
+ connection.publish(responses_channel, JSON.generate(
40
+ id: command.fetch('id'),
41
+ command: command.fetch('command'),
42
+ dyno_name: dyno_name,
43
+ result: result
44
+ ))
45
+ end
46
+
47
+ def listen_to_responses
48
+ connection.subscribe(responses_channel) do |on|
49
+ on.message do |_channel_name, message|
50
+ response = JSON.parse(message)
51
+ yield(response)
52
+ end
53
+ end
54
+
55
+ end
56
+
57
+ private
58
+
59
+ def connection
60
+ @connection ||= Redis.connect(redis_opts)
61
+ end
62
+
63
+ ALL = "all".freeze
64
+ def command_channels(dyno)
65
+ type = dyno.split('.', 2).first
66
+ [requests_channel(type), requests_channel(dyno), requests_channel(ALL)]
67
+ end
68
+
69
+ def requests_channel(selector)
70
+ "#{namespace}:requests:#{selector}"
71
+ end
72
+
73
+ def responses_channel
74
+ "#{namespace}:responses"
75
+ end
76
+
77
+ end
78
+ end
79
+
80
+
@@ -0,0 +1,3 @@
1
+ module Endoscope
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ require 'open3'
4
+
5
+ describe Endoscope do
6
+ it 'has a version number' do
7
+ expect(Endoscope::VERSION).not_to be nil
8
+ end
9
+
10
+ context "with a patient process running the Endoscope agent" do
11
+ let(:bin) { File.expand_path("../../bin", __FILE__) }
12
+
13
+ before do
14
+ @patient = IO.popen(File.join(bin, 'patient'))
15
+ end
16
+
17
+ after do
18
+ Process.kill "TERM", @patient.pid
19
+ end
20
+
21
+ it 'allows an endoscope process to evaluate ruby code inside of a patient process and show the result' do
22
+ q = Queue.new
23
+
24
+ Open3.popen2(File.join(bin, 'endoscope')) do |endo_in, endo_out, _endo_wait|
25
+ Thread.new { endo_out.each_line { |l| q.push l.chomp } }
26
+
27
+ endo_in.puts "$0"
28
+
29
+ Timeout.timeout(5) do
30
+ expect(q.pop).to eql ">> $0" # user input
31
+ expect(q.pop).to eql ">> Sending command $0..." # command confirmation
32
+ expect(q.pop).to eql "From patient.1 :" # response banner
33
+ expect(q.pop).to include "endoscope/bin/patient" # response contents
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'endoscope'
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: endoscope
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Mathieu Ravaux
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-06-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: redis
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.6'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: ''
70
+ email:
71
+ - mathieu.ravaux@gmail.com
72
+ executables:
73
+ - endoscope
74
+ - patient
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - ".gitignore"
79
+ - ".rspec"
80
+ - ".travis.yml"
81
+ - Gemfile
82
+ - LICENSE.txt
83
+ - README.md
84
+ - Rakefile
85
+ - bin/endoscope
86
+ - bin/patient
87
+ - endoscope.gemspec
88
+ - lib/endoscope.rb
89
+ - lib/endoscope/agent.rb
90
+ - lib/endoscope/cli.rb
91
+ - lib/endoscope/transport.rb
92
+ - lib/endoscope/version.rb
93
+ - spec/endoscope_spec.rb
94
+ - spec/spec_helper.rb
95
+ homepage: ''
96
+ licenses:
97
+ - MIT
98
+ metadata: {}
99
+ post_install_message:
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubyforge_project:
115
+ rubygems_version: 2.2.2
116
+ signing_key:
117
+ specification_version: 4
118
+ summary: Remote shell for live interaction with Ruby processes
119
+ test_files:
120
+ - spec/endoscope_spec.rb
121
+ - spec/spec_helper.rb
122
+ has_rdoc: