Skip to content

Commit

Permalink
Merge pull request jupyterhub#79 from jupyterhub/server
Browse files Browse the repository at this point in the history
Add a "Hub Control Panel" menu item if running inside a JupyterHub
  • Loading branch information
manics authored Feb 5, 2024
2 parents 457ad49 + 0fadd16 commit a0f7620
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 64 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
6 changes: 4 additions & 2 deletions js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ 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);
// This page is served under the /desktop/, and the websockify websocket is served
// under /desktop-websockify/ with the same base url as /desktop/. We resolve it relatively
// this way.
let websockifyUrl = new URL("../desktop-websockify/", 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
24 changes: 24 additions & 0 deletions jupyter_remote_desktop_proxy/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import os

import jinja2
from jupyter_server.base.handlers import JupyterHandler
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__))


class DesktopHandler(JupyterHandler):
@web.authenticated
async def get(self):
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))
32 changes: 32 additions & 0 deletions jupyter_remote_desktop_proxy/server_extension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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(
".*",
[
# Serve our own static files
(
url_path_join(base_url, "/desktop/static/(.*)"),
AuthenticatedFileHandler,
{"path": (str(HERE / "static"))},
),
# To simplify URL mapping, we make sure that /desktop/ always
# has a trailing slash
(url_path_join(base_url, "/desktop"), AddSlashHandler),
(url_path_join(base_url, "/desktop/"), DesktopHandler),
],
)
65 changes: 65 additions & 0 deletions jupyter_remote_desktop_proxy/setup_websockify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import os
import shlex
import tempfile
from shutil import which

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


def setup_websockify():
# 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 {
'command': [
'websockify',
'-v',
'--heartbeat',
'30',
'{port}',
]
+ socket_args
+ ['--', '/bin/sh', '-c', f'cd {os.getcwd()} && {vnc_command}'],
'timeout': 30,
'new_browser_window': True,
# We want the launcher entry to point to /desktop/, not to /desktop-websockify/
# /desktop/ is the user facing URL, while /desktop-websockify/ now *only* serves
# websockets.
"launcher_entry": {"title": "Desktop", "path_info": "desktop"},
}
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>
16 changes: 15 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def run(self):
description="Run a desktop environments on Jupyter",
entry_points={
'jupyter_serverproxy_servers': [
'desktop = jupyter_remote_desktop_proxy:setup_desktop',
'desktop-websockify = jupyter_remote_desktop_proxy.setup_websockify:setup_websockify',
]
},
install_requires=[
Expand All @@ -85,4 +85,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 a0f7620

Please sign in to comment.