Skip to content

fix(office): stop all of LibreOffice at its time limit — kill the soffice process group - #302

Open
MrChengLen wants to merge 1 commit into
mainfrom
pr-libreoffice-process-group
Open

MrChengLen wants to merge 1 commit into
mainfrom
pr-libreoffice-process-group

Conversation

@MrChengLen

Copy link
Copy Markdown
Owner

What

DOCX → PDF through LibreOffice (office image) — and Word files inserted by /pdf/insert — now stops all of LibreOffice when OFFICE_SUBPROCESS_TIMEOUT_SECONDS is reached, not only the program the app started.

Why

soffice only launches LibreOffice: on Linux the soffice script execs the oosplash launcher, which runs the worker (soffice.bin) as its own child. Past its timeout, subprocess.run() stops only the program it started, so the conversion ended for the request, but not for LibreOffice's worker.

How

  • New private helper _run_soffice() in app/converters/document.py: subprocess.Popen(..., start_new_session=True) makes the call one process group; on a timeout or any other error while waiting, os.killpg(..., SIGKILL) stops the whole group before the exception is re-raised. It returns a CompletedProcess, so _convert_via_libreoffice and its callers are unchanged (Windows, without os.killpg: proc.kill() as before).
  • Rejected: running soffice.bin directly (bypasses the launcher, which also handles LibreOffice's restart requests); wrapping the call in coreutils timeout (extra dependency); killing the group after a normal exit too (the group can be empty by then and its ID reused).

Tests

  • tests/test_office_timeout.py (Linux): a stand-in launcher that starts a worker and waits for it.
    • Control: a plain subprocess.run timeout leaves the worker running (proves the stand-in shows the case).
    • Fix: _convert_via_libreoffice with a 1 s limit leaves nothing running.
  • .github/workflows/docker-pr.yml, office image: the real LibreOffice runs into a 5 s limit on a long document; the step checks that its worker ran mid-conversion and that none is left after the limit.
  • The soffice fakes in test_office_gs_isolation.py and test_log_privacy.py now replace _run_soffice; a new test there runs _run_soffice for real against a Python child on any platform (the env it is given reaches the child; status and output come back like subprocess.run(capture_output=True)).
  • Locally (Windows): full suite 3105 passed, 274 skipped; the one local failure is the env-only test_stripe_ca_bundle (local venv lags the lock; green in CI). ruff clean; gitleaks + scope guard clean; security and code review without blocking findings (their test and wording points are in). The two process tests skip on Windows and run here in CI.

Docs

docs/self-hosting.md, docs/architecture.md, docs/security-overview.md: the limit stops LibreOffice's worker along with the launcher. Changelog fragment changelog.d/2026-10-29-libreoffice-timeout.md.

🤖 Generated with Claude Code

…fice process group

soffice only launches LibreOffice: on Linux the soffice script execs the
oosplash launcher, which runs the worker (soffice.bin) as its own child.
Past its timeout, subprocess.run() stops only the program it started, so a
DOCX -> PDF conversion that reached OFFICE_SUBPROCESS_TIMEOUT_SECONDS ended
for the request, but not for LibreOffice's worker.

_run_soffice() now starts soffice in a session of its own
(start_new_session=True), so the call is one process group, and on a
timeout or any other error while waiting it kills that group with SIGKILL
before re-raising. It returns a CompletedProcess, so _convert_via_libreoffice
and its callers (DocxToPdfConverter, Word files inserted by /pdf/insert) are
unchanged. Where os.killpg does not exist (Windows) it falls back to
proc.kill(), as before.

Rejected: running soffice.bin directly (bypasses the launcher, which also
handles LibreOffice's restart requests); wrapping the call in coreutils
timeout (an extra dependency for what the standard library offers); killing
the group after a normal exit as well (the group can be empty by then and
its ID reused).

Tests: tests/test_office_timeout.py (Linux) runs a stand-in launcher that
starts a worker and waits for it. A control test shows that a plain
subprocess.run timeout leaves the worker running; the fix test shows that
_convert_via_libreoffice leaves nothing. docker-pr.yml gains a step that
runs the real LibreOffice in the office image into its time limit on a long
document and checks that its worker ran mid-conversion and is gone after
the limit. The soffice fakes in test_office_gs_isolation.py and
test_log_privacy.py now replace _run_soffice, and a new test there runs
_run_soffice for real against a Python child on any platform: the env it
is given reaches the child, and status and output come back the way
subprocess.run(capture_output=True) returns them.

Docs: self-hosting, architecture and the security overview say that the
limit stops LibreOffice's worker along with the launcher; changelog
fragment added.

Full suite locally 3105 passed, 274 skipped (the 2 process tests skip on
Windows and run in CI); the one local failure is the env-only
test_stripe_ca_bundle (the local venv lags the lock; green in CI).
ruff clean; gitleaks + scope guard clean; security and code review done.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant