diff --git a/yacv_server/__init__.py b/yacv_server/__init__.py index e69de29..9fa4f5a 100644 --- a/yacv_server/__init__.py +++ b/yacv_server/__init__.py @@ -0,0 +1,29 @@ +import os +import time + +from OCP.TopoDS import TopoDS_Shape + +from server import Server + +server = Server() + +if 'YACV_DISABLE_SERVER' not in os.environ: + # Start a new server ASAP to let the polling client connect while still building CAD objects + # This is a bit of a hack, but it is seamless to the user. This behavior can be disabled by setting + # the environment variable YACV_DISABLE_SERVER to a non-empty value + server.start() + + +def show_object(obj: TopoDS_Shape): + """Show a CAD object in the default server""" + server.show_object(obj) + + +if __name__ == '__main__': + # Publish the logo to the server + from logo.logo import build_logo + + assert server is not None + server.show_object(build_logo()) + + time.sleep(60) diff --git a/yacv_server/events.py b/yacv_server/events.py index 1831c09..eeb6c06 100644 --- a/yacv_server/events.py +++ b/yacv_server/events.py @@ -1,4 +1,5 @@ -from typing import TypeVar, Generic, List, Callable +import asyncio +from typing import TypeVar, Generic, List, Callable, Tuple T = TypeVar('T') @@ -8,20 +9,32 @@ class EventPublisher(Generic[T]): _listeners: List[Callable[[T], None]] _buffer: List[T] + _lock: asyncio.Lock def __init__(self): self._listeners = [] self._buffer = [] + self._lock = asyncio.Lock() - def subscribe(self, listener: Callable[[T], None]): - self._listeners.append(listener) - for data in self._buffer: - listener(data) + async def subscribe(self, listener: Callable[[T], None]): + async with self._lock: + self._listeners.append(listener) + for data in self._buffer: + listener(data) def unsubscribe(self, listener: Callable[[T], None]): - self._listeners.remove(listener) + async with self._lock: + self._listeners.remove(listener) def emit(self, data: T): - self._buffer.append(data) - for listener in self._listeners: - listener(data) + async with self._lock: + self._buffer.append(data) + for listener in self._listeners: + listener(data) + + def buffer(self) -> Tuple[List[T], asyncio.Lock]: + return self._buffer, self._lock + + def clear(self): + async with self._lock: + self._buffer.clear() diff --git a/yacv_server/logo/logo.py b/yacv_server/logo/logo.py index e3b481c..9419913 100644 --- a/yacv_server/logo/logo.py +++ b/yacv_server/logo/logo.py @@ -1,18 +1,19 @@ +from OCP.TopoDS import TopoDS_Shape from build123d import * from tqdm import tqdm from tessellate import tessellate, tessellate_count -def logo() -> Compound: +def build_logo() -> TopoDS_Shape: """Builds the CAD part of the logo""" with BuildPart() as logo_obj: Box(1, 2, 3) - return logo_obj.part + return logo_obj.part.wrapped if __name__ == "__main__": - obj = logo() + obj = build_logo() for update in tqdm(tessellate(obj.wrapped), total=tessellate_count(obj.wrapped)): # print(update.gltf) diff --git a/yacv_server/main.py b/yacv_server/main.py new file mode 100644 index 0000000..17f672f --- /dev/null +++ b/yacv_server/main.py @@ -0,0 +1,6 @@ +from os import system + +if __name__ == '__main__': + # Just a reminder that a hot-reloading server can be started with the following command: + # Need to disable auto-start to avoid conflicts with the hot-reloading server + system('YACV_DISABLE_SERVER=true aiohttp-devtools runserver server.py --port 32323') diff --git a/yacv_server/server.py b/yacv_server/server.py index e69de29..8b9c26a 100644 --- a/yacv_server/server.py +++ b/yacv_server/server.py @@ -0,0 +1,69 @@ +import asyncio +import atexit +import os +import signal +from threading import Thread +from typing import Optional + +from OCP.TopoDS import TopoDS_Shape +from aiohttp import web + +FRONTEND_BASE_PATH = os.getenv('FRONTEND_BASE_PATH', '../dist') + + +# noinspection PyUnusedLocal +async def _index_handler(request: web.Request) -> web.Response: + return web.HTTPTemporaryRedirect(location='index.html') + + +class Server: + app = web.Application() + runner: web.AppRunner + thread: Optional[Thread] = None + + def __init__(self, *args, **kwargs): + # --- Routes --- + # - API + # self.app.router.add_route({'POST','GET'}, '/api/{collection}', api_handler) + # - Static files from the frontend + self.app.router.add_get('/{path:(.*/|)}', _index_handler) # Any folder -> index.html + self.app.router.add_static('/', path=FRONTEND_BASE_PATH, name='static_frontend') + # --- Misc --- + self.runner = web.AppRunner(self.app) + + def start(self): + """Starts the web server in the background""" + assert self.thread is None, "Server already started" + self.thread = Thread(target=self.run_server, name='yacv_server', daemon=True) + signal.signal(signal.SIGINT | signal.SIGTERM, self.stop) + atexit.register(self.stop) + self.thread.start() + + # noinspection PyUnusedLocal + def stop(self, *args): + """Stops the web server""" + print('Stopping server...') + if self.thread is None: + print('Cannot stop server because it is not running') + return + asyncio.run(self.runner.shutdown()) + asyncio.run(self.app.cleanup()) + self.thread = None # FIXME: Not properly cleaned up (join blocks forever) + print('Cleanup done') + + def run_server(self): + """Runs the web server""" + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(self.runner.setup()) + site = web.TCPSite(self.runner, os.getenv('YACV_HOST', 'localhost'), int(os.getenv('YACV_PORT', 32323))) + loop.run_until_complete(site.start()) + loop.run_forever() + + def show_object(self, obj: TopoDS_Shape): + pass + + +def get_app() -> web.Application: + """Required by aiohttp-devtools""" + return Server().app