The Gaudi Framework  v36r0 (4abb4d13)
gaudirun.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 
12 
13 from __future__ import print_function
14 import os
15 import sys
16 from tempfile import mkstemp
17 
18 
20  """
21  Remove from the arguments the presence of the profiler and its output in
22  order to relaunch the script w/o infinite loops.
23 
24  >>> getArgsWithoutProfilerInfo(['--profilerName', 'igprof', 'myopts.py'])
25  ['myopts.py']
26 
27  >>> getArgsWithoutProfilerInfo(['--profilerName=igprof', 'myopts.py'])
28  ['myopts.py']
29 
30  >>> getArgsWithoutProfilerInfo(['--profilerName', 'igprof', '--profilerExtraOptions', 'a b c', 'myopts.py'])
31  ['myopts.py']
32 
33  >>> getArgsWithoutProfilerInfo(['--profilerName', 'igprof', '--options', 'a b c', 'myopts.py'])
34  ['--options', 'a b c', 'myopts.py']
35  """
36  newargs = []
37  args = list(args) # make a temp copy
38  while args:
39  o = args.pop(0)
40  if o.startswith('--profile'):
41  if '=' not in o:
42  args.pop(0)
43  else:
44  newargs.append(o)
45  return newargs
46 
47 
48 def setLibraryPreload(newpreload):
49  ''' Adds a list of libraries to LD_PRELOAD '''
50  preload = os.environ.get("LD_PRELOAD", "")
51  if preload:
52  preload = preload.replace(" ", ":").split(":")
53  else:
54  preload = []
55 
56  for libname in set(preload).intersection(newpreload):
57  logging.warning(
58  "Ignoring preload of library %s because it is "
59  "already in LD_PRELOAD.", libname)
60 
61  to_load = [
62  libname for libname in newpreload if libname not in set(preload)
63  ]
64 
65  if to_load:
66  preload += to_load
67  preload = ":".join(preload)
68  os.environ["LD_PRELOAD"] = preload
69  logging.info("Setting LD_PRELOAD='%s'", preload)
70 
71  return to_load
72 
73 
74 def rationalizepath(path):
75  '''
76  Convert the given path to a real path if the pointed file exists, otherwise
77  just normalize it.
78  '''
79  path = os.path.normpath(os.path.expandvars(path))
80  if os.path.exists(path):
81  path = os.path.realpath(path)
82  return path
83 
84 
85 # variable used to keep alive the temporary option files extracted
86 # from the .qmt
87 _qmt_tmp_opt_files = []
88 
89 
90 def getArgsFromQmt(qmtfile):
91  '''
92  Given a .qmt file, return the command line arguments of the corresponding
93  test.
94  '''
95  from xml.etree import ElementTree as ET
96  global _qmt_tmp_opt_files
97  # parse the .qmt file and extract args and options
98  qmt = ET.parse(qmtfile)
99  args = [a.text for a in qmt.findall("argument[@name='args']//text")]
100  options = qmt.find("argument[@name='options']/text")
101 
102  if options is not None: # options need to be dumped in a temporary file
103  from tempfile import NamedTemporaryFile
104  import re
105  if re.search(
106  r"from\s+Gaudi.Configuration\s+import\s+\*"
107  r"|from\s+Configurables\s+import", options.text):
108  tmp_opts = NamedTemporaryFile(suffix='.py')
109  else:
110  tmp_opts = NamedTemporaryFile(suffix='.opts')
111  tmp_opts.write(options.text.encode('ascii'))
112  tmp_opts.flush()
113  args.append(tmp_opts.name)
114  _qmt_tmp_opt_files.append(tmp_opts)
115 
116  # relative paths in a .qmt are rooted in the qmtest directory, so
117  # - find where the .qmt lives
118  qmtfile = os.path.abspath(qmtfile)
119  if 'qmtest' in qmtfile.split(os.path.sep):
120  # this return the path up to the 'qmtest' entry in qmtfile
121  testdir = qmtfile
122  while os.path.basename(testdir) != 'qmtest':
123  testdir = os.path.dirname(testdir)
124  else:
125  testdir = '.'
126  # - temporarily switch to that directory and rationalize the paths
127  old_cwd = os.getcwd()
128  os.chdir(testdir)
129  args = [rationalizepath(arg) for arg in args]
130  os.chdir(old_cwd)
131 
132  return args
133 
134 
135 # ---------------------------------------------------------------------
136 if __name__ == "__main__":
137  # ensure that we (and the subprocesses) use the C standard localization
138  if os.environ.get('LC_ALL') != 'C':
139  print('# setting LC_ALL to "C"')
140  # !!!!
141  os.environ['LC_ALL'] = 'C'
142 
143  from optparse import OptionParser
144  parser = OptionParser(usage="%prog [options] <opts_file|function_id> ...")
145  parser.add_option(
146  "-n",
147  "--dry-run",
148  action="store_true",
149  help="do not run the application, just parse option files")
150  parser.add_option(
151  "-p",
152  "--pickle-output",
153  action="store",
154  type="string",
155  metavar="FILE",
156  help="DEPRECATED: use '--output file.pkl' instead. Write "
157  "the parsed options as a pickle file (static option "
158  "file)")
159  parser.add_option(
160  "-v",
161  "--verbose",
162  action="store_true",
163  help="print the parsed options")
164  parser.add_option(
165  "--old-opts",
166  action="store_true",
167  help="format printed options in old option files style")
168  parser.add_option(
169  "--all-opts",
170  action="store_true",
171  help="print all the option (even if equal to default)")
172  # GaudiPython Parallel Mode Option
173  # Argument must be an integer in range [ -1, sys_cpus ]
174  # -1 : All available cpus
175  # 0 : Serial Mode (traditional gaudirun)
176  # n>0 : parallel with n cpus (n <= sys_cpus)
177  parser.add_option(
178  "--ncpus",
179  action="store",
180  type="int",
181  default=0,
182  help="start the application in parallel mode using NCPUS processes. "
183  "0 => serial mode (default), -1 => use all CPUs")
184 
185  def option_cb(option, opt, value, parser):
186  """Add the option line to a list together with its position in the
187  argument list.
188  """
189  parser.values.options.append((len(parser.largs), value))
190 
191  parser.add_option(
192  "--option",
193  action="callback",
194  callback=option_cb,
195  type="string",
196  nargs=1,
197  help="add a single line (Python) option to the configuration. "
198  "All options lines are executed, one after the other, in "
199  "the same context.")
200  parser.add_option(
201  "--no-conf-user-apply",
202  action="store_true",
203  help="disable the automatic application of configurable "
204  "users (for backward compatibility)")
205  parser.add_option(
206  "--old-conf-user-apply",
207  action="store_true",
208  help="use the old logic when applying ConfigurableUsers "
209  "(with bug #103803) [default]")
210  parser.add_option(
211  "--new-conf-user-apply",
212  action="store_false",
213  dest="old_conf_user_apply",
214  help="use the new (correct) logic when applying "
215  "ConfigurableUsers (fixed bug #103803), can be "
216  "turned on also with the environment variable "
217  "GAUDI_FIXED_APPLY_CONF")
218  parser.add_option(
219  "-o",
220  "--output",
221  action="store",
222  type="string",
223  help="dump the configuration to a file. The format of "
224  "the options is determined by the extension of the "
225  "file name: .pkl = pickle, .py = python, .opts = "
226  "old style options. The python format cannot be "
227  "used to run the application and it contains the "
228  "same dictionary printed with -v")
229  parser.add_option(
230  "--post-option",
231  action="append",
232  type="string",
233  dest="post_options",
234  help="Python options to be executed after the ConfigurableUser "
235  "are applied. "
236  "All options lines are executed, one after the other, in "
237  "the same context.")
238  parser.add_option(
239  "--debug", action="store_true", help="enable some debug print-out")
240  parser.add_option("--gdb", action="store_true", help="attach gdb")
241  parser.add_option(
242  "--printsequence", action="store_true", help="print the sequence")
243  if not sys.platform.startswith("win"):
244  # These options can be used only on unix platforms
245  parser.add_option(
246  "-T",
247  "--tcmalloc",
248  action="store_true",
249  help="Use the Google malloc replacement. The environment "
250  "variable TCMALLOCLIB can be used to specify a different "
251  "name for the library (the default is libtcmalloc.so)")
252  parser.add_option(
253  "--preload",
254  action="append",
255  help="Allow pre-loading of special libraries (e.g. Google "
256  "profiling libraries).")
257  # Option to use a profiler
258  parser.add_option(
259  "--profilerName",
260  type="string",
261  help=
262  "Select one profiler among: igprofPerf, igprofMem and valgrind<toolname>"
263  )
264 
265  # Option to specify the filename where to collect the profiler's output
266  parser.add_option(
267  "--profilerOutput",
268  type="string",
269  help="Specify the name of the output file for the profiler output")
270 
271  # Option to specify the filename where to collect the profiler's output
272  parser.add_option(
273  "--profilerExtraOptions",
274  type="string",
275  help=
276  "Specify additional options for the profiler. The '--' string should be expressed as '__' (--my-opt becomes __my-opt)"
277  )
278 
279  parser.add_option(
280  '--use-temp-opts',
281  action='store_true',
282  help='when this option is enabled, the options are parsed'
283  ' and stored in a temporary file, then the job is '
284  'restarted using that file as input (to save '
285  'memory)')
286  parser.add_option(
287  "--run-info-file",
288  type="string",
289  help=
290  "Save gaudi process information to the file specified (in JSON format)"
291  )
292  parser.add_option(
293  '--application',
294  help='name of the Gaudi::Application to use [default: %default]')
295 
296  parser.set_defaults(
297  options=[],
298  tcmalloc=False,
299  profilerName='',
300  profilerOutput='',
301  profilerExtraOptions='',
302  preload=[],
303  ncpus=None,
304  # the old logic can be turned off with an env variable
305  old_conf_user_apply='GAUDI_FIXED_APPLY_CONF' not in os.environ,
306  run_info_file=None,
307  application='Gaudi::Application')
308 
309  # replace .qmt files in the command line with their contained args
310  argv = []
311  for a in sys.argv[1:]:
312  if a.endswith('.qmt') and os.path.exists(a):
313  argv.extend(getArgsFromQmt(a))
314  else:
315  argv.append(a)
316  if argv != sys.argv[1:]:
317  print('# Running', sys.argv[0], 'with arguments', argv)
318 
319  opts, args = parser.parse_args(args=argv)
320 
321  # Check consistency of options
322 
323  # Parallel Option ---------------------------------------------------------
324  if opts.ncpus:
325  from multiprocessing import cpu_count
326  sys_cpus = cpu_count()
327  if opts.ncpus > sys_cpus:
328  s = "Invalid value : --ncpus : only %i cpus available" % sys_cpus
329  parser.error(s)
330  elif opts.ncpus < -1:
331  s = "Invalid value : --ncpus must be integer >= -1"
332  parser.error(s)
333  else:
334  # FIXME: is it really needed to set it to None if it is 0 or False?
335  opts.ncpus = None
336 
337  # configure the logging
338  import logging
339  from GaudiKernel.ProcessJobOptions import (InstallRootLoggingHandler,
340  PrintOff)
341 
342  if opts.old_opts:
343  prefix = "// "
344  else:
345  prefix = "# "
346  level = logging.INFO
347  if opts.debug:
348  level = logging.DEBUG
349  InstallRootLoggingHandler(prefix, level=level, with_time=opts.debug)
350  root_logger = logging.getLogger()
351 
352  # Sanitizer support
353  sanitizer = os.environ.get("PRELOAD_SANITIZER_LIB", "")
354  if sanitizer:
355  if sanitizer not in os.environ.get("LD_PRELOAD", ""):
356  opts.preload.insert(0, sanitizer)
357  os.environ["PRELOAD_SANITIZER_LIB"] = ""
358 
359  # tcmalloc support
360  if opts.tcmalloc:
361  # Disable tcmalloc if sanitizer is selected
362  if sanitizer:
363  logging.warning("tcmalloc preload disabled when using a sanitizer")
364  else:
365  opts.preload.insert(
366  0, os.environ.get("TCMALLOCLIB", "libtcmalloc.so"))
367 
368  # allow preloading of libraries
369  if opts.preload:
370  preload = os.environ.get("LD_PRELOAD", "")
371  if preload:
372  preload = preload.replace(" ", ":").split(":")
373  else:
374  preload = []
375  for libname in set(preload).intersection(opts.preload):
376  logging.warning(
377  "Ignoring preload of library %s because it is "
378  "already in LD_PRELOAD.", libname)
379  to_load = [
380  libname for libname in opts.preload if libname not in set(preload)
381  ]
382  if to_load:
383  preload += to_load
384  preload = ":".join(preload)
385  os.environ["LD_PRELOAD"] = preload
386  logging.info("Restarting with LD_PRELOAD='%s'", preload)
387  # remove the --tcmalloc option from the arguments
388  # FIXME: the --preload arguments will issue a warning but it's tricky to remove them
389  args = [
390  a for a in sys.argv
391  if a != '-T' and not '--tcmalloc'.startswith(a)
392  ]
393  os.execv(sys.executable, [sys.executable] + args)
394 
395  # Profiler Support ------
396  if opts.profilerName:
397  profilerName = opts.profilerName
398  profilerExecName = ""
399  profilerOutput = opts.profilerOutput or (profilerName + ".output")
400 
401  # To restart the application removing the igprof option and prepending the string
402  args = getArgsWithoutProfilerInfo(sys.argv)
403 
404  igprofPerfOptions = "-d -pp -z -o igprof.pp.gz".split()
405 
406  profilerOptions = ""
407  if profilerName == "igprof":
408  if not opts.profilerOutput:
409  profilerOutput += ".profile.gz"
410  profilerOptions = "-d -z -o %s" % profilerOutput
411  profilerExecName = "igprof"
412 
413  elif profilerName == "igprofPerf":
414  if not opts.profilerOutput:
415  profilerOutput += ".pp.gz"
416  profilerOptions = "-d -pp -z -o %s" % profilerOutput
417  profilerExecName = "igprof"
418 
419  elif profilerName == "igprofMem":
420  if not opts.profilerOutput:
421  profilerOutput += ".mp.gz"
422  profilerOptions = "-d -mp -z -o %s" % profilerOutput
423  profilerExecName = "igprof"
424 
425  elif "valgrind" in profilerName:
426  # extract the tool
427  if not opts.profilerOutput:
428  profilerOutput += ".log"
429  toolname = profilerName.replace('valgrind', '')
430  outoption = "--log-file"
431  if toolname in ("massif", "callgrind", "cachegrind"):
432  outoption = "--%s-out-file" % toolname
433  profilerOptions = "--tool=%s %s=%s" % (toolname, outoption,
434  profilerOutput)
435  profilerExecName = "valgrind"
436 
437  elif profilerName == "jemalloc":
438  opts.preload.insert(
439  0, os.environ.get("JEMALLOCLIB", "libjemalloc.so"))
440  os.environ['MALLOC_CONF'] = "prof:true,prof_leak:true"
441  else:
442  root_logger.warning("Profiler %s not recognized!" % profilerName)
443 
444  # Add potential extra options
445  if opts.profilerExtraOptions != "":
446  profilerExtraOptions = opts.profilerExtraOptions
447  profilerExtraOptions = profilerExtraOptions.replace("__", "--")
448  profilerOptions += " %s" % profilerExtraOptions
449 
450  # now we look for the full path of the profiler: is it really there?
451  if profilerExecName:
452  import distutils.spawn
453  profilerPath = distutils.spawn.find_executable(profilerExecName)
454  if not profilerPath:
455  root_logger.error(
456  "Cannot locate profiler %s" % profilerExecName)
457  sys.exit(1)
458 
459  root_logger.info("------ Profiling options are on ------ \n"
460  " o Profiler: %s\n"
461  " o Options: '%s'.\n"
462  " o Output: %s" % (profilerExecName or profilerName,
463  profilerOptions, profilerOutput))
464 
465  # allow preloading of libraries
466  # That code need to be acsracted from above
467  to_reload = []
468  if opts.preload:
469  to_reload = setLibraryPreload(opts.preload)
470 
471  if profilerExecName:
472  # We profile python
473  profilerOptions += " python"
474 
475  # now we have all the ingredients to prepare our command
476  arglist = [profilerPath] + profilerOptions.split() + args
477  arglist = [a for a in arglist if a != '']
478  # print profilerPath
479  # for arg in arglist:
480  # print arg
481  os.execv(profilerPath, arglist)
482  else:
483  arglist = [a for a in sys.argv if not a.startswith("--profiler")]
484  os.execv(sys.executable, [sys.executable] + arglist)
485 
486  # End Profiler Support ------
487 
488  if opts.pickle_output:
489  if opts.output:
490  root_logger.error(
491  "Conflicting options: use only --pickle-output or --output")
492  sys.exit(1)
493  else:
494  root_logger.warning(
495  "--pickle-output is deprecated, use --output instead")
496  opts.output = opts.pickle_output
497 
498  from Gaudi.Main import gaudimain
499  c = gaudimain()
500 
501  from GaudiConfig2 import CALLABLE_FORMAT, mergeConfigs, invokeConfig, Configurable
502 
503  callables = []
504  opt_files = []
505  for arg in args:
506  if CALLABLE_FORMAT.match(arg):
507  callables.append(arg)
508  else:
509  opt_files.append(arg)
510 
511  # Prepare the "configuration script" to parse (like this it is easier than
512  # having a list with files and python commands, with an if statements that
513  # decides to do importOptions or exec)
514  options = ["importOptions(%r)" % f for f in opt_files]
515  # The option lines are inserted into the list of commands using their
516  # position on the command line
517  optlines = list(opts.options)
518  # this allows to avoid to have to care about corrections of the positions
519  optlines.reverse()
520  for pos, l in optlines:
521  options.insert(pos, l)
522 
523  # prevent the usage of GaudiPython
524  class FakeModule(object):
525  def __init__(self, exception):
526  self.exception = exception
527 
528  def __getattr__(self, *args, **kwargs):
529  raise self.exception
530 
531  sys.modules["GaudiPython"] = FakeModule(
532  RuntimeError("GaudiPython cannot be used in option files"))
533 
534  # when the special env GAUDI_TEMP_OPTS_FILE is set, it overrides any
535  # option(file) on the command line
536  if 'GAUDI_TEMP_OPTS_FILE' in os.environ:
537  options = ['importOptions(%r)' % os.environ['GAUDI_TEMP_OPTS_FILE']]
538  PrintOff(100)
539 
540  # "execute" the configuration script generated (if any)
541  if options:
542  g = {}
543  l = {}
544  exec("from Gaudi.Configuration import *", g, l)
545  for o in options:
546  logging.debug(o)
547  exec(o, g, l)
548 
549  import GaudiKernel.Proxy.Configurable
550  if opts.no_conf_user_apply:
551  logging.info("Disabling automatic apply of ConfigurableUser")
552  # pretend that they have been already applied
553  GaudiKernel.Proxy.Configurable._appliedConfigurableUsers_ = True
554 
555  # This need to be done before dumping
556  if opts.old_conf_user_apply:
557  from GaudiKernel.Proxy.Configurable import applyConfigurableUsers_old as applyConfigurableUsers
558  else:
559  from GaudiKernel.Proxy.Configurable import applyConfigurableUsers
561 
562  # Options to be processed after applyConfigurableUsers
563  if opts.post_options:
564  g = {}
565  l = {}
566  exec("from Gaudi.Configuration import *", g, l)
567  for o in opts.post_options:
568  logging.debug(o)
569  exec(o, g, l)
570 
571  if 'GAUDI_TEMP_OPTS_FILE' in os.environ:
572  os.remove(os.environ['GAUDI_TEMP_OPTS_FILE'])
573  opts.use_temp_opts = False
574 
575  # Run callables
576  config = mergeConfigs(*[invokeConfig(f) for f in callables])
577  # make configurations available to getAllOpts
578  # FIXME the whole machinery has to be inverted, to avoid relying on globals
579  Configurable.instances = mergeConfigs(Configurable.instances, config)
580 
581  if opts.verbose and not opts.use_temp_opts:
582  c.printconfig(opts.old_opts, opts.all_opts)
583  if opts.output:
584  c.writeconfig(opts.output, opts.all_opts)
585 
586  if opts.use_temp_opts:
587  fd, tmpfile = mkstemp('.opts')
588  os.close(fd)
589  c.writeconfig(tmpfile, opts.all_opts)
590  os.environ['GAUDI_TEMP_OPTS_FILE'] = tmpfile
591  logging.info('Restarting from pre-parsed options')
592  os.execv(sys.executable, [sys.executable] + sys.argv)
593 
594  c.printsequence = opts.printsequence
595  if opts.printsequence:
596  if opts.ncpus:
597  logging.warning(
598  "--printsequence not supported with --ncpus: ignored")
599  elif opts.dry_run:
600  logging.warning(
601  "--printsequence not supported with --dry-run: ignored")
602 
603  c.application = opts.application
604 
605  # re-enable the GaudiPython module
606  del sys.modules["GaudiPython"]
607 
608  if not opts.dry_run:
609  # Do the real processing
610  retcode = c.run(opts.gdb, opts.ncpus)
611 
612  # Now saving the run information pid, retcode and executable path to
613  # a file is requested
614  if opts.run_info_file:
615  import os
616  import json
617  run_info = {}
618  run_info["pid"] = os.getpid()
619  run_info["retcode"] = retcode
620  if os.path.exists('/proc/self/exe'):
621  # These options can be used only on unix platforms
622  run_info["exe"] = os.readlink('/proc/self/exe')
623 
624  logging.info("Saving run info to: %s" % opts.run_info_file)
625  with open(opts.run_info_file, "w") as f:
626  json.dump(run_info, f)
627 
628  sys.exit(retcode)
gaudirun.FakeModule.__init__
def __init__(self, exception)
Definition: gaudirun.py:525
gaudirun.FakeModule
Definition: gaudirun.py:524
gaudirun.FakeModule.exception
exception
Definition: gaudirun.py:526
GaudiConfig2.invokeConfig
def invokeConfig(func, *args, **kwargs)
Definition: __init__.py:60
gaudirun.FakeModule.__getattr__
def __getattr__(self, *args, **kwargs)
Definition: gaudirun.py:528
gaudirun.getArgsFromQmt
def getArgsFromQmt(qmtfile)
Definition: gaudirun.py:90
GaudiKernel.ProcessJobOptions
Definition: ProcessJobOptions.py:1
gaudirun.getArgsWithoutProfilerInfo
def getArgsWithoutProfilerInfo(args)
Definition: gaudirun.py:19
GaudiConfig2.mergeConfigs
def mergeConfigs(*configs)
Definition: __init__.py:41
Gaudi.Main
Definition: Main.py:1
Gaudi.Main.gaudimain
Definition: Main.py:314
GaudiKernel.ProcessJobOptions.InstallRootLoggingHandler
def InstallRootLoggingHandler(prefix=None, level=None, stream=None, with_time=False)
Definition: ProcessJobOptions.py:126
gaudirun.option_cb
def option_cb(option, opt, value, parser)
Definition: gaudirun.py:185
gaudirun.setLibraryPreload
def setLibraryPreload(newpreload)
Definition: gaudirun.py:48
gaudirun.rationalizepath
def rationalizepath(path)
Definition: gaudirun.py:74
GaudiKernel.ProcessJobOptions.PrintOff
def PrintOff(step=1)
Definition: ProcessJobOptions.py:142
GaudiKernel.Configurable.applyConfigurableUsers
def applyConfigurableUsers()
Definition: Configurable.py:1542