remote_ruby 0.3.0 → 1.0.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 +64 -0
  5. data/LICENSE.txt +1 -1
  6. data/README.md +348 -81
  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 +128 -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,22 +16,25 @@ 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-assignements)
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
 
@@ -40,7 +45,7 @@ Here is a short example on how you can run your code remotely.
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
+ After that, RemoteRuby opens an SSH connection to the specified host, copies the script to the host (to a temporary file), and then launches runs this script using the Ruby interpreter on the host. 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,19 +84,29 @@ 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).
109
+
96
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 [usage](#local-variables-and-return-value) section below for more detail.
97
111
 
98
112
  ## Installation
@@ -115,62 +129,140 @@ 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
+ | 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
190
  | 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
191
  | 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 |
192
+ RemoteRuby will try to create it. Refer to the [Caching](#caching) section to find out more about caching. |
193
+ | in_stream | Stream open for reading | no | `$stdin` | Source stream for server standard input |
194
+ | out_stream | Stream open for writing | no | `$stdout` | Redirection stream for server standard output |
195
+ | err_stream | Stream open for writing | no | `$stderr` | Redirection stream for server standard error|
196
+ | 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. |
197
+ | 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. |
198
+
199
+ ### SSH Parameters
200
+
201
+ 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.
202
+
203
+ If the SSH configuration file is used (see `ssh_config` parameter in the table above), the explicitly specified values **will override** the values taken from SSH config.
204
+
205
+ | Parameter | Type | Description |
206
+ | --------- | ---- | ----------- |
207
+ | user | String | the user name to log in as |
208
+ | password | String | the password to use to log in |
209
+ | keys | Array of strings | an array of file names of private keys to use for publickey and hostbased authentication |
210
+ | passphrase | String | the passphrase to use when loading a private key (default is `nil`, for no passphrase) |
211
+ | auth_methods | Array of strings | an array of authentication methods to try |
212
+
213
+ Example SSH configurations may look like:
214
+
215
+ ```ruby
216
+ # Use ~/.ssh/config file, but override some parameters
217
+ ec1 = RemoteRuby::ExecutionContext.new(
218
+ host: 'my_ssh_server',
219
+ auth_methods: %w(password),
220
+ user: 'jdoe',
221
+ password: 'p@ssw0rd'
222
+ )
223
+
224
+ # Custom key file
225
+ ec2 = RemoteRuby::ExecutionContext.new(
226
+ host: 'my_ssh_server',
227
+ keys: '/home/jdoe/.ssh/custom_id_rsa'
228
+ )
229
+
230
+ # Ignore SSH configuration and provide everything explicitly
231
+ ec3 = RemoteRuby::ExecutionContext.new(
232
+ host: 'my_ssh_server',
233
+ use_ssh_config_file: false,
234
+ auth_methods: %w(password),
235
+ user: 'jdoe',
236
+ password: 'p@ssw0rd'
237
+ )
238
+
239
+ ```
156
240
 
157
241
  ### Output
158
242
 
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.
243
+ Standard output and standard error streams from the remote process are captured, and then, depending on your parameters are either forwarded to local STDOUT/STDERR or to the specified streams.
160
244
 
161
245
  ```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
246
+ remotely(host: 'my_ssh_server', working_dir: '/home/john') do
247
+ puts 'This is an output'
248
+ warn 'This is a warning'
249
+ end
166
250
  ```
167
251
 
168
- ```bash
169
- my_ssh_server:/home/john> This is an output
170
- my_ssh_server:/home/john> This is a warning
252
+ ### Input
253
+
254
+ Standard input from the client is captured and passed to the remote code. By default the input is captured from STDIN.
255
+
256
+ ```ruby
257
+ name = remotely(host: 'my_ssh_server') do
258
+ puts "What is your name?"
259
+ gets
260
+ end
261
+
262
+ puts "Hello locally, #{name}!"
171
263
  ```
172
264
 
173
- ### Local variables and return value
265
+ ### Local variables
174
266
 
175
267
  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
268
 
@@ -181,7 +273,7 @@ some_number = 3
181
273
  name = 'Alice'
182
274
 
183
275
  # Explicitly setting locals with .remotely method
184
- remotely(locals: { name: 'John Doe' }, server: 'my_ssh_server') do
276
+ remotely(locals: { name: 'John Doe' }, host: 'my_ssh_server') do
185
277
  # name is 'John Doe', not 'Alice'
186
278
  puts name # => John Doe
187
279
  # some_number is not defined
@@ -189,7 +281,7 @@ remotely(locals: { name: 'John Doe' }, server: 'my_ssh_server') do
189
281
  end
190
282
 
191
283
  # Explicitly setting locals with ExecutionContext#execute method
192
- execution_context = ::RemoteRuby::ExecutionContext.new(server: 'my_ssh_server')
284
+ execution_context = ::RemoteRuby::ExecutionContext.new(host: 'my_ssh_server')
193
285
 
194
286
  execution_context.execute(name: 'John Doe') do
195
287
  # name is 'John Doe', not 'Alice'
@@ -205,7 +297,7 @@ However, some objects cannot be serialized. In this case, RemoteRuby will print
205
297
  # We cannot serialize a file stream
206
298
  file = File.open('some_file.txt', 'rb')
207
299
 
208
- remotely(server: 'my_ssh_server') do
300
+ remotely(host: 'my_ssh_server') do
209
301
  puts file.read # undefined local variable or method `file'
210
302
  end
211
303
  ```
@@ -215,7 +307,7 @@ Moreover, if such variables are assigned to in the remote block, their value **w
215
307
  ```ruby
216
308
  file = File.open('some_file.txt', 'rb')
217
309
 
218
- remotely(server: 'my_ssh_server') do
310
+ remotely(host: 'my_ssh_server') do
219
311
  file = 3 # No exception here, as we are assigning
220
312
  end
221
313
 
@@ -226,10 +318,11 @@ puts file == 3 # false
226
318
  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
319
 
228
320
  ```ruby
229
- # Something, which is not present on the remote server
321
+ # Something, that is not present on the remote server
230
322
  special_thing = SomeSpecialGem::SpecialThing.new
231
323
 
232
- remotely(server: 'my_ssh_server') do
324
+ remotely(host: 'my_ssh_server') do
325
+ puts defined?(special_thing) # => local-variable
233
326
  # special_thing is defined, but its value is nil
234
327
  puts special_thing.nil? # => true
235
328
 
@@ -242,17 +335,46 @@ puts special_thing == 3 # => true
242
335
 
243
336
  If RemoteRuby cannot deserialize variable on server side, it will print a warning to server's STDERR stream.
244
337
 
338
+ 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:
339
+
340
+ ```ruby
341
+ RemoteRuby::Configure do |c|
342
+ c.ignore_types SomeSpecialGem::SpecialThing
343
+ end
344
+ ```
345
+
346
+ If a type is ignored, the remote block will behave as if the local variable is not defined:
347
+
348
+ ```ruby
349
+ RemoteRuby::Configure do |c|
350
+ c.ignore_types SomeSpecialGem::SpecialThing
351
+ end
352
+
353
+ special_thing = SomeSpecialGem::SpecialThing.new
354
+
355
+ remotely(host: 'my_ssh_server') do
356
+ puts defined?(special_thing) # => nil
357
+
358
+ # special_thing is not defined
359
+ puts special_thing.nil? # NameError undefined local variable or method `special_thing' for main:Object
360
+ end
361
+ ```
362
+
363
+ RemoteRuby always ignores variables of type `RemoteRuby::ExecutionContext`.
364
+
365
+ ### Return value and remote assignements
366
+
245
367
  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:
246
368
 
247
369
  ```ruby
248
370
  # Unsupportable return value example
249
371
 
250
- remotely(server: 'my_ssh_server') do
372
+ remotely(host: 'my_ssh_server') do
251
373
  # this is not present in the client context
252
374
  server_specific_var = ServerSpecificClass.new
253
375
  end
254
376
 
255
- # RemoteRuby::Unmarshaler::UnmarshalError
377
+ # undefined class/module ServerSpecificClass (ArgumentError)
256
378
  ```
257
379
 
258
380
  ```ruby
@@ -260,13 +382,13 @@ end
260
382
 
261
383
  my_local = nil
262
384
 
263
- remotely(server: 'my_ssh_server') do
385
+ remotely(host: 'my_ssh_server') do
264
386
  # this is not present in the client context
265
387
  my_local = ServerSpecificClass.new
266
388
  nil
267
389
  end
268
390
 
269
- # RemoteRuby::Unmarshaler::UnmarshalError
391
+ # undefined class/module ServerSpecificClass (ArgumentError)
270
392
  ```
271
393
 
272
394
  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 +396,61 @@ To avoid these situations, do not assign/return values unsupported on the client
274
396
  ```ruby
275
397
  # No exception
276
398
 
277
- remotely(server: 'my_ssh_server') do
399
+ remotely(host: 'my_ssh_server') do
278
400
  # this is not present in the client context
279
401
  server_specific_var = ServerSpecificClass.new
280
402
  nil
281
403
  end
282
404
  ```
283
405
 
406
+ ### Error handling
407
+
408
+ 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.
409
+
410
+ ```ruby
411
+ a = 1
412
+ b = 2
413
+ res = 'unchanged'
414
+ begin
415
+ res = remotely(host: 'my_ssh_server', dump_code: true) do
416
+ a = 10
417
+ raise StandardError.new('remote error text')
418
+ b = 20
419
+ 'changed'
420
+ end
421
+ rescue RemoteRuby::RemoteError
422
+ puts a
423
+ puts b
424
+ puts res
425
+
426
+ raise
427
+ end
428
+ ```
429
+
430
+ This will produce something like the following.
431
+
432
+ ```
433
+ 10
434
+ 2
435
+ unchanged
436
+ /path/to/gemset/remote_ruby/lib/remote_ruby/execution_context.rb:36:in 'RemoteRuby::ExecutionContext#execute': Remote error: StandardError (RemoteRuby::RemoteError)
437
+ remote error text
438
+
439
+ from /tmp/remote_ruby.qwGUHP:71:in `block in <main>'
440
+ (See /home/jdoe/Work/remote_ruby_test/.remote_ruby/code/dcbb2493288b1d10be042a32a31bf8af43da660234f1731f03966aa67ac870e3.rb:71:in `block in <main>'
441
+ 68:
442
+ 69: # Start of client code
443
+ 70: a = 10
444
+ 71: >> raise(StandardError.new("remote error text"))
445
+ 72: b = 20
446
+ 73: "changed"
447
+ 74: # End of client code
448
+ ```
449
+
450
+ 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`.
451
+
452
+ 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.
453
+
284
454
 
285
455
  ### Caching
286
456
 
@@ -293,10 +463,9 @@ RemoteRuby allows you to save the result of previous block excutions in the loca
293
463
 
294
464
  require 'remote_ruby'
295
465
 
296
- res = remotely(server: 'my_ssh_server', save_cache: true, use_cache: true) do
466
+ res = remotely(host: 'my_ssh_server', save_cache: true, use_cache: true) do
297
467
  60.times do
298
468
  puts 'One second has passed'
299
- STDOUT.flush
300
469
  sleep 1
301
470
  end
302
471
 
@@ -312,48 +481,147 @@ RemoteRuby calculates the cache file to use, based on the code you pass to the r
312
481
 
313
482
  **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
483
 
315
- ### Adapters
484
+ ### Text mode
316
485
 
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.
486
+ 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 ouput, and the output coming from the remote code. Additionally it may help distinguishing when the output is taken from cache.
318
487
 
319
- #### SSH STDIN adapter
488
+ The text mode is controlled by the `text_mode` parameter to the `::RemoteRuby::ExecutionContext` initializer, and is `false` by default.
320
489
 
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.
490
+ The easiest way to enable it is to set `text_mode` to `true`.
322
491
 
323
- ##### Parameters
492
+ ```ruby
493
+ ctx = ::RemoteRuby::ExecutionContext.new(
494
+ host: 'my_ssh_server',
495
+ user: 'jdoe'
496
+ text_mode: true,
497
+ )
498
+
499
+ ctx.execute do
500
+ puts "This is a greeting"
501
+ warn "This is an error"
502
+ end
503
+ ```
324
504
 
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 |
505
+ This will produce:
332
506
 
507
+ ```
508
+ jdoe@my_ssh_server:~> This is a greeting
509
+ jdoe@my_ssh_server:~> This is a greeting
510
+ ```
333
511
 
334
- #### Local STDIN adapter
512
+ By default, the prefixes for stdout and stderr 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 ouput line.
335
513
 
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
514
 
515
+ You can fine-tune the text mode to your needs by passing a hash as a value to `text_mode` parameter:
338
516
 
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` |
517
+ ```ruby
518
+ ctx = ::RemoteRuby::ExecutionContext.new(
519
+ host: 'my_ssh_server',
520
+ user: 'jdoe'
521
+ text_mode: {
522
+ stdout_prefix: 'server says: ',
523
+ stdout_prefix: 'server warns: ',
524
+ stdout_mode: { color: :blue, mode: :underline }
525
+ }
526
+ )
527
+ ```
343
528
 
529
+ This will produce
344
530
 
345
- #### Evaluating adapter
531
+ ```
532
+ server says: This is a greeting
533
+ server warns: This is a greeting
534
+ ```
346
535
 
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.
536
+ It is reasonable to avoid text mode if you want to put binary data to stdout:
537
+
538
+ ```ruby
539
+ # copy_avatar.rb
540
+ # Reads a file from remote server and writes it to client's stdout.
541
+
542
+ remotely(host: 'my_ssh_server', text_mode: false) do
543
+ STDOUT.write File.read('avatar.jpg')
544
+ end
545
+ ```
546
+
547
+ Now you could do:
548
+
549
+ ```shell
550
+ ruby copy_avatar.rb > avatar.jpg
551
+ ```
348
552
 
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
553
 
554
+ The complete list of text mode parameters is in the table below:
354
555
 
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:
556
+ | Parameter | Type | Default value | Description |
557
+ |-|-|-|-|
558
+ | stdout_prefix | String | `user@host:/path/to/working/dir> ` | Prepended to standard output lines. Set to `nil` to disable |
559
+ | stderr_prefix | String | `user@host:/path/to/working/dir> ` | Prepended to standard error lines. Set to `nil` to disable |
560
+ | 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 |
561
+ | 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 |
562
+ | 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.
563
+ | 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.
564
+ | 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.
565
+
566
+ ### Plugins
567
+ 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.
568
+
569
+ #### Adding custom plugins
570
+
571
+ 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.
572
+
573
+ You may inherit your class from `::RemoteRuby::Plugin` class but that is not necessary.
574
+
575
+ Let's take a look at an example plugin:
576
+
577
+ ```ruby
578
+ class UsernamePlugin < RemoteRuby::Plugin
579
+ # This plugin prints a name of the user on the calling host.
580
+ attr_reader :username
581
+
582
+ def initialize(username:)
583
+ @username = username
584
+ end
585
+
586
+ def code_header
587
+ <<~RUBY
588
+ puts "This code is run by #{username}"
589
+ RUBY
590
+ end
591
+ end
592
+ ```
593
+
594
+ In order to be used, the plugin needs to be registered. You can register a plugin by calling `#register_plugin` method.
595
+
596
+ ```ruby
597
+ RemoteRuby.configure do |c|
598
+ c.register_plugin(:username_printer, UsernamePlugin)
599
+ end
600
+ ```
601
+
602
+ 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.
603
+
604
+ ```ruby
605
+ ec = RemoteRuby::ExecutionContext.new(
606
+ host: 'my_ssh_server',
607
+ username_printer: { username: ENV['USER'] }
608
+ )
609
+
610
+ ec.execute do
611
+ puts "Hello world!"
612
+ end
613
+ ```
614
+
615
+ This should print the following:
616
+
617
+ ```
618
+ This code is run by jdoe
619
+ Hello world!
620
+ ```
621
+
622
+
623
+ #### Rails plugin
624
+ RemoteRuby can load Rails environment for you, if you want to execute a script in a Rails context. To do this, simply add add built-in Rails plugin by adding `rails` argument to your call:
357
625
 
358
626
  ```ruby
359
627
  # Rails integration example
@@ -361,7 +629,7 @@ RemoteRuby can load Rails environment for you, if you want to execute a script i
361
629
  require 'remote_ruby'
362
630
 
363
631
  remote_service = ::RemoteRuby::ExecutionContext.new(
364
- server: 'rails-server',
632
+ host: 'rails-server',
365
633
  working_dir: '/var/www/rails_app/www/current',
366
634
  # This specifies ENV['RAILS_ENV'] and can be changed
367
635
  rails: { environment: :production }
@@ -377,7 +645,6 @@ end
377
645
  puts phone
378
646
  ```
379
647
 
380
-
381
648
  ## Contributing
382
649
 
383
650
  Bug reports and pull requests are welcome on GitHub at https://github.com/nu-hin/remote_ruby.