mirror of
https://github.com/freedomofpress/dangerzone.git
synced 2025-04-28 18:02:38 +02:00
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, and stderr is captured in a separate thread, before printing its output.
This commit is contained in:
parent
3499010d8e
commit
acf20ef700
4 changed files with 62 additions and 15 deletions
|
@ -42,6 +42,12 @@ 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, to get logs from gVisor.",
|
||||||
|
)
|
||||||
@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 +56,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 +65,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:
|
||||||
|
|
|
@ -5,7 +5,9 @@ import platform
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import threading
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from io import BytesIO
|
||||||
from typing import IO, Callable, Iterator, Optional
|
from typing import IO, Callable, Iterator, Optional
|
||||||
|
|
||||||
import fitz
|
import fitz
|
||||||
|
@ -18,7 +20,6 @@ from ..util import get_tessdata_dir, replace_control_chars
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
MAX_CONVERSION_LOG_CHARS = 150 * 50 # up to ~150 lines of 50 characters
|
|
||||||
DOC_TO_PIXELS_LOG_START = "----- DOC TO PIXELS LOG START -----"
|
DOC_TO_PIXELS_LOG_START = "----- DOC TO PIXELS LOG START -----"
|
||||||
DOC_TO_PIXELS_LOG_END = "----- DOC TO PIXELS LOG END -----"
|
DOC_TO_PIXELS_LOG_END = "----- DOC TO PIXELS LOG END -----"
|
||||||
|
|
||||||
|
@ -75,9 +76,9 @@ def read_int(f: IO[bytes]) -> int:
|
||||||
return int.from_bytes(untrusted_int, "big", signed=False)
|
return int.from_bytes(untrusted_int, "big", signed=False)
|
||||||
|
|
||||||
|
|
||||||
def read_debug_text(f: IO[bytes], size: int) -> str:
|
def sanitize_debug_text(text: bytes) -> str:
|
||||||
"""Read arbitrarily long text (for debug purposes), and sanitize it."""
|
"""Read all the buffer and return a sanitized version"""
|
||||||
untrusted_text = f.read(size).decode("ascii", errors="replace")
|
untrusted_text = text.decode("ascii", errors="replace")
|
||||||
return replace_control_chars(untrusted_text, keep_newlines=True)
|
return replace_control_chars(untrusted_text, keep_newlines=True)
|
||||||
|
|
||||||
|
|
||||||
|
@ -86,12 +87,16 @@ class IsolationProvider(ABC):
|
||||||
Abstracts an isolation provider
|
Abstracts an isolation provider
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self, debug: bool = False) -> None:
|
||||||
if getattr(sys, "dangerzone_dev", False) is True:
|
self.debug = debug
|
||||||
|
if self.should_capture_stderr():
|
||||||
self.proc_stderr = subprocess.PIPE
|
self.proc_stderr = subprocess.PIPE
|
||||||
else:
|
else:
|
||||||
self.proc_stderr = subprocess.DEVNULL
|
self.proc_stderr = subprocess.DEVNULL
|
||||||
|
|
||||||
|
def should_capture_stderr(self) -> bool:
|
||||||
|
return self.debug or getattr(sys, "dangerzone_dev", False)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def install(self) -> bool:
|
def install(self) -> bool:
|
||||||
pass
|
pass
|
||||||
|
@ -327,7 +332,11 @@ class IsolationProvider(ABC):
|
||||||
timeout_force: int = TIMEOUT_FORCE,
|
timeout_force: int = TIMEOUT_FORCE,
|
||||||
) -> Iterator[subprocess.Popen]:
|
) -> Iterator[subprocess.Popen]:
|
||||||
"""Start a conversion process, pass it to the caller, and then clean it up."""
|
"""Start a conversion process, pass it to the caller, and then clean it up."""
|
||||||
|
# Store the proc stderr in memory
|
||||||
|
stderr = BytesIO()
|
||||||
p = self.start_doc_to_pixels_proc(document)
|
p = self.start_doc_to_pixels_proc(document)
|
||||||
|
stderr_thread = self.start_stderr_thread(p, stderr)
|
||||||
|
|
||||||
if platform.system() != "Windows":
|
if platform.system() != "Windows":
|
||||||
assert os.getpgid(p.pid) != os.getpgid(
|
assert os.getpgid(p.pid) != os.getpgid(
|
||||||
os.getpid()
|
os.getpid()
|
||||||
|
@ -343,15 +352,41 @@ class IsolationProvider(ABC):
|
||||||
document, p, timeout_grace=timeout_grace, timeout_force=timeout_force
|
document, p, timeout_grace=timeout_grace, timeout_force=timeout_force
|
||||||
)
|
)
|
||||||
|
|
||||||
# Read the stderr of the process only if:
|
if stderr_thread:
|
||||||
# * Dev mode is enabled.
|
# Wait for the thread to complete. If it's still alive, mention it in the debug log.
|
||||||
# * The process has exited (else we risk hanging).
|
stderr_thread.join(timeout=1)
|
||||||
if getattr(sys, "dangerzone_dev", False) and p.poll() is not None:
|
|
||||||
assert p.stderr
|
debug_bytes = stderr.getvalue()
|
||||||
debug_log = read_debug_text(p.stderr, MAX_CONVERSION_LOG_CHARS)
|
debug_log = sanitize_debug_text(debug_bytes)
|
||||||
|
|
||||||
|
incomplete = "(incomplete)\n" if stderr_thread.is_alive() else ""
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
"Conversion output (doc to pixels)\n"
|
"Conversion output (doc to pixels)\n"
|
||||||
f"{DOC_TO_PIXELS_LOG_START}\n"
|
f"{DOC_TO_PIXELS_LOG_START}\n"
|
||||||
f"{debug_log}" # no need for an extra newline here
|
f"{debug_log}" # no need for an extra newline here
|
||||||
|
f"{incomplete}"
|
||||||
f"{DOC_TO_PIXELS_LOG_END}"
|
f"{DOC_TO_PIXELS_LOG_END}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def start_stderr_thread(
|
||||||
|
self, process: subprocess.Popen, stderr: IO[bytes]
|
||||||
|
) -> Optional[threading.Thread]:
|
||||||
|
"""Start a thread to read stderr from the process"""
|
||||||
|
|
||||||
|
def _stream_stderr(process_stderr: IO[bytes]) -> None:
|
||||||
|
try:
|
||||||
|
for line in process_stderr:
|
||||||
|
stderr.write(line)
|
||||||
|
except (ValueError, IOError) as e:
|
||||||
|
log.debug(f"Stderr stream closed: {e}")
|
||||||
|
|
||||||
|
if process.stderr:
|
||||||
|
stderr_thread = threading.Thread(
|
||||||
|
target=_stream_stderr,
|
||||||
|
args=(process.stderr,),
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
stderr_thread.start()
|
||||||
|
return stderr_thread
|
||||||
|
return None
|
||||||
|
|
|
@ -168,6 +168,10 @@ class Container(IsolationProvider):
|
||||||
) -> subprocess.Popen:
|
) -> subprocess.Popen:
|
||||||
container_runtime = container_utils.get_runtime()
|
container_runtime = container_utils.get_runtime()
|
||||||
security_args = self.get_runtime_security_args()
|
security_args = self.get_runtime_security_args()
|
||||||
|
debug_args = []
|
||||||
|
if self.debug:
|
||||||
|
debug_args += ["-e", "RUNSC_DEBUG=1"]
|
||||||
|
|
||||||
enable_stdin = ["-i"]
|
enable_stdin = ["-i"]
|
||||||
set_name = ["--name", name]
|
set_name = ["--name", name]
|
||||||
prevent_leakage_args = ["--rm"]
|
prevent_leakage_args = ["--rm"]
|
||||||
|
@ -177,14 +181,14 @@ class Container(IsolationProvider):
|
||||||
args = (
|
args = (
|
||||||
["run"]
|
["run"]
|
||||||
+ security_args
|
+ security_args
|
||||||
|
+ debug_args
|
||||||
+ prevent_leakage_args
|
+ prevent_leakage_args
|
||||||
+ enable_stdin
|
+ enable_stdin
|
||||||
+ set_name
|
+ set_name
|
||||||
+ image_name
|
+ image_name
|
||||||
+ command
|
+ command
|
||||||
)
|
)
|
||||||
args = [container_runtime] + args
|
return self.exec([container_runtime] + args)
|
||||||
return self.exec(args)
|
|
||||||
|
|
||||||
def kill_container(self, name: str) -> None:
|
def kill_container(self, name: str) -> None:
|
||||||
"""Terminate a spawned container.
|
"""Terminate a spawned container.
|
||||||
|
|
|
@ -71,6 +71,7 @@ class DangerzoneCore(object):
|
||||||
ocr_lang,
|
ocr_lang,
|
||||||
stdout_callback,
|
stdout_callback,
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
log.exception(
|
log.exception(
|
||||||
f"Unexpected error occurred while converting '{document}'"
|
f"Unexpected error occurred while converting '{document}'"
|
||||||
|
|
Loading…
Reference in a new issue