remote_ruby 0.3.0 → 1.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -2
  3. data/.rubocop.yml +37 -6
  4. data/CHANGELOG.md +69 -0
  5. data/LICENSE.txt +1 -1
  6. data/README.md +358 -90
  7. data/lib/remote_ruby/adapter_builder.rb +75 -0
  8. data/lib/remote_ruby/cache_adapter.rb +41 -0
  9. data/lib/remote_ruby/{connection_adapter/caching_adapter.rb → caching_adapter.rb} +23 -13
  10. data/lib/remote_ruby/code_templates/compiler/main.rb.erb +17 -29
  11. data/lib/remote_ruby/compat_io_reader.rb +36 -0
  12. data/lib/remote_ruby/compat_io_writer.rb +38 -0
  13. data/lib/remote_ruby/compiler.rb +8 -8
  14. data/lib/remote_ruby/connection_adapter.rb +16 -16
  15. data/lib/remote_ruby/execution_context.rb +56 -74
  16. data/lib/remote_ruby/extensions.rb +14 -0
  17. data/lib/remote_ruby/parser_factory.rb +29 -0
  18. data/lib/remote_ruby/plugin.rb +25 -0
  19. data/lib/remote_ruby/{flavour/rails_flavour.rb → rails_plugin.rb} +7 -5
  20. data/lib/remote_ruby/remote_context.rb +52 -0
  21. data/lib/remote_ruby/remote_error.rb +55 -0
  22. data/lib/remote_ruby/source_extractor.rb +2 -12
  23. data/lib/remote_ruby/ssh_adapter.rb +129 -0
  24. data/lib/remote_ruby/stream_prefixer.rb +25 -0
  25. data/lib/remote_ruby/tee_writer.rb +16 -0
  26. data/lib/remote_ruby/text_mode_adapter.rb +63 -0
  27. data/lib/remote_ruby/text_mode_builder.rb +44 -0
  28. data/lib/remote_ruby/tmp_file_adapter.rb +62 -0
  29. data/lib/remote_ruby/version.rb +1 -1
  30. data/lib/remote_ruby.rb +57 -15
  31. metadata +72 -28
  32. data/.github/workflows/main.yml +0 -26
  33. data/.gitignore +0 -17
  34. data/Gemfile +0 -23
  35. data/lib/remote_ruby/connection_adapter/cache_adapter.rb +0 -41
  36. data/lib/remote_ruby/connection_adapter/eval_adapter.rb +0 -96
  37. data/lib/remote_ruby/connection_adapter/local_stdin_adapter.rb +0 -29
  38. data/lib/remote_ruby/connection_adapter/ssh_stdin_adapter.rb +0 -34
  39. data/lib/remote_ruby/connection_adapter/stdin_process_adapter.rb +0 -35
  40. data/lib/remote_ruby/flavour.rb +0 -27
  41. data/lib/remote_ruby/runner.rb +0 -55
  42. data/lib/remote_ruby/stream_cacher.rb +0 -36
  43. data/lib/remote_ruby/unmarshaler.rb +0 -59
  44. data/remote_ruby.gemspec +0 -36
data/README.md CHANGED
@@ -6,6 +6,8 @@
6
6
 
7
7
  RemoteRuby allows you to execute Ruby code on remote servers via SSH right from the Ruby script running on your local machine, as if it was executed locally.
8
8
 
9
+ [Changelog](CHANGELOG.md)
10
+
9
11
  ## Contents
