mirror of
https://github.com/freedomofpress/dangerzone.git
synced 2025-05-05 21:21:49 +02:00
Compare commits
12 commits
ff4704cf6c
...
0c450e8559
Author | SHA1 | Date | |
---|---|---|---|
0c450e8559 | |||
![]() |
006e6e292e | ||
![]() |
cc94c8ef5b | ||
![]() |
3abbbad2e5 | ||
![]() |
a70a684fca | ||
![]() |
0c2c9bb91a | ||
![]() |
b3c0f76060 | ||
![]() |
15e5fa7de8 | ||
![]() |
d14b209e79 | ||
![]() |
0a4140b713 | ||
![]() |
1353fa76d9 | ||
![]() |
5b5bef2a58 |
16 changed files with 228 additions and 168 deletions
|
@ -11,6 +11,15 @@ since 0.4.1, and this project adheres to [Semantic Versioning](https://semver.or
|
|||
- Platform support: Add support for Fedora 42 ([#1091](https://github.com/freedomofpress/dangerzone/issues/1091))
|
||||
- Platform support: Add support for Ubuntu 25.04 (Plucky Puffin)([#1090](https://github.com/freedomofpress/dangerzone/issues/1090))
|
||||
|
||||
## Added
|
||||
|
||||
- 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))
|
||||
|
||||
## [0.8.1](https://github.com/freedomofpress/dangerzone/compare/v0.8.1...0.8.0)
|
||||
|
||||
- Update the container image
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import logging
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
from typing import List, Tuple
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from . import errors
|
||||
from .settings import Settings
|
||||
from .util import get_resource_path, get_subprocess_startupinfo
|
||||
|
||||
CONTAINER_NAME = "dangerzone.rocks/dangerzone"
|
||||
|
@ -12,16 +15,26 @@ CONTAINER_NAME = "dangerzone.rocks/dangerzone"
|
|||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_runtime_name() -> str:
|
||||
if platform.system() == "Linux":
|
||||
runtime_name = "podman"
|
||||
else:
|
||||
# Windows, Darwin, and unknown use docker for now, dangerzone-vm eventually
|
||||
runtime_name = "docker"
|
||||
return runtime_name
|
||||
class Runtime(object):
|
||||
def __init__(self) -> None:
|
||||
settings = Settings()
|
||||
|
||||
if settings.custom_runtime_specified():
|
||||
self.path = Path(settings.get("container_runtime"))
|
||||
self.name = self.path.stem
|
||||
else:
|
||||
self.name = self.get_default_runtime_name()
|
||||
binary_path = shutil.which(self.name)
|
||||
if binary_path is None or not os.path.exists(binary_path):
|
||||
raise errors.NoContainerTechException(self.name)
|
||||
self.path = Path(binary_path)
|
||||
|
||||
@staticmethod
|
||||
def get_default_runtime_name() -> str:
|
||||
return "podman" if platform.system() == "Linux" else "docker"
|
||||
|
||||
|
||||
def get_runtime_version() -> Tuple[int, int]:
|
||||
def get_runtime_version(runtime: Optional[Runtime] = None) -> 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
|
||||
|
@ -30,14 +43,15 @@ def get_runtime_version() -> 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.
|
||||
runtime = get_runtime_name()
|
||||
if runtime == "podman":
|
||||
if runtime.name == "podman":
|
||||
query = "{{.Client.Version}}"
|
||||
else:
|
||||
query = "{{.Server.Version}}"
|
||||
|
||||
cmd = [runtime, "version", "-f", query]
|
||||
cmd = [str(runtime.path), "version", "-f", query]
|
||||
try:
|
||||
version = subprocess.run(
|
||||
cmd,
|
||||
|
@ -46,7 +60,7 @@ def get_runtime_version() -> Tuple[int, int]:
|
|||
check=True,
|
||||
).stdout.decode()
|
||||
except Exception as e:
|
||||
msg = f"Could not get the version of the {runtime.capitalize()} tool: {e}"
|
||||
msg = f"Could not get the version of the {runtime_name.capitalize()} tool: {e}"
|
||||
raise RuntimeError(msg) from e
|
||||
|
||||
# Parse this version and return the major/minor parts, since we don't need the
|
||||
|
@ -56,20 +70,12 @@ def get_runtime_version() -> Tuple[int, int]:
|
|||
return (int(major), int(minor))
|
||||
except Exception as e:
|
||||
msg = (
|
||||
f"Could not parse the version of the {runtime.capitalize()} tool"
|
||||
f"Could not parse the version of the {runtime_name.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.
|
||||
|
||||
|
@ -77,10 +83,11 @@ 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(
|
||||
[
|
||||
get_runtime(),
|
||||
str(runtime.path),
|
||||
"image",
|
||||
"list",
|
||||
"--format",
|
||||
|
@ -97,19 +104,21 @@ 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(
|
||||
[get_runtime(), "tag", image_id, new_tag],
|
||||
[str(runtime.path), "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(
|
||||
[get_runtime(), "rmi", "--force", tag],
|
||||
[str(runtime.name), "rmi", "--force", tag],
|
||||
startupinfo=get_subprocess_startupinfo(),
|
||||
)
|
||||
except Exception as e:
|
||||
|
@ -121,16 +130,17 @@ 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 open(get_resource_path("image-id.txt")) as f:
|
||||
with get_resource_path("image-id.txt").open() 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(
|
||||
[get_runtime(), "load", "-i", tarball_path],
|
||||
[str(runtime.path), "load", "-i", str(tarball_path)],
|
||||
startupinfo=get_subprocess_startupinfo(),
|
||||
capture_output=True,
|
||||
check=True,
|
||||
|
@ -155,7 +165,7 @@ def load_image_tarball() -> None:
|
|||
# `share/image-id.txt` and delete the incorrect tag.
|
||||
#
|
||||
# [1] https://github.com/containers/podman/issues/16490
|
||||
if get_runtime_name() == "podman" and get_runtime_version() == (3, 4):
|
||||
if runtime.name == "podman" and get_runtime_version(runtime) == (3, 4):
|
||||
expected_tag = get_expected_tag()
|
||||
bad_tag = f"localhost/{expected_tag}:latest"
|
||||
good_tag = f"{CONTAINER_NAME}:{expected_tag}"
|
||||
|
|
|
@ -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 open(get_resource_path("dangerzone.css"), "r") as f:
|
||||
with get_resource_path("dangerzone.css").open("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(path)
|
||||
return QtGui.QIcon(str(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(get_resource_path("icon.png")))
|
||||
QtGui.QPixmap.fromImage(QtGui.QImage(str(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(path)
|
||||
svg_renderer = QtSvg.QSvgRenderer(str(path))
|
||||
image = QtGui.QImage(width, height, QtGui.QImage.Format_ARGB32)
|
||||
# Set the ARGB to 0 to prevent rendering artifacts
|
||||
image.fill(0x00000000)
|
||||
|
@ -130,9 +130,8 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
|
||||
# Header
|
||||
logo = QtWidgets.QLabel()
|
||||
logo.setPixmap(
|
||||
QtGui.QPixmap.fromImage(QtGui.QImage(get_resource_path("icon.png")))
|
||||
)
|
||||
icon_path = str(get_resource_path("icon.png"))
|
||||
logo.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(icon_path)))
|
||||
header_label = QtWidgets.QLabel("Dangerzone")
|
||||
header_label.setFont(self.dangerzone.fixed_font)
|
||||
header_label.setStyleSheet("QLabel { font-weight: bold; font-size: 50px; }")
|
||||
|
@ -575,8 +574,15 @@ 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 platform.system() == "Linux":
|
||||
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":
|
||||
self.show_error(
|
||||
"<strong>Dangerzone requires Podman</strong><br><br>"
|
||||
"Install it and retry."
|
||||
|
@ -589,7 +595,12 @@ class WaitingWidgetContainer(WaitingWidget):
|
|||
)
|
||||
|
||||
elif state == "not_running":
|
||||
if platform.system() == "Linux":
|
||||
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":
|
||||
# "not_running" here means that the `podman image ls` command failed.
|
||||
message = (
|
||||
"<strong>Dangerzone requires Podman</strong><br><br>"
|
||||
|
@ -1222,7 +1233,7 @@ class DocumentsListWidget(QtWidgets.QListWidget):
|
|||
if not self.thread_pool_initized:
|
||||
max_jobs = self.dangerzone.isolation_provider.get_max_parallel_conversions()
|
||||
# Call freeze_support() to avoid passing unknown options to the subprocess.
|
||||
# See https://github.com/freedomofpress/dangerzone/issues/873
|
||||
# See https://github.com/freedomofpress/Adangerzone/issues/873
|
||||
freeze_support()
|
||||
self.thread_pool = ThreadPool(max_jobs)
|
||||
|
||||
|
@ -1306,7 +1317,7 @@ class DocumentWidget(QtWidgets.QWidget):
|
|||
|
||||
def load_status_image(self, filename: str) -> QtGui.QPixmap:
|
||||
path = get_resource_path(filename)
|
||||
img = QtGui.QImage(path)
|
||||
img = QtGui.QImage(str(path))
|
||||
image = QtGui.QPixmap.fromImage(img)
|
||||
return image.scaled(QtCore.QSize(15, 15))
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ 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
|
||||
|
@ -50,7 +51,8 @@ class Container(IsolationProvider):
|
|||
* Do not map the host user to the container, with `--userns nomap` (available
|
||||
from Podman 4.1 onwards)
|
||||
"""
|
||||
if container_utils.get_runtime_name() == "podman":
|
||||
runtime = Runtime()
|
||||
if runtime.name == "podman":
|
||||
security_args = ["--log-driver", "none"]
|
||||
security_args += ["--security-opt", "no-new-privileges"]
|
||||
if container_utils.get_runtime_version() >= (4, 1):
|
||||
|
@ -64,7 +66,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 = get_resource_path("seccomp.gvisor.json")
|
||||
seccomp_json_path = str(get_resource_path("seccomp.gvisor.json"))
|
||||
security_args += ["--security-opt", f"seccomp={seccomp_json_path}"]
|
||||
|
||||
security_args += ["--cap-drop", "all"]
|
||||
|
@ -123,12 +125,11 @@ class Container(IsolationProvider):
|
|||
|
||||
@staticmethod
|
||||
def is_available() -> bool:
|
||||
container_runtime = container_utils.get_runtime()
|
||||
runtime_name = container_utils.get_runtime_name()
|
||||
runtime = Runtime()
|
||||
|
||||
# Can we run `docker/podman image ls` without an error
|
||||
with subprocess.Popen(
|
||||
[container_runtime, "image", "ls"],
|
||||
[str(runtime.path), "image", "ls"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE,
|
||||
startupinfo=get_subprocess_startupinfo(),
|
||||
|
@ -136,14 +137,18 @@ 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 = ""
|
||||
if platform.system() != "Linux":
|
||||
runtime = Runtime()
|
||||
runtime_is_docker = runtime.name == "docker"
|
||||
platform_is_not_linux = platform.system() != "Linux"
|
||||
|
||||
if runtime_is_docker and platform_is_not_linux:
|
||||
with subprocess.Popen(
|
||||
["docker", "version", "--format", "{{.Server.Platform.Name}}"],
|
||||
stdout=subprocess.PIPE,
|
||||
|
@ -193,7 +198,7 @@ class Container(IsolationProvider):
|
|||
command: List[str],
|
||||
name: str,
|
||||
) -> subprocess.Popen:
|
||||
container_runtime = container_utils.get_runtime()
|
||||
runtime = Runtime()
|
||||
security_args = self.get_runtime_security_args()
|
||||
debug_args = []
|
||||
if self.debug:
|
||||
|
@ -215,7 +220,7 @@ class Container(IsolationProvider):
|
|||
+ image_name
|
||||
+ command
|
||||
)
|
||||
return self.exec([container_runtime] + args)
|
||||
return self.exec([str(runtime.path)] + args)
|
||||
|
||||
def kill_container(self, name: str) -> None:
|
||||
"""Terminate a spawned container.
|
||||
|
@ -227,8 +232,8 @@ class Container(IsolationProvider):
|
|||
connected to the Docker daemon, and killing it will just close the associated
|
||||
standard streams.
|
||||
"""
|
||||
container_runtime = container_utils.get_runtime()
|
||||
cmd = [container_runtime, "kill", name]
|
||||
runtime = Runtime()
|
||||
cmd = [str(runtime.path), "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
|
||||
|
@ -284,10 +289,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.
|
||||
container_runtime = container_utils.get_runtime()
|
||||
runtime = Runtime()
|
||||
name = self.doc_to_pixels_container_name(document)
|
||||
all_containers = subprocess.run(
|
||||
[container_runtime, "ps", "-a"],
|
||||
[str(runtime.path), "ps", "-a"],
|
||||
capture_output=True,
|
||||
startupinfo=get_subprocess_startupinfo(),
|
||||
)
|
||||
|
@ -298,6 +303,7 @@ class Container(IsolationProvider):
|
|||
# FIXME hardcoded 1 until length conversions are better handled
|
||||
# https://github.com/freedomofpress/dangerzone/issues/257
|
||||
return 1
|
||||
runtime = Runtime()
|
||||
|
||||
n_cpu = 1 # type: ignore [unreachable]
|
||||
if platform.system() == "Linux":
|
||||
|
@ -306,11 +312,11 @@ class Container(IsolationProvider):
|
|||
if cpu_count is not None:
|
||||
n_cpu = cpu_count
|
||||
|
||||
elif container_utils.get_runtime_name() == "docker":
|
||||
elif 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(
|
||||
[container_utils.get_runtime(), "info", "--format", "{{.NCPU}}"],
|
||||
[str(runtime.path), "info", "--format", "{{.NCPU}}"],
|
||||
text=True,
|
||||
startupinfo=get_subprocess_startupinfo(),
|
||||
)
|
||||
|
|
|
@ -130,7 +130,6 @@ 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)
|
||||
|
||||
container_image_path = get_resource_path("container.tar")
|
||||
return not os.path.exists(container_image_path)
|
||||
return not get_resource_path("container.tar").exists()
|
||||
else:
|
||||
return False
|
||||
|
|
|
@ -23,16 +23,13 @@ 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 open(get_resource_path("ocr-languages.json"), "r") as f:
|
||||
with get_resource_path("ocr-languages.json").open("r") as f:
|
||||
unsorted_ocr_languages = json.load(f)
|
||||
self.ocr_languages = dict(sorted(unsorted_ocr_languages.items()))
|
||||
|
||||
# Load settings
|
||||
self.settings = Settings(self)
|
||||
self.settings = Settings()
|
||||
self.documents: List[Document] = []
|
||||
self.isolation_provider = isolation_provider
|
||||
|
||||
|
|
|
@ -6,24 +6,18 @@ from typing import TYPE_CHECKING, Any, Dict
|
|||
from packaging import version
|
||||
|
||||
from .document import SAFE_EXTENSION
|
||||
from .util import get_version
|
||||
from .util import get_config_dir, 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, dangerzone: "DangerzoneCore") -> None:
|
||||
self.dangerzone = dangerzone
|
||||
self.settings_filename = os.path.join(
|
||||
self.dangerzone.appdata_path, SETTINGS_FILENAME
|
||||
)
|
||||
def __init__(self) -> None:
|
||||
self.settings_filename = get_config_dir() / SETTINGS_FILENAME
|
||||
self.default_settings: Dict[str, Any] = self.generate_default_settings()
|
||||
self.load()
|
||||
|
||||
|
@ -45,6 +39,9 @@ class Settings:
|
|||
"updater_errors": 0,
|
||||
}
|
||||
|
||||
def custom_runtime_specified(self) -> bool:
|
||||
return "container_runtime" in self.settings
|
||||
|
||||
def get(self, key: str) -> Any:
|
||||
return self.settings[key]
|
||||
|
||||
|
@ -91,6 +88,6 @@ class Settings:
|
|||
self.save()
|
||||
|
||||
def save(self) -> None:
|
||||
os.makedirs(self.dangerzone.appdata_path, exist_ok=True)
|
||||
with open(self.settings_filename, "w") as settings_file:
|
||||
self.settings_filename.parent.mkdir(parents=True, exist_ok=True)
|
||||
with self.settings_filename.open("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,40 +11,39 @@ except ImportError:
|
|||
import appdirs as platformdirs
|
||||
|
||||
|
||||
def get_config_dir() -> str:
|
||||
return platformdirs.user_config_dir("dangerzone")
|
||||
def get_config_dir() -> Path:
|
||||
return Path(platformdirs.user_config_dir("dangerzone"))
|
||||
|
||||
|
||||
def get_resource_path(filename: str) -> str:
|
||||
def get_resource_path(filename: str) -> Path:
|
||||
if getattr(sys, "dangerzone_dev", False):
|
||||
# Look for resources directory relative to python file
|
||||
project_root = pathlib.Path(__file__).parent.parent
|
||||
project_root = Path(__file__).parent.parent
|
||||
prefix = project_root / "share"
|
||||
else:
|
||||
if platform.system() == "Darwin":
|
||||
bin_path = pathlib.Path(sys.executable)
|
||||
bin_path = Path(sys.executable)
|
||||
app_path = bin_path.parent.parent
|
||||
prefix = app_path / "Resources" / "share"
|
||||
elif platform.system() == "Linux":
|
||||
prefix = pathlib.Path(sys.prefix) / "share" / "dangerzone"
|
||||
prefix = Path(sys.prefix) / "share" / "dangerzone"
|
||||
elif platform.system() == "Windows":
|
||||
exe_path = pathlib.Path(sys.executable)
|
||||
exe_path = Path(sys.executable)
|
||||
dz_install_path = exe_path.parent
|
||||
prefix = dz_install_path / "share"
|
||||
else:
|
||||
raise NotImplementedError(f"Unsupported system {platform.system()}")
|
||||
resource_path = prefix / filename
|
||||
return str(resource_path)
|
||||
return prefix / filename
|
||||
|
||||
|
||||
def get_tessdata_dir() -> pathlib.Path:
|
||||
def get_tessdata_dir() -> 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 pathlib.Path(get_resource_path("tessdata"))
|
||||
return 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
|
||||
|
@ -55,11 +54,11 @@ def get_tessdata_dir() -> pathlib.Path:
|
|||
#
|
||||
# [1] https://tesseract-ocr.github.io/tessdoc/Installation.html
|
||||
tessdata_dirs = [
|
||||
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
|
||||
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
|
||||
]
|
||||
|
||||
for dir in tessdata_dirs:
|
||||
|
@ -71,7 +70,7 @@ def get_tessdata_dir() -> pathlib.Path:
|
|||
|
||||
def get_version() -> str:
|
||||
try:
|
||||
with open(get_resource_path("version.txt")) as f:
|
||||
with get_resource_path("version.txt").open() 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
|
||||
|
|
|
@ -21,34 +21,25 @@ def get_qt_app() -> Application:
|
|||
|
||||
def generate_isolated_updater(
|
||||
tmp_path: Path,
|
||||
monkeypatch: MonkeyPatch,
|
||||
app_mocker: Optional[MockerFixture] = None,
|
||||
mocker: MockerFixture,
|
||||
mock_app: bool = False,
|
||||
) -> UpdaterThread:
|
||||
"""Generate an Updater class with its own settings."""
|
||||
if app_mocker:
|
||||
app = app_mocker.MagicMock()
|
||||
else:
|
||||
app = get_qt_app()
|
||||
app = mocker.MagicMock() if mock_app else get_qt_app()
|
||||
|
||||
dummy = Dummy()
|
||||
# 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)
|
||||
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||
|
||||
dangerzone = DangerzoneGui(app, isolation_provider=dummy)
|
||||
updater = UpdaterThread(dangerzone)
|
||||
return updater
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def updater(
|
||||
tmp_path: Path, monkeypatch: MonkeyPatch, mocker: MockerFixture
|
||||
) -> UpdaterThread:
|
||||
return generate_isolated_updater(tmp_path, monkeypatch, mocker)
|
||||
def updater(tmp_path: Path, mocker: MockerFixture) -> UpdaterThread:
|
||||
return generate_isolated_updater(tmp_path, mocker, mock_app=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def qt_updater(tmp_path: Path, monkeypatch: MonkeyPatch) -> UpdaterThread:
|
||||
return generate_isolated_updater(tmp_path, monkeypatch)
|
||||
def qt_updater(tmp_path: Path, mocker: MockerFixture) -> UpdaterThread:
|
||||
return generate_isolated_updater(tmp_path, mocker, mock_app=False)
|
||||
|
|
|
@ -48,9 +48,7 @@ def test_default_updater_settings(updater: UpdaterThread) -> None:
|
|||
)
|
||||
|
||||
|
||||
def test_pre_0_4_2_settings(
|
||||
tmp_path: Path, monkeypatch: MonkeyPatch, mocker: MockerFixture
|
||||
) -> None:
|
||||
def test_pre_0_4_2_settings(tmp_path: Path, 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
|
||||
|
@ -58,7 +56,7 @@ def test_pre_0_4_2_settings(
|
|||
in their settings.json file.
|
||||
"""
|
||||
save_settings(tmp_path, default_settings_0_4_1())
|
||||
updater = generate_isolated_updater(tmp_path, monkeypatch, mocker)
|
||||
updater = generate_isolated_updater(tmp_path, mocker, mock_app=True)
|
||||
assert (
|
||||
updater.dangerzone.settings.get_updater_settings() == default_updater_settings()
|
||||
)
|
||||
|
@ -83,12 +81,10 @@ 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: expected_settings["updater_latest_version"]
|
||||
)
|
||||
monkeypatch.setattr(settings, "get_version", lambda: "0.4.3")
|
||||
|
||||
# Ensure that the Settings class will correct the latest version field to 0.4.3.
|
||||
updater = generate_isolated_updater(tmp_path, monkeypatch, mocker)
|
||||
updater = generate_isolated_updater(tmp_path, mocker, mock_app=True)
|
||||
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
|
||||
|
||||
# Simulate an updater check that found a newer Dangerzone version (e.g., 0.4.4).
|
||||
|
@ -118,9 +114,7 @@ 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, monkeypatch: MonkeyPatch, mocker: MockerFixture
|
||||
) -> None:
|
||||
def test_user_prompts(updater: UpdaterThread, mocker: MockerFixture) -> None:
|
||||
"""Test prompting users to ask them if they want to enable update checks."""
|
||||
# First run
|
||||
#
|
||||
|
@ -370,8 +364,6 @@ 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,7 +5,8 @@ import pytest
|
|||
from pytest_mock import MockerFixture
|
||||
from pytest_subprocess import FakeProcess
|
||||
|
||||
from dangerzone import container_utils, errors
|
||||
from dangerzone import errors
|
||||
from dangerzone.container_utils import Runtime
|
||||
from dangerzone.isolation_provider.container import Container
|
||||
from dangerzone.isolation_provider.qubes import is_qubes_native_conversion
|
||||
from dangerzone.util import get_resource_path
|
||||
|
@ -24,42 +25,51 @@ 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) -> None:
|
||||
def test_is_available_raises(
|
||||
self, provider: Container, fp: FakeProcess, runtime_path: str
|
||||
) -> None:
|
||||
"""
|
||||
NotAvailableContainerTechException should be raised when
|
||||
the "podman image ls" command fails.
|
||||
"""
|
||||
fp.register_subprocess(
|
||||
[container_utils.get_runtime(), "image", "ls"],
|
||||
[runtime_path, "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) -> None:
|
||||
def test_is_available_works(
|
||||
self, provider: Container, fp: FakeProcess, runtime_path: str
|
||||
) -> None:
|
||||
"""
|
||||
No exception should be raised when the "podman image ls" can return properly.
|
||||
"""
|
||||
fp.register_subprocess(
|
||||
[container_utils.get_runtime(), "image", "ls"],
|
||||
[runtime_path, "image", "ls"],
|
||||
)
|
||||
provider.is_available()
|
||||
|
||||
def test_install_raise_if_image_cant_be_installed(
|
||||
self, provider: Container, fp: FakeProcess
|
||||
self, provider: Container, fp: FakeProcess, runtime_path: str
|
||||
) -> None:
|
||||
"""When an image installation fails, an exception should be raised"""
|
||||
|
||||
fp.register_subprocess(
|
||||
[container_utils.get_runtime(), "image", "ls"],
|
||||
[runtime_path, "image", "ls"],
|
||||
)
|
||||
|
||||
# First check should return nothing.
|
||||
fp.register_subprocess(
|
||||
[
|
||||
container_utils.get_runtime(),
|
||||
runtime_path,
|
||||
"image",
|
||||
"list",
|
||||
"--format",
|
||||
|
@ -71,10 +81,10 @@ class TestContainer(IsolationProviderTest):
|
|||
|
||||
fp.register_subprocess(
|
||||
[
|
||||
container_utils.get_runtime(),
|
||||
runtime_path,
|
||||
"load",
|
||||
"-i",
|
||||
get_resource_path("container.tar"),
|
||||
get_resource_path("container.tar").absolute(),
|
||||
],
|
||||
returncode=-1,
|
||||
)
|
||||
|
@ -83,22 +93,22 @@ class TestContainer(IsolationProviderTest):
|
|||
provider.install()
|
||||
|
||||
def test_install_raises_if_still_not_installed(
|
||||
self, provider: Container, fp: FakeProcess
|
||||
self, provider: Container, fp: FakeProcess, runtime_path: str
|
||||
) -> None:
|
||||
"""When an image keep being not installed, it should return False"""
|
||||
fp.register_subprocess(
|
||||
["podman", "version", "-f", "{{.Client.Version}}"],
|
||||
[runtime_path, "version", "-f", "{{.Client.Version}}"],
|
||||
stdout="4.0.0",
|
||||
)
|
||||
|
||||
fp.register_subprocess(
|
||||
[container_utils.get_runtime(), "image", "ls"],
|
||||
[runtime_path, "image", "ls"],
|
||||
)
|
||||
|
||||
# First check should return nothing.
|
||||
fp.register_subprocess(
|
||||
[
|
||||
container_utils.get_runtime(),
|
||||
runtime_path,
|
||||
"image",
|
||||
"list",
|
||||
"--format",
|
||||
|
@ -110,10 +120,10 @@ class TestContainer(IsolationProviderTest):
|
|||
|
||||
fp.register_subprocess(
|
||||
[
|
||||
container_utils.get_runtime(),
|
||||
runtime_path,
|
||||
"load",
|
||||
"-i",
|
||||
get_resource_path("container.tar"),
|
||||
get_resource_path("container.tar").absolute(),
|
||||
],
|
||||
)
|
||||
with pytest.raises(errors.ImageNotPresentException):
|
||||
|
|
48
tests/test_container_utils.py
Normal file
48
tests/test_container_utils.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
from pathlib import Path
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
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)
|
||||
|
||||
settings = Settings()
|
||||
settings.set(
|
||||
"container_runtime", "/opt/somewhere/new-kid-on-the-block", autosave=True
|
||||
)
|
||||
|
||||
assert Runtime().name == "new-kid-on-the-block"
|
||||
|
||||
|
||||
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"
|
|
@ -1,5 +1,4 @@
|
|||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import PropertyMock
|
||||
|
||||
|
@ -22,13 +21,6 @@ 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"
|
||||
|
@ -36,26 +28,31 @@ def save_settings(tmp_path: Path, settings: dict) -> None:
|
|||
json.dump(settings, settings_file, indent=4)
|
||||
|
||||
|
||||
def test_no_settings_file_creates_new_one(settings: Settings) -> None:
|
||||
def test_no_settings_file_creates_new_one(
|
||||
tmp_path: Path,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
"""Default settings file is created on first run"""
|
||||
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()
|
||||
)
|
||||
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 sorted(new_settings_dict.items()) == sorted(
|
||||
settings.generate_default_settings().items()
|
||||
)
|
||||
|
||||
|
||||
def test_corrupt_settings(tmp_path: Path, mocker: MockerFixture) -> None:
|
||||
# Set some broken settings file
|
||||
corrupt_settings_dict = "{:}"
|
||||
with open(tmp_path / SETTINGS_FILENAME, "w") as settings_file:
|
||||
with (tmp_path / SETTINGS_FILENAME).open("w") as settings_file:
|
||||
settings_file.write(corrupt_settings_dict)
|
||||
|
||||
# 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)
|
||||
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||
settings = Settings()
|
||||
assert settings.settings_filename.is_file()
|
||||
|
||||
# Check if settings file was reset to the default
|
||||
new_settings_dict = json.load(open(settings.settings_filename))
|
||||
|
@ -66,10 +63,7 @@ def test_corrupt_settings(tmp_path: Path, mocker: MockerFixture) -> None:
|
|||
|
||||
|
||||
def test_new_default_setting(tmp_path: Path, mocker: MockerFixture) -> None:
|
||||
# Initialize settings
|
||||
dz_core = mocker.MagicMock()
|
||||
type(dz_core).appdata_path = PropertyMock(return_value=tmp_path)
|
||||
settings = Settings(dz_core)
|
||||
settings = Settings()
|
||||
settings.save()
|
||||
|
||||
# Ensure new default setting is imported into settings
|
||||
|
@ -78,15 +72,12 @@ def test_new_default_setting(tmp_path: Path, mocker: MockerFixture) -> None:
|
|||
return_value={"mock_setting": 1},
|
||||
)
|
||||
|
||||
settings2 = Settings(dz_core)
|
||||
settings2 = Settings()
|
||||
assert settings2.get("mock_setting") == 1
|
||||
|
||||
|
||||
def test_new_settings_added(tmp_path: Path, mocker: MockerFixture) -> None:
|
||||
# Initialize settings
|
||||
dz_core = mocker.MagicMock()
|
||||
type(dz_core).appdata_path = PropertyMock(return_value=tmp_path)
|
||||
settings = Settings(dz_core)
|
||||
settings = Settings()
|
||||
|
||||
# Add new setting
|
||||
settings.set("new_setting_autosaved", 20, autosave=True)
|
||||
|
@ -95,7 +86,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(dz_core)
|
||||
settings2 = Settings()
|
||||
|
||||
# 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 = Path(util.get_resource_path(VERSION_FILE_NAME)).parent
|
||||
resource_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