Skip to content

Commit 77a3528

Browse files
authored
Merge pull request #438 from wtsi-hgi/feature-js-console-output
Adds --js-console option to facilitate debugging
2 parents ec43270 + 4e19de8 commit 77a3528

12 files changed

+173
-51
lines changed

cwltool/builder.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def __init__(self): # type: () -> None
5151
self.stagedir = None # type: Text
5252
self.make_fs_access = None # type: Type[StdFsAccess]
5353
self.debug = False # type: bool
54+
self.js_console = False # type: bool
5455
self.mutation_manager = None # type: MutationManager
5556
self.force_docker_pull = False # type: bool
5657

@@ -256,5 +257,6 @@ def do_eval(self, ex, context=None, pull_image=True, recursive=False):
256257
self.resources,
257258
context=context, pull_image=pull_image,
258259
timeout=self.timeout,
259-
force_docker_pull=self.force_docker_pull,
260-
debug=self.debug)
260+
debug=self.debug,
261+
js_console=self.js_console,
262+
force_docker_pull=self.force_docker_pull)

cwltool/cwlNodeEngine.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,17 @@ process.stdin.on("data", function(chunk) {
55
incoming += chunk;
66
var i = incoming.indexOf("\n");
77
if (i > -1) {
8-
var fn = JSON.parse(incoming.substr(0, i));
9-
incoming = incoming.substr(i+1);
10-
process.stdout.write(JSON.stringify(require("vm").runInNewContext(fn, {})) + "\n");
8+
try{
9+
var fn = JSON.parse(incoming.substr(0, i));
10+
incoming = incoming.substr(i+1);
11+
process.stdout.write(JSON.stringify(require("vm").runInNewContext(fn, {})) + "\n");
12+
}
13+
catch(e){
14+
console.error(e)
15+
}
16+
/*strings to indicate the process has finished*/
17+
console.log("r1cepzbhUTxtykz5XTC4");
18+
console.error("r1cepzbhUTxtykz5XTC4");
1119
}
1220
});
1321
process.stdin.on("end", process.exit);

cwltool/cwlNodeEngineJSConsole.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"use strict";
2+
function js_console_log(){
3+
console.error("[log] "+require("util").format.apply(this, arguments).split("\n").join("\n[log] "));
4+
}
5+
function js_console_err(){
6+
console.error("[err] "+require("util").format.apply(this, arguments).split("\n").join("\n[err] "));
7+
}
8+
process.stdin.setEncoding("utf8");
9+
var incoming = "";
10+
process.stdin.on("data", function(chunk) {
11+
incoming += chunk;
12+
var i = incoming.indexOf("\n");
13+
if (i > -1) {
14+
try{
15+
var fn = JSON.parse(incoming.substr(0, i));
16+
incoming = incoming.substr(i+1);
17+
process.stdout.write(JSON.stringify(require("vm").runInNewContext(fn, {
18+
console: {
19+
log: js_console_log,
20+
error: js_console_err
21+
}
22+
})) + "\n");
23+
}
24+
catch(e){
25+
console.error(e)
26+
}
27+
/*strings to indicate the process has finished*/
28+
console.log("r1cepzbhUTxtykz5XTC4");
29+
console.error("r1cepzbhUTxtykz5XTC4");
30+
}
31+
});
32+
process.stdin.on("end", process.exit);

cwltool/expression.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,8 @@ def next_seg(remain, obj): # type: (Text, Any) -> Any
153153
return obj
154154

155155

