From 29ca4eb3a8985004f84d6df15d98d371911b41fd Mon Sep 17 00:00:00 2001 From: donoban Date: Sat, 20 Oct 2018 18:34:15 +0200 Subject: [PATCH] Migration from python threads and to QThread obj Simple design and better performance, just start() thread and finished call back will show any error or msg, stop progress dialog and do cleanup. Full remove of QT process_events() calls. --- qubesmanager/backup.py | 151 ++++++++++++--------- qubesmanager/common_threads.py | 51 +++++++ qubesmanager/create_new_vm.py | 95 ++++++------- qubesmanager/qube_manager.py | 81 ++++------- qubesmanager/restore.py | 158 +++++++++++----------- qubesmanager/settings.py | 239 +++++++++++++++------------------ qubesmanager/thread_monitor.py | 42 ------ 7 files changed, 398 insertions(+), 419 deletions(-) create mode 100644 qubesmanager/common_threads.py delete mode 100644 qubesmanager/thread_monitor.py diff --git a/qubesmanager/backup.py b/qubesmanager/backup.py index d974842..fdc284c 100644 --- a/qubesmanager/backup.py +++ b/qubesmanager/backup.py @@ -23,9 +23,11 @@ import traceback import signal +import quamash from qubesadmin import Qubes, exc from qubesadmin import utils as admin_utils +from qubesadmin import events from qubes.storage.file import get_disk_usage from PyQt4 import QtCore # pylint: disable=import-error @@ -35,18 +37,38 @@ from . import multiselectwidget from . import backup_utils from . import utils + import grp import pwd import sys import os -from . import thread_monitor -import threading +import asyncio +from contextlib import suppress import time +class BackupThread(QtCore.QThread): + def __init__(self, vm): + QtCore.QThread.__init__(self) + self.vm = vm + self.msg = None + + def run(self): + msg = [] + try: + if not self.vm.is_running(): + self.vm.start() + self.vm.app.qubesd_call( + 'dom0', 'admin.backup.Execute', + backup_utils.get_profile_name(True)) + except Exception as ex: # pylint: disable=broad-except + msg.append(str(ex)) + + if msg: + self.msg = '\n'.join(msg) + class BackupVMsWindow(ui_backupdlg.Ui_Backup, multiselectwidget.QtGui.QWizard): - - def __init__(self, qt_app, qubes_app, parent=None): + def __init__(self, qt_app, qubes_app, dispatcher, parent=None): super(BackupVMsWindow, self).__init__(parent) self.qt_app = qt_app @@ -54,8 +76,6 @@ class BackupVMsWindow(ui_backupdlg.Ui_Backup, multiselectwidget.QtGui.QWizard): self.backup_settings = QtCore.QSettings() self.selected_vms = [] - self.canceled = False - self.thread_monitor = None self.setupUi(self) @@ -112,6 +132,17 @@ class BackupVMsWindow(ui_backupdlg.Ui_Backup, multiselectwidget.QtGui.QWizard): selected = self.load_settings() self.__fill_vms_list__(selected) + # Connect backup events for progress_bar + self.progress_bar.setMinimum(0) + self.progress_bar.setMaximum(100) + self.dispatcher = dispatcher + dispatcher.add_handler('backup-progress', self.on_backup_progress) + + def on_backup_progress(self, __submitter, _event, **kwargs): + print(kwargs['progress']) + self.progress_bar.setValue(int(float(kwargs['progress']))) + + def load_settings(self): """ Helper function that tries to load existing backup profile @@ -260,24 +291,6 @@ class BackupVMsWindow(ui_backupdlg.Ui_Backup, multiselectwidget.QtGui.QWizard): return True - def __do_backup__(self, t_monitor): - msg = [] - - try: - vm = self.qubes_app.domains[ - self.appvm_combobox.currentText()] - if not vm.is_running(): - vm.start() - self.qubes_app.qubesd_call( - 'dom0', 'admin.backup.Execute', - backup_utils.get_profile_name(True)) - except Exception as ex: # pylint: disable=broad-except - msg.append(str(ex)) - - if msg: - t_monitor.set_error_msg('\n'.join(msg)) - - t_monitor.set_finished() @staticmethod def cleanup_temporary_files(): @@ -310,33 +323,27 @@ class BackupVMsWindow(ui_backupdlg.Ui_Backup, multiselectwidget.QtGui.QWizard): self.showFileDialog.setChecked(self.showFileDialog.isEnabled() and str(self.dir_line_edit.text()) .count("media/") > 0) - self.thread_monitor = thread_monitor.ThreadMonitor() - thread = threading.Thread( - target=self.__do_backup__, - args=(self.thread_monitor,)) - thread.daemon = True - thread.start() - while not self.thread_monitor.is_finished(): - self.qt_app.processEvents() - time.sleep(0.1) + vm = self.qubes_app.domains[ + self.appvm_combobox.currentText()] + + self.thread = BackupThread(vm) + self.thread.finished.connect(self.backup_finished) + self.thread.start() + + signal.signal(signal.SIGCHLD, old_sigchld_handler) + + def backup_finished(self): + if self.thread.msg: + self.progress_status.setText(self.tr("Backup error.")) + QtGui.QMessageBox.warning( + self, self.tr("Backup error!"), + self.tr("ERROR: {}").format( + self.thread.msg)) + else: + self.progress_bar.setValue(100) + self.progress_status.setText(self.tr("Backup finished.")) - if not self.thread_monitor.success: - if self.canceled: - self.progress_status.setText( - self.tr( - "Backup aborted. " - "Temporary file may be left at backup location.")) - else: - self.progress_status.setText(self.tr("Backup error.")) - QtGui.QMessageBox.warning( - self, self.tr("Backup error!"), - self.tr("ERROR: {}").format( - self.thread_monitor.error_msg)) - else: - self.progress_bar.setMaximum(100) - self.progress_bar.setValue(100) - self.progress_status.setText(self.tr("Backup finished.")) if self.showFileDialog.isChecked(): orig_text = self.progress_status.text self.progress_status.setText( @@ -344,31 +351,28 @@ class BackupVMsWindow(ui_backupdlg.Ui_Backup, multiselectwidget.QtGui.QWizard): " Please unmount your backup volume and cancel " "the file selection dialog.")) backup_utils.select_path_button_clicked(self, False, True) + self.button(self.CancelButton).setEnabled(False) self.button(self.FinishButton).setEnabled(True) self.showFileDialog.setEnabled(False) self.cleanup_temporary_files() # turn off only when backup was successful - if self.thread_monitor.success and \ - self.turn_off_checkbox.isChecked(): + if self.turn_off_checkbox.isChecked(): os.system('systemctl poweroff') - signal.signal(signal.SIGCHLD, old_sigchld_handler) - def reject(self): if self.currentPage() is self.commit_page: - self.canceled = True + self.thread.terminate() self.qubes_app.qubesd_call( 'dom0', 'admin.backup.Cancel', backup_utils.get_profile_name(True)) - self.progress_bar.setMaximum(100) - self.progress_bar.setValue(0) - self.button(self.CancelButton).setDisabled(True) - self.cleanup_temporary_files() - else: - self.cleanup_temporary_files() - self.done(0) + QtGui.QMessageBox.warning( + self, self.tr("Backup aborted!"), + self.tr("ERROR: {}").format("Aborted!")) + + self.cleanup_temporary_files() + self.done(0) def has_selected_vms(self): return self.select_vms_widget.selected_list.count() > 0 @@ -402,9 +406,14 @@ def handle_exception(exc_type, exc_value, exc_traceback): error + "at line %d of file %s.

