Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

app: move export calls to separate thread #563

Merged
merged 3 commits into from
Oct 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 39 additions & 6 deletions securedrop_client/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import os
import subprocess
import tarfile
import threading

from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt

from enum import Enum
from io import BytesIO
Expand Down Expand Up @@ -34,7 +37,7 @@ class ExportStatus(Enum):
UNEXPECTED_RETURN_STATUS = 'UNEXPECTED_RETURN_STATUS'


class Export:
class Export(QObject):
'''
This class sends files over to the Export VM so that they can be copied to a luks-encrypted USB
disk drive or printed by a USB-connected printer.
Expand Down Expand Up @@ -63,8 +66,21 @@ class Export:
DISK_ENCRYPTION_KEY_NAME = 'encryption_key'
DISK_EXPORT_DIR = 'export_data'

# Set up signals for communication with the GUI thread
preflight_check_call_failure = pyqtSignal(object)
preflight_check_call_success = pyqtSignal(str)
begin_usb_export = pyqtSignal(list, str)
begin_preflight_check = pyqtSignal()
export_usb_call_failure = pyqtSignal(object)
export_usb_call_success = pyqtSignal(list)

def __init__(self) -> None:
pass
super().__init__()

self.begin_preflight_check.connect(self.run_preflight_checks,
type=Qt.QueuedConnection)
self.begin_usb_export.connect(self.send_file_to_usb_device,
type=Qt.QueuedConnection)

def _export_archive(cls, archive_path: str) -> str:
'''
Expand Down Expand Up @@ -206,14 +222,24 @@ def _run_disk_export(self, archive_dir: str, filepaths: List[str], passphrase: s
if status:
raise ExportError(status)

@pyqtSlot()
def run_preflight_checks(self) -> None:
'''
Run preflight checks to verify that the usb device is connected and luks-encrypted.
'''
with TemporaryDirectory() as temp_dir:
self._run_usb_test(temp_dir)
self._run_disk_test(temp_dir)

try:
logger.debug('beginning preflight checks in thread {}'.format(
threading.current_thread().ident))
self._run_usb_test(temp_dir)
self._run_disk_test(temp_dir)
logger.debug('completed preflight checks: success')
self.preflight_check_call_success.emit('success')
except ExportError as e:
logger.debug('completed preflight checks: failure')
self.preflight_check_call_failure.emit(e.status)

@pyqtSlot(list, str)
def send_file_to_usb_device(self, filepaths: List[str], passphrase: str) -> None:
'''
Export the file to the luks-encrypted usb disk drive attached to the Export VM.
Expand All @@ -223,4 +249,11 @@ def send_file_to_usb_device(self, filepaths: List[str], passphrase: str) -> None
passphrase: The passphrase to unlock the luks-encrypted usb disk drive.
'''
with TemporaryDirectory() as temp_dir:
self._run_disk_export(temp_dir, filepaths, passphrase)
try:
logger.debug('beginning export from thread {}'.format(
threading.current_thread().ident))
self._run_disk_export(temp_dir, filepaths, passphrase)
logger.debug('Export successful')
self.export_usb_call_success.emit(filepaths)
except ExportError as e:
self.export_usb_call_failure.emit(e.status)
76 changes: 44 additions & 32 deletions securedrop_client/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

from securedrop_client.db import Source, Message, File, Reply, User
from securedrop_client.storage import source_exists
from securedrop_client.export import ExportStatus, ExportError
from securedrop_client.export import ExportStatus
from securedrop_client.gui import SecureQLabel, SvgLabel, SvgPushButton, SvgToggleButton
from securedrop_client.logic import Controller
from securedrop_client.resources import load_icon, load_image
Expand Down Expand Up @@ -1858,6 +1858,7 @@ def __init__(self, controller, file_uuid):
self.setObjectName('export_dialog')
self.setStyleSheet(self.CSS)
self.setWindowFlags(Qt.Popup)
self.setWindowModality(Qt.WindowModal)

layout = QVBoxLayout(self)
self.setLayout(layout)
Expand Down Expand Up @@ -1949,44 +1950,33 @@ def __init__(self, controller, file_uuid):
retry_export_button.clicked.connect(self._on_retry_export_button_clicked)
unlock_disk_button.clicked.connect(self._on_unlock_disk_clicked)

self.controller.export.preflight_check_call_failure.connect(
self._update_preflight, type=Qt.QueuedConnection)
self.controller.export.preflight_check_call_success.connect(
self._request_passphrase, type=Qt.QueuedConnection)
self.controller.export.export_usb_call_failure.connect(
self._update_usb_export, type=Qt.QueuedConnection)
self.controller.export.export_usb_call_success.connect(
self._on_export_success, type=Qt.QueuedConnection)

def export(self):
try:
self.controller.run_export_preflight_checks()
self._request_passphrase()
except ExportError as e:
# The first time we see a CALLED_PROCESS_ERROR, tell the user to insert the USB device
# in case the issue is that the Export VM cannot start due to a USB device being
# unavailable for attachment. According to the Qubes docs:
#
# "If the device is unavailable (physically missing or sourceVM not started), booting
# the targetVM fails."
#
# For information, see https://www.qubes-os.org/doc/device-handling
if e.status == ExportStatus.CALLED_PROCESS_ERROR.value:
self._request_to_insert_usb_device()
else:
self._update(e.status)
self.controller.run_export_preflight_checks()

@pyqtSlot()
def _on_retry_export_button_clicked(self):
try:
self.starting_export_message.hide()
self.controller.run_export_preflight_checks()
self._request_passphrase()
except ExportError as e:
self._update(e.status)
self.starting_export_message.hide()
self.controller.run_export_preflight_checks()

@pyqtSlot()
def _on_unlock_disk_clicked(self):
try:
self.passphrase_form.hide()
self.exporting_message.show()
QApplication.processEvents()
passphrase = self.passphrase_field.text()
self.controller.export_file_to_usb_drive(self.file_uuid, passphrase)
self.close()
except ExportError as e:
self._update(e.status)
self.passphrase_form.hide()
self.exporting_message.show()
passphrase = self.passphrase_field.text()
self.controller.export_file_to_usb_drive(self.file_uuid, passphrase)

@pyqtSlot()
def _on_export_success(self):
self.close()

def _request_to_insert_usb_device(self, encryption_not_supported: bool = False):
self.starting_export_message.hide()
Expand All @@ -1998,7 +1988,9 @@ def _request_to_insert_usb_device(self, encryption_not_supported: bool = False):
else:
self.usb_error_message.hide()

@pyqtSlot()
def _request_passphrase(self, bad_passphrase: bool = False):
logger.debug('requesting passphrase... ')
self.starting_export_message.hide()
self.exporting_message.hide()
self.passphrase_form.show()
Expand All @@ -2010,6 +2002,7 @@ def _request_passphrase(self, bad_passphrase: bool = False):
self.passphrase_error_message.hide()

def _update(self, status):
logger.debug('updating status... ')
if status == ExportStatus.USB_NOT_CONNECTED.value:
self._request_to_insert_usb_device()
elif status == ExportStatus.BAD_PASSPHRASE.value:
Expand All @@ -2023,6 +2016,25 @@ def _update(self, status):
self.passphrase_form.hide()
self.insert_usb_form.hide()

@pyqtSlot(object)
def _update_preflight(self, status):
# The first time we see a CALLED_PROCESS_ERROR, tell the user to insert the USB device
# in case the issue is that the Export VM cannot start due to a USB device being
# unavailable for attachment. According to the Qubes docs:
#
# "If the device is unavailable (physically missing or sourceVM not started), booting
# the targetVM fails."
#
# For information, see https://www.qubes-os.org/doc/device-handling
if status == ExportStatus.CALLED_PROCESS_ERROR.value:
redshiftzero marked this conversation as resolved.
Show resolved Hide resolved
self._request_to_insert_usb_device()
else:
self._update(status)

@pyqtSlot(object)
def _update_usb_export(self, status):
self._update(status)


class ConversationView(QWidget):
"""
Expand Down
22 changes: 16 additions & 6 deletions securedrop_client/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import logging
import os
import sdclientapi
import threading
import uuid
from typing import Dict, Tuple, Union, Any, Type # noqa: F401

Expand Down Expand Up @@ -223,6 +224,14 @@ def setup(self):
self.sync_update.timeout.connect(self.sync_api)
self.sync_update.start(1000 * 60 * 5) # every 5 minutes.

# Run export object in a separate thread context (a reference to the
# thread is kept on self such that it does not get garbage collected
# after this method returns) - we want to keep our export thread around for
# later processing.
self.export_thread = QThread()
self.export.moveToThread(self.export_thread)
self.export_thread.start()

def call_api(self,
api_call_func,
success_callback,
Expand Down Expand Up @@ -594,27 +603,28 @@ def on_file_open(self, file_uuid: str) -> None:

def run_export_preflight_checks(self):
'''
Run preflight checks to make sure the Export VM is configured correctly and
Run preflight checks to make sure the Export VM is configured correctly
'''
logger.debug('Running export preflight checks')
logger.debug('Calling export preflight checks from thread {}'.format(
threading.current_thread().ident))

if not self.qubes:
return

self.export.run_preflight_checks()
self.export.begin_preflight_check.emit()

def export_file_to_usb_drive(self, file_uuid: str, passphrase: str) -> None:
file = self.get_file(file_uuid)

logger.debug('Exporting {}'.format(file.original_filename))
logger.debug('Exporting {} from thread {}'.format(
file.original_filename, threading.current_thread().ident))

if not self.qubes:
return

filepath = os.path.join(self.data_dir, file.original_filename)
self.export.send_file_to_usb_device([filepath], passphrase)

logger.debug('Export successful')
self.export.begin_usb_export.emit([filepath], passphrase)

def on_submission_download(
self,
Expand Down
Loading