Implement new structure

This commit is contained in:
Asher 2020-02-04 13:27:46 -06:00
parent ef8da3864f
commit b29346ecdf
No known key found for this signature in database
GPG Key ID: D63C1EF81242354A
94 changed files with 12427 additions and 4936 deletions

View File

@ -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:

6
.editorconfig Normal file
View File

@ -0,0 +1,6 @@
root = true
[*]
indent_style = space
trim_trailing_whitespace = true
indent_size = 2

39
.eslintrc.yaml Normal file
View File

@ -0,0 +1,39 @@
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.

17
.gitignore vendored
View File

@ -1,5 +1,14 @@
*.tsbuildinfo
.cache
binaries
binary-upload
build
cache-upload
dist
dist-build
node_modules
/build
/release
/binaries
/lib
out
out-build
release
source
yarn-cache

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "lib/vscode"]
path = lib/vscode
url = https://github.com/microsoft/vscode

7
.prettierrc.yaml Normal file
View File

@ -0,0 +1,7 @@
printWidth: 120
semi: false
tabWidth: 2
singleQuote: false
trailingComma: es5
useTabs: false
arrowParens: always

2
.stylelintrc.yaml Normal file
View File

@ -0,0 +1,2 @@
extends:
- stylelint-config-standard

View File

@ -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

View File

@ -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 extensions 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

1
lib/vscode Submodule

@ -0,0 +1 @@
Subproject commit 26076a4de974ead31f97692a0d32f90d735645c0

View File

@ -1,7 +0,0 @@
// 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");

View File

@ -1,42 +1,70 @@
{
"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"
}
}

View File

@ -1,391 +1,451 @@
import { Binary } from "@coder/nbin";
import * as cp from "child_process";
// import * as crypto from "crypto";
import * as fs from "fs-extra";
import * as os from "os";
import * as path from "path";
import * as util from "util";
import { Binary } from "@coder/nbin"
import * as cp from "child_process"
import * as fs from "fs-extra"
import * as os from "os"
import Bundler from "parcel-bundler"
import * as path from "path"
import * as util from "util"
enum Task {
/**
* Use before running anything that only works inside VS Code.
*/
EnsureInVscode = "ensure-in-vscode",
Binary = "binary",
Package = "package",
Build = "build",
Binary = "binary",
Package = "package",
Build = "build",
Watch = "watch",
}
class Builder {
private readonly rootPath = path.resolve(__dirname, "..");
private readonly outPath = process.env.OUT || this.rootPath;
private _target?: "darwin" | "alpine" | "linux";
private currentTask?: Task;
private readonly rootPath = path.resolve(__dirname, "..")
private readonly vscodeSourcePath = path.join(this.rootPath, "lib/vscode")
private readonly binariesPath = path.join(this.rootPath, "binaries")
private readonly buildPath = path.join(this.rootPath, "build")
private readonly codeServerVersion: string
private _target?: "darwin" | "alpine" | "linux"
private currentTask?: Task
public run(task: Task | undefined, args: string[]): void {
this.currentTask = task;
this.doRun(task, args).catch((error) => {
console.error(error.message);
process.exit(1);
});
}
public constructor() {
this.ensureArgument("rootPath", this.rootPath)
this.codeServerVersion = this.ensureArgument(
"codeServerVersion",
process.env.VERSION || require(path.join(this.rootPath, "package.json")).version
)
}
private async task<T>(message: string, fn: () => Promise<T>): Promise<T> {
const time = Date.now();
this.log(`${message}...`, true);
try {
const t = await fn();
process.stdout.write(`took ${Date.now() - time}ms\n`);
return t;
} catch (error) {
process.stdout.write("failed\n");
throw error;
}
}
public run(task: Task | undefined): void {
this.currentTask = task
this.doRun(task).catch((error) => {
console.error(error.message)
process.exit(1)
})
}
/**
* Writes to stdout with an optional newline.
*/
private log(message: string, skipNewline: boolean = false): void {
process.stdout.write(`[${this.currentTask || "default"}] ${message}`);
if (!skipNewline) {
process.stdout.write("\n");
}
}
private async task<T>(message: string, fn: () => Promise<T>): Promise<T> {
const time = Date.now()
this.log(`${message}...`, true)
try {
const t = await fn()
process.stdout.write(`took ${Date.now() - time}ms\n`)
return t
} catch (error) {
process.stdout.write("failed\n")
throw error
}
}
private async doRun(task: Task | undefined, args: string[]): Promise<void> {
if (!task) {
throw new Error("No task provided");
}
/**
* Writes to stdout with an optional newline.
*/
private log(message: string, skipNewline = false): void {
process.stdout.write(`[${this.currentTask || "default"}] ${message}`)
if (!skipNewline) {
process.stdout.write("\n")
}
}
if (task === Task.EnsureInVscode) {
return process.exit(this.isInVscode(this.rootPath) ? 0 : 1);
}
private async doRun(task: Task | undefined): Promise<void> {
if (!task) {
throw new Error("No task provided")
}
// If we're inside VS Code assume we want to develop. In that case we should
// set an OUT directory and not build in this directory, otherwise when you
// build/watch VS Code the build directory will be included.
if (this.isInVscode(this.outPath)) {
throw new Error("Should not build inside VS Code; set the OUT environment variable");
}
const arch = this.ensureArgument("arch", os.arch().replace(/^x/, "x86_"))
const target = this.ensureArgument("target", await this.target())
const binaryName = `code-server-${this.codeServerVersion}-${target}-${arch}`
this.ensureArgument("rootPath", this.rootPath);
this.ensureArgument("outPath", this.outPath);
switch (task) {
case Task.Watch:
return this.watch()
case Task.Binary:
return this.binary(binaryName)
case Task.Package:
return this.package(binaryName)
case Task.Build:
return this.build()
default:
throw new Error(`No task matching "${task}"`)
}
}
const arch = this.ensureArgument("arch", os.arch().replace(/^x/, "x86_"));
const target = this.ensureArgument("target", await this.target());
const vscodeVersion = this.ensureArgument("vscodeVersion", args[0]);
const codeServerVersion = this.ensureArgument("codeServerVersion", args[1]);
/**
* Get the target of the system.
*/
private async target(): Promise<"darwin" | "alpine" | "linux"> {
if (!this._target) {
if (os.platform() === "darwin" || (process.env.OSTYPE && /^darwin/.test(process.env.OSTYPE))) {
this._target = "darwin"
} else {
// Alpine's ldd doesn't have a version flag but if you use an invalid flag
// (like --version) it outputs the version to stderr and exits with 1.
const result = await util
.promisify(cp.exec)("ldd --version")
.catch((error) => ({ stderr: error.message, stdout: "" }))
if (/musl/.test(result.stderr) || /musl/.test(result.stdout)) {
this._target = "alpine"
} else {
this._target = "linux"
}
}
}
return this._target
}
const vscodeSourcePath = path.join(this.outPath, "source", `vscode-${vscodeVersion}-source`);
const binariesPath = path.join(this.outPath, "binaries");
const binaryName = `code-server${codeServerVersion}-vsc${vscodeVersion}-${target}-${arch}`;
const finalBuildPath = path.join(this.outPath, "build", `${binaryName}-built`);
/**
* Make sure the argument is set. Display the value if it is.
*/
private ensureArgument(name: string, arg?: string): string {
if (!arg) {
throw new Error(`${name} is missing`)
}
this.log(`${name} is "${arg}"`)
return arg
}
switch (task) {
case Task.Binary:
return this.binary(finalBuildPath, binariesPath, binaryName);
case Task.Package:
return this.package(vscodeSourcePath, binariesPath, binaryName);
case Task.Build:
return this.build(vscodeSourcePath, vscodeVersion, codeServerVersion, finalBuildPath);
default:
throw new Error(`No task matching "${task}"`);
}
}
/**
* Build VS Code and code-server.
*/
private async build(): Promise<void> {
process.env.NODE_OPTIONS = "--max-old-space-size=32384 " + (process.env.NODE_OPTIONS || "")
process.env.NODE_ENV = "production"
/**
* Get the target of the system.
*/
private async target(): Promise<"darwin" | "alpine" | "linux"> {
if (!this._target) {
if (os.platform() === "darwin" || (process.env.OSTYPE && /^darwin/.test(process.env.OSTYPE))) {
this._target = "darwin";
} else {
// Alpine's ldd doesn't have a version flag but if you use an invalid flag
// (like --version) it outputs the version to stderr and exits with 1.
const result = await util.promisify(cp.exec)("ldd --version")
.catch((error) => ({ stderr: error.message, stdout: "" }));
if (/musl/.test(result.stderr) || /musl/.test(result.stdout)) {
this._target = "alpine";
} else {
this._target = "linux";
}
}
}
return this._target;
}
await this.task("cleaning up old build", async () => {
if (!process.env.SKIP_VSCODE) {
return fs.remove(this.buildPath)
}
// If skipping VS Code, keep the existing build if any.
try {
const files = await fs.readdir(this.buildPath)
return Promise.all(files.filter((f) => f !== "lib").map((f) => fs.remove(path.join(this.buildPath, f))))
} catch (error) {
if (error.code !== "ENOENT") {
throw error
}
}
})
/**
* Make sure the argument is set. Display the value if it is.
*/
private ensureArgument(name: string, arg?: string): string {
if (!arg) {
this.log(`${name} is missing`);
throw new Error("Usage: <vscodeVersion> <codeServerVersion>");
}
this.log(`${name} is "${arg}"`);
return arg;
}
const commit = require(path.join(this.vscodeSourcePath, "build/lib/util")).getVersion(this.rootPath) as string
if (!process.env.SKIP_VSCODE) {
await this.buildVscode(commit)
} else {
this.log("skipping vs code build")
}
await this.buildCodeServer(commit)
/**
* Return true if it looks like we're inside VS Code. This is used to prevent
* accidentally building inside VS Code while developing which causes issues
* because the watcher will try compiling those built files.
*/
private isInVscode(pathToCheck: string): boolean {
let inside = false;
const maybeVsCode = path.join(pathToCheck, "../../../");
try {
// If it has a package.json with the right name it's probably VS Code.
inside = require(path.join(maybeVsCode, "package.json")).name === "code-oss-dev";
} catch (error) {}
this.log(
inside
? `Running inside VS Code ([${maybeVsCode}]${path.relative(maybeVsCode, pathToCheck)})`
: "Not running inside VS Code"
);
return inside;
}
this.log(`final build: ${this.buildPath}`)
}
/**
* Build code-server within VS Code.
*/
private async build(vscodeSourcePath: string, vscodeVersion: string, codeServerVersion: string, finalBuildPath: string): Promise<void> {
// Install dependencies (should be cached by CI).
await this.task("Installing code-server dependencies", async () => {
await util.promisify(cp.exec)("yarn", { cwd: this.rootPath });
});
private async buildCodeServer(commit: string): Promise<void> {
await this.task("building code-server", async () => {
return util.promisify(cp.exec)("tsc --outDir ./out-build --tsBuildInfoFile ./.prod.tsbuildinfo", {
cwd: this.rootPath,
})
})
// Download and prepare VS Code if necessary (should be cached by CI).
if (fs.existsSync(vscodeSourcePath)) {
this.log("Using existing VS Code clone");
} else {
await this.task("Cloning VS Code", () => {
return util.promisify(cp.exec)(
"git clone https://github.com/microsoft/vscode"
+ ` --quiet --branch "${vscodeVersion}"`
+ ` --single-branch --depth=1 "${vscodeSourcePath}"`);
});
}
await this.task("bundling code-server", async () => {
return this.createBundler("dist-build", commit).bundle()
})
await this.task("Installing VS Code dependencies", () => {
return util.promisify(cp.exec)("yarn", { cwd: vscodeSourcePath });
});
await this.task("copying code-server into build directory", async () => {
await fs.mkdirp(this.buildPath)
await Promise.all([
fs.copy(path.join(this.rootPath, "out-build"), path.join(this.buildPath, "out")),
fs.copy(path.join(this.rootPath, "dist-build"), path.join(this.buildPath, "dist")),
// For source maps and images.
fs.copy(path.join(this.rootPath, "src"), path.join(this.buildPath, "src")),
])
})
if (fs.existsSync(path.join(vscodeSourcePath, ".build/extensions"))) {
this.log("Using existing built-in-extensions");
} else {
await this.task("Building default extensions", () => {
return util.promisify(cp.exec)(
"yarn gulp compile-extensions-build --max-old-space-size=32384",
{ cwd: vscodeSourcePath },
);
});
}
await this.copyDependencies("code-server", this.rootPath, this.buildPath)
}
// Clean before patching or it could fail if already patched.
await this.task("Patching VS Code", async () => {
await util.promisify(cp.exec)("git reset --hard", { cwd: vscodeSourcePath });
await util.promisify(cp.exec)("git clean -fd", { cwd: vscodeSourcePath });
await util.promisify(cp.exec)(`git apply ${this.rootPath}/scripts/vscode.patch`, { cwd: vscodeSourcePath });
});
private async buildVscode(commit: string): Promise<void> {
await this.task("building vs code", () => {
return util.promisify(cp.exec)("yarn gulp compile-build", { cwd: this.vscodeSourcePath })
})
const serverPath = path.join(vscodeSourcePath, "src/vs/server");
await this.task("Copying code-server into VS Code", async () => {
await fs.remove(serverPath);
await fs.mkdirp(serverPath);
await Promise.all(["main.js", "node_modules", "src", "typings"].map((fileName) => {
return fs.copy(path.join(this.rootPath, fileName), path.join(serverPath, fileName));
}));
});
await this.task("building builtin extensions", async () => {
const exists = await fs.pathExists(path.join(this.vscodeSourcePath, ".build/extensions"))
if (exists) {
process.stdout.write("already built, skipping...")
} else {
await util.promisify(cp.exec)("yarn gulp compile-extensions-build", { cwd: this.vscodeSourcePath })
}
})
await this.task("Building VS Code", () => {
return util.promisify(cp.exec)("yarn gulp compile-build --max-old-space-size=32384", { cwd: vscodeSourcePath });
});
await this.task("optimizing vs code", async () => {
return util.promisify(cp.exec)("yarn gulp optimize --gulpfile ./coder.js", { cwd: this.vscodeSourcePath })
})
await this.task("Optimizing VS Code", async () => {
await fs.copyFile(path.join(this.rootPath, "scripts/optimize.js"), path.join(vscodeSourcePath, "coder.js"));
await util.promisify(cp.exec)(`yarn gulp optimize --max-old-space-size=32384 --gulpfile ./coder.js`, { cwd: vscodeSourcePath });
});
if (process.env.MINIFY) {
await this.task("minifying vs code", () => {
return util.promisify(cp.exec)("yarn gulp minify --gulpfile ./coder.js", { cwd: this.vscodeSourcePath })
})
}
const { productJson, packageJson } = await this.task("Generating final package.json and product.json", async () => {
const merge = async (name: string, extraJson: { [key: string]: string } = {}): Promise<{ [key: string]: string }> => {
const [aJson, bJson] = (await Promise.all([
fs.readFile(path.join(vscodeSourcePath, `${name}.json`), "utf8"),
fs.readFile(path.join(this.rootPath, `scripts/${name}.json`), "utf8"),
])).map((raw) => {
const json = JSON.parse(raw);
delete json.scripts;
delete json.dependencies;
delete json.devDependencies;
delete json.optionalDependencies;
return json;
});
const { productJson, packageJson } = await this.task("generating vs code product configuration", async () => {
const merge = async (name: string, json: { [key: string]: string } = {}): Promise<{ [key: string]: string }> => {
return {
...JSON.parse(await fs.readFile(path.join(this.vscodeSourcePath, `${name}.json`), "utf8")),
...json,
}
}
return { ...aJson, ...bJson, ...extraJson };
};
const date = new Date().toISOString()
const [packageJson, productJson] = await Promise.all([merge("package", {}), merge("product", { commit, date })])
const date = new Date().toISOString();
const commit = require(path.join(vscodeSourcePath, "build/lib/util")).getVersion(this.rootPath);
return { productJson, packageJson }
})
const [productJson, packageJson] = await Promise.all([
merge("product", { commit, date }),
merge("package", { codeServerVersion: `${codeServerVersion}-vsc${vscodeVersion}` }),
]);
await this.task("inserting vs code product configuration", async () => {
const filePath = path.join(this.vscodeSourcePath, "out-build/vs/platform/product/common/product.js")
return fs.writeFile(
filePath,
(await fs.readFile(filePath, "utf8")).replace(
"{ /*BUILD->INSERT_PRODUCT_CONFIGURATION*/}",
JSON.stringify({
version: packageJson.version,
...productJson,
})
)
)
})
// We could do this before the optimization but then it'd be copied into
// three files and unused in two which seems like a waste of bytes.
const apiPath = path.join(vscodeSourcePath, "out-vscode/vs/workbench/workbench.web.api.js");
await fs.writeFile(apiPath, (await fs.readFile(apiPath, "utf8")).replace('{ /*BUILD->INSERT_PRODUCT_CONFIGURATION*/}', JSON.stringify({
version: packageJson.version,
codeServerVersion: packageJson.codeServerVersion,
...productJson,
})));
const vscodeBuildPath = path.join(this.buildPath, "lib/vscode")
await this.task("copying vs code into build directory", async () => {
await fs.mkdirp(vscodeBuildPath)
await Promise.all([
(async (): Promise<void> => {
await fs.move(
path.join(this.vscodeSourcePath, `out-vscode${process.env.MINIFY ? "-min" : ""}`),
path.join(vscodeBuildPath, "out")
)
await fs.remove(path.join(vscodeBuildPath, "out/vs/server/browser/workbench.html"))
await fs.move(
path.join(vscodeBuildPath, "out/vs/server/browser/workbench-build.html"),
path.join(vscodeBuildPath, "out/vs/server/browser/workbench.html")
)
})(),
await fs.copy(path.join(this.vscodeSourcePath, ".build/extensions"), path.join(vscodeBuildPath, "extensions")),
])
})
return { productJson, packageJson };
});
await this.copyDependencies("vs code", this.vscodeSourcePath, vscodeBuildPath)
if (process.env.MINIFY) {
await this.task("Minifying VS Code", () => {
return util.promisify(cp.exec)("yarn gulp minify --max-old-space-size=32384 --gulpfile ./coder.js", { cwd: vscodeSourcePath });
});
}
await this.task("writing final vs code product.json", () => {
return fs.writeFile(path.join(vscodeBuildPath, "product.json"), JSON.stringify(productJson, null, 2))
})
}
const finalServerPath = path.join(finalBuildPath, "out/vs/server");
await this.task("Copying into final build directory", async () => {
await fs.remove(finalBuildPath);
await fs.mkdirp(finalBuildPath);
await Promise.all([
fs.copy(path.join(vscodeSourcePath, "remote/node_modules"), path.join(finalBuildPath, "node_modules")),
fs.copy(path.join(vscodeSourcePath, ".build/extensions"), path.join(finalBuildPath, "extensions")),
fs.copy(path.join(vscodeSourcePath, `out-vscode${process.env.MINIFY ? "-min" : ""}`), path.join(finalBuildPath, "out")).then(() => {
return Promise.all([
fs.remove(path.join(finalServerPath, "node_modules")).then(() => {
return fs.copy(path.join(serverPath, "node_modules"), path.join(finalServerPath, "node_modules"));
}),
fs.copy(path.join(finalServerPath, "src/browser/workbench-build.html"), path.join(finalServerPath, "src/browser/workbench.html")),
]);
}),
]);
});
private async copyDependencies(name: string, sourcePath: string, buildPath: string): Promise<void> {
await this.task(`copying ${name} dependencies`, async () => {
return Promise.all(
["node_modules", "package.json", "yarn.lock"].map((fileName) => {
return fs.copy(path.join(sourcePath, fileName), path.join(buildPath, fileName))
})
)
})
if (process.env.MINIFY) {
await this.task("Restricting to production dependencies", async () => {
await Promise.all(["package.json", "yarn.lock"].map((fileName) => {
Promise.all([
fs.copy(path.join(this.rootPath, fileName), path.join(finalServerPath, fileName)),
fs.copy(path.join(path.join(vscodeSourcePath, "remote"), fileName), path.join(finalBuildPath, fileName)),
]);
}));
if (process.env.MINIFY) {
await this.task(`restricting ${name} to production dependencies`, async () => {
return util.promisify(cp.exec)("yarn --production --ignore-scripts", { cwd: buildPath })
})
}
}
await Promise.all([finalServerPath, finalBuildPath].map((cwd) => {
return util.promisify(cp.exec)("yarn --production", { cwd });
}));
/**
* Bundles the built code into a binary.
*/
private async binary(binaryName: string): Promise<void> {
const bin = new Binary({
mainFile: path.join(this.buildPath, "out/node/entry.js"),
target: await this.target(),
})
await Promise.all(["package.json", "yarn.lock"].map((fileName) => {
return Promise.all([
fs.remove(path.join(finalServerPath, fileName)),
fs.remove(path.join(finalBuildPath, fileName)),
]);
}));
});
}
bin.writeFiles(path.join(this.buildPath, "**"))
await this.task("Writing final package.json and product.json", () => {
return Promise.all([
fs.writeFile(path.join(finalBuildPath, "package.json"), JSON.stringify(packageJson, null, 2)),
fs.writeFile(path.join(finalBuildPath, "product.json"), JSON.stringify(productJson, null, 2)),
]);
});
await fs.mkdirp(this.binariesPath)
// Prevent needless cache changes.
await this.task("Cleaning for smaller cache", () => {
return Promise.all([
fs.remove(serverPath),
fs.remove(path.join(vscodeSourcePath, "out-vscode")),
fs.remove(path.join(vscodeSourcePath, "out-vscode-min")),
fs.remove(path.join(vscodeSourcePath, "out-build")),
util.promisify(cp.exec)("git reset --hard", { cwd: vscodeSourcePath }).then(() => {
return util.promisify(cp.exec)("git clean -fd", { cwd: vscodeSourcePath });
}),
]);
});
const binaryPath = path.join(this.binariesPath, binaryName)
await fs.writeFile(binaryPath, await bin.build())
await fs.chmod(binaryPath, "755")
// Prepend code to the target which enables finding files within the binary.
const prependLoader = async (relativeFilePath: string): Promise<void> => {
const filePath = path.join(finalBuildPath, relativeFilePath);
const shim = `
if (!global.NBIN_LOADED) {
try {
const nbin = require("nbin");
nbin.shimNativeFs("${finalBuildPath}");
global.NBIN_LOADED = true;
const path = require("path");
const rg = require("vscode-ripgrep");
rg.binaryRgPath = rg.rgPath;
rg.rgPath = path.join(require("os").tmpdir(), "code-server", path.basename(rg.binaryRgPath));
} catch (error) { /* Not in the binary. */ }
}
`;
await fs.writeFile(filePath, shim + (await fs.readFile(filePath, "utf8")));
};
this.log(`binary: ${binaryPath}`)
}
await this.task("Prepending nbin loader", () => {
return Promise.all([
prependLoader("out/vs/server/main.js"),
prependLoader("out/bootstrap-fork.js"),
prependLoader("extensions/node_modules/typescript/lib/tsserver.js"),
]);
});
/**
* Package the binary into a release archive.
*/
private async package(binaryName: string): Promise<void> {
const releasePath = path.join(this.rootPath, "release")
const archivePath = path.join(releasePath, binaryName)
this.log(`Final build: ${finalBuildPath}`);
}
await fs.remove(archivePath)
await fs.mkdirp(archivePath)
/**
* Bundles the built code into a binary.
*/
private async binary(targetPath: string, binariesPath: string, binaryName: string): Promise<void> {
const bin = new Binary({
mainFile: path.join(targetPath, "out/vs/server/main.js"),
target: await this.target(),
});
await fs.copyFile(path.join(this.binariesPath, binaryName), path.join(archivePath, "code-server"))
await fs.copyFile(path.join(this.rootPath, "README.md"), path.join(archivePath, "README.md"))
await fs.copyFile(path.join(this.vscodeSourcePath, "LICENSE.txt"), path.join(archivePath, "LICENSE.txt"))
await fs.copyFile(
path.join(this.vscodeSourcePath, "ThirdPartyNotices.txt"),
path.join(archivePath, "ThirdPartyNotices.txt")
)
bin.writeFiles(path.join(targetPath, "**"));
if ((await this.target()) === "darwin") {
await util.promisify(cp.exec)(`zip -r "${binaryName}.zip" "${binaryName}"`, { cwd: releasePath })
this.log(`archive: ${archivePath}.zip`)
} else {
await util.promisify(cp.exec)(`tar -czf "${binaryName}.tar.gz" "${binaryName}"`, { cwd: releasePath })
this.log(`archive: ${archivePath}.tar.gz`)
}
}
await fs.mkdirp(binariesPath);
private async watch(): Promise<void> {
let server: cp.ChildProcess | undefined
const restartServer = (): void => {
if (server) {
server.kill()
}
const s = cp.fork(path.join(this.rootPath, "out/node/entry.js"))
console.log(`[server] spawned process ${s.pid}`)
s.on("exit", () => console.log(`[server] process ${s.pid} exited`))
server = s
}
const binaryPath = path.join(binariesPath, binaryName);
await fs.writeFile(binaryPath, await bin.build());
await fs.chmod(binaryPath, "755");
const vscode = cp.spawn("yarn", ["watch"], { cwd: this.vscodeSourcePath })
const tsc = cp.spawn("tsc", ["--watch", "--pretty", "--preserveWatchOutput"], { cwd: this.rootPath })
const bundler = this.createBundler()
this.log(`Binary: ${binaryPath}`);
}
const cleanup = (code?: number | null): void => {
this.log("killing vs code watcher")
vscode.removeAllListeners()
vscode.kill()
/**
* Package the binary into a release archive.
*/
private async package(vscodeSourcePath: string, binariesPath: string, binaryName: string): Promise<void> {
const releasePath = path.join(this.outPath, "release");
const archivePath = path.join(releasePath, binaryName);
this.log("killing tsc")
tsc.removeAllListeners()
tsc.kill()
await fs.remove(archivePath);
await fs.mkdirp(archivePath);
if (server) {
this.log("killing server")
server.removeAllListeners()
server.kill()
}
await fs.copyFile(path.join(binariesPath, binaryName), path.join(archivePath, "code-server"));
await fs.copyFile(path.join(this.rootPath, "README.md"), path.join(archivePath, "README.md"));
await fs.copyFile(path.join(vscodeSourcePath, "LICENSE.txt"), path.join(archivePath, "LICENSE.txt"));
await fs.copyFile(path.join(vscodeSourcePath, "ThirdPartyNotices.txt"), path.join(archivePath, "ThirdPartyNotices.txt"));
this.log("killing bundler")
process.exit(code || 0)
}
if ((await this.target()) === "darwin") {
await util.promisify(cp.exec)(`zip -r "${binaryName}.zip" "${binaryName}"`, { cwd: releasePath });
this.log(`Archive: ${archivePath}.zip`);
} else {
await util.promisify(cp.exec)(`tar -czf "${binaryName}.tar.gz" "${binaryName}"`, { cwd: releasePath });
this.log(`Archive: ${archivePath}.tar.gz`);
}
}
process.on("SIGINT", () => cleanup())
process.on("SIGTERM", () => cleanup())
vscode.on("exit", (code) => {
this.log("vs code watcher terminated unexpectedly")
cleanup(code)
})
tsc.on("exit", (code) => {
this.log("tsc terminated unexpectedly")
cleanup(code)
})
const bundle = bundler.bundle().catch(() => {
this.log("parcel watcher terminated unexpectedly")
cleanup(1)
})
bundler.on("buildEnd", () => {
console.log("[parcel] bundled")
})
vscode.stderr.on("data", (d) => process.stderr.write(d))
tsc.stderr.on("data", (d) => process.stderr.write(d))
// From https://github.com/chalk/ansi-regex
const pattern = [
"[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))",
].join("|")
const re = new RegExp(pattern, "g")
/**
* Split stdout on newlines and strip ANSI codes.
*/
const onLine = (proc: cp.ChildProcess, callback: (strippedLine: string, originalLine: string) => void): void => {
let buffer = ""
if (!proc.stdout) {
throw new Error("no stdout")
}
proc.stdout.setEncoding("utf8")
proc.stdout.on("data", (d) => {
const data = buffer + d
const split = data.split("\n")
const last = split.length - 1
for (let i = 0; i < last; ++i) {
callback(split[i].replace(re, ""), split[i])
}
// The last item will either be an empty string (the data ended with a
// newline) or a partial line (did not end with a newline) and we must
// wait to parse it until we get a full line.
buffer = split[last]
})
}
let startingVscode = false
onLine(vscode, (line, original) => {
console.log("[vscode]", original)
// Wait for watch-client since "Finished compilation" will appear multiple
// times before the client starts building.
if (!startingVscode && line.includes("Starting watch-client")) {
startingVscode = true
} else if (startingVscode && line.includes("Finished compilation") && process.env.AUTO_PATCH) {
cp.exec("yarn patch:generate", { cwd: this.rootPath }, (error, _, stderr) => {
if (error || stderr) {
console.error(error ? error.message : stderr)
}
})
}
})
onLine(tsc, (line, original) => {
// tsc outputs blank lines; skip them.
if (line !== "") {
console.log("[tsc]", original)
}
if (line.includes("Watching for file changes")) {
bundle.then(restartServer)
}
})
}
private createBundler(out = "dist", commit?: string): Bundler {
return new Bundler(path.join(this.rootPath, "src/browser/index.tsx"), {
cache: true,
cacheDir: path.join(this.rootPath, ".cache"),
detailedReport: true,
minify: !!process.env.MINIFY,
hmr: false,
logLevel: 1,
outDir: path.join(this.rootPath, out),
publicUrl: `/static-${commit}/dist`,
target: "browser",
})
}
}
const builder = new Builder();
builder.run(process.argv[2] as Task, process.argv.slice(3));
const builder = new Builder()
builder.run(process.argv[2] as Task)

