Просмотр исходного кода

Merge remote-tracking branch 'origin/pr/132'

* origin/pr/132: (48 commits)
  Fixed unreachable code thanks to travis
  Clear searchbox pressing esc without need of selecting it
  Fix errors when some domain fails to start
  wait thread to finish when aborting
  Don't terminate backup thread when aborting
  Fix iterating and removing over list
  Use 'qube' instead 'Qube'
  Qube -> qube
  Catch possible KeyError when starting dispVM
  Fix authorship and python version
  Create backup_window on the stack
  Removed sigchld_handler old code
  Fix pylint
  Do not terminate the thread
  Fix error/success message on dialog
  Fix opening settings/boot dialog after VM creation
  Add pylint disable too-few-methods
  Fix travis errors
  Removed unused import
  Workaround for backup dialog modeless behaviour
  ...
Marek Marczykowski-Górecki 5 лет назад
Родитель
Сommit
334fefe559

+ 1 - 1
qubesmanager.pro

@@ -17,6 +17,7 @@ SOURCES = \
           qubesmanager/block.py \
           qubesmanager/clipboard.py \
           qubesmanager/create_new_vm.py \
+          qubesmanager/common_threads.py \
           qubesmanager/firewall.py \
           qubesmanager/global_settings.py \
           qubesmanager/log_dialog.py \
@@ -27,7 +28,6 @@ SOURCES = \
           qubesmanager/restore.py \
           qubesmanager/settings.py \
           qubesmanager/table_widgets.py \
-          qubesmanager/thread_monitor.py \
           qubesmanager/ui_about.py \
           qubesmanager/ui_backupdlg.py \
           qubesmanager/ui_globalsettingsdlg.py \

+ 12 - 16
qubesmanager/about.py

@@ -20,7 +20,6 @@
 # with this program; if not, see <http://www.gnu.org/licenses/>.
 #
 #
-from PyQt4.QtCore import SIGNAL, SLOT  # pylint: disable=import-error
 from PyQt4.QtGui import QDialog, QIcon  # pylint: disable=import-error
 from qubesmanager.releasenotes import ReleaseNotesDialog
 from qubesmanager.informationnotes import InformationNotesDialog
@@ -28,6 +27,7 @@ from qubesmanager.informationnotes import InformationNotesDialog
 from . import ui_about  # pylint: disable=no-name-in-module
 
 
+# pylint: disable=too-few-public-methods
 class AboutDialog(ui_about.Ui_AboutDialog, QDialog):
     def __init__(self):
         super(AboutDialog, self).__init__()
@@ -38,18 +38,14 @@ class AboutDialog(ui_about.Ui_AboutDialog, QDialog):
         with open('/etc/qubes-release', 'r') as release_file:
             self.release.setText(release_file.read())
 
-        self.connect(self.ok, SIGNAL("clicked()"), SLOT("accept()"))
-        self.connect(self.releaseNotes, SIGNAL("clicked()"),
-                     self.on_release_notes_clicked)
-        self.connect(self.informationNotes, SIGNAL("clicked()"),
-                     self.on_information_notes_clicked)
-
-    def on_release_notes_clicked(self):
-        release_notes_dialog = ReleaseNotesDialog()
-        release_notes_dialog.exec_()
-        self.accept()
-
-    def on_information_notes_clicked(self):
-        information_notes_dialog = InformationNotesDialog()
-        information_notes_dialog.exec_()
-        self.accept()
+        self.ok.clicked.connect(self.accept)
+        self.releaseNotes.clicked.connect(on_release_notes_clicked)
+        self.informationNotes.clicked.connect(on_information_notes_clicked)
+
+def on_release_notes_clicked():
+    release_notes_dialog = ReleaseNotesDialog()
+    release_notes_dialog.exec_()
+
+def on_information_notes_clicked():
+    information_notes_dialog = InformationNotesDialog()
+    information_notes_dialog.exec_()

+ 88 - 69
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 time
+import asyncio
+from contextlib import suppress
 
+# pylint: disable=too-few-public-methods
+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):
+class BackupVMsWindow(ui_backupdlg.Ui_Backup, multiselectwidget.QtGui.QWizard):
+    def __init__(self, qt_app, qubes_app, dispatcher, parent=None):
         super(BackupVMsWindow, self).__init__(parent)
 
         self.qt_app = qt_app
@@ -54,8 +76,7 @@ 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.thread = None
 
         self.setupUi(self)
 
@@ -112,6 +133,16 @@ 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):
+        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)
-
-            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."))
+
+            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 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.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)
+            self.thread.wait()
+            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 <b>line %d</b> of file <b>%s</b>.<br/><br/>"
         % (line, filename))
 
+def loop_shutdown():
+    pending = asyncio.Task.all_tasks()
+    for task in pending:
+        with suppress(asyncio.CancelledError):
+            task.cancel()
 
-def main():
 
+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__":

+ 54 - 0
qubesmanager/common_threads.py

