Skip to content
Merged
Show file tree
Hide file tree
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
21 changes: 21 additions & 0 deletions src/deno_sandbox/bridge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import asyncio
import threading


class AsyncBridge:
def __init__(self):
self.loop = asyncio.new_event_loop()
self.thread = threading.Thread(target=self._run_loop, daemon=True)
self.thread.start()

def _run_loop(self):
asyncio.set_event_loop(self.loop)
self.loop.run_forever()

def run(self, coro):
"""Submit a coroutine to the loop and wait for the result."""
return asyncio.run_coroutine_threadsafe(coro, self.loop).result()

def stop(self):
self.loop.call_soon_threadsafe(self.loop.stop)
self.thread.join()
13 changes: 13 additions & 0 deletions src/deno_sandbox/rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pydantic import BaseModel
from websockets import ConnectionClosed

from deno_sandbox.bridge import AsyncBridge
from deno_sandbox.transport import Transport


Expand Down Expand Up @@ -95,3 +96,15 @@ async def _listener(self) -> None:
for future in self._pending_requests.values():
if not future.done():
future.set_exception(e)


class RpcClient:
def __init__(self, async_client: AsyncRpcClient, bridge: AsyncBridge):
self._async_client = async_client
self._bridge = bridge

def call(self, method: str, params: Dict[str, Any]) -> Any:
return self._bridge.run(self._async_client.call(method, params))

def close(self):
self._bridge.run(self._async_client.close())
32 changes: 28 additions & 4 deletions src/deno_sandbox/sandbox.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import asyncio
from contextlib import asynccontextmanager
from contextlib import asynccontextmanager, contextmanager
from dataclasses import asdict, dataclass, field
import json
from typing import AsyncIterator
import uuid
from pydantic import BaseModel, Field
from typing_extensions import Literal

from deno_sandbox.bridge import AsyncBridge
from deno_sandbox.options import Options, get_internal_options
from deno_sandbox.rpc import AsyncRpcClient
from deno_sandbox.rpc import AsyncRpcClient, RpcClient
from deno_sandbox.sandbox_generated import (
AsyncSandboxHandle as GeneratedAsyncSandboxHandle,
SpawnArgs,
AsyncSandboxProcess as GeneratedAsyncSandboxProcess,
SandboxHandle,
)
from deno_sandbox.transport import (
Transport,
Expand Down Expand Up @@ -80,6 +82,27 @@ class AppConfig:
memory_mb: int | None


class Sandbox:
def __init__(self, options=None):
self._bridge = AsyncBridge()
self._async_sandbox = AsyncSandbox(options)

@contextmanager
def create(self, options=None):
async_cm = self._async_sandbox.create(options)
async_handle = self._bridge.run(async_cm.__aenter__())

rpc = RpcClient(async_handle._rpc, self._bridge)

try:
yield SandboxHandle(rpc, async_handle.id)
finally:
self._bridge.run(async_cm.__aexit__(None, None, None))

def close(self):
self._bridge.stop()


class AsyncSandboxProcess(GeneratedAsyncSandboxProcess):
async def spawn(self, args: SpawnArgs) -> RemoteProcess:
result: SpawnResult = await super().spawn(args)
Expand All @@ -99,6 +122,7 @@ def __init__(
):
self.__options = get_internal_options(options or Options())
self._transport_factory = WebSocketTransportFactory()
self._rpc: AsyncRpcClient | None = None

async def _init_transport(self, app_config: AppConfig) -> Transport:
transport = self._transport_factory.create_transport()
Expand Down Expand Up @@ -128,8 +152,8 @@ async def create(
transport = await self._init_transport(app_config)

try:
rpc = AsyncRpcClient(transport)
yield AsyncSandboxHandle(rpc, sandbox_id)
self._rpc = AsyncRpcClient(transport)
yield AsyncSandboxHandle(self._rpc, sandbox_id)
finally:
await transport.close()

Expand Down
Loading