Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/prime-tunnel/src/prime_tunnel/tunnel.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import atexit
import fcntl
import os
import re
Expand Down Expand Up @@ -29,6 +30,22 @@
)
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")

# Started-but-not-stopped tunnels, so an atexit backstop can delete their backend
# registrations if the caller never stops them (forgotten cleanup, unhandled
# exception)
_active_tunnels: "set[Tunnel]" = set()


def _stop_active_tunnels() -> None:
for tunnel in list(_active_tunnels):
try:
tunnel.sync_stop()
except Exception:
pass


atexit.register(_stop_active_tunnels)
Comment thread
kcoopermiller marked this conversation as resolved.


def _parse_frpc_error(
output_lines: list[str],
Expand Down Expand Up @@ -205,6 +222,8 @@ async def start(self) -> str:

self._started = True

_active_tunnels.add(self)

return self.url

async def stop(self) -> None:
Expand All @@ -220,6 +239,8 @@ def sync_stop(self) -> None:
if not self._started:
return

_active_tunnels.discard(self)

if self._process is not None:
try:
self._process.terminate()
Expand Down Expand Up @@ -258,6 +279,8 @@ def sync_stop(self) -> None:

async def _cleanup(self) -> None:
"""Clean up tunnel resources."""
_active_tunnels.discard(self)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Premature discard defeats atexit backstop on cancellation

Low Severity

_active_tunnels.discard(self) at the top of _cleanup() removes the tunnel from the atexit tracking set before any actual cleanup runs. If the async cleanup is interrupted by CancelledError at an await point (e.g., delete_tunnel or client.close), the error escapes the except Exception handlers (since CancelledError is a BaseException), skipping remaining cleanup steps. Because the tunnel was already discarded, the atexit backstop in _stop_active_tunnels can never retry the incomplete cleanup — defeating its stated purpose of catching "forgotten cleanup" and "unhandled exception" scenarios. Moving the discard to the end of _cleanup() would let the atexit handler pick up partially-cleaned tunnels via sync_stop.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7784d73. Configure here.


# Stop frpc process (this will cause drain threads to exit via EOF)
if self._process is not None:
try:
Expand Down
Loading