yeicor-3d / yet-another-cad-viewer

A CAD viewer capable of displaying OCP models (CadQuery/Build123d) in a web browser.
https://yeicor-3d.github.io/yet-another-cad-viewer/
MIT License
48 stars 2 forks source link

example file object.py: server exits after 1 request or after a short timeout #149

Closed ccazabon closed 2 months ago

ccazabon commented 2 months ago

I'm probably doing something wrong here - but I have yacv-server 0.8.10 installed in a venv with cadquery 2.4.0 (and their various dependencies) under Python 3.11 on Debian Linux 12/bookworm (64-bit).

The .../examples/object.py file has comments saying that it should support hot-reloading with a client connected, but I can't seem to keep it running. If I start it with python3.11 object.py and do nothing, it logs that it has rendered the object and says it's waiting for at least one frontend request before stopping the server:

$ python3.11 object.py 
INFO:yacv_server:Using yacv-server v0.8.10
INFO:yacv_server:Starting server...
INFO:yacv_server:Server started (requested)...
INFO:yacv_server:Serving at http://localhost:32323
INFO:yacv_server:Server started (received)...
INFO:build123d:None context requested by None
INFO:build123d:Entering BuildPart with mode=Mode.ADD which is in different scope as parent
INFO:build123d:WorkplaneList is pushing 1 workplanes: [Plane(o=(0.00, 0.00, 0.00), x=(1.00, 0.00, 0.00), z=(0.00, 0.00, 1.00))]
INFO:build123d:LocationList is pushing 1 points: [(p=(0.00, 0.00, 0.00), o=(-0.00, 0.00, -0.00))]
INFO:build123d:BuildPart context requested by Box
DEBUG:build123d:Attempting to integrate 1 object(s) into part with Mode=Mode.ADD
INFO:build123d:Completed integrating 1 object(s) into part with Mode=Mode.ADD
INFO:build123d:BuildPart context requested by Cylinder
DEBUG:build123d:Attempting to integrate 1 object(s) into part with Mode=Mode.SUBTRACT
INFO:build123d:Completed integrating 1 object(s) into part with Mode=Mode.SUBTRACT
INFO:build123d:LocationList is popping 1 points
INFO:build123d:WorkplaneList is popping 1 workplanes
INFO:build123d:Exiting BuildPart
INFO:yacv_server:show ['example'] took 0.058 seconds
WARNING:yacv_server:Waiting for at least one frontend request before stopping server, cancel with CTRL+C...
$ 

... but it actually exits after waiting roughly 12s. During that interval, I can hit the server with a browser (Firefox) and it will load the object, but the server then immediately exits, so no hot-reloading can take place. The logs are the same as above with the following additional entries:

[...]
WARNING:yacv_server:Waiting for at least one frontend request before stopping server, cancel with CTRL+C...
DEBUG:yacv_server:"GET / HTTP/1.1" 304 -
DEBUG:yacv_server:"GET /index-CgxuMaV_.js HTTP/1.1" 304 -
DEBUG:yacv_server:"GET /style-DsMpVLof.css HTTP/1.1" 304 -
DEBUG:yacv_server:"GET /LineSegments2-CrgyrjsM.js HTTP/1.1" 304 -
DEBUG:yacv_server:"GET /three.module-_581LcUX.js HTTP/1.1" 304 -
DEBUG:yacv_server:"GET /LineSegmentsGeometry-MIc9wox0.js HTTP/1.1" 304 -
DEBUG:yacv_server:"GET /LineMaterial-CYPE-Cag.js HTTP/1.1" 304 -
DEBUG:yacv_server:Updates client connected
DEBUG:yacv_server:"GET /?api_updates=true HTTP/1.1" 200 -
DEBUG:yacv_server:Subscribed to <yacv_server.pubsub.BufferedPubSub object at 0x7f4e687435d0> (1 subscribers)
DEBUG:yacv_server:Sending info about example: UpdatesApiFullData(name='example', hash='478bebe443b7d4497fefe318aa49bc1a', is_remove=False)
DEBUG:yacv_server:"GET /ModelViewerWrapper-CtJbhGBE.js HTTP/1.1" 304 -
DEBUG:yacv_server:Sending info about __shutdown: UpdatesApiFullData(name='__shutdown', hash='', is_remove=None)
DEBUG:yacv_server:"GET /Selection-DQCU0oez.js HTTP/1.1" 304 -
DEBUG:yacv_server:Building object example with hash 478bebe443b7d4497fefe318aa49bc1a
INFO:yacv_server:export(example) took 0.014 seconds, 37.0KiB
DEBUG:yacv_server:Subscribed to <yacv_server.pubsub.BufferedPubSub object at 0x7f4e90188f10> (1 subscribers)
DEBUG:yacv_server:Unsubscribed from <yacv_server.pubsub.BufferedPubSub object at 0x7f4e90188f10> (0 subscribers)
DEBUG:yacv_server:"GET /?api_object=example HTTP/1.1" 200 -
DEBUG:yacv_server:code 503, message Server is shutting down
DEBUG:yacv_server:"GET /?api_updates=true HTTP/1.1" 503 -
DEBUG:yacv_server:Unsubscribed from <yacv_server.pubsub.BufferedPubSub object at 0x7f4e687435d0> (0 subscribers)
DEBUG:yacv_server:Updates client disconnected
$

