subprocess.py
1 ##############################################################################
2 # Medical Image Registration ToolKit (MIRTK)
3 #
4 # Copyright 2016-2017 Imperial College London
5 # Copyright 2016-2017 Andreas Schuh
6 #
7 # Licensed under the Apache License, Version 2.0 (the "License");
8 # you may not use this file except in compliance with the License.
9 # You may obtain a copy of the License at
10 #
11 # http://www.apache.org/licenses/LICENSE-2.0
12 #
13 # Unless required by applicable law or agreed to in writing, software
14 # distributed under the License is distributed on an "AS IS" BASIS,
15 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
18 ##############################################################################
19 
20 """
21 This module can be used to execute the MIRTK command-line tools from an image
22 processing pipeline script. Its main usage is identical to the actual subprocess
23 module which is used internally by this mirtk.subprocess module.
24 """
25 
26 from __future__ import absolute_import, unicode_literals
27 
28 import os
29 import sys
30 import shlex
31 import traceback
32 import subprocess
33 
34 from collections import OrderedDict
35 
36 
37 # ============================================================================
38 # configuration
39 # ============================================================================
40 
41 __dir__ = os.path.dirname(os.path.realpath(__file__))
42 
43 config = 'Release'
44 libexec_dir = os.path.realpath(os.path.join(__dir__, '../../tools'))
45 
46 if sys.platform.startswith('win'): libexec_ext = ['.exe', '.cmd', '.bat']
47 else: libexec_ext = ['']
48 
49 # default values for kwargs of run function
50 showcmd = False
51 workdir = None
52 threads = 0
53 onerror = 'throw'
54 onexit = None
55 
56 # ============================================================================
57 # command execution
58 # ============================================================================
59 
60 # ----------------------------------------------------------------------------
61 def remove_kwargs(kwargs, keys):
62  if not isinstance(keys, (tuple, list)):
63  keys = [keys]
64  for k in keys:
65  if k in kwargs:
66  del kwargs[k]
67 
68 
69 # ----------------------------------------------------------------------------
70 def path(argv, quiet=False):
71  """Get full path of MIRTK command executable."""
72  if isinstance(argv, list): command = argv[0]
73  else: command = argv
74  if command == 'calculate': command = 'calculate-element-wise'
75  elif command == 'convert-dof2csv': command = 'convert-dof'
76  elif command == 'convert-dof2velo': command = 'convert-dof'
77  elif command == 'concatenate-dofs': command = 'compose-dofs'
78  elif command == 'concatenate-images': command = 'combine-images'
79  elif command == 'remesh': command = 'remesh-surface'
80  fpath = None
81  for ext in libexec_ext:
82  p = os.path.join(libexec_dir, config, command + ext)
83  if os.path.isfile(p):
84  fpath = p
85  break
86  if not fpath:
87  for ext in libexec_ext:
88  p = os.path.join(libexec_dir, command + ext)
89  if os.path.isfile(p):
90  fpath = p
91  break
92  if not quiet:
93  if not fpath:
94  sys.stderr.write('Error: Missing execuable for command: ' + command)
95  elif not os.access(fpath, os.X_OK):
96  sys.stderr.write('Error: Insufficient permissions to execute command: ' + fpath)
97  return fpath
98 
99 
100 # ----------------------------------------------------------------------------
101 def flatten(arg):
102  """Given an argument, possibly a list nested to any level, return a flat list."""
103  if isinstance(arg, (tuple, list)):
104  lis = []
105  for item in arg:
106  lis.extend(flatten(item))
107  return lis
108  else:
109  return [arg]
110 
111 
112 # ----------------------------------------------------------------------------
113 def quote(argv):
114  """Return quoted command arguments."""
115  args = []
116  for arg in argv:
117  arg = str(arg)
118  if ' ' in arg:
119  arg = '"' + arg + '"'
120  args.append(arg)
121  return args
122 
123 
124 # ----------------------------------------------------------------------------
125 def _call(argv, verbose=0, execfunc=subprocess.call):
126  """Execute MIRTK command."""
127  if not isinstance(argv, list):
128  argv = shlex.split(argv)
129  argv = [str(arg) for arg in argv]
130  if argv[0] == 'convert-dof2csv':
131  new_argv = ['convert-dof']
132  out_fmt = 'star_ccm_table'
133  for arg in argv[1:]:
134  if arg == '-pos': out_fmt = 'star_ccm_table_xyz'
135  else: new_argv.append(arg)
136  new_argv.extend(['-output-format', out_fmt])
137  argv = new_argv
138  argv[0] = path(argv[0])
139  if not argv[0]: return 1
140  if verbose > 0:
141  sys.stdout.write("> ")
142  sys.stdout.write(" ".join(quote(argv)))
143  sys.stdout.write("\n\n")
144  sys.stdout.flush()
145  return execfunc(argv)
146 
147 
148 # ----------------------------------------------------------------------------
149 def call(argv, verbose=0):
150  """Execute MIRTK command and return exit code."""
151  return _call(argv, verbose=verbose, execfunc=subprocess.call)
152 
153 
154 # ----------------------------------------------------------------------------
155 def check_call(argv, verbose=0):
156  """Execute MIRTK command and throw exception on error."""
157  _call(argv, verbose=verbose, execfunc=subprocess.check_call)
158 
159 
160 # ----------------------------------------------------------------------------
161 def check_output(argv, verbose=0, code='utf-8'):
162  """Execute MIRTK command and return its output."""
163  output = _call(argv, verbose=verbose, execfunc=subprocess.check_output)
164  if code: output = output.decode(code)
165  return output
166 
167 
168 # ----------------------------------------------------------------------------
169 def run(cmd, *args, **kwargs):
170  """Execute MIRTK command and throw exception or exit on error.
171 
172  This function calls the specified MIRTK command with the given positional
173  and option arguments (keyword arguments prepended by single dash, '-').
174  The 'onexit' and 'onexit' keyword arguments define the return value and
175  the error handling when the subprocess returns a non-zero exit code.
176  By default, this function behaves identical to check_call, i.e., throws
177  and exception when the command failed and does not return a value otherwise.
178 
179  Arguments
180  ---------
181 
182  onexit: str
183  Defines action to take when command subprocess finished.
184  - 'none': Always return None, even when process failed when onerror='return'
185  - 'output': Return standard output of command instead of printing it
186  - 'returncode': Return exit code of command. When onerror is not 'return',
187  this always returns 0 indicating successful execution of the command.
188  onerror: str
189  - 'return': Return value specified by 'onexit' even when command failed.
190  - 'throw': Throw CalledProcessError when command failed.
191  - 'exit': Exit this process (sys.exit) with exit command of failed command.
192 
193 
194  Deprecated
195  ----------
196 
197  This convenience wrapper for check_call throws a subprocess.CalledProcessError
198  when the command returned a non-zero exit code when exit_on_error=False.
199  If exit_on_error=True, a the command arguments, the exit code, and the
200  Python stack trace are printed to sys.stderr and this process terminated
201  using sys.exit with the return code of the executed command.
202 
203  """
204  default = globals()
205  if "exit_on_error" in kwargs and "onerror" in kwargs:
206  raise ValueError("Deprecated keyword argument 'exit_on_error' given together with new 'onerror' argument!")
207  showcmd = kwargs.get("showcmd", default["showcmd"])
208  workdir = kwargs.get("workdir", default["workdir"])
209  threads = kwargs.get("threads", default["threads"])
210  if "exit_on_error" in kwargs:
211  onerror = kwargs.get("onerror", "exit" if kwargs.get("exit_on_error", False) else "throw")
212  else:
213  onerror = kwargs.get("onerror", default["onerror"])
214  if onerror is None:
215  onerror = 'return'
216  else:
217  onerror = onerror.lower()
218  if onerror not in ('return', 'throw', 'exit'):
219  raise ValueError("Invalid 'onerror={}' argument! Must be 'return', 'throw', or 'exit'.".format(onerror))
220  onexit = kwargs.get("onexit", default["onexit"])
221  if onexit is None:
222  onexit = 'none'
223  else:
224  onexit = onexit.lower()
225  if onexit in ('code', 'exit_code', 'return_code'):
226  onexit = 'returncode'
227  if onexit in ('stdout', 'return_stdout', 'return_output'):
228  onexit = 'output'
229  if onexit not in ('none', 'output', 'returncode'):
230  raise ValueError("Invalid 'onexit={}' argument! Must be 'none', 'returncode', 'output' or alternatives thereof.".format(onexit))
231  remove_kwargs(kwargs, ["showcmd", "workdir", "threads", "onexit", "onerror", "exit_on_error"])
232  # command arguments
233  argv = [cmd]
234  argv.extend(args)
235  argv.extend(kwargs.get("args", []))
236  remove_kwargs(kwargs, "args")
237  # add ordered list of options
238  opts = kwargs.get("opts", {})
239  remove_kwargs(kwargs, "opts")
240  if isinstance(opts, list):
241  for item in opts:
242  if isinstance(item, (tuple, list)):
243  opt = item[0]
244  arg = flatten(item[1:])
245  else:
246  opt = item
247  arg = None
248  if not opt.startswith('-'):
249  opt = '-' + opt
250  argv.append(opt)
251  if not arg is None:
252  argv.extend(flatten(arg))
253  # add unordered dict of options
254  else:
255  for opt, arg in opts.items():
256  if not opt.startswith('-'):
257  opt = '-' + opt
258  argv.append(opt)
259  if not arg is None:
260  argv.extend(flatten(arg))
261  # add options given as kwargs
262  for opt, arg in kwargs.items():
263  opt = opt.replace('_', '-')
264  if not opt.startswith('-'):
265  opt = '-' + opt
266  argv.append(opt)
267  if not arg is None:
268  argv.extend(flatten(arg))
269  # add -threads option
270  if threads > 0 and not 'threads' in opts:
271  argv.extend(['-threads', str(threads)])
272  # execute command
273  retval = None
274  prevdir = os.getcwd()
275  if workdir:
276  os.chdir(workdir)
277  try:
278  if onexit == 'output':
279  retval = check_output(argv, verbose=1 if showcmd else 0)
280  else:
281  check_call(argv, verbose=1 if showcmd else 0)
282  if onexit == 'returncode':
283  retval = 0
284  except subprocess.CalledProcessError as e:
285  if onerror == 'exit':
286  sys.stdout.flush()
287  sys.stderr.write("\n")
288  sys.stderr.write("Command: ")
289  sys.stderr.write(" ".join(quote(argv)))
290  sys.stderr.write("\n")
291  sys.stderr.write("\nError: Command returned non-zero exit status {}\n".format(e.returncode))
292  sys.stderr.write("\nTraceback (most recent call last):\n")
293  traceback.print_stack()
294  sys.exit(e.returncode)
295  elif onerror == 'throw':
296  raise e
297  if onexit == 'returncode':
298  retval = e.returncode
299  finally:
300  os.chdir(prevdir)
301  return retval