The Gaudi Framework  master (bdbe7744)
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 = f"Invalid value : --ncpus : only {sys_cpus} cpus available"
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 sanitizer_list = sanitizers.split(":")
376 # PRELOAD_SANITIZER_LIB may be a bare filename (e.g. "libasan.so")
377 # while LD_PRELOAD has already been set to its resolved absolute
378 # path by another mechanism (e.g. gaudi_add_pytest()'s collection-
379 # time resolution); comparing the two strings for exact equality
380 # would then spuriously report LD_PRELOAD as "different", even
381 # though it already covers every entry of PRELOAD_SANITIZER_LIB.
382 # Check containment instead, matching the check already used below
383 # to avoid re-adding an already-preloaded library.
384 if preload and not all(s in preload for s in sanitizer_list):
385 logging.warning(
386 f"Ignoring PRELOAD_SANITIZER_LIB (={sanitizers}) as LD_PRELOAD (={preload}) is "
387 "different and takes precedence."
388 )
389 else:
390 for sanitizer in reversed(sanitizer_list):
391 if sanitizer not in preload:
392 opts.preload.insert(0, sanitizer)
393 if opts.profilerName == "jemalloc":
394 logging.warning("jemalloc disabled when using a sanitizer")
395 opts.profilerName = None
396
397 # tcmalloc support
398 if opts.tcmalloc:
399 # Disable tcmalloc if sanitizer is selected
400 if sanitizers:
401 logging.warning("tcmalloc preload disabled when using a sanitizer")
402 else:
403 opts.preload.insert(0, os.environ.get("TCMALLOCLIB", "libtcmalloc.so"))
404
405 # allow preloading of libraries
406 if opts.preload:
407 preload = os.environ.get("LD_PRELOAD", "")
408 if preload:
409 preload = preload.replace(" ", ":").split(":")
410 else:
411 preload = []
412 for libname in set(preload).intersection(opts.preload):
413 logging.warning(
414 "Ignoring preload of library %s because it is "
415 "already in LD_PRELOAD.",
416 libname,
417 )
418 to_load = [libname for libname in opts.preload if libname not in set(preload)]
419 if to_load:
420 preload += to_load
421 preload = ":".join(preload)
422 os.environ["LD_PRELOAD"] = preload
423 logging.info("Restarting with LD_PRELOAD='%s'", preload)
424 # remove the --tcmalloc option from the arguments
425 # FIXME: the --preload arguments will issue a warning but it's tricky to remove them
426 args = [a for a in sys.argv if a != "-T" and not "--tcmalloc".startswith(a)]
427 os.execv(sys.executable, [sys.executable] + args)
428
429 # Profiler Support ------
430 if opts.profilerName:
431 profilerName = opts.profilerName
432 profilerExecName = ""
433 profilerOutput = opts.profilerOutput or (profilerName + ".output")
434
435 # To restart the application removing the igprof option and prepending the string
436 args = getArgsWithoutProfilerInfo(sys.argv)
437
438 igprofPerfOptions = "-d -pp -z -o igprof.pp.gz".split()
439
440 profilerOptions = ""
441 if profilerName == "igprof":
442 if not opts.profilerOutput:
443 profilerOutput += ".profile.gz"
444 profilerOptions = f"-d -z -o {profilerOutput}"
445 profilerExecName = "igprof"
446
447 elif profilerName == "igprofPerf":
448 if not opts.profilerOutput:
449 profilerOutput += ".pp.gz"
450 profilerOptions = f"-d -pp -z -o {profilerOutput}"
451 profilerExecName = "igprof"
452
453 elif profilerName == "igprofMem":
454 if not opts.profilerOutput:
455 profilerOutput += ".mp.gz"
456 profilerOptions = f"-d -mp -z -o {profilerOutput}"
457 profilerExecName = "igprof"
458
459 elif "valgrind" in profilerName:
460 # extract the tool
461 if not opts.profilerOutput:
462 profilerOutput += ".log"
463 toolname = profilerName.replace("valgrind", "")
464 outoption = "--log-file"
465 if toolname in ("massif", "callgrind", "cachegrind"):
466 outoption = f"--{toolname}-out-file"
467 profilerOptions = f"--tool={toolname} {outoption}={profilerOutput}"
468 profilerExecName = "valgrind"
469
470 elif profilerName == "jemalloc":
471 opts.preload.insert(0, os.environ.get("JEMALLOCLIB", "libjemalloc.so"))
472 os.environ["MALLOC_CONF"] = "prof:true,prof_leak:true"
473 else:
474 root_logger.warning(f"Profiler {profilerName} not recognized!")
475
476 # Add potential extra options
477 if opts.profilerExtraOptions != "":
478 profilerExtraOptions = opts.profilerExtraOptions
479 profilerExtraOptions = profilerExtraOptions.replace("__", "--")
480 profilerOptions += f" {profilerExtraOptions}"
481
482 # now we look for the full path of the profiler: is it really there?
483 if profilerExecName:
484 import distutils.spawn
485
486 profilerPath = distutils.spawn.find_executable(profilerExecName)
487 if not profilerPath:
488 root_logger.error(f"Cannot locate profiler {profilerExecName}")
489 sys.exit(1)
490
491 root_logger.info(
492 "------ Profiling options are on ------ \n"
493 f" o Profiler: {profilerExecName or profilerName}\n"
494 f" o Options: '{profilerOptions}'.\n"
495 f" o Output: {profilerOutput}"
496 )
497
498 # allow preloading of libraries
499 # That code need to be acsracted from above
500 to_reload = []
501 if opts.preload:
502 to_reload = setLibraryPreload(opts.preload)
503
504 if profilerExecName:
505 # We profile python
506 profilerOptions += " python"
507
508 # now we have all the ingredients to prepare our command
509 arglist = [profilerPath] + profilerOptions.split() + args
510 arglist = [a for a in arglist if a != ""]
511 # print profilerPath
512 # for arg in arglist:
513 # print arg
514 os.execv(profilerPath, arglist)
515 else:
516 arglist = [a for a in sys.argv if not a.startswith("--profiler")]
517 os.execv(sys.executable, [sys.executable] + arglist)
518
519 # End Profiler Support ------
520
521 if opts.pickle_output:
522 if opts.output:
523 root_logger.error(
524 "Conflicting options: use only --pickle-output or --output"
525 )
526 sys.exit(1)
527 else:
528 root_logger.warning("--pickle-output is deprecated, use --output instead")
529 opts.output = opts.pickle_output
530
531 from Gaudi.Main import gaudimain
532
534
536 """
537 Helper class to be able to process option files or options
538 callables as they come along in the arguments.
539 """
540
541 def __init__(self, initial_config=None):
542 self.config = {} if initial_config is None else initial_config
543
544 def __call__(self, arg):
545 from GaudiConfig2 import CALLABLE_FORMAT, invokeConfig, mergeConfigs
546
547 from Gaudi.Configuration import importOptions
548
549 arg = os.path.expandvars(arg)
550
551 if CALLABLE_FORMAT.match(arg):
552 self.config = mergeConfigs(self.config, invokeConfig(arg))
553 else:
554 importOptions(arg)
555
556 process = ArgProcessor()
557
558 # Prepare the "configuration script" to parse (like this it is easier than
559 # having a list with files and python commands, with an if statements that
560 # decides to do importOptions or exec)
561 options = [f"process({arg!r})" for arg in args]
562 # The option lines are inserted into the list of commands using their
563 # position on the command line
564 optlines = list(opts.options)
565 # this allows to avoid to have to care about corrections of the positions
566 optlines.reverse()
567 for pos, l in optlines:
568 options.insert(pos, l)
569
570 # prevent the usage of GaudiPython
572 def __init__(self, exception):
573 self.exception = exception
574
575 def __getattr__(self, *args, **kwargs):
576 raise self.exception
577
578 sys.modules["GaudiPython"] = FakeModule(
579 RuntimeError("GaudiPython cannot be used in option files")
580 )
581
582 # when the special env GAUDI_TEMP_OPTS_FILE is set, it overrides any
583 # option(file) on the command line
584 if "GAUDI_TEMP_OPTS_FILE" in os.environ:
585 options = ["process({!r})".format(os.environ["GAUDI_TEMP_OPTS_FILE"])]
586 PrintOff(100)
587
588 # "execute" the configuration script generated (if any)
589 if options:
590 g = {"process": process}
591 l = {}
592 exec("from Gaudi.Configuration import *", g, l)
593 for o in options:
594 logging.debug(o)
595 exec(o, g, l)
596
597 import GaudiKernel.Proxy.Configurable
598
599 if opts.no_conf_user_apply:
600 logging.info("Disabling automatic apply of ConfigurableUser")
601 # pretend that they have been already applied
602 GaudiKernel.Proxy.Configurable._appliedConfigurableUsers_ = True
603
604 # This need to be done before dumping
605 if opts.old_conf_user_apply:
606 from GaudiKernel.Proxy.Configurable import (
607 applyConfigurableUsers_old as applyConfigurableUsers,
608 )
609 else:
610 from GaudiKernel.Proxy.Configurable import applyConfigurableUsers
611 applyConfigurableUsers()
612
613 # Options to be processed after applyConfigurableUsers
614 if opts.post_options:
615 g = {"process": process}
616 l = {}
617 exec("from Gaudi.Configuration import *", g, l)
618 for o in opts.post_options:
619 logging.debug(o)
620 exec(o, g, l)
621
622 if "GAUDI_TEMP_OPTS_FILE" in os.environ:
623 os.remove(os.environ["GAUDI_TEMP_OPTS_FILE"])
624 opts.use_temp_opts = False
625
626 # make configurations available to getAllOpts
627 # FIXME the whole machinery has to be inverted, to avoid relying on globals
628 from GaudiConfig2 import Configurable, mergeConfigs
629
630 Configurable.instances = mergeConfigs(Configurable.instances, process.config)
631
632 if opts.verbose and not opts.use_temp_opts:
633 c.printconfig(opts.old_opts, opts.all_opts)
634 if opts.output:
635 c.writeconfig(opts.output, opts.all_opts)
636
637 if opts.use_temp_opts:
638 fd, tmpfile = mkstemp(".opts")
639 os.close(fd)
640 c.writeconfig(tmpfile, opts.all_opts)
641 os.environ["GAUDI_TEMP_OPTS_FILE"] = tmpfile
642 logging.info("Restarting from pre-parsed options")
643 os.execv(sys.executable, [sys.executable] + sys.argv)
644
645 c.printsequence = opts.printsequence
646 if opts.printsequence:
647 if opts.ncpus:
648 logging.warning("--printsequence not supported with --ncpus: ignored")
649 elif opts.dry_run:
650 logging.warning("--printsequence not supported with --dry-run: ignored")
651
652 c.application = opts.application
653
654 # re-enable the GaudiPython module
655 del sys.modules["GaudiPython"]
656
657 if not opts.dry_run:
658 # Do the real processing
659 retcode = c.run(opts.gdb, opts.ncpus)
660
661 # Now saving the run information pid, retcode and executable path to
662 # a file is requested
663 if opts.run_info_file:
664 import json
665 import os
666
667 run_info = {}
668 run_info["pid"] = os.getpid()
669 run_info["retcode"] = retcode
670 if os.path.exists("/proc/self/exe"):
671 # These options can be used only on unix platforms
672 run_info["exe"] = os.readlink("/proc/self/exe")
673
674 logging.info(f"Saving run info to: {opts.run_info_file}")
675 with open(opts.run_info_file, "w") as f:
676 json.dump(run_info, f)
677
678 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:544
__init__(self, initial_config=None)
Definition gaudirun.py:541
__getattr__(self, *args, **kwargs)
Definition gaudirun.py:575
__init__(self, exception)
Definition gaudirun.py:572
option_cb(option, opt, value, parser)
Definition gaudirun.py:193
getArgsWithoutProfilerInfo(args)
Definition gaudirun.py:18
getArgsFromQmt(qmtfile)
Definition gaudirun.py:88
rationalizepath(path)
Definition gaudirun.py:72
setLibraryPreload(newpreload)
Definition gaudirun.py:47