View File

@ -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")/.."
cd "$(dirname "$0")/.."
# Get the branch for this build.
branch=${DRONE_BRANCH:-${DRONE_SOURCE_BRANCH:-${DRONE_TAG:-}}}
# 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
# 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"
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
# 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 "$@"

View File

@ -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}")/.."
# 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}}}
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
# 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
YARN_CACHE_FOLDER="$(pwd)/yarn-cache"
export YARN_CACHE_FOLDER
YARN_CACHE_FOLDER="$(pwd)/yarn-cache"
export YARN_CACHE_FOLDER
# 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
# 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
if [[ -z ${SKIP_YARN:-} ]] ; then
yarn
fi
function run-yarn() {
yarn "$1" "$vscode_version" "$code_server_version"
}
yarn build
yarn binary
if [[ -n ${PACKAGE:-} ]] ; then
yarn package
fi
run-yarn build
run-yarn binary
if [[ -n ${PACKAGE:-} ]] ; then
run-yarn package
fi
cd binaries
# In this case provide a plainly named "code-server" binary.
if [[ -n ${BINARY:-} ]] ; then
mv binaries/code-server*-vsc* binaries/code-server
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"
# 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 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
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
local gcp_dir
gcp_dir="../binary-upload/releases/$code_server_version/$target"
mkdir -p "$gcp_dir"
echo "Copying $binary to $gcp_dir/code-server"
cp "$binary" "$gcp_dir/code-server"
done
fi
}
main "$@"

View File

@ -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

11
scripts/lint.sh Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env sh
# lint.sh -- Lint CSS and JS files.
set -eu
main() {
yarn lint:css "$@"
yarn lint:js "$@"
}
main "$@"

View File

@ -1,71 +0,0 @@
// 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")
));

View File

@ -1,5 +0,0 @@
{
"name": "code-server",
"main": "out/vs/server/main",
"desktopName": "code-server-url-handler.desktop"
}

10
scripts/postinstall.sh Executable file
View File

@ -0,0 +1,10 @@
#!/usr/bin/env sh
# postinstall.sh - Does nothing at the moment.
set -eu
main() {
cd "$(dirname "${0}")/.."
}
main "$@"

21
scripts/preinstall.sh Executable file
View File

@ -0,0 +1,21 @@
#!/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 "$@"

View File

@ -1,21 +0,0 @@
{
"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"
}

View File

@ -1,19 +0,0 @@
#!/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 "$@"

View File

@ -1,17 +1,21 @@
{
"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"
]
}

File diff suppressed because it is too large Load Diff

View File

@ -1,369 +1,82 @@
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;
}
}
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);
}
public dispose(): void {
this.hide();
this.disposed = true;
}
export const getApplications = async (): Promise<ApplicationsResponse> => {
const response = await tryRequest(ApiEndpoint.applications)
return response.json()
}
export const getSession = async (app: Application): Promise<CreateSessionResponse> => {
const response = await tryRequest(ApiEndpoint.session, {
method: "POST",
body: JSON.stringify(app),
})
return response.json()
}
export const killSession = async (app: Application): Promise<Response> => {
return tryRequest(ApiEndpoint.session, {
method: "DELETE",
body: JSON.stringify(app),
})
}

18
src/browser/app.css Normal file
View File

@ -0,0 +1,18 @@
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;
}

37
src/browser/app.tsx Normal file
View File

@ -0,0 +1,37 @@
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

View File

@ -1,133 +0,0 @@
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);
};

View File

@ -0,0 +1,27 @@
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
}

View File

@ -0,0 +1,28 @@
.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;
}

View File

@ -0,0 +1,48 @@
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>
}

View File

@ -0,0 +1,108 @@
.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;
}

View File

@ -0,0 +1,169 @@
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} />
))}
</>
)
}

View File

@ -0,0 +1,147 @@
.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;
}

View File

@ -0,0 +1,192 @@
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>
)
}

View File

@ -1,46 +0,0 @@
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");

19
src/browser/index.html Normal file
View File

@ -0,0 +1,19 @@
<!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>

18
src/browser/index.tsx Normal file
View File

@ -0,0 +1,18 @@
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")
)

View File

@ -1,31 +0,0 @@
<!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>

View File

@ -1,37 +0,0 @@
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;
}
}

View File

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,13 @@
{
"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"
}]
}

View File

@ -0,0 +1,34 @@
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>
)}
</>
)
}

View File

@ -0,0 +1,8 @@
.orientation-guide {
align-items: center;
color: #b6b6b6;
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
}

View File

@ -0,0 +1,22 @@
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>
)
}

View File

@ -0,0 +1,35 @@
.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;
}

View File

@ -0,0 +1,55 @@
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>
)
}

View File

@ -0,0 +1,29 @@
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}
/>
)
}

View File

@ -0,0 +1,33 @@
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}
/>
)
}

208
src/browser/socket.ts Normal file
View File

@ -0,0 +1,208 @@
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
}
}

View File

@ -1,92 +0,0 @@
<!-- 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>

View File

@ -1,53 +0,0 @@
<!-- 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>

View File

@ -1,57 +0,0 @@
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();
}
};

78
src/common/api.ts Normal file
View File

@ -0,0 +1,78 @@
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
}

40
src/common/emitter.ts Normal file
View File

@ -0,0 +1,40 @@
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 = []
}
}

24
src/common/http.ts Normal file
View File

@ -0,0 +1,24 @@
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",
}

View File

@ -1,47 +0,0 @@
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]);
}
}

View File

@ -1,49 +0,0 @@
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;
}
}

View File

@ -1,10 +1,48 @@
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
}
}

View File

@ -1,94 +0,0 @@
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;
}

View File

@ -1,13 +0,0 @@
{
"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"
}]
}

159
src/node/api/server.ts Normal file
View File

@ -0,0 +1,159 @@
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",
},
],
},
}
}
}

37
src/node/app/server.tsx Normal file
View File

@ -0,0 +1,37 @@
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
}
}

View File

@ -1,343 +0,0 @@
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);
}
}

View File

