0.0
A long-lived project that still receives updates
Execute Ruby code on the remote servers from local Ruby script.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Runtime

~> 0.2
~> 1.0
~> 1.6
~> 7.3
~> 0.6
~> 3.3
~> 0.6
 Project Readme

remote_ruby

Lint & Test Coverage Status Maintainability Gem Version

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.

Changelog

Contents

  • Requirements
  • Overview
    • How it works
    • Key features
    • Limitations
  • Installation
  • Usage
    • Configuration
      • Basic usage
      • Output
      • Parameters
    • SSH Parameters
    • Local variables
    • Return value and remote assignments
    • Error handling
    • Caching
    • Text mode
    • Plugins
      • Adding custom plugins
      • Rails
  • Contributing
  • License

Requirements

RemoteRuby requires at least Ruby 2.7 to run.

Overview

Here is a short example of how you can run your code remotely.

# This is test.rb file on the local developer machine

require 'remote_ruby'

remotely(host: 'my_ssh_server') do
  # Everything inside this block is executed on my_ssh_server
  puts 'Hello, RemoteRuby!'
end

How it works

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 for more detail).

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.

Key features

  • Access local variables inside the remote block, just as in case of a regular block:
user_id = 1213

remotely(host: 'my_ssh_server') do
  puts user_id # => 1213
end
  • Access return value of the remote block, just as in case of a regular block:
res = remotely(host: 'my_ssh_server') do
  'My result'
end

puts res # => My result
  • Assignment to local variables inside remote block, just as in case of a regular block:
a = 1

remotely(host: 'my_ssh_server') do
  a = 100
end

puts a # => 100
  • Reading from the client's standard input in the remote block:
remotely(host: 'my_ssh_server') do
  puts 'What is your name?'
  name = gets
  puts "Hello, #{name}!"
end

Limitations

  • 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.

  • 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).

  • 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 section below for more details.

Installation

Add this line to your application's Gemfile:

gem 'remote_ruby'

And then execute:

bundle

Alternatively, install RemoteRuby in your gemset with the following command:

gem install remote_ruby

Usage

Configuration

There are a few options that may be configured on the global level.

RemoteRuby.configure do |c|
  # Defines, where Remote Ruby will cache output, error and result streams on the
  # local machine.
  # By default they are saved to .remote_ruby/cache (relative to the working directory).
  c.cache_dir = File.join(Dir.pwd, '.remote_ruby/cache')

  # Defines, where Remote Ruby will store compiled code on the local machine, if
  # `dump_code` is set to `true` in the ExecutionContext.
  # By default code is saved to .remote_ruby/code (relative to the working directory).
  c.code_dir = File.join(Dir.pwd, '.remote_ruby/code')

  # Set to true if you don't want to see warnings about parser gem compatibility with
  # current Ruby version.
  # False by default.
  c.suppress_parser_warnings = false
end

Basic usage

The main class to work with is the ExecutionContext, which provides an #execute method:

my_server = ::RemoteRuby::ExecutionContext.new(host: 'my_ssh_server')

my_server.execute do
  puts Dir.pwd
end

You can easily define more than one context to access several servers.

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:

include RemoteRuby::Extensions

remotely(host: 'my_ssh_server') do
  puts Dir.pwd
end

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).

In all the examples in this document, where .remote method is used, it is assumed, that RemoteRuby::Extensions is included to the scope.

Parameters

Parameter Type Required Default value Description
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
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
working_dir String no '~' if running over SSH, or current dir, if running locally Path to the directory where the script should be executed
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 section to find out more about caching.
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 section to find out more about caching.
RemoteRuby will try to create it. Refer to the Caching section to find out more about caching.
in_stream Stream open for reading no $stdin Source stream for server standard input
out_stream Stream open for writing no $stdout Redirection stream for server standard output
err_stream Stream open for writing no $stderr Redirection stream for server standard error
text_mode Boolean or Hash no false Specifies, if the connection should be run in text mode. See Text Mode section below to find out more about text mode.
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 to configure where the code is written.

SSH Parameters

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. The complete list of parameters can be found in the documentation for net-ssh. Some of the parameters are in the table below.

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.

Parameter Type Description
user String the user name to log in as
password String the password to use to log in
keys Array of strings an array of file names of private keys to use for publickey and hostbased authentication
passphrase String the passphrase to use when loading a private key (default is nil, for no passphrase)
auth_methods Array of strings an array of authentication methods to try

Example SSH configurations may look like:

# Use ~/.ssh/config file, but override some parameters
ec1 = RemoteRuby::ExecutionContext.new(
  host: 'my_ssh_server',
  auth_methods: %w(password),
  user: 'jdoe',
  password: 'p@ssw0rd'
)