@@ -0,0 +1,54 @@
+#!/usr/bin/python3
+#
+# The Qubes OS Project, http://www.qubes-os.org
+#
+# Copyright (C) 2018  Donoban <donoban@riseup.net>
+#
+# 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 <http://www.gnu.org/licenses/>.
+#
+#
+
+
+from PyQt4 import QtCore  # pylint: disable=import-error
+from qubesadmin import exc
+
+
+# pylint: disable=too-few-public-methods
+class RemoveVMThread(QtCore.QThread):
+    def __init__(self, vm):
+        QtCore.QThread.__init__(self)
+        self.vm = vm
+        self.msg = None
+
+    def run(self):
+        try:
+            del self.vm.app.domains[self.vm.name]
+        except (exc.QubesException, KeyError) as ex:
+            self.msg = ("Error removing qube!", str(ex))
+
+
+# pylint: disable=too-few-public-methods
+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
+        self.msg = None
+
+    def run(self):
+        try:
+            self.src_vm.app.clone_vm(self.src_vm, self.dst_name)
+            self.msg = ("Sucess", "The qube was cloned sucessfully.")
+        except exc.QubesException as ex:
+            self.msg = ("Error while cloning qube!", str(ex))

+ 53 - 49
qubesmanager/create_new_vm.py

@@ -22,8 +22,6 @@
 #
 
 import sys
-import threading
-import time
 import subprocess
 
 from PyQt4 import QtCore, QtGui  # pylint: disable=import-error
@@ -35,7 +33,40 @@ import qubesadmin.exc
 from . import utils
 
 from .ui_newappvmdlg import Ui_NewVMDlg  # pylint: disable=import-error
-from .thread_monitor import ThreadMonitor
+
+# pylint: disable=too-few-public-methods
+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.msg = 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.msg = str(qex)
+        except Exception as ex:  # pylint: disable=broad-except
+            self.msg = repr(ex)
 
 
 class NewVmDlg(QtGui.QDialog, Ui_NewVMDlg):
@@ -46,6 +77,9 @@ class NewVmDlg(QtGui.QDialog, Ui_NewVMDlg):
         self.qtapp = qtapp
         self.app = app
 
+        self.thread = None
+        self.progress = None
+
         # Theoretically we should be locking for writing here and unlock
         # only after the VM creation finished. But the code would be
         # more messy...
@@ -125,67 +159,37 @@ 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 <b>{}</b>...").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.msg:
             QtGui.QMessageBox.warning(None,
                 self.tr("Error creating the qube!"),
-                self.tr("ERROR: {}").format(thread_monitor.error_msg))
+                self.tr("ERROR: {}").format(self.thread.msg))
 
         self.done(0)
 
-        if thread_monitor.success:
+        if not self.thread.msg:
             if self.launch_settings.isChecked():
-                subprocess.check_call(['qubes-vm-settings', name])
+                subprocess.check_call(['qubes-vm-settings',
+                    str(self.name.text())])
             if self.install_system.isChecked():
                 subprocess.check_call(
-                    ['qubes-vm-boot-from-device', name])
+                    ['qubes-vm-boot-from-device', str(self.name.text())])
 
-    @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)

+ 141 - 223
qubesmanager/qube_manager.py

@@ -25,13 +25,12 @@ import sys
 import os
 import os.path
 import subprocess
-import time
 from datetime import datetime, timedelta
 import traceback
-import threading
+from contextlib import suppress
+
 import quamash
 import asyncio
-from contextlib import suppress
 
 from qubesadmin import Qubes
 from qubesadmin import exc
@@ -44,14 +43,15 @@ from PyQt4 import QtCore  # pylint: disable=import-error
 from qubesmanager.about import AboutDialog
 
 from . import ui_qubemanager  # pylint: disable=no-name-in-module
-from . import thread_monitor
 from . import table_widgets
 from . import settings
 from . import global_settings
 from . import restore
 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):
@@ -209,7 +209,7 @@ class VmShutdownMonitor(QtCore.QObject):
                     self.tr(
                         "The Qube <b>'{0}'</b> hasn't shutdown within the last "
                         "{1} seconds, do you want to kill it?<br>").format(
-                        vm.name, self.shutdown_time / 1000),
+                            vm.name, self.shutdown_time / 1000),
                     self.tr("Kill it!"),
                     self.tr("Wait another {0} seconds...").format(
                         self.shutdown_time / 1000))
@@ -238,6 +238,56 @@ class VmShutdownMonitor(QtCore.QObject):
             self.restart_vm_if_needed()
 
 
