remote_ruby 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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +4 -0
- data/.rubocop.yml +3 -0
- data/.travis.yml +12 -0
- data/Gemfile +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +374 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/remote_ruby/code_templates/compiler/main.rb.erb +41 -0
- data/lib/remote_ruby/compiler.rb +55 -0
- data/lib/remote_ruby/connection_adapter/cache_adapter.rb +38 -0
- data/lib/remote_ruby/connection_adapter/caching_adapter.rb +47 -0
- data/lib/remote_ruby/connection_adapter/eval_adapter.rb +97 -0
- data/lib/remote_ruby/connection_adapter/local_stdin_adapter.rb +21 -0
- data/lib/remote_ruby/connection_adapter/ssh_stdin_adapter.rb +25 -0
- data/lib/remote_ruby/connection_adapter/stdin_process_adapter.rb +33 -0
- data/lib/remote_ruby/connection_adapter.rb +28 -0
- data/lib/remote_ruby/execution_context.rb +134 -0
- data/lib/remote_ruby/flavour/rails_flavour.rb +19 -0
- data/lib/remote_ruby/flavour.rb +25 -0
- data/lib/remote_ruby/locals_extractor.rb +41 -0
- data/lib/remote_ruby/runner.rb +53 -0
- data/lib/remote_ruby/source_extractor.rb +39 -0
- data/lib/remote_ruby/stream_cacher.rb +34 -0
- data/lib/remote_ruby/unmarshaler.rb +58 -0
- data/lib/remote_ruby/version.rb +3 -0
- data/lib/remote_ruby.rb +25 -0
- data/remote_ruby.gemspec +29 -0
- metadata +131 -0
@@ -0,0 +1,97 @@
|
|
1
|
+
module RemoteRuby
|
2
|
+
# An adapter to expecute Ruby code in the current process in an isolated
|
3
|
+
# scope
|
4
|
+
class EvalAdapter < ConnectionAdapter
|
5
|
+
attr_reader :async, :working_dir
|
6
|
+
|
7
|
+
def initialize(working_dir: Dir.pwd, async: false)
|
8
|
+
@async = async
|
9
|
+
@working_dir = working_dir
|
10
|
+
end
|
11
|
+
|
12
|
+
def connection_name
|
13
|
+
''
|
14
|
+
end
|
15
|
+
|
16
|
+
def open(code)
|
17
|
+
if async
|
18
|
+
run_async(code) do |out, err|
|
19
|
+
yield out, err
|
20
|
+
end
|
21
|
+
else
|
22
|
+
run_sync(code) do |out, err|
|
23
|
+
yield out, err
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def run_code(code)
|
31
|
+
binder = Object.new
|
32
|
+
|
33
|
+
Dir.chdir(working_dir) do
|
34
|
+
binder.instance_eval(code)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def run_sync(code)
|
39
|
+
with_stringio do |tmp_stdout, tmp_stderr|
|
40
|
+
with_tmp_streams(tmp_stdout, tmp_stderr) do
|
41
|
+
run_code(code)
|
42
|
+
end
|
43
|
+
|
44
|
+
tmp_stdout.close_write
|
45
|
+
tmp_stderr.close_write
|
46
|
+
tmp_stdout.rewind
|
47
|
+
tmp_stderr.rewind
|
48
|
+
yield tmp_stdout, tmp_stderr
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def run_async(code)
|
53
|
+
with_pipes do |out_read, out_write, err_read, err_write|
|
54
|
+
Thread.new do
|
55
|
+
with_tmp_streams(out_write, err_write) do
|
56
|
+
run_code(code)
|
57
|
+
end
|
58
|
+
|
59
|
+
out_write.close
|
60
|
+
err_write.close
|
61
|
+
end
|
62
|
+
|
63
|
+
yield out_read, err_read
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def with_pipes
|
68
|
+
out_read, out_write = IO.pipe
|
69
|
+
err_read, err_write = IO.pipe
|
70
|
+
yield out_read, out_write, err_read, err_write
|
71
|
+
ensure
|
72
|
+
out_read.close
|
73
|
+
err_read.close
|
74
|
+
end
|
75
|
+
|
76
|
+
def with_stringio
|
77
|
+
out = StringIO.new
|
78
|
+
err = StringIO.new
|
79
|
+
|
80
|
+
yield out, err
|
81
|
+
ensure
|
82
|
+
out.close
|
83
|
+
err.close
|
84
|
+
end
|
85
|
+
|
86
|
+
def with_tmp_streams(out, err)
|
87
|
+
old_stdout = $stdout
|
88
|
+
old_stderr = $stderr
|
89
|
+
$stdout = out
|
90
|
+
$stderr = err
|
91
|
+
yield
|
92
|
+
ensure
|
93
|
+
$stdout = old_stdout
|
94
|
+
$stderr = old_stderr
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module RemoteRuby
|
2
|
+
# An adapter to expecute Ruby code on the local macine
|
3
|
+
# inside a specified directory
|
4
|
+
class LocalStdinAdapter < ::RemoteRuby::StdinProcessAdapter
|
5
|
+
attr_reader :working_dir
|
6
|
+
|
7
|
+
def initialize(working_dir: '.')
|
8
|
+
@working_dir = working_dir
|
9
|
+
end
|
10
|
+
|
11
|
+
def connection_name
|
12
|
+
working_dir
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def command
|
18
|
+
"cd \"#{working_dir}\" && ruby"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module RemoteRuby
|
2
|
+
# An adapter to execute Ruby code on the remote server via SSH
|
3
|
+
class SSHStdinAdapter < StdinProcessAdapter
|
4
|
+
attr_reader :server, :working_dir, :user, :key_file
|
5
|
+
|
6
|
+
def initialize(server:, working_dir: '~', user: nil, key_file: nil)
|
7
|
+
@working_dir = working_dir
|
8
|
+
@server = user.nil? ? server : "#{user}@#{server}"
|
9
|
+
@user = user
|
10
|
+
@key_file = key_file
|
11
|
+
end
|
12
|
+
|
13
|
+
def connection_name
|
14
|
+
"#{server}:#{working_dir}"
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def command
|
20
|
+
command = 'ssh'
|
21
|
+
command = "#{command} -i #{key_file}" if key_file
|
22
|
+
"#{command} #{server} \"cd #{working_dir} && ruby\""
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module RemoteRuby
|
4
|
+
# Base class for adapters which launch an external process to execute
|
5
|
+
# Ruby code and send the code to its standard input.
|
6
|
+
class StdinProcessAdapter < ::RemoteRuby::ConnectionAdapter
|
7
|
+
include Open3
|
8
|
+
|
9
|
+
def open(code)
|
10
|
+
result = nil
|
11
|
+
|
12
|
+
popen3(command) do |stdin, stdout, stderr, wait_thr|
|
13
|
+
stdin.write(code)
|
14
|
+
stdin.close
|
15
|
+
|
16
|
+
yield stdout, stderr
|
17
|
+
|
18
|
+
result = wait_thr.value
|
19
|
+
end
|
20
|
+
|
21
|
+
return if result.success?
|
22
|
+
|
23
|
+
raise "Remote connection exited with code #{result}"
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
# Command to run an external process. Override in a child class.
|
29
|
+
def command
|
30
|
+
raise NotImplementedError
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module RemoteRuby
|
2
|
+
# Base class for other connection adapters.
|
3
|
+
class ConnectionAdapter
|
4
|
+
# Initializers of adapters should receive only keyword arguments.
|
5
|
+
# May be overriden in a child class.
|
6
|
+
def initialize(**args); end
|
7
|
+
|
8
|
+
# This will be displayed as a prefix when adapter writes something to
|
9
|
+
# emulated standard output or standard error. May be overriden in a child
|
10
|
+
# class.
|
11
|
+
def connection_name
|
12
|
+
self.class.name
|
13
|
+
end
|
14
|
+
|
15
|
+
# Override in child class. Receives Ruby code as string and yields
|
16
|
+
# two readable streams: for emulated standard output and standard error.
|
17
|
+
def open(_code)
|
18
|
+
raise NotImplementedError
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'remote_ruby/connection_adapter/eval_adapter.rb'
|
24
|
+
require 'remote_ruby/connection_adapter/stdin_process_adapter'
|
25
|
+
require 'remote_ruby/connection_adapter/ssh_stdin_adapter'
|
26
|
+
require 'remote_ruby/connection_adapter/local_stdin_adapter'
|
27
|
+
require 'remote_ruby/connection_adapter/cache_adapter'
|
28
|
+
require 'remote_ruby/connection_adapter/caching_adapter'
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
require 'remote_ruby/compiler'
|
5
|
+
require 'remote_ruby/connection_adapter'
|
6
|
+
require 'remote_ruby/locals_extractor'
|
7
|
+
require 'remote_ruby/source_extractor'
|
8
|
+
require 'remote_ruby/flavour'
|
9
|
+
require 'remote_ruby/runner'
|
10
|
+
|
11
|
+
module RemoteRuby
|
12
|
+
# This class is responsible for executing blocks on the remote host with the
|
13
|
+
# specified adapters. This is the entrypoint to RemoteRuby logic.
|
14
|
+
class ExecutionContext
|
15
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
16
|
+
def initialize(**params)
|
17
|
+
add_flavours(params)
|
18
|
+
@use_cache = params.delete(:use_cache) || false
|
19
|
+
@save_cache = params.delete(:save_cache) || false
|
20
|
+
@cache_dir = params.delete(:cache_dir) || File.join(Dir.pwd, 'cache')
|
21
|
+
@out_stream = params.delete(:out_stream) || $stdout
|
22
|
+
@err_stream = params.delete(:err_stream) || $stderr
|
23
|
+
@adapter_klass = params.delete(:adapter) || ::RemoteRuby::SSHStdinAdapter
|
24
|
+
@params = params
|
25
|
+
|
26
|
+
FileUtils.mkdir_p(@cache_dir)
|
27
|
+
end
|
28
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
29
|
+
|
30
|
+
def execute(locals = nil, &block)
|
31
|
+
source = code_source(block)
|
32
|
+
locals ||= extract_locals(block)
|
33
|
+
|
34
|
+
result = execute_code(source, **locals)
|
35
|
+
|
36
|
+
assign_locals(locals.keys, result[:locals], block)
|
37
|
+
|
38
|
+
result[:result]
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
attr_reader :params, :adapter_klass, :use_cache, :save_cache, :cache_dir,
|
44
|
+
:out_stream, :err_stream, :flavours
|
45
|
+
|
46
|
+
def assign_locals(local_names, values, block)
|
47
|
+
local_names.each do |local|
|
48
|
+
next unless values.key?(local)
|
49
|
+
block.binding.local_variable_set(local, values[local])
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def extract_locals(block)
|
54
|
+
extractor =
|
55
|
+
::RemoteRuby::LocalsExtractor.new(block, ignore_types: self.class)
|
56
|
+
extractor.locals
|
57
|
+
end
|
58
|
+
|
59
|
+
def code_source(block)
|
60
|
+
source_extractor = ::RemoteRuby::SourceExtractor.new
|
61
|
+
source_extractor.extract(&block)
|
62
|
+
end
|
63
|
+
|
64
|
+
def context_hash(code_hash)
|
65
|
+
Digest::MD5.hexdigest(
|
66
|
+
self.class.name +
|
67
|
+
adapter_klass.name.to_s +
|
68
|
+
params.to_s +
|
69
|
+
code_hash
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
def cache_path(code_hash)
|
74
|
+
hsh = context_hash(code_hash)
|
75
|
+
File.join(cache_dir, hsh)
|
76
|
+
end
|
77
|
+
|
78
|
+
def cache_exists?(code_hash)
|
79
|
+
hsh = cache_path(code_hash)
|
80
|
+
File.exist?("#{hsh}.stdout") || File.exist?("#{hsh}.stderr")
|
81
|
+
end
|
82
|
+
|
83
|
+
def compiler(ruby_code, client_locals)
|
84
|
+
RemoteRuby::Compiler.new(
|
85
|
+
ruby_code,
|
86
|
+
client_locals: client_locals,
|
87
|
+
flavours: flavours
|
88
|
+
)
|
89
|
+
end
|
90
|
+
|
91
|
+
def execute_code(ruby_code, client_locals = {})
|
92
|
+
compiler = compiler(ruby_code, client_locals)
|
93
|
+
|
94
|
+
runner = ::RemoteRuby::Runner.new(
|
95
|
+
code: compiler.compiled_code,
|
96
|
+
adapter: adapter(compiler.code_hash),
|
97
|
+
out_stream: out_stream,
|
98
|
+
err_stream: err_stream
|
99
|
+
)
|
100
|
+
|
101
|
+
runner.run
|
102
|
+
end
|
103
|
+
|
104
|
+
def adapter(code_hash)
|
105
|
+
actual_adapter = adapter_klass.new(params)
|
106
|
+
|
107
|
+
if use_cache && cache_exists?(code_hash)
|
108
|
+
cache_adapter(actual_adapter, code_hash)
|
109
|
+
elsif save_cache
|
110
|
+
caching_adapter(actual_adapter, code_hash)
|
111
|
+
else
|
112
|
+
actual_adapter
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def cache_adapter(adapter, code_hash)
|
117
|
+
::RemoteRuby::CacheAdapter.new(
|
118
|
+
connection_name: adapter.connection_name,
|
119
|
+
cache_path: cache_path(code_hash)
|
120
|
+
)
|
121
|
+
end
|
122
|
+
|
123
|
+
def caching_adapter(adapter, code_hash)
|
124
|
+
::RemoteRuby::CachingAdapter.new(
|
125
|
+
adapter: adapter,
|
126
|
+
cache_path: cache_path(code_hash)
|
127
|
+
)
|
128
|
+
end
|
129
|
+
|
130
|
+
def add_flavours(params)
|
131
|
+
@flavours = ::RemoteRuby::Flavour.build_flavours(params)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module RemoteRuby
|
2
|
+
# Flavour to load Rails environment
|
3
|
+
class RailsFlavour < ::RemoteRuby::Flavour
|
4
|
+
def initialize(environment: :development)
|
5
|
+
@environment = environment
|
6
|
+
end
|
7
|
+
|
8
|
+
def code_header
|
9
|
+
<<-RUBY
|
10
|
+
ENV['RAILS_ENV'] = '#{environment}'
|
11
|
+
require './config/environment'
|
12
|
+
RUBY
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
attr_reader :environment
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module RemoteRuby
|
2
|
+
# Base class for Flavours: addons to execution context to insert additonal
|
3
|
+
# code to the generated remote code.
|
4
|
+
class Flavour
|
5
|
+
def self.build_flavours(params = {})
|
6
|
+
res = []
|
7
|
+
|
8
|
+
{
|
9
|
+
rails: RemoteRuby::RailsFlavour
|
10
|
+
}.each do |name, klass|
|
11
|
+
options = params.delete(name)
|
12
|
+
|
13
|
+
res << klass.new(**options) if options
|
14
|
+
end
|
15
|
+
|
16
|
+
res
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(params: {}); end
|
20
|
+
|
21
|
+
def code_header; end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
require 'remote_ruby/flavour/rails_flavour'
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module RemoteRuby
|
2
|
+
# Extracts local variable from given context
|
3
|
+
class LocalsExtractor
|
4
|
+
attr_reader :block, :ignore_types
|
5
|
+
|
6
|
+
def initialize(block, ignore_types: [])
|
7
|
+
@block = block
|
8
|
+
@ignore_types = Array(ignore_types)
|
9
|
+
end
|
10
|
+
|
11
|
+
def locals
|
12
|
+
locals = {}
|
13
|
+
|
14
|
+
local_variable_names.each do |name|
|
15
|
+
value = block.binding.eval(name.to_s)
|
16
|
+
next if ignored_type?(value)
|
17
|
+
locals[name] = value
|
18
|
+
end
|
19
|
+
|
20
|
+
locals
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def local_variable_names
|
26
|
+
if RUBY_VERSION >= '2.2'
|
27
|
+
block.binding.local_variables
|
28
|
+
else
|
29
|
+
# A hack to support Ruby 2.1 due to the absence
|
30
|
+
# of Binding#local_variables method. For some reason
|
31
|
+
# just calling `block.binding.send(:local_variables)`
|
32
|
+
# returns variables of the current context.
|
33
|
+
block.binding.eval('binding.send(:local_variables)')
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def ignored_type?(var)
|
38
|
+
ignore_types.any? { |klass| var.is_a? klass }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'colorize'
|
2
|
+
|
3
|
+
require 'remote_ruby/unmarshaler'
|
4
|
+
|
5
|
+
module RemoteRuby
|
6
|
+
# Runner class is responsible for running a prepared Ruby code with given
|
7
|
+
# connection adapter, reading output and unmarshalling result and local
|
8
|
+
# variables values.
|
9
|
+
class Runner
|
10
|
+
def initialize(code:, adapter:, out_stream: $stdout, err_stream: $stderr)
|
11
|
+
@code = code
|
12
|
+
@adapter = adapter
|
13
|
+
@out_stream = out_stream
|
14
|
+
@err_stream = err_stream
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
locals = nil
|
19
|
+
|
20
|
+
adapter.open(code) do |stdout, stderr|
|
21
|
+
out_thread = read_stream(stdout, out_stream, :green)
|
22
|
+
err_thread = read_stream(stderr, err_stream, :red)
|
23
|
+
[out_thread, err_thread].each(&:join)
|
24
|
+
locals = out_thread[:locals]
|
25
|
+
end
|
26
|
+
|
27
|
+
{ result: locals[:__return_val__], locals: locals }
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :code, :adapter, :out_stream, :err_stream
|
33
|
+
|
34
|
+
def read_stream(read_from, write_to, color)
|
35
|
+
Thread.new do
|
36
|
+
until read_from.eof?
|
37
|
+
line = read_from.readline
|
38
|
+
|
39
|
+
if line.start_with?('%%%MARSHAL')
|
40
|
+
Thread.current[:locals] ||= unmarshal(read_from)
|
41
|
+
else
|
42
|
+
write_to.puts "#{adapter.connection_name.send(color)}>\t#{line}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def unmarshal(stdout)
|
49
|
+
unmarshaler = RemoteRuby::Unmarshaler.new(stdout)
|
50
|
+
unmarshaler.unmarshal
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'method_source'
|
2
|
+
require 'parser/current'
|
3
|
+
require 'unparser'
|
4
|
+
|
5
|
+
module RemoteRuby
|
6
|
+
# Receives a block and extracts Ruby code (as a string) with this block's
|
7
|
+
# source
|
8
|
+
class SourceExtractor
|
9
|
+
def extract(&block)
|
10
|
+
ast = Parser::CurrentRuby.parse(block.source)
|
11
|
+
block_node = find_block(ast)
|
12
|
+
|
13
|
+
return '' unless block_node
|
14
|
+
|
15
|
+
_, body = parse(block_node)
|
16
|
+
Unparser.unparse(body)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def find_block(node)
|
22
|
+
return nil unless node.is_a? AST::Node
|
23
|
+
return node if node.type == :block
|
24
|
+
|
25
|
+
node.children.each do |child|
|
26
|
+
res = find_block(child)
|
27
|
+
return res if res
|
28
|
+
end
|
29
|
+
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse(node)
|
34
|
+
args = node.children[1].children
|
35
|
+
body = node.children[2]
|
36
|
+
[args, body]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module RemoteRuby
|
2
|
+
# Decorates the source stream and writes to the cache stream as
|
3
|
+
# the source is being read
|
4
|
+
class StreamCacher
|
5
|
+
def initialize(source_stream, cache_stream)
|
6
|
+
@source_stream = source_stream
|
7
|
+
@cache_stream = cache_stream
|
8
|
+
end
|
9
|
+
|
10
|
+
def read(*args)
|
11
|
+
res = source_stream.read(*args)
|
12
|
+
cache_stream.write(res)
|
13
|
+
res
|
14
|
+
end
|
15
|
+
|
16
|
+
def readline
|
17
|
+
res = source_stream.readline
|
18
|
+
cache_stream.write(res)
|
19
|
+
res
|
20
|
+
end
|
21
|
+
|
22
|
+
def eof?
|
23
|
+
source_stream.eof?
|
24
|
+
end
|
25
|
+
|
26
|
+
def close
|
27
|
+
source_stream.close
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :source_stream, :cache_stream
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module RemoteRuby
|
2
|
+
# Unmarshals variables from given stream
|
3
|
+
class Unmarshaler
|
4
|
+
UnmarshalError = Class.new(StandardError)
|
5
|
+
|
6
|
+
def initialize(stream, terminator = nil)
|
7
|
+
@stream = stream
|
8
|
+
@terminator = terminator
|
9
|
+
end
|
10
|
+
|
11
|
+
def unmarshal
|
12
|
+
res = {}
|
13
|
+
|
14
|
+
until stream.eof?
|
15
|
+
line = stream.readline
|
16
|
+
|
17
|
+
break if terminator && line == terminator
|
18
|
+
|
19
|
+
var = read_var(line)
|
20
|
+
res[var.first] = var[1]
|
21
|
+
end
|
22
|
+
|
23
|
+
res
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :stream, :terminator
|
29
|
+
|
30
|
+
def read_var(line)
|
31
|
+
varname, length = read_var_header(line)
|
32
|
+
data = read_var_data(length)
|
33
|
+
[varname.to_sym, data]
|
34
|
+
rescue ArgumentError => e
|
35
|
+
raise UnmarshalError,
|
36
|
+
"Could not resolve type for #{varname} variable: #{e.message}"
|
37
|
+
rescue TypeError
|
38
|
+
raise UnmarshalError, 'Incorrect data format'
|
39
|
+
end
|
40
|
+
|
41
|
+
def read_var_header(line)
|
42
|
+
varname, length = line.split(':')
|
43
|
+
|
44
|
+
if varname.nil? || length.nil?
|
45
|
+
raise UnmarshalError, "Incorrect header '#{line}'"
|
46
|
+
end
|
47
|
+
|
48
|
+
[varname, length]
|
49
|
+
end
|
50
|
+
|
51
|
+
# rubocop:disable Security/MarshalLoad
|
52
|
+
def read_var_data(length)
|
53
|
+
data = stream.read(length.to_i)
|
54
|
+
Marshal.load(data)
|
55
|
+
end
|
56
|
+
# rubocop:enable Security/MarshalLoad
|
57
|
+
end
|
58
|
+
end
|
data/lib/remote_ruby.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'remote_ruby/version'
|
2
|
+
require 'remote_ruby/execution_context'
|
3
|
+
|
4
|
+
# Namespace module for other RemoteRuby classes. Also contains methods, which
|
5
|
+
# are included in the global scope
|
6
|
+
module RemoteRuby
|
7
|
+
def remotely(args = {}, &block)
|
8
|
+
locals = args.delete(:locals)
|
9
|
+
execution_context = ::RemoteRuby::ExecutionContext.new(**args)
|
10
|
+
execution_context.execute(locals, &block)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.root(*params)
|
14
|
+
root_dir = ::Gem::Specification.find_by_name('remote_ruby').gem_dir
|
15
|
+
File.join(root_dir, *params)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.lib_path(*params)
|
19
|
+
File.join(root, 'lib', *params)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# rubocop:disable Style/MixinUsage
|
24
|
+
include RemoteRuby
|
25
|
+
# rubocop:enable Style/MixinUsage
|
data/remote_ruby.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
lib = File.expand_path('lib', __dir__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'remote_ruby/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'remote_ruby'
|
7
|
+
spec.version = RemoteRuby::VERSION
|
8
|
+
spec.authors = ['Nikita Chernukhin']
|
9
|
+
spec.email = ['nuinuhin@gmail.com']
|
10
|
+
|
11
|
+
spec.summary = 'Execute Ruby code on the remote servers.'
|
12
|
+
spec.description =
|
13
|
+
'Execute Ruby code on the remote servers from local Ruby script.'
|
14
|
+
spec.homepage = 'https://github.com/nu-hin/remote_ruby'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
|
21
|
+
spec.bindir = 'bin'
|
22
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
23
|
+
spec.require_paths = ['lib']
|
24
|
+
|
25
|
+
spec.add_runtime_dependency 'colorize', '~> 0.8'
|
26
|
+
spec.add_runtime_dependency 'method_source', '~> 0.9'
|
27
|
+
spec.add_runtime_dependency 'parser', '~> 2.5'
|
28
|
+
spec.add_runtime_dependency 'unparser', '~> 0.2'
|
29
|
+
end
|