diff --git a/src/libprs500/gui2/dialogs/jobs.py b/src/libprs500/gui2/dialogs/jobs.py index b69a784010..002ace89f3 100644 --- a/src/libprs500/gui2/dialogs/jobs.py +++ b/src/libprs500/gui2/dialogs/jobs.py @@ -31,7 +31,17 @@ class JobsDialog(QDialog, Ui_JobsDialog): self.setWindowTitle(__appname__ + ' - Active Jobs') QObject.connect(self.jobs_view.model(), SIGNAL('modelReset()'), self.jobs_view.resizeColumnsToContents) - + QObject.connect(self.kill_button, SIGNAL('clicked()'), + self.kill_job) + QObject.connect(self, SIGNAL('kill_job(int, PyQt_PyObject)'), + self.jobs_view.model().kill_job) + + def kill_job(self): + for index in self.jobs_view.selectedIndexes(): + row = index.row() + self.emit(SIGNAL('kill_job(int, PyQt_PyObject)'), row, self) + return + def closeEvent(self, e): self.jobs_view.write_settings() e.accept() diff --git a/src/libprs500/gui2/dialogs/jobs.ui b/src/libprs500/gui2/dialogs/jobs.ui index 14a754072a..a14be17f78 100644 --- a/src/libprs500/gui2/dialogs/jobs.ui +++ b/src/libprs500/gui2/dialogs/jobs.ui @@ -41,6 +41,13 @@ + + + + &Stop selected job + + + diff --git a/src/libprs500/gui2/jobs.py b/src/libprs500/gui2/jobs.py index f598204067..0be82b9ee9 100644 --- a/src/libprs500/gui2/jobs.py +++ b/src/libprs500/gui2/jobs.py @@ -12,6 +12,7 @@ ## 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., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from libprs500.gui2 import error_dialog import traceback, logging, collections from PyQt4.QtCore import QAbstractTableModel, QMutex, QObject, SIGNAL, Qt, \ @@ -170,7 +171,8 @@ class JobManager(QAbstractTableModel): for job in [job for job in self.running_jobs if job.isFinished()]: self.running_jobs.remove(job) self.finished_jobs.appendleft(job) - job.notify() + if job.result != self.process_server.KILL_RESULT: + job.notify() self.emit(SIGNAL('job_done(int)'), job.id) refresh = True @@ -317,7 +319,9 @@ class JobManager(QAbstractTableModel): if status == 0: return self.running_icon if status == 2: - return self.done_icon if job.exception is None else self.error_icon + if job.exception or job.result == self.process_server.KILL_RESULT: + return self.error_icon + return self.done_icon return NONE def status_update(self, id, progress): @@ -327,6 +331,22 @@ class JobManager(QAbstractTableModel): index = self.index(i, 2) self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), index, index) break + + def kill_job(self, row, gui_parent): + job, status = self.row_to_job(row) + if isinstance(job, DeviceJob): + error_dialog(gui_parent, _('Cannot kill job'), + _('Cannot kill jobs that are communicating with the device as this may cause data corruption.')).exec_() + return + if status == 2: + error_dialog(gui_parent, _('Cannot kill job'), + _('Cannot kill already completed jobs.')).exec_() + return + if status == 1: + error_dialog(gui_parent, _('Cannot kill job'), + _('Cannot kill waiting jobs.')).exec_() + return + self.process_server.kill(job.id) class DetailView(QDialog, Ui_Dialog): diff --git a/src/libprs500/parallel.py b/src/libprs500/parallel.py index 5bf0d9636f..5d800cc3fb 100644 --- a/src/libprs500/parallel.py +++ b/src/libprs500/parallel.py @@ -20,7 +20,7 @@ from functools import partial from libprs500.ebooks.lrf.any.convert_from import main as any2lrf from libprs500.ebooks.lrf.web.convert_from import main as web2lrf from libprs500.gui2.lrf_renderer.main import main as lrfviewer -from libprs500 import iswindows +from libprs500 import iswindows, __appname__ PARALLEL_FUNCS = { 'any2lrf' : partial(any2lrf, gui_mode=True), @@ -47,11 +47,44 @@ def cleanup(tdir): class Server(object): + #: Interval in seconds at which child processes are polled for status information + INTERVAL = 0.1 + KILL_RESULT = 'Server: job killed by user|||#@#$%&*)*(*$#$%#$@&' + def __init__(self): - self.tdir = tempfile.mkdtemp('', 'libprs500_IPC_') + self.tdir = tempfile.mkdtemp('', '%s_IPC_'%__appname__) atexit.register(cleanup, self.tdir) self.stdout = {} + self.kill_jobs = [] + def kill(self, job_id): + ''' + Kill the job identified by job_id. + ''' + self.kill_jobs.append(str(job_id)) + + def _terminate(self, pid): + ''' + Kill process identified by C{pid}. + @param pid: On unix a process number, on windows a process handle. + ''' + if iswindows: + import win32api + try: + win32api.TerminateProcess(int(pid), -1) + except: + pass + else: + import signal + try: + try: + os.kill(pid, signal.SIGTERM) + finally: + time.sleep(2) + os.kill(pid, signal.SIGKILL) + except: + pass + def run(self, job_id, func, args=[], kwdargs={}, monitor=True): ''' Run a job in a separate process. @@ -61,7 +94,8 @@ class Server(object): @param kwdargs: A dictionary of keyword arguments to pass to C{func} @param monitor: If False launch the child process and return. Do not monitor/communicate with it. @return: (result, exception, formatted_traceback, log) where log is the combined - stdout + stderr of the child process; or None if monitor is True. + stdout + stderr of the child process; or None if monitor is True. If a job is killed + by a call to L{kill()} then result will be L{KILL_RESULT} ''' job_id = str(job_id) job_dir = os.path.join(self.tdir, job_id) @@ -86,7 +120,9 @@ class Server(object): Popen((python, '-c', cmd)) return while p.returncode is None: - self.stdout[job_id].write(p.stdout.readline()) + if job_id in self.kill_jobs: + self._terminate(p._handle if iswindows else p.pid) + return self.KILL_RESULT, None, None, _('Job killed by user') p.poll() time.sleep(0.5) # Wait for half a second self.stdout[job_id].write(p.stdout.read())