未验证 提交 b29346ec 编写于 作者: A Asher

Implement new structure

上级 ef8da386
......@@ -6,6 +6,11 @@ platform:
arch: amd64
steps:
- name: submodules
image: alpine/git
commands:
- git submodule update --init
- name: cache:restore
image: node:12
commands:
......@@ -69,7 +74,7 @@ steps:
- name: publish:gcs
image: plugins/gcs
settings:
source: gcs_bucket
source: binary-upload
target: codesrv-ci.cdr.sh/
token:
from_secret: gcs-token
......@@ -85,6 +90,11 @@ platform:
arch: amd64
steps:
- name: submodules
image: alpine/git
commands:
- git submodule update --init
- name: cache:restore
image: node:12-alpine
commands:
......@@ -133,7 +143,7 @@ steps:
- name: publish:gcs
image: plugins/gcs
settings:
source: gcs_bucket
source: binary-upload
target: codesrv-ci.cdr.sh/
token:
from_secret: gcs-token
......@@ -149,6 +159,12 @@ platform:
arch: arm64
steps:
- name: submodules
image: alpine
commands:
- apk add git
- git submodule update --init
- name: cache:restore
image: node:12
commands:
......@@ -211,7 +227,7 @@ steps:
- name: publish:gcs
image: plugins/gcs
settings:
source: gcs_bucket
source: binary-upload
target: codesrv-ci.cdr.sh/
token:
from_secret: gcs-token
......@@ -227,6 +243,12 @@ platform:
arch: arm64
steps:
- name: submodules
image: alpine
commands:
- apk add git
- git submodule update --init
- name: cache:restore
image: node:12-alpine
commands:
......@@ -275,7 +297,7 @@ steps:
- name: publish:gcs
image: plugins/gcs
settings:
source: gcs_bucket
source: binary-upload
target: codesrv-ci.cdr.sh/
token:
from_secret: gcs-token
......@@ -291,6 +313,12 @@ platform:
arch: arm
steps:
- name: submodules
image: alpine
commands:
- apk add git
- git submodule update --init
- name: cache:restore
image: node:12
commands:
......@@ -360,6 +388,12 @@ platform:
arch: arm
steps:
- name: submodules
image: alpine
commands:
- apk add git
- git submodule update --init
- name: cache:restore
image: node:12-alpine
commands:
......
root = true
[*]
indent_style = space
trim_trailing_whitespace = true
indent_size = 2
parser: "@typescript-eslint/parser"
env:
browser: true
es6: true # Map, etc.
mocha: true
node: true
parserOptions:
ecmaVersion: 2018
sourceType: module
ecmaFeatures:
jsx: true
extends:
- eslint:recommended
- plugin:@typescript-eslint/recommended
- plugin:import/recommended
- plugin:import/typescript
- plugin:react/recommended
- plugin:prettier/recommended
- prettier # Removes eslint rules that conflict with prettier.
- prettier/@typescript-eslint # Remove conflicts again.
plugins:
- react-hooks
# Need to set this explicitly for the eslint-plugin-react.
settings:
react:
version: detect
rules:
# For overloads.
no-dupe-class-members: off
# https://www.npmjs.com/package/eslint-plugin-react-hooks
react-hooks/rules-of-hooks: error
react/prop-types: off # We use Typescript to verify prop types.
*.tsbuildinfo
.cache
binaries
binary-upload
build
cache-upload
dist
dist-build
node_modules
/build
/release
/binaries
/lib
out
out-build
release
source
yarn-cache
[submodule "lib/vscode"]
path = lib/vscode
url = https://github.com/microsoft/vscode
printWidth: 120
semi: false
tabWidth: 2
singleQuote: false
trailingComma: es5
useTabs: false
arrowParens: always
extends:
- stylelint-config-standard
......@@ -4,40 +4,40 @@ ARG githubToken
# Install VS Code's deps. These are the only two it seems we need.
RUN apt-get update && apt-get install -y \
libxkbfile-dev \
libsecret-1-dev
libxkbfile-dev \
libsecret-1-dev
WORKDIR /src
COPY . .
RUN yarn \
&& DRONE_TAG="$tag" MINIFY=true BINARY=true GITHUB_TOKEN="$githubToken" ./scripts/ci.bash \
&& rm -r /src/build \
&& rm -r /src/source
&& DRONE_TAG="$tag" MINIFY=true STRIP_BIN_TARGET=true GITHUB_TOKEN="$githubToken" ./scripts/ci.bash \
&& rm -r /src/build \
&& rm -r /src/source
# We deploy with Ubuntu so that devs have a familiar environment.
FROM ubuntu:18.04
RUN apt-get update && apt-get install -y \
openssl \
net-tools \
git \
locales \
sudo \
dumb-init \
vim \
curl \
wget \
&& rm -rf /var/lib/apt/lists/*
openssl \
net-tools \
git \
locales \
sudo \
dumb-init \
vim \
curl \
wget \
&& rm -rf /var/lib/apt/lists/*
RUN locale-gen en_US.UTF-8
# We cannot use update-locale because docker will not use the env variables
# configured in /etc/default/locale so we need to set it manually.
ENV LC_ALL=en_US.UTF-8 \
SHELL=/bin/bash
SHELL=/bin/bash
RUN adduser --gecos '' --disabled-password coder && \
echo "coder ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/nopasswd
echo "coder ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/nopasswd
USER coder
# Create first so these directories will be owned by coder instead of root
......
......@@ -10,14 +10,19 @@ docker run -it -p 127.0.0.1:8080:8080 -v "$PWD:/home/coder/project" codercom/cod
```
- **Consistent environment:** Code on your Chromebook, tablet, and laptop with a
consistent dev environment. develop more easily for Linux if you have a
Windows or Mac, and pick up where you left off when switching workstations.
consistent dev environment. Develop more easily for Linux if you have a
Windows or Mac and pick up where you left off when switching workstations.
- **Server-powered:** Take advantage of large cloud servers to speed up tests,
compilations, downloads, and more. Preserve battery life when you're on the go
since all intensive computation runs on your server.
![Screenshot](/doc/assets/ide.gif)
## VS Code
- See [our VS Code readme](./src/vscode) for more information about how
code-server and VS Code work together.
## Getting Started
### Requirements
......@@ -25,7 +30,8 @@ docker run -it -p 127.0.0.1:8080:8080 -v "$PWD:/home/coder/project" codercom/cod
- 64-bit host.
- At least 1GB of RAM.
- 2 cores or more are recommended (1 core works but not optimally).
- Secure connection over HTTPS or localhost (required for service workers).
- Secure connection over HTTPS or localhost (required for service workers and
clipboard support).
- For Linux: GLIBC 2.17 or later and GLIBCXX 3.4.15 or later.
- Docker (for Docker versions of `code-server`).
......@@ -37,12 +43,6 @@ Use [sshcode](https://github.com/codercom/sshcode) for a simple setup.
See the Docker one-liner mentioned above. Dockerfile is at [/Dockerfile](/Dockerfile).
To debug Golang using the
[ms-vscode-go extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.Go),
you need to add `--security-opt seccomp=unconfined` to your `docker run`
arguments when launching code-server with Docker. See
[#725](https://github.com/cdr/code-server/issues/725) for details.
### Digital Ocean
[![Create a Droplet](./doc/assets/droplet.svg)](https://marketplace.digitalocean.com/apps/code-server?action=deploy)
......@@ -59,18 +59,18 @@ arguments when launching code-server with Docker. See
### Build
See
[VS Code's prerequisites](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites)
before building.
- [VS Code prerequisites](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites)
```shell
export OUT=/path/to/output/build # Optional if only building. Required if also developing.
yarn build $vscodeVersion $codeServerVersion # See scripts/ci.bash for the VS Code version to use.
# The code-server version can be anything you want.
node /path/to/output/build/out/vs/server/main.js # You can run the built JavaScript with Node.
yarn binary $vscodeVersion $codeServerVersion # Or you can package it into a binary.
yarn
yarn build
node build/out/entry.js # You can run the built JavaScript with Node.
yarn binary # Or you can package it into a binary.
```
If changes are made to the patch and you've built previously you must manually
reset VS Code then run `yarn patch:apply`.
## Security
### Authentication
......@@ -98,32 +98,11 @@ for free.
Do not expose `code-server` to the open internet without SSL, whether built-in
or through a proxy.
## Known Issues
- Creating custom VS Code extensions and debugging them doesn't work.
- Extension profiling and tips are currently disabled.
## Future
- **Stay up to date!** Get notified about new releases of code-server.
- **Stay up to date!** Get notified about new releases of `code-server`.
![Screenshot](/doc/assets/release.gif)
- Windows support.
- Electron and Chrome OS applications to bridge the gap between local<->remote.
- Run VS Code unit tests against our builds to ensure features work as expected.
## Extensions
code-server does not provide access to the official
[Visual Studio Marketplace](https://marketplace.visualstudio.com/vscode). Instead,
Coder has created a custom extension marketplace that we manage for open-source
extensions. If you want to use an extension with code-server that we do not have
in our marketplace please look for a release in the extension’s repository,
contact us to see if we have one in the works or, if you build an extension
locally from open source, you can copy it to the `extensions` folder. If you
build one locally from open-source please contribute it to the project and let
us know so we can give you props! If you have your own custom marketplace, it is
possible to point code-server to it by setting the `SERVICE_URL` and `ITEM_URL`
environment variables.
## Telemetry
......@@ -134,51 +113,18 @@ data collected to improve code-server.
### Development
See
[VS Code's prerequisites](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites)
before developing.
- [VS Code prerequisites](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites)
```shell
git clone https://github.com/microsoft/vscode
cd vscode
git checkout ${vscodeVersion} # See scripts/ci.bash for the version to use.
yarn
git clone https://github.com/cdr/code-server src/vs/server
cd src/vs/server
yarn
yarn patch:apply
yarn watch
# Wait for the initial compilation to complete (it will say "Finished compilation").
# Run the next command in another shell.
yarn start
# Visit http://localhost:8080
yarn watch # Visit http://localhost:8080 once completed.
```
If you run into issues about a different version of Node being used, try running
`npm rebuild` in the VS Code directory.
### Upgrading VS Code
We patch VS Code to provide and fix some functionality. As the web portion of VS
Code matures, we'll be able to shrink and maybe even entirely eliminate our
patch. In the meantime, however, upgrading the VS Code version requires ensuring
that the patch still applies and has the intended effects.
To generate a new patch, **stage all the changes** you want to be included in
the patch in the VS Code source, then run `yarn patch:generate` in this
directory.
Our changes include:
- Allow multiple extension directories (both user and built-in).
- Modify the loader, websocket, webview, service worker, and asset requests to
use the URL of the page as a base (and TLS if necessary for the websocket).
- Send client-side telemetry through the server.
- Make changing the display language work.
- Make it possible for us to load code on the client.
- Make extensions work in the browser.
- Fix getting permanently disconnected when you sleep or hibernate for a while.
- Make it possible to automatically update the binary.
If changes are made to the patch and you've built previously you must manually
reset VS Code then run `yarn patch:apply`.
## License
......
Subproject commit 26076a4de974ead31f97692a0d32f90d735645c0
// Once our entry file is loaded we no longer need nbin to bypass normal Node
// execution. We can still shim the fs into the binary even when bypassing. This
// will ensure for example that a spawn like `${process.argv[0]} -e` will work
// while still allowing us to access files within the binary.
process.env.NBIN_BYPASS = true;
require("../../bootstrap-amd").load("vs/server/src/node/cli");
{
"name": "code-server",
"license": "MIT",
"version": "2.1.0",
"scripts": {
"i": "yarn install --ignore-scripts",
"preinstall": "./scripts/preinstall.sh",
"postinstall": "./scripts/postinstall.sh",
"patch:generate": "cd ./lib/vscode && git diff HEAD > ../../scripts/vscode.patch",
"patch:apply": "cd ./lib/vscode && git apply ../../scripts/vscode.patch",
"test": "mocha -r ts-node/register ./test/*.test.ts",
"lint:js": "eslint {src,test,scripts} --ext .ts,.tsx",
"lint:css": "stylelint 'src/**/*.css'",
"lint": "./scripts/lint.sh",
"watch": "yarn runner watch",
"runner": "cd ./scripts && node --max-old-space-size=32384 -r ts-node/register ./build.ts",
"start": "nodemon --watch ../../../out --verbose ../../../out/vs/server/main.js",
"test": "./scripts/test.sh",
"watch": "cd ../../../ && yarn watch",
"build": "yarn && yarn runner build",
"package": "yarn runner package",
"build": "yarn runner build",
"binary": "yarn runner binary",
"patch:generate": "cd ../../../ && git diff --staged > ./src/vs/server/scripts/vscode.patch",
"patch:apply": "cd ../../../ && git apply ./src/vs/server/scripts/vscode.patch"
"package": "yarn runner package"
},
"devDependencies": {
"@coder/nbin": "^1.2.7",
"@types/fs-extra": "^8.0.1",
"@types/node": "^10.12.12",
"@types/mocha": "^5.2.7",
"@types/node": "^12.12.7",
"@types/parcel-bundler": "^1.12.1",
"@types/pem": "^1.9.5",
"@types/react": "^16.9.18",
"@types/react-dom": "^16.9.5",
"@types/react-router-dom": "^5.1.3",
"@types/safe-compare": "^1.1.0",
"@types/tar-fs": "^1.16.1",
"@types/tar-stream": "^1.6.1",
"fs-extra": "^8.1.0",
"nodemon": "^1.19.1",
"@types/ws": "^6.0.4",
"@typescript-eslint/eslint-plugin": "^2.0.0",
"@typescript-eslint/parser": "^2.0.0",
"eslint": "^6.2.0",
"eslint-config-prettier": "^6.0.0",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-prettier": "^3.1.0",
"eslint-plugin-react": "^7.14.3",
"eslint-plugin-react-hooks": "^1.7.0",
"leaked-handles": "^5.2.0",
"mocha": "^6.2.0",
"parcel-bundler": "^1.12.4",
"prettier": "^1.18.2",
"stylelint": "^13.0.0",
"stylelint-config-standard": "^19.0.0",
"ts-node": "^8.4.1",
"typescript": "3.6"
"typescript": "3.7.2"
},
"resolutions": {
"@types/node": "^10.12.12",
"safe-buffer": "^5.1.1"
"@types/node": "^12.12.7",
"safe-buffer": "^5.1.1",
"vfile-message": "^2.0.2"
},
"dependencies": {
"@coder/logger": "^1.1.12",
"@coder/node-browser": "^1.0.6",
"@coder/requirefs": "^1.0.6",
"@coder/logger": "1.1.11",
"fs-extra": "^8.1.0",
"httpolyglot": "^0.1.2",
"pem": "^1.14.2",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-router-dom": "^5.1.2",
"safe-compare": "^1.1.4",
"tar-fs": "^2.0.0",
"tar-stream": "^2.1.0",
"util": "^0.12.1"
"ws": "^7.2.0"
}
}
此差异已折叠。
......@@ -8,46 +8,48 @@ set -eu
# Try restoring from each argument in turn until we get something.
restore() {
for branch in "$@" ; do
if [ -n "$branch" ] ; then
cache_path="https://codesrv-ci.cdr.sh/cache/$branch/$tar.tar.gz"
if wget "$cache_path" ; then
tar xzvf "$tar.tar.gz"
break
fi
fi
done
for branch in "$@" ; do
if [ -n "$branch" ] ; then
cache_path="https://codesrv-ci.cdr.sh/cache/$branch/$tar.tar.gz"
if wget "$cache_path" ; then
tar xzvf "$tar.tar.gz"
break
fi
fi
done
}
# We need to cache the built-in extensions and Node modules. Everything inside
# the cache-upload directory will be uploaded as-is to the code-server bucket.
package() {
mkdir -p "cache-upload/cache/$1"
tar czfv "cache-upload/cache/$1/$tar.tar.gz" node_modules source yarn-cache
mkdir -p "cache-upload/cache/$1"
tar czfv "cache-upload/cache/$1/$tar.tar.gz" node_modules yarn-cache \
lib/vscode/.build \
lib/vscode/node_modules
}
main() {
cd "$(dirname "$0")/.."
# Get the branch for this build.
branch=${DRONE_BRANCH:-${DRONE_SOURCE_BRANCH:-${DRONE_TAG:-}}}
# The cache will be named based on the arch, platform, and libc.
arch=$DRONE_STAGE_ARCH
platform=${PLATFORM:-linux}
case $DRONE_STAGE_NAME in
*alpine*) libc=musl ;;
* ) libc=glibc ;;
esac
tar="$platform-$arch-$libc"
# The action is determined by the name of the step.
case $DRONE_STEP_NAME in
*restore*) restore "$branch" "$DRONE_REPO_BRANCH" ;;
*rebuild*|*package*) package "$branch" ;;
*) exit 1 ;;
esac
cd "$(dirname "$0")/.."
# Get the branch for this build.
branch=${DRONE_BRANCH:-${DRONE_SOURCE_BRANCH:-${DRONE_TAG:-}}}
# The cache will be named based on the arch, platform, and libc.
arch=$DRONE_STAGE_ARCH
platform=${PLATFORM:-linux}
case $DRONE_STAGE_NAME in
*alpine*) libc=musl ;;
* ) libc=glibc ;;
esac
tar="$platform-$arch-$libc"
# The action is determined by the name of the step.
case $DRONE_STEP_NAME in
*restore*) restore "$branch" "$DRONE_REPO_BRANCH" ;;
*rebuild*|*package*) package "$branch" ;;
*) exit 1 ;;
esac
}
main "$@"
......@@ -3,71 +3,62 @@
set -euo pipefail
function target() {
local os=$(uname | tr '[:upper:]' '[:lower:]')
if [[ "$os" == "linux" ]]; then
# Using the same strategy to detect Alpine as build.ts.
local ldd_output=$(ldd --version 2>&1 || true)
if echo "$ldd_output" | grep -iq musl; then
os="alpine"
fi
fi
echo "${os}-$(uname -m)"
}
function main() {
cd "$(dirname "${0}")/.."
cd "$(dirname "${0}")/.."
local code_server_version=${VERSION:-${TRAVIS_TAG:-${DRONE_TAG:-}}}
if [[ -z $code_server_version ]] ; then
code_server_version=$(grep version ./package.json | head -1 | awk -F: '{ print $2 }' | sed 's/[",]//g' | tr -d '[:space:]')
fi
export VERSION=$code_server_version
# Get the version information. If a specific version wasn't set, generate it
# from the tag and VS Code version.
local vscode_version=${VSCODE_VERSION:-1.41.1}
local code_server_version=${VERSION:-${TRAVIS_TAG:-${DRONE_TAG:-daily}}}
YARN_CACHE_FOLDER="$(pwd)/yarn-cache"
export YARN_CACHE_FOLDER
# Remove everything that isn't the current VS Code source for caching
# (otherwise the cache will contain old versions).
if [[ -d "source/vscode-$vscode_version-source" ]] ; then
mv "source/vscode-$vscode_version-source" "vscode-$vscode_version-source"
fi
rm -rf source/vscode-*-source
if [[ -d "vscode-$vscode_version-source" ]] ; then
mv "vscode-$vscode_version-source" "source/vscode-$vscode_version-source"
fi
# Always minify and package on tags since that's when releases are pushed.
if [[ -n ${DRONE_TAG:-} || -n ${TRAVIS_TAG:-} ]] ; then
export MINIFY="true"
export PACKAGE="true"
fi
YARN_CACHE_FOLDER="$(pwd)/yarn-cache"
export YARN_CACHE_FOLDER
if [[ -z ${SKIP_YARN:-} ]] ; then
yarn
fi
# Always minify and package on tags since that's when releases are pushed.
if [[ -n ${DRONE_TAG:-} || -n ${TRAVIS_TAG:-} ]] ; then
export MINIFY="true"
export PACKAGE="true"
fi
yarn build
yarn binary
if [[ -n ${PACKAGE:-} ]] ; then
yarn package
fi
function run-yarn() {
yarn "$1" "$vscode_version" "$code_server_version"
}
cd binaries
run-yarn build
run-yarn binary
if [[ -n ${PACKAGE:-} ]] ; then
run-yarn package
fi
if [[ -n ${STRIP_BIN_TARGET:-} ]] ; then
# In this case provide plainly named binaries.
for binary in code-server* ; do
echo "Moving $binary to code-server"
mv "$binary" code-server
done
elif [[ -n ${DRONE_TAG:-} || -n ${TRAVIS_TAG:-} ]] ; then
# Prepare directory for uploading binaries on release.
for binary in code-server* ; do
mkdir -p "../binary-upload"
# In this case provide a plainly named "code-server" binary.
if [[ -n ${BINARY:-} ]] ; then
mv binaries/code-server*-vsc* binaries/code-server
fi
local prefix="code-server-$code_server_version-"
local target="${binary#$prefix}"
if [[ $target == "linux-x86_64" ]] ; then
echo "Copying $binary to ../binary-upload/latest-linux"
cp "$binary" "../binary-upload/latest-linux"
fi
# Prepare GCS bucket directory on release.
if [[ -n ${DRONE_TAG:-} || -n ${TRAVIS_TAG:-} ]] ; then
local gcp_dir="gcs_bucket/releases/$code_server_version/$(target)"
local gcp_dir
gcp_dir="../binary-upload/releases/$code_server_version/$target"
mkdir -p "$gcp_dir"
mkdir -p "$gcp_dir"
mv binaries/code-server*-vsc* "$gcp_dir"
if [[ "$(target)" == "linux-x86_64" ]] ; then
mv binaries/code-server*-vsc* "gcs_bucket/latest-linux"
fi
fi
echo "Copying $binary to $gcp_dir/code-server"
cp "$binary" "$gcp_dir/code-server"
done
fi
}
main "$@"
......@@ -2,24 +2,24 @@
FROM ubuntu:18.04
RUN apt-get update && apt-get install -y \
openssl \
net-tools \
git \
locales \
sudo \
dumb-init \
vim \
curl \
wget
openssl \
net-tools \
git \
locales \
sudo \
dumb-init \
vim \
curl \
wget
RUN locale-gen en_US.UTF-8
# We cannot use update-locale because docker will not use the env variables
# configured in /etc/default/locale so we need to set it manually.
ENV LC_ALL=en_US.UTF-8 \
SHELL=/bin/bash
SHELL=/bin/bash
RUN adduser --gecos '' --disabled-password coder && \
echo "coder ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/nopasswd
echo "coder ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/nopasswd
USER coder
# Create first so these directories will be owned by coder instead of root
......
#!/usr/bin/env sh
# lint.sh -- Lint CSS and JS files.
set -eu
main() {
yarn lint:css "$@"
yarn lint:js "$@"
}
main "$@"
// This must be ran from VS Code's root.
const gulp = require("gulp");
const path = require("path");
const _ = require("underscore");
const buildfile = require("./src/buildfile");
const common = require("./build/lib/optimize");
const util = require("./build/lib/util");
const deps = require("./build/dependencies");
const vscodeEntryPoints = _.flatten([
buildfile.entrypoint("vs/workbench/workbench.web.api"),
buildfile.entrypoint("vs/server/src/node/cli"),
buildfile.base,
buildfile.workbenchWeb,
buildfile.workerExtensionHost,
buildfile.keyboardMaps,
buildfile.entrypoint('vs/platform/files/node/watcher/unix/watcherApp', ["vs/css", "vs/nls"]),
buildfile.entrypoint('vs/platform/files/node/watcher/nsfw/watcherApp', ["vs/css", "vs/nls"]),
buildfile.entrypoint('vs/workbench/services/extensions/node/extensionHostProcess', ["vs/css", "vs/nls"]),
]);
const vscodeResources = [
"out-build/vs/server/main.js",
"out-build/vs/server/src/node/uriTransformer.js",
"!out-build/vs/server/doc/**",
"out-build/vs/server/src/media/*",
"out-build/vs/workbench/services/extensions/worker/extensionHostWorkerMain.js",
"out-build/bootstrap.js",
"out-build/bootstrap-fork.js",
"out-build/bootstrap-amd.js",
"out-build/paths.js",
'out-build/vs/**/*.{svg,png,html}',
"!out-build/vs/code/browser/workbench/*.html",
'!out-build/vs/code/electron-browser/**',
"out-build/vs/base/common/performance.js",
"out-build/vs/base/node/languagePacks.js",
"out-build/vs/base/browser/ui/octiconLabel/octicons/**",
"out-build/vs/base/browser/ui/codiconLabel/codicon/**",
"out-build/vs/workbench/browser/media/*-theme.css",
"out-build/vs/workbench/contrib/debug/**/*.json",
"out-build/vs/workbench/contrib/externalTerminal/**/*.scpt",
"out-build/vs/workbench/contrib/webview/browser/pre/*.js",
"out-build/vs/**/markdown.css",
"out-build/vs/workbench/contrib/tasks/**/*.json",
"out-build/vs/platform/files/**/*.md",
"!**/test/**"
];
const rootPath = __dirname;
const nodeModules = ["electron", "original-fs"]
.concat(_.uniq(deps.getProductionDependencies(rootPath).map((d) => d.name)))
.concat(_.uniq(deps.getProductionDependencies(path.join(rootPath, "src/vs/server")).map((d) => d.name)))
.concat(Object.keys(process.binding("natives")).filter((n) => !/^_|\//.test(n)));
gulp.task("optimize", gulp.series(
util.rimraf("out-vscode"),
common.optimizeTask({
src: "out-build",
entryPoints: vscodeEntryPoints,
resources: vscodeResources,
loaderConfig: common.loaderConfig(nodeModules),
out: "out-vscode",
inlineAmdImages: true,
bundleInfo: undefined
}),
));
gulp.task("minify", gulp.series(
util.rimraf("out-vscode-min"),
common.minifyTask("out-vscode")
));
{
"name": "code-server",
"main": "out/vs/server/main",
"desktopName": "code-server-url-handler.desktop"
}
#!/usr/bin/env sh
# postinstall.sh - Does nothing at the moment.
set -eu
main() {
cd "$(dirname "${0}")/.."
}
main "$@"
#!/usr/bin/env sh
# preinstall.sh -- Prepare VS Code.
set -eu
main() {
cd "$(dirname "${0}")/.."
# Ensure submodules are cloned and up to date.
git submodule update --init
# Try patching but don't worry too much if it fails. It's possible VS Code has
# already been patched.
yarn patch:apply || echo "Unable to patch; assuming already patched"
# Install VS Code dependencies.
cd ./lib/vscode
yarn
}
main "$@"
{
"nameShort": "code-server",
"nameLong": "code-server",
"applicationName": "code-server",
"dataFolderName": ".code-server",
"win32MutexName": "codeserver",
"win32DirName": "Code Server",
"win32NameVersion": "Code Server",
"win32RegValueName": "CodeServer",
"win32AppId": "",
"win32x64AppId": "",
"win32UserAppId": "",
"win32x64UserAppId": "",
"win32AppUserModelId": "CodeServer",
"win32ShellNameShort": "C&ode Server",
"darwinBundleIdentifier": "com.code.server",
"linuxIconName": "com.code.server",
"urlProtocol": "code-server",
"updateUrl": "https://api.github.com/repos/cdr/code-server/releases",
"quality": "latest"
}
#!/usr/bin/env sh
# test.sh -- Simple test for CI.
# We'll have more involved tests eventually. This just ensures the binary has
# been built and runs.
set -eu
main() {
cd "$(dirname "$0")/.."
version=$(./binaries/code-server* --version | head -1)
echo "Got '$version' for the version"
case $version in
*-vsc1.41.1) exit 0 ;;
*) exit 1 ;;
esac
}
main "$@"
{
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"noImplicitAny": true,
"experimentalDecorators": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noImplicitThis": true,
"alwaysStrict": true,
"strictBindCallApply": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"target": "esnext"
}
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"strict": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./out",
"declaration": true,
"experimentalDecorators": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"sourceMap": true
},
"include": [
"./**/*.ts"
]
}
此差异已折叠。
import * as vscode from "vscode";
import { CoderApi, VSCodeApi } from "../../typings/api";
import { createCSSRule } from "vs/base/browser/dom";
import { Emitter, Event } from "vs/base/common/event";
import { IDisposable } from "vs/base/common/lifecycle";
import { URI } from "vs/base/common/uri";
import { generateUuid } from "vs/base/common/uuid";
import { localize } from "vs/nls";
import { SyncActionDescriptor } from "vs/platform/actions/common/actions";
import { CommandsRegistry, ICommandService } from "vs/platform/commands/common/commands";
import { IConfigurationService } from "vs/platform/configuration/common/configuration";
import { IContextMenuService } from "vs/platform/contextview/browser/contextView";
import { FileDeleteOptions, FileOpenOptions, FileOverwriteOptions, FileSystemProviderCapabilities, FileType, FileWriteOptions, IFileChange, IFileService, IFileSystemProvider, IStat, IWatchOptions } from "vs/platform/files/common/files";
import { IInstantiationService, ServiceIdentifier } from "vs/platform/instantiation/common/instantiation";
import { ServiceCollection } from "vs/platform/instantiation/common/serviceCollection";
import { INotificationService } from "vs/platform/notification/common/notification";
import { Registry } from "vs/platform/registry/common/platform";
import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from "vs/workbench/services/statusbar/common/statusbar";
import { IStorageService } from "vs/platform/storage/common/storage";
import { ITelemetryService } from "vs/platform/telemetry/common/telemetry";
import { IThemeService } from "vs/platform/theme/common/themeService";
import { IWorkspaceContextService } from "vs/platform/workspace/common/workspace";
import * as extHostTypes from "vs/workbench/api/common/extHostTypes";
import { CustomTreeView, CustomTreeViewPane } from "vs/workbench/browser/parts/views/customView";
import { ViewContainerViewlet } from "vs/workbench/browser/parts/views/viewsViewlet";
import { Extensions as ViewletExtensions, ShowViewletAction, ViewletDescriptor, ViewletRegistry } from "vs/workbench/browser/viewlet";
import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from "vs/workbench/common/actions";
import { Extensions as ViewsExtensions, ITreeItem, ITreeViewDataProvider, ITreeViewDescriptor, IViewContainersRegistry, IViewsRegistry, TreeItemCollapsibleState } from "vs/workbench/common/views";
import { IEditorGroupsService } from "vs/workbench/services/editor/common/editorGroupsService";
import { IEditorService } from "vs/workbench/services/editor/common/editorService";
import { IExtensionService } from "vs/workbench/services/extensions/common/extensions";
import { IWorkbenchLayoutService } from "vs/workbench/services/layout/browser/layoutService";
import { IViewletService } from "vs/workbench/services/viewlet/browser/viewlet";
import { Application, ApplicationsResponse, CreateSessionResponse, FilesResponse, RecentResponse } from "../common/api"
import { ApiEndpoint, HttpCode, HttpError } from "../common/http"
export interface AuthBody {
password: string
}
/**
* Client-side implementation of VS Code's API.
* TODO: Views aren't quite working.
* TODO: Implement menu items for views (for item actions).
* TODO: File system provider doesn't work.
* Set authenticated status.
*/
export const vscodeApi = (serviceCollection: ServiceCollection): VSCodeApi => {
const getService = <T>(id: ServiceIdentifier<T>): T => serviceCollection.get<T>(id) as T;
const commandService = getService(ICommandService);
const notificationService = getService(INotificationService);
const fileService = getService(IFileService);
const viewsRegistry = Registry.as<IViewsRegistry>(ViewsExtensions.ViewsRegistry);
const statusbarService = getService(IStatusbarService);
// It would be nice to just export what VS Code creates but it looks to me
// that it assumes it's running in the extension host and wouldn't work here.
// It is probably possible to create an extension host that runs in the
// browser's main thread, but I'm not sure how much jank that would require.
// We could have a web worker host but we want DOM access.
return {
EventEmitter: <any>Emitter, // It can take T so T | undefined should work.
FileSystemError: extHostTypes.FileSystemError,
FileType,
StatusBarAlignment: extHostTypes.StatusBarAlignment,
ThemeColor: extHostTypes.ThemeColor,
TreeItemCollapsibleState: extHostTypes.TreeItemCollapsibleState,
Uri: URI,
commands: {
executeCommand: <T = any>(commandId: string, ...args: any[]): Promise<T | undefined> => {
return commandService.executeCommand(commandId, ...args);
},
registerCommand: (id: string, command: (...args: any[]) => any): IDisposable => {
return CommandsRegistry.registerCommand(id, command);
},
},
window: {
createStatusBarItem(alignmentOrOptions?: extHostTypes.StatusBarAlignment | vscode.window.StatusBarItemOptions, priority?: number): StatusBarEntry {
return new StatusBarEntry(statusbarService, alignmentOrOptions, priority);
},
registerTreeDataProvider: <T>(id: string, dataProvider: vscode.TreeDataProvider<T>): IDisposable => {
const tree = new TreeViewDataProvider(dataProvider);
const view = viewsRegistry.getView(id);
(view as ITreeViewDescriptor).treeView.dataProvider = tree;
return {
dispose: () => tree.dispose(),
};
},
showErrorMessage: async (message: string): Promise<string | undefined> => {
notificationService.error(message);
return undefined;
},
},
workspace: {
registerFileSystemProvider: (scheme: string, provider: vscode.FileSystemProvider): IDisposable => {
return fileService.registerProvider(scheme, new FileSystemProvider(provider));
},
},
};
};
export function setAuthed(authed: boolean): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(window as any).setAuthed(authed)
}
/**
* Coder API. This should only provide functionality that can't be made
* available through the VS Code API.
* Try making a request. Throw an error if the request is anything except OK.
* Also set authed to false if the request returns unauthorized.
*/
export const coderApi = (serviceCollection: ServiceCollection): CoderApi => {
const getService = <T>(id: ServiceIdentifier<T>): T => serviceCollection.get<T>(id) as T;
return {
registerView: (viewId, viewName, containerId, containerName, icon): void => {
const cssClass = `extensionViewlet-${containerId}`;
const id = `workbench.view.extension.${containerId}`;
class CustomViewlet extends ViewContainerViewlet {
public constructor(
@IConfigurationService configurationService: IConfigurationService,
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
@ITelemetryService telemetryService: ITelemetryService,
@IWorkspaceContextService contextService: IWorkspaceContextService,
@IStorageService storageService: IStorageService,
@IEditorService _editorService: IEditorService,
@IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService,
@IContextMenuService contextMenuService: IContextMenuService,
@IExtensionService extensionService: IExtensionService,
) {
super(id, `${id}.state`, true, configurationService, layoutService, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService);
}
}
Registry.as<ViewletRegistry>(ViewletExtensions.Viewlets).registerViewlet(
ViewletDescriptor.create(CustomViewlet as any, id, containerName, cssClass, undefined, URI.parse(icon)),
);
Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions).registerWorkbenchAction(
SyncActionDescriptor.create(OpenCustomViewletAction as any, id, localize("showViewlet", "Show {0}", containerName)),
"View: Show {0}",
localize("view", "View"),
);
// Generate CSS to show the icon in the activity bar.
const iconClass = `.monaco-workbench .activitybar .monaco-action-bar .action-label.${cssClass}`;
createCSSRule(iconClass, `-webkit-mask: url('${icon}') no-repeat 50% 50%`);
const container = Registry.as<IViewContainersRegistry>(ViewsExtensions.ViewContainersRegistry).registerViewContainer(containerId);
Registry.as<IViewsRegistry>(ViewsExtensions.ViewsRegistry).registerViews([{
id: viewId,
name: viewName,
ctorDescriptor: { ctor: CustomTreeViewPane },
treeView: getService(IInstantiationService).createInstance(CustomTreeView as any, viewId, container),
}] as ITreeViewDescriptor[], container);
},
};
};
class OpenCustomViewletAction extends ShowViewletAction {
public constructor(
id: string, label: string,
@IViewletService viewletService: IViewletService,
@IEditorGroupsService editorGroupService: IEditorGroupsService,
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
) {
super(id, label, id, viewletService, editorGroupService, layoutService);
}
const tryRequest = async (endpoint: string, options?: RequestInit): Promise<Response> => {
const response = await fetch("/api" + endpoint + "/", options)
if (response.status === HttpCode.Unauthorized) {
setAuthed(false)
}
if (response.status !== HttpCode.Ok) {
const text = await response.text()
throw new HttpError(text || response.statusText || "unknown error", response.status)
}
return response
}
class FileSystemProvider implements IFileSystemProvider {
private readonly _onDidChange = new Emitter<IFileChange[]>();
public readonly onDidChangeFile: Event<IFileChange[]> = this._onDidChange.event;
public readonly capabilities: FileSystemProviderCapabilities;
public readonly onDidChangeCapabilities: Event<void> = Event.None;
public constructor(private readonly provider: vscode.FileSystemProvider) {
this.capabilities = FileSystemProviderCapabilities.Readonly;
}
public watch(resource: URI, opts: IWatchOptions): IDisposable {
return this.provider.watch(resource, opts);
}
public async stat(resource: URI): Promise<IStat> {
return this.provider.stat(resource);
}
public async readFile(resource: URI): Promise<Uint8Array> {
return this.provider.readFile(resource);
}
public async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
return this.provider.writeFile(resource, content, opts);
}
public async delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
return this.provider.delete(resource, opts);
}
public mkdir(_resource: URI): Promise<void> {
throw new Error("not implemented");
}
public async readdir(resource: URI): Promise<[string, FileType][]> {
return this.provider.readDirectory(resource);
}
public async rename(resource: URI, target: URI, opts: FileOverwriteOptions): Promise<void> {
return this.provider.rename(resource, target, opts);
}
public async copy(resource: URI, target: URI, opts: FileOverwriteOptions): Promise<void> {
return this.provider.copy!(resource, target, opts);
}
public open(_resource: URI, _opts: FileOpenOptions): Promise<number> {
throw new Error("not implemented");
}
public close(_fd: number): Promise<void> {
throw new Error("not implemented");
}
public read(_fd: number, _pos: number, _data: Uint8Array, _offset: number, _length: number): Promise<number> {
throw new Error("not implemented");
}
public write(_fd: number, _pos: number, _data: Uint8Array, _offset: number, _length: number): Promise<number> {
throw new Error("not implemented");
}
/**
* Try authenticating.
*/
export const authenticate = async (body?: AuthBody): Promise<void> => {
let formBody: URLSearchParams | undefined
if (body) {
formBody = new URLSearchParams()
formBody.append("password", body.password)
}
const response = await tryRequest(ApiEndpoint.login, {
method: "POST",
body: formBody,
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
},
})
const json = await response.json()
if (json && json.success) {
setAuthed(true)
}
}
class TreeViewDataProvider<T> implements ITreeViewDataProvider {
private readonly root = Symbol("root");
private readonly values = new Map<string, T>();
private readonly children = new Map<T | Symbol, ITreeItem[]>();
public constructor(private readonly provider: vscode.TreeDataProvider<T>) {}
public async getChildren(item?: ITreeItem): Promise<ITreeItem[]> {
const value = item && this.itemToValue(item);
const children = await Promise.all(
(await this.provider.getChildren(value) || [])
.map(async (childValue) => {
const treeItem = await this.provider.getTreeItem(childValue);
const handle = this.createHandle(treeItem);
this.values.set(handle, childValue);
return {
handle,
collapsibleState: TreeItemCollapsibleState.Collapsed,
};
})
);
this.clear(value || this.root, item);
this.children.set(value || this.root, children);
return children;
}
public dispose(): void {
throw new Error("not implemented");
}
private itemToValue(item: ITreeItem): T {
if (!this.values.has(item.handle)) {
throw new Error(`No element found with handle ${item.handle}`);
}
return this.values.get(item.handle)!;
}
private clear(value: T | Symbol, item?: ITreeItem): void {
if (this.children.has(value)) {
this.children.get(value)!.map((c) => this.clear(this.itemToValue(c), c));
this.children.delete(value);
}
if (item) {
this.values.delete(item.handle);
}
}
private createHandle(item: vscode.TreeItem): string {
return item.id
? `coder-tree-item-id/${item.id}`
: `coder-tree-item-uuid/${generateUuid()}`;
}
export const getFiles = async (): Promise<FilesResponse> => {
const response = await tryRequest(ApiEndpoint.files)
return response.json()
}
interface IStatusBarEntry extends IStatusbarEntry {
alignment: StatusbarAlignment;
priority?: number;
export const getRecent = async (): Promise<RecentResponse> => {
const response = await tryRequest(ApiEndpoint.recent)
return response.json()
}
class StatusBarEntry implements vscode.StatusBarItem {
private static ID = 0;
private _id: number;
private entry: IStatusBarEntry;
private visible?: boolean;
private disposed?: boolean;
private statusId: string;
private statusName: string;
private accessor?: IStatusbarEntryAccessor;
private timeout: any;
constructor(private readonly statusbarService: IStatusbarService, alignmentOrOptions?: extHostTypes.StatusBarAlignment | vscode.window.StatusBarItemOptions, priority?: number) {
this._id = StatusBarEntry.ID--;
if (alignmentOrOptions && typeof alignmentOrOptions !== "number") {
this.statusId = alignmentOrOptions.id;
this.statusName = alignmentOrOptions.name;
this.entry = {
alignment: alignmentOrOptions.alignment === extHostTypes.StatusBarAlignment.Right
? StatusbarAlignment.RIGHT : StatusbarAlignment.LEFT,
priority,
text: "",
};
} else {
this.statusId = "web-api";
this.statusName = "Web API";
this.entry = {
alignment: alignmentOrOptions === extHostTypes.StatusBarAlignment.Right
? StatusbarAlignment.RIGHT : StatusbarAlignment.LEFT,
priority,
text: "",
};
}
}
public get alignment(): extHostTypes.StatusBarAlignment {
return this.entry.alignment === StatusbarAlignment.RIGHT
? extHostTypes.StatusBarAlignment.Right : extHostTypes.StatusBarAlignment.Left;
}
public get id(): number { return this._id; }
public get priority(): number | undefined { return this.entry.priority; }
public get text(): string { return this.entry.text; }
public get tooltip(): string | undefined { return this.entry.tooltip; }
public get color(): string | extHostTypes.ThemeColor | undefined { return this.entry.color; }
public get command(): string | undefined { return this.entry.command; }
public set text(text: string) { this.update({ text }); }
public set tooltip(tooltip: string | undefined) { this.update({ tooltip }); }
public set color(color: string | extHostTypes.ThemeColor | undefined) { this.update({ color }); }
public set command(command: string | undefined) { this.update({ command }); }
public show(): void {
this.visible = true;
this.update();
}
public hide(): void {
clearTimeout(this.timeout);
this.visible = false;
if (this.accessor) {
this.accessor.dispose();
this.accessor = undefined;
}
}
export const getApplications = async (): Promise<ApplicationsResponse> => {
const response = await tryRequest(ApiEndpoint.applications)
return response.json()
}
private update(values?: Partial<IStatusBarEntry>): void {
this.entry = { ...this.entry, ...values };
if (this.disposed || !this.visible) {
return;
}
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
if (!this.accessor) {
this.accessor = this.statusbarService.addEntry(this.entry, this.statusId, this.statusName, this.entry.alignment, this.priority);
} else {
this.accessor.update(this.entry);
}
}, 0);
}
export const getSession = async (app: Application): Promise<CreateSessionResponse> => {
const response = await tryRequest(ApiEndpoint.session, {
method: "POST",
body: JSON.stringify(app),
})
return response.json()
}
public dispose(): void {
this.hide();
this.disposed = true;
}
export const killSession = async (app: Application): Promise<Response> => {
return tryRequest(ApiEndpoint.session, {
method: "DELETE",
body: JSON.stringify(app),
})
}
html,
body,
#root,
iframe {
height: 100%;
width: 100%;
}
iframe {
border: none;
}
body {
background: #272727;
margin: 0;
font-family: 'IBM Plex Sans', sans-serif;
overflow: hidden;
}
import * as React from "react"
import { Application } from "../common/api"
import { Route, Switch } from "react-router-dom"
import { HttpError } from "../common/http"
import { Modal } from "./components/modal"
import { getOptions } from "../common/util"
const App: React.FunctionComponent = () => {
const [authed, setAuthed] = React.useState<boolean>(false)
const [app, setApp] = React.useState<Application>()
const [error, setError] = React.useState<HttpError | Error | string>()
React.useEffect(() => {
getOptions()
}, [])
if (typeof window !== "undefined") {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(window as any).setAuthed = setAuthed
}
return (
<>
<Switch>
<Route path="/vscode" render={(): React.ReactElement => <iframe id="iframe" src="/vscode-embed"></iframe>} />
<Route
path="/"
render={(): React.ReactElement => (
<Modal app={app} setApp={setApp} authed={authed} error={error} setError={setError} />
)}
/>
</Switch>
</>
)
}
export default App
import { Emitter } from "vs/base/common/event";
import { URI } from "vs/base/common/uri";
import { localize } from "vs/nls";
import { Extensions, IConfigurationRegistry } from "vs/platform/configuration/common/configurationRegistry";
import { registerSingleton } from "vs/platform/instantiation/common/extensions";
import { ServiceCollection } from "vs/platform/instantiation/common/serviceCollection";
import { ILocalizationsService } from "vs/platform/localizations/common/localizations";
import { INotificationService, Severity } from "vs/platform/notification/common/notification";
import { Registry } from "vs/platform/registry/common/platform";
import { PersistentConnectionEventType } from "vs/platform/remote/common/remoteAgentConnection";
import { ITelemetryService } from "vs/platform/telemetry/common/telemetry";
import { coderApi, vscodeApi } from "vs/server/src/browser/api";
import { INodeProxyService, NodeProxyChannelClient } from "vs/server/src/common/nodeProxy";
import { TelemetryChannelClient } from "vs/server/src/common/telemetry";
import { split } from "vs/server/src/common/util";
import "vs/workbench/contrib/localizations/browser/localizations.contribution";
import { LocalizationsService } from "vs/workbench/services/localizations/electron-browser/localizationsService";
import { IRemoteAgentService } from "vs/workbench/services/remote/common/remoteAgentService";
class TelemetryService extends TelemetryChannelClient {
public constructor(
@IRemoteAgentService remoteAgentService: IRemoteAgentService,
) {
super(remoteAgentService.getConnection()!.getChannel("telemetry"));
}
}
const TELEMETRY_SECTION_ID = "telemetry";
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
"id": TELEMETRY_SECTION_ID,
"order": 110,
"type": "object",
"title": localize("telemetryConfigurationTitle", "Telemetry"),
"properties": {
"telemetry.enableTelemetry": {
"type": "boolean",
"description": localize("telemetry.enableTelemetry", "Enable usage data and errors to be sent to a Microsoft online service."),
"default": true,
"tags": ["usesOnlineServices"]
}
}
});
class NodeProxyService extends NodeProxyChannelClient implements INodeProxyService {
private readonly _onClose = new Emitter<void>();
public readonly onClose = this._onClose.event;
private readonly _onDown = new Emitter<void>();
public readonly onDown = this._onDown.event;
private readonly _onUp = new Emitter<void>();
public readonly onUp = this._onUp.event;
public constructor(
@IRemoteAgentService remoteAgentService: IRemoteAgentService,
) {
super(remoteAgentService.getConnection()!.getChannel("nodeProxy"));
remoteAgentService.getConnection()!.onDidStateChange((state) => {
switch (state.type) {
case PersistentConnectionEventType.ConnectionGain:
return this._onUp.fire();
case PersistentConnectionEventType.ConnectionLost:
return this._onDown.fire();
case PersistentConnectionEventType.ReconnectionPermanentFailure:
return this._onClose.fire();
}
});
}
}
registerSingleton(ILocalizationsService, LocalizationsService);
registerSingleton(INodeProxyService, NodeProxyService);
registerSingleton(ITelemetryService, TelemetryService);
/**
* This is called by vs/workbench/browser/web.main.ts after the workbench has
* been initialized so we can initialize our own client-side code.
*/
export const initialize = async (services: ServiceCollection): Promise<void> => {
const target = window as any;
target.ide = coderApi(services);
target.vscode = vscodeApi(services);
const event = new CustomEvent("ide-ready");
(event as any).ide = target.ide;
(event as any).vscode = target.vscode;
window.dispatchEvent(event);
if (!window.isSecureContext) {
(services.get(INotificationService) as INotificationService).notify({
severity: Severity.Warning,
message: "code-server is being accessed over an insecure domain. Some functionality may not work as expected.",
actions: {
primary: [{
id: "understand",
label: "I understand",
tooltip: "",
class: undefined,
enabled: true,
checked: true,
dispose: () => undefined,
run: () => {
return Promise.resolve();
}
}],
}
});
}
};
export interface Query {
[key: string]: string | undefined;
}
/**
* Return the URL modified with the specified query variables. It's pretty
* stupid so it probably doesn't cover any edge cases. Undefined values will
* unset existing values. Doesn't allow duplicates.
*/
export const withQuery = (url: string, replace: Query): string => {
const uri = URI.parse(url);
const query = { ...replace };
uri.query.split("&").forEach((kv) => {
const [key, value] = split(kv, "=");
if (!(key in query)) {
query[key] = value;
}
});
return uri.with({
query: Object.keys(query)
.filter((k) => typeof query[k] !== "undefined")
.map((k) => `${k}=${query[k]}`).join("&"),
}).toString(true);
};
import * as React from "react"
export interface DelayProps {
readonly show: boolean
readonly delay: number
}
export const Animate: React.FunctionComponent<DelayProps> = (props) => {
const [timer, setTimer] = React.useState<NodeJS.Timeout>()
const [mount, setMount] = React.useState<boolean>(false)
const [visible, setVisible] = React.useState<boolean>(false)
React.useEffect(() => {
if (timer) {
clearTimeout(timer)
}
if (!props.show) {
setVisible(false)
setTimer(setTimeout(() => setMount(false), props.delay))
} else {
setTimer(setTimeout(() => setVisible(true), props.delay))
setMount(true)
}
}, [props])
return mount ? <div className={`animate -${visible ? "show" : "hide"}`}>{props.children}</div> : null
}
.field-error {
color: red;
}
.request-error {
align-items: center;
color: rgba(0, 0, 0, 0.37);
display: flex;
flex: 1;
flex-direction: column;
font-weight: 700;
justify-content: center;
padding: 20px;
text-transform: uppercase;
}
.request-error > .close {
background: transparent;
border: none;
color: #b6b6b6;
cursor: pointer;
margin-top: 10px;
width: 100%;
}
.request-error + .request-error {
border-top: 1px solid #b6b6b6;
}
import * as React from "react"
import { HttpError } from "../../common/http"
export interface ErrorProps {
error: HttpError | Error | string
onClose?: () => void
}
/**
* An error to be displayed in a section where a request has failed.
*/
export const RequestError: React.FunctionComponent<ErrorProps> = (props) => {
return (
<div className="request-error">
<div className="error">{typeof props.error === "string" ? props.error : props.error.message}</div>
{props.onClose ? (
<button className="close" onClick={props.onClose}>
Go Back
</button>
) : (
undefined
)}
</div>
)
}
/**
* Return a more human/natural/useful message for some error codes resulting
* from a form submission.
*/
const humanizeFormError = (error: HttpError | Error | string): string => {
if (typeof error === "string") {
return error
}
switch ((error as HttpError).code) {
case 401:
return "Wrong password"
default:
return error.message
}
}
/**
* An error to be displayed underneath a field.
*/
export const FieldError: React.FunctionComponent<ErrorProps> = (props) => {
return <div className="field-error">{humanizeFormError(props.error)}</div>
}
.app-list {
list-style-type: none;
padding: 0;
margin: 0 -10px; /* To counter app padding. */
flex: 1;
}
.app-loader {
align-items: center;
color: #b6b6b6;
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
}
.app-loader > .loader {
color: #b6b6b6;
}
.app-row {
color: #b6b6b6;
cursor: pointer;
display: flex;
font-size: 1em;
line-height: 1em;
width: 100%;
}
.app-row > .launch,
.app-row > .kill {
background-color: transparent;
border: none;
color: inherit;
cursor: pointer;
font-size: 1em;
line-height: 1em;
margin: 1px 0;
padding: 3px 10px;
}
.app-row > .launch {
border-radius: 50px;
display: flex;
flex: 1;
}
.app-row > .launch:hover,
.app-row > .kill:hover {
color: #000;
}
.app-row .icon {
height: 1em;
margin-right: 5px;
width: 1em;
}
.app-row .icon.-missing {
background-color: #eee;
color: #b6b6b6;
text-align: center;
}
.app-row .icon.-missing::after {
content: "?";
font-size: 0.7em;
vertical-align: middle;
}
.app-row.-selected {
background-color: #bcc6fa;
}
.app-loader > .opening {
margin-bottom: 10px;
}
.app-loader > .app-row {
color: #000;
justify-content: center;
}
.app-loader > .cancel {
background: transparent;
border: none;
color: #b6b6b6;
cursor: pointer;
margin-top: 10px;
width: 100%;
}
.app-list + .app-list {
border-top: 1px solid #b6b6b6;
margin-top: 1em;
padding-top: 1em;
}
.app-list > .header {
color: #b6b6b6;
font-size: 1em;
margin-bottom: 1em;
margin-top: 0;
}
.app-list > .loader {
color: #b6b6b6;
}
import * as React from "react"
import { Application, isExecutableApplication, isRunningApplication } from "../../common/api"
import { HttpError } from "../../common/http"
import { getSession, killSession } from "../api"
import { RequestError } from "../components/error"
export const AppDetails: React.FunctionComponent<Application> = (props) => {
return (
<>
{props.icon ? (
<img className="icon" src={`data:image/png;base64,${props.icon}`}></img>
) : (
<div className="icon -missing"></div>
)}
<div className="name">{props.name}</div>
</>
)
}
export interface AppRowProps {
readonly app: Application
onKilled(app: Application): void
open(app: Application): void
}
export const AppRow: React.FunctionComponent<AppRowProps> = (props) => {
const [killing, setKilling] = React.useState<boolean>(false)
const [error, setError] = React.useState<HttpError>()
function kill(): void {
if (isRunningApplication(props.app)) {
setKilling(true)
killSession(props.app)
.then(() => {
setKilling(false)
props.onKilled(props.app)
})
.catch((error) => {
setError(error)
setKilling(false)
})
}
}
return (
<div className="app-row">
<button className="launch" onClick={(): void => props.open(props.app)}>
<AppDetails {...props.app} />
</button>
{isRunningApplication(props.app) && !killing ? (
<button className="kill" onClick={(): void => kill()}>
{error ? error.message : killing ? "..." : "kill"}
</button>
) : (
undefined
)}
</div>
)
}
export interface AppListProps {
readonly header: string
readonly apps?: ReadonlyArray<Application>
open(app: Application): void
onKilled(app: Application): void
}
export const AppList: React.FunctionComponent<AppListProps> = (props) => {
return (
<div className="app-list">
<h2 className="header">{props.header}</h2>
{props.apps && props.apps.length > 0 ? (
props.apps.map((app, i) => <AppRow key={i} app={app} {...props} />)
) : props.apps ? (
<RequestError error={`No ${props.header.toLowerCase()} found`} />
) : (
<div className="loader">loading...</div>
)}
</div>
)
}
export interface ApplicationSection {
readonly apps?: ReadonlyArray<Application>
readonly header: string
}
export interface AppLoaderProps {
readonly app?: Application
setApp(app?: Application): void
getApps(): Promise<ReadonlyArray<ApplicationSection>>
}
/**
* Display provided applications or sessions and allow opening them.
*/
export const AppLoader: React.FunctionComponent<AppLoaderProps> = (props) => {
const [apps, setApps] = React.useState<ReadonlyArray<ApplicationSection>>()
const [error, setError] = React.useState<HttpError>()
const refresh = (): void => {
props
.getApps()
.then(setApps)
.catch((e) => setError(e.message))
}
React.useEffect(() => {
refresh()
}, [props])
function open(app: Application): void {
props.setApp(app)
if (!isRunningApplication(app) && isExecutableApplication(app)) {
getSession(app)
.then((session) => {
props.setApp({ ...app, ...session })
})
.catch(setError)
}
}
if (error) {
props.setApp(undefined)
return (
<RequestError
error={error}
onClose={(): void => {
setError(undefined)
}}
/>
)
}
if (props.app && !props.app.loaded) {
return (
<div className="app-loader">
<div className="opening">Opening</div>
<div className="app-row">
<AppDetails {...props.app} />
</div>
<button
className="cancel"
onClick={(): void => {
props.setApp(undefined)
}}
>
Cancel
</button>
</div>
)
}
if (!apps) {
return (
<div className="app-loader">
<div className="loader">loading</div>
</div>
)
}
return (
<>
{apps.map((section, i) => (
<AppList key={i} open={open} onKilled={refresh} {...section} />
))}
</>
)
}
.modal-bar {
box-sizing: border-box;
display: flex;
justify-content: center;
left: 0;
padding: 20px;
position: fixed;
pointer-events: none;
top: 0;
width: 100%;
z-index: 30;
}
.animate > .modal-bar {
transform: translateY(-100%);
transition: transform 200ms;
}
.animate.-show > .modal-bar {
transform: translateY(0);
}
.modal-bar > .bar {
background-color: #fcfcfc;
border-radius: 5px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
color: #101010;
display: flex;
font-size: 0.8em;
max-width: 400px;
padding: 20px;
pointer-events: initial;
position: relative;
}
.modal-bar > .bar > .content {
display: flex;
flex-direction: column;
flex: 1;
justify-content: center;
padding-right: 20px;
}
.modal-bar > .bar > .open {
display: flex;
flex-direction: column;
justify-content: center;
}
.modal-bar > .bar > .close {
background-color: transparent;
border: none;
color: #b6b6b6;
cursor: pointer;
position: absolute;
right: 1px;
top: 1px;
}
.modal-bar > .bar > .open > .button {
background-color: transparent;
border-radius: 5px;
border: 1px solid #101010;
color: #101010;
cursor: pointer;
padding: 1em;
}
.modal-bar > .bar > .open > .button:hover {
background-color: #bcc6fa;
}
.modal-container {
align-items: center;
background: rgba(0, 0, 0, 0.1);
box-sizing: border-box;
display: flex;
height: 100%;
justify-content: center;
left: 0;
padding: 20px;
position: fixed;
top: 0;
width: 100%;
z-index: 9999999;
}
.modal-container > .modal {
background: #fcfcfc;
border-radius: 10px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: row;
height: 100%;
max-height: 400px;
max-width: 664px;
padding: 20px 0;
position: relative;
width: 100%;
}
.modal-container > .modal > .sidebar {
border-right: 1.5px solid rgba(0, 0, 0, 0.37);
display: flex;
flex-direction: column;
justify-content: space-between;
}
.modal-container > .modal > .sidebar > .links {
display: flex;
flex-direction: column;
}
.modal-container > .modal > .sidebar > .links > .link {
color: rgba(0, 0, 0, 0.37);
font-size: 1.4em;
height: 31px;
margin-bottom: 20px;
padding: 0 35px;
text-decoration: none;
transition: 150ms color ease, 150ms height ease, 150ms margin-bottom ease;
}
.modal-container > .modal > .sidebar > .footer > .close {
background: transparent;
border: none;
color: #b6b6b6;
cursor: pointer;
width: 100%;
}
.modal-container > .modal > .sidebar > .footer > .close:hover {
color: #000;
}
.modal-container > .modal > .links > .link[aria-current="page"] {
color: rgba(0, 0, 0, 1);
}
.modal-container > .modal > .content {
display: flex;
flex: 1;
flex-direction: column;
overflow: auto;
padding: 0 20px;
}
import { logger } from "@coder/logger"
import * as React from "react"
import { NavLink, Route, RouteComponentProps, Switch } from "react-router-dom"
import { Application, isExecutableApplication } from "../../common/api"
import { HttpError } from "../../common/http"
import { RequestError } from "../components/error"
import { Browse } from "../pages/browse"
import { Home } from "../pages/home"
import { Login } from "../pages/login"
import { Open } from "../pages/open"
import { Recent } from "../pages/recent"
import { Animate } from "./animate"
export interface ModalProps {
app?: Application
authed: boolean
error?: HttpError | Error | string
setApp(app?: Application): void
setError(error?: HttpError | Error | string): void
}
export const Modal: React.FunctionComponent<ModalProps> = (props) => {
const [showModal, setShowModal] = React.useState<boolean>(false)
const [showBar, setShowBar] = React.useState<boolean>(true)
const setApp = (app: Application): void => {
setShowModal(false)
props.setApp(app)
}
React.useEffect(() => {
// Show the bar when hovering around the top area for a while.
let timeout: NodeJS.Timeout | undefined
const hover = (clientY: number): void => {
if (clientY > 30 && timeout) {
clearTimeout(timeout)
timeout = undefined
} else if (clientY <= 30 && !timeout) {
timeout = setTimeout(() => setShowBar(true), 1000)
}
}
const iframe =
props.app && !isExecutableApplication(props.app) && (document.getElementById("iframe") as HTMLIFrameElement)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const postIframeMessage = (message: any): void => {
if (iframe && iframe.contentWindow) {
iframe.contentWindow.postMessage(message, window.location.origin)
} else {
logger.warn("Tried to post message to missing iframe")
}
}
const onHover = (event: MouseEvent | MessageEvent): void => {
hover((event as MessageEvent).data ? (event as MessageEvent).data.clientY : (event as MouseEvent).clientY)
}
const onIframeLoaded = (): void => {
if (props.app) {
setApp({ ...props.app, loaded: true })
}
}
// No need to track the mouse if we don't have a hidden bar.
const hasHiddenBar = !props.error && !showModal && props.app && !showBar
if (props.app && !isExecutableApplication(props.app)) {
// Once the iframe reports it has loaded, tell it to bind mousemove and
// start listening for that instead.
if (!props.app.loaded) {
window.addEventListener("message", onIframeLoaded)
} else if (hasHiddenBar) {
postIframeMessage({ bind: "mousemove", prop: "clientY" })
window.removeEventListener("message", onIframeLoaded)
window.addEventListener("message", onHover)
}
} else if (hasHiddenBar) {
document.addEventListener("mousemove", onHover)
}
return (): void => {
document.removeEventListener("mousemove", onHover)
window.removeEventListener("message", onHover)
window.removeEventListener("message", onIframeLoaded)
if (props.app && !isExecutableApplication(props.app)) {
postIframeMessage({ unbind: "mousemove" })
}
if (timeout) {
clearTimeout(timeout)
}
}
}, [showBar, props.error, showModal, props.app])
return props.error || showModal || !props.app || !props.app.loaded ? (
<div className="modal-container">
<div className="modal">
{props.authed && (!props.app || props.app.loaded) ? (
<aside className="sidebar">
<nav className="links">
{!props.authed ? (
<NavLink className="link" to="/login">
Login
</NavLink>
) : (
undefined
)}
{props.authed ? (
<NavLink className="link" exact to="/recent/">
Recent
</NavLink>
) : (
undefined
)}
{props.authed ? (
<NavLink className="link" exact to="/open/">
Open
</NavLink>
) : (
undefined
)}
{props.authed ? (
<NavLink className="link" exact to="/browse/">
Browse
</NavLink>
) : (
undefined
)}
</nav>
<div className="footer">
{props.app && props.app.loaded && !props.error ? (
<button className="close" onClick={(): void => setShowModal(false)}>
Close
</button>
) : (
undefined
)}
</div>
</aside>
) : (
undefined
)}
{props.error ? (
<RequestError
error={props.error}
onClose={(): void => {
props.setApp(undefined)
props.setError(undefined)
}}
/>
) : (
<div className="content">
<Switch>
<Route path="/login" component={Login} />
<Route
path="/recent"
render={(p: RouteComponentProps): React.ReactElement => (
<Recent app={props.app} setApp={setApp} {...p} />
)}
/>
<Route path="/browse" component={Browse} />
<Route
path="/open"
render={(p: RouteComponentProps): React.ReactElement => <Open app={props.app} setApp={setApp} {...p} />}
/>
<Route path="/" component={Home} />
</Switch>
</div>
)}
</div>
</div>
) : (
<Animate show={showBar} delay={200}>
<div className="modal-bar">
<div className="bar">
<div className="content">
<div className="help">
Hover at the top {/*or press <strong>Ctrl+Shift+G</strong>*/} to display this menu.
</div>
</div>
<div className="open">
<button className="button" onClick={(): void => setShowModal(true)}>
Open Modal
</button>
</div>
<button className="close" onClick={(): void => setShowBar(false)}>
x
</button>
</div>
</div>
</Animate>
)
}
import { Emitter } from "vs/base/common/event";
import { createDecorator } from "vs/platform/instantiation/common/instantiation";
import { ExtHostNodeProxyShape, MainContext, MainThreadNodeProxyShape } from "vs/workbench/api/common/extHost.protocol";
import { IExtHostRpcService } from "vs/workbench/api/common/extHostRpcService";
export class ExtHostNodeProxy implements ExtHostNodeProxyShape {
_serviceBrand: any;
private readonly _onMessage = new Emitter<string>();
public readonly onMessage = this._onMessage.event;
private readonly _onClose = new Emitter<void>();
public readonly onClose = this._onClose.event;
private readonly _onDown = new Emitter<void>();
public readonly onDown = this._onDown.event;
private readonly _onUp = new Emitter<void>();
public readonly onUp = this._onUp.event;
private readonly proxy: MainThreadNodeProxyShape;
constructor(@IExtHostRpcService rpc: IExtHostRpcService) {
this.proxy = rpc.getProxy(MainContext.MainThreadNodeProxy);
}
public $onMessage(message: string): void {
this._onMessage.fire(message);
}
public $onClose(): void {
this._onClose.fire();
}
public $onUp(): void {
this._onUp.fire();
}
public $onDown(): void {
this._onDown.fire();
}
public send(message: string): void {
this.proxy.$send(message);
}
}
export interface IExtHostNodeProxy extends ExtHostNodeProxy { }
export const IExtHostNodeProxy = createDecorator<IExtHostNodeProxy>("IExtHostNodeProxy");
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<!-- <meta http-equiv="Content-Security-Policy" content="font-src 'self'; connect-src 'self'; default-src ws: wss:; style-src 'self'; script-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:;"> -->
<title>code-server</title>
<link rel="icon" href="/static-{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
<link rel="manifest" href="/static-{{COMMIT}}/src/browser/media/manifest.json" crossorigin="use-credentials">
<link rel="apple-touch-icon" href="/static-{{COMMIT}}/src/browser/media/code-server.png" />
<link href="https://fonts.googleapis.com/css?family=IBM+Plex+Sans&display=swap" rel="stylesheet" />
<link href="/static-{{COMMIT}}/dist/index.css" rel="stylesheet">
<meta id="coder-options" data-settings="{{OPTIONS}}">
</head>
<body>
<div id="root">{{COMPONENT}}</div>
<script src="/static-{{COMMIT}}/dist/index.js"></script>
</body>
</html>
import * as React from "react"
import * as ReactDOM from "react-dom"
import App from "./app"
import { BrowserRouter } from "react-router-dom"
import "./app.css"
import "./pages/home.css"
import "./pages/login.css"
import "./components/error.css"
import "./components/list.css"
import "./components/modal.css"
ReactDOM.hydrate(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById("root")
)
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'self' 'unsafe-inline'; script-src 'unsafe-inline'; manifest-src 'self'; img-src 'self';">
<title>Authenticate: code-server</title>
<link rel="icon" href="./static/out/vs/server/src/media/favicon.ico" type="image/x-icon" />
<link rel="manifest" href="./manifest.json" crossorigin="use-credentials">
<link rel="apple-touch-icon" href="./static/out/vs/server/src/media/code-server.png" />
<meta name="apple-mobile-web-app-capable" content="yes">
<link href="./static/out/vs/server/src/media/login.css" rel="stylesheet">
</head>
<body>
<form class="login-form" method="post">
<h4 class="title">code-server</h4>
<h2 class="subtitle">
Enter server password
</h2>
<div class="field">
<!-- The onfocus code places the cursor at the end of the value. -->
<input name="password" type="password" class="input" value=""
required autofocus
onfocus="const value=this.value;this.value='';this.value=value;">
</div>
<button class="button" type="submit">
<span class="label">Enter IDE</span>
</button>
<div class="error-display" style="display:none">{{ERROR}}</div>
</form>
</body>
</html>
import { IDisposable } from "vs/base/common/lifecycle";
import { INodeProxyService } from "vs/server/src/common/nodeProxy";
import { ExtHostContext, IExtHostContext, MainContext, MainThreadNodeProxyShape } from "vs/workbench/api/common/extHost.protocol";
import { extHostNamedCustomer } from "vs/workbench/api/common/extHostCustomers";
@extHostNamedCustomer(MainContext.MainThreadNodeProxy)
export class MainThreadNodeProxy implements MainThreadNodeProxyShape {
private disposed = false;
private disposables = <IDisposable[]>[];
constructor(
extHostContext: IExtHostContext,
@INodeProxyService private readonly proxyService: INodeProxyService,
) {
if (!extHostContext.remoteAuthority) { // HACK: A terrible way to detect if running in the worker.
const proxy = extHostContext.getProxy(ExtHostContext.ExtHostNodeProxy);
this.disposables = [
this.proxyService.onMessage((message: string) => proxy.$onMessage(message)),
this.proxyService.onClose(() => proxy.$onClose()),
this.proxyService.onDown(() => proxy.$onDown()),
this.proxyService.onUp(() => proxy.$onUp()),
];
}
}
$send(message: string): void {
if (!this.disposed) {
this.proxyService.send(message);
}
}
dispose(): void {
this.disposables.forEach((d) => d.dispose());
this.disposables = [];
this.disposed = true;
}
}
{
"name": "code-server",
"short_name": "code-server",
"start_url": "../../../..",
"display": "fullscreen",
"background-color": "#fff",
"description": "Run editors on a remote server.",
"icons": [{
"src": "./code-server.png",
"sizes": "384x384",
"type": "image/png"
}]
}
import * as React from "react"
import { RouteComponentProps } from "react-router"
import { FilesResponse } from "../../common/api"
import { HttpError } from "../../common/http"
import { getFiles } from "../api"
import { RequestError } from "../components/error"
/**
* File browser.
*/
export const Browse: React.FunctionComponent<RouteComponentProps> = (props) => {
const [response, setResponse] = React.useState<FilesResponse>()
const [error, setError] = React.useState<HttpError>()
React.useEffect(() => {
getFiles()
.then(setResponse)
.catch((e) => setError(e.message))
}, [props])
return (
<>
{error || (response && response.files.length === 0) ? (
<RequestError error={error || "Empty directory"} />
) : (
<ul>
{((response && response.files) || []).map((f, i) => (
<li key={i}>{f.name}</li>
))}
</ul>
)}
</>
)
}
.orientation-guide {
align-items: center;
color: #b6b6b6;
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
}
import * as React from "react"
import { RouteComponentProps } from "react-router"
import { authenticate } from "../api"
export const Home: React.FunctionComponent<RouteComponentProps> = (props) => {
React.useEffect(() => {
authenticate()
.then(() => {
// TEMP: Always redirect to VS Code.
props.history.push("./vscode/")
})
.catch(() => {
props.history.push("./login/")
})
}, [])
return (
<div className="orientation-guide">
<div className="welcome">Welcome to code-server.</div>
</div>
)
}
.login-form {
align-items: center;
color: rgba(0, 0, 0, 0.37);
display: flex;
flex: 1;
flex-direction: column;
font-weight: 700;
justify-content: center;
text-transform: uppercase;
}
.login-form > .field {
display: flex;
flex-direction: row;
width: 100%;
}
.login-form > .field-error {
margin-top: 10px;
}
.login-form > .field > .input {
border: 1px solid #b6b6b6;
box-sizing: border-box;
padding: 10px;
flex: 1;
}
.login-form > .field > .submit {
background-color: transparent;
border: 1px solid #b6b6b6;
box-sizing: border-box;
margin-left: -1px;
padding: 10px 20px;
}
import * as React from "react"
import { RouteComponentProps } from "react-router"
import { HttpError } from "../../common/http"
import { authenticate } from "../api"
import { FieldError } from "../components/error"
/**
* Login page. Will redirect on success.
*/
export const Login: React.FunctionComponent<RouteComponentProps> = (props) => {
const [password, setPassword] = React.useState<string>("")
const [error, setError] = React.useState<HttpError>()
function redirect(): void {
// TEMP: Always redirect to VS Code.
console.log("is authed")
props.history.push("../vscode/")
// const params = new URLSearchParams(window.location.search)
// props.history.push(params.get("to") || "/")
}
async function handleSubmit(event: React.FormEvent<HTMLFormElement>): Promise<void> {
event.preventDefault()
authenticate({ password })
.then(redirect)
.catch(setError)
}
React.useEffect(() => {
authenticate()
.then(redirect)
.catch(() => {
// Do nothing; we're already at the login page.
})
}, [])
return (
<form className="login-form" onSubmit={handleSubmit}>
<div className="field">
<input
autoFocus
className="input"
type="password"
placeholder="password"
autoComplete="current-password"
onChange={(event: React.ChangeEvent<HTMLInputElement>): void => setPassword(event.target.value)}
/>
<button className="submit" type="submit">
Log In
</button>
</div>
{error ? <FieldError error={error} /> : undefined}
</form>
)
}
import * as React from "react"
import { Application } from "../../common/api"
import { getApplications } from "../api"
import { ApplicationSection, AppLoader } from "../components/list"
export interface OpenProps {
app?: Application
setApp(app: Application): void
}
/**
* Display recently used applications.
*/
export const Open: React.FunctionComponent<OpenProps> = (props) => {
return (
<AppLoader
getApps={async (): Promise<ReadonlyArray<ApplicationSection>> => {
const response = await getApplications()
return [
{
header: "Applications",
apps: response && response.applications,
},
]
}}
{...props}
/>
)
}
import * as React from "react"
import { Application } from "../../common/api"
import { getRecent } from "../api"
import { ApplicationSection, AppLoader } from "../components/list"
export interface RecentProps {
app?: Application
setApp(app: Application): void
}
/**
* Display recently used applications.
*/
export const Recent: React.FunctionComponent<RecentProps> = (props) => {
return (
<AppLoader
getApps={async (): Promise<ReadonlyArray<ApplicationSection>> => {
const response = await getRecent()
return [
{
header: "Running Applications",
apps: response && response.running,
},
{
header: "Recent Applications",
apps: response && response.recent,
},
]
}}
{...props}
/>
)
}
import { field, logger, Logger } from "@coder/logger"
import { Emitter } from "../common/emitter"
import { generateUuid } from "../common/util"
const decoder = new TextDecoder("utf8")
export const decode = (buffer: string | ArrayBuffer): string => {
return typeof buffer !== "string" ? decoder.decode(buffer) : buffer
}
/**
* A web socket that reconnects itself when it closes. Sending messages while
* disconnected will throw an error.
*/
export class ReconnectingSocket {
protected readonly _onMessage = new Emitter<string | ArrayBuffer>()
public readonly onMessage = this._onMessage.event
protected readonly _onDisconnect = new Emitter<number | undefined>()
public readonly onDisconnect = this._onDisconnect.event
protected readonly _onClose = new Emitter<number | undefined>()
public readonly onClose = this._onClose.event
protected readonly _onConnect = new Emitter<void>()
public readonly onConnect = this._onConnect.event
// This helps distinguish messages between sockets.
private readonly logger: Logger
private socket?: WebSocket
private connecting?: Promise<void>
private closed = false
private readonly openTimeout = 10000
// Every time the socket fails to connect, the retry will be increasingly
// delayed up to a maximum.
private readonly retryBaseDelay = 1000
private readonly retryMaxDelay = 10000
private retryDelay?: number
private readonly retryDelayFactor = 1.5
// The socket must be connected for this amount of time before resetting the
// retry delay. This prevents rapid retries when the socket does connect but
// is closed shortly after.
private resetRetryTimeout?: NodeJS.Timeout
private readonly resetRetryDelay = 10000
private _binaryType: typeof WebSocket.prototype.binaryType = "arraybuffer"
public constructor(private customPath?: string, public readonly id: string = generateUuid(4)) {
// On Firefox the socket seems to somehow persist a page reload so the close
// event runs and we see "attempting to reconnect".
if (typeof window !== "undefined") {
window.addEventListener("beforeunload", () => this.close())
}
this.logger = logger.named(this.id)
}
public set binaryType(b: typeof WebSocket.prototype.binaryType) {
this._binaryType = b
if (this.socket) {
this.socket.binaryType = b
}
}
/**
* Permanently close the connection. Will not attempt to reconnect. Will
* remove event listeners.
*/
public close(code?: number): void {
if (this.closed) {
return
}
if (code) {
this.logger.info(`closing with code ${code}`)
}
if (this.resetRetryTimeout) {
clearTimeout(this.resetRetryTimeout)
}
this.closed = true
if (this.socket) {
this.socket.close()
} else {
this._onClose.emit(code)
}
}
public dispose(): void {
this._onMessage.dispose()
this._onDisconnect.dispose()
this._onClose.dispose()
this._onConnect.dispose()
this.logger.debug("disposed handlers")
}
/**
* Send a message on the socket. Logs an error if currently disconnected.
*/
public send(message: string | ArrayBuffer): void {
this.logger.trace(() => ["sending message", field("message", decode(message))])
if (!this.socket) {
return logger.error("tried to send message on closed socket")
}
this.socket.send(message)
}
/**
* Connect to the socket. Can also be called to wait until the connection is
* established in the case of disconnections. Multiple calls will be handled
* correctly.
*/
public async connect(): Promise<void> {
if (!this.connecting) {
this.connecting = new Promise((resolve, reject) => {
const tryConnect = (): void => {
if (this.closed) {
return reject(new Error("disconnected")) // Don't keep trying if we've closed permanently.
}
if (typeof this.retryDelay === "undefined") {
this.retryDelay = 0
} else {
this.retryDelay = this.retryDelay * this.retryDelayFactor || this.retryBaseDelay
if (this.retryDelay > this.retryMaxDelay) {
this.retryDelay = this.retryMaxDelay
}
}
this._connect()
.then((socket) => {
this.logger.info("connected")
this.socket = socket
this.socket.binaryType = this._binaryType
if (this.resetRetryTimeout) {
clearTimeout(this.resetRetryTimeout)
}
this.resetRetryTimeout = setTimeout(() => (this.retryDelay = undefined), this.resetRetryDelay)
this.connecting = undefined
this._onConnect.emit()
resolve()
})
.catch((error) => {
this.logger.error(`failed to connect: ${error.message}`)
tryConnect()
})
}
tryConnect()
})
}
return this.connecting
}
private async _connect(): Promise<WebSocket> {
const socket = await new Promise<WebSocket>((resolve, _reject) => {
if (this.retryDelay) {
this.logger.info(`retrying in ${this.retryDelay}ms...`)
}
setTimeout(() => {
this.logger.info("connecting...")
const socket = new WebSocket(
`${location.protocol === "https:" ? "wss" : "ws"}://${location.host}${this.customPath || location.pathname}${
location.search ? `?${location.search}` : ""
}`
)
const reject = (): void => {
_reject(new Error("socket closed"))
}
const timeout = setTimeout(() => {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
socket.removeEventListener("open", open)
socket.removeEventListener("close", reject)
_reject(new Error("timeout"))
}, this.openTimeout)
const open = (): void => {
clearTimeout(timeout)
socket.removeEventListener("close", reject)
resolve(socket)
}
socket.addEventListener("open", open)
socket.addEventListener("close", reject)
}, this.retryDelay)
})
socket.addEventListener("message", (event) => {
this.logger.trace(() => ["got message", field("message", decode(event.data))])
this._onMessage.emit(event.data)
})
socket.addEventListener("close", (event) => {
this.socket = undefined
if (!this.closed) {
this._onDisconnect.emit(event.code)
// It might be closed in the event handler.
if (!this.closed) {
this.logger.info("connection closed; attempting to reconnect")
this.connect()
}
} else {
this._onClose.emit(event.code)
this.logger.info("connection closed permanently")
}
})
return socket
}
}
<!-- Copyright (C) Microsoft Corporation. All rights reserved. -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<!-- Disable pinch zooming -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<!-- Workbench Configuration -->
<meta id="vscode-workbench-web-configuration" data-settings="{{WORKBENCH_WEB_CONFIGURATION}}">
<!-- Workarounds/Hacks (remote user data uri) -->
<meta id="vscode-remote-user-data-uri" data-settings="{{REMOTE_USER_DATA_URI}}">
<!-- NOTE@coder: Added the commit for use in caching, the product for the
extensions gallery URL, and nls for language support. -->
<meta id="vscode-remote-commit" data-settings="{{COMMIT}}">
<meta id="vscode-remote-product-configuration" data-settings="{{PRODUCT_CONFIGURATION}}">
<meta id="vscode-remote-nls-configuration" data-settings="{{NLS_CONFIGURATION}}">
<!-- Workbench Icon/Manifest/CSS -->
<link rel="icon" href="./static-{{COMMIT}}/out/vs/server/src/media/favicon.ico" type="image/x-icon" />
<link rel="manifest" href="./manifest.json" crossorigin="use-credentials">
<link data-name="vs/workbench/workbench.web.api" rel="stylesheet" href="./static-{{COMMIT}}/out/vs/workbench/workbench.web.api.css">
<link rel="apple-touch-icon" href="./static-{{COMMIT}}/out/vs/server/src/media/code-server.png" />
<meta name="apple-mobile-web-app-capable" content="yes">
<!-- Prefetch to avoid waterfall -->
<link rel="prefetch" href="./static-{{COMMIT}}/node_modules/semver-umd/lib/semver-umd.js">
</head>
<body aria-label="">
</body>
<!-- Startup (do not modify order of script tags!) -->
<!-- NOTE:coder: Modified to work against the current path and use the commit for caching. -->
<script>
// NOTE: Changes to inline scripts require update of content security policy
const basePath = window.location.pathname.replace(/\/+$/, '');
const base = window.location.origin + basePath;
const el = document.getElementById('vscode-remote-commit');
const commit = el ? el.getAttribute('data-settings') : "";
const staticBase = base + '/static-' + commit;
let nlsConfig;
try {
nlsConfig = JSON.parse(document.getElementById('vscode-remote-nls-configuration').getAttribute('data-settings'));
if (nlsConfig._resolvedLanguagePackCoreLocation) {
const bundles = Object.create(null);
nlsConfig.loadBundle = (bundle, language, cb) => {
let result = bundles[bundle];
if (result) {
return cb(undefined, result);
}
// FIXME: Only works if path separators are /.
const path = nlsConfig._resolvedLanguagePackCoreLocation
+ '/' + bundle.replace(/\//g, '!') + '.nls.json';
fetch(`${base}/resource/?path=${encodeURIComponent(path)}`)
.then((response) => response.json())
.then((json) => {
bundles[bundle] = json;
cb(undefined, json);
})
.catch(cb);
};
}
} catch (error) { /* Probably fine. */ }
self.require = {
baseUrl: `${staticBase}/out`,
paths: {
'vscode-textmate': `${staticBase}/node_modules/vscode-textmate/release/main`,
'onigasm-umd': `${staticBase}/node_modules/onigasm-umd/release/main`,
'xterm': `${staticBase}/node_modules/xterm/lib/xterm.js`,
'xterm-addon-search': `${staticBase}/node_modules/xterm-addon-search/lib/xterm-addon-search.js`,
'xterm-addon-web-links': `${staticBase}/node_modules/xterm-addon-web-links/lib/xterm-addon-web-links.js`,
'xterm-addon-webgl': `${staticBase}/node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`,
'semver-umd': `${staticBase}/node_modules/semver-umd/lib/semver-umd.js`,
},
'vs/nls': nlsConfig,
};
</script>
<script src="./static-{{COMMIT}}/out/vs/loader.js"></script>
<script src="./static-{{COMMIT}}/out/vs/workbench/workbench.web.api.nls.js"></script>
<script src="./static-{{COMMIT}}/out/vs/workbench/workbench.web.api.js"></script>
<!-- TODO@coder: This errors with multiple anonymous define calls (one is
workbench.js and one is semver-umd.js). For now use the same method found in
workbench-dev.html. Appears related to the timing of the script load events. -->
<!-- <script src="./static-{{COMMIT}}/out/vs/workbench/workbench.js"></script> -->
<script>
// NOTE: Changes to inline scripts require update of content security policy
require(['vs/code/browser/workbench/workbench'], function() {});
</script>
</html>
<!-- Copyright (C) Microsoft Corporation. All rights reserved. -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<!-- Disable pinch zooming -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<!-- Workbench Configuration -->
<meta id="vscode-workbench-web-configuration" data-settings="{{WORKBENCH_WEB_CONFIGURATION}}">
<!-- Workarounds/Hacks (remote user data uri) -->
<meta id="vscode-remote-user-data-uri" data-settings="{{REMOTE_USER_DATA_URI}}">
<!-- NOTE@coder: Added the commit for use in caching, the product for the
extensions gallery URL, and nls for language support. -->
<meta id="vscode-remote-commit" data-settings="{{COMMIT}}">
<meta id="vscode-remote-product-configuration" data-settings="{{PRODUCT_CONFIGURATION}}">
<meta id="vscode-remote-nls-configuration" data-settings="{{NLS_CONFIGURATION}}">
<!-- Workbench Icon/Manifest/CSS -->
<link rel="icon" href="./static/out/vs/server/src/media/favicon.ico" type="image/x-icon" />
<link rel="manifest" href="./manifest.json" crossorigin="use-credentials">
</head>
<body aria-label="">
</body>
<!-- Startup (do not modify order of script tags!) -->
<script>
const basePath = window.location.pathname.replace(/\/+$/, '');
const base = window.location.origin + basePath;
const el = document.getElementById('vscode-remote-commit');
const commit = el ? el.getAttribute('data-settings') : "";
const staticBase = base + '/static-' + commit;
self.require = {
baseUrl: `${staticBase}/out`,
paths: {
'vscode-textmate': `${staticBase}/node_modules/vscode-textmate/release/main`,
'onigasm-umd': `${staticBase}/node_modules/onigasm-umd/release/main`,
'xterm': `${staticBase}/node_modules/xterm/lib/xterm.js`,
'xterm-addon-search': `${staticBase}/node_modules/xterm-addon-search/lib/xterm-addon-search.js`,
'xterm-addon-web-links': `${staticBase}/node_modules/xterm-addon-web-links/lib/xterm-addon-web-links.js`,
'xterm-addon-webgl': `${staticBase}/node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`,
'semver-umd': `${staticBase}/node_modules/semver-umd/lib/semver-umd.js`,
},
};
</script>
<script src="./static/out/vs/loader.js"></script>
<script>
require(['vs/code/browser/workbench/workbench'], function() {});
</script>
</html>
import { URI } from "vs/base/common/uri";
import { IExtensionDescription } from "vs/platform/extensions/common/extensions";
import { ILogService } from "vs/platform/log/common/log";
import { Client } from "vs/server/node_modules/@coder/node-browser/out/client/client";
import { fromTar } from "vs/server/node_modules/@coder/requirefs/out/requirefs";
import { ExtensionActivationTimesBuilder } from "vs/workbench/api/common/extHostExtensionActivator";
import { IExtHostNodeProxy } from "./extHostNodeProxy";
export const loadCommonJSModule = async <T>(
module: IExtensionDescription,
activationTimesBuilder: ExtensionActivationTimesBuilder,
nodeProxy: IExtHostNodeProxy,
logService: ILogService,
vscode: any,
): Promise<T> => {
const fetchUri = URI.from({
scheme: self.location.protocol.replace(":", ""),
authority: self.location.host,
path: `${self.location.pathname.replace(/\/static.*\/out\/vs\/workbench\/services\/extensions\/worker\/extensionHostWorkerMain.js$/, "")}/tar`,
query: `path=${encodeURIComponent(module.extensionLocation.path)}`,
});
const response = await fetch(fetchUri.toString(true));
if (response.status !== 200) {
throw new Error(`Failed to download extension "${module.extensionLocation.path}"`);
}
const client = new Client(nodeProxy, { logger: logService });
const init = await client.handshake();
const buffer = new Uint8Array(await response.arrayBuffer());
const rfs = fromTar(buffer);
(<any>self).global = self;
rfs.provide("vscode", vscode);
Object.keys(client.modules).forEach((key) => {
const mod = (client.modules as any)[key];
if (key === "process") {
(<any>self).process = mod;
(<any>self).process.env = init.env;
return;
}
rfs.provide(key, mod);
switch (key) {
case "buffer":
(<any>self).Buffer = mod.Buffer;
break;
case "timers":
(<any>self).setImmediate = mod.setImmediate;
break;
}
});
try {
activationTimesBuilder.codeLoadingStart();
return rfs.require(".");
} finally {
activationTimesBuilder.codeLoadingStop();
}
};
export interface Application {
readonly comment?: string
readonly directory?: string
readonly exec?: string
readonly icon?: string
readonly loaded?: boolean
readonly name: string
readonly path: string
readonly sessionId?: string
}
export interface ApplicationsResponse {
readonly applications: ReadonlyArray<Application>
}
export enum SessionError {
NotFound = 4000,
FailedToStart,
Starting,
InvalidState,
Unknown,
}
export interface LoginResponse {
success: boolean
}
export interface CreateSessionResponse {
sessionId: string
}
export interface ExecutableApplication extends Application {
exec: string
}
export const isExecutableApplication = (app: Application): app is ExecutableApplication => {
return !!(app as ExecutableApplication).exec
}
export interface RunningApplication extends ExecutableApplication {
sessionId: string
}
export const isRunningApplication = (app: Application): app is RunningApplication => {
return !!(app as RunningApplication).sessionId
}
export interface RecentResponse {
readonly recent: ReadonlyArray<Application>
readonly running: ReadonlyArray<RunningApplication>
}
export interface FileEntry {
readonly type: "file" | "directory"
readonly name: string
readonly size: number
}
export interface FilesResponse {
files: FileEntry[]
}
export interface HealthRequest {
readonly event: "health"
}
export type ClientMessage = HealthRequest
export interface HealthResponse {
readonly event: "health"
readonly connections: number
}
export type ServerMessage = HealthResponse
export interface ReadyMessage {
protocol: string
}
export interface Disposable {
dispose(): void
}
export interface Event<T> {
(listener: (value: T) => void): Disposable
}
/**
* Emitter typecasts for a single event type.
*/
export class Emitter<T> {
private listeners: Array<(value: T) => void> = []
public get event(): Event<T> {
return (cb: (value: T) => void): Disposable => {
this.listeners.push(cb)
return {
dispose: (): void => {
const i = this.listeners.indexOf(cb)
if (i !== -1) {
this.listeners.splice(i, 1)
}
},
}
}
}
/**
* Emit an event with a value.
*/
public emit(value: T): void {
this.listeners.forEach((cb) => cb(value))
}
public dispose(): void {
this.listeners = []
}
}
export enum HttpCode {
Ok = 200,
Redirect = 302,
NotFound = 404,
BadRequest = 400,
Unauthorized = 401,
LargePayload = 413,
ServerError = 500,
}
export class HttpError extends Error {
public constructor(message: string, public readonly code: number) {
super(message)
this.name = this.constructor.name
}
}
export enum ApiEndpoint {
applications = "/applications",
files = "/files",
login = "/login",
recent = "/recent",
session = "/session",
}
import { Event } from "vs/base/common/event";
import { IChannel, IServerChannel } from "vs/base/parts/ipc/common/ipc";
import { createDecorator } from "vs/platform/instantiation/common/instantiation";
import { ReadWriteConnection } from "vs/server/node_modules/@coder/node-browser/out/common/connection";
export const INodeProxyService = createDecorator<INodeProxyService>("nodeProxyService");
export interface INodeProxyService extends ReadWriteConnection {
_serviceBrand: any;
send(message: string): void;
onMessage: Event<string>;
onUp: Event<void>;
onClose: Event<void>;
onDown: Event<void>;
}
export class NodeProxyChannel implements IServerChannel {
constructor(private service: INodeProxyService) {}
listen(_: unknown, event: string): Event<any> {
switch (event) {
case "onMessage": return this.service.onMessage;
}
throw new Error(`Invalid listen ${event}`);
}
async call(_: unknown, command: string, args?: any): Promise<any> {
switch (command) {
case "send": return this.service.send(args[0]);
}
throw new Error(`Invalid call ${command}`);
}
}
export class NodeProxyChannelClient {
_serviceBrand: any;
public readonly onMessage: Event<string>;
constructor(private readonly channel: IChannel) {
this.onMessage = this.channel.listen<string>("onMessage");
}
public send(data: string): void {
this.channel.call("send", [data]);
}
}
import { ITelemetryData } from "vs/base/common/actions";
import { Event } from "vs/base/common/event";
import { IChannel, IServerChannel } from "vs/base/parts/ipc/common/ipc";
import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from "vs/platform/telemetry/common/gdprTypings";
import { ITelemetryInfo, ITelemetryService } from "vs/platform/telemetry/common/telemetry";
export class TelemetryChannel implements IServerChannel {
constructor(private service: ITelemetryService) {}
listen(_: unknown, event: string): Event<any> {
throw new Error(`Invalid listen ${event}`);
}
call(_: unknown, command: string, args?: any): Promise<any> {
switch (command) {
case "publicLog": return this.service.publicLog(args[0], args[1], args[2]);
case "publicLog2": return this.service.publicLog2(args[0], args[1], args[2]);
case "setEnabled": return Promise.resolve(this.service.setEnabled(args[0]));
case "getTelemetryInfo": return this.service.getTelemetryInfo();
}
throw new Error(`Invalid call ${command}`);
}
}
export class TelemetryChannelClient implements ITelemetryService {
_serviceBrand: any;
constructor(private readonly channel: IChannel) {}
public publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise<void> {
return this.channel.call("publicLog", [eventName, data, anonymizeFilePaths]);
}
public publicLog2<E extends ClassifiedEvent<T> = never, T extends GDPRClassification<T> = never>(eventName: string, data?: StrictPropertyCheck<T, E>, anonymizeFilePaths?: boolean): Promise<void> {
return this.channel.call("publicLog2", [eventName, data, anonymizeFilePaths]);
}
public setEnabled(value: boolean): void {
this.channel.call("setEnable", [value]);
}
public getTelemetryInfo(): Promise<ITelemetryInfo> {
return this.channel.call("getTelemetryInfo");
}
public get isOptedIn(): boolean {
return true;
}
}
import { logger } from "@coder/logger"
export interface Options {
logLevel?: number
}
/**
* Split a string up to the delimiter. If the delimiter doesn't exist the first
* item will have all the text and the second item will be an empty string.
*/
export const split = (str: string, delimiter: string): [string, string] => {
const index = str.indexOf(delimiter);
return index !== -1
? [str.substring(0, index).trim(), str.substring(index + 1)]
: [str, ""];
};
const index = str.indexOf(delimiter)
return index !== -1 ? [str.substring(0, index).trim(), str.substring(index + 1)] : [str, ""]
}
export const plural = (count: number): string => (count === 1 ? "" : "s")
export const generateUuid = (length = 24): string => {
const possible = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
return Array(length)
.fill(1)
.map(() => possible[Math.floor(Math.random() * possible.length)])
.join("")
}
/**
* Get options embedded in the HTML from the server.
*/
export const getOptions = <T extends Options>(): T => {
const el = document.getElementById("coder-options")
try {
if (!el) {
throw new Error("no options element")
}
const value = el.getAttribute("data-settings")
if (!value) {
throw new Error("no options value")
}
const options = JSON.parse(value)
if (typeof options.logLevel !== "undefined") {
logger.level = options.logLevel
}
return options
} catch (error) {
logger.warn(error.message)
return {} as T
}
}
html {
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: inherit;
}
html, body {
background-color: #FFFFFF;
height: 100%;
min-height: 100%;
}
body {
align-items: center;
display: flex;
font-family: "monospace";
justify-content: center;
margin: 0;
padding: 10px;
}
.login-form {
border-radius: 5px;
box-shadow: 0 18px 80px 10px rgba(69, 65, 78, 0.08);
color: #575962;
margin-top: -10%;
max-width: 328px;
padding: 40px;
position: relative;
width: 100%;
}
.login-form > .title {
text-align: center;
text-transform: uppercase;
font-size: 12px;
font-weight: 500;
letter-spacing: 1.5px;
line-height: 15px;
margin-bottom: 0px;
margin-bottom: 5px;
margin-top: 0px;
}
.login-form > .subtitle {
font-size: 19px;
font-weight: bold;
line-height: 25px;
margin-bottom: 45px;
margin: 0;
text-align: center;
}
.login-form > .field {
text-align: left;
font-size: 12px;
color: #797E84;
margin: 16px 0;
}
.login-form > .field > .input {
background: none !important;
border: 1px solid #ccc;
border-radius: 2px;
padding: 5px;
width: 100%;
}
.login-form > .button {
border: none;
border-radius: 24px;
box-shadow: 0 12px 17px 2px rgba(171,173,163,0.14), 0 5px 22px 4px rgba(171,173,163,0.12), 0 7px 8px -4px rgba(171,173,163,0.2);
cursor: pointer;
display: block;
padding: 15px 5px;
width: 100%;
}
.login-form > .button:hover {
background-color: rgb(0, 122, 204);
color: #fff;
}
.error-display {
box-sizing: border-box;
color: #bb2d0f;
font-size: 14px;
font-weight: 400;
line-height: 12px;
padding: 20px 8px 0;
text-align: center;
}
{
"name": "code-server",
"short_name": "code-server",
"start_url": ".",
"display": "standalone",
"background-color": "#fff",
"description": "Run VS Code on a remote server.",
"icons": [{
"src": "./code-server.png",
"sizes": "384x384",
"type": "image/png"
}]
}
import { field, logger } from "@coder/logger"
import * as http from "http"
import * as net from "net"
import * as querystring from "querystring"
import * as ws from "ws"
import { ApplicationsResponse, ClientMessage, FilesResponse, LoginResponse, ServerMessage } from "../../common/api"
import { ApiEndpoint, HttpCode } from "../../common/http"
import { HttpProvider, HttpProviderOptions, HttpResponse, HttpServer, PostData } from "../http"
import { hash } from "../util"
interface LoginPayload extends PostData {
password?: string | string[]
}
/**
* API HTTP provider.
*/
export class ApiHttpProvider extends HttpProvider {
private readonly ws = new ws.Server({ noServer: true })
public constructor(private readonly server: HttpServer, options: HttpProviderOptions) {
super(options)
}
public async handleRequest(
base: string,
_requestPath: string,
_query: querystring.ParsedUrlQuery,
request: http.IncomingMessage
): Promise<HttpResponse | undefined> {
switch (base) {
case ApiEndpoint.login:
if (request.method === "POST") {
return this.login(request)
}
break
default:
if (!this.authenticated(request)) {
return { code: HttpCode.Unauthorized }
}
switch (base) {
case ApiEndpoint.applications:
return this.applications()
case ApiEndpoint.files:
return this.files()
}
}
return undefined
}
public async handleWebSocket(
_base: string,
_requestPath: string,
_query: querystring.ParsedUrlQuery,
request: http.IncomingMessage,
socket: net.Socket,
head: Buffer
): Promise<true> {
if (!this.authenticated(request)) {
throw new Error("not authenticated")
}
await new Promise<ws>((resolve) => {
this.ws.handleUpgrade(request, socket, head, (ws) => {
const send = (event: ServerMessage): void => {
ws.send(JSON.stringify(event))
}
ws.on("message", (data) => {
logger.trace("got message", field("message", data))
try {
const message: ClientMessage = JSON.parse(data.toString())
this.getMessageResponse(message.event).then(send)
} catch (error) {
logger.error(error.message, field("message", data))
}
})
resolve()
})
})
return true
}
private async getMessageResponse(event: "health"): Promise<ServerMessage> {
switch (event) {
case "health":
return { event, connections: await this.server.getConnections() }
default:
throw new Error("unexpected message")
}
}
/**
* Return OK and a cookie if the user is authenticated otherwise return
* unauthorized.
*/
private async login(request: http.IncomingMessage): Promise<HttpResponse<LoginResponse>> {
const ok = (password: string | true): HttpResponse<LoginResponse> => {
return {
content: {
success: true,
},
cookie: typeof password === "string" ? { key: "key", value: password } : undefined,
}
}
// Already authenticated via cookies?
const providedPassword = this.authenticated(request)
if (providedPassword) {
return ok(providedPassword)
}
const data = await this.getData(request)
const payload: LoginPayload = data ? querystring.parse(data) : {}
const password = this.authenticated(request, {
key: typeof payload.password === "string" ? [hash(payload.password)] : undefined,
})
if (password) {
return ok(password)
}
console.error(
"Failed login attempt",
JSON.stringify({
xForwardedFor: request.headers["x-forwarded-for"],
remoteAddress: request.connection.remoteAddress,
userAgent: request.headers["user-agent"],
timestamp: Math.floor(new Date().getTime() / 1000),
})
)
return { code: HttpCode.Unauthorized }
}
/**
* Return files at the requested directory.
*/
private async files(): Promise<HttpResponse<FilesResponse>> {
return {
content: {
files: [],
},
}
}
/**
* Return available applications.
*/
private async applications(): Promise<HttpResponse<ApplicationsResponse>> {
return {
content: {
applications: [
{
name: "VS Code",
path: "/vscode",
},
],
},
}
}
}
import { logger } from "@coder/logger"
import * as React from "react"
import * as ReactDOMServer from "react-dom/server"
import * as ReactRouterDOM from "react-router-dom"
import App from "../../browser/app"
import { HttpProvider, HttpResponse } from "../http"
/**
* Top-level and fallback HTTP provider.
*/
export class MainHttpProvider extends HttpProvider {
public async handleRequest(base: string, requestPath: string): Promise<HttpResponse | undefined> {
if (base === "/static") {
const response = await this.getResource(this.rootPath, requestPath)
response.cache = true
return response
}
const response = await this.getUtf8Resource(this.rootPath, "src/browser/index.html")
response.content = response.content
.replace(/{{COMMIT}}/g, "") // TODO
.replace(/"{{OPTIONS}}"/g, `'${JSON.stringify({ logLevel: logger.level })}'`)
.replace(
/{{COMPONENT}}/g,
ReactDOMServer.renderToString(
<ReactRouterDOM.StaticRouter location={base}>
<App />
</ReactRouterDOM.StaticRouter>
)
)
return response
}
public async handleWebSocket(): Promise<undefined> {
return undefined
}
}
import * as path from "path";
import { VSBuffer, VSBufferReadableStream } from "vs/base/common/buffer";
import { Emitter, Event } from "vs/base/common/event";
import { IDisposable } from "vs/base/common/lifecycle";
import { OS } from "vs/base/common/platform";
import { ReadableStreamEventPayload } from "vs/base/common/stream";
import { URI, UriComponents } from "vs/base/common/uri";
import { transformOutgoingURIs } from "vs/base/common/uriIpc";
import { IServerChannel } from "vs/base/parts/ipc/common/ipc";
import { IDiagnosticInfo } from "vs/platform/diagnostics/common/diagnostics";
import { IEnvironmentService } from "vs/platform/environment/common/environment";
import { ExtensionIdentifier, IExtensionDescription } from "vs/platform/extensions/common/extensions";
import { FileDeleteOptions, FileOpenOptions, FileOverwriteOptions, FileReadStreamOptions, FileType, FileWriteOptions, IStat, IWatchOptions } from "vs/platform/files/common/files";
import { createReadStream } from "vs/platform/files/common/io";
import { DiskFileSystemProvider } from "vs/platform/files/node/diskFileSystemProvider";
import { ILogService } from "vs/platform/log/common/log";
import product from "vs/platform/product/common/product";
import { IRemoteAgentEnvironment, RemoteAgentConnectionContext } from "vs/platform/remote/common/remoteAgentEnvironment";
import { ITelemetryService } from "vs/platform/telemetry/common/telemetry";
import { INodeProxyService } from "vs/server/src/common/nodeProxy";
import { getTranslations } from "vs/server/src/node/nls";
import { getUriTransformer, localRequire } from "vs/server/src/node/util";
import { IFileChangeDto } from "vs/workbench/api/common/extHost.protocol";
import { ExtensionScanner, ExtensionScannerInput } from "vs/workbench/services/extensions/node/extensionPoints";
/**
* Extend the file provider to allow unwatching.
*/
class Watcher extends DiskFileSystemProvider {
public readonly watches = new Map<number, IDisposable>();
public dispose(): void {
this.watches.forEach((w) => w.dispose());
this.watches.clear();
super.dispose();
}
public _watch(req: number, resource: URI, opts: IWatchOptions): void {
this.watches.set(req, this.watch(resource, opts));
}
public unwatch(req: number): void {
this.watches.get(req)!.dispose();
this.watches.delete(req);
}
}
export class FileProviderChannel implements IServerChannel<RemoteAgentConnectionContext>, IDisposable {
private readonly provider: DiskFileSystemProvider;
private readonly watchers = new Map<string, Watcher>();
public constructor(
private readonly environmentService: IEnvironmentService,
private readonly logService: ILogService,
) {
this.provider = new DiskFileSystemProvider(this.logService);
}
public listen(context: RemoteAgentConnectionContext, event: string, args?: any): Event<any> {
switch (event) {
case "filechange": return this.filechange(context, args[0]);
case "readFileStream": return this.readFileStream(args[0], args[1]);
}
throw new Error(`Invalid listen "${event}"`);
}
private filechange(context: RemoteAgentConnectionContext, session: string): Event<IFileChangeDto[]> {
const emitter = new Emitter<IFileChangeDto[]>({
onFirstListenerAdd: () => {
const provider = new Watcher(this.logService);
this.watchers.set(session, provider);
const transformer = getUriTransformer(context.remoteAuthority);
provider.onDidChangeFile((events) => {
emitter.fire(events.map((event) => ({
...event,
resource: transformer.transformOutgoing(event.resource),
})));
});
provider.onDidErrorOccur((event) => this.logService.error(event));
},
onLastListenerRemove: () => {
this.watchers.get(session)!.dispose();
this.watchers.delete(session);
},
});
return emitter.event;
}
private readFileStream(resource: UriComponents, opts: FileReadStreamOptions): Event<ReadableStreamEventPayload<VSBuffer>> {
let fileStream: VSBufferReadableStream | undefined;
const emitter = new Emitter<ReadableStreamEventPayload<VSBuffer>>({
onFirstListenerAdd: () => {
if (!fileStream) {
fileStream = createReadStream(this.provider, this.transform(resource), {
...opts,
bufferSize: 64 * 1024, // From DiskFileSystemProvider
});
fileStream.on("data", (data) => emitter.fire(data));
fileStream.on("error", (error) => emitter.fire(error));
fileStream.on("end", () => emitter.fire("end"));
}
},
onLastListenerRemove: () => fileStream && fileStream.destroy(),
});
return emitter.event;
}
public call(_: unknown, command: string, args?: any): Promise<any> {
switch (command) {
case "stat": return this.stat(args[0]);
case "open": return this.open(args[0], args[1]);
case "close": return this.close(args[0]);
case "read": return this.read(args[0], args[1], args[2]);
case "readFile": return this.readFile(args[0]);
case "write": return this.write(args[0], args[1], args[2], args[3], args[4]);
case "writeFile": return this.writeFile(args[0], args[1], args[2]);
case "delete": return this.delete(args[0], args[1]);
case "mkdir": return this.mkdir(args[0]);
case "readdir": return this.readdir(args[0]);
case "rename": return this.rename(args[0], args[1], args[2]);
case "copy": return this.copy(args[0], args[1], args[2]);
case "watch": return this.watch(args[0], args[1], args[2], args[3]);
case "unwatch": return this.unwatch(args[0], args[1]);
}
throw new Error(`Invalid call "${command}"`);
}
public dispose(): void {
this.watchers.forEach((w) => w.dispose());
this.watchers.clear();
}
private async stat(resource: UriComponents): Promise<IStat> {
return this.provider.stat(this.transform(resource));
}
private async open(resource: UriComponents, opts: FileOpenOptions): Promise<number> {
return this.provider.open(this.transform(resource), opts);
}
private async close(fd: number): Promise<void> {
return this.provider.close(fd);
}
private async read(fd: number, pos: number, length: number): Promise<[VSBuffer, number]> {
const buffer = VSBuffer.alloc(length);
const bytesRead = await this.provider.read(fd, pos, buffer.buffer, 0, length);
return [buffer, bytesRead];
}
private async readFile(resource: UriComponents): Promise<VSBuffer> {
return VSBuffer.wrap(await this.provider.readFile(this.transform(resource)));
}
private write(fd: number, pos: number, buffer: VSBuffer, offset: number, length: number): Promise<number> {
return this.provider.write(fd, pos, buffer.buffer, offset, length);
}
private writeFile(resource: UriComponents, buffer: VSBuffer, opts: FileWriteOptions): Promise<void> {
return this.provider.writeFile(this.transform(resource), buffer.buffer, opts);
}
private async delete(resource: UriComponents, opts: FileDeleteOptions): Promise<void> {
return this.provider.delete(this.transform(resource), opts);
}
private async mkdir(resource: UriComponents): Promise<void> {
return this.provider.mkdir(this.transform(resource));
}
private async readdir(resource: UriComponents): Promise<[string, FileType][]> {
return this.provider.readdir(this.transform(resource));
}
private async rename(resource: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise<void> {
return this.provider.rename(this.transform(resource), URI.from(target), opts);
}
private copy(resource: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise<void> {
return this.provider.copy(this.transform(resource), URI.from(target), opts);
}
private async watch(session: string, req: number, resource: UriComponents, opts: IWatchOptions): Promise<void> {
this.watchers.get(session)!._watch(req, this.transform(resource), opts);
}
private async unwatch(session: string, req: number): Promise<void> {
this.watchers.get(session)!.unwatch(req);
}
private transform(resource: UriComponents): URI {
// Used for walkthrough content.
if (/^\/static[^/]*\//.test(resource.path)) {
return URI.file(this.environmentService.appRoot + resource.path.replace(/^\/static[^/]*\//, "/"));
// Used by the webview service worker to load resources.
} else if (resource.path === "/vscode-resource" && resource.query) {
try {
const query = JSON.parse(resource.query);
if (query.requestResourcePath) {
return URI.file(query.requestResourcePath);
}
} catch (error) { /* Carry on. */ }
}
return URI.from(resource);
}
}
export class ExtensionEnvironmentChannel implements IServerChannel {
public constructor(
private readonly environment: IEnvironmentService,
private readonly log: ILogService,
private readonly telemetry: ITelemetryService,
private readonly connectionToken: string,
) {}
public listen(_: unknown, event: string): Event<any> {
throw new Error(`Invalid listen "${event}"`);
}
public async call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case "getEnvironmentData":
return transformOutgoingURIs(
await this.getEnvironmentData(args.language),
getUriTransformer(context.remoteAuthority),
);
case "getDiagnosticInfo": return this.getDiagnosticInfo();
case "disableTelemetry": return this.disableTelemetry();
}
throw new Error(`Invalid call "${command}"`);
}
private async getEnvironmentData(locale: string): Promise<IRemoteAgentEnvironment> {
return {
pid: process.pid,
connectionToken: this.connectionToken,
appRoot: URI.file(this.environment.appRoot),
appSettingsHome: this.environment.appSettingsHome,
settingsPath: this.environment.machineSettingsHome,
logsPath: URI.file(this.environment.logsPath),
extensionsPath: URI.file(this.environment.extensionsPath!),
extensionHostLogsPath: URI.file(path.join(this.environment.logsPath, "extension-host")),
globalStorageHome: URI.file(this.environment.globalStorageHome),
userHome: URI.file(this.environment.userHome),
extensions: await this.scanExtensions(locale),
os: OS,
};
}
private async scanExtensions(locale: string): Promise<IExtensionDescription[]> {
const translations = await getTranslations(locale, this.environment.userDataPath);
const scanMultiple = (isBuiltin: boolean, isUnderDevelopment: boolean, paths: string[]): Promise<IExtensionDescription[][]> => {
return Promise.all(paths.map((path) => {
return ExtensionScanner.scanExtensions(new ExtensionScannerInput(
product.version,
product.commit,
locale,
!!process.env.VSCODE_DEV,
path,
isBuiltin,
isUnderDevelopment,
translations,
), this.log);
}));
};
const scanBuiltin = async (): Promise<IExtensionDescription[][]> => {
return scanMultiple(true, false, [this.environment.builtinExtensionsPath, ...this.environment.extraBuiltinExtensionPaths]);
};
const scanInstalled = async (): Promise<IExtensionDescription[][]> => {
return scanMultiple(false, true, [this.environment.extensionsPath!, ...this.environment.extraExtensionPaths]);
};
return Promise.all([scanBuiltin(), scanInstalled()]).then((allExtensions) => {
const uniqueExtensions = new Map<string, IExtensionDescription>();
allExtensions.forEach((multipleExtensions) => {
multipleExtensions.forEach((extensions) => {
extensions.forEach((extension) => {
const id = ExtensionIdentifier.toKey(extension.identifier);
if (uniqueExtensions.has(id)) {
const oldPath = uniqueExtensions.get(id)!.extensionLocation.fsPath;
const newPath = extension.extensionLocation.fsPath;
this.log.warn(`${oldPath} has been overridden ${newPath}`);
}
uniqueExtensions.set(id, extension);
});
});
});
return Array.from(uniqueExtensions.values());
});
}
private getDiagnosticInfo(): Promise<IDiagnosticInfo> {
throw new Error("not implemented");
}
private async disableTelemetry(): Promise<void> {
this.telemetry.setEnabled(false);
}
}
export class NodeProxyService implements INodeProxyService {
public _serviceBrand = undefined;
public readonly server: import("@coder/node-browser/out/server/server").Server;
private readonly _onMessage = new Emitter<string>();
public readonly onMessage = this._onMessage.event;
private readonly _$onMessage = new Emitter<string>();
public readonly $onMessage = this._$onMessage.event;
public readonly _onDown = new Emitter<void>();
public readonly onDown = this._onDown.event;
public readonly _onUp = new Emitter<void>();
public readonly onUp = this._onUp.event;
// Unused because the server connection will never permanently close.
private readonly _onClose = new Emitter<void>();
public readonly onClose = this._onClose.event;
public constructor() {
// TODO: down/up
const { Server } = localRequire<typeof import("@coder/node-browser/out/server/server")>("@coder/node-browser/out/server/server");
this.server = new Server({
onMessage: this.$onMessage,
onClose: this.onClose,
onDown: this.onDown,
onUp: this.onUp,
send: (message: string): void => {
this._onMessage.fire(message);
}
});
}
public send(message: string): void {
this._$onMessage.fire(message);
}
}
此差异已折叠。
import * as cp from "child_process";
import { getPathFromAmdModule } from "vs/base/common/amd";
import { VSBuffer } from "vs/base/common/buffer";
import { Emitter } from "vs/base/common/event";
import { ISocket } from "vs/base/parts/ipc/common/ipc.net";
import { NodeSocket } from "vs/base/parts/ipc/node/ipc.net";
import { IEnvironmentService } from "vs/platform/environment/common/environment";
import { ILogService } from "vs/platform/log/common/log";
import { getNlsConfiguration } from "vs/server/src/node/nls";
import { Protocol } from "vs/server/src/node/protocol";
import { uriTransformerPath } from "vs/server/src/node/util";
import { IExtHostReadyMessage } from "vs/workbench/services/extensions/common/extensionHostProtocol";
export abstract class Connection {
private readonly _onClose = new Emitter<void>();
public readonly onClose = this._onClose.event;
private disposed = false;
private _offline: number | undefined;
public constructor(protected protocol: Protocol, public readonly token: string) {}
public get offline(): number | undefined {
return this._offline;
}
public reconnect(socket: ISocket, buffer: VSBuffer): void {
this._offline = undefined;
this.doReconnect(socket, buffer);
}
public dispose(): void {
if (!this.disposed) {
this.disposed = true;
this.doDispose();
this._onClose.fire();
}
}
protected setOffline(): void {
if (!this._offline) {
this._offline = Date.now();
}
}
/**
* Set up the connection on a new socket.
*/
protected abstract doReconnect(socket: ISocket, buffer: VSBuffer): void;
protected abstract doDispose(): void;
}
/**
* Used for all the IPC channels.
*/
export class ManagementConnection extends Connection {
public constructor(protected protocol: Protocol, token: string) {
super(protocol, token);
protocol.onClose(() => this.dispose()); // Explicit close.
protocol.onSocketClose(() => this.setOffline()); // Might reconnect.
}
protected doDispose(): void {
this.protocol.sendDisconnect();
this.protocol.dispose();
this.protocol.getSocket().end();
}
protected doReconnect(socket: ISocket, buffer: VSBuffer): void {
this.protocol.beginAcceptReconnection(socket, buffer);
this.protocol.endAcceptReconnection();
}
}
export class ExtensionHostConnection extends Connection {
private process?: cp.ChildProcess;
public constructor(
locale:string, protocol: Protocol, buffer: VSBuffer, token: string,
private readonly log: ILogService,
private readonly environment: IEnvironmentService,
) {
super(protocol, token);
this.protocol.dispose();
this.spawn(locale, buffer).then((p) => this.process = p);
this.protocol.getUnderlyingSocket().pause();
}
protected doDispose(): void {
if (this.process) {
this.process.kill();
}
this.protocol.getSocket().end();
}
protected doReconnect(socket: ISocket, buffer: VSBuffer): void {
// This is just to set the new socket.
this.protocol.beginAcceptReconnection(socket, null);
this.protocol.dispose();
this.sendInitMessage(buffer);
}
private sendInitMessage(buffer: VSBuffer): void {
const socket = this.protocol.getUnderlyingSocket();
socket.pause();
this.process!.send({ // Process must be set at this point.
type: "VSCODE_EXTHOST_IPC_SOCKET",
initialDataChunk: (buffer.buffer as Buffer).toString("base64"),
skipWebSocketFrames: this.protocol.getSocket() instanceof NodeSocket,
}, socket);
}
private async spawn(locale: string, buffer: VSBuffer): Promise<cp.ChildProcess> {
const config = await getNlsConfiguration(locale, this.environment.userDataPath);
const proc = cp.fork(
getPathFromAmdModule(require, "bootstrap-fork"),
[ "--type=extensionHost", `--uriTransformerPath=${uriTransformerPath}` ],
{
env: {
...process.env,
AMD_ENTRYPOINT: "vs/workbench/services/extensions/node/extensionHostProcess",
PIPE_LOGGING: "true",
VERBOSE_LOGGING: "true",
VSCODE_EXTHOST_WILL_SEND_SOCKET: "true",
VSCODE_HANDLES_UNCAUGHT_ERRORS: "true",
VSCODE_LOG_STACK: "false",
VSCODE_LOG_LEVEL: this.environment.verbose ? "trace" : this.environment.log,
VSCODE_NLS_CONFIG: JSON.stringify(config),
},
silent: true,
},
);
proc.on("error", () => this.dispose());
proc.on("exit", () => this.dispose());
proc.stdout.setEncoding("utf8").on("data", (d) => this.log.info("Extension host stdout", d));
proc.stderr.setEncoding("utf8").on("data", (d) => this.log.error("Extension host stderr", d));
proc.on("message", (event) => {
if (event && event.type === "__$console") {
const severity = (<any>this.log)[event.severity] ? event.severity : "info";
(<any>this.log)[severity]("Extension host", event.arguments);
}
if (event && event.type === "VSCODE_EXTHOST_DISCONNECTED") {
this.setOffline();
}
});
const listen = (message: IExtHostReadyMessage) => {
if (message.type === "VSCODE_EXTHOST_IPC_READY") {
proc.removeListener("message", listen);
this.sendInitMessage(buffer);
}
};
return proc.on("message", listen);
}
}
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
// This file is included via a regular Node require. I'm not sure how (or if)
// we can write this in Typescript and have it compile to non-AMD syntax.
module.exports = (remoteAuthority) => {
return {
transformIncoming: (uri) => {
switch (uri.scheme) {
case "vscode-remote": return { scheme: "file", path: uri.path };
default: return uri;
}
},
transformOutgoing: (uri) => {
switch (uri.scheme) {
case "file": return { scheme: "vscode-remote", authority: remoteAuthority, path: uri.path };
default: return uri;
}
},
transformOutgoingScheme: (scheme) => {
switch (scheme) {
case "file": return "vscode-remote";
default: return scheme;
}
},
};
};
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
httpolyglot.d.ts
\ No newline at end of file
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册