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