+# pylint: disable=too-few-public-methods
+class StartVMThread(QtCore.QThread):
+    def __init__(self, vm):
+        QtCore.QThread.__init__(self)
+        self.vm = vm
+        self.msg = None
+
+    def run(self):
+        try:
+            self.vm.start()
+        except exc.QubesException as ex:
+            self.msg = ("Error starting Qube!", str(ex))
+
+
+# pylint: disable=too-few-public-methods
+class UpdateVMThread(QtCore.QThread):
+    def __init__(self, vm):
+        QtCore.QThread.__init__(self)
+        self.vm = vm
+        self.msg = None
+
+    def run(self):
+        try:
+            if self.vm.qid == 0:
+                subprocess.check_call(
+                    ["/usr/bin/qubes-dom0-update", "--clean", "--gui"])
+            else:
+                if not self.vm.is_running():
+                    self.vm.start()
+                self.vm.run_service("qubes.InstallUpdatesGUI",\
+                        user="root", wait=False)
+        except (ChildProcessError, exc.QubesException) as ex:
+            self.msg = ("Error on qube update!", str(ex))
+
+
+# pylint: disable=too-few-public-methods
+class RunCommandThread(QtCore.QThread):
+    def __init__(self, vm, command_to_run):
+        QtCore.QThread.__init__(self)
+        self.vm = vm
+        self.command_to_run = command_to_run
+        self.msg = None
+
+    def run(self):
+        try:
+            self.vm.run(self.command_to_run)
+        except (ChildProcessError, exc.QubesException) as ex:
+            self.msg = ("Error while running command!", str(ex))
+
+
 class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow):
     # pylint: disable=too-many-instance-attributes
     row_height = 30
@@ -256,7 +306,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
@@ -398,7 +448,11 @@ 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)
@@ -410,12 +464,40 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow):
         dispatcher.add_handler('property-load',
                                self.on_domain_changed)
 
+        # It needs to store threads until they finish
+        self.threads_list = []
+        self.progress = None
+
         # Check Updates Timer
         timer = QtCore.QTimer(self)
         timer.timeout.connect(self.check_updates)
         timer.start(1000 * 30) # 30s
         self.check_updates()
 
+    def keyPressEvent(self, event):  # pylint: disable=invalid-name
+        if event.key() == QtCore.Qt.Key_Escape:
+            self.searchbox.clear()
+        super(VmManagerWindow, self).keyPressEvent(event)
+
+    def clear_threads(self):
+        for thread in self.threads_list:
+            if thread.isFinished():
+                if self.progress:
+                    self.progress.hide()
+                    self.progress = None
+
+                if thread.msg:
+                    (title, msg) = thread.msg
+                    QtGui.QMessageBox.warning(
+                        None,
+                        self.tr(title),
+                        self.tr(msg))
+
+                self.threads_list.remove(thread)
+                return
+
+        raise RuntimeError('No finished thread found')
+
     def closeEvent(self, event):
         # pylint: disable=invalid-name
         # save window size at close
@@ -433,21 +515,19 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow):
                     pass
 
     def on_domain_added(self, _submitter, _event, vm, **_kwargs):
+        row_no = 0
         self.table.setSortingEnabled(False)
-
-        row_no = self.table.rowCount()
-        self.table.setRowCount(row_no + 1)
-
-        for domain in self.qubes_app.domains:
-            if domain == vm:
-                vm_row = VmRowInTable(domain, row_no, self.table)
-                self.vms_in_table[domain.qid] = vm_row
-                self.table.setSortingEnabled(True)
-                self.showhide_vms()
-                return
-
-        # Never should reach here
-        raise RuntimeError('Added domain not found')
+        try:
+            domain = self.qubes_app.domains[vm]
+            row_no = self.table.rowCount()
+            self.table.setRowCount(row_no + 1)
+            vm_row = VmRowInTable(domain, row_no, self.table)
+            self.vms_in_table[domain.qid] = vm_row
+        except (exc.QubesException, KeyError):
+            if row_no != 0:
+                self.table.removeRow(row_no)
+        self.table.setSortingEnabled(True)
+        self.showhide_vms()
 
     def on_domain_removed(self, _submitter, _event, **kwargs):
         row_to_delete = None
@@ -520,17 +600,6 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow):
         return [vm for vm in self.qubes_app.domains]
 
     def fill_table(self):
-        progress = QtGui.QProgressDialog(
-            self.tr(
-                "Loading Qube Manager..."), "", 0, 0)
-        progress.setWindowTitle(self.tr("Qube Manager"))
-        progress.setWindowFlags(QtCore.Qt.Window |
-                                QtCore.Qt.WindowTitleHint |
-                                QtCore.Qt.CustomizeWindowHint)
-        progress.setCancelButton(None)
-        progress.setModal(True)
-        progress.show()
-
         self.table.setSortingEnabled(False)
         vms_list = self.get_vms_list()
 
@@ -538,19 +607,26 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow):
 
         self.table.setRowCount(len(vms_list))
 
+        progress = QtGui.QProgressDialog(
+            self.tr(
+                "Loading Qube Manager..."), "", 0, len(vms_list))
+        progress.setWindowTitle(self.tr("Qube Manager"))
+        progress.setMinimumDuration(1000)
+        progress.setCancelButton(None)
+
         row_no = 0
         for vm in vms_list:
