# ***************************************************************************** # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later # version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Module authors: # Markus Zolliker # ***************************************************************************** import threading from pathlib import Path from frappy.core import Drivable, Property, Parameter, Command, StringType, \ IDLE, BUSY, WARN, ERROR from frappy.errors import IsBusyError from frappy.client.interactive import run_script, ClientEnvironment from frappy.lib import formatExtendedTraceback class ScriptThread(threading.Thread): exc = None def __init__(self, completer, *args): super().__init__() self.completer = completer self.args = args self.clenv = ClientEnvironment() self.clenv.init() def run(self): try: run_script(*self.args, clenv=self.clenv) except BaseException as e: # print(formatExtendedTraceback()) print(e) self.exc = e finally: self.completer() def stop(self): self.clenv.trigger.shutdown() class Scripts(Drivable): scriptdir = Property('directory of scripts used as commands', StringType()) secnodes = Parameter('remote nodes, space separated', StringType(), readonly=False, default='') target = Parameter('script to start', StringType(), readonly=False, default='') value = Parameter(datatype=StringType(), default='') _status = IDLE, '' _thread = None _script = '' def __new__(cls, modname, logger, cfgdict, dispatcher): scriptdir = cfgdict['scriptdir']['value'] scripts = Path(scriptdir).expanduser().glob('*.py') attrs = {} for script in scripts: name = script.stem print(name, script) def cmdwrapper(self, target=name): self.write_target(target) attrs[name] = Command(description=f'script {name}')(cmdwrapper) return super().__new__(type(f'Script_{modname}', (cls,), attrs)) def _start_script(self, script): if self.isBusy(): raise IsBusyError('can not start a script while running') self._script = script self._thread = ScriptThread(self.read_status, str(script), *self.secnodes.split()) self._thread.start() def read_status(self): if self._thread is None: return IDLE, '' if self._thread.is_alive(): if self._stopping: return BUSY, f'stopping {self.target}' self.value = f'{self.target} running' return BUSY, self.value if self._stopping: self.value = f'{self.target} stopped' return WARN, self.value if self._thread.exc is None: self.value = f'{self.target} finished' return IDLE, self.value return ERROR, f'raised in {self.target}: {self._thread.exc!r}' def write_target(self, script): """start script given as target""" self._start_script(Path(self.scriptdir).expanduser() / f'{script}.py') self._stopping = False self.value = f'{script} started' self.status = BUSY, self.value self.setFastPoll(True) @Command() def stop(self): """interrupt script a script can only be stopped during setting a parameter, when starting a command or when waiting """ if self._thread: self._stopping = True self._thread.stop() self.read_status() @Command() def reset(self): """clear error status""" self._thread = None self._stopping = False self.read_status()