Merge pull request #1927 from cdr/dead-code

Remove dead code
This commit is contained in:
Asher 2020-07-31 12:25:56 -05:00 committed by GitHub
commit 5370f7876d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 197 additions and 1164 deletions

View File

@ -21,7 +21,6 @@ main() {
--public-url "/static/$(git rev-parse HEAD)/dist" \ --public-url "/static/$(git rev-parse HEAD)/dist" \
--out-dir dist \ --out-dir dist \
$([[ $MINIFY ]] || echo --no-minify) \ $([[ $MINIFY ]] || echo --no-minify) \
src/browser/pages/app.ts \
src/browser/register.ts \ src/browser/register.ts \
src/browser/serviceWorker.ts src/browser/serviceWorker.ts
} }

View File

@ -252,7 +252,7 @@ index da4fa3e9d0..50f3e1144f 100644
function processLib() { function processLib() {
diff --git a/package.json b/package.json diff --git a/package.json b/package.json
index 86e3d5140d..2e52256e49 100644 index 86e3d5140d..962050280c 100644
--- a/package.json --- a/package.json
+++ b/package.json +++ b/package.json
@@ -42,6 +42,9 @@ @@ -42,6 +42,9 @@
@ -265,6 +265,15 @@ index 86e3d5140d..2e52256e49 100644
"applicationinsights": "1.0.8", "applicationinsights": "1.0.8",
"chokidar": "3.2.3", "chokidar": "3.2.3",
"graceful-fs": "4.2.3", "graceful-fs": "4.2.3",
@@ -185,5 +188,8 @@
"windows-foreground-love": "0.2.0",
"windows-mutex": "0.3.0",
"windows-process-tree": "0.2.4"
+ },
+ "resolutions": {
+ "minimist": "^1.2.5"
}
}
diff --git a/product.json b/product.json diff --git a/product.json b/product.json
index 5378b017c8..afdadda974 100644 index 5378b017c8..afdadda974 100644
--- a/product.json --- a/product.json
@ -713,10 +722,10 @@ index eab8591492..26668701f7 100644
options.logService.error(`${logPrefix} socketFactory.connect() failed. Error:`); options.logService.error(`${logPrefix} socketFactory.connect() failed. Error:`);
diff --git a/src/vs/server/browser/client.ts b/src/vs/server/browser/client.ts diff --git a/src/vs/server/browser/client.ts b/src/vs/server/browser/client.ts
new file mode 100644 new file mode 100644
index 0000000000..649cf32f0a index 0000000000..8fb2a87303
--- /dev/null --- /dev/null
+++ b/src/vs/server/browser/client.ts +++ b/src/vs/server/browser/client.ts
@@ -0,0 +1,264 @@ @@ -0,0 +1,208 @@
+import { Emitter } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event';
+import { URI } from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri';
+import { localize } from 'vs/nls'; +import { localize } from 'vs/nls';
@ -724,7 +733,6 @@ index 0000000000..649cf32f0a
+import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
+import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
+import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; +import { ILocalizationsService } from 'vs/platform/localizations/common/localizations';
+import { ILogService } from 'vs/platform/log/common/log';
+import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
+import { Registry } from 'vs/platform/registry/common/platform'; +import { Registry } from 'vs/platform/registry/common/platform';
+import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection'; +import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection';
@ -886,61 +894,6 @@ index 0000000000..649cf32f0a
+ }); + });
+ } + }
+ +
+ const applyUpdate = async (): Promise<void> => {
+ (services.get(ILogService) as ILogService).debug("Applying update...");
+
+ const response = await fetch(normalize(`${options.base}/update/apply`), {
+ headers: { "content-type": "application/json" },
+ });
+ const json = await response.json();
+ if (response.status !== 200 || json.error) {
+ throw new Error(json.error || response.statusText);
+ }
+ (services.get(INotificationService) as INotificationService).info(`Updated to ${json.version}`);
+ };
+
+ const getUpdate = async (): Promise<void> => {
+ (services.get(ILogService) as ILogService).debug("Checking for update...");
+
+ const response = await fetch(normalize(`${options.base}/update`), {
+ headers: { "content-type": "application/json" },
+ });
+ const json = await response.json();
+ if (response.status !== 200 || json.error) {
+ throw new Error(json.error || response.statusText);
+ }
+ if (json.isLatest) {
+ return;
+ }
+
+ (services.get(INotificationService) as INotificationService).notify({
+ severity: Severity.Info,
+ message: `code-server has an update: ${json.version}`,
+ actions: {
+ primary: [{
+ id: 'update',
+ label: 'Apply Update',
+ tooltip: '',
+ class: undefined,
+ enabled: true,
+ checked: true,
+ dispose: () => undefined,
+ run: applyUpdate,
+ }],
+ }
+ });
+ };
+
+ const updateLoop = (): void => {
+ getUpdate().catch((error) => {
+ (services.get(ILogService) as ILogService).warn(error);
+ }).finally(() => {
+ setTimeout(updateLoop, 300000);
+ });
+ };
+
+ updateLoop();
+
+ // This will be used to set the background color while VS Code loads. + // This will be used to set the background color while VS Code loads.
+ const theme = (services.get(IStorageService) as IStorageService).get("colorThemeData", StorageScope.GLOBAL); + const theme = (services.get(IStorageService) as IStorageService).get("colorThemeData", StorageScope.GLOBAL);
+ if (theme) { + if (theme) {
@ -3465,7 +3418,7 @@ index 153ac595d0..a6eb49c5dd 100644
import 'vs/workbench/services/credentials/browser/credentialsService'; import 'vs/workbench/services/credentials/browser/credentialsService';
import 'vs/workbench/services/url/browser/urlService'; import 'vs/workbench/services/url/browser/urlService';
diff --git a/yarn.lock b/yarn.lock diff --git a/yarn.lock b/yarn.lock
index 6bc96e8377..585401f144 100644 index 6bc96e8377..a2baf909d6 100644
--- a/yarn.lock --- a/yarn.lock
+++ b/yarn.lock +++ b/yarn.lock
@@ -140,6 +140,23 @@ @@ -140,6 +140,23 @@
@ -3506,7 +3459,35 @@ index 6bc96e8377..585401f144 100644
just-debounce@^1.0.0: just-debounce@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/just-debounce/-/just-debounce-1.0.0.tgz#87fccfaeffc0b68cd19d55f6722943f929ea35ea" resolved "https://registry.yarnpkg.com/just-debounce/-/just-debounce-1.0.0.tgz#87fccfaeffc0b68cd19d55f6722943f929ea35ea"
@@ -6798,6 +6822,11 @@ p-try@^2.0.0: @@ -6009,26 +6033,11 @@ minimatch@0.3:
dependencies:
brace-expansion "^1.1.7"
-minimist@0.0.8:
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
- integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
-
-minimist@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
- integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
-
-minimist@^1.2.5:
+minimist@0.0.8, minimist@^1.2.0, minimist@^1.2.5, minimist@~0.0.1:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
-minimist@~0.0.1:
- version "0.0.10"
- resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
- integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=
-
minipass@^2.2.1, minipass@^2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.3.tgz#a7dcc8b7b833f5d368759cce544dccb55f50f233"
@@ -6798,6 +6807,11 @@ p-try@^2.0.0:
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1"
integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ== integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==

View File

@ -144,11 +144,7 @@ class Watcher {
private createBundler(out = "dist"): Bundler { private createBundler(out = "dist"): Bundler {
return new Bundler( return new Bundler(
[ [path.join(this.rootPath, "src/browser/register.ts"), path.join(this.rootPath, "src/browser/serviceWorker.ts")],
path.join(this.rootPath, "src/browser/pages/app.ts"),
path.join(this.rootPath, "src/browser/register.ts"),
path.join(this.rootPath, "src/browser/serviceWorker.ts"),
],
{ {
outDir: path.join(this.rootPath, out), outDir: path.join(this.rootPath, out),
cacheDir: path.join(this.rootPath, ".cache"), cacheDir: path.join(this.rootPath, ".cache"),

View File

@ -64,7 +64,7 @@
"vfile-message": "^2.0.2" "vfile-message": "^2.0.2"
}, },
"dependencies": { "dependencies": {
"@coder/logger": "1.1.11", "@coder/logger": "1.1.16",
"env-paths": "^2.2.0", "env-paths": "^2.2.0",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"http-proxy": "^1.18.0", "http-proxy": "^1.18.0",
@ -72,6 +72,7 @@
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"limiter": "^1.1.5", "limiter": "^1.1.5",
"pem": "^1.14.2", "pem": "^1.14.2",
"rotating-file-stream": "^2.1.1",
"safe-compare": "^1.1.4", "safe-compare": "^1.1.4",
"semver": "^7.1.3", "semver": "^7.1.3",
"tar": "^6.0.1", "tar": "^6.0.1",

View File

@ -1,28 +0,0 @@
<!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="style-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
/>
<title>code-server</title>
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
<link
rel="manifest"
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
crossorigin="use-credentials"
/>
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" />
<meta id="coder-options" data-settings="{{OPTIONS}}" />
</head>
<body>
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/pages/app.js"></script>
</body>
</html>

View File

@ -1,37 +0,0 @@
import { getOptions, normalize } from "../../common/util"
import { ApiEndpoint } from "../../common/http"
import "./error.css"
import "./global.css"
import "./home.css"
import "./login.css"
import "./update.css"
const options = getOptions()
const isInput = (el: Element): el is HTMLInputElement => {
return !!(el as HTMLInputElement).name
}
document.querySelectorAll("form").forEach((form) => {
if (!form.classList.contains("-x11")) {
return
}
form.addEventListener("submit", (event) => {
event.preventDefault()
const values: { [key: string]: string } = {}
Array.from(form.elements).forEach((element) => {
if (isInput(element)) {
values[element.name] = element.value
}
})
fetch(normalize(`${options.base}/api/${ApiEndpoint.process}`), {
method: "POST",
body: JSON.stringify(values),
})
})
})
// TEMP: Until we can get the real ready event.
const event = new CustomEvent("ide-ready")
window.dispatchEvent(event)

View File

@ -18,7 +18,7 @@
crossorigin="use-credentials" crossorigin="use-credentials"
/> />
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" /> <link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" /> <link href="{{BASE}}/static/{{COMMIT}}/dist/register.css" rel="stylesheet" />
<meta id="coder-options" data-settings="{{OPTIONS}}" /> <meta id="coder-options" data-settings="{{OPTIONS}}" />
</head> </head>
<body> <body>

View File

@ -1,51 +0,0 @@
.block-row {
display: flex;
}
.block-row > .item {
flex: 1;
margin: 2px 0;
}
.block-row > button.item {
background: none;
border: none;
cursor: pointer;
text-align: left;
}
.block-row > .item > .sub {
font-size: 0.95em;
}
.block-row .-link {
color: rgb(87, 114, 245);
display: block;
text-decoration: none;
}
.block-row .-link:hover {
text-decoration: underline;
}
.block-row > .item > .icon {
height: 1rem;
margin-right: 5px;
vertical-align: top;
width: 1rem;
}
.block-row > .item > .icon.-missing {
background-color: rgba(87, 114, 245, 0.2);
display: inline-block;
text-align: center;
}
.kill-form {
display: inline-block;
}
.kill-form > .kill {
border-radius: 3px;
padding: 2px 5px;
}

View File

@ -1,59 +0,0 @@
<!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="style-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
/>
<title>code-server</title>
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
<link
rel="manifest"
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
crossorigin="use-credentials"
/>
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" />
<meta id="coder-options" data-settings="{{OPTIONS}}" />
</head>
<body>
<div class="center-container">
<div class="card-box">
<div class="header">
<h2 class="main">Editors</h2>
<div class="sub">Choose an editor to launch below.</div>
</div>
<div class="content">
{{APP_LIST:EDITORS}}
</div>
</div>
<div class="card-box">
<div class="header">
<h2 class="main">Other</h2>
<div class="sub">Choose an application to launch below.</div>
</div>
<div class="content">
{{APP_LIST:OTHER}}
</div>
</div>
<div class="card-box">
<div class="header">
<h2 class="main">Version</h2>
<div class="sub">Version information and updates.</div>
</div>
<div class="content">
{{UPDATE:NAME}}
</div>
</div>
</div>
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/pages/app.js"></script>
</body>
</html>

View File

@ -18,7 +18,7 @@
crossorigin="use-credentials" crossorigin="use-credentials"
/> />
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" /> <link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" /> <link href="{{BASE}}/static/{{COMMIT}}/dist/register.css" rel="stylesheet" />
<meta id="coder-options" data-settings="{{OPTIONS}}" /> <meta id="coder-options" data-settings="{{OPTIONS}}" />
</head> </head>
<body> <body>

View File

@ -1,43 +0,0 @@
<!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="style-src 'self'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
/>
<title>code-server</title>
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
<link
rel="manifest"
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
crossorigin="use-credentials"
/>
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" />
<meta id="coder-options" data-settings="{{OPTIONS}}" />
</head>
<body>
<div class="center-container">
<div class="card-box">
<div class="header">
<h1 class="main">Update</h1>
<div class="sub">Update code-server.</div>
</div>
<div class="content">
<form class="update-form" action="{{BASE}}/update/apply">
{{UPDATE_STATUS}} {{ERROR}}
<div class="links">
<a class="link" href="{{BASE}}{{TO}}">go home</a>
</div>
</form>
</div>
</div>
</div>
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
</body>
</html>

View File

@ -2,13 +2,17 @@ import { getOptions, normalize } from "../common/util"
const options = getOptions() const options = getOptions()
import "./pages/error.css"
import "./pages/global.css"
import "./pages/login.css"
if ("serviceWorker" in navigator) { if ("serviceWorker" in navigator) {
const path = normalize(`${options.base}/static/${options.commit}/dist/serviceWorker.js`) const path = normalize(`${options.base}/static/${options.commit}/dist/serviceWorker.js`)
navigator.serviceWorker navigator.serviceWorker
.register(path, { .register(path, {
scope: options.base || "/", scope: options.base || "/",
}) })
.then(function () { .then(() => {
console.log("[Service Worker] registered") console.log("[Service Worker] registered")
}) })
} }

View File

@ -1,60 +0,0 @@
export interface Application {
readonly categories?: string[]
readonly comment?: string
readonly directory?: string
readonly exec?: string
readonly genericName?: string
readonly icon?: string
readonly installed?: boolean
readonly name: string
/**
* Path if this is a browser app (like VS Code).
*/
readonly path?: string
/**
* PID if this is a process.
*/
readonly pid?: number
readonly version?: string
}
export interface ApplicationsResponse {
readonly applications: ReadonlyArray<Application>
}
export enum SessionError {
FailedToStart = 4000,
Starting = 4001,
InvalidState = 4002,
Unknown = 4003,
}
export interface SessionResponse {
/**
* Whether the process was spawned or an existing one was returned.
*/
created: boolean
pid: number
}
export interface RecentResponse {
readonly paths: string[]
readonly workspaces: string[]
}
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
}

View File

@ -9,16 +9,8 @@ export enum HttpCode {
} }
export class HttpError extends Error { export class HttpError extends Error {
public constructor(message: string, public readonly code: number) { public constructor(message: string, public readonly code: number, public readonly details?: object) {
super(message) super(message)
this.name = this.constructor.name this.name = this.constructor.name
} }
} }
export enum ApiEndpoint {
applications = "/applications",
process = "/process",
recent = "/recent",
run = "/run",
status = "/status",
}

View File

@ -33,6 +33,13 @@ export const normalize = (url: string, keepTrailing = false): string => {
return url.replace(/\/\/+/g, "/").replace(/\/+$/, keepTrailing ? "/" : "") return url.replace(/\/\/+/g, "/").replace(/\/+$/, keepTrailing ? "/" : "")
} }
/**
* Remove leading and trailing slashes.
*/
export const trimSlashes = (url: string): string => {
return url.replace(/^\/+|\/+$/g, "")
}
/** /**
* Get options embedded in the HTML or query params. * Get options embedded in the HTML or query params.
*/ */
@ -75,3 +82,17 @@ export const getOptions = <T extends Options>(): T => {
return options return options
} }
/**
* Wrap the value in an array if it's not already an array. If the value is
* undefined return an empty array.
*/
export const arrayify = <T>(value?: T | T[]): T[] => {
if (Array.isArray(value)) {
return value
}
if (typeof value === "undefined") {
return []
}
return [value]
}

View File

@ -1,312 +0,0 @@
import { field, logger } from "@coder/logger"
import * as cp from "child_process"
import * as fs from "fs-extra"
import * as http from "http"
import * as net from "net"
import * as path from "path"
import * as url from "url"
import * as WebSocket from "ws"
import {
Application,
ApplicationsResponse,
ClientMessage,
RecentResponse,
ServerMessage,
SessionError,
SessionResponse,
} from "../../common/api"
import { ApiEndpoint, HttpCode, HttpError } from "../../common/http"
import { HttpProvider, HttpProviderOptions, HttpResponse, HttpServer, Route } from "../http"
import { findApplications, findWhitelistedApplications, Vscode } from "./bin"
import { VscodeHttpProvider } from "./vscode"
interface VsRecents {
[key: string]: (string | { configURIPath: string })[]
}
type VsSettings = [string, string][]
/**
* API HTTP provider.
*/
export class ApiHttpProvider extends HttpProvider {
private readonly ws = new WebSocket.Server({ noServer: true })
public constructor(
options: HttpProviderOptions,
private readonly server: HttpServer,
private readonly vscode: VscodeHttpProvider,
private readonly dataDir?: string,
) {
super(options)
}
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
this.ensureAuthenticated(request)
if (!this.isRoot(route)) {
throw new HttpError("Not found", HttpCode.NotFound)
}
switch (route.base) {
case ApiEndpoint.applications:
this.ensureMethod(request)
return {
mime: "application/json",
content: {
applications: await this.applications(),
},
} as HttpResponse<ApplicationsResponse>
case ApiEndpoint.process:
return this.process(request)
case ApiEndpoint.recent:
this.ensureMethod(request)
return {
mime: "application/json",
content: await this.recent(),
} as HttpResponse<RecentResponse>
}
throw new HttpError("Not found", HttpCode.NotFound)
}
public async handleWebSocket(
route: Route,
request: http.IncomingMessage,
socket: net.Socket,
head: Buffer,
): Promise<void> {
if (!this.authenticated(request)) {
throw new Error("not authenticated")
}
switch (route.base) {
case ApiEndpoint.status:
return this.handleStatusSocket(request, socket, head)
case ApiEndpoint.run:
return this.handleRunSocket(route, request, socket, head)
}
throw new HttpError("Not found", HttpCode.NotFound)
}
private async handleStatusSocket(request: http.IncomingMessage, socket: net.Socket, head: Buffer): Promise<void> {
const getMessageResponse = async (event: "health"): Promise<ServerMessage> => {
switch (event) {
case "health":
return { event, connections: await this.server.getConnections() }
default:
throw new Error("unexpected message")
}
}
await new Promise<WebSocket>((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())
getMessageResponse(message.event).then(send)
} catch (error) {
logger.error(error.message, field("message", data))
}
})
resolve()
})
})
}
/**
* A socket that connects to the process.
*/
private async handleRunSocket(
_route: Route,
request: http.IncomingMessage,
socket: net.Socket,
head: Buffer,
): Promise<void> {
logger.debug("connecting to process")
const ws = await new Promise<WebSocket>((resolve, reject) => {
this.ws.handleUpgrade(request, socket, head, (socket) => {
socket.binaryType = "arraybuffer"
socket.on("error", (error) => {
socket.close(SessionError.FailedToStart)
logger.error("got error while connecting socket", field("error", error))
reject(error)
})
resolve(socket as WebSocket)
})
})
logger.debug("connected to process")
// Send ready message.
ws.send(
Buffer.from(
JSON.stringify({
protocol: "TODO",
}),
),
)
}
/**
* Return whitelisted applications.
*/
public async applications(): Promise<ReadonlyArray<Application>> {
return findWhitelistedApplications()
}
/**
* Return installed applications.
*/
public async installedApplications(): Promise<ReadonlyArray<Application>> {
return findApplications()
}
/**
* Handle /process endpoint.
*/
private async process(request: http.IncomingMessage): Promise<HttpResponse> {
this.ensureMethod(request, ["DELETE", "POST"])
const data = await this.getData(request)
if (!data) {
throw new HttpError("No data was provided", HttpCode.BadRequest)
}
const parsed: Application = JSON.parse(data)
switch (request.method) {
case "DELETE":
if (parsed.pid) {
await this.killProcess(parsed.pid)
} else if (parsed.path) {
await this.killProcess(parsed.path)
} else {
throw new Error("No pid or path was provided")
}
return {
mime: "application/json",
code: HttpCode.Ok,
}
case "POST": {
if (!parsed.exec) {
throw new Error("No exec was provided")
}
return {
mime: "application/json",
content: {
created: true,
pid: await this.spawnProcess(parsed.exec),
},
} as HttpResponse<SessionResponse>
}
}
throw new HttpError("Not found", HttpCode.NotFound)
}
/**
* Kill a process identified by pid or path if a web app.
*/
public async killProcess(pid: number | string): Promise<void> {
if (typeof pid === "string") {
switch (pid) {
case Vscode.path:
await this.vscode.dispose()
break
default:
throw new Error(`Process "${pid}" does not exist`)
}
} else {
process.kill(pid)
}
}
/**
* Spawn a process and return the pid.
*/
public async spawnProcess(exec: string): Promise<number> {
const proc = cp.spawn(exec, {
shell: process.env.SHELL || true,
env: {
...process.env,
},
})
proc.on("error", (error) => logger.error("process errored", field("pid", proc.pid), field("error", error)))
proc.on("exit", () => logger.debug("process exited", field("pid", proc.pid)))
logger.debug("started process", field("pid", proc.pid))
return proc.pid
}
/**
* Return VS Code's recent paths.
*/
public async recent(): Promise<RecentResponse> {
try {
if (!this.dataDir) {
throw new Error("data directory is not set")
}
const state: VsSettings = JSON.parse(await fs.readFile(path.join(this.dataDir, "User/state/global.json"), "utf8"))
const setting = Array.isArray(state) && state.find((item) => item[0] === "recently.opened")
if (!setting) {
return { paths: [], workspaces: [] }
}
const pathPromises: { [key: string]: Promise<string> } = {}
const workspacePromises: { [key: string]: Promise<string> } = {}
Object.values(JSON.parse(setting[1]) as VsRecents).forEach((recents) => {
recents.forEach((recent) => {
try {
const target = typeof recent === "string" ? pathPromises : workspacePromises
const pathname = url.parse(typeof recent === "string" ? recent : recent.configURIPath).pathname
if (pathname && !target[pathname]) {
target[pathname] = new Promise<string>((resolve) => {
fs.stat(pathname)
.then(() => resolve(pathname))
.catch(() => resolve())
})
}
} catch (error) {
logger.debug("invalid path", field("path", recent))
}
})
})
const [paths, workspaces] = await Promise.all([
Promise.all(Object.values(pathPromises)),
Promise.all(Object.values(workspacePromises)),
])
return {
paths: paths.filter((p) => !!p),
workspaces: workspaces.filter((p) => !!p),
}
} catch (error) {
if (error.code !== "ENOENT") {
throw error
}
}
return { paths: [], workspaces: [] }
}
/**
* For these, just return the error message since they'll be requested as
* JSON.
*/
public async getErrorRoot(_route: Route, _title: string, _header: string, error: string): Promise<HttpResponse> {
return {
mime: "application/json",
content: JSON.stringify({ error }),
}
}
}

View File

@ -1,30 +0,0 @@
import * as fs from "fs"
import * as path from "path"
import { Application } from "../../common/api"
const getVscodeVersion = (): string => {
try {
return require(path.resolve(__dirname, "../../../lib/vscode/package.json")).version
} catch (error) {
return "unknown"
}
}
export const Vscode: Application = {
categories: ["Editor"],
icon: fs.readFileSync(path.resolve(__dirname, "../../../lib/vscode/resources/linux/code.png")).toString("base64"),
installed: true,
name: "VS Code",
path: "/",
version: getVscodeVersion(),
}
export const findApplications = async (): Promise<ReadonlyArray<Application>> => {
const apps: Application[] = [Vscode]
return apps.sort((a, b): number => a.name.localeCompare(b.name))
}
export const findWhitelistedApplications = async (): Promise<ReadonlyArray<Application>> => {
return [Vscode]
}

View File

@ -1,147 +0,0 @@
import * as http from "http"
import * as querystring from "querystring"
import { Application } from "../../common/api"
import { HttpCode, HttpError } from "../../common/http"
import { normalize } from "../../common/util"
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
import { ApiHttpProvider } from "./api"
import { UpdateHttpProvider } from "./update"
/**
* Dashboard HTTP provider.
*/
export class DashboardHttpProvider extends HttpProvider {
public constructor(
options: HttpProviderOptions,
private readonly api: ApiHttpProvider,
private readonly update: UpdateHttpProvider,
) {
super(options)
}
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
if (!this.isRoot(route)) {
throw new HttpError("Not found", HttpCode.NotFound)
}
switch (route.base) {
case "/spawn": {
this.ensureAuthenticated(request)
this.ensureMethod(request, "POST")
const data = await this.getData(request)
const app = data ? querystring.parse(data) : {}
if (app.path) {
return { redirect: Array.isArray(app.path) ? app.path[0] : app.path }
}
if (!app.exec) {
throw new Error("No exec was provided")
}
this.api.spawnProcess(Array.isArray(app.exec) ? app.exec[0] : app.exec)
return { redirect: this.options.base }
}
case "/app":
case "/": {
this.ensureMethod(request)
if (!this.authenticated(request)) {
return { redirect: "/login", query: { to: this.options.base } }
}
return route.base === "/" ? this.getRoot(route) : this.getAppRoot(route)
}
}
throw new HttpError("Not found", HttpCode.NotFound)
}
public async getRoot(route: Route): Promise<HttpResponse> {
const base = this.base(route)
const apps = await this.api.installedApplications()
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/home.html")
response.content = response.content
.replace(/{{UPDATE:NAME}}/, await this.getUpdate(base))
.replace(
/{{APP_LIST:EDITORS}}/,
this.getAppRows(
base,
apps.filter((app) => app.categories && app.categories.includes("Editor")),
),
)
.replace(
/{{APP_LIST:OTHER}}/,
this.getAppRows(
base,
apps.filter((app) => !app.categories || !app.categories.includes("Editor")),
),
)
return this.replaceTemplates(route, response)
}
public async getAppRoot(route: Route): Promise<HttpResponse> {
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/app.html")
return this.replaceTemplates(route, response)
}
private getAppRows(base: string, apps: ReadonlyArray<Application>): string {
return apps.length > 0
? apps.map((app) => this.getAppRow(base, app)).join("\n")
: `<div class="none">No applications found.</div>`
}
private getAppRow(base: string, app: Application): string {
return `<form class="block-row${app.exec ? " -x11" : ""}" method="post" action="${normalize(
`${base}${this.options.base}/spawn`,
)}">
<button class="item -row -link">
<input type="hidden" name="path" value="${app.path || ""}">
<input type="hidden" name="exec" value="${app.exec || ""}">
${
app.icon
? `<img class="icon" src="data:image/png;base64,${app.icon}"></img>`
: `<span class="icon -missing"></span>`
}
<span class="name">${app.name}</span>
</button>
</form>`
}
private async getUpdate(base: string): Promise<string> {
if (!this.update.enabled) {
return `<div class="block-row"><div class="item"><div class="sub">Updates are disabled</div></div></div>`
}
const humanize = (time: number): string => {
const d = new Date(time)
const pad = (t: number): string => (t < 10 ? "0" : "") + t
return (
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` +
` ${pad(d.getHours())}:${pad(d.getMinutes())}`
)
}
const update = await this.update.getUpdate()
if (this.update.isLatestVersion(update)) {
return `<div class="block-row">
<div class="item">
Latest: ${update.version}
<div class="sub">Up to date</div>
</div>
<div class="item">
${humanize(update.checked)}
<a class="sub -link" href="${base}/update/check?to=${this.options.base}">Check now</a>
</div>
<div class="item" >Current: ${this.update.currentVersion}</div>
</div>`
}
return `<div class="block-row">
<div class="item">
Latest: ${update.version}
<div class="sub">Out of date</div>
</div>
<div class="item">
${humanize(update.checked)}
<a class="sub -link" href="${base}/update?to=${this.options.base}">Update now</a>
</div>
<div class="item" >Current: ${this.update.currentVersion}</div>
</div>`
}
}

View File

@ -24,7 +24,7 @@ export class ProxyHttpProvider extends HttpProvider {
const port = route.base.replace(/^\//, "") const port = route.base.replace(/^\//, "")
return { return {
proxy: { proxy: {
base: `${this.options.base}/${port}`, strip: `${route.providerBase}/${port}`,
port, port,
}, },
} }
@ -35,7 +35,7 @@ export class ProxyHttpProvider extends HttpProvider {
const port = route.base.replace(/^\//, "") const port = route.base.replace(/^\//, "")
return { return {
proxy: { proxy: {
base: `${this.options.base}/${port}`, strip: `${route.providerBase}/${port}`,
port, port,
}, },
} }

View File

@ -1,21 +1,12 @@
import { field, logger } from "@coder/logger" import { field, logger } from "@coder/logger"
import * as cp from "child_process"
import * as fs from "fs-extra"
import * as http from "http" import * as http from "http"
import * as https from "https" import * as https from "https"
import * as os from "os"
import * as path from "path" import * as path from "path"
import * as semver from "semver" import * as semver from "semver"
import { Readable, Writable } from "stream"
import * as tar from "tar-fs"
import * as url from "url" import * as url from "url"
import * as util from "util"
import * as zlib from "zlib"
import { HttpCode, HttpError } from "../../common/http" import { HttpCode, HttpError } from "../../common/http"
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http" import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
import { settings as globalSettings, SettingsProvider, UpdateSettings } from "../settings" import { settings as globalSettings, SettingsProvider, UpdateSettings } from "../settings"
import { tmpdir } from "../util"
import { ipcMain } from "../wrapper"
export interface Update { export interface Update {
checked: number checked: number
@ -27,7 +18,7 @@ export interface LatestResponse {
} }
/** /**
* Update HTTP provider. * HTTP provider for checking updates (does not download/install them).
*/ */
export class UpdateHttpProvider extends HttpProvider { export class UpdateHttpProvider extends HttpProvider {
private update?: Promise<Update> private update?: Promise<Update>
@ -41,12 +32,6 @@ export class UpdateHttpProvider extends HttpProvider {
* that fulfills `LatestResponse`. * that fulfills `LatestResponse`.
*/ */
private readonly latestUrl = "https://api.github.com/repos/cdr/code-server/releases/latest", private readonly latestUrl = "https://api.github.com/repos/cdr/code-server/releases/latest",
/**
* The URL for downloading a version of code-server. {{VERSION}} and
* {{RELEASE_NAME}} will be replaced (for example 2.1.0 and
* code-server-2.1.0-linux-x86_64.tar.gz).
*/
private readonly downloadUrl = "https://github.com/cdr/code-server/releases/download/{{VERSION}}/{{RELEASE_NAME}}",
/** /**
* Update information will be stored here. If not provided, the global * Update information will be stored here. If not provided, the global
* settings will be used. * settings will be used.
@ -64,66 +49,30 @@ export class UpdateHttpProvider extends HttpProvider {
throw new HttpError("Not found", HttpCode.NotFound) throw new HttpError("Not found", HttpCode.NotFound)
} }
switch (route.base) { if (!this.enabled) {
case "/check": throw new Error("update checks are disabled")
this.getUpdate(true)
if (route.query && route.query.to) {
return {
redirect: Array.isArray(route.query.to) ? route.query.to[0] : route.query.to,
query: { to: undefined },
}
}
return this.getRoot(route, request)
case "/apply":
return this.tryUpdate(route, request)
case "/":
return this.getRoot(route, request)
} }
throw new HttpError("Not found", HttpCode.NotFound) switch (route.base) {
} case "/check":
case "/": {
public async getRoot( const update = await this.getUpdate(route.base === "/check")
route: Route,
request: http.IncomingMessage,
errorOrUpdate?: Update | Error,
): Promise<HttpResponse> {
if (request.headers["content-type"] === "application/json") {
if (!this.enabled) {
return { return {
content: { content: {
isLatest: true, ...update,
isLatest: this.isLatestVersion(update),
}, },
} }
} }
const update = await this.getUpdate()
return {
content: {
...update,
isLatest: this.isLatestVersion(update),
},
}
} }
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/update.html")
response.content = response.content throw new HttpError("Not found", HttpCode.NotFound)
.replace(
/{{UPDATE_STATUS}}/,
errorOrUpdate && !(errorOrUpdate instanceof Error)
? `Updated to ${errorOrUpdate.version}`
: await this.getUpdateHtml(),
)
.replace(/{{ERROR}}/, errorOrUpdate instanceof Error ? `<div class="error">${errorOrUpdate.message}</div>` : "")
return this.replaceTemplates(route, response)
} }
/** /**
* Query for and return the latest update. * Query for and return the latest update.
*/ */
public async getUpdate(force?: boolean): Promise<Update> { public async getUpdate(force?: boolean): Promise<Update> {
if (!this.enabled) {
throw new Error("updates are not enabled")
}
// Don't run multiple requests at a time. // Don't run multiple requests at a time.
if (!this.update) { if (!this.update) {
this.update = this._getUpdate(force) this.update = this._getUpdate(force)
@ -171,128 +120,6 @@ export class UpdateHttpProvider extends HttpProvider {
} }
} }
private async getUpdateHtml(): Promise<string> {
if (!this.enabled) {
return "Updates are disabled"
}
const update = await this.getUpdate()
if (this.isLatestVersion(update)) {
return "No update available"
}
return `<button type="submit" class="apply -button">Update to ${update.version}</button>`
}
public async tryUpdate(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
try {
const update = await this.getUpdate()
if (!this.isLatestVersion(update)) {
await this.downloadAndApplyUpdate(update)
return this.getRoot(route, request, update)
}
return this.getRoot(route, request)
} catch (error) {
// For JSON requests propagate the error. Otherwise catch it so we can
// show the error inline with the update button instead of an error page.
if (request.headers["content-type"] === "application/json") {
throw error
}
return this.getRoot(route, error)
}
}
public async downloadAndApplyUpdate(update: Update, targetPath?: string): Promise<void> {
const releaseName = await this.getReleaseName(update)
const url = this.downloadUrl.replace("{{VERSION}}", update.version).replace("{{RELEASE_NAME}}", releaseName)
let downloadPath = path.join(tmpdir, "updates", releaseName)
fs.mkdirp(path.dirname(downloadPath))
const response = await this.requestResponse(url)
try {
downloadPath = await this.extractTar(response, downloadPath)
logger.debug("Downloaded update", field("path", downloadPath))
// The archive should have a directory inside at the top level with the
// same name as the archive.
const directoryPath = path.join(downloadPath, path.basename(downloadPath))
await fs.stat(directoryPath)
if (!targetPath) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
targetPath = path.resolve(__dirname, "../../../")
}
// Move the old directory to prevent potential data loss.
const backupPath = path.resolve(targetPath, `../${path.basename(targetPath)}.${Date.now().toString()}`)
logger.debug("Replacing files", field("target", targetPath), field("backup", backupPath))
await fs.move(targetPath, backupPath)
// Move the new directory.
await fs.move(directoryPath, targetPath)
await fs.remove(downloadPath)
if (process.send) {
ipcMain().relaunch(update.version)
}
} catch (error) {
response.destroy(error)
throw error
}
}
private async extractTar(response: Readable, downloadPath: string): Promise<string> {
downloadPath = downloadPath.replace(/\.tar\.gz$/, "")
logger.debug("Extracting tar", field("path", downloadPath))
response.pause()
await fs.remove(downloadPath)
const decompress = zlib.createGunzip()
response.pipe(decompress as Writable)
response.on("error", (error) => decompress.destroy(error))
response.on("close", () => decompress.end())
const destination = tar.extract(downloadPath)
decompress.pipe(destination)
decompress.on("error", (error) => destination.destroy(error))
decompress.on("close", () => destination.end())
await new Promise((resolve, reject) => {
destination.on("finish", resolve)
destination.on("error", reject)
response.resume()
})
return downloadPath
}
/**
* Given an update return the name for the packaged archived.
*/
public async getReleaseName(update: Update): 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-${update.version}-${target}-${arch}.tar.gz`
}
private async request(uri: string): Promise<Buffer> { private async request(uri: string): Promise<Buffer> {
const response = await this.requestResponse(uri) const response = await this.requestResponse(uri)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@ -14,7 +14,7 @@ import {
WorkbenchOptions, WorkbenchOptions,
} from "../../../lib/vscode/src/vs/server/ipc" } from "../../../lib/vscode/src/vs/server/ipc"
import { HttpCode, HttpError } from "../../common/http" import { HttpCode, HttpError } from "../../common/http"
import { generateUuid } from "../../common/util" import { arrayify, generateUuid } from "../../common/util"
import { Args } from "../cli" import { Args } from "../cli"
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http" import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
import { settings } from "../settings" import { settings } from "../settings"
@ -131,7 +131,7 @@ export class VscodeHttpProvider extends HttpProvider {
if (!this.isRoot(route)) { if (!this.isRoot(route)) {
throw new HttpError("Not found", HttpCode.NotFound) throw new HttpError("Not found", HttpCode.NotFound)
} else if (!this.authenticated(request)) { } else if (!this.authenticated(request)) {
return { redirect: "/login", query: { to: this.options.base } } return { redirect: "/login", query: { to: route.providerBase } }
} }
try { try {
return await this.getRoot(request, route) return await this.getRoot(request, route)
@ -223,8 +223,7 @@ export class VscodeHttpProvider extends HttpProvider {
} }
for (let i = 0; i < startPaths.length; ++i) { for (let i = 0; i < startPaths.length; ++i) {
const startPath = startPaths[i] const startPath = startPaths[i]
const url = const url = arrayify(startPath && startPath.url).find((p) => !!p)
startPath && (typeof startPath.url === "string" ? [startPath.url] : startPath.url || []).find((p) => !!p)
if (startPath && url) { if (startPath && url) {
return { return {
url, url,

View File

@ -2,8 +2,6 @@ import { field, logger } from "@coder/logger"
import * as cp from "child_process" import * as cp from "child_process"
import * as path from "path" import * as path from "path"
import { CliMessage } from "../../lib/vscode/src/vs/server/ipc" import { CliMessage } from "../../lib/vscode/src/vs/server/ipc"
import { ApiHttpProvider } from "./app/api"
import { DashboardHttpProvider } from "./app/dashboard"
import { LoginHttpProvider } from "./app/login" import { LoginHttpProvider } from "./app/login"
import { ProxyHttpProvider } from "./app/proxy" import { ProxyHttpProvider } from "./app/proxy"
import { StaticHttpProvider } from "./app/static" import { StaticHttpProvider } from "./app/static"
@ -73,15 +71,17 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise<void>
} }
const httpServer = new HttpServer(options) const httpServer = new HttpServer(options)
const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args) httpServer.registerHttpProvider(["/", "/vscode"], VscodeHttpProvider, args)
const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"]) httpServer.registerHttpProvider("/update", UpdateHttpProvider, false)
const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, false)
httpServer.registerHttpProvider("/proxy", ProxyHttpProvider) httpServer.registerHttpProvider("/proxy", ProxyHttpProvider)
httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, envPassword) httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, envPassword)
httpServer.registerHttpProvider("/static", StaticHttpProvider) httpServer.registerHttpProvider("/static", StaticHttpProvider)
httpServer.registerHttpProvider("/dashboard", DashboardHttpProvider, api, update)
ipcMain().onDispose(() => httpServer.dispose()) ipcMain().onDispose(() => {
httpServer.dispose().then((errors) => {
errors.forEach((error) => logger.error(error.message))
})
})
logger.info(`code-server ${version} ${commit}`) logger.info(`code-server ${version} ${commit}`)
const serverAddress = await httpServer.listen() const serverAddress = await httpServer.listen()

View File

@ -12,7 +12,7 @@ import { Readable } from "stream"
import * as tls from "tls" import * as tls from "tls"
import * as url from "url" import * as url from "url"
import { HttpCode, HttpError } from "../common/http" import { HttpCode, HttpError } from "../common/http"
import { normalize, Options, plural, split } from "../common/util" import { arrayify, normalize, Options, plural, split, trimSlashes } from "../common/util"
import { SocketProxyProvider } from "./socket" import { SocketProxyProvider } from "./socket"
import { getMediaMime, paths } from "./util" import { getMediaMime, paths } from "./util"
@ -36,9 +36,13 @@ export type Query = { [key: string]: string | string[] | undefined }
export interface ProxyOptions { export interface ProxyOptions {
/** /**
* A base path to strip from from the request before proxying if necessary. * A path to strip from from the beginning of the request before proxying
*/ */
base?: string strip?: string
/**
* A path to add to the beginning of the request before proxying.
*/
prepend?: string
/** /**
* The port to proxy. * The port to proxy.
*/ */
@ -79,9 +83,8 @@ export interface HttpResponse<T = string | Buffer | object> {
*/ */
mime?: string mime?: string
/** /**
* Redirect to this path. Will rewrite against the base path but NOT the * Redirect to this path. This is constructed against the site base (not the
* provider endpoint so you must include it. This allows redirecting outside * provider's base).
* of your endpoint.
*/ */
redirect?: string redirect?: string
/** /**
@ -133,12 +136,16 @@ export interface HttpServerOptions {
export interface Route { export interface Route {
/** /**
* Base path part (in /test/path it would be "/test"). * Provider base path part (for /provider/base/path it would be /provider).
*/
providerBase: string
/**
* Base path part (for /provider/base/path it would be /base).
*/ */
base: string base: string
/** /**
* Remaining part of the route (in /test/path it would be "/path"). It can be * Remaining part of the route after factoring out the base and provider base
* blank. * (for /provider/base/path it would be /path). It can be blank.
*/ */
requestPath: string requestPath: string
/** /**
@ -161,7 +168,6 @@ interface ProviderRoute extends Route {
export interface HttpProviderOptions { export interface HttpProviderOptions {
readonly auth: AuthType readonly auth: AuthType
readonly base: string
readonly commit: string readonly commit: string
readonly password?: string readonly password?: string
} }
@ -175,7 +181,7 @@ export abstract class HttpProvider {
public constructor(protected readonly options: HttpProviderOptions) {} public constructor(protected readonly options: HttpProviderOptions) {}
public dispose(): void { public async dispose(): Promise<void> {
// No default behavior. // No default behavior.
} }
@ -281,7 +287,7 @@ export abstract class HttpProvider {
* Helper to error on invalid methods (default GET). * Helper to error on invalid methods (default GET).
*/ */
protected ensureMethod(request: http.IncomingMessage, method?: string | string[]): void { protected ensureMethod(request: http.IncomingMessage, method?: string | string[]): void {
const check = Array.isArray(method) ? method : [method || "GET"] const check = arrayify(method || "GET")
if (!request.method || !check.includes(request.method)) { if (!request.method || !check.includes(request.method)) {
throw new HttpError(`Unsupported method ${request.method}`, HttpCode.BadRequest) throw new HttpError(`Unsupported method ${request.method}`, HttpCode.BadRequest)
} }
@ -502,9 +508,15 @@ export class HttpServer {
}) })
} }
public dispose(): void { /**
* Stop and dispose everything. Return an array of disposal errors.
*/
public async dispose(): Promise<Error[]> {
this.socketProvider.stop() this.socketProvider.stop()
this.providers.forEach((p) => p.dispose()) const providers = Array.from(this.providers.values())
// Catch so all the errors can be seen rather than just the first one.
const responses = await Promise.all<Error | undefined>(providers.map((p) => p.dispose().catch((e) => e)))
return responses.filter<Error>((r): r is Error => typeof r !== "undefined")
} }
public async getConnections(): Promise<number> { public async getConnections(): Promise<number> {
@ -518,41 +530,51 @@ export class HttpServer {
/** /**
* Register a provider for a top-level endpoint. * Register a provider for a top-level endpoint.
*/ */
public registerHttpProvider<T extends HttpProvider>(endpoint: string, provider: HttpProvider0<T>): T public registerHttpProvider<T extends HttpProvider>(endpoint: string | string[], provider: HttpProvider0<T>): T
public registerHttpProvider<A1, T extends HttpProvider>(endpoint: string, provider: HttpProvider1<A1, T>, a1: A1): T public registerHttpProvider<A1, T extends HttpProvider>(
endpoint: string | string[],
provider: HttpProvider1<A1, T>,
a1: A1,
): T
public registerHttpProvider<A1, A2, T extends HttpProvider>( public registerHttpProvider<A1, A2, T extends HttpProvider>(
endpoint: string, endpoint: string | string[],
provider: HttpProvider2<A1, A2, T>, provider: HttpProvider2<A1, A2, T>,
a1: A1, a1: A1,
a2: A2, a2: A2,
): T ): T
public registerHttpProvider<A1, A2, A3, T extends HttpProvider>( public registerHttpProvider<A1, A2, A3, T extends HttpProvider>(
endpoint: string, endpoint: string | string[],
provider: HttpProvider3<A1, A2, A3, T>, provider: HttpProvider3<A1, A2, A3, T>,
a1: A1, a1: A1,
a2: A2, a2: A2,
a3: A3, a3: A3,
): T ): T
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
public registerHttpProvider(endpoint: string, provider: any, ...args: any[]): any { public registerHttpProvider(endpoint: string | string[], provider: any, ...args: any[]): 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})`)
}
const p = new provider( const p = new provider(
{ {
auth: this.options.auth || AuthType.None, auth: this.options.auth || AuthType.None,
base: `/${endpoint}`,
commit: this.options.commit, commit: this.options.commit,
password: this.options.password, password: this.options.password,
}, },
...args, ...args,
) )
this.providers.set(`/${endpoint}`, p) const endpoints = arrayify(endpoint).map(trimSlashes)
return p endpoints.forEach((endpoint) => {
if (/\//.test(endpoint)) {
throw new Error(`Only top-level endpoints are supported (got ${endpoint})`)
}
const existingProvider = this.providers.get(`/${endpoint}`)
this.providers.set(`/${endpoint}`, p)
if (existingProvider) {
logger.debug(`Overridding existing /${endpoint} provider`)
// If the existing provider isn't registered elsewhere we can dispose.
if (!Array.from(this.providers.values()).find((p) => p === existingProvider)) {
logger.debug(`Disposing existing /${endpoint} provider`)
existingProvider.dispose()
}
}
})
} }
/** /**
@ -649,8 +671,10 @@ export class HttpServer {
if (request.headers["content-type"] === "application/json") { if (request.headers["content-type"] === "application/json") {
write({ write({
code, code,
mime: "application/json",
content: { content: {
error: e.message, error: e.message,
...(e.details || {}),
}, },
}) })
} else { } else {
@ -759,7 +783,7 @@ export class HttpServer {
// that by shifting the next base out of the request path. // that by shifting the next base out of the request path.
let provider = this.providers.get(base) let provider = this.providers.get(base)
if (base !== "/" && provider) { if (base !== "/" && provider) {
return { ...parse(requestPath), fullPath, query: parsedUrl.query, provider, originalPath } return { ...parse(requestPath), providerBase: base, fullPath, query: parsedUrl.query, provider, originalPath }
} }
// Fall back to the top-level provider. // Fall back to the top-level provider.
@ -767,7 +791,7 @@ export class HttpServer {
if (!provider) { if (!provider) {
throw new Error(`No provider for ${base}`) throw new Error(`No provider for ${base}`)
} }
return { base, fullPath, requestPath, query: parsedUrl.query, provider, originalPath } return { base, providerBase: "/", fullPath, requestPath, query: parsedUrl.query, provider, originalPath }
} }
/** /**
@ -806,10 +830,11 @@ export class HttpServer {
// sure how best to get this information to the `proxyRes` event handler. // sure how best to get this information to the `proxyRes` event handler.
// For now I'm sticking it on the request object which is passed through to // For now I'm sticking it on the request object which is passed through to
// the event. // the event.
;(request as ProxyRequest).base = options.base ;(request as ProxyRequest).base = options.strip
const isHttp = response instanceof http.ServerResponse const isHttp = response instanceof http.ServerResponse
const path = options.base ? route.fullPath.replace(options.base, "") : route.fullPath const base = options.strip ? route.fullPath.replace(options.strip, "") : route.fullPath
const path = normalize("/" + (options.prepend || "") + "/" + base, true)
const proxyOptions: proxy.ServerOptions = { const proxyOptions: proxy.ServerOptions = {
changeOrigin: true, changeOrigin: true,
ignorePath: true, ignorePath: true,

View File

@ -1,6 +1,9 @@
import { logger, field } from "@coder/logger" import { field, logger } from "@coder/logger"
import * as cp from "child_process" import * as cp from "child_process"
import * as path from "path"
import * as rfs from "rotating-file-stream"
import { Emitter } from "../common/emitter" import { Emitter } from "../common/emitter"
import { paths } from "./util"
interface HandshakeMessage { interface HandshakeMessage {
type: "handshake" type: "handshake"
@ -140,8 +143,17 @@ export interface WrapperOptions {
export class WrapperProcess { export class WrapperProcess {
private process?: cp.ChildProcess private process?: cp.ChildProcess
private started?: Promise<void> private started?: Promise<void>
private readonly logStdoutStream: rfs.RotatingFileStream
private readonly logStderrStream: rfs.RotatingFileStream
public constructor(private currentVersion: string, private readonly options?: WrapperOptions) { public constructor(private currentVersion: string, private readonly options?: WrapperOptions) {
const opts = {
size: "10M",
maxFiles: 10,
}
this.logStdoutStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stdout.log"), opts)
this.logStderrStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stderr.log"), opts)
ipcMain().onDispose(() => { ipcMain().onDispose(() => {
if (this.process) { if (this.process) {
this.process.removeAllListeners() this.process.removeAllListeners()
@ -176,6 +188,15 @@ export class WrapperProcess {
public start(): Promise<void> { public start(): Promise<void> {
if (!this.started) { if (!this.started) {
this.started = this.spawn().then((child) => { this.started = this.spawn().then((child) => {
// Log both to stdout and to the log directory.
if (child.stdout) {
child.stdout.pipe(this.logStdoutStream)
child.stdout.pipe(process.stdout)
}
if (child.stderr) {
child.stderr.pipe(this.logStderrStream)
child.stderr.pipe(process.stderr)
}
logger.debug(`spawned inner process ${child.pid}`) logger.debug(`spawned inner process ${child.pid}`)
ipcMain() ipcMain()
.handshake(child) .handshake(child)
@ -205,7 +226,7 @@ export class WrapperProcess {
CODE_SERVER_PARENT_PID: process.pid.toString(), CODE_SERVER_PARENT_PID: process.pid.toString(),
NODE_OPTIONS: nodeOptions, NODE_OPTIONS: nodeOptions,
}, },
stdio: ["inherit", "inherit", "inherit", "ipc"], stdio: ["ipc"],
}) })
} }
} }

View File

@ -2,40 +2,33 @@ import * as assert from "assert"
import * as fs from "fs-extra" import * as fs from "fs-extra"
import * as http from "http" import * as http from "http"
import * as path from "path" import * as path from "path"
import * as tar from "tar-fs"
import * as zlib from "zlib"
import { LatestResponse, UpdateHttpProvider } from "../src/node/app/update" import { LatestResponse, UpdateHttpProvider } from "../src/node/app/update"
import { AuthType } from "../src/node/http" import { AuthType } from "../src/node/http"
import { SettingsProvider, UpdateSettings } from "../src/node/settings" import { SettingsProvider, UpdateSettings } from "../src/node/settings"
import { tmpdir } from "../src/node/util" import { tmpdir } from "../src/node/util"
describe("update", () => { describe("update", () => {
const archivePath = path.join(tmpdir, "tests/updates/code-server-loose-source")
let version = "1.0.0" let version = "1.0.0"
let spy: string[] = [] let spy: string[] = []
const server = http.createServer((request: http.IncomingMessage, response: http.ServerResponse) => { const server = http.createServer((request: http.IncomingMessage, response: http.ServerResponse) => {
if (!request.url) { if (!request.url) {
throw new Error("no url") throw new Error("no url")
} }
spy.push(request.url) spy.push(request.url)
response.writeHead(200)
// Return the latest version.
if (request.url === "/latest") { if (request.url === "/latest") {
const latest: LatestResponse = { const latest: LatestResponse = {
name: version, name: version,
} }
response.writeHead(200)
return response.end(JSON.stringify(latest)) return response.end(JSON.stringify(latest))
} }
const path = archivePath + (request.url.endsWith(".tar.gz") ? ".tar.gz" : ".zip") // Anything else is a 404.
response.writeHead(404)
const stream = fs.createReadStream(path) response.end("not found")
stream.on("error", (error: NodeJS.ErrnoException) => {
response.writeHead(500)
response.end(error.message)
})
response.writeHead(200)
stream.on("close", () => response.end())
stream.pipe(response)
}) })
const jsonPath = path.join(tmpdir, "tests/updates/update.json") const jsonPath = path.join(tmpdir, "tests/updates/update.json")
@ -51,12 +44,10 @@ describe("update", () => {
_provider = new UpdateHttpProvider( _provider = new UpdateHttpProvider(
{ {
auth: AuthType.None, auth: AuthType.None,
base: "/update",
commit: "test", commit: "test",
}, },
true, true,
`http://${address.address}:${address.port}/latest`, `http://${address.address}:${address.port}/latest`,
`http://${address.address}:${address.port}/download/{{VERSION}}/{{RELEASE_NAME}}`,
settings, settings,
) )
} }
@ -72,32 +63,8 @@ describe("update", () => {
host: "localhost", host: "localhost",
}) })
}) })
const p = provider()
const archiveName = (await p.getReleaseName({ version: "9999999.99999.9999", checked: 0 })).replace(
/.tar.gz$|.zip$/,
"",
)
await fs.remove(path.join(tmpdir, "tests/updates")) await fs.remove(path.join(tmpdir, "tests/updates"))
await fs.mkdirp(path.join(archivePath, archiveName)) await fs.mkdirp(path.join(tmpdir, "tests/updates"))
await Promise.all([
fs.writeFile(path.join(archivePath, archiveName, "code-server"), `console.log("UPDATED")`),
fs.writeFile(path.join(archivePath, archiveName, "node"), `NODE BINARY`),
])
await new Promise((resolve, reject) => {
const write = fs.createWriteStream(archivePath + ".tar.gz")
const compress = zlib.createGzip()
compress.pipe(write)
compress.on("error", (error) => compress.destroy(error))
compress.on("close", () => write.end())
tar.pack(archivePath).pipe(compress)
write.on("close", reject)
write.on("finish", () => {
resolve()
})
})
}) })
after(() => { after(() => {
@ -185,53 +152,15 @@ describe("update", () => {
assert.equal(p.isLatestVersion(update), true) assert.equal(p.isLatestVersion(update), true)
}) })
it("should download and apply an update", async () => {
version = "9999999.99999.9999"
const p = provider()
const update = await p.getUpdate(true)
// Create an existing version.
const destination = path.join(tmpdir, "tests/updates/code-server")
await fs.mkdirp(destination)
const entry = path.join(destination, "code-server")
await fs.writeFile(entry, `console.log("OLD")`)
assert.equal(`console.log("OLD")`, await fs.readFile(entry, "utf8"))
// Updating should replace the existing version.
await p.downloadAndApplyUpdate(update, destination)
assert.equal(`console.log("UPDATED")`, await fs.readFile(entry, "utf8"))
// There should be a backup.
const dir = (await fs.readdir(path.join(tmpdir, "tests/updates"))).filter((dir) => {
return dir.startsWith("code-server.")
})
assert.equal(dir.length, 1)
assert.equal(
`console.log("OLD")`,
await fs.readFile(path.join(tmpdir, "tests/updates", dir[0], "code-server"), "utf8"),
)
const archiveName = await p.getReleaseName(update)
assert.deepEqual(spy, ["/latest", `/download/${version}/${archiveName}`])
})
it("should not reject if unable to fetch", async () => { it("should not reject if unable to fetch", async () => {
const options = { const options = {
auth: AuthType.None, auth: AuthType.None,
base: "/update",
commit: "test", commit: "test",
} }
let provider = new UpdateHttpProvider(options, true, "invalid", "invalid", settings) let provider = new UpdateHttpProvider(options, true, "invalid", settings)
await assert.doesNotReject(() => provider.getUpdate(true)) await assert.doesNotReject(() => provider.getUpdate(true))
provider = new UpdateHttpProvider( provider = new UpdateHttpProvider(options, true, "http://probably.invalid.dev.localhost/latest", settings)
options,
true,
"http://probably.invalid.dev.localhost/latest",
"http://probably.invalid.dev.localhost/download",
settings,
)
await assert.doesNotReject(() => provider.getUpdate(true)) await assert.doesNotReject(() => provider.getUpdate(true))
}) })
}) })

