mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
4222f1ed32
Summary: This switches to using stdin/stdout for RPC calls to the sandbox, rather than specially allocated side channels. Plain text error information remains on stderr. The motivation for the change is to simplify use of sandboxes, some of which support extra file descriptors and some of which don't. The new style of communication is made the default, but I'm not committed to this, just that it be easy to switch to if needed. It is possible I'll need to switch the communication method again in the near future. One reason not to make this default would be windows support, which is likely broken since stdin/stdout are by default in text mode. Test Plan: existing tests pass Reviewers: dsagal, alexmojaki Reviewed By: dsagal, alexmojaki Differential Revision: https://phab.getgrist.com/D2897
130 lines
4.0 KiB
Python
130 lines
4.0 KiB
Python
"""
|
|
Implements the python side of the data engine sandbox, which allows us to register functions on
|
|
the python side and call them from Node.js.
|
|
|
|
Usage:
|
|
import sandbox
|
|
sandbox.register(func_name, func)
|
|
sandbox.call_external("hello", 1, 2, 3)
|
|
sandbox.run()
|
|
"""
|
|
|
|
import os
|
|
import marshal
|
|
import sys
|
|
import traceback
|
|
|
|
def log(msg):
|
|
sys.stderr.write(str(msg) + "\n")
|
|
sys.stderr.flush()
|
|
|
|
class Sandbox(object):
|
|
"""
|
|
This class works in conjunction with Sandbox.js to allow function calls
|
|
between the Node process and this sandbox.
|
|
|
|
The sandbox provides two pipes (on fds 3 and 4) to send data to and from the sandboxed
|
|
process. Data on these is serialized using `marshal` module. All messages are comprised of a
|
|
msgCode followed immediatedly by msgBody, with the following msgCodes:
|
|
CALL = call to the other side. The data must be an array of [func_name, arguments...]
|
|
DATA = data must be a value to return to a call from the other side
|
|
EXC = data must be an exception to return to a call from the other side
|
|
"""
|
|
|
|
CALL = None
|
|
DATA = True
|
|
EXC = False
|
|
|
|
def __init__(self, external_input, external_output):
|
|
self._functions = {}
|
|
self._external_input = external_input
|
|
self._external_output = external_output
|
|
|
|
@classmethod
|
|
def connected_to_js_pipes(cls):
|
|
"""
|
|
Send data on two specially-opened side channels.
|
|
"""
|
|
external_input = os.fdopen(3, "rb", 64 * 1024)
|
|
external_output = os.fdopen(4, "wb", 64 * 1024)
|
|
return cls(external_input, external_output)
|
|
|
|
@classmethod
|
|
def use_common_pipes(cls):
|
|
"""
|
|
Send data via stdin/stdout, rather than specially-opened side channels.
|
|
Duplicate stdin/stdout, close, and reopen as binary file objects.
|
|
"""
|
|
os.dup2(0, 3)
|
|
os.dup2(1, 4)
|
|
os.close(0)
|
|
os.close(1)
|
|
sys.stdout = sys.stderr
|
|
return Sandbox.connected_to_js_pipes()
|
|
|
|
def _send_to_js(self, msgCode, msgBody):
|
|
# (Note that marshal version 2 is the default; we specify it explicitly for clarity. The
|
|
# difference with version 0 is that version 2 uses a faster binary format for floats.)
|
|
|
|
# For large data, JS's Unmarshaller is very inefficient parsing it if it gets it piecewise.
|
|
# It's much better to ensure the whole blob is sent as one write. We marshal the resulting
|
|
# buffer again so that the reader can quickly tell how many bytes to expect.
|
|
buf = marshal.dumps((msgCode, msgBody), 2)
|
|
marshal.dump(buf, self._external_output, 2)
|
|
self._external_output.flush()
|
|
|
|
def call_external(self, name, *args):
|
|
self._send_to_js(Sandbox.CALL, (name,) + args)
|
|
(msgCode, data) = self.run(break_on_response=True)
|
|
if msgCode == Sandbox.EXC:
|
|
raise Exception(data)
|
|
return data
|
|
|
|
def register(self, func_name, func):
|
|
self._functions[func_name] = func
|
|
|
|
def run(self, break_on_response=False):
|
|
while True:
|
|
try:
|
|
msgCode = marshal.load(self._external_input)
|
|
data = marshal.load(self._external_input)
|
|
except EOFError:
|
|
break
|
|
if msgCode != Sandbox.CALL:
|
|
if break_on_response:
|
|
return (msgCode, data)
|
|
continue
|
|
|
|
if not isinstance(data, list) or len(data) < 1:
|
|
raise ValueError("Bad call " + data)
|
|
try:
|
|
fname = data[0]
|
|
args = data[1:]
|
|
ret = self._functions[fname](*args)
|
|
self._send_to_js(Sandbox.DATA, ret)
|
|
except Exception as e:
|
|
traceback.print_exc()
|
|
self._send_to_js(Sandbox.EXC, "%s %s" % (type(e).__name__, e))
|
|
if break_on_response:
|
|
raise Exception("Sandbox disconnected unexpectedly")
|
|
|
|
default_sandbox = None
|
|
|
|
def get_default_sandbox():
|
|
global default_sandbox
|
|
if default_sandbox is None:
|
|
if os.environ.get('PIPE_MODE') == 'minimal':
|
|
default_sandbox = Sandbox.use_common_pipes()
|
|
else:
|
|
default_sandbox = Sandbox.connected_to_js_pipes()
|
|
return default_sandbox
|
|
|
|
def call_external(name, *args):
|
|
return get_default_sandbox().call_external(name, *args)
|
|
|
|
def register(func_name, func):
|
|
get_default_sandbox().register(func_name, func)
|
|
|
|
def run():
|
|
get_default_sandbox().run()
|