[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