Skip to content

Commit 9bdd09c

Browse files
committed
ref: lazy load psutil
1 parent a15da15 commit 9bdd09c

1 file changed

Lines changed: 105 additions & 91 deletions

File tree

singbox2proxy/base.py

Lines changed: 105 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,29 @@
1414
import signal
1515
import threading
1616
import weakref
17-
import psutil
1817
import shutil
1918
import sys
2019
from pathlib import Path
2120

2221

22+
_psutil_module = None
23+
24+
def _get_psutil():
25+
"""Import psutil when needed.
26+
27+
Returns:
28+
module | None: psutil module if available, else None.
29+
"""
30+
global _psutil_module
31+
if _psutil_module is None:
32+
try:
33+
import psutil # type: ignore
34+
_psutil_module = psutil
35+
except ImportError:
36+
_psutil_module = False
37+
return _psutil_module if _psutil_module is not False else None
38+
39+
2340
logger = logging.getLogger("singbox2proxy")
2441
logger.setLevel(logging.WARNING)
2542

@@ -65,12 +82,11 @@ def cleanup_handler(signum, frame):
6582
signal.signal(signum, signal.SIG_DFL)
6683
os.kill(os.getpid(), signum)
6784

68-
# Register handlers for common termination signals
69-
if os.name != "nt": # Unix-like systems
85+
if os.name != "nt":
7086
signal.signal(signal.SIGTERM, cleanup_handler)
7187
signal.signal(signal.SIGINT, cleanup_handler)
7288
signal.signal(signal.SIGHUP, cleanup_handler)
73-
else: # Windows
89+
else:
7490
signal.signal(signal.SIGTERM, cleanup_handler)
7591
signal.signal(signal.SIGINT, cleanup_handler)
7692

