Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: add WebServer module #5999

Merged
merged 12 commits into from
May 12, 2022
Merged

ENH: add WebServer module #5999

merged 12 commits into from
May 12, 2022

Conversation

pieper
Copy link
Member

@pieper pieper commented Nov 5, 2021

Code imported from Steve Pieper's development repository.

https://github.com/pieper/SlicerWeb

Code imported from Steve Pieper's developments.

https://github.com/pieper/SlicerWeb
@jcfr
Copy link
Member

jcfr commented Nov 5, 2021

Thanks for the contribution.

To give the code a chance to mature and be more broadly tested. What do you think of:

  • transferring the repository to the Slicer organization
  • creating an extension

Later, we could then add the module to the core as a Slicer remote like we do for the Surface Toolbox (see Slicer/SuperBuild.cmake)

@pieper
Copy link
Member Author

pieper commented Nov 5, 2021

@jcfr I went with this option based on a discussion with @lassoan on the Slicer discourse. We felt there was a motivating use case to have this feature in the core and little downside for anyone who's not using the module.

Regarding where to keep the code, my experience has been that having many repositories for core modules leads to extra burden for maintainers since we need multiple PRs to fix common issues.

@pieper pieper marked this pull request as draft November 5, 2021 22:18
@pieper
Copy link
Member Author

pieper commented Nov 5, 2021

I've marked this as draft for now so we can discuss. There are some TODO items in the code and we should think about which of those are important enough to put in place before merging this. The most important in my mind is putting an explicit start/stop button in the module (currently it starts on enter).

@pieper pieper requested review from jcfr and lassoan November 5, 2021 22:22
Copy link
Contributor

@lassoan lassoan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, this is great! I added some comments and suggestions inline.

I think it is good to have it in the Slicer core, as the amount of code is quite small and we don't get much benefit from other half-integrated extensions either (LandmarkRegistration, SurfaceToolbox).


