Add a --debug flag to the CLI to help retrieve more logs.

When the flag is set:

- The `RUNSC_DEBUG=1` environment variable is added to the outer container ;
- the stderr from the outer container is attached to the exception, and
  displayed to the user on failures.
This commit is contained in:
Alexis Métaireau 2024-10-07 16:28:09 +02:00
parent 03b3c9eba8
commit 981fcd4e3f
No known key found for this signature in database
GPG key ID: C65C7A89A8FFC56E
4 changed files with 50 additions and 9 deletions

View file

@ -42,6 +42,11 @@ def print_header(s: str) -> None:
type=click.UNPROCESSED, type=click.UNPROCESSED,
callback=args.validate_input_filenames, callback=args.validate_input_filenames,
) )
@click.option(
"--debug",
"debug",
flag_value=True,
help="Run Dangerzone in debug mode, and get more logs from the conversion process.")
@click.version_option(version=get_version(), message="%(version)s") @click.version_option(version=get_version(), message="%(version)s")
@errors.handle_document_errors @errors.handle_document_errors
def cli_main( def cli_main(
@ -50,6 +55,7 @@ def cli_main(
filenames: List[str], filenames: List[str],
archive: bool, archive: bool,
dummy_conversion: bool, dummy_conversion: bool,
debug: bool,
) -> None: ) -> None:
setup_logging() setup_logging()
@ -58,7 +64,7 @@ def cli_main(
elif is_qubes_native_conversion(): elif is_qubes_native_conversion():
dangerzone = DangerzoneCore(Qubes()) dangerzone = DangerzoneCore(Qubes())
else: else:
dangerzone = DangerzoneCore(Container()) dangerzone = DangerzoneCore(Container(debug=debug))
display_banner() display_banner()
if len(filenames) == 1 and output_filename: if len(filenames) == 1 and output_filename:

View file

@ -18,11 +18,20 @@ class ConversionException(Exception):
error_message = "Unspecified error" error_message = "Unspecified error"
error_code = ERROR_SHIFT error_code = ERROR_SHIFT
def __init__(self, error_message: Optional[str] = None) -> None: def __init__(
self, error_message: Optional[str] = None, logs: Optional[str] = None
) -> None:
if error_message: if error_message:
self.error_message = error_message self.error_message = error_message
self.logs = logs
super().__init__(self.error_message) super().__init__(self.error_message)
def __str__(self):
msg = f"{self.error_message}"
if self.logs:
msg += f" {self.logs}"
return msg
@classmethod @classmethod
def get_subclasses(cls) -> List[Type["ConversionException"]]: def get_subclasses(cls) -> List[Type["ConversionException"]]:
subclasses = [cls] subclasses = [cls]
@ -100,9 +109,10 @@ class UnexpectedConversionError(ConversionException):
def exception_from_error_code( def exception_from_error_code(
error_code: int, error_code: int,
logs: Optional[str] = None,
) -> Union[ConversionException, ValueError]: ) -> Union[ConversionException, ValueError]:
"""returns the conversion exception corresponding to the error code""" """returns the conversion exception corresponding to the error code"""
for cls in ConversionException.get_subclasses(): for cls in ConversionException.get_subclasses():
if cls.error_code == error_code: if cls.error_code == error_code:
return cls() return cls(logs=logs)
return UnexpectedConversionError(f"Unknown error code '{error_code}'") return UnexpectedConversionError(f"Unknown error code '{error_code}', logs= {logs}")

View file

@ -5,6 +5,7 @@ import platform
import signal import signal
import subprocess import subprocess
import sys import sys
import tempfile
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from pathlib import Path from pathlib import Path
from typing import IO, Callable, Iterator, Optional from typing import IO, Callable, Iterator, Optional
@ -87,12 +88,19 @@ class IsolationProvider(ABC):
Abstracts an isolation provider Abstracts an isolation provider
""" """
def __init__(self) -> None: def __init__(self, debug) -> None:
if getattr(sys, "dangerzone_dev", False) is True: self.debug = debug
if self.should_display_errors():
self.proc_stderr = subprocess.PIPE self.proc_stderr = subprocess.PIPE
else: else:
# We do not trust STDERR from the inner container,
# which could be exploited to ask users to open the document with
# a default PDF reader for instance.
self.proc_stderr = subprocess.DEVNULL self.proc_stderr = subprocess.DEVNULL
def should_display_errors(self) -> bool:
return self.debug or getattr(sys, "dangerzone_dev", False)
@abstractmethod @abstractmethod
def install(self) -> bool: def install(self) -> bool:
pass pass
@ -220,6 +228,17 @@ class IsolationProvider(ABC):
text = "Successfully converted document" text = "Successfully converted document"
self.print_progress(document, False, text, 100) self.print_progress(document, False, text, 100)
if self.should_display_errors():
assert p.stderr
debug_log = read_debug_text(p.stderr, MAX_CONVERSION_LOG_CHARS)
p.stderr.close()
log.debug(
"Conversion output (doc to pixels)\n"
f"{DOC_TO_PIXELS_LOG_START}\n"
f"{debug_log}" # no need for an extra newline here
f"{DOC_TO_PIXELS_LOG_END}"
)
def print_progress( def print_progress(
self, document: Document, error: bool, text: str, percentage: float self, document: Document, error: bool, text: str, percentage: float
) -> None: ) -> None:
@ -252,7 +271,10 @@ class IsolationProvider(ABC):
"Encountered an I/O error during document to pixels conversion," "Encountered an I/O error during document to pixels conversion,"
f" but the status of the conversion process is unknown (PID: {p.pid})" f" but the status of the conversion process is unknown (PID: {p.pid})"
) )
return errors.exception_from_error_code(error_code) logs = None
if self.debug:
logs = "".join([line.decode() for line in p.stderr.readlines()])
return errors.exception_from_error_code(error_code, logs=logs)
@abstractmethod @abstractmethod
def get_max_parallel_conversions(self) -> int: def get_max_parallel_conversions(self) -> int:

View file

@ -93,7 +93,7 @@ class Container(IsolationProvider):
return runtime return runtime
@staticmethod @staticmethod
def get_runtime_security_args() -> List[str]: def get_runtime_security_args(debug: bool) -> List[str]:
"""Security options applicable to the outer Dangerzone container. """Security options applicable to the outer Dangerzone container.
Our security precautions for the outer Dangerzone container are the following: Our security precautions for the outer Dangerzone container are the following:
@ -137,6 +137,9 @@ class Container(IsolationProvider):
security_args += ["--network=none"] security_args += ["--network=none"]
security_args += ["-u", "dangerzone"] security_args += ["-u", "dangerzone"]
if debug:
security_args += ["-e", "RUNSC_DEBUG=1"]
return security_args return security_args
@staticmethod @staticmethod
@ -250,7 +253,7 @@ class Container(IsolationProvider):
extra_args: List[str] = [], extra_args: List[str] = [],
) -> subprocess.Popen: ) -> subprocess.Popen:
container_runtime = self.get_runtime() container_runtime = self.get_runtime()
security_args = self.get_runtime_security_args() security_args = self.get_runtime_security_args(self.debug)
enable_stdin = ["-i"] enable_stdin = ["-i"]
set_name = ["--name", name] set_name = ["--name", name]
prevent_leakage_args = ["--rm"] prevent_leakage_args = ["--rm"]