Skip to content

Commit

Permalink
Catch installation errors and display them.
Browse files Browse the repository at this point in the history
Fixes #193
  • Loading branch information
almet committed Oct 17, 2024
1 parent 03b3c9e commit a95b612
Show file tree
Hide file tree
Showing 9 changed files with 667 additions and 366 deletions.
109 changes: 63 additions & 46 deletions dangerzone/gui/main_window.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import logging
import os
import platform
import subprocess
import tempfile
import typing
from multiprocessing.pool import ThreadPool
Expand All @@ -20,15 +19,19 @@
from PySide6.QtWidgets import QTextEdit
except ImportError:
from PySide2 import QtCore, QtGui, QtSvg, QtWidgets
from PySide2.QtWidgets import QAction, QTextEdit
from PySide2.QtCore import Qt
from PySide2.QtWidgets import QAction, QTextEdit

from .. import errors
from ..document import SAFE_EXTENSION, Document
from ..isolation_provider.container import Container, NoContainerTechException
from ..isolation_provider.container import (
Container,
NoContainerTechException,
NotAvailableContainerTechException,
)
from ..isolation_provider.dummy import Dummy
from ..isolation_provider.qubes import Qubes, is_qubes_native_conversion
from ..util import get_resource_path, get_subprocess_startupinfo, get_version
from ..util import format_exception, get_resource_path, get_version
from .logic import Alert, CollapsibleBox, DangerzoneGui, UpdateDialog
from .updater import UpdateReport

Expand Down Expand Up @@ -388,15 +391,24 @@ def closeEvent(self, e: QtGui.QCloseEvent) -> None:


class InstallContainerThread(QtCore.QThread):
finished = QtCore.Signal()
finished = QtCore.Signal(str)

def __init__(self, dangerzone: DangerzoneGui) -> None:
super(InstallContainerThread, self).__init__()
self.dangerzone = dangerzone

def run(self) -> None:
self.dangerzone.isolation_provider.install()
self.finished.emit()
error = None
try:
installed = self.dangerzone.isolation_provider.install()
except Exception as e:
log.error("Container installation problem")
error = format_exception(e)
else:
if not installed:
error = "The image cannot be found. This can be caused by a faulty container image."
finally:
self.finished.emit(error)


class WaitingWidget(QtWidgets.QWidget):
Expand All @@ -423,9 +435,10 @@ def __init__(self) -> None:
# Enable copying
self.setTextInteractionFlags(Qt.TextSelectableByMouse)

def set_content(self, error: str) -> None:
self.setPlainText(error)
self.setVisible(True)
def set_content(self, error: Optional[str] = None) -> None:
if error:
self.setPlainText(error)
self.setVisible(True)


class WaitingWidgetContainer(WaitingWidget):
Expand All @@ -438,7 +451,6 @@ class WaitingWidgetContainer(WaitingWidget):
#
# Linux states
# - "install_container"
finished = QtCore.Signal()

def __init__(self, dangerzone: DangerzoneGui) -> None:
super(WaitingWidgetContainer, self).__init__()
Expand Down Expand Up @@ -480,77 +492,82 @@ def check_state(self) -> None:
error: Optional[str] = None

try:
if isinstance( # Sanity check
self.dangerzone.isolation_provider, Container
):
container_runtime = self.dangerzone.isolation_provider.get_runtime()
runtime_name = self.dangerzone.isolation_provider.get_runtime_name()
self.dangerzone.isolation_provider.is_runtime_available()
except NoContainerTechException as e:
log.error(str(e))
state = "not_installed"

except NotAvailableContainerTechException as e:
log.error(str(e))
state = "not_running"
error = e.error
except Exception as e:
log.error(str(e))
state = "not_running"
error = format_exception(e)
else:
# Can we run `docker/podman image ls` without an error
with subprocess.Popen(
[container_runtime, "image", "ls"],
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
startupinfo=get_subprocess_startupinfo(),
) as p:
_, stderr = p.communicate()
if p.returncode != 0:
log.error(f"{runtime_name} is not running")
state = "not_running"
error = stderr.decode()
else:
# Always try installing the container
state = "install_container"
state = "install_container"

# Update the state
self.state_change(state, error)

def show_error(self, msg: str, details: Optional[str] = None) -> None:
self.label.setText(msg)
show_traceback = details is not None
if show_traceback:
self.traceback.set_content(details)
self.traceback.setVisible(show_traceback)
self.buttons.show()

def show_message(self, msg: str) -> None:
self.label.setText(msg)
self.traceback.setVisible(False)
self.buttons.hide()

def installation_finished(self, error: Optional[str] = None) -> None:
if error:
msg = (
"During installation of the dangerzone image, <br>"
"the following error occured:"
)
self.show_error(msg, error)
else:
self.finished.emit()

