File: tut1.rst

package info (click to toggle)
rpyc 6.0.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,324 kB
  • sloc: python: 6,442; makefile: 122
file content (150 lines) | stat: -rw-r--r-- 5,189 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
.. _tut1:

Part 1: Introduction to *Classic RPyC*
======================================

We'll kick-start the tutorial with what is known as *classic-style* RPyC, i.e., the
methodology of RPyC 2.60. Since RPyC 3 is a complete redesign of the library, there are some
minor changes, but if you were familiar with RPyC 2.60, you'll feel right at home. And even
if you were not -- we'll make sure you feel at home in a moment ;)

Running a Server
----------------
Let's start with the basics: running a server. In this tutorial we'll run both the server and
the client on the same machine (the ``localhost``). The classic server can be
started using::

    $ python bin/rpyc_classic.py
    INFO:SLAVE/18812:server started on [127.0.0.1]:18812

This shows the parameters this server is running with:

- ``SLAVE`` indicates the ``SlaveService`` (you'll learn more about
  :ref:`services <services>` later on), and

- ``[127.0.0.1]:18812`` is the address on which the server binds, in this case
  the server will only accept connections from localhost. If you run a server
  with ``--host 0.0.0.0``, you are free for arbitrary code execution from
  anywhere.

Running a Client
----------------
The next step is running a client which connects to the server. The code needed to create a
connection to the server is quite simple, you'd agree ::

    import rpyc
    conn = rpyc.classic.connect("localhost")

If your server is not running on the default port (``TCP 18812``), you'll have
to pass the ``port=`` parameter to :func:`.classic.connect`.

The ``modules`` Namespace
-------------------------
The ``modules`` property of connection objects exposes the server's
module-space, i.e., it lets you access remote modules. Here's how::

    rsys = conn.modules.sys     # remote module on the server!

This *dot notation* only works for top level modules. Whenever you would
require a nested import for modules contained within a package, you have to
use the *bracket notation* to import the remote module, e.g.::

    minidom = conn.modules["xml.dom.minidom"]

With this alone you are already set to do almost anything. For example, here
is how you see the server's command line::

    >>> rsys.argv
    ['bin/rpyc_classic.py']

…add module search paths for the server's import mechanism::

    >>> rsys.path.append('/tmp/totally-secure-package-location)

…change the current working directory of the server process::

    >>> conn.modules.os.chdir('..')

…or even print something on the server's stdout:

    >>> print("Hello World!", file=conn.modules.sys.stdout)


The ``builtins`` Namespace
---------------------------

The ``builtins`` property of classic connection exposes all builtin functions
available in the server's python environment. You could use it for example to
access a file on the server:

    >>> f = conn.builtins.open('/home/oblivious/.ssh/id_rsa')
    >>> f.read()
    '-----BEGIN RSA PRIVATE KEY-----\nMIIJKQIBAAKCAgEA0...XuVmz/ywq+5m\n-----END RSA PRIVATE KEY-----\n'

Ooopsies, I just leaked my private key…;)

The ``eval`` and ``execute`` Methods
------------------------------------
If you are not satisfied already, here is more: Classic connections also have
properties ``eval`` and ``execute`` that allow you to directly evaluate
arbitrary expressions or even execute arbitrary statements on the server.
For example::

    >>> conn.execute('import math')
    >>> conn.eval('2*math.pi')
    6.283185307179586

But wait, this requires that rpyc classic connections have some notion of
global variables, how can you see them? They are accessible via the
``namespace`` property that will be initialized as empty dictionary for every
new connection. So, after our import, we now have::

    >>> conn.namespace
    {'__builtins__': <...>, 'math': <...>}

The aware reader will have noticed that neither of these shenanigans are
strictly needed, as the same functionality could be achieved by using the
``conn.builtins.compile()`` function, which is also accessible via
``conn.modules.builtins.compile()``, and manually feeding it with a remotely
created dict.

That's true, but we sometimes like a bit of sugar;)


The ``teleport`` method
-----------------------
There is another interesting method that allows you to transmit functions to
the other sides and execute them over there::

   >>> def square(x):
   ...    return x**2
   >>> fn = conn.teleport(square)
   >>> fn(2)

This calculates the square of two as expected, but the computation takes place
on the remote!

Furthermore, teleported functions are automatically defined in the remote
namespace::

   >>> conn.eval('square(3)')
   9

   >>> conn.namespace['square'] is fn
   True

And the teleported code can also access the namespace::

   >>> conn.execute('import sys')
   >>> version = conn.teleport(lambda: print(sys.version_info))
   >>> version()

prints the version on the remote terminal.

Note that currently it is not possible to teleport arbitrary functions, in
particular there can be issues with closures to non-trivial objects. In case
of problems it may be worth taking a look at external libraries such as dill_.

.. _dill: https://pypi.org/project/dill/

Continue to :ref:`tut2`...