@ -1,299 +0,0 @@
import * as cp from "child_process";
import * as os from "os";
import * as path from "path";
import { setUnexpectedErrorHandler } from "vs/base/common/errors";
import { main as vsCli } from "vs/code/node/cliProcessMain";
import { validatePaths } from "vs/code/node/paths";
import { ParsedArgs } from "vs/platform/environment/common/environment";
import { buildHelpMessage, buildVersionMessage, Option as VsOption, OPTIONS, OptionDescriptions } from "vs/platform/environment/node/argv";
import { parseMainProcessArgv } from "vs/platform/environment/node/argvHelper";
import product from "vs/platform/product/common/product";
import { ipcMain } from "vs/server/src/node/ipc";
import { enableCustomMarketplace } from "vs/server/src/node/marketplace";
import { MainServer } from "vs/server/src/node/server";
import { AuthType, buildAllowedMessage, enumToArray, FormatType, generateCertificate, generatePassword, localRequire, open, unpackExecutables } from "vs/server/src/node/util";
const { logger } = localRequire<typeof import("@coder/logger/out/index")>("@coder/logger/out/index");
setUnexpectedErrorHandler((error) => logger.warn(error.message));
interface Args extends ParsedArgs {
auth?: AuthType;
"base-path"?: string;
cert?: string;
"cert-key"?: string;
format?: string;
host?: string;
open?: boolean;
port?: string;
socket?: string;
}
// @ts-ignore: Force `keyof Args` to work.
interface Option extends VsOption {
id: keyof Args;
}
const getArgs = (): Args => {
// Remove options that won't work or don't make sense.
for (let key in OPTIONS) {
switch (key) {
case "add":
case "diff":
case "file-uri":
case "folder-uri":
case "goto":
case "new-window":
case "reuse-window":
case "wait":
case "disable-gpu":
// TODO: pretty sure these don't work but not 100%.
case "prof-startup":
case "inspect-extensions":
case "inspect-brk-extensions":
delete OPTIONS[key];
break;
}
}
const options = OPTIONS as OptionDescriptions<Required<Args>>;
options["base-path"] = { type: "string", cat: "o", description: "Base path of the URL at which code-server is hosted (used for login redirects)." };
options["cert"] = { type: "string", cat: "o", description: "Path to certificate. If the path is omitted, both this and --cert-key will be generated." };
options["cert-key"] = { type: "string", cat: "o", description: "Path to the certificate's key if one was provided." };
options["format"] = { type: "string", cat: "o", description: `Format for the version. ${buildAllowedMessage(FormatType)}.` };
options["host"] = { type: "string", cat: "o", description: "Host for the server." };
options["auth"] = { type: "string", cat: "o", description: `The type of authentication to use. ${buildAllowedMessage(AuthType)}.` };
options["open"] = { type: "boolean", cat: "o", description: "Open in the browser on startup." };
options["port"] = { type: "string", cat: "o", description: "Port for the main server." };
options["socket"] = { type: "string", cat: "o", description: "Listen on a socket instead of host:port." };
const args = parseMainProcessArgv(process.argv);
if (!args["user-data-dir"]) {
args["user-data-dir"] = path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local/share"), "code-server");
}
if (!args["extensions-dir"]) {
args["extensions-dir"] = path.join(args["user-data-dir"], "extensions");
}
if (!args.verbose && !args.log && process.env.LOG_LEVEL) {
args.log = process.env.LOG_LEVEL;
}
return validatePaths(args);
};
const startVscode = async (args: Args): Promise<void | void[]> => {
const extra = args["_"] || [];
const options = {
auth: args.auth || AuthType.Password,
basePath: args["base-path"],
cert: args.cert,
certKey: args["cert-key"],
openUri: extra.length > 1 ? extra[extra.length - 1] : undefined,
host: args.host,
password: process.env.PASSWORD,
};
if (enumToArray(AuthType).filter((t) => t === options.auth).length === 0) {
throw new Error(`'${options.auth}' is not a valid authentication type.`);
} else if (options.auth === "password" && !options.password) {
options.password = await generatePassword();
}
if (!options.certKey && typeof options.certKey !== "undefined") {
throw new Error(`--cert-key cannot be blank`);
} else if (options.certKey && !options.cert) {
throw new Error(`--cert-key was provided but --cert was not`);
} if (!options.cert && typeof options.cert !== "undefined") {
const { cert, certKey } = await generateCertificate();
options.cert = cert;
options.certKey = certKey;
}
enableCustomMarketplace();
const server = new MainServer({
...options,
port: typeof args.port !== "undefined" ? parseInt(args.port, 10) : 8080,
socket: args.socket,
}, args);
const [serverAddress, /* ignore */] = await Promise.all([
server.listen(),
unpackExecutables(),
]);
logger.info(`Server listening on ${serverAddress}`);
if (options.auth === "password" && !process.env.PASSWORD) {
logger.info(` - Password is ${options.password}`);
logger.info(" - To use your own password, set the PASSWORD environment variable");
if (!args.auth) {
logger.info(" - To disable use `--auth none`");
}
} else if (options.auth === "password") {
logger.info(" - Using custom password for authentication");
} else {
logger.info(" - No authentication");
}
if (server.protocol === "https") {
logger.info(
args.cert
? ` - Using provided certificate${args["cert-key"] ? " and key" : ""} for HTTPS`
: ` - Using generated certificate and key for HTTPS`,
);
} else {
logger.info(" - Not serving HTTPS");
}
if (!server.options.socket && args.open) {
// The web socket doesn't seem to work if browsing with 0.0.0.0.
const openAddress = serverAddress.replace(/:\/\/0.0.0.0/, "://localhost");
await open(openAddress).catch(console.error);
logger.info(` - Opened ${openAddress}`);
}
};
const startCli = (args: Args): boolean | Promise<void> => {
if (args.help) {
const executable = `${product.applicationName}${os.platform() === "win32" ? ".exe" : ""}`;
console.log(buildHelpMessage(product.nameLong, executable, product.codeServerVersion, OPTIONS, false));
return true;
}
if (args.version) {
if (args.format === "json") {
console.log(JSON.stringify({
codeServerVersion: product.codeServerVersion,
commit: product.commit,
vscodeVersion: product.version,
}));
} else {
buildVersionMessage(product.codeServerVersion, product.commit).split("\n").map((line) => logger.info(line));
}
return true;
}
const shouldSpawnCliProcess = (): boolean => {
return !!args["install-source"]
|| !!args["list-extensions"]
|| !!args["install-extension"]
|| !!args["uninstall-extension"]
|| !!args["locate-extension"]
|| !!args["telemetry"];
};
if (shouldSpawnCliProcess()) {
enableCustomMarketplace();
return vsCli(args);
}
return false;
};
export class WrapperProcess {
private process?: cp.ChildProcess;
private started?: Promise<void>;
private currentVersion = product.codeServerVersion;
public constructor(private readonly args: Args) {
ipcMain.onMessage(async (message) => {
switch (message.type) {
case "relaunch":
logger.info(`Relaunching: ${this.currentVersion} -> ${message.version}`);
this.currentVersion = message.version;
this.started = undefined;
if (this.process) {
this.process.removeAllListeners();
this.process.kill();
}
try {
await this.start();
} catch (error) {
logger.error(error.message);
process.exit(typeof error.code === "number" ? error.code : 1);
}
break;
default:
logger.error(`Unrecognized message ${message}`);
break;
}
});
}
public start(): Promise<void> {
if (!this.started) {
const child = this.spawn();
this.started = ipcMain.handshake(child).then(() => {
child.once("exit", (code) => exit(code!));
});
this.process = child;
}
return this.started;
}
private spawn(): cp.ChildProcess {
// Flags to pass along to the Node binary. We use the environment variable
// since otherwise the code-server binary will swallow them.
const maxMemory = this.args["max-memory"] || 2048;
let nodeOptions = `${process.env.NODE_OPTIONS || ""} ${this.args["js-flags"] || ""}`;
if (!/max_old_space_size=(\d+)/g.exec(nodeOptions)) {
nodeOptions += ` --max_old_space_size=${maxMemory}`;
}
// If we're using loose files then we need to specify the path. If we're in
// the binary we need to let the binary determine the path (via nbin) since
// it could be different between binaries which presents a problem when
// upgrading (different version numbers or different staging directories).
const isBinary = (global as any).NBIN_LOADED;
return cp.spawn(process.argv[0], process.argv.slice(isBinary ? 2 : 1), {
env: {
...process.env,
LAUNCH_VSCODE: "true",
NBIN_BYPASS: undefined,
VSCODE_PARENT_PID: process.pid.toString(),
NODE_OPTIONS: nodeOptions,
},
stdio: ["inherit", "inherit", "inherit", "ipc"],
});
}
}
const main = async(): Promise<boolean | void | void[]> => {
const args = getArgs();
if (process.env.LAUNCH_VSCODE) {
await ipcMain.handshake();
return startVscode(args);
}
return startCli(args) || new WrapperProcess(args).start();
};
const exit = process.exit;
process.exit = function (code?: number) {
const err = new Error(`process.exit() was prevented: ${code || "unknown code"}.`);
console.warn(err.stack);
} as (code?: number) => never;
// Copy the extension host behavior of killing oneself if the parent dies. This
// also exists in bootstrap-fork.js but spawning with that won't work because we
// override process.exit.
if (typeof process.env.VSCODE_PARENT_PID !== "undefined") {
const parentPid = parseInt(process.env.VSCODE_PARENT_PID, 10);
setInterval(() => {
try {
process.kill(parentPid, 0); // Throws an exception if the process doesn't exist anymore.
} catch (e) {
exit();
}
}, 5000);
}
// It's possible that the pipe has closed (for example if you run code-server
// --version | head -1). Assume that means we're done.
if (!process.stdout.isTTY) {
process.stdout.on("error", () => exit());
}
main().catch((error) => {
logger.error(error.message);
exit(typeof error.code === "number" ? error.code : 1);
});

View File

@ -1,156 +0,0 @@
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);
}
}

87
src/node/entry.ts Normal file
View File

@ -0,0 +1,87 @@
import { logger } from "@coder/logger"
import { ApiHttpProvider } from "./api/server"
import { MainHttpProvider } from "./app/server"
import { AuthType, HttpServer } from "./http"
import { generateCertificate, generatePassword, hash, open } from "./util"
import { VscodeHttpProvider } from "./vscode/server"
import { ipcMain, wrap } from "./wrapper"
export interface Args {
auth?: AuthType
"base-path"?: string
cert?: string
"cert-key"?: string
format?: string
host?: string
open?: boolean
port?: string
socket?: string
_?: string[]
}
const main = async (args: Args = {}): Promise<void> => {
// Spawn the main HTTP server.
const options = {
basePath: args["base-path"],
cert: args.cert,
certKey: args["cert-key"],
host: args.host || (args.auth === AuthType.Password && typeof args.cert !== "undefined" ? "0.0.0.0" : "localhost"),
port: typeof args.port !== "undefined" ? parseInt(args.port, 10) : 8080,
socket: args.socket,
}
if (!options.cert && typeof options.cert !== "undefined") {
const { cert, certKey } = await generateCertificate()
options.cert = cert
options.certKey = certKey
}
const httpServer = new HttpServer(options)
// Register all the providers.
// TODO: Might be cleaner to be able to register with just the class name
// then let HttpServer instantiate with the common arguments.
const auth = args.auth || AuthType.Password
const originalPassword = auth === AuthType.Password && (process.env.PASSWORD || (await generatePassword()))
const password = originalPassword && hash(originalPassword)
httpServer.registerHttpProvider("/", new MainHttpProvider({ base: "/", auth, password }))
httpServer.registerHttpProvider("/api", new ApiHttpProvider(httpServer, { base: "/", auth, password }))
httpServer.registerHttpProvider(
"/vscode-embed",
new VscodeHttpProvider([], { base: "/vscode-embed", auth, password })
)
ipcMain.onDispose(() => httpServer.dispose())
const serverAddress = await httpServer.listen()
logger.info(`Server listening on ${serverAddress}`)
if (auth === AuthType.Password && !process.env.PASSWORD) {
logger.info(` - Password is ${originalPassword}`)
logger.info(" - To use your own password, set the PASSWORD environment variable")
if (!args.auth) {
logger.info(" - To disable use `--auth none`")
}
} else if (auth === AuthType.Password) {
logger.info(" - Using custom password for authentication")
} else {
logger.info(" - No authentication")
}
if (httpServer.protocol === "https") {
logger.info(
args.cert
? ` - Using provided certificate${args["cert-key"] ? " and key" : ""} for HTTPS`
: ` - Using generated certificate and key for HTTPS`
)
} else {
logger.info(" - Not serving HTTPS")
}
if (serverAddress && !options.socket && args.open) {
// The web socket doesn't seem to work if browsing with 0.0.0.0.
const openAddress = serverAddress.replace(/:\/\/0.0.0.0/, "://localhost")
await open(openAddress).catch(console.error)
logger.info(` - Opened ${openAddress}`)
}
}
wrap(main)

579
src/node/http.ts Normal file
View File

@ -0,0 +1,579 @@
import { logger } from "@coder/logger"
import * as fs from "fs-extra"
import * as http from "http"
import * as httpolyglot from "httpolyglot"
import * as https from "https"
import * as net from "net"
import * as path from "path"
import * as querystring from "querystring"
import safeCompare from "safe-compare"
import { Readable } from "stream"
import * as tarFs from "tar-fs"
import * as tls from "tls"
import * as url from "url"
import { HttpCode, HttpError } from "../common/http"
import { plural, split } from "../common/util"
import { getMediaMime, normalize, xdgLocalDir } from "./util"
export type Cookies = { [key: string]: string[] | undefined }
export type PostData = { [key: string]: string | string[] | undefined }
interface AuthPayload extends Cookies {
key?: string[]
}
export enum AuthType {
Password = "password",
None = "none",
}
export type Query = { [key: string]: string | string[] | undefined }
export interface HttpResponse<T = string | Buffer | object> {
/*
* Whether to set cache-control headers for this response.
*/
cache?: boolean
/**
* If the code cannot be determined automatically set it here. The
* defaults are 302 for redirects and 200 for successful requests. For errors
* you should throw an HttpError and include the code there. If you
* use Error it will default to 404 for ENOENT and EISDIR and 500 otherwise.
*/
code?: number
/**
* Content to write in the response. Mutually exclusive with stream.
*/
content?: T
/**
* Cookie to write with the response.
*/
cookie?: { key: string; value: string }
/**
* Used to automatically determine the appropriate mime type.
*/
filePath?: string
/**
* Additional headers to include.
*/
headers?: http.OutgoingHttpHeaders
/**
* If the mime type cannot be determined automatically set it here.
*/
mime?: string
/**
* Redirect to this path. Will rewrite against the base path but NOT the
* provider endpoint so you must include it. This allows redirecting outside
* of your endpoint. Use `withBase()` to redirect within your endpoint.
*/
redirect?: string
/**
* Stream this to the response. Mutually exclusive with content.
*/
stream?: Readable
/**
* Query variables to add in addition to current ones when redirecting. Use
* `undefined` to remove a query variable.
*/
query?: Query
}
/**
* Use when you need to run search and replace on a file's content before
* sending it.
*/
export interface HttpStringFileResponse extends HttpResponse {
content: string
filePath: string
}
export interface HttpServerOptions {
readonly basePath?: string
readonly cert?: string
readonly certKey?: string
readonly host?: string
readonly port?: number
readonly socket?: string
}
interface ProviderRoute {
base: string
requestPath: string
query: querystring.ParsedUrlQuery
provider: HttpProvider
fullPath: string
originalPath: string
}
export interface HttpProviderOptions {
readonly base: string
readonly auth: AuthType
readonly password: string | false
}
/**
* Provides HTTP responses. This abstract class provides some helpers for
* interpreting, creating, and authenticating responses.
*/
export abstract class HttpProvider {
protected readonly rootPath = path.resolve(__dirname, "../..")
public constructor(private readonly options: HttpProviderOptions) {}
public dispose(): void {
// No default behavior.
}
/**
* Handle web sockets on the registered endpoint.
*/
public abstract handleWebSocket(
base: string,
requestPath: string,
query: querystring.ParsedUrlQuery,
request: http.IncomingMessage,
socket: net.Socket,
head: Buffer
): Promise<true | undefined>
/**
* Handle requests to the registered endpoint.
*/
public abstract handleRequest(
base: string,
requestPath: string,
query: querystring.ParsedUrlQuery,
request: http.IncomingMessage
): Promise<HttpResponse | undefined>
/**
* Return the specified path with the base path prepended.
*/
protected withBase(path: string): string {
return normalize(`${this.options.base}/${path}`)
}
/**
* Get a file resource.
* TODO: Would a stream be faster, at least for large files?
*/
protected async getResource(...parts: string[]): Promise<HttpResponse> {
const filePath = path.join(...parts)
return { content: await fs.readFile(filePath), filePath }
}
/**
* Get a file resource as a string.
*/
protected async getUtf8Resource(...parts: string[]): Promise<HttpStringFileResponse> {
const filePath = path.join(...parts)
return { content: await fs.readFile(filePath, "utf8"), filePath }
}
/**
* Tar up and stream a directory.
*/
protected async getTarredResource(...parts: string[]): Promise<HttpResponse> {
const filePath = path.join(...parts)
return { stream: tarFs.pack(filePath), filePath, mime: "application/tar", cache: true }
}
/**
* Helper to error on anything that's not a GET.
*/
protected ensureGet(request: http.IncomingMessage): void {
if (request.method !== "GET") {
throw new HttpError(`Unsupported method ${request.method}`, HttpCode.BadRequest)
}
}
/**
* Helper to error if not authorized.
*/
protected ensureAuthenticated(request: http.IncomingMessage): void {
if (!this.authenticated(request)) {
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
}
}
/**
* Use the first query value or the default if there isn't one.
*/
protected queryOrDefault(value: string | string[] | undefined, def: string): string {
if (Array.isArray(value)) {
value = value[0]
}
return typeof value !== "undefined" ? value : def
}
/**
* Return the provided password value if the payload contains the right
* password otherwise return false. If no payload is specified use cookies.
*/
protected authenticated(request: http.IncomingMessage, payload?: AuthPayload): string | boolean {
switch (this.options.auth) {
case AuthType.None:
return true
case AuthType.Password:
if (typeof payload === "undefined") {
payload = this.parseCookies<AuthPayload>(request)
}
if (this.options.password && payload.key) {
for (let i = 0; i < payload.key.length; ++i) {
if (safeCompare(payload.key[i], this.options.password)) {
return payload.key[i]
}
}
}
return false
default:
throw new Error(`Unsupported auth type ${this.options.auth}`)
}
}
/**
* Parse POST data.
*/
protected getData(request: http.IncomingMessage): Promise<string | undefined> {
return request.method === "POST" || request.method === "DELETE"
? new Promise<string>((resolve, reject) => {
let body = ""
const onEnd = (): void => {
off() // eslint-disable-line @typescript-eslint/no-use-before-define
resolve(body || undefined)
}
const onError = (error: Error): void => {
off() // eslint-disable-line @typescript-eslint/no-use-before-define
reject(error)
}
const onData = (d: Buffer): void => {
body += d
if (body.length > 1e6) {
onError(new HttpError("Payload is too large", HttpCode.LargePayload))
request.connection.destroy()
}
}
const off = (): void => {
request.off("error", onError)
request.off("data", onError)
request.off("end", onEnd)
}
request.on("error", onError)
request.on("data", onData)
request.on("end", onEnd)
})
: Promise.resolve(undefined)
}
/**
* Parse cookies.
*/
protected parseCookies<T extends Cookies>(request: http.IncomingMessage): T {
const cookies: { [key: string]: string[] } = {}
if (request.headers.cookie) {
request.headers.cookie.split(";").forEach((keyValue) => {
const [key, value] = split(keyValue, "=")
if (!cookies[key]) {
cookies[key] = []
}
cookies[key].push(decodeURI(value))
})
}
return cookies as T
}
}
/**
* Provides a heartbeat using a local file to indicate activity.
*/
export class Heart {
private heartbeatTimer?: NodeJS.Timeout
private heartbeatInterval = 60000
private lastHeartbeat = 0
public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise<boolean>) {}
/**
* Write to the heartbeat file if we haven't already done so within the
* timeout and start or reset a timer that keeps running as long as there is
* activity. Failures are logged as warnings.
*/
public beat(): void {
const now = Date.now()
if (now - this.lastHeartbeat >= this.heartbeatInterval) {
logger.trace("heartbeat")
fs.outputFile(this.heartbeatPath, "").catch((error) => {
logger.warn(error.message)
})
this.lastHeartbeat = now
if (typeof this.heartbeatTimer !== "undefined") {
clearTimeout(this.heartbeatTimer)
}
this.heartbeatTimer = setTimeout(() => {
this.isActive().then((active) => {
if (active) {
this.beat()
}
})
}, this.heartbeatInterval)
}
}
}
/**
* An HTTP server. Its main role is to route incoming HTTP requests to the
* appropriate provider for that endpoint then write out the response. It also
* covers some common use cases like redirects and caching.
*/
export class HttpServer {
protected readonly server: http.Server | https.Server
private listenPromise: Promise<string | null> | undefined
public readonly protocol: "http" | "https"
private readonly providers = new Map<string, HttpProvider>()
private readonly options: HttpServerOptions
private readonly heart: Heart
public constructor(options: HttpServerOptions) {
this.heart = new Heart(path.join(xdgLocalDir, "heartbeat"), async () => {
const connections = await this.getConnections()
logger.trace(`${connections} active connection${plural(connections)}`)
return connections !== 0
})
this.options = {
...options,
basePath: options.basePath ? options.basePath.replace(/\/+$/, "") : "",
}
this.protocol = this.options.cert ? "https" : "http"
if (this.protocol === "https") {
this.server = httpolyglot.createServer(
{
cert: this.options.cert && fs.readFileSync(this.options.cert),
key: this.options.certKey && fs.readFileSync(this.options.certKey),
},
this.onRequest
)
} else {
this.server = http.createServer(this.onRequest)
}
}
public dispose(): void {
this.providers.forEach((p) => p.dispose())
}
public async getConnections(): Promise<number> {
return new Promise((resolve, reject) => {
this.server.getConnections((error, count) => {
return error ? reject(error) : resolve(count)
})
})
}
/**
* Register a provider for a top-level endpoint.
*/
public registerHttpProvider<T extends HttpProvider>(endpoint: string, provider: T): void {
endpoint = endpoint.replace(/^\/+|\/+$/g, "")
if (this.providers.has(`/${endpoint}`)) {
throw new Error(`${endpoint} is already registered`)
}
if (/\//.test(endpoint)) {
throw new Error(`Only top-level endpoints are supported (got ${endpoint})`)
}
this.providers.set(`/${endpoint}`, provider)
}
/**
* Start listening on the specified port.
*/
public listen(): Promise<string | null> {
if (!this.listenPromise) {
this.listenPromise = new Promise((resolve, reject) => {
this.server.on("error", reject)
this.server.on("upgrade", this.onUpgrade)
const onListen = (): void => resolve(this.address())
if (this.options.socket) {
this.server.listen(this.options.socket, onListen)
} else {
this.server.listen(this.options.port, this.options.host, onListen)
}
})
}
return this.listenPromise
}
/**
* The *local* address of the server.
*/
public address(): string | null {
const address = this.server.address()
const endpoint =
typeof address !== "string" && address !== null
? (address.address === "::" ? "localhost" : address.address) + ":" + address.port
: address
return endpoint && `${this.protocol}://${endpoint}`
}
private onRequest = async (request: http.IncomingMessage, response: http.ServerResponse): Promise<void> => {
try {
this.heart.beat()
const route = this.parseUrl(request)
const payload =
this.maybeRedirect(request, route) ||
(await route.provider.handleRequest(route.base, route.requestPath, route.query, request))
if (!payload) {
throw new HttpError("Not found", HttpCode.NotFound)
}
const basePath = this.options.basePath || "/"
response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, {
"Content-Type": payload.mime || getMediaMime(payload.filePath),
...(payload.redirect
? {
Location: this.constructRedirect(
request.headers.host as string,
route.fullPath,
normalize(`${basePath}/${payload.redirect}`) + "/",
{ ...route.query, ...(payload.query || {}) }
),
}
: {}),
...(request.headers["service-worker"] ? { "Service-Worker-Allowed": basePath } : {}),
...(payload.cache ? { "Cache-Control": "public, max-age=31536000" } : {}),
...(payload.cookie
? {
"Set-Cookie": `${payload.cookie.key}=${payload.cookie.value}; Path=${basePath}; HttpOnly; SameSite=strict`,
}
: {}),
...payload.headers,
})
if (payload.stream) {
payload.stream.on("error", (error: NodeJS.ErrnoException) => {
response.writeHead(error.code === "ENOENT" ? HttpCode.NotFound : HttpCode.ServerError)
response.end(error.message)
})
payload.stream.pipe(response)
} else if (typeof payload.content === "string" || payload.content instanceof Buffer) {
response.end(payload.content)
} else if (payload.content && typeof payload.content === "object") {
response.end(JSON.stringify(payload.content))
} else {
response.end()
}
} catch (error) {
let e = error
if (error.code === "ENOENT" || error.code === "EISDIR") {
e = new HttpError("Not found", HttpCode.NotFound)
} else {
logger.error(error.stack)
}
response.writeHead(typeof e.code === "number" ? e.code : HttpCode.ServerError)
response.end(error.message)
}
}
/**
* Return any necessary redirection before delegating to a provider.
*/
private maybeRedirect(request: http.IncomingMessage, route: ProviderRoute): HttpResponse | undefined {
// Redirect to HTTPS.
if (this.options.cert && !(request.connection as tls.TLSSocket).encrypted) {
return { redirect: route.fullPath }
}
// Redirect indexes to a trailing slash so relative paths will operate
// against the provider.
if (route.requestPath === "/index.html" && !route.originalPath.endsWith("/")) {
return { redirect: route.fullPath } // Redirect always includes a trailing slash.
}
return undefined
}
private onUpgrade = async (request: http.IncomingMessage, socket: net.Socket, head: Buffer): Promise<void> => {
try {
this.heart.beat()
socket.on("error", () => socket.destroy())
if (this.options.cert && !(socket as tls.TLSSocket).encrypted) {
throw new HttpError("HTTP websocket", HttpCode.BadRequest)
}
if (!request.headers.upgrade || request.headers.upgrade.toLowerCase() !== "websocket") {
throw new HttpError("HTTP/1.1 400 Bad Request", HttpCode.BadRequest)
}
const { base, requestPath, query, provider } = this.parseUrl(request)
if (!provider) {
throw new HttpError("Not found", HttpCode.NotFound)
}
if (!(await provider.handleWebSocket(base, requestPath, query, request, socket, head))) {
throw new HttpError("Not found", HttpCode.NotFound)
}
} catch (error) {
socket.destroy(error)
logger.warn(`discarding socket connection: ${error.message}`)
}
}
/**
* Parse a request URL so we can route it.
*/
private parseUrl(request: http.IncomingMessage): ProviderRoute {
const parse = (fullPath: string): { base: string; requestPath: string } => {
const match = fullPath.match(/^(\/?[^/]*)(.*)$/)
let [, /* ignore */ base, requestPath] = match ? match.map((p) => p.replace(/\/+$/, "")) : ["", "", ""]
if (base.indexOf(".") !== -1) {
// Assume it's a file at the root.
requestPath = base
base = "/"
} else if (base === "") {
// Happens if it's a plain `domain.com`.
base = "/"
}
requestPath = requestPath || "/index.html"
// Allow for a versioned static endpoint. This lets us cache every static
// resource underneath the path based on the version without any work and
// without adding query parameters which have their own issues.
if (/^\/static-/.test(base)) {
base = "/static"
}
return { base, requestPath }
}
const parsedUrl = request.url ? url.parse(request.url, true) : { query: {}, pathname: "" }
const originalPath = parsedUrl.pathname || ""
const fullPath = normalize(originalPath)
const { base, requestPath } = parse(fullPath)
// Providers match on the path after their base so we need to account for
// that by shifting the next base out of the request path.
let provider = this.providers.get(base)
if (base !== "/" && provider) {
return { ...parse(requestPath), fullPath, query: parsedUrl.query, provider, originalPath }
}
// Fall back to the top-level provider.
provider = this.providers.get("/")
if (!provider) {
throw new Error(`No provider for ${base}`)
}
return { base, fullPath, requestPath, query: parsedUrl.query, provider, originalPath }
}
/**
* Return the request URL with the specified base and new path.
*/
private constructRedirect(host: string, oldPath: string, newPath: string, query: Query): string {
if (oldPath && oldPath !== "/" && !query.to && /\/login(\/|$)/.test(newPath) && !/\/login(\/|$)/.test(oldPath)) {
query.to = oldPath
}
Object.keys(query).forEach((key) => {
if (typeof query[key] === "undefined") {
delete query[key]
}
})
return (
`${this.protocol}://${host}${newPath}` + (Object.keys(query).length > 0 ? `?${querystring.stringify(query)}` : "")
)
}
}

