Skip to content

Commit a0f7620

Browse files
authored
Merge pull request #79 from jupyterhub/server
Add a "Hub Control Panel" menu item if running inside a JupyterHub
2 parents 457ad49 + 0fadd16 commit a0f7620

File tree

10 files changed

+174
-64
lines changed

10 files changed

+174
-64
lines changed

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@ RUN . /opt/conda/bin/activate && \
3131

3232
COPY --chown=$NB_UID:$NB_GID . /opt/install
3333
RUN . /opt/conda/bin/activate && \
34-
pip install -e /opt/install
34+
pip install -e /opt/install && \
35+
jupyter server extension enable jupyter_remote_desktop_proxy

js/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ function status(text) {
3030
document.getElementById("status").textContent = text;
3131
}
3232

33-
// Construct the websockify websocket URL we want to connect to
34-
let websockifyUrl = new URL("websockify", window.location);
33+
// This page is served under the /desktop/, and the websockify websocket is served
34+
// under /desktop-websockify/ with the same base url as /desktop/. We resolve it relatively
35+
// this way.
36+
let websockifyUrl = new URL("../desktop-websockify/", window.location);
3537
websockifyUrl.protocol = window.location.protocol === "https:" ? "wss" : "ws";
3638

3739
// Creating a new RFB object will start a new connection
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"NotebookApp": {
3+
"nbserver_extensions": {
4+
"jupyter_remote_desktop_proxy": true
5+
}
6+
}
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"ServerApp": {
3+
"jpserver_extensions": {
4+
"jupyter_remote_desktop_proxy": true
5+
}
6+
}
7+
}
Lines changed: 10 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,17 @@
11
import os
2-
import shlex
3-
import tempfile
4-
from shutil import which
52

6-
HERE = os.path.dirname(os.path.abspath(__file__))
7-
8-
9-
def setup_desktop():
10-
# make a secure temporary directory for sockets
11-
# This is only readable, writeable & searchable by our uid
12-
sockets_dir = tempfile.mkdtemp()
13-
sockets_path = os.path.join(sockets_dir, 'vnc-socket')
14-
vncserver = which('vncserver')
3+
from .server_extension import load_jupyter_server_extension
154

16-
if vncserver is None:
17-
# Use bundled tigervnc
18-
vncserver = os.path.join(HERE, 'share/tigervnc/bin/vncserver')
19-
20-
# TigerVNC provides the option to connect a Unix socket. TurboVNC does not.
21-
# TurboVNC and TigerVNC share the same origin and both use a Perl script
22-
# as the executable vncserver. We can determine if vncserver is TigerVNC
23-
# by searching TigerVNC string in the Perl script.
24-
with open(vncserver) as vncserver_file:
25-
is_tigervnc = "TigerVNC" in vncserver_file.read()
5+
HERE = os.path.dirname(os.path.abspath(__file__))
266

27-
if is_tigervnc:
28-
vnc_args = [vncserver, '-rfbunixpath', sockets_path]
29-
socket_args = ['--unix-target', sockets_path]
30-
else:
31-
vnc_args = [vncserver]
32-
socket_args = []
337

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

37-
vnc_command = shlex.join(
38-
vnc_args
39-
+ [
40-
'-verbose',
41-
'-geometry',
42-
'1680x1050',
43-
'-SecurityTypes',
44-
'None',
45-
'-fg',
46-
]
47-
)
4814