Am I doing something incorrectly here? Or do I need to add something else to the object.py example to get it to keep running and serving updated objects?

Yeicor commented 2 months ago

The issue you're encountering arises from a misunderstanding of how the hot-reloading feature of yacv-server works. The hot-reloading functionality is specifically designed to be used in a Jupyter Notebook or a similar interactive environment, not when running a script directly with the Python interpreter.

When you run the script directly using python object.py, the server starts, renders the object, and then waits briefly for a frontend request. Once it serves the initial request, it knows no further updates can be made, and thus it shuts down. This is expected behavior when running the script directly, as the Python interpreter does not (easily and cleanly) support the dynamic re-execution of code that would be needed for hot-reloading.

The hot-reloading feature of yacv-server is designed to be used within a Jupyter notebook, where you can run cells independently using the same Python process. By running the server in a Jupyter Notebook, you can modify your code and re-run the cell that shows the object without restarting the server. This allows the server and viewer to stay active and reflect changes dynamically. It also allows reusing all previously defined objects and imports for increased performance.

ccazabon commented 2 months ago

Ah - I was planning on having my object(s) defined in separate files, and hot-reloading those from the server script. That's definitely possible (witness various web frameworks supporting it for development).

I may give implementing this a go myself. Would you be interested in a local-server script with hot-reloading support as another example/utility in the package? Is calling show(object) repeatedly (after reloading/re-running the code that creates object) (a) possible and (b) all that is needed?

I don't use notebooks; old-fashioned programmer, I guess. I just write code and run it.

ccazabon commented 2 months ago

FWIW I have a very simplistic version of this running now, and it behaves as expected; the browser view updates any time I make changes to my object file.

If you have any interest in including such a script, I can clean it up and probably make it smarter and submit it.

Thanks!

Yeicor commented 2 months ago

Is calling show(object) repeatedly (after reloading/re-running the code that creates object) (a) possible and (b) all that is needed?

Yes, calling show(...) with a different object sharing the same name will always replace the previous one and will work as hot-reloading in the frontend.

If you have any interest in including such a script, I can clean it up and probably make it smarter and submit it.

The problem with re-executing the whole file each time is that just importing cadquery/build123d takes a couple of seconds. Furthermore, complex objects may require an initialization or creating other intermediate objects that take some time to be ready. So the time from editing to viewing the result quickly increases when creating more complex objects and re-executing the whole file.

If you have a solution/mitigation for this, I'd be happy to review a PR. Otherwise, I feel like I should guide the users toward faster execution by mainly supporting the use of jupyter notebooks.

However, feel free to post it here so that if any user also prefers/needs direct python execution with hot reloading, they can find a starting point here. Thanks for considering contributing!

ccazabon commented 2 months ago

I think you may be misunderstanding something here; the user's object module (and any sub-modules or other related modules) get reloaded when those files change, but cadquery, build123d, and other 3rd-party packages (which are almost never going to be changing) don't. When the user's object module is reloaded, the import build123d is just the normal almost-a-no-op binding of the already-loaded and initialized module from sys.modules to the local name.

With my simple object testing so far, I don't notice the reload time - it's swamped by the 1s polling interval I'm currently using.

Yeicor commented 2 months ago

I wrongly assumed you would be executing a new Python process any time the file changes, I shouldn't have commented before seeing your script.

The polling interval is only used when the connection to the server is lost. To avoid the 1s polling interval, the server should be kept open at all times so that show calls instantly appear on the already connected frontend. If I remember correctly (I haven't used this tool in a couple of months) the server starts automatically in a secondary thread when importing yacv_server (unless disabled through a environment variable). Once the frontend connects to the server it will keep that connection alive receiving updates instantly. So, if you can, you should try to keep the server alive in your script without restarting it when the object changes.

ccazabon commented 1 month ago

I ended up making this more than just a simple script, so I'm publishing it as its own package. It uses your fantastic interactive viewer and lets the user edit their code in whatever editor they like, and it will hot-reload in the browser on any change.

It's here: https://github.com/ccazabon/cq-studio

Thanks again for YACV!

Yeicor commented 3 weeks ago

Thanks for developing cq-studio! It’s a solid option for users who prefer working outside of Jupyter Notebooks. Makes sense that you spun it off into its own project to reduce the maintenance work of YACV.

I added a mention in the README so others can check it out :wink:.