+            progress.setValue(row_no)
             vm_row = VmRowInTable(vm, row_no, self.table)
             vms_in_table[vm.qid] = vm_row
             row_no += 1
-            self.qt_app.processEvents()
+
+        progress.setValue(row_no)
 
         self.vms_list = vms_list
         self.vms_in_table = vms_in_table
         self.table.setSortingEnabled(True)
 
-        progress.hide()
-
     def showhide_vms(self):
         if not self.search:
             for row_no in range(self.table.rowCount()):
@@ -584,11 +660,9 @@ 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:
-
             #  TODO: add boot from device to menu and add windows tools there
             # Update available actions:
             self.action_settings.setEnabled(vm.klass != 'AdminVM')
@@ -640,7 +714,8 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow):
     # noinspection PyArgumentList
     @QtCore.pyqtSlot(name='on_action_createvm_triggered')
     def action_createvm_triggered(self):  # pylint: disable=no-self-use
-        subprocess.check_call('qubes-vm-create')
+        create_window = create_new_vm.NewVmDlg(self.qt_app, self.qubes_app)
+        create_window.exec_()
 
     def get_selected_vm(self):
         # vm selection relies on the VmInfo widget's value used
@@ -681,7 +756,6 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow):
                         "or setting that uses it.</small>").format(list_text))
             info_dialog.setModal(False)
             info_dialog.show()
-            self.qt_app.processEvents()
 
             return
 
@@ -708,44 +782,11 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow):
 
         else:
             # remove the VM
-            t_monitor = thread_monitor.ThreadMonitor()
-            thread = threading.Thread(target=self.do_remove_vm,
-                                      args=(vm, self.qubes_app, t_monitor))
-            thread.daemon = True
+            thread = common_threads.RemoveVMThread(vm)
+            self.threads_list.append(thread)
+            thread.finished.connect(self.clear_threads)
             thread.start()
 
-            progress = QtGui.QProgressDialog(
-                self.tr(
-                    "Removing Qube: <b>{0}</b>...").format(vm.name), "", 0, 0)
-            progress.setWindowFlags(QtCore.Qt.Window |
-                                    QtCore.Qt.WindowTitleHint |
-                                    QtCore.Qt.CustomizeWindowHint)
-            progress.setCancelButton(None)
-            progress.setModal(True)
-            progress.show()
-
-            while not t_monitor.is_finished():
-                self.qt_app.processEvents()
-                time.sleep(0.1)
-
-            progress.hide()
-
-            if t_monitor.success:
-                pass
-            else:
-                QtGui.QMessageBox.warning(None, self.tr("Error removing Qube!"),
-                                          self.tr("ERROR: {0}").format(
-                                              t_monitor.error_msg))
-
-    @staticmethod
-    def do_remove_vm(vm, qubes_app, t_monitor):
-        try:
-            del qubes_app.domains[vm.name]
-        except exc.QubesException as ex:
-            t_monitor.set_error_msg(str(ex))
-
-        t_monitor.set_finished()
-
     # noinspection PyArgumentList
     @QtCore.pyqtSlot(name='on_action_clonevm_triggered')
     def action_clonevm_triggered(self):
@@ -763,48 +804,18 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow):
         if not ok or clone_name == "":
             return
 
-        t_monitor = thread_monitor.ThreadMonitor()
-        thread = threading.Thread(target=self.do_clone_vm,
-                                  args=(vm, self.qubes_app,
-                                        clone_name, t_monitor))
-        thread.daemon = True
+        self.progress = QtGui.QProgressDialog(
+            self.tr(
+                "Cloning Qube..."), "", 0, 0)
+        self.progress.setCancelButton(None)
+        self.progress.setModal(True)
+        self.progress.show()
+
+        thread = common_threads.CloneVMThread(vm, clone_name)
+        thread.finished.connect(self.clear_threads)
+        self.threads_list.append(thread)
         thread.start()
 
-        progress = QtGui.QProgressDialog(
-            self.tr("Cloning Qube <b>{0}</b> to <b>{1}</b>...").format(
-                vm.name, clone_name), "", 0, 0)
-        progress.setWindowFlags(QtCore.Qt.Window |
-                                QtCore.Qt.WindowTitleHint |
-                                QtCore.Qt.CustomizeWindowHint)
-        progress.setCancelButton(None)
-        progress.setModal(True)
-        progress.show()
-
-        while not t_monitor.is_finished():
-            self.qt_app.processEvents()
-            time.sleep(0.2)
-
-        progress.hide()
-
-        if not t_monitor.success:
-            QtGui.QMessageBox.warning(
-                None,
-                self.tr("Error while cloning Qube"),
-                self.tr("Exception while cloning:<br>{0}").format(
-                    t_monitor.error_msg))
-
-
-    @staticmethod
-    def do_clone_vm(src_vm, qubes_app, dst_name, t_monitor):
-        dst_vm = None
-        try:
-            dst_vm = qubes_app.clone_vm(src_vm, dst_name)
-        except exc.QubesException as ex:
-            t_monitor.set_error_msg(str(ex))
-            if dst_vm:
-                pass
-        t_monitor.set_finished()
-
     # noinspection PyArgumentList
     @QtCore.pyqtSlot(name='on_action_resumevm_triggered')
     def action_resumevm_triggered(self):