View File

@ -1,124 +0,0 @@
import * as appInsights from "applicationinsights";
import * as https from "https";
import * as http from "http";
import * as os from "os";
class Channel {
public get _sender() {
throw new Error("unimplemented");
}
public get _buffer() {
throw new Error("unimplemented");
}
public setUseDiskRetryCaching(): void {
throw new Error("unimplemented");
}
public send(): void {
throw new Error("unimplemented");
}
public triggerSend(): void {
throw new Error("unimplemented");
}
}
export class TelemetryClient {
public context: any = undefined;
public commonProperties: any = undefined;
public config: any = {};
public channel: any = new Channel();
public addTelemetryProcessor(): void {
throw new Error("unimplemented");
}
public clearTelemetryProcessors(): void {
throw new Error("unimplemented");
}
public runTelemetryProcessors(): void {
throw new Error("unimplemented");
}
public trackTrace(): void {
throw new Error("unimplemented");
}
public trackMetric(): void {
throw new Error("unimplemented");
}
public trackException(): void {
throw new Error("unimplemented");
}
public trackRequest(): void {
throw new Error("unimplemented");
}
public trackDependency(): void {
throw new Error("unimplemented");
}
public track(): void {
throw new Error("unimplemented");
}
public trackNodeHttpRequestSync(): void {
throw new Error("unimplemented");
}
public trackNodeHttpRequest(): void {
throw new Error("unimplemented");
}
public trackNodeHttpDependency(): void {
throw new Error("unimplemented");
}
public trackEvent(options: appInsights.Contracts.EventTelemetry): void {
if (!options.properties) {
options.properties = {};
}
if (!options.measurements) {
options.measurements = {};
}
try {
const cpus = os.cpus();
options.measurements.cores = cpus.length;
options.properties["common.cpuModel"] = cpus[0].model;
} catch (error) {}
try {
options.measurements.memoryFree = os.freemem();
options.measurements.memoryTotal = os.totalmem();
} catch (error) {}
try {
options.properties["common.shell"] = os.userInfo().shell;
options.properties["common.release"] = os.release();
options.properties["common.arch"] = os.arch();
} catch (error) {}
try {
const url = process.env.TELEMETRY_URL || "https://v1.telemetry.coder.com/track";
const request = (/^http:/.test(url) ? http : https).request(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
request.on("error", () => { /* We don"t care. */ });
request.write(JSON.stringify(options));
request.end();
} catch (error) {}
}
public flush(options: { callback: (v: string) => void }): void {
if (options.callback) {
options.callback("");
}
}
}

View File

@ -1,61 +0,0 @@
import * as cp from "child_process";
import { Emitter } from "vs/base/common/event";
enum ControlMessage {
okToChild = "ok>",
okFromChild = "ok<",
}
interface RelaunchMessage {
type: "relaunch";
version: string;
}
export type Message = RelaunchMessage;
class IpcMain {
protected readonly _onMessage = new Emitter<Message>();
public readonly onMessage = this._onMessage.event;
public handshake(child?: cp.ChildProcess): Promise<void> {
return new Promise((resolve, reject) => {
const target = child || process;
if (!target.send) {
throw new Error("Not spawned with IPC enabled");
}
target.on("message", (message) => {
if (message === child ? ControlMessage.okFromChild : ControlMessage.okToChild) {
target.removeAllListeners();
target.on("message", (msg) => this._onMessage.fire(msg));
if (child) {
target.send!(ControlMessage.okToChild);
}
resolve();
}
});
if (child) {
child.once("error", reject);
child.once("exit", (code) => {
const error = new Error(`Unexpected exit with code ${code}`);
(error as any).code = code;
reject(error);
});
} else {
target.send(ControlMessage.okFromChild);
}
});
}
public relaunch(version: string): void {
this.send({ type: "relaunch", version });
}
private send(message: Message): void {
if (!process.send) {
throw new Error("Not a child process with IPC enabled");
}
process.send(message);
}
}
export const ipcMain = new IpcMain();

View File

@ -1,176 +0,0 @@
import * as fs from "fs";
import * as path from "path";
import * as util from "util";
import { CancellationToken } from "vs/base/common/cancellation";
import { mkdirp } from "vs/base/node/pfs";
import * as vszip from "vs/base/node/zip";
import * as nls from "vs/nls";
import product from "vs/platform/product/common/product";
import { localRequire } from "vs/server/src/node/util";
const tarStream = localRequire<typeof import("tar-stream")>("tar-stream/index");
// We will be overriding these, so keep a reference to the original.
const vszipExtract = vszip.extract;
const vszipBuffer = vszip.buffer;
export interface IExtractOptions {
overwrite?: boolean;
/**
* Source path within the TAR/ZIP archive. Only the files
* contained in this path will be extracted.
*/
sourcePath?: string;
}
export interface IFile {
path: string;
contents?: Buffer | string;
localPath?: string;
}
export const tar = async (tarPath: string, files: IFile[]): Promise<string> => {
const pack = tarStream.pack();
const chunks: Buffer[] = [];
const ended = new Promise<Buffer>((resolve) => {
pack.on("end", () => resolve(Buffer.concat(chunks)));
});
pack.on("data", (chunk: Buffer) => chunks.push(chunk));
for (let i = 0; i < files.length; i++) {
const file = files[i];
pack.entry({ name: file.path }, file.contents);
}
pack.finalize();
await util.promisify(fs.writeFile)(tarPath, await ended);
return tarPath;
};
export const extract = async (archivePath: string, extractPath: string, options: IExtractOptions = {}, token: CancellationToken): Promise<void> => {
try {
await extractTar(archivePath, extractPath, options, token);
} catch (error) {
if (error.toString().includes("Invalid tar header")) {
await vszipExtract(archivePath, extractPath, options, token);
}
}
};
export const buffer = (targetPath: string, filePath: string): Promise<Buffer> => {
return new Promise<Buffer>(async (resolve, reject) => {
try {
let done: boolean = false;
await extractAssets(targetPath, new RegExp(filePath), (assetPath: string, data: Buffer) => {
if (path.normalize(assetPath) === path.normalize(filePath)) {
done = true;
resolve(data);
}
});
if (!done) {
throw new Error("couldn't find asset " + filePath);
}
} catch (error) {
if (error.toString().includes("Invalid tar header")) {
vszipBuffer(targetPath, filePath).then(resolve).catch(reject);
} else {
reject(error);
}
}
});
};
const extractAssets = async (tarPath: string, match: RegExp, callback: (path: string, data: Buffer) => void): Promise<void> => {
return new Promise<void>((resolve, reject): void => {
const extractor = tarStream.extract();
const fail = (error: Error) => {
extractor.destroy();
reject(error);
};
extractor.once("error", fail);
extractor.on("entry", async (header, stream, next) => {
const name = header.name;
if (match.test(name)) {
extractData(stream).then((data) => {
callback(name, data);
next();
}).catch(fail);
} else {
stream.on("end", () => next());
stream.resume(); // Just drain it.
}
});
extractor.on("finish", resolve);
fs.createReadStream(tarPath).pipe(extractor);
});
};
const extractData = (stream: NodeJS.ReadableStream): Promise<Buffer> => {
return new Promise((resolve, reject): void => {
const fileData: Buffer[] = [];
stream.on("error", reject);
stream.on("end", () => resolve(Buffer.concat(fileData)));
stream.on("data", (data) => fileData.push(data));
});
};
const extractTar = async (tarPath: string, targetPath: string, options: IExtractOptions = {}, token: CancellationToken): Promise<void> => {
return new Promise<void>((resolve, reject): void => {
const sourcePathRegex = new RegExp(options.sourcePath ? `^${options.sourcePath}` : "");
const extractor = tarStream.extract();
const fail = (error: Error) => {
extractor.destroy();
reject(error);
};
extractor.once("error", fail);
extractor.on("entry", async (header, stream, next) => {
const nextEntry = (): void => {
stream.on("end", () => next());
stream.resume();
};
const rawName = path.normalize(header.name);
if (token.isCancellationRequested || !sourcePathRegex.test(rawName)) {
return nextEntry();
}
const fileName = rawName.replace(sourcePathRegex, "");
const targetFileName = path.join(targetPath, fileName);
if (/\/$/.test(fileName)) {
return mkdirp(targetFileName).then(nextEntry);
}
const dirName = path.dirname(fileName);
const targetDirName = path.join(targetPath, dirName);
if (targetDirName.indexOf(targetPath) !== 0) {
return fail(new Error(nls.localize("invalid file", "Error extracting {0}. Invalid file.", fileName)));
}
await mkdirp(targetDirName, undefined);
const fstream = fs.createWriteStream(targetFileName, { mode: header.mode });
fstream.once("close", () => next());
fstream.once("error", fail);
stream.pipe(fstream);
});
extractor.once("finish", resolve);
fs.createReadStream(tarPath).pipe(extractor);
});
};
/**
* Override original functionality so we can use a custom marketplace with
* either tars or zips.
*/
export const enableCustomMarketplace = (): void => {
(<any>product).extensionsGallery = { // Use `any` to override readonly.
serviceUrl: process.env.SERVICE_URL || "https://v1.extapi.coder.com",
itemUrl: process.env.ITEM_URL || "",
controlUrl: "",
recommendationsUrl: "",
...(product.extensionsGallery || {}),
};
const target = vszip as typeof vszip;
target.zip = tar;
target.extract = extract;
target.buffer = buffer;
};

View File

@ -1,86 +0,0 @@
import * as fs from "fs";
import * as path from "path";
import * as util from "util";
import { getPathFromAmdModule } from "vs/base/common/amd";
import * as lp from "vs/base/node/languagePacks";
import product from "vs/platform/product/common/product";
import { Translations } from "vs/workbench/services/extensions/common/extensionPoints";
const configurations = new Map<string, Promise<lp.NLSConfiguration>>();
const metadataPath = path.join(getPathFromAmdModule(require, ""), "nls.metadata.json");
export const isInternalConfiguration = (config: lp.NLSConfiguration): config is lp.InternalNLSConfiguration => {
return config && !!(<lp.InternalNLSConfiguration>config)._languagePackId;
};
const DefaultConfiguration = {
locale: "en",
availableLanguages: {},
};
export const getNlsConfiguration = async (locale: string, userDataPath: string): Promise<lp.NLSConfiguration> => {
const id = `${locale}: ${userDataPath}`;
if (!configurations.has(id)) {
configurations.set(id, new Promise(async (resolve) => {
const config = product.commit && await util.promisify(fs.exists)(metadataPath)
? await lp.getNLSConfiguration(product.commit, userDataPath, metadataPath, locale)
: DefaultConfiguration;
if (isInternalConfiguration(config)) {
config._languagePackSupport = true;
}
// If the configuration has no results keep trying since code-server
// doesn't restart when a language is installed so this result would
// persist (the plugin might not be installed yet or something).
if (config.locale !== "en" && config.locale !== "en-us" && Object.keys(config.availableLanguages).length === 0) {
configurations.delete(id);
}
resolve(config);
}));
}
return configurations.get(id)!;
};
export const getTranslations = async (locale: string, userDataPath: string): Promise<Translations> => {
const config = await getNlsConfiguration(locale, userDataPath);
if (isInternalConfiguration(config)) {
try {
return JSON.parse(await util.promisify(fs.readFile)(config._translationsConfigFile, "utf8"));
} catch (error) { /* Nothing yet. */}
}
return {};
};
export const getLocaleFromConfig = async (userDataPath: string): Promise<string> => {
let locale = "en";
try {
const localeConfigUri = path.join(userDataPath, "User/locale.json");
const content = stripComments(await util.promisify(fs.readFile)(localeConfigUri, "utf8"));
locale = JSON.parse(content).locale;
} catch (error) { /* Ignore. */ }
return locale;
};
// Taken from src/main.js in the main VS Code source.
const stripComments = (content: string): string => {
const regexp = /("(?:[^\\"]*(?:\\.)?)*")|('(?:[^\\']*(?:\\.)?)*')|(\/\*(?:\r?\n|.)*?\*\/)|(\/{2,}.*?(?:(?:\r?\n)|$))/g;
return content.replace(regexp, (match, _m1, _m2, m3, m4) => {
// Only one of m1, m2, m3, m4 matches
if (m3) {
// A block comment. Replace with nothing
return '';
} else if (m4) {
// A line comment. If it ends in \r?\n then keep it.
const length_1 = m4.length;
if (length_1 > 2 && m4[length_1 - 1] === '\n') {
return m4[length_1 - 2] === '\r' ? '\r\n' : '\n';
}
else {
return '';
}
} else {
// We match a string
return match;
}
});
};

View File

