Skip to content

Commit 5c8ee81

Browse files
authored
Merge pull request #212 from jmchilton/job_script_extensions
Add extension point allowing writing up a job script file for tool commands.
2 parents 2352d16 + 879c254 commit 5c8ee81

File tree

3 files changed

+181
-36
lines changed

3 files changed

+181
-36
lines changed

cwltool/builder.py

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def __init__(self): # type: () -> None
2626
self.fs_access = None # type: StdFsAccess
2727
self.job = None # type: Dict[Text, Union[Dict[Text, Any], List, Text]]
2828
self.requirements = None # type: List[Dict[Text, Any]]
29+
self.hints = None # type: List[Dict[Text, Any]]
2930
self.outdir = None # type: Text
3031
self.tmpdir = None # type: Text
3132
self.resources = None # type: Dict[Text, Union[int, Text]]
@@ -34,6 +35,7 @@ def __init__(self): # type: () -> None
3435
self.pathmapper = None # type: PathMapper
3536
self.stagedir = None # type: Text
3637
self.make_fs_access = None # type: Type[StdFsAccess]
38+
self.build_job_script = None # type: Callable[[List[str]], Text]
3739

3840
def bind_input(self, schema, datum, lead_pos=[], tail_pos=[]):
3941
# type: (Dict[Text, Any], Any, Union[int, List[int]], List[int]) -> List[Dict[Text, Any]]

cwltool/job.py

