├── .dockerignore ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .gitmodules ├── ARCHITECTURE.md ├── DEVELOPMENT.md ├── Dockerfile ├── Dockerfile.amd64 ├── Dockerfile.arm64 ├── Dockerfile.armhf ├── Dockerfile.j2 ├── LICENSE ├── Makefile ├── README.md ├── TODO ├── flavors ├── lxde.yml ├── lxqt.yml └── xfce4.yml ├── hooks └── pre_build ├── rootfs ├── etc │ ├── nginx │ │ └── sites-enabled │ │ │ └── default │ └── supervisor │ │ └── conf.d │ │ ├── supervisord.conf │ │ └── supervisord.conf.j2 ├── root │ ├── .asoundrc │ └── .gtkrc-2.0 ├── startup.sh └── usr │ ├── local │ ├── bin │ │ ├── chromium-browser-sound.sh │ │ └── xvfb.sh │ ├── lib │ │ └── web │ │ │ ├── backend │ │ │ ├── config │ │ │ │ └── __init__.py │ │ │ ├── log │ │ │ │ ├── __init__.py │ │ │ │ └── config.py │ │ │ ├── requirements.txt │ │ │ ├── run.py │ │ │ └── vnc │ │ │ │ ├── __init__.py │ │ │ │ ├── app.py │ │ │ │ ├── log.py │ │ │ │ ├── response.py │ │ │ │ ├── state.py │ │ │ │ └── util.py │ │ │ └── frontend │ │ │ └── .gitkeep │ └── share │ │ └── doro-lxde-wallpapers │ │ ├── bg1.jpg │ │ ├── bg2.jpg │ │ ├── bg3.jpg │ │ ├── bg4.jpg │ │ └── desktop-items-0.conf │ └── share │ └── applications │ └── chromium-browser-sound.desktop ├── screenshots └── lxde.png └── web ├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .postcssrc.js ├── README.md ├── build ├── build.js ├── check-versions.js ├── logo.png ├── utils.js ├── vue-loader.conf.js ├── webpack.base.conf.js ├── webpack.dev.conf.js └── webpack.prod.conf.js ├── config ├── dev.env.js ├── index.js ├── prod.env.js └── test.env.js ├── index.html ├── novnc-armhf-1.patch ├── package.json ├── src ├── App.vue ├── assets │ └── .gitkeep ├── components │ └── Vnc.vue ├── main.js └── router │ └── index.js ├── static ├── .gitkeep ├── scripts │ ├── flv.min.js │ └── jquery-3.3.1.min.js ├── video.html └── vnc.html ├── test ├── e2e │ ├── custom-assertions │ │ └── elementCount.js │ ├── nightwatch.conf.js │ ├── runner.js │ └── specs │ │ └── test.js └── unit │ ├── .eslintrc │ └── specs │ └── HelloWorld.spec.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | web/node_modules 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Versions (please complete the following information):** 27 | - OS: [e.g. Ubuntu 18.04] 28 | - image tag [e.g. develop] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | tags 3 | cscope* 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "web/static/novnc"] 2 | path = web/static/novnc 3 | url = https://github.com/novnc/noVNC 4 | [submodule "web/static/websockify"] 5 | path = web/static/websockify 6 | url = https://github.com/novnc/websockify 7 | -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # Architecture of the container # 2 | 3 | Components 4 | ============ 5 | 6 | The container contains the following components : 7 | - An Ubuntu base system 8 | - The tini + supervisord startup and daemon control system 9 | - Nginx Web server 10 | - A backend ("novnc2") Python Web app providing an API (written with 11 | Flask) on port 6079 12 | - A frontend VueJS Web app displayed to the user, which will wrap noVNC 13 | - noVNC + WebSockify providing the Web VNC client in an HTML5 canvas 14 | - Xvfb running the X11 server in memory 15 | - x11vnc exporting the X11 display through VNC 16 | - and all regular X applications, like the LXDE desktop and apps 17 | 18 | Wiring them all 19 | ------------------ 20 | 21 | Internally, Xvfb will be started in DISPLAY :1, then x11vnc will 22 | provide access to it on the default VNC port (5900). 23 | 24 | noVNC will be started listening to HTTP requests on port 6081. 25 | It is possible to connect directly to port 6081 of the container, to 26 | only use the regular noVNC Web interface (provided it is exported by 27 | the container). 28 | 29 | Above noVNC stands the VueJS frontend Web app provided by nginx, which 30 | will proxy the noVNC canvas, and will add some useful features over 31 | noVNC. 32 | 33 | User-oriented features 34 | ========================== 35 | 36 | The Web frontend adds the following features : 37 | - upon display of the Web page, the app will detect the size of the 38 | Web browser's window, and will invoke the backend API so as to make 39 | sure the noVNC rendering ajusts to that size 40 | - provide a flash video rendering transporting the sound (???) 41 | 42 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Get code 2 | 3 | ``` 4 | git clone --recursive https://github.com/fcwu/docker-ubuntu-vnc-desktop 5 | ``` 6 | 7 | or, if you have already cloned it, get submodules contents : 8 | ``` 9 | git submodule init; git submodule update 10 | ``` 11 | 12 | # Test local code 13 | 14 | ## Test-run in container rebuilt from local repo 15 | 16 | You may edit the code in your local copy of the repo, rebuild the 17 | container, and test the changes: 18 | 19 | ``` 20 | make clean 21 | FLAVOR=lxqt ARCH=amd64 IMAGE=ubuntu:18.04 make build 22 | make run 23 | ``` 24 | 25 | ## develop backend 26 | 27 | You may wish to work on the backend app. As the "make run" makes sure 28 | to mount the current dir contents under /src in the container, you can 29 | proceed as such (no compilation of the Python code): 30 | ``` 31 | make shell 32 | supervisorctl -c /etc/supervisor/supervisord.conf stop web 33 | cd /src/image/usr/local/lib/web/backend 34 | ./run.py --debug 35 | ``` 36 | 37 | ## develop frontend 38 | 39 | ``` 40 | cd web 41 | yarn add 42 | BACKEND=http://127.0.0.1:6080 npm run dev 43 | ``` 44 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | Dockerfile.amd64 -------------------------------------------------------------------------------- /Dockerfile.amd64: -------------------------------------------------------------------------------- 1 | # Built with arch: amd64 flavor: lxde image: ubuntu:20.04 2 | # 3 | ################################################################################ 4 | # base system 5 | ################################################################################ 6 | 7 | FROM ubuntu:20.04 as system 8 | 9 | 10 | 11 | RUN sed -i 's#http://archive.ubuntu.com/ubuntu/#mirror://mirrors.ubuntu.com/mirrors.txt#' /etc/apt/sources.list; 12 | 13 | 14 | # built-in packages 15 | ENV DEBIAN_FRONTEND noninteractive 16 | RUN apt update \ 17 | && apt install -y --no-install-recommends software-properties-common curl apache2-utils \ 18 | && apt update \ 19 | && apt install -y --no-install-recommends --allow-unauthenticated \ 20 | supervisor nginx sudo net-tools zenity xz-utils \ 21 | dbus-x11 x11-utils alsa-utils \ 22 | mesa-utils libgl1-mesa-dri \ 23 | && apt autoclean -y \ 24 | && apt autoremove -y \ 25 | && rm -rf /var/lib/apt/lists/* 26 | # install debs error if combine together 27 | RUN apt update \ 28 | && apt install -y --no-install-recommends --allow-unauthenticated \ 29 | xvfb x11vnc \ 30 | vim-tiny firefox ttf-ubuntu-font-family ttf-wqy-zenhei \ 31 | && apt autoclean -y \ 32 | && apt autoremove -y \ 33 | && rm -rf /var/lib/apt/lists/* 34 | 35 | RUN apt update \ 36 | && apt install -y gpg-agent \ 37 | && curl -LO https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb \ 38 | && (dpkg -i ./google-chrome-stable_current_amd64.deb || apt-get install -fy) \ 39 | && curl -sSL https://dl.google.com/linux/linux_signing_key.pub | apt-key add \ 40 | && rm google-chrome-stable_current_amd64.deb \ 41 | && rm -rf /var/lib/apt/lists/* 42 | 43 | RUN apt update \ 44 | && apt install -y --no-install-recommends --allow-unauthenticated \ 45 | lxde gtk2-engines-murrine gnome-themes-standard gtk2-engines-pixbuf gtk2-engines-murrine arc-theme \ 46 | && apt autoclean -y \ 47 | && apt autoremove -y \ 48 | && rm -rf /var/lib/apt/lists/* 49 | 50 | 51 | # Additional packages require ~600MB 52 | # libreoffice pinta language-pack-zh-hant language-pack-gnome-zh-hant firefox-locale-zh-hant libreoffice-l10n-zh-tw 53 | 54 | # tini to fix subreap 55 | ARG TINI_VERSION=v0.18.0 56 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /bin/tini 57 | RUN chmod +x /bin/tini 58 | 59 | # ffmpeg 60 | RUN apt update \ 61 | && apt install -y --no-install-recommends --allow-unauthenticated \ 62 | ffmpeg \ 63 | && rm -rf /var/lib/apt/lists/* \ 64 | && mkdir /usr/local/ffmpeg \ 65 | && ln -s /usr/bin/ffmpeg /usr/local/ffmpeg/ffmpeg 66 | 67 | # python library 68 | COPY rootfs/usr/local/lib/web/backend/requirements.txt /tmp/ 69 | RUN apt-get update \ 70 | && dpkg-query -W -f='${Package}\n' > /tmp/a.txt \ 71 | && apt-get install -y python3-pip python3-dev build-essential \ 72 | && pip3 install setuptools wheel && pip3 install -r /tmp/requirements.txt \ 73 | && ln -s /usr/bin/python3 /usr/local/bin/python \ 74 | && dpkg-query -W -f='${Package}\n' > /tmp/b.txt \ 75 | && apt-get remove -y `diff --changed-group-format='%>' --unchanged-group-format='' /tmp/a.txt /tmp/b.txt | xargs` \ 76 | && apt-get autoclean -y \ 77 | && apt-get autoremove -y \ 78 | && rm -rf /var/lib/apt/lists/* \ 79 | && rm -rf /var/cache/apt/* /tmp/a.txt /tmp/b.txt 80 | 81 | 82 | ################################################################################ 83 | # builder 84 | ################################################################################ 85 | FROM ubuntu:20.04 as builder 86 | 87 | 88 | RUN sed -i 's#http://archive.ubuntu.com/ubuntu/#mirror://mirrors.ubuntu.com/mirrors.txt#' /etc/apt/sources.list; 89 | 90 | 91 | RUN apt-get update \ 92 | && apt-get install -y --no-install-recommends curl ca-certificates gnupg patch 93 | 94 | # nodejs 95 | RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - \ 96 | && apt-get install -y nodejs 97 | 98 | # yarn 99 | RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ 100 | && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ 101 | && apt-get update \ 102 | && apt-get install -y yarn 103 | 104 | # build frontend 105 | COPY web /src/web 106 | RUN cd /src/web \ 107 | && yarn \ 108 | && yarn build 109 | RUN sed -i 's#app/locale/#novnc/app/locale/#' /src/web/dist/static/novnc/app/ui.js 110 | 111 | 112 | 113 | ################################################################################ 114 | # merge 115 | ################################################################################ 116 | FROM system 117 | LABEL maintainer="fcwu.tw@gmail.com" 118 | 119 | COPY --from=builder /src/web/dist/ /usr/local/lib/web/frontend/ 120 | COPY rootfs / 121 | RUN ln -sf /usr/local/lib/web/frontend/static/websockify /usr/local/lib/web/frontend/static/novnc/utils/websockify && \ 122 | chmod +x /usr/local/lib/web/frontend/static/websockify/run 123 | 124 | EXPOSE 80 125 | WORKDIR /root 126 | ENV HOME=/home/ubuntu \ 127 | SHELL=/bin/bash 128 | HEALTHCHECK --interval=30s --timeout=5s CMD curl --fail http://127.0.0.1:6079/api/health 129 | ENTRYPOINT ["/startup.sh"] 130 | -------------------------------------------------------------------------------- /Dockerfile.arm64: -------------------------------------------------------------------------------- 1 | # Built with arch: arm64 flavor: lxde image: ubuntu:18.04 2 | # 3 | ################################################################################ 4 | # base system 5 | ################################################################################ 6 | 7 | # qemu helper for arm build 8 | FROM ubuntu:20.04 as amd64 9 | RUN apt update && apt install -y qemu-user-static 10 | FROM arm64v8/ubuntu:20.04 as system 11 | COPY --from=amd64 /usr/bin/qemu-aarch64-static /usr/bin/ 12 | 13 | 14 | 15 | RUN sed -i 's#http://archive.ubuntu.com/ubuntu/#mirror://mirrors.ubuntu.com/mirrors.txt#' /etc/apt/sources.list; 16 | 17 | 18 | # built-in packages 19 | ENV DEBIAN_FRONTEND noninteractive 20 | RUN apt update \ 21 | && apt install -y --no-install-recommends software-properties-common curl apache2-utils \ 22 | && apt update \ 23 | && apt install -y --no-install-recommends --allow-unauthenticated \ 24 | supervisor nginx sudo net-tools zenity xz-utils \ 25 | dbus-x11 x11-utils alsa-utils \ 26 | mesa-utils libgl1-mesa-dri \ 27 | && apt autoclean -y \ 28 | && apt autoremove -y \ 29 | && rm -rf /var/lib/apt/lists/* 30 | # install debs error if combine together 31 | RUN apt update \ 32 | && apt install -y --no-install-recommends --allow-unauthenticated \ 33 | xvfb x11vnc \ 34 | vim-tiny firefox chromium-browser ttf-ubuntu-font-family ttf-wqy-zenhei \ 35 | && add-apt-repository -r ppa:fcwu-tw/apps \ 36 | && apt autoclean -y \ 37 | && apt autoremove -y \ 38 | && rm -rf /var/lib/apt/lists/* 39 | 40 | RUN apt update \ 41 | && apt install -y --no-install-recommends --allow-unauthenticated \ 42 | lxde gtk2-engines-murrine gnome-themes-standard gtk2-engines-pixbuf gtk2-engines-murrine arc-theme \ 43 | && apt autoclean -y \ 44 | && apt autoremove -y \ 45 | && rm -rf /var/lib/apt/lists/* 46 | 47 | 48 | # Additional packages require ~600MB 49 | # libreoffice pinta language-pack-zh-hant language-pack-gnome-zh-hant firefox-locale-zh-hant libreoffice-l10n-zh-tw 50 | 51 | # tini for subreap 52 | ARG TINI_VERSION=v0.18.0 53 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-arm64 /bin/tini 54 | RUN chmod +x /bin/tini 55 | 56 | # ffmpeg 57 | RUN apt update \ 58 | && apt install -y --no-install-recommends --allow-unauthenticated \ 59 | ffmpeg \ 60 | && rm -rf /var/lib/apt/lists/* \ 61 | && mkdir /usr/local/ffmpeg \ 62 | && ln -s /usr/bin/ffmpeg /usr/local/ffmpeg/ffmpeg 63 | 64 | # python library 65 | COPY rootfs/usr/local/lib/web/backend/requirements.txt /tmp/ 66 | RUN apt-get update \ 67 | && dpkg-query -W -f='${Package}\n' > /tmp/a.txt \ 68 | && apt-get install -y python3-pip python3-dev build-essential \ 69 | && pip3 install setuptools wheel && pip3 install -r /tmp/requirements.txt \ 70 | && ln -s /usr/bin/python3 /usr/local/bin/python \ 71 | && dpkg-query -W -f='${Package}\n' > /tmp/b.txt \ 72 | && apt-get remove -y `diff --changed-group-format='%>' --unchanged-group-format='' /tmp/a.txt /tmp/b.txt | xargs` \ 73 | && apt-get autoclean -y \ 74 | && apt-get autoremove -y \ 75 | && rm -rf /var/lib/apt/lists/* \ 76 | && rm -rf /var/cache/apt/* /tmp/a.txt /tmp/b.txt 77 | 78 | 79 | ################################################################################ 80 | # builder 81 | ################################################################################ 82 | FROM ubuntu:20.04 as builder 83 | 84 | 85 | RUN sed -i 's#http://archive.ubuntu.com/ubuntu/#mirror://mirrors.ubuntu.com/mirrors.txt#' /etc/apt/sources.list; 86 | 87 | 88 | RUN apt-get update \ 89 | && apt-get install -y --no-install-recommends curl ca-certificates gnupg patch 90 | 91 | # nodejs 92 | RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - \ 93 | && apt-get install -y nodejs 94 | 95 | # yarn 96 | RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ 97 | && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ 98 | && apt-get update \ 99 | && apt-get install -y yarn 100 | 101 | # build frontend 102 | COPY web /src/web 103 | RUN cd /src/web \ 104 | && yarn \ 105 | && yarn build 106 | RUN sed -i 's#app/locale/#novnc/app/locale/#' /src/web/dist/static/novnc/app/ui.js 107 | 108 | 109 | RUN cd /src/web/dist/static/novnc && patch -p0 < /src/web/novnc-armhf-1.patch 110 | 111 | 112 | ################################################################################ 113 | # merge 114 | ################################################################################ 115 | FROM system 116 | LABEL maintainer="fcwu.tw@gmail.com" 117 | 118 | COPY --from=builder /src/web/dist/ /usr/local/lib/web/frontend/ 119 | COPY rootfs / 120 | RUN ln -sf /usr/local/lib/web/frontend/static/websockify" "/usr/local/lib/web/frontend/static/novnc/utils/websockify && chmod +x /usr/local/lib/web/frontend/static/websockify/run 121 | RUN ln -sf /usr/local/lib/web/frontend/static/websockify /usr/local/lib/web/frontend/static/novnc/utils/websockify && \ 122 | chmod +x /usr/local/lib/web/frontend/static/websockify/run 123 | 124 | EXPOSE 80 125 | WORKDIR /root 126 | ENV HOME=/home/ubuntu \ 127 | SHELL=/bin/bash 128 | HEALTHCHECK --interval=30s --timeout=5s CMD curl --fail http://127.0.0.1:6079/api/health 129 | ENTRYPOINT ["/startup.sh"] 130 | -------------------------------------------------------------------------------- /Dockerfile.armhf: -------------------------------------------------------------------------------- 1 | # Built with arch: armhf flavor: lxde image: ubuntu:18.04 2 | # 3 | ################################################################################ 4 | # base system 5 | ################################################################################ 6 | 7 | # qemu helper for arm build 8 | FROM ubuntu:18.04 as amd64 9 | RUN apt update && apt install -y qemu-user-static 10 | FROM arm32v7/ubuntu:18.04 as system 11 | COPY --from=amd64 /usr/bin/qemu-arm-static /usr/bin/ 12 | 13 | 14 | 15 | RUN sed -i 's#http://archive.ubuntu.com/ubuntu/#mirror://mirrors.ubuntu.com/mirrors.txt#' /etc/apt/sources.list; 16 | 17 | 18 | # built-in packages 19 | ENV DEBIAN_FRONTEND noninteractive 20 | RUN apt update \ 21 | && apt install -y --no-install-recommends software-properties-common curl apache2-utils \ 22 | && apt update \ 23 | && apt install -y --no-install-recommends --allow-unauthenticated \ 24 | supervisor nginx sudo net-tools zenity xz-utils \ 25 | dbus-x11 x11-utils alsa-utils \ 26 | mesa-utils libgl1-mesa-dri \ 27 | && apt autoclean -y \ 28 | && apt autoremove -y \ 29 | && rm -rf /var/lib/apt/lists/* 30 | # install debs error if combine together 31 | RUN add-apt-repository -y ppa:fcwu-tw/apps \ 32 | && apt update \ 33 | && apt install -y --no-install-recommends --allow-unauthenticated \ 34 | xvfb x11vnc=0.9.16-1 \ 35 | vim-tiny firefox chromium-browser ttf-ubuntu-font-family ttf-wqy-zenhei \ 36 | && add-apt-repository -r ppa:fcwu-tw/apps \ 37 | && apt autoclean -y \ 38 | && apt autoremove -y \ 39 | && rm -rf /var/lib/apt/lists/* 40 | 41 | RUN apt update \ 42 | && apt install -y --no-install-recommends --allow-unauthenticated \ 43 | lxde gtk2-engines-murrine gnome-themes-standard gtk2-engines-pixbuf gtk2-engines-murrine arc-theme \ 44 | && apt autoclean -y \ 45 | && apt autoremove -y \ 46 | && rm -rf /var/lib/apt/lists/* 47 | 48 | 49 | # Additional packages require ~600MB 50 | # libreoffice pinta language-pack-zh-hant language-pack-gnome-zh-hant firefox-locale-zh-hant libreoffice-l10n-zh-tw 51 | 52 | # tini for subreap 53 | ARG TINI_VERSION=v0.18.0 54 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-armhf /bin/tini 55 | RUN chmod +x /bin/tini 56 | 57 | # ffmpeg 58 | RUN mkdir -p /usr/local/ffmpeg \ 59 | && curl -sSL https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz | tar xJvf - -C /usr/local/ffmpeg/ --strip 1 60 | 61 | # python library 62 | COPY rootfs/usr/local/lib/web/backend/requirements.txt /tmp/ 63 | RUN apt-get update \ 64 | && dpkg-query -W -f='${Package}\n' > /tmp/a.txt \ 65 | && apt-get install -y python-pip python-dev build-essential \ 66 | && pip install setuptools wheel && pip install -r /tmp/requirements.txt \ 67 | && dpkg-query -W -f='${Package}\n' > /tmp/b.txt \ 68 | && apt-get remove -y `diff --changed-group-format='%>' --unchanged-group-format='' /tmp/a.txt /tmp/b.txt | xargs` \ 69 | && apt-get autoclean -y \ 70 | && apt-get autoremove -y \ 71 | && rm -rf /var/lib/apt/lists/* \ 72 | && rm -rf /var/cache/apt/* /tmp/a.txt /tmp/b.txt 73 | 74 | 75 | ################################################################################ 76 | # builder 77 | ################################################################################ 78 | FROM ubuntu:18.04 as builder 79 | 80 | 81 | RUN sed -i 's#http://archive.ubuntu.com/ubuntu/#mirror://mirrors.ubuntu.com/mirrors.txt#' /etc/apt/sources.list; 82 | 83 | 84 | RUN apt-get update \ 85 | && apt-get install -y --no-install-recommends curl ca-certificates gnupg patch 86 | 87 | # nodejs 88 | RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - \ 89 | && apt-get install -y nodejs 90 | 91 | # yarn 92 | RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ 93 | && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ 94 | && apt-get update \ 95 | && apt-get install -y yarn 96 | 97 | # build frontend 98 | COPY web /src/web 99 | RUN cd /src/web \ 100 | && yarn \ 101 | && yarn run build 102 | RUN sed -i 's#app/locale/#novnc/app/locale/#' /src/web/dist/static/novnc/app/ui.js 103 | 104 | RUN cd /src/web/dist/static/novnc && patch -p0 < /src/web/novnc-armhf-1.patch 105 | 106 | 107 | ################################################################################ 108 | # merge 109 | ################################################################################ 110 | FROM system 111 | LABEL maintainer="fcwu.tw@gmail.com" 112 | 113 | COPY --from=builder /src/web/dist/ /usr/local/lib/web/frontend/ 114 | COPY rootfs / 115 | RUN ln -sf /usr/local/lib/web/frontend/static/websockify" "/usr/local/lib/web/frontend/static/novnc/utils/websockify && chmod +x /usr/local/lib/web/frontend/static/websockify/run 116 | RUN ln -sf /usr/local/lib/web/frontend/static/websockify /usr/local/lib/web/frontend/static/novnc/utils/websockify && \ 117 | chmod +x /usr/local/lib/web/frontend/static/websockify/run 118 | 119 | EXPOSE 80 120 | WORKDIR /root 121 | ENV HOME=/home/ubuntu \ 122 | SHELL=/bin/bash 123 | HEALTHCHECK --interval=30s --timeout=5s CMD curl --fail http://127.0.0.1:6079/api/health 124 | ENTRYPOINT ["/startup.sh"] 125 | -------------------------------------------------------------------------------- /Dockerfile.j2: -------------------------------------------------------------------------------- 1 | # Built with arch: {{ arch }} flavor: {{ flavor }} image: {{ image }} 2 | # 3 | ################################################################################ 4 | # base system 5 | ################################################################################ 6 | {%if arch == "amd64"%} 7 | FROM {{image}} as system 8 | {%elif arch == "armhf"%} 9 | # qemu helper for arm build 10 | FROM {{image}} as amd64 11 | RUN apt update && apt install -y qemu-user-static 12 | FROM arm32v7/{{image}} as system 13 | COPY --from=amd64 /usr/bin/qemu-arm-static /usr/bin/ 14 | {%endif%} 15 | 16 | RUN sed -i 's#http://archive.ubuntu.com/ubuntu/#mirror://mirrors.ubuntu.com/mirrors.txt#' /etc/apt/sources.list; 17 | 18 | # built-in packages 19 | ENV DEBIAN_FRONTEND noninteractive 20 | RUN apt update \ 21 | && apt install -y --no-install-recommends software-properties-common curl apache2-utils \ 22 | && apt update \ 23 | && apt install -y --no-install-recommends --allow-unauthenticated \ 24 | supervisor nginx sudo net-tools zenity xz-utils \ 25 | dbus-x11 x11-utils alsa-utils \ 26 | mesa-utils libgl1-mesa-dri \ 27 | && apt autoclean -y \ 28 | && apt autoremove -y \ 29 | && rm -rf /var/lib/apt/lists/* 30 | # install debs error if combine together 31 | RUN apt update \ 32 | && apt install -y --no-install-recommends --allow-unauthenticated \ 33 | xvfb x11vnc \ 34 | vim-tiny firefox ttf-ubuntu-font-family ttf-wqy-zenhei \ 35 | && apt autoclean -y \ 36 | && apt autoremove -y \ 37 | && rm -rf /var/lib/apt/lists/* 38 | RUN apt update \ 39 | && apt install -y gpg-agent \ 40 | && curl -LO https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb \ 41 | && (dpkg -i ./google-chrome-stable_current_amd64.deb || apt-get install -fy) \ 42 | && curl -sSL https://dl.google.com/linux/linux_signing_key.pub | apt-key add \ 43 | && rm google-chrome-stable_current_amd64.deb \ 44 | && rm -rf /var/lib/apt/lists/* 45 | {%if desktop == "lxde" %} 46 | {%endif%} 47 | {%if desktop == "lxqt" %} 48 | {%endif%} 49 | {%if desktop == "xfce4" %} 50 | {%endif%} 51 | RUN apt update \ 52 | && apt install -y --no-install-recommends --allow-unauthenticated \ 53 | lxde gtk2-engines-murrine gnome-themes-standard gtk2-engines-pixbuf gtk2-engines-murrine arc-theme \ 54 | && apt autoclean -y \ 55 | && apt autoremove -y \ 56 | && rm -rf /var/lib/apt/lists/* 57 | # Additional packages require ~600MB 58 | # libreoffice pinta language-pack-zh-hant language-pack-gnome-zh-hant firefox-locale-zh-hant libreoffice-l10n-zh-tw 59 | 60 | # tini to fix subreap 61 | ARG TINI_VERSION=v0.18.0 62 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /bin/tini 63 | RUN chmod +x /bin/tini 64 | 65 | # ffmpeg 66 | RUN apt update \ 67 | && apt install -y --no-install-recommends --allow-unauthenticated \ 68 | ffmpeg \ 69 | && rm -rf /var/lib/apt/lists/* \ 70 | && mkdir /usr/local/ffmpeg \ 71 | && ln -s /usr/bin/ffmpeg /usr/local/ffmpeg/ffmpeg 72 | 73 | # python library 74 | COPY rootfs/usr/local/lib/web/backend/requirements.txt /tmp/ 75 | RUN apt-get update \ 76 | && dpkg-query -W -f='${Package}\n' > /tmp/a.txt \ 77 | && apt-get install -y python3-pip python3-dev build-essential \ 78 | && pip3 install setuptools wheel && pip3 install -r /tmp/requirements.txt \ 79 | && ln -s /usr/bin/python3 /usr/local/bin/python \ 80 | && dpkg-query -W -f='${Package}\n' > /tmp/b.txt \ 81 | && apt-get remove -y `diff --changed-group-format='%>' --unchanged-group-format='' /tmp/a.txt /tmp/b.txt | xargs` \ 82 | && apt-get autoclean -y \ 83 | && apt-get autoremove -y \ 84 | && rm -rf /var/lib/apt/lists/* \ 85 | && rm -rf /var/cache/apt/* /tmp/a.txt /tmp/b.txt 86 | 87 | 88 | ################################################################################ 89 | # builder 90 | ################################################################################ 91 | FROM {{image}} as builder 92 | 93 | {% if localbuild == 1 %} 94 | RUN sed -i 's#http://archive.ubuntu.com/ubuntu/#mirror://mirrors.ubuntu.com/mirrors.txt#' /etc/apt/sources.list; 95 | {% endif %} 96 | 97 | RUN apt-get update \ 98 | && apt-get install -y --no-install-recommends curl ca-certificates gnupg patch 99 | 100 | # nodejs 101 | RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - \ 102 | && apt-get install -y nodejs 103 | 104 | # yarn 105 | RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ 106 | && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ 107 | && apt-get update \ 108 | && apt-get install -y yarn 109 | 110 | # build frontend 111 | COPY web /src/web 112 | RUN cd /src/web \ 113 | && yarn \ 114 | && yarn build 115 | RUN sed -i 's#app/locale/#novnc/app/locale/#' /src/web/dist/static/novnc/app/ui.js 116 | 117 | {%if arch == "armhf"%} 118 | RUN cd /src/web/dist/static/novnc && patch -p0 < /src/web/novnc-armhf-1.patch 119 | {%endif%} 120 | 121 | ################################################################################ 122 | # merge 123 | ################################################################################ 124 | FROM system 125 | LABEL maintainer="fcwu.tw@gmail.com" 126 | 127 | COPY --from=builder /src/web/dist/ /usr/local/lib/web/frontend/ 128 | COPY rootfs / 129 | RUN ln -sf /usr/local/lib/web/frontend/static/websockify /usr/local/lib/web/frontend/static/novnc/utils/websockify && \ 130 | chmod +x /usr/local/lib/web/frontend/static/websockify/run 131 | 132 | EXPOSE 80 133 | WORKDIR /root 134 | ENV HOME=/home/ubuntu \ 135 | SHELL=/bin/bash 136 | HEALTHCHECK --interval=30s --timeout=5s CMD curl --fail http://127.0.0.1:6079/api/health 137 | ENTRYPOINT ["/startup.sh"] 138 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build run 2 | 3 | # Default values for variables 4 | REPO ?= dorowu/ubuntu-desktop-lxde-vnc 5 | TAG ?= latest 6 | # you can choose other base image versions 7 | IMAGE ?= ubuntu:20.04 8 | # IMAGE ?= nvidia/cuda:10.1-cudnn7-devel-ubuntu18.04 9 | # choose from supported flavors (see available ones in ./flavors/*.yml) 10 | FLAVOR ?= lxde 11 | # armhf or amd64 12 | ARCH ?= amd64 13 | 14 | # These files will be generated from teh Jinja templates (.j2 sources) 15 | templates = Dockerfile rootfs/etc/supervisor/conf.d/supervisord.conf 16 | 17 | # Rebuild the container image 18 | build: $(templates) 19 | docker build -t $(REPO):$(TAG) . 20 | 21 | # Test run the container 22 | # the local dir will be mounted under /src read-only 23 | run: 24 | docker run --privileged --rm \ 25 | -p 6080:80 -p 6081:443 \ 26 | -v ${PWD}:/src:ro \ 27 | -e USER=doro -e PASSWORD=mypassword \ 28 | -e ALSADEV=hw:2,0 \ 29 | -e SSL_PORT=443 \ 30 | -e RELATIVE_URL_ROOT=approot \ 31 | -e OPENBOX_ARGS="--startup /usr/bin/galculator" \ 32 | -v ${PWD}/ssl:/etc/nginx/ssl \ 33 | --device /dev/snd \ 34 | --name ubuntu-desktop-lxde-test \ 35 | $(REPO):$(TAG) 36 | 37 | # Connect inside the running container for debugging 38 | shell: 39 | docker exec -it ubuntu-desktop-lxde-test bash 40 | 41 | # Generate the SSL/TLS config for HTTPS 42 | gen-ssl: 43 | mkdir -p ssl 44 | openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ 45 | -keyout ssl/nginx.key -out ssl/nginx.crt 46 | 47 | clean: 48 | rm -f $(templates) 49 | 50 | extra-clean: 51 | docker rmi $(REPO):$(TAG) 52 | docker image prune -f 53 | 54 | # Run jinja2cli to parse Jinja template applying rules defined in the flavors definitions 55 | %: %.j2 flavors/$(FLAVOR).yml 56 | docker run -v $(shell pwd):/data vikingco/jinja2cli \ 57 | -D flavor=$(FLAVOR) \ 58 | -D image=$(IMAGE) \ 59 | -D localbuild=$(LOCALBUILD) \ 60 | -D arch=$(ARCH) \ 61 | $< flavors/$(FLAVOR).yml > $@ || rm $@ 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-ubuntu-vnc-desktop 2 | 3 | [![Docker Pulls](https://img.shields.io/docker/pulls/dorowu/ubuntu-desktop-lxde-vnc.svg)](https://hub.docker.com/r/dorowu/ubuntu-desktop-lxde-vnc/) 4 | [![Docker Stars](https://img.shields.io/docker/stars/dorowu/ubuntu-desktop-lxde-vnc.svg)](https://hub.docker.com/r/dorowu/ubuntu-desktop-lxde-vnc/) 5 | 6 | docker-ubuntu-vnc-desktop is a Docker image to provide web VNC interface to access Ubuntu LXDE/LxQT desktop environment. 7 | 8 | 9 | 10 | 11 | 12 | - [Quick Start](#quick-start) 13 | - [VNC Viewer](#vnc-viewer) 14 | - [HTTP Base Authentication](#http-base-authentication) 15 | - [SSL](#ssl) 16 | - [Screen Resolution](#screen-resolution) 17 | - [Default Desktop User](#default-desktop-user) 18 | - [Deploy to a subdirectory (relative url root)](#deploy-to-a-subdirectory-relative-url-root) 19 | - [Sound (Preview version and Linux only)](#sound-preview-version-and-linux-only) 20 | - [Generate Dockerfile from jinja template](#generate-dockerfile-from-jinja-template) 21 | - [Troubleshooting and FAQ](#troubleshooting-and-faq) 22 | - [License](#license) 23 | 24 | 25 | 26 | ## Quick Start 27 | 28 | Run the docker container and access with port `6080` 29 | 30 | ```shell 31 | docker run -p 6080:80 -v /dev/shm:/dev/shm dorowu/ubuntu-desktop-lxde-vnc 32 | ``` 33 | 34 | Browse http://127.0.0.1:6080/ 35 | 36 | 37 | 38 | ### Ubuntu Flavors 39 | 40 | Choose your favorite Ubuntu version with [tags](https://hub.docker.com/r/dorowu/ubuntu-desktop-lxde-vnc/tags/) 41 | 42 | - focal: Ubuntu 20.04 (latest) 43 | - focal-lxqt: Ubuntu 20.04 LXQt 44 | - bionic: Ubuntu 18.04 45 | - bionic-lxqt: Ubuntu 18.04 LXQt 46 | - xenial: Ubuntu 16.04 (deprecated) 47 | - trusty: Ubuntu 14.04 (deprecated) 48 | 49 | ## VNC Viewer 50 | 51 | Forward VNC service port 5900 to host by 52 | 53 | ```shell 54 | docker run -p 6080:80 -p 5900:5900 -v /dev/shm:/dev/shm dorowu/ubuntu-desktop-lxde-vnc 55 | ``` 56 | 57 | Now, open the vnc viewer and connect to port 5900. If you would like to protect vnc service by password, set environment variable `VNC_PASSWORD`, for example 58 | 59 | ```shell 60 | docker run -p 6080:80 -p 5900:5900 -e VNC_PASSWORD=mypassword -v /dev/shm:/dev/shm dorowu/ubuntu-desktop-lxde-vnc 61 | ``` 62 | 63 | A prompt will ask password either in the browser or vnc viewer. 64 | 65 | ## HTTP Base Authentication 66 | 67 | This image provides base access authentication of HTTP via `HTTP_PASSWORD` 68 | 69 | ```shell 70 | docker run -p 6080:80 -e HTTP_PASSWORD=mypassword -v /dev/shm:/dev/shm dorowu/ubuntu-desktop-lxde-vnc 71 | ``` 72 | 73 | ## SSL 74 | 75 | To connect with SSL, generate self signed SSL certificate first if you don't have it 76 | 77 | ```shell 78 | mkdir -p ssl 79 | openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ssl/nginx.key -out ssl/nginx.crt 80 | ``` 81 | 82 | Specify SSL port by `SSL_PORT`, certificate path to `/etc/nginx/ssl`, and forward it to 6081 83 | 84 | ```shell 85 | docker run -p 6081:443 -e SSL_PORT=443 -v ${PWD}/ssl:/etc/nginx/ssl -v /dev/shm:/dev/shm dorowu/ubuntu-desktop-lxde-vnc 86 | ``` 87 | 88 | ## Screen Resolution 89 | 90 | The Resolution of virtual desktop adapts browser window size when first connecting the server. You may choose a fixed resolution by passing `RESOLUTION` environment variable, for example 91 | 92 | ```shell 93 | docker run -p 6080:80 -e RESOLUTION=1920x1080 -v /dev/shm:/dev/shm dorowu/ubuntu-desktop-lxde-vnc 94 | ``` 95 | 96 | ## Default Desktop User 97 | 98 | The default user is `root`. You may change the user and password respectively by `USER` and `PASSWORD` environment variable, for example, 99 | 100 | ```shell 101 | docker run -p 6080:80 -e USER=doro -e PASSWORD=password -v /dev/shm:/dev/shm dorowu/ubuntu-desktop-lxde-vnc 102 | ``` 103 | 104 | ## Deploy to a subdirectory (relative url root) 105 | 106 | You may deploy this application to a subdirectory, for example `/some-prefix/`. You then can access application by `http://127.0.0.1:6080/some-prefix/`. This can be specified using the `RELATIVE_URL_ROOT` configuration option like this 107 | 108 | ```shell 109 | docker run -p 6080:80 -e RELATIVE_URL_ROOT=some-prefix dorowu/ubuntu-desktop-lxde-vnc 110 | ``` 111 | 112 | NOTE: this variable should not have any leading and trailing splash (/) 113 | 114 | ## Sound (Preview version and Linux only) 115 | 116 | It only works in Linux. 117 | 118 | First of all, insert kernel module `snd-aloop` and specify `2` as the index of sound loop device 119 | 120 | ```shell 121 | sudo modprobe snd-aloop index=2 122 | ``` 123 | 124 | Start the container 125 | 126 | ```shell 127 | docker run -it --rm -p 6080:80 --device /dev/snd -e ALSADEV=hw:2,0 dorowu/ubuntu-desktop-lxde-vnc 128 | ``` 129 | 130 | where `--device /dev/snd -e ALSADEV=hw:2,0` means to grant sound device to container and set basic ASLA config to use card 2. 131 | 132 | Launch a browser with URL http://127.0.0.1:6080/#/?video, where `video` means to start with video mode. Now you can start Chromium in start menu (Internet -> Chromium Web Browser Sound) and try to play some video. 133 | 134 | Following is the screen capture of these operations. Turn on your sound at the end of video! 135 | 136 | [![demo video](http://img.youtube.com/vi/Kv9FGClP1-k/0.jpg)](http://www.youtube.com/watch?v=Kv9FGClP1-k) 137 | 138 | 139 | ## Generate Dockerfile from jinja template 140 | 141 | WARNING: Deprecated 142 | 143 | Dockerfile and configuration can be generated by template. 144 | 145 | - arch: one of `amd64` or `armhf` 146 | - flavor: refer to file in flavor/`flavor`.yml 147 | - image: base image 148 | - desktop: desktop environment which is set in flavor 149 | - addon_package: Debian package to be installed which is set in flavor 150 | 151 | Dockerfile and configuration are re-generate if they do not exist. Or you may force to re-generate by removing them with the command `make clean`. 152 | 153 | ## Troubleshooting and FAQ 154 | 155 | 1. boot2docker connection issue, https://github.com/fcwu/docker-ubuntu-vnc-desktop/issues/2 156 | 2. Multi-language supports, https://github.com/fcwu/docker-ubuntu-vnc-desktop/issues/80 157 | 3. Autostart, https://github.com/fcwu/docker-ubuntu-vnc-desktop/issues/85#issuecomment-466778407 158 | 4. x11vnc arguments(multiptr), https://github.com/fcwu/docker-ubuntu-vnc-desktop/issues/101 159 | 5. firefox/chrome crash (/dev/shm), https://github.com/fcwu/docker-ubuntu-vnc-desktop/issues/112 160 | 6. resize display size without destroying container, https://github.com/fcwu/docker-ubuntu-vnc-desktop/issues/115#issuecomment-522426037 161 | 162 | ## License 163 | 164 | See the LICENSE file for details. 165 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | - upgrade frontend packages 2 | - rewrite backend by golang 3 | -------------------------------------------------------------------------------- /flavors/lxde.yml: -------------------------------------------------------------------------------- 1 | --- 2 | addon_packages: 3 | - vim-tiny 4 | - firefox 5 | - chromium-browser 6 | - ttf-ubuntu-font-family 7 | - ttf-wqy-zenhei 8 | desktop: lxde 9 | -------------------------------------------------------------------------------- /flavors/lxqt.yml: -------------------------------------------------------------------------------- 1 | --- 2 | addon_packages: 3 | - vim-tiny 4 | - firefox 5 | - chromium-browser 6 | - ttf-ubuntu-font-family 7 | - ttf-wqy-zenhei 8 | desktop: lxqt 9 | -------------------------------------------------------------------------------- /flavors/xfce4.yml: -------------------------------------------------------------------------------- 1 | --- 2 | addon_packages: 3 | - vim-tiny 4 | - firefox 5 | - xfce4-terminal 6 | desktop: xfce4 7 | -------------------------------------------------------------------------------- /hooks/pre_build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Register qemu-*-static for all supported processors except the 3 | # current one, but also remove all registered binfmt_misc before 4 | docker run --rm --privileged multiarch/qemu-user-static:register --reset 5 | -------------------------------------------------------------------------------- /rootfs/etc/nginx/sites-enabled/default: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | # listen [::]:80 default_server ipv6only=on; 4 | 5 | #_SSL_PORT_#listen 443 ssl default_server; 6 | #_SSL_PORT_#listen [::]:443 ssl default_server ipv6only=on; 7 | #_SSL_PORT_#ssl_certificate /etc/nginx/ssl/nginx.crt; 8 | #_SSL_PORT_#ssl_certificate_key /etc/nginx/ssl/nginx.key; 9 | 10 | #_HTTP_PASSWORD_#auth_basic "Private Property"; 11 | #_HTTP_PASSWORD_#auth_basic_user_file /etc/nginx/.htpasswd; 12 | 13 | root /usr/local/lib/web/frontend/; 14 | index index.html index.htm; 15 | 16 | #_RELATIVE_URL_ROOT_location /_RELATIVE_URL_ROOT_/ { 17 | #_RELATIVE_URL_ROOT_ rewrite /_RELATIVE_URL_ROOT_/(.*) /$1 break; 18 | #_RELATIVE_URL_ROOT_ root /usr/local/lib/web/frontend/; 19 | #_RELATIVE_URL_ROOT_} 20 | 21 | location ~ .*/(api/.*|websockify) { 22 | try_files $uri @api$http_upgrade; 23 | } 24 | 25 | location / { 26 | rewrite /approot/(.*) /$1 break; 27 | root /usr/local/lib/web/frontend/; 28 | } 29 | 30 | location @apiwebsocket { 31 | #_RELATIVE_URL_ROOT_rewrite /_RELATIVE_URL_ROOT_/(.*) $1 break; 32 | proxy_connect_timeout 7d; 33 | proxy_send_timeout 7d; 34 | proxy_read_timeout 7d; 35 | proxy_buffering off; 36 | 37 | proxy_http_version 1.1; 38 | proxy_set_header Upgrade $http_upgrade; 39 | proxy_set_header Connection "upgrade"; 40 | proxy_pass http://127.0.0.1:6081; 41 | } 42 | 43 | location @api { 44 | #_RELATIVE_URL_ROOT_rewrite /_RELATIVE_URL_ROOT_/(.*) $1 break; 45 | proxy_set_header X-Real-IP $remote_addr; 46 | proxy_set_header X-Forwarded-For $remote_addr; 47 | proxy_set_header Host $host; 48 | max_ranges 0; 49 | proxy_pass http://127.0.0.1:6079; 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /rootfs/etc/supervisor/conf.d/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | redirect_stderr=true 3 | stopsignal=QUIT 4 | autorestart=true 5 | directory=/root 6 | 7 | [program:nginx] 8 | priority=10 9 | command=nginx -c /etc/nginx/nginx.conf -g 'daemon off;' 10 | 11 | [program:web] 12 | priority=10 13 | directory=/usr/local/lib/web/backend 14 | command=/usr/local/lib/web/backend/run.py 15 | stdout_logfile=/dev/fd/1 16 | stdout_logfile_maxbytes=0 17 | stderr_logfile=/dev/fd/1 18 | stderr_logfile_maxbytes=0 19 | 20 | 21 | [group:x] 22 | programs=xvfb,wm,lxpanel,pcmanfm,x11vnc,novnc 23 | 24 | [program:wm] 25 | priority=15 26 | command=/usr/bin/openbox 27 | environment=DISPLAY=":1",HOME="/root",USER="root" 28 | 29 | [program:lxpanel] 30 | priority=15 31 | directory=%HOME% 32 | command=/usr/bin/lxpanel --profile LXDE 33 | user=%USER% 34 | environment=DISPLAY=":1",HOME="%HOME%",USER="%USER%" 35 | 36 | [program:pcmanfm] 37 | priority=15 38 | directory=%HOME% 39 | command=/usr/bin/pcmanfm --desktop --profile LXDE 40 | user=%USER% 41 | stopwaitsecs=3 42 | environment=DISPLAY=":1",HOME="%HOME%",USER="%USER%" 43 | 44 | 45 | 46 | 47 | 48 | 49 | [program:xvfb] 50 | priority=10 51 | command=/usr/local/bin/xvfb.sh 52 | stopsignal=KILL 53 | 54 | [program:x11vnc] 55 | priority=20 56 | command=x11vnc -display :1 -xkb -forever -shared -repeat -capslock 57 | 58 | [program:novnc] 59 | priority=25 60 | directory=/usr/local/lib/web/frontend/static/novnc 61 | command=bash /usr/local/lib/web/frontend/static/novnc/utils/launch.sh --listen 6081 62 | stopasgroup=true 63 | -------------------------------------------------------------------------------- /rootfs/etc/supervisor/conf.d/supervisord.conf.j2: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | redirect_stderr=true 3 | stopsignal=QUIT 4 | autorestart=true 5 | directory=/root 6 | 7 | [program:nginx] 8 | priority=10 9 | command=nginx -c /etc/nginx/nginx.conf -g 'daemon off;' 10 | 11 | [program:web] 12 | priority=10 13 | directory=/usr/local/lib/web/backend 14 | command=/usr/local/lib/web/backend/run.py 15 | stdout_logfile=/dev/fd/1 16 | stdout_logfile_maxbytes=0 17 | stderr_logfile=/dev/fd/1 18 | stderr_logfile_maxbytes=0 19 | 20 | {% if desktop == "lxde" %} 21 | [group:x] 22 | programs=xvfb,wm,lxpanel,pcmanfm,x11vnc,novnc 23 | 24 | [program:wm] 25 | priority=15 26 | command=/usr/bin/openbox 27 | environment=DISPLAY=":1",HOME="/root",USER="root" 28 | 29 | [program:lxpanel] 30 | priority=15 31 | directory=%HOME% 32 | command=/usr/bin/lxpanel --profile LXDE 33 | user=%USER% 34 | environment=DISPLAY=":1",HOME="%HOME%",USER="%USER%" 35 | 36 | [program:pcmanfm] 37 | priority=15 38 | directory=%HOME% 39 | command=/usr/bin/pcmanfm --desktop --profile LXDE 40 | user=%USER% 41 | environment=DISPLAY=":1",HOME="%HOME%",USER="%USER%" 42 | {% endif %} 43 | 44 | {% if desktop == "lxqt" %} 45 | [group:x] 46 | programs=xvfb,wm,lxpanel,x11vnc,novnc 47 | 48 | [program:wm] 49 | priority=15 50 | command=/usr/bin/openbox 51 | environment=DISPLAY=":1",HOME="/root",USER="root" 52 | 53 | [program:lxpanel] 54 | priority=15 55 | directory=%HOME% 56 | command=/usr/bin/startlxqt 57 | user=%USER% 58 | environment=DISPLAY=":1",HOME="%HOME%",USER="%USER%" 59 | {% endif %} 60 | 61 | {% if desktop == "xfce4" %} 62 | [group:x] 63 | programs=xvfb,lxpanel,x11vnc,novnc 64 | 65 | [program:lxpanel] 66 | priority=15 67 | directory=%HOME% 68 | command=/usr/bin/startxfce4 69 | user=%USER% 70 | environment=DISPLAY=":1",HOME="%HOME%",USER="%USER%" 71 | {% endif %} 72 | 73 | [program:xvfb] 74 | priority=10 75 | command=/usr/local/bin/xvfb.sh 76 | stopsignal=KILL 77 | 78 | [program:x11vnc] 79 | priority=20 80 | command=x11vnc -display :1 -xkb -forever -shared -repeat -capslock 81 | 82 | [program:novnc] 83 | priority=25 84 | directory=/usr/local/lib/web/frontend/static/novnc 85 | command=bash /usr/local/lib/web/frontend/static/novnc/utils/launch.sh --listen 6081 86 | stopasgroup=true 87 | -------------------------------------------------------------------------------- /rootfs/root/.asoundrc: -------------------------------------------------------------------------------- 1 | pcm.loop { 2 | type plug 3 | slave.pcm "hw:Loopback,2,0" 4 | } 5 | -------------------------------------------------------------------------------- /rootfs/root/.gtkrc-2.0: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT! This file will be overwritten by LXAppearance. 2 | # Any customization should be done in ~/.gtkrc-2.0.mine instead. 3 | include "/usr/share/themes/Arc/gtk-2.0/gtkrc" 4 | 5 | gtk-theme-name="Arc" 6 | gtk-icon-theme-name="nuoveXT2" 7 | gtk-font-name="Sans 10" 8 | gtk-cursor-theme-name="DMZ-White" 9 | gtk-cursor-theme-size=18 10 | gtk-toolbar-style=GTK_TOOLBAR_BOTH_HORIZ 11 | gtk-toolbar-icon-size=GTK_ICON_SIZE_LARGE_TOOLBAR 12 | gtk-button-images=1 13 | gtk-menu-images=1 14 | gtk-enable-event-sounds=1 15 | gtk-enable-input-feedback-sounds=1 16 | gtk-xft-antialias=1 17 | gtk-xft-hinting=1 18 | gtk-xft-hintstyle="hintslight" 19 | gtk-xft-rgba="rgb" 20 | include "/root/.gtkrc-2.0.mine" 21 | -------------------------------------------------------------------------------- /rootfs/startup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -n "$VNC_PASSWORD" ]; then 4 | echo -n "$VNC_PASSWORD" > /.password1 5 | x11vnc -storepasswd $(cat /.password1) /.password2 6 | chmod 400 /.password* 7 | sed -i 's/^command=x11vnc.*/& -rfbauth \/.password2/' /etc/supervisor/conf.d/supervisord.conf 8 | export VNC_PASSWORD= 9 | fi 10 | 11 | if [ -n "$X11VNC_ARGS" ]; then 12 | sed -i "s/^command=x11vnc.*/& ${X11VNC_ARGS}/" /etc/supervisor/conf.d/supervisord.conf 13 | fi 14 | 15 | if [ -n "$OPENBOX_ARGS" ]; then 16 | sed -i "s#^command=/usr/bin/openbox\$#& ${OPENBOX_ARGS}#" /etc/supervisor/conf.d/supervisord.conf 17 | fi 18 | 19 | if [ -n "$RESOLUTION" ]; then 20 | sed -i "s/1024x768/$RESOLUTION/" /usr/local/bin/xvfb.sh 21 | fi 22 | 23 | USER=${USER:-root} 24 | HOME=/root 25 | if [ "$USER" != "root" ]; then 26 | echo "* enable custom user: $USER" 27 | useradd --create-home --shell /bin/bash --user-group --groups adm,sudo $USER 28 | if [ -z "$PASSWORD" ]; then 29 | echo " set default password to \"ubuntu\"" 30 | PASSWORD=ubuntu 31 | fi 32 | HOME=/home/$USER 33 | echo "$USER:$PASSWORD" | chpasswd 34 | cp -r /root/{.config,.gtkrc-2.0,.asoundrc} ${HOME} 35 | chown -R $USER:$USER ${HOME} 36 | [ -d "/dev/snd" ] && chgrp -R adm /dev/snd 37 | fi 38 | sed -i -e "s|%USER%|$USER|" -e "s|%HOME%|$HOME|" /etc/supervisor/conf.d/supervisord.conf 39 | 40 | # home folder 41 | if [ ! -x "$HOME/.config/pcmanfm/LXDE/" ]; then 42 | mkdir -p $HOME/.config/pcmanfm/LXDE/ 43 | ln -sf /usr/local/share/doro-lxde-wallpapers/desktop-items-0.conf $HOME/.config/pcmanfm/LXDE/ 44 | chown -R $USER:$USER $HOME 45 | fi 46 | 47 | # nginx workers 48 | sed -i 's|worker_processes .*|worker_processes 1;|' /etc/nginx/nginx.conf 49 | 50 | # nginx ssl 51 | if [ -n "$SSL_PORT" ] && [ -e "/etc/nginx/ssl/nginx.key" ]; then 52 | echo "* enable SSL" 53 | sed -i 's|#_SSL_PORT_#\(.*\)443\(.*\)|\1'$SSL_PORT'\2|' /etc/nginx/sites-enabled/default 54 | sed -i 's|#_SSL_PORT_#||' /etc/nginx/sites-enabled/default 55 | fi 56 | 57 | # nginx http base authentication 58 | if [ -n "$HTTP_PASSWORD" ]; then 59 | echo "* enable HTTP base authentication" 60 | htpasswd -bc /etc/nginx/.htpasswd $USER $HTTP_PASSWORD 61 | sed -i 's|#_HTTP_PASSWORD_#||' /etc/nginx/sites-enabled/default 62 | fi 63 | 64 | # dynamic prefix path renaming 65 | if [ -n "$RELATIVE_URL_ROOT" ]; then 66 | echo "* enable RELATIVE_URL_ROOT: $RELATIVE_URL_ROOT" 67 | sed -i 's|#_RELATIVE_URL_ROOT_||' /etc/nginx/sites-enabled/default 68 | sed -i 's|_RELATIVE_URL_ROOT_|'$RELATIVE_URL_ROOT'|' /etc/nginx/sites-enabled/default 69 | fi 70 | 71 | # clearup 72 | PASSWORD= 73 | HTTP_PASSWORD= 74 | 75 | exec /bin/tini -- supervisord -n -c /etc/supervisor/supervisord.conf 76 | -------------------------------------------------------------------------------- /rootfs/usr/local/bin/chromium-browser-sound.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z "$ALSADEV" ]; then 4 | zenity --error --text "To support audio, please read README.md and run container with --device /dev/snd -e ALSADEV=..." 5 | exit 1 6 | fi 7 | 8 | exec /usr/bin/google-chrome --no-sandbox --alsa-output-device="$ALSADEV" "$@" 9 | -------------------------------------------------------------------------------- /rootfs/usr/local/bin/xvfb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | exec /usr/bin/Xvfb :1 -screen 0 1024x768x24 4 | -------------------------------------------------------------------------------- /rootfs/usr/local/lib/web/backend/config/__init__.py: -------------------------------------------------------------------------------- 1 | class Default(object): 2 | DEBUG = True 3 | 4 | 5 | class Development(Default): 6 | PHASE = 'development' 7 | 8 | 9 | class Production(Default): 10 | PHASE = 'production' 11 | DEBUG = False 12 | -------------------------------------------------------------------------------- /rootfs/usr/local/lib/web/backend/log/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcwu/docker-ubuntu-vnc-desktop/e4922ce92f945fc482994b7a0fd95ca5de7295b3/rootfs/usr/local/lib/web/backend/log/__init__.py -------------------------------------------------------------------------------- /rootfs/usr/local/lib/web/backend/log/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import logging 4 | import logging.handlers 5 | 6 | 7 | # The terminal has 8 colors with codes from 0 to 7 8 | BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) 9 | 10 | # These are the sequences need to get colored ouput 11 | RESET_SEQ = "\033[0m" 12 | COLOR_SEQ = "\033[1;%dm" 13 | BOLD_SEQ = "\033[1m" 14 | 15 | # The background is set with 40 plus the number of the color, 16 | # and the foreground with 30 17 | COLORS = { 18 | 'WARNING': COLOR_SEQ % (30 + YELLOW) + 'WARN ' + RESET_SEQ, 19 | 'INFO': COLOR_SEQ % (30 + WHITE) + 'INFO ' + RESET_SEQ, 20 | 'DEBUG': COLOR_SEQ % (30 + BLUE) + 'DEBUG' + RESET_SEQ, 21 | 'CRITICAL': COLOR_SEQ % (30 + YELLOW) + 'CRITI' + RESET_SEQ, 22 | 'ERROR': COLOR_SEQ % (30 + RED) + 'ERROR' + RESET_SEQ, 23 | } 24 | 25 | 26 | class ColoredFormatter(logging.Formatter): 27 | def __init__(self, msg, use_color=True): 28 | logging.Formatter.__init__(self, msg) 29 | self.use_color = use_color 30 | 31 | def format(self, record): 32 | if self.use_color: 33 | record.levelname = COLORS.get(record.levelname, record.levelname) 34 | return logging.Formatter.format(self, record) 35 | 36 | 37 | class LoggingConfiguration(object): 38 | COLOR_FORMAT = "%(asctime)s" + \ 39 | " %(levelname)s %(message)s " + \ 40 | "(" + BOLD_SEQ + "%(filename)s" + RESET_SEQ + ":%(lineno)d)" 41 | NO_COLOR_FORMAT = "%(asctime)s %(levelname)s " + \ 42 | "%(message)s " + \ 43 | "(%(filename)s:%(lineno)d)" 44 | FILE_FORMAT = "%(asctime)s %(levelname)s " + \ 45 | "%(message)s " 46 | 47 | @classmethod 48 | def set(cls, log_level, log_filename, append=None, **kwargs): 49 | """ Configure a rotating file logging 50 | """ 51 | logger = logging.getLogger() 52 | logger.setLevel(log_level) 53 | 54 | COLOR_FORMAT = cls.COLOR_FORMAT 55 | NO_COLOR_FORMAT = cls.NO_COLOR_FORMAT 56 | FILE_FORMAT = cls.FILE_FORMAT 57 | if 'name' in kwargs: 58 | COLOR_FORMAT = COLOR_FORMAT.replace('%(threadName)-22s', 59 | '%-22s' % (kwargs['name'])) 60 | NO_COLOR_FORMAT = NO_COLOR_FORMAT.replace( 61 | '%(threadName)-22s', '%-22s' % (kwargs['name'])) 62 | FILE_FORMAT = FILE_FORMAT.replace( 63 | '%(threadName)-22s', '%s' % (kwargs['name'])) 64 | 65 | # Log to rotating file 66 | try: 67 | fh = logging.handlers.RotatingFileHandler( 68 | log_filename, 69 | mode='a+', 70 | backupCount=3 71 | ) 72 | fh.setFormatter(ColoredFormatter(FILE_FORMAT, False)) 73 | fh.setLevel(log_level) 74 | logger.addHandler(fh) 75 | if not append: 76 | # Create a new log file on every new 77 | fh.doRollover() 78 | except IOError as e: 79 | print('ignore to log to {}: {}'.format(log_filename, e)) 80 | 81 | # Log to sys.stderr using log level passed through command line 82 | if log_level != logging.NOTSET: 83 | log_handler = logging.StreamHandler(sys.stdout) 84 | if sys.platform.find('linux') >= 0: 85 | formatter = ColoredFormatter(COLOR_FORMAT) 86 | else: 87 | formatter = ColoredFormatter(NO_COLOR_FORMAT, False) 88 | log_handler.setFormatter(formatter) 89 | log_handler.setLevel(log_level) 90 | logger.addHandler(log_handler) 91 | -------------------------------------------------------------------------------- /rootfs/usr/local/lib/web/backend/requirements.txt: -------------------------------------------------------------------------------- 1 | backports.ssl-match-hostname==3.7.0.1 2 | certifi==2019.9.11 3 | chardet==3.0.4 4 | Click==7.0 5 | Flask==1.1.1 6 | Flask-Login==0.4.1 7 | gevent==1.4.0 8 | gevent-websocket==0.10.1 9 | greenlet==0.4.15 10 | idna==2.8 11 | itsdangerous==1.1.0 12 | Jinja2==2.11.3 13 | MarkupSafe==1.1.1 14 | meld3==2.0.0 15 | requests==2.22.0 16 | six==1.12.0 17 | urllib3==1.25.6 18 | websocket-client==0.47.0 19 | Werkzeug==0.16.0 20 | -------------------------------------------------------------------------------- /rootfs/usr/local/lib/web/backend/run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import ( 3 | absolute_import, division, print_function, with_statement 4 | ) 5 | import os 6 | import time 7 | import sys 8 | import subprocess 9 | from vnc.util import ignored 10 | 11 | 12 | def main(): 13 | def run_with_reloader(main_func, extra_files=None, interval=3): 14 | """Run the given function in an independent python interpreter.""" 15 | def find_files(directory="./"): 16 | for root, dirs, files in os.walk(directory): 17 | for basename in files: 18 | if basename.endswith('.py'): 19 | filename = os.path.join(root, basename) 20 | yield filename 21 | 22 | if os.environ.get('WERKZEUG_RUN_MAIN') == 'true': 23 | try: 24 | main_func() 25 | except KeyboardInterrupt: 26 | pass 27 | return 28 | 29 | proc = None 30 | try: 31 | while True: 32 | log.info('Restarting with reloader {} {}'.format( 33 | sys.executable, 34 | ' '.join(sys.argv)) 35 | ) 36 | args = [sys.executable] + sys.argv 37 | new_environ = os.environ.copy() 38 | new_environ['WERKZEUG_RUN_MAIN'] = 'true' 39 | 40 | proc = subprocess.Popen( 41 | args, 42 | env=new_environ, 43 | close_fds=True, 44 | preexec_fn=os.setsid 45 | ) 46 | mtimes = {} 47 | restart = False 48 | while not restart: 49 | for filename in find_files(): 50 | try: 51 | mtime = os.stat(filename).st_mtime 52 | except OSError: 53 | continue 54 | 55 | old_time = mtimes.get(filename) 56 | if old_time is None: 57 | mtimes[filename] = mtime 58 | continue 59 | elif mtime > old_time: 60 | log.info( 61 | 'Detected change in {}, reloading'.format( 62 | filename 63 | ) 64 | ) 65 | restart = True 66 | proc.terminate() 67 | break 68 | time.sleep(interval) 69 | except KeyboardInterrupt: 70 | pass 71 | finally: 72 | with ignored(Exception): 73 | proc.terminate() 74 | 75 | def run_server(): 76 | import socket 77 | from gevent.pywsgi import WSGIServer 78 | from vnc.app import app 79 | 80 | # websocket conflict: WebSocketHandler 81 | if DEBUG: 82 | # from werkzeug.debug import DebuggedApplication 83 | app.debug = True 84 | # app = DebuggedApplication(app, evalex=True) 85 | 86 | try: 87 | log.info('Listening on http://localhost:{}'.format(PORT)) 88 | http_server = WSGIServer(('localhost', PORT), app) 89 | http_server.serve_forever() 90 | # app.run(host='localhost', port=PORT) 91 | except socket.error as e: 92 | log.exception(e) 93 | except KeyboardInterrupt: 94 | pass 95 | finally: 96 | http_server.stop(timeout=10) 97 | log.info('shutdown gracefully') 98 | 99 | PORT = 6079 100 | DEBUG = False 101 | os.environ['CONFIG'] = 'config.Production' 102 | entrypoint = run_server 103 | if '--debug' in sys.argv: 104 | DEBUG = True 105 | os.environ['CONFIG'] = 'config.Development' 106 | entrypoint = lambda: run_with_reloader(run_server) 107 | 108 | # logging 109 | import logging 110 | from log.config import LoggingConfiguration 111 | LoggingConfiguration.set( 112 | logging.DEBUG if DEBUG else logging.INFO, 113 | '/var/log/web.log' 114 | ) 115 | logging.getLogger("werkzeug").setLevel(logging.WARNING) 116 | log = logging.getLogger('novnc2') 117 | 118 | entrypoint() 119 | 120 | 121 | if __name__ == "__main__": 122 | main() 123 | -------------------------------------------------------------------------------- /rootfs/usr/local/lib/web/backend/vnc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcwu/docker-ubuntu-vnc-desktop/e4922ce92f945fc482994b7a0fd95ca5de7295b3/rootfs/usr/local/lib/web/backend/vnc/__init__.py -------------------------------------------------------------------------------- /rootfs/usr/local/lib/web/backend/vnc/app.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | absolute_import, division, print_function, with_statement 3 | ) 4 | import re 5 | import os 6 | from flask import ( 7 | Flask, 8 | request, 9 | Response, 10 | jsonify, 11 | abort, 12 | ) 13 | from gevent import subprocess as gsp, spawn, sleep 14 | from geventwebsocket.exceptions import WebSocketError 15 | from .response import httperror 16 | from .util import ignored 17 | from .state import state 18 | from .log import log 19 | 20 | 21 | # Flask app 22 | app = Flask('novnc2') 23 | app.config.from_object('config.Default') 24 | app.config.from_object(os.environ.get('CONFIG') or 'config.Development') 25 | 26 | 27 | @app.route('/api/state') 28 | @httperror 29 | def apistate(): 30 | state.wait(int(request.args.get('id', -1)), 30) 31 | state.switch_video(request.args.get('video', 'false') == 'true') 32 | mystate = state.to_dict() 33 | return jsonify({ 34 | 'code': 200, 35 | 'data': mystate, 36 | }) 37 | 38 | 39 | @app.route('/api/health') 40 | def apihealth(): 41 | if state.health: 42 | return 'success' 43 | abort(503, 'unhealthy') 44 | 45 | 46 | @app.route('/api/reset') 47 | def reset(): 48 | if 'w' in request.args and 'h' in request.args: 49 | args = { 50 | 'w': int(request.args.get('w')), 51 | 'h': int(request.args.get('h')), 52 | } 53 | state.set_size(args['w'], args['h']) 54 | 55 | state.apply_and_restart() 56 | 57 | # check all running 58 | for i in range(40): 59 | if state.health: 60 | break 61 | sleep(1) 62 | log.info('wait services is ready...') 63 | else: 64 | return jsonify({ 65 | 'code': 500, 66 | 'errorMessage': 'service is not ready, please restart container' 67 | }) 68 | return jsonify({'code': 200}) 69 | 70 | 71 | @app.route('/resize') 72 | @httperror 73 | def apiresize(): 74 | state.reset_size() 75 | return '' 76 | 77 | 78 | @app.route('/api/live.flv') 79 | @httperror 80 | def liveflv(): 81 | def generate(): 82 | xenvs = { 83 | 'DISPLAY': ':1', 84 | } 85 | bufsize = 1024 * 1 86 | framerate = 20 87 | 88 | # sound 89 | sound_cmd_input = [] 90 | sound_cmd_parameters = [] 91 | zero_latency_make_sound_not_good = [ 92 | '-tune', 'zerolatency', 93 | ] 94 | 95 | xenvs['X_WIDTH'] = state.w 96 | xenvs['X_HEIGHT'] = state.h 97 | xenvs['X_WIDTH'] -= state.w % 2 98 | xenvs['X_HEIGHT'] -= state.h % 2 99 | 100 | pixels_count = xenvs['X_WIDTH'] * xenvs['X_HEIGHT'] 101 | # factor (720p) 102 | # 383: 2400k 103 | # 300: 3000k 104 | # 230: 4000k 105 | factor = 265 106 | maxbitrate_cmd = [ 107 | '-maxrate', str(int(pixels_count / factor)) + 'k', 108 | '-bufsize', str(int(pixels_count / factor / 3)) + 'k' 109 | ] 110 | 111 | # TODO move to global 112 | # get default source 113 | sound_cmd_input = [ 114 | '-f', 'alsa', 115 | '-i', 'hw:2,1', 116 | ] 117 | sound_cmd_parameters = [ 118 | '-ar', '44100', 119 | '-c:a', 'mp3', 120 | ] 121 | # flv.js report error if enabling hw acceleration 122 | # hwaccel_dev = ['-vaapi_device', '/dev/dri/renderD128'] 123 | # hwaccel_if = ['-vf', 'format=nv12,hwupload'] 124 | # vcodec = 'h264_vaapi' 125 | hwaccel_dev = [] 126 | hwaccel_if = [] 127 | vcodec = 'libx264' 128 | # zero_latency_make_sound_not_good = [] 129 | # sound_cmd_parameters = [] 130 | # sound_cmd_input = [] 131 | cmd = ['/usr/local/ffmpeg/ffmpeg'] + sound_cmd_input + hwaccel_dev + [ 132 | '-video_size', '{X_WIDTH}x{X_HEIGHT}'.format(**xenvs), 133 | '-framerate', '{}'.format(framerate), 134 | '-f', 'x11grab', '-draw_mouse', '1', 135 | '-i', '{DISPLAY}'.format(**xenvs), 136 | ] + hwaccel_if + [ 137 | '-r', '{}'.format(framerate), 138 | '-g', '{}'.format(framerate), 139 | '-flags:v', '+global_header', 140 | '-vcodec', vcodec, 141 | '-preset', 'ultrafast', 142 | '-b_strategy', '0', 143 | '-pix_fmt', 'yuv420p', 144 | '-bsf:v', 'dump_extra=freq=e', 145 | ] + maxbitrate_cmd \ 146 | + sound_cmd_parameters + zero_latency_make_sound_not_good + [ 147 | '-f', 'flv', 'pipe:1', 148 | ] 149 | log.info('command: ' + ' '.join(cmd)) 150 | pobj = gsp.Popen( 151 | cmd, 152 | stdout=gsp.PIPE, 153 | stderr=gsp.PIPE, 154 | env={k: str(v) for k, v in xenvs.items()}, 155 | ) 156 | 157 | def readerr(f): 158 | reobj = re.compile(r'bitrate=(\S+)') 159 | global av_bitrate 160 | try: 161 | while True: 162 | buf = f.read(bufsize) 163 | if len(buf) == 0: 164 | break 165 | patterns = reobj.findall(buf.decode('utf-8', 'ignore')) 166 | if len(patterns) > 0: 167 | av_bitrate = patterns[-1] 168 | # log.info(str(buf)) 169 | except Exception as e: 170 | log.exception(e) 171 | 172 | preaderr = None 173 | try: 174 | preaderr = spawn(readerr, pobj.stderr) 175 | try: 176 | while True: 177 | buf = pobj.stdout.read(bufsize) 178 | if len(buf) == 0: 179 | break 180 | # ws.send(buf) 181 | yield buf 182 | except WebSocketError: 183 | pass 184 | except Exception as e: 185 | log.exception(e) 186 | finally: 187 | with ignored(Exception): 188 | pobj.kill() 189 | preaderr.join() 190 | except Exception as e: 191 | log.exception(e) 192 | finally: 193 | log.info('exiting') 194 | with ignored(Exception): 195 | pobj.kill() 196 | with ignored(Exception): 197 | preaderr.kill() 198 | log.info('exited') 199 | return Response(generate(), mimetype='video/x-flv') 200 | 201 | 202 | if __name__ == '__main__': 203 | app.run(host=app.config['ADDRESS'], port=app.config['PORT']) 204 | -------------------------------------------------------------------------------- /rootfs/usr/local/lib/web/backend/vnc/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | log = logging.getLogger('novnc2') 3 | -------------------------------------------------------------------------------- /rootfs/usr/local/lib/web/backend/vnc/response.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | absolute_import, division, print_function, with_statement 3 | ) 4 | from functools import wraps 5 | import logging 6 | from flask import jsonify 7 | 8 | 9 | log = logging.getLogger() 10 | 11 | 12 | class PermissionDenied(Exception): 13 | pass 14 | 15 | 16 | class BadRequest(Exception): 17 | pass 18 | 19 | 20 | def httperror(f): 21 | @wraps(f) 22 | def func(*args, **kwargs): 23 | result = { 24 | 'code': 400, 25 | 'errorMessage': '', 26 | } 27 | try: 28 | return f(*args, **kwargs) 29 | except PermissionDenied as e: 30 | result['code'] = 403 31 | result['errorMessage'] = str(e) 32 | except BadRequest as e: 33 | result['code'] = 400 34 | result['errorMessage'] = str(e) 35 | except Exception as e: 36 | logging.exception(e) 37 | result['code'] = 500 38 | result['errorMessage'] = str(e) 39 | return jsonify(result) 40 | return func 41 | -------------------------------------------------------------------------------- /rootfs/usr/local/lib/web/backend/vnc/state.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | absolute_import, division, print_function, with_statement 3 | ) 4 | from os import environ 5 | from gevent.event import Event 6 | from gevent import subprocess as gsp 7 | from re import search as research 8 | from .log import log 9 | 10 | 11 | class State(object): 12 | def __init__(self): 13 | self._eid = 0 14 | self._event = Event() 15 | self._w = self._h = self._health = None 16 | self.size_changed_count = 0 17 | 18 | def wait(self, eid, timeout=5): 19 | if eid < self._eid: 20 | return 21 | self._event.clear() 22 | self._event.wait(timeout) 23 | return self._eid 24 | 25 | def notify(self): 26 | self._eid += 1 27 | self._event.set() 28 | 29 | def _update_health(self): 30 | health = True 31 | output = gsp.check_output([ 32 | 'supervisorctl', '-c', '/etc/supervisor/supervisord.conf', 33 | 'status' 34 | ], encoding='UTF-8') 35 | for line in output.strip().split('\n'): 36 | if not line.startswith('web') and line.find('RUNNING') < 0: 37 | health = False 38 | break 39 | if self._health != health: 40 | self._health = health 41 | self.notify() 42 | return self._health 43 | 44 | def to_dict(self): 45 | self._update_health() 46 | 47 | state = { 48 | 'id': self._eid, 49 | 'config': { 50 | 'fixedResolution': 'RESOLUTION' in environ, 51 | 'sizeChangedCount': self.size_changed_count 52 | } 53 | } 54 | 55 | self._update_size() 56 | state.update({ 57 | 'width': self.w, 58 | 'height': self.h, 59 | }) 60 | 61 | return state 62 | 63 | def set_size(self, w, h): 64 | gsp.check_call(( 65 | 'sed -i \'s#' 66 | '^exec /usr/bin/Xvfb.*$' 67 | '#' 68 | 'exec /usr/bin/Xvfb :1 -screen 0 {}x{}x24' 69 | '#\' /usr/local/bin/xvfb.sh' 70 | ).format(w, h), shell=True) 71 | self.size_changed_count += 1 72 | 73 | def apply_and_restart(self): 74 | gsp.check_call([ 75 | 'supervisorctl', '-c', '/etc/supervisor/supervisord.conf', 76 | 'restart', 'x:' 77 | ]) 78 | self._w = self._h = self._health = None 79 | self.notify() 80 | 81 | def switch_video(self, onoff): 82 | xenvs = { 83 | 'DISPLAY': ':1', 84 | } 85 | try: 86 | cmd = 'nofb' if onoff else 'fb' 87 | gsp.check_output(['x11vnc', '-remote', cmd], env=xenvs) 88 | except gsp.CalledProcessError as e: 89 | log.warn('failed to set x11vnc fb: ' + str(e)) 90 | 91 | def _update_size(self): 92 | if self._w is not None and self._h is not None: 93 | return 94 | xenvs = { 95 | 'DISPLAY': ':1', 96 | } 97 | try: 98 | output = gsp.check_output([ 99 | 'x11vnc', '-query', 'dpy_x,dpy_y' 100 | ], env=xenvs).decode('utf-8') 101 | mobj = research(r'dpy_x:(\d+).*dpy_y:(\d+)', output) 102 | if mobj is not None: 103 | w, h = int(mobj.group(1)), int(mobj.group(2)) 104 | changed = False 105 | if self._w != w: 106 | changed = True 107 | self._w = w 108 | if self._h != h: 109 | changed = True 110 | self._h = h 111 | if changed: 112 | self.notify() 113 | except gsp.CalledProcessError as e: 114 | log.warn('failed to get dispaly size: ' + str(e)) 115 | 116 | def reset_size(self): 117 | self.size_changed_count = 0 118 | 119 | @property 120 | def w(self): 121 | return self._w 122 | 123 | @property 124 | def h(self): 125 | return self._h 126 | 127 | @property 128 | def health(self): 129 | self._update_health() 130 | return self._health 131 | 132 | 133 | state = State() 134 | -------------------------------------------------------------------------------- /rootfs/usr/local/lib/web/backend/vnc/util.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | absolute_import, division, print_function, with_statement 3 | ) 4 | from contextlib import contextmanager 5 | from gevent import GreenletExit 6 | 7 | 8 | @contextmanager 9 | def ignored(*exceptions): 10 | try: 11 | yield 12 | except GreenletExit as e: 13 | raise e 14 | except exceptions: 15 | pass 16 | -------------------------------------------------------------------------------- /rootfs/usr/local/lib/web/frontend/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcwu/docker-ubuntu-vnc-desktop/e4922ce92f945fc482994b7a0fd95ca5de7295b3/rootfs/usr/local/lib/web/frontend/.gitkeep -------------------------------------------------------------------------------- /rootfs/usr/local/share/doro-lxde-wallpapers/bg1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcwu/docker-ubuntu-vnc-desktop/e4922ce92f945fc482994b7a0fd95ca5de7295b3/rootfs/usr/local/share/doro-lxde-wallpapers/bg1.jpg -------------------------------------------------------------------------------- /rootfs/usr/local/share/doro-lxde-wallpapers/bg2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcwu/docker-ubuntu-vnc-desktop/e4922ce92f945fc482994b7a0fd95ca5de7295b3/rootfs/usr/local/share/doro-lxde-wallpapers/bg2.jpg -------------------------------------------------------------------------------- /rootfs/usr/local/share/doro-lxde-wallpapers/bg3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcwu/docker-ubuntu-vnc-desktop/e4922ce92f945fc482994b7a0fd95ca5de7295b3/rootfs/usr/local/share/doro-lxde-wallpapers/bg3.jpg -------------------------------------------------------------------------------- /rootfs/usr/local/share/doro-lxde-wallpapers/bg4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcwu/docker-ubuntu-vnc-desktop/e4922ce92f945fc482994b7a0fd95ca5de7295b3/rootfs/usr/local/share/doro-lxde-wallpapers/bg4.jpg -------------------------------------------------------------------------------- /rootfs/usr/local/share/doro-lxde-wallpapers/desktop-items-0.conf: -------------------------------------------------------------------------------- 1 | [*] 2 | wallpaper_mode=stretch 3 | wallpaper_common=0 4 | wallpapers_configured=4 5 | wallpaper0=/usr/local/share/doro-lxde-wallpapers/bg1.jpg 6 | wallpaper1=/usr/local/share/doro-lxde-wallpapers/bg2.jpg 7 | wallpaper2=/usr/local/share/doro-lxde-wallpapers/bg3.jpg 8 | wallpaper3=/usr/local/share/doro-lxde-wallpapers/bg4.jpg 9 | desktop_bg=#000000 10 | desktop_fg=#ffffff 11 | desktop_shadow=#000000 12 | desktop_font=Sans 12 13 | show_wm_menu=0 14 | sort=mtime;ascending;mingle; 15 | show_documents=0 16 | show_trash=0 17 | show_mounts=0 18 | 19 | -------------------------------------------------------------------------------- /rootfs/usr/share/applications/chromium-browser-sound.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.0 3 | Name=Google Chrome Sound 4 | # Only KDE 4 seems to use GenericName, so we reuse the KDE strings. 5 | # From Ubuntu's language-pack-kde-XX-base packages, version 9.04-20090413. 6 | GenericName=Web Browser 7 | GenericName[ar]=متصفح الشبكة 8 | GenericName[bg]=Уеб браузър 9 | GenericName[ca]=Navegador web 10 | GenericName[cs]=WWW prohlížeč 11 | GenericName[da]=Browser 12 | GenericName[de]=Web-Browser 13 | GenericName[el]=Περιηγητής ιστού 14 | GenericName[en_GB]=Web Browser 15 | GenericName[es]=Navegador web 16 | GenericName[et]=Veebibrauser 17 | GenericName[fi]=WWW-selain 18 | GenericName[fr]=Navigateur Web 19 | GenericName[gu]=વેબ બ્રાઉઝર 20 | GenericName[he]=דפדפן אינטרנט 21 | GenericName[hi]=वेब ब्राउज़र 22 | GenericName[hu]=Webböngésző 23 | GenericName[it]=Browser Web 24 | GenericName[ja]=ウェブブラウザ 25 | GenericName[kn]=ಜಾಲ ವೀಕ್ಷಕ 26 | GenericName[ko]=웹 브라우저 27 | GenericName[lt]=Žiniatinklio naršyklė 28 | GenericName[lv]=Tīmekļa pārlūks 29 | GenericName[ml]=വെബ് ബ്രൌസര്‍ 30 | GenericName[mr]=वेब ब्राऊजर 31 | GenericName[nb]=Nettleser 32 | GenericName[nl]=Webbrowser 33 | GenericName[pl]=Przeglądarka WWW 34 | GenericName[pt]=Navegador Web 35 | GenericName[pt_BR]=Navegador da Internet 36 | GenericName[ro]=Navigator de Internet 37 | GenericName[ru]=Веб-браузер 38 | GenericName[sl]=Spletni brskalnik 39 | GenericName[sv]=Webbläsare 40 | GenericName[ta]=இணைய உலாவி 41 | GenericName[th]=เว็บเบราว์เซอร์ 42 | GenericName[tr]=Web Tarayıcı 43 | GenericName[uk]=Навігатор Тенет 44 | GenericName[zh_CN]=网页浏览器 45 | GenericName[zh_HK]=網頁瀏覽器 46 | GenericName[zh_TW]=網頁瀏覽器 47 | # Not translated in KDE, from Epiphany 2.26.1-0ubuntu1. 48 | GenericName[bn]=ওয়েব ব্রাউজার 49 | GenericName[fil]=Web Browser 50 | GenericName[hr]=Web preglednik 51 | GenericName[id]=Browser Web 52 | GenericName[or]=ଓ୍ବେବ ବ୍ରାଉଜର 53 | GenericName[sk]=WWW prehliadač 54 | GenericName[sr]=Интернет прегледник 55 | GenericName[te]=మహాతల అన్వేషి 56 | GenericName[vi]=Bộ duyệt Web 57 | # Gnome and KDE 3 uses Comment. 58 | Comment=Access the Internet 59 | Comment[ar]=الدخول إلى الإنترنت 60 | Comment[bg]=Достъп до интернет 61 | Comment[bn]=ইন্টারনেটটি অ্যাক্সেস করুন 62 | Comment[ca]=Accedeix a Internet 63 | Comment[cs]=Přístup k internetu 64 | Comment[da]=Få adgang til internettet 65 | Comment[de]=Internetzugriff 66 | Comment[el]=Πρόσβαση στο Διαδίκτυο 67 | Comment[en_GB]=Access the Internet 68 | Comment[es]=Accede a Internet. 69 | Comment[et]=Pääs Internetti 70 | Comment[fi]=Käytä internetiä 71 | Comment[fil]=I-access ang Internet 72 | Comment[fr]=Accéder à Internet 73 | Comment[gu]=ઇંટરનેટ ઍક્સેસ કરો 74 | Comment[he]=גישה אל האינטרנט 75 | Comment[hi]=इंटरनेट तक पहुंच स्थापित करें 76 | Comment[hr]=Pristup Internetu 77 | Comment[hu]=Internetelérés 78 | Comment[id]=Akses Internet 79 | Comment[it]=Accesso a Internet 80 | Comment[ja]=インターネットにアクセス 81 | Comment[kn]=ಇಂಟರ್ನೆಟ್ ಅನ್ನು ಪ್ರವೇಶಿಸಿ 82 | Comment[ko]=인터넷 연결 83 | Comment[lt]=Interneto prieiga 84 | Comment[lv]=Piekļūt internetam 85 | Comment[ml]=ഇന്റര്‍‌നെറ്റ് ആക്‌സസ് ചെയ്യുക 86 | Comment[mr]=इंटरनेटमध्ये प्रवेश करा 87 | Comment[nb]=Gå til Internett 88 | Comment[nl]=Verbinding maken met internet 89 | Comment[or]=ଇଣ୍ଟର୍ନେଟ୍ ପ୍ରବେଶ କରନ୍ତୁ 90 | Comment[pl]=Skorzystaj z internetu 91 | Comment[pt]=Aceder à Internet 92 | Comment[pt_BR]=Acessar a internet 93 | Comment[ro]=Accesaţi Internetul 94 | Comment[ru]=Доступ в Интернет 95 | Comment[sk]=Prístup do siete Internet 96 | Comment[sl]=Dostop do interneta 97 | Comment[sr]=Приступите Интернету 98 | Comment[sv]=Gå ut på Internet 99 | Comment[ta]=இணையத்தை அணுகுதல் 100 | Comment[te]=ఇంటర్నెట్‌ను ఆక్సెస్ చెయ్యండి 101 | Comment[th]=เข้าถึงอินเทอร์เน็ต 102 | Comment[tr]=İnternet'e erişin 103 | Comment[uk]=Доступ до Інтернету 104 | Comment[vi]=Truy cập Internet 105 | Comment[zh_CN]=访问互联网 106 | Comment[zh_HK]=連線到網際網路 107 | Comment[zh_TW]=連線到網際網路 108 | Exec=/usr/local/bin/chromium-browser-sound.sh %U 109 | StartupNotify=true 110 | Terminal=false 111 | Icon=google-chrome 112 | Type=Application 113 | Categories=Network;WebBrowser; 114 | MimeType=application/pdf;application/rdf+xml;application/rss+xml;application/xhtml+xml;application/xhtml_xml;application/xml;image/gif;image/jpeg;image/png;image/webp;text/html;text/xml;x-scheme-handler/ftp;x-scheme-handler/http;x-scheme-handler/https; 115 | Actions=new-window;new-private-window; 116 | 117 | [Desktop Action new-window] 118 | Name=New Window 119 | Name[am]=አዲስ መስኮት 120 | Name[ar]=نافذة جديدة 121 | Name[bg]=Нов прозорец 122 | Name[bn]=নতুন উইন্ডো 123 | Name[ca]=Finestra nova 124 | Name[cs]=Nové okno 125 | Name[da]=Nyt vindue 126 | Name[de]=Neues Fenster 127 | Name[el]=Νέο Παράθυρο 128 | Name[en_GB]=New Window 129 | Name[es]=Nueva ventana 130 | Name[et]=Uus aken 131 | Name[fa]=پنجره جدید 132 | Name[fi]=Uusi ikkuna 133 | Name[fil]=New Window 134 | Name[fr]=Nouvelle fenêtre 135 | Name[gu]=નવી વિંડો 136 | Name[hi]=नई विंडो 137 | Name[hr]=Novi prozor 138 | Name[hu]=Új ablak 139 | Name[id]=Jendela Baru 140 | Name[it]=Nuova finestra 141 | Name[iw]=חלון חדש 142 | Name[ja]=新規ウインドウ 143 | Name[kn]=ಹೊಸ ವಿಂಡೊ 144 | Name[ko]=새 창 145 | Name[lt]=Naujas langas 146 | Name[lv]=Jauns logs 147 | Name[ml]=പുതിയ വിന്‍ഡോ 148 | Name[mr]=नवीन विंडो 149 | Name[nl]=Nieuw venster 150 | Name[no]=Nytt vindu 151 | Name[pl]=Nowe okno 152 | Name[pt]=Nova janela 153 | Name[pt_BR]=Nova janela 154 | Name[ro]=Fereastră nouă 155 | Name[ru]=Новое окно 156 | Name[sk]=Nové okno 157 | Name[sl]=Novo okno 158 | Name[sr]=Нови прозор 159 | Name[sv]=Nytt fönster 160 | Name[sw]=Dirisha Jipya 161 | Name[ta]=புதிய சாளரம் 162 | Name[te]=క్రొత్త విండో 163 | Name[th]=หน้าต่างใหม่ 164 | Name[tr]=Yeni Pencere 165 | Name[uk]=Нове вікно 166 | Name[vi]=Cửa sổ Mới 167 | Name[zh_CN]=新建窗口 168 | Name[zh_TW]=開新視窗 169 | Exec=/usr/bin/google-chrome-stable 170 | 171 | [Desktop Action new-private-window] 172 | Name=New Incognito Window 173 | Name[ar]=نافذة جديدة للتصفح المتخفي 174 | Name[bg]=Нов прозорец „инкогнито“ 175 | Name[bn]=নতুন ছদ্মবেশী উইন্ডো 176 | Name[ca]=Finestra d'incògnit nova 177 | Name[cs]=Nové anonymní okno 178 | Name[da]=Nyt inkognitovindue 179 | Name[de]=Neues Inkognito-Fenster 180 | Name[el]=Νέο παράθυρο για ανώνυμη περιήγηση 181 | Name[en_GB]=New Incognito window 182 | Name[es]=Nueva ventana de incógnito 183 | Name[et]=Uus inkognito aken 184 | Name[fa]=پنجره جدید حالت ناشناس 185 | Name[fi]=Uusi incognito-ikkuna 186 | Name[fil]=Bagong Incognito window 187 | Name[fr]=Nouvelle fenêtre de navigation privée 188 | Name[gu]=નવી છુપી વિંડો 189 | Name[hi]=नई गुप्त विंडो 190 | Name[hr]=Novi anoniman prozor 191 | Name[hu]=Új Inkognitóablak 192 | Name[id]=Jendela Penyamaran baru 193 | Name[it]=Nuova finestra di navigazione in incognito 194 | Name[iw]=חלון חדש לגלישה בסתר 195 | Name[ja]=新しいシークレット ウィンドウ 196 | Name[kn]=ಹೊಸ ಅಜ್ಞಾತ ವಿಂಡೋ 197 | Name[ko]=새 시크릿 창 198 | Name[lt]=Naujas inkognito langas 199 | Name[lv]=Jauns inkognito režīma logs 200 | Name[ml]=പുതിയ വേഷ പ്രച്ഛന്ന വിന്‍ഡോ 201 | Name[mr]=नवीन गुप्त विंडो 202 | Name[nl]=Nieuw incognitovenster 203 | Name[no]=Nytt inkognitovindu 204 | Name[pl]=Nowe okno incognito 205 | Name[pt]=Nova janela de navegação anónima 206 | Name[pt_BR]=Nova janela anônima 207 | Name[ro]=Fereastră nouă incognito 208 | Name[ru]=Новое окно в режиме инкогнито 209 | Name[sk]=Nové okno inkognito 210 | Name[sl]=Novo okno brez beleženja zgodovine 211 | Name[sr]=Нови прозор за прегледање без архивирања 212 | Name[sv]=Nytt inkognitofönster 213 | Name[ta]=புதிய மறைநிலைச் சாளரம் 214 | Name[te]=క్రొత్త అజ్ఞాత విండో 215 | Name[th]=หน้าต่างใหม่ที่ไม่ระบุตัวตน 216 | Name[tr]=Yeni Gizli pencere 217 | Name[uk]=Нове вікно в режимі анонімного перегляду 218 | Name[vi]=Cửa sổ ẩn danh mới 219 | Name[zh_CN]=新建隐身窗口 220 | Name[zh_TW]=新增無痕式視窗 221 | Exec=/usr/bin/google-chrome-stable --incognito 222 | -------------------------------------------------------------------------------- /screenshots/lxde.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcwu/docker-ubuntu-vnc-desktop/e4922ce92f945fc482994b7a0fd95ca5de7295b3/screenshots/lxde.png -------------------------------------------------------------------------------- /web/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"], 12 | "env": { 13 | "test": { 14 | "presets": ["env", "stage-2"] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /web/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /web/.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | /test/unit/coverage/ 6 | -------------------------------------------------------------------------------- /web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parserOptions: { 6 | parser: 'babel-eslint' 7 | }, 8 | env: { 9 | browser: true, 10 | }, 11 | extends: [ 12 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 13 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 14 | 'plugin:vue/essential', 15 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 16 | 'standard' 17 | ], 18 | // required to lint *.vue files 19 | plugins: [ 20 | 'vue' 21 | ], 22 | // add your custom rules here 23 | rules: { 24 | // allow async-await 25 | 'generator-star-spacing': 'off', 26 | // allow debugger during development 27 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | /test/unit/coverage/ 8 | /test/e2e/reports/ 9 | selenium-debug.log 10 | 11 | # Editor directories and files 12 | .idea 13 | .vscode 14 | *.suo 15 | *.ntvs* 16 | *.njsproj 17 | *.sln 18 | -------------------------------------------------------------------------------- /web/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # novnc2 2 | 3 | > extended novnc 4 | 5 | ## Build Setup 6 | 7 | ``` bash 8 | # install dependencies 9 | npm install 10 | 11 | # serve with hot reload at localhost:8080 12 | npm run dev 13 | 14 | # build for production with minification 15 | npm run build 16 | 17 | # build for production and view the bundle analyzer report 18 | npm run build --report 19 | 20 | # run unit tests 21 | npm run unit 22 | 23 | # run e2e tests 24 | npm run e2e 25 | 26 | # run all tests 27 | npm test 28 | ``` 29 | 30 | For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). 31 | -------------------------------------------------------------------------------- /web/build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, (err, stats) => { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /web/build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | 7 | function exec (cmd) { 8 | return require('child_process').execSync(cmd).toString().trim() 9 | } 10 | 11 | const versionRequirements = [ 12 | { 13 | name: 'node', 14 | currentVersion: semver.clean(process.version), 15 | versionRequirement: packageConfig.engines.node 16 | } 17 | ] 18 | 19 | if (shell.which('npm')) { 20 | versionRequirements.push({ 21 | name: 'npm', 22 | currentVersion: exec('npm --version'), 23 | versionRequirement: packageConfig.engines.npm 24 | }) 25 | } 26 | 27 | module.exports = function () { 28 | const warnings = [] 29 | 30 | for (let i = 0; i < versionRequirements.length; i++) { 31 | const mod = versionRequirements[i] 32 | 33 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 34 | warnings.push(mod.name + ': ' + 35 | chalk.red(mod.currentVersion) + ' should be ' + 36 | chalk.green(mod.versionRequirement) 37 | ) 38 | } 39 | } 40 | 41 | if (warnings.length) { 42 | console.log('') 43 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 44 | console.log() 45 | 46 | for (let i = 0; i < warnings.length; i++) { 47 | const warning = warnings[i] 48 | console.log(' ' + warning) 49 | } 50 | 51 | console.log() 52 | process.exit(1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /web/build/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcwu/docker-ubuntu-vnc-desktop/e4922ce92f945fc482994b7a0fd95ca5de7295b3/web/build/logo.png -------------------------------------------------------------------------------- /web/build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | const packageConfig = require('../package.json') 6 | 7 | exports.assetsPath = function (_path) { 8 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 9 | ? config.build.assetsSubDirectory 10 | : config.dev.assetsSubDirectory 11 | 12 | return path.posix.join(assetsSubDirectory, _path) 13 | } 14 | 15 | exports.cssLoaders = function (options) { 16 | options = options || {} 17 | 18 | const cssLoader = { 19 | loader: 'css-loader', 20 | options: { 21 | sourceMap: options.sourceMap 22 | } 23 | } 24 | 25 | const postcssLoader = { 26 | loader: 'postcss-loader', 27 | options: { 28 | sourceMap: options.sourceMap 29 | } 30 | } 31 | 32 | // generate loader string to be used with extract text plugin 33 | function generateLoaders (loader, loaderOptions) { 34 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] 35 | 36 | if (loader) { 37 | loaders.push({ 38 | loader: loader + '-loader', 39 | options: Object.assign({}, loaderOptions, { 40 | sourceMap: options.sourceMap 41 | }) 42 | }) 43 | } 44 | 45 | // Extract CSS when that option is specified 46 | // (which is the case during production build) 47 | if (options.extract) { 48 | return ExtractTextPlugin.extract({ 49 | use: loaders, 50 | fallback: 'vue-style-loader' 51 | }) 52 | } else { 53 | return ['vue-style-loader'].concat(loaders) 54 | } 55 | } 56 | 57 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 58 | return { 59 | css: generateLoaders(), 60 | postcss: generateLoaders(), 61 | less: generateLoaders('less'), 62 | sass: generateLoaders('sass', { indentedSyntax: true }), 63 | scss: generateLoaders('sass'), 64 | stylus: generateLoaders('stylus'), 65 | styl: generateLoaders('stylus') 66 | } 67 | } 68 | 69 | // Generate loaders for standalone style files (outside of .vue) 70 | exports.styleLoaders = function (options) { 71 | const output = [] 72 | const loaders = exports.cssLoaders(options) 73 | 74 | for (const extension in loaders) { 75 | const loader = loaders[extension] 76 | output.push({ 77 | test: new RegExp('\\.' + extension + '$'), 78 | use: loader 79 | }) 80 | } 81 | 82 | return output 83 | } 84 | 85 | exports.createNotifierCallback = () => { 86 | const notifier = require('node-notifier') 87 | 88 | return (severity, errors) => { 89 | if (severity !== 'error') return 90 | 91 | const error = errors[0] 92 | const filename = error.file && error.file.split('!').pop() 93 | 94 | notifier.notify({ 95 | title: packageConfig.name, 96 | message: severity + ': ' + error.name, 97 | subtitle: filename || '', 98 | icon: path.join(__dirname, 'logo.png') 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /web/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | const sourceMapEnabled = isProduction 6 | ? config.build.productionSourceMap 7 | : config.dev.cssSourceMap 8 | 9 | module.exports = { 10 | loaders: utils.cssLoaders({ 11 | sourceMap: sourceMapEnabled, 12 | extract: isProduction 13 | }), 14 | cssSourceMap: sourceMapEnabled, 15 | cacheBusting: config.dev.cacheBusting, 16 | transformToRequire: { 17 | video: ['src', 'poster'], 18 | source: 'src', 19 | img: 'src', 20 | image: 'xlink:href' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /web/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const config = require('../config') 5 | const vueLoaderConfig = require('./vue-loader.conf') 6 | 7 | function resolve (dir) { 8 | return path.join(__dirname, '..', dir) 9 | } 10 | 11 | const createLintingRule = () => ({ 12 | test: /\.(js|vue)$/, 13 | loader: 'eslint-loader', 14 | enforce: 'pre', 15 | include: [resolve('src'), resolve('test')], 16 | options: { 17 | formatter: require('eslint-friendly-formatter'), 18 | emitWarning: !config.dev.showEslintErrorsInOverlay 19 | } 20 | }) 21 | 22 | module.exports = { 23 | context: path.resolve(__dirname, '../'), 24 | entry: { 25 | app: './src/main.js' 26 | }, 27 | output: { 28 | path: config.build.assetsRoot, 29 | filename: '[name].js', 30 | publicPath: process.env.NODE_ENV === 'production' 31 | ? config.build.assetsPublicPath 32 | : config.dev.assetsPublicPath 33 | }, 34 | resolve: { 35 | extensions: ['.js', '.vue', '.json'], 36 | alias: { 37 | 'vue$': 'vue/dist/vue.esm.js', 38 | '@': resolve('src'), 39 | } 40 | }, 41 | module: { 42 | rules: [ 43 | ...(config.dev.useEslint ? [createLintingRule()] : []), 44 | { 45 | test: /\.vue$/, 46 | loader: 'vue-loader', 47 | options: vueLoaderConfig 48 | }, 49 | { 50 | test: /\.js$/, 51 | loader: 'babel-loader', 52 | include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] 53 | }, 54 | { 55 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 56 | loader: 'url-loader', 57 | options: { 58 | limit: 10000, 59 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 60 | } 61 | }, 62 | { 63 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 64 | loader: 'url-loader', 65 | options: { 66 | limit: 10000, 67 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 68 | } 69 | }, 70 | { 71 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 72 | loader: 'url-loader', 73 | options: { 74 | limit: 10000, 75 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 76 | } 77 | } 78 | ] 79 | }, 80 | node: { 81 | // prevent webpack from injecting useless setImmediate polyfill because Vue 82 | // source contains it (although only uses it if it's native). 83 | setImmediate: false, 84 | // prevent webpack from injecting mocks to Node native modules 85 | // that does not make sense for the client 86 | dgram: 'empty', 87 | fs: 'empty', 88 | net: 'empty', 89 | tls: 'empty', 90 | child_process: 'empty' 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /web/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const path = require('path') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 11 | const portfinder = require('portfinder') 12 | 13 | const HOST = process.env.HOST 14 | const PORT = process.env.PORT && Number(process.env.PORT) 15 | 16 | const devWebpackConfig = merge(baseWebpackConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) 19 | }, 20 | // cheap-module-eval-source-map is faster for development 21 | devtool: config.dev.devtool, 22 | 23 | // these devServer options should be customized in /config/index.js 24 | devServer: { 25 | clientLogLevel: 'warning', 26 | historyApiFallback: { 27 | rewrites: [ 28 | { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, 29 | ], 30 | }, 31 | hot: true, 32 | contentBase: false, // since we use CopyWebpackPlugin. 33 | compress: true, 34 | host: HOST || config.dev.host, 35 | port: PORT || config.dev.port, 36 | open: config.dev.autoOpenBrowser, 37 | overlay: config.dev.errorOverlay 38 | ? { warnings: false, errors: true } 39 | : false, 40 | publicPath: config.dev.assetsPublicPath, 41 | proxy: config.dev.proxyTable, 42 | quiet: true, // necessary for FriendlyErrorsPlugin 43 | watchOptions: { 44 | poll: config.dev.poll, 45 | } 46 | }, 47 | plugins: [ 48 | new webpack.DefinePlugin({ 49 | 'process.env': require('../config/dev.env') 50 | }), 51 | new webpack.HotModuleReplacementPlugin(), 52 | new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. 53 | new webpack.NoEmitOnErrorsPlugin(), 54 | // https://github.com/ampedandwired/html-webpack-plugin 55 | new HtmlWebpackPlugin({ 56 | filename: 'index.html', 57 | template: 'index.html', 58 | inject: true 59 | }), 60 | // copy custom static assets 61 | new CopyWebpackPlugin([ 62 | { 63 | from: path.resolve(__dirname, '../static'), 64 | to: config.dev.assetsSubDirectory, 65 | ignore: ['.*'] 66 | } 67 | ]) 68 | ] 69 | }) 70 | 71 | module.exports = new Promise((resolve, reject) => { 72 | portfinder.basePort = process.env.PORT || config.dev.port 73 | portfinder.getPort((err, port) => { 74 | if (err) { 75 | reject(err) 76 | } else { 77 | // publish the new Port, necessary for e2e tests 78 | process.env.PORT = port 79 | // add port to devServer config 80 | devWebpackConfig.devServer.port = port 81 | 82 | // Add FriendlyErrorsPlugin 83 | devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ 84 | compilationSuccessInfo: { 85 | messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], 86 | }, 87 | onErrors: config.dev.notifyOnErrors 88 | ? utils.createNotifierCallback() 89 | : undefined 90 | })) 91 | 92 | resolve(devWebpackConfig) 93 | } 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /web/build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 12 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 13 | 14 | const env = process.env.NODE_ENV === 'testing' 15 | ? require('../config/test.env') 16 | : require('../config/prod.env') 17 | 18 | const webpackConfig = merge(baseWebpackConfig, { 19 | module: { 20 | rules: utils.styleLoaders({ 21 | sourceMap: config.build.productionSourceMap, 22 | extract: true, 23 | usePostCSS: true 24 | }) 25 | }, 26 | devtool: config.build.productionSourceMap ? config.build.devtool : false, 27 | output: { 28 | path: config.build.assetsRoot, 29 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 30 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 31 | }, 32 | plugins: [ 33 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 34 | new webpack.DefinePlugin({ 35 | 'process.env': env 36 | }), 37 | new UglifyJsPlugin({ 38 | uglifyOptions: { 39 | compress: { 40 | warnings: false 41 | } 42 | }, 43 | sourceMap: config.build.productionSourceMap, 44 | parallel: true 45 | }), 46 | // extract css into its own file 47 | new ExtractTextPlugin({ 48 | filename: utils.assetsPath('css/[name].[contenthash].css'), 49 | // Setting the following option to `false` will not extract CSS from codesplit chunks. 50 | // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack. 51 | // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, 52 | // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110 53 | allChunks: true, 54 | }), 55 | // Compress extracted CSS. We are using this plugin so that possible 56 | // duplicated CSS from different components can be deduped. 57 | new OptimizeCSSPlugin({ 58 | cssProcessorOptions: config.build.productionSourceMap 59 | ? { safe: true, map: { inline: false } } 60 | : { safe: true } 61 | }), 62 | // generate dist index.html with correct asset hash for caching. 63 | // you can customize output by editing /index.html 64 | // see https://github.com/ampedandwired/html-webpack-plugin 65 | new HtmlWebpackPlugin({ 66 | filename: process.env.NODE_ENV === 'testing' 67 | ? 'index.html' 68 | : config.build.index, 69 | template: 'index.html', 70 | inject: true, 71 | minify: { 72 | removeComments: true, 73 | collapseWhitespace: true, 74 | removeAttributeQuotes: true 75 | // more options: 76 | // https://github.com/kangax/html-minifier#options-quick-reference 77 | }, 78 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 79 | chunksSortMode: 'dependency' 80 | }), 81 | // keep module.id stable when vendor modules does not change 82 | new webpack.HashedModuleIdsPlugin(), 83 | // enable scope hoisting 84 | new webpack.optimize.ModuleConcatenationPlugin(), 85 | // split vendor js into its own file 86 | new webpack.optimize.CommonsChunkPlugin({ 87 | name: 'vendor', 88 | minChunks (module) { 89 | // any required modules inside node_modules are extracted to vendor 90 | return ( 91 | module.resource && 92 | /\.js$/.test(module.resource) && 93 | module.resource.indexOf( 94 | path.join(__dirname, '../node_modules') 95 | ) === 0 96 | ) 97 | } 98 | }), 99 | // extract webpack runtime and module manifest to its own file in order to 100 | // prevent vendor hash from being updated whenever app bundle is updated 101 | new webpack.optimize.CommonsChunkPlugin({ 102 | name: 'manifest', 103 | minChunks: Infinity 104 | }), 105 | // This instance extracts shared chunks from code splitted chunks and bundles them 106 | // in a separate chunk, similar to the vendor chunk 107 | // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk 108 | new webpack.optimize.CommonsChunkPlugin({ 109 | name: 'app', 110 | async: 'vendor-async', 111 | children: true, 112 | minChunks: 3 113 | }), 114 | 115 | // copy custom static assets 116 | new CopyWebpackPlugin([ 117 | { 118 | from: path.resolve(__dirname, '../static'), 119 | to: config.build.assetsSubDirectory, 120 | ignore: ['.*'] 121 | } 122 | ]) 123 | ] 124 | }) 125 | 126 | if (config.build.productionGzip) { 127 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 128 | 129 | webpackConfig.plugins.push( 130 | new CompressionWebpackPlugin({ 131 | asset: '[path].gz[query]', 132 | algorithm: 'gzip', 133 | test: new RegExp( 134 | '\\.(' + 135 | config.build.productionGzipExtensions.join('|') + 136 | ')$' 137 | ), 138 | threshold: 10240, 139 | minRatio: 0.8 140 | }) 141 | ) 142 | } 143 | 144 | if (config.build.bundleAnalyzerReport) { 145 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 146 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 147 | } 148 | 149 | module.exports = webpackConfig 150 | -------------------------------------------------------------------------------- /web/config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"', 7 | PREFIX_PATH: `"${process.env.PREFIX_PATH}"` 8 | }) 9 | -------------------------------------------------------------------------------- /web/config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | const BACKEND = process.env.BACKEND || 'http://127.0.0.1:6080' 7 | 8 | module.exports = { 9 | dev: { 10 | 11 | // Paths 12 | assetsSubDirectory: 'static', 13 | assetsPublicPath: './', 14 | proxyTable: { 15 | '/api': { 16 | target: BACKEND, 17 | changeOrigin: true, 18 | secure: false 19 | }, 20 | '/websockify': { 21 | target: BACKEND, 22 | // logLevel: 'debug', 23 | ws: true, 24 | changeOrigin: true 25 | } 26 | }, 27 | 28 | // Various Dev Server settings 29 | host: 'localhost', // can be overwritten by process.env.HOST 30 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 31 | autoOpenBrowser: false, 32 | errorOverlay: true, 33 | notifyOnErrors: true, 34 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 35 | 36 | // Use Eslint Loader? 37 | // If true, your code will be linted during bundling and 38 | // linting errors and warnings will be shown in the console. 39 | useEslint: true, 40 | // If true, eslint errors and warnings will also be shown in the error overlay 41 | // in the browser. 42 | showEslintErrorsInOverlay: false, 43 | 44 | /** 45 | * Source Maps 46 | */ 47 | 48 | // https://webpack.js.org/configuration/devtool/#development 49 | devtool: 'cheap-module-eval-source-map', 50 | 51 | // If you have problems debugging vue-files in devtools, 52 | // set this to false - it *may* help 53 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 54 | cacheBusting: true, 55 | 56 | cssSourceMap: true 57 | }, 58 | 59 | build: { 60 | // Template for index.html 61 | index: path.resolve(__dirname, '../dist/index.html'), 62 | 63 | // Paths 64 | assetsRoot: path.resolve(__dirname, '../dist'), 65 | assetsSubDirectory: 'static', 66 | assetsPublicPath: './', 67 | 68 | /** 69 | * Source Maps 70 | */ 71 | 72 | productionSourceMap: true, 73 | // https://webpack.js.org/configuration/devtool/#production 74 | devtool: '#source-map', 75 | 76 | // Gzip off by default as many popular static hosts such as 77 | // Surge or Netlify already gzip all static assets for you. 78 | // Before setting to `true`, make sure to: 79 | // npm install --save-dev compression-webpack-plugin 80 | productionGzip: false, 81 | productionGzipExtensions: ['js', 'css'], 82 | 83 | // Run the build command with an extra argument to 84 | // View the bundle analyzer report after build finishes: 85 | // `npm run build --report` 86 | // Set to `true` or `false` to always turn it on or off 87 | bundleAnalyzerReport: process.env.npm_config_report 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /web/config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"', 4 | PREFIX_PATH: `"${process.env.PREFIX_PATH}"` 5 | } 6 | -------------------------------------------------------------------------------- /web/config/test.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const devEnv = require('./dev.env') 4 | 5 | module.exports = merge(devEnv, { 6 | NODE_ENV: '"testing"' 7 | }) 8 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | novnc2 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /web/novnc-armhf-1.patch: -------------------------------------------------------------------------------- 1 | --- core/rfb.js 2019-02-24 21:17:04.402944311 +0800 2 | +++ core/rfb.js 2019-02-24 21:17:34.782484107 +0800 3 | @@ -1256,8 +1256,8 @@ 4 | encs.push(encodings.encodingCopyRect); 5 | // Only supported with full depth support 6 | if (this._fb_depth == 24) { 7 | - encs.push(encodings.encodingTight); 8 | - encs.push(encodings.encodingTightPNG); 9 | + // encs.push(encodings.encodingTight); 10 | + // encs.push(encodings.encodingTightPNG); 11 | encs.push(encodings.encodingHextile); 12 | encs.push(encodings.encodingRRE); 13 | } 14 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "novnc2", 3 | "version": "1.0.0", 4 | "description": "extended novnc", 5 | "author": "Doro Wu ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 9 | "start": "npm run dev", 10 | "e2e": "node test/e2e/runner.js", 11 | "test": "npm run unit && npm run e2e", 12 | "lint": "eslint --ext .js,.vue src test/unit test/e2e/specs", 13 | "build": "node build/build.js" 14 | }, 15 | "dependencies": { 16 | "axios": "^0.21.1", 17 | "vue": "^2.5.2", 18 | "vue-material": "^1.0.0-beta-10.2", 19 | "vue-router": "^3.0.1" 20 | }, 21 | "devDependencies": { 22 | "autoprefixer": "^7.1.2", 23 | "babel-core": "^6.22.1", 24 | "babel-eslint": "^8.2.1", 25 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 26 | "babel-loader": "^7.1.1", 27 | "babel-plugin-syntax-jsx": "^6.18.0", 28 | "babel-plugin-transform-runtime": "^6.22.0", 29 | "babel-plugin-transform-vue-jsx": "^3.5.0", 30 | "babel-preset-env": "^1.3.2", 31 | "babel-preset-stage-2": "^6.22.0", 32 | "babel-register": "^6.22.0", 33 | "chalk": "^2.0.1", 34 | "chromedriver": "^2.27.2", 35 | "copy-webpack-plugin": "^4.0.1", 36 | "cross-spawn": "^5.0.1", 37 | "css-loader": "^0.28.0", 38 | "eslint": "^4.15.0", 39 | "eslint-config-standard": "^10.2.1", 40 | "eslint-friendly-formatter": "^3.0.0", 41 | "eslint-loader": "^1.7.1", 42 | "eslint-plugin-import": "^2.7.0", 43 | "eslint-plugin-node": "^5.2.0", 44 | "eslint-plugin-promise": "^3.4.0", 45 | "eslint-plugin-standard": "^3.0.1", 46 | "eslint-plugin-vue": "^4.0.0", 47 | "extract-text-webpack-plugin": "^3.0.0", 48 | "file-loader": "^1.1.4", 49 | "friendly-errors-webpack-plugin": "^1.6.1", 50 | "html-webpack-plugin": "^2.30.1", 51 | "nightwatch": "^0.9.12", 52 | "node-notifier": "^8.0.1", 53 | "optimize-css-assets-webpack-plugin": "^3.2.0", 54 | "ora": "^1.2.0", 55 | "portfinder": "^1.0.13", 56 | "postcss-import": "^11.0.0", 57 | "postcss-loader": "^2.0.8", 58 | "postcss-url": "^7.2.1", 59 | "rimraf": "^2.6.0", 60 | "selenium-server": "^3.0.1", 61 | "semver": "^5.3.0", 62 | "shelljs": "^0.7.6", 63 | "uglifyjs-webpack-plugin": "^1.1.1", 64 | "url-loader": "^0.5.8", 65 | "vue-loader": "^13.3.0", 66 | "vue-style-loader": "^3.0.1", 67 | "vue-template-compiler": "^2.5.2", 68 | "webpack": "^3.6.0", 69 | "webpack-bundle-analyzer": "^2.9.0", 70 | "webpack-cli": "^3.3.7", 71 | "webpack-dev-server": "^3.2.1", 72 | "webpack-merge": "^4.1.0" 73 | }, 74 | "engines": { 75 | "node": ">= 8.0.0", 76 | "npm": ">= 3.0.0" 77 | }, 78 | "browserslist": [ 79 | "> 1%", 80 | "last 2 versions", 81 | "not ie <= 8" 82 | ] 83 | } 84 | -------------------------------------------------------------------------------- /web/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 23 | -------------------------------------------------------------------------------- /web/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcwu/docker-ubuntu-vnc-desktop/e4922ce92f945fc482994b7a0fd95ca5de7295b3/web/src/assets/.gitkeep -------------------------------------------------------------------------------- /web/src/components/Vnc.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 199 | 200 | 218 | -------------------------------------------------------------------------------- /web/src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import router from './router' 6 | import axios from 'axios' 7 | 8 | Vue.config.productionTip = false 9 | Vue.prototype.$http = axios 10 | 11 | /* eslint-disable no-new */ 12 | new Vue({ 13 | el: '#app', 14 | router, 15 | components: { App }, 16 | template: '' 17 | }) 18 | -------------------------------------------------------------------------------- /web/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Vnc from '@/components/Vnc' 4 | 5 | Vue.use(Router) 6 | 7 | export default new Router({ 8 | mode: 'history', 9 | base: window.location.pathname, 10 | routes: [ 11 | { 12 | path: '/', 13 | name: 'Vnc', 14 | component: Vnc 15 | } 16 | ] 17 | }) 18 | -------------------------------------------------------------------------------- /web/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcwu/docker-ubuntu-vnc-desktop/e4922ce92f945fc482994b7a0fd95ca5de7295b3/web/static/.gitkeep -------------------------------------------------------------------------------- /web/static/video.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | video 5 | 32 | 33 | 34 |
35 | 38 |
39 |
40 | 41 | 42 | 43 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /web/static/vnc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | noVNC 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 63 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 |
79 |
80 |
noVNC encountered an error:
81 |
82 |
83 |
84 |
85 | 86 | 87 |
88 | 89 |
90 |
91 | 92 |
93 | 94 |

