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
This commit is contained in:
Alexis Métaireau 2024-08-30 17:12:28 +02:00
parent 9b9e265b11
commit c3c7fbbc20
No known key found for this signature in database
GPG key ID: C65C7A89A8FFC56E
4 changed files with 81 additions and 17 deletions

View file

@ -5,7 +5,7 @@ import platform
import signal import signal
import sys import sys
import typing import typing
from typing import Dict, List, Optional from typing import List, Optional
import click import click
import colorama import colorama

View file

@ -10,14 +10,18 @@ from typing import List, Optional
# FIXME: See https://github.com/freedomofpress/dangerzone/issues/320 for more details. # FIXME: See https://github.com/freedomofpress/dangerzone/issues/320 for more details.
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from PySide2 import QtCore, QtGui, QtSvg, QtWidgets from PySide2 import QtCore, QtGui, QtSvg, QtWidgets
from PySide2.QtWidgets import QAction from PySide2.QtCore import Qt
from PySide2.QtWidgets import QAction, QTextEdit
else: else:
try: try:
from PySide6 import QtCore, QtGui, QtSvg, QtWidgets from PySide6 import QtCore, QtGui, QtSvg, QtWidgets
from PySide6.QtCore import Qt
from PySide6.QtGui import QAction from PySide6.QtGui import QAction
from PySide6.QtWidgets import QTextEdit
except ImportError: except ImportError:
from PySide2 import QtCore, QtGui, QtSvg, QtWidgets 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 .. import errors
from ..document import SAFE_EXTENSION, Document from ..document import SAFE_EXTENSION, Document
@ -402,6 +406,28 @@ class WaitingWidget(QtWidgets.QWidget):
super(WaitingWidget, self).__init__() 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): class WaitingWidgetContainer(WaitingWidget):
# These are the possible states that the WaitingWidget can show. # These are the possible states that the WaitingWidget can show.
# #
@ -434,10 +460,13 @@ class WaitingWidgetContainer(WaitingWidget):
self.buttons = QtWidgets.QWidget() self.buttons = QtWidgets.QWidget()
self.buttons.setLayout(buttons_layout) self.buttons.setLayout(buttons_layout)
self.traceback = TracebackWidget()
# Layout # Layout
layout = QtWidgets.QVBoxLayout() layout = QtWidgets.QVBoxLayout()
layout.addStretch() layout.addStretch()
layout.addWidget(self.label) layout.addWidget(self.label)
layout.addWidget(self.traceback)
layout.addStretch() layout.addStretch()
layout.addWidget(self.buttons) layout.addWidget(self.buttons)
layout.addStretch() layout.addStretch()
@ -448,51 +477,78 @@ class WaitingWidgetContainer(WaitingWidget):
def check_state(self) -> None: def check_state(self) -> None:
state: Optional[str] = None state: Optional[str] = None
error: Optional[str] = None
try: try:
if isinstance( # Sanity check if isinstance( # Sanity check
self.dangerzone.isolation_provider, Container self.dangerzone.isolation_provider, Container
): ):
container_runtime = self.dangerzone.isolation_provider.get_runtime() container_runtime = self.dangerzone.isolation_provider.get_runtime()
runtime_name = self.dangerzone.isolation_provider.get_runtime_name()
except NoContainerTechException as e: except NoContainerTechException as e:
log.error(str(e)) log.error(str(e))
state = "not_installed" state = "not_installed"
else: else:
# Can we run `docker image ls` without an error # Can we run `docker/podman image ls` without an error
with subprocess.Popen( with subprocess.Popen(
[container_runtime, "image", "ls"], [container_runtime, "image", "ls"],
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, stderr=subprocess.PIPE,
startupinfo=get_subprocess_startupinfo(), startupinfo=get_subprocess_startupinfo(),
) as p: ) as p:
p.communicate() _, stderr = p.communicate()
if p.returncode != 0: if p.returncode != 0:
log.error("Docker is not running") log.error(f"{runtime_name} is not running")
state = "not_running" state = "not_running"
error = stderr.decode()
else: else:
# Always try installing the container # Always try installing the container
state = "install_container" state = "install_container"
# Update the state # 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": if state == "not_installed":
if platform.system() == "Linux":
self.label.setText( self.label.setText(
"<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." "<strong>Dangerzone requires Podman</strong><br><br>"
"Install it and retry."
)
else:
self.label.setText(
"<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() self.buttons.show()
elif state == "not_running": 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( self.label.setText(
"<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." "<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.buttons.show()
else: else:
self.label.setText( self.label.setText(
"Installing the Dangerzone container image.<br><br>This might take a few minutes..." "Installing the Dangerzone container image.<br><br>"
"This might take a few minutes..."
) )
self.buttons.hide() self.buttons.hide()
self.traceback.setVisible(False)
self.install_container_t = InstallContainerThread(self.dangerzone) self.install_container_t = InstallContainerThread(self.dangerzone)
self.install_container_t.finished.connect(self.finished) self.install_container_t.finished.connect(self.finished)
self.install_container_t.start() self.install_container_t.start()

View file

@ -242,7 +242,7 @@ class Container(IsolationProvider):
# `int`. # `int`.
# #
# See https://stackoverflow.com/a/37888668 # See https://stackoverflow.com/a/37888668
if not type(val) == _type: if type(val) is not _type:
raise ValueError("Status field has incorrect type") raise ValueError("Status field has incorrect type")
def parse_progress_trusted(self, document: Document, line: str) -> None: def parse_progress_trusted(self, document: Document, line: str) -> None:

View file

@ -48,3 +48,11 @@ QLabel.version {
font-size: 20px; font-size: 20px;
padding-bottom: 5px; /* align with 'dangerzone' font */ padding-bottom: 5px; /* align with 'dangerzone' font */
} }
QTextEdit[style="traceback"] {
font-family: Consolas, Monospace;
font-size: 12px;
background-color: #ffffff;
color: #000000;
padding: 10px;
}