[RavenclawDev 203] [1] src:

tk@edam.speech.cs.cmu.edu tk at edam.speech.cs.cmu.edu
Sun Dec 10 20:15:59 EST 2006


An HTML attachment was scrubbed...
URL: http://mailman.srv.cs.cmu.edu/pipermail/ravenclaw-developers/attachments/20061210/e0f006cb/attachment-0001.html
-------------- next part --------------
Added: src/basic_process_monitor.py
===================================================================
--- src/basic_process_monitor.py	                        (rev 0)
+++ src/basic_process_monitor.py	2006-12-11 01:15:58 UTC (rev 1)
@@ -0,0 +1,778 @@
+# This file (c) Copyright 1998 - 2002 The MITRE Corporation
+# 
+# This file is part of the Galaxy Communicator system. It is licensed
+# under the conditions described in the file LICENSE in the root 
+# directory of the Galaxy Communicator system.
+
+import os, string, re, tempfile
+
+if os.name == 'posix':
+    import unix_pm_core
+    from unix_pm_core import _MAYBE_ALIVE, _DEFINITELY_ALIVE, _DEFINITELY_DEAD
+    pm_core = unix_pm_core
+elif os.name in ['dos', 'nt']:
+    import win_pm_core
+    from win_pm_core import _MAYBE_ALIVE, _DEFINITELY_ALIVE, _DEFINITELY_DEAD
+    pm_core = win_pm_core
+else:
+    raise "Can't run process monitor on", os.name
+
+# This class controls the subprocess.
+
+# This class has the following basic behavior:
+# PROCESS_TITLE: or -T is the title
+# PAUSE: or --sleep is how long to sleep after startup
+# PROCESS_KEEP_ALIVE: --keep_alive is whether to restart after shutdown
+# PROCESS: or -c is the command line
+# PROCESS_WORKDIR: or --workdir is the working directory of a process (defaults to current directory)
+
+# Tk has --input_line, --input_return, --start, --open
+
+# 5-tuples are (config_file_entry, cmdline, needs arg, method name, block starter).
+
+# ONE BACKWARD COMPATIBILITY GLITCH: for PROCESS:, the keyword 
+# STARTS the block, while the command line ENDS the block. All the
+# others optionally start the block in the command line, while
+# they can't in the configuration file case. Sorry about that.
+# See the MustEndBlock() method on ProcessContainerSet.
+
+# For run_test_suite.py, ProcessContainer needs to be able to 
+# stand alone.
+
+ProcessContainerError = "ProcessContainerError"
+
+class ProcessContainer(pm_core.ProcessContainer):
+    config_table = [("PROCESS_TITLE:", "-T", 1, "AddTitle", 0),
+		    ("PROCESS_WORKDIR:", "--workdir", 1, "AddWorkDir", 0),
+		    ("PAUSE:", "--sleep", 1, "AddPause", 0),
+		    ("PROCESS_KEEP_ALIVE:", "--keep_alive", 0,
+		     "AddKeepAlive", 0),
+		    ("PROCESS:", "-c", 1, "AddCommandLine", 1)]
+
+    def __init__(self, process_set = None):
+	if process_set is not None:
+	    process_set.AddProcess(self)
+	self.process_set = process_set
+	self.title = ""
+	self.workdir = "."
+	self.pause = 0
+	self.keepalive = 0
+	self.cmdline = None
+        # Doing away with the file descriptor objects.
+	self._SetupHandles()
+	self.alive = 0
+	self.history_buffer = ""
+	self.error_buffer = ""
+	self.interleave_error = 1
+
+    # Configuration methods.
+
+    def AddTitle(self, title):
+	if self.title:
+	    raise ConfigurationError, "title already specified"
+	self.title = title
+
+    def AddPause(self, pause):
+	if self.pause:
+	    raise ConfigurationError, "pause already specified"
+	try:
+	    self.pause = string.atoi(pause)
+	except:
+	    raise ConfigurationError, \
+		  ("bad pause specification `%s'" % pause)
+
+    def AddKeepAlive(self):
+	self.keepalive = 1
+
+    def AddCommandLine(self, command_line):
+	if self.process_set is not None:
+	    env = self.process_set.process_environment
+	    if not env.suppress_expansions:
+		print("command_line: `%s'" % command_line)
+		command_line = env.ExpandFile(command_line)
+		print("expanded command_line: `%s'" % command_line)
+	if self.cmdline:
+	    raise ConfigurationError, "command line already specified"
+	self.cmdline = command_line
+
+    def AddWorkDir(self, workdir):
+	env = self.process_set.process_environment
+	if not env.suppress_expansions:
+	    workdir = env.ExpandFile(workdir)
+	self.workdir = workdir
+
+    # Runtime behavior.
+
+    def ConfigDone(self):
+	if not self.cmdline:
+	    raise ConfigurationError, "no command line"
+
+    def _SetupHandles(self):
+	self.in_handle = None
+	self.err_handle = None
+	self.out_handle = None
+        self.child_handle = None
+
+    def KeepAlive(self):
+	self.keepalive = 1
+
+    def LetDie(self):
+	self.keepalive = 0
+
+    def UpdateIdle(self):
+	pass
+    
+    def ConsiderShutdown(self):
+	if not (self.in_handle or self.err_handle):
+	    self.Shutdown()
+
+    def ConsiderRestart(self):
+	if self.keepalive:
+	    # If we're supposed to keep this process alive,
+	    # then restart the damn thing.
+	    self.Restart()
+
+    def WriteHistory(self, str, fd):
+	# Keep everything yourself.
+        if (fd is not None) and (fd == self.err_handle):
+	    self.error_buffer = self.error_buffer + str
+	self.history_buffer = self.history_buffer + str
+
+    def ClearHistory(self):
+	self.history_buffer = ""
+	self.error_buffer = ""
+
+    def ReadCallback(self, handle, num_bytes):
+        # num_bytes should be -1 when you can read as
+        # much as you want.
+        global _DEFINITELY_ALIVE, _MAYBE_ALIVE, _DEFINITELY_DEAD
+        status, data = self._ReadBytes(handle, num_bytes)
+        if status == _DEFINITELY_ALIVE:
+            if data:
+                self.WriteHistory(data, handle)
+            self.UpdateIdle()
+        elif ((status == _MAYBE_ALIVE) and self.Dead()) or \
+             (status == _DEFINITELY_DEAD):
+            # If it might have been alive, there may be data.
+            if data:
+                self.WriteHistory(data, handle)
+            # Try one last read, just in case something
+            # was written before it was definitely dead.
+            final_status, final_data = self._ReadBytes(handle, -1)
+            if final_data:
+                self.WriteHistory(final_data, handle)
+            self._ShutdownHandle(handle)
+            self.ConsiderShutdown()
+            self.ConsiderRestart()
+
+    def WriteCallback(self, handle):
+        raise ProcessContainerError, "not implemented"
+
+    def _ShutdownHandle(self, handle):
+        if handle == self.in_handle:
+            self._ForceShutdown(handle)
+            self.in_handle = None
+        elif handle == self.out_handle:
+            self._ForceShutdown(handle)
+            self.out_handle = None
+        elif handle == self.err_handle:
+            self._ForceShutdown(handle)
+            self.err_handle = None
+            
+    def _ShutdownHandles(self):
+        if self.in_handle is not None:
+	    self._ShutdownHandle(self.in_handle)
+        if self.out_handle is not None:
+	    self._ShutdownHandle(self.out_handle)
+        if self.err_handle is not None:
+	    self._ShutdownHandle(self.err_handle)
+        
+    def Shutdown(self, really_kill = 1):
+	# Unmonitor the output, and set the pause button
+	# to "resting" state.
+	# Returns the child pid of the killed element.
+	# If really_kill is 1, make sure it's dead, by sleeping
+	# for a brief time and then really killing it
+	# if it's not dead.
+        # Return the handle of the process if it's still alive.
+        self._ShutdownHandles()
+	p = self.child_handle
+	if self.child_handle:
+	    # It may already be dead...
+            p = self._ShutdownSubprocess(self.child_handle, really_kill)
+	    self.child_handle = None
+	    self.WriteHistory("======================================\n", None)
+	self.alive = 0
+	return p
+
+    def Dead(self):
+	if not self.alive:
+	    return 1
+        return self._CheckDead(self.child_handle)
+    
+    # In order to interleave stdout and stderr better, I've
+    # redirected stderr to stdout in the subshell. The problem is
+    # that under this configuration, the shell does not return
+    # a valid fd for the error stream. So I need not to monitor it.
+
+    def MonitorOutput(self):
+	# I need this for the child classes, I think.
+	pass
+
+    def Restart(self):
+	self.Shutdown()
+	# Don't buffer anything! If you do, the filehandler
+	# stuff won't work...
+	cmdline = self.GetCmdlineForExecution()
+        # Restart may fail. In Unix, this will just print
+        # something to stderr of an apparently live subprocess,
+        # but in Windows, it will raise an error. This is
+        # because we're running a shell in one case, but not
+        # in the other.
+        # Problem: we have to make sure that the right things
+        # happen when we get a failure. In particular, the
+        # folks built on top of this (e.g., the process monitor
+        # itself) assumes restart was successful, and that
+        # failure and shutdown will be processed as a subsequent
+        # read. After careful consideration, I decided to
+        # prepare fake handles.
+        info = self._RestartChildProcess(self.workdir, cmdline, self.interleave_error)
+        self.in_handle, self.out_handle, self.err_handle, self.child_handle = info
+        self.MonitorOutput()
+        self.alive = 1
+
+    def GetCmdlineForDisplay(self):
+	return self.cmdline
+
+    def GetCmdlineForExecution(self):
+	cmdline = self.cmdline
+	if self.process_set is not None:
+	    env = self.process_set.process_environment
+	    if not env.initialized:
+		env.Initialize()
+	    # If expansions are suppressed, the command line
+	    # needs to be expanded when it's executed.
+	    if env.suppress_expansions or env.tempfiles_created:
+		cmdline = env.ExpandFile(self.cmdline)
+	return cmdline
+
+    def SetCmdline(self, str):
+	if self.process_set is not None:
+	    env = self.process_set.process_environment
+	    if not env.suppress_expansions:
+		str = env.ExpandFile(str)
+	self.cmdline = str
+
+    def WriteString(self, str):
+        if self.out_handle:
+            self._WriteString(self.out_handle, str)
+	else:
+	    self.WriteHistory("[no process]\n", None)
+
+    def TokenizeCmdlineArgs(self, args):
+        in_double_quote = 0
+	in_single_quote = 0
+	in_escape = 0
+	cur_token = []
+	cmdline_args = []
+	# Don't forget, we also need to compute escapes...
+	for c in args:
+	    if in_escape:
+		cur_token.append(c)
+		in_escape = 0
+#	    elif c == "\\":
+#		in_escape = 1	    
+	    elif in_double_quote:
+		if c == '"':
+		    in_double_quote = 0
+		else:
+		    cur_token.append(c)
+	    elif in_single_quote:
+		if c == "'":
+		    in_single_quote = 0
+		else:
+		    cur_token.append(c)
+	    elif c == "'":
+		in_single_quote = 1
+	    elif c == '"':
+		in_double_quote = 1
+	    elif c in string.whitespace:
+		if cur_token:
+		    cmdline_args.append(string.join(cur_token, ""))
+		    cur_token = []
+	    else:
+		cur_token.append(c)
+	if cur_token:
+	    cmdline_args.append(string.join(cur_token, ""))
+        return cmdline_args
+
+# This class controls a set of processes. It can be
+# subclassed as an individual test (for the test harness)
+# or a process monitor window (for the interactive
+# controller).
+
+class ProcessContainerSet(pm_core.ProcessContainerSet):
+    config_table = [("TITLE:", "--master_title", 1, "AddTitle", 1),
+                    ("REQUIRES:", None, 1, "AddRequirements", 0)]
+
+    def __init__(self, environment = None):
+	self.processes = []
+        if environment is not None:
+            environment.AddProcessSet(self)
+        self.process_environment = environment
+	self.title = ""
+        self.requirements = []
+
+    def AddProcess(self, process):
+	self.processes.append(process)
+    
+    def AddTitle(self, title):
+	self.title = title
+
+    def AddRequirements(self, remainder):
+        # Requirements can now have values.
+        reqs = string.split(remainder)
+        for r in reqs:
+            res = string.split(r, "=", 1)
+            if len(res) == 2:
+                self.requirements.append((res[0], res[1]))
+            else:
+                self.requirements.append((res[0], None))
+
+    def UnmetRequirements(self):
+	# Return the missing requirements
+	failed = []
+	for req, value in self.requirements:
+	    if string.upper(req) not in self.process_environment.requirements_met:
+		failed.append(req)
+            elif (value is not None) and \
+                 (self.process_environment.ExpandFile("$"+string.upper(req)) != value):
+                failed.append(req+"="+value)
+	return failed
+    
+    def CanStartBlock(self, config_entry, from_cmdline):
+	# Anything can start a process block from the command line.	
+	if from_cmdline:
+	    return 1
+	else:
+	    return config_entry[-2]
+
+    def MustStartBlock(self, config_entry, from_cmdline):
+	# Nothing MUST start a block on the command line.
+	if from_cmdline:
+	    return 0
+	else:
+	    return config_entry[-2]
+
+    def MustEndBlock(self, config_entry, from_cmdline):
+	# However, on the command line, -c must end a block.
+	# See the backward compatibiliy notes for the ProcessContainer class.
+	if from_cmdline:
+	    return config_entry[1] == "-c"
+	else:
+	    return 0
+
+    def Run(self):
+        some_alive = 1
+        while some_alive:
+            p_info = []
+            for p in self.processes:
+                if p.alive:
+                    if p.in_handle is not None:
+                        p_info.append((p, p.in_handle))
+                    if p.err_handle is not None:
+                        p_info.append((p, p.err_handle))
+            if not p_info:
+                break
+            else:
+                res_info = self._CheckInput(p_info)
+                for p, handle, num_bytes in res_info:
+                    p.ReadCallback(handle, num_bytes)
+
+    def EnforceShutdown(self, process_handles):
+	self._EnforceShutdown(process_handles, really_kill = 1)
+
+    def ExecutableExists(self, executable):
+        return self._ExecutableExists(executable)
+
+
+# This class controls a set of process sets. This 
+# contains the global keyword/value substitution environment.
+# It also figures out how to process the command line and
+# configuration file.
+
+import getopt
+
+ConfigurationError = "ConfigurationError"
+
+# INCLUDE: is also an option.
+
+class ProcessEnvironment(pm_core.ProcessEnvironment):
+    
+    config_table = [("EXPAND:", None, 1, "AddDirectoryExpansion", 0),
+                    ("TEMPFILE:", None, 1, "AddTempFile", 0),
+		    ("ARGUMENTS:", None, 1, "AddArguments", 0),
+		    ("SUPPRESS_EXPANSIONS:", None, 0, "SuppressExpansions", 0)]
+    
+    def __init__(self, container_set_class = ProcessContainerSet,
+		 container_class = ProcessContainer):
+	self.container_set_class = container_set_class
+	self.container_class = container_class
+	self.expansions = []
+	self.process_sets = []
+	self.config_choice = None
+	self.file = "<cmdline>"
+	self.suppress_expansions = 0
+	self.files_to_remove = []
+	self.initialized = 0
+	self.tempfiles_created = 0
+        self.num_conforming_process_sets = 0
+        
+	self.AddDirectoryExpansion("$GC_HOME " + os.environ["GC_HOME"])
+        archos = self.ComputeArchos(os.environ["GC_HOME"])
+        self.AddDirectoryExpansion("$ARCHOS " + archos)
+        # Finally, I want to add the OSTYPE, so I can distinguish
+        # coarsely between platform types. Values will be
+        # values of os.name, e.g., posix, nt.
+        self.AddDirectoryExpansion("$OSTYPE " + os.name)
+        # Add config file data. We're now generating a values
+        self.requirements_met = ["GC_HOME", "ARCHOS", "OSTYPE"] 
+        # file, which means we don't have to parse the Makefile.
+        try:
+            value_fp = open(os.path.join(os.environ["GC_HOME"], "templates", archos, "config.values"), "r")
+            for line in value_fp.readlines():
+                l = string.strip(line)
+                if l and l[0] != "#" and (len(string.split(line)) > 1):
+                    # Don't record it if there's no value. This can happen
+                    # in config.make.
+                    self.AddDirectoryExpansion("$"+l)
+                    self.requirements_met.append(string.upper(string.split(l)[0]))
+        except IOError:
+            pass
+        
+    def ComputeArchos(self, gc_home):
+        return self._ComputeArchos(gc_home)
+	
+    def DigestCommandLine(self, command_line,
+                          accept_args = 1, accept_config_file = 1):
+	expanded_config = self._AssembleConfiguration()
+	optlist, from_cmdline = self._DigestConfiguration(command_line,
+							  expanded_config,
+							  accept_args,
+							  accept_config_file)
+	self._Configure(optlist, from_cmdline, expanded_config)
+	for s in self.process_sets:
+	    for p in s.processes:
+		p.ConfigDone()        
+        for p_set in self.process_sets:
+            if not p_set.UnmetRequirements():
+                self.num_conforming_process_sets = 1 + self.num_conforming_process_sets
+                p_set.meets_requirements = 1
+            else:
+                p_set.meets_requirements = 0
+	
+    # Config table method.
+    
+    def AddDirectoryExpansion(self, instructions):
+	# SAM 11/30/01: Special case where instructions
+	# of length 1 generate a temp file which is
+	# instantiated at the beginning and removed
+	# at the end.
+        # SAM 7/23/02: I've decided to allow values
+        # with whitespace.
+	l = string.split(instructions, None, 1)
+	# Also, make sure that when you record the expansion,
+	# YOU USE THE PREVIOUS EXPANSIONS.
+        if len(l) > 2:
+            raise ConfigurationError, "Ill-formed EXPAND: directive"
+        elif len(l) == 2:
+	    self._IAddDirectoryExpansion(l[0], l[1])
+	else:
+            self.expansions.append((l[0], ""))
+
+    def AddTempFile(self, instructions):
+        l = string.split(instructions, None, 1)
+        if len(l) != 1:
+            raise ConfigurationError, "Ill-formed TEMPFILE: directive"
+        self.expansions.append((l[0], None))
+
+    # For tempfiles.
+    
+    def Initialize(self):
+	if self.initialized: return
+	new_exps = []
+	for key, val in self.expansions:
+	    if val is None:
+		self.tempfiles_created = 1
+		f = tempfile.mktemp()
+		self.files_to_remove.append(f)
+		new_exps.append((key, self._CleanupExpansion(f)))
+	    else:
+		new_exps.append((key, val))
+	self.expansions = new_exps
+	self.initialized = 1
+	
+    def Cleanup(self):
+	for f in self.files_to_remove:
+	    try:
+		os.unlink(f)
+	    except: pass
+	self.files_to_remove = []
+
+    def _IAddDirectoryExpansion(self, entry, segment):
+	self.expansions.append((entry, self.ExpandFile(segment)))
+
+    def ExpandFile(self, file_string):
+	for key, val in self.expansions:
+	    # val is None if we haven't generated a
+	    # temporary file yet.
+	    if val is not None:
+		file_string = string.replace(file_string, key, val)
+	return file_string
+
+    def AddProcessSet(self, set):
+	self.process_sets.append(set)
+
+    def SuppressExpansions(self):
+	# If expansions are suppressed, then the command which
+	# is shown will be expanded only when we execute.
+	# Otherwise, it's expanded before it's ever saved
+	# as a command line. 
+	self.suppress_expansions = 1
+
+    def AddArguments(self, instructions):
+	# If there are args, and they don't match the
+	# available remaining arguments, then it's an error.
+	# No optional arguments permitted.
+	config_args = string.split(instructions)
+	# Well, we've already recorded the arguments in the
+	# directory expansion table. So for each argument,
+	# we should be able to find an entry. If we
+	# can't, do usage and raise an error.
+	num_args = len(config_args)
+	j = 1
+	while j <= num_args:
+	    num_str = "$%d" % j
+	    expansion = self.ExpandFile(num_str)
+	    if expansion == num_str:
+		# No expansion.
+		raise ConfigurationError, ("missing some config args: -- %s" % instructions)
+	    # Otherwise, record the expansion.
+	    self._IAddDirectoryExpansion(("$"+config_args[j - 1]), expansion)
+	    j = j + 1
+	# Now, see if there's an expansion for the next index.
+	num_str = "$%d" % j
+	expansion = self.ExpandFile(num_str)
+	if expansion != num_str:
+	    print "Ignoring extra config arguments"
+	
+    # Configuration processing.
+    
+    def _AssembleConfiguration(self):
+	expanded_configs_dict = {}
+	self._AddConfigTable(self.__class__, expanded_configs_dict)
+	self._AddConfigTable(self.container_set_class, expanded_configs_dict)
+	self._AddConfigTable(self.container_class, expanded_configs_dict)
+	return expanded_configs_dict
+    
+    def _AddConfigTable(self, config_class, expanded_configs):
+	for c in config_class.config_table:
+	    if c[0] is not None:
+		expanded_configs[c[0]] = c + (config_class,)
+	    if c[1] is not None:
+		expanded_configs[c[1]] = c + (config_class,)
+		
+    def _DigestConfiguration(self, command_line, expanded_config_dict,
+			     accept_args = 1, accept_config_file = 1):
+	short_args = ""
+	long_args = []
+	# Just look at the entries which are the command line.
+	for key, e in expanded_config_dict.items():
+	    # e[1] is the command line.
+	    if e[1] is None: continue
+	    if key != e[1]: continue
+	    if (len(key) == 2) and \
+	       (key[0] == "-") and \
+	       (key[1] != "-"):
+		# Short arg.
+		if e[2]: short_args = short_args + key[1] + ":"
+		else: short_args = short_args + key[1]
+	    elif (len(key) > 2) and \
+		 (key[0:2] == "--"):
+		# Long arg.
+		if e[2]: long_args.append(key[2:] + "=")
+		else: long_args.append(key[2:])
+	    else:
+		print "Unknown arg type %s; skipping." % key
+	try:
+	    optlist, args = getopt.getopt(command_line, short_args, long_args)
+	except getopt.error:
+	    raise ConfigurationError, "error parsing command line"
+	if optlist and args:
+	    raise ConfigurationError, "can't have both command line arguments and configuration file"
+	if optlist:
+	    if (not accept_args):
+		raise ConfigurationError, "can't use command line arguments"
+	    else:
+		return optlist, 1
+	elif args:
+	    if (not accept_config_file):
+		raise ConfigurationError, "can't use configuration file"
+	    # Now, we "parse" the file.
+	    if '--' in args:
+		i = args.index('--')
+		# Add these things as directory expansions.
+		j = 1
+		for elt in args[i+1:]:		    
+		    self._IAddDirectoryExpansion(("$%d" % j), "'"+self._CleanupExpansion(elt)+"'")
+		    j = j + 1
+		args = args[:i]
+	    if len(args) == 2:
+		self.config_choice = args[1]
+	    self.file = args[0]
+            return self._DigestConfigurationFile(self.file), 0        
+	else:
+	    raise ConfigurationError, "neither args nor configuration file"
+        
+    def _DigestConfigurationFile(self, test_file):
+        try:
+            fp = open(test_file, "r")
+        except:
+            raise ConfigurationError, "couldn't find configuration file "+ test_file
+        lines = fp.readlines()
+        fp.close()
+        directives = []
+        for line in lines:
+            l = string.strip(line)
+            if l and l[0] != '#':
+                # Empty lines don't count, # is comment character.
+                try:
+                    d = string.split(l, None, 1)
+	            # Handle INCLUDE:
+                    if d[0] == "INCLUDE:":
+                        if len(d) != 2:
+                            raise ConfigurationError, "INCLUDE: requires exactly one argument"
+                        else:
+                            directives = directives + self._DigestConfigurationFile(self.ExpandFile(d[1]))
+                    elif len(d) == 1:
+                        directives.append((d[0], ""))
+                    else:
+                        directives.append((d[0], d[1]))
+			print("appending `%s', `%s'" % (d[0], d[1]))
+                except ConfigurationError, msg:
+                    raise ConfigurationError, msg
+                except:
+                    raise ConfigurationError, ("ill-formed line `%s'" % l)
+        return directives
+
+    def CanStartBlock(self, config_entry, from_cmdline):
+	# From the command line, anything can start a
+	# set block.
+	if from_cmdline:
+	    return 1
+	else:
+	    return (config_entry[-1] == self.container_set_class) and \
+		   (config_entry[-2] == 1)
+
+    def MustStartBlock(self, config_entry, from_cmdline):
+	# From the command line, nothing MUST start a set block.
+	if from_cmdline:
+	    return 0
+	else:
+	    return (config_entry[-1] == self.container_set_class) and \
+		   (config_entry[-2] == 1)
+	
+    def _Configure(self, optlist, from_cmdline, config_dict):
+	# So now we work our way through the optlist.
+	cur_set = None
+	cur_container = None
+	for key, val in optlist:
+	    cur_set, cur_container = self._ConfigureOption(cur_set, cur_container, key, val, from_cmdline, config_dict)
+
+    def _ConfigureOption(self, cur_set, cur_container,
+			 key, val, from_cmdline, config_dict):
+	# We ignore directives we don't know about.
+	try:
+	    config_entry = config_dict[key]
+	except:
+	    return cur_set, cur_container
+	config_key, cmd_key, need_arg, \
+		    mname, start_block, kclass = config_entry
+	kmethod = None
+	if kclass is self.__class__:
+	    kmethod = eval("self." + mname)
+	    # Treat locally.
+	elif kclass is self.container_set_class:
+	    if cur_set is None:
+		if self.CanStartBlock(config_entry, from_cmdline):
+		    cur_set = self.container_set_class(self)
+	    elif self.MustStartBlock(config_entry, from_cmdline):
+		cur_set = self.container_set_class(self)
+	    if cur_set is None:
+		raise ConfigurationError, \
+		      ("no current set for %s %s" % (key, val))
+	    else:
+		kmethod = eval("cur_set." + mname)
+	elif kclass is self.container_class:
+	    if cur_set is None:
+		if self.CanStartBlock(config_entry, from_cmdline):
+		    cur_set = self.container_set_class(self)
+		else:
+		    raise ConfigurationError, \
+			  "no current set for container"
+	    if cur_container is None:
+		if cur_set.CanStartBlock(config_entry, from_cmdline):
+		    # This needs to be a method because of
+		    # backward compatibility problems with
+		    # -c vs. PROCESS:.
+		    cur_container = self.container_class(cur_set)
+	    elif cur_set.MustStartBlock(config_entry, from_cmdline):
+		cur_container = self.container_class(cur_set)
+	    if cur_container is None:
+		raise ConfigurationError, \
+		      ("no current container for %s %s" % (key, val))
+	    else:
+		kmethod = eval("cur_container." + mname)
+	    if cur_set.MustEndBlock(config_entry, from_cmdline):
+		cur_container = None
+	# The method object now embodies the instance,
+	# so I don't care about cur_set or cur_container.
+	if kmethod:
+	    if need_arg:
+		kmethod(val)
+	    else:
+		kmethod()
+	return cur_set, cur_container
+        
+    def ChooseConfig(self):
+        p_set = None
+        if self.num_conforming_process_sets == 0:
+            raise ConfigurationError, "No eligible process sets."
+	elif self.num_conforming_process_sets == 1:
+            for p in self.process_sets:
+                if p.meets_requirements:
+                    p_set = p
+                    break
+	elif (self.num_conforming_process_sets > 1) and \
+	     (self.config_choice is None):
+            pass
+	else:
+	    # We should be able to choose the config now based
+	    # on the config choice.
+	    try:
+		index = string.atoi(self.config_choice)
+		try:                    
+		    p_set = self.process_sets[index - 1]
+		except IndexError:
+		    raise ConfigurationError, "List index out of range."
+	    except ValueError:
+		# Try by name.
+		for p in self.process_sets:
+		    if p.title == self.config_choice:
+			p_set = p
+                        break
+                if p_set is None:
+                    raise ConfigurationError, "Couldn't find set named `%s'." % self.config_choice
+            if p_set and (p_set.meets_requirements == 0):
+                raise ConfigurationError, "Requirements failed: " + string.join(p_set.UnmetRequirements())
+        return p_set

