Skip to content

Commit 7f16655

Browse files
authored
Merge pull request #119 from manics/raw_socket_proxy
Remove websockify, add Playwright test
2 parents 3f8e3a2 + 70393e5 commit 7f16655

File tree

11 files changed

+140
-105
lines changed

11 files changed

+140
-105
lines changed

.github/workflows/test.yaml

Lines changed: 27 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -36,29 +36,29 @@ jobs:
3636
steps:
3737
- uses: actions/checkout@v4
3838

39+
- uses: actions/setup-python@v5
40+
with:
41+
python-version: "3.11"
42+
43+
- name: Cache playwright binaries
44+
uses: actions/cache@v4
45+
with:
46+
path: |
47+
~/.cache/ms-playwright
48+
key: ${{ runner.os }}-playwright
49+
3950
- name: Build image
4051
run: |
4152
docker build --progress=plain --build-arg vncserver=${{ matrix.vncserver }} -t test .
4253
43-
- name: (inside container) websockify --help
44-
run: |
45-
docker run test websockify --help
46-
4754
- name: (inside container) vncserver -help
4855
run: |
4956
# -help flag is not available for TurboVNC, but it emits the -help
5057
# equivalent information anyhow if passed -help, but also errors. Due
5158
# to this, we fallback to use the errorcode of vncsrever -list.
5259
docker run test bash -c "vncserver -help || vncserver -list > /dev/null"
5360
54-
- name: Install websocat, a test dependency"
55-
run: |
56-
wget -q https://github.com/vi/websocat/releases/download/v1.12.0/websocat.x86_64-unknown-linux-musl \
57-
-O /usr/local/bin/websocat
58-
chmod +x /usr/local/bin/websocat
59-
6061
- name: Test vncserver
61-
if: always()
6262
run: |
6363
container_id=$(docker run -d -it -p 5901:5901 test vncserver -xstartup /opt/install/jupyter_remote_desktop_proxy/share/xstartup -verbose -fg -geometry 1680x1050 -SecurityTypes None -rfbport 5901)
6464
sleep 1
@@ -79,71 +79,24 @@ jobs:
7979
8080
docker stop $container_id > /dev/null
8181
if [ "$TEST_OK" == "false" ]; then
82-
echo "One or more tests failed!"
82+
echo "Test failed!"
8383
exit 1
8484
fi
8585
86-
- name: Test websockify'ed vncserver
87-
if: always()
86+
- name: Install playwright
8887
run: |
89-
container_id=$(docker run -d -it -p 5901:5901 test websockify --verbose --log-file=/tmp/websockify.log --heartbeat=30 5901 -- vncserver -xstartup /opt/install/jupyter_remote_desktop_proxy/share/xstartup -verbose -fg -geometry 1680x1050 -SecurityTypes None -rfbport 5901)
90-
sleep 1
91-
92-
echo "::group::Install websocat, a test dependency"
93-
docker exec --user root $container_id bash -c '
94-
wget -q https://github.com/vi/websocat/releases/download/v1.12.0/websocat.x86_64-unknown-linux-musl \
95-
-O /usr/local/bin/websocat
96-
chmod +x /usr/local/bin/websocat
97-
'
98-
echo "::endgroup::"
99-
100-
docker exec -it $container_id websocat --binary --one-message --exit-on-eof "ws://localhost:5901/" 2>&1 | tee -a /dev/stderr | \
101-
grep --quiet RFB && echo "Passed test" || { echo "Failed test" && TEST_OK=false; }
102-
103-
echo "::group::websockify logs"
104-
docker exec $container_id bash -c "cat /tmp/websockify.log"
105-
echo "::endgroup::"
106-
107-
echo "::group::vncserver logs"
108-
docker exec $container_id bash -c 'cat ~/.vnc/*.log'
109-
echo "::endgroup::"
88+
python -mpip install -r dev-requirements.txt
89+
python -mplaywright install --with-deps
11090
111-
docker stop $container_id > /dev/null
112-
if [ "$TEST_OK" == "false" ]; then
113-
echo "One or more tests failed!"
114-
exit 1
115-
fi
116-
117-
- name: Test project's proxy to websockify'ed vncserver
118-
if: always()
91+
- name: Playwright browser test
11992
run: |
12093
container_id=$(docker run -d -it -p 8888:8888 -e JUPYTER_TOKEN=secret test)
12194
sleep 3
95+
export CONTAINER_ID=$container_id
96+
export JUPYTER_HOST=http://localhost:8888
97+
export JUPYTER_TOKEN=secret
12298
123-
curl --silent --fail 'http://localhost:8888/desktop/?token=secret' | grep --quiet 'Jupyter Remote Desktop Proxy' && echo "Passed get index.html test" || { echo "Failed get index.html test" && TEST_OK=false; }
124-
curl --silent --fail 'http://localhost:8888/desktop/static/dist/viewer.js?token=secret' > /dev/null && echo "Passed get viewer.js test" || { echo "Failed get viewer.js test" && TEST_OK=false; }
125-
126-
# The first attempt often fails, but the second always(?) succeeds.
127-
#
128-
# This could be related to jupyter-server-proxy's issue
129-
# https://github.com/jupyterhub/jupyter-server-proxy/issues/459
130-
# because the client/proxy websocket handshake completes before the
131-
# proxy/server handshake. This issue is tracked for this project by
132-
# https://github.com/jupyterhub/jupyter-remote-desktop-proxy/issues/105.
133-
#
134-
websocat --binary --one-message --exit-on-eof 'ws://localhost:8888/desktop-websockify/?token=secret' 2>&1 \
135-
| tee -a /dev/stderr \
136-
| grep --quiet RFB \
137-
&& echo "Passed initial websocket test" \
138-
|| { \
139-
echo "Failed initial websocket test" \
140-
&& sleep 1 \
141-
&& websocat --binary --one-message --exit-on-eof 'ws://localhost:8888/desktop-websockify/?token=secret' 2>&1 \
142-
| tee -a /dev/stderr \
143-
| grep --quiet RFB \
144-
&& echo "Passed second websocket test" \
145-
|| { echo "Failed second websocket test" && TEST_OK=false; } \
146-
}
99+
python -mpytest -vs
147100
148101
echo "::group::jupyter_server logs"
149102
docker logs $container_id
@@ -160,5 +113,10 @@ jobs:
160113
exit 1
161114
fi
162115
163-
# TODO: Check VNC desktop works, e.g. by comparing Playwright screenshots
164-
# https://playwright.dev/docs/test-snapshots
116+
- name: Upload screenshot
117+
uses: actions/upload-artifact@v4
118+
if: always()
119+
with:
120+
name: screenshots-${{ matrix.vncserver }}
121+
path: screenshots/*
122+
if-no-files-found: error

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,6 @@ dmypy.json
134134

135135
# Pyre type checker
136136
.pyre/
137+
138+
# Additional ignores
139+
screenshots/

Dockerfile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ USER root
55
RUN apt-get -y -qq update \
66
&& apt-get -y -qq install \
77
dbus-x11 \
8+
# xclip is added as jupyter-remote-desktop-proxy's tests requires it
9+
xclip \
810
xfce4 \
911
xfce4-panel \
1012
xfce4-session \
@@ -55,5 +57,4 @@ RUN . /opt/conda/bin/activate && \
5557

5658
COPY --chown=$NB_UID:$NB_GID . /opt/install
5759
RUN . /opt/conda/bin/activate && \
58-
pip install -e /opt/install && \
59-
jupyter server extension enable jupyter_remote_desktop_proxy
60+
pip install /opt/install

README.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,7 @@ For an example, see the [`Dockerfile`](./Dockerfile) in this repository which in
3131
pip install jupyter-remote-desktop-proxy
3232
```
3333

34-
2. Install the [websockify](https://github.com/novnc/websockify) dependency. Unfortunately,
35-
the PyPI `websockify` package is broken, so you need to install it either
36-
from [conda-forge](https://anaconda.org/conda-forge/websockify) or with
37-
[apt](https://packages.ubuntu.com/search?suite=all&searchon=names&keywords=websockify)
38-
39-
3. Install the packages needed to provide a VNC server and the actual Linux Desktop environment.
34+
2. Install the packages needed to provide a VNC server and the actual Linux Desktop environment.
4035
You need to pick a desktop environment (there are many!) - here are the packages
4136
to use TigerVNC and the light-weight [XFCE4](https://www.xfce.org/) desktop environment on Ubuntu 22.04:
4237

dev-requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pillow==10.3.0
2+
playwright==1.44.0
3+
pytest==8.2.2

environment.yml

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
1-
# Unfortunately the version of websockify on PyPI doesn't include the [compiled
2-
# wrapper library](https://github.com/novnc/websockify#wrap-a-program) which is
3-
# used by this extension, so either you'd have to manually compile it after pip
4-
# installing websockify, or use the conda package.
5-
#
61
channels:
72
- conda-forge
83
dependencies:
9-
- jupyter-server-proxy>=1.4
4+
- jupyter-server-proxy>=4.3.0
105
- jupyterhub-singleuser
116
- pip
12-
- websockify

jupyter_remote_desktop_proxy/setup_websockify.py

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import os
22
import shlex
3-
import tempfile
43
from shutil import which
54

65
HERE = os.path.dirname(os.path.abspath(__file__))
@@ -12,8 +11,6 @@ def setup_websockify():
1211
raise RuntimeError(
1312
"vncserver executable not found, please install a VNC server"
1413
)
15-
if not which('websockify'):
16-
raise RuntimeError("websockify executable not found, please install websockify")
1714

1815
# TurboVNC and TigerVNC share the same origin and both use a Perl script
1916
# as the executable vncserver. We can determine if vncserver is TigerVNC
@@ -30,16 +27,10 @@ def setup_websockify():
3027
is_tigervnc = "tigervnc" in vncserver_file.read().casefold()
3128

3229
if is_tigervnc:
33-
# Make a secure temporary directory for sockets that is only readable,
34-
# writeable, and searchable by our uid - TigerVNC can listen to a Unix
35-
# socket!
36-
sockets_dir = tempfile.mkdtemp()
37-
sockets_path = os.path.join(sockets_dir, 'vnc-socket')
38-
39-
websockify_args = ['--unix-target', sockets_path]
40-
vnc_args = [vncserver, '-rfbunixpath', sockets_path]
30+
unix_socket = True
31+
vnc_args = [vncserver, '-rfbunixpath', "{unix_socket}"]
4132
else:
42-
websockify_args = []
33+
unix_socket = False
4334
vnc_args = [vncserver, '-localhost', '-rfbport', '{port}']
4435

4536
if not os.path.exists(os.path.expanduser('~/.vnc/xstartup')):
@@ -58,18 +49,13 @@ def setup_websockify():
5849
)
5950

6051
return {
61-
'command': [
62-
'websockify',
63-
'--verbose',
64-
'--heartbeat=30',
65-
'{port}',
66-
]
67-
+ websockify_args
68-
+ ['--', '/bin/sh', '-c', f'cd {os.getcwd()} && {vnc_command}'],
52+
'command': ['/bin/sh', '-c', f'cd {os.getcwd()} && {vnc_command}'],
6953
'timeout': 30,
7054
'new_browser_window': True,
7155
# We want the launcher entry to point to /desktop/, not to /desktop-websockify/
7256
# /desktop/ is the user facing URL, while /desktop-websockify/ now *only* serves
7357
# websockets.
7458
"launcher_entry": {"title": "Desktop", "path_info": "desktop"},
59+
"unix_socket": unix_socket,
60+
"raw_socket_proxy": True,
7561
}

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def run(self):
6464
]
6565
},
6666
install_requires=[
67-
'jupyter-server-proxy>=4.1.1',
67+
'jupyter-server-proxy>=4.3.0',
6868
],
6969
include_package_data=True,
7070
keywords=["Interactive", "Desktop", "Jupyter"],

tests/conftest.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from os import getenv
2+
3+
import pytest
4+
from playwright.sync_api import sync_playwright
5+
6+
HEADLESS = getenv("HEADLESS", "1") == "1"
7+
8+
9+
@pytest.fixture()
10+
def browser():
11+
# browser_type in ["chromium", "firefox", "webkit"]
12+
with sync_playwright() as playwright:
13+
browser = playwright.firefox.launch(headless=HEADLESS)
14+
context = browser.new_context()
15+
page = context.new_page()
16+
yield page
17+
context.clear_cookies()
18+
browser.close()

tests/reference/desktop.png

209 KB
Loading

0 commit comments

Comments
 (0)