@ -1,73 +0,0 @@
import * as net from "net";
import { VSBuffer } from "vs/base/common/buffer";
import { PersistentProtocol } from "vs/base/parts/ipc/common/ipc.net";
import { NodeSocket, WebSocketNodeSocket } from "vs/base/parts/ipc/node/ipc.net";
import { AuthRequest, ConnectionTypeRequest, HandshakeMessage } from "vs/platform/remote/common/remoteAgentConnection";
export interface SocketOptions {
readonly reconnectionToken: string;
readonly reconnection: boolean;
readonly skipWebSocketFrames: boolean;
}
export class Protocol extends PersistentProtocol {
public constructor(socket: net.Socket, public readonly options: SocketOptions) {
super(
options.skipWebSocketFrames
? new NodeSocket(socket)
: new WebSocketNodeSocket(new NodeSocket(socket)),
);
}
public getUnderlyingSocket(): net.Socket {
const socket = this.getSocket();
return socket instanceof NodeSocket
? socket.socket
: (socket as WebSocketNodeSocket).socket.socket;
}
/**
* Perform a handshake to get a connection request.
*/
public handshake(): Promise<ConnectionTypeRequest> {
return new Promise((resolve, reject) => {
const handler = this.onControlMessage((rawMessage) => {
try {
const message = JSON.parse(rawMessage.toString());
switch (message.type) {
case "auth": return this.authenticate(message);
case "connectionType":
handler.dispose();
return resolve(message);
default: throw new Error("Unrecognized message type");
}
} catch (error) {
handler.dispose();
reject(error);
}
});
});
}
/**
* TODO: This ignores the authentication process entirely for now.
*/
private authenticate(_message: AuthRequest): void {
this.sendMessage({ type: "sign", data: "" });
}
/**
* TODO: implement.
*/
public tunnel(): void {
throw new Error("Tunnel is not implemented yet");
}
/**
* Send a handshake message. In the case of the extension host, it just sends
* back a debug port.
*/
public sendMessage(message: HandshakeMessage | { debugPort?: number } ): void {
this.sendControl(VSBuffer.fromString(JSON.stringify(message)));
}
}

View File

@ -1,957 +0,0 @@
import * as crypto from "crypto";
import * as fs from "fs";
import * as http from "http";
import * as https from "https";
import * as net from "net";
import * as path from "path";
import * as querystring from "querystring";
import { Readable } from "stream";
import * as tls from "tls";
import * as url from "url";
import * as util from "util";
import { Emitter } from "vs/base/common/event";
import { sanitizeFilePath } from "vs/base/common/extpath";
import { Schemas } from "vs/base/common/network";
import { URI, UriComponents } from "vs/base/common/uri";
import { generateUuid } from "vs/base/common/uuid";
import { getMachineId } from 'vs/base/node/id';
import { NLSConfiguration } from "vs/base/node/languagePacks";
import { mkdirp, rimraf } from "vs/base/node/pfs";
import { ClientConnectionEvent, IPCServer, IServerChannel } from "vs/base/parts/ipc/common/ipc";
import { createChannelReceiver } from "vs/base/parts/ipc/node/ipc";
import { LogsDataCleaner } from "vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner";
import { IConfigurationService } from "vs/platform/configuration/common/configuration";
import { ConfigurationService } from "vs/platform/configuration/node/configurationService";
import { ExtensionHostDebugBroadcastChannel } from "vs/platform/debug/common/extensionHostDebugIpc";
import { IEnvironmentService, ParsedArgs } from "vs/platform/environment/common/environment";
import { EnvironmentService } from "vs/platform/environment/node/environmentService";
import { ExtensionGalleryService } from "vs/platform/extensionManagement/common/extensionGalleryService";
import { IExtensionGalleryService, IExtensionManagementService } from "vs/platform/extensionManagement/common/extensionManagement";
import { ExtensionManagementChannel } from "vs/platform/extensionManagement/common/extensionManagementIpc";
import { ExtensionManagementService } from "vs/platform/extensionManagement/node/extensionManagementService";
import { IFileService } from "vs/platform/files/common/files";
import { FileService } from "vs/platform/files/common/fileService";
import { DiskFileSystemProvider } from "vs/platform/files/node/diskFileSystemProvider";
import { SyncDescriptor } from "vs/platform/instantiation/common/descriptors";
import { InstantiationService } from "vs/platform/instantiation/common/instantiationService";
import { ServiceCollection } from "vs/platform/instantiation/common/serviceCollection";
import { ILocalizationsService } from "vs/platform/localizations/common/localizations";
import { LocalizationsService } from "vs/platform/localizations/node/localizations";
import { getLogLevel, ILogService } from "vs/platform/log/common/log";
import { LoggerChannel } from "vs/platform/log/common/logIpc";
import { SpdLogService } from "vs/platform/log/node/spdlogService";
import product from 'vs/platform/product/common/product';
import { IProductService } from "vs/platform/product/common/productService";
import { ConnectionType, ConnectionTypeRequest } from "vs/platform/remote/common/remoteAgentConnection";
import { RemoteAgentConnectionContext } from "vs/platform/remote/common/remoteAgentEnvironment";
import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from "vs/platform/remote/common/remoteAgentFileSystemChannel";
import { IRequestService } from "vs/platform/request/common/request";
import { RequestChannel } from "vs/platform/request/common/requestIpc";
import { RequestService } from "vs/platform/request/node/requestService";
import ErrorTelemetry from "vs/platform/telemetry/browser/errorTelemetry";
import { ITelemetryService } from "vs/platform/telemetry/common/telemetry";
import { ITelemetryServiceConfig, TelemetryService } from "vs/platform/telemetry/common/telemetryService";
import { combinedAppender, LogAppender, NullTelemetryService } from "vs/platform/telemetry/common/telemetryUtils";
import { AppInsightsAppender } from "vs/platform/telemetry/node/appInsightsAppender";
import { resolveCommonProperties } from "vs/platform/telemetry/node/commonProperties";
import { UpdateChannel } from "vs/platform/update/electron-main/updateIpc";
import { INodeProxyService, NodeProxyChannel } from "vs/server/src/common/nodeProxy";
import { TelemetryChannel } from "vs/server/src/common/telemetry";
import { split } from "vs/server/src/common/util";
import { ExtensionEnvironmentChannel, FileProviderChannel, NodeProxyService } from "vs/server/src/node/channel";
import { Connection, ExtensionHostConnection, ManagementConnection } from "vs/server/src/node/connection";
import { TelemetryClient } from "vs/server/src/node/insights";
import { getLocaleFromConfig, getNlsConfiguration } from "vs/server/src/node/nls";
import { Protocol } from "vs/server/src/node/protocol";
import { UpdateService } from "vs/server/src/node/update";
import { AuthType, getMediaMime, getUriTransformer, hash, localRequire, tmpdir } from "vs/server/src/node/util";
import { RemoteExtensionLogFileName } from "vs/workbench/services/remote/common/remoteAgentService";
import { IWorkbenchConstructionOptions } from "vs/workbench/workbench.web.api";
const tarFs = localRequire<typeof import("tar-fs")>("tar-fs/index");
export enum HttpCode {
Ok = 200,
Redirect = 302,
NotFound = 404,
BadRequest = 400,
Unauthorized = 401,
LargePayload = 413,
ServerError = 500,
}
export interface Options {
WORKBENCH_WEB_CONFIGURATION: IWorkbenchConstructionOptions & { folderUri?: UriComponents, workspaceUri?: UriComponents };
REMOTE_USER_DATA_URI: UriComponents | URI;
PRODUCT_CONFIGURATION: Partial<IProductService>;
NLS_CONFIGURATION: NLSConfiguration;
}
export interface Response {
cache?: boolean;
code?: number;
content?: string | Buffer;
filePath?: string;
headers?: http.OutgoingHttpHeaders;
mime?: string;
redirect?: string;
stream?: Readable;
}
export interface LoginPayload {
password?: string;
}
export interface AuthPayload {
key?: string[];
}
export class HttpError extends Error {
public constructor(message: string, public readonly code: number) {
super(message);
// @ts-ignore
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
export interface ServerOptions {
readonly auth: AuthType;
readonly basePath?: string;
readonly connectionToken?: string;
readonly cert?: string;
readonly certKey?: string;
readonly openUri?: string;
readonly host?: string;
readonly password?: string;
readonly port?: number;
readonly socket?: string;
}
export abstract class Server {
protected readonly server: http.Server | https.Server;
protected rootPath = path.resolve(__dirname, "../../../../..");
protected serverRoot = path.join(this.rootPath, "/out/vs/server/src");
protected readonly allowedRequestPaths: string[] = [this.rootPath];
private listenPromise: Promise<string> | undefined;
public readonly protocol: "http" | "https";
public readonly options: ServerOptions;
public constructor(options: ServerOptions) {
this.options = {
host: options.auth === "password" && options.cert ? "0.0.0.0" : "localhost",
...options,
basePath: options.basePath ? options.basePath.replace(/\/+$/, "") : "",
password: options.password ? hash(options.password) : undefined,
};
this.protocol = this.options.cert ? "https" : "http";
if (this.protocol === "https") {
const httpolyglot = localRequire<typeof import("httpolyglot")>("httpolyglot/lib/index");
this.server = httpolyglot.createServer({
cert: this.options.cert && fs.readFileSync(this.options.cert),
key: this.options.certKey && fs.readFileSync(this.options.certKey),
}, this.onRequest);
} else {
this.server = http.createServer(this.onRequest);
}
}
public listen(): Promise<string> {
if (!this.listenPromise) {
this.listenPromise = new Promise((resolve, reject) => {
this.server.on("error", reject);
this.server.on("upgrade", this.onUpgrade);
const onListen = () => resolve(this.address());
if (this.options.socket) {
this.server.listen(this.options.socket, onListen);
} else {
this.server.listen(this.options.port, this.options.host, onListen);
}
});
}
return this.listenPromise;
}
/**
* The *local* address of the server.
*/
public address(): string {
const address = this.server.address();
const endpoint = typeof address !== "string"
? (address.address === "::" ? "localhost" : address.address) + ":" + address.port
: address;
return `${this.protocol}://${endpoint}`;
}
protected abstract handleWebSocket(
socket: net.Socket,
parsedUrl: url.UrlWithParsedQuery
): Promise<void>;
protected abstract handleRequest(
base: string,
requestPath: string,
parsedUrl: url.UrlWithParsedQuery,
request: http.IncomingMessage,
): Promise<Response>;
protected async getResource(...parts: string[]): Promise<Response> {
const filePath = this.ensureAuthorizedFilePath(...parts);
return { content: await util.promisify(fs.readFile)(filePath), filePath };
}
protected async getAnyResource(...parts: string[]): Promise<Response> {
const filePath = path.join(...parts);
return { content: await util.promisify(fs.readFile)(filePath), filePath };
}
protected async getTarredResource(...parts: string[]): Promise<Response> {
const filePath = this.ensureAuthorizedFilePath(...parts);
return { stream: tarFs.pack(filePath), filePath, mime: "application/tar", cache: true };
}
protected ensureAuthorizedFilePath(...parts: string[]): string {
const filePath = path.join(...parts);
if (!this.isAllowedRequestPath(filePath)) {
throw new HttpError("Unauthorized", HttpCode.Unauthorized);
}
return filePath;
}
protected withBase(request: http.IncomingMessage, path: string): string {
const [, query] = request.url ? split(request.url, "?") : [];
return `${this.protocol}://${request.headers.host}${this.options.basePath}${path}${query ? `?${query}` : ""}`;
}
private isAllowedRequestPath(path: string): boolean {
for (let i = 0; i < this.allowedRequestPaths.length; ++i) {
if (path.indexOf(this.allowedRequestPaths[i]) === 0) {
return true;
}
}
return false;
}
private onRequest = async (request: http.IncomingMessage, response: http.ServerResponse): Promise<void> => {
try {
const parsedUrl = request.url ? url.parse(request.url, true) : { query: {}};
const payload = await this.preHandleRequest(request, parsedUrl);
response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, {
"Content-Type": payload.mime || getMediaMime(payload.filePath),
...(payload.redirect ? { Location: this.withBase(request, payload.redirect) } : {}),
...(request.headers["service-worker"] ? { "Service-Worker-Allowed": this.options.basePath || "/" } : {}),
...(payload.cache ? { "Cache-Control": "public, max-age=31536000" } : {}),
...payload.headers,
});
if (payload.stream) {
payload.stream.on("error", (error: NodeJS.ErrnoException) => {
response.writeHead(error.code === "ENOENT" ? HttpCode.NotFound : HttpCode.ServerError);
response.end(error.message);
});
payload.stream.pipe(response);
} else {
response.end(payload.content);
}
} catch (error) {
if (error.code === "ENOENT" || error.code === "EISDIR") {
error = new HttpError("Not found", HttpCode.NotFound);
}
response.writeHead(typeof error.code === "number" ? error.code : HttpCode.ServerError);
response.end(error.message);
}
}
private async preHandleRequest(request: http.IncomingMessage, parsedUrl: url.UrlWithParsedQuery): Promise<Response> {
const secure = (request.connection as tls.TLSSocket).encrypted;
if (this.options.cert && !secure) {
return { redirect: request.url };
}
const fullPath = decodeURIComponent(parsedUrl.pathname || "/");
const match = fullPath.match(/^(\/?[^/]*)(.*)$/);
let [/* ignore */, base, requestPath] = match
? match.map((p) => p.replace(/\/+$/, ""))
: ["", "", ""];
if (base.indexOf(".") !== -1) { // Assume it's a file at the root.
requestPath = base;
base = "/";
} else if (base === "") { // Happens if it's a plain `domain.com`.
base = "/";
}
base = path.normalize(base);
requestPath = path.normalize(requestPath || "/index.html");
if (base !== "/login" || this.options.auth !== "password" || requestPath !== "/index.html") {
this.ensureGet(request);
}
// Allow for a versioned static endpoint. This lets us cache every static
// resource underneath the path based on the version without any work and
// without adding query parameters which have their own issues.
// REVIEW: Discuss whether this is the best option; this is sort of a quick
// hack almost to get caching in the meantime but it does work pretty well.
if (/^\/static-/.test(base)) {
base = "/static";
}
switch (base) {
case "/":
switch (requestPath) {
// NOTE: This must be served at the correct location based on the
// start_url in the manifest.
case "/manifest.json":
case "/code-server.png":
const response = await this.getResource(this.serverRoot, "media", requestPath);
response.cache = true;
return response;
}
if (!this.authenticate(request)) {
return { redirect: "/login" };
}
break;
case "/static":
const response = await this.getResource(this.rootPath, requestPath);
response.cache = true;
return response;
case "/login":
if (this.options.auth !== "password" || requestPath !== "/index.html") {
throw new HttpError("Not found", HttpCode.NotFound);
}
return this.tryLogin(request);
default:
if (!this.authenticate(request)) {
throw new HttpError("Unauthorized", HttpCode.Unauthorized);
}
break;
}
return this.handleRequest(base, requestPath, parsedUrl, request);
}
private onUpgrade = async (request: http.IncomingMessage, socket: net.Socket): Promise<void> => {
try {
await this.preHandleWebSocket(request, socket);
} catch (error) {
socket.destroy();
console.error(error.message);
}
}
private preHandleWebSocket(request: http.IncomingMessage, socket: net.Socket): Promise<void> {
socket.on("error", () => socket.destroy());
socket.on("end", () => socket.destroy());
this.ensureGet(request);
if (!this.authenticate(request)) {
throw new HttpError("Unauthorized", HttpCode.Unauthorized);
} else if (!request.headers.upgrade || request.headers.upgrade.toLowerCase() !== "websocket") {
throw new Error("HTTP/1.1 400 Bad Request");
}
// This magic value is specified by the websocket spec.
const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
const reply = crypto.createHash("sha1")
.update(<string>request.headers["sec-websocket-key"] + magic)
.digest("base64");
socket.write([
"HTTP/1.1 101 Switching Protocols",
"Upgrade: websocket",
"Connection: Upgrade",
`Sec-WebSocket-Accept: ${reply}`,
].join("\r\n") + "\r\n\r\n");
const parsedUrl = request.url ? url.parse(request.url, true) : { query: {}};
return this.handleWebSocket(socket, parsedUrl);
}
private async tryLogin(request: http.IncomingMessage): Promise<Response> {
const redirect = (password: string | true) => {
return {
redirect: "/",
headers: typeof password === "string"
? { "Set-Cookie": `key=${password}; Path=${this.options.basePath || "/"}; HttpOnly; SameSite=strict` }
: {},
};
};
const providedPassword = this.authenticate(request);
if (providedPassword && (request.method === "GET" || request.method === "POST")) {
return redirect(providedPassword);
}
if (request.method === "POST") {
const data = await this.getData<LoginPayload>(request);
const password = this.authenticate(request, {
key: typeof data.password === "string" ? [hash(data.password)] : undefined,
});
if (password) {
return redirect(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 this.getLogin("Invalid password", data);
}
this.ensureGet(request);
return this.getLogin();
}
private async getLogin(error: string = "", payload?: LoginPayload): Promise<Response> {
const filePath = path.join(this.serverRoot, "browser/login.html");
const content = (await util.promisify(fs.readFile)(filePath, "utf8"))
.replace("{{ERROR}}", error)
.replace("display:none", error ? "display:block" : "display:none")
.replace('value=""', `value="${payload && payload.password || ""}"`);
return { content, filePath };
}
private ensureGet(request: http.IncomingMessage): void {
if (request.method !== "GET") {
throw new HttpError(`Unsupported method ${request.method}`, HttpCode.BadRequest);
}
}
private getData<T extends object>(request: http.IncomingMessage): Promise<T> {
return request.method === "POST"
? new Promise<T>((resolve, reject) => {
let body = "";
const onEnd = (): void => {
off();
resolve(querystring.parse(body) as T);
};
const onError = (error: Error): void => {
off();
reject(error);
};
const onData = (d: Buffer): void => {
body += d;
if (body.length > 1e6) {
onError(new HttpError("Payload is too large", HttpCode.LargePayload));
request.connection.destroy();
}
};
const off = (): void => {
request.off("error", onError);
request.off("data", onError);
request.off("end", onEnd);
};
request.on("error", onError);
request.on("data", onData);
request.on("end", onEnd);
})
: Promise.resolve({} as T);
}
private authenticate(request: http.IncomingMessage, payload?: AuthPayload): string | boolean {
if (this.options.auth === "none") {
return true;
}
const safeCompare = localRequire<typeof import("safe-compare")>("safe-compare/index");
if (typeof payload === "undefined") {
payload = this.parseCookies<AuthPayload>(request);
}
if (this.options.password && payload.key) {
for (let i = 0; i < payload.key.length; ++i) {
if (safeCompare(payload.key[i], this.options.password)) {
return payload.key[i];
}
}
}
return false;
}
private parseCookies<T extends object>(request: http.IncomingMessage): T {
const cookies: { [key: string]: string[] } = {};
if (request.headers.cookie) {
request.headers.cookie.split(";").forEach((keyValue) => {
const [key, value] = split(keyValue, "=");
if (!cookies[key]) {
cookies[key] = [];
}
cookies[key].push(decodeURI(value));
});
}
return cookies as T;
}
}
interface StartPath {
path?: string[] | string;
workspace?: boolean;
}
interface Settings {
lastVisited?: StartPath;
}
export class MainServer extends Server {
public readonly _onDidClientConnect = new Emitter<ClientConnectionEvent>();
public readonly onDidClientConnect = this._onDidClientConnect.event;
private readonly ipc = new IPCServer<RemoteAgentConnectionContext>(this.onDidClientConnect);
private readonly maxExtraOfflineConnections = 0;
private readonly connections = new Map<ConnectionType, Map<string, Connection>>();
private readonly services = new ServiceCollection();
private readonly servicesPromise: Promise<void>;
public readonly _onProxyConnect = new Emitter<net.Socket>();
private proxyPipe = path.join(tmpdir, "tls-proxy");
private _proxyServer?: Promise<net.Server>;
private readonly proxyTimeout = 5000;
private settings: Settings = {};
private heartbeatTimer?: NodeJS.Timeout;
private heartbeatInterval = 60000;
private lastHeartbeat = 0;
public constructor(options: ServerOptions, args: ParsedArgs) {
super(options);
this.servicesPromise = this.initializeServices(args);
}
public async listen(): Promise<string> {
const environment = (this.services.get(IEnvironmentService) as EnvironmentService);
const [address] = await Promise.all<string>([
super.listen(), ...[
environment.extensionsPath,
].map((p) => mkdirp(p).then(() => p)),
]);
return address;
}
protected async handleWebSocket(socket: net.Socket, parsedUrl: url.UrlWithParsedQuery): Promise<void> {
this.heartbeat();
if (!parsedUrl.query.reconnectionToken) {
throw new Error("Reconnection token is missing from query parameters");
}
const protocol = new Protocol(await this.createProxy(socket), {
reconnectionToken: <string>parsedUrl.query.reconnectionToken,
reconnection: parsedUrl.query.reconnection === "true",
skipWebSocketFrames: parsedUrl.query.skipWebSocketFrames === "true",
});
try {
await this.connect(await protocol.handshake(), protocol);
} catch (error) {
protocol.sendMessage({ type: "error", reason: error.message });
protocol.dispose();
protocol.getSocket().dispose();
}
}
protected async handleRequest(
base: string,
requestPath: string,
parsedUrl: url.UrlWithParsedQuery,
request: http.IncomingMessage,
): Promise<Response> {
this.heartbeat();
switch (base) {
case "/": return this.getRoot(request, parsedUrl);
case "/resource":
case "/vscode-remote-resource":
if (typeof parsedUrl.query.path === "string") {
return this.getAnyResource(parsedUrl.query.path);
}
break;
case "/tar":
if (typeof parsedUrl.query.path === "string") {
return this.getTarredResource(parsedUrl.query.path);
}
break;
case "/webview":
if (/^\/vscode-resource/.test(requestPath)) {
return this.getAnyResource(requestPath.replace(/^\/vscode-resource(\/file)?/, ""));
}
return this.getResource(
this.rootPath,
"out/vs/workbench/contrib/webview/browser/pre",
requestPath
);
}
throw new HttpError("Not found", HttpCode.NotFound);
}
private async getRoot(request: http.IncomingMessage, parsedUrl: url.UrlWithParsedQuery): Promise<Response> {
const filePath = path.join(this.serverRoot, "browser/workbench.html");
let [content, startPath] = await Promise.all([
util.promisify(fs.readFile)(filePath, "utf8"),
this.getFirstValidPath([
{ path: parsedUrl.query.workspace, workspace: true },
{ path: parsedUrl.query.folder, workspace: false },
(await this.readSettings()).lastVisited,
{ path: this.options.openUri }
]),
this.servicesPromise,
]);
if (startPath) {
this.writeSettings({
lastVisited: {
path: startPath.uri.fsPath,
workspace: startPath.workspace
},
});
}
const logger = this.services.get(ILogService) as ILogService;
logger.info("request.url", `"${request.url}"`);
const remoteAuthority = request.headers.host as string;
const transformer = getUriTransformer(remoteAuthority);
const environment = this.services.get(IEnvironmentService) as IEnvironmentService;
const options: Options = {
WORKBENCH_WEB_CONFIGURATION: {
workspaceUri: startPath && startPath.workspace ? transformer.transformOutgoing(startPath.uri) : undefined,
folderUri: startPath && !startPath.workspace ? transformer.transformOutgoing(startPath.uri) : undefined,
remoteAuthority,
logLevel: getLogLevel(environment),
},
REMOTE_USER_DATA_URI: transformer.transformOutgoing(URI.file(environment.userDataPath)),
PRODUCT_CONFIGURATION: {
extensionsGallery: product.extensionsGallery,
},
NLS_CONFIGURATION: await getNlsConfiguration(environment.args.locale || await getLocaleFromConfig(environment.userDataPath), environment.userDataPath),
};
content = content.replace(/{{COMMIT}}/g, product.commit || "");
for (const key in options) {
content = content.replace(`"{{${key}}}"`, `'${JSON.stringify(options[key as keyof Options])}'`);
}
return { content, filePath };
}
/**
* Choose the first valid path. If `workspace` is undefined then either a
* workspace or a directory are acceptable. Otherwise it must be a file if a
* workspace or a directory otherwise.
*/
private async getFirstValidPath(startPaths: Array<StartPath | undefined>): Promise<{ uri: URI, workspace?: boolean} | undefined> {
const logger = this.services.get(ILogService) as ILogService;
const cwd = process.env.VSCODE_CWD || process.cwd();
for (let i = 0; i < startPaths.length; ++i) {
const startPath = startPaths[i];
if (!startPath) {
continue;
}
const paths = typeof startPath.path === "string" ? [startPath.path] : (startPath.path || []);
for (let j = 0; j < paths.length; ++j) {
const uri = URI.file(sanitizeFilePath(paths[j], cwd));
try {
const stat = await util.promisify(fs.stat)(uri.fsPath);
if (typeof startPath.workspace === "undefined" || startPath.workspace !== stat.isDirectory()) {
return { uri, workspace: !stat.isDirectory() };
}
} catch (error) {
logger.warn(error.message);
}
}
}
return undefined;
}
private async connect(message: ConnectionTypeRequest, protocol: Protocol): Promise<void> {
if (product.commit && message.commit !== product.commit) {
throw new Error(`Version mismatch (${message.commit} instead of ${product.commit})`);
}
switch (message.desiredConnectionType) {
case ConnectionType.ExtensionHost:
case ConnectionType.Management:
if (!this.connections.has(message.desiredConnectionType)) {
this.connections.set(message.desiredConnectionType, new Map());
}
const connections = this.connections.get(message.desiredConnectionType)!;
const ok = async () => {
return message.desiredConnectionType === ConnectionType.ExtensionHost
? { debugPort: await this.getDebugPort() }
: { type: "ok" };
};
const token = protocol.options.reconnectionToken;
if (protocol.options.reconnection && connections.has(token)) {
protocol.sendMessage(await ok());
const buffer = protocol.readEntireBuffer();
protocol.dispose();
return connections.get(token)!.reconnect(protocol.getSocket(), buffer);
} else if (protocol.options.reconnection || connections.has(token)) {
throw new Error(protocol.options.reconnection
? "Unrecognized reconnection token"
: "Duplicate reconnection token"
);
}
protocol.sendMessage(await ok());
let connection: Connection;
if (message.desiredConnectionType === ConnectionType.Management) {
connection = new ManagementConnection(protocol, token);
this._onDidClientConnect.fire({
protocol, onDidClientDisconnect: connection.onClose,
});
// TODO: Need a way to match clients with a connection. For now
// dispose everything which only works because no extensions currently
// utilize long-running proxies.
(this.services.get(INodeProxyService) as NodeProxyService)._onUp.fire();
connection.onClose(() => (this.services.get(INodeProxyService) as NodeProxyService)._onDown.fire());
} else {
const buffer = protocol.readEntireBuffer();
connection = new ExtensionHostConnection(
message.args ? message.args.language : "en",
protocol, buffer, token,
this.services.get(ILogService) as ILogService,
this.services.get(IEnvironmentService) as IEnvironmentService,
);
}
connections.set(token, connection);
connection.onClose(() => connections.delete(token));
this.disposeOldOfflineConnections(connections);
break;
case ConnectionType.Tunnel: return protocol.tunnel();
default: throw new Error("Unrecognized connection type");
}
}
private disposeOldOfflineConnections(connections: Map<string, Connection>): void {
const offline = Array.from(connections.values())
.filter((connection) => typeof connection.offline !== "undefined");
for (let i = 0, max = offline.length - this.maxExtraOfflineConnections; i < max; ++i) {
offline[i].dispose();
}
}
private async initializeServices(args: ParsedArgs): Promise<void> {
const environmentService = new EnvironmentService(args, process.execPath);
const logService = new SpdLogService(RemoteExtensionLogFileName, environmentService.logsPath, getLogLevel(environmentService));
const fileService = new FileService(logService);
fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(logService));
this.allowedRequestPaths.push(
path.join(environmentService.userDataPath, "clp"), // Language packs.
environmentService.extensionsPath,
environmentService.builtinExtensionsPath,
...environmentService.extraExtensionPaths,
...environmentService.extraBuiltinExtensionPaths,
);
this.ipc.registerChannel("logger", new LoggerChannel(logService));
this.ipc.registerChannel(ExtensionHostDebugBroadcastChannel.ChannelName, new ExtensionHostDebugBroadcastChannel());
this.services.set(ILogService, logService);
this.services.set(IEnvironmentService, environmentService);
this.services.set(IConfigurationService, new SyncDescriptor(ConfigurationService, [environmentService.machineSettingsResource]));
this.services.set(IRequestService, new SyncDescriptor(RequestService));
this.services.set(IFileService, fileService);
this.services.set(IProductService, { _serviceBrand: undefined, ...product });
this.services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService));
this.services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService));
if (!environmentService.args["disable-telemetry"]) {
this.services.set(ITelemetryService, new SyncDescriptor(TelemetryService, [{
appender: combinedAppender(
new AppInsightsAppender("code-server", null, () => new TelemetryClient() as any, logService),
new LogAppender(logService),
),
commonProperties: resolveCommonProperties(
product.commit, product.codeServerVersion, await getMachineId(),
[], environmentService.installSourcePath, "code-server",
),
piiPaths: this.allowedRequestPaths,
} as ITelemetryServiceConfig]));
} else {
this.services.set(ITelemetryService, NullTelemetryService);
}
await new Promise((resolve) => {
const instantiationService = new InstantiationService(this.services);
this.services.set(ILocalizationsService, instantiationService.createInstance(LocalizationsService));
this.services.set(INodeProxyService, instantiationService.createInstance(NodeProxyService));
instantiationService.invokeFunction(() => {
instantiationService.createInstance(LogsDataCleaner);
const telemetryService = this.services.get(ITelemetryService) as ITelemetryService;
this.ipc.registerChannel("extensions", new ExtensionManagementChannel(
this.services.get(IExtensionManagementService) as IExtensionManagementService,
(context) => getUriTransformer(context.remoteAuthority),
));
this.ipc.registerChannel("remoteextensionsenvironment", new ExtensionEnvironmentChannel(
environmentService, logService, telemetryService, this.options.connectionToken || "",
));
this.ipc.registerChannel("request", new RequestChannel(this.services.get(IRequestService) as IRequestService));
this.ipc.registerChannel("telemetry", new TelemetryChannel(telemetryService));
this.ipc.registerChannel("nodeProxy", new NodeProxyChannel(this.services.get(INodeProxyService) as INodeProxyService));
this.ipc.registerChannel("localizations", <IServerChannel<any>>createChannelReceiver(this.services.get(ILocalizationsService) as ILocalizationsService));
this.ipc.registerChannel("update", new UpdateChannel(instantiationService.createInstance(UpdateService)));
this.ipc.registerChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME, new FileProviderChannel(environmentService, logService));
resolve(new ErrorTelemetry(telemetryService));
});
});
}
/**
* TODO: implement.
*/
private async getDebugPort(): Promise<number | undefined> {
return undefined;
}
/**
* Since we can't pass TLS sockets to children, use this to proxy the socket
* and pass a non-TLS socket.
*/
private createProxy = async (socket: net.Socket): Promise<net.Socket> => {
if (!(socket instanceof tls.TLSSocket)) {
return socket;
}
await this.startProxyServer();
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
listener.dispose();
socket.destroy();
proxy.destroy();
reject(new Error("TLS socket proxy timed out"));
}, this.proxyTimeout);
const listener = this._onProxyConnect.event((connection) => {
connection.once("data", (data) => {
if (!socket.destroyed && !proxy.destroyed && data.toString() === id) {
clearTimeout(timeout);
listener.dispose();
[[proxy, socket], [socket, proxy]].forEach(([a, b]) => {
a.pipe(b);
a.on("error", () => b.destroy());
a.on("close", () => b.destroy());
a.on("end", () => b.end());
});
resolve(connection);
}
});
});
const id = generateUuid();
const proxy = net.connect(this.proxyPipe);
proxy.once("connect", () => proxy.write(id));
});
}
private async startProxyServer(): Promise<net.Server> {
if (!this._proxyServer) {
this._proxyServer = new Promise(async (resolve) => {
this.proxyPipe = await this.findFreeSocketPath(this.proxyPipe);
await mkdirp(tmpdir);
await rimraf(this.proxyPipe);
const proxyServer = net.createServer((p) => this._onProxyConnect.fire(p));
proxyServer.once("listening", resolve);
proxyServer.listen(this.proxyPipe);
});
}
return this._proxyServer;
}
private async findFreeSocketPath(basePath: string, maxTries: number = 100): Promise<string> {
const canConnect = (path: string): Promise<boolean> => {
return new Promise((resolve) => {
const socket = net.connect(path);
socket.once("error", () => resolve(false));
socket.once("connect", () => {
socket.destroy();
resolve(true);
});
});
};
let i = 0;
let path = basePath;
while (await canConnect(path) && i < maxTries) {
path = `${basePath}-${++i}`;
}
return path;
}
/**
* Return the file path for Coder settings.
*/
private get settingsPath(): string {
const environment = this.services.get(IEnvironmentService) as IEnvironmentService;
return path.join(environment.userDataPath, "coder.json");
}
/**
* Read settings from the file. On a failure return last known settings and
* log a warning.
*
*/
private async readSettings(): Promise<Settings> {
try {
const raw = (await util.promisify(fs.readFile)(this.settingsPath, "utf8")).trim();
this.settings = raw ? JSON.parse(raw) : {};
} catch (error) {
if (error.code !== "ENOENT") {
(this.services.get(ILogService) as ILogService).warn(error.message);
}
}
return this.settings;
}
/**
* Write settings combined with current settings. On failure log a warning.
*/
private async writeSettings(newSettings: Partial<Settings>): Promise<void> {
this.settings = { ...this.settings, ...newSettings };
try {
await util.promisify(fs.writeFile)(this.settingsPath, JSON.stringify(this.settings));
} catch (error) {
(this.services.get(ILogService) as ILogService).warn(error.message);
}
}
/**
* Return the file path for the heartbeat file.
*/
private get heartbeatPath(): string {
const environment = this.services.get(IEnvironmentService) as IEnvironmentService;
return path.join(environment.userDataPath, "heartbeat");
}
/**
* Return all online connections regardless of type.
*/
private get onlineConnections(): Connection[] {
const online = <Connection[]>[];
this.connections.forEach((connections) => {
connections.forEach((connection) => {
if (typeof connection.offline === "undefined") {
online.push(connection);
}
});
});
return online;
}
/**
* Write to the heartbeat file if we haven't already done so within the
* timeout and start or reset a timer that keeps running as long as there are
* active connections. Failures are logged as warnings.
*/
private heartbeat(): void {
const now = Date.now();
if (now - this.lastHeartbeat >= this.heartbeatInterval) {
util.promisify(fs.writeFile)(this.heartbeatPath, "").catch((error) => {
(this.services.get(ILogService) as ILogService).warn(error.message);
});
this.lastHeartbeat = now;
clearTimeout(this.heartbeatTimer!); // We can clear undefined so ! is fine.
this.heartbeatTimer = setTimeout(() => {
if (this.onlineConnections.length > 0) {
this.heartbeat();
}
}, this.heartbeatInterval);
}
}
}

