mirror of
https://github.com/freedomofpress/dangerzone.git
synced 2025-05-17 18:51:50 +02:00
Compare commits
8 commits
442c683262
...
1739384e6c
Author | SHA1 | Date | |
---|---|---|---|
1739384e6c | |||
![]() |
48ad749965 | ||
![]() |
a7e39a04ad | ||
![]() |
abc72ffe0e | ||
![]() |
02602b072a | ||
![]() |
acf20ef700 | ||
![]() |
3499010d8e | ||
![]() |
2423fc18c5 |
10 changed files with 303 additions and 31 deletions
7
.github/workflows/check_repos.yml
vendored
7
.github/workflows/check_repos.yml
vendored
|
@ -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: |
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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}'"
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue