11import json
2- import subprocess
2+ import getpass
33import re
4+ import subprocess
45import logging
5- from typing import Set , Dict , List
6+ from typing import Set , Optional , Tuple
67from jupyter_server .base .handlers import APIHandler
78from jupyter_server .utils import url_path_join
89import tornado
10+ from tornado .process import Subprocess
11+ from tornado .gen import with_timeout
12+ from datetime import timedelta
913
1014logger = logging .getLogger (__name__ )
1115
@@ -14,8 +18,8 @@ class PortMonitorHandler(APIHandler):
1418 """Handler for monitoring listening ports"""
1519
1620 @tornado .web .authenticated
17- def get (self ):
18- """Get currently listening ports"""
21+ async def get (self ):
22+ """Get currently listening ports asynchronously """
1923 try :
2024 # Check if jupyter-server-proxy is installed
2125 try :
@@ -30,7 +34,7 @@ def get(self):
3034 return
3135
3236 logger .info ('Port monitor API called' )
33- ports , warning = self ._get_listening_ports ()
37+ ports , warning = await self ._get_listening_ports ()
3438 logger .info (f'Found { len (ports )} listening ports: { sorted (ports )} ' )
3539 response = {'ports' : list (ports )}
3640 if warning :
@@ -43,48 +47,61 @@ def get(self):
4347 'error' : str (e )
4448 }))
4549
46- def _get_listening_ports (self ) -> tuple [Set [int ], str ]:
47- """Get list of listening ports using lsof (user-owned only)
50+ async def _get_listening_ports (self ) -> Tuple [Set [int ], Optional [ str ] ]:
51+ """Get list of listening ports using lsof asynchronously (user-owned only)
4852
4953 Returns:
5054 tuple: (set of port numbers, warning message or None)
5155 """
5256 ports = set ()
5357 warning = None
5458
59+ # Get current username
60+ username = getpass .getuser ()
61+
62+ # Use lsof to find listening TCP ports owned by current user
63+ # -a means AND (combine conditions)
64+ # -u $USER shows only current user's processes
65+ # -i TCP -s TCP:LISTEN shows only listening TCP sockets
66+ # -P prevents port name resolution (shows numbers)
67+ # -n prevents hostname resolution (faster)
5568 try :
56- # Use lsof to find listening TCP ports owned by current user
57- # -a means AND (combine conditions)
58- # -u $USER shows only current user's processes
59- # -i TCP -s TCP:LISTEN shows only listening TCP sockets
60- # -P prevents port name resolution (shows numbers)
61- # -n prevents hostname resolution (faster)
62- result = subprocess .run (
63- ['lsof' , '-a' , '-u' , str (subprocess .getoutput ('whoami' )), '-i' , 'TCP' , '-s' , 'TCP:LISTEN' , '-P' , '-n' ],
64- capture_output = True ,
65- text = True ,
66- timeout = 5
69+ process = Subprocess (
70+ ['lsof' , '-a' , '-u' , username , '-i' , 'TCP' , '-s' , 'TCP:LISTEN' , '-P' , '-n' ],
71+ stdout = Subprocess .STREAM ,
72+ stderr = Subprocess .STREAM
6773 )
74+ except FileNotFoundError :
75+ logger .warning ('lsof command not found, cannot detect listening ports' )
76+ warning = 'lsof not installed'
77+ return ports , warning
6878
69- if result .returncode == 0 :
70- # Parse lsof output
71- # Example line: python3 12345 user 3u IPv4 0x1234 0t0 TCP *:8080 (LISTEN)
72- # python3 12345 user 3u IPv4 0x1234 0t0 TCP 127.0.0.1:8080 (LISTEN)
73- for line in result .stdout .split ('\n ' ):
74- if 'LISTEN' in line :
75- # Extract port from patterns like:
76- # *:PORT, 127.0.0.1:PORT, localhost:PORT, [::]:PORT
77- # Port is always after the last colon before (LISTEN)
78- match = re .search (r':(\d+)\s+\(LISTEN\)' , line )
79- if match :
80- ports .add (int (match .group (1 )))
81- except subprocess .SubprocessError as e :
79+ try :
80+ await with_timeout (timedelta (seconds = 5 ), process .wait_for_exit (raise_error = True ))
81+ except tornado .gen .TimeoutError :
82+ process .proc .kill ()
83+ await process .wait_for_exit (raise_error = False )
84+ logger .warning ('lsof command timed out' )
85+ warning = 'lsof timed out'
86+ return ports , warning
87+ except subprocess .CalledProcessError as e :
8288 # lsof may return exit code 1 if no results found, which is fine
8389 if e .returncode != 1 :
8490 raise
85- except FileNotFoundError :
86- logger .warning ('lsof command not found, cannot detect listening ports' )
87- warning = 'lsof not installed'
91+ return ports , warning
92+
93+ # Parse lsof output
94+ # Example line: python3 12345 user 3u IPv4 0x1234 0t0 TCP *:8080 (LISTEN)
95+ # python3 12345 user 3u IPv4 0x1234 0t0 TCP 127.0.0.1:8080 (LISTEN)
96+ output = (await process .stdout .read_until_close ()).decode ('utf-8' )
97+ for line in output .split ('\n ' ):
98+ if 'LISTEN' in line :
99+ # Extract port from patterns like:
100+ # *:PORT, 127.0.0.1:PORT, localhost:PORT, [::]:PORT
101+ # Port is always after the last colon before (LISTEN)
102+ match = re .search (r':(\d+)\s+\(LISTEN\)' , line )
103+ if match :
104+ ports .add (int (match .group (1 )))
88105
89106 return ports , warning
90107
0 commit comments