diff --git a/dangerzone/container_utils.py b/dangerzone/container_utils.py
index 7c8f06d..26621fb 100644
--- a/dangerzone/container_utils.py
+++ b/dangerzone/container_utils.py
@@ -4,14 +4,16 @@ import platform
import shutil
import subprocess
from pathlib import Path
-from typing import List, Optional, Tuple
+from typing import IO, Callable, List, Optional, Tuple
from . import errors
from .settings import Settings
from .util import get_resource_path, get_subprocess_startupinfo
OLD_CONTAINER_NAME = "dangerzone.rocks/dangerzone"
-CONTAINER_NAME = "ghcr.io/freedomofpress/dangerzone/dangerzone"
+CONTAINER_NAME = (
+ "ghcr.io/almet/dangerzone/dangerzone"
+) # FIXME: Change this to the correct container name
log = logging.getLogger(__name__)
@@ -228,16 +230,27 @@ def get_image_id_by_digest(digest: str) -> str:
return process.stdout.decode().strip().split("\n")[0]
-def container_pull(image: str, manifest_digest: str):
+def container_pull(image: str, manifest_digest: str, callback: Callable):
"""Pull a container image from a registry."""
runtime = Runtime()
cmd = [str(runtime.path), "pull", f"{image}@sha256:{manifest_digest}"]
- try:
- subprocess_run(cmd, check=True)
- except subprocess.CalledProcessError as e:
+ process = subprocess.Popen(
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ bufsize=1,
+ universal_newlines=True,
+ )
+
+ for line in process.stdout: # type: ignore
+ callback(line)
+
+ process.wait()
+ if process.returncode != 0:
raise errors.ContainerPullException(
- f"Could not pull the container image: {e}"
- ) from e
+ f"Could not pull the container image: {process.returncode}"
+ )
def get_local_image_digest(image: str) -> str:
diff --git a/dangerzone/gui/main_window.py b/dangerzone/gui/main_window.py
index 5924aae..2383222 100644
--- a/dangerzone/gui/main_window.py
+++ b/dangerzone/gui/main_window.py
@@ -1,3 +1,4 @@
+import io
import logging
import os
import platform
@@ -5,22 +6,24 @@ import tempfile
import typing
from multiprocessing.pool import ThreadPool
from pathlib import Path
-from typing import List, Optional
+from typing import Callable, List, Optional
# FIXME: See https://github.com/freedomofpress/dangerzone/issues/320 for more details.
if typing.TYPE_CHECKING:
from PySide2 import QtCore, QtGui, QtSvg, QtWidgets
from PySide2.QtCore import Qt
+ from PySide2.QtGui import QTextCursor
from PySide2.QtWidgets import QAction, QTextEdit
else:
try:
from PySide6 import QtCore, QtGui, QtSvg, QtWidgets
from PySide6.QtCore import Qt
- from PySide6.QtGui import QAction
+ from PySide6.QtGui import QAction, QTextCursor
from PySide6.QtWidgets import QTextEdit
except ImportError:
from PySide2 import QtCore, QtGui, QtSvg, QtWidgets
from PySide2.QtCore import Qt
+ from PySide2.QtGui import QTextCursor
from PySide2.QtWidgets import QAction, QTextEdit
from .. import errors
@@ -436,15 +439,21 @@ class MainWindow(QtWidgets.QMainWindow):
class InstallContainerThread(QtCore.QThread):
finished = QtCore.Signal(str)
+ process_stdout = QtCore.Signal(str)
- def __init__(self, dangerzone: DangerzoneGui) -> None:
+ def __init__(
+ self, dangerzone: DangerzoneGui, callback: Optional[Callable] = None
+ ) -> None:
super(InstallContainerThread, self).__init__()
self.dangerzone = dangerzone
def run(self) -> None:
error = None
try:
- installed = self.dangerzone.isolation_provider.install()
+ should_upgrade = self.dangerzone.settings.get("updater_check_all")
+ installed = self.dangerzone.isolation_provider.install(
+ should_upgrade=bool(should_upgrade), callback=self.process_stdout.emit
+ )
except Exception as e:
log.error("Container installation problem")
error = format_exception(e)
@@ -479,11 +488,20 @@ class TracebackWidget(QTextEdit):
# Enable copying
self.setTextInteractionFlags(Qt.TextSelectableByMouse)
+ self.current_output = ""
+
def set_content(self, error: Optional[str] = None) -> None:
if error:
self.setPlainText(error)
self.setVisible(True)
+ def process_output(self, line):
+ self.current_output += line
+ self.setText(self.current_output)
+ cursor = self.textCursor()
+ cursor.movePosition(QTextCursor.MoveOperation.End)
+ self.setTextCursor(cursor)
+
class WaitingWidgetContainer(WaitingWidget):
# These are the possible states that the WaitingWidget can show.
@@ -623,8 +641,14 @@ class WaitingWidgetContainer(WaitingWidget):
"Installing the Dangerzone container image.
"
"This might take a few minutes..."
)
+ self.traceback.setVisible(True)
+
self.install_container_t = InstallContainerThread(self.dangerzone)
self.install_container_t.finished.connect(self.installation_finished)
+
+ self.install_container_t.process_stdout.connect(
+ self.traceback.process_output
+ )
self.install_container_t.start()
diff --git a/dangerzone/isolation_provider/base.py b/dangerzone/isolation_provider/base.py
index 0a12be1..27d1c09 100644
--- a/dangerzone/isolation_provider/base.py
+++ b/dangerzone/isolation_provider/base.py
@@ -95,7 +95,7 @@ class IsolationProvider(ABC):
return self.debug or getattr(sys, "dangerzone_dev", False)
@abstractmethod
- def install(self) -> bool:
+ def install(self, should_upgrade: bool, callback: Callable) -> bool:
pass
def convert(
diff --git a/dangerzone/isolation_provider/container.py b/dangerzone/isolation_provider/container.py
index a5bb6b7..50bf64d 100644
--- a/dangerzone/isolation_provider/container.py
+++ b/dangerzone/isolation_provider/container.py
@@ -3,7 +3,7 @@ import os
import platform
import shlex
import subprocess
-from typing import List, Tuple
+from typing import Callable, List, Tuple
from .. import container_utils, errors, updater
from ..container_utils import Runtime
@@ -94,27 +94,38 @@ class Container(IsolationProvider):
return security_args
@staticmethod
- def install() -> bool:
+ def install(
+ should_upgrade: bool, callback: Callable, last_try: bool = False
+ ) -> bool:
"""Check if an update is available and install it if necessary."""
- # XXX Do this only if users have opted in to auto-updates
- if False: # Comment this for now, just as an exemple of this can be implemented
- # # Load the image tarball into the container runtime.
+ if not should_upgrade:
+ log.debug("Skipping container upgrade check as requested by the settings")
+ else:
update_available, image_digest = updater.is_update_available(
- container_utils.CONTAINER_NAME
+ container_utils.CONTAINER_NAME,
+ updater.DEFAULT_PUBKEY_LOCATION,
)
if update_available and image_digest:
+ log.debug("Upgrading container image to %s", image_digest)
updater.upgrade_container_image(
container_utils.CONTAINER_NAME,
image_digest,
updater.DEFAULT_PUBKEY_LOCATION,
+ callback=callback,
)
- for tag in old_tags:
- tag = container_utils.CONTAINER_NAME + ":" + tag
- container_utils.delete_image_tag(tag)
-
- updater.verify_local_image(
- container_utils.CONTAINER_NAME, updater.DEFAULT_PUBKEY_LOCATION
- )
+ else:
+ log.debug("No update available for the container")
+ try:
+ updater.verify_local_image(
+ container_utils.CONTAINER_NAME, updater.DEFAULT_PUBKEY_LOCATION
+ )
+ except errors.ImageNotPresentException:
+ if last_try:
+ raise
+ log.debug("Container image not found, trying to install it.")
+ return Container.install(
+ should_upgrade=should_upgrade, callback=callback, last_try=True
+ )
return True
diff --git a/dangerzone/updater/signatures.py b/dangerzone/updater/signatures.py
index 0c58b8c..5312c2c 100644
--- a/dangerzone/updater/signatures.py
+++ b/dangerzone/updater/signatures.py
@@ -9,7 +9,7 @@ from hashlib import sha256
from io import BytesIO
from pathlib import Path
from tempfile import NamedTemporaryFile, TemporaryDirectory
-from typing import Dict, List, Optional, Tuple
+from typing import Callable, Dict, List, Optional, Tuple
from .. import container_utils as runtime
from .. import errors as dzerrors
@@ -370,7 +370,9 @@ def load_and_verify_signatures(
return signatures
-def store_signatures(signatures: list[Dict], image_digest: str, pubkey: str) -> None:
+def store_signatures(
+ signatures: list[Dict], image_digest: str, pubkey: str, update_logindex: bool = True
+) -> None:
"""
Store signatures locally in the SIGNATURE_PATH folder, like this:
@@ -415,7 +417,8 @@ def store_signatures(signatures: list[Dict], image_digest: str, pubkey: str) ->
)
json.dump(signatures, f)
- write_log_index(get_log_index_from_signatures(signatures))
+ if update_logindex:
+ write_log_index(get_log_index_from_signatures(signatures))
def verify_local_image(image: str, pubkey: str) -> bool:
@@ -479,14 +482,16 @@ def prepare_airgapped_archive(image_name: str, destination: str) -> None:
archive.add(tmpdir, arcname=".")
-def upgrade_container_image(image: str, manifest_digest: str, pubkey: str) -> str:
+def upgrade_container_image(
+ image: str, manifest_digest: str, pubkey: str, callback: Callable
+) -> str:
"""Verify and upgrade the image to the latest, if signed."""
update_available, remote_digest = registry.is_new_remote_image_available(image)
if not update_available:
raise errors.ImageAlreadyUpToDate("The image is already up to date")
signatures = check_signatures_and_logindex(image, remote_digest, pubkey)
- runtime.container_pull(image, manifest_digest)
+ runtime.container_pull(image, manifest_digest, callback=callback)
# Store the signatures just now to avoid storing them unverified
store_signatures(signatures, manifest_digest, pubkey)