@@ -813,8 +824,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!"),
@@ -826,33 +835,11 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow):
     def start_vm(self, vm):
         if vm.is_running():
             return
-        t_monitor = thread_monitor.ThreadMonitor()
-        thread = threading.Thread(target=self.do_start_vm,
-                                  args=(vm, t_monitor))
-        thread.daemon = True
-        thread.start()
-
-        while not t_monitor.is_finished():
-            self.qt_app.processEvents()
-            time.sleep(0.1)
-
-        if not t_monitor.success:
-            QtGui.QMessageBox.warning(
-                None,
-                self.tr("Error starting Qube!"),
-                self.tr("ERROR: {0}").format(t_monitor.error_msg))
-
-
-    @staticmethod
-    def do_start_vm(vm, t_monitor):
-        try:
-            vm.start()
-        except exc.QubesException as ex:
-            t_monitor.set_error_msg(str(ex))
-            t_monitor.set_finished()
-            return
 
-        t_monitor.set_finished()
+        thread = StartVMThread(vm)
+        self.threads_list.append(thread)
+        thread.finished.connect(self.clear_threads)
+        thread.start()
 
     # noinspection PyArgumentList
     @QtCore.pyqtSlot(name='on_action_startvm_tools_install_triggered')
@@ -866,8 +853,6 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow):
         vm = self.get_selected_vm()
         try:
             vm.pause()
-            self.vms_in_table[vm.qid].update()
-            self.table_selection_changed()
         except exc.QubesException as ex:
             QtGui.QMessageBox.warning(
                 None,
@@ -885,9 +870,7 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow):
             self.tr("Are you sure you want to power down the Qube"
                     " <b>'{0}'</b>?<br><small>This will shutdown all the "
                     "running applications within this Qube.</small>").format(
-                vm.name), QtGui.QMessageBox.Yes | QtGui.QMessageBox.Cancel)
-
-        self.qt_app.processEvents()
+                     vm.name), QtGui.QMessageBox.Yes | QtGui.QMessageBox.Cancel)
 
         if reply == QtGui.QMessageBox.Yes:
             self.shutdown_vm(vm)
@@ -922,8 +905,6 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow):
                     "applications within this Qube.</small>").format(vm.name),
             QtGui.QMessageBox.Yes | QtGui.QMessageBox.Cancel)
 
-        self.qt_app.processEvents()
-
         if reply == QtGui.QMessageBox.Yes:
             # in case the user shut down the VM in the meantime
             if vm.is_running():
@@ -938,7 +919,7 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow):
         if not (vm.is_running() or vm.is_paused()):
             info = self.tr("Qube <b>'{0}'</b> is not running. Are you "
                            "absolutely sure you want to try to kill it?<br>"
-                            "<small>This will end <b>(not shutdown!)</b> all "
+                           "<small>This will end <b>(not shutdown!)</b> all "
                            "the running applications within this "
                            "Qube.</small>").format(vm.name)
         else:
@@ -952,8 +933,6 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow):
             QtGui.QMessageBox.Yes | QtGui.QMessageBox.Cancel,
             QtGui.QMessageBox.Cancel)
 
-        self.qt_app.processEvents()
-
         if reply == QtGui.QMessageBox.Yes:
             try:
                 vm.kill()
@@ -1017,55 +996,11 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow):
             if reply != QtGui.QMessageBox.Yes:
                 return
 
-        self.qt_app.processEvents()
-
-        t_monitor = thread_monitor.ThreadMonitor()
-        thread = threading.Thread(target=self.do_update_vm,
-                                  args=(vm, t_monitor))
-        thread.daemon = True
+        thread = UpdateVMThread(vm)
+        self.threads_list.append(thread)
+        thread.finished.connect(self.clear_threads)
         thread.start()
 
