Skip to content

Commit cd4b0e1

Browse files
Make server proxy notif port listing async (#301)
1 parent 4b10273 commit cd4b0e1

1 file changed

Lines changed: 51 additions & 34 deletions

File tree

  • src/jupyter-common/extension-builder/extension/server-proxy-notif/server_proxy_notif

src/jupyter-common/extension-builder/extension/server-proxy-notif/server_proxy_notif/handlers.py

Lines changed: 51 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import json
2-
import subprocess
2+
import getpass
33
import re
4+
import subprocess
45
import logging
5-
from typing import Set, Dict, List
6+
from typing import Set, Optional, Tuple
67
from jupyter_server.base.handlers import APIHandler
78
from jupyter_server.utils import url_path_join
89
import tornado
10+
from tornado.process import Subprocess
11+
from tornado.gen import with_timeout
12+
from datetime import timedelta
913

1014
logger = 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

Comments
 (0)