# this module requires Python 2.6+
|
|
from __future__ import with_statement
|
|
from contextlib import contextmanager
|
|
from operator import attrgetter
|
|
from gettext import gettext as _
|
|
import imp
|
|
import inspect
|
|
import os
|
|
import sys
|
|
import cmd
|
|
import shlex
|
|
import subprocess
|
|
import argparse
|
|
import itertools
|
|
import traceback
|
|
import multiprocessing
|
|
import signal
|
|
import threading
|
|
import plac_core
|
|
|
|
if sys.version < '3':
|
|
def exec_(_code_, _globs_=None, _locs_=None):
|
|
if _globs_ is None:
|
|
frame = sys._getframe(1)
|
|
_globs_ = frame.f_globals
|
|
if _locs_ is None:
|
|
_locs_ = frame.f_locals
|
|
del frame
|
|
elif _locs_ is None:
|
|
_locs_ = _globs_
|
|
exec("""exec _code_ in _globs_, _locs_""")
|
|
|
|
exec('''
|
|
def raise_(tp, value=None, tb=None):
|
|
raise tp, value, tb
|
|
''')
|
|
else:
|
|
exec_ = eval('exec')
|
|
|
|
def raise_(tp, value=None, tb=None):
|
|
"""
|
|
A function that matches the Python 2.x ``raise`` statement. This
|
|
allows re-raising exceptions with the cls value and traceback on
|
|
Python 2 and 3.
|
|
"""
|
|
if value is not None and isinstance(tp, Exception):
|
|
raise TypeError("instance exception may not have a separate value")
|
|
if value is not None:
|
|
exc = tp(value)
|
|
else:
|
|
exc = tp
|
|
if exc.__traceback__ is not tb:
|
|
raise exc.with_traceback(tb)
|
|
raise exc
|
|
|
|
|
|
def decode(val):
|
|
"""
|
|
Decode an object assuming the encoding is UTF-8.
|
|
"""
|
|
try:
|
|
# assume it is an encoded bytes object
|
|
return val.decode('utf-8')
|
|
except AttributeError:
|
|
# it was an already decoded unicode object
|
|
return val
|
|
|
|
# ############################ generic utils ############################### #
|
|
|
|
|
|
@contextmanager
|
|
def stdout(fileobj):
|
|
"usage: with stdout(file('out.txt', 'a')): do_something()"
|
|
orig_stdout = sys.stdout
|
|
sys.stdout = fileobj
|
|
try:
|
|
yield
|
|
finally:
|
|
sys.stdout = orig_stdout
|
|
|
|
|
|
def write(x):
|
|
"Write str(x) on stdout and flush, no newline added"
|
|
sys.stdout.write(str(x))
|
|
sys.stdout.flush()
|
|
|
|
|
|
def gen_val(value):
|
|
"Return a generator object with a single element"
|
|
yield value
|
|
|
|
|
|
def gen_exc(etype, exc, tb):
|
|
"Return a generator object raising an exception"
|
|
raise_(etype, exc, tb)
|
|
yield
|
|
|
|
|
|
def less(text):
|
|
"Send a text to less via a pipe"
|
|
# -c clear the screen before starting less
|
|
po = subprocess.Popen(['less', '-c'], stdin=subprocess.PIPE)
|
|
try:
|
|
po.stdin.write(text)
|
|
except IOError:
|
|
pass
|
|
po.stdin.close()
|
|
po.wait()
|
|
|
|
use_less = (sys.platform != 'win32') # unices
|
|
|
|
|
|
class TerminatedProcess(Exception):
|
|
pass
|
|
|
|
|
|
def terminatedProcess(signum, frame):
|
|
raise TerminatedProcess
|
|
|
|
|
|
# ########################## readline support ############################ #
|
|
|
|
def read_line(stdin, prompt=''):
|
|
"Read a line from stdin, using readline when possible"
|
|
if isinstance(stdin, ReadlineInput):
|
|
return stdin.readline(prompt)
|
|
else:
|
|
write(prompt)
|
|
return stdin.readline()
|
|
|
|
|
|
def read_long_line(stdin, terminator):
|
|
"""
|
|
Read multiple lines from stdin until the terminator character is found,
|
|
then yield a single space-separated long line.
|
|
"""
|
|
while True:
|
|
lines = []
|
|
while True:
|
|
line = stdin.readline() # ends with \n
|
|
if not line: # EOF
|
|
return
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
elif line[-1] == terminator:
|
|
lines.append(line[:-1])
|
|
break
|
|
else:
|
|
lines.append(line)
|
|
yield ' '.join(lines)
|
|
|
|
|
|
class ReadlineInput(object):
|
|
"""
|
|
An iterable with a .readline method reading from stdin.
|
|
"""
|
|
def __init__(self, completions, case_sensitive=True, histfile=None):
|
|
self.completions = completions
|
|
self.case_sensitive = case_sensitive
|
|
self.histfile = histfile
|
|
if not case_sensitive:
|
|
self.completions = [c.upper() for c in completions]
|
|
import readline
|
|
self.rl = readline
|
|
readline.parse_and_bind("tab: complete")
|
|
readline.set_completer(self.complete)
|
|
|
|
def __enter__(self):
|
|
self.old_completer = self.rl.get_completer()
|
|
try:
|
|
if self.histfile:
|
|
self.rl.read_history_file(self.histfile)
|
|
except IOError: # the first time
|
|
pass
|
|
return self
|
|
|
|
def __exit__(self, etype, exc, tb):
|
|
self.rl.set_completer(self.old_completer)
|
|
if self.histfile:
|
|
self.rl.write_history_file(self.histfile)
|
|
|
|
def complete(self, kw, state):
|
|
# state is 0, 1, 2, ... and increases by hitting TAB
|
|
if not self.case_sensitive:
|
|
kw = kw.upper()
|
|
try:
|
|
return [k for k in self.completions if k.startswith(kw)][state]
|
|
except IndexError: # no completions
|
|
return # exit
|
|
|
|
def readline(self, prompt=''):
|
|
try:
|
|
return raw_input(prompt) + '\n'
|
|
except EOFError:
|
|
return ''
|
|
|
|
def __iter__(self):
|
|
return iter(self.readline, '')
|
|
|
|
# ################# help functionality in plac interpreters ################# #
|
|
|
|
|
|
class HelpSummary(object):
|
|
"Build the help summary consistently with the cmd module"
|
|
|
|
@classmethod
|
|
def add(cls, obj, specialcommands):
|
|
p = plac_core.parser_from(obj)
|
|
c = cmd.Cmd(stdout=cls())
|
|
c.stdout.write('\n')
|
|
c.print_topics('special commands',
|
|
sorted(specialcommands), 15, 80)
|
|
c.print_topics('custom commands',
|
|
sorted(obj.commands), 15, 80)
|
|
c.print_topics('commands run in external processes',
|
|
sorted(obj.mpcommands), 15, 80)
|
|
c.print_topics('threaded commands',
|
|
sorted(obj.thcommands), 15, 80)
|
|
p.helpsummary = str(c.stdout)
|
|
|
|
def __init__(self):
|
|
self._ls = []
|
|
|
|
def write(self, s):
|
|
self._ls.append(s)
|
|
|
|
def __str__(self):
|
|
return ''.join(self._ls)
|
|
|
|
|
|
class PlacFormatter(argparse.RawDescriptionHelpFormatter):
|
|
def _metavar_formatter(self, action, default_metavar):
|
|
'Remove special commands from the usage message'
|
|
choices = action.choices or {}
|
|
action.choices = dict((n, c) for n, c in choices.items()
|
|
if not n.startswith('.'))
|
|
return super(PlacFormatter, self)._metavar_formatter(
|
|
action, default_metavar)
|
|
|
|
|
|
def format_help(self):
|
|
"Attached to plac_core.ArgumentParser for plac interpreters"
|
|
try:
|
|
return self.helpsummary
|
|
except AttributeError:
|
|
return super(plac_core.ArgumentParser, self).format_help()
|
|
plac_core.ArgumentParser.format_help = format_help
|
|
|
|
|
|
def default_help(obj, cmd=None):
|
|
"The default help functionality in plac interpreters"
|
|
parser = plac_core.parser_from(obj)
|
|
if cmd is None:
|
|
yield parser.format_help()
|
|
return
|
|
subp = parser.subparsers._name_parser_map.get(cmd)
|
|
if subp is None:
|
|
yield _('Unknown command %s' % cmd)
|
|
elif getattr(obj, '_interact_', False): # in interactive mode
|
|
formatter = subp._get_formatter()
|
|
formatter._prog = cmd # remove the program name from the usage
|
|
formatter.add_usage(
|
|
subp.usage, [a for a in subp._actions if a.dest != 'help'],
|
|
subp._mutually_exclusive_groups)
|
|
formatter.add_text(subp.description)
|
|
for action_group in subp._action_groups:
|
|
formatter.start_section(action_group.title)
|
|
formatter.add_text(action_group.description)
|
|
formatter.add_arguments(a for a in action_group._group_actions
|
|
if a.dest != 'help')
|
|
formatter.end_section()
|
|
yield formatter.format_help()
|
|
else: # regular argparse help
|
|
yield subp.format_help()
|
|
|
|
# ######################## import management ############################## #
|
|
|
|
try:
|
|
PLACDIRS = os.environ.get('PLACPATH', '.').split(':')
|
|
except:
|
|
raise ValueError(_('Ill-formed PLACPATH: got %PLACPATHs') % os.environ)
|
|
|
|
|
|
def partial_call(factory, arglist):
|
|
"Call a container factory with the arglist and return a plac object"
|
|
a = plac_core.parser_from(factory).argspec
|
|
if a.defaults or a.varargs or a.varkw:
|
|
raise TypeError('Interpreter.call must be invoked on '
|
|
'factories with required arguments only')
|
|
required_args = ', '.join(a.args)
|
|
if required_args:
|
|
required_args += ',' # trailing comma
|
|
code = '''def makeobj(interact, %s *args):
|
|
obj = factory(%s)
|
|
obj._interact_ = interact
|
|
obj._args_ = args
|
|
return obj\n''' % (required_args, required_args)
|
|
dic = dict(factory=factory)
|
|
exec_(code, dic)
|
|
makeobj = dic['makeobj']
|
|
makeobj.add_help = False
|
|
if inspect.isclass(factory):
|
|
makeobj.__annotations__ = getattr(
|
|
factory.__init__, '__annotations__', {})
|
|
else:
|
|
makeobj.__annotations__ = getattr(
|
|
factory, '__annotations__', {})
|
|
makeobj.__annotations__['interact'] = (
|
|
'start interactive interpreter', 'flag', 'i')
|
|
return plac_core.call(makeobj, arglist)
|
|
|
|
|
|
def import_main(path, *args):
|
|
"""
|
|
An utility to import the main function of a plac tool. It also
|
|
works with command container factories.
|
|
"""
|
|
if ':' in path: # importing a factory
|
|
path, factory_name = path.split(':')
|
|
else: # importing the main function
|
|
factory_name = None
|
|
if not os.path.isabs(path): # relative path, look at PLACDIRS
|
|
for placdir in PLACDIRS:
|
|
fullpath = os.path.join(placdir, path)
|
|
if os.path.exists(fullpath):
|
|
break
|
|
else: # no break
|
|
raise ImportError(_('Cannot find %s' % path))
|
|
else:
|
|
fullpath = path
|
|
name, ext = os.path.splitext(os.path.basename(fullpath))
|
|
module = imp.load_module(name, open(fullpath), fullpath, (ext, 'U', 1))
|
|
if factory_name:
|
|
tool = partial_call(getattr(module, factory_name), args)
|
|
else:
|
|
tool = module.main
|
|
return tool
|
|
|
|
# ############################ Task classes ############################# #
|
|
|
|
|
|
# base class not instantiated directly
|
|
class BaseTask(object):
|
|
"""
|
|
A task is a wrapper over a generator object with signature
|
|
Task(no, arglist, genobj), attributes
|
|
.no
|
|
.arglist
|
|
.outlist
|
|
.str
|
|
.etype
|
|
.exc
|
|
.tb
|
|
.status
|
|
and methods .run and .kill.
|
|
"""
|
|
STATES = ('SUBMITTED', 'RUNNING', 'TOBEKILLED', 'KILLED', 'FINISHED',
|
|
'ABORTED')
|
|
|
|
def __init__(self, no, arglist, genobj):
|
|
self.no = no
|
|
self.arglist = arglist
|
|
self._genobj = self._wrap(genobj)
|
|
self.str, self.etype, self.exc, self.tb = '', None, None, None
|
|
self.status = 'SUBMITTED'
|
|
self.outlist = []
|
|
|
|
def notify(self, msg):
|
|
"Notifies the underlying monitor. To be implemented"
|
|
|
|
def _wrap(self, genobj, stringify_tb=False):
|
|
"""
|
|
Wrap the genobj into a generator managing the exceptions,
|
|
populating the .outlist, setting the .status and yielding None.
|
|
stringify_tb must be True if the traceback must be sent to a process.
|
|
"""
|
|
self.status = 'RUNNING'
|
|
try:
|
|
for value in genobj:
|
|
if self.status == 'TOBEKILLED': # exit from the loop
|
|
raise GeneratorExit
|
|
if value is not None: # add output
|
|
self.outlist.append(value)
|
|
self.notify(decode(value))
|
|
yield
|
|
except Interpreter.Exit: # wanted exit
|
|
self._regular_exit()
|
|
raise
|
|
except (GeneratorExit, TerminatedProcess, KeyboardInterrupt):
|
|
# soft termination
|
|
self.status = 'KILLED'
|
|
except: # unexpected exception
|
|
self.etype, self.exc, tb = sys.exc_info()
|
|
self.tb = ''.join(traceback.format_tb(tb)) if stringify_tb else tb
|
|
self.status = 'ABORTED'
|
|
else:
|
|
self._regular_exit()
|
|
|
|
def _regular_exit(self):
|
|
self.status = 'FINISHED'
|
|
try:
|
|
self.str = '\n'.join(map(decode, self.outlist))
|
|
except IndexError:
|
|
self.str = 'no result'
|
|
|
|
def run(self):
|
|
"Run the inner generator"
|
|
for none in self._genobj:
|
|
pass
|
|
|
|
def kill(self):
|
|
"Set a TOBEKILLED status"
|
|
self.status = 'TOBEKILLED'
|
|
|
|
def wait(self):
|
|
"Wait for the task to finish: to be overridden"
|
|
|
|
@property
|
|
def traceback(self):
|
|
"Return the traceback as a (possibly empty) string"
|
|
if self.tb is None:
|
|
return ''
|
|
elif isinstance(self.tb, (str, bytes)):
|
|
return self.tb
|
|
else:
|
|
return ''.join(traceback.format_tb(self.tb))
|
|
|
|
@property
|
|
def result(self):
|
|
self.wait()
|
|
if self.exc:
|
|
if isinstance(self.tb, (str, bytes)):
|
|
raise self.etype(self.tb)
|
|
else:
|
|
raise_(self.etype, self.exc, self.tb or None)
|
|
if not self.outlist:
|
|
return None
|
|
return self.outlist[-1]
|
|
|
|
def __repr__(self):
|
|
"String representation containing class name, number, arglist, status"
|
|
return '<%s %d [%s] %s>' % (
|
|
self.__class__.__name__, self.no,
|
|
' '.join(self.arglist), self.status)
|
|
|
|
nulltask = BaseTask(0, [], ('skip' for dummy in (1,)))
|
|
|
|
# ######################## synchronous tasks ############################## #
|
|
|
|
|
|
class SynTask(BaseTask):
|
|
"""
|
|
Synchronous task running in the interpreter loop and displaying its
|
|
output as soon as available.
|
|
"""
|
|
def __str__(self):
|
|
"Return the output string or the error message"
|
|
if self.etype: # there was an error
|
|
return '%s: %s' % (self.etype.__name__, self.exc)
|
|
else:
|
|
return '\n'.join(map(str, self.outlist))
|
|
|
|
|
|
class ThreadedTask(BaseTask):
|
|
"""
|
|
A task running in a separated thread.
|
|
"""
|
|
def __init__(self, no, arglist, genobj):
|
|
BaseTask.__init__(self, no, arglist, genobj)
|
|
self.thread = threading.Thread(target=super(ThreadedTask, self).run)
|
|
|
|
def run(self):
|
|
"Run the task into a thread"
|
|
self.thread.start()
|
|
|
|
def wait(self):
|
|
"Block until the thread ends"
|
|
self.thread.join()
|
|
|
|
|
|
# ######################## multiprocessing tasks ######################### #
|
|
|
|
def sharedattr(name, on_error):
|
|
"Return a property to be attached to an MPTask"
|
|
def get(self):
|
|
try:
|
|
return getattr(self.ns, name)
|
|
except: # the process was killed or died hard
|
|
return on_error
|
|
|
|
def set(self, value):
|
|
try:
|
|
setattr(self.ns, name, value)
|
|
except: # the process was killed or died hard
|
|
pass
|
|
return property(get, set)
|
|
|
|
|
|
class MPTask(BaseTask):
|
|
"""
|
|
A task running as an external process. The current implementation
|
|
only works on Unix-like systems, where multiprocessing use forks.
|
|
"""
|
|
str = sharedattr('str', '')
|
|
etype = sharedattr('etype', None)
|
|
exc = sharedattr('exc', None)
|
|
tb = sharedattr('tb', None)
|
|
status = sharedattr('status', 'ABORTED')
|
|
|
|
@property
|
|
def outlist(self):
|
|
try:
|
|
return self._outlist
|
|
except: # the process died hard
|
|
return []
|
|
|
|
def notify(self, msg):
|
|
self.man.notify_listener(self.no, msg)
|
|
|
|
def __init__(self, no, arglist, genobj, manager):
|
|
"""
|
|
The monitor has a .send method and a .man multiprocessing.Manager
|
|
"""
|
|
self.no = no
|
|
self.arglist = arglist
|
|
self._genobj = self._wrap(genobj, stringify_tb=True)
|
|
self.man = manager
|
|
self._outlist = manager.mp.list()
|
|
self.ns = manager.mp.Namespace()
|
|
self.status = 'SUBMITTED'
|
|
self.etype, self.exc, self.tb = None, None, None
|
|
self.str = repr(self)
|
|
self.proc = multiprocessing.Process(target=super(MPTask, self).run)
|
|
|
|
def run(self):
|
|
"Run the task into an external process"
|
|
self.proc.start()
|
|
|
|
def wait(self):
|
|
"Block until the external process ends or is killed"
|
|
self.proc.join()
|
|
|
|
def kill(self):
|
|
"""Kill the process with a SIGTERM inducing a TerminatedProcess
|
|
exception in the children"""
|
|
self.proc.terminate()
|
|
|
|
# ######################## Task Manager ###################### #
|
|
|
|
|
|
class TaskManager(object):
|
|
"""
|
|
Store the given commands into a task registry. Provides methods to
|
|
manage the submitted tasks.
|
|
"""
|
|
cmdprefix = '.'
|
|
specialcommands = set(['.last_tb'])
|
|
|
|
def __init__(self, obj):
|
|
self.obj = obj
|
|
self.registry = {} # {taskno : task}
|
|
if obj.mpcommands or obj.thcommands:
|
|
self.specialcommands.update(['.kill', '.list', '.output'])
|
|
interact = getattr(obj, '_interact_', False)
|
|
self.parser = plac_core.parser_from(
|
|
obj, prog='' if interact else None, formatter_class=PlacFormatter)
|
|
HelpSummary.add(obj, self.specialcommands)
|
|
self.man = Manager() if obj.mpcommands else None
|
|
signal.signal(signal.SIGTERM, terminatedProcess)
|
|
|
|
def close(self):
|
|
"Kill all the running tasks"
|
|
for task in self.registry.values():
|
|
try:
|
|
if task.status == 'RUNNING':
|
|
task.kill()
|
|
task.wait()
|
|
except: # task killed, nothing to wait
|
|
pass
|
|
if self.man:
|
|
self.man.stop()
|
|
|
|
def _get_latest(self, taskno=-1, status=None):
|
|
"Get the latest submitted task from the registry"
|
|
assert taskno < 0, 'You must pass a negative number'
|
|
if status:
|
|
tasks = [t for t in self.registry.values()
|
|
if t.status == status]
|
|
else:
|
|
tasks = [t for t in self.registry.values()]
|
|
tasks.sort(key=attrgetter('no'))
|
|
if len(tasks) >= abs(taskno):
|
|
return tasks[taskno]
|
|
|
|
# ########################## special commands ######################## #
|
|
|
|
@plac_core.annotations(
|
|
taskno=('task to kill', 'positional', None, int))
|
|
def kill(self, taskno=-1):
|
|
'kill the given task (-1 to kill the latest running task)'
|
|
if taskno < 0:
|
|
task = self._get_latest(taskno, status='RUNNING')
|
|
if task is None:
|
|
yield 'Nothing to kill'
|
|
return
|
|
elif taskno not in self.registry:
|
|
yield 'Unknown task %d' % taskno
|
|
return
|
|
else:
|
|
task = self.registry[taskno]
|
|
if task.status in ('ABORTED', 'KILLED', 'FINISHED'):
|
|
yield 'Already finished %s' % task
|
|
return
|
|
task.kill()
|
|
yield task
|
|
|
|
@plac_core.annotations(
|
|
status=('', 'positional', None, str, BaseTask.STATES))
|
|
def list(self, status='RUNNING'):
|
|
'list tasks with a given status'
|
|
for task in self.registry.values():
|
|
if task.status == status:
|
|
yield task
|
|
|
|
@plac_core.annotations(
|
|
taskno=('task number', 'positional', None, int))
|
|
def output(self, taskno=-1, fname=None):
|
|
'show the output of a given task (and optionally save it to a file)'
|
|
if taskno < 0:
|
|
task = self._get_latest(taskno)
|
|
if task is None:
|
|
yield 'Nothing to show'
|
|
return
|
|
elif taskno not in self.registry:
|
|
yield 'Unknown task %d' % taskno
|
|
return
|
|
else:
|
|
task = self.registry[taskno]
|
|
outstr = '\n'.join(map(str, task.outlist))
|
|
if fname:
|
|
open(fname, 'w').write(outstr)
|
|
yield 'saved output of %d into %s' % (taskno, fname)
|
|
return
|
|
yield task
|
|
if len(task.outlist) > 20 and use_less:
|
|
less(outstr) # has no meaning for a plac server
|
|
else:
|
|
yield outstr
|
|
|
|
@plac_core.annotations(
|
|
taskno=('task number', 'positional', None, int))
|
|
def last_tb(self, taskno=-1):
|
|
"show the traceback of a given task, if any"
|
|
task = self._get_latest(taskno)
|
|
if task:
|
|
yield task.traceback
|
|
else:
|
|
yield 'Nothing to show'
|
|
|
|
# ########################## SyncProcess ############################# #
|
|
|
|
|
|
class Process(subprocess.Popen):
|
|
"Start the interpreter specified by the params in a subprocess"
|
|
|
|
def __init__(self, params):
|
|
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
|
|
# to avoid broken pipe messages
|
|
code = '''import plac, sys
|
|
sys.argv[0] = '<%s>'
|
|
plac.Interpreter(plac.import_main(*%s)).interact(prompt='i>\\n')
|
|
''' % (params[0], params)
|
|
subprocess.Popen.__init__(
|
|
self, [sys.executable, '-u', '-c', code],
|
|
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
|
self.man = multiprocessing.Manager()
|
|
|
|
def close(self):
|
|
"Close stdin and stdout"
|
|
self.stdin.close()
|
|
self.stdout.close()
|
|
self.man.shutdown()
|
|
|
|
def recv(self): # char-by-char cannot work
|
|
"Return the output of the subprocess, line-by-line until the prompt"
|
|
lines = []
|
|
while True:
|
|
lines.append(self.stdout.readline())
|
|
if lines[-1] == 'i>\n':
|
|
out = ''.join(lines)
|
|
return out[:-1] + ' ' # remove last newline
|
|
|
|
def send(self, line):
|
|
"""Send a line (adding a newline) to the underlying subprocess
|
|
and wait for the answer"""
|
|
self.stdin.write(line + os.linesep)
|
|
return self.recv()
|
|
|
|
|
|
class StartStopObject(object):
|
|
started = False
|
|
|
|
def start(self):
|
|
pass
|
|
|
|
def stop(self):
|
|
pass
|
|
|
|
|
|
class Monitor(StartStopObject):
|
|
"""
|
|
Base monitor class with methods add_listener/del_listener/notify_listener
|
|
read_queue and and start/stop.
|
|
"""
|
|
def __init__(self, name, queue=None):
|
|
self.name = name
|
|
self.queue = queue or multiprocessing.Queue()
|
|
|
|
def add_listener(self, taskno):
|
|
pass
|
|
|
|
def del_listener(self, taskno):
|
|
pass
|
|
|
|
def notify_listener(self, taskno, msg):
|
|
pass
|
|
|
|
def start(self):
|
|
pass
|
|
|
|
def stop(self):
|
|
pass
|
|
|
|
def read_queue(self):
|
|
pass
|
|
|
|
|
|
class Manager(StartStopObject):
|
|
"""
|
|
The plac Manager contains a multiprocessing.Manager and a set
|
|
of slave monitor processes to which we can send commands. There
|
|
is a manager for each interpreter with mpcommands.
|
|
"""
|
|
def __init__(self):
|
|
self.registry = {}
|
|
self.started = False
|
|
self.mp = None
|
|
|
|
def add(self, monitor):
|
|
'Add or replace a monitor in the registry'
|
|
proc = multiprocessing.Process(None, monitor.start, monitor.name)
|
|
proc.queue = monitor.queue
|
|
self.registry[monitor.name] = proc
|
|
|
|
def delete(self, name):
|
|
'Remove a named monitor from the registry'
|
|
del self.registry[name]
|
|
|
|
# can be called more than once
|
|
def start(self):
|
|
if self.mp is None:
|
|
self.mp = multiprocessing.Manager()
|
|
for monitor in self.registry.values():
|
|
monitor.start()
|
|
self.started = True
|
|
|
|
def stop(self):
|
|
for monitor in self.registry.values():
|
|
monitor.queue.close()
|
|
monitor.terminate()
|
|
if self.mp:
|
|
self.mp.shutdown()
|
|
self.mp = None
|
|
self.started = False
|
|
|
|
def notify_listener(self, taskno, msg):
|
|
for monitor in self.registry.values():
|
|
monitor.queue.put(('notify_listener', taskno, msg))
|
|
|
|
def add_listener(self, no):
|
|
for monitor in self.registry.values():
|
|
monitor.queue.put(('add_listener', no))
|
|
|
|
# ######################### plac server ############################# #
|
|
|
|
import asyncore
|
|
import asynchat
|
|
import socket
|
|
|
|
|
|
class _AsynHandler(asynchat.async_chat):
|
|
"asynchat handler starting a new interpreter loop for each connection"
|
|
|
|
terminator = '\r\n' # the standard one for telnet
|
|
prompt = 'i> '
|
|
|
|
def __init__(self, socket, interpreter):
|
|
asynchat.async_chat.__init__(self, socket)
|
|
self.set_terminator(self.terminator)
|
|
self.i = interpreter
|
|
self.i.__enter__()
|
|
self.data = []
|
|
self.write(self.prompt)
|
|
|
|
def write(self, data, *args):
|
|
"Push a string back to the client"
|
|
if args:
|
|
data %= args
|
|
if data.endswith('\n') and not data.endswith(self.terminator):
|
|
data = data[:-1] + self.terminator # fix newlines
|
|
self.push(data)
|
|
|
|
def collect_incoming_data(self, data):
|
|
"Collect one character at the time"
|
|
self.data.append(data)
|
|
|
|
def found_terminator(self):
|
|
"Put in the queue the line received from the client"
|
|
line = ''.join(self.data)
|
|
self.log('Received line %r from %s' % (line, self.addr))
|
|
if line == 'EOF':
|
|
self.i.__exit__(None, None, None)
|
|
self.handle_close()
|
|
else:
|
|
task = self.i.submit(line)
|
|
task.run() # synchronous or not
|
|
if task.etype: # manage exception
|
|
error = '%s: %s\nReceived: %s' % (
|
|
task.etype.__name__, task.exc, ' '.join(task.arglist))
|
|
self.log_info(task.traceback + error) # on the server
|
|
self.write(error + self.terminator) # back to the client
|
|
else: # no exception
|
|
self.write(task.str + self.terminator)
|
|
self.data = []
|
|
self.write(self.prompt)
|
|
|
|
|
|
class _AsynServer(asyncore.dispatcher):
|
|
"asyncore-based server spawning AsynHandlers"
|
|
|
|
def __init__(self, interpreter, newhandler, port, listen=5):
|
|
self.interpreter = interpreter
|
|
self.newhandler = newhandler
|
|
self.port = port
|
|
asyncore.dispatcher.__init__(self)
|
|
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
self.bind(('', port))
|
|
self.listen(listen)
|
|
|
|
def handle_accept(self):
|
|
clientsock, clientaddr = self.accept()
|
|
self.log('Connected from %s' % str(clientaddr))
|
|
i = self.interpreter.__class__(self.interpreter.obj) # new interpreter
|
|
self.newhandler(clientsock, i) # spawn a new handler
|
|
|
|
|
|
# ########################## the Interpreter ############################ #
|
|
|
|
class Interpreter(object):
|
|
"""
|
|
A context manager with a .send method and a few utility methods:
|
|
execute, test and doctest.
|
|
"""
|
|
class Exit(Exception):
|
|
pass
|
|
|
|
def __init__(self, obj, commentchar='#', split=shlex.split):
|
|
self.obj = obj
|
|
try:
|
|
self.name = obj.__module__
|
|
except AttributeError:
|
|
self.name = 'plac'
|
|
self.commentchar = commentchar
|
|
self.split = split
|
|
self._set_commands(obj)
|
|
self.tm = TaskManager(obj)
|
|
self.man = self.tm.man
|
|
self.parser = self.tm.parser
|
|
if self.commands:
|
|
self.parser.addsubcommands(
|
|
self.tm.specialcommands, self.tm, title='special commands')
|
|
if obj.mpcommands:
|
|
self.parser.addsubcommands(
|
|
obj.mpcommands, obj,
|
|
title='commands run in external processes')
|
|
if obj.thcommands:
|
|
self.parser.addsubcommands(
|
|
obj.thcommands, obj, title='threaded commands')
|
|
self.parser.error = lambda msg: sys.exit(msg) # patch the parser
|
|
self._interpreter = None
|
|
|
|
def _set_commands(self, obj):
|
|
"Make sure obj has the right command attributes as Python sets"
|
|
for attrname in ('commands', 'mpcommands', 'thcommands'):
|
|
setattr(self, attrname, set(getattr(self.__class__, attrname, [])))
|
|
setattr(obj, attrname, set(getattr(obj, attrname, [])))
|
|
self.commands = obj.commands
|
|
self.mpcommands.update(obj.mpcommands)
|
|
self.thcommands.update(obj.thcommands)
|
|
if (obj.commands or obj.mpcommands or obj.thcommands) and \
|
|
not hasattr(obj, 'help'): # add default help
|
|
obj.help = default_help.__get__(obj, obj.__class__)
|
|
self.commands.add('help')
|
|
|
|
def __enter__(self):
|
|
"Start the inner interpreter loop"
|
|
self._interpreter = self._make_interpreter()
|
|
self._interpreter.send(None)
|
|
return self
|
|
|
|
def __exit__(self, exctype, exc, tb):
|
|
"Close the inner interpreter and the task manager"
|
|
self.close(exctype, exc, tb)
|
|
|
|
def submit(self, line):
|
|
"Send a line to the underlying interpreter and return a task object"
|
|
if self._interpreter is None:
|
|
raise RuntimeError(_('%r not initialized: probably you forgot to '
|
|
'use the with statement') % self)
|
|
if isinstance(line, (str, bytes)):
|
|
arglist = self.split(line, self.commentchar)
|
|
else: # expects a list of strings
|
|
arglist = line
|
|
if not arglist:
|
|
return nulltask
|
|
m = self.tm.man # manager
|
|
if m and not m.started:
|
|
m.start()
|
|
task = self._interpreter.send(arglist) # nonblocking
|
|
if not plac_core._match_cmd(arglist[0], self.tm.specialcommands):
|
|
self.tm.registry[task.no] = task
|
|
if m:
|
|
m.add_listener(task.no)
|
|
return task
|
|
|
|
def send(self, line):
|
|
"""Send a line to the underlying interpreter and return
|
|
the finished task"""
|
|
task = self.submit(line)
|
|
BaseTask.run(task) # blocking
|
|
return task
|
|
|
|
def tasks(self):
|
|
"The full lists of the submitted tasks"
|
|
return self.tm.registry.values()
|
|
|
|
def close(self, exctype=None, exc=None, tb=None):
|
|
"Can be called to close the interpreter prematurely"
|
|
self.tm.close()
|
|
if exctype is not None:
|
|
self._interpreter.throw(exctype, exc, tb)
|
|
else:
|
|
self._interpreter.close()
|
|
|
|
def _make_interpreter(self):
|
|
"The interpreter main loop, from lists of arguments to task objects"
|
|
enter = getattr(self.obj, '__enter__', lambda: None)
|
|
exit = getattr(self.obj, '__exit__', lambda et, ex, tb: None)
|
|
enter()
|
|
task = None
|
|
try:
|
|
for no in itertools.count(1):
|
|
arglist = yield task
|
|
try:
|
|
cmd, result = self.parser.consume(arglist)
|
|
except SystemExit as e: # for invalid commands
|
|
if e.args == (0,): # raised as sys.exit(0)
|
|
errlist = []
|
|
else:
|
|
errlist = [str(e)]
|
|
task = SynTask(no, arglist, iter(errlist))
|
|
continue
|
|
except: # anything else
|
|
task = SynTask(no, arglist, gen_exc(*sys.exc_info()))
|
|
continue
|
|
if not plac_core.iterable(result): # atomic result
|
|
task = SynTask(no, arglist, gen_val(result))
|
|
elif cmd in self.obj.mpcommands:
|
|
task = MPTask(no, arglist, result, self.tm.man)
|
|
elif cmd in self.obj.thcommands:
|
|
task = ThreadedTask(no, arglist, result)
|
|
else: # blocking task
|
|
task = SynTask(no, arglist, result)
|
|
except GeneratorExit: # regular exit
|
|
exit(None, None, None)
|
|
except: # exceptional exit
|
|
exit(*sys.exc_info())
|
|
raise
|
|
|
|
def check(self, given_input, expected_output):
|
|
"Make sure you get the expected_output from the given_input"
|
|
output = self.send(given_input).str # blocking
|
|
ok = (output == expected_output)
|
|
if not ok:
|
|
# the message here is not internationalized on purpose
|
|
msg = 'input: %s\noutput: %s\nexpected: %s' % (
|
|
given_input, output, expected_output)
|
|
raise AssertionError(msg)
|
|
|
|
def _parse_doctest(self, lineiter):
|
|
"Returns the lines of input, the lines of output, and the line number"
|
|
lines = [line.strip() for line in lineiter]
|
|
inputs = []
|
|
positions = []
|
|
for i, line in enumerate(lines):
|
|
if line.startswith('i> '):
|
|
inputs.append(line[3:])
|
|
positions.append(i)
|
|
positions.append(len(lines) + 1) # last position
|
|
outputs = []
|
|
for i, start in enumerate(positions[:-1]):
|
|
end = positions[i + 1]
|
|
outputs.append('\n'.join(lines[start+1:end]))
|
|
return zip(inputs, outputs, positions)
|
|
|
|
def doctest(self, lineiter, verbose=False):
|
|
"""
|
|
Parse a text containing doctests in a context and tests of all them.
|
|
Raise an error even if a single doctest if broken. Use this for
|
|
sequential tests which are logically grouped.
|
|
"""
|
|
with self:
|
|
try:
|
|
for input, output, no in self._parse_doctest(lineiter):
|
|
if verbose:
|
|
write('i> %s\n' % input)
|
|
write('-> %s\n' % output)
|
|
task = self.send(input) # blocking
|
|
if not str(task) == output:
|
|
msg = ('line %d: input: %s\noutput: %s\nexpected: %s\n'
|
|
% (no + 1, input, task, output))
|
|
write(msg)
|
|
if task.exc:
|
|
raise_(task.etype, task.exc, task.tb)
|
|
except self.Exit:
|
|
pass
|
|
|
|
def execute(self, lineiter, verbose=False):
|
|
"Execute a lineiter of commands in a context and print the output"
|
|
with self:
|
|
try:
|
|
for line in lineiter:
|
|
if verbose:
|
|
write('i> ' + line)
|
|
task = self.send(line) # finished task
|
|
if task.etype: # there was an error
|
|
raise_(task.etype, task.exc, task.tb)
|
|
write('%s\n' % task.str)
|
|
except self.Exit:
|
|
pass
|
|
|
|
def multiline(self, stdin=sys.stdin, terminator=';', verbose=False):
|
|
"The multiline mode is especially suited for usage with emacs"
|
|
with self:
|
|
try:
|
|
for line in read_long_line(stdin, terminator):
|
|
task = self.submit(line)
|
|
task.run()
|
|
write('%s\n' % task.str)
|
|
if verbose and task.traceback:
|
|
write(task.traceback)
|
|
except self.Exit:
|
|
pass
|
|
|
|
def interact(self, stdin=sys.stdin, prompt='i> ', verbose=False):
|
|
"Starts an interactive command loop reading commands from the consolle"
|
|
try:
|
|
import readline
|
|
readline_present = True
|
|
except ImportError:
|
|
readline_present = False
|
|
if stdin is sys.stdin and readline_present: # use readline
|
|
histfile = os.path.expanduser('~/.%s.history' % self.name)
|
|
completions = list(self.commands) + list(self.mpcommands) + \
|
|
list(self.thcommands) + list(self.tm.specialcommands)
|
|
self.stdin = ReadlineInput(completions, histfile=histfile)
|
|
else:
|
|
self.stdin = stdin
|
|
self.prompt = prompt
|
|
self.verbose = verbose
|
|
intro = self.obj.__doc__ or ''
|
|
write(intro + '\n')
|
|
with self:
|
|
self.obj._interact_ = True
|
|
if self.stdin is sys.stdin: # do not close stdin automatically
|
|
self._manage_input()
|
|
else:
|
|
with self.stdin: # close stdin automatically
|
|
self._manage_input()
|
|
|
|
def _manage_input(self):
|
|
"Convert input lines into task which are then executed"
|
|
try:
|
|
for line in iter(lambda: read_line(self.stdin, self.prompt), ''):
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
task = self.submit(line)
|
|
task.run() # synchronous or not
|
|
write(str(task) + '\n')
|
|
if self.verbose and task.etype:
|
|
write(task.traceback)
|
|
except self.Exit:
|
|
pass
|
|
|
|
def start_server(self, port=2199, **kw):
|
|
"""Starts an asyncore server reading commands for clients and opening
|
|
a new interpreter for each connection."""
|
|
_AsynServer(self, _AsynHandler, port) # register the server
|
|
try:
|
|
asyncore.loop(**kw)
|
|
except (KeyboardInterrupt, TerminatedProcess):
|
|
pass
|
|
finally:
|
|
asyncore.close_all()
|
|
|
|
def add_monitor(self, mon):
|
|
self.man.add(mon)
|
|
|
|
def del_monitor(self, name):
|
|
self.man.delete(name)
|
|
|
|
@classmethod
|
|
def call(cls, factory, arglist=sys.argv[1:],
|
|
commentchar='#', split=shlex.split,
|
|
stdin=sys.stdin, prompt='i> ', verbose=False):
|
|
"""
|
|
Call a container factory with the arglist and instantiate an
|
|
interpreter object. If there are remaining arguments, send them to the
|
|
interpreter, else start an interactive session.
|
|
"""
|
|
obj = partial_call(factory, arglist)
|
|
i = cls(obj, commentchar, split)
|
|
if i.obj._args_:
|
|
with i:
|
|
task = i.send(i.obj._args_) # synchronous
|
|
if task.exc:
|
|
raise_(task.etype, task.exc, task.tb)
|
|
out = str(task)
|
|
if out:
|
|
print(out)
|
|
elif i.obj._interact_:
|
|
i.interact(stdin, prompt, verbose)
|
|
else:
|
|
i.parser.print_usage()
|
|
|
|
# ################################## runp ################################### #
|
|
|
|
|
|
class _TaskLauncher(object):
|
|
"Helper for runp"
|
|
|
|
def __init__(self, genseq, mode):
|
|
if mode == 'p':
|
|
self.mpcommands = ['rungen']
|
|
else:
|
|
self.thcommands = ['rungen']
|
|
self.genlist = list(genseq)
|
|
|
|
def rungen(self, i):
|
|
for out in self.genlist[int(i) - 1]:
|
|
yield out
|
|
|
|
|
|
def runp(genseq, mode='p'):
|
|
"""Run a sequence of generators in parallel. Mode can be 'p' (use processes)
|
|
or 't' (use threads). After all of them are finished, return a list of
|
|
task objects.
|
|
"""
|
|
assert mode in 'pt', mode
|
|
launcher = _TaskLauncher(genseq, mode)
|
|
res = []
|
|
with Interpreter(launcher) as inter:
|
|
for i in range(len(launcher.genlist)):
|
|
inter.submit('rungen %d' % (i + 1)).run()
|
|
for task in inter.tasks():
|
|
try:
|
|
res.append(task.result)
|
|
except Exception as e:
|
|
res.append(e)
|
|
return res
|