mirror of
https://github.com/freedomofpress/dangerzone.git
synced 2025-05-05 21:21:49 +02:00
Compare commits
1 commit
491cca6341
...
d28d6f9479
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d28d6f9479 |
22 changed files with 191 additions and 327 deletions
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -16,20 +16,6 @@ since 0.4.1, and this project adheres to [Semantic Versioning](https://semver.or
|
|||
- Document Operating System support [#986](https://github.com/freedomofpress/dangerzone/issues/986)
|
||||
- Tests: Look for regressions when converting PDFs [#321](https://github.com/freedomofpress/dangerzone/issues/321)
|
||||
|
||||
## Added
|
||||
|
||||
- (experimental): It is now possible to specify a custom container runtime in
|
||||
the settings, by using the `container_runtime` key. It should contain the path
|
||||
to the container runtime you want to use. Please note that this doesn't mean
|
||||
we support more container runtimes than Podman and Docker for the time being,
|
||||
but enables you to chose which one you want to use, independently of your
|
||||
platform. ([#925](https://github.com/freedomofpress/dangerzone/issues/925))
|
||||
|
||||
### Changed
|
||||
|
||||
- The `debian` base image is now fetched by digest. As a result, your local
|
||||
container storage will no longer show a tag for this dependency.
|
||||
|
||||
## [0.8.1](https://github.com/freedomofpress/dangerzone/compare/v0.8.1...0.8.0)
|
||||
|
||||
- Update the container image
|
||||
|
|
|
@ -2,9 +2,10 @@
|
|||
# Dockerfile args below. For more info about this file, read
|
||||
# docs/developer/reproducibility.md.
|
||||
|
||||
ARG DEBIAN_IMAGE_DATE=20250224
|
||||
ARG DEBIAN_IMAGE_DIGEST=sha256:12c396bd585df7ec21d5679bb6a83d4878bc4415ce926c9e5ea6426d23c60bdc
|
||||
|
||||
FROM debian@${DEBIAN_IMAGE_DIGEST} AS dangerzone-image
|
||||
FROM debian:bookworm-${DEBIAN_IMAGE_DATE}-slim@${DEBIAN_IMAGE_DIGEST} AS dangerzone-image
|
||||
|
||||
ARG GVISOR_ARCHIVE_DATE=20250217
|
||||
ARG DEBIAN_ARCHIVE_DATE=20250226
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# Should be the INDEX DIGEST from an image tagged `bookworm-<DATE>-slim`:
|
||||
# https://hub.docker.com/_/debian/tags?name=bookworm-
|
||||
#
|
||||
# Tag for this digest: bookworm-20250224-slim
|
||||
# Can be bumped to the latest date in https://hub.docker.com/_/debian/tags?name=bookworm-
|
||||
DEBIAN_IMAGE_DATE=20250224
|
||||
# Should be the INDEX DIGEST for the tag with the selected build date
|
||||
DEBIAN_IMAGE_DIGEST=sha256:12c396bd585df7ec21d5679bb6a83d4878bc4415ce926c9e5ea6426d23c60bdc
|
||||
# Can be bumped to today's date
|
||||
DEBIAN_ARCHIVE_DATE=20250226
|
||||
|
|
|
@ -2,9 +2,10 @@
|
|||
# Dockerfile args below. For more info about this file, read
|
||||
# docs/developer/reproducibility.md.
|
||||
|
||||
ARG DEBIAN_IMAGE_DATE={{DEBIAN_IMAGE_DATE}}
|
||||
ARG DEBIAN_IMAGE_DIGEST={{DEBIAN_IMAGE_DIGEST}}
|
||||
|
||||
FROM debian@${DEBIAN_IMAGE_DIGEST} AS dangerzone-image
|
||||
FROM debian:bookworm-${DEBIAN_IMAGE_DATE}-slim@${DEBIAN_IMAGE_DIGEST} AS dangerzone-image
|
||||
|
||||
ARG GVISOR_ARCHIVE_DATE={{GVISOR_ARCHIVE_DATE}}
|
||||
ARG DEBIAN_ARCHIVE_DATE={{DEBIAN_ARCHIVE_DATE}}
|
||||
|
|
|
@ -11,7 +11,6 @@ from .isolation_provider.container import Container
|
|||
from .isolation_provider.dummy import Dummy
|
||||
from .isolation_provider.qubes import Qubes, is_qubes_native_conversion
|
||||
from .logic import DangerzoneCore
|
||||
from .settings import Settings
|
||||
from .util import get_version, replace_control_chars
|
||||
|
||||
|
||||
|
@ -38,7 +37,7 @@ def print_header(s: str) -> None:
|
|||
)
|
||||
@click.argument(
|
||||
"filenames",
|
||||
required=False,
|
||||
required=True,
|
||||
nargs=-1,
|
||||
type=click.UNPROCESSED,
|
||||
callback=args.validate_input_filenames,
|
||||
|
@ -49,33 +48,17 @@ def print_header(s: str) -> None:
|
|||
flag_value=True,
|
||||
help="Run Dangerzone in debug mode, to get logs from gVisor.",
|
||||
)
|
||||
@click.option(
|
||||
"--set-container-runtime",
|
||||
required=False,
|
||||
help="The path to the container runtime you want to set in the settings",
|
||||
)
|
||||
@click.version_option(version=get_version(), message="%(version)s")
|
||||
@errors.handle_document_errors
|
||||
def cli_main(
|
||||
output_filename: Optional[str],
|
||||
ocr_lang: Optional[str],
|
||||
filenames: Optional[List[str]],
|
||||
filenames: List[str],
|
||||
archive: bool,
|
||||
dummy_conversion: bool,
|
||||
debug: bool,
|
||||
set_container_runtime: Optional[str] = None,
|
||||
) -> None:
|
||||
setup_logging()
|
||||
display_banner()
|
||||
if set_container_runtime:
|
||||
settings = Settings()
|
||||
container_runtime = settings.set_custom_runtime(
|
||||
set_container_runtime, autosave=True
|
||||
)
|
||||
click.echo(f"Set the settings container_runtime to {container_runtime}")
|
||||
sys.exit(0)
|
||||
elif not filenames:
|
||||
raise click.UsageError("Missing argument 'FILENAMES...'")
|
||||
|
||||
if getattr(sys, "dangerzone_dev", False) and dummy_conversion:
|
||||
dangerzone = DangerzoneCore(Dummy())
|
||||
|
@ -84,6 +67,7 @@ def cli_main(
|
|||
else:
|
||||
dangerzone = DangerzoneCore(Container(debug=debug))
|
||||
|
||||
display_banner()
|
||||
if len(filenames) == 1 and output_filename:
|
||||
dangerzone.add_document_from_filename(filenames[0], output_filename, archive)
|
||||
elif len(filenames) > 1 and output_filename:
|
||||
|
@ -336,10 +320,4 @@ def display_banner() -> None:
|
|||
+ Style.DIM
|
||||
+ "│"
|
||||
)
|
||||
print(
|
||||
Back.BLACK
|
||||
+ Fore.YELLOW
|
||||
+ Style.DIM
|
||||
+ "╰──────────────────────────╯"
|
||||
+ Style.RESET_ALL
|
||||
)
|
||||
print(Back.BLACK + Fore.YELLOW + Style.DIM + "╰──────────────────────────╯")
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import logging
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
from typing import List, Tuple
|
||||
|
||||
from . import errors
|
||||
from .settings import Settings
|
||||
from .util import get_resource_path, get_subprocess_startupinfo
|
||||
|
||||
CONTAINER_NAME = "dangerzone.rocks/dangerzone"
|
||||
|
@ -15,47 +12,16 @@ CONTAINER_NAME = "dangerzone.rocks/dangerzone"
|
|||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Runtime(object):
|
||||
"""Represents the container runtime to use.
|
||||
|
||||
- It can be specified via the settings, using the "container_runtime" key,
|
||||
which should point to the full path of the runtime;
|
||||
- If the runtime is not specified via the settings, it defaults
|
||||
to "podman" on Linux and "docker" on macOS and Windows.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
settings = Settings()
|
||||
|
||||
if settings.custom_runtime_specified():
|
||||
self.path = Path(settings.get("container_runtime"))
|
||||
if not self.path.exists():
|
||||
raise errors.UnsupportedContainerRuntime(self.path)
|
||||
self.name = self.path.stem
|
||||
def get_runtime_name() -> str:
|
||||
if platform.system() == "Linux":
|
||||
runtime_name = "podman"
|
||||
else:
|
||||
self.name = self.get_default_runtime_name()
|
||||
self.path = Runtime.path_from_name(self.name)
|
||||
|
||||
if self.name not in ("podman", "docker"):
|
||||
raise errors.UnsupportedContainerRuntime(self.name)
|
||||
|
||||
@staticmethod
|
||||
def path_from_name(name: str) -> Path:
|
||||
name_path = Path(name)
|
||||
if name_path.is_file():
|
||||
return name_path
|
||||
else:
|
||||
runtime = shutil.which(name_path)
|
||||
if runtime is None:
|
||||
raise errors.NoContainerTechException(name)
|
||||
return Path(runtime)
|
||||
|
||||
@staticmethod
|
||||
def get_default_runtime_name() -> str:
|
||||
return "podman" if platform.system() == "Linux" else "docker"
|
||||
# Windows, Darwin, and unknown use docker for now, dangerzone-vm eventually
|
||||
runtime_name = "docker"
|
||||
return runtime_name
|
||||
|
||||
|
||||
def get_runtime_version(runtime: Optional[Runtime] = None) -> Tuple[int, int]:
|
||||
def get_runtime_version() -> Tuple[int, int]:
|
||||
"""Get the major/minor parts of the Docker/Podman version.
|
||||
|
||||
Some of the operations we perform in this module rely on some Podman features
|
||||
|
@ -64,15 +30,14 @@ def get_runtime_version(runtime: Optional[Runtime] = None) -> Tuple[int, int]:
|
|||
just knowing the major and minor version, since writing/installing a full-blown
|
||||
semver parser is an overkill.
|
||||
"""
|
||||
runtime = runtime or Runtime()
|
||||
|
||||
# Get the Docker/Podman version, using a Go template.
|
||||
if runtime.name == "podman":
|
||||
runtime = get_runtime_name()
|
||||
if runtime == "podman":
|
||||
query = "{{.Client.Version}}"
|
||||
else:
|
||||
query = "{{.Server.Version}}"
|
||||
|
||||
cmd = [str(runtime.path), "version", "-f", query]
|
||||
cmd = [runtime, "version", "-f", query]
|
||||
try:
|
||||
version = subprocess.run(
|
||||
cmd,
|
||||
|
@ -81,7 +46,7 @@ def get_runtime_version(runtime: Optional[Runtime] = None) -> Tuple[int, int]:
|
|||
check=True,
|
||||
).stdout.decode()
|
||||
except Exception as e:
|
||||
msg = f"Could not get the version of the {runtime.name.capitalize()} tool: {e}"
|
||||
msg = f"Could not get the version of the {runtime.capitalize()} tool: {e}"
|
||||
raise RuntimeError(msg) from e
|
||||
|
||||
# Parse this version and return the major/minor parts, since we don't need the
|
||||
|
@ -91,12 +56,20 @@ def get_runtime_version(runtime: Optional[Runtime] = None) -> Tuple[int, int]:
|
|||
return (int(major), int(minor))
|
||||
except Exception as e:
|
||||
msg = (
|
||||
f"Could not parse the version of the {runtime.name.capitalize()} tool"
|
||||
f"Could not parse the version of the {runtime.capitalize()} tool"
|
||||
f" (found: '{version}') due to the following error: {e}"
|
||||
)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
|
||||
def get_runtime() -> str:
|
||||
container_tech = get_runtime_name()
|
||||
runtime = shutil.which(container_tech)
|
||||
if runtime is None:
|
||||
raise errors.NoContainerTechException(container_tech)
|
||||
return runtime
|
||||
|
||||
|
||||
def list_image_tags() -> List[str]:
|
||||
"""Get the tags of all loaded Dangerzone images.
|
||||
|
||||
|
@ -104,11 +77,10 @@ def list_image_tags() -> List[str]:
|
|||
images. This can be useful when we want to find which are the local image tags,
|
||||
and which image ID does the "latest" tag point to.
|
||||
"""
|
||||
runtime = Runtime()
|
||||
return (
|
||||
subprocess.check_output(
|
||||
[
|
||||
str(runtime.path),
|
||||
get_runtime(),
|
||||
"image",
|
||||
"list",
|
||||
"--format",
|
||||
|
@ -125,21 +97,19 @@ def list_image_tags() -> List[str]:
|
|||
|
||||
def add_image_tag(image_id: str, new_tag: str) -> None:
|
||||
"""Add a tag to the Dangerzone image."""
|
||||
runtime = Runtime()
|
||||
log.debug(f"Adding tag '{new_tag}' to image '{image_id}'")
|
||||
subprocess.check_output(
|
||||
[str(runtime.path), "tag", image_id, new_tag],
|
||||
[get_runtime(), "tag", image_id, new_tag],
|
||||
startupinfo=get_subprocess_startupinfo(),
|
||||
)
|
||||
|
||||
|
||||
def delete_image_tag(tag: str) -> None:
|
||||
"""Delete a Dangerzone image tag."""
|
||||
runtime = Runtime()
|
||||
log.warning(f"Deleting old container image: {tag}")
|
||||
try:
|
||||
subprocess.check_output(
|
||||
[str(runtime.name), "rmi", "--force", tag],
|
||||
[get_runtime(), "rmi", "--force", tag],
|
||||
startupinfo=get_subprocess_startupinfo(),
|
||||
)
|
||||
except Exception as e:
|
||||
|
@ -151,17 +121,16 @@ def delete_image_tag(tag: str) -> None:
|
|||
|
||||
def get_expected_tag() -> str:
|
||||
"""Get the tag of the Dangerzone image tarball from the image-id.txt file."""
|
||||
with get_resource_path("image-id.txt").open() as f:
|
||||
with open(get_resource_path("image-id.txt")) as f:
|
||||
return f.read().strip()
|
||||
|
||||
|
||||
def load_image_tarball() -> None:
|
||||
runtime = Runtime()
|
||||
log.info("Installing Dangerzone container image...")
|
||||
tarball_path = get_resource_path("container.tar")
|
||||
try:
|
||||
res = subprocess.run(
|
||||
[str(runtime.path), "load", "-i", str(tarball_path)],
|
||||
[get_runtime(), "load", "-i", tarball_path],
|
||||
startupinfo=get_subprocess_startupinfo(),
|
||||
capture_output=True,
|
||||
check=True,
|
||||
|
@ -186,7 +155,7 @@ def load_image_tarball() -> None:
|
|||
# `share/image-id.txt` and delete the incorrect tag.
|
||||
#
|
||||
# [1] https://github.com/containers/podman/issues/16490
|
||||
if runtime.name == "podman" and get_runtime_version(runtime) == (3, 4):
|
||||
if get_runtime_name() == "podman" and get_runtime_version() == (3, 4):
|
||||
expected_tag = get_expected_tag()
|
||||
bad_tag = f"localhost/{expected_tag}:latest"
|
||||
good_tag = f"{CONTAINER_NAME}:{expected_tag}"
|
||||
|
|
|
@ -140,7 +140,3 @@ class NotAvailableContainerTechException(Exception):
|
|||
self.error = error
|
||||
self.container_tech = container_tech
|
||||
super().__init__(f"{container_tech} is not available")
|
||||
|
||||
|
||||
class UnsupportedContainerRuntime(Exception):
|
||||
pass
|
||||
|
|
|
@ -51,7 +51,7 @@ class Application(QtWidgets.QApplication):
|
|||
def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
|
||||
super(Application, self).__init__(*args, **kwargs)
|
||||
self.setQuitOnLastWindowClosed(False)
|
||||
with get_resource_path("dangerzone.css").open("r") as f:
|
||||
with open(get_resource_path("dangerzone.css"), "r") as f:
|
||||
style = f.read()
|
||||
self.setStyleSheet(style)
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ class DangerzoneGui(DangerzoneCore):
|
|||
path = get_resource_path("dangerzone.ico")
|
||||
else:
|
||||
path = get_resource_path("icon.png")
|
||||
return QtGui.QIcon(str(path))
|
||||
return QtGui.QIcon(path)
|
||||
|
||||
def open_pdf_viewer(self, filename: str) -> None:
|
||||
if platform.system() == "Darwin":
|
||||
|
@ -252,7 +252,7 @@ class Alert(Dialog):
|
|||
def create_layout(self) -> QtWidgets.QBoxLayout:
|
||||
logo = QtWidgets.QLabel()
|
||||
logo.setPixmap(
|
||||
QtGui.QPixmap.fromImage(QtGui.QImage(str(get_resource_path("icon.png"))))
|
||||
QtGui.QPixmap.fromImage(QtGui.QImage(get_resource_path("icon.png")))
|
||||
)
|
||||
|
||||
label = QtWidgets.QLabel()
|
||||
|
|
|
@ -62,7 +62,7 @@ def load_svg_image(filename: str, width: int, height: int) -> QtGui.QPixmap:
|
|||
This answer is basically taken from: https://stackoverflow.com/a/25689790
|
||||
"""
|
||||
path = get_resource_path(filename)
|
||||
svg_renderer = QtSvg.QSvgRenderer(str(path))
|
||||
svg_renderer = QtSvg.QSvgRenderer(path)
|
||||
image = QtGui.QImage(width, height, QtGui.QImage.Format_ARGB32)
|
||||
# Set the ARGB to 0 to prevent rendering artifacts
|
||||
image.fill(0x00000000)
|
||||
|
@ -130,8 +130,9 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
|
||||
# Header
|
||||
logo = QtWidgets.QLabel()
|
||||
icon_path = str(get_resource_path("icon.png"))
|
||||
logo.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(icon_path)))
|
||||
logo.setPixmap(
|
||||
QtGui.QPixmap.fromImage(QtGui.QImage(get_resource_path("icon.png")))
|
||||
)
|
||||
header_label = QtWidgets.QLabel("Dangerzone")
|
||||
header_label.setFont(self.dangerzone.fixed_font)
|
||||
header_label.setStyleSheet("QLabel { font-weight: bold; font-size: 50px; }")
|
||||
|
@ -221,14 +222,11 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
self.setProperty("OSColorMode", self.dangerzone.app.os_color_mode.value)
|
||||
|
||||
if hasattr(self.dangerzone.isolation_provider, "check_docker_desktop_version"):
|
||||
try:
|
||||
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)
|
||||
except errors.UnsupportedContainerRuntime as e:
|
||||
pass # It's catched later in the flow.
|
||||
|
||||
self.show()
|
||||
|
||||
|
@ -577,15 +575,8 @@ class WaitingWidgetContainer(WaitingWidget):
|
|||
self.finished.emit()
|
||||
|
||||
def state_change(self, state: str, error: Optional[str] = None) -> None:
|
||||
custom_runtime = self.dangerzone.settings.custom_runtime_specified()
|
||||
|
||||
if state == "not_installed":
|
||||
if custom_runtime:
|
||||
self.show_error(
|
||||
"<strong>We could not find the container runtime defined in your settings</strong><br><br>"
|
||||
"Please check your settings, install it if needed, and retry."
|
||||
)
|
||||
elif platform.system() == "Linux":
|
||||
if platform.system() == "Linux":
|
||||
self.show_error(
|
||||
"<strong>Dangerzone requires Podman</strong><br><br>"
|
||||
"Install it and retry."
|
||||
|
@ -598,25 +589,19 @@ class WaitingWidgetContainer(WaitingWidget):
|
|||
)
|
||||
|
||||
elif state == "not_running":
|
||||
if custom_runtime:
|
||||
self.show_error(
|
||||
"<strong>We were unable to start the container runtime defined in your settings</strong><br><br>"
|
||||
"Please check your settings, install it if needed, and retry."
|
||||
)
|
||||
elif platform.system() == "Linux":
|
||||
if platform.system() == "Linux":
|
||||
# "not_running" here means that the `podman image ls` command failed.
|
||||
self.show_error(
|
||||
message = (
|
||||
"<strong>Dangerzone requires Podman</strong><br><br>"
|
||||
"Podman is installed but cannot run properly. See errors below",
|
||||
error,
|
||||
"Podman is installed but cannot run properly. See errors below"
|
||||
)
|
||||
else:
|
||||
self.show_error(
|
||||
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.",
|
||||
error,
|
||||
"Open Docker and make sure it's running in the background."
|
||||
)
|
||||
self.show_error(message, error)
|
||||
else:
|
||||
self.show_message(
|
||||
"Installing the Dangerzone container image.<br><br>"
|
||||
|
@ -1321,7 +1306,7 @@ class DocumentWidget(QtWidgets.QWidget):
|
|||
|
||||
def load_status_image(self, filename: str) -> QtGui.QPixmap:
|
||||
path = get_resource_path(filename)
|
||||
img = QtGui.QImage(str(path))
|
||||
img = QtGui.QImage(path)
|
||||
image = QtGui.QPixmap.fromImage(img)
|
||||
return image.scaled(QtCore.QSize(15, 15))
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ import subprocess
|
|||
from typing import List, Tuple
|
||||
|
||||
from .. import container_utils, errors
|
||||
from ..container_utils import Runtime
|
||||
from ..document import Document
|
||||
from ..util import get_resource_path, get_subprocess_startupinfo
|
||||
from .base import IsolationProvider, terminate_process_group
|
||||
|
@ -51,8 +50,7 @@ class Container(IsolationProvider):
|
|||
* Do not map the host user to the container, with `--userns nomap` (available
|
||||
from Podman 4.1 onwards)
|
||||
"""
|
||||
runtime = Runtime()
|
||||
if runtime.name == "podman":
|
||||
if container_utils.get_runtime_name() == "podman":
|
||||
security_args = ["--log-driver", "none"]
|
||||
security_args += ["--security-opt", "no-new-privileges"]
|
||||
if container_utils.get_runtime_version() >= (4, 1):
|
||||
|
@ -66,7 +64,7 @@ class Container(IsolationProvider):
|
|||
#
|
||||
# [1] https://github.com/freedomofpress/dangerzone/issues/846
|
||||
# [2] https://github.com/containers/common/blob/d3283f8401eeeb21f3c59a425b5461f069e199a7/pkg/seccomp/seccomp.json
|
||||
seccomp_json_path = str(get_resource_path("seccomp.gvisor.json"))
|
||||
seccomp_json_path = get_resource_path("seccomp.gvisor.json")
|
||||
security_args += ["--security-opt", f"seccomp={seccomp_json_path}"]
|
||||
|
||||
security_args += ["--cap-drop", "all"]
|
||||
|
@ -125,11 +123,12 @@ class Container(IsolationProvider):
|
|||
|
||||
@staticmethod
|
||||
def is_available() -> bool:
|
||||
runtime = Runtime()
|
||||
container_runtime = container_utils.get_runtime()
|
||||
runtime_name = container_utils.get_runtime_name()
|
||||
|
||||
# Can we run `docker/podman image ls` without an error
|
||||
with subprocess.Popen(
|
||||
[str(runtime.path), "image", "ls"],
|
||||
[container_runtime, "image", "ls"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE,
|
||||
startupinfo=get_subprocess_startupinfo(),
|
||||
|
@ -137,18 +136,14 @@ class Container(IsolationProvider):
|
|||
_, stderr = p.communicate()
|
||||
if p.returncode != 0:
|
||||
raise errors.NotAvailableContainerTechException(
|
||||
runtime.name, stderr.decode()
|
||||
runtime_name, stderr.decode()
|
||||
)
|
||||
return True
|
||||
|
||||
def check_docker_desktop_version(self) -> Tuple[bool, str]:
|
||||
# On windows and darwin, check that the minimum version is met
|
||||
version = ""
|
||||
runtime = Runtime()
|
||||
runtime_is_docker = runtime.name == "docker"
|
||||
platform_is_not_linux = platform.system() != "Linux"
|
||||
|
||||
if runtime_is_docker and platform_is_not_linux:
|
||||
if platform.system() != "Linux":
|
||||
with subprocess.Popen(
|
||||
["docker", "version", "--format", "{{.Server.Platform.Name}}"],
|
||||
stdout=subprocess.PIPE,
|
||||
|
@ -198,7 +193,7 @@ class Container(IsolationProvider):
|
|||
command: List[str],
|
||||
name: str,
|
||||
) -> subprocess.Popen:
|
||||
runtime = Runtime()
|
||||
container_runtime = container_utils.get_runtime()
|
||||
security_args = self.get_runtime_security_args()
|
||||
debug_args = []
|
||||
if self.debug:
|
||||
|
@ -220,7 +215,7 @@ class Container(IsolationProvider):
|
|||
+ image_name
|
||||
+ command
|
||||
)
|
||||
return self.exec([str(runtime.path)] + args)
|
||||
return self.exec([container_runtime] + args)
|
||||
|
||||
def kill_container(self, name: str) -> None:
|
||||
"""Terminate a spawned container.
|
||||
|
@ -232,8 +227,8 @@ class Container(IsolationProvider):
|
|||
connected to the Docker daemon, and killing it will just close the associated
|
||||
standard streams.
|
||||
"""
|
||||
runtime = Runtime()
|
||||
cmd = [str(runtime.path), "kill", name]
|
||||
container_runtime = container_utils.get_runtime()
|
||||
cmd = [container_runtime, "kill", name]
|
||||
try:
|
||||
# We do not check the exit code of the process here, since the container may
|
||||
# have stopped right before invoking this command. In that case, the
|
||||
|
@ -289,10 +284,10 @@ class Container(IsolationProvider):
|
|||
# after a podman kill / docker kill invocation, this will likely be the case,
|
||||
# else the container runtime (Docker/Podman) has experienced a problem, and we
|
||||
# should report it.
|
||||
runtime = Runtime()
|
||||
container_runtime = container_utils.get_runtime()
|
||||
name = self.doc_to_pixels_container_name(document)
|
||||
all_containers = subprocess.run(
|
||||
[str(runtime.path), "ps", "-a"],
|
||||
[container_runtime, "ps", "-a"],
|
||||
capture_output=True,
|
||||
startupinfo=get_subprocess_startupinfo(),
|
||||
)
|
||||
|
@ -303,20 +298,19 @@ class Container(IsolationProvider):
|
|||
# FIXME hardcoded 1 until length conversions are better handled
|
||||
# https://github.com/freedomofpress/dangerzone/issues/257
|
||||
return 1
|
||||
runtime = Runtime() # type: ignore [unreachable]
|
||||
|
||||
n_cpu = 1
|
||||
n_cpu = 1 # type: ignore [unreachable]
|
||||
if platform.system() == "Linux":
|
||||
# if on linux containers run natively
|
||||
cpu_count = os.cpu_count()
|
||||
if cpu_count is not None:
|
||||
n_cpu = cpu_count
|
||||
|
||||
elif runtime.name == "docker":
|
||||
elif container_utils.get_runtime_name() == "docker":
|
||||
# For Windows and MacOS containers run in VM
|
||||
# So we obtain the CPU count for the VM
|
||||
n_cpu_str = subprocess.check_output(
|
||||
[str(runtime.path), "info", "--format", "{{.NCPU}}"],
|
||||
[container_utils.get_runtime(), "info", "--format", "{{.NCPU}}"],
|
||||
text=True,
|
||||
startupinfo=get_subprocess_startupinfo(),
|
||||
)
|
||||
|
|
|
@ -130,6 +130,7 @@ def is_qubes_native_conversion() -> bool:
|
|||
# This disambiguates if it is running a Qubes targetted build or not
|
||||
# (Qubes-specific builds don't ship the container image)
|
||||
|
||||
return not get_resource_path("container.tar").exists()
|
||||
container_image_path = get_resource_path("container.tar")
|
||||
return not os.path.exists(container_image_path)
|
||||
else:
|
||||
return False
|
||||
|
|
|
@ -23,13 +23,16 @@ class DangerzoneCore(object):
|
|||
# Initialize terminal colors
|
||||
colorama.init(autoreset=True)
|
||||
|
||||
# App data folder
|
||||
self.appdata_path = util.get_config_dir()
|
||||
|
||||
# Languages supported by tesseract
|
||||
with get_resource_path("ocr-languages.json").open("r") as f:
|
||||
with open(get_resource_path("ocr-languages.json"), "r") as f:
|
||||
unsorted_ocr_languages = json.load(f)
|
||||
self.ocr_languages = dict(sorted(unsorted_ocr_languages.items()))
|
||||
|
||||
# Load settings
|
||||
self.settings = Settings()
|
||||
self.settings = Settings(self)
|
||||
self.documents: List[Document] = []
|
||||
self.isolation_provider = isolation_provider
|
||||
|
||||
|
|
|
@ -1,24 +1,29 @@
|
|||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Dict
|
||||
|
||||
from packaging import version
|
||||
|
||||
from .document import SAFE_EXTENSION
|
||||
from .util import get_config_dir, get_version
|
||||
from .util import get_version
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .logic import DangerzoneCore
|
||||
|
||||
SETTINGS_FILENAME: str = "settings.json"
|
||||
|
||||
|
||||
class Settings:
|
||||
settings: Dict[str, Any]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.settings_filename = get_config_dir() / SETTINGS_FILENAME
|
||||
def __init__(self, dangerzone: "DangerzoneCore") -> None:
|
||||
self.dangerzone = dangerzone
|
||||
self.settings_filename = os.path.join(
|
||||
self.dangerzone.appdata_path, SETTINGS_FILENAME
|
||||
)
|
||||
self.default_settings: Dict[str, Any] = self.generate_default_settings()
|
||||
self.load()
|
||||
|
||||
|
@ -40,18 +45,6 @@ class Settings:
|
|||
"updater_errors": 0,
|
||||
}
|
||||
|
||||
def custom_runtime_specified(self) -> bool:
|
||||
return "container_runtime" in self.settings
|
||||
|
||||
def set_custom_runtime(self, runtime: str, autosave: bool = False) -> Path:
|
||||
from .container_utils import Runtime # Avoid circular import
|
||||
|
||||
container_runtime = Runtime.path_from_name(runtime)
|
||||
self.settings["container_runtime"] = str(container_runtime)
|
||||
if autosave:
|
||||
self.save()
|
||||
return container_runtime
|
||||
|
||||
def get(self, key: str) -> Any:
|
||||
return self.settings[key]
|
||||
|
||||
|
@ -98,6 +91,6 @@ class Settings:
|
|||
self.save()
|
||||
|
||||
def save(self) -> None:
|
||||
self.settings_filename.parent.mkdir(parents=True, exist_ok=True)
|
||||
with self.settings_filename.open("w") as settings_file:
|
||||
os.makedirs(self.dangerzone.appdata_path, exist_ok=True)
|
||||
with open(self.settings_filename, "w") as settings_file:
|
||||
json.dump(self.settings, settings_file, indent=4)
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import pathlib
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
import unicodedata
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import platformdirs
|
||||
|
@ -11,39 +11,40 @@ except ImportError:
|
|||
import appdirs as platformdirs
|
||||
|
||||
|
||||
def get_config_dir() -> Path:
|
||||
return Path(platformdirs.user_config_dir("dangerzone"))
|
||||
def get_config_dir() -> str:
|
||||
return platformdirs.user_config_dir("dangerzone")
|
||||
|
||||
|
||||
def get_resource_path(filename: str) -> Path:
|
||||
def get_resource_path(filename: str) -> str:
|
||||
if getattr(sys, "dangerzone_dev", False):
|
||||
# Look for resources directory relative to python file
|
||||
project_root = Path(__file__).parent.parent
|
||||
project_root = pathlib.Path(__file__).parent.parent
|
||||
prefix = project_root / "share"
|
||||
else:
|
||||
if platform.system() == "Darwin":
|
||||
bin_path = Path(sys.executable)
|
||||
bin_path = pathlib.Path(sys.executable)
|
||||
app_path = bin_path.parent.parent
|
||||
prefix = app_path / "Resources" / "share"
|
||||
elif platform.system() == "Linux":
|
||||
prefix = Path(sys.prefix) / "share" / "dangerzone"
|
||||
prefix = pathlib.Path(sys.prefix) / "share" / "dangerzone"
|
||||
elif platform.system() == "Windows":
|
||||
exe_path = Path(sys.executable)
|
||||
exe_path = pathlib.Path(sys.executable)
|
||||
dz_install_path = exe_path.parent
|
||||
prefix = dz_install_path / "share"
|
||||
else:
|
||||
raise NotImplementedError(f"Unsupported system {platform.system()}")
|
||||
return prefix / filename
|
||||
resource_path = prefix / filename
|
||||
return str(resource_path)
|
||||
|
||||
|
||||
def get_tessdata_dir() -> Path:
|
||||
def get_tessdata_dir() -> pathlib.Path:
|
||||
if getattr(sys, "dangerzone_dev", False) or platform.system() in (
|
||||
"Windows",
|
||||
"Darwin",
|
||||
):
|
||||
# Always use the tessdata path from the Dangerzone ./share directory, for
|
||||
# development builds, or in Windows/macOS platforms.
|
||||
return get_resource_path("tessdata")
|
||||
return pathlib.Path(get_resource_path("tessdata"))
|
||||
|
||||
# In case of Linux systems, grab the Tesseract data from any of the following
|
||||
# locations. We have found some of the locations through trial and error, whereas
|
||||
|
@ -54,11 +55,11 @@ def get_tessdata_dir() -> Path:
|
|||
#
|
||||
# [1] https://tesseract-ocr.github.io/tessdoc/Installation.html
|
||||
tessdata_dirs = [
|
||||
Path("/usr/share/tessdata/"), # on some Debian
|
||||
Path("/usr/share/tesseract/tessdata/"), # on Fedora
|
||||
Path("/usr/share/tesseract-ocr/tessdata/"), # ? (documented)
|
||||
Path("/usr/share/tesseract-ocr/4.00/tessdata/"), # on Debian Bullseye
|
||||
Path("/usr/share/tesseract-ocr/5/tessdata/"), # on Debian Trixie
|
||||
pathlib.Path("/usr/share/tessdata/"), # on some Debian
|
||||
pathlib.Path("/usr/share/tesseract/tessdata/"), # on Fedora
|
||||
pathlib.Path("/usr/share/tesseract-ocr/tessdata/"), # ? (documented)
|
||||
pathlib.Path("/usr/share/tesseract-ocr/4.00/tessdata/"), # on Debian Bullseye
|
||||
pathlib.Path("/usr/share/tesseract-ocr/5/tessdata/"), # on Debian Trixie
|
||||
]
|
||||
|
||||
for dir in tessdata_dirs:
|
||||
|
@ -70,7 +71,7 @@ def get_tessdata_dir() -> Path:
|
|||
|
||||
def get_version() -> str:
|
||||
try:
|
||||
with get_resource_path("version.txt").open() as f:
|
||||
with open(get_resource_path("version.txt")) as f:
|
||||
version = f.read().strip()
|
||||
except FileNotFoundError:
|
||||
# In dev mode, in Windows, get_resource_path doesn't work properly for the container, but luckily
|
||||
|
|
|
@ -27,7 +27,8 @@ This means that rebuilding the image without updating our Dockerfile will
|
|||
|
||||
Here are the necessary variables that make up our image in the `Dockerfile.env`
|
||||
file:
|
||||
* `DEBIAN_IMAGE_DIGEST`: The index digest for the Debian container image
|
||||
* `DEBIAN_IMAGE_DATE`: The date that the Debian container image was released
|
||||
* `DEBIAN_IMAGE_DIGEST`: The date that the Debian container image was released
|
||||
* `DEBIAN_ARCHIVE_DATE`: The Debian snapshot repo that we want to use
|
||||
* `GVISOR_ARCHIVE_DATE`: The gVisor APT repo that we want to use
|
||||
* `H2ORESTART_CHECKSUM`: The SHA-256 checksum of the H2ORestart plugin
|
||||
|
|
|
@ -21,25 +21,34 @@ def get_qt_app() -> Application:
|
|||
|
||||
def generate_isolated_updater(
|
||||
tmp_path: Path,
|
||||
mocker: MockerFixture,
|
||||
mock_app: bool = False,
|
||||
monkeypatch: MonkeyPatch,
|
||||
app_mocker: Optional[MockerFixture] = None,
|
||||
) -> UpdaterThread:
|
||||
"""Generate an Updater class with its own settings."""
|
||||
app = mocker.MagicMock() if mock_app else get_qt_app()
|
||||
if app_mocker:
|
||||
app = app_mocker.MagicMock()
|
||||
else:
|
||||
app = get_qt_app()
|
||||
|
||||
dummy = Dummy()
|
||||
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||
|
||||
# XXX: We can monkey-patch global state without wrapping it in a context manager, or
|
||||
# worrying that it will leak between tests, for two reasons:
|
||||
#
|
||||
# 1. Parallel tests in PyTest take place in different processes.
|
||||
# 2. The monkeypatch fixture tears down the monkey-patch after each test ends.
|
||||
monkeypatch.setattr(util, "get_config_dir", lambda: tmp_path)
|
||||
dangerzone = DangerzoneGui(app, isolation_provider=dummy)
|
||||
updater = UpdaterThread(dangerzone)
|
||||
return updater
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def updater(tmp_path: Path, mocker: MockerFixture) -> UpdaterThread:
|
||||
return generate_isolated_updater(tmp_path, mocker, mock_app=True)
|
||||
def updater(
|
||||
tmp_path: Path, monkeypatch: MonkeyPatch, mocker: MockerFixture
|
||||
) -> UpdaterThread:
|
||||
return generate_isolated_updater(tmp_path, monkeypatch, mocker)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def qt_updater(tmp_path: Path, mocker: MockerFixture) -> UpdaterThread:
|
||||
return generate_isolated_updater(tmp_path, mocker, mock_app=False)
|
||||
def qt_updater(tmp_path: Path, monkeypatch: MonkeyPatch) -> UpdaterThread:
|
||||
return generate_isolated_updater(tmp_path, monkeypatch)
|
||||
|
|
|
@ -48,7 +48,9 @@ def test_default_updater_settings(updater: UpdaterThread) -> None:
|
|||
)
|
||||
|
||||
|
||||
def test_pre_0_4_2_settings(tmp_path: Path, mocker: MockerFixture) -> None:
|
||||
def test_pre_0_4_2_settings(
|
||||
tmp_path: Path, monkeypatch: MonkeyPatch, mocker: MockerFixture
|
||||
) -> None:
|
||||
"""Check settings of installations prior to 0.4.2.
|
||||
|
||||
Check that installations that have been upgraded from a version < 0.4.2 to >= 0.4.2
|
||||
|
@ -56,7 +58,7 @@ def test_pre_0_4_2_settings(tmp_path: Path, mocker: MockerFixture) -> None:
|
|||
in their settings.json file.
|
||||
"""
|
||||
save_settings(tmp_path, default_settings_0_4_1())
|
||||
updater = generate_isolated_updater(tmp_path, mocker, mock_app=True)
|
||||
updater = generate_isolated_updater(tmp_path, monkeypatch, mocker)
|
||||
assert (
|
||||
updater.dangerzone.settings.get_updater_settings() == default_updater_settings()
|
||||
)
|
||||
|
@ -81,10 +83,12 @@ def test_post_0_4_2_settings(
|
|||
# version is 0.4.3.
|
||||
expected_settings = default_updater_settings()
|
||||
expected_settings["updater_latest_version"] = "0.4.3"
|
||||
monkeypatch.setattr(settings, "get_version", lambda: "0.4.3")
|
||||
monkeypatch.setattr(
|
||||
settings, "get_version", lambda: expected_settings["updater_latest_version"]
|
||||
)
|
||||
|
||||
# Ensure that the Settings class will correct the latest version field to 0.4.3.
|
||||
updater = generate_isolated_updater(tmp_path, mocker, mock_app=True)
|
||||
updater = generate_isolated_updater(tmp_path, monkeypatch, mocker)
|
||||
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
|
||||
|
||||
# Simulate an updater check that found a newer Dangerzone version (e.g., 0.4.4).
|
||||
|
@ -114,7 +118,9 @@ def test_linux_no_check(updater: UpdaterThread, monkeypatch: MonkeyPatch) -> Non
|
|||
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
|
||||
|
||||
|
||||
def test_user_prompts(updater: UpdaterThread, mocker: MockerFixture) -> None:
|
||||
def test_user_prompts(
|
||||
updater: UpdaterThread, monkeypatch: MonkeyPatch, mocker: MockerFixture
|
||||
) -> None:
|
||||
"""Test prompting users to ask them if they want to enable update checks."""
|
||||
# First run
|
||||
#
|
||||
|
@ -364,6 +370,8 @@ def test_update_errors(
|
|||
def test_update_check_prompt(
|
||||
qtbot: QtBot,
|
||||
qt_updater: UpdaterThread,
|
||||
monkeypatch: MonkeyPatch,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
"""Test that the prompt to enable update checks works properly."""
|
||||
# Force Dangerzone to check immediately for updates
|
||||
|
|
|
@ -5,8 +5,7 @@ import pytest
|
|||
from pytest_mock import MockerFixture
|
||||
from pytest_subprocess import FakeProcess
|
||||
|
||||
from dangerzone import errors
|
||||
from dangerzone.container_utils import Runtime
|
||||
from dangerzone import container_utils, errors
|
||||
from dangerzone.isolation_provider.container import Container
|
||||
from dangerzone.isolation_provider.qubes import is_qubes_native_conversion
|
||||
from dangerzone.util import get_resource_path
|
||||
|
@ -25,51 +24,42 @@ def provider() -> Container:
|
|||
return Container()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runtime_path() -> str:
|
||||
return str(Runtime().path)
|
||||
|
||||
|
||||
class TestContainer(IsolationProviderTest):
|
||||
def test_is_available_raises(
|
||||
self, provider: Container, fp: FakeProcess, runtime_path: str
|
||||
) -> None:
|
||||
def test_is_available_raises(self, provider: Container, fp: FakeProcess) -> None:
|
||||
"""
|
||||
NotAvailableContainerTechException should be raised when
|
||||
the "podman image ls" command fails.
|
||||
"""
|
||||
fp.register_subprocess(
|
||||
[runtime_path, "image", "ls"],
|
||||
[container_utils.get_runtime(), "image", "ls"],
|
||||
returncode=-1,
|
||||
stderr="podman image ls logs",
|
||||
)
|
||||
with pytest.raises(errors.NotAvailableContainerTechException):
|
||||
provider.is_available()
|
||||
|
||||
def test_is_available_works(
|
||||
self, provider: Container, fp: FakeProcess, runtime_path: str
|
||||
) -> None:
|
||||
def test_is_available_works(self, provider: Container, fp: FakeProcess) -> None:
|
||||
"""
|
||||
No exception should be raised when the "podman image ls" can return properly.
|
||||
"""
|
||||
fp.register_subprocess(
|
||||
[runtime_path, "image", "ls"],
|
||||
[container_utils.get_runtime(), "image", "ls"],
|
||||
)
|
||||
provider.is_available()
|
||||
|
||||
def test_install_raise_if_image_cant_be_installed(
|
||||
self, provider: Container, fp: FakeProcess, runtime_path: str
|
||||
self, provider: Container, fp: FakeProcess
|
||||
) -> None:
|
||||
"""When an image installation fails, an exception should be raised"""
|
||||
|
||||
fp.register_subprocess(
|
||||
[runtime_path, "image", "ls"],
|
||||
[container_utils.get_runtime(), "image", "ls"],
|
||||
)
|
||||
|
||||
# First check should return nothing.
|
||||
fp.register_subprocess(
|
||||
[
|
||||
runtime_path,
|
||||
container_utils.get_runtime(),
|
||||
"image",
|
||||
"list",
|
||||
"--format",
|
||||
|
@ -81,10 +71,10 @@ class TestContainer(IsolationProviderTest):
|
|||
|
||||
fp.register_subprocess(
|
||||
[
|
||||
runtime_path,
|
||||
container_utils.get_runtime(),
|
||||
"load",
|
||||
"-i",
|
||||
get_resource_path("container.tar").absolute(),
|
||||
get_resource_path("container.tar"),
|
||||
],
|
||||
returncode=-1,
|
||||
)
|
||||
|
@ -93,22 +83,22 @@ class TestContainer(IsolationProviderTest):
|
|||
provider.install()
|
||||
|
||||
def test_install_raises_if_still_not_installed(
|
||||
self, provider: Container, fp: FakeProcess, runtime_path: str
|
||||
self, provider: Container, fp: FakeProcess
|
||||
) -> None:
|
||||
"""When an image keep being not installed, it should return False"""
|
||||
fp.register_subprocess(
|
||||
[runtime_path, "version", "-f", "{{.Client.Version}}"],
|
||||
["podman", "version", "-f", "{{.Client.Version}}"],
|
||||
stdout="4.0.0",
|
||||
)
|
||||
|
||||
fp.register_subprocess(
|
||||
[runtime_path, "image", "ls"],
|
||||
[container_utils.get_runtime(), "image", "ls"],
|
||||
)
|
||||
|
||||
# First check should return nothing.
|
||||
fp.register_subprocess(
|
||||
[
|
||||
runtime_path,
|
||||
container_utils.get_runtime(),
|
||||
"image",
|
||||
"list",
|
||||
"--format",
|
||||
|
@ -120,10 +110,10 @@ class TestContainer(IsolationProviderTest):
|
|||
|
||||
fp.register_subprocess(
|
||||
[
|
||||
runtime_path,
|
||||
container_utils.get_runtime(),
|
||||
"load",
|
||||
"-i",
|
||||
get_resource_path("container.tar").absolute(),
|
||||
get_resource_path("container.tar"),
|
||||
],
|
||||
)
|
||||
with pytest.raises(errors.ImageNotPresentException):
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from dangerzone import errors
|
||||
from dangerzone.container_utils import Runtime
|
||||
from dangerzone.settings import Settings
|
||||
|
||||
|
||||
def test_get_runtime_name_from_settings(mocker: MockerFixture, tmp_path: Path) -> None:
|
||||
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||
mocker.patch("dangerzone.container_utils.Path.exists", return_value=True)
|
||||
|
||||
settings = Settings()
|
||||
settings.set("container_runtime", "/opt/somewhere/docker", autosave=True)
|
||||
|
||||
assert Runtime().name == "docker"
|
||||
|
||||
|
||||
def test_get_runtime_name_linux(mocker: MockerFixture, tmp_path: Path) -> None:
|
||||
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||
mocker.patch("platform.system", return_value="Linux")
|
||||
mocker.patch(
|
||||
"dangerzone.container_utils.shutil.which", return_value="/usr/bin/podman"
|
||||
)
|
||||
mocker.patch("dangerzone.container_utils.os.path.exists", return_value=True)
|
||||
runtime = Runtime()
|
||||
assert runtime.name == "podman"
|
||||
assert runtime.path == Path("/usr/bin/podman")
|
||||
|
||||
|
||||
def test_get_runtime_name_non_linux(mocker: MockerFixture, tmp_path: Path) -> None:
|
||||
mocker.patch("platform.system", return_value="Windows")
|
||||
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||
mocker.patch(
|
||||
"dangerzone.container_utils.shutil.which", return_value="/usr/bin/docker"
|
||||
)
|
||||
mocker.patch("dangerzone.container_utils.os.path.exists", return_value=True)
|
||||
runtime = Runtime()
|
||||
assert runtime.name == "docker"
|
||||
assert runtime.path == Path("/usr/bin/docker")
|
||||
|
||||
mocker.patch("platform.system", return_value="Something else")
|
||||
|
||||
runtime = Runtime()
|
||||
assert runtime.name == "docker"
|
||||
assert runtime.path == Path("/usr/bin/docker")
|
||||
assert Runtime().name == "docker"
|
||||
|
||||
|
||||
def test_get_unsupported_runtime_name(mocker: MockerFixture, tmp_path: Path) -> None:
|
||||
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||
settings = Settings()
|
||||
settings.set(
|
||||
"container_runtime", "/opt/somewhere/new-kid-on-the-block", autosave=True
|
||||
)
|
||||
|
||||
with pytest.raises(errors.UnsupportedContainerRuntime):
|
||||
assert Runtime().name == "new-kid-on-the-block"
|
|
@ -1,4 +1,5 @@
|
|||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import PropertyMock
|
||||
|
||||
|
@ -21,6 +22,13 @@ def default_settings_0_4_1() -> dict:
|
|||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def settings(tmp_path: Path, mocker: MockerFixture) -> Settings:
|
||||
dz_core = mocker.MagicMock()
|
||||
type(dz_core).appdata_path = PropertyMock(return_value=tmp_path)
|
||||
return Settings(dz_core)
|
||||
|
||||
|
||||
def save_settings(tmp_path: Path, settings: dict) -> None:
|
||||
"""Mimic the way Settings save a dictionary to a settings.json file."""
|
||||
settings_filename = tmp_path / "settings.json"
|
||||
|
@ -28,17 +36,10 @@ def save_settings(tmp_path: Path, settings: dict) -> None:
|
|||
json.dump(settings, settings_file, indent=4)
|
||||
|
||||
|
||||
def test_no_settings_file_creates_new_one(
|
||||
tmp_path: Path,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
def test_no_settings_file_creates_new_one(settings: Settings) -> None:
|
||||
"""Default settings file is created on first run"""
|
||||
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||
settings = Settings()
|
||||
|
||||
assert settings.settings_filename.is_file()
|
||||
with settings.settings_filename.open() as settings_file:
|
||||
new_settings_dict = json.load(settings_file)
|
||||
assert os.path.isfile(settings.settings_filename)
|
||||
new_settings_dict = json.load(open(settings.settings_filename))
|
||||
assert sorted(new_settings_dict.items()) == sorted(
|
||||
settings.generate_default_settings().items()
|
||||
)
|
||||
|
@ -47,12 +48,14 @@ def test_no_settings_file_creates_new_one(
|
|||
def test_corrupt_settings(tmp_path: Path, mocker: MockerFixture) -> None:
|
||||
# Set some broken settings file
|
||||
corrupt_settings_dict = "{:}"
|
||||
with (tmp_path / SETTINGS_FILENAME).open("w") as settings_file:
|
||||
with open(tmp_path / SETTINGS_FILENAME, "w") as settings_file:
|
||||
settings_file.write(corrupt_settings_dict)
|
||||
|
||||
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||
settings = Settings()
|
||||
assert settings.settings_filename.is_file()
|
||||
# Initialize settings
|
||||
dz_core = mocker.MagicMock()
|
||||
type(dz_core).appdata_path = PropertyMock(return_value=tmp_path)
|
||||
settings = Settings(dz_core)
|
||||
assert os.path.isfile(settings.settings_filename)
|
||||
|
||||
# Check if settings file was reset to the default
|
||||
new_settings_dict = json.load(open(settings.settings_filename))
|
||||
|
@ -63,7 +66,10 @@ def test_corrupt_settings(tmp_path: Path, mocker: MockerFixture) -> None:
|
|||
|
||||
|
||||
def test_new_default_setting(tmp_path: Path, mocker: MockerFixture) -> None:
|
||||
settings = Settings()
|
||||
# Initialize settings
|
||||
dz_core = mocker.MagicMock()
|
||||
type(dz_core).appdata_path = PropertyMock(return_value=tmp_path)
|
||||
settings = Settings(dz_core)
|
||||
settings.save()
|
||||
|
||||
# Ensure new default setting is imported into settings
|
||||
|
@ -72,12 +78,15 @@ def test_new_default_setting(tmp_path: Path, mocker: MockerFixture) -> None:
|
|||
return_value={"mock_setting": 1},
|
||||
)
|
||||
|
||||
settings2 = Settings()
|
||||
settings2 = Settings(dz_core)
|
||||
assert settings2.get("mock_setting") == 1
|
||||
|
||||
|
||||
def test_new_settings_added(tmp_path: Path, mocker: MockerFixture) -> None:
|
||||
settings = Settings()
|
||||
# Initialize settings
|
||||
dz_core = mocker.MagicMock()
|
||||
type(dz_core).appdata_path = PropertyMock(return_value=tmp_path)
|
||||
settings = Settings(dz_core)
|
||||
|
||||
# Add new setting
|
||||
settings.set("new_setting_autosaved", 20, autosave=True)
|
||||
|
@ -86,7 +95,7 @@ def test_new_settings_added(tmp_path: Path, mocker: MockerFixture) -> None:
|
|||
) # XXX has to be afterwards; otherwise this will be saved
|
||||
|
||||
# Simulate new app startup (settings recreation)
|
||||
settings2 = Settings()
|
||||
settings2 = Settings(dz_core)
|
||||
|
||||
# Check if new setting persisted
|
||||
assert 20 == settings2.get("new_setting_autosaved")
|
||||
|
|
|
@ -11,7 +11,7 @@ VERSION_FILE_NAME = "version.txt"
|
|||
|
||||
def test_get_resource_path() -> None:
|
||||
share_dir = Path("share").resolve()
|
||||
resource_path = util.get_resource_path(VERSION_FILE_NAME).parent
|
||||
resource_path = Path(util.get_resource_path(VERSION_FILE_NAME)).parent
|
||||
assert share_dir.samefile(resource_path), (
|
||||
f"{share_dir} is not the same file as {resource_path}"
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue