Skip to content

Commit

Permalink
Switch to using a Server Extension
Browse files Browse the repository at this point in the history
I want to provide a button to the Hub Control Panel
when we are running under a JupyterHub. This requires conditional
rendering in some way - either we render the initial HTML as
a template, or setup an API endpoint that JS can use.

This PR templatizes the initial rendering. The following changes
are made:

1. Convert into a Jupyter Server extension. This gives us the
   flexibility we need here.
2. Inherit a handler directly from jupyter_server_proxy, enabling
   us to use the *same* URL for both the initial HTTP rendering
   and the websockify!

However, this means we lose the launcher icon - let's try
mitigate that somehow.
  • Loading branch information
yuvipanda committed Feb 4, 2024
1 parent a018435 commit 7970c32
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 68 deletions.
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ RUN . /opt/conda/bin/activate && \

COPY --chown=$NB_UID:$NB_GID . /opt/install
RUN . /opt/conda/bin/activate && \
pip install -e /opt/install
pip install -e /opt/install && \
jupyter server extension enable jupyter_remote_desktop_proxy
4 changes: 2 additions & 2 deletions js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ function status(text) {
document.getElementById("status").textContent = text;
}

// Construct the websockify websocket URL we want to connect to
let websockifyUrl = new URL("websockify", window.location);
// The websockify URL to connect to is the same URL we are rendering this page on!
let websockifyUrl = new URL(window.location);
websockifyUrl.protocol = window.location.protocol === "https" ? "wss" : "ws";