# Custom key file
ec2 = RemoteRuby::ExecutionContext.new(
  host: 'my_ssh_server',
  keys: '/home/jdoe/.ssh/custom_id_rsa'
)

# Ignore SSH configuration and provide everything explicitly
ec3 = RemoteRuby::ExecutionContext.new(
  host: 'my_ssh_server',
  use_ssh_config_file: false,
  auth_methods: %w(password),
  user: 'jdoe',
  password: 'p@ssw0rd'
)

Output

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.

remotely(host: 'my_ssh_server', working_dir: '/home/john') do
  puts 'This is an output'
  warn 'This is a warning'
end

Input

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.

name = remotely(host: 'my_ssh_server') do
  puts "What is your name?"
  gets
end

puts "Hello locally, #{name}!"

Local variables

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.

If you do not want all local variables to be sent to the server, you can explicitly specify a set of local variables and their values.

some_number = 3
name = 'Alice'

# Explicitly setting locals with .remotely method
remotely(locals: { name: 'John Doe' }, host: 'my_ssh_server') do
  # name is 'John Doe', not 'Alice'
  puts name # => John Doe
  # some_number is not defined
  puts some_number # undefined local variable or method `some_number'
end

# Explicitly setting locals with ExecutionContext#execute method
execution_context = ::RemoteRuby::ExecutionContext.new(host: 'my_ssh_server')

execution_context.execute(name: 'John Doe') do
  # name is 'John Doe', not 'Alice'
  puts name # => John Doe
  # some_number is not defined
  puts some_number # undefined local variable or method `some_number'
end

However, some objects cannot be serialized. In this case, RemoteRuby will print a warning, and the variable will not be defined inside the remote block.

# We cannot serialize a file stream
file = File.open('some_file.txt', 'rb')

remotely(host: 'my_ssh_server') do
  puts file.read # undefined local variable or method `file'
end

Moreover, if such variables are assigned to in the remote block, their value will not change in the calling scope:

file = File.open('some_file.txt', 'rb')

remotely(host: 'my_ssh_server') do
  file = 3 # No exception here, as we are assigning
end

# Old value is retained
puts file == 3 # false

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:

# Something, that is not present on the remote server
special_thing = SomeSpecialGem::SpecialThing.new

remotely(host: 'my_ssh_server') do
  puts defined?(special_thing) # => local-variable
  # special_thing is defined, but its value is nil
  puts special_thing.nil? # => true

  # but we can still reassign it:
  special_thing = 3
end

puts special_thing == 3 # => true

If RemoteRuby cannot deserialize variable on server side, it will print a warning to server's standard error.

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:

RemoteRuby::Configure do |c|
  c.ignore_types SomeSpecialGem::SpecialThing
end

If a type is ignored, the remote block will behave as if the local variable is not defined:

RemoteRuby::Configure do |c|
  c.ignore_types SomeSpecialGem::SpecialThing
end

special_thing = SomeSpecialGem::SpecialThing.new

remotely(host: 'my_ssh_server') do
  puts defined?(special_thing) # => nil

  # special_thing is not defined
  puts special_thing.nil? # NameError undefined local variable or method `special_thing' for main:Object
end

RemoteRuby always ignores variables of type RemoteRuby::ExecutionContext.

Return value and remote assignments

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:

# Unsupported return value example

remotely(host: 'my_ssh_server') do
  # this is not present in the client context
  server_specific_var = ServerSpecificClass.new
end

# undefined class/module ServerSpecificClass (ArgumentError)
# Unsupported local value example

my_local = nil

remotely(host: 'my_ssh_server') do
  # this is not present in the client context
  my_local = ServerSpecificClass.new
  nil
end

# undefined class/module ServerSpecificClass (ArgumentError)

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:

# No exception

remotely(host: 'my_ssh_server') do
  # this is not present in the client context
  server_specific_var = ServerSpecificClass.new
  nil
end

Error handling

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.

a = 1
b = 2
res = 'unchanged'
begin
  res = remotely(host: 'my_ssh_server', dump_code: true) do
    a = 10
    raise StandardError.new('remote error text')
    b = 20
    'changed'
  end
rescue RemoteRuby::RemoteError
  puts a
  puts b
  puts res

  raise
end

This will produce something like the following.

10
2
unchanged
/path/to/gemset/remote_ruby/lib/remote_ruby/execution_context.rb:36:in 'RemoteRuby::ExecutionContext#execute': Remote error: StandardError (RemoteRuby::RemoteError)
remote error text

