RPyC provides remote procedure call, or more precisely, Remote Python Call between two computers.

On both machines we need to have Python and the RPyC package installed and on the server we need to launch a process before we can talk to it. The nice thing is that it runs on all the 3 major Operating systems: Linux, Mac OSX, and MS Windows, and you can make RPyC between machines running different OS-es.

Install RPyC

It is simple just type

pip install rpyc

Run the RPyC server

Nothing special needed. just run the rpyc_classic.py script that came with the RPyC installation:

rpyc_classic.py

It will print something like this to the console, and then it will wait for a client to connect. As the client access the server we will see further output appearing on the console.

INFO:SLAVE/18812:server started on [0.0.0.0]:18812

Simple module use

examples/python/rpyc_modules.py

import rpyc
import sys

if len(sys.argv) < 2:
   exit("Usage {} SERVER".format(sys.argv[0]))

server = sys.argv[1]

conn = rpyc.classic.connect(server)
rsys = conn.modules.sys
print(rsys.version)

ros = conn.modules.os
print(ros.uname())
 

This example, as all the other examples here require the name or IP address of remote server on the command line. So you'd run

python rpyc_modules.py remote_machine

If you wanted to run this on remote_machine

or you'd run

python rpyc_modules.py localhost

if you had only one computer and wanted to see how you can talk to yourself.

In this simple script we connect to a remote server using the following call:

conn = rpyc.classic.connect(server)

Then we can use the connection object stored in conn to execute commands on the remote server.

We can attache a local name to a remote Python object. For example this line connects the rsys name on our client to the sys object on the server. (It automatically imports the sys module on the server.)

rsys = conn.modules.sys

Once we have made that call we can use the rsys on the client just as we would use sys on the server. For example we can access its attributes and print them on the console of the client:

print(rsys.version)

The same with the os module in the above code.

Persistent variables on the server

On the next example we use the execute and eval methods of the connection object. The execute method will, execute arbitrary code on the server. In our example first we create a variable with a new value. Then we change the content of the variable.

Finally we use the eval method to evaluate an expression and return the results.

examples/python/rpyc_variables.py

import rpyc
import sys

if len(sys.argv) < 2:
   exit("Usage {} SERVER".format(sys.argv[0]))

server = sys.argv[1]

conn = rpyc.classic.connect(server)
conn.execute('x = 21')
conn.execute('x *= 2')
print(conn.eval('x'))     # 42


conn.execute('scores = { "Foo" : 10 }')
conn.execute('scores["Foo"] += 1')
conn.execute('scores["Bar"] = 42')

local_scores = conn.eval('scores')
print(local_scores)         # {'Foo': 11, 'Bar': 42}
print(local_scores['Foo'])  # 11


conn.namespace["scores"]["Bar"] += 58
print(conn.eval('scores'))  # {'Foo': 11, 'Bar': 100}

The super nice thing is that it will take any data structure we have on the server and replicate it locally. So in the second set of calls we create a dictionary on the server, we modify it, and then we copy it back to the client using the eval method.

On the client we get a copy of the remote object.

Better yet, we can use the namespace attribute of the connection object which is a dictionary where the keys are the object in on the remote server. For example the variables we created there. Thus conn.namespace["scores"] refers to the object called scores on the remote machine. We can access and even change the object and its attributes with regular Python code like this:

conn.namespace["scores"]["Bar"] += 58

Remote exception handling

examples/python/rpyc_exception.py

import rpyc
import sys

if len(sys.argv) < 2:
   exit("Usage {} SERVER".format(sys.argv[0]))

server = sys.argv[1]

conn = rpyc.classic.connect(server)
conn.execute('x = 42')
conn.execute('y = 0')
conn.execute('z = x/y')

print("Hello")

If an exception occurs on the server, it is printed to the console of the server and the exception itself is propagated to the client.

Meaning that the last print-statement will not be executed.

You can catch the remote exception locally the same way you'd catch local exceptions.

Traceback (most recent call last):
  File "rpyc_exception.py", line 12, in <module>
    conn.execute('z = x/y')
  File "/Users/gabor/venv3/lib/python3.6/site-packages/rpyc/core/netref.py", line 199, in __call__
    return syncreq(_self, consts.HANDLE_CALL, args, kwargs)
  File "/Users/gabor/venv3/lib/python3.6/site-packages/rpyc/core/netref.py", line 72, in syncreq
    return conn.sync_request(handler, oid, *args)
  File "/Users/gabor/venv3/lib/python3.6/site-packages/rpyc/core/protocol.py", line 523, in sync_request
    raise obj
ZeroDivisionError: division by zero

