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

Add a "Hub Control Panel" menu item if running inside a JupyterHub #79

Merged
merged 3 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
18 changes: 16 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ 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=[
stall_requires=[
yuvipanda marked this conversation as resolved.
Show resolved Hide resolved
'jupyter-server-proxy>=1.4.0',
],
include_package_data=True,
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'
],
),
],
)
Loading