endoscope 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: