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())