no
VNC

95 | 96 | 97 | 100 | 101 | 102 |
103 | 106 | 109 | 112 | 115 | 117 |
118 | 119 | 120 |
121 | 124 |
125 |
126 | 129 | 132 | 135 | 138 | 141 | 144 |
145 |
146 |
147 | 148 | 149 | 152 |
153 |
154 |
155 | Power 156 |
157 | 158 | 159 | 160 |
161 |
162 | 163 | 164 | 167 |
168 |
169 |
170 | Clipboard 171 |
172 | 173 |
174 | 176 |
177 |
178 | 179 | 180 | 183 | 184 | 185 | 188 |
189 |
190 |
    191 |
  • 192 | Settings 193 |
  • 194 |
  • 195 | 196 |
  • 197 |
  • 198 | 199 |
  • 200 |

  • 201 |
  • 202 | 203 |
  • 204 |
  • 205 | 206 | 211 |
  • 212 |

  • 213 |
  • 214 |
    Advanced
    215 |
      216 |
    • 217 | 218 | 219 |
    • 220 |
    • 221 |
      WebSocket
      222 |
        223 |
      • 224 | 225 |
      • 226 |
      • 227 | 228 | 229 |
      • 230 |
      • 231 | 232 | 233 |
      • 234 |
      • 235 | 236 | 237 |
      • 238 |
      239 |
    • 240 |

    • 241 |
    • 242 | 243 |
    • 244 |
    • 245 | 246 | 247 |
    • 248 |

    • 249 |
    • 250 | 251 |
    • 252 |

    • 253 | 254 |
    • 255 | 259 |
    • 260 |
    261 |
  • 262 |