156-
def evaluator(ex, jslib, obj, fullJS=False, timeout=None, force_docker_pull=False, debug=False):
157-
# type: (Text, Text, Dict[Text, Any], bool, int, bool, bool) -> JSON
156+
def evaluator(ex, jslib, obj, fullJS=False, timeout=None, force_docker_pull=False, debug=False, js_console=False):
157+
# type: (Text, Text, Dict[Text, Any], bool, int, bool, bool, bool) -> JSON
158158
m = param_re.match(ex)
159159
if m:
160160
if m.end(1)+1 == len(ex) and m.group(1) == "null":
@@ -164,17 +164,17 @@ def evaluator(ex, jslib, obj, fullJS=False, timeout=None, force_docker_pull=Fals
164164
except Exception as w:
165165
raise WorkflowException("%s%s" % (m.group(1), w))
166166
elif fullJS:
167-
return sandboxjs.execjs(ex, jslib, timeout=timeout, force_docker_pull=force_docker_pull, debug=debug)
167+
return sandboxjs.execjs(ex, jslib, timeout=timeout, force_docker_pull=force_docker_pull, debug=debug, js_console=js_console)
168168
else:
169169
raise sandboxjs.JavascriptException(
170170
"Syntax error in parameter reference '%s' or used Javascript code without specifying InlineJavascriptRequirement.",
171171
ex)
172172

173173

174174
def interpolate(scan, rootvars,
175-
timeout=None, fullJS=None, jslib="",force_docker_pull=False,
176-
debug=False):
177-
# type: (Text, Dict[Text, Any], int, bool, Union[str, Text], bool, bool) -> JSON
175+
timeout=None, fullJS=None, jslib="", force_docker_pull=False,
176+
debug=False, js_console=False):
177+
# type: (Text, Dict[Text, Any], int, bool, Union[str, Text], bool, bool, bool) -> JSON
178178
scan = scan.strip()
179179
parts = []
180180
w = scanner(scan)
@@ -184,7 +184,7 @@ def interpolate(scan, rootvars,
184184
if scan[w[0]] == '$':
185185
e = evaluator(scan[w[0] + 1:w[1]], jslib, rootvars, fullJS=fullJS,
186186
timeout=timeout, force_docker_pull=force_docker_pull,
187-
debug=debug)
187+
debug=debug, js_console=js_console)
188188
if w[0] == 0 and w[1] == len(scan):
189189
return e
190190
leaf = json.dumps(e, sort_keys=True)
@@ -202,8 +202,8 @@ def interpolate(scan, rootvars,
202202

203203

204204
def do_eval(ex, jobinput, requirements, outdir, tmpdir, resources,
205-
context=None, pull_image=True, timeout=None, force_docker_pull=False, debug=False):
206-
# type: (Union[dict, AnyStr], Dict[Text, Union[Dict, List, Text]], List[Dict[Text, Any]], Text, Text, Dict[Text, Union[int, Text]], Any, bool, int, bool, bool) -> Any
205+
context=None, pull_image=True, timeout=None, force_docker_pull=False, debug=False, js_console=False):
206+
# type: (Union[dict, AnyStr], Dict[Text, Union[Dict, List, Text]], List[Dict[Text, Any]], Text, Text, Dict[Text, Union[int, Text]], Any, bool, int, bool, bool, bool) -> Any
207207

208208
runtime = copy.copy(resources)
209209
runtime["tmpdir"] = docker_windows_path_adjust(tmpdir)
@@ -230,7 +230,9 @@ def do_eval(ex, jobinput, requirements, outdir, tmpdir, resources,
230230
fullJS=fullJS,
231231
jslib=jslib,
232232
force_docker_pull=force_docker_pull,
233-
debug=debug)
233+
debug=debug,
234+
js_console=js_console)
235+
234236
except Exception as e:
235237
raise WorkflowException("Expression evaluation error:\n%s" % e)
236238
else:

cwltool/main.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ def arg_parser(): # type: () -> argparse.ArgumentParser
158158
exgroup.add_argument("--quiet", action="store_true", help="Only print warnings and errors.")
159159
exgroup.add_argument("--debug", action="store_true", help="Print even more logging")
160160