-        progress = QtGui.QProgressDialog(
-                self.tr(
-                    "<b>{0}</b><br>Please wait for the updater to "
-                    "launch...").format(vm.name), "", 0, 0)
-        progress.setWindowFlags(QtCore.Qt.Window |
-                                QtCore.Qt.WindowTitleHint |
-                                QtCore.Qt.CustomizeWindowHint)
-        progress.setCancelButton(None)
-        progress.setModal(True)
-        progress.show()
-
-        while not t_monitor.is_finished():
-            self.qt_app.processEvents()
-            time.sleep(0.2)
-
-        progress.hide()
-
-        if vm.qid != 0:
-            if not t_monitor.success:
-                QtGui.QMessageBox.warning(
-                    None,
-                    self.tr("Error on Qube update!"),
-                    self.tr("ERROR: {0}").format(t_monitor.error_msg))
-
-
-    @staticmethod
-    def do_update_vm(vm, t_monitor):
-        try:
-            if vm.qid == 0:
-                subprocess.check_call(
-                    ["/usr/bin/qubes-dom0-update", "--clean", "--gui"])
-            else:
-                if not vm.is_running():
-                    vm.start()
-                vm.run_service("qubes.InstallUpdatesGUI",
-                               user="root", wait=False)
-        except (ChildProcessError, exc.QubesException) as ex:
-            t_monitor.set_error_msg(str(ex))
-            t_monitor.set_finished()
-            return
-        t_monitor.set_finished()
 
     # noinspection PyArgumentList
     @QtCore.pyqtSlot(name='on_action_run_command_in_vm_triggered')
@@ -1078,29 +1013,11 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow):
             self.tr('Run command in <b>{}</b>:').format(vm.name))
         if not ok or command_to_run == "":
             return
-        t_monitor = thread_monitor.ThreadMonitor()
-        thread = threading.Thread(target=self.do_run_command_in_vm, args=(
-            vm, command_to_run, t_monitor))
-        thread.daemon = True
-        thread.start()
 
-        while not t_monitor.is_finished():
-            self.qt_app.processEvents()
-            time.sleep(0.2)
-
-        if not t_monitor.success:
-            QtGui.QMessageBox.warning(
-                None, self.tr("Error while running command"),
-                self.tr("Exception while running command:<br>{0}").format(
-                    t_monitor.error_msg))
-
-    @staticmethod
-    def do_run_command_in_vm(vm, command_to_run, t_monitor):
-        try:
-            vm.run(command_to_run)
-        except (ChildProcessError, exc.QubesException) as ex:
-            t_monitor.set_error_msg(str(ex))
-        t_monitor.set_finished()
+        thread = RunCommandThread(vm, command_to_run)
+        self.threads_list.append(thread)
+        thread.finished.connect(self.clear_threads)
+        thread.start()
 
     # noinspection PyArgumentList
     @QtCore.pyqtSlot(name='on_action_set_keyboard_layout_triggered')
@@ -1141,8 +1058,9 @@ 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.exec_()
+        backup_window = backup.BackupVMsWindow(self.qt_app, self.qubes_app,
+                                            self.dispatcher, self)
+        backup_window.show()
 
     # noinspection PyArgumentList
     @QtCore.pyqtSlot(name='on_action_exit_triggered')

+ 86 - 86
qubesmanager/restore.py

@@ -23,31 +23,56 @@
 import sys
 from PyQt4 import QtCore  # pylint: disable=import-error
 from PyQt4 import QtGui  # pylint: disable=import-error
-import threading
-import time
 import os
 import os.path
 import traceback
 import logging
 import logging.handlers
 
-import signal
-
 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 import Queue
 from multiprocessing.queues import Empty
 from qubesadmin import Qubes, exc
 from qubesadmin.backup import restore
 
 
-class RestoreVMsWindow(ui_restoredlg.Ui_Restore, QtGui.QWizard):
+# pylint: disable=too-few-public-methods
+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.msg = None
+        self.canceled = 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.msg = '\n'.join(err_msg)
+            self.msg = '<b><font color="red">{0}</font></b>'.format(
+                self.tr("Finished with errors!"))
+        else:
+            self.msg = '<font color="green">{0}</font>'.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)
 
@@ -57,6 +82,8 @@ class RestoreVMsWindow(ui_restoredlg.Ui_Restore, QtGui.QWizard):
         self.vms_to_restore = None
         self.func_output = []
 
+        self.thread = None
+
         # Set up logging
         self.feedback_queue = Queue()
         handler = logging.handlers.QueueHandler(self.feedback_queue)
@@ -64,9 +91,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
 
@@ -144,41 +168,12 @@ class RestoreVMsWindow(ui_restoredlg.Ui_Restore, QtGui.QWizard):
                 self.select_vms_widget.available_list.addItem(vmname)
         except exc.QubesException as ex:
             QtGui.QMessageBox.warning(None, self.tr("Restore error!"), str(ex))