def state_change(self, state: str, error: Optional[str] = None) -> None:
if state == "not_installed":
if platform.system() == "Linux":
self.label.setText(
self.show_error(
"<strong>Dangerzone requires Podman</strong><br><br>"
"Install it and retry."
)
else:
self.label.setText(
self.show_error(
"<strong>Dangerzone requires Docker Desktop</strong><br><br>"
"<a href='https://www.docker.com/products/docker-desktop'>Download Docker Desktop</a>"
", install it, and open it."
)
self.buttons.show()

elif state == "not_running":
if platform.system() == "Linux":
# "not_running" here means that the `podman image ls` command failed.
message = (
"<strong>Dangerzone requires Podman</strong><br><br>"
"Podman is installed but cannot run properly. See errors below"
)
if error:
self.traceback.set_content(error)

self.label.setText(message)

else:
self.label.setText(
message = (
"<strong>Dangerzone requires Docker Desktop</strong><br><br>"
"Docker is installed but isn't running.<br><br>"
"Open Docker and make sure it's running in the background."
)
self.buttons.show()
self.show_error(message, error)
else:
self.label.setText(
self.show_message(
"Installing the Dangerzone container image.<br><br>"
"This might take a few minutes..."
)
self.buttons.hide()
self.traceback.setVisible(False)
self.install_container_t = InstallContainerThread(self.dangerzone)
self.install_container_t.finished.connect(self.finished)
self.install_container_t.finished.connect(self.installation_finished)
self.install_container_t.start()


Expand Down
4 changes: 4 additions & 0 deletions dangerzone/isolation_provider/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ def __init__(self) -> None:
else:
self.proc_stderr = subprocess.DEVNULL

@staticmethod
def is_runtime_available() -> bool:
return True

@abstractmethod
def install(self) -> bool:
pass
Expand Down
62 changes: 55 additions & 7 deletions dangerzone/isolation_provider/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,21 @@ def __init__(self, container_tech: str) -> None:
super().__init__(f"{container_tech} is not installed")


class NotAvailableContainerTechException(Exception):
def __init__(self, container_tech: str, error: str) -> None:
self.error = error
self.container_tech = container_tech
super().__init__(f"{container_tech} is not available")


class ImageNotPresentException(Exception):
pass


class ImageInstallationException(Exception):
pass


class Container(IsolationProvider):
# Name of the dangerzone container
CONTAINER_NAME = "dangerzone.rocks/dangerzone"
Expand Down Expand Up @@ -156,7 +171,7 @@ def install() -> bool:
startupinfo=get_subprocess_startupinfo(),
)

chunk_size = 10240
chunk_size = 4 << 20
compressed_container_path = get_resource_path("container.tar.gz")
with gzip.open(compressed_container_path) as f:
while True:
Expand All @@ -166,19 +181,42 @@ def install() -> bool:
p.stdin.write(chunk)
else:
break
p.communicate()
_, err = p.communicate()
if p.returncode < 0:
if err:
error = err.decode()
else:
error = "No output"
raise ImageInstallationException(
f"Could not install container image: {error}"
)

if not Container.is_container_installed():
log.error("Failed to install the container image")
if not Container.is_container_installed(raise_on_error=True):
return False

log.info("Container image installed")
return True

@staticmethod
def is_container_installed() -> bool:
def is_runtime_available() -> bool:
container_runtime = Container.get_runtime()
runtime_name = Container.get_runtime_name()
# Can we run `docker/podman image ls` without an error
with subprocess.Popen(
[container_runtime, "image", "ls"],
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
startupinfo=get_subprocess_startupinfo(),
) as p:
_, stderr = p.communicate()
if p.returncode != 0:
raise NotAvailableContainerTechException(runtime_name, stderr.decode())
return True

@staticmethod
def is_container_installed(raise_on_error: bool = False) -> bool:
"""
See if the podman container is installed. Linux only.
See if the container is installed.
"""
# Get the image id
with open(get_resource_path("image-id.txt")) as f:
Expand All @@ -203,8 +241,18 @@ def is_container_installed() -> bool:
if found_image_id in expected_image_ids:
installed = True
elif found_image_id == "":
pass
if raise_on_error:
raise ImageNotPresentException(
"Image is not listed after installation. Bailing out."
)
else:
msg = (
f"{Container.CONTAINER_NAME} images found, but IDs do not match."
f"Found: {found_image_id}, Expected: {','.join(expected_image_ids)}"
)
if raise_on_error:
raise ImageNotPresentException(msg)
log.info(msg)
log.info("Deleting old dangerzone container image")

try:
Expand Down
11 changes: 11 additions & 0 deletions dangerzone/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import platform
import subprocess
import sys
import traceback
import unicodedata

import appdirs
Expand Down Expand Up @@ -117,3 +118,13 @@ def is_safe(chr: str) -> bool:
else:
sanitized_str += "�"
return sanitized_str


def format_exception(e: Exception) -> str:
# The signature of traceback.format_exception has changed in python 3.10
if sys.version_info < (3, 10):
output = traceback.format_exception(*sys.exc_info())
else:
output = traceback.format_exception(e)

return "".join(output)
Loading

0 comments on commit a95b612

Please sign in to comment.