161+
parser.add_argument("--js-console", action="store_true", help="Enable javascript console output")
162+
161163
dependency_resolvers_configuration_help = argparse.SUPPRESS
162164
dependencies_directory_help = argparse.SUPPRESS
163165
use_biocontainers_help = argparse.SUPPRESS
@@ -730,6 +732,7 @@ def main(argsl=None, # type: List[str]
730732
'cachedir': None,
731733
'quiet': False,
732734
'debug': False,
735+
'js_console': False,
733736
'version': False,
734737
'enable_dev': False,
735738
'enable_ext': False,

cwltool/process.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,8 @@ def _init_job(self, joborder, **kwargs):
535535
tmpdir: tmpdir on host for this job
536536
stagedir: stagedir on host for this job
537537
select_resources: callback to select compute resources
538+
debug: enable debugging output
539+
js_console: enable javascript console output
538540
"""
539541

540542
builder = Builder()
@@ -559,6 +561,7 @@ def _init_job(self, joborder, **kwargs):
559561
builder.resources = {}
560562
builder.timeout = kwargs.get("eval_timeout")
561563
builder.debug = kwargs.get("debug")
564+
builder.js_console = kwargs.get("js_console")
562565
builder.mutation_manager = kwargs.get("mutation_manager")
563566

564567
builder.make_fs_access = kwargs.get("make_fs_access") or StdFsAccess

cwltool/sandboxjs.py

Lines changed: 33 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
import logging
55
import os
6+
import re
67
import select
78
import subprocess
89
import threading
@@ -53,10 +54,15 @@ def check_js_threshold_version(working_alias):
5354
return False
5455

5556

56-
def new_js_proc(force_docker_pull=False):
57-
# type: (bool) -> subprocess.Popen
57+
def new_js_proc(force_docker_pull=False, js_console=False):
58+
# type: (bool, bool) -> subprocess.Popen
59+
60+
cwl_node_engine_js = 'cwlNodeEngine.js'
61+
if js_console:
62+
cwl_node_engine_js = 'cwlNodeEngineJSConsole.js'
63+
_logger.warn("Running with support for javascript console in expressions (DO NOT USE IN PRODUCTION)")
5864

59-
res = resource_stream(__name__, 'cwlNodeEngine.js')
65+
res = resource_stream(__name__, cwl_node_engine_js)
6066
nodecode = res.read().decode('utf-8')
6167

6268
required_node_version, docker = (False,)*2
@@ -125,10 +131,10 @@ def new_js_proc(force_docker_pull=False):
125131
return nodejs
126132

127133

128-
def execjs(js, jslib, timeout=None, force_docker_pull=False, debug=False): # type: (Union[Mapping, Text], Any, int, bool, bool) -> JSON
134+
def execjs(js, jslib, timeout=None, force_docker_pull=False, debug=False, js_console=False): # type: (Union[Mapping, Text], Any, int, bool, bool, bool) -> JSON
129135

130136
if not hasattr(localdata, "proc") or localdata.proc.poll() is not None or onWindows():
131-
localdata.proc = new_js_proc(force_docker_pull=force_docker_pull)
137+
localdata.proc = new_js_proc(force_docker_pull=force_docker_pull, js_console=js_console)
132138

133139
nodejs = localdata.proc
134140

@@ -158,6 +164,12 @@ def terminate():
158164
rselect = [nodejs.stdout, nodejs.stderr] # type: List[BytesIO]
159165
wselect = [nodejs.stdin] # type: List[BytesIO]
160166

167+
PROCESS_FINISHED_STR = "r1cepzbhUTxtykz5XTC4\n"
168+
169+
def process_finished(): # type: () -> bool
170+
return stdout_buf.getvalue().decode().endswith(PROCESS_FINISHED_STR) and \
171+
stderr_buf.getvalue().decode().endswith(PROCESS_FINISHED_STR)
172+
161173
# On windows system standard input/output are not handled properly by select module
162174
# (modules like pywin32, msvcrt, gevent don't work either)
163175
if sys.platform=='win32':
@@ -208,11 +220,9 @@ def get_error(error_queue):
208220
error_thread.daemon=True
209221
error_thread.start()
210222

211-
# mark if output/error is ready
212-
output_ready=False
213-
error_ready=False
223+
finished = False
214224

215-
while (len(wselect) + len(rselect)) > 0:
225+
while not finished and tm.is_alive():
216226
try:
217227
if nodejs.stdin in wselect:
218228
if not input_queue.empty():
@@ -221,58 +231,39 @@ def get_error(error_queue):
221231
wselect = []
222232
if nodejs.stdout in rselect:
223233
if not output_queue.empty():
224-
output_ready = True
225234
stdout_buf.write(output_queue.get())
226-
elif output_ready:
227-
rselect = []
228-
no_more_output.release()
229-
no_more_error.release()
230-
output_thread.join()
231235

232236
if nodejs.stderr in rselect:
233237
if not error_queue.empty():
234-
error_ready = True
235-
stderr_buf.write(error_queue.get())
236-
elif error_ready:
237-
rselect = []
238-
no_more_output.release()
239-
no_more_error.release()
240-
output_thread.join()
241-
error_thread.join()
242-
if stdout_buf.getvalue().endswith("\n".encode()):
243-
rselect = []
238+
stderr_buf.write(error_queue.get())
239+
240+
if process_finished() and error_queue.empty() and output_queue.empty():
241+
finished = True
244242
no_more_output.release()
245243
no_more_error.release()
246-
output_thread.join()
247244
except OSError as e:
248245
break
249246

250247
else:
251-
while (len(wselect) + len(rselect)) > 0:
248+
while not process_finished() and tm.is_alive():
252249
rready, wready, _ = select.select(rselect, wselect, [])
253250
try:
254251
if nodejs.stdin in wready:
255252
b = stdin_buf.read(select.PIPE_BUF)
256253
if b:
257254
os.write(nodejs.stdin.fileno(), b)
258-
else:
259-
wselect = []
260255
for pipes in ((nodejs.stdout, stdout_buf), (nodejs.stderr, stderr_buf)):
261256
if pipes[0] in rready:
262257
b = os.read(pipes[0].fileno(), select.PIPE_BUF)
263258
if b:
264259
pipes[1].write(b)
265-
else:
266-
rselect.remove(pipes[0])
267-
if stdout_buf.getvalue().endswith("\n".encode()):
268-
rselect = []
269260
except OSError as e:
270261
break
271262
tm.cancel()
272263

273264
stdin_buf.close()
274-
stdoutdata = stdout_buf.getvalue()
275-
stderrdata = stderr_buf.getvalue()
265+
stdoutdata = stdout_buf.getvalue()[:-len(PROCESS_FINISHED_STR) - 1]
266+
stderrdata = stderr_buf.getvalue()[:-len(PROCESS_FINISHED_STR) - 1]
276267

277268
def fn_linenum(): # type: () -> Text
278269
lines = fn.splitlines()
@@ -290,6 +281,13 @@ def stdfmt(data): # type: (Text) -> Text
290281

291282
nodejs.poll()
292283

284+
if js_console:
285+
if len(stderrdata) > 0:
286+
_logger.info("Javascript console output:")
287+
_logger.info("----------------------------------------")
288+
_logger.info('\n'.join(re.findall(r'^[[](?:log|err)[]].*$', stderrdata.decode('utf-8'), flags=re.MULTILINE)))
289+
_logger.info("----------------------------------------")
290+
293291
if debug:
294292
info = u"returncode was: %s\nscript was:\n%s\nstdout was: %s\nstderr was: %s\n" %\
295293
(nodejs.returncode, fn_linenum(), stdfmt(stdoutdata.decode('utf-8')), stdfmt(stderrdata.decode('utf-8')))

cwltool/workflow.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,10 @@ def receive_output(self, step, outputparms, final_output_callback, jobout, proce
337337

338338
def try_make_job(self, step, final_output_callback, **kwargs):
339339
# type: (WorkflowJobStep, Callable[[Any, Any], Any], **Any) -> Generator
340+
341+
js_console = kwargs.get("js_console", False)
342+
debug = kwargs.get("debug", False)
343+
340344
inputparms = step.tool["inputs"]
341345
outputparms = step.tool["outputs"]
342346

@@ -375,7 +379,7 @@ def valueFromFunc(k, v): # type: (Any, Any) -> Any
375379
if k in valueFrom:
376380
return expression.do_eval(
377381
valueFrom[k], shortio, self.workflow.requirements,
378-
None, None, {}, context=v)
382+
None, None, {}, context=v, debug=debug, js_console=js_console)
379383
else:
380384
return v
381385

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
'schemas/v1.1.0-dev1/salad/schema_salad/metaschema/*.yml',
4545
'schemas/v1.1.0-dev1/salad/schema_salad/metaschema/*.md',
4646
'cwlNodeEngine.js',
47+
'cwlNodeEngineJSConsole.js',
4748
'extensions.yml']},
4849
include_package_data=True,
4950
install_requires=[

0 commit comments

Comments
 (0)