1414import signal
1515import threading
1616import weakref
17- import psutil
1817import shutil
1918import sys
2019from 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+
2340logger = logging .getLogger ("singbox2proxy" )
2441logger .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