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