Line Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
#!/usr/bin/env python3
import os
import sys
import argparse
import fnmatch

import FWCore.ParameterSet.Config as cms
import HLTrigger.Configuration.Tools.pipe as pipe
import HLTrigger.Configuration.Tools.options as options
from HLTrigger.Configuration.extend_argparse import *

def getPathList(args):

  if isinstance(args.menu, options.ConnectionHLTMenu):
    # cmd to download HLT configuration
    cmdline = 'hltConfigFromDB'
    if args.menu.run:
      cmdline += f' --runNumber {args.menu.run}'
    else:
      cmdline += f' --{args.menu.database} --{args.menu.version} --configName {args.menu.name}'
    cmdline += ' --noedsources --noes --noservices'
    if args.proxy:
      cmdline += f' --dbproxy --dbproxyhost {args.proxy_host} --dbproxyport {args.proxy_port}'

  else:
    # use edmConfigDump to ensure the config can be executed
    cmdline = f'edmConfigDump {args.menu}'

  # load HLT configuration
  try:
    foo = {'process': None}
    exec(pipe.pipe(cmdline).decode(), foo)
    process = foo['process']
  except:
    raise Exception(f'query did not return a valid python file:\n query="{cmdline}"')

  if not isinstance(process, cms.Process):
    raise Exception(f'query did not return a valid HLT menu:\n query="{cmdline}"')

  usePaths, useEndPaths = False, False

  # Paths only
  if args.selection == 'paths':
    usePaths = True

  # EndPaths only
  elif args.selection == 'endpaths':
    useEndPaths = True

  # Paths and EndPaths ('all')
  elif args.selection == 'all':
    usePaths, useEndPaths = True, True

  # invalid value
  else:
    raise RuntimeError(f'ERROR: invalid value for option "--selection" (must be "paths", "endpaths", or "all"): {args.selection}')

  path_keep_rules = []
  for path_keep_rule in args.path_keep_rules.split(','):
    if not path_keep_rule:
      continue
    keep_rule = not path_keep_rule.startswith('-')
    pattern_idx = 0 if keep_rule else 1
    rule_pattern = path_keep_rule[pattern_idx:]
    path_keep_rules += [(keep_rule, rule_pattern)]

  ret = []
  for pathDict in [
    process.paths_() if usePaths else None,
    process.endpaths_() if useEndPaths else None,
  ]:
    if pathDict == None:
      continue

    for pathName in pathDict:

      # keep or drop the Path based on whether or not
      # its name complies with the patterns in path_keep_rules (if any)
      keepPath = not path_keep_rules
      for (keep_rule, rule_pattern) in path_keep_rules:
        if fnmatch.fnmatch(pathName, rule_pattern):
          keepPath = keep_rule
      if not keepPath:
        continue

      if args.no_dependent_paths:
        # do not include "dependent paths", i.e. paths that depend on the result of other paths in the same job
        # the current criterion to identify a path as "dependent" is that
        # (1) the path contains a "TriggerResultsFilter" module and
        # (2) the latter module uses the TriggerResults of the current process, and has a non-empty list of "triggerConditions"
        path = pathDict[pathName]
        pathIsDependent = False
        isPath = isinstance(path, cms.Path)

        for moduleName in path.moduleNames():
          module = getattr(process, moduleName)
          if module.type_() != 'TriggerResultsFilter' or (hasattr(module, 'triggerConditions') and len(module.triggerConditions) == 0):
            continue

          usesPathStatus = hasattr(module, 'usePathStatus') and module.usePathStatus
          usesTrigResOfCurrentProcess = hasattr(module, 'hltResults') and module.hltResults.getProcessName() in [process.name_(), '@currentProcess']+['']*(not isPath)

          if isPath:
            if usesPathStatus:
              pathIsDependent = True
            elif usesTrigResOfCurrentProcess:
              # The Path contains a TriggerResultsFilter with usePathStatus=False and forcing access to the TriggerResults of the current Process.
              #  - This is not supported, and should result in a runtime error when using cmsRun.
              #  - Here, a warning is returned to stderr, and the Path is omitted from the output list.
              warning_msg = 'WARNING -- the cms.Path named "'+pathName+'" will be ignored.'
              warning_msg += '\n'+' '*12+'- It contains a "TriggerResultsFilter" attempting to access the "TriggerResults" of the current Process (module: "'+moduleName+'").'
              warning_msg += '\n'+' '*12+'- This is not supported, and should result in a runtime error when using cmsRun. Please check again the HLT configuration.'
              print(warning_msg, file=sys.stderr)
              pathIsDependent = True
          else:
            pathIsDependent = usesPathStatus or usesTrigResOfCurrentProcess

          if pathIsDependent:
            break

        if pathIsDependent:
          continue

      ret.append(pathName)

  return ret