40
src/node/settings.ts Normal file
View File

@ -0,0 +1,40 @@
import * as fs from "fs-extra"
import { logger } from "@coder/logger"
import { extend } from "./util"
export type Settings = { [key: string]: Settings | string | boolean | number }
/**
* Provides read and write access to settings.
*/
export class SettingsProvider<T> {
public constructor(private readonly settingsPath: string) {}
/**
* Read settings from the file. On a failure return last known settings and
* log a warning.
*/
public async read(): Promise<T> {
try {
const raw = (await fs.readFile(this.settingsPath, "utf8")).trim()
return raw ? JSON.parse(raw) : {}
} catch (error) {
if (error.code !== "ENOENT") {
logger.warn(error.message)
}
}
return {} as T
}
/**
* Write settings combined with current settings. On failure log a warning.
* Objects will be merged and everything else will be replaced.
*/
public async write(settings: Partial<T>): Promise<void> {
try {
await fs.writeFile(this.settingsPath, JSON.stringify(extend(this.read(), settings)))
} catch (error) {
logger.warn(error.message)
}
}
}

110
src/node/socket.ts Normal file
View File

@ -0,0 +1,110 @@
import * as fs from "fs-extra"
import * as net from "net"
import * as path from "path"
import * as tls from "tls"
import { Emitter } from "../common/emitter"
import { generateUuid } from "../common/util"
import { tmpdir } from "./util"
/**
* Provides a way to proxy a TLS socket. Can be used when you need to pass a
* socket to a child process since you can't pass the TLS socket.
*/
export class SocketProxyProvider {
private readonly onProxyConnect = new Emitter<net.Socket>()
private proxyPipe = path.join(tmpdir, "tls-proxy")
private _proxyServer?: Promise<net.Server>
private readonly proxyTimeout = 5000
/**
* Stop the proxy server.
*/
public stop(): void {
if (this._proxyServer) {
this._proxyServer.then((server) => server.close())
this._proxyServer = undefined
}
}
/**
* Create a socket proxy for TLS sockets. If it's not a TLS socket the
* original socket is returned. This will spawn a proxy server on demand.
*/
public async createProxy(socket: net.Socket): Promise<net.Socket> {
if (!(socket instanceof tls.TLSSocket)) {
return socket
}
await this.startProxyServer()
return new Promise((resolve, reject) => {
const id = generateUuid()
const proxy = net.connect(this.proxyPipe)
proxy.once("connect", () => proxy.write(id))
const timeout = setTimeout(() => {
listener.dispose() // eslint-disable-line @typescript-eslint/no-use-before-define
socket.destroy()
proxy.destroy()
reject(new Error("TLS socket proxy timed out"))
}, this.proxyTimeout)
const listener = this.onProxyConnect.event((connection) => {
connection.once("data", (data) => {
if (!socket.destroyed && !proxy.destroyed && data.toString() === id) {
clearTimeout(timeout)
listener.dispose()
;[
[proxy, socket],
[socket, proxy],
].forEach(([a, b]) => {
a.pipe(b)
a.on("error", () => b.destroy())
a.on("close", () => b.destroy())
a.on("end", () => b.end())
})
resolve(connection)
}
})
})
})
}
private async startProxyServer(): Promise<net.Server> {
if (!this._proxyServer) {
this._proxyServer = this.findFreeSocketPath(this.proxyPipe)
.then((pipe) => {
this.proxyPipe = pipe
return Promise.all([fs.mkdirp(tmpdir), fs.remove(this.proxyPipe)])
})
.then(() => {
return new Promise((resolve) => {
const proxyServer = net.createServer((p) => this.onProxyConnect.emit(p))
proxyServer.once("listening", () => resolve(proxyServer))
proxyServer.listen(this.proxyPipe)
})
})
}
return this._proxyServer
}
public async findFreeSocketPath(basePath: string, maxTries = 100): Promise<string> {
const canConnect = (path: string): Promise<boolean> => {
return new Promise((resolve) => {
const socket = net.connect(path)
socket.once("error", () => resolve(false))
socket.once("connect", () => {
socket.destroy()
resolve(true)
})
})
}
let i = 0
let path = basePath
while ((await canConnect(path)) && i < maxTries) {
path = `${basePath}-${++i}`
}
return path
}
}

