mirror of
https://github.com/freedomofpress/dangerzone.git
synced 2025-05-05 21:21:49 +02:00
Merge 6c8a75732e
into dfcb74b427
This commit is contained in:
commit
cc4d0cd03d
18 changed files with 268 additions and 170 deletions
|
@ -16,6 +16,15 @@ 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)
|
- 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)
|
- 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))
|
||||||
|
|
||||||
## [0.8.1](https://github.com/freedomofpress/dangerzone/compare/v0.8.1...0.8.0)
|
## [0.8.1](https://github.com/freedomofpress/dangerzone/compare/v0.8.1...0.8.0)
|
||||||
|
|
||||||
- Update the container image
|
- Update the container image
|
||||||
|
|
|
@ -11,6 +11,7 @@ from .isolation_provider.container import Container
|
||||||
from .isolation_provider.dummy import Dummy
|
from .isolation_provider.dummy import Dummy
|
||||||
from .isolation_provider.qubes import Qubes, is_qubes_native_conversion
|
from .isolation_provider.qubes import Qubes, is_qubes_native_conversion
|
||||||
from .logic import DangerzoneCore
|
from .logic import DangerzoneCore
|
||||||
|
from .settings import Settings
|
||||||
from .util import get_version, replace_control_chars
|
from .util import get_version, replace_control_chars
|
||||||
|
|
||||||
|
|
||||||
|
@ -48,6 +49,11 @@ def print_header(s: str) -> None:
|
||||||
flag_value=True,
|
flag_value=True,
|
||||||
help="Run Dangerzone in debug mode, to get logs from gVisor.",
|
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")
|
@click.version_option(version=get_version(), message="%(version)s")
|
||||||
@errors.handle_document_errors
|
@errors.handle_document_errors
|
||||||
def cli_main(
|
def cli_main(
|
||||||
|
@ -57,8 +63,14 @@ def cli_main(
|
||||||
archive: bool,
|
archive: bool,
|
||||||
dummy_conversion: bool,
|
dummy_conversion: bool,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
|
set_container_runtime: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
setup_logging()
|
setup_logging()
|
||||||
|
display_banner()
|
||||||
|
if set_container_runtime:
|
||||||
|
settings = Settings()
|
||||||
|
settings.set("container_runtime", set_container_runtime, autosave=True)
|
||||||
|
click.echo(f"Set the settings container_runtime to {set_container_runtime}")
|
||||||
|
|
||||||
if getattr(sys, "dangerzone_dev", False) and dummy_conversion:
|
if getattr(sys, "dangerzone_dev", False) and dummy_conversion:
|
||||||
dangerzone = DangerzoneCore(Dummy())
|
dangerzone = DangerzoneCore(Dummy())
|
||||||
|
@ -67,7 +79,6 @@ def cli_main(
|
||||||
else:
|
else:
|
||||||
dangerzone = DangerzoneCore(Container(debug=debug))
|
dangerzone = DangerzoneCore(Container(debug=debug))
|
||||||
|
|
||||||
display_banner()
|
|
||||||
if len(filenames) == 1 and output_filename:
|
if len(filenames) == 1 and output_filename:
|
||||||
dangerzone.add_document_from_filename(filenames[0], output_filename, archive)
|
dangerzone.add_document_from_filename(filenames[0], output_filename, archive)
|
||||||
elif len(filenames) > 1 and output_filename:
|
elif len(filenames) > 1 and output_filename:
|
||||||
|
@ -320,4 +331,10 @@ def display_banner() -> None:
|
||||||
+ Style.DIM
|
+ Style.DIM
|
||||||
+ "│"
|
+ "│"
|
||||||
)
|
)
|
||||||
print(Back.BLACK + Fore.YELLOW + Style.DIM + "╰──────────────────────────╯")
|
print(
|
||||||
|
Back.BLACK
|
||||||
|
+ Fore.YELLOW
|
||||||
|
+ Style.DIM
|
||||||
|
+ "╰──────────────────────────╯"
|
||||||
|
+ Style.RESET_ALL
|
||||||
|
)
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import platform
|
import platform
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import List, Tuple
|
from pathlib import Path
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
from . import errors
|
from . import errors
|
||||||
|
from .settings import Settings
|
||||||
from .util import get_resource_path, get_subprocess_startupinfo
|
from .util import get_resource_path, get_subprocess_startupinfo
|
||||||
|
|
||||||
CONTAINER_NAME = "dangerzone.rocks/dangerzone"
|
CONTAINER_NAME = "dangerzone.rocks/dangerzone"
|
||||||
|
@ -12,16 +15,31 @@ CONTAINER_NAME = "dangerzone.rocks/dangerzone"
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_runtime_name() -> str:
|
class Runtime(object):
|
||||||
if platform.system() == "Linux":
|
def __init__(self) -> None:
|
||||||
runtime_name = "podman"
|
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
|
||||||
else:
|
else:
|
||||||
# Windows, Darwin, and unknown use docker for now, dangerzone-vm eventually
|
self.name = self.get_default_runtime_name()
|
||||||
runtime_name = "docker"
|
binary_path = shutil.which(self.name)
|
||||||
return runtime_name
|
if binary_path is None or not os.path.exists(binary_path):
|
||||||
|
raise errors.NoContainerTechException(self.name)
|
||||||
|
self.path = Path(binary_path)
|
||||||
|
|
||||||
|
if self.name not in ("podman", "docker"):
|
||||||
|
raise errors.UnsupportedContainerRuntime(self.name)
|
||||||
|
|
||||||
|
@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.
|
"""Get the major/minor parts of the Docker/Podman version.
|
||||||
|
|
||||||
Some of the operations we perform in this module rely on some Podman features
|
Some of the operations we perform in this module rely on some Podman features
|
||||||
|
@ -30,14 +48,15 @@ def get_runtime_version() -> Tuple[int, int]:
|
||||||
just knowing the major and minor version, since writing/installing a full-blown
|
just knowing the major and minor version, since writing/installing a full-blown
|
||||||
semver parser is an overkill.
|
semver parser is an overkill.
|
||||||
"""
|
"""
|
||||||
|
runtime = runtime or Runtime()
|
||||||
|
|
||||||
# Get the Docker/Podman version, using a Go template.
|
# Get the Docker/Podman version, using a Go template.
|
||||||
runtime = get_runtime_name()
|
if runtime.name == "podman":
|
||||||
if runtime == "podman":
|
|
||||||
query = "{{.Client.Version}}"
|
query = "{{.Client.Version}}"
|
||||||
else:
|
else:
|
||||||
query = "{{.Server.Version}}"
|
query = "{{.Server.Version}}"
|
||||||
|
|
||||||
cmd = [runtime, "version", "-f", query]
|
cmd = [str(runtime.path), "version", "-f", query]
|
||||||
try:
|
try:
|
||||||
version = subprocess.run(
|
version = subprocess.run(
|
||||||
cmd,
|
cmd,
|
||||||
|
@ -46,7 +65,7 @@ def get_runtime_version() -> Tuple[int, int]:
|
||||||
check=True,
|
check=True,
|
||||||
).stdout.decode()
|
).stdout.decode()
|
||||||
except Exception as e:
|
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
|
raise RuntimeError(msg) from e
|
||||||
|
|
||||||
# Parse this version and return the major/minor parts, since we don't need the
|
# Parse this version and return the major/minor parts, since we don't need the
|
||||||
|
@ -56,20 +75,12 @@ def get_runtime_version() -> Tuple[int, int]:
|
||||||
return (int(major), int(minor))
|
return (int(major), int(minor))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
msg = (
|
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}"
|
f" (found: '{version}') due to the following error: {e}"
|
||||||
)
|
)
|
||||||
raise RuntimeError(msg)
|
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]:
|
def list_image_tags() -> List[str]:
|
||||||
"""Get the tags of all loaded Dangerzone images.
|
"""Get the tags of all loaded Dangerzone images.
|
||||||
|
|
||||||
|
@ -77,10 +88,11 @@ def list_image_tags() -> List[str]:
|
||||||
images. This can be useful when we want to find which are the local image tags,
|
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.
|
and which image ID does the "latest" tag point to.
|
||||||
"""
|
"""
|
||||||
|
runtime = Runtime()
|
||||||
return (
|
return (
|
||||||
subprocess.check_output(
|
subprocess.check_output(
|
||||||
[
|
[
|
||||||
get_runtime(),
|
str(runtime.path),
|
||||||
"image",
|
"image",
|
||||||
"list",
|
"list",
|
||||||
"--format",
|
"--format",
|
||||||
|
@ -97,19 +109,21 @@ def list_image_tags() -> List[str]:
|
||||||
|
|
||||||
def add_image_tag(image_id: str, new_tag: str) -> None:
|
def add_image_tag(image_id: str, new_tag: str) -> None:
|
||||||
"""Add a tag to the Dangerzone image."""
|
"""Add a tag to the Dangerzone image."""
|
||||||
|
runtime = Runtime()
|
||||||
log.debug(f"Adding tag '{new_tag}' to image '{image_id}'")
|
log.debug(f"Adding tag '{new_tag}' to image '{image_id}'")
|
||||||
subprocess.check_output(
|
subprocess.check_output(
|
||||||
[get_runtime(), "tag", image_id, new_tag],
|
[str(runtime.path), "tag", image_id, new_tag],
|
||||||
startupinfo=get_subprocess_startupinfo(),
|
startupinfo=get_subprocess_startupinfo(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def delete_image_tag(tag: str) -> None:
|
def delete_image_tag(tag: str) -> None:
|
||||||
"""Delete a Dangerzone image tag."""
|
"""Delete a Dangerzone image tag."""
|
||||||
|
runtime = Runtime()
|
||||||
log.warning(f"Deleting old container image: {tag}")
|
log.warning(f"Deleting old container image: {tag}")
|
||||||
try:
|
try:
|
||||||
subprocess.check_output(
|
subprocess.check_output(
|
||||||
[get_runtime(), "rmi", "--force", tag],
|
[str(runtime.name), "rmi", "--force", tag],
|
||||||
startupinfo=get_subprocess_startupinfo(),
|
startupinfo=get_subprocess_startupinfo(),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -121,16 +135,17 @@ def delete_image_tag(tag: str) -> None:
|
||||||
|
|
||||||
def get_expected_tag() -> str:
|
def get_expected_tag() -> str:
|
||||||
"""Get the tag of the Dangerzone image tarball from the image-id.txt file."""
|
"""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()
|
return f.read().strip()
|
||||||
|
|
||||||
|
|
||||||
def load_image_tarball() -> None:
|
def load_image_tarball() -> None:
|
||||||
|
runtime = Runtime()
|
||||||
log.info("Installing Dangerzone container image...")
|
log.info("Installing Dangerzone container image...")
|
||||||
tarball_path = get_resource_path("container.tar")
|
tarball_path = get_resource_path("container.tar")
|
||||||
try:
|
try:
|
||||||
res = subprocess.run(
|
res = subprocess.run(
|
||||||
[get_runtime(), "load", "-i", tarball_path],
|
[str(runtime.path), "load", "-i", str(tarball_path)],
|
||||||
startupinfo=get_subprocess_startupinfo(),
|
startupinfo=get_subprocess_startupinfo(),
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
check=True,
|
check=True,
|
||||||
|
@ -155,7 +170,7 @@ def load_image_tarball() -> None:
|
||||||
# `share/image-id.txt` and delete the incorrect tag.
|
# `share/image-id.txt` and delete the incorrect tag.
|
||||||
#
|
#
|
||||||
# [1] https://github.com/containers/podman/issues/16490
|
# [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()
|
expected_tag = get_expected_tag()
|
||||||
bad_tag = f"localhost/{expected_tag}:latest"
|
bad_tag = f"localhost/{expected_tag}:latest"
|
||||||
good_tag = f"{CONTAINER_NAME}:{expected_tag}"
|
good_tag = f"{CONTAINER_NAME}:{expected_tag}"
|
||||||
|
|
|
@ -140,3 +140,7 @@ class NotAvailableContainerTechException(Exception):
|
||||||
self.error = error
|
self.error = error
|
||||||
self.container_tech = container_tech
|
self.container_tech = container_tech
|
||||||
super().__init__(f"{container_tech} is not available")
|
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:
|
def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
|
||||||
super(Application, self).__init__(*args, **kwargs)
|
super(Application, self).__init__(*args, **kwargs)
|
||||||
self.setQuitOnLastWindowClosed(False)
|
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()
|
style = f.read()
|
||||||
self.setStyleSheet(style)
|
self.setStyleSheet(style)
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,7 @@ class DangerzoneGui(DangerzoneCore):
|
||||||
path = get_resource_path("dangerzone.ico")
|
path = get_resource_path("dangerzone.ico")
|
||||||
else:
|
else:
|
||||||
path = get_resource_path("icon.png")
|
path = get_resource_path("icon.png")
|
||||||
return QtGui.QIcon(path)
|
return QtGui.QIcon(str(path))
|
||||||
|
|
||||||
def open_pdf_viewer(self, filename: str) -> None:
|
def open_pdf_viewer(self, filename: str) -> None:
|
||||||
if platform.system() == "Darwin":
|
if platform.system() == "Darwin":
|
||||||
|
@ -252,7 +252,7 @@ class Alert(Dialog):
|
||||||
def create_layout(self) -> QtWidgets.QBoxLayout:
|
def create_layout(self) -> QtWidgets.QBoxLayout:
|
||||||
logo = QtWidgets.QLabel()
|
logo = QtWidgets.QLabel()
|
||||||
logo.setPixmap(
|
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()
|
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
|
This answer is basically taken from: https://stackoverflow.com/a/25689790
|
||||||
"""
|
"""
|
||||||
path = get_resource_path(filename)
|
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)
|
image = QtGui.QImage(width, height, QtGui.QImage.Format_ARGB32)
|
||||||
# Set the ARGB to 0 to prevent rendering artifacts
|
# Set the ARGB to 0 to prevent rendering artifacts
|
||||||
image.fill(0x00000000)
|
image.fill(0x00000000)
|
||||||
|
@ -130,9 +130,8 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||||
|
|
||||||
# Header
|
# Header
|
||||||
logo = QtWidgets.QLabel()
|
logo = QtWidgets.QLabel()
|
||||||
logo.setPixmap(
|
icon_path = str(get_resource_path("icon.png"))
|
||||||
QtGui.QPixmap.fromImage(QtGui.QImage(get_resource_path("icon.png")))
|
logo.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(icon_path)))
|
||||||
)
|
|
||||||
header_label = QtWidgets.QLabel("Dangerzone")
|
header_label = QtWidgets.QLabel("Dangerzone")
|
||||||
header_label.setFont(self.dangerzone.fixed_font)
|
header_label.setFont(self.dangerzone.fixed_font)
|
||||||
header_label.setStyleSheet("QLabel { font-weight: bold; font-size: 50px; }")
|
header_label.setStyleSheet("QLabel { font-weight: bold; font-size: 50px; }")
|
||||||
|
@ -575,8 +574,15 @@ class WaitingWidgetContainer(WaitingWidget):
|
||||||
self.finished.emit()
|
self.finished.emit()
|
||||||
|
|
||||||
def state_change(self, state: str, error: Optional[str] = None) -> None:
|
def state_change(self, state: str, error: Optional[str] = None) -> None:
|
||||||
|
custom_runtime = self.dangerzone.settings.custom_runtime_specified()
|
||||||
|
|
||||||
if state == "not_installed":
|
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(
|
self.show_error(
|
||||||
"<strong>Dangerzone requires Podman</strong><br><br>"
|
"<strong>Dangerzone requires Podman</strong><br><br>"
|
||||||
"Install it and retry."
|
"Install it and retry."
|
||||||
|
@ -589,7 +595,12 @@ class WaitingWidgetContainer(WaitingWidget):
|
||||||
)
|
)
|
||||||
|
|
||||||
elif state == "not_running":
|
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.
|
# "not_running" here means that the `podman image ls` command failed.
|
||||||
message = (
|
message = (
|
||||||
"<strong>Dangerzone requires Podman</strong><br><br>"
|
"<strong>Dangerzone requires Podman</strong><br><br>"
|
||||||
|
@ -1306,7 +1317,7 @@ class DocumentWidget(QtWidgets.QWidget):
|
||||||
|
|
||||||
def load_status_image(self, filename: str) -> QtGui.QPixmap:
|
def load_status_image(self, filename: str) -> QtGui.QPixmap:
|
||||||
path = get_resource_path(filename)
|
path = get_resource_path(filename)
|
||||||
img = QtGui.QImage(path)
|
img = QtGui.QImage(str(path))
|
||||||
image = QtGui.QPixmap.fromImage(img)
|
image = QtGui.QPixmap.fromImage(img)
|
||||||
return image.scaled(QtCore.QSize(15, 15))
|
return image.scaled(QtCore.QSize(15, 15))
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import subprocess
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
from .. import container_utils, errors
|
from .. import container_utils, errors
|
||||||
|
from ..container_utils import Runtime
|
||||||
from ..document import Document
|
from ..document import Document
|
||||||
from ..util import get_resource_path, get_subprocess_startupinfo
|
from ..util import get_resource_path, get_subprocess_startupinfo
|
||||||
from .base import IsolationProvider, terminate_process_group
|
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
|
* Do not map the host user to the container, with `--userns nomap` (available
|
||||||
from Podman 4.1 onwards)
|
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 = ["--log-driver", "none"]
|
||||||
security_args += ["--security-opt", "no-new-privileges"]
|
security_args += ["--security-opt", "no-new-privileges"]
|
||||||
if container_utils.get_runtime_version() >= (4, 1):
|
if container_utils.get_runtime_version() >= (4, 1):
|
||||||
|
@ -64,7 +66,7 @@ class Container(IsolationProvider):
|
||||||
#
|
#
|
||||||
# [1] https://github.com/freedomofpress/dangerzone/issues/846
|
# [1] https://github.com/freedomofpress/dangerzone/issues/846
|
||||||
# [2] https://github.com/containers/common/blob/d3283f8401eeeb21f3c59a425b5461f069e199a7/pkg/seccomp/seccomp.json
|
# [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 += ["--security-opt", f"seccomp={seccomp_json_path}"]
|
||||||
|
|
||||||
security_args += ["--cap-drop", "all"]
|
security_args += ["--cap-drop", "all"]
|
||||||
|
@ -123,12 +125,11 @@ class Container(IsolationProvider):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_available() -> bool:
|
def is_available() -> bool:
|
||||||
container_runtime = container_utils.get_runtime()
|
runtime = Runtime()
|
||||||
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"],
|
[str(runtime.path), "image", "ls"],
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
startupinfo=get_subprocess_startupinfo(),
|
startupinfo=get_subprocess_startupinfo(),
|
||||||
|
@ -136,14 +137,18 @@ class Container(IsolationProvider):
|
||||||
_, stderr = p.communicate()
|
_, stderr = p.communicate()
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
raise errors.NotAvailableContainerTechException(
|
raise errors.NotAvailableContainerTechException(
|
||||||
runtime_name, stderr.decode()
|
runtime.name, stderr.decode()
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def check_docker_desktop_version(self) -> Tuple[bool, str]:
|
def check_docker_desktop_version(self) -> Tuple[bool, str]:
|
||||||
# On windows and darwin, check that the minimum version is met
|
# On windows and darwin, check that the minimum version is met
|
||||||
version = ""
|
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(
|
with subprocess.Popen(
|
||||||
["docker", "version", "--format", "{{.Server.Platform.Name}}"],
|
["docker", "version", "--format", "{{.Server.Platform.Name}}"],
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
|
@ -193,7 +198,7 @@ class Container(IsolationProvider):
|
||||||
command: List[str],
|
command: List[str],
|
||||||
name: str,
|
name: str,
|
||||||
) -> subprocess.Popen:
|
) -> subprocess.Popen:
|
||||||
container_runtime = container_utils.get_runtime()
|
runtime = Runtime()
|
||||||
security_args = self.get_runtime_security_args()
|
security_args = self.get_runtime_security_args()
|
||||||
debug_args = []
|
debug_args = []
|
||||||
if self.debug:
|
if self.debug:
|
||||||
|
@ -215,7 +220,7 @@ class Container(IsolationProvider):
|
||||||
+ image_name
|
+ image_name
|
||||||
+ command
|
+ command
|
||||||
)
|
)
|
||||||
return self.exec([container_runtime] + args)
|
return self.exec([str(runtime.path)] + args)
|
||||||
|
|
||||||
def kill_container(self, name: str) -> None:
|
def kill_container(self, name: str) -> None:
|
||||||
"""Terminate a spawned container.
|
"""Terminate a spawned container.
|
||||||
|
@ -227,8 +232,8 @@ class Container(IsolationProvider):
|
||||||
connected to the Docker daemon, and killing it will just close the associated
|
connected to the Docker daemon, and killing it will just close the associated
|
||||||
standard streams.
|
standard streams.
|
||||||
"""
|
"""
|
||||||
container_runtime = container_utils.get_runtime()
|
runtime = Runtime()
|
||||||
cmd = [container_runtime, "kill", name]
|
cmd = [str(runtime.path), "kill", name]
|
||||||
try:
|
try:
|
||||||
# We do not check the exit code of the process here, since the container may
|
# 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
|
# 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,
|
# 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
|
# else the container runtime (Docker/Podman) has experienced a problem, and we
|
||||||
# should report it.
|
# should report it.
|
||||||
container_runtime = container_utils.get_runtime()
|
runtime = Runtime()
|
||||||
name = self.doc_to_pixels_container_name(document)
|
name = self.doc_to_pixels_container_name(document)
|
||||||
all_containers = subprocess.run(
|
all_containers = subprocess.run(
|
||||||
[container_runtime, "ps", "-a"],
|
[str(runtime.path), "ps", "-a"],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
startupinfo=get_subprocess_startupinfo(),
|
startupinfo=get_subprocess_startupinfo(),
|
||||||
)
|
)
|
||||||
|
@ -298,19 +303,20 @@ class Container(IsolationProvider):
|
||||||
# FIXME hardcoded 1 until length conversions are better handled
|
# FIXME hardcoded 1 until length conversions are better handled
|
||||||
# https://github.com/freedomofpress/dangerzone/issues/257
|
# https://github.com/freedomofpress/dangerzone/issues/257
|
||||||
return 1
|
return 1
|
||||||
|
runtime = Runtime() # type: ignore [unreachable]
|
||||||
|
|
||||||
n_cpu = 1 # type: ignore [unreachable]
|
n_cpu = 1
|
||||||
if platform.system() == "Linux":
|
if platform.system() == "Linux":
|
||||||
# if on linux containers run natively
|
# if on linux containers run natively
|
||||||
cpu_count = os.cpu_count()
|
cpu_count = os.cpu_count()
|
||||||
if cpu_count is not None:
|
if cpu_count is not None:
|
||||||
n_cpu = cpu_count
|
n_cpu = cpu_count
|
||||||
|
|
||||||
elif container_utils.get_runtime_name() == "docker":
|
elif runtime.name == "docker":
|
||||||
# For Windows and MacOS containers run in VM
|
# For Windows and MacOS containers run in VM
|
||||||
# So we obtain the CPU count for the VM
|
# So we obtain the CPU count for the VM
|
||||||
n_cpu_str = subprocess.check_output(
|
n_cpu_str = subprocess.check_output(
|
||||||
[container_utils.get_runtime(), "info", "--format", "{{.NCPU}}"],
|
[str(runtime.path), "info", "--format", "{{.NCPU}}"],
|
||||||
text=True,
|
text=True,
|
||||||
startupinfo=get_subprocess_startupinfo(),
|
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
|
# This disambiguates if it is running a Qubes targetted build or not
|
||||||
# (Qubes-specific builds don't ship the container image)
|
# (Qubes-specific builds don't ship the container image)
|
||||||
|
|
||||||
container_image_path = get_resource_path("container.tar")
|
return not get_resource_path("container.tar").exists()
|
||||||
return not os.path.exists(container_image_path)
|
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -23,16 +23,13 @@ class DangerzoneCore(object):
|
||||||
# Initialize terminal colors
|
# Initialize terminal colors
|
||||||
colorama.init(autoreset=True)
|
colorama.init(autoreset=True)
|
||||||
|
|
||||||
# App data folder
|
|
||||||
self.appdata_path = util.get_config_dir()
|
|
||||||
|
|
||||||
# Languages supported by tesseract
|
# 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)
|
unsorted_ocr_languages = json.load(f)
|
||||||
self.ocr_languages = dict(sorted(unsorted_ocr_languages.items()))
|
self.ocr_languages = dict(sorted(unsorted_ocr_languages.items()))
|
||||||
|
|
||||||
# Load settings
|
# Load settings
|
||||||
self.settings = Settings(self)
|
self.settings = Settings()
|
||||||
self.documents: List[Document] = []
|
self.documents: List[Document] = []
|
||||||
self.isolation_provider = isolation_provider
|
self.isolation_provider = isolation_provider
|
||||||
|
|
||||||
|
|
|
@ -6,24 +6,18 @@ from typing import TYPE_CHECKING, Any, Dict
|
||||||
from packaging import version
|
from packaging import version
|
||||||
|
|
||||||
from .document import SAFE_EXTENSION
|
from .document import SAFE_EXTENSION
|
||||||
from .util import get_version
|
from .util import get_config_dir, get_version
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .logic import DangerzoneCore
|
|
||||||
|
|
||||||
SETTINGS_FILENAME: str = "settings.json"
|
SETTINGS_FILENAME: str = "settings.json"
|
||||||
|
|
||||||
|
|
||||||
class Settings:
|
class Settings:
|
||||||
settings: Dict[str, Any]
|
settings: Dict[str, Any]
|
||||||
|
|
||||||
def __init__(self, dangerzone: "DangerzoneCore") -> None:
|
def __init__(self) -> None:
|
||||||
self.dangerzone = dangerzone
|
self.settings_filename = get_config_dir() / SETTINGS_FILENAME
|
||||||
self.settings_filename = os.path.join(
|
|
||||||
self.dangerzone.appdata_path, SETTINGS_FILENAME
|
|
||||||
)
|
|
||||||
self.default_settings: Dict[str, Any] = self.generate_default_settings()
|
self.default_settings: Dict[str, Any] = self.generate_default_settings()
|
||||||
self.load()
|
self.load()
|
||||||
|
|
||||||
|
@ -45,6 +39,9 @@ class Settings:
|
||||||
"updater_errors": 0,
|
"updater_errors": 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def custom_runtime_specified(self) -> bool:
|
||||||
|
return "container_runtime" in self.settings
|
||||||
|
|
||||||
def get(self, key: str) -> Any:
|
def get(self, key: str) -> Any:
|
||||||
return self.settings[key]
|
return self.settings[key]
|
||||||
|
|
||||||
|
@ -91,6 +88,6 @@ class Settings:
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def save(self) -> None:
|
def save(self) -> None:
|
||||||
os.makedirs(self.dangerzone.appdata_path, exist_ok=True)
|
self.settings_filename.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with open(self.settings_filename, "w") as settings_file:
|
with self.settings_filename.open("w") as settings_file:
|
||||||
json.dump(self.settings, settings_file, indent=4)
|
json.dump(self.settings, settings_file, indent=4)
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import pathlib
|
|
||||||
import platform
|
import platform
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
import unicodedata
|
import unicodedata
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import platformdirs
|
import platformdirs
|
||||||
|
@ -11,40 +11,39 @@ except ImportError:
|
||||||
import appdirs as platformdirs
|
import appdirs as platformdirs
|
||||||
|
|
||||||
|
|
||||||
def get_config_dir() -> str:
|
def get_config_dir() -> Path:
|
||||||
return platformdirs.user_config_dir("dangerzone")
|
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):
|
if getattr(sys, "dangerzone_dev", False):
|
||||||
# Look for resources directory relative to python file
|
# 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"
|
prefix = project_root / "share"
|
||||||
else:
|
else:
|
||||||
if platform.system() == "Darwin":
|
if platform.system() == "Darwin":
|
||||||
bin_path = pathlib.Path(sys.executable)
|
bin_path = Path(sys.executable)
|
||||||
app_path = bin_path.parent.parent
|
app_path = bin_path.parent.parent
|
||||||
prefix = app_path / "Resources" / "share"
|
prefix = app_path / "Resources" / "share"
|
||||||
elif platform.system() == "Linux":
|
elif platform.system() == "Linux":
|
||||||
prefix = pathlib.Path(sys.prefix) / "share" / "dangerzone"
|
prefix = Path(sys.prefix) / "share" / "dangerzone"
|
||||||
elif platform.system() == "Windows":
|
elif platform.system() == "Windows":
|
||||||
exe_path = pathlib.Path(sys.executable)
|
exe_path = Path(sys.executable)
|
||||||
dz_install_path = exe_path.parent
|
dz_install_path = exe_path.parent
|
||||||
prefix = dz_install_path / "share"
|
prefix = dz_install_path / "share"
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(f"Unsupported system {platform.system()}")
|
raise NotImplementedError(f"Unsupported system {platform.system()}")
|
||||||
resource_path = prefix / filename
|
return prefix / filename
|
||||||
return str(resource_path)
|
|
||||||
|
|
||||||
|
|
||||||
def get_tessdata_dir() -> pathlib.Path:
|
def get_tessdata_dir() -> Path:
|
||||||
if getattr(sys, "dangerzone_dev", False) or platform.system() in (
|
if getattr(sys, "dangerzone_dev", False) or platform.system() in (
|
||||||
"Windows",
|
"Windows",
|
||||||
"Darwin",
|
"Darwin",
|
||||||
):
|
):
|
||||||
# Always use the tessdata path from the Dangerzone ./share directory, for
|
# Always use the tessdata path from the Dangerzone ./share directory, for
|
||||||
# development builds, or in Windows/macOS platforms.
|
# 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
|
# 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
|
# 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
|
# [1] https://tesseract-ocr.github.io/tessdoc/Installation.html
|
||||||
tessdata_dirs = [
|
tessdata_dirs = [
|
||||||
pathlib.Path("/usr/share/tessdata/"), # on some Debian
|
Path("/usr/share/tessdata/"), # on some Debian
|
||||||
pathlib.Path("/usr/share/tesseract/tessdata/"), # on Fedora
|
Path("/usr/share/tesseract/tessdata/"), # on Fedora
|
||||||
pathlib.Path("/usr/share/tesseract-ocr/tessdata/"), # ? (documented)
|
Path("/usr/share/tesseract-ocr/tessdata/"), # ? (documented)
|
||||||
pathlib.Path("/usr/share/tesseract-ocr/4.00/tessdata/"), # on Debian Bullseye
|
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/tesseract-ocr/5/tessdata/"), # on Debian Trixie
|
||||||
]
|
]
|
||||||
|
|
||||||
for dir in tessdata_dirs:
|
for dir in tessdata_dirs:
|
||||||
|
@ -71,7 +70,7 @@ def get_tessdata_dir() -> pathlib.Path:
|
||||||
|
|
||||||
def get_version() -> str:
|
def get_version() -> str:
|
||||||
try:
|
try:
|
||||||
with open(get_resource_path("version.txt")) as f:
|
with get_resource_path("version.txt").open() as f:
|
||||||
version = f.read().strip()
|
version = f.read().strip()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
# In dev mode, in Windows, get_resource_path doesn't work properly for the container, but luckily
|
# 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(
|
def generate_isolated_updater(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
monkeypatch: MonkeyPatch,
|
mocker: MockerFixture,
|
||||||
app_mocker: Optional[MockerFixture] = None,
|
mock_app: bool = False,
|
||||||
) -> UpdaterThread:
|
) -> UpdaterThread:
|
||||||
"""Generate an Updater class with its own settings."""
|
"""Generate an Updater class with its own settings."""
|
||||||
if app_mocker:
|
app = mocker.MagicMock() if mock_app else get_qt_app()
|
||||||
app = app_mocker.MagicMock()
|
|
||||||
else:
|
|
||||||
app = get_qt_app()
|
|
||||||
|
|
||||||
dummy = Dummy()
|
dummy = Dummy()
|
||||||
# XXX: We can monkey-patch global state without wrapping it in a context manager, or
|
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||||
# 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)
|
dangerzone = DangerzoneGui(app, isolation_provider=dummy)
|
||||||
updater = UpdaterThread(dangerzone)
|
updater = UpdaterThread(dangerzone)
|
||||||
return updater
|
return updater
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def updater(
|
def updater(tmp_path: Path, mocker: MockerFixture) -> UpdaterThread:
|
||||||
tmp_path: Path, monkeypatch: MonkeyPatch, mocker: MockerFixture
|
return generate_isolated_updater(tmp_path, mocker, mock_app=True)
|
||||||
) -> UpdaterThread:
|
|
||||||
return generate_isolated_updater(tmp_path, monkeypatch, mocker)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def qt_updater(tmp_path: Path, monkeypatch: MonkeyPatch) -> UpdaterThread:
|
def qt_updater(tmp_path: Path, mocker: MockerFixture) -> UpdaterThread:
|
||||||
return generate_isolated_updater(tmp_path, monkeypatch)
|
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(
|
def test_pre_0_4_2_settings(tmp_path: Path, mocker: MockerFixture) -> None:
|
||||||
tmp_path: Path, monkeypatch: MonkeyPatch, mocker: MockerFixture
|
|
||||||
) -> None:
|
|
||||||
"""Check settings of installations prior to 0.4.2.
|
"""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
|
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.
|
in their settings.json file.
|
||||||
"""
|
"""
|
||||||
save_settings(tmp_path, default_settings_0_4_1())
|
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 (
|
assert (
|
||||||
updater.dangerzone.settings.get_updater_settings() == default_updater_settings()
|
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.
|
# version is 0.4.3.
|
||||||
expected_settings = default_updater_settings()
|
expected_settings = default_updater_settings()
|
||||||
expected_settings["updater_latest_version"] = "0.4.3"
|
expected_settings["updater_latest_version"] = "0.4.3"
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(settings, "get_version", lambda: "0.4.3")
|
||||||
settings, "get_version", lambda: expected_settings["updater_latest_version"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ensure that the Settings class will correct the latest version field to 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
|
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
|
||||||
|
|
||||||
# Simulate an updater check that found a newer Dangerzone version (e.g., 0.4.4).
|
# 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
|
assert updater.dangerzone.settings.get_updater_settings() == expected_settings
|
||||||
|
|
||||||
|
|
||||||
def test_user_prompts(
|
def test_user_prompts(updater: UpdaterThread, mocker: MockerFixture) -> None:
|
||||||
updater: UpdaterThread, monkeypatch: MonkeyPatch, mocker: MockerFixture
|
|
||||||
) -> None:
|
|
||||||
"""Test prompting users to ask them if they want to enable update checks."""
|
"""Test prompting users to ask them if they want to enable update checks."""
|
||||||
# First run
|
# First run
|
||||||
#
|
#
|
||||||
|
@ -370,8 +364,6 @@ def test_update_errors(
|
||||||
def test_update_check_prompt(
|
def test_update_check_prompt(
|
||||||
qtbot: QtBot,
|
qtbot: QtBot,
|
||||||
qt_updater: UpdaterThread,
|
qt_updater: UpdaterThread,
|
||||||
monkeypatch: MonkeyPatch,
|
|
||||||
mocker: MockerFixture,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test that the prompt to enable update checks works properly."""
|
"""Test that the prompt to enable update checks works properly."""
|
||||||
# Force Dangerzone to check immediately for updates
|
# Force Dangerzone to check immediately for updates
|
||||||
|
|
|
@ -5,7 +5,8 @@ import pytest
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
from pytest_subprocess import FakeProcess
|
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.container import Container
|
||||||
from dangerzone.isolation_provider.qubes import is_qubes_native_conversion
|
from dangerzone.isolation_provider.qubes import is_qubes_native_conversion
|
||||||
from dangerzone.util import get_resource_path
|
from dangerzone.util import get_resource_path
|
||||||
|
@ -24,42 +25,51 @@ def provider() -> Container:
|
||||||
return Container()
|
return Container()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def runtime_path() -> str:
|
||||||
|
return str(Runtime().path)
|
||||||
|
|
||||||
|
|
||||||
class TestContainer(IsolationProviderTest):
|
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
|
NotAvailableContainerTechException should be raised when
|
||||||
the "podman image ls" command fails.
|
the "podman image ls" command fails.
|
||||||
"""
|
"""
|
||||||
fp.register_subprocess(
|
fp.register_subprocess(
|
||||||
[container_utils.get_runtime(), "image", "ls"],
|
[runtime_path, "image", "ls"],
|
||||||
returncode=-1,
|
returncode=-1,
|
||||||
stderr="podman image ls logs",
|
stderr="podman image ls logs",
|
||||||
)
|
)
|
||||||
with pytest.raises(errors.NotAvailableContainerTechException):
|
with pytest.raises(errors.NotAvailableContainerTechException):
|
||||||
provider.is_available()
|
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.
|
No exception should be raised when the "podman image ls" can return properly.
|
||||||
"""
|
"""
|
||||||
fp.register_subprocess(
|
fp.register_subprocess(
|
||||||
[container_utils.get_runtime(), "image", "ls"],
|
[runtime_path, "image", "ls"],
|
||||||
)
|
)
|
||||||
provider.is_available()
|
provider.is_available()
|
||||||
|
|
||||||
def test_install_raise_if_image_cant_be_installed(
|
def test_install_raise_if_image_cant_be_installed(
|
||||||
self, provider: Container, fp: FakeProcess
|
self, provider: Container, fp: FakeProcess, runtime_path: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""When an image installation fails, an exception should be raised"""
|
"""When an image installation fails, an exception should be raised"""
|
||||||
|
|
||||||
fp.register_subprocess(
|
fp.register_subprocess(
|
||||||
[container_utils.get_runtime(), "image", "ls"],
|
[runtime_path, "image", "ls"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# First check should return nothing.
|
# First check should return nothing.
|
||||||
fp.register_subprocess(
|
fp.register_subprocess(
|
||||||
[
|
[
|
||||||
container_utils.get_runtime(),
|
runtime_path,
|
||||||
"image",
|
"image",
|
||||||
"list",
|
"list",
|
||||||
"--format",
|
"--format",
|
||||||
|
@ -71,10 +81,10 @@ class TestContainer(IsolationProviderTest):
|
||||||
|
|
||||||
fp.register_subprocess(
|
fp.register_subprocess(
|
||||||
[
|
[
|
||||||
container_utils.get_runtime(),
|
runtime_path,
|
||||||
"load",
|
"load",
|
||||||
"-i",
|
"-i",
|
||||||
get_resource_path("container.tar"),
|
get_resource_path("container.tar").absolute(),
|
||||||
],
|
],
|
||||||
returncode=-1,
|
returncode=-1,
|
||||||
)
|
)
|
||||||
|
@ -83,22 +93,22 @@ class TestContainer(IsolationProviderTest):
|
||||||
provider.install()
|
provider.install()
|
||||||
|
|
||||||
def test_install_raises_if_still_not_installed(
|
def test_install_raises_if_still_not_installed(
|
||||||
self, provider: Container, fp: FakeProcess
|
self, provider: Container, fp: FakeProcess, runtime_path: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""When an image keep being not installed, it should return False"""
|
"""When an image keep being not installed, it should return False"""
|
||||||
fp.register_subprocess(
|
fp.register_subprocess(
|
||||||
["podman", "version", "-f", "{{.Client.Version}}"],
|
[runtime_path, "version", "-f", "{{.Client.Version}}"],
|
||||||
stdout="4.0.0",
|
stdout="4.0.0",
|
||||||
)
|
)
|
||||||
|
|
||||||
fp.register_subprocess(
|
fp.register_subprocess(
|
||||||
[container_utils.get_runtime(), "image", "ls"],
|
[runtime_path, "image", "ls"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# First check should return nothing.
|
# First check should return nothing.
|
||||||
fp.register_subprocess(
|
fp.register_subprocess(
|
||||||
[
|
[
|
||||||
container_utils.get_runtime(),
|
runtime_path,
|
||||||
"image",
|
"image",
|
||||||
"list",
|
"list",
|
||||||
"--format",
|
"--format",
|
||||||
|
@ -110,10 +120,10 @@ class TestContainer(IsolationProviderTest):
|
||||||
|
|
||||||
fp.register_subprocess(
|
fp.register_subprocess(
|
||||||
[
|
[
|
||||||
container_utils.get_runtime(),
|
runtime_path,
|
||||||
"load",
|
"load",
|
||||||
"-i",
|
"-i",
|
||||||
get_resource_path("container.tar"),
|
get_resource_path("container.tar").absolute(),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
with pytest.raises(errors.ImageNotPresentException):
|
with pytest.raises(errors.ImageNotPresentException):
|
||||||
|
|
60
tests/test_container_utils.py
Normal file
60
tests/test_container_utils.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
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,5 +1,4 @@
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import PropertyMock
|
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:
|
def save_settings(tmp_path: Path, settings: dict) -> None:
|
||||||
"""Mimic the way Settings save a dictionary to a settings.json file."""
|
"""Mimic the way Settings save a dictionary to a settings.json file."""
|
||||||
settings_filename = tmp_path / "settings.json"
|
settings_filename = tmp_path / "settings.json"
|
||||||
|
@ -36,10 +28,17 @@ def save_settings(tmp_path: Path, settings: dict) -> None:
|
||||||
json.dump(settings, settings_file, indent=4)
|
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"""
|
"""Default settings file is created on first run"""
|
||||||
assert os.path.isfile(settings.settings_filename)
|
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||||
new_settings_dict = json.load(open(settings.settings_filename))
|
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(
|
assert sorted(new_settings_dict.items()) == sorted(
|
||||||
settings.generate_default_settings().items()
|
settings.generate_default_settings().items()
|
||||||
)
|
)
|
||||||
|
@ -48,14 +47,12 @@ def test_no_settings_file_creates_new_one(settings: Settings) -> None:
|
||||||
def test_corrupt_settings(tmp_path: Path, mocker: MockerFixture) -> None:
|
def test_corrupt_settings(tmp_path: Path, mocker: MockerFixture) -> None:
|
||||||
# Set some broken settings file
|
# Set some broken settings file
|
||||||
corrupt_settings_dict = "{:}"
|
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)
|
settings_file.write(corrupt_settings_dict)
|
||||||
|
|
||||||
# Initialize settings
|
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path)
|
||||||
dz_core = mocker.MagicMock()
|
settings = Settings()
|
||||||
type(dz_core).appdata_path = PropertyMock(return_value=tmp_path)
|
assert settings.settings_filename.is_file()
|
||||||
settings = Settings(dz_core)
|
|
||||||
assert os.path.isfile(settings.settings_filename)
|
|
||||||
|
|
||||||
# Check if settings file was reset to the default
|
# Check if settings file was reset to the default
|
||||||
new_settings_dict = json.load(open(settings.settings_filename))
|
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:
|
def test_new_default_setting(tmp_path: Path, mocker: MockerFixture) -> None:
|
||||||
# Initialize settings
|
settings = Settings()
|
||||||
dz_core = mocker.MagicMock()
|
|
||||||
type(dz_core).appdata_path = PropertyMock(return_value=tmp_path)
|
|
||||||
settings = Settings(dz_core)
|
|
||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
# Ensure new default setting is imported into settings
|
# 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},
|
return_value={"mock_setting": 1},
|
||||||
)
|
)
|
||||||
|
|
||||||
settings2 = Settings(dz_core)
|
settings2 = Settings()
|
||||||
assert settings2.get("mock_setting") == 1
|
assert settings2.get("mock_setting") == 1
|
||||||
|
|
||||||
|
|
||||||
def test_new_settings_added(tmp_path: Path, mocker: MockerFixture) -> None:
|
def test_new_settings_added(tmp_path: Path, mocker: MockerFixture) -> None:
|
||||||
# Initialize settings
|
settings = Settings()
|
||||||
dz_core = mocker.MagicMock()
|
|
||||||
type(dz_core).appdata_path = PropertyMock(return_value=tmp_path)
|
|
||||||
settings = Settings(dz_core)
|
|
||||||
|
|
||||||
# Add new setting
|
# Add new setting
|
||||||
settings.set("new_setting_autosaved", 20, autosave=True)
|
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
|
) # XXX has to be afterwards; otherwise this will be saved
|
||||||
|
|
||||||
# Simulate new app startup (settings recreation)
|
# Simulate new app startup (settings recreation)
|
||||||
settings2 = Settings(dz_core)
|
settings2 = Settings()
|
||||||
|
|
||||||
# Check if new setting persisted
|
# Check if new setting persisted
|
||||||
assert 20 == settings2.get("new_setting_autosaved")
|
assert 20 == settings2.get("new_setting_autosaved")
|
||||||
|
|
|
@ -11,7 +11,7 @@ VERSION_FILE_NAME = "version.txt"
|
||||||
|
|
||||||
def test_get_resource_path() -> None:
|
def test_get_resource_path() -> None:
|
||||||
share_dir = Path("share").resolve()
|
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), (
|
assert share_dir.samefile(resource_path), (
|
||||||
f"{share_dir} is not the same file as {resource_path}"
|
f"{share_dir} is not the same file as {resource_path}"
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue