diff --git a/qubesmanager.pro b/qubesmanager.pro index 51f4d1c..f186c52 100644 --- a/qubesmanager.pro +++ b/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 \ diff --git a/qubesmanager/about.py b/qubesmanager/about.py index b12ae36..da54508 100644 --- a/qubesmanager/about.py +++ b/qubesmanager/about.py @@ -20,7 +20,6 @@ # with this program; if not, see . # # -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) + 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(self): - release_notes_dialog = ReleaseNotesDialog() - release_notes_dialog.exec_() - self.accept() +def on_release_notes_clicked(): + release_notes_dialog = ReleaseNotesDialog() + release_notes_dialog.exec_() - def on_information_notes_clicked(self): - information_notes_dialog = InformationNotesDialog() - information_notes_dialog.exec_() - self.accept() +def on_information_notes_clicked(): + information_notes_dialog = InformationNotesDialog() + information_notes_dialog.exec_() diff --git a/qubesmanager/backup.py b/qubesmanager/backup.py index d974842..358f1be 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 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): + 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) + 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.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 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..4f35539 --- /dev/null +++ b/qubesmanager/common_threads.py @@ -0,0 +1,54 @@ +#!/usr/bin/python3 +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2018 Donoban +# +# 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 + + +# 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)) diff --git a/qubesmanager/create_new_vm.py b/qubesmanager/create_new_vm.py index 6e7fa52..5949833 100644 --- a/qubesmanager/create_new_vm.py +++ b/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 {}...").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) diff --git a/qubesmanager/qube_manager.py b/qubesmanager/qube_manager.py index 7babde8..1693064 100644 --- a/qubesmanager/qube_manager.py +++ b/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 '{0}' hasn't shutdown within the last " "{1} seconds, do you want to kill it?
").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.").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: {0}...").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 {0} to {1}...").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:
{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,34 +835,12 @@ 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 = StartVMThread(vm) + self.threads_list.append(thread) + thread.finished.connect(self.clear_threads) 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() - # noinspection PyArgumentList @QtCore.pyqtSlot(name='on_action_startvm_tools_install_triggered') # TODO: replace with boot from device @@ -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" " '{0}'?
This will shutdown all the " "running applications within this Qube.").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.").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 '{0}' is not running. Are you " "absolutely sure you want to try to kill it?
" - "This will end (not shutdown!) all " + "This will end (not shutdown!) all " "the running applications within this " "Qube.").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( - "{0}
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,30 +1013,12 @@ class VmManagerWindow(ui_qubemanager.Ui_VmManagerWindow, QtGui.QMainWindow): self.tr('Run command in {}:').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 = RunCommandThread(vm, command_to_run) + self.threads_list.append(thread) + thread.finished.connect(self.clear_threads) 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:
{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() - # noinspection PyArgumentList @QtCore.pyqtSlot(name='on_action_set_keyboard_layout_triggered') def action_set_keyboard_layout_triggered(self): @@ -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') diff --git a/qubesmanager/restore.py b/qubesmanager/restore.py index 18e628c..0c1d07c 100644 --- a/qubesmanager/restore.py +++ b/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 = '{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) @@ -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('{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) 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 = '{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.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( + '{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) + + 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,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('{0}'.format( self.tr("Aborting the operation..."))) diff --git a/qubesmanager/settings.py b/qubesmanager/settings.py index 5391ad4..363c5f4 100755 --- a/qubesmanager/settings.py +++ b/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.
").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() + 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 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!
" - "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!
"\ + "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.
").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) + thread = RenameVMThread(self.vm, new_vm_name, dependencies) + self.threads_list.append(thread) + thread.finished.connect(self.clear_threads) - def _remove_vm(self, t_monitor): - try: - del self.vm.app.domains[self.vm.name] + 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() - 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.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')) 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() diff --git a/rpm_spec/qmgr.spec.in b/rpm_spec/qmgr.spec.in index 554eeb0..b00e42e 100644 --- a/rpm_spec/qmgr.spec.in +++ b/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