View File

@ -1,141 +0,0 @@
import * as cp from "child_process";
import * as os from "os";
import * as path from "path";
import * as util from "util";
import { CancellationToken } from "vs/base/common/cancellation";
import { URI } from "vs/base/common/uri";
import * as pfs from "vs/base/node/pfs";
import { IConfigurationService } from "vs/platform/configuration/common/configuration";
import { IEnvironmentService } from "vs/platform/environment/common/environment";
import { IFileService } from "vs/platform/files/common/files";
import { ILogService } from "vs/platform/log/common/log";
import product from "vs/platform/product/common/product";
import { asJson, IRequestService } from "vs/platform/request/common/request";
import { AvailableForDownload, State, UpdateType, StateType } from "vs/platform/update/common/update";
import { AbstractUpdateService } from "vs/platform/update/electron-main/abstractUpdateService";
import { ipcMain } from "vs/server/src/node/ipc";
import { extract } from "vs/server/src/node/marketplace";
import { tmpdir } from "vs/server/src/node/util";
interface IUpdate {
name: string;
}
export class UpdateService extends AbstractUpdateService {
_serviceBrand: any;
constructor(
@IConfigurationService configurationService: IConfigurationService,
@IEnvironmentService environmentService: IEnvironmentService,
@IRequestService requestService: IRequestService,
@ILogService logService: ILogService,
@IFileService private readonly fileService: IFileService,
) {
super(null, configurationService, environmentService, requestService, logService);
}
/**
* Return true if the currently installed version is the latest.
*/
public async isLatestVersion(latest?: IUpdate | null): Promise<boolean | undefined> {
if (!latest) {
latest = await this.getLatestVersion();
}
if (latest) {
const latestMajor = parseInt(latest.name);
const currentMajor = parseInt(product.codeServerVersion);
// If these are invalid versions we can't compare meaningfully.
return isNaN(latestMajor) || isNaN(currentMajor) ||
// This can happen when there is a pre-release for a new major version.
currentMajor > latestMajor ||
// Otherwise assume that if it's not the same then we're out of date.
latest.name === product.codeServerVersion;
}
return true;
}
protected buildUpdateFeedUrl(quality: string): string {
return `${product.updateUrl}/${quality}`;
}
public async doQuitAndInstall(): Promise<void> {
if (this.state.type === StateType.Ready) {
ipcMain.relaunch(this.state.update.version);
}
}
protected async doCheckForUpdates(context: any): Promise<void> {
this.setState(State.CheckingForUpdates(context));
try {
const update = await this.getLatestVersion();
if (!update || await this.isLatestVersion(update)) {
this.setState(State.Idle(UpdateType.Archive));
} else {
this.setState(State.AvailableForDownload({
version: update.name,
productVersion: update.name,
}));
}
} catch (error) {
this.onRequestError(error, !!context);
}
}
private async getLatestVersion(): Promise<IUpdate | null> {
const data = await this.requestService.request({
url: this.url,
headers: { "User-Agent": "code-server" },
}, CancellationToken.None);
return asJson(data);
}
protected async doDownloadUpdate(state: AvailableForDownload): Promise<void> {
this.setState(State.Downloading(state.update));
const target = os.platform();
const releaseName = await this.buildReleaseName(state.update.version);
const url = "https://github.com/cdr/code-server/releases/download/"
+ `${state.update.version}/${releaseName}`
+ `.${target === "darwin" ? "zip" : "tar.gz"}`;
const downloadPath = path.join(tmpdir, `${state.update.version}-archive`);
const extractPath = path.join(tmpdir, state.update.version);
try {
await pfs.mkdirp(tmpdir);
const context = await this.requestService.request({ url }, CancellationToken.None, true);
await this.fileService.writeFile(URI.file(downloadPath), context.stream);
await extract(downloadPath, extractPath, undefined, CancellationToken.None);
const newBinary = path.join(extractPath, releaseName, "code-server");
if (!pfs.exists(newBinary)) {
throw new Error("No code-server binary in extracted archive");
}
await pfs.unlink(process.argv[0]); // Must unlink first to avoid ETXTBSY.
await pfs.move(newBinary, process.argv[0]);
this.setState(State.Ready(state.update));
} catch (error) {
this.onRequestError(error, true);
}
await Promise.all([downloadPath, extractPath].map((p) => pfs.rimraf(p)));
}
private onRequestError(error: Error, showNotification?: boolean): void {
this.logService.error(error);
this.setState(State.Idle(UpdateType.Archive, showNotification ? (error.message || error.toString()) : undefined));
}
private async buildReleaseName(release: string): Promise<string> {
let target: string = os.platform();
if (target === "linux") {
const result = await util.promisify(cp.exec)("ldd --version").catch((error) => ({
stderr: error.message,
stdout: "",
}));
if (/musl/.test(result.stderr) || /musl/.test(result.stdout)) {
target = "alpine";
}
}
let arch = os.arch();
if (arch === "x64") {
arch = "x86_64";
}
return `code-server${release}-${target}-${arch}`;
}
}

View File

@ -1,24 +0,0 @@
// 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;
}
},
};
};

View File

@ -1,144 +1,216 @@
import * as cp from "child_process";
import * as crypto from "crypto";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import * as util from "util";
import * as rg from "vscode-ripgrep";
import * as cp from "child_process"
import * as crypto from "crypto"
import * as fs from "fs-extra"
import * as os from "os"
import * as path from "path"
import * as util from "util"
import { getPathFromAmdModule } from "vs/base/common/amd";
import { getMediaMime as vsGetMediaMime } from "vs/base/common/mime";
import { extname } from "vs/base/common/path";
import { URITransformer, IRawURITransformer } from "vs/base/common/uriIpc";
import { mkdirp } from "vs/base/node/pfs";
export const tmpdir = path.join(os.tmpdir(), "code-server")
export enum AuthType {
Password = "password",
None = "none",
const getXdgDataDir = (): string => {
switch (process.platform) {
case "win32":
return path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), "AppData/Local"), "code-server/Data")
case "darwin":
return path.join(
process.env.XDG_DATA_HOME || path.join(os.homedir(), "Library/Application Support"),
"code-server"
)
default:
return path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local/share"), "code-server")
}
}
export enum FormatType {
Json = "json",
export const xdgLocalDir = getXdgDataDir()
export const generateCertificate = async (): Promise<{ cert: string; certKey: string }> => {
const paths = {
cert: path.join(tmpdir, "self-signed.cert"),
certKey: path.join(tmpdir, "self-signed.key"),
}
const checks = await Promise.all([fs.pathExists(paths.cert), fs.pathExists(paths.certKey)])
if (!checks[0] || !checks[1]) {
// Require on demand so openssl isn't required if you aren't going to
// generate certificates.
const pem = require("pem") as typeof import("pem")
const certs = await new Promise<import("pem").CertificateCreationResult>((resolve, reject): void => {
pem.createCertificate({ selfSigned: true }, (error, result) => {
return error ? reject(error) : resolve(result)
})
})
await fs.mkdirp(tmpdir)
await Promise.all([fs.writeFile(paths.cert, certs.certificate), fs.writeFile(paths.certKey, certs.serviceKey)])
}
return paths
}
export const tmpdir = path.join(os.tmpdir(), "code-server");
export const generateCertificate = async (): Promise<{ cert: string, certKey: string }> => {
const paths = {
cert: path.join(tmpdir, "self-signed.cert"),
certKey: path.join(tmpdir, "self-signed.key"),
};
const exists = await Promise.all([
util.promisify(fs.exists)(paths.cert),
util.promisify(fs.exists)(paths.certKey),
]);
if (!exists[0] || !exists[1]) {
const pem = localRequire<typeof import("pem")>("pem/lib/pem");
const certs = await new Promise<import("pem").CertificateCreationResult>((resolve, reject): void => {
pem.createCertificate({ selfSigned: true }, (error, result) => {
if (error) {
return reject(error);
}
resolve(result);
});
});
await mkdirp(tmpdir);
await Promise.all([
util.promisify(fs.writeFile)(paths.cert, certs.certificate),
util.promisify(fs.writeFile)(paths.certKey, certs.serviceKey),
]);
}
return paths;
};
export const uriTransformerPath = getPathFromAmdModule(require, "vs/server/src/node/uriTransformer");
export const getUriTransformer = (remoteAuthority: string): URITransformer => {
const rawURITransformerFactory = <any>require.__$__nodeRequire(uriTransformerPath);
const rawURITransformer = <IRawURITransformer>rawURITransformerFactory(remoteAuthority);
return new URITransformer(rawURITransformer);
};
export const generatePassword = async (length: number = 24): Promise<string> => {
const buffer = Buffer.alloc(Math.ceil(length / 2));
await util.promisify(crypto.randomFill)(buffer);
return buffer.toString("hex").substring(0, length);
};
export const generatePassword = async (length = 24): Promise<string> => {
const buffer = Buffer.alloc(Math.ceil(length / 2))
await util.promisify(crypto.randomFill)(buffer)
return buffer.toString("hex").substring(0, length)
}
export const hash = (str: string): string => {
return crypto.createHash("sha256").update(str).digest("hex");
};
return crypto
.createHash("sha256")
.update(str)
.digest("hex")
}
const mimeTypes: { [key: string]: string } = {
".aac": "audio/x-aac",
".avi": "video/x-msvideo",
".bmp": "image/bmp",
".css": "text/css",
".flv": "video/x-flv",
".gif": "image/gif",
".html": "text/html",
".ico": "image/x-icon",
".jpe": "image/jpg",
".jpeg": "image/jpg",
".jpg": "image/jpg",
".js": "application/javascript",
".json": "application/json",
".m1v": "video/mpeg",
".m2a": "audio/mpeg",
".m2v": "video/mpeg",
".m3a": "audio/mpeg",
".mid": "audio/midi",
".midi": "audio/midi",
".mk3d": "video/x-matroska",
".mks": "video/x-matroska",
".mkv": "video/x-matroska",
".mov": "video/quicktime",
".movie": "video/x-sgi-movie",
".mp2": "audio/mpeg",
".mp2a": "audio/mpeg",
".mp3": "audio/mpeg",
".mp4": "video/mp4",
".mp4a": "audio/mp4",
".mp4v": "video/mp4",
".mpe": "video/mpeg",
".mpeg": "video/mpeg",
".mpg": "video/mpeg",
".mpg4": "video/mp4",
".mpga": "audio/mpeg",
".oga": "audio/ogg",
".ogg": "audio/ogg",
".ogv": "video/ogg",
".png": "image/png",
".psd": "image/vnd.adobe.photoshop",
".qt": "video/quicktime",
".spx": "audio/ogg",
".svg": "image/svg+xml",
".tga": "image/x-tga",
".tif": "image/tiff",
".tiff": "image/tiff",
".txt": "text/plain",
".wav": "audio/x-wav",
".wasm": "application/wasm",
".webm": "video/webm",
".webp": "image/webp",
".wma": "audio/x-ms-wma",
".wmv": "video/x-ms-wmv",
".woff": "application/font-woff",
}
export const getMediaMime = (filePath?: string): string => {
return filePath && (vsGetMediaMime(filePath) || (<{[index: string]: string}>{
".css": "text/css",
".html": "text/html",
".js": "application/javascript",
".json": "application/json",
})[extname(filePath)]) || "text/plain";
};
return (filePath && mimeTypes[path.extname(filePath)]) || "text/plain"
}
export const isWsl = async (): Promise<boolean> => {
return process.platform === "linux"
&& os.release().toLowerCase().indexOf("microsoft") !== -1
|| (await util.promisify(fs.readFile)("/proc/version", "utf8"))
.toLowerCase().indexOf("microsoft") !== -1;
};
return (
(process.platform === "linux" &&
os
.release()
.toLowerCase()
.indexOf("microsoft") !== -1) ||
(await fs.readFile("/proc/version", "utf8")).toLowerCase().indexOf("microsoft") !== -1
)
}
/**
* Try opening a URL using whatever the system has set for opening URLs.
*/
export const open = async (url: string): Promise<void> => {
const args = <string[]>[];
const options = <cp.SpawnOptions>{};
const platform = await isWsl() ? "wsl" : process.platform;
let command = platform === "darwin" ? "open" : "xdg-open";
if (platform === "win32" || platform === "wsl") {
command = platform === "wsl" ? "cmd.exe" : "cmd";
args.push("/c", "start", '""', "/b");
url = url.replace(/&/g, "^&");
}
const proc = cp.spawn(command, [...args, url], options);
await new Promise((resolve, reject) => {
proc.on("error", reject);
proc.on("close", (code) => {
return code !== 0
? reject(new Error(`Failed to open with code ${code}`))
: resolve();
});
});
};
const args = [] as string[]
const options = {} as cp.SpawnOptions
const platform = (await isWsl()) ? "wsl" : process.platform
let command = platform === "darwin" ? "open" : "xdg-open"
if (platform === "win32" || platform === "wsl") {
command = platform === "wsl" ? "cmd.exe" : "cmd"
args.push("/c", "start", '""', "/b")
url = url.replace(/&/g, "^&")
}
const proc = cp.spawn(command, [...args, url], options)
await new Promise((resolve, reject) => {
proc.on("error", reject)
proc.on("close", (code) => {
return code !== 0 ? reject(new Error(`Failed to open with code ${code}`)) : resolve()
})
})
}
/**
* Extract executables to the temporary directory. This is required since we
* can't execute binaries stored within our binary.
* Extract a file to the temporary directory and make it executable. This is
* required since we can't execute binaries stored within our binary.
*/
export const unpackExecutables = async (): Promise<void> => {
const rgPath = (rg as any).binaryRgPath;
const destination = path.join(tmpdir, path.basename(rgPath || ""));
if (rgPath && !(await util.promisify(fs.exists)(destination))) {
await mkdirp(tmpdir);
await util.promisify(fs.writeFile)(destination, await util.promisify(fs.readFile)(rgPath));
await util.promisify(fs.chmod)(destination, "755");
}
};
export const unpackExecutables = async (filePath: string): Promise<void> => {
const destination = path.join(tmpdir, "binaries", path.basename(filePath))
if (filePath && !(await util.promisify(fs.exists)(destination))) {
await fs.mkdirp(tmpdir)
await fs.writeFile(destination, await fs.readFile(filePath))
await util.promisify(fs.chmod)(destination, "755")
}
}
/**
* For iterating over an enum's values.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const enumToArray = (t: any): string[] => {
const values = <string[]>[];
for (const k in t) {
values.push(t[k]);
}
return values;
};
export const buildAllowedMessage = (t: any): string => {
const values = enumToArray(t);
return `Allowed value${values.length === 1 ? " is" : "s are"} ${values.map((t) => `'${t}'`).join(", ")}`;
};
const values = [] as string[]
for (const k in t) {
values.push(t[k])
}
return values
}
/**
* Require a local module. This is necessary since VS Code's loader only looks
* at the root for Node modules.
* For displaying all allowed options in an enum.
*/
export const localRequire = <T>(modulePath: string): T => {
return require.__$__nodeRequire(path.resolve(__dirname, "../../node_modules", modulePath));
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const buildAllowedMessage = (t: any): string => {
const values = enumToArray(t)
return `Allowed value${values.length === 1 ? " is" : "s are"} ${values.map((t) => `'${t}'`).join(", ")}`
}
export const isObject = <T extends object>(obj: T): obj is T => {
return !Array.isArray(obj) && typeof obj === "object" && obj !== null
}
/**
* Extend a with b and return a new object. Properties with objects will be
* recursively merged while all other properties are just overwritten.
*/
export function extend<A, B>(a: A, b: B): A & B
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function extend(...args: any[]): any {
const c = {} as any // eslint-disable-line @typescript-eslint/no-explicit-any
for (const obj of args) {
if (!isObject(obj)) {
continue
}
for (const key in obj) {
c[key] = isObject(obj[key]) ? extend(c[key], obj[key]) : obj[key]
}
}
return c
}
/**
* Remove extra and trailing slashes in a URL.
*/
export const normalize = (url: string): string => {
return url.replace(/\/\/+/g, "/").replace(/\/+$/, "")
}

59
src/node/vscode/README.md Normal file
View File

@ -0,0 +1,59 @@
Implementation of [VS Code](https://code.visualstudio.com/) remote/web for use
in `code-server`.
## Docker
To debug Golang in VS Code 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.
## Known Issues
- Creating custom VS Code extensions and debugging them doesn't work.
- Extension profiling and tips are currently disabled.
## 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 extensions 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.
## Development: 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.
If functionality doesn't depend on code from VS Code then it should be moved
into code-server otherwise it should be in the patch.
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.
## Future
- Run VS Code unit tests against our builds to ensure features work as expected.

204
src/node/vscode/server.ts Normal file
View File

@ -0,0 +1,204 @@
import { field, logger } from "@coder/logger"
import * as cp from "child_process"
import * as crypto from "crypto"
import * as http from "http"
import * as net from "net"
import * as path from "path"
import * as querystring from "querystring"
import {
CodeServerMessage,
Settings,
VscodeMessage,
VscodeOptions,
WorkbenchOptions,
} from "../../../lib/vscode/src/vs/server/ipc"
import { generateUuid } from "../../common/util"
import { HttpProvider, HttpProviderOptions, HttpResponse } from "../http"
import { SettingsProvider } from "../settings"
import { xdgLocalDir } from "../util"
export class VscodeHttpProvider extends HttpProvider {
private readonly serverRootPath: string
private readonly vsRootPath: string
private readonly settings = new SettingsProvider<Settings>(path.join(xdgLocalDir, "coder.json"))
private _vscode?: Promise<cp.ChildProcess>
private workbenchOptions?: WorkbenchOptions
public constructor(private readonly args: string[], options: HttpProviderOptions) {
super(options)
this.vsRootPath = path.resolve(this.rootPath, "lib/vscode")
this.serverRootPath = path.join(this.vsRootPath, "out/vs/server")
}
private async initialize(options: VscodeOptions): Promise<WorkbenchOptions> {
const id = generateUuid()
const vscode = await this.fork()
logger.debug("Setting up VS Code...")
return new Promise<WorkbenchOptions>((resolve, reject) => {
vscode.once("message", (message: VscodeMessage) => {
logger.debug("Got message from VS Code", field("message", message))
return message.type === "options" && message.id === id
? resolve(message.options)
: reject(new Error("Unexpected response during initialization"))
})
vscode.once("error", reject)
vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`)))
this.send({ type: "init", id, options }, vscode)
})
}
private fork(): Promise<cp.ChildProcess> {
if (!this._vscode) {
logger.debug("Forking VS Code...")
const vscode = cp.fork(path.join(this.serverRootPath, "fork"))
vscode.on("error", (error) => {
logger.error(error.message)
this._vscode = undefined
})
vscode.on("exit", (code) => {
logger.error(`VS Code exited unexpectedly with code ${code}`)
this._vscode = undefined
})
this._vscode = new Promise((resolve, reject) => {
vscode.once("message", (message: VscodeMessage) => {
logger.debug("Got message from VS Code", field("message", message))
return message.type === "ready"
? resolve(vscode)
: reject(new Error("Unexpected response waiting for ready response"))
})
vscode.once("error", reject)
vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`)))
})
}
return this._vscode
}
public async handleWebSocket(
_base: string,
_requestPath: string,
query: querystring.ParsedUrlQuery,
request: http.IncomingMessage,
socket: net.Socket
): Promise<true> {
if (!this.authenticated(request)) {
throw new Error("not authenticated")
}
// VS Code expects a raw socket. It will handle all the web socket frames.
// We just need to handle the initial upgrade.
// This magic value is specified by the websocket spec.
const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
const reply = crypto
.createHash("sha1")
.update(request.headers["sec-websocket-key"] + magic)
.digest("base64")
socket.write(
[
"HTTP/1.1 101 Switching Protocols",
"Upgrade: websocket",
"Connection: Upgrade",
`Sec-WebSocket-Accept: ${reply}`,
].join("\r\n") + "\r\n\r\n"
)
const vscode = await this._vscode
this.send({ type: "socket", query }, vscode, socket)
return true
}
private send(message: CodeServerMessage, vscode?: cp.ChildProcess, socket?: net.Socket): void {
if (!vscode || vscode.killed) {
throw new Error("vscode is not running")
}
vscode.send(message, socket)
}
public async handleRequest(
base: string,
requestPath: string,
query: querystring.ParsedUrlQuery,
request: http.IncomingMessage
): Promise<HttpResponse | undefined> {
this.ensureGet(request)
switch (base) {
case "/":
if (!this.authenticated(request)) {
return { redirect: "/login" }
}
return this.getRoot(request, query)
case "/static": {
switch (requestPath) {
case "/out/vs/workbench/services/extensions/worker/extensionHostWorkerMain.js": {
const response = await this.getUtf8Resource(this.vsRootPath, requestPath)
response.content = response.content.replace(
/{{COMMIT}}/g,
this.workbenchOptions ? this.workbenchOptions.commit : ""
)
response.cache = true
return response
}
}
const response = await this.getResource(this.vsRootPath, requestPath)
response.cache = true
return response
}
case "/resource":
case "/vscode-remote-resource":
this.ensureAuthenticated(request)
if (typeof query.path === "string") {
return this.getResource(query.path)
}
break
case "/tar":
this.ensureAuthenticated(request)
if (typeof query.path === "string") {
return this.getTarredResource(query.path)
}
break
case "/webview":
this.ensureAuthenticated(request)
if (/^\/vscode-resource/.test(requestPath)) {
return this.getResource(requestPath.replace(/^\/vscode-resource(\/file)?/, ""))
}
return this.getResource(this.vsRootPath, "out/vs/workbench/contrib/webview/browser/pre", requestPath)
}
return undefined
}
private async getRoot(request: http.IncomingMessage, query: querystring.ParsedUrlQuery): Promise<HttpResponse> {
const settings = await this.settings.read()
const [response, options] = await Promise.all([
this.getUtf8Resource(this.serverRootPath, "browser/workbench.html"),
this.initialize({
args: this.args,
query,
remoteAuthority: request.headers.host as string,
settings,
}),
])
this.workbenchOptions = options
if (options.startPath) {
this.settings.write({
lastVisited: {
path: options.startPath.path,
workspace: options.startPath.workspace,
},
})
}
return {
...response,
content: response.content
.replace(/{{COMMIT}}/g, options.commit)
.replace(`"{{REMOTE_USER_DATA_URI}}"`, `'${JSON.stringify(options.remoteUserDataUri)}'`)
.replace(`"{{PRODUCT_CONFIGURATION}}"`, `'${JSON.stringify(options.productConfiguration)}'`)
.replace(`"{{WORKBENCH_WEB_CONFIGURATION}}"`, `'${JSON.stringify(options.workbenchWebConfiguration)}'`)
.replace(`"{{NLS_CONFIGURATION}}"`, `'${JSON.stringify(options.nlsConfiguration)}'`),
}
}
}