+            self.restart()
 
     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('<b><font color="red">{0}</font></b>'.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('<b><font color="red">{0}</font></b>'.format(
-                self.tr("Finished with errors!")))
-        else:
-            self.append_output('<font color="green">{0}</font>'.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)
         if self.currentPage() is self.select_vms_page:
             self.__fill_vms_list__()
 
@@ -210,51 +205,56 @@ 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 = '<font color="red">{0}</font>'.format(
-                                log_record.getMessage())
-                        else:
-                            output = log_record.getMessage()
-                        self.append_output(output)
-                        log_record = self.feedback_queue.get_nowait()
-                except Empty:
-                    pass
-
-            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)
-
-            if self.showFileDialog.isChecked():
-                self.append_output(
-                    '<b><font color="black">{0}</font></b>'.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.button(self.FinishButton).setEnabled(True)
-            self.button(self.CancelButton).setEnabled(False)
-            self.showFileDialog.setEnabled(False)
-
-        signal.signal(signal.SIGCHLD, old_sigchld_handler)
+            self.thread = RestoreThread(self.backup_restore,
+                                        self.vms_to_restore)
+            self.thread.finished.connect(self.thread_finished)
+
+            # Start log timer
+            timer = QtCore.QTimer(self)
+            timer.timeout.connect(self.update_log)
+            timer.start(1000)
+
+            self.thread.start()
+
+    def thread_finished(self):
+        self.progress_bar.setMaximum(100)
+        self.progress_bar.setValue(100)
+
+        if self.thread.msg:
+            QtGui.QMessageBox.warning(
+                None,
+                self.tr("Restore qubes"),
+                self.tr(self.thread.msg))
+
+        if self.thread.msg:
+            self.append_output(self.thread.msg)
+
+        if self.showFileDialog.isChecked():
+            self.append_output(
+                '<b><font color="black">{0}</font></b>'.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)
+
+    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 = '<font color="red">{0}</font>'.format(
+                        log_record.getMessage())
+                else:
+                    output = log_record.getMessage()
+                self.append_output(output)
+                log_record = self.feedback_queue.get_nowait()
+        except Empty:
+            pass
+
 
     def all_vms_good(self):
         for vm_info in self.vms_to_restore.values():
@@ -265,7 +265,7 @@ 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('<font color="red">{0}</font>'.format(
                 self.tr("Aborting the operation...")))

+ 156 - 161
qubesmanager/settings.py

@@ -27,8 +27,6 @@ import os.path
 import os
 import re
 import subprocess
-import threading
-import time
 import traceback
 import sys
 from qubesadmin.tools import QubesArgumentParser
@@ -38,7 +36,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
@@ -47,16 +45,90 @@ from PyQt4 import QtCore, QtGui  # pylint: disable=import-error
 
 from . import ui_settingsdlg  # pylint: disable=no-name-in-module
 
+# pylint: disable=too-few-public-methods
+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
+        self.msg = None
+
+    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 qubesadmin.exc.QubesException:
+                    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.<br> ").format(
+                                self.vm.name, self.vm.name, self.vm.name)\
+                                        + list_text)
+
+        except qubesadmin.exc.QubesException as ex:
+            self.msg = ("Rename error!", str(ex))
+        except Exception as ex:  # pylint: disable=broad-except
+            self.msg = ("Rename error!", repr(ex))
+
+
+# pylint: disable=too-few-public-methods
+class RefreshAppsVMThread(QtCore.QThread):
+    def __init__(self, vm):
+        QtCore.QThread.__init__(self)
+        self.vm = vm
+        self.msg = None
+
+    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:  # pylint: disable=broad-except
+            self.msg = ("Refresh failed!", str(ex))
+
 
 # pylint: disable=too-many-instance-attributes
 class VMSettingsWindow(ui_settingsdlg.Ui_SettingsDialog, QtGui.QDialog):
     tabs_indices = collections.OrderedDict((
-            ('basic', 0),
-            ('advanced', 1),
-            ('firewall', 2),
-            ('devices', 3),
-            ('applications', 4),
-            ('services', 5),
+        ('basic', 0),
+        ('advanced', 1),
+        ('firewall', 2),
+        ('devices', 3),
+        ('applications', 4),
+        ('services', 5),
         ))
 
     def __init__(self, vm, qapp, init_page="basic", parent=None):
@@ -64,6 +136,9 @@ class VMSettingsWindow(ui_settingsdlg.Ui_SettingsDialog, QtGui.QDialog):
 
         self.vm = vm
         self.qapp = qapp
+        self.threads_list = []
+        self.progress = None
+        self.thread_closes = False
         try:
             self.source_vm = self.vm.template
         except AttributeError:
@@ -149,6 +224,29 @@ 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 self.progress:
+                    self.progress.hide()
+                    self.progress = None
+
+                if thread.msg:
+                    (title, msg) = thread.msg
+                    QtGui.QMessageBox.warning(
+                        None,
+                        self.tr(title),
+                        self.tr(msg))
+
+                self.threads_list.remove(thread)
+
+                if self.thread_closes:
+                    self.done(0)
+
+                return
+
+        raise RuntimeError('No finished thread found')
+
     def keyPressEvent(self, event):  # pylint: disable=invalid-name
         if event.key() == QtCore.Qt.Key_Enter \
                 or event.key() == QtCore.Qt.Key_Return:
@@ -163,31 +261,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()
-
-        progress = QtGui.QProgressDialog(
-            self.tr("Applying settings to <b>{0}</b>...").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)
+        error = self.__save_changes__()
 
