#!/usr/bin/env python import argparse import collections import multiprocessing import time from six.moves import configparser ProcessNode = collections.namedtuple("ProcessNode", ["maxtime", "children"]) class ProcessLauncher(object): """Create and Launch process trees specified by a '.ini' file Typical .ini file accepted by this class : [main] children=c1, 1*c2, 4*c3 maxtime=10 [c1] children= 2*c2, c3 maxtime=20 [c2] children=3*c3 maxtime=5 [c3] maxtime=3 This generates a process tree of the form: [main] |---[c1] | |---[c2] | | |---[c3] | | |---[c3] | | |---[c3] | | | |---[c2] | | |---[c3] | | |---[c3] | | |---[c3] | | | |---[c3] | |---[c2] | |---[c3] | |---[c3] | |---[c3] | |---[c3] |---[c3] |---[c3] Caveat: The section names cannot contain a '*'(asterisk) or a ','(comma) character as these are used as delimiters for parsing. """ # Unit time for processes in seconds UNIT_TIME = 1 def __init__(self, manifest, verbose=False): """ Parses the manifest and stores the information about the process tree in a format usable by the class. Raises IOError if : - The path does not exist - The file cannot be read Raises ConfigParser.*Error if: - Files does not contain section headers - File cannot be parsed because of incorrect specification :param manifest: Path to the manifest file that contains the configuration for the process tree to be launched :verbose: Print the process start and end information. Genrates a lot of output. Disabled by default. """ self.verbose = verbose # Children is a dictionary used to store information from the, # Configuration file in a more usable format. # Key : string contain the name of child process # Value : A Named tuple of the form (max_time, (list of child processes of Key)) # Where each child process is a list of type: [count to run, name of child] self.children = {} cfgparser = configparser.ConfigParser() if not cfgparser.read(manifest): raise IOError("The manifest %s could not be found/opened", manifest) sections = cfgparser.sections() for section in sections: # Maxtime is a mandatory option # ConfigParser.NoOptionError is raised if maxtime does not exist if "*" in section or "," in section: raise configparser.ParsingError( "%s is not a valid section name. " "Section names cannot contain a '*' or ','." % section ) m_time = cfgparser.get(section, "maxtime") try: m_time = int(m_time) except ValueError: raise ValueError( "Expected maxtime to be an integer, specified %s" % m_time ) # No children option implies there are no further children # Leaving the children option blank is an error. try: c = cfgparser.get(section, "children") if not c: # If children is an empty field, assume no children children = None else: # Tokenize chilren field, ignore empty strings children = [ [y.strip() for y in x.strip().split("*", 1)] for x in c.split(",") if x ] try: for i, child in enumerate(children): # No multiplicate factor infront of a process implies 1 if len(child) == 1: children[i] = [1, child[0]] else: children[i][0] = int(child[0]) if children[i][1] not in sections: raise configparser.ParsingError( "No section corresponding to child %s" % child[1] ) except ValueError: raise ValueError( "Expected process count to be an integer, specified %s" % child[0] ) except configparser.NoOptionError: children = None pn = ProcessNode(maxtime=m_time, children=children) self.children[section] = pn def run(self): """ This function launches the process tree. """ self._run("main", 0) def _run(self, proc_name, level): """ Runs the process specified by the section-name `proc_name` in the manifest file. Then makes calls to launch the child processes of `proc_name` :param proc_name: File name of the manifest as a string. :param level: Depth of the current process in the tree. """ if proc_name not in self.children: raise IOError("%s is not a valid process" % proc_name) maxtime = self.children[proc_name].maxtime if self.verbose: print( "%sLaunching %s for %d*%d seconds" % (" " * level, proc_name, maxtime, self.UNIT_TIME) ) while self.children[proc_name].children: child = self.children[proc_name].children.pop() count, child_proc = child for i in range(count): p = multiprocessing.Process( target=self._run, args=(child[1], level + 1) ) p.start() self._launch(maxtime) if self.verbose: print("%sFinished %s" % (" " * level, proc_name)) def _launch(self, running_time): """ Create and launch a process and idles for the time specified by `running_time` :param running_time: Running time of the process in seconds. """ elapsed_time = 0 while elapsed_time < running_time: time.sleep(self.UNIT_TIME) elapsed_time += self.UNIT_TIME if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("manifest", help="Specify the configuration .ini file") args = parser.parse_args() proclaunch = ProcessLauncher(args.manifest) proclaunch.run()