223
src/node/wrapper.ts Normal file
View File

@ -0,0 +1,223 @@
import { logger, field } from "@coder/logger"
import * as cp from "child_process"
import { Emitter } from "../common/emitter"
interface HandshakeMessage {
type: "handshake"
}
interface RelaunchMessage {
type: "relaunch"
version: string
}
export type Message = RelaunchMessage | HandshakeMessage
export class ProcessError extends Error {
public constructor(message: string, public readonly code: number | undefined) {
super(message)
this.name = this.constructor.name
Error.captureStackTrace(this, this.constructor)
}
}
/**
* Ensure we control when the process exits.
*/
const exit = process.exit
process.exit = function(code?: number) {
logger.warn(`process.exit() was prevented: ${code || "unknown code"}.`)
} as (code?: number) => never
/**
* Allows the wrapper and inner processes to communicate.
*/
export class IpcMain {
private readonly _onMessage = new Emitter<Message>()
public readonly onMessage = this._onMessage.event
private readonly _onDispose = new Emitter<NodeJS.Signals | undefined>()
public readonly onDispose = this._onDispose.event
public constructor(public readonly parentPid?: number) {
process.on("SIGINT", () => this._onDispose.emit("SIGINT"))
process.on("SIGTERM", () => this._onDispose.emit("SIGTERM"))
process.on("exit", () => this._onDispose.emit(undefined))
this.onDispose((signal) => {
// Remove listeners to avoid possibly triggering disposal again.
process.removeAllListeners()
// Let any other handlers run first then exit.
logger.debug(`${parentPid ? "inner process" : "wrapper"} ${process.pid} disposing`, field("code", signal))
setTimeout(() => exit(0), 0)
})
// Kill the inner process if the parent dies. This is for the case where the
// parent process is forcefully terminated and cannot clean up.
if (parentPid) {
setInterval(() => {
try {
// process.kill throws an exception if the process doesn't exist.
process.kill(parentPid, 0)
} catch (_) {
// Consider this an error since it should have been able to clean up
// the child process unless it was forcefully killed.
logger.error(`parent process ${parentPid} died`)
this._onDispose.emit(undefined)
}
}, 5000)
}
}
public handshake(child?: cp.ChildProcess): Promise<void> {
return new Promise((resolve, reject) => {
const target = child || process
const onMessage = (message: Message): void => {
logger.debug(
`${child ? "wrapper" : "inner process"} ${process.pid} received message from ${
child ? child.pid : this.parentPid
}`,
field("message", message)
)
if (message.type === "handshake") {
target.removeListener("message", onMessage)
target.on("message", (msg) => this._onMessage.emit(msg))
// The wrapper responds once the inner process starts the handshake.
if (child) {
if (!target.send) {
throw new Error("child not spawned with IPC")
}
target.send({ type: "handshake" })
}
resolve()
}
}
target.on("message", onMessage)
if (child) {
child.once("error", reject)
child.once("exit", (code) => {
reject(new ProcessError(`Unexpected exit with code ${code}`, code !== null ? code : undefined))
})
} else {
// The inner process initiates the handshake.
this.send({ type: "handshake" })
}
})
}
public relaunch(version: string): void {
this.send({ type: "relaunch", version })
}
private send(message: Message): void {
if (!process.send) {
throw new Error("not spawned with IPC")
}
process.send(message)
}
}
export const ipcMain = new IpcMain(
typeof process.env.CODE_SERVER_PARENT_PID !== "undefined" ? parseInt(process.env.CODE_SERVER_PARENT_PID) : undefined
)
export interface WrapperOptions {
maxMemory?: number
nodeOptions?: string
}
/**
* Provides a way to wrap a process for the purpose of updating the running
* instance.
*/
export class WrapperProcess {
private process?: cp.ChildProcess
private started?: Promise<void>
public constructor(private currentVersion: string, private readonly options?: WrapperOptions) {
ipcMain.onDispose(() => {
if (this.process) {
this.process.removeAllListeners()
this.process.kill()
}
})
ipcMain.onMessage(async (message) => {
switch (message.type) {
case "relaunch":
logger.info(`Relaunching: ${this.currentVersion} -> ${message.version}`)
this.currentVersion = message.version
this.started = undefined
if (this.process) {
this.process.removeAllListeners()
this.process.kill()
}
try {
await this.start()
} catch (error) {
logger.error(error.message)
exit(typeof error.code === "number" ? error.code : 1)
}
break
default:
logger.error(`Unrecognized message ${message}`)
break
}
})
}
public start(): Promise<void> {
if (!this.started) {
const child = this.spawn()
logger.debug(`spawned inner process ${child.pid}`)
this.started = ipcMain.handshake(child).then(() => {
child.once("exit", (code) => {
logger.debug(`inner process ${child.pid} exited unexpectedly`)
exit(code || 0)
})
})
this.process = child
}
return this.started
}
private spawn(): cp.ChildProcess {
// Flags to pass along to the Node binary.
let nodeOptions = `${process.env.NODE_OPTIONS || ""} ${(this.options && this.options.nodeOptions) || ""}`
if (!/max_old_space_size=(\d+)/g.exec(nodeOptions)) {
nodeOptions += ` --max_old_space_size=${(this.options && this.options.maxMemory) || 2048}`
}
return cp.fork(process.argv[1], process.argv.slice(2), {
env: {
...process.env,
CODE_SERVER_PARENT_PID: process.pid.toString(),
NODE_OPTIONS: nodeOptions,
},
})
}
}
// // It's possible that the pipe has closed (for example if you run code-server
// // --version | head -1). Assume that means we're done.
if (!process.stdout.isTTY) {
process.stdout.on("error", () => exit())
}
export const wrap = (fn: () => Promise<void>): void => {
if (ipcMain.parentPid) {
ipcMain
.handshake()
.then(() => fn())
.catch((error: ProcessError): void => {
logger.error(error.message)
exit(typeof error.code === "number" ? error.code : 1)
})
} else {
const wrapper = new WrapperProcess(require("../../package.json").version)
wrapper.start().catch((error) => {
logger.error(error.message)
exit(typeof error.code === "number" ? error.code : 1)
})
}
}

124
test/socket.test.ts Normal file
View File

@ -0,0 +1,124 @@
import { field, logger } from "@coder/logger"
import * as assert from "assert"
import * as fs from "fs-extra"
import "leaked-handles"
import * as net from "net"
import * as path from "path"
import * as tls from "tls"
import { Emitter } from "../src/common/emitter"
import { generateCertificate, tmpdir } from "../src/node/util"
import { SocketProxyProvider } from "../src/node/socket"
describe("SocketProxyProvider", () => {
const provider = new SocketProxyProvider()
const onServerError = new Emitter<{ event: string; error: Error }>()
const onClientError = new Emitter<{ event: string; error: Error }>()
const onProxyError = new Emitter<{ event: string; error: Error }>()
const fromServerToClient = new Emitter<string>()
const fromClientToServer = new Emitter<string>()
const fromClientToProxy = new Emitter<Buffer>()
let errors = 0
let close = false
const onError = ({ event, error }: { event: string; error: Error }): void => {
if (!close || event === "error") {
logger.error(event, field("error", error.message))
++errors
}
}
onServerError.event(onError)
onClientError.event(onError)
onProxyError.event(onError)
let server: tls.TLSSocket
let proxy: net.Socket
let client: tls.TLSSocket
const getData = <T>(emitter: Emitter<T>): Promise<T> => {
return new Promise((resolve) => {
const d = emitter.event((t) => {
d.dispose()
resolve(t)
})
})
}
before(async () => {
const cert = await generateCertificate()
const options = {
cert: fs.readFileSync(cert.cert),
key: fs.readFileSync(cert.certKey),
rejectUnauthorized: false,
}
await fs.mkdirp(path.join(tmpdir, "tests"))
const socketPath = await provider.findFreeSocketPath(path.join(tmpdir, "tests/tls-socket-proxy"))
await fs.remove(socketPath)
return new Promise((_resolve) => {
const resolved: { [key: string]: boolean } = { client: false, server: false }
const resolve = (type: "client" | "server"): void => {
resolved[type] = true
if (resolved.client && resolved.server) {
// We don't need any more connections.
main.close() // eslint-disable-line @typescript-eslint/no-use-before-define
_resolve()
}
}
const main = tls
.createServer(options, (s) => {
server = s
server
.on("data", (d) => fromClientToServer.emit(d))
.on("error", (error) => onServerError.emit({ event: "error", error }))
.on("end", () => onServerError.emit({ event: "end", error: new Error("unexpected end") }))
.on("close", () => onServerError.emit({ event: "close", error: new Error("unexpected close") }))
resolve("server")
})
.on("error", (error) => onServerError.emit({ event: "error", error }))
.on("end", () => onServerError.emit({ event: "end", error: new Error("unexpected end") }))
.on("close", () => onServerError.emit({ event: "close", error: new Error("unexpected close") }))
.listen(socketPath, () => {
client = tls
.connect({ ...options, path: socketPath })
.on("data", (d) => fromServerToClient.emit(d))
.on("error", (error) => onClientError.emit({ event: "error", error }))
.on("end", () => onClientError.emit({ event: "end", error: new Error("unexpected end") }))
.on("close", () => onClientError.emit({ event: "close", error: new Error("unexpected close") }))
.once("connect", () => resolve("client"))
})
})
})
it("should work without a proxy", async () => {
server.write("server->client")
assert.equal(await getData(fromServerToClient), "server->client")
client.write("client->server")
assert.equal(await getData(fromClientToServer), "client->server")
assert.equal(errors, 0)
})
it("should work with a proxy", async () => {
assert.equal(server instanceof tls.TLSSocket, true)
proxy = (await provider.createProxy(server))
.on("data", (d) => fromClientToProxy.emit(d))
.on("error", (error) => onProxyError.emit({ event: "error", error }))
.on("end", () => onProxyError.emit({ event: "end", error: new Error("unexpected end") }))
.on("close", () => onProxyError.emit({ event: "close", error: new Error("unexpected close") }))
provider.stop() // We don't need more proxies.
proxy.write("server proxy->client")
assert.equal(await getData(fromServerToClient), "server proxy->client")
client.write("client->server proxy")
assert.equal(await getData(fromClientToProxy), "client->server proxy")
assert.equal(errors, 0)
})
it("should close", async () => {
close = true
client.end()
proxy.end()
})
})

49
test/util.test.ts Normal file
View File

@ -0,0 +1,49 @@
import * as assert from "assert"
import { extend, normalize } from "../src/node/util"
describe("util", () => {
describe("extend", () => {
it("should extend", () => {
const a = { foo: { bar: 0, baz: 2 }, garply: 4, waldo: 6 }
const b = { foo: { bar: 1, qux: 3 }, garply: "5", fred: 7 }
const extended = extend(a, b)
assert.deepEqual(extended, {
foo: { bar: 1, baz: 2, qux: 3 },
garply: "5",
waldo: 6,
fred: 7,
})
})
it("should make deep copies of the original objects", () => {
const a = { foo: 0, bar: { frobnozzle: 2 }, mumble: { qux: { thud: 4 } } }
const b = { foo: 1, bar: { chad: 3 } }
const extended = extend(a, b)
assert.notEqual(a.bar, extended.bar)
assert.notEqual(b.bar, extended.bar)
assert.notEqual(a.mumble, extended.mumble)
assert.notEqual(a.mumble.qux, extended.mumble.qux)
})
it("should handle mismatch in type", () => {
const a = { foo: { bar: 0, baz: 2, qux: { mumble: 11 } }, garply: 4, waldo: { thud: 10 } }
const b = { foo: { bar: [1], baz: { plugh: 8 }, qux: 12 }, garply: { nox: 9 }, waldo: 7 }
const extended = extend(a, b)
assert.deepEqual(extended, {
foo: { bar: [1], baz: { plugh: 8 }, qux: 12 },
garply: { nox: 9 },
waldo: 7,
})
})
})
describe("normalize", () => {
it("should remove multiple slashes", () => {
assert.equal(normalize("//foo//bar//baz///mumble"), "/foo/bar/baz/mumble")
})
it("should remove trailing slashes", () => {
assert.equal(normalize("qux///"), "qux")
})
})
})

27
tsconfig.json Normal file
View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"strict": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./out",
"allowJs": false,
"jsx": "react",
"declaration": true,
"experimentalDecorators": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"tsBuildInfoFile": "./.tsbuildinfo",
"incremental": true,
"rootDir": "./src",
"typeRoots": ["./node_modules/@types", "./typings"]
},
"include": [
"./src/**/*.ts",
"./src/**/*.tsx"
]
}

View File

@ -1,8 +0,0 @@
{
"extends": [
"../../../tslint.json"
],
"rules": {
"no-unexternalized-strings": false
}
}

View File

@ -1 +0,0 @@
httpolyglot.d.ts

52
typings/api.d.ts vendored
View File

@ -1,52 +0,0 @@
import * as vscode from "vscode";
// Only export the subset of VS Code we have implemented.
export interface VSCodeApi {
EventEmitter: typeof vscode.EventEmitter;
FileSystemError: typeof vscode.FileSystemError;
FileType: typeof vscode.FileType;
StatusBarAlignment: typeof vscode.StatusBarAlignment;
ThemeColor: typeof vscode.ThemeColor;
TreeItemCollapsibleState: typeof vscode.TreeItemCollapsibleState;
Uri: typeof vscode.Uri;
commands: {
executeCommand: typeof vscode.commands.executeCommand;
registerCommand: typeof vscode.commands.registerCommand;
};
window: {
createStatusBarItem: typeof vscode.window.createStatusBarItem;
registerTreeDataProvider: typeof vscode.window.registerTreeDataProvider;
showErrorMessage: typeof vscode.window.showErrorMessage;
};
workspace: {
registerFileSystemProvider: typeof vscode.workspace.registerFileSystemProvider;
};
}
export interface CoderApi {
registerView: (viewId: string, viewName: string, containerId: string, containerName: string, icon: string) => void;
}
export interface IdeReadyEvent extends CustomEvent<void> {
readonly vscode: VSCodeApi;
readonly ide: CoderApi;
}
declare global {
interface Window {
/**
* Full VS Code extension API.
*/
vscode?: VSCodeApi;
/**
* Coder API.
*/
ide?: CoderApi;
/**
* Listen for when the IDE API has been set and is ready to use.
*/
addEventListener(event: "ide-ready", callback: (event: IdeReadyEvent) => void): void;
}
}

View File

@ -1,7 +0,0 @@
declare module "httpolyglot" {
import * as http from "http";
import * as https from "https";
function createServer(requestListener?: (req: http.IncomingMessage, res: http.ServerResponse) => void): http.Server;
function createServer(options: https.ServerOptions, requestListener?: (req: http.IncomingMessage, res: http.ServerResponse) => void): https.Server;
}

10
typings/httpolyglot/index.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
declare module "httpolyglot" {
import * as http from "http"
import * as https from "https"
function createServer(requestListener?: (req: http.IncomingMessage, res: http.ServerResponse) => void): http.Server
function createServer(
options: https.ServerOptions,
requestListener?: (req: http.IncomingMessage, res: http.ServerResponse) => void
): https.Server
}

View File

@ -1,11 +0,0 @@
{
"name": "@coder/ide-api",
"version": "2.0.3",
"typings": "api.d.ts",
"license": "MIT",
"author": "Coder",
"description": "API for interfacing with the API created for content-scripts.",
"dependencies": {
"@types/vscode": "^1.37.0"
}
}

6095
yarn.lock

File diff suppressed because it is too large Load Diff