diff --git a/.gitignore b/.gitignore index 88d7561b4f9cbe930034729397f58a9c2c6ca588..642b3f502a52a2faa7f277d14252314e7cd4b204 100644 --- a/.gitignore +++ b/.gitignore @@ -155,3 +155,6 @@ pvlink/labextension/*.tgz # Packed lab extensions pvlink/labextension + +# Visual Studio Code +.vscode \ No newline at end of file diff --git a/MANIFEST b/MANIFEST index d9403a85159e8f472ded146e7e3596b304af087a..fcc538179b4bd9d216ed2ac70a5bdd26d5b0e2ca 100644 --- a/MANIFEST +++ b/MANIFEST @@ -22,13 +22,16 @@ docs/source/_static/embed-bundle.js.map docs/source/_static/helper.js docs/source/examples/index.rst docs/source/examples/introduction.nblink -examples/introduction.ipynb -examples/.ipynb_checkpoints/introduction-checkpoint.ipynb +examples/Examples.ipynb +examples/.ipynb_checkpoints/Examples-checkpoint.ipynb pvlink/__init__.py pvlink/_frontend.py pvlink/_version.py pvlink/remoterenderer.py -pvlink/labextension/pvlink-0.1.8.tgz +pvlink/server.py +pvlink/simplerenderer.py +pvlink/utility.py +pvlink/labextension/pvlink-0.2.0.tgz pvlink/nbextension/__init__.py pvlink/nbextension/static/extension.js pvlink/nbextension/static/index.js diff --git a/README.md b/README.md index 5454ab4486bc67c048ed9d016de782a32b251e72..5ed9c3dc88118ee6db5d464463dc7d2e0b725197 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,5 @@ # pvlink -[](https://travis-ci.org//pvlink) -[](https://codecov.io/gh//pvlink) - - ParaView-Web RemoteRenderer in Jupyter ## Installation @@ -20,3 +16,43 @@ the nbextension: ```bash jupyter nbextension enable --py [--sys-prefix|--user|--system] pvlink ``` + + +## Usage +For examples see the [example notebook](examples/Examples.ipynb). +The RemoteRenderer additionally requires the `paraview.simple` and `paraview.web modules`. + + +## Jupyter Proxy Setup (using nginx) + +To enable streaming these settings need to be set, in the nginx config file for Jupyter (for example: in /etc/nginx/conf.d/): + +``` +# top-level http config for websocket headers +# If Upgrade is defined, Connection = upgrade +# If Upgrade is empty, Connection = close +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +... location ... { + ... + # websocket headers + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + ... +} +``` + +An unused stream is automatically disconnected by nginx, after `proxy_read_timeout`'s seconds are exceeded. The default value of 60s is reached quite fast, therefore it is recommended to increase this value. +For example: +``` +# HTTPS server to handle JupyterHub +server { + listen 443 ssl; + ... + proxy_read_timeout 3600s; + ... +``` diff --git a/css/widget.css b/css/widget.css index 8b137891791fe96927ad78e64b0aad7bded08bdc..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/css/widget.css +++ b/css/widget.css @@ -1 +0,0 @@ - diff --git a/examples/Examples.ipynb b/examples/Examples.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..ca5fcc5c5dc04af6cb345e819d5b1111923231bd --- /dev/null +++ b/examples/Examples.ipynb @@ -0,0 +1,403 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# SimpleRenderer" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Does nothing more than show the ParaViewWeb server application in the Output Area." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Usage" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Start a ParaViewWeb server application over the command line (see the [ParaView example](https://kitware.github.io/paraviewweb/examples/RemoteRenderer.html#Using-ParaView-as-server))\n", + "```bash\n", + "pvpython pv_server.py --port 1234 --authKey wslink-secret \n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "1b7145fe486c4a3b8f99fe3f59fe840b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "SimpleRenderer(sessionURL='ws://localhost:1234/ws')" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from pvlink import SimpleRenderer\n", + "\n", + "simple = SimpleRenderer(sessionURL='ws://localhost:1234/ws', authKey='wslink-secret')\n", + "display(simple)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Sizing\n", + "Widgets scales with container size." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "16ccf845baec4bc49832d3c39c639a7e", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Box(children=(SimpleRenderer(sessionURL='ws://localhost:1234/ws'),), layout=Layout(height='500px'))" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from ipywidgets import Box\n", + "\n", + "Box(children=[simple], layout={'height':'500px'})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# RemoteRenderer\n", + "\n", + "This renderer requires the `paraview.simple` and `paraview.web` modules.\n", + "\n", + "Upon initialization, the RemoteRenderer starts a webserver for you. If nothing is specified, the webserver will try to start on port 8080 or the next free port thereafter and create a random authentication key. You can pass your own arguments to the webserver. To display help on the possible arguments, you can call `<yourRenderer>.webserver_arguments_help()`.\n", + "\n", + "If `pvserver_host` and `pvserver_port` (default 11111) are specified, the webserver will try to establish a connection to the ParaView server (pvserver) at the given host and port. The pvserver can then take over the heavy lifting and handle very large geometries. Locally, only the image data of the processed data is recieved. To prevent data from being rendered locally, we recommend using the `SetRecommendedRenderSettings` function from `pvlink.utilities` on your displayed view." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Usage \n", + "### Default" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "a7598ebbced145e5886f9e997b6e0c2d", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "RemoteRenderer(authKey='1412e786544799a40e7231cd3e06d7bc22ecb0d6d16b978c', sessionURL='ws://localhost:8082/ws'…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from pvlink import RemoteRenderer\n", + "\n", + "renderer = RemoteRenderer(port=8082)\n", + "# Alternatively, if you want to render your data with a ParaView server:\n", + "# Start a pvserver over the command line and run the following line\n", + "# renderer = RemoteRenderer(pvserver_host='localhost', pvserver_port=11111, p=8082)\n", + "display(renderer)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "At this point, there is no view or sources, so the output will be a blank canvas." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "from paraview import simple\n", + "from pvlink.utility import SetRecommendedRenderSettings\n", + "\n", + "# Create a view and...\n", + "view = simple.CreateView('RenderView', 'example')\n", + "# ...disable interactor-based render calls and\n", + "# ensure pvserver-side rendering (if applicable)\n", + "SetRecommendedRenderSettings(view)\n", + "# Create and show a source\n", + "source = simple.Cone()\n", + "simple.Show(source, view)\n", + "# Update the renderer widget to display the changes\n", + "renderer.update_render()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Using Jupyter Server Proxy\n", + "\n", + "If you want to access your webserver using Jupyter Server Proxy, you need to set `use_jupyter_server_proxy` to True and specify the baseURL. \n", + "\n", + "Example: If your notebook url is `http://localhost:8888` and you would access a process using `http://localhost:88888/proxy/8080`, the baseURL would be the part before 'proxy, `localhost:8888`." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "proxied_renderer = RemoteRenderer(baseURL='localhost:8888', use_jupyter_server_proxy=True, \n", + " port=8080, ws='pvwebserver/ws')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If no `viewID` is specified, the widget will always show the active view. To bind it to a view, we need the GlobalID of the view and pass it to the renderer." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "14e4c3f7ba9f4a29b4d15475f3c9165c", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "RemoteRenderer(authKey='2d94f211be34b2c58ea069de4182f23f5d3986b9732ebed0', sessionURL='ws://localhost:8888/pro…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "proxied_renderer.viewID = view.GetGlobalIDAsString()\n", + "display(proxied_renderer)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can display help for the possible webserver arugments." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ParaView Web Server\n", + "\n", + "optional arguments:\n", + " -h, --help show this help message and exit\n", + " -d, --debug log debugging messages to stdout\n", + " -s, --nosignalhandlers\n", + " Prevent Twisted to install the signal handlers so it\n", + " can be started inside a thread.\n", + " -i HOST, --host HOST the interface for the web-server to listen on\n", + " (default: localhost)\n", + " -p PORT, --port PORT port number for the web-server to listen on (default:\n", + " 8080)\n", + " -t TIMEOUT, --timeout TIMEOUT\n", + " timeout for reaping process on idle in seconds\n", + " (default: 300s)\n", + " -c CONTENT, --content CONTENT\n", + " root for web-pages to serve (default: none)\n", + " -a AUTHKEY, --authKey AUTHKEY\n", + " Authentication key for clients to connect to the\n", + " WebSocket.\n", + " -f, --force-flush If provided, this option will force additional padding\n", + " content to the output. Useful when application is\n", + " triggered by a session manager.\n", + " -k SSLKEY, --sslKey SSLKEY\n", + " SSL key. Use this and --sslCert to start the server on\n", + " https.\n", + " -j SSLCERT, --sslCert SSLCERT\n", + " SSL certificate. Use this and --sslKey to start the\n", + " server on https.\n", + " -ws WS, --ws-endpoint WS\n", + " Specify WebSocket endpoint. (e.g. foo/bar/ws, Default:\n", + " ws)\n", + " --no-ws-endpoint If provided, disables the websocket endpoint\n", + " --fs-endpoints FSENDPOINTS\n", + " add another fs location to a specific endpoint (i.e:\n", + " data=/Users/seb/Download|images=/Users/seb/Pictures)\n", + " --upload-directory UPLOADPATH\n", + " path to root upload directory\n", + "\n" + ] + } + ], + "source": [ + "proxied_renderer.webserver_arguments_help()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Using a custom protocol\n", + "You can define your own pipeline which should be run when the webserver starts up.\n", + "\n", + "First, define a class which inherits from `pv_wslink.PVServerProtocol` and add your pipeline there. Per default, the RemoteRenderer shows the active view.\n", + "\n", + "To ensure that the view you created in the pipeline is the one shown in the rendering widget and does not get replaced by a new active view, we need to make it available to the outside and later on bind the viewID to the rendering widget. " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "from paraview import simple\n", + "from paraview.web import pv_wslink\n", + "from paraview.web import protocols as pv_protocols\n", + "\n", + "from pvlink.utility import SetRecommendedRenderSettings\n", + "\n", + "\n", + "class DemoServer(pv_wslink.PVServerProtocol):\n", + " authKey = 'wslink-secret'\n", + "\n", + " def initialize(self):\n", + " # Bring used components\n", + " self.registerVtkWebProtocol(pv_protocols.ParaViewWebMouseHandler())\n", + " self.registerVtkWebProtocol(pv_protocols.ParaViewWebViewPort())\n", + " self.registerVtkWebProtocol(pv_protocols.ParaViewWebViewPortImageDelivery())\n", + " # Update authentication key to use\n", + " self.updateSecret(DemoServer.authKey)\n", + "\n", + " # Your pipeline\n", + " demo_view = simple.CreateView('RenderView', 'SphereView')\n", + " SetRecommendedRenderSettings(demo_view)\n", + " demo_viewID = demo_view.GetGlobalIDAsString()\n", + " # Make the viewID available so we can bind our widget to the correct view\n", + " self.setSharedObject('viewID', demo_viewID)\n", + " sphere = simple.Sphere()\n", + " simple.Show(sphere, demo_view)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "a631c61900784286872fcf391bedc7c6", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "RemoteRenderer(authKey='mysecretkey', sessionURL='ws://localhost:8081/ws', viewID='555')" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "custom_renderer = RemoteRenderer(protocol=DemoServer, a='mysecretkey')\n", + "# Bind the viewID to the widget to avoid it showing a different view\n", + "# when a different view is set to the active view\n", + "custom_renderer.viewID = custom_renderer.protocol.getSharedObject('viewID')\n", + "display(custom_renderer)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Since we did not bind a view to `renderer`, the sphere should now also be visible in the `renderer` [output](#Default) after an update call." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "renderer.update_render()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.6" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} \ No newline at end of file diff --git a/examples/introduction.ipynb b/examples/introduction.ipynb deleted file mode 100644 index 4f9faba4e1327d38971424fb5231d02138efdefe..0000000000000000000000000000000000000000 --- a/examples/introduction.ipynb +++ /dev/null @@ -1,131 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Introduction" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import pvlink" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Start a ParaView-Web server (see the [ParaView example](https://kitware.github.io/paraviewweb/examples/RemoteRenderer.html#Using-ParaView-as-server))\n", - "```bash\n", - "pvpython pv_server.py --port 8080 --authKey wslink-secret \n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "w = pvlink.RemoteRenderer(sessionURL='ws://localhost:8080/ws', authKey='wslink-secret')" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "708e09f2fe9e4a5a883b2441b06aecb0", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RemoteRenderer()" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "display(w)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Widgets scales with container size" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "from ipywidgets import Box" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "bc30c04151414d61aa52c616506580c4", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Box(children=(RemoteRenderer(),), layout=Layout(height='500px'))" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "Box(children=[w], layout={'height':'500px'})" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.9" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/package-lock.json b/package-lock.json index eb01fb91eb6be9ce7ea67de64ed7452b9783e7b3..18351d482991e5094e5dbe959f21fae91fcdfc23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "pvlink", - "version": "0.1.8", + "version": "0.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -7082,9 +7082,9 @@ "integrity": "sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A==" }, "wslink": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/wslink/-/wslink-0.1.12.tgz", - "integrity": "sha512-W5gmX1gWXKJWMa0KuzbPA7Wr3/TXKE8Z3rzIZZ4VyfvtdDNMsgBr12lezrB0smIoCxLiDcjW1CB2kPgnn4cmFA==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/wslink/-/wslink-0.1.13.tgz", + "integrity": "sha512-7I5JOoUW3YMFKDG+WfuAZwlZPbD33eIqfk7vJS5XcOSnbHpwm2NUc6qu7req+k1HjwkRgc3+addBxkcwbN/XyQ==", "requires": { "json5": "2.1.0" }, diff --git a/package.json b/package.json index 4c07f65dbd15cb1ca9e664c1ee667d8eb6b79356..63e16271019f1bd17ca33be747b2f1064f96659c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pvlink", - "version": "0.1.2", + "version": "0.2.1", "description": "ParaView-Web RemoteRenderer in Jupyter", "keywords": [ "jupyter", @@ -52,8 +52,8 @@ "@jupyter-widgets/base": "^1.1.10 || ^2", "hammerjs": "^2.0.8", "monologue.js": "^0.3.5", - "paraviewweb": "^3.2.12", - "wslink": "^0.1.12" + "paraviewweb": "3.2.12", + "wslink": "0.1.13" }, "devDependencies": { "@phosphor/application": "^1.6.0", diff --git a/pvlink/.vscode/settings.json b/pvlink/.vscode/settings.json deleted file mode 100644 index dee5e2928989b75dfb306752dacb5b6d92d172e0..0000000000000000000000000000000000000000 --- a/pvlink/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "restructuredtext.confPath": "${workspaceFolder}/docs/source" -} \ No newline at end of file diff --git a/pvlink/__init__.py b/pvlink/__init__.py index 20ffb82e260bbd356a343820cc074ba9920a37f6..b4b4ac1fdb9bc6ad85c9669f22e0b49239627a30 100644 --- a/pvlink/__init__.py +++ b/pvlink/__init__.py @@ -4,6 +4,7 @@ # Copyright (c) Juelich Supercomputing Centre (JSC). # Distributed under the terms of the Modified BSD License. +from .simplerenderer import SimpleRenderer from .remoterenderer import RemoteRenderer from ._version import __version__, version_info diff --git a/pvlink/_version.py b/pvlink/_version.py index d5871ba44054c5661c707e53a54ec694eba4df5c..beaaf7537e75afab0945bb8f2d6fd7cd1fdaf60c 100644 --- a/pvlink/_version.py +++ b/pvlink/_version.py @@ -4,5 +4,5 @@ # Copyright (c) Juelich Supercomputing Centre (JSC). # Distributed under the terms of the Modified BSD License. -version_info = (0, 1, 2) +version_info = (0, 2, 1, 'dev') __version__ = ".".join(map(str, version_info)) diff --git a/pvlink/remoterenderer.py b/pvlink/remoterenderer.py index a3912ff911dca8ce57a68e53b8ed15d2f4a656a9..6a6ace73f729cdab2be59ee8f9bbffc36a86e93a 100644 --- a/pvlink/remoterenderer.py +++ b/pvlink/remoterenderer.py @@ -5,30 +5,87 @@ # Distributed under the terms of the Modified BSD License. """ -This module creates a ParaView-Web RemoteRenderer Widget in the output area below a cell in a Jupyter Notebook. This requires a VTK-Web or ParaView-Web server, see https://kitware.github.io/paraviewweb/examples/RemoteRenderer.html#RemoteRenderer. - -Example: - Start a ParaView-Web server with - ``` - $ pvpython pv_server.py --port 8080 --authKey wslink-secret - ``` - - In the notebook, display the RemoteRenderer Widget with - ``` - display(pvlink.RemoteRenderer(sessionURL='ws://localhost:8080/ws', authKey='wslink-secret')) - ``` +This module creates a ParaViewWeb RemoteRenderer Widget in the output +area below a cell in a Jupyter Notebook. It automatically starts a +ParaViewWeb server, which can optionally be connected to a pvserver. +Views and sources can be manipulated from within the notebook. + +Notes +----- + This requires the ParaView Python module. """ +import argparse +import binascii +import os +import psutil + from ipywidgets import DOMWidget from traitlets import Int, Unicode from ._frontend import module_name, module_version +from .server import start_webserver +from wslink import server +server.start_webserver = start_webserver + class RemoteRenderer(DOMWidget): - """A ParaView-Web RemoteRenderer Widget. + """ + A ParaViewWeb RemoteRenderer Widget which automatically starts a + ParaViewWeb server. Optionally connects to a pvserver, + if a pvserver host is specified. - It must be used with a VTK-Web or ParaView-Web server, - see https://kitware.github.io/paraviewweb/examples/RemoteRenderer.html#RemoteRenderer. + The arguments for the ParaViewWeb server can be passed as kwargs. + If no port and/or authentication key are specified, the server will + be started on the next free port starting from 8080 and/or a + random authentication key will be generated. + + Parameters + ---------- + pvserver_host: str + if not None, create a connection to a pvserver at the given host. + Default = None + pvserver_port: str + port on which the pvserver is listening + Default = 11111 + baseURL: str + the baseURL under which the ParaViewWeb server will be started. + If the notebook is running under `http://localhost:8888`, + the baseURL would be `localhost`. + If Jupyter Server Proxy is being used, the baseURL is the part + in front of 'proxy'. So if your notebook url is `http://localhost:8888` + and you would access a process using `http://localhost:88888/proxy/8080`, + the baseURL would be `localhost:8888`. + Default = "localhost" + use_jupyter_server_proxy: bool + whether the connection should be established using Jupyter Server Proxy. + If True, the baseURL needs to be adjusted accordingly. + Default = False + protocol: pv_wslink.PVServerProtocol + a custom PVServerProtocol class which handles clients requests and run + a default pipeline exactly once + Default = None + **kwargs + arguments, which would usually be passed to the ParaViewWeb + server application on the command line can be specified here. + For example, to specify a port, pass `port=1234` or `p=1234`. + Flags, that are set without value, should be passed with + the value True. For example, `--debug` becomes `debug=True`. + + For help on all possible arguments, + use the `webserver_arguments_help` function. + + Examples + -------- + >>> from pvlink import RemoteRenderer + >>> from paraview import simple + >>> from pvlink.utility import SetRecommendedRenderSettings + >>> view = simple.CreateView('RenderView', 'ConeView') + >>> SetRecommendedRenderSettings(view) + >>> source = simple.Cone() + >>> simple.Show(source, view) + >>> renderer = RemoteRenderer(port=1234) # starts a server on port 1234 + >>> display(renderer) """ _model_name = Unicode('RemoteRendererModel').tag(sync=True) _model_module = Unicode(module_name).tag(sync=True) @@ -37,25 +94,127 @@ class RemoteRenderer(DOMWidget): _view_module = Unicode(module_name).tag(sync=True) _view_module_version = Unicode(module_version).tag(sync=True) - sessionURL = Unicode('ws://localhost:8080/ws').tag(sync=True) - authKey = Unicode('wslink-secret').tag(sync=True) - viewID = Unicode("-1").tag(sync=True) + sessionURL = Unicode().tag(sync=True) + authKey = Unicode().tag(sync=True) + viewID = Unicode('-1').tag(sync=True) # Placeholder to force rendering updates on change. _update = Int(0).tag(sync=True) + def __init__(self, pvserver_host=None, pvserver_port=11111, baseURL='localhost', use_jupyter_server_proxy=False, protocol=None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.baseURL = baseURL + self.use_jupyter_server_proxy = use_jupyter_server_proxy + self.pvserver_host = pvserver_host + self.pvserver_port = pvserver_port + self.protocol = protocol - def __init__(self, sessionURL='ws://localhost:8080/ws', authKey='wslink-secret', viewID='-1', *args, **kwargs): - """Args: - sessionURL (str): URL where the webserver is running. - authKey (str): Authentication key for clients to connect to the WebSocket. - viewID (str): ViewID of the view to connect to (only relevant if multiple views exist on the server side). - """ - super(RemoteRenderer, self).__init__(*args, **kwargs) - self.sessionURL = sessionURL - self.authKey = authKey - self.viewID = viewID + self.start_webserver(self.protocol, **kwargs) + + def start_webserver(self, protocol=None, **kwargs): + """Start a ParaViewWeb server with the given arguments.""" + from paraview.web import pv_wslink + from paraview.web import protocols as pv_protocols + class _CustomServer(pv_wslink.PVServerProtocol): + """Custom PVServerProtocol class to handle clients requests.""" + # authKey = 'wslink-secret' + pv_host = None + pv_port = 11111 + + def initialize(self): + # Bring used components + self.registerVtkWebProtocol(pv_protocols.ParaViewWebMouseHandler()) + self.registerVtkWebProtocol(pv_protocols.ParaViewWebViewPort()) + self.registerVtkWebProtocol(pv_protocols.ParaViewWebViewPortImageDelivery()) + # Update authentication key to use + self.updateSecret(_CustomServer.authKey) + # Connect to paraview server if a server is specified + if _CustomServer.pv_host != None: + from paraview.simple import Connect as PVConnect + PVConnect(_CustomServer.pv_host, _CustomServer.pv_port) + + if protocol == None: + protocol = _CustomServer + + # Create argument parser and add arguments (imitates command line) + parser = argparse.ArgumentParser(description="ParaView Web Server") + # Add default arguments + server.add_arguments(parser) + + # Extract arguments from **kwargs + arg_list = [] + for key, value in kwargs.items(): + # Set key + key = key.replace('_', '-') + if len(key) > 2: + arg_list.append('--' + key) + else: + arg_list.append('-' + key) + # Set value + if key == 'p' or key == 'port': + arg_list.append(str(value)) + elif value == True and (key != 'f' and key != 'force-flush'): + break + else: + arg_list.append(value) + # If no port is given, check for the next free port starting from 8080 + if 'p' not in kwargs.keys() and 'port' not in kwargs.keys(): + port = self._find_next_free_port(8080) + arg_list.append('-p') + arg_list.append(str(port)) + # If no authKey is given, create a randon authentication key + if 'a' not in kwargs.keys() and 'authKey' not in kwargs.keys(): + authKey = binascii.hexlify(os.urandom(24)).decode('ascii') + arg_list.append('-a') + arg_list.append(authKey) + args = parser.parse_args(arg_list) + # print(args) + + # Start server + if protocol == _CustomServer: + _CustomServer.authKey = args.authKey + if self.pvserver_host is not None: + _CustomServer.pv_host = self.pvserver_host + _CustomServer.pv_port = self.pvserver_port + else: + protocol.authKey = args.authKey + self.protocol = server.start_webserver(options=args, protocol=protocol) + # Set authKey and URL for the websocket connection + self.authKey = args.authKey + self.port = args.port + self.sessionURL = '{wsProtocol}://{baseURL}{use_proxy}{port}/{ws_endpoint}'.format( + wsProtocol='wss' if args.sslKey and args.sslCert else 'ws', + use_proxy='/proxy/' if self.use_jupyter_server_proxy else ':', + baseURL=self.baseURL, port=args.port, ws_endpoint=args.ws + ) + + def _find_next_free_port(self, port): + """ + Finds next free port starting from a given port using the psutil module. + """ + free = False + while not free: + for conn in psutil.net_connections(): + if conn.status == 'LISTEN' and conn.laddr.port == port: + port += 1 + else: + free = True + return port def update_render(self): """Explicit call for the renderer on the javascript side to render.""" - self._update += 1 \ No newline at end of file + self._update += 1 + + def get_webserver_port(self): + """Get the port, that can be used to reach the websocket.""" + return self.port + + def get_authKey(self): + """Get the key, used for authentification for the websocket connection.""" + return self.authKey + + def webserver_arguments_help(): + """Returns a string with the possile arguments for the ParaViewWeb server.""" + parser = argparse.ArgumentParser(description="ParaView Web Server") + server.add_arguments(parser) + return (parser.format_help()[339:]) diff --git a/pvlink/server.py b/pvlink/server.py new file mode 100644 index 0000000000000000000000000000000000000000..546eb725697125c7f12b592e4db7dddf830204de --- /dev/null +++ b/pvlink/server.py @@ -0,0 +1,123 @@ +""" +This is a helper module. It rewrites the `start_webserver` function +of the wslink.server module. + +To enable starting a server from within a Jupyter Notebook without +blocking the kernel, the `reactor.run` function starts in +a Python Thread. If a second webserver is started, we cannot start +a new reactor, so we add a new connection to the running reactor. +Returns the `wslinkServer` object so that we can access its +`sharedObjects` attribute. +""" + +import logging, sys + +from wslink import websocket as wsl +from wslink.server import * + +from autobahn.twisted.resource import WebSocketResource +from twisted.web.resource import Resource +from twisted.python import log + +def start_webserver(options, protocol=wsl.ServerProtocol, disableLogging=False): + """ + Starts the web-server with the given protocol. Options must be an object + with the following members: + options.host : the interface for the web-server to listen on + options.port : port number for the web-server to listen on + options.timeout : timeout for reaping process on idle in seconds + options.content : root for web-pages to serve. + """ + from twisted.internet import reactor + from twisted.web.server import Site + from twisted.web.static import File + import sys + + if not disableLogging: + # redirect twisted logs to python standard logging. + observer = log.PythonLoggingObserver() + observer.start() + # log.startLogging(sys.stdout) + # Set logging level. + if (options.debug): + logging.basicConfig(level=logging.DEBUG) + else: + logging.basicConfig(level=logging.ERROR) + + contextFactory = None + + use_SSL = False + if options.sslKey and options.sslCert: + use_SSL = True + wsProtocol = "wss" + from twisted.internet import ssl + contextFactory = ssl.DefaultOpenSSLContextFactory(options.sslKey, options.sslCert) + else: + wsProtocol = "ws" + + # Create default or custom ServerProtocol + wslinkServer = protocol() + + # create a wslink-over-WebSocket transport server factory + transport_factory = wsl.TimeoutWebSocketServerFactory(\ + url = "%s://%s:%d" % (wsProtocol, options.host, options.port), \ + timeout = options.timeout ) + transport_factory.protocol = wsl.WslinkWebSocketServerProtocol + transport_factory.setServerProtocol(wslinkServer) + + root = Resource() + + # Do we serve static content or just websocket ? + if len(options.content) > 0: + # Static HTTP + WebSocket + root = File(options.content) + + # Handle possibly complex ws endpoint + if not options.nows: + wsResource = WebSocketResource(transport_factory) + handle_complex_resource_path(options.ws, root, wsResource) + + if options.uploadPath != None : + from wslink.upload import UploadPage + uploadResource = UploadPage(options.uploadPath) + root.putChild("upload", uploadResource) + + if len(options.fsEndpoints) > 3: + for fsResourceInfo in options.fsEndpoints.split('|'): + infoSplit = fsResourceInfo.split('=') + handle_complex_resource_path(infoSplit[0], root, File(infoSplit[1])) + + site = Site(root) + + if use_SSL: + reactor.listenSSL(options.port, site, contextFactory) + else: + reactor.listenTCP(options.port, site) + + # flush ready line + sys.stdout.flush() + + # Work around to force the output buffer to be flushed + # This allow the process launcher to parse the output and + # wait for "Start factory" to know that the WebServer + # is running. + if options.forceFlush : + for i in range(200): + log.msg("+"*80, logLevel=logging.CRITICAL) + + # reactor.callWhenRunning(print_ready) + + # ============================================================= + # Modification: Run `reactor.run` in thread + # ============================================================= + # Reactor already running, we are just adding a connection + if reactor.running: + return wslinkServer + + from threading import Thread + if options.nosignalhandlers: + Thread(target=reactor.run, kwargs={'installSignalHandlers':0}).start() + else: + Thread(target=reactor.run, args=(False,)).start() + + return wslinkServer \ No newline at end of file diff --git a/pvlink/simplerenderer.py b/pvlink/simplerenderer.py new file mode 100644 index 0000000000000000000000000000000000000000..ab8a6814736ee8b7029d8adae6bc6c8b3baf7d32 --- /dev/null +++ b/pvlink/simplerenderer.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# coding: utf-8 + +# Copyright (c) Juelich Supercomputing Centre (JSC). +# Distributed under the terms of the Modified BSD License. + +""" +This module creates a ParaViewWeb RemoteRenderer Widget +in the output area below a cell in a Jupyter Notebook. +This requires a VTK Web or ParaViewWeb server application, see +https://kitware.github.io/paraviewweb/examples/RemoteRenderer.html#RemoteRenderer. +""" + +from ipywidgets import DOMWidget +from traitlets import Int, Unicode +from ._frontend import module_name, module_version + + +class SimpleRenderer(DOMWidget): + """ + A simple ParaViewWeb RemoteRenderer Widget. + + It must be used with a VTK Web or ParaViewWeb server application, see + https://kitware.github.io/paraviewweb/examples/RemoteRenderer.html#RemoteRenderer. + + Parameters + ---------- + sessionURL: str + URL where the server application is running + Default = "ws://localhost:8080/ws" + authKey: str + authentication key for clients to connect to the server + Default = "wslink-secret" + viewID: str + viewID of the view to connect to (only relevant if multiple views exist on the server side) + Default = "-1" + + Examples + -------- + From command line, start a ParaViewWeb server application. + $ pvpython pv_server.py --port 8080 --authKey wslink-secret + + >>> from pvlink import SimpleRenderer + >>> renderer = SimpleRenderer(sessionURL='ws://localhost:8080/ws, authkey='wslink-secret') + >>> display(renderer) + """ + _model_name = Unicode('RemoteRendererModel').tag(sync=True) + _model_module = Unicode(module_name).tag(sync=True) + _model_module_version = Unicode(module_version).tag(sync=True) + _view_name = Unicode('RemoteRendererView').tag(sync=True) + _view_module = Unicode(module_name).tag(sync=True) + _view_module_version = Unicode(module_version).tag(sync=True) + + sessionURL = Unicode('ws://localhost:8080/ws').tag(sync=True) + authKey = Unicode('wslink-secret').tag(sync=True) + viewID = Unicode('-1').tag(sync=True) + # Placeholder to force rendering updates on change. + _update = Int(0).tag(sync=True) + + def __init__(self, sessionURL='ws://localhost:8080/ws', authKey='wslink-secret', viewID='-1', *args, **kwargs): + super().__init__(*args, **kwargs) + self.sessionURL = sessionURL + self.authKey = authKey + self.viewID = viewID + + def update_render(self): + """Explicit call for the renderer on the javascript side to render.""" + self._update += 1 \ No newline at end of file diff --git a/pvlink/utility.py b/pvlink/utility.py new file mode 100644 index 0000000000000000000000000000000000000000..ccc8e0aa1e06f44fd7b2e95003e53b62deb859af --- /dev/null +++ b/pvlink/utility.py @@ -0,0 +1,30 @@ + +def SetRecommendedRenderSettings(view): + """ + Set view settings to enable smooth interaction with the rendering widget. + Disables interactor-based render calls and forces server-side rendering. + + Parameters + ---------- + view: ParaView proxy view + """ + # Disable interactor-based render calls. + view.EnableRenderOnInteraction = 0 + # Force server-side rendering. + view.RemoteRenderThreshold = 0 + + +def ResetCamera(view, widget): + """ + Reset camera center of rotation of the view and update the widget. + + Parameters + ---------- + view: ParaView proxy view + widget: RemoteRenderer widget + """ + from paraview import simple + simple.ResetCamera() + view.CenterOfRotation = simple.GetActiveCamera().GetFocalPoint() + # Update the rendering widget on the javascript side to display the changes + widget.update_render() \ No newline at end of file diff --git a/setup.py b/setup.py index e9c822f87ef6656bbf6dd968b9b6af7e7ce53c68..6234e363b71862f9ff40cdd8e102c9b854acf148 100644 --- a/setup.py +++ b/setup.py @@ -58,11 +58,14 @@ cmdclass['jsdeps'] = combine_commands( ensure_targets(jstargets), ) +with open(pjoin(HERE, 'README.md'), encoding='utf-8') as f: + long_description = f.read() setup_args = dict( name = name, description = 'ParaView-Web RemoteRenderer in Jupyter', - long_description = 'ParaView-Web RemoteRenderer in Jupyter', + long_description = long_description, + long_description_content_type='text/markdown', version = version, scripts = glob(pjoin('scripts', '*')), cmdclass = cmdclass, @@ -88,8 +91,9 @@ setup_args = dict( include_package_data = True, install_requires = [ 'ipywidgets>=7.0.0', - 'wslink>=0.1.11', + 'psutil==5.7.0', 'twisted>=19.2.1', + 'wslink==0.1.13', ], extras_require = { 'test': [ diff --git a/src/widget.ts b/src/widget.ts index 13694e3dd2a5fb5dc5bafefcc88126baf430c614..310bde1df3ceed20077762ac2b6e0f15cd05d66c 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -51,6 +51,7 @@ class RemoteRendererView extends DOMWidgetView { // div to hold the canvas of the RemoteRenderer. var render_div = document.createElement('div'); render_div.style.height = '100%'; + render_div.style.minHeight = '300px'; render_div.style.width = '100%'; this.el.appendChild(render_div); @@ -63,6 +64,8 @@ class RemoteRendererView extends DOMWidgetView { secret: this.model.get('authKey') }; var smartConnect = SmartConnect.newInstance({ config: config }); + console.log(smartConnect); + smartConnect.onConnectionReady(function (connection: any) { // Create the RemoteRenderer @@ -71,9 +74,10 @@ class RemoteRendererView extends DOMWidgetView { 'ViewPort', 'ViewPortImageDelivery'] ); - var renderer = new RemoteRenderer(pvwClient); - renderer.setContainer(render_div); - renderer.setView(that.model.get('viewID')); + + var renderer = new RemoteRenderer(pvwClient, render_div, that.model.get('viewID')); + // renderer.setContainer(render_div); + // renderer.setView(that.model.get('viewID')); renderer.onImageReady(function () { // Resize when the renderer is placed within a widget. if (that.el.style.width != '100%') { @@ -83,6 +87,14 @@ class RemoteRendererView extends DOMWidgetView { console.log("We are good."); }); + render_div.onresize = function () { + if (that.el.style.width != '100%') { + that.el.style.width = '100%'; + renderer.resize(); + } + renderer.resize(); + }; + // Handle size changes when the entire window is resized. SizeHelper.onSizeChange(function () { renderer.resize();