Compare commits

...

8 commits

Author SHA1 Message Date
1739384e6c
Merge 48ad749965 into 02602b072a 2025-01-16 11:51:05 +01:00
Alexis Métaireau
48ad749965
doc: bump the Docker Desktop version as part of the RELEASE procedure 2025-01-16 11:51:00 +01:00
Alexis Métaireau
a7e39a04ad
Bind Alert instances to the main window alert property 2025-01-16 11:50:59 +01:00
Alexis Métaireau
abc72ffe0e
Warn users if the minimum version of Docker Desktop is not met
This only happens on Windows and macOS.

Fixes #693
2025-01-16 11:50:58 +01:00
Alexis Métaireau
02602b072a
Remove intermediate variables for conversion start/end logs
Some checks are pending
Tests / run tests (ubuntu 22.04) (push) Blocked by required conditions
Tests / run tests (ubuntu 24.04) (push) Blocked by required conditions
Tests / run tests (ubuntu 24.10) (push) Blocked by required conditions
Tests / run-lint (push) Waiting to run
Tests / build-container-image (push) Waiting to run
Tests / Download and cache Tesseract data (push) Waiting to run
Tests / windows (push) Blocked by required conditions
Tests / macOS (arch64) (push) Blocked by required conditions
Tests / macOS (x86_64) (push) Blocked by required conditions
Tests / build-deb (debian bookworm) (push) Blocked by required conditions
Tests / build-deb (debian bullseye) (push) Blocked by required conditions
Tests / build-deb (debian trixie) (push) Blocked by required conditions
Tests / build-deb (ubuntu 20.04) (push) Blocked by required conditions
Tests / build-deb (ubuntu 22.04) (push) Blocked by required conditions
Tests / build-deb (ubuntu 24.04) (push) Blocked by required conditions
Tests / build-deb (ubuntu 24.10) (push) Blocked by required conditions
Tests / install-deb (debian bookworm) (push) Blocked by required conditions
Tests / install-deb (debian bullseye) (push) Blocked by required conditions
Tests / install-deb (debian trixie) (push) Blocked by required conditions
Tests / install-deb (ubuntu 20.04) (push) Blocked by required conditions
Tests / install-deb (ubuntu 22.04) (push) Blocked by required conditions
Tests / install-deb (ubuntu 24.04) (push) Blocked by required conditions
Tests / install-deb (ubuntu 24.10) (push) Blocked by required conditions
Tests / build-install-rpm (fedora 40) (push) Blocked by required conditions
Tests / build-install-rpm (fedora 41) (push) Blocked by required conditions
Tests / run tests (debian bookworm) (push) Blocked by required conditions
Tests / run tests (debian bullseye) (push) Blocked by required conditions
Tests / run tests (debian trixie) (push) Blocked by required conditions
Scan latest app and container / security-scan-container (push) Waiting to run
Scan latest app and container / security-scan-app (push) Waiting to run
Also, state that the logs are incomplete in the header.
2025-01-16 11:35:07 +01:00
Alexis Métaireau
acf20ef700
Add a --debug flag to the CLI to help retrieve more logs
When the flag is set, the `RUNSC_DEBUG=1` environment variable is added
to the outer container, and stderr is captured in a separate thread, before printing its output.
2025-01-16 11:35:06 +01:00
Alexis Métaireau
3499010d8e
docs(install): store GPG keys in the base64 format 2025-01-15 19:48:00 +01:00
Alexis Métaireau
2423fc18c5
CI: Store the signature key using the base64 format
The GPG binary format used until now doesn't seem to please `sqv` which
is now used by default on debian trixie.

Fixes #1052
2025-01-15 19:39:02 +01:00
10 changed files with 303 additions and 31 deletions

View file