// Creating a new RFB object will start a new connection
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"NotebookApp": {
"nbserver_extensions": {
"jupyter_remote_desktop_proxy": true
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"ServerApp": {
"jpserver_extensions": {
"jupyter_remote_desktop_proxy": true
}
}
}
67 changes: 10 additions & 57 deletions jupyter_remote_desktop_proxy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,64 +1,17 @@
import os
import shlex
import tempfile
from shutil import which

HERE = os.path.dirname(os.path.abspath(__file__))


def setup_desktop():
# make a secure temporary directory for sockets
# This is only readable, writeable & searchable by our uid
sockets_dir = tempfile.mkdtemp()
sockets_path = os.path.join(sockets_dir, 'vnc-socket')
vncserver = which('vncserver')
from .server_extension import load_jupyter_server_extension

if vncserver is None:
# Use bundled tigervnc
vncserver = os.path.join(HERE, 'share/tigervnc/bin/vncserver')

# TigerVNC provides the option to connect a Unix socket. TurboVNC does not.
# TurboVNC and TigerVNC share the same origin and both use a Perl script
# as the executable vncserver. We can determine if vncserver is TigerVNC
# by searching TigerVNC string in the Perl script.
with open(vncserver) as vncserver_file:
is_tigervnc = "TigerVNC" in vncserver_file.read()
HERE = os.path.dirname(os.path.abspath(__file__))

if is_tigervnc:
vnc_args = [vncserver, '-rfbunixpath', sockets_path]
socket_args = ['--unix-target', sockets_path]
else:
vnc_args = [vncserver]
socket_args = []

if not os.path.exists(os.path.expanduser('~/.vnc/xstartup')):
vnc_args.extend(['-xstartup', os.path.join(HERE, 'share/xstartup')])
def _jupyter_server_extension_points():
"""
Set up the server extension for collecting metrics
"""
return [{"module": "jupyter_remote_desktop_proxy"}]

vnc_command = shlex.join(
vnc_args
+ [
'-verbose',
'-geometry',
'1680x1050',
'-SecurityTypes',
'None',
'-fg',
]
)

return {
'command': [
'websockify',
'-v',
'--web',
os.path.join(HERE, 'static'),
'--heartbeat',
'30',
'{port}',
]
+ socket_args
+ ['--', '/bin/sh', '-c', f'cd {os.getcwd()} && {vnc_command}'],
'timeout': 30,
'mappath': {'/': '/index.html'},
'new_browser_window': True,
}
# For backward compatibility
_load_jupyter_server_extension = load_jupyter_server_extension
_jupyter_server_extension_paths = _jupyter_server_extension_points
84 changes: 84 additions & 0 deletions jupyter_remote_desktop_proxy/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import os
import shlex
import tempfile
from shutil import which

import jinja2
from jupyter_server_proxy.handlers import SuperviseAndProxyHandler
from tornado import web

jinja_env = jinja2.Environment(
loader=jinja2.FileSystemLoader(
os.path.join(os.path.dirname(__file__), 'templates')
),
)


HERE = os.path.dirname(os.path.abspath(__file__))


def get_websockify_command():
# make a secure temporary directory for sockets
# This is only readable, writeable & searchable by our uid
sockets_dir = tempfile.mkdtemp()
sockets_path = os.path.join(sockets_dir, 'vnc-socket')
vncserver = which('vncserver')

if vncserver is None:
# Use bundled tigervnc
vncserver = os.path.join(HERE, 'share/tigervnc/bin/vncserver')

# TigerVNC provides the option to connect a Unix socket. TurboVNC does not.
# TurboVNC and TigerVNC share the same origin and both use a Perl script
# as the executable vncserver. We can determine if vncserver is TigerVNC
# by searching TigerVNC string in the Perl script.
with open(vncserver) as vncserver_file:
is_tigervnc = "TigerVNC" in vncserver_file.read()

if is_tigervnc:
vnc_args = [vncserver, '-rfbunixpath', sockets_path]
socket_args = ['--unix-target', sockets_path]
else:
vnc_args = [vncserver]
socket_args = []

if not os.path.exists(os.path.expanduser('~/.vnc/xstartup')):
vnc_args.extend(['-xstartup', os.path.join(HERE, 'share/xstartup')])

vnc_command = shlex.join(
vnc_args
+ [
'-verbose',
'-geometry',
'1680x1050',
'-SecurityTypes',
'None',
'-fg',
]
)

return (
[
'websockify',
'-v',
'--heartbeat',
'30',
'{port}',
]
+ socket_args
+ ['--', '/bin/sh', '-c', f'cd {os.getcwd()} && {vnc_command}']
)


class DesktopHandler(SuperviseAndProxyHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.command = get_websockify_command()

@web.authenticated
async def http_get(self, *args, **kwargs):
template_params = {
'base_url': self.base_url,
}
template_params.update(self.serverapp.jinja_template_vars)
self.write(jinja_env.get_template("index.html").render(**template_params))
29 changes: 29 additions & 0 deletions jupyter_remote_desktop_proxy/server_extension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from pathlib import Path

from jupyter_server.base.handlers import AuthenticatedFileHandler
from jupyter_server.utils import url_path_join
from jupyter_server_proxy.handlers import AddSlashHandler

from .handlers import DesktopHandler

HERE = Path(__file__).parent


def load_jupyter_server_extension(server_app):
"""
Called during notebook start
"""
base_url = server_app.web_app.settings["base_url"]

server_app.web_app.add_handlers(
".*",
[
(
url_path_join(base_url, "/desktop/static/(.*)"),
AuthenticatedFileHandler,
{"path": (str(HERE / "static"))},
),
(url_path_join(base_url, "/desktop"), AddSlashHandler),
(url_path_join(base_url, "/desktop/()"), DesktopHandler, {'state': {}}),
],
)
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@
Chrome Frame. -->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />

<link href="./dist/index.css" rel="stylesheet" />
<link href="{{ base_url }}desktop/static/dist/index.css" rel="stylesheet" />
</head>

<body>
<div id="top-bar">
<a href=".." id="logo">
<img src="./jupyter-logo.svg" />
<img src="{{base_url}}desktop/static/jupyter-logo.svg" />
</a>
<ul id="menu">
<li id="status-container">
Expand All @@ -29,6 +29,11 @@
<li>
<a id="clipboard-button" href="#">Remote Clipboard</a>
</li>
{% if hub_control_panel_url %}
<li>
<a href="{{ hub_control_panel_url }}">Hub Control Panel</a>
</li>
{% endif %}
</ul>
</div>
<div id="screen">
Expand All @@ -47,6 +52,6 @@
</div>
</div>

<script src="./dist/viewer.js"></script>
<script src="{{base_url}}desktop/static/dist/viewer.js"></script>
</body>
</html>
19 changes: 14 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,6 @@ def run(self):
"Programming Language :: Python :: 3",
],
description="Run a desktop environments on Jupyter",
entry_points={
'jupyter_serverproxy_servers': [
'desktop = jupyter_remote_desktop_proxy:setup_desktop',
]
},
install_requires=[
'jupyter-server-proxy>=1.4.0',
],
Expand All @@ -85,4 +80,18 @@ def run(self):
# Handles `pip install` directly
"build_py": webpacked_command(build_py),
},
data_files=[
(
'etc/jupyter/jupyter_server_config.d',
[
'jupyter-config/jupyter_server_config.d/jupyter_remote_desktop_proxy.json'
],
),
(
'etc/jupyter/jupyter_notebook_config.d',
[
'jupyter-config/jupyter_notebook_config.d/jupyter_remote_desktop_proxy.json'
],
),
],
)

0 comments on commit 7970c32

Please sign in to comment.