Source code for subsequence.live_server
"""TCP eval server for live coding a running composition.
Start the server by calling ``composition.live()`` before ``composition.play()``.
The server listens on a TCP port (default 5555) and accepts Python code from any
source - the bundled REPL client, an editor plugin, or a raw socket connection.
Protocol
────────
Messages are delimited by ``\\x04`` (ASCII EOT). The server reads until it
receives this sentinel, evaluates the code, and sends the result (or error
traceback) followed by ``\\x04``.
Security note: the server binds to ``localhost`` only and is opt-in via
``composition.live()``. It executes arbitrary Python in the composition's
process - this is intentional for live coding. Any process on the same
machine that can connect to the port has full code execution in this
process, so do not enable live mode on shared or multi-user hosts, and
never expose the port to a network.
"""
import asyncio
import logging
import traceback
import typing
if typing.TYPE_CHECKING:
from subsequence.composition import Composition
logger = logging.getLogger(__name__)
SENTINEL = b"\x04"
[docs]
class LiveServer:
"""Async TCP server that evaluates Python code inside a running composition."""
def __init__ (self, composition: "Composition", port: int = 5555) -> None:
"""Store a reference to the composition and the port to listen on."""
self._composition = composition
self._port = port
self._server: typing.Optional[asyncio.AbstractServer] = None
self._namespace: typing.Dict[str, typing.Any] = {}
[docs]
async def start (self) -> None:
"""Start listening for connections on localhost."""
self._namespace = self._composition._build_live_namespace()
self._server = await asyncio.start_server(
self._handle_connection,
host = "127.0.0.1",
port = self._port
)
logger.info(f"Live server listening on 127.0.0.1:{self._port}")
[docs]
async def stop (self) -> None:
"""Close the server and wait for it to shut down."""
if self._server is not None:
self._server.close()
await self._server.wait_closed()
self._server = None
logger.info("Live server stopped")
async def _handle_connection (self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
"""Handle a single client connection with an eval/exec loop."""
peer = writer.get_extra_info("peername")
logger.info(f"Live client connected: {peer}")
try:
while True:
code = await self._read_message(reader)
if code is None:
break
response = await asyncio.to_thread(self._evaluate, code)
writer.write(response.encode() + SENTINEL)
await writer.drain()
except ConnectionResetError:
logger.info(f"Live client disconnected (reset): {peer}")
except Exception as exc:
logger.warning(f"Live connection error: {exc}")
finally:
writer.close()
try:
await writer.wait_closed()
except Exception:
pass
logger.info(f"Live client disconnected: {peer}")
async def _read_message (self, reader: asyncio.StreamReader) -> typing.Optional[str]:
"""Read bytes until the sentinel or EOF, returning the decoded string or None."""
chunks: typing.List[bytes] = []
while True:
try:
chunk = await reader.read(4096)
except ConnectionResetError:
return None
if not chunk:
return None
if SENTINEL in chunk:
before, _, _ = chunk.partition(SENTINEL)
chunks.append(before)
break
chunks.append(chunk)
data = b"".join(chunks).decode("utf-8").strip()
return data if data else None
def _evaluate (self, code: str) -> str:
"""Validate, then eval/exec the code string. Return the result or error traceback."""
# Validate syntax before executing - never run invalid code.
try:
compile(code, "<live>", "exec")
except SyntaxError:
return traceback.format_exc()
# Try as an expression first (returns a value).
try:
result = eval(compile(code, "<live>", "eval"), self._namespace)
return repr(result) if result is not None else "OK"
except SyntaxError:
pass
except SystemExit:
return "SystemExit is not allowed in live mode."
except Exception:
return traceback.format_exc()
# Fall back to statement execution.
try:
exec(compile(code, "<live>", "exec"), self._namespace)
return "OK"
except SystemExit:
return "SystemExit is not allowed in live mode."
except Exception:
return traceback.format_exc()