-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Run reports in a separate Python process.
The runpy module we've been using to run reports provides very little isolation. In particular, "any side effects (such as cached imports of other modules) will remain in place after the functions have returned". This means that if a user splits their report into multiple files, with one file importing another, the latter will be cached and never reloaded by Python, even if it is modified. Similarly, if the module was already loaded before, the report runs against old code that does not match the files included in the packet. Using a separate subprocess gives us a much stronger isolation, including all global variables and not sharing any of previously loaded modules. It means a user can run a report, modify some of the imported code, run the report again and have this behave as expected. The sandbox is implemented by pickling the function and its argument into a file, starting a new Python process which unpickles the file and calls the target. Similarly, the function's return value, or any exception it may throw, it pickled to an output file, which is read by the parent. I tried to use the multiprocessing module to implement this, but it puts inconvenient requirements on the way the top-level source file is written. The module re-evaluates the file in the subprocess, meaning its contents must check for `__name__ == "__main__"` to avoid doing the work again. It also doesn't work too well with the REPL. Using our own implementation fixes these issues. The Packet class is refactored a little and its scope is reduced to just creating the packet and its metadata, without inserting it into the repository. The insertion is now handled by a separate function. This is done as a way of distinguishing what happens in the child process and what happens in the parent.
- Loading branch information
Showing
13 changed files
with
407 additions
and
149 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import os | ||
import pickle | ||
import subprocess | ||
import sys | ||
|
||
from tblib import pickling_support | ||
|
||
from outpack.util import openable_temporary_file | ||
|
||
|
||
def run_in_sandbox(target, args=(), cwd=None, syspath=None): | ||
""" | ||
Run a function as a separate process. | ||
The function, its arguments and return value must be picklable. | ||
Parameters | ||
---------- | ||
target: | ||
The function to run in the subprocess. This function must be accessible | ||
by name from the top level of a module in order to be pickled. | ||
args: | ||
The arguments to be passed to the function | ||
cwd: | ||
The working directory in which the subprocess runs. If None, it inherits | ||
the current process' working directory. | ||
syspath: | ||
A list of paths to be added to the child process' Python search path. | ||
This is used when the target function's module is not globally | ||
available. | ||
""" | ||
with openable_temporary_file() as input_file: | ||
with openable_temporary_file(mode="rb") as output_file: | ||
pickle.dump((target, args), input_file) | ||
input_file.flush() | ||
|
||
cmd = [ | ||
sys.executable, | ||
"-m", | ||
"outpack.sandbox", | ||
input_file.name, | ||
output_file.name, | ||
] | ||
|
||
if syspath is not None: | ||
env = os.environ.copy() | ||
pythonpath = ":".join(str(s) for s in syspath) | ||
|
||
if "PYTHONPATH" in env: | ||
env["PYTHONPATH"] = f"{pythonpath}:{env['PYTHONPATH']}" | ||
else: | ||
env["PYTHONPATH"] = pythonpath | ||
else: | ||
env = None | ||
|
||
p = subprocess.run(cmd, cwd=cwd, env=env, check=False) # noqa: S603 | ||
p.check_returncode() | ||
|
||
(ok, value) = pickle.load(output_file) # noqa: S301 | ||
if ok: | ||
return value | ||
else: | ||
raise value | ||
|
||
|
||
if __name__ == "__main__": | ||
with open(sys.argv[1], "rb") as input_file: | ||
(target, args) = pickle.load(input_file) # noqa: S301 | ||
|
||
try: | ||
result = (True, target(*args)) | ||
except BaseException as e: | ||
# This allows the traceback to be pickled and communicated out of the | ||
# the sandbox. | ||
pickling_support.install(e) | ||
result = (False, e) | ||
|
||
with open(sys.argv[2], "wb") as output_file: | ||
pickle.dump(result, output_file) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
def get_description(): | ||
return "Hello from module" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import orderly | ||
from helpers import get_description # type: ignore | ||
|
||
orderly.description(display=get_description()) |
Oops, something went wrong.