mind_control 0.1.0
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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +22 -0
- data/README.md +105 -0
- data/Rakefile +8 -0
- data/bin/mind_control +50 -0
- data/lib/mind_control/client.rb +42 -0
- data/lib/mind_control/cui.rb +52 -0
- data/lib/mind_control/em.rb +41 -0
- data/lib/mind_control/loggable.rb +47 -0
- data/lib/mind_control/pry_commands/capture_output.rb +99 -0
- data/lib/mind_control/pry_commands.rb +9 -0
- data/lib/mind_control/pry_monkey_patches.rb +16 -0
- data/lib/mind_control/repl.rb +103 -0
- data/lib/mind_control/server.rb +145 -0
- data/lib/mind_control/version.rb +4 -0
- data/lib/mind_control.rb +63 -0
- data/mind_control.gemspec +29 -0
- metadata +175 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9d3b0a0e916eeadf4ef8d8d7bf4ceafe05c45fcf
|
4
|
+
data.tar.gz: f93222fed2eb383935dfc31b8bdebbf5386652b4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dea7601b54a74ac38a745dbf604a537f079f87f16c011e5bb4890be00ce1d677e1d3bac34a1c40ed44bd9435af3cc6ce3bb8ef56a863e7ebf8ba15c9bbe9e038
|
7
|
+
data.tar.gz: 22ba438dfddb4cbac25c4192889a5491af89041e63bd22b4f85a3302f48457a72aafc7e76b5e556279aebb713343fe768a5a7195bd9b4a4ba401f25465520abb
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Denis Diachkov
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
# Mind Control
|
2
|
+
|
3
|
+
Embeddable runtime Pry-based REPL console for long-running programs.
|
4
|
+
|
5
|
+
Features:
|
6
|
+
|
7
|
+
- Executes code without interruption of the host program;
|
8
|
+
- Full fledged Pry console with code highlighting and completion;
|
9
|
+
- Allows multiple connections;
|
10
|
+
- EventMachine integration;
|
11
|
+
- Has very few dependencies (no DRb or EventMachine);
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem "mind_control"
|
19
|
+
```
|
20
|
+
|
21
|
+
## Requirements
|
22
|
+
|
23
|
+
- Ruby 1.9+;
|
24
|
+
- *NIX operating system (uses UNIX sockets);
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
To start console server:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
require "mind_control"
|
32
|
+
MindControl.start
|
33
|
+
```
|
34
|
+
|
35
|
+
You can also set Pry target (`something.pry`):
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
...
|
39
|
+
MindControl.start :target => something
|
40
|
+
```
|
41
|
+
|
42
|
+
Or Pry options:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
...
|
46
|
+
MindControl.start :pry => { .. options for pry instance .. }
|
47
|
+
```
|
48
|
+
|
49
|
+
Or set program name (see "Connection"):
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
...
|
53
|
+
MindControl.start :name => "some name"
|
54
|
+
```
|
55
|
+
|
56
|
+
### Connection
|
57
|
+
|
58
|
+
Run in terminal:
|
59
|
+
|
60
|
+
```console
|
61
|
+
$ bundle exec mind_control
|
62
|
+
```
|
63
|
+
|
64
|
+
You will be prompted with a list of currently running MindControlled processes.
|
65
|
+
|
66
|
+
Or, if you already know name or PID of process:
|
67
|
+
|
68
|
+
```console
|
69
|
+
$ bundle exec mind_control name_or_pid
|
70
|
+
```
|
71
|
+
|
72
|
+
### Capture output
|
73
|
+
|
74
|
+
You can capture STDOUT/STDERR of host program. To do that execute `capture-output` in REPL.
|
75
|
+
|
76
|
+
```text
|
77
|
+
[1] pry(main)> capture-output --help
|
78
|
+
|
79
|
+
Usage: capture_output [ --no-stdout | --no-stderr ] [ -f, --filter <regexp> ]
|
80
|
+
|
81
|
+
Captures host program STDOUT and STDERR and prints it to user.
|
82
|
+
|
83
|
+
--no-stdout Do not capture STDOUT.
|
84
|
+
--no-stderr Do not capture STDERR.
|
85
|
+
-f, --filter Filter output with given regular expression.
|
86
|
+
-h, --help Show this message.
|
87
|
+
```
|
88
|
+
|
89
|
+
### EventMachine
|
90
|
+
|
91
|
+
MindControl can be used with EventMachine. Just require file and set `EventMachine` as target and
|
92
|
+
all commands will be evaluated in the context of running reactor.
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
require "mind_control"
|
96
|
+
require "mind_control/em"
|
97
|
+
|
98
|
+
MindControl.start :target => EventMachine
|
99
|
+
```
|
100
|
+
|
101
|
+
## TODO
|
102
|
+
|
103
|
+
- Better readme;
|
104
|
+
- Tests;
|
105
|
+
- Get rid of Engrish;
|
data/Rakefile
ADDED
data/bin/mind_control
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
Signal.trap( :INT ) { exit }
|
3
|
+
|
4
|
+
# Add gem's lib dir to load path
|
5
|
+
$LOAD_PATH.unshift File.expand_path( "../../lib", __FILE__ )
|
6
|
+
|
7
|
+
require "slop"
|
8
|
+
require "mind_control"
|
9
|
+
|
10
|
+
# Parse options
|
11
|
+
begin
|
12
|
+
options = Slop.parse!( help: true ) do
|
13
|
+
banner "Usage: #{File.basename( $0 )} [socket name] [options]"
|
14
|
+
on "d", "sockets-dir=", "Set sockets search path."
|
15
|
+
end
|
16
|
+
rescue Slop::MissingArgumentError => e
|
17
|
+
abort e.message
|
18
|
+
end
|
19
|
+
|
20
|
+
# Console interface
|
21
|
+
cui = MindControl::CUI.new
|
22
|
+
|
23
|
+
# Get list of running MindControlled processes
|
24
|
+
running_processes =
|
25
|
+
MindControl::Client.get_running_processes( options[ "sockets-dir" ] || MindControl::DEFAULT_SOCKETS_DIR )
|
26
|
+
|
27
|
+
unless running_processes.any?
|
28
|
+
cui.show_error "No running processes found!"
|
29
|
+
exit
|
30
|
+
end
|
31
|
+
|
32
|
+
process =
|
33
|
+
# If filter given
|
34
|
+
if filter = ARGV[ 0 ]
|
35
|
+
# Filter processes by substring in name or pid equality
|
36
|
+
filtered_processes = running_processes.select! { |p| p.name.include?( filter ) || p.pid.to_s == filter.to_i }
|
37
|
+
|
38
|
+
# If filter matches 1 process -- connect to it, otherwise ask user to select process from filtered list
|
39
|
+
filtered_processes.size == 1 ? filtered_processes[ 0 ] : cui.select_process( filtered_processes )
|
40
|
+
else
|
41
|
+
# Ask user to select process
|
42
|
+
cui.select_process( running_processes )
|
43
|
+
end
|
44
|
+
|
45
|
+
begin
|
46
|
+
cui.show_debug "Connecting to #{process.name} via #{process.socket} ..."
|
47
|
+
MindControl::Client.connect( process )
|
48
|
+
rescue Exception => e
|
49
|
+
cui.show_error e.message
|
50
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "mind_control"
|
3
|
+
require "socket"
|
4
|
+
|
5
|
+
module MindControl
|
6
|
+
##
|
7
|
+
# MindControl client.
|
8
|
+
#
|
9
|
+
module Client
|
10
|
+
extend self
|
11
|
+
|
12
|
+
# Running process struct.
|
13
|
+
Process = Struct.new( :name, :pid, :socket )
|
14
|
+
|
15
|
+
##
|
16
|
+
# Returns running processes.
|
17
|
+
#
|
18
|
+
# @param [String] sockets_dir Directory with MindControl sockets.
|
19
|
+
# @return [Array<MindControl::Client::Process>]
|
20
|
+
#
|
21
|
+
def get_running_processes( sockets_dir )
|
22
|
+
Dir.glob( File.join( sockets_dir, "*.sock" )).map do |file|
|
23
|
+
name, pid = File.basename( file, ".sock" ).split( "." )
|
24
|
+
Process.new( name, pid, file )
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# Connect to given process.
|
30
|
+
# @param [MindControl::Client::Process] process Process to connect to.
|
31
|
+
#
|
32
|
+
def connect( process )
|
33
|
+
UNIXSocket.open( process.socket ) do |socket|
|
34
|
+
socket.send_io STDIN
|
35
|
+
socket.send_io STDOUT
|
36
|
+
|
37
|
+
# Wait for disconnect
|
38
|
+
socket.recv( 0 )
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "highline"
|
3
|
+
|
4
|
+
module MindControl
|
5
|
+
##
|
6
|
+
# Console User Interface.
|
7
|
+
#
|
8
|
+
class CUI
|
9
|
+
attr_reader :highline
|
10
|
+
|
11
|
+
##
|
12
|
+
# @param [IO] stdin (STDIN) Console input.
|
13
|
+
# @param [IO] stdout (STDOUT) Console output.
|
14
|
+
#
|
15
|
+
def initialize( stdin = STDIN, stdout = STDOUT )
|
16
|
+
@highline = ::HighLine.new( stdin, stdout )
|
17
|
+
end
|
18
|
+
|
19
|
+
##
|
20
|
+
# Ask user to select process from list.
|
21
|
+
#
|
22
|
+
# @param [Array<MindControl::Client::Process>] process_list
|
23
|
+
# @return [MindControl::Client::Process]
|
24
|
+
#
|
25
|
+
def select_process( process_list )
|
26
|
+
highline.choose do |menu|
|
27
|
+
menu.header = "Select process"
|
28
|
+
menu.select_by = :index
|
29
|
+
|
30
|
+
process_list.each do |process|
|
31
|
+
menu.choice( "#{process.name} (PID: #{process.pid})" ) { process }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# Show debug message.
|
38
|
+
# @param [String] message
|
39
|
+
#
|
40
|
+
def show_debug( message )
|
41
|
+
highline.say HighLine::String.new( message ).white
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# Show error message.
|
46
|
+
# @param [String] message
|
47
|
+
#
|
48
|
+
def show_error( message )
|
49
|
+
highline.say HighLine::String.new( message ).red
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "thread"
|
3
|
+
|
4
|
+
module EventMachine
|
5
|
+
##
|
6
|
+
# Context for Pry.
|
7
|
+
#
|
8
|
+
def self.__binding__
|
9
|
+
@binding ||= EventMachine.send( :binding ).tap do |binding|
|
10
|
+
orig_eval = binding.method( :eval )
|
11
|
+
|
12
|
+
##
|
13
|
+
# Eval code in reactor context and return result.
|
14
|
+
#
|
15
|
+
binding.define_singleton_method :eval do |*args, &block|
|
16
|
+
raise "Reactor not running!" unless EventMachine.reactor_running?
|
17
|
+
|
18
|
+
# Channel between our and reactor threads
|
19
|
+
queue = ::Queue.new
|
20
|
+
|
21
|
+
# In reactor context
|
22
|
+
EventMachine::next_tick do
|
23
|
+
# In case of fibered code we should create Fiber
|
24
|
+
Fiber.new {
|
25
|
+
begin
|
26
|
+
queue.push orig_eval.call( *args, &block )
|
27
|
+
rescue Exception => e
|
28
|
+
# Return errors too
|
29
|
+
queue.push e
|
30
|
+
end
|
31
|
+
}.resume
|
32
|
+
end
|
33
|
+
|
34
|
+
# Wait for result
|
35
|
+
return queue.pop.tap do |result|
|
36
|
+
raise result if result.is_a?( Exception )
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "logger"
|
3
|
+
|
4
|
+
module MindControl
|
5
|
+
##
|
6
|
+
# Mixin for event logging.
|
7
|
+
#
|
8
|
+
module Loggable
|
9
|
+
|
10
|
+
#
|
11
|
+
# Add all logging methods from standard Logger class.
|
12
|
+
#
|
13
|
+
|
14
|
+
{ :debug => Logger::Severity::DEBUG,
|
15
|
+
:info => Logger::Severity::INFO,
|
16
|
+
:warn => Logger::Severity::WARN,
|
17
|
+
:error => Logger::Severity::ERROR,
|
18
|
+
:fatal => Logger::Severity::FATAL }.each do |name, severity|
|
19
|
+
|
20
|
+
##
|
21
|
+
# @param [String] message Event text.
|
22
|
+
#
|
23
|
+
define_method name do |message = nil, &block|
|
24
|
+
raise ArgumentError, "block is missing" if !message && !block
|
25
|
+
logger.add severity, message, facility, &block if logger # NB: log ONLY if logger is set
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
##
|
32
|
+
# Returns global logger.
|
33
|
+
# @return [Logger, nil]
|
34
|
+
#
|
35
|
+
def logger
|
36
|
+
MindControl.logger
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Returns log facility for current class: demodulized snake_cased class name.
|
41
|
+
# @return [String]
|
42
|
+
#
|
43
|
+
def facility
|
44
|
+
@facility ||= self.class.name.split( "::" ).last.gsub( /([a-z])([A-Z])/, "\\1_\\2" ).downcase
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "thread"
|
3
|
+
|
4
|
+
MindControl::PryCommands.create_command "capture-output" do
|
5
|
+
description "Captures host program STDOUT and STDERR."
|
6
|
+
|
7
|
+
banner <<-BANNER
|
8
|
+
Usage: capture_output [ --no-stdout | --no-stderr ] [ -f, --filter <regexp> ]
|
9
|
+
|
10
|
+
Captures host program STDOUT and STDERR and prints it to user.
|
11
|
+
BANNER
|
12
|
+
|
13
|
+
command_options :shellwords => false
|
14
|
+
|
15
|
+
def options( opt )
|
16
|
+
opt.on :"no-stdout", "Do not capture STDOUT."
|
17
|
+
opt.on :"no-stderr", "Do not capture STDERR."
|
18
|
+
opt.on :f, :filter=, "Filter output with given regular expression."
|
19
|
+
end
|
20
|
+
|
21
|
+
def process
|
22
|
+
raise CommandError, "You can't use --no-stdout simultaneously with --no-stderr" \
|
23
|
+
if opts[ :"no-stdout" ] && opts[ :"no-stderr" ]
|
24
|
+
|
25
|
+
# Line filter
|
26
|
+
filter = opts[ :filter ] ? Regexp.new( opts[ :filter ]) : nil
|
27
|
+
|
28
|
+
line_buffer = Hash.new
|
29
|
+
|
30
|
+
capture_output do |kind, string|
|
31
|
+
# Ignoring user specified outputs
|
32
|
+
next if kind == :stdout && opts[ :"no-stdout" ]
|
33
|
+
next if kind == :stderr && opts[ :"no-stderr" ]
|
34
|
+
|
35
|
+
# Buffering input
|
36
|
+
buffer = line_buffer[ kind ] ||= ""
|
37
|
+
buffer << string
|
38
|
+
|
39
|
+
# Print buffered lines
|
40
|
+
while eol = buffer.index( "\n" )
|
41
|
+
line = buffer.slice!( 0 .. eol )
|
42
|
+
|
43
|
+
next if filter && line !~ filter
|
44
|
+
|
45
|
+
# Display STDERR in red
|
46
|
+
output.write "\e[31m" if kind == :stderr
|
47
|
+
output.write line
|
48
|
+
|
49
|
+
# We work in raw mode and we need manually move carret to next line
|
50
|
+
output.write "\e[0m\e[E"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Capture writes to STDOUT/STDERR and yield it.
|
57
|
+
#
|
58
|
+
def capture_output( &block )
|
59
|
+
output_queue = Queue.new
|
60
|
+
|
61
|
+
# Save original write implementation
|
62
|
+
orig_stdout_write = STDOUT.method( :write )
|
63
|
+
orig_stderr_write = STDERR.method( :write )
|
64
|
+
|
65
|
+
#
|
66
|
+
# Hijack #write method and push input string to queue.
|
67
|
+
#
|
68
|
+
|
69
|
+
STDOUT.define_singleton_method :write do |string|
|
70
|
+
orig_stdout_write.call( string ).tap do
|
71
|
+
output_queue << [ :stdout, string ]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
STDERR.define_singleton_method :write do |string|
|
76
|
+
orig_stderr_write.call( string ).tap do
|
77
|
+
output_queue << [ :stderr, string ]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Separate thread to push strings to block in background.
|
82
|
+
capture_thread = Thread.new {
|
83
|
+
loop do
|
84
|
+
block.call( *output_queue.pop )
|
85
|
+
end
|
86
|
+
}
|
87
|
+
|
88
|
+
# Wait for Ctrl+c
|
89
|
+
loop do
|
90
|
+
break if _pry_.input.getch == ?\C-c
|
91
|
+
end
|
92
|
+
ensure
|
93
|
+
capture_thread.kill
|
94
|
+
|
95
|
+
# Restore original write implementation.
|
96
|
+
STDOUT.define_singleton_method :write, orig_stdout_write
|
97
|
+
STDERR.define_singleton_method :write, orig_stderr_write
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "pry/pager"
|
3
|
+
|
4
|
+
::Pry::Pager.instance_eval do
|
5
|
+
alias :vanilla_page :page
|
6
|
+
|
7
|
+
# Redefine standard paging method, so it will always write text to current MindControl
|
8
|
+
# output instead of calling system pager or writing to host program $stdout.
|
9
|
+
def self.page( text, pager = nil )
|
10
|
+
if pry_instance = Pry.current[ :mind_control_pry_instance ]
|
11
|
+
pry_instance.output.puts( text )
|
12
|
+
else
|
13
|
+
vanilla_page( text, pager )
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "pry"
|
3
|
+
require "coolline"
|
4
|
+
require "mind_control/pry_monkey_patches"
|
5
|
+
require "mind_control/pry_commands"
|
6
|
+
|
7
|
+
module MindControl
|
8
|
+
##
|
9
|
+
# Pry based REPL.
|
10
|
+
#
|
11
|
+
class REPL
|
12
|
+
##
|
13
|
+
# @param [Object, Proc] target The receiver of the Pry session.
|
14
|
+
# @param [Hash] options The optional configuration parameters for Pry.
|
15
|
+
#
|
16
|
+
def initialize( target, options = {} )
|
17
|
+
@target = target
|
18
|
+
@options = options
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# Start REPL session.
|
23
|
+
#
|
24
|
+
# @param [IO] stdin The object to use for input.
|
25
|
+
# @param [IO] stdout The object to use for output.
|
26
|
+
#
|
27
|
+
def start( stdin, stdout )
|
28
|
+
# NB: We cannot use Readline, because it always uses STDOUT / STDIN.
|
29
|
+
input = CoollineAdapter.new( stdin, stdout )
|
30
|
+
|
31
|
+
# Default command set
|
32
|
+
commands = @options[ :commands ] || ::Pry::CommandSet.new.import( ::Pry::Commands )
|
33
|
+
|
34
|
+
# Import our MindControl commands
|
35
|
+
commands.import MindControl::PryCommands
|
36
|
+
|
37
|
+
# Target can be callable
|
38
|
+
target = @target.respond_to?( :call ) ? @target.call : @target
|
39
|
+
|
40
|
+
# NB: input/input can't be changed via pry options!
|
41
|
+
pry = ::Pry.new @options.merge( :commands => commands, :input => input, :output => stdout )
|
42
|
+
|
43
|
+
# Store pry instance in thread-local context so that we can later determine
|
44
|
+
# whether we are running inside MindControl session or not.
|
45
|
+
::Pry.current[ :mind_control_pry_instance ] = pry
|
46
|
+
|
47
|
+
# Start session
|
48
|
+
pry.repl target
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Adapter for Coolline for use with Pry.
|
53
|
+
#
|
54
|
+
class CoollineAdapter
|
55
|
+
attr_reader :cool
|
56
|
+
|
57
|
+
##
|
58
|
+
# @param [IO] stdin The object to use for input.
|
59
|
+
# @param [IO] stdout The object to use for output.
|
60
|
+
#
|
61
|
+
def initialize( input, output )
|
62
|
+
# Setup Coolline
|
63
|
+
@cool = Coolline.new do |cool|
|
64
|
+
cool.input = input
|
65
|
+
cool.output = output
|
66
|
+
cool.word_boundaries = [ " ", "\t", ",", ";", '"', "'", "`", "<", ">", "=", ";", "|", "{", "}", "(", ")", "-" ]
|
67
|
+
|
68
|
+
# By default Coolline will kill host program on Ctrl+c. Override it.
|
69
|
+
ctrl_c_handler = cool.handlers.find { |handler| handler.char == ?\C-c }
|
70
|
+
ctrl_c_handler.block = lambda { |instance|
|
71
|
+
# Just close Pry
|
72
|
+
instance.instance_variable_set "@should_exit", true
|
73
|
+
}
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# Read user input with given prompt.
|
79
|
+
#
|
80
|
+
# @param [String] prompt
|
81
|
+
# @return [String]
|
82
|
+
#
|
83
|
+
def readline( prompt )
|
84
|
+
cool.readline prompt
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Read char from input in raw mode.#
|
89
|
+
#
|
90
|
+
# @return [Fixnum]
|
91
|
+
#
|
92
|
+
def getch
|
93
|
+
cool.input.getch
|
94
|
+
end
|
95
|
+
|
96
|
+
def completion_proc=( proc )
|
97
|
+
cool.completion_proc = proc do
|
98
|
+
proc.call cool.completed_word
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "mind_control/repl"
|
3
|
+
require "mind_control/loggable"
|
4
|
+
require "socket"
|
5
|
+
require "thread"
|
6
|
+
require "fileutils"
|
7
|
+
require "etc"
|
8
|
+
|
9
|
+
module MindControl
|
10
|
+
##
|
11
|
+
# Listens UNIX socket and starts REPL sessions.
|
12
|
+
#
|
13
|
+
class Server
|
14
|
+
include Loggable
|
15
|
+
|
16
|
+
attr_reader :socket_path
|
17
|
+
attr_reader :repl
|
18
|
+
|
19
|
+
##
|
20
|
+
# @param [String] socket_path Absolute path of UNIX socket.
|
21
|
+
# @param [#start] repl Instance of REPL (@see MindControl::REPL).
|
22
|
+
#
|
23
|
+
def initialize( socket_path, repl )
|
24
|
+
@socket_path, @repl = socket_path, repl
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# Start server.
|
29
|
+
#
|
30
|
+
def start
|
31
|
+
return if running?
|
32
|
+
|
33
|
+
info "Starting MindControl server on #{socket_path} ..."
|
34
|
+
|
35
|
+
# Storage for client threads
|
36
|
+
client_threads = ThreadGroup.new
|
37
|
+
|
38
|
+
# Start acceptor thread
|
39
|
+
@server_thread = Thread.new do
|
40
|
+
begin
|
41
|
+
start_server_loop( socket_path ) do |client_socket|
|
42
|
+
# Process client in new thread
|
43
|
+
client_threads.add Thread.new {
|
44
|
+
begin
|
45
|
+
handle_client_connection( client_socket, get_client_id( client_socket ))
|
46
|
+
ensure
|
47
|
+
# We MUST close the socket
|
48
|
+
client_socket.close
|
49
|
+
end
|
50
|
+
}
|
51
|
+
end
|
52
|
+
ensure
|
53
|
+
# Kill all client threads on server stop
|
54
|
+
client_threads.list.each( &:kill )
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# We should known if our server failed
|
59
|
+
@server_thread.abort_on_exception = true
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Stop server.
|
64
|
+
#
|
65
|
+
def stop
|
66
|
+
return unless running?
|
67
|
+
|
68
|
+
info "Stopping MindControl server ..."
|
69
|
+
|
70
|
+
# Kill acceptor thread
|
71
|
+
@server_thread.kill
|
72
|
+
@server_thread.join
|
73
|
+
@server_thread = nil
|
74
|
+
end
|
75
|
+
|
76
|
+
##
|
77
|
+
# Server is running?
|
78
|
+
#
|
79
|
+
def running?
|
80
|
+
@server_thread && @server_thread.alive?
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# Starts UNIX server, accepts clients and yields its sockets in loop.
|
85
|
+
#
|
86
|
+
# @param [String] socket_path Path to UNIX socket to bind to.
|
87
|
+
# @yeilds [UNIXSocket]
|
88
|
+
#
|
89
|
+
def start_server_loop( socket_path, &block )
|
90
|
+
# Remove old file
|
91
|
+
FileUtils.rm_f socket_path
|
92
|
+
FileUtils.mkdir_p File.dirname( socket_path )
|
93
|
+
|
94
|
+
UNIXServer.open( socket_path ) do |server|
|
95
|
+
loop do
|
96
|
+
# Wait for client
|
97
|
+
block.call server.accept
|
98
|
+
end
|
99
|
+
end
|
100
|
+
ensure
|
101
|
+
# Cleanup
|
102
|
+
FileUtils.rm socket_path
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Return id string for connected client.
|
107
|
+
#
|
108
|
+
# @param [UNIXSocket] socket Client socket.
|
109
|
+
# @return [String]
|
110
|
+
#
|
111
|
+
def get_client_id( socket )
|
112
|
+
# UNIX socket return effective UID/GID for connected client
|
113
|
+
euid, _ = socket.getpeereid
|
114
|
+
|
115
|
+
# Find record in /etc/passwd
|
116
|
+
user_info = Etc.getpwuid euid
|
117
|
+
|
118
|
+
return "#{user_info.name} (#{user_info.gecos})"
|
119
|
+
end
|
120
|
+
|
121
|
+
##
|
122
|
+
# Starts REPL session in separate thread for connected client.
|
123
|
+
#
|
124
|
+
# @param [Socket] socket Client socket.
|
125
|
+
# @param [String] client_id ID string for connected client.
|
126
|
+
#
|
127
|
+
def handle_client_connection( socket, client_id )
|
128
|
+
info "Starting new MindControl session for user #{client_id} ..."
|
129
|
+
|
130
|
+
# Client will send us his STDIN and STDOUT
|
131
|
+
client_stdin, client_stdout = socket.recv_io, socket.recv_io
|
132
|
+
|
133
|
+
# Start REPL
|
134
|
+
repl.start client_stdin, client_stdout
|
135
|
+
|
136
|
+
info "MindControl session for user #{client_id} has ended!"
|
137
|
+
rescue Exception => e
|
138
|
+
error_message = "REPL exception: #{e.message} (#{e.class.name})\n#{e.backtrace.join( "\n" )}"
|
139
|
+
error error_message
|
140
|
+
|
141
|
+
# Send error to client
|
142
|
+
client_stdout.puts error_message if client_stdout
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
data/lib/mind_control.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module MindControl
|
3
|
+
autoload :REPL, "mind_control/repl"
|
4
|
+
autoload :Server, "mind_control/server"
|
5
|
+
autoload :Client, "mind_control/client"
|
6
|
+
autoload :CUI, "mind_control/cui"
|
7
|
+
|
8
|
+
# Default directory for UNIX socket files.
|
9
|
+
DEFAULT_SOCKETS_DIR = "/tmp/ruby_mind_control"
|
10
|
+
|
11
|
+
# Global logger.
|
12
|
+
class << self; attr_accessor :logger; end
|
13
|
+
|
14
|
+
##
|
15
|
+
# Start MindControl server.
|
16
|
+
#
|
17
|
+
# @param [Hash] options
|
18
|
+
# @option options [Object] :target (TOPLEVEL_BINDING)
|
19
|
+
# REPL target context.
|
20
|
+
#
|
21
|
+
# @option option [Hash] :pry ({})
|
22
|
+
# Options for Pry instance.
|
23
|
+
#
|
24
|
+
# @option options [String] :name ($PROGRAM_NAME)
|
25
|
+
# Program name.
|
26
|
+
#
|
27
|
+
# @option options [String] :sockets_dir (DEFAULT_SOCKETS_DIR)
|
28
|
+
# Directory where control socket will be created.
|
29
|
+
#
|
30
|
+
def self.start( options = {} )
|
31
|
+
raise "MindControl already started!" if @server && @server.running?
|
32
|
+
|
33
|
+
# Name that will be displayed in process list of mind-control client.
|
34
|
+
# The default is process name of the host program (eg. "ruby").
|
35
|
+
process_name = options[ :name ] || $PROGRAM_NAME
|
36
|
+
|
37
|
+
# Some shared temp directory for sockets.
|
38
|
+
socket_dir = options[ :sockets_dir ] || DEFAULT_SOCKETS_DIR
|
39
|
+
|
40
|
+
# Construct unique socket path for current process
|
41
|
+
socket_name = "#{process_name}.#{Process.pid}.sock"
|
42
|
+
socket_path = File.join( socket_dir, socket_name )
|
43
|
+
|
44
|
+
# Construct REPL (NB: same settings for all connections!)
|
45
|
+
repl = MindControl::REPL.new( options[ :target ] || TOPLEVEL_BINDING, options[ :pry ] || {} )
|
46
|
+
|
47
|
+
# Start server
|
48
|
+
@server = MindControl::Server.new( socket_path, repl )
|
49
|
+
@server.start
|
50
|
+
|
51
|
+
return nil
|
52
|
+
end
|
53
|
+
|
54
|
+
##
|
55
|
+
# Stop MindControl server.
|
56
|
+
#
|
57
|
+
def self.stop
|
58
|
+
return unless @server
|
59
|
+
|
60
|
+
@server.stop
|
61
|
+
@server = nil
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
lib = File.expand_path( "../lib", __FILE__ )
|
3
|
+
$LOAD_PATH.unshift( lib ) unless $LOAD_PATH.include? lib
|
4
|
+
require "mind_control/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "mind_control"
|
8
|
+
spec.version = MindControl::VERSION
|
9
|
+
spec.authors = [ "Denis Diachkov" ]
|
10
|
+
spec.email = [ "d.diachkov@gmail.com" ]
|
11
|
+
spec.summary = "Embeddable runtime Pry-based REPL console"
|
12
|
+
spec.homepage = "https://github.com/ddiachkov/mind_control"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files`.split( $/ )
|
16
|
+
spec.executables = spec.files.grep( %r{^bin/} ) { |f| File.basename f }
|
17
|
+
spec.test_files = spec.files.grep( %r{^(test|spec|features)/} )
|
18
|
+
spec.require_paths = [ "lib" ]
|
19
|
+
|
20
|
+
spec.add_dependency "highline", "~> 1.6"
|
21
|
+
spec.add_dependency "pry", "~> 0.9"
|
22
|
+
spec.add_dependency "coolline", "~> 0.4"
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
25
|
+
spec.add_development_dependency "rake"
|
26
|
+
spec.add_development_dependency "minitest"
|
27
|
+
spec.add_development_dependency "mocha"
|
28
|
+
spec.add_development_dependency "eventmachine"
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mind_control
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Denis Diachkov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-06-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: highline
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.6'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.6'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pry
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.9'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.9'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: coolline
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.4'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.4'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.3'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.3'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: minitest
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: mocha
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: eventmachine
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description:
|
126
|
+
email:
|
127
|
+
- d.diachkov@gmail.com
|
128
|
+
executables:
|
129
|
+
- mind_control
|
130
|
+
extensions: []
|
131
|
+
extra_rdoc_files: []
|
132
|
+
files:
|
133
|
+
- .gitignore
|
134
|
+
- Gemfile
|
135
|
+
- LICENSE.txt
|
136
|
+
- README.md
|
137
|
+
- Rakefile
|
138
|
+
- bin/mind_control
|
139
|
+
- lib/mind_control.rb
|
140
|
+
- lib/mind_control/client.rb
|
141
|
+
- lib/mind_control/cui.rb
|
142
|
+
- lib/mind_control/em.rb
|
143
|
+
- lib/mind_control/loggable.rb
|
144
|
+
- lib/mind_control/pry_commands.rb
|
145
|
+
- lib/mind_control/pry_commands/capture_output.rb
|
146
|
+
- lib/mind_control/pry_monkey_patches.rb
|
147
|
+
- lib/mind_control/repl.rb
|
148
|
+
- lib/mind_control/server.rb
|
149
|
+
- lib/mind_control/version.rb
|
150
|
+
- mind_control.gemspec
|
151
|
+
homepage: https://github.com/ddiachkov/mind_control
|
152
|
+
licenses:
|
153
|
+
- MIT
|
154
|
+
metadata: {}
|
155
|
+
post_install_message:
|
156
|
+
rdoc_options: []
|
157
|
+
require_paths:
|
158
|
+
- lib
|
159
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
160
|
+
requirements:
|
161
|
+
- - '>='
|
162
|
+
- !ruby/object:Gem::Version
|
163
|
+
version: '0'
|
164
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
165
|
+
requirements:
|
166
|
+
- - '>='
|
167
|
+
- !ruby/object:Gem::Version
|
168
|
+
version: '0'
|
169
|
+
requirements: []
|
170
|
+
rubyforge_project:
|
171
|
+
rubygems_version: 2.0.0
|
172
|
+
signing_key:
|
173
|
+
specification_version: 4
|
174
|
+
summary: Embeddable runtime Pry-based REPL console
|
175
|
+
test_files: []
|