Files
2026-07-13 12:47:58 +08:00

98 lines
3.6 KiB
Python

import asyncio
import inspect
from typing import Any, AsyncGenerator, Awaitable, Callable, Optional
from e2b.envd.rpc import ahandle_rpc_exception_with_health
from e2b.envd.filesystem.filesystem_pb2 import WatchDirResponse
from e2b.sandbox.filesystem.filesystem import map_entry_info
from e2b.sandbox.filesystem.watch_handle import FilesystemEvent, map_event_type
from e2b.sandbox_async.utils import OutputHandler
class AsyncWatchHandle:
"""
Handle for watching a directory in the sandbox filesystem.
Use `.stop()` to stop watching the directory.
"""
def __init__(
self,
events: AsyncGenerator[WatchDirResponse, Any],
on_event: OutputHandler[FilesystemEvent],
on_exit: Optional[OutputHandler[Optional[Exception]]] = None,
check_health: Optional[Callable[[], Awaitable[Optional[bool]]]] = None,
):
self._events = events
self._on_event = on_event
self._on_exit = on_exit
self._check_health = check_health
self._wait = asyncio.create_task(self._handle_events())
async def stop(self):
"""
Stop watching the directory.
"""
self._wait.cancel()
await asyncio.wait([self._wait])
try:
await self._events.aclose()
except Exception:
pass
async def _iterate_events(self):
try:
async for event in self._events:
if event.HasField("filesystem"):
event_type = map_event_type(event.filesystem.type)
if event_type:
yield FilesystemEvent(
name=event.filesystem.name,
type=event_type,
entry=(
map_entry_info(event.filesystem.entry)
if event.filesystem.HasField("entry")
else None
),
)
except Exception as e:
raise await ahandle_rpc_exception_with_health(e, self._check_health)
async def _call_on_exit(self, error: Optional[Exception]):
if self._on_exit is None:
return
try:
cb = self._on_exit(error)
if inspect.isawaitable(cb):
await cb
except Exception:
# `on_exit` is the terminal callback; an error it raises has nowhere
# to propagate in this background task, so it's swallowed to avoid an
# "Task exception was never retrieved" warning. A `CancelledError`
# (a `BaseException`) is intentionally not caught here.
pass
async def _handle_events(self):
error: Optional[Exception] = None
try:
async for event in self._iterate_events():
cb = self._on_event(event)
if inspect.isawaitable(cb):
await cb
except asyncio.CancelledError:
# `stop()` cancels this task to end the watch. Treat it as a clean,
# user-initiated end: fire `on_exit` (with no error), then propagate
# the cancellation so the task still finishes as cancelled.
await self._call_on_exit(None)
raise
except Exception as e:
error = e
# `on_exit` fires exactly once when the watch ends — with the error when
# the stream failed, or with `None` on a clean end. This matches the JS
# SDK, which calls `onExit()` after the loop completes and `onExit(err)`
# on error.
await self._call_on_exit(error)