========= Remote Traceback (1) =========
Traceback (most recent call last):
  File "/home/gabor/venv3/lib/python3.6/site-packages/rpyc/core/protocol.py", line 347, in _dispatch_request
    res = self._HANDLERS[handler](self, *args)
  File "/home/gabor/venv3/lib/python3.6/site-packages/rpyc/core/protocol.py", line 624, in _handle_call
    return self._local_objects[oid](*args, **dict(kwargs))
  File "/home/gabor/venv3/lib/python3.6/site-packages/rpyc/core/service.py", line 154, in exposed_execute
    execute(text, self.exposed_namespace)
  File "<string>", line 1, in <module>
ZeroDivisionError: division by zero

Upload code - run function

Based on the above example we could upload and execute any Python code. If that was a function definition then we would effectively create a new function on the remote server which can be later executed.

For clarity I've moved the code that should be executed remotely to a separate file. It is a simple implementation of the Fibonacci function:

examples/python/remote_code.py

def fib(n):
    if n == 1:
        return [1]
    if n == 2:
        return [1, 1]

    values = [1, 1]
    for _ in range(2, n):
        values.append(values[-1] + values[-2])
    return values
    

Then there is code that will load this file in the client as a plain text, then call execute on the connection object to execute the function definition on the remote machine.

After we executed the code, we can use the namespace attribute of the connection to access the function and call it with any parameter.

examples/python/rpyc_exception.py

import rpyc
import sys

if len(sys.argv) < 2:
   exit("Usage {} SERVER".format(sys.argv[0]))

server = sys.argv[1]

conn = rpyc.classic.connect(server)
conn.execute('x = 42')
conn.execute('y = 0')
conn.execute('z = x/y')

print("Hello")

examples/python/rpyc_timeout.py

import rpyc
import sys

if len(sys.argv) < 2:
   exit("Usage {} SERVER".format(sys.argv[0]))

server = sys.argv[1]


conn = rpyc.classic.connect(server)
#conn = rpyc.classic.factory.connect(server, 18812, rpyc.classic.SlaveService, config={ 'sync_request_timeout' : 1 }, ipv6 = False, keepalive = False)
my_code = '''
import time
def wait_a_bit(n):
    start = time.time()
    #for _ in range(n):
    #    time.sleep(1)
    time.sleep(n)
    end = time.time()
    return { "start" : start, "end" : end, "diff" : int(end - start) }
'''
conn.execute(my_code)

rf = conn.namespace['wait_a_bit']
print(rf(50))


examples/python/rpyc_reconnect_objects.py

import sys
import time
from rpyc_with_reconnect import ReConn

if len(sys.argv) < 2:
   exit("Usage {} SERVER".format(sys.argv[0]))

conn = ReConn(sys.argv[1])

conn.execute('x = 21')
print(conn.eval('x'))     # 21
print("You have 10 sec to restart the RPyC server")
time.sleep(10)

conn.execute('y = 42')
print(conn.eval('y'))     # 42

print("You have 10 sec to restart the RPyC server")
time.sleep(10)
print(conn.eval('6*7'))     # 42


#conn.execute('x = 21')
#print(conn.eval('x'))     # 42
#print("assigned")
#time.sleep(80)
#print("waiting a bit more")
##time.sleep(20)
#conn.reconnect()
#conn.execute('x *= 2')
#print(conn.eval('x'))     # 42


examples/python/rpyc_ping.py

import rpyc
import sys
import time

if len(sys.argv) < 2:
   exit("Usage {} SERVER".format(sys.argv[0]))

server = sys.argv[1]

conn = rpyc.classic.connect(server)
rsys = conn.modules.sys
print(rsys.version)

for _ in range(10):
    print("pinging")
    conn.ping()
    #try:
    #    conn.ping()
    #except Exception as e:
    #    print(e)
    #    conn = rpyc.classic.connect(server)
    time.sleep(3)
    print("after wait")
    time.sleep(3)
    print("after more wait")

    # raise EOFError("connection closed by peer")

ros = conn.modules.os
print(ros.uname())
   # ConnectionRefusedError: [Errno 61] Connection refused 

examples/python/rpyc_with_reconnect.py

import rpyc
class ReConn(object):
    def __init__(self, server):
        self.server = server
        self.connect()

    def connect(self):
        print("connecting")
        self.conn = rpyc.classic.connect(self.server)

    def execute(self, code):
        try:
            self.conn.execute(code)
        except EOFError:
            self.connect()
            self.conn.execute(code)

    def eval(self, code):
        try:
            return self.conn.eval(code)
        except EOFError:
            self.connect()
            return self.conn.eval(code)

Modules problem, might be solved with properties L109