Files
OCRmyPDF/src/ocrmypdf/__main__.py
T
James R. Barlow a13d27bfb5 Protect stdout from corruption when writing PDF to standard output
Writing the output PDF to stdout (ocrmypdf in.pdf -) previously relied on
an honor system: no in-process code -- third-party libraries, plugins, or
stray print() calls -- was supposed to write to stdout, enforced only
indirectly. A single accidental write to fd 1 would silently corrupt the
output PDF.

Enforce this at the OS level. At CLI startup, before plugins load or any
worker process/thread starts, save the real stdout via os.dup() and point
fd 1 at stderr, so stray writes are diverted to stderr while only the final
"produce the PDF" step writes to the preserved descriptor. Exposed as the
opt-in public API function configure_stdout_protection(), mirroring
configure_logging(); it is not enabled inside ocr() so in-process library
users keep their own stdout.

Also fix check_requested_output_file() to test the preserved real stdout
for tty-ness, since after the redirect sys.stdout reports stderr's status.

Fold unreleased v17.7.2 notes into v17.8.0.
2026-06-28 12:02:34 -07:00

90 lines
2.5 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
"""ocrmypdf command line entrypoint."""
from __future__ import annotations
import logging
import multiprocessing
import os
import signal
import sys
from contextlib import suppress
from ocrmypdf import __version__
from ocrmypdf._pipelines.ocr import run_pipeline_cli
from ocrmypdf._validation import check_options
from ocrmypdf.api import Verbosity, configure_logging, configure_stdout_protection
from ocrmypdf.cli import get_options_and_plugins
from ocrmypdf.exceptions import (
BadArgsError,
ExitCode,
InputFileError,
MissingDependencyError,
)
log = logging.getLogger('ocrmypdf')
def sigbus(*args):
"""Handle SIGBUS signals.
pikepdf, depending on configuration, may use mmap so SIGBUS is a
possibility.
"""
raise InputFileError("Lost access to the input file")
def run(args=None):
"""Run the ocrmypdf command line interface."""
# Protect the real stdout before loading plugins or starting any worker
# processes/threads, so that only our final PDF output can reach it and
# stray writes from plugins or libraries are diverted to stderr.
configure_stdout_protection()
options, plugin_manager = get_options_and_plugins(args=args)
with suppress(AttributeError, PermissionError):
os.nice(5)
verbosity = options.verbose
if not os.isatty(sys.stderr.fileno()):
options.progress_bar = False
if options.quiet:
verbosity = Verbosity.quiet
options.progress_bar = False
configure_logging(
verbosity,
progress_bar_friendly=options.progress_bar,
manage_root_logger=True,
plugin_manager=plugin_manager,
)
log.debug('ocrmypdf %s', __version__)
try:
check_options(options, plugin_manager)
except ValueError as e:
log.error(e)
return ExitCode.bad_args
except BadArgsError as e:
log.error(e)
return e.exit_code
except MissingDependencyError as e:
log.error(e)
return ExitCode.missing_dependency
with suppress(AttributeError, OSError):
signal.signal(signal.SIGBUS, sigbus)
result = run_pipeline_cli(options=options, plugin_manager=plugin_manager)
return result
if __name__ == '__main__':
multiprocessing.freeze_support()
if sys.platform not in ('win32', 'darwin'):
with suppress(RuntimeError):
multiprocessing.set_start_method('forkserver')
sys.exit(run())