from /tmp/remote_ruby.qwGUHP:71:in `block in <main>'
(See /home/jdoe/Work/remote_ruby_test/.remote_ruby/code/dcbb2493288b1d10be042a32a31bf8af43da660234f1731f03966aa67ac870e3.rb:71:in `block in <main>'
68:
69:      # Start of client code
70:      a = 10
71: >>   raise(StandardError.new("remote error text"))
72:      b = 20
73:      "changed"
74:      # End of client code

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.

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.

Caching

RemoteRuby allows you to save the result of previous block excutions in the local cache on the client machine to save you time on subsequent script runs. To enable saving of the cache, set save_cache: true parameter. To turn reading from cache on, use use_cache: true parameter.

# Caching example
# First time this script will take 60 seconds to run,
# but on subsequent runs it will return the result immediately

require 'remote_ruby'

res = remotely(host: 'my_ssh_server', save_cache: true, use_cache: true) do
  60.times do
    puts 'One second has passed'
    sleep 1
  end

  'Some result'
end

puts res # => Some result

You can specify where to put your cache files explicitly, by configuring the cache_dir which is by default set to ".remote_ruby/cache" inside your current working directory.

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.

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.

Text mode

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.

The text mode is controlled by the text_mode parameter to the ::RemoteRuby::ExecutionContext initializer, and is false by default.

The easiest way to enable it is to set text_mode to true.

ctx = ::RemoteRuby::ExecutionContext.new(
  host: 'my_ssh_server',
  user: 'jdoe'
  text_mode: true,
)

ctx.execute do
  puts "This is a greeting"
  warn "This is an error"
end

This will produce:

jdoe@my_ssh_server:~> This is a greeting
jdoe@my_ssh_server:~> This is a greeting

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.

You can fine-tune the text mode to your needs by passing a hash as a value to text_mode parameter:

ctx = ::RemoteRuby::ExecutionContext.new(
  host: 'my_ssh_server',
  user: 'jdoe'
  text_mode: {
    stdout_prefix: 'server says: ',
    stdout_prefix: 'server warns: ',
    stdout_mode: { color: :blue, mode: :underline }
  }
)

This will produce

server says: This is a greeting
server warns: This is a greeting

It is reasonable to avoid text mode if you want to put binary data to the standard output:

# copy_avatar.rb
# Reads a file from remote server and writes it to client's standard output.

remotely(host: 'my_ssh_server', text_mode: false) do
  STDOUT.write File.read('avatar.jpg')
end

Now you could do:

ruby copy_avatar.rb > avatar.jpg

The complete list of text mode parameters is in the table below:

Parameter Type Default value Description
stdout_prefix String user@host:/path/to/working/dir> Prepended to standard output lines. Set to nil to disable
stderr_prefix String user@host:/path/to/working/dir> Prepended to standard error lines. Set to nil to disable
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
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
stdout_mode Hash { color: :green, mode: :italic } Text effects and colors applied to the standard output prefix. See colorize gem for available parameters.
stderr_mode Hash { color: :red, mode: :italic } Text effects and colors applied to the standard error prefix. See colorize gem for available parameters.
cache_mode Hash { color: :blue, mode: :bold } Text effects and colors applied to the cache prefix. See colorize gem for available parameters.

Plugins

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.

Adding custom plugins

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.

You may inherit your class from ::RemoteRuby::Plugin class but that is not necessary.

Let's take a look at an example plugin:

class UsernamePlugin < RemoteRuby::Plugin
# This plugin prints a name of the user on the calling host.
  attr_reader :username

  def initialize(username:)
    @username = username
  end

  def code_header
    <<~RUBY
      puts "This code is run by #{username}"
    RUBY
  end
end

In order to be used, the plugin needs to be registered. You can register a plugin by calling #register_plugin method.

RemoteRuby.configure do |c|
  c.register_plugin(:username_printer, UsernamePlugin)
end

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.

  ec = RemoteRuby::ExecutionContext.new(
    host: 'my_ssh_server',
    username_printer: { username: ENV['USER'] }
  )

  ec.execute do
    puts "Hello world!"
  end

This should print the following:

This code is run by jdoe
Hello world!

Rails plugin

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:

# Rails integration example

require 'remote_ruby'

remote_service = ::RemoteRuby::ExecutionContext.new(
  host: 'rails-server',
  working_dir: '/var/www/rails_app/www/current',
  # This specifies ENV['RAILS_ENV'] and can be changed
  rails: { environment: :production }
 )

user_email = 'john_doe@mydomain.com'

phone = remote_service.execute do
  user = User.find_by(email: user_email)
  user.try(:phone)
end

puts phone

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/nu-hin/remote_ruby.

License

The gem is available as open source under the terms of the MIT License.