From c3c7fbbc2047221ccdec052d680f64c36f1e8bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20M=C3=A9taireau?= Date: Fri, 30 Aug 2024 17:12:28 +0200 Subject: [PATCH] Fix wrong container-runtime detection on Linux Use "podman" when on Linux, and "docker" otherwise. This commit also adds a text widget to the interface, showing the actual content fo the error that happened, to help debug further if needed. Fixes #212 --- dangerzone/gui/__init__.py | 2 +- dangerzone/gui/main_window.py | 86 ++++++++++++++++++---- dangerzone/isolation_provider/container.py | 2 +- share/dangerzone.css | 8 ++ 4 files changed, 81 insertions(+), 17 deletions(-) diff --git a/dangerzone/gui/__init__.py b/dangerzone/gui/__init__.py index 664fe1cfc..0f51759d4 100644 --- a/dangerzone/gui/__init__.py +++ b/dangerzone/gui/__init__.py @@ -5,7 +5,7 @@ import signal import sys import typing -from typing import Dict, List, Optional +from typing import List, Optional import click import colorama diff --git a/dangerzone/gui/main_window.py b/dangerzone/gui/main_window.py index 4c739d061..7d4e3c62f 100644 --- a/dangerzone/gui/main_window.py +++ b/dangerzone/gui/main_window.py @@ -10,14 +10,18 @@ # FIXME: See https://github.com/freedomofpress/dangerzone/issues/320 for more details. if typing.TYPE_CHECKING: from PySide2 import QtCore, QtGui, QtSvg, QtWidgets - from PySide2.QtWidgets import QAction + from PySide2.QtCore import Qt + from PySide2.QtWidgets import QAction, QTextEdit else: try: from PySide6 import QtCore, QtGui, QtSvg, QtWidgets + from PySide6.QtCore import Qt from PySide6.QtGui import QAction + from PySide6.QtWidgets import QTextEdit except ImportError: from PySide2 import QtCore, QtGui, QtSvg, QtWidgets - from PySide2.QtWidgets import QAction + from PySide2.QtWidgets import QAction, QTextEdit + from PySide2.QtCore import Qt from .. import errors from ..document import SAFE_EXTENSION, Document @@ -402,6 +406,28 @@ def __init__(self) -> None: super(WaitingWidget, self).__init__() +class TracebackWidget(QTextEdit): + """Reusable component to present tracebacks to the user. + + By default, the widget is initialized but does not appear. + You need to call `.set_content("traceback")` on it so the + traceback is displayed. + """ + + def __init__(self) -> None: + super(TracebackWidget, self).__init__() + # Error + self.setReadOnly(True) + self.setVisible(False) + self.setProperty("style", "traceback") + # Enable copying + self.setTextInteractionFlags(Qt.TextSelectableByMouse) + + def set_content(self, error: str) -> None: + self.setPlainText(error) + self.setVisible(True) + + class WaitingWidgetContainer(WaitingWidget): # These are the possible states that the WaitingWidget can show. # @@ -434,10 +460,13 @@ def __init__(self, dangerzone: DangerzoneGui) -> None: self.buttons = QtWidgets.QWidget() self.buttons.setLayout(buttons_layout) + self.traceback = TracebackWidget() + # Layout layout = QtWidgets.QVBoxLayout() layout.addStretch() layout.addWidget(self.label) + layout.addWidget(self.traceback) layout.addStretch() layout.addWidget(self.buttons) layout.addStretch() @@ -448,51 +477,78 @@ def __init__(self, dangerzone: DangerzoneGui) -> None: def check_state(self) -> None: state: Optional[str] = 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() except NoContainerTechException as e: log.error(str(e)) state = "not_installed" else: - # Can we run `docker image ls` without an error + # Can we run `docker/podman image ls` without an error with subprocess.Popen( [container_runtime, "image", "ls"], stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, + stderr=subprocess.PIPE, startupinfo=get_subprocess_startupinfo(), ) as p: - p.communicate() + _, stderr = p.communicate() if p.returncode != 0: - log.error("Docker is not running") + log.error(f"{runtime_name} is not running") state = "not_running" + error = stderr.decode() else: # Always try installing the container state = "install_container" # Update the state - self.state_change(state) + self.state_change(state, error) - def state_change(self, state: str) -> None: + def state_change(self, state: str, error: Optional[str] = None) -> None: if state == "not_installed": - self.label.setText( - "Dangerzone Requires Docker Desktop

Download Docker Desktop, install it, and open it." - ) + if platform.system() == "Linux": + self.label.setText( + "Dangerzone requires Podman

" + "Install it and retry." + ) + else: + self.label.setText( + "Dangerzone requires Docker Desktop

" + "Download Docker Desktop" + ", install it, and open it." + ) self.buttons.show() elif state == "not_running": - self.label.setText( - "Dangerzone Requires Docker Desktop

Docker is installed but isn't running.

Open Docker and make sure it's running in the background." - ) + if platform.system() == "Linux": + # "not_running" here means that the `podman image ls` command failed. + message = ( + "Dangerzone requires Podman

" + "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( + "Dangerzone requires Docker Desktop

" + "Docker is installed but isn't running.

" + "Open Docker and make sure it's running in the background." + ) self.buttons.show() else: self.label.setText( - "Installing the Dangerzone container image.

This might take a few minutes..." + "Installing the Dangerzone container image.

" + "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.start() diff --git a/dangerzone/isolation_provider/container.py b/dangerzone/isolation_provider/container.py index 76c4aa047..7137d57e2 100644 --- a/dangerzone/isolation_provider/container.py +++ b/dangerzone/isolation_provider/container.py @@ -242,7 +242,7 @@ def assert_field_type(self, val: Any, _type: object) -> None: # `int`. # # See https://stackoverflow.com/a/37888668 - if not type(val) == _type: + if type(val) is not _type: raise ValueError("Status field has incorrect type") def parse_progress_trusted(self, document: Document, line: str) -> None: diff --git a/share/dangerzone.css b/share/dangerzone.css index 45fdc2b45..38bf32bc9 100644 --- a/share/dangerzone.css +++ b/share/dangerzone.css @@ -48,3 +48,11 @@ QLabel.version { font-size: 20px; padding-bottom: 5px; /* align with 'dangerzone' font */ } + +QTextEdit[style="traceback"] { + font-family: Consolas, Monospace; + font-size: 12px; + background-color: #ffffff; + color: #000000; + padding: 10px; +} \ No newline at end of file