263 |
264 |
265 | 266 | 267 | 270 | 271 |
272 |
273 | 274 |
275 | 276 |
277 | 278 | 279 |
280 | 281 | 282 |
283 |
284 | 285 |
286 | Connect 287 |
288 |
289 |
290 | 291 | 292 |
293 |
294 |
    295 |
  • 296 | 297 | 298 |
  • 299 |
  • 300 | 301 |
  • 302 |
303 |
304 |
305 | 306 | 307 |
308 |
309 |
310 | 311 |
312 |
313 |
314 | 315 | 316 |
317 | 321 | 323 |
324 | 325 | 329 | 349 | 350 | 351 | -------------------------------------------------------------------------------- /web/test/e2e/custom-assertions/elementCount.js: -------------------------------------------------------------------------------- 1 | // A custom Nightwatch assertion. 2 | // The assertion name is the filename. 3 | // Example usage: 4 | // 5 | // browser.assert.elementCount(selector, count) 6 | // 7 | // For more information on custom assertions see: 8 | // http://nightwatchjs.org/guide#writing-custom-assertions 9 | 10 | exports.assertion = function (selector, count) { 11 | this.message = 'Testing if element <' + selector + '> has count: ' + count 12 | this.expected = count 13 | this.pass = function (val) { 14 | return val === this.expected 15 | } 16 | this.value = function (res) { 17 | return res.value 18 | } 19 | this.command = function (cb) { 20 | var self = this 21 | return this.api.execute(function (selector) { 22 | return document.querySelectorAll(selector).length 23 | }, [selector], function (res) { 24 | cb.call(self, res) 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /web/test/e2e/nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | var config = require('../../config') 3 | 4 | // http://nightwatchjs.org/gettingstarted#settings-file 5 | module.exports = { 6 | src_folders: ['test/e2e/specs'], 7 | output_folder: 'test/e2e/reports', 8 | custom_assertions_path: ['test/e2e/custom-assertions'], 9 | 10 | selenium: { 11 | start_process: true, 12 | server_path: require('selenium-server').path, 13 | host: '127.0.0.1', 14 | port: 4444, 15 | cli_args: { 16 | 'webdriver.chrome.driver': require('chromedriver').path 17 | } 18 | }, 19 | 20 | test_settings: { 21 | default: { 22 | selenium_port: 4444, 23 | selenium_host: 'localhost', 24 | silent: true, 25 | globals: { 26 | devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port) 27 | } 28 | }, 29 | 30 | chrome: { 31 | desiredCapabilities: { 32 | browserName: 'chrome', 33 | javascriptEnabled: true, 34 | acceptSslCerts: true 35 | } 36 | }, 37 | 38 | firefox: { 39 | desiredCapabilities: { 40 | browserName: 'firefox', 41 | javascriptEnabled: true, 42 | acceptSslCerts: true 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /web/test/e2e/runner.js: -------------------------------------------------------------------------------- 1 | // 1. start the dev server using production config 2 | process.env.NODE_ENV = 'testing' 3 | 4 | const webpack = require('webpack') 5 | const DevServer = require('webpack-dev-server') 6 | 7 | const webpackConfig = require('../../build/webpack.prod.conf') 8 | const devConfigPromise = require('../../build/webpack.dev.conf') 9 | 10 | let server 11 | 12 | devConfigPromise.then(devConfig => { 13 | const devServerOptions = devConfig.devServer 14 | const compiler = webpack(webpackConfig) 15 | server = new DevServer(compiler, devServerOptions) 16 | const port = devServerOptions.port 17 | const host = devServerOptions.host 18 | return server.listen(port, host) 19 | }) 20 | .then(() => { 21 | // 2. run the nightwatch test suite against it 22 | // to run in additional browsers: 23 | // 1. add an entry in test/e2e/nightwatch.conf.js under "test_settings" 24 | // 2. add it to the --env flag below 25 | // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox` 26 | // For more information on Nightwatch's config file, see 27 | // http://nightwatchjs.org/guide#settings-file 28 | let opts = process.argv.slice(2) 29 | if (opts.indexOf('--config') === -1) { 30 | opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js']) 31 | } 32 | if (opts.indexOf('--env') === -1) { 33 | opts = opts.concat(['--env', 'chrome']) 34 | } 35 | 36 | const spawn = require('cross-spawn') 37 | const runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' }) 38 | 39 | runner.on('exit', function (code) { 40 | server.close() 41 | process.exit(code) 42 | }) 43 | 44 | runner.on('error', function (err) { 45 | server.close() 46 | throw err 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /web/test/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // For authoring Nightwatch tests, see 2 | // http://nightwatchjs.org/guide#usage 3 | 4 | module.exports = { 5 | 'default e2e tests': function (browser) { 6 | // automatically uses dev Server port from /config.index.js 7 | // default: http://localhost:8080 8 | // see nightwatch.conf.js 9 | const devServer = browser.globals.devServerURL 10 | 11 | browser 12 | .url(devServer) 13 | .waitForElementVisible('#app', 5000) 14 | .assert.elementPresent('.hello') 15 | .assert.containsText('h1', 'Welcome to Your Vue.js App') 16 | .assert.elementCount('img', 1) 17 | .end() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /web/test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | }, 4 | "globals": { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /web/test/unit/specs/HelloWorld.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import HelloWorld from '@/components/HelloWorld' 3 | 4 | describe('HelloWorld.vue', () => { 5 | it('should render correct contents', () => { 6 | const Constructor = Vue.extend(HelloWorld) 7 | const vm = new Constructor().$mount() 8 | expect(vm.$el.querySelector('.hello h1').textContent) 9 | }) 10 | }) 11 | --------------------------------------------------------------------------------