Added: src/process_monitor.py
===================================================================
--- src/process_monitor.py	                        (rev 0)
+++ src/process_monitor.py	2006-12-11 01:15:58 UTC (rev 1)
@@ -0,0 +1,1187 @@
+# This file (c) Copyright 1998 - 2002 The MITRE Corporation
+# 
+# This file is part of the Galaxy Communicator system. It is licensed
+# under the conditions described in the file LICENSE in the root 
+# directory of the Galaxy Communicator system.
+
+"""Process monitor
+
+The process monitor controls an arbitrary number of processes. It 
+permits a nuber of arguments for each process, of which -c
+terminates each argument sequence. 
+
+--compressed: start in compressed configuration
+--ncols <num>: how many columns to display the panes in
+     (also the number of rows of buttons in compressed mode)
+-T <title>: title of process
+--open: start the monitor with this process's window open
+--start: start the monitor with this process started
+--keep_alive: restart this process if it dies
+--input_line: provide a line input pane
+--input_return: provide a button to send a newline
+-c <cmdline>: command line for this process
+
+The first element of the command line must be something that can 
+be preceded by 'exec'. Example:
+
+process_monitor -T "Add and multiply" --open --start \
+    -c "run_server bin/addmultiply" -T "Hub" --open \
+    -c "bin/hub -pgm_file addmultiply.pgm"
+"""
+
+import os, sys, socket, string, signal, select, math, time
+
+import Tkinter
+from Tkinter import *
+
+########################################
+#
+# GUI units
+#
+########################################
+
+BadLabelJustification = 'BadLabelJustification'
+
+HEADERFONT = '-*-lucida-bold-r-normal-sans-14-140-*'
+CONTENTSFONT = '-*-lucida-medium-r-normal-sans-12-120-*'
+
+class LabeledEntry(Frame):
+    def __init__(self, master, label_text,
+		 entry_value = '',
+		 entry_width = 20,
+		 return_cmd = None,
+		 label_width = None):
+	Frame.__init__(self, master)
+	if label_width:
+	    fontvar = StringVar()
+	    fontvar.set(label_text)
+	    self.label = Entry(self, relief = 'flat',
+			       borderwidth = 0,
+			       font = HEADERFONT, justify = 'left',
+			       textvariable = fontvar,
+			       width = label_width, state = 'disabled')
+	else:
+	    self.label = Label(self, text=label_text, font= HEADERFONT)
+	self.entry = Entry(self, borderwidth=2, relief='sunken',
+			   font=CONTENTSFONT, width=entry_width)
+	if type(entry_value) is StringType:
+	    self.textvar = StringVar()
+	else:
+	    self.textvar = IntVar()
+	self.textvar.set(entry_value)
+	self.entry['textvariable']=self.textvar
+	if return_cmd:
+	    self.entry.bind('<KeyPress-Return>',
+			    lambda event, cmd = return_cmd, s = self: \
+			    cmd(s.GetValue()))
+	else:
+	    self.entry['state'] = 'disabled'
+	self.label.pack(side = 'left')
+	self.entry.pack(side = 'left', fill = 'x', expand = 1)
+    def Enable(self):
+	self.entry['state'] = 'normal'
+    def GetValue(self):
+	return self.textvar.get()
+    def SetValue(self, val):
+	self.textvar.set(val)
+
+# Sam 11/5/99: We need to enforce line breaks, lest the
+# process monitor choke on extra-long lines. The Hub does exactly
+# this, by printing out its wait dots without line breaks.
+# After each print, I'll probe the current position, and if it's greater
+# than the width of the widget, I'll print a line break. 
+
+class ScrollPane(Frame):
+    def __init__(self, master, height = 12, width = 60,
+		 label = '', label_justification = 'left',
+		 action_button_label = None, action_button_action = None):
+	Frame.__init__(self, master)
+	if label:
+	    self.labelframe = Frame(self)
+	    self.labelframe.pack(side = 'top', expand = 0, fill = 'x')	    
+	    self.label = Label(self.labelframe, text=label, font= HEADERFONT)
+	    if label_justification == 'left':
+		# self.label.pack(side = 'top', anchor = 'w')
+		self.label.pack(side = 'left')
+		if action_button_label or action_button_action:		
+		    self.button = Button(self.labelframe,
+					 text = action_button_label,
+					 font = HEADERFONT,
+					 command = action_button_action)
+		    self.button.pack(side = 'right')
+	    elif label_justification == 'right':
+		# self.label.pack(side = 'top', anchor = 'e')
+		self.label.pack(side = 'right')
+		if action_button_label or action_button_action:		
+		    self.button = Button(self.labelframe,
+					 text = action_button_label,
+					 font = HEADERFONT,
+					 command = action_button_action)
+		    self.button.pack(side = 'left')
+	    elif label_justification == 'center':
+		self.label.pack(side = 'top')
+	    else:
+		raise BadLabelJustification, label_justification
+	    self.textframe = Frame(self)
+	    self.textframe.pack(side = 'top', expand = 1, fill = 'both')
+	    if (action_button_label or action_button_action) and \
+	       label_justification == 'center':
+		self.button = Button(self,
+				     text = action_button_label,
+				     font = HEADERFONT,
+				     command = action_button_action)
+		self.button.pack(side = 'top')
+	else:
+	    self.textframe = self
+	self.textbox = Text(self.textframe, borderwidth=1, relief='sunken',
+			    state='disabled', height=height, width=width,
+			    font= CONTENTSFONT
+			    )
+	self.scrollbar = Scrollbar(self.textframe, borderwidth=1,
+				   relief='sunken',
+				   command=self.textbox.yview)
+	self.scrollbar.pack(side = "right", fill = "y")
+	self.textbox['yscrollcommand'] = self.scrollbar.set
+	self.textbox.pack(side='left', expand=1, fill='both')	
+    def write(self, text):
+	self.Write(text)
+    def Write(self, text):
+        # For some bizarre reason, when I send Windows output (\r\n)
+        # to the widget, I get \r showing up as a string character
+        # (i.e., Tkinter doesn't seem to be dealing with this
+        # very well). So we need to replace it.
+        # Actually, it appears that this is a general problem,
+        # when we read from something that isn't a file descriptor;
+        # it appears that \r\n is converted to \n when we read
+        # in a file in Python using open(). So I think I need
+        # to move this to the Windows specific stuff.
+	self.textbox['state']='normal'
+	self.textbox.insert('end', text)
+	self.textbox.yview_pickplace('end')
+	# Get the current end.
+	line, pos = string.splitfields(self.textbox.index('end - 1 chars'), ".")
+	# We'll stick with the requested width, because
+	# figuring out the actual width is too damn hard.
+	if string.atoi(pos) >= string.atoi(self.textbox['width']):
+	    self.textbox.insert('end', '\n')
+	    self.textbox.yview_pickplace('end')                
+	self.textbox['state']='disabled'
+        return string.atoi(line)
+    def Clear(self):
+	self.textbox['state']='normal'
+	self.textbox.delete('0.0', 'end')
+	self.textbox['state']='disabled'
+
+UnknownMenuType = 'UnknownMenuType'
+
+class MyMenu(Menu):
+    def __init__(self, master, vals_pairs):
+	Menu.__init__(self, master, font= CONTENTSFONT, tearoff=0)
+	for key, val in vals_pairs:
+	    type, trueval = val
+	    if type == 'command':
+		self.add('command', label = key,
+			 command = lambda com = trueval: com())
+	    elif type == 'cascade':
+		self.add('cascade', label = key,
+			 menu = MyMenu(self, trueval))
+	    else:
+		raise UnknownMenuType
+
+class MenuBarEntry(Menubutton):    
+    def __init__(self, master, text, vals_pairs):
+	Menubutton.__init__(self, master, font = CONTENTSFONT,
+			    text = text)
+	self.menu = MyMenu(self, vals_pairs)
+	self['menu'] = self.menu
+
+class Error(Frame):
+    def __init__(self, master, error_string):
+	self.master = master
+	
+	Frame.__init__(self, master)
+	self['borderwidth'] = 2
+	self['relief'] = 'ridge'
+	self.pack(side='top')
+	self.master.title('You should know...')
+	
+	self.msg = Message(self, text=error_string, width=200,
+			   font= CONTENTSFONT,
+			   justify='center')
+	self.msg.pack(side = 'top')
+	self.dismiss_row = Frame(self)	
+	self.dismiss_row.pack(side='top')
+	self.dismiss_row['borderwidth'] = 2
+	self.CLOSE = Button(self.dismiss_row, text='OK',
+			    font= HEADERFONT,
+			    command=self.close_cmd)
+	self.CLOSE.pack(side='left')
+	self.pack(fill = 'both')
+	
+    def close_cmd(self):
+	self.master.destroy()
+
+###################################
+#
+# Toplevel monitor window
+#
+###################################
+
+import popen2
+
+NoCmdlineError = "NoCmdlineError"
+
+HISTORYFONT = '-*-courier-medium-r-normal-sans-12-120-*'
+
+class StatusButton(Label):
+    def __init__(self, *args, **kw):
+	apply(Label.__init__, (self,) + args, kw)
+	self.visible = 1
+	self.configure_text = "(stopped)"
+	Label.configure(self, text = self.configure_text)
+    def configure(self, text = None, command = None):
+	if text == "Start":
+	    self.configure_text = "(stopped)"
+	elif text == "Stop":
+	    self.configure_text = "(running)"
+	if self.visible:
+	    self.MakeVisible()
+    def MakeVisible(self):
+	Label.configure(self, text = self.configure_text)
+	self.visible = 1
+    def MakeInvisible(self):
+	Label.configure(self, text = (9 * " "))
+	self.visible = 0
+
+# This is the window which displays the process. It contains
+# a frame which has all the controls in it. In addition,
+# it has a header for the command and a show/hide button.
+
+class ReferenceEntry(Frame):
+    def __init__(self, master, entry_host, process):
+	Frame.__init__(self, master)
+	self.entry_host = entry_host
+	header_host = Frame(self)
+	header_host.pack(side = 'top', anchor = 'w')
+	self.header = Frame(header_host)
+	self.empty_header = Frame(header_host)
+	self.show_button = Button(self.header, text = "Show",
+				  relief = "groove",
+				  command = lambda s = self: s.Enlarge())
+	self.show_button.pack(side = 'left')
+	cmd = process.title
+	# This is now 71, because the other 9 will come from
+	# the run button.
+	label = Label(self.header, text = cmd  + ((71 - len(cmd)) * " "),
+		      font = HISTORYFONT,
+		      justify = 'left', 
+		      relief = "flat")
+	label.pack(side = 'left')
+	self.header.pack(side = 'top', anchor = 'w')
+	self.pframe = SingleProcessFrame(self, process = process)
+	self.run_button = StatusButton(self.header, relief = "flat")
+	self.pframe.run_buttons.append(self.run_button)
+	self.run_button.pack(side = "right")
+
+    def Enlarge(self):
+	# Reset the geometry.
+	self.entry_host.master.geometry("")
+	self.pframe.pack(side = 'top', anchor = 'w',
+			 fill = 'both', expand = 1)
+	# Remove the text for the run buttons.
+	self.run_button.MakeInvisible()
+	self.show_button.configure(text = "Hide",
+				   command = self.Reduce)
+
+    def Reduce(self):
+	# Reset the geometry.
+	self.entry_host.master.geometry("")
+	self.pframe.forget()
+	# Show the text for the run button.
+	self.run_button.MakeVisible()
+	self.show_button.configure(text = "Show",
+				   command = self.Enlarge)
+
+    def UseCompressedConfig(self):
+	# Convert to the compressed configuration. Enlarge and
+	# get rid of the header line.
+	self.header.forget()
+	self.empty_header.pack(side = 'top', anchor = 'w')
+	# Make sure that it's enlarged.
+	self.Enlarge()
+
+    def UseColumnConfig(self):
+	self.empty_header.forget()
+	self.header.pack(side = 'top', anchor = 'w')
+
+class ProcessFrame(Frame):
+    def __init__(self, process_env, process_set, master = None):
+	Frame.__init__(self, master)
+	self['borderwidth'] = 2
+        # Default title.
+        self.master.title("Process monitor")
+	self.pack(fill = 'both', expand = 1)
+	self.top = Frame(self)
+	self.top.pack(side = 'top', fill = 'x', anchor = 'w')
+	self.topmenubar = Frame(self.top, relief = 'groove', borderwidth = 2)
+	self.topmenubar.pack(side = 'top', fill = 'x', anchor = 'w')
+	b1 = MenuBarEntry(self.topmenubar, "File",
+			  [("Quit", ('command', self.quit))])
+	b1.pack(side = 'left')
+	b2 = MenuBarEntry(self.topmenubar, "Process Control",
+			  [("Stop all", ('command', self.Shutdown)),
+			   ("Clear all", ('command', self.Clear)),
+			   ("Restart all", ('command', self.Restart))])
+	b2.pack(side = "left")
+	b3 = MenuBarEntry(self.topmenubar, "Configuration",
+			  [("Column configuration",
+			    ('cascade',
+			     [("One column",
+			       ('command',
+				lambda s = self: s.UseColumnConfig(1))),
+			     ("Two columns",
+			      ('command',
+			       lambda s = self: s.UseColumnConfig(2))),
+			     ("Three columns",
+			      ('command',
+			       lambda s = self: s.UseColumnConfig(3)))])),
+			   ("Compressed configuration",
+			    ('cascade',
+			     [("One button row",
+			       ('command',
+				lambda s = self: s.UseCompressedConfig(1))),
+			      ("Two button rows",
+			       ('command',
+				lambda s = self: s.UseCompressedConfig(2))),
+			      ("Three button rows",
+			       ('command',
+				lambda s = self: s.UseCompressedConfig(3)))]))])
+	b3.pack(side = "left")
+        all_sets = []
+        for p in process_env.process_sets:
+            if p.meets_requirements:
+                f = lambda s = self, p_set = p: s.UseProcessSet(p_set)
+                all_sets.append((p.title, ('command', f)))
+        if len(all_sets) > 1:
+            b4 = MenuBarEntry(self.topmenubar, "Process Set", all_sets)
+            b4.pack(side = "left")
+	bottom = Frame(self)
+	bottom.pack(side = 'left', anchor = 'n',
+		    fill = 'both', expand = 1)
+	self.pane_frame = Frame(bottom)
+	self.pane_frame.pack(side = 'top', anchor = 'w',
+			     fill = 'both', expand = 1)
+ 	# Don't pack the clone button, either, until the
+ 	# compressed config is used.
+ 	self.clone_button = Button(bottom,
+ 				   text = "Detach this pane",
+ 				   relief = 'groove',
+ 				   font = HEADERFONT,
+ 				   command = self.Detach)
+	# Now, we build the buttons, but we don't pack them.
+	self.button_row = Frame(self.top, relief = 'groove', borderwidth = 2)
+	self.processes = []
+	self.num_nondetached_processes = 0
+	self.active_process = None
+	self.separator_list = []
+	self.process_env = process_env
+	# For temporary files.
+	self.process_env.Initialize()
+        self.process_set = None
+        self.UseProcessSet(process_set)
+
+    def UseProcessSet(self, new_set):
+
+        # First, shut down and delete all windows, if
+        # appropriate.
+        if self.process_set:
+            # Shutdown the process set.
+            self.Shutdown()
+            # Unuse all the currently used elements.
+            if self.compressed:
+                self.UnuseCompressedConfig()
+            else:
+                self.UnuseColumnConfig()
+            # Destroy all the currently used elements.
+            for s in self.separator_list:
+                s.destroy()
+            self.separator_list = []
+            for p in self.process_set.processes:
+                if p.reference_entry:
+                    p.reference_entry.destroy()
+                    p.reference_entry = None
+                p.compressed_button.destroy()
+                p.compressed_button = None
+
+        # Now, set up the new configuration.
+
+        if new_set:
+            self.process_set = new_set
+            self.master.title(self.process_set.title)
+            # Now, we build the individual panes and buttons.
+            for p in self.process_set.processes:
+                # Build a frame which has the lower pane as a hidden
+                # component.
+                # Set up the reference line.
+                p.AddTk(self.tk)
+                self.AttachProcess(p)
+                if p.open:
+                    p.reference_entry.Enlarge()
+                if p.start:
+                    p.window_host.Restart()
+                if p.keepalive:
+                    p.window_host.KeepAlive()
+                # Now, build the button.
+                b = Button(self.button_row,
+                           text = p.title,
+                           relief = 'raised',
+                           font = HEADERFONT)
+                p.compressed_button = b
+                b.configure(command = lambda p = p, s = self:
+                            s.MakeButtonActive(p))
+            for i in range(len(self.process_set.processes) - 1):
+                # Build as many separators as we need.
+                self.separator_list.append(Frame(self.pane_frame,
+                                                 relief = 'sunken',
+                                                 height = 2,
+                                                 borderwidth = 1))
+            self.detached_entries = []
+            self.numcols = 0
+            self.compressed = 0
+            if self.process_env.compressed:
+                self.UseCompressedConfig(self.process_env.ncols)
+            else:
+                self.UseColumnConfig(self.process_env.ncols)
+
+    def AttachProcess(self, p):
+	ref = ReferenceEntry(self.pane_frame, self, p)
+	p.SetReferenceEntry(ref)
+	self.num_nondetached_processes = self.num_nondetached_processes + 1
+    
+    def UnuseColumnConfig(self):
+	for s in self.separator_list:
+	    s.grid_forget()
+	for p in self.process_set.processes:
+	    if p.window_host and p.Nondetached():
+		p.reference_entry.grid_forget()
+	
+    def UseCompressedConfig(self, ncols, force = 0):
+	if self.compressed and (ncols == self.numcols) and (not force):
+	    return
+	if (not self.compressed):
+	    if not force: self.UnuseColumnConfig()
+	    self.compressed = 1
+	# We want to use the gridder instead of the
+	# packer for the buttons.
+	cols, rows = self.button_row.grid_size()
+	# First, get rid of all the weights.
+	for i in range(cols):
+	    self.button_row.grid_columnconfigure(i, weight = '0')
+	for i in range(rows):
+	    self.button_row.grid_rowconfigure(i, weight = '0')
+	# Now, compute the buttons per button row.
+	buttons_per_button_row = int(math.ceil(float(self.num_nondetached_processes)/float(ncols)))
+	i = j = 0
+	for p in self.process_set.processes:
+	    if p.Nondetached():
+		p.reference_entry.UseCompressedConfig()
+		# Pack every compressed button.		
+		p.compressed_button.grid(row = j, column = i,
+					 sticky = 'news')
+		i = i + 1
+		if (i % buttons_per_button_row) == 0:
+		    j = j + 1
+		    i = 0
+	cols, rows = self.button_row.grid_size()
+	# Now, restore the weights.
+	for i in range(cols):
+	    self.button_row.grid_columnconfigure(i, weight = '1')
+	for i in range(rows):
+	    self.button_row.grid_rowconfigure(i, weight = '1')
+	self.numcols = rows
+	
+	cols, rows = self.pane_frame.grid_size()
+	for i in range(cols):
+	    self.pane_frame.grid_columnconfigure(i, weight = '0')
+	for i in range(rows):
+	    self.pane_frame.grid_rowconfigure(i, weight = '0')
+	self.MakeButtonActive(self.process_set.processes[0])
+	self.button_row.pack(side = 'top', anchor = 'w', fill = 'x')
+	# We only pack the clone button if there is more than
+	# one pane.
+	if self.num_nondetached_processes > 1:
+	    self.clone_button.pack(side = 'bottom', anchor = 'w', fill = 'x')
+	    
+    # These two methods are specific to the compressed configuration.
+    def MakeButtonEntryInactive(self, p):
+	if p.Nondetached():
+	    if p.reference_entry:
+		p.reference_entry.grid_forget()
+	    p.compressed_button.configure(relief = 'raised')
+    
+    def MakeButtonActive(self, process):
+	for entry in self.process_set.processes:
+	    self.MakeButtonEntryInactive(entry)
+	if process:
+	    self.active_process = process
+	    process.compressed_button.configure(relief = 'sunken')
+	    process.reference_entry.grid(row = '0',
+					 column = '0', sticky = 'news')
+	    self.pane_frame.grid_columnconfigure(0, weight = '1')
+	    self.pane_frame.grid_rowconfigure(0, weight = '1')
+    
+    def Reattach(self, p, old_toplevel):
+	# Reattaching is tricky, because you have to regenerate
+	# using the current configuration. First, you build
+	# a new reference entry which has the right pane as
+	# its parent, Then, you have to
+	# unforget the button and regenerate the grid. Because
+	# the processes remain in order, they'll get regenerated
+	# in the same order.
+	self.AttachProcess(p)
+	p.SetDetached(0)
+	old_toplevel.destroy()
+	p.reference_entry.Enlarge()
+	# Now, we need to regenerate things.
+	if self.compressed:
+	    # Regenerate the compressed list.
+	    self.UnuseCompressedConfig()
+	    self.UseCompressedConfig(self.numcols, force = 1)
+	else:
+	    # Regenerate the uncompressed structure.
+	    self.UnuseColumnConfig()
+	    self.UseColumnConfig(self.numcols, force = 1)
+	    
+    def Detach(self):
+	# First, make another button active.
+	if self.active_process:
+	    # Get rid of the button.
+	    p = self.active_process
+	    p.compressed_button.grid_forget()
+	    self.update()
+	    # Deactivate the button entry.
+	    self.MakeButtonEntryInactive(p)
+	    # Make it detached.
+	    p.SetDetached(1)
+	    self.num_nondetached_processes = self.num_nondetached_processes - 1
+	    # Activate the first nondetached element.
+	    for next_p in self.process_set.processes:
+		if next_p.Nondetached():
+		    self.MakeButtonActive(next_p)
+		    break
+	    # Now,the complicated part. We want to detach the
+	    # process from the old ref_entry and give it
+	    # to a new reference entry in a new toplevel.
+	    # There will be no reattach option at this point.
+	    # The window reattachment doesn't work quite
+	    # right yet.
+	    new_top = Toplevel()
+	    new_top.protocol('WM_DELETE_WINDOW', lambda s = self, p = p, t = new_top: s.Reattach(p, t))
+	    f = Frame(new_top)
+	    f.pack(fill = 'both', expand = '1')
+	    new_ref_entry = ReferenceEntry(f, new_top, p)
+	    p.SetReferenceEntry(new_ref_entry)
+	    new_ref_entry.pack(side = 'top', fill = 'both', expand = '1')
+	    reattach_button = Button(f,
+				     text = "Reattach this pane",
+				     relief = 'groove',
+				     font = HEADERFONT,
+				     command = lambda s = self, p = p, t = new_top: s.Reattach(p, t))
+	    reattach_button.pack(side = 'top', fill = 'x')
+	    new_top.title(p.title)
+	    new_ref_entry.UseCompressedConfig()
+	    if self.num_nondetached_processes == 1:
+		# If we only have one pane left, don't
+		# allow it to be detached.
+		self.clone_button.forget()
+    
+    def UnuseCompressedConfig(self):
+	self.button_row.forget()
+	self.clone_button.forget()
+	self.active_process = None
+	for p in self.process_set.processes:
+	    p.compressed_button.grid_forget()
+	    if p.reference_entry and p.Nondetached():
+		p.reference_entry.grid_forget()	
+	for s in self.separator_list:
+	    s.grid_forget()
+    
+    def UseColumnConfig(self, ncols, force = 0):
+	if self.compressed == 0 and ncols == self.numcols and (not force):
+	    return
+	# Make it uncompressed	
+	if self.compressed:
+	    if not force: self.UnuseCompressedConfig()
+	    self.compressed = 0
+	# First, get rid of all the weights.
+	cols, rows = self.pane_frame.grid_size()
+	for i in range(cols):
+	    self.pane_frame.grid_columnconfigure(i, weight = '0')
+	for i in range(rows):
+	    self.pane_frame.grid_rowconfigure(i, weight = '0')
+	panes_per_column = int(math.ceil(float(self.num_nondetached_processes)/float(ncols)))
+	i = j = 0
+	for p in self.process_set.processes:
+	    if not p.Nondetached():
+		continue
+	    p.reference_entry.UseColumnConfig()
+	    # Add a separator above every nonzeroth element.
+	    if i > 0:
+		self.separator_list[i - 1].grid(row = ((i * 2) - 1), column = j, sticky = 'ew')
+	    p.reference_entry.grid(row = (i * 2), column = `j`,
+				   sticky = 'news')
+	    i = i + 1
+	    if (i % panes_per_column) == 0:
+		j = j + 1
+		i = 0	
+	cols, rows = self.pane_frame.grid_size()
+	for i in range(cols):
+	    self.pane_frame.grid_columnconfigure(i, weight = '1')
+	for i in range(rows):
+	    self.pane_frame.grid_rowconfigure(i, weight = '1')
+	self.numcols = ncols
+
+    def Shutdown(self):
+	# Don't really kill. Collect the pids and then do the really kill
+	# afterward.
+        if self.process_set:
+            all_handles = []        
+            for p in self.process_set.processes:
+                handle = p.Shutdown(really_kill = 0)
+                if handle is not None: all_handles.append(handle)
+            if all_handles:
+                self.process_set.EnforceShutdown(all_handles)
+
+    def Restart(self):
+	for p in self.process_set.processes:
+	    p.Restart()
+
+    def Clear(self):
+	for p in self.process_set.processes:
+	    p.Clear()
+    	
+    def quit(self):
+	# self.timer.deletetimerhandler()
+	self.Shutdown()
+	# For temporary files.
+	self.process_env.Cleanup()
+	self.master.destroy()
+	self.master.quit()
+
+    def force_quit(self):
+	self.quit()
+
+    def Poll(self):
+	for p in self.process_set.processes:
+	    p.Poll()
+	self.timer = self.tk.createtimerhandler(500, self.Poll)
+
+    def Run(self):
+	# self.timer = self.tk.createtimerhandler(500, self.Poll)
+	self.mainloop()
+
+# We adopt a more complex model, where all the info about
+# the process lives in a ProcessContainer, which can be
+# made to point to different windows (looking forward to the
+# point where we reuse a single pane for multiple processes).
+
+# This callback structure has to tolerate the possibility
+# that we'll have either timer callbacks or file
+# callbacks.
+
+def _CreateHandleCallback(handle, process_container):
+    if process_container.FDCallbackAvailable():
+        return FDHandleCallback(handle, process_container)
+    else:
+        return TimerHandleCallback(handle, process_container)
+
+class HandleCallback:
+    def __init__(self, handle, process_container):
+        self.handle = handle
+        self.process = process_container
+
+# I don't think the writes are every used.
+
+class FDHandleCallback(HandleCallback):
+    def __init__(self, handle, process_container):
+        HandleCallback.__init__(self, handle, process_container)
+        if handle == process_container.out_handle:
+            self.fh_mask = Tkinter.WRITABLE
+            self.cb = lambda s, m, h = handle, o = self: o.process.WriteCallback(h)
+        else:
+	    self.fh_mask = Tkinter.READABLE
+            # Read as much as you can.
+            self.cb = lambda s, m, h = handle, o = self: o.process.ReadCallback(h, -1)
+        self.process.tk.createfilehandler(handle, self.fh_mask, self.cb)
+        self.has_filehandler = 1
+    def Remove(self):
+        if self.has_filehandler:
+            self.process.tk.deletefilehandler(self.handle.fileno())
+            self.has_filehandler = 0
+
+class TimerHandleCallback(HandleCallback):
+    def __init__(self, handle, process_container):
+        HandleCallback.__init__(self, handle, process_container)
+        if handle == process_container.out_handle:
+            self.cb = self.WriteTimer
+        else:
+	    self.cb = self.ReadTimer
+        self.timer = self.process.tk.createtimerhandler(10, self.cb)
+    def WriteTimer(self):
+        self.process.WriteCallback(self.handle)
+        self.timer = self.process.tk.createtimerhandler(10, self.WriteTimer)
+    def ReadTimer(self):
+        self.process.ReadCallback(self.handle, -1)
+        if self.timer is not None:
+            # It may have been removed as a result of shutting down.
+            self.timer = self.process.tk.createtimerhandler(10, self.cb)
+    def Remove(self):
+        if self.timer:
+            self.timer.deletetimerhandler()
+            self.timer = None
+
+# The process container is the object which houses the
+# process and all the information associated with it. The
+# window host for the process is where the output goes.
+# When we set a new window host, we destroy the old one;
+# window hosts are never reused. There will be no need
+# for a dummy process, because windows without processes
+# won't exist. It may be possible to have processes without
+# windows, but we still need to track the process. Crucial
+# information about the process includes the title, the
+# command line, and the input_mode.
+
+from basic_process_monitor import ProcessContainer, \
+     ProcessContainerSet, ProcessEnvironment, ConfigurationError
+
+class TkProcessContainer(ProcessContainer):
+    config_table = ProcessContainer.config_table + \
+		   [(None, "--input_line", 0, "LineInputMode", 0),
+		    (None, "--input_return", 0, "ReturnInputMode", 0),
+		    (None, "--start", 0, "SetAutoStart", 0),
+		    (None, "--open", 0, "SetVisible", 0),
+		    ("PROCESS_MONITOR_ARGS:", None, 1, \
+		     "AddProcessMonitorArgs", 0)]
+
+    def __init__(self, set):
+	ProcessContainer.__init__(self, set)
+	self.input_mode = None
+	self.open = 0
+	self.start = 0
+	self.window_host = None
+	self.reference_entry = None
+	self.detached = 0
+
+    # Configuration methods.
+	
+    def LineInputMode(self):
+	if self.input_mode is not None:
+	    raise ConfigurationError, "input mode already specified"
+	self.input_mode = "input_line"
+
+    def ReturnInputMode(self):
+	if self.input_mode is not None:
+	    raise ConfigurationError, "input mode already specified"
+	self.input_mode = "input_return"
+
+    def SetAutoStart(self):
+	self.start = 1
+
+    def SetVisible(self):
+	self.open = 1
+
+    def AddCommandLine(self, cmdline):
+	ProcessContainer.AddCommandLine(self, cmdline)
+	self.WriteCommandLine()
+
+    def ConfigDone(self):
+	ProcessContainer.ConfigDone(self)
+	if not self.title:
+	    if len(self.cmdline) > 60:
+		self.title = self.cmdline[:57] + "..."
+	    else:
+		self.title = self.cmdline
+
+    def AddProcessMonitorArgs(self, args):
+	# Just splitting the args isn't good enough, because
+	# the args are expecting to be in a command line. That
+	# means that there may be quoted material in the lists.
+	# Grrrr.
+        cmdline_args = self.TokenizeCmdlineArgs(args)
+	env = self.process_set.process_environment
+	# Just use the local configuration.
+	expanded_dict = {}
+	env._AddConfigTable(self.__class__, expanded_dict)
+	optlist, ignore = env._DigestConfiguration(cmdline_args,
+						   expanded_dict,
+						   1, 0)
+	for key, value in optlist:
+	    cur_set, cur_container = env._ConfigureOption(self.process_set,
+							  self, key, value,
+							  1, expanded_dict)
+	    # If we ever reach the end, barf.
+	    if cur_container is None:
+		raise ConfigurationError, "process monitor args can't close block"
+	
+    # Runtime methods.
+    
+    def _SetupHandles(self):
+        self.callback_dict = {}
+        ProcessContainer._SetupHandles(self)
+
+    def AddTk(self, tk):
+        self.tk = tk
+
+    def SetDetached(self, detached):
+	self.detached = detached
+    
+    def Nondetached(self):
+	return self.detached == 0
+
+    def Clear(self):
+	if self.window_host is not None:
+	    self.window_host.Clear()
+	
+    def UpdateIdle(self):
+	if self.window_host is not None:
+	    # Don't update(), just update_idletasks().
+	    # If you update(), constant reads will
+	    # lock the display.
+	    self.window_host.update_idletasks()
+
+    def SetReferenceEntry(self, ref):
+	old_ref = self.reference_entry
+	self.SetWindowHost(ref.pframe)
+	if old_ref is not None:
+	    old_ref.destroy()
+	self.reference_entry = ref
+    
+    def SetWindowHost(self, host):
+	old_host = self.window_host
+	if old_host is not None:
+	    self.UnmonitorOutput()
+	    # And destroy it.
+	    old_host.destroy()
+	self.window_host = host
+	if (old_host is not self.window_host) and \
+	   (host is not None):
+	    # Update the new host.
+	    self.window_host.SetProcess(self)	    
+	    # Also, monitor the output, I think.
+	    self.MonitorOutput()
+
+    def Pause(self):
+	self.UnmonitorOutput()
+	if self.window_host:
+	    self.window_host.LocalPause()
+
+    def Resume(self):
+	self.MonitorOutput()
+	if self.window_host:
+	    self.window_host.LocalResume()
+
+    def KeepAlive(self):
+	ProcessContainer.KeepAlive(self)
+	if self.window_host:
+	    self.window_host.LocalKeepAlive()
+
+    def LetDie(self):
+	ProcessContainer.LetDie(self)
+	if self.window_host:
+	    self.window_host.LocalLetDie()
+    
+    def UnmonitorOutput(self):
+        for cb in self.callback_dict.values():
+            cb.Remove()
+        self.callback_dict = {}
+
+    def MonitorOutput(self):
+        if self.in_handle:
+            cb = _CreateHandleCallback(self.in_handle, self)
+            self.callback_dict[self.in_handle] = cb
+        if self.err_handle:
+            cb = _CreateHandleCallback(self.err_handle, self)
+            self.callback_dict[self.err_handle] = cb
+
+    def WriteHistory(self, str, fd):
+	ProcessContainer.WriteHistory(self, str, fd)
+	if self.window_host is not None:
+	    cur_line = self.window_host.history.Write(str)
+	    # If the clear history instruction is a number,
+	    # treat it as the number of lines to save.
+            if (self.process_set.process_environment.clear_history is not None) and \
+               cur_line > self.process_set.process_environment.clear_history:
+                self.ClearHistory()
+
+    def WriteCommandLine(self):
+	self.WriteHistory(("[%s]\n" % self.WrapCmdlineForExecution(self.GetCmdlineForDisplay(), self.interleave_error)), None)
+
+    def ClearHistory(self):
+	ProcessContainer.ClearHistory(self)
+	if self.window_host is not None:
+	    self.window_host.LocalClearHistory()
+    
+    def Shutdown(self, really_kill = 1):
+	p = ProcessContainer.Shutdown(self, really_kill)
+	if self.window_host:
+	    self.window_host.LocalShutdown()
+	return p
+    
+    # In order to interleave stdout and stderr better, I've
+    # redirected stderr to stdout in the subshell. The problem is
+    # that under this configuration, the shell does not return
+    # a valid fd for the error stream. So I need not to monitor it.
+
+    def Restart(self):
+	ProcessContainer.Restart(self)
+	if self.window_host:
+	    self.window_host.LocalRestart()
+
+    def GetCmdlineForDisplay(self):
+	if self.window_host:
+	    self.window_host.UpdateCmdline()
+	return ProcessContainer.GetCmdlineForDisplay(self)
+
+    def GetCmdlineForExecution(self):
+	if self.window_host:
+	    self.window_host.UpdateCmdline()
+	return ProcessContainer.GetCmdlineForExecution(self)
+
+    def SetCmdline(self, str):
+	ProcessContainer.SetCmdline(self, str)

@@ Diff output truncated at 60000 characters. @@


More information about the Ravenclaw-developers mailing list