Allow jobs to be killed from the GUI

This commit is contained in:
Kovid Goyal 2008-03-04 19:49:55 +00:00
parent ff46adc03e
commit 76840c13e4
4 changed files with 80 additions and 7 deletions

View File

@ -31,7 +31,17 @@ class JobsDialog(QDialog, Ui_JobsDialog):
self.setWindowTitle(__appname__ + ' - Active Jobs') self.setWindowTitle(__appname__ + ' - Active Jobs')
QObject.connect(self.jobs_view.model(), SIGNAL('modelReset()'), QObject.connect(self.jobs_view.model(), SIGNAL('modelReset()'),
self.jobs_view.resizeColumnsToContents) 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): def closeEvent(self, e):
self.jobs_view.write_settings() self.jobs_view.write_settings()
e.accept() e.accept()

View File

@ -41,6 +41,13 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QPushButton" name="kill_button" >
<property name="text" >
<string>&amp;Stop selected job</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<customwidgets> <customwidgets>

View File

@ -12,6 +12,7 @@
## You should have received a copy of the GNU General Public License along ## 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., ## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from libprs500.gui2 import error_dialog
import traceback, logging, collections import traceback, logging, collections
from PyQt4.QtCore import QAbstractTableModel, QMutex, QObject, SIGNAL, Qt, \ 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()]: for job in [job for job in self.running_jobs if job.isFinished()]:
self.running_jobs.remove(job) self.running_jobs.remove(job)
self.finished_jobs.appendleft(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) self.emit(SIGNAL('job_done(int)'), job.id)
refresh = True refresh = True
@ -317,7 +319,9 @@ class JobManager(QAbstractTableModel):
if status == 0: if status == 0:
return self.running_icon return self.running_icon
if status == 2: 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 return NONE
def status_update(self, id, progress): def status_update(self, id, progress):
@ -327,6 +331,22 @@ class JobManager(QAbstractTableModel):
index = self.index(i, 2) index = self.index(i, 2)
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), index, index) self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), index, index)
break 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): class DetailView(QDialog, Ui_Dialog):

View File

@ -20,7 +20,7 @@ from functools import partial
from libprs500.ebooks.lrf.any.convert_from import main as any2lrf from libprs500.ebooks.lrf.any.convert_from import main as any2lrf
from libprs500.ebooks.lrf.web.convert_from import main as web2lrf from libprs500.ebooks.lrf.web.convert_from import main as web2lrf
from libprs500.gui2.lrf_renderer.main import main as lrfviewer from libprs500.gui2.lrf_renderer.main import main as lrfviewer
from libprs500 import iswindows from libprs500 import iswindows, __appname__
PARALLEL_FUNCS = { PARALLEL_FUNCS = {
'any2lrf' : partial(any2lrf, gui_mode=True), 'any2lrf' : partial(any2lrf, gui_mode=True),
@ -47,11 +47,44 @@ def cleanup(tdir):
class Server(object): 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): def __init__(self):
self.tdir = tempfile.mkdtemp('', 'libprs500_IPC_') self.tdir = tempfile.mkdtemp('', '%s_IPC_'%__appname__)
atexit.register(cleanup, self.tdir) atexit.register(cleanup, self.tdir)
self.stdout = {} 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): def run(self, job_id, func, args=[], kwdargs={}, monitor=True):
''' '''
Run a job in a separate process. 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 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. @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 @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_id = str(job_id)
job_dir = os.path.join(self.tdir, job_id) job_dir = os.path.join(self.tdir, job_id)
@ -86,7 +120,9 @@ class Server(object):
Popen((python, '-c', cmd)) Popen((python, '-c', cmd))
return return
while p.returncode is None: 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() p.poll()
time.sleep(0.5) # Wait for half a second time.sleep(0.5) # Wait for half a second
self.stdout[job_id].write(p.stdout.read()) self.stdout[job_id].write(p.stdout.read())