View File

@ -792,10 +792,10 @@
lodash "^4.17.13" lodash "^4.17.13"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@coder/logger@1.1.11": "@coder/logger@1.1.16":
version "1.1.11" version "1.1.16"
resolved "https://registry.yarnpkg.com/@coder/logger/-/logger-1.1.11.tgz#e6f36dba9436ae61e66e3f66787d75c768617605" resolved "https://registry.yarnpkg.com/@coder/logger/-/logger-1.1.16.tgz#ee5b1b188f680733f35c11b065bbd139d618c1e1"
integrity sha512-EEh1dqSU0AaqjjjMsVqumgZGbrZimKFKIb4t5E6o3FLfVUxJCReSME78Yj2N1xWUVAHMnqafDCxLostpuIotzw== integrity sha512-X6VB1++IkosYY6amRAiMvuvCf12NA4+ooX+gOuu5bJIkdjmh4Lz7QpJcWRdgxesvo1msriDDr9E/sDbIWf6vsQ==
"@iarna/toml@^2.2.0": "@iarna/toml@^2.2.0":
version "2.2.5" version "2.2.5"
@ -6144,6 +6144,11 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^3.0.0" hash-base "^3.0.0"
inherits "^2.0.1" inherits "^2.0.1"
rotating-file-stream@^2.1.1:
version "2.1.3"
resolved "https://registry.yarnpkg.com/rotating-file-stream/-/rotating-file-stream-2.1.3.tgz#4b3cc8f56ae70b3e30ccdb4ee6b14d95e66b02bb"
integrity sha512-zZ4Tkngxispo7DgiTqX0s4ChLtM3qET6iYsDA9tmgDEqJ3BFgRq/ZotsKEDAYQt9pAn9JwwqT27CSwQt3CTxNg==
run-async@^2.4.0: run-async@^2.4.0:
version "2.4.1" version "2.4.1"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"