Source code for law.sandbox.bash
# coding: utf-8
"""
Bash sandbox implementation.
"""
from __future__ import annotations
__all__ = ["BashSandbox"]
import os
import pickle
from law.sandbox.base import Sandbox
from law.task.proxy import ProxyCommand
from law.util import tmp_file, interruptable_popen, quote_cmd, flatten, makedirs
from law.config import Config
from law._types import Any
[docs]
class BashSandbox(Sandbox):
sandbox_type: str = "bash" # type: ignore[assignment]
config_section_prefix = sandbox_type
@property
def script(self) -> str:
return os.path.expandvars(os.path.expanduser(str(self.name)))
@property
def env_cache_key(self) -> str:
return self.script
def get_custom_config_section_postfix(self) -> str:
return self.name
def create_env(self) -> dict[str, Any]:
# strategy: create a tempfile, let python dump its full env in a subprocess and load the
# env file again afterwards
# helper to write the env
def write_env(path: str) -> None:
# get the bash command
bash_cmd = self._bash_cmd()
# pre-setup commands
pre_setup_cmds = self._build_pre_setup_cmds()
# post-setup commands
post_env = self._get_env()
post_setup_cmds = self._build_post_setup_cmds(post_env)
# build the python command that dumps the environment
py_cmd = (
"import os,pickle;"
f"pickle.dump(dict(os.environ),open('{path}','wb'),protocol=2)"
)
# build the full command
cmd = quote_cmd(bash_cmd + ["-c", " && ".join(flatten(
pre_setup_cmds,
f"source \"{self.script}\" \"\"",
post_setup_cmds,
quote_cmd(["python", "-c", py_cmd]),
))])
# run it
returncode = interruptable_popen(cmd, shell=True, executable="/bin/bash")[0]
if returncode != 0:
raise Exception(f"{self} env loading failed with exit code {returncode}")
# helper to load the env
def load_env(path: str) -> dict[str, Any]:
with open(path, "rb") as f:
try:
return dict(pickle.load(f, encoding="utf-8"))
except Exception as e:
raise Exception(f"{self} env deserialization failed: {e}")
# use the cache path if set
if self.env_cache_path:
env_cache_path = str(self.env_cache_path)
# write it if it does not exist yet
if not os.path.exists(env_cache_path):
makedirs(os.path.dirname(env_cache_path))
write_env(env_cache_path)
# load it
env = load_env(env_cache_path)
else:
# use a temp file
with tmp_file() as tmp:
tmp_path = os.path.realpath(tmp[1])
# write and load it
write_env(tmp_path)
env = load_env(tmp_path)
return env
def _bash_cmd(self) -> list[str]:
cmd = ["bash"]
# login flag
cfg = Config.instance()
cfg_section = self.get_config_section()
if cfg.get_expanded_bool(cfg_section, "login"):
cmd.extend(["-l"])
return cmd
def cmd(self, proxy_cmd: ProxyCommand) -> str:
# get the bash command
bash_cmd = self._bash_cmd()
# pre-setup commands
pre_setup_cmds = self._build_pre_setup_cmds()
# post-setup commands
post_env = self._get_env()
# add staging directories
if self.stagein_info:
post_env["LAW_SANDBOX_STAGEIN_DIR"] = self.stagein_info.stage_dir.path
if self.stageout_info:
post_env["LAW_SANDBOX_STAGEOUT_DIR"] = self.stageout_info.stage_dir.path
post_setup_cmds = self._build_post_setup_cmds(post_env)
# handle local scheduling within the container
if self.force_local_scheduler():
proxy_cmd.add_arg("--local-scheduler", "True", overwrite=True)
# build the final command
cmd = quote_cmd(bash_cmd + ["-c", " && ".join(flatten(
pre_setup_cmds,
f"source \"{self.script}\" \"\"",
post_setup_cmds,
proxy_cmd.build(),
))])
return cmd