This code has been developed over a number of years in a separate repository where there are additional experiments demontrating other potential uses. The version in Slicer core has been stripped down to address the most common expected use cases. See [https://github.com/pieper/SlicerWeb](https://github.com/pieper/SlicerWeb).

Also note that the code should be considered somewhat experimental and a likely security risk. Do not expose web server endpoints on the public internet without careful consideration.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make it clear that it is only a security risk if the endpoints are enabled (and they are disabled by default).

Creates a fairly simple but pretty powerful web server that can respond to http(s) requests with data from the current Slicer or modify the Slicer state. This module is meant to be the basis for implemeting web applications that use Slicer as a remote render / computation engine or for controlling your Slicer instance for interaction with other system code like shell scripts or other applications.

There are three basic types of endpoints:
- **Static:** Hosts files out of the module's `docroot` like any standard http server. Currently this is used just for examples.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should separate potentially clinically useful examples from from tests and pure technology demonstrations. Clinically useful stuff should be at the top and the rest (timeimage, timecube, spinning cube) should be in a separate section, but preferably in a separate page (can be linked from the main page).


## Current Features

The server launches on port 2016 by default when you enter the module. Access logs are shown in the GUI by default and can be routed to the console if persistence is needed (the GUI logs are cleared periodically). If port 2016is in use, other ports are checked sequentially until an open port is found, allowing more than one Slicer web server to run concurrently.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The server launches on port 2016 by default when you enter the module. Access logs are shown in the GUI by default and can be routed to the console if persistence is needed (the GUI logs are cleared periodically). If port 2016is in use, other ports are checked sequentially until an open port is found, allowing more than one Slicer web server to run concurrently.
The server launches on port 2016 by default when you enter the module. Access logs are shown in the GUI by default and can be routed to the console if persistence is needed (the GUI logs are cleared periodically). If port 2016 is in use, other ports are checked sequentially until an open port is found, allowing more than one Slicer web server to run concurrently.

For example, these command can be used to download data and change the screen layout:

```
curl -X POST localhost:2016/slicer/repl --data "import SampleData; volumeNode = SampleData.SampleDataLogic().downloadMRHead(); slicer.app.layoutManager().setLayout(slicer.vtkMRMLLayoutNode.SlicerLayoutOneUpRedSliceView)"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

repl is a bit too abstract (and it is not really a loop but we really just perform a single command). What are some other endpoint names that are commonly used for this kind of functionality?

Some examples that look more readable to me:

  • localhost:2016/slicer/execute
  • localhost:2016/slicer/exec
  • localhost:2016/slicer/cmd

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Internally it uses python exec so that would be the best choice in terms of clarity and documenting the feature. I'll add it and deprecate repl.


## Related modules

- The [OpenIGTLink](https://github.com/openigtlink/SlicerOpenIGTLink) Extension has some similar functionality cusomized for image guided therapy applications. It should be preferred for integration with imaging devices and use in a clinical setting.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- The [OpenIGTLink](https://github.com/openigtlink/SlicerOpenIGTLink) Extension has some similar functionality cusomized for image guided therapy applications. It should be preferred for integration with imaging devices and use in a clinical setting.
- The [OpenIGTLink](https://github.com/openigtlink/SlicerOpenIGTLink) Extension has some similar functionality customized for image guided therapy applications. It should be preferred for integration with imaging devices and use in a clinical setting or setting up continuous high-throughput image and transform streams.


# python imports
try:
from BaseHTTPServer import HTTPServer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this Python 2/3 compatibility? We can drop Python2.

import string
import time
try:
import urlparse
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python 2/3 compatibility?

# SlicerRequestHandler
#

class SlicerRequestHandler(object):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we move out each request handler to a separate file?
It would make this file shorter and simpler and it could server as example for extensions defining and registering their own custom request handlers.

@lassoan
Copy link
Contributor

lassoan commented Nov 6, 2021

Some more findings:

  • The web server is started when the module GUI is opened. Instead, there should be start/stop buttons. Maybe the server state could be persistent (server would automatically start with Slicer if the user started the server and did not stop it).
  • There should be also a simple way to start Slicer from the command-line with a server listening at a specified port (with specified handlers enabled).
  • Processing a repl request on Windows takes about 2-3 seconds (even if I just execute a print() function). This is very noticeable, even when it is used for opening an image.
  • http://localhost:2016/slicer/slice returns with error 404 if no image is loaded. Since most newcomers would click through the sample API calls from top to bottom, it would be probably sufficient to add loading of a sample image above the slice call.
  • Links on the http://localhost:2016/ServerTests page do not work ("Error loading page" message is displayed).
  • It would be nice to add an example where there is some basic widgets and interaction, for example, a slider to browse slices?

@lassoan
Copy link
Contributor

lassoan commented Nov 7, 2021

I've tried this WebServer module to open a file in a running Slicer instance by running a .bat file and it works very nicely using this command (%1 refers to the first command-line argument in .bat files):

PythonSlicer.exe -c "import requests; filename=r'%1'.replace('\\','/'); requests.post(url='http://localhost:2016/slicer/repl', data='slicer.app.loadFiles([\''+filename+'\'])', timeout=15)"

The only thing that is not nice that there is a few-second delay before Slicer starts to process the command. It might be an issue with the QSocketNotifier. We tried to use it in SlicerJupyter but it had issues (100% CPU use for sure, but maybe performance problems, too). First we had to switch to simple polling in Windows, but then problems were found on Linux, too; so we ended up using polling on all platforms.

@pieper
Copy link
Member Author

pieper commented Nov 8, 2021

Thanks for the review - good feedback 👍

I'll do some more tweaks and test on windows. Good topic to discuss in tomorrow's call.

@jcfr
Copy link
Member

jcfr commented Nov 8, 2021

Few high level comments

There are few aspects:

  • registering handlers for exposing Slicer functionalities through REST API
  • serving web pages
  • REPL

This is why I suggest to split WebServer.py into different "logic" or even modules:

  • SlicerRESTApi: Provide a way to handle request, register new request handlers, describe available handlers (ideally using the OpenAPI specification). For now, I suggest to focus on integrating this in Slicer core.
  • SlicerREPL
  • SlicerWebServer: I think we should approach this as a list of web/html/javascript components (may be these could even be distributed using npm?) and provide a gallery of examples show-casing how to use these.

@jcfr
Copy link
Member

jcfr commented Nov 8, 2021

Good topic to discuss in tomorrow's call.

I will not be available tomorrow. That said, I will look forward the meeting notes.

Resources/docroot/index.html
Resources/docroot/favicon.ico
Resources/docroot/images/3DSlicer-DesktopIcon.png
Resources/docroot/ServerTests/glMatrix-0.9.5.min.js
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to update the example to directly download the script instead of packaging it.

@lassoan
Copy link
Contributor

lassoan commented Nov 8, 2021

I agree that the module should be split up to a module that has the infrastructure to register/unregister handlers (plugins). I suggested the same in my comments - have handlers in separate files; and have API that allows other modules to register their handlers, too.

I like the WebServer module name because it describes the scope of the module nicely and does not limit itself to a particular style of web API (although right now all handlers use REST API).

Distributing components via npm is a great idea (once we have some usable components and not just technology demos).

self.port = SlicerHTTPServer.findFreePort(self.port)
self.logMessage("Starting server on port %d" % self.port)
self.logMessage('docroot: %s' % self.docroot)
# example: certfile = '/Users/pieper/slicer/latest/SlicerWeb/localhost.pem'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Steve. Here is some feedback:
The default pem file path and an "autostart" flag (telling if the WebServer should start automatically at application launch) could be read from application settings. Or autostart could be turned off by default and you could launch Slicer with specifying a small script that starts the WebServer with the parameters you like (--python-code "import WebServer; server=WebServer.... ; server.start(...)“.

@mhalle
Copy link
Contributor

mhalle commented Feb 1, 2022

It might be worth looking at FastAPI, which provides a really easy way to create an API from python code, and includes a high performance web server. Self-documenting and uses typing as well.

https://fastapi.tiangolo.com/

@pieper
Copy link
Member Author

pieper commented Feb 1, 2022

It might be worth looking at FastAPI, which provides a really easy way to create an API from python code, and includes a high performance web server. Self-documenting and uses typing as well.

https://fastapi.tiangolo.com/

This could be good. The problem I found with pretty much all python web servers is that they want to "own" the event loop where here event loop is owned by the Qt application.

@haehn is trying to recruit some students to help with this as part of another project so we can hope to get some more experiments going soon.

@mhalle
Copy link
Contributor

mhalle commented Feb 1, 2022

This could be good. The problem I found with pretty much all python web servers is that they want to "own" the event loop where here event loop is owned by the Qt application.

It looks like there are ways to integrate the Qt event loop with asyncio, which in turn runs FastAPI through another layer (uvicorn or similar). On the other hand, I don't know if async event would be what you want.

- Moved out request handlers to separate files.
- Allow registration of custom request handlers.
- Iterate through registered request handlers and let that one process the request that returned the highest confidence.
- Save default handler and logging options in application settings.
- Start/stop the server when a start/stop button is pressed (not start when opening the WebServer module GUI).
- Updated Slicer icon.
- Updated module icon (API icon from Google Material Icon Set)
- Added `sampleData` command to allow easy testing and demoing of the API
- Added `gui` command to switch between full application/viewers only, switch viewer layout
- Added `screenshot` command to get screenshot of the application main window
@lassoan
Copy link
Contributor

lassoan commented Apr 19, 2022

I've fixed most of the review comments and TODOs. I haven't tested the DICOM handler.
It would be great if this could be integrated before Slicer5, for example to allow running Slicer as a viewer for SimpleITK/ITKPython.

@lassoan lassoan added this to the Slicer 4.13 milestone Apr 19, 2022
@lassoan lassoan marked this pull request as ready for review April 19, 2022 16:21
@jcfr jcfr modified the milestones: Slicer 4.13, Slicer 5.1 Apr 29, 2022
pieper added 2 commits May 1, 2022 15:16
* add entry on demo page to reset view to four up with controls
* change "got it" messages to more meaningful feedback
* add extra tooltip hints on usage
* fix logic of local connection button
pieper added 4 commits May 1, 2022 15:52
This puts the stack trace in the log and prevents
any bad request handlers from leaving the network connection
dangling.
A newer version of pydicom is required to fix a bug
in json serialization of a pydicom multivalue class.

Also fix a warning from bad uri syntax.
* Implements study level query and metadata.
* Adds offset and limits to study listing
* Adds documentation on using with OHIF
@lassoan
Copy link
Contributor

lassoan commented May 11, 2022

@pieper Is there anything else you want to add/change in this module? If not, then I think we should rebase and merge it, as remote control of the application is quite important to be able to better compete with other viewers that start up faster and/or are more integrated into the Python environment.

I'm thinking about adding an ITKSphinxExample for using Slicer as a viewer at the upcoming May 20 ITK event and this feature would be a prerequisite. It may take a few days to get all the issues ironed out on all platforms, so there is not too much time left.

@pieper
Copy link
Member Author

pieper commented May 12, 2022

@lassoan yes, I am at a good spot with it. The only outstanding conflice is that I needed to upgrade pydicom. I don't think that will be an issue so let's merge it and we if anything pops up we can deal with it independently.

@pieper pieper merged commit bdc1ee0 into Slicer:master May 12, 2022
@pieper pieper deleted the add-webserver branch May 12, 2022 01:57
@jamesobutler
Copy link
Contributor

The master branch is now failing CI checks. https://github.com/Slicer/Slicer/actions/runs/2310736284

@pieper
Copy link
Member Author

pieper commented May 12, 2022

Thanks for the heads up @jamesobutler - looks like some simple format issues. It would be nice if the lint just created a PR automatically. I guess I can fix these up in the browser (I'm away from my regular setup).

@jcfr
Copy link
Member

jcfr commented May 12, 2022

it would be nice if the lint just created a PR

This should be possible - I am taking note of this and will evaluate our options.

jcfr pushed a commit to jcfr/Slicer that referenced this pull request Jun 30, 2022
Cherry picked from:
* bdc1ee0 - ENH: add WebServer module (Slicer#5999)
* d00bc5b - STYLE: Fix python lint issues in WebServer module
* fdcdbe5 - DOC: Improve Webserver module documentation
* fba5df4 - DOC: Improve Webserver module documentation (part 2)
* 9c7f205 - DOC: extra details and warnings about WebServer (Slicer#6370)
* c9a95b1 - ENH: Add DICOMweb example to webserver examples page
* 0f29c7c - BUG: Fix WebServer failing to load when Slicer built without DICOM support (Slicer#6407)
jcfr pushed a commit to jcfr/Slicer that referenced this pull request Jul 7, 2022
Cherry picked from:
* bdc1ee0 - ENH: add WebServer module (Slicer#5999)
* d00bc5b - STYLE: Fix python lint issues in WebServer module
* fdcdbe5 - DOC: Improve Webserver module documentation
* fba5df4 - DOC: Improve Webserver module documentation (part 2)
* 9c7f205 - DOC: extra details and warnings about WebServer (Slicer#6370)
* c9a95b1 - ENH: Add DICOMweb example to webserver examples page
* 0f29c7c - BUG: Fix WebServer failing to load when Slicer built without DICOM support (Slicer#6407)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

6 participants