49-
return {
50-
'command': [
51-
'websockify',
52-
'-v',
53-
'--web',
54-
os.path.join(HERE, 'static'),
55-
'--heartbeat',
56-
'30',
57-
'{port}',
58-
]
59-
+ socket_args
60-
+ ['--', '/bin/sh', '-c', f'cd {os.getcwd()} && {vnc_command}'],
61-
'timeout': 30,
62-
'mappath': {'/': '/index.html'},
63-
'new_browser_window': True,
64-
}
15+
# For backward compatibility
16+
_load_jupyter_server_extension = load_jupyter_server_extension
17+
_jupyter_server_extension_paths = _jupyter_server_extension_points
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import os
2+
3+
import jinja2
4+
from jupyter_server.base.handlers import JupyterHandler
5+
from tornado import web
6+
7+
jinja_env = jinja2.Environment(
8+
loader=jinja2.FileSystemLoader(
9+
os.path.join(os.path.dirname(__file__), 'templates')
10+
),
11+
)
12+
13+
14+
HERE = os.path.dirname(os.path.abspath(__file__))
15+
16+
17+
class DesktopHandler(JupyterHandler):
18+
@web.authenticated
19+
async def get(self):
20+
template_params = {
21+
'base_url': self.base_url,
22+
}
23+
template_params.update(self.serverapp.jinja_template_vars)
24+
self.write(jinja_env.get_template("index.html").render(**template_params))
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from pathlib import Path
2+
3+
from jupyter_server.base.handlers import AuthenticatedFileHandler
4+
from jupyter_server.utils import url_path_join
5+
from jupyter_server_proxy.handlers import AddSlashHandler
6+
7+
from .handlers import DesktopHandler
8+
9+
HERE = Path(__file__).parent
10+
11+
12+
def load_jupyter_server_extension(server_app):
13+
"""
14+
Called during notebook start
15+
"""
16+
base_url = server_app.web_app.settings["base_url"]
17+
18+
server_app.web_app.add_handlers(
19+
".*",
20+
[
21+
# Serve our own static files
22+
(
23+
url_path_join(base_url, "/desktop/static/(.*)"),
24+
AuthenticatedFileHandler,
25+
{"path": (str(HERE / "static"))},
26+
),
27+
# To simplify URL mapping, we make sure that /desktop/ always
28+
# has a trailing slash
29+
(url_path_join(base_url, "/desktop"), AddSlashHandler),
30+
(url_path_join(base_url, "/desktop/"), DesktopHandler),
31+
],
32+
)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import os
2+
import shlex
3+
import tempfile
4+
from shutil import which
5+
6+
HERE = os.path.dirname(os.path.abspath(__file__))
7+
8+
9+
def setup_websockify():
10+
# make a secure temporary directory for sockets
11+
# This is only readable, writeable & searchable by our uid
12+
sockets_dir = tempfile.mkdtemp()
13+
sockets_path = os.path.join(sockets_dir, 'vnc-socket')
14+
vncserver = which('vncserver')
15+
16+
if vncserver is None:
17+
# Use bundled tigervnc
18+
vncserver = os.path.join(HERE, 'share/tigervnc/bin/vncserver')
19+
20+
# TigerVNC provides the option to connect a Unix socket. TurboVNC does not.
21+
# TurboVNC and TigerVNC share the same origin and both use a Perl script
22+
# as the executable vncserver. We can determine if vncserver is TigerVNC
23+
# by searching TigerVNC string in the Perl script.
24+
with open(vncserver) as vncserver_file:
25+
is_tigervnc = "TigerVNC" in vncserver_file.read()
26+
27+
if is_tigervnc:
28+
vnc_args = [vncserver, '-rfbunixpath', sockets_path]
29+
socket_args = ['--unix-target', sockets_path]
30+
else:
31+
vnc_args = [vncserver]
32+
socket_args = []
33+
34+
if not os.path.exists(os.path.expanduser('~/.vnc/xstartup')):
35+
vnc_args.extend(['-xstartup', os.path.join(HERE, 'share/xstartup')])
36+
37+
vnc_command = shlex.join(
38+
vnc_args
39+
+ [
40+
'-verbose',
41+
'-geometry',
42+
'1680x1050',
43+
'-SecurityTypes',
44+
'None',
45+
'-fg',
46+
]
47+
)
48+
49+
return {
50+
'command': [
51+
'websockify',
52+
'-v',
53+
'--heartbeat',
54+
'30',
55+
'{port}',
56+
]
57+
+ socket_args
58+
+ ['--', '/bin/sh', '-c', f'cd {os.getcwd()} && {vnc_command}'],
59+
'timeout': 30,
60+
'new_browser_window': True,
61+
# We want the launcher entry to point to /desktop/, not to /desktop-websockify/
62+
# /desktop/ is the user facing URL, while /desktop-websockify/ now *only* serves
63+
# websockets.
64+
"launcher_entry": {"title": "Desktop", "path_info": "desktop"},
65+
}

jupyter_remote_desktop_proxy/static/index.html renamed to jupyter_remote_desktop_proxy/templates/index.html

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@
1313
Chrome Frame. -->
1414
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
1515

16-
<link href="./dist/index.css" rel="stylesheet" />
16+
<link href="{{ base_url }}desktop/static/dist/index.css" rel="stylesheet" />
1717
</head>
1818

1919
<body>
2020
<div id="top-bar">
2121
<a href=".." id="logo">
22-
<img src="./jupyter-logo.svg" />
22+
<img src="{{base_url}}desktop/static/jupyter-logo.svg" />
2323
</a>
2424
<ul id="menu">
2525
<li id="status-container">
@@ -29,6 +29,11 @@
2929
<li>
3030
<a id="clipboard-button" href="#">Remote Clipboard</a>
3131
</li>
32+
{% if hub_control_panel_url %}
33+
<li>
34+
<a href="{{ hub_control_panel_url }}">Hub Control Panel</a>
35+
</li>
36+
{% endif %}
3237
</ul>
3338
</div>
3439
<div id="screen">
@@ -47,6 +52,6 @@
4752
</div>
4853
</div>
4954

50-
<script src="./dist/viewer.js"></script>
55+
<script src="{{base_url}}desktop/static/dist/viewer.js"></script>
5156
</body>
5257
</html>

setup.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def run(self):
6060
description="Run a desktop environments on Jupyter",
6161
entry_points={
6262
'jupyter_serverproxy_servers': [
63-
'desktop = jupyter_remote_desktop_proxy:setup_desktop',
63+
'desktop-websockify = jupyter_remote_desktop_proxy.setup_websockify:setup_websockify',
6464
]
6565
},
6666
install_requires=[
@@ -85,4 +85,18 @@ def run(self):
8585
# Handles `pip install` directly
8686
"build_py": webpacked_command(build_py),
8787
},
88+
data_files=[
89+
(
90+
'etc/jupyter/jupyter_server_config.d',
91+
[
92+
'jupyter-config/jupyter_server_config.d/jupyter_remote_desktop_proxy.json'
93+
],
94+
),
95+
(
96+
'etc/jupyter/jupyter_notebook_config.d',
97+
[
98+
'jupyter-config/jupyter_notebook_config.d/jupyter_remote_desktop_proxy.json'
99+
],
100+
),
101+
],
88102
)

0 commit comments

Comments
 (0)