-        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 while changing settings for {0}!"\
+                        ).format(self.vm.name),\
+                self.tr("ERROR: {0}").format('\n'.join(error)))
 
     def apply(self):
         self.save_changes()
@@ -196,9 +277,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:
@@ -236,12 +317,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
@@ -259,18 +336,17 @@ class VMSettingsWindow(ui_settingsdlg.Ui_SettingsDialog, QtGui.QDialog):
                         'please enable networking.')
             )
         if netvm is not None and \
-                not netvm.features.check_with_template(
-                    'qubes-firewall',
-                    False):
+                not netvm.features.check_with_template(\
+                    'qubes-firewall', False):
             QtGui.QMessageBox.warning(
                 self,
                 self.tr("Qube configuration problem!"),
-                self.tr("The '{vm}' qube is network connected to "
-                        "'{netvm}', which does not support firewall!<br/>"
-                        "You may edit the '{vm}' qube firewall rules, but "
-                        "these will not take any effect until you connect it "
-                        "to a working Firewall qube.").format(
-                    vm=self.vm.name, netvm=netvm.name))
+                self.tr("The '{vm}' qube is network connected to "\
+                        "'{netvm}', which does not support firewall!<br/>"\
+                        "You may edit the '{vm}' qube firewall rules, but "\
+                        "these will not take any effect until you connect it "\
+                        "to a working Firewall qube.").format(\
+                        vm=self.vm.name, netvm=netvm.name))
 
     def current_tab_changed(self, idx):
         if idx == self.tabs_indices["firewall"]:
@@ -463,61 +539,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.<br> ").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)
@@ -543,19 +564,19 @@ 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]
+            thread = RenameVMThread(self.vm, new_vm_name, dependencies)
+            self.threads_list.append(thread)
+            thread.finished.connect(self.clear_threads)
 
-        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))
+            self.progress = QtGui.QProgressDialog(
+                self.tr(
+                    "Renaming Qube..."), "", 0, 0)
+            self.progress.setCancelButton(None)
+            self.progress.setModal(True)
+            self.thread_closes = True
+            self.progress.show()
 
-        t_monitor.set_finished()
+            thread.start()
 
     def remove_vm(self):
 
@@ -583,7 +604,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:
@@ -592,17 +614,6 @@ class VMSettingsWindow(ui_settingsdlg.Ui_SettingsDialog, QtGui.QDialog):
                 self.tr("Removal cancelled"),
                 self.tr("The qube will not be removed."))
 
-    def _clone_vm(self, t_monitor, name):
-        try:
-            self.vm.app.clone_vm(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()
-
     def clone_vm(self):
 
         cloned_vm_name, ok = QtGui.QInputDialog.getText(
@@ -611,11 +622,19 @@ 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)
+
+            self.progress = QtGui.QProgressDialog(
+                self.tr(
+                    "Cloning Qube..."), "", 0, 0)
+            self.progress.setCancelButton(None)
+            self.progress.setModal(True)
+            self.thread_closes = True
+            self.progress.show()
+
+            thread.start()
 
     ######### advanced tab
 
@@ -766,8 +785,8 @@ class VMSettingsWindow(ui_settingsdlg.Ui_SettingsDialog, QtGui.QDialog):
         self.virt_mode.clear()
 
         # pylint: disable=attribute-defined-outside-init
-        self.virt_mode_list, self.virt_mode_idx = utils.prepare_choice(
-                self.virt_mode, self.vm, 'virt_mode', choices, None,
+        self.virt_mode_list, self.virt_mode_idx = utils.prepare_choice(\
+                self.virt_mode, self.vm, 'virt_mode', choices, None,\
                 allow_default=True, transform=(lambda x: str(x).upper()))
 
         if old_mode is not None:
@@ -958,43 +977,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'))
 

+ 0 - 42
qubesmanager/thread_monitor.py

@@ -1,42 +0,0 @@
-#!/usr/bin/python2
-#
-# The Qubes OS Project, http://www.qubes-os.org
-#
-# Copyright (C) 2011  Marek Marczykowski <marmarek@mimuw.edu.pl>
-#
-# 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 <http://www.gnu.org/licenses/>.
-#
-#
-
-
-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()

+ 1 - 1
rpm_spec/qmgr.spec.in

@@ -89,7 +89,7 @@ rm -rf $RPM_BUILD_ROOT
 %{python3_sitelib}/qubesmanager/releasenotes.py
 %{python3_sitelib}/qubesmanager/informationnotes.py
 %{python3_sitelib}/qubesmanager/create_new_vm.py
-%{python3_sitelib}/qubesmanager/thread_monitor.py
+%{python3_sitelib}/qubesmanager/common_threads.py
 %{python3_sitelib}/qubesmanager/qube_manager.py
 %{python3_sitelib}/qubesmanager/utils.py
 %{python3_sitelib}/qubesmanager/bootfromdevice.py