" % (line, filename)) +def loop_shutdown(): + pending = asyncio.Task.all_tasks() + for task in pending: + with suppress(asyncio.CancelledError): + task.cancel() + def main(): - qt_app = QtGui.QApplication(sys.argv) qt_app.setOrganizationName("The Qubes Project") qt_app.setOrganizationDomain("http://qubes-os.org") @@ -412,14 +421,24 @@ def main(): sys.excepthook = handle_exception - app = Qubes() + qubes_app = Qubes() - backup_window = BackupVMsWindow(qt_app, app) + loop = quamash.QEventLoop(qt_app) + asyncio.set_event_loop(loop) + dispatcher = events.EventsDispatcher(qubes_app) + backup_window = BackupVMsWindow(qt_app, qubes_app, dispatcher) backup_window.show() - qt_app.exec_() - qt_app.exit() + try: + loop.run_until_complete( + asyncio.ensure_future(dispatcher.listen_for_events())) + except asyncio.CancelledError: + pass + except Exception: # pylint: disable=broad-except + loop_shutdown() + exc_type, exc_value, exc_traceback = sys.exc_info()[:3] + handle_exception(exc_type, exc_value, exc_traceback) if __name__ == "__main__": diff --git a/qubesmanager/common_threads.py b/qubesmanager/common_threads.py new file mode 100644 index 0000000..c7fd1ff --- /dev/null +++ b/qubesmanager/common_threads.py @@ -0,0 +1,51 @@ +#!/usr/bin/python2 +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2011 Marek Marczykowski +# +# 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 Lesser General Public License along +# with this program; if not, see . +# +# + + +from PyQt4 import QtCore # pylint: disable=import-error +from qubesadmin import exc + + +class RemoveVMThread(QtCore.QThread): + def __init__(self, vm): + QtCore.QThread.__init__(self) + self.vm = vm + self.error = None + + def run(self): + try: + del self.vm.app.domains[self.vm.name] + except exc.QubesException as ex: + self.error("Error removing Qube!", str(ex)) + + +class CloneVMThread(QtCore.QThread): + def __init__(self, src_vm, dst_name): + QtCore.QThread.__init__(self) + self.src_vm = src_vm + self.dst_name = dst_name + + def run(self): + try: + dst_vm = self.src_vm.app.clone_vm(self.src_vm, self.dst_name) + self.error = ("Sucess", "The qube was cloned sucessfully.") + except exc.QubesException as ex: + self.error = ("Error while cloning Qube!", str(ex)) diff --git a/qubesmanager/create_new_vm.py b/qubesmanager/create_new_vm.py index 6e7fa52..73959ad 100644 --- a/qubesmanager/create_new_vm.py +++ b/qubesmanager/create_new_vm.py @@ -35,7 +35,39 @@ import qubesadmin.exc from . import utils from .ui_newappvmdlg import Ui_NewVMDlg # pylint: disable=import-error -from .thread_monitor import ThreadMonitor + +class CreateVMThread(QtCore.QThread): + def __init__(self, app, vmclass, name, label, template, properties): + QtCore.QThread.__init__(self) + self.app = app + self.vmclass = vmclass + self.name = name + self.label = label + self.template = template + self.properties = properties + self.error = None + + def run(self): + try: + if self.vmclass == 'StandaloneVM' and self.template is not None: + if self.template is qubesadmin.DEFAULT: + src_vm = self.app.default_template + else: + src_vm = self.template + vm = self.app.clone_vm(src_vm, self.name, self.vmclass) + vm.label = self.label + for k, v in self.properties.items(): + setattr(vm, k, v) + else: + vm = self.app.add_new_vm(self.vmclass, + name=self.name, label=self.label, template=self.template) + for k, v in self.properties.items(): + setattr(vm, k, v) + + except qubesadmin.exc.QubesException as qex: + self.error = str(qex) + except Exception as ex: # pylint: disable=broad-except + self.error = repr(ex) class NewVmDlg(QtGui.QDialog, Ui_NewVMDlg): @@ -125,67 +157,36 @@ class NewVmDlg(QtGui.QDialog, Ui_NewVMDlg): properties['virt_mode'] = 'hvm' properties['kernel'] = None - thread_monitor = ThreadMonitor() - thread = threading.Thread(target=self.do_create_vm, - args=(self.app, vmclass, name, label, template, properties, - thread_monitor)) - thread.daemon = True - thread.start() + self.thread = CreateVMThread(self.app, vmclass, name, label, + template, properties) + self.thread.finished.connect(self.create_finished) + self.thread.start() - progress = QtGui.QProgressDialog( + self.progress = QtGui.QProgressDialog( self.tr("Creating new qube {}...").format(name), "", 0, 0) - progress.setCancelButton(None) - progress.setModal(True) - progress.show() + self.progress.setCancelButton(None) + self.progress.setModal(True) + self.progress.show() - while not thread_monitor.is_finished(): - self.qtapp.processEvents() - time.sleep(0.1) + def create_finished(self): + self.progress.hide() - progress.hide() - - if not thread_monitor.success: + if self.thread.error: QtGui.QMessageBox.warning(None, self.tr("Error creating the qube!"), - self.tr("ERROR: {}").format(thread_monitor.error_msg)) + self.tr("ERROR: {}").format(thread.error)) self.done(0) - if thread_monitor.success: + if not self.thread.error: if self.launch_settings.isChecked(): - subprocess.check_call(['qubes-vm-settings', name]) + subprocess.check_call(['qubes-vm-settings', str(self.name)]) if self.install_system.isChecked(): subprocess.check_call( - ['qubes-vm-boot-from-device', name]) + ['qubes-vm-boot-from-device', srt(self.name)]) - @staticmethod - def do_create_vm(app, vmclass, name, label, template, properties, - thread_monitor): - try: - if vmclass == 'StandaloneVM' and template is not None: - if template is qubesadmin.DEFAULT: - src_vm = app.default_template - else: - src_vm = template - vm = app.clone_vm(src_vm, name, vmclass) - vm.label = label - for k, v in properties.items(): - setattr(vm, k, v) - else: - vm = app.add_new_vm(vmclass, - name=name, label=label, template=template) - for k, v in properties.items(): - setattr(vm, k, v) - - except qubesadmin.exc.QubesException as qex: - thread_monitor.set_error_msg(str(qex)) - except Exception as ex: # pylint: disable=broad-except - thread_monitor.set_error_msg(repr(ex)) - - thread_monitor.set_finished() def type_change(self): - # AppVM if self.vm_type.currentIndex() == 0: self.template_vm.setEnabled(True) diff --git a/qubesmanager/qube_manager.py b/qubesmanager/qube_manager.py index 63c8bf0..0022f59 100644 --- a/qubesmanager/qube_manager.py +++ b/qubesmanager/qube_manager.py @@ -25,12 +25,13 @@ import sys import os import os.path import subprocess -import time from datetime import datetime, timedelta import traceback +from contextlib import suppress +import time + import quamash import asyncio -from contextlib import suppress from qubesadmin import Qubes from qubesadmin import exc @@ -51,6 +52,7 @@ from . import backup from . import create_new_vm from . import log_dialog from . import utils as manager_utils +from . import common_threads class SearchBox(QtGui.QLineEdit): @@ -241,32 +243,20 @@ class StartVMThread(QtCore.QThread): def __init__(self, vm): QtCore.QThread.__init__(self) self.vm = vm + self.error = None def run(self): try: self.vm.start() except exc.QubesException as ex: - self.emit(QtCore.SIGNAL('show_error(QString, QString)'),\ - "Error starting Qube!", str(ex)) + self.error = ("Error starting Qube!", str(ex)) -class RemoveVMThread(QtCore.QThread): - def __init__(self, vm, qubes_app): - QtCore.QThread.__init__(self) - self.vm = vm - self.qubes_app = qubes_app - - def run(self): - try: - del self.qubes_app.domains[self.vm.name] - except exc.QubesException as ex: - self.emit(QtCore.SIGNAL('show_error(QString, QString)'),\ - "Error removing Qube!", str(ex)) - class UpdateVMThread(QtCore.QThread): def __init__(self, vm): QtCore.QThread.__init__(self) self.vm = vm + self.error = None def run(self): try: @@ -279,23 +269,7 @@ class UpdateVMThread(QtCore.QThread): self.vm.run_service("qubes.InstallUpdatesGUI",\ user="root", wait=False) except (ChildProcessError, exc.QubesException) as ex: - self.emit(QtCore.SIGNAL('show_error(QString, QString)'),\ - "Error on Qube update!", str(ex)) - - -class CloneVMThread(QtCore.QThread): - def __init__(self, src_vm, qubes_app, dst_name): - QtCore.QThread.__init__(self) - self.src_vm = src_vm - self.qubes_app = qubes_app - self.dst_name = dst_name - - def run(self): - try: - dst_vm = self.qubes_app.clone_vm(self.src_vm, self.dst_name) - except exc.QubesException as ex: - self.emit(QtCore.SIGNAL('show_error(QString, QString)'),\ - "Error while cloning Qube!", str(ex)) + self.error = ("Error on Qube update!", str(ex)) class RunCommandThread(QtCore.QThread): @@ -308,8 +282,7 @@ class RunCommandThread(QtCore.QThread): try: self.vm.run(self.command_to_run) except (ChildProcessError, exc.QubesException) as ex: - self.emit(QtCore.SIGNAL('show_error(QString, QString)'),\ - "Error while running command!", str(ex)) + self.error = ("Error while running command!", str(ex)) class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow): @@ -330,7 +303,7 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow): "IP": 8, "Backups": 9, "Last backup": 10, - } + } def __init__(self, qt_app, qubes_app, dispatcher, parent=None): # pylint: disable=unused-argument @@ -472,7 +445,10 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow): dispatcher.add_handler('domain-start-failed', self.on_domain_status_changed) dispatcher.add_handler('domain-stopped', self.on_domain_status_changed) + dispatcher.add_handler('domain-pre-shutdown', self.on_domain_status_changed) dispatcher.add_handler('domain-shutdown', self.on_domain_status_changed) + dispatcher.add_handler('domain-paused', self.on_domain_status_changed) + dispatcher.add_handler('domain-unpaused', self.on_domain_status_changed) dispatcher.add_handler('domain-add', self.on_domain_added) dispatcher.add_handler('domain-delete', self.on_domain_removed) @@ -496,6 +472,12 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow): def clear_threads(self): for thread in self.threads_list: if thread.isFinished(): + if thread.error: + (title, msg) = thread.error + QtGui.QMessageBox.warning( + None, + self.tr(title), + self.tr("ERROR: {0}").format(msg)) self.threads_list.remove(thread) def closeEvent(self, event): @@ -659,7 +641,6 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow): self.manager_settings.sync() def table_selection_changed(self): - vm = self.get_selected_vm() if vm is not None and vm in self.qubes_app.domains: @@ -783,7 +764,7 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow): else: # remove the VM - thread = RemoveVMThread(vm, self.qubes_app) + thread = common_threads.RemoveVMThread(vm) self.threads_list.append(thread) self.connect(thread, QtCore.SIGNAL("show_error(QString, QString)"), self.show_error) thread.finished.connect(self.clear_threads) @@ -806,16 +787,10 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow): if not ok or clone_name == "": return - self.thread = CloneVMThread(vm, self.qubes_app, clone_name) - self.thread.start() - return - - progress = QtGui.QProgressDialog( - self.tr("Cloning Qube {0} to {1}...").format( - vm.name, clone_name), "", 0, 0) - progress.setCancelButton(None) - progress.setModal(True) - progress.show() + thread = common_threads.CloneVMThread(vm, clone_name) + thread.finished.connect(self.clear_threads) + self.threads_list.append(thread) + thread.start() # noinspection PyArgumentList @QtCore.pyqtSlot(name='on_action_resumevm_triggered') @@ -825,8 +800,6 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow): if vm.get_power_state() in ["Paused", "Suspended"]: try: vm.unpause() - self.vms_in_table[vm.qid].update() - self.table_selection_changed() except exc.QubesException as ex: QtGui.QMessageBox.warning( None, self.tr("Error unpausing Qube!"), @@ -841,7 +814,6 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow): thread = StartVMThread(vm) self.threads_list.append(thread) - self.connect(thread, QtCore.SIGNAL("show_error(QString, QString)"), self.show_error) thread.finished.connect(self.clear_threads) thread.start() @@ -1016,7 +988,6 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow): thread = UpdateVMThread(vm) self.threads_list.append(thread) - self.connect(thread, QtCore.SIGNAL("show_error(QString, QString)"), self.show_error) thread.finished.connect(self.clear_threads) thread.start() @@ -1035,11 +1006,9 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow): thread = RunCommandThread(vm, command_to_run) self.threads_list.append(thread) - self.connect(thread, QtCore.SIGNAL("show_error(QString, QString)"), self.show_error) thread.finished.connect(self.clear_threads) thread.start() - # noinspection PyArgumentList @QtCore.pyqtSlot(name='on_action_set_keyboard_layout_triggered') def action_set_keyboard_layout_triggered(self): @@ -1079,7 +1048,7 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow): # noinspection PyArgumentList @QtCore.pyqtSlot(name='on_action_backup_triggered') def action_backup_triggered(self): - backup_window = backup.BackupVMsWindow(self.qt_app, self.qubes_app) + backup_window = backup.BackupVMsWindow(self.qt_app, self.qubes_app, self.dispatcher) backup_window.show() # noinspection PyArgumentList diff --git a/qubesmanager/restore.py b/qubesmanager/restore.py index 18e628c..985e346 100644 --- a/qubesmanager/restore.py +++ b/qubesmanager/restore.py @@ -38,7 +38,6 @@ from qubes import backup from . import ui_restoredlg # pylint: disable=no-name-in-module from . import multiselectwidget from . import backup_utils -from . import thread_monitor from multiprocessing import Queue, Event from multiprocessing.queues import Empty @@ -46,8 +45,37 @@ from qubesadmin import Qubes, exc from qubesadmin.backup import restore -class RestoreVMsWindow(ui_restoredlg.Ui_Restore, QtGui.QWizard): +class RestoreThread(QtCore.QThread): + def __init__(self, backup_restore, vms_to_restore): + QtCore.QThread.__init__(self) + self.backup_restore = backup_restore + self.vms_to_restore = vms_to_restore + self.error = None + self.msg = None + def run(self): + err_msg = [] + try: + self.backup_restore.restore_do(self.vms_to_restore) + + except backup.BackupCanceledError as ex: + self.canceled = True + err_msg.append(str(ex)) + except Exception as ex: # pylint: disable=broad-except + err_msg.append(str(ex)) + err_msg.append( + self.tr("Partially restored files left in /var/tmp/restore_*, " + "investigate them and/or clean them up")) + if err_msg: + self.error = '\n'.join(err_msg) + self.msg = '{0}'.format( + self.tr("Finished with errors!")) + else: + self.msg = '{0}'.format( + self.tr("Finished successfully!")) + + +class RestoreVMsWindow(ui_restoredlg.Ui_Restore, QtGui.QWizard): def __init__(self, qt_app, qubes_app, parent=None): super(RestoreVMsWindow, self).__init__(parent) @@ -64,9 +92,6 @@ class RestoreVMsWindow(ui_restoredlg.Ui_Restore, QtGui.QWizard): logger.addHandler(handler) logger.setLevel(logging.INFO) - self.canceled = False - self.error_detected = Event() - self.thread_monitor = None self.backup_restore = None self.target_appvm = None @@ -148,37 +173,8 @@ class RestoreVMsWindow(ui_restoredlg.Ui_Restore, QtGui.QWizard): def append_output(self, text): self.commit_text_edit.append(text) - def __do_restore__(self, t_monitor): - err_msg = [] - try: - self.backup_restore.restore_do(self.vms_to_restore) - - except backup.BackupCanceledError as ex: - self.canceled = True - err_msg.append(str(ex)) - except Exception as ex: # pylint: disable=broad-except - err_msg.append(str(ex)) - err_msg.append( - self.tr("Partially restored files left in /var/tmp/restore_*, " - "investigate them and/or clean them up")) - - if self.canceled: - self.append_output('{0}'.format( - self.tr("Restore aborted!"))) - elif err_msg or self.error_detected.is_set(): - if err_msg: - t_monitor.set_error_msg('\n'.join(err_msg)) - self.append_output('{0}'.format( - self.tr("Finished with errors!"))) - else: - self.append_output('{0}'.format( - self.tr("Finished successfully!"))) - - t_monitor.set_finished() - def current_page_changed(self, page_id): # pylint: disable=unused-argument - - old_sigchld_handler = signal.signal(signal.SIGCHLD, signal.SIG_DFL) + self.old_sigchld_handler = signal.signal(signal.SIGCHLD, signal.SIG_DFL) if self.currentPage() is self.select_vms_page: self.__fill_vms_list__() @@ -210,51 +206,57 @@ class RestoreVMsWindow(ui_restoredlg.Ui_Restore, QtGui.QWizard): and str(self.dir_line_edit.text()) .count("media/") > 0) - self.thread_monitor = thread_monitor.ThreadMonitor() - thread = threading.Thread(target=self.__do_restore__, - args=(self.thread_monitor,)) - thread.daemon = True - thread.start() - while not self.thread_monitor.is_finished(): - self.qt_app.processEvents() - time.sleep(0.1) - try: - log_record = self.feedback_queue.get_nowait() - while log_record: - if log_record.levelno == logging.ERROR or\ - log_record.levelno == logging.CRITICAL: - output = '{0}'.format( - log_record.getMessage()) - else: - output = log_record.getMessage() - self.append_output(output) - log_record = self.feedback_queue.get_nowait() - except Empty: - pass + self.thread = RestoreThread(self.backup_restore, self.vms_to_restore) + self.thread.finished.connect(self.thread_finished) - if not self.thread_monitor.success: - if not self.canceled: - QtGui.QMessageBox.warning( - None, - self.tr("Backup error!"), - self.tr("ERROR: {0}").format( - self.thread_monitor.error_msg)) - self.progress_bar.setMaximum(100) - self.progress_bar.setValue(100) + # Start log timer + timer = QtCore.QTimer(self) + timer.timeout.connect(self.update_log) + timer.start(1000) - if self.showFileDialog.isChecked(): - self.append_output( - '{0}'.format( - self.tr("Please unmount your backup volume and cancel " - "the file selection dialog."))) - self.qt_app.processEvents() - backup_utils.select_path_button_clicked(self, False, True) + self.thread.start() - self.button(self.FinishButton).setEnabled(True) - self.button(self.CancelButton).setEnabled(False) - self.showFileDialog.setEnabled(False) + def thread_finished(self): + self.progress_bar.setMaximum(100) + self.progress_bar.setValue(100) + + if self.thread.error: + QtGui.QMessageBox.warning( + None, + self.tr("Backup error!"), + self.tr("ERROR: {0}").format( + self.thread.error)) + + if self.thread.msg: + self.append_output(self.thread.msg) + + if self.showFileDialog.isChecked(): + self.append_output( + '{0}'.format( + self.tr("Please unmount your backup volume and cancel " + "the file selection dialog."))) + backup_utils.select_path_button_clicked(self, False, True) + + self.button(self.FinishButton).setEnabled(True) + self.button(self.CancelButton).setEnabled(False) + self.showFileDialog.setEnabled(False) + signal.signal(signal.SIGCHLD, self.old_sigchld_handler) + + def update_log(self): + try: + log_record = self.feedback_queue.get_nowait() + while log_record: + if log_record.levelno == logging.ERROR or\ + log_record.levelno == logging.CRITICAL: + output = '{0}'.format( + log_record.getMessage()) + else: + output = log_record.getMessage() + self.append_output(output) + log_record = self.feedback_queue.get_nowait() + except Empty: + pass - signal.signal(signal.SIGCHLD, old_sigchld_handler) def all_vms_good(self): for vm_info in self.vms_to_restore.values(): @@ -265,11 +267,13 @@ class RestoreVMsWindow(ui_restoredlg.Ui_Restore, QtGui.QWizard): return True def reject(self): - if self.currentPage() is self.commit_page: + if self.currentPage() is self.commit_page and self.thread.isRunning(): self.backup_restore.canceled = True self.append_output('{0}'.format( self.tr("Aborting the operation..."))) self.button(self.CancelButton).setDisabled(True) + + self.thread.terminate() else: self.done(0) diff --git a/qubesmanager/settings.py b/qubesmanager/settings.py index fcb749d..87845da 100755 --- a/qubesmanager/settings.py +++ b/qubesmanager/settings.py @@ -39,7 +39,7 @@ import qubesadmin.exc from . import utils from . import multiselectwidget -from . import thread_monitor +from . import common_threads from . import device_list from .appmenu_select import AppmenuSelectManager @@ -49,6 +49,77 @@ from PyQt4 import QtCore, QtGui # pylint: disable=import-error from . import ui_settingsdlg # pylint: disable=no-name-in-module +class RenameVMThread(QtCore.QThread): + def __init__(self, vm, new_vm_name, dependencies): + QtCore.QThread.__init__(self) + self.vm = vm + self.new_vm_name = new_vm_name + self.dependencies = dependencies + + def run(self): + try: + new_vm = self.vm.app.clone_vm(self.vm, self.new_vm_name) + + failed_props = [] + + for (holder, prop) in self.dependencies: + try: + if holder is None: + setattr(self.vm.app, prop, new_vm) + else: + setattr(holder, prop, new_vm) + except exc.QubesException as qex: + failed_props += [(holder, prop)] + + if not failed_props: + del self.vm.app.domains[self.vm.name] + else: + list_text = utils.format_dependencies_list(failed_props) + + QtGui.QMessageBox.warning( + self, + self.tr("Warning: rename partially unsuccessful"), + self.tr("Some properties could not be changed to the new " + "name. The system has now both {} and {} qubes. " + "To resolve this, please check and change the " + "following properties and remove the qube {} " + "manually.
").format( + self.vm.name, name, self.vm.name) + list_text) + + except exc.QubesException as qex: + self.error = ("Rename error!", str(ex)) + except Exception as ex: # pylint: disable=broad-except + self.error = ("Rename error!", repr(ex)) + + +class RefreshAppsVMThread(QtCore.QThread): + def __init__(self, vm): + QtCore.QThread.__init__(self) + self.error = None + self.vm = vm + + def run(self): + try: + try: + target_vm = self.vm.template + except AttributeError: + target_vm = self.vm + + if not target_vm.is_running(): + not_running = True + target_vm.start() + else: + not_running = False + + subprocess.check_call(['qvm-sync-appmenus', target_vm.name]) + + if not_running: + target_vm.shutdown() + + except Exception as ex: + self.error = ("Refresh failed!", str(ex)) + + # pylint: disable=too-many-instance-attributes class VMSettingsWindow(ui_settingsdlg.Ui_SettingsDialog, QtGui.QDialog): tabs_indices = collections.OrderedDict(( @@ -65,6 +136,7 @@ class VMSettingsWindow(ui_settingsdlg.Ui_SettingsDialog, QtGui.QDialog): self.vm = vm self.qapp = qapp + self.threads_list = [] try: self.source_vm = self.vm.template except AttributeError: @@ -150,6 +222,18 @@ class VMSettingsWindow(ui_settingsdlg.Ui_SettingsDialog, QtGui.QDialog): self.refresh_apps_button.clicked.connect( self.refresh_apps_button_pressed) + def clear_threads(self): + for thread in self.threads_list: + if thread.isFinished(): + if thread.error: + (title, msg) = thread.error + QtGui.QMessageBox.warning( + None, + self.tr(title), + self.tr("ERROR: {0}").format(msg)) + + self.threads_list.remove(thread) + def keyPressEvent(self, event): # pylint: disable=invalid-name if event.key() == QtCore.Qt.Key_Enter \ or event.key() == QtCore.Qt.Key_Return: @@ -164,31 +248,14 @@ class VMSettingsWindow(ui_settingsdlg.Ui_SettingsDialog, QtGui.QDialog): pass def save_changes(self): - t_monitor = thread_monitor.ThreadMonitor() - thread = threading.Thread(target=self.__save_changes__, - args=(t_monitor,)) - thread.daemon = True - thread.start() + error = self.__save_changes__() - progress = QtGui.QProgressDialog( - self.tr("Applying settings to {0}...").format(self.vm.name), - "", 0, 0) - progress.setCancelButton(None) - progress.setModal(True) - progress.show() - - while not t_monitor.is_finished(): - self.qapp.processEvents() - time.sleep(0.1) - - progress.hide() - - if not t_monitor.success: + if error: QtGui.QMessageBox.warning( self, self.tr("Error while changing settings for {0}!" ).format(self.vm.name), - self.tr("ERROR: {0}").format(t_monitor.error_msg)) + self.tr("ERROR: {0}").format('\n'.join(ret))) def apply(self): self.save_changes() @@ -197,9 +264,9 @@ class VMSettingsWindow(ui_settingsdlg.Ui_SettingsDialog, QtGui.QDialog): self.save_changes() self.done(0) - def __save_changes__(self, t_monitor): - + def __save_changes__(self): ret = [] + try: ret_tmp = self.__apply_basic_tab__() if ret_tmp: @@ -237,12 +304,8 @@ class VMSettingsWindow(ui_settingsdlg.Ui_SettingsDialog, QtGui.QDialog): except Exception as ex: # pylint: disable=broad-except ret += [self.tr("Applications tab:"), repr(ex)] - if ret: - t_monitor.set_error_msg('\n'.join(ret)) - utils.debug('\n'.join(ret)) - - t_monitor.set_finished() + return ret def check_network_availability(self): netvm = self.vm.netvm @@ -464,61 +527,6 @@ class VMSettingsWindow(ui_settingsdlg.Ui_SettingsDialog, QtGui.QDialog): "allowed value.")) self.init_mem.setValue(self.max_mem_size.value() / 10) - def _run_in_thread(self, func, *args): - t_monitor = thread_monitor.ThreadMonitor() - thread = threading.Thread(target=func, args=(t_monitor, *args,)) - thread.daemon = True - thread.start() - - while not t_monitor.is_finished(): - self.qapp.processEvents() - time.sleep(0.1) - - if not t_monitor.success: - QtGui.QMessageBox.warning(self, - self.tr("Error!"), - self.tr("ERROR: {}").format( - t_monitor.error_msg)) - return False - return True - - def _rename_vm(self, t_monitor, name, dependencies): - try: - new_vm = self.vm.app.clone_vm(self.vm, name) - - failed_props = [] - - for (holder, prop) in dependencies: - try: - if holder is None: - setattr(self.vm.app, prop, new_vm) - else: - setattr(holder, prop, new_vm) - except qubesadmin.exc.QubesException as qex: - failed_props += [(holder, prop)] - - if not failed_props: - del self.vm.app.domains[self.vm.name] - else: - list_text = utils.format_dependencies_list(failed_props) - - QtGui.QMessageBox.warning( - self, - self.tr("Warning: rename partially unsuccessful"), - self.tr("Some properties could not be changed to the new " - "name. The system has now both {} and {} qubes. " - "To resolve this, please check and change the " - "following properties and remove the qube {} " - "manually.
").format( - self.vm.name, name, self.vm.name) + list_text) - - except qubesadmin.exc.QubesException as qex: - t_monitor.set_error_msg(str(qex)) - except Exception as ex: # pylint: disable=broad-except - t_monitor.set_error_msg(repr(ex)) - - t_monitor.set_finished() - def rename_vm(self): dependencies = admin_utils.vm_dependencies(self.vm.app, self.vm) @@ -544,19 +552,12 @@ class VMSettingsWindow(ui_settingsdlg.Ui_SettingsDialog, QtGui.QDialog): self.tr('New name: (WARNING: all other changes will be discarded)')) if ok: - if self._run_in_thread(self._rename_vm, new_vm_name, dependencies): - self.done(0) - - def _remove_vm(self, t_monitor): - try: - del self.vm.app.domains[self.vm.name] - - except qubesadmin.exc.QubesException as qex: - t_monitor.set_error_msg(str(qex)) - except Exception as ex: # pylint: disable=broad-except - t_monitor.set_error_msg(repr(ex)) - - t_monitor.set_finished() + thread = RenameVMThread(self.vm, new_vm_name, dependencies) + self.threads_list.append(thread) + thread.finished.connect(self.clear_threads) + thread.start() + thread.wait() + #self.done(0) def remove_vm(self): @@ -584,7 +585,8 @@ class VMSettingsWindow(ui_settingsdlg.Ui_SettingsDialog, QtGui.QDialog): 'qube\'s name below.')) if ok and answer == self.vm.name: - self._run_in_thread(self._remove_vm) + thread = common_threads.RemoveVMThread(self.vm) + thread.start() self.done(0) elif ok: @@ -612,11 +614,10 @@ class VMSettingsWindow(ui_settingsdlg.Ui_SettingsDialog, QtGui.QDialog): self.tr('Name for the cloned qube:')) if ok: - self._run_in_thread(self._clone_vm, cloned_vm_name) - QtGui.QMessageBox.warning( - self, - self.tr("Success"), - self.tr("The qube was cloned successfully.")) + thread = common_threads.CloneVMThread(self.vm, cloned_vm_name) + thread.finished.connect(self.clear_threads) + self.threads_list.append(thread) + thread.start() ######### advanced tab @@ -944,43 +945,19 @@ class VMSettingsWindow(ui_settingsdlg.Ui_SettingsDialog, QtGui.QDialog): ######## applications tab - def refresh_apps_in_vm(self, t_monitor): - try: - target_vm = self.vm.template - except AttributeError: - target_vm = self.vm - - if not target_vm.is_running(): - not_running = True - target_vm.start() - else: - not_running = False - - subprocess.check_call(['qvm-sync-appmenus', target_vm.name]) - - if not_running: - target_vm.shutdown() - - t_monitor.set_finished() - def refresh_apps_button_pressed(self): self.refresh_apps_button.setEnabled(False) self.refresh_apps_button.setText(self.tr('Refresh in progress...')) - t_monitor = thread_monitor.ThreadMonitor() - thread = threading.Thread( - target=self.refresh_apps_in_vm, - args=(t_monitor,)) - thread.daemon = True + thread = RefreshAppsVMThread(self.vm) + thread.finished.connect(self.clear_threads) + thread.finished.connect(self.refresh_finished) + self.threads_list.append(thread) thread.start() - while not t_monitor.is_finished(): - self.qapp.processEvents() - time.sleep(0.1) - + def refresh_finished(self): self.app_list_manager = AppmenuSelectManager(self.vm, self.app_list) - self.refresh_apps_button.setEnabled(True) self.refresh_apps_button.setText(self.tr('Refresh Applications')) diff --git a/qubesmanager/thread_monitor.py b/qubesmanager/thread_monitor.py deleted file mode 100644 index 8515aff..0000000 --- a/qubesmanager/thread_monitor.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/python2 -# -# The Qubes OS Project, http://www.qubes-os.org -# -# Copyright (C) 2011 Marek Marczykowski -# -# 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 Lesser General Public License along -# with this program; if not, see . -# -# - - -import PyQt4.QtCore # pylint: disable=import-error - -import threading - -class ThreadMonitor(PyQt4.QtCore.QObject): - def __init__(self): - self.success = True - self.error_msg = None - self.event_finished = threading.Event() - - def set_error_msg(self, error_msg): - self.success = False - self.error_msg = error_msg - self.set_finished() - - def is_finished(self): - return self.event_finished.is_set() - - def set_finished(self): - self.event_finished.set()