@@ -152,13 +168,7 @@ def _install_via_sh(
152168
"""Download and run the official sing-box install script.
153169
154170
This uses the upstream install.sh which handles deb/rpm/Arch/OpenWrt/etc.
155-
Parameters:
156-
- beta: pass --beta to installer to install latest beta
157-
- version: pass --version <version> to installer
158-
- use_sudo: if True and not running as root, attempts to prefix with sudo
159-
- install_url: URL of the install script (default official URL)
160-
Returns:
161-
- True on success, raises RuntimeError on failure.
171+
Uses upstream install.sh (deb/rpm/Arch/OpenWrt/etc.).
162172
"""
163173
logger.info("Installing sing-box via upstream install script")
164174
if os.name == "nt":
@@ -1585,16 +1595,16 @@ def _terminate_process(self, timeout=2) -> bool:
15851595

15861596
def _terminate_windows_process(self, pid, timeout):
15871597
"""Terminate process on Windows."""
1588-
try:
1589-
# Use psutil
1598+
ps = _get_psutil()
1599+
if ps is not None:
15901600
try:
1591-
parent = psutil.Process(pid)
1601+
parent = ps.Process(pid)
15921602
children = parent.children(recursive=True)
15931603

15941604
for child in children:
15951605
try:
15961606
child.terminate()
1597-
except psutil.NoSuchProcess:
1607+
except ps.NoSuchProcess:
15981608
pass
15991609

16001610
parent.terminate()
@@ -1603,57 +1613,58 @@ def _terminate_windows_process(self, pid, timeout):
16031613
parent.wait(timeout=timeout)
16041614
self._process_terminated.set()
16051615
return True
1606-
except psutil.TimeoutExpired:
1616+
except ps.TimeoutExpired:
16071617
# Force kill if timeout
16081618
logger.warning("Process didn't terminate gracefully, force killing")
16091619
for child in children:
16101620
try:
16111621
child.kill()
1612-
except psutil.NoSuchProcess:
1622+
except ps.NoSuchProcess:
16131623
pass
16141624
parent.kill()
16151625
parent.wait(timeout=1)
16161626
self._process_terminated.set()
16171627
return True
1618-
1619-
except psutil.NoSuchProcess:
1628+
except ps.NoSuchProcess:
16201629
self._process_terminated.set()
16211630
return True
1631+
except Exception as e:
1632+
logger.debug(f"psutil termination path failed, falling back: {e}")
16221633

1623-
except ImportError:
1624-
# Fallback to subprocess
1625-
try:
1626-
subprocess.run(["taskkill", "/F", "/T", "/PID", str(pid)], check=False, capture_output=True, timeout=timeout)
1627-
time.sleep(0.001)
1628-
if self.singbox_process.poll() is not None:
1629-
self._process_terminated.set()
1630-
return True
1631-
except (subprocess.TimeoutExpired, FileNotFoundError):
1632-
pass
1633-
1634-
# Final fallback
1635-
try:
1636-
self.singbox_process.terminate()
1637-
self.singbox_process.wait(timeout=timeout)
1638-
self._process_terminated.set()
1639-
return True
1640-
except subprocess.TimeoutExpired:
1641-
self.singbox_process.kill()
1642-
self.singbox_process.wait(timeout=1)
1634+
# Fallback to subprocess / default methods
1635+
try:
1636+
subprocess.run(["taskkill", "/F", "/T", "/PID", str(pid)], check=False, capture_output=True, timeout=timeout)
1637+
time.sleep(0.001)
1638+
if self.singbox_process.poll() is not None:
16431639
self._process_terminated.set()
16441640
return True
1641+
except (subprocess.TimeoutExpired, FileNotFoundError):
1642+
pass
1643+
1644+
# Final fallback
1645+
try:
1646+
self.singbox_process.terminate()
1647+
self.singbox_process.wait(timeout=timeout)
1648+
self._process_terminated.set()
1649+
return True
1650+
except subprocess.TimeoutExpired:
1651+
self.singbox_process.kill()
1652+
self.singbox_process.wait(timeout=1)
1653+
self._process_terminated.set()
1654+
return True
16451655

16461656
def _terminate_unix_process(self, pid, timeout):
16471657
"""Terminate process on Unix-like systems."""
1648-
try:
1658+
ps = _get_psutil()
1659+
if ps is not None:
16491660
try:
1650-
parent = psutil.Process(pid)
1661+
parent = ps.Process(pid)
16511662
children = parent.children(recursive=True)
16521663

16531664
for child in children:
16541665
try:
16551666
child.terminate()
1656-
except psutil.NoSuchProcess:
1667+
except ps.NoSuchProcess:
16571668
pass
16581669
parent.terminate()
16591670

@@ -1662,66 +1673,65 @@ def _terminate_unix_process(self, pid, timeout):
16621673
parent.wait(timeout=timeout)
16631674
self._process_terminated.set()
16641675
return True
1665-
except psutil.TimeoutExpired:
1676+
except ps.TimeoutExpired:
16661677
# Force kill if timeout
16671678
logger.warning("Process didn't terminate gracefully, sending SIGKILL")
16681679
for child in children:
16691680
try:
16701681
child.kill()
1671-
except psutil.NoSuchProcess:
1682+
except ps.NoSuchProcess:
16721683
pass
16731684
parent.kill()
16741685
parent.wait(timeout=1)
16751686
self._process_terminated.set()
16761687
return True
1677-
1678-
except psutil.NoSuchProcess:
1679-
# Process already terminated
1688+
except ps.NoSuchProcess:
16801689
self._process_terminated.set()
16811690
return True
1691+
except Exception as e:
1692+
logger.debug(f"psutil unix termination path failed, falling back: {e}")
16821693

1683-
except ImportError:
1684-
# Fallback without psutil
1685-
try:
1686-
# Create process group to manage child processes
1687-
if hasattr(os, "killpg"):
1688-
try:
1689-
# Try to kill the entire process group
1690-
os.killpg(os.getpgid(pid), signal.SIGTERM)
1691-
1692-
# Wait for termination
1693-
start_time = time.time()
1694-
while time.time() - start_time < timeout:
1695-
if self.singbox_process.poll() is not None:
1696-
self._process_terminated.set()
1697-
return True
1698-
time.sleep(0.001)
1699-
1700-
# Force kill if timeout
1701-
os.killpg(os.getpgid(pid), signal.SIGKILL)
1702-
self.singbox_process.wait(timeout=1)
1703-
self._process_terminated.set()
1704-
return True
1694+
# Fallback without psutil
1695+
try:
1696+
# Create process group to manage child processes
1697+
if hasattr(os, "killpg"):
1698+
try:
1699+
# Try to kill the entire process group
1700+
os.killpg(os.getpgid(pid), signal.SIGTERM)
17051701

1706-
except (ProcessLookupError, OSError):
1707-
pass
1702+
# Wait for termination
1703+
start_time = time.time()
1704+
while time.time() - start_time < timeout:
1705+
if self.singbox_process.poll() is not None:
1706+
self._process_terminated.set()
1707+
return True
1708+
time.sleep(0.001)
17081709

1709-
# Fallback to individual process termination
1710-
self.singbox_process.terminate()
1711-
try:
1712-
self.singbox_process.wait(timeout=timeout)
1713-
self._process_terminated.set()
1714-
return True
1715-
except subprocess.TimeoutExpired:
1716-
self.singbox_process.kill()
1710+
# Force kill if timeout
1711+
os.killpg(os.getpgid(pid), signal.SIGKILL)
17171712
self.singbox_process.wait(timeout=1)
17181713
self._process_terminated.set()
17191714
return True
17201715

1721-
except (ProcessLookupError, OSError):
1722-
# Process already terminated
1716+
except (ProcessLookupError, OSError):
1717+
pass
1718+
1719+
# Fallback to individual process termination
1720+
self.singbox_process.terminate()
1721+
try:
1722+
self.singbox_process.wait(timeout=timeout)
17231723
self._process_terminated.set()
17241724
return True
1725+
except subprocess.TimeoutExpired:
1726+
self.singbox_process.kill()
1727+
self.singbox_process.wait(timeout=1)
1728+
self._process_terminated.set()
1729+
return True
1730+
1731+
except (ProcessLookupError, OSError):
1732+
# Process already terminated
1733+
self._process_terminated.set()
1734+
return True
17251735

17261736
def _emergency_cleanup(self):
17271737
"""Emergency cleanup called by signal handler."""
@@ -1800,11 +1810,13 @@ def http_proxy_url(self):
18001810
def usage_memory(self):
18011811
"""Get the memory usage of the sing-box process."""
18021812
if self.singbox_process and self.singbox_process.pid:
1803-
try:
1804-
process = psutil.Process(self.singbox_process.pid)
1805-
return process.memory_info().rss
1806-
except Exception as exc:
1807-
logger.error(f"Error getting memory usage: {exc}")
1813+
ps = _get_psutil()
1814+
if ps is not None:
1815+
try:
1816+
process = ps.Process(self.singbox_process.pid)
1817+
return process.memory_info().rss
1818+
except Exception as exc:
1819+
logger.debug(f"Error getting memory usage: {exc}")
18081820
return 0
18091821

18101822
@property
@@ -1816,11 +1828,13 @@ def usage_memory_mb(self):
18161828
def usage_cpu(self):
18171829
"""Get the CPU usage of the sing-box process."""
18181830
if self.singbox_process and self.singbox_process.pid:
1819-
try:
1820-
process = psutil.Process(self.singbox_process.pid)
1821-
return process.cpu_percent(interval=1)
1822-
except Exception as exc:
1823-
logger.error(f"Error getting CPU usage: {exc}")
1831+
ps = _get_psutil()
1832+
if ps is not None:
1833+
try:
1834+
process = ps.Process(self.singbox_process.pid)
1835+
return process.cpu_percent(interval=1)
1836+
except Exception as exc:
1837+
logger.debug(f"Error getting CPU usage: {exc}")
18241838
return 0
18251839

18261840
def __enter__(self):

0 commit comments

Comments
 (0)