# define an argparse parser to parse our options
textwidth = int( 80 )
try:
  textwidth = int( os.popen("stty size", "r").read().split()[1] )
except:
  pass
formatter = FixedWidthFormatter( HelpFormatterRespectNewlines, width = textwidth )

# read defaults
defaults = options.HLTProcessOptions()

def hltMenu(name):
  return name if os.path.isfile(name) else options.ConnectionHLTMenu(name)

parser = argparse.ArgumentParser(
  description       = 'List all the Paths and EndPaths of an HLT configuration.',
  argument_default  = argparse.SUPPRESS,
  formatter_class   = formatter,
  add_help          = False )

# required argument
parser.add_argument('menu',
                    action  = 'store',
                    type    = hltMenu,
                    metavar = 'MENU',
                    help    = 'HLT menu (can be a local cmsRun configuration file, or the name of a configuration in the ConfDB database).\nFor ConfDB configurations, supported formats are:\n- /path/to/configuration[/Vn]\n- [[{v1|v2|v3}/]{run3|run2|online|adg}:]/path/to/configuration[/Vn]\n- run:runnumber\nThe possible converters are "v1", "v2, and "v3" (default).\nThe possible databases are\n"run3" (default, used for offline development in Run 3),\n"run2" (used for accessing Run-2 offline development menus),\n"online" (used to extract online menus from inside Point 5) and\n"adg" (used to extract the online menus from outside Point 5).\nIf no menu version is specified, the latest one is automatically used.\nIf "run:" is used instead, the HLT menu used for the given run number is looked up and used.\nNote: other converters and databases exist, but they are only for expert/special use.' )

# options
parser.add_argument('--dbproxy',
                    dest    = 'proxy',
                    action  = 'store_true',
                    default = defaults.proxy,
                    help    = 'Use a socks proxy to connect outside CERN network (default: False)' )
parser.add_argument('--dbproxyport',
                    dest    = 'proxy_port',
                    action  = 'store',
                    metavar = 'PROXYPORT',
                    default = defaults.proxy_port,
                    help    = 'Port of the socks proxy (default: 8080)' )
parser.add_argument('--dbproxyhost',
                    dest    = 'proxy_host',
                    action  = 'store',
                    metavar = 'PROXYHOST',
                    default = defaults.proxy_host,
                    help    = 'Host of the socks proxy (default: "localhost")' )

group = parser.add_mutually_exclusive_group()
group.add_argument('-p', '--only-paths',
                    dest    = 'selection',
                    action  = 'store_const',
                    const   = 'paths',
                    help    = 'List only Paths' )
group.add_argument('-e', '--only-endpaths',
                    dest    = 'selection',
                    action  = 'store_const',
                    const   = 'endpaths',
                    help    = 'List only EndPaths' )
group.add_argument('-a', '--all', 
                    dest    = 'selection',
                    action  = 'store_const',
                    const   = 'all',
                    default = 'all',
                    help    = 'List Paths and EndPaths (default)' )

parser.add_argument('--no-dependent-paths',
                    dest    = 'no_dependent_paths',
                    action  = 'store_true',
                    default = False,
                    help    = 'Do not list paths which depend on the result of other paths (default: false)' )

parser.add_argument('-s', '--select-paths',
                    dest    = 'path_keep_rules',
                    action  = 'store',
                    default = '',
                    help    = 'Comma-separated list of Path-name patterns (incl. wildcards) to select a subset of Paths using fnmatch.\nIf a Path-name pattern starts with the dash character (-), the Paths whose name matches that pattern are not selected.\nThe patterns are ordered: a given pattern can override previous ones (example: "*,-Foo,*" retains all Paths)\n(default: empty, meaning all Paths are kept)')

# redefine "--help" to be the last option, and use a customized message 
parser.add_argument('-h', '--help', 
                    action  = 'help', 
                    help    = 'Show this help message and exit' )

# parse command line arguments and options
args = parser.parse_args()

paths = getPathList(args)
for path in paths:
  print(path)