+178-36
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import stat
1515
import re
1616
import shellescape
17+
import string
1718
from .docker_uid import docker_vm_uid
1819
from .builder import Builder
1920
from typing import (Any, Callable, Union, Iterable, Mapping, MutableMapping,
@@ -25,6 +26,58 @@
2526

2627
needs_shell_quoting_re = re.compile(r"""(^$|[\s|&;()<>\'"$@])""")
2728

29+
FORCE_SHELLED_POPEN = os.getenv("CWLTOOL_FORCE_SHELL_POPEN", "0") == "1"
30+
31+
SHELL_COMMAND_TEMPLATE = """#!/bin/bash
32+
python "run_job.py" "job.json"
33+
"""
34+
35+
PYTHON_RUN_SCRIPT = """
36+
import json
37+
import sys
38+
import subprocess
39+
40+
with open(sys.argv[1], "r") as f:
41+
popen_description = json.load(f)
42+
commands = popen_description["commands"]
43+
cwd = popen_description["cwd"]
44+
env = popen_description["env"]
45+
stdin_path = popen_description["stdin_path"]
46+
stdout_path = popen_description["stdout_path"]
47+
stderr_path = popen_description["stderr_path"]
48+
if stdin_path is not None:
49+
stdin = open(stdin_path, "rb")
50+
else:
51+
stdin = subprocess.PIPE
52+
if stdout_path is not None:
53+
stdout = open(stdout_path, "wb")
54+
else:
55+
stdout = sys.stderr
56+
if stderr_path is not None:
57+
stderr = open(stderr_path, "wb")
58+
else:
59+
stderr = sys.stderr
60+
sp = subprocess.Popen(commands,
61+
shell=False,
62+
close_fds=True,
63+
stdin=stdin,
64+
stdout=stdout,
65+
stderr=stderr,
66+
env=env,
67+
cwd=cwd)
68+
if sp.stdin:
69+
sp.stdin.close()
70+
rcode = sp.wait()
71+
if isinstance(stdin, file):
72+
stdin.close()
73+
if stdout is not sys.stderr:
74+
stdout.close()
75+
if stderr is not sys.stderr:
76+
stderr.close()
77+
sys.exit(rcode)
78+
"""
79+
80+
2881
def deref_links(outputs): # type: (Any) -> None
2982
if isinstance(outputs, dict):
3083
if outputs.get("class") == "File":
@@ -151,10 +204,6 @@ def run(self, dry_run=False, pull_image=True, rm_container=True,
151204

152205
stageFiles(self.pathmapper, os.symlink)
153206

154-
stdin = None # type: Union[IO[Any], int]
155-
stderr = None # type: IO[Any]
156-
stdout = None # type: IO[Any]
157-
158207
scr, _ = get_feature(self, "ShellCommandRequirement")
159208

160209
if scr:
@@ -191,51 +240,36 @@ def linkoutdir(src, tgt):
191240
break
192241
stageFiles(generatemapper, linkoutdir)
193242

243+
stdin_path = None
194244
if self.stdin:
195-
stdin = open(self.pathmapper.reversemap(self.stdin)[1], "rb")
196-
else:
197-
stdin = subprocess.PIPE
245+
stdin_path = self.pathmapper.reversemap(self.stdin)[1]
198246

247+
stderr_path = None
199248
if self.stderr:
200249
abserr = os.path.join(self.outdir, self.stderr)
201250
dnerr = os.path.dirname(abserr)
202251
if dnerr and not os.path.exists(dnerr):
203252
os.makedirs(dnerr)
204-
stderr = open(abserr, "wb")
205-
else:
206-
stderr = sys.stderr
253+
stderr_path = abserr
207254

255+
stdout_path = None
208256
if self.stdout:
209257
absout = os.path.join(self.outdir, self.stdout)
210258
dn = os.path.dirname(absout)
211259
if dn and not os.path.exists(dn):
212260
os.makedirs(dn)
213-
stdout = open(absout, "wb")
214-
else:
215-
stdout = sys.stderr
216-
217-
sp = subprocess.Popen([Text(x).encode('utf-8') for x in runtime + self.command_line],
218-
shell=False,
219-
close_fds=True,
220-
stdin=stdin,
221-
stderr=stderr,
222-
stdout=stdout,
223-
env=env,
224-
cwd=self.outdir)
225-
226-
if sp.stdin:
227-
sp.stdin.close()
228-
229-
rcode = sp.wait()
230-
231-
if isinstance(stdin, file):
232-
stdin.close()
233-
234-
if stderr is not sys.stderr:
235-
stderr.close()
236-
237-
if stdout is not sys.stderr:
238-
stdout.close()
261+
stdout_path = absout
262+
263+
build_job_script = self.builder.build_job_script # type: Callable[[List[str]], Text]
264+
rcode = _job_popen(
265+
[Text(x).encode('utf-8') for x in runtime + self.command_line],
266+
stdin_path=stdin_path,
267+
stdout_path=stdout_path,
268+
stderr_path=stderr_path,
269+
env=env,
270+
cwd=self.outdir,
271+
build_job_script=build_job_script,
272+
)
239273

240274
if self.successCodes and rcode in self.successCodes:
241275
processStatus = "success"
@@ -294,3 +328,111 @@ def linkoutdir(src, tgt):
294328
if move_outputs == "move" and empty_subtree(self.outdir):
295329
_logger.debug(u"[job %s] Removing empty output directory %s", self.name, self.outdir)
296330
shutil.rmtree(self.outdir, True)
331+
332+
333+
def _job_popen(
334+
commands, # type: List[str]
335+
stdin_path, # type: Text
336+
stdout_path, # type: Text
337+
stderr_path, # type: Text
338+
env, # type: Union[MutableMapping[Text, Text], MutableMapping[str, str]]
339+
cwd, # type: Text
340+
job_dir=None, # type: Text
341+
build_job_script=None, # type: Callable[[List[str]], Text]
342+
):
343+
# type: (...) -> int
344+
345+
job_script_contents = None # type: Text
346+
if build_job_script:
347+
job_script_contents = build_job_script(commands)
348+
349+
if not job_script_contents and not FORCE_SHELLED_POPEN:
350+
351+
stdin = None # type: Union[IO[Any], int]
352+
stderr = None # type: IO[Any]
353+
stdout = None # type: IO[Any]
354+
355+
if stdin_path is not None:
356+
stdin = open(stdin_path, "rb")
357+
else:
358+
stdin = subprocess.PIPE
359+
360+
if stdout_path is not None:
361+
stdout = open(stdout_path, "wb")
362+
else:
363+
stdout = sys.stderr
364+
365+
if stderr_path is not None:
366+
stderr = open(stderr_path, "wb")
367+
else:
368+
stderr = sys.stderr
369+
370+
sp = subprocess.Popen(commands,
371+
shell=False,
372+
close_fds=True,
373+
stdin=stdin,
374+
stdout=stdout,
375+
stderr=stderr,
376+
env=env,
377+
cwd=cwd)
378+
379+
if sp.stdin:
380+
sp.stdin.close()
381+
382+
rcode = sp.wait()
383+
384+
if isinstance(stdin, file):
385+
stdin.close()
386+
387+
if stdout is not sys.stderr:
388+
stdout.close()
389+
390+
if stderr is not sys.stderr:
391+
stderr.close()
392+
393+
return rcode
394+
else:
395+
if job_dir is None:
396+
job_dir = tempfile.mkdtemp(prefix="cwltooljob")
397+
398+
if not job_script_contents:
399+
job_script_contents = SHELL_COMMAND_TEMPLATE
400+
401+
env_copy = {}
402+
for key in env:
403+
key = key.encode("utf-8")
404+
env_copy[key] = env[key]
405+
406+
job_description = dict(
407+
commands=commands,
408+
cwd=cwd,
409+
env=env_copy,
410+
stdout_path=stdout_path,
411+
stderr_path=stderr_path,
412+
stdin_path=stdin_path,
413+
)
414+
with open(os.path.join(job_dir, "job.json"), "w") as f:
415+
json.dump(job_description, f)
416+
try:
417+
job_script = os.path.join(job_dir, "run_job.bash")
418+
with open(job_script, "w") as f:
419+
f.write(job_script_contents)
420+
job_run = os.path.join(job_dir, "run_job.py")
421+
with open(job_run, "w") as f:
422+
f.write(PYTHON_RUN_SCRIPT)
423+
sp = subprocess.Popen(
424+
["bash", job_script.encode("utf-8")],
425+
shell=False,
426+
cwd=job_dir,
427+
stdout=subprocess.PIPE,
428+
stderr=subprocess.PIPE,
429+
stdin=subprocess.PIPE,
430+
)
431+
if sp.stdin:
432+
sp.stdin.close()
433+
434+
rcode = sp.wait()
435+
436+
return rcode
437+
finally:
438+
shutil.rmtree(job_dir)

cwltool/process.py

+1
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,7 @@ def _init_job(self, joborder, **kwargs):
442442
builder.schemaDefs = self.schemaDefs
443443
builder.names = self.names
444444
builder.requirements = self.requirements
445+
builder.hints = self.hints
445446
builder.resources = {}
446447
builder.timeout = kwargs.get("eval_timeout")
447448

0 commit comments

Comments
 (0)