diff --git a/archivebox/opencode/views.py b/archivebox/opencode/views.py index f5c33108..2a541a3e 100644 --- a/archivebox/opencode/views.py +++ b/archivebox/opencode/views.py @@ -5,6 +5,7 @@ import base64 import logging import os import re +import signal import shutil import subprocess import threading @@ -85,17 +86,27 @@ You are running inside an ArchiveBox collection directory. """ +def _signal_owned_process(process: subprocess.Popen, sig: signal.Signals) -> None: + try: + os.killpg(process.pid, sig) + except OSError: + try: + process.send_signal(sig) + except ProcessLookupError: + pass + + def _stop_owned_process(process: subprocess.Popen | None = None) -> None: global _PROCESS owned_process = process or _PROCESS if owned_process is None: return if owned_process.poll() is None: - owned_process.terminate() + _signal_owned_process(owned_process, signal.SIGTERM) try: owned_process.wait(timeout=5) except subprocess.TimeoutExpired: - owned_process.kill() + _signal_owned_process(owned_process, signal.SIGKILL) owned_process.wait() if _PROCESS is owned_process: _PROCESS = None diff --git a/archivebox/tests/test_opencode_agent.py b/archivebox/tests/test_opencode_agent.py index bf1c670e..2ead6d81 100644 --- a/archivebox/tests/test_opencode_agent.py +++ b/archivebox/tests/test_opencode_agent.py @@ -1,5 +1,6 @@ import os import socket +import subprocess from concurrent.futures import ThreadPoolExecutor from pathlib import Path from types import SimpleNamespace @@ -105,10 +106,7 @@ def live_opencode(opencode_archive_config): try: yield SimpleNamespace(config=opencode_archive_config, settings=settings, process=process) finally: - if views._PROCESS and views._PROCESS.poll() is None: - views._PROCESS.terminate() - views._PROCESS.wait(timeout=10) - views._PROCESS = None + views._stop_owned_process() def test_opencode_disabled_route_does_not_start_server(client, initialized_archive): @@ -126,6 +124,19 @@ def test_opencode_disabled_route_does_not_start_server(client, initialized_archi assert views._PROCESS is None or views._PROCESS.poll() is not None +def test_stop_owned_process_falls_back_when_process_has_no_dedicated_group(): + from archivebox.opencode import views + + process = subprocess.Popen(["sleep", "60"]) + try: + views._stop_owned_process(process) + assert process.poll() is not None + finally: + if process.poll() is None: + process.kill() + process.wait() + + def test_opencode_agent_requires_superuser_when_enabled(client, db, django_user_model, live_opencode): response = client.get("/admin/agent", HTTP_HOST=ADMIN_TEST_HOST) assert response.status_code == 302