@ -53,8 +53,13 @@ jobs:
gpg --keyserver hkps://keys.openpgp.org \ gpg --keyserver hkps://keys.openpgp.org \
--no-default-keyring --keyring ./fpf-apt-tools-archive-keyring.gpg \ --no-default-keyring --keyring ./fpf-apt-tools-archive-keyring.gpg \
--recv-keys "DE28 AB24 1FA4 8260 FAC9 B8BA A7C9 B385 2260 4281" --recv-keys "DE28 AB24 1FA4 8260 FAC9 B8BA A7C9 B385 2260 4281"
# Export the GPG key in armor mode because sequoia needs it this way
# (sqv is used on debian trixie by default to check the keys)
mkdir -p /etc/apt/keyrings/ mkdir -p /etc/apt/keyrings/
mv fpf-apt-tools-archive-keyring.gpg /etc/apt/keyrings gpg --no-default-keyring --keyring ./fpf-apt-tools-archive-keyring.gpg \
--armor --export "DE28 AB24 1FA4 8260 FAC9 B8BA A7C9 B385 2260 4281" \
> /etc/apt/keyrings/fpf-apt-tools-archive-keyring.gpg
- name: Add packages.freedom.press to our APT sources - name: Add packages.freedom.press to our APT sources
run: | run: |

View file

@ -94,7 +94,9 @@ gpg --keyserver hkps://keys.openpgp.org \
--no-default-keyring --keyring ./fpf-apt-tools-archive-keyring.gpg \ --no-default-keyring --keyring ./fpf-apt-tools-archive-keyring.gpg \
--recv-keys "DE28 AB24 1FA4 8260 FAC9 B8BA A7C9 B385 2260 4281" --recv-keys "DE28 AB24 1FA4 8260 FAC9 B8BA A7C9 B385 2260 4281"
sudo mkdir -p /etc/apt/keyrings/ sudo mkdir -p /etc/apt/keyrings/
sudo mv fpf-apt-tools-archive-keyring.gpg /etc/apt/keyrings sudo gpg --no-default-keyring --keyring ./fpf-apt-tools-archive-keyring.gpg \
--armor --export "DE28 AB24 1FA4 8260 FAC9 B8BA A7C9 B385 2260 4281" \
> /etc/apt/keyrings/fpf-apt-tools-archive-keyring.gpg
``` ```
Add the URL of the repo in your APT sources: Add the URL of the repo in your APT sources:

View file

@ -8,12 +8,13 @@ Here is a list of tasks that should be done before issuing the release:
- [ ] Create a new issue named **QA and Release for version \<VERSION\>**, to track the general progress. - [ ] Create a new issue named **QA and Release for version \<VERSION\>**, to track the general progress.
You can generate its content with the the `poetry run ./dev_scripts/generate-release-tasks.py` command. You can generate its content with the the `poetry run ./dev_scripts/generate-release-tasks.py` command.
- [ ] [Add new Linux platforms and remove obsolete ones](https://github.com/freedomofpress/dangerzone/blob/main/RELEASE.md#add-new-platforms-and-remove-obsolete-ones) - [ ] [Add new Linux platforms and remove obsolete ones](https://github.com/freedomofpress/dangerzone/blob/main/RELEASE.md#add-new-linux-platforms-and-remove-obsolete-ones)
- [ ] Bump the Python dependencies using `poetry lock` - [ ] Bump the Python dependencies using `poetry lock`
- [ ] Update `version` in `pyproject.toml` - [ ] Update `version` in `pyproject.toml`
- [ ] Update `share/version.txt` - [ ] Update `share/version.txt`
- [ ] Update the "Version" field in `install/linux/dangerzone.spec` - [ ] Update the "Version" field in `install/linux/dangerzone.spec`
- [ ] Bump the Debian version by adding a new changelog entry in `debian/changelog` - [ ] Bump the Debian version by adding a new changelog entry in `debian/changelog`
- [ ] [Bump the minimum Docker Desktop versions](https://github.com/freedomofpress/dangerzone/blob/main/RELEASE.md#bump-the-minimum-docker-desktop-version) in `isolation_provider/container.py`
- [ ] Update screenshot in `README.md`, if necessary - [ ] Update screenshot in `README.md`, if necessary
- [ ] CHANGELOG.md should be updated to include a list of all major changes since the last release - [ ] CHANGELOG.md should be updated to include a list of all major changes since the last release
- [ ] A draft release should be created. Copy the release notes text from the template at [`docs/templates/release-notes`](https://github.com/freedomofpress/dangerzone/tree/main/docs/templates/) - [ ] A draft release should be created. Copy the release notes text from the template at [`docs/templates/release-notes`](https://github.com/freedomofpress/dangerzone/tree/main/docs/templates/)
@ -46,6 +47,12 @@ In case of the removal of a version:
* Consult the previous paragraph, but also `grep` your way around. * Consult the previous paragraph, but also `grep` your way around.
2. Add a notice in our `CHANGELOG.md` about the version removal. 2. Add a notice in our `CHANGELOG.md` about the version removal.
## Bump the minimum Docker Desktop version
We embed the minimum docker desktop versions inside Dangerzone, as an incentive for our macOS and Windows users to upgrade to the latests version.
You can find the latest version at the time of the release by looking at [their release notes](https://docs.docker.com/desktop/release-notes/)
## Large Document Testing ## Large Document Testing
Parallel to the QA process, the release candidate should be put through the large document tests in a dedicated machine to run overnight. Parallel to the QA process, the release candidate should be put through the large document tests in a dedicated machine to run overnight.

View file

@ -42,6 +42,12 @@ def print_header(s: str) -> None:
type=click.UNPROCESSED, type=click.UNPROCESSED,
callback=args.validate_input_filenames, callback=args.validate_input_filenames,
) )
@click.option(
"--debug",
"debug",
flag_value=True,
help="Run Dangerzone in debug mode, to get logs from gVisor.",
)
@click.version_option(version=get_version(), message="%(version)s") @click.version_option(version=get_version(), message="%(version)s")
@errors.handle_document_errors @errors.handle_document_errors
def cli_main( def cli_main(
@ -50,6 +56,7 @@ def cli_main(
filenames: List[str], filenames: List[str],
archive: bool, archive: bool,
dummy_conversion: bool, dummy_conversion: bool,
debug: bool,
) -> None: ) -> None:
setup_logging() setup_logging()
@ -58,7 +65,7 @@ def cli_main(
elif is_qubes_native_conversion(): elif is_qubes_native_conversion():
dangerzone = DangerzoneCore(Qubes()) dangerzone = DangerzoneCore(Qubes())
else: else:
dangerzone = DangerzoneCore(Container()) dangerzone = DangerzoneCore(Container(debug=debug))
display_banner() display_banner()
if len(filenames) == 1 and output_filename: if len(filenames) == 1 and output_filename:

View file

@ -124,6 +124,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.setWindowTitle("Dangerzone") self.setWindowTitle("Dangerzone")
self.setWindowIcon(self.dangerzone.get_window_icon()) self.setWindowIcon(self.dangerzone.get_window_icon())
self.alert: Optional[Alert] = None
self.setMinimumWidth(600) self.setMinimumWidth(600)
if platform.system() == "Darwin": if platform.system() == "Darwin":
@ -226,6 +227,13 @@ class MainWindow(QtWidgets.QMainWindow):
# This allows us to make QSS rules conditional on the OS color mode. # This allows us to make QSS rules conditional on the OS color mode.
self.setProperty("OSColorMode", self.dangerzone.app.os_color_mode.value) self.setProperty("OSColorMode", self.dangerzone.app.os_color_mode.value)
if hasattr(self.dangerzone.isolation_provider, "check_docker_desktop_version"):
is_version_valid, version = (
self.dangerzone.isolation_provider.check_docker_desktop_version()
)
if not is_version_valid:
self.handle_docker_desktop_version_check(is_version_valid, version)
self.show() self.show()
def show_update_success(self) -> None: def show_update_success(self) -> None:
@ -279,6 +287,46 @@ class MainWindow(QtWidgets.QMainWindow):
self.dangerzone.settings.set("updater_check", check) self.dangerzone.settings.set("updater_check", check)
self.dangerzone.settings.save() self.dangerzone.settings.save()
def handle_docker_desktop_version_check(
self, is_version_valid: bool, version: str
) -> None:
hamburger_menu = self.hamburger_button.menu()
sep = hamburger_menu.insertSeparator(hamburger_menu.actions()[0])
upgrade_action = QAction("Docker Desktop should be upgraded", hamburger_menu)
upgrade_action.setIcon(
QtGui.QIcon(
load_svg_image(
"hamburger_menu_update_dot_error.svg", width=64, height=64
)
)
)
message = """
<p>A new version of Docker Desktop is available. Please upgrade your system.</p>
<p>Visit the <a href="https://www.docker.com/products/docker-desktop">Docker Desktop website</a> to download the latest version.</p>
<em>Keeping Docker Desktop up to date allows you to have more confidence that your documents are processed safely.</em>
"""
self.alert = Alert(
self.dangerzone,
title="Upgrade Docker Desktop",
message=message,
ok_text="Ok",
has_cancel=False,
)
def _launch_alert() -> None:
if self.alert:
self.alert.launch()
upgrade_action.triggered.connect(_launch_alert)
hamburger_menu.insertAction(sep, upgrade_action)
self.hamburger_button.setIcon(
QtGui.QIcon(
load_svg_image("hamburger_menu_update_error.svg", width=64, height=64)
)
)
def handle_updates(self, report: UpdateReport) -> None: def handle_updates(self, report: UpdateReport) -> None:
"""Handle update reports from the update checker thread. """Handle update reports from the update checker thread.
@ -365,7 +413,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.content_widget.show() self.content_widget.show()
def closeEvent(self, e: QtGui.QCloseEvent) -> None: def closeEvent(self, e: QtGui.QCloseEvent) -> None:
alert_widget = Alert( self.alert = Alert(
self.dangerzone, self.dangerzone,
message="Some documents are still being converted.\n Are you sure you want to quit?", message="Some documents are still being converted.\n Are you sure you want to quit?",
ok_text="Abort conversions", ok_text="Abort conversions",
@ -379,7 +427,7 @@ class MainWindow(QtWidgets.QMainWindow):
else: else:
self.dangerzone.app.exit(0) self.dangerzone.app.exit(0)
else: else:
accept_exit = alert_widget.launch() accept_exit = self.alert.launch()
if not accept_exit: if not accept_exit:
e.ignore() e.ignore()
return return
@ -623,7 +671,7 @@ class ContentWidget(QtWidgets.QWidget):
def documents_selected(self, docs: List[Document]) -> None: def documents_selected(self, docs: List[Document]) -> None:
if self.conversion_started: if self.conversion_started:
Alert( self.alert = Alert(
self.dangerzone, self.dangerzone,
message="Dangerzone does not support adding documents after the conversion has started.", message="Dangerzone does not support adding documents after the conversion has started.",
has_cancel=False, has_cancel=False,
@ -633,7 +681,7 @@ class ContentWidget(QtWidgets.QWidget):
# Ensure all files in batch are in the same directory # Ensure all files in batch are in the same directory
dirnames = {os.path.dirname(doc.input_filename) for doc in docs} dirnames = {os.path.dirname(doc.input_filename) for doc in docs}
if len(dirnames) > 1: if len(dirnames) > 1:
Alert( self.alert = Alert(
self.dangerzone, self.dangerzone,
message="Dangerzone does not support adding documents from multiple locations.\n\n The newly added documents were ignored.", message="Dangerzone does not support adding documents from multiple locations.\n\n The newly added documents were ignored.",
has_cancel=False, has_cancel=False,
@ -802,14 +850,14 @@ class DocSelectionDropFrame(QtWidgets.QFrame):
text = f"{num_unsupported_docs} files are not supported." text = f"{num_unsupported_docs} files are not supported."
ok_text = "Continue without these files" ok_text = "Continue without these files"
alert_widget = Alert( self.alert = Alert(
self.dangerzone, self.dangerzone,
message=f"{text}\nThe supported extensions are: " message=f"{text}\nThe supported extensions are: "
+ ", ".join(get_supported_extensions()), + ", ".join(get_supported_extensions()),
ok_text=ok_text, ok_text=ok_text,
) )
return alert_widget.exec_() return self.alert.exec_()
class SettingsWidget(QtWidgets.QWidget): class SettingsWidget(QtWidgets.QWidget):

View file

@ -5,7 +5,9 @@ import platform
import signal import signal
import subprocess import subprocess
import sys import sys
import threading
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from io import BytesIO
from typing import IO, Callable, Iterator, Optional from typing import IO, Callable, Iterator, Optional
import fitz import fitz
@ -18,10 +20,6 @@ from ..util import get_tessdata_dir, replace_control_chars
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
MAX_CONVERSION_LOG_CHARS = 150 * 50 # up to ~150 lines of 50 characters
DOC_TO_PIXELS_LOG_START = "----- DOC TO PIXELS LOG START -----"
DOC_TO_PIXELS_LOG_END = "----- DOC TO PIXELS LOG END -----"
TIMEOUT_EXCEPTION = 15 TIMEOUT_EXCEPTION = 15
TIMEOUT_GRACE = 15 TIMEOUT_GRACE = 15
TIMEOUT_FORCE = 5 TIMEOUT_FORCE = 5
@ -75,9 +73,9 @@ def read_int(f: IO[bytes]) -> int:
return int.from_bytes(untrusted_int, "big", signed=False) return int.from_bytes(untrusted_int, "big", signed=False)
def read_debug_text(f: IO[bytes], size: int) -> str: def sanitize_debug_text(text: bytes) -> str:
"""Read arbitrarily long text (for debug purposes), and sanitize it.""" """Read all the buffer and return a sanitized version"""
untrusted_text = f.read(size).decode("ascii", errors="replace") untrusted_text = text.decode("ascii", errors="replace")
return replace_control_chars(untrusted_text, keep_newlines=True) return replace_control_chars(untrusted_text, keep_newlines=True)
@ -86,12 +84,16 @@ class IsolationProvider(ABC):
Abstracts an isolation provider Abstracts an isolation provider
""" """
def __init__(self) -> None: def __init__(self, debug: bool = False) -> None:
if getattr(sys, "dangerzone_dev", False) is True: self.debug = debug
if self.should_capture_stderr():
self.proc_stderr = subprocess.PIPE self.proc_stderr = subprocess.PIPE
else: else:
self.proc_stderr = subprocess.DEVNULL self.proc_stderr = subprocess.DEVNULL
def should_capture_stderr(self) -> bool:
return self.debug or getattr(sys, "dangerzone_dev", False)
@abstractmethod @abstractmethod
def install(self) -> bool: def install(self) -> bool:
pass pass
@ -327,7 +329,11 @@ class IsolationProvider(ABC):
timeout_force: int = TIMEOUT_FORCE, timeout_force: int = TIMEOUT_FORCE,
) -> Iterator[subprocess.Popen]: ) -> Iterator[subprocess.Popen]:
"""Start a conversion process, pass it to the caller, and then clean it up.""" """Start a conversion process, pass it to the caller, and then clean it up."""
# Store the proc stderr in memory
stderr = BytesIO()
p = self.start_doc_to_pixels_proc(document) p = self.start_doc_to_pixels_proc(document)
stderr_thread = self.start_stderr_thread(p, stderr)
if platform.system() != "Windows": if platform.system() != "Windows":
assert os.getpgid(p.pid) != os.getpgid( assert os.getpgid(p.pid) != os.getpgid(
os.getpid() os.getpid()
@ -343,15 +349,40 @@ class IsolationProvider(ABC):
document, p, timeout_grace=timeout_grace, timeout_force=timeout_force document, p, timeout_grace=timeout_grace, timeout_force=timeout_force
) )
# Read the stderr of the process only if: if stderr_thread:
# * Dev mode is enabled. # Wait for the thread to complete. If it's still alive, mention it in the debug log.
# * The process has exited (else we risk hanging). stderr_thread.join(timeout=1)
if getattr(sys, "dangerzone_dev", False) and p.poll() is not None:
assert p.stderr debug_bytes = stderr.getvalue()
debug_log = read_debug_text(p.stderr, MAX_CONVERSION_LOG_CHARS) debug_log = sanitize_debug_text(debug_bytes)
incomplete = "(incomplete) " if stderr_thread.is_alive() else ""
log.info( log.info(
"Conversion output (doc to pixels)\n" "Conversion output (doc to pixels)\n"
f"{DOC_TO_PIXELS_LOG_START}\n" f"----- DOC TO PIXELS LOG START {incomplete}-----\n"
f"{debug_log}" # no need for an extra newline here f"{debug_log}" # no need for an extra newline here
f"{DOC_TO_PIXELS_LOG_END}" "----- DOC TO PIXELS LOG END -----"
) )
def start_stderr_thread(
self, process: subprocess.Popen, stderr: IO[bytes]
) -> Optional[threading.Thread]:
"""Start a thread to read stderr from the process"""
def _stream_stderr(process_stderr: IO[bytes]) -> None:
try:
for line in process_stderr:
stderr.write(line)
except (ValueError, IOError) as e:
log.debug(f"Stderr stream closed: {e}")
if process.stderr:
stderr_thread = threading.Thread(
target=_stream_stderr,
args=(process.stderr,),
daemon=True,
)
stderr_thread.start()
return stderr_thread
return None

View file

@ -3,7 +3,7 @@ import os
import platform import platform
import shlex import shlex
import subprocess import subprocess
from typing import List from typing import List, Tuple
from .. import container_utils, errors from .. import container_utils, errors
from ..document import Document from ..document import Document
@ -11,7 +11,10 @@ from ..util import get_resource_path, get_subprocess_startupinfo
from .base import IsolationProvider, terminate_process_group from .base import IsolationProvider, terminate_process_group
TIMEOUT_KILL = 5 # Timeout in seconds until the kill command returns. TIMEOUT_KILL = 5 # Timeout in seconds until the kill command returns.
MINIMUM_DOCKER_DESKTOP = {
"Darwin": "4.36.0",
"Windows": "4.36.0",
}
# Define startupinfo for subprocesses # Define startupinfo for subprocesses
if platform.system() == "Windows": if platform.system() == "Windows":
@ -121,6 +124,7 @@ class Container(IsolationProvider):
def is_available() -> bool: def is_available() -> bool:
container_runtime = container_utils.get_runtime() container_runtime = container_utils.get_runtime()
runtime_name = container_utils.get_runtime_name() runtime_name = container_utils.get_runtime_name()
# Can we run `docker/podman 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"],
@ -135,6 +139,28 @@ class Container(IsolationProvider):
) )
return True return True
def check_docker_desktop_version(self) -> Tuple[bool, str]:
# On windows and darwin, check that the minimum version is met
version = ""
if platform.system() != "Linux":
with subprocess.Popen(
["docker", "version", "--format", "{{.Server.Platform.Name}}"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
startupinfo=get_subprocess_startupinfo(),
) as p:
stdout, stderr = p.communicate()
if p.returncode != 0:
# In the case where there were an error, consider that
# the check went trough, as we're checking for installation
# compatibiliy somewhere else already
return True, version
# The output is like "Docker Desktop 4.35.1 (173168)"
version = stdout.decode().replace("Docker Desktop", "").split()[0]
if version < MINIMUM_DOCKER_DESKTOP[platform.system()]:
return False, version
return True, version
def doc_to_pixels_container_name(self, document: Document) -> str: def doc_to_pixels_container_name(self, document: Document) -> str:
"""Unique container name for the doc-to-pixels phase.""" """Unique container name for the doc-to-pixels phase."""
return f"dangerzone-doc-to-pixels-{document.id}" return f"dangerzone-doc-to-pixels-{document.id}"
@ -168,6 +194,10 @@ class Container(IsolationProvider):
) -> subprocess.Popen: ) -> subprocess.Popen:
container_runtime = container_utils.get_runtime() container_runtime = container_utils.get_runtime()
security_args = self.get_runtime_security_args() security_args = self.get_runtime_security_args()
debug_args = []
if self.debug:
debug_args += ["-e", "RUNSC_DEBUG=1"]
enable_stdin = ["-i"] enable_stdin = ["-i"]
set_name = ["--name", name] set_name = ["--name", name]
prevent_leakage_args = ["--rm"] prevent_leakage_args = ["--rm"]
@ -177,14 +207,14 @@ class Container(IsolationProvider):
args = ( args = (
["run"] ["run"]
+ security_args + security_args
+ debug_args
+ prevent_leakage_args + prevent_leakage_args
+ enable_stdin + enable_stdin
+ set_name + set_name
+ image_name + image_name
+ command + command
) )
args = [container_runtime] + args return self.exec([container_runtime] + args)
return self.exec(args)
def kill_container(self, name: str) -> None: def kill_container(self, name: str) -> None:
"""Terminate a spawned container. """Terminate a spawned container.

View file

@ -71,6 +71,7 @@ class DangerzoneCore(object):
ocr_lang, ocr_lang,
stdout_callback, stdout_callback,
) )
except Exception: except Exception:
log.exception( log.exception(
f"Unexpected error occurred while converting '{document}'" f"Unexpected error occurred while converting '{document}'"

View file

@ -587,3 +587,57 @@ def test_installation_failure_return_false(qtbot: QtBot, mocker: MockerFixture)
assert "the following error occured" in widget.label.text() assert "the following error occured" in widget.label.text()
assert "The image cannot be found" in widget.traceback.toPlainText() assert "The image cannot be found" in widget.traceback.toPlainText()
def test_up_to_date_docker_desktop_does_nothing(
qtbot: QtBot, mocker: MockerFixture
) -> None:
# Setup install to return False
mock_app = mocker.MagicMock()
dummy = mocker.MagicMock(spec=Container)
dummy.check_docker_desktop_version.return_value = (True, "1.0.0")
dz = DangerzoneGui(mock_app, dummy)
window = MainWindow(dz)
qtbot.addWidget(window)
menu_actions = window.hamburger_button.menu().actions()
assert "Docker Desktop should be upgraded" not in [
a.toolTip() for a in menu_actions
]
def test_outdated_docker_desktop_displays_warning(
qtbot: QtBot, mocker: MockerFixture
) -> None:
# Setup install to return False
mock_app = mocker.MagicMock()
dummy = mocker.MagicMock(spec=Container)
dummy.check_docker_desktop_version.return_value = (False, "1.0.0")
dz = DangerzoneGui(mock_app, dummy)
load_svg_spy = mocker.spy(main_window_module, "load_svg_image")
window = MainWindow(dz)
qtbot.addWidget(window)
menu_actions = window.hamburger_button.menu().actions()
assert menu_actions[0].toolTip() == "Docker Desktop should be upgraded"
# Check that the hamburger icon has changed with the expected SVG image.
assert load_svg_spy.call_count == 4
assert (
load_svg_spy.call_args_list[2].args[0] == "hamburger_menu_update_dot_error.svg"
)
alert_spy = mocker.spy(window.alert, "launch")
# Clicking the menu item should open a warning message
def _check_alert_displayed() -> None:
alert_spy.assert_any_call()
if window.alert:
window.alert.close()
QtCore.QTimer.singleShot(0, _check_alert_displayed)
menu_actions[0].trigger()

View file

@ -1,4 +1,5 @@
import os import os
import platform
import pytest import pytest
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
@ -108,6 +109,92 @@ class TestContainer(IsolationProviderTest):
with pytest.raises(errors.ImageNotPresentException): with pytest.raises(errors.ImageNotPresentException):
provider.install() provider.install()
@pytest.mark.skipif(
platform.system() not in ("Windows", "Darwin"),
reason="macOS and Windows specific",
)
def test_old_docker_desktop_version_is_detected(
self, mocker: MockerFixture, provider: Container, fp: FakeProcess
) -> None:
fp.register_subprocess(
[
"docker",
"version",
"--format",
"{{.Server.Platform.Name}}",
],
stdout="Docker Desktop 1.0.0 (173100)",
)
mocker.patch(
"dangerzone.isolation_provider.container.MINIMUM_DOCKER_DESKTOP",
{"Darwin": "1.0.1", "Windows": "1.0.1"},
)
assert (False, "1.0.0") == provider.check_docker_desktop_version()
@pytest.mark.skipif(
platform.system() not in ("Windows", "Darwin"),
reason="macOS and Windows specific",
)
def test_up_to_date_docker_desktop_version_is_detected(
self, mocker: MockerFixture, provider: Container, fp: FakeProcess
) -> None:
fp.register_subprocess(
[
"docker",
"version",
"--format",
"{{.Server.Platform.Name}}",
],
stdout="Docker Desktop 1.0.1 (173100)",
)
# Require version 1.0.1
mocker.patch(
"dangerzone.isolation_provider.container.MINIMUM_DOCKER_DESKTOP",
{"Darwin": "1.0.1", "Windows": "1.0.1"},
)
assert (True, "1.0.1") == provider.check_docker_desktop_version()
fp.register_subprocess(
[
"docker",
"version",
"--format",
"{{.Server.Platform.Name}}",
],
stdout="Docker Desktop 2.0.0 (173100)",
)
assert (True, "2.0.0") == provider.check_docker_desktop_version()
@pytest.mark.skipif(
platform.system() not in ("Windows", "Darwin"),
reason="macOS and Windows specific",
)
def test_docker_desktop_version_failure_returns_true(
self, mocker: MockerFixture, provider: Container, fp: FakeProcess
) -> None:
fp.register_subprocess(
[
"docker",
"version",
"--format",
"{{.Server.Platform.Name}}",
],
stderr="Oopsie",
returncode=1,
)
assert provider.check_docker_desktop_version() == (True, "")
@pytest.mark.skipif(
platform.system() != "Linux",
reason="Linux specific",
)
def test_linux_skips_desktop_version_check_returns_true(
self, mocker: MockerFixture, provider: Container
) -> None:
assert (True, "") == provider.check_docker_desktop_version()
class TestContainerTermination(IsolationProviderTermination): class TestContainerTermination(IsolationProviderTermination):
pass pass