10
12
  * [Requirements](#requirements)
11
13
  * [Overview](#overview)
@@ -14,33 +16,36 @@ RemoteRuby allows you to execute Ruby code on remote servers via SSH right from
14
16
  * [Limitations](#limitations)
15
17
  * [Installation](#installation)
16
18
  * [Usage](#usage)
19
+ * [Configuration](#configuration)
17
20
  * [Basic usage](#basic-usage)
18
21
  * [Output](#output)
19
22
  * [Parameters](#parameters)
20
- * [Local variables and return value](#local-variables-and-return-value)
21
- * [Caching](#caching)
22
- * [Adapters](#adapters)
23
- * [SSH STDIN adapter](#ssh-stdin-adapter)
24
- * [Local SDIN adapter](#local-stdin-adapter)
25
- * [Evaluating adapter](#evaluating-adapter)
26
- * [Rails](#rails)
23
+ * [SSH Parameters](#ssh-parameters)
24
+ * [Local variables](#local-variables)
25
+ * [Return value and remote assignments](#return-value-and-remote-assignments)
26
+ * [Error handling](#error-handling)
27
+ * [Caching](#caching)
28
+ * [Text mode](#text-mode)
29
+ * [Plugins](#plugins)
30
+ * [Adding custom plugins](#adding-custom-plugins)
31
+ * [Rails](#rails)
27
32
  * [Contributing](#contributing)
28
33
  * [License](#license)
29
34
 
30
35
  ## Requirements
31
36
 
32
- RemoteRuby requires at least Ruby 2.6 to run.
37
+ RemoteRuby requires at least Ruby 2.7 to run.
33
38
 
34
39
  ## Overview
35
40
 
36
- Here is a short example on how you can run your code remotely.
41
+ Here is a short example of how you can run your code remotely.
37
42
 
38
43
  ```ruby
39
44
  # This is test.rb file on the local developer machine
40
45
 
41
46
  require 'remote_ruby'
42
47
 
43
- remotely(server: 'my_ssh_server') do
48
+ remotely(host: 'my_ssh_server') do
44
49
  # Everything inside this block is executed on my_ssh_server
45
50
  puts 'Hello, RemoteRuby!'
46
51
  end
@@ -50,7 +55,7 @@ end
50
55
 
51
56
  When you call `#remotely` or `RemoteRuby::ExecutionContext#execute`, the passed block source is read and is then transformed to a standalone Ruby script, which also includes serialization/deserialization of local variables and return value, and other features (see [compiler.rb](lib/remote_ruby/compiler.rb) for more detail).
52
57
 
53
- After that, RemoteRuby opens an SSH connection to the specified host, launches Ruby interpreter there, and feeds the generated script to it. Standard output and standard error streams of SSH client are being captured.
58
+ RemoteRuby then opens an SSH connection to the specified host, copies the script to a temporary file on the host, and launches it remotely using the Ruby interpreter. Standard output and standard error streams of SSH client are being captured. Standard input is passed to the remote code as well.
54
59
 
55
60
  ### Key features
56
61
 
@@ -59,20 +64,19 @@ After that, RemoteRuby opens an SSH connection to the specified host, launches R
59
64
  ```ruby
60
65
  user_id = 1213
61
66
 
62
- remotely(server: 'my_ssh_server') do
67
+ remotely(host: 'my_ssh_server') do
63
68
  puts user_id # => 1213
64
69
  end
65
-
66
70
  ```
67
71
 
68
72
  * Access return value of the remote block, just as in case of a regular block:
73
+
69
74
  ```ruby
70
- res = remotely(server: 'my_ssh_server') do
75
+ res = remotely(host: 'my_ssh_server') do
71
76
  'My result'
72
77
  end
73
78
 
74
79
  puts res # => My result
75
-
76
80
  ```
77
81
 
78
82
  * Assignment to local variables inside remote block, just as in case of a regular block:
@@ -80,20 +84,30 @@ puts res # => My result
80
84
  ```ruby
81
85
  a = 1
82
86
 
83
- remotely(server: 'my_ssh_server') do
87
+ remotely(host: 'my_ssh_server') do
84
88
  a = 100
85
89
  end
86
90
 
87
91
  puts a # => 100
88
92
  ```
89
93
 
90
- ### Limitations
91
- * Remote SSH server must be accessible with public-key authentication. Password authentication is not supported.
94
+ * Reading from the client's standard input in the remote block:
95
+
96
+
97
+ ```ruby
98
+ remotely(host: 'my_ssh_server') do
99
+ puts 'What is your name?'
100
+ name = gets
101
+ puts "Hello, #{name}!"
102
+ end
103
+ ```
92
104
 
93
- * Currently, code to be executed remotely cannot read anything from STDIN, because STDIN is used to pass the source to the Ruby interpreter.
105
+ ### Limitations
106
+ * macOS keychain is not supported. If you are using a private SSH key with a passphrase, and you don't want to enter a passphrase each time a context is executed, the identity must be added to the SSH-agent, e.g. using `ssh-add`.
94
107
 
95
108
  * As RemoteRuby reads the block source from the script's source file, the script source file should reside on your machine's disk (e.g. you cannot use RemoteRuby from IRB console).
96
- * Since local and server scripts have different execution contexts, can have different gems (and even Ruby versions) installed, sometimes local variables as well as the block return value, will not be accessible, assigned or can even cause exception. See [usage](#local-variables-and-return-value) section below for more detail.
109
+
110
+ * Since local and server scripts have different execution contexts, can have different gems (and even Ruby versions) installed, sometimes local variables as well as the block return value, will not be accessible, assigned or can even cause exception. See the [usage](#local-variables-and-return-value) section below for more details.
97
111
 
98
112
  ## Installation
99
113
 
@@ -115,62 +129,141 @@ gem install remote_ruby
115
129
 
116
130
  ## Usage
117
131
 
132
+ ### Configuration
133
+
134
+ There are a few options that may be configured on the global level.
135
+
136
+ ```ruby
137
+ RemoteRuby.configure do |c|
138
+ # Defines, where Remote Ruby will cache output, error and result streams on the
139
+ # local machine.
140
+ # By default they are saved to .remote_ruby/cache (relative to the working directory).
141
+ c.cache_dir = File.join(Dir.pwd, '.remote_ruby/cache')
142
+
143
+ # Defines, where Remote Ruby will store compiled code on the local machine, if
144
+ # `dump_code` is set to `true` in the ExecutionContext.
145
+ # By default code is saved to .remote_ruby/code (relative to the working directory).
146
+ c.code_dir = File.join(Dir.pwd, '.remote_ruby/code')
147
+
148
+ # Set to true if you don't want to see warnings about parser gem compatibility with
149
+ # current Ruby version.
150
+ # False by default.
151
+ c.suppress_parser_warnings = false
152
+ end
153
+ ```
154
+
118
155
  ### Basic usage
119
156
 
120
157
  The main class to work with is the `ExecutionContext`, which provides an `#execute` method:
121
158
 
122
159
  ```ruby
123
- my_server = ::RemoteRuby::ExecutionContext.new(server: 'my_ssh_server')
160
+ my_server = ::RemoteRuby::ExecutionContext.new(host: 'my_ssh_server')
124
161
 
125
162
  my_server.execute do
126
- put Dir.pwd
163
+ puts Dir.pwd
127
164
  end
128
165
  ```
129
166
 
130
167
  You can easily define more than one context to access several servers.
131
168
 
132
- Along with `ExecutionContext#execute` method there is also `.remotely` method, which is included into the global scope. For instance, the code above is equivalent to the code below:
169
+ Along with `ExecutionContext#execute` method there is also `.remotely` method, which can be included from `RemoteRuby::Extensions` module. For instance, the code above is equivalent to the code below:
133
170
 
134
171
  ```ruby
135
- remotely(server: 'my_ssh_server') do
136
- put Dir.pwd
172
+ include RemoteRuby::Extensions
173
+
174
+ remotely(host: 'my_ssh_server') do
175
+ puts Dir.pwd
137
176
  end
138
177
  ```
139
178
 
140
179
  All parameters passed to the `remotely` method will be passed to the underlying `ExecutionContext` initializer. The only exception is an optional `locals` parameter, which will be passed to the `#execute` method (see [below](#local-variables-and-return-value)).
141
180
 
142
- ### Parameters
143
-
144
- Parameters, passed to the `ExecutionContext` can be general and _adapter-specific_. For adapter-specific parameters, refer to the [Adapters](#adapters) section below.
181
+ In all the examples in this document, where `.remote` method is used, it is assumed, that `RemoteRuby::Extensions` is included to the scope.
145
182
 
146
- The list of general parameters:
183
+ ### Parameters
147
184
 
148
185
  | Parameter | Type | Required | Default value | Description |
149
186
  | --------- | ---- | ---------| ------------- | ----------- |
150
- | adapter | Class | no | `::RemoteRuby::SSHStdinAdapter` | An adapter to use. Refer to the [Adapters](#adapters) section to learn about available adapters. |
187
+ | host | String | no | - | Name of the SSH host to connect to. If omitted, the code will be executed on the local host, in a separate Ruby process |
188
+ | use_ssh_config_file | String or Boolean | no | true | When boolean, specifies, whether to use ~/.ssh/config file for the initial set of parameters. When string, interpreted as a path to an SSH configuration file to use |
189
+ | ruby_executable | String | no | `ruby` | Absolute path to Ruby executable on the remote host, or executable name, reachable from $PATH |
190
+ | working_dir | String | no | '~' if running over SSH, or current dir, if running locally | Path to the directory where the script should be executed |
151
191
  | use_cache | Boolean | no | `false` | Specifies if the cache should be used for execution of the block (if the cache is available). Refer to the [Caching](#caching) section to find out more about caching. |
152
192
  | save_cache | Boolean | no | `false` | Specifies if the result of the block execution (i.e. output and error streams) should be cached for the subsequent use. Refer to the [Caching](#caching) section to find out more about caching. |
153
- | cache_dir | String | no | ./cache | Path to the directory on the local machine, where cache files should be saved. If the directory doesn't exist, RemoteRuby will try to create it. Refer to the [Caching](#caching) section to find out more about caching. |
154
- | stdout | Stream open for writing | no | `$stdout` | Redirection stream for server standard output |
155
- | stderr | Stream open for writing | no | `$stderr` | Redirection stream for server standard error output |
193
+ RemoteRuby will try to create it. Refer to the [Caching](#caching) section to find out more about caching. |
194
+ | in_stream | Stream open for reading | no | `$stdin` | Source stream for server standard input |
195
+ | out_stream | Stream open for writing | no | `$stdout` | Redirection stream for server standard output |
196
+ | err_stream | Stream open for writing | no | `$stderr` | Redirection stream for server standard error|
197
+ | text_mode | Boolean or Hash | no | `false` | Specifies, if the connection should be run in text mode. See [Text Mode](#text-mode) section below to find out more about text mode. |
198
+ | dump_code | Boolean | no | `false` | When set to true, the compiled script that will be run on the remote server will be dumped to a local file for inspection. See [Configuration](#configuration) to configure where the code is written. |
199
+
200
+ ### SSH Parameters
201
+
202
+ In addition to the arguments above, you can fine-tune the SSH connection to the remote host, if SSH is used (that is, if the `host` argument is specified). The arguments for SSH configuration can be anything that is supported by [net-ssh gem](https://github.com/net-ssh/net-ssh). The complete list of parameters can be found in [the documentation for net-ssh](https://net-ssh.github.io/net-ssh/Net/SSH.html#method-c-start). Some of the parameters are in the table below.
203
+
204
+ If the SSH configuration file is used (see `ssh_config` parameter in the table above), the explicitly specified values **will override** those taken from SSH config.
205
+
206
+ | Parameter | Type | Description |
207
+ | --------- | ---- | ----------- |
208
+ | user | String | the user name to log in as |
209
+ | password | String | the password to use to log in |
210
+ | keys | Array of strings | an array of file names of private keys to use for publickey and hostbased authentication |
211
+ | passphrase | String | the passphrase to use when loading a private key (default is `nil`, for no passphrase) |
212
+ | auth_methods | Array of strings | an array of authentication methods to try |
213
+
214
+ Example SSH configurations may look like:
215
+
216
+ ```ruby
217
+ # Use ~/.ssh/config file, but override some parameters
218
+ ec1 = RemoteRuby::ExecutionContext.new(
219
+ host: 'my_ssh_server',
220
+ auth_methods: %w(password),
221
+ user: 'jdoe',
222
+ password: 'p@ssw0rd'
223
+ )
224
+
225
+ # Custom key file
226
+ ec2 = RemoteRuby::ExecutionContext.new(
227
+ host: 'my_ssh_server',
228
+ keys: '/home/jdoe/.ssh/custom_id_rsa'
229
+ )
230
+
231
+ # Ignore SSH configuration and provide everything explicitly
232
+ ec3 = RemoteRuby::ExecutionContext.new(
233
+ host: 'my_ssh_server',
234
+ use_ssh_config_file: false,
235
+ auth_methods: %w(password),
236
+ user: 'jdoe',
237
+ password: 'p@ssw0rd'
238
+ )
239
+
240
+ ```
156
241
 
157
242
  ### Output
158
243
 
159
- Standard output and standard error streams from the remote process are captured, and then, depending on your parameters are either forwarded to local STOUT/STDERR or to the specified streams. RemoteRuby will add a prefix to each line of server output to distinguish between local and server output. STDOUT prefix is displayed in green, STDERR prefix is red. If output is read from cache, then `[CACHE]` prefix will also be added. The prefix may also depend on the adapter used.
244
+ Standard output and standard error streams from the remote process are captured, and then, depending on your parameters are either forwarded to local standard output/error or to the specified streams.
160
245
 
161
246
  ```ruby
162
- remotely(server: 'my_ssh_server', working_dir: '/home/john') do
163
- puts 'This is an output'
164
- warn 'This is a warning'
165
- end
247
+ remotely(host: 'my_ssh_server', working_dir: '/home/john') do
248
+ puts 'This is an output'
249
+ warn 'This is a warning'
250
+ end
166
251
  ```
167
252
 
168
- ```bash
169
- my_ssh_server:/home/john> This is an output
170
- my_ssh_server:/home/john> This is a warning
253
+ ### Input
254
+
255
+ Remote script can receive data from standard input. By default the input is captured from client's standard input, but this can be set to any readable stream using `in_stream` argument to the `ExecutionContext` initializer.
256
+
257
+ ```ruby
258
+ name = remotely(host: 'my_ssh_server') do
259
+ puts "What is your name?"
260
+ gets
261
+ end
262
+
263
+ puts "Hello locally, #{name}!"
171
264
  ```
172
265
 
173
- ### Local variables and return value
266
+ ### Local variables
174
267
 
175
268
  When you call a remote block RemoteRuby will try to serialize all local variables from the calling context, and include them to the remote script.
176
269
 
@@ -181,7 +274,7 @@ some_number = 3
181
274
  name = 'Alice'
182
275
 
183
276
  # Explicitly setting locals with .remotely method
184
- remotely(locals: { name: 'John Doe' }, server: 'my_ssh_server') do
277
+ remotely(locals: { name: 'John Doe' }, host: 'my_ssh_server') do
185
278
  # name is 'John Doe', not 'Alice'
186
279
  puts name # => John Doe
187
280
  # some_number is not defined
@@ -189,7 +282,7 @@ remotely(locals: { name: 'John Doe' }, server: 'my_ssh_server') do
189
282
  end
190
283
 
191
284
  # Explicitly setting locals with ExecutionContext#execute method
192
- execution_context = ::RemoteRuby::ExecutionContext.new(server: 'my_ssh_server')
285
+ execution_context = ::RemoteRuby::ExecutionContext.new(host: 'my_ssh_server')
193
286
 
194
287
  execution_context.execute(name: 'John Doe') do
195
288
  # name is 'John Doe', not 'Alice'
@@ -205,7 +298,7 @@ However, some objects cannot be serialized. In this case, RemoteRuby will print
205
298
  # We cannot serialize a file stream
206
299
  file = File.open('some_file.txt', 'rb')
207
300
 
208
- remotely(server: 'my_ssh_server') do
301
+ remotely(host: 'my_ssh_server') do
209
302
  puts file.read # undefined local variable or method `file'
210
303
  end
211
304
  ```
@@ -215,7 +308,7 @@ Moreover, if such variables are assigned to in the remote block, their value **w
215
308
  ```ruby
216
309
  file = File.open('some_file.txt', 'rb')
217
310
 
218
- remotely(server: 'my_ssh_server') do
311
+ remotely(host: 'my_ssh_server') do
219
312
  file = 3 # No exception here, as we are assigning
220
313
  end
221
314
 
@@ -226,10 +319,11 @@ puts file == 3 # false
226
319
  If the variable can be serialized, but the remote server context lacks the knowledge on how to deserialize it, the variable will be defined inside the remote block, but its value will be `nil`:
227
320
 
228
321
  ```ruby
229
- # Something, which is not present on the remote server
322
+ # Something, that is not present on the remote server
230
323
  special_thing = SomeSpecialGem::SpecialThing.new
231
324
 
232
- remotely(server: 'my_ssh_server') do
325
+ remotely(host: 'my_ssh_server') do
326
+ puts defined?(special_thing) # => local-variable
233
327
  # special_thing is defined, but its value is nil
234
328
  puts special_thing.nil? # => true
235
329
 
@@ -240,33 +334,62 @@ end
240
334
  puts special_thing == 3 # => true
241
335
  ```
242
336
 
243
- If RemoteRuby cannot deserialize variable on server side, it will print a warning to server's STDERR stream.
337
+ If RemoteRuby cannot deserialize variable on server side, it will print a warning to server's standard error.
338
+
339
+ It is possible to ignore certain types, so that RemoteRuby will never try to send variables of these types to the remote host. This can be done by adding configuration:
244
340
 
245
- If remote block returns a value which cannot be deserialized on the client side, or if it assigns such a value to the local variable, the exception on the client side will be always raised:
341
+ ```ruby
342
+ RemoteRuby::Configure do |c|
343
+ c.ignore_types SomeSpecialGem::SpecialThing
344
+ end
345
+ ```
346
+
347
+ If a type is ignored, the remote block will behave as if the local variable is not defined:
246
348
 
247
349
  ```ruby
248
- # Unsupportable return value example
350
+ RemoteRuby::Configure do |c|
351
+ c.ignore_types SomeSpecialGem::SpecialThing
352
+ end
353
+
354
+ special_thing = SomeSpecialGem::SpecialThing.new
355
+
356
+ remotely(host: 'my_ssh_server') do
357
+ puts defined?(special_thing) # => nil
249
358
 
250
- remotely(server: 'my_ssh_server') do
359
+ # special_thing is not defined
360
+ puts special_thing.nil? # NameError undefined local variable or method `special_thing' for main:Object
361
+ end
362
+ ```
363
+
364
+ RemoteRuby always ignores variables of type `RemoteRuby::ExecutionContext`.
365
+
366
+ ### Return value and remote assignments
367
+
368
+ If remote block returns a value that cannot be deserialized on the client side, or if it assigns such a value to the local variable, the exception on the client side will be always raised:
369
+
370
+ ```ruby
371
+ # Unsupported return value example
372
+
373
+ remotely(host: 'my_ssh_server') do
251
374
  # this is not present in the client context
252
375
  server_specific_var = ServerSpecificClass.new
253
376
  end
254
377
 
255
- # RemoteRuby::Unmarshaler::UnmarshalError
378
+ # undefined class/module ServerSpecificClass (ArgumentError)
256
379
  ```
257
380
 
258
381
  ```ruby
259
- # Unsupportable local value example
382
+ # Unsupported local value example
260
383
 
261
384
  my_local = nil
262
385
 
263
- remotely(server: 'my_ssh_server') do
386
+ remotely(host: 'my_ssh_server') do
264
387
  # this is not present in the client context
265
388
  my_local = ServerSpecificClass.new
266
389
  nil
267
390
  end
268
391
 
269
- # RemoteRuby::Unmarshaler::UnmarshalError
392
+ # undefined class/module ServerSpecificClass (ArgumentError)
270
393
  ```
271
394
 
272
395
  To avoid these situations, do not assign/return values unsupported on the client side, or, if you don't need any return value, add `nil` at the end of your block:
@@ -274,13 +397,61 @@ To avoid these situations, do not assign/return values unsupported on the client
274
397
  ```ruby
275
398
  # No exception
276
399
 
277
- remotely(server: 'my_ssh_server') do
400
+ remotely(host: 'my_ssh_server') do
278
401
  # this is not present in the client context
279
402
  server_specific_var = ServerSpecificClass.new
280
403
  nil
281
404
  end
282
405
  ```
283
406
 
407
+ ### Error handling
408
+
409
+ If remote code raises an error, RemoteRuby intercepts it and raises a `RemoteRuby::RemoteError` on the local machine. Since remote code potentially can raise an exception of a type (or containing a type), that is not present in the local context, the exception itself is not wrapped. Instead, `RemoteRuby::RemoteError` will contain the class name, message and the stack trace of the causing remote error.
410
+
411
+ ```ruby
412
+ a = 1
413
+ b = 2
414
+ res = 'unchanged'
415
+ begin
416
+ res = remotely(host: 'my_ssh_server', dump_code: true) do
417
+ a = 10
418
+ raise StandardError.new('remote error text')
419
+ b = 20
420
+ 'changed'
421
+ end
422
+ rescue RemoteRuby::RemoteError
423
+ puts a
424
+ puts b
425
+ puts res
426
+
427
+ raise
428
+ end
429
+ ```
430
+
431
+ This will produce something like the following.
432
+
433
+ ```
434
+ 10
435
+ 2
436
+ unchanged
437
+ /path/to/gemset/remote_ruby/lib/remote_ruby/execution_context.rb:36:in 'RemoteRuby::ExecutionContext#execute': Remote error: StandardError (RemoteRuby::RemoteError)
438
+ remote error text
439
+
440
+ from /tmp/remote_ruby.qwGUHP:71:in `block in <main>'
441
+ (See /home/jdoe/Work/remote_ruby_test/.remote_ruby/code/dcbb2493288b1d10be042a32a31bf8af43da660234f1731f03966aa67ac870e3.rb:71:in `block in <main>'
442
+ 68:
443
+ 69: # Start of client code
444
+ 70: a = 10
445
+ 71: >> raise(StandardError.new("remote error text"))
446
+ 72: b = 20
447
+ 73: "changed"
448
+ 74: # End of client code
449
+ ```
450
+
451
+ As you can see, the behaviour of remote error corresponds to the situation when an error is raised in normal block. Local variables that are assigned before the error have the new value. The result of block in case of an error is `nil`.
452
+
453
+ Note that when printed to console `RemoteError` displays the context of the error in the **remote script**. If `dump_code` is set to `true`, `RemoteError` will also print the location of the line in the local copy of the remote script. This may be very useful for debugging.
454
+
284
455
 
285
456
  ### Caching
286
457
 
@@ -289,14 +460,13 @@ RemoteRuby allows you to save the result of previous block excutions in the loca
289
460
  ```ruby
290
461
  # Caching example
291
462
  # First time this script will take 60 seconds to run,
292
- # but on subsequent runs it will return the result immidiately
463
+ # but on subsequent runs it will return the result immediately
293
464
 
294
465
  require 'remote_ruby'
295
466
 
296
- res = remotely(server: 'my_ssh_server', save_cache: true, use_cache: true) do
467
+ res = remotely(host: 'my_ssh_server', save_cache: true, use_cache: true) do
297
468
  60.times do
298
469
  puts 'One second has passed'
299
- STDOUT.flush
300
470
  sleep 1
301
471
  end
302
472
 
@@ -306,54 +476,153 @@ end
306
476
  puts res # => Some result
307
477
  ```
308
478
 
309
- You can specify where to put your cache files explicitly, by passing `cache_dir` parameter which is the "cache" directory inside your current working directory by default.
479
+ You can specify where to put your cache files explicitly, by [configuring](#configuration) the `cache_dir` which is by default set to ".remote_ruby/cache" inside your current working directory.
310
480
 
311
- RemoteRuby calculates the cache file to use, based on the code you pass to the remote block, as well as on ExecutionContext 'contextual' parameters (e. g. server or working directory) and serialized local variables. Therefore, if you change anything in your remote block, local variables (passed to the block), or in any of the 'contextual' parameters, RemoteRuby will use different cache file. However, if you revert all your changes back, the old file will be used again.
481
+ RemoteRuby calculates the cache file to use, based on the code you pass to the remote block, as well as on `ExecutionContext` 'contextual' parameters (e. g. server or working directory) and serialized local variables. Therefore, if you change anything in your remote block, local variables (passed to the block), or in any of the 'contextual' parameters, RemoteRuby will use different cache file. However, if you revert all your changes back, the old file will be used again.
312
482
 
313
483
  **IMPORTANT**: RemoteRuby does not know when to clear the cache. Therefore, it is up to you to take care of cleaning the cache when you no longer need it. This is especially important if your output can contain sensitive data.
314
484
 
315
- ### Adapters
485
+ ### Text mode
316
486
 
317
- RemoteRuby can use different adapters to execute remote Ruby code. To specify an adapter you want to use, pass an `:adapter` argument to the initializer of `ExecutionContext` or to the `remotely` method.
487
+ Text mode allows to treat the output and/or the standard error of the remote process as text. If it is enabled, the server output is prefixed with some string, which makes it easier to distinguish local output, and the output coming from the remote code. Additionally, it may help distinguishing when the output is taken from cache.
318
488
 
319
- #### SSH STDIN adapter
489
+ The text mode is controlled by the `text_mode` parameter to the `::RemoteRuby::ExecutionContext` initializer, and is `false` by default.
320
490
 
321
- This adapter uses SSH console client to connect to the remote machine, launches Ruby interpreter there, and feeds the script to the interpreter via STDIN. This is the main and the **default** adapter. It assumes that the SSH client is installed on the client machine, and that the access to the remote host is possible with public-key authenitcation. Password authentication is not supported. To use this adapter, pass `adapter: ::RemoteRuby::SSHStdinAdapter` parameter to the `ExecutionContext` initializer, or do not specify adapter at all.
491
+ The easiest way to enable it is to set `text_mode` to `true`.
322
492
 
323
- ##### Parameters
493
+ ```ruby
494
+ ctx = ::RemoteRuby::ExecutionContext.new(
495
+ host: 'my_ssh_server',
496
+ user: 'jdoe'
497
+ text_mode: true,
498
+ )
499
+
500
+ ctx.execute do
501
+ puts "This is a greeting"
502
+ warn "This is an error"
503
+ end
504
+ ```
324
505
 
325
- | Parameter | Type | Required | Default value | Description |
326
- | --------- | ---- | ---------| ------------- | ----------- |
327
- | server | String | yes | - | Name of the SSH server to connect to |
328
- | working_dir | String | no | ~ | Path to the directory on the remote server where the script should be executed |
329
- | user | String | no | - | User on the remote host to connect as |
330
- | key_file| String | no | - | Path to the private SSH key |
331
- | bundler | Boolean | no | false | Specifies, whether the code should be executed with `bundle exec` on the remote server |
506
+ This will produce:
332
507
 
508
+ ```
509
+ jdoe@my_ssh_server:~> This is a greeting
510
+ jdoe@my_ssh_server:~> This is a greeting
511
+ ```
333
512
 
334
- #### Local STDIN adapter
513
+ By default, the prefixes for standard output and standard error can be different when running over SSH and locally. Output prefix is marked with green italic, and error with red italic. If the cache is used, the default configuration will append a bold blue '[C]' prefix in front of each output line.
335
514
 
336
- This adapter changes to the specified directory on the **local** machine, launches Ruby interpreter there, and feeds the script to the interpreter via STDIN. Therefore everything will be executed on the local machine, but in a child process. This adapter can be used for testing, or it can be useful if you want to execute some code in context of several code bases you have on the local machine. To use this adapter, pass `adapter: ::RemoteRuby::LocalStdinAdapter` parameter to the `ExecutionContext` initializer.
337
515
 
516
+ You can fine-tune the text mode to your needs by passing a hash as a value to `text_mode` parameter:
338
517
 
339
- | Parameter | Type | Required | Default value | Description |
340
- | --------- | ---- | ---------| ------------- | ----------- |
341
- | working_dir | String | no | . | Path to the directory on the local machine where the script should be executed |
342
- | bundler | Boolean | no | false | Specifies, whether the code should be executed with `bundle exec` |
518
+ ```ruby
519
+ ctx = ::RemoteRuby::ExecutionContext.new(
520
+ host: 'my_ssh_server',
521
+ user: 'jdoe'
522
+ text_mode: {
523
+ stdout_prefix: 'server says: ',
524
+ stdout_prefix: 'server warns: ',
525
+ stdout_mode: { color: :blue, mode: :underline }
526
+ }
527
+ )
528
+ ```
343
529
 
530
+ This will produce
344
531
 
345
- #### Evaluating adapter
532
+ ```
533
+ server says: This is a greeting
534
+ server warns: This is a greeting
535
+ ```
346
536
 
347
- This adapter executes Ruby code in the same process, by running it in an isolated scope. It can optionally change to a specified directory before execution (and change back after completion). There is also an option to run this asynchronously; if enabled, the code will run on a separate thread to mimic SSH connection to a remote machine. Please note, that async feature is experimental, and probably will not work on all platforms. This adapter is intended for testing, and it shows better performance than `LocalStdinAdapter`. To use this adapter, pass `adapter: ::RemoteRuby::EvalAdapter` parameter to the `ExecutionContext` initializer.
537
+ It is reasonable to avoid text mode if you want to put binary data to the standard output:
538
+
539
+ ```ruby
540
+ # copy_avatar.rb
541
+ # Reads a file from remote server and writes it to client's standard output.
542
+
543
+ remotely(host: 'my_ssh_server', text_mode: false) do
544
+ STDOUT.write File.read('avatar.jpg')
545
+ end
546
+ ```
547
+
548
+ Now you could do:
549
+
550
+ ```shell
551
+ ruby copy_avatar.rb > avatar.jpg
552
+ ```
348
553
 
349
- | Parameter | Type | Required | Default value | Description |
350
- | --------- | ---- | ---------| ------------- | ----------- |
351
- | working_dir | String | no | . | Path to the directory on the local machine where the script should be executed |
352
- | async | Boolean | no | false | Enables or disables asynchronous mode of the adapter |
353
554
 
555
+ The complete list of text mode parameters is in the table below:
354
556
 
355
- ### Rails
356
- RemoteRuby can load Rails environment for you, if you want to execute a script in a Rails context. To do this, simply add `rails` parameter to your call:
557
+ | Parameter | Type | Default value | Description |
558
+ |-|-|-|-|
559
+ | stdout_prefix | String | `user@host:/path/to/working/dir> ` | Prepended to standard output lines. Set to `nil` to disable |
560
+ | stderr_prefix | String | `user@host:/path/to/working/dir> ` | Prepended to standard error lines. Set to `nil` to disable |
561
+ | cache_prefix | String | `'[C] '` | Prepended to standard output and standard error lines if the context is using cache. Only added if corresponding prefix is not `nil`. Set to `nil` to disable |
562
+ | disable_unless_tty | Boolean | true | Disables the text mode if the corresponding IO is not TTY. Useful if you want to disable the prefixes and coloring when e.g. outputting to a file |
563
+ | stdout_mode | Hash | `{ color: :green, mode: :italic }` | Text effects and colors applied to the standard output prefix. See [colorize gem](https://github.com/fazibear/colorize) for available parameters.
564
+ | stderr_mode | Hash | `{ color: :red, mode: :italic }` | Text effects and colors applied to the standard error prefix. See [colorize gem](https://github.com/fazibear/colorize) for available parameters.
565
+ | cache_mode | Hash | `{ color: :blue, mode: :bold }` | Text effects and colors applied to the cache prefix. See [colorize gem](https://github.com/fazibear/colorize) for available parameters.
566
+
567
+ ### Plugins
568
+ RemoteRuby can be extended with plugins. Plugins are used to insert additional code to the script, which is executed in the remote context. There is also a built-in plugin that allows for automatically loading Rails environment.
569
+
570
+ #### Adding custom plugins
571
+
572
+ RemoteRuby plugin must be a class. Instances of a plugin class must respond to `#code_header` method without any parameters. Plugins are instantiated when the `ExecutionContext` is created.
573
+
574
+ You may inherit your class from `::RemoteRuby::Plugin` class but that is not necessary.
575
+
576
+ Let's take a look at an example plugin:
577
+
578
+ ```ruby
579
+ class UsernamePlugin < RemoteRuby::Plugin
580
+ # This plugin prints a name of the user on the calling host.
581
+ attr_reader :username
582
+
583
+ def initialize(username:)
584
+ @username = username
585
+ end
586
+
587
+ def code_header
588
+ <<~RUBY
589
+ puts "This code is run by #{username}"
590
+ RUBY
591
+ end
592
+ end
593
+ ```
594
+
595
+ In order to be used, the plugin needs to be registered. You can register a plugin by calling `#register_plugin` method.
596
+
597
+ ```ruby
598
+ RemoteRuby.configure do |c|
599
+ c.register_plugin(:username_printer, UsernamePlugin)
600
+ end
601
+ ```
602
+
603
+ Now, when creating an `ExecutionContext` we can use `username_printer` argument to the initializer. Plugin argument value must be a hash. All hash values will be passed to plugin class initializer as name arguments.
604
+
605
+ ```ruby
606
+ ec = RemoteRuby::ExecutionContext.new(
607
+ host: 'my_ssh_server',
608
+ username_printer: { username: ENV['USER'] }
609
+ )
610
+
611
+ ec.execute do
612
+ puts "Hello world!"
613
+ end
614
+ ```
615
+
616
+ This should print the following:
617
+
618
+ ```
619
+ This code is run by jdoe
620
+ Hello world!
621
+ ```
622
+
623
+
624
+ #### Rails plugin
625
+ RemoteRuby can load Rails environment for you, if you want to execute a script in a Rails context. To do this, simply add built-in Rails plugin by adding `rails` argument to your call:
357
626
 
358
627
  ```ruby
359
628
  # Rails integration example
@@ -361,7 +630,7 @@ RemoteRuby can load Rails environment for you, if you want to execute a script i
361
630
  require 'remote_ruby'
362
631
 
363
632
  remote_service = ::RemoteRuby::ExecutionContext.new(
364
- server: 'rails-server',
633
+ host: 'rails-server',
365
634
  working_dir: '/var/www/rails_app/www/current',
366
635
  # This specifies ENV['RAILS_ENV'] and can be changed
367
636
  rails: { environment: :production }
@@ -377,7 +646,6 @@ end
377
646
  puts phone
378
647
  ```
379
648
 
380
-
381
649
  ## Contributing
382
650
 
383
651
  Bug reports and pull requests are welcome on GitHub at https://github.com/nu-hin/remote_ruby.