mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-15 02:17:26 +00:00
Fix: LSP process-group cleanup no longer requires process-table enumeration
An independently-started LSP process is already its own POSIX process group leader (start_new_session=True), so cleanup can signal that known group directly via os.killpg instead of walking the tree with psutil.Process.children(recursive=True), which requires system-wide process-table enumeration that can be denied even for processes we started and own (#1818).
This commit is contained in:
4 files changed
+284
-4
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import threading
|
||||
@@ -510,6 +511,9 @@ class StdioLanguageServer(LanguageServerInterface):
|
||||
self.process_launch_info = process_launch_info
|
||||
self.process: subprocess.Popen[bytes] | None = None
|
||||
self.start_independent_lsp_process = start_independent_lsp_process
|
||||
self._process_group_id: int | None = None
|
||||
"""the POSIX process group ID owned by ``self.process``, captured at launch time (see
|
||||
``_start``); ``None`` on Windows or when the process was not started in its own session"""
|
||||
|
||||
self._stdin_lock = threading.Lock()
|
||||
|
||||
@@ -523,6 +527,8 @@ class StdioLanguageServer(LanguageServerInterface):
|
||||
self.process_launch_info, start_new_session=self.start_independent_lsp_process
|
||||
)
|
||||
self.process = process
|
||||
# start_new_session=True makes the new process its own group leader, so its PGID is its PID.
|
||||
self._process_group_id = process.pid if (self.start_independent_lsp_process and os.name == "posix") else None
|
||||
|
||||
# Check if process terminated immediately
|
||||
if process.returncode is not None:
|
||||
@@ -559,10 +565,14 @@ class StdioLanguageServer(LanguageServerInterface):
|
||||
# Ignore errors here, we are proceeding to terminate anyway.
|
||||
# terminate the process
|
||||
subprocess_util.terminate_process_tree_with_kill_fallback(
|
||||
self.process, terminate_timeout=timeout, process_name=f"LS[{self.ls_id.value}]"
|
||||
self.process,
|
||||
terminate_timeout=timeout,
|
||||
process_name=f"LS[{self.ls_id.value}]",
|
||||
process_group_id=self._process_group_id,
|
||||
)
|
||||
finally:
|
||||
self.process = None
|
||||
self._process_group_id = None
|
||||
|
||||
@staticmethod
|
||||
def _safely_close_pipe(pipe: IO[AnyStr] | None) -> None:
|
||||
|
||||
@@ -246,7 +246,33 @@ def _signal_process_tree(process: subprocess.Popen[bytes], terminate: bool = Tru
|
||||
signal_process(process)
|
||||
|
||||
|
||||
def terminate_process_tree_with_kill_fallback(process: subprocess.Popen, terminate_timeout: float, process_name: str = "Process") -> None:
|
||||
def _signal_process_group(pgid: int, terminate: bool = True) -> None:
|
||||
"""
|
||||
Sends a signal to every process in the given POSIX process group by group ID, without
|
||||
enumerating the system process table. Requires the caller to know the group already exists
|
||||
and is owned by us (see ``terminate_process_tree_with_kill_fallback``'s ``process_group_id``).
|
||||
|
||||
:param pgid: the process group ID to signal
|
||||
:param terminate: if True, signal terminate (SIGTERM), otherwise signal kill (SIGKILL)
|
||||
"""
|
||||
sig = signal.SIGTERM if terminate else signal.SIGKILL
|
||||
try:
|
||||
os.killpg(pgid, sig)
|
||||
except ProcessLookupError:
|
||||
# the group is already gone; nothing left to signal
|
||||
pass
|
||||
except PermissionError as e:
|
||||
log.warning(f"Permission denied signaling process group {pgid} with {sig.name}: {e}")
|
||||
except Exception as e:
|
||||
log.warning(f"Unexpected error signaling process group {pgid} with {sig.name}: {e}")
|
||||
|
||||
|
||||
def terminate_process_tree_with_kill_fallback(
|
||||
process: subprocess.Popen,
|
||||
terminate_timeout: float,
|
||||
process_name: str = "Process",
|
||||
process_group_id: int | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Attempts to terminate the given process and its children by signaling them to terminate,
|
||||
and if that fails (i.e. they don't exit within the given timeout), forcefully kills them.
|
||||
@@ -256,9 +282,23 @@ def terminate_process_tree_with_kill_fallback(process: subprocess.Popen, termina
|
||||
:param process: the process to terminate
|
||||
:param terminate_timeout: the time to wait for the process to terminate gracefully before killing it
|
||||
:param process_name: the name of the process (used for logging purposes); should start with capital letter
|
||||
:param process_group_id: if given, the POSIX process group ID that ``process`` leads (i.e. it was
|
||||
launched with ``start_new_session=True``, so its PGID equals its PID). When set, cleanup
|
||||
signals the whole group directly via ``os.killpg`` instead of walking the process tree with
|
||||
``psutil``, which requires system-wide process-table enumeration (``sysctl(KERN_PROC_ALL)`` on
|
||||
macOS) that can be denied even for processes we started and own. Only pass this for a process
|
||||
that was started in its own session: signaling the group of a process that shares ours would
|
||||
also signal us.
|
||||
"""
|
||||
log.debug(f"Terminating process {process.pid}, current status: {process.poll()}")
|
||||
_signal_process_tree(process, terminate=True)
|
||||
|
||||
def signal_tree(terminate: bool) -> None:
|
||||
if process_group_id is not None:
|
||||
_signal_process_group(process_group_id, terminate=terminate)
|
||||
else:
|
||||
_signal_process_tree(process, terminate=terminate)
|
||||
|
||||
signal_tree(terminate=True)
|
||||
try:
|
||||
log.debug(f"Waiting for process {process.pid} to terminate...")
|
||||
exit_code = process.wait(timeout=terminate_timeout)
|
||||
@@ -266,7 +306,7 @@ def terminate_process_tree_with_kill_fallback(process: subprocess.Popen, termina
|
||||
except subprocess.TimeoutExpired:
|
||||
# If termination failed, forcefully kill the process
|
||||
log.warning(f"{process_name} (pid={process.pid}) termination timed out, killing process forcefully...")
|
||||
_signal_process_tree(process, terminate=False)
|
||||
signal_tree(terminate=False)
|
||||
try:
|
||||
exit_code = process.wait(timeout=2.0)
|
||||
log.info(f"{process_name} killed successfully with exit code {exit_code}.")
|
||||
|
||||
Reference in new issue
Block a user