|
|
|
|
|
|
|
How econtext Works
|
|
|
|
==================
|
|
|
|
|
|
|
|
Some effort is required to accomplish the seemingly magical feat of
|
|
|
|
bootstrapping a remote Python process without any software installed on the
|
|
|
|
remote machine. The steps involved are unlikely to be immediately obvious to
|
|
|
|
the casual reader, and they required several iterations to discover, so we
|
|
|
|
document them thoroughly below.
|
|
|
|
|
|
|
|
|
|
|
|
The UNIX First Stage
|
|
|
|
--------------------
|
|
|
|
|
|
|
|
To allow delivery of the bootstrap compressed using ``zlib``, it is necessary
|
|
|
|
for something on the remote to be prepared to decompress the payload and feed
|
|
|
|
it to a Python interpreter. Since we would like to avoid writing an error-prone
|
|
|
|
shell fragment to implement this, and since we must avoid writing to the remote
|
|
|
|
machine's disk in case it is read-only, the Python process started on the
|
|
|
|
remote machine by ``econtext`` immediately forks in order to implement the
|
|
|
|
decompression.
|
|
|
|
|
|
|
|
|
|
|
|
Python Command Line
|
|
|
|
###################
|
|
|
|
|
|
|
|
The Python command line sent to the host is a base64-encoded copy of the
|
|
|
|
:py:meth:`econtext.master.LocalStream._first_stage` function, which has been
|
|
|
|
carefully optimized to reduce its size. Prior to base64 encoding,
|
|
|
|
``CONTEXT_NAME`` is replaced with the desired context name in the function's
|
|
|
|
source code.
|
|
|
|
|
|
|
|
.. code::
|
|
|
|
|
|
|
|
python -c 'exec "xxx".decode("base64")'
|
|
|
|
|
|
|
|
The command-line arranges for the Python interpreter to decode the base64'd
|
|
|
|
component and execute it as Python code. Base64 is used since the first stage
|
|
|
|
implementation contains newlines, and many special characters that may be
|
|
|
|
interpreted by the system shell in use.
|
|
|
|
|
|
|
|
|
|
|
|
Forking The First Stage
|
|
|
|
#######################
|
|
|
|
|
|
|
|
The first stage creates a UNIX pipe and saves a copy of the process's real
|
|
|
|
``stdin`` file descriptor (used for communication with the master) so that it
|
|
|
|
can be recovered by the bootstrapped process later. It then forks into a new
|
|
|
|
process.
|
|
|
|
|
|
|
|
After fork, the parent half overwrites its ``stdin`` with the read end of the
|
|
|
|
pipe, and the child half begins reading the ``zlib``-compressed payload
|
|
|
|
supplied on ``stdin`` by the econtext master, and writing the decompressed
|
|
|
|
result to the write-end of the UNIX pipe.
|
|
|
|
|
|
|
|
To allow recovery of ``stdin`` for reuse by the bootstrapped process for
|
|
|
|
master<->slave communication, it is necessary for the first stage to avoid
|
|
|
|
closing ``stdin`` or reading from it until until EOF. Therefore, the master
|
|
|
|
sends the zlib-compressed payload prefixed with an integer size, allowing
|
|
|
|
reading by the first stage of exactly the required bytes.
|
|
|
|
|
|
|
|
|
|
|
|
Configuring argv[0]
|
|
|
|
###################
|
|
|
|
|
|
|
|
Forking provides us with an excellent opportunity for tidying up the eventual
|
|
|
|
Python interpreter, in particular, restarting it using a fresh command-line to
|
|
|
|
get rid of the large base64-encoded first stage parameter, and to replace
|
|
|
|
**argv[0]** with something descriptive.
|
|
|
|
|
|
|
|
After configuring its ``stdin`` to point to the read end of the pipe, the
|
|
|
|
parent half of the fork re-executes Python, with **argv[0]** taken from the
|
|
|
|
``CONTEXT_NAME`` variable earlier substituted into its source code. As no
|
|
|
|
arguments are provided to this new execution of Python, and since ``stdin`` is
|
|
|
|
connected to a pipe (whose write end is connected to the first stage), the
|
|
|
|
Python interpreter begins reading source code to execute from the pipe
|
|
|
|
connected to ``stdin``.
|
|
|
|
|
|
|
|
|
|
|
|
Bootstrap Preparation
|
|
|
|
#####################
|
|
|
|
|
|
|
|
Now we have the mechanism in place to send a zlib-compressed script to the
|
|
|
|
remote Python interpreter, it is time to choose what to send.
|
|
|
|
|
|
|
|
The script sent is simply the source code for :py:mod:`econtext.core`, with a
|
|
|
|
single line suffixed to trigger execution of the
|
|
|
|
:py:meth:`econtext.core.ExternalContext.main` function. The encoded arguments
|
|
|
|
to the main function include some additional details, such as the logging package
|
|
|
|
level that was active in the parent process, and a random secret key used to
|
|
|
|
generate HMAC signatures over the data frames that will be exchanged after
|
|
|
|
bootstrap.
|
|
|
|
|
|
|
|
After the script source code is prepared, it is passed through
|
|
|
|
:py:func:`econtext.master.minimize_source` to strip it of docstrings and
|
|
|
|
comments, while preserving original line numbers. This reduces the compressed
|
|
|
|
payload size by around 20%.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Signalling Success
|
|
|
|
##################
|
|
|
|
|
|
|
|
|
|
|
|
ExternalContext main()
|
|
|
|
----------------------
|
|
|
|
|
|
|
|
|
|
|
|
Reaping The First Stage
|
|
|
|
#######################
|
|
|
|
|
|
|
|
|
|
|
|
Generating A Synthetic `econtext` Package
|
|
|
|
#########################################
|
|
|
|
|
|
|
|
|
|
|
|
Setup The Broker And Master Context
|
|
|
|
###################################
|
|
|
|
|
|
|
|
|
|
|
|
Setup Logging
|
|
|
|
#############
|
|
|
|
|
|
|
|
|
|
|
|
The Module Importer
|
|
|
|
###################
|
|
|
|
|
|
|
|
|
|
|
|
Standard IO Redirection
|
|
|
|
#######################
|
|
|
|
|
|
|
|
|
|
|
|
Function Call Dispatch
|
|
|
|
######################
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Stream Protocol
|
|
|
|
---------------
|
|
|
|
|
|
|
|
|
|
|
|
Use of HMAC
|
|
|
|
###########
|
|
|
|
|
|
|
|
In the current version of econtext, the use of HMAC signatures over data frames
|
|
|
|
is mostly redundant, since all communication occurs over SSH, however in order
|
|
|
|
to reduce resource usage, it is planned to support connecting back to the
|
|
|
|
master via plain TCP, at which point the signatures become important.
|
|
|
|
|
|
|
|
|
|
|
|
The IO Multiplexer
|
|
|
|
------------------
|
|
|
|
|
|
|
|
Since we must include our IO multiplexer as part of the bootstrap,
|
|
|
|
off-the-shelf implementations are for the most part entirely inappropriate. For
|
|
|
|
example, a minimal copy of Twisted weighs in at around 440KiB and is composed
|
|
|
|
of approximately 115 files. Even if we could arrange for an entire Python
|
|
|
|
package to be transferred during bootstrap, this minimal configuration is
|
|
|
|
massive in comparison to econtext's solution, and would require manually
|
|
|
|
splitting up the parts of Twisted that we would like to use.
|
|
|
|
|
|
|
|
|
|
|
|
Differences Between Master And Slave Brokers
|
|
|
|
############################################
|
|
|
|
|
|
|
|
* Self destruct.
|
|
|
|
|
|
|
|
|
|
|
|
The Module Importer
|
|
|
|
-------------------
|
|
|
|
|
|
|
|
Child Package Enumeration
|
|
|
|
#########################
|
|
|
|
|
|
|
|
|
|
|
|
Negative Cache Hits
|
|
|
|
###################
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Use Of Threads
|
|
|
|
--------------
|
|
|
|
|
|
|
|
The package mandatorily runs the IO multiplexer in a thread. This is so the
|
|
|
|
multiplexer always retains control flow in order to shut down gracefully, say,
|
|
|
|
if the user's code has hung and the master context has disconnected.
|
|
|
|
|
|
|
|
While it is possible for the IO multiplexer to recover control of a hung
|
|
|
|
function call on UNIX using for example ``signal.SIGALRM``, this mechanism is
|
|
|
|
not portable to non-UNIX operating systems, and does not work in every case,
|
|
|
|
for example when Python blocks signals during a variety of :py:mod:`threading`
|
|
|
|
package operations.
|
|
|
|
|
|
|
|
At some point it is likely econtext will be extended to support starting slaves
|
|
|
|
running on Windows. When that happens, it would be nice if the process model on
|
|
|
|
Windows and UNIX did not differ, and in fact the code used on both were
|
|
|
|
identical.
|