Compare commits

...

1 Commits

Author SHA1 Message Date
Teffen Ellis ad08948664 Update code-server to match. 2021-09-16 15:04:50 -04:00
37 changed files with 363 additions and 976 deletions

View File

@ -3,4 +3,9 @@ root = true
[*]
indent_style = space
trim_trailing_whitespace = true
# The indent size used in the `package.json` file cannot be changed
# https://github.com/npm/npm/pull/3180#issuecomment-16336516
[{*.yml,*.yaml,package.json}]
indent_style = space
indent_size = 2

View File

@ -2,3 +2,16 @@ printWidth: 120
semi: false
trailingComma: all
arrowParens: always
singleQuote: false
useTabs: false
overrides:
# Attempt to keep VScode's existing code style intact.
- files: "lib/vscode/**/*.ts"
options:
# No limit defined upstream.
printWidth: 10000
semi: true
singleQuote: true
useTabs: true
arrowParens: avoid

View File

@ -10,7 +10,7 @@ main() {
cd "$(dirname "${0}")/../.."
cd lib/vscode
yarn gulp compile-build compile-extensions-build
yarn gulp compile-build compile-web compile-extensions-build
yarn gulp optimize --gulpfile ./coder.js
if [[ $MINIFY ]]; then
yarn gulp minify --gulpfile ./coder.js

View File

@ -8,7 +8,7 @@ async function main(): Promise<void> {
try {
const watcher = new Watcher()
await watcher.watch()
} catch (error) {
} catch (error: any) {
console.error(error.message)
process.exit(1)
}
@ -38,6 +38,9 @@ class Watcher {
}
const vscode = cp.spawn("yarn", ["watch"], { cwd: this.vscodeSourcePath })
const vscodeWebExtensions = cp.spawn("yarn", ["watch-web"], { cwd: this.vscodeSourcePath })
const tsc = cp.spawn("tsc", ["--watch", "--pretty", "--preserveWatchOutput"], { cwd: this.rootPath })
const plugin = process.env.PLUGIN_DIR
? cp.spawn("yarn", ["build", "--watch"], { cwd: process.env.PLUGIN_DIR })
@ -48,6 +51,10 @@ class Watcher {
vscode.removeAllListeners()
vscode.kill()
Watcher.log("killing vs code web extension watcher")
vscodeWebExtensions.removeAllListeners()
vscodeWebExtensions.kill()
Watcher.log("killing tsc")
tsc.removeAllListeners()
tsc.kill()
@ -75,10 +82,17 @@ class Watcher {
Watcher.log("vs code watcher terminated unexpectedly")
cleanup(code)
})
vscodeWebExtensions.on("exit", (code) => {
Watcher.log("vs code extension watcher terminated unexpectedly")
cleanup(code)
})
tsc.on("exit", (code) => {
Watcher.log("tsc terminated unexpectedly")
cleanup(code)
})
if (plugin) {
plugin.on("exit", (code) => {
Watcher.log("plugin terminated unexpectedly")
@ -86,8 +100,10 @@ class Watcher {
})
}
vscodeWebExtensions.stderr.on("data", (d) => process.stderr.write(d))
vscode.stderr.on("data", (d) => process.stderr.write(d))
tsc.stderr.on("data", (d) => process.stderr.write(d))
if (plugin) {
plugin.stderr.on("data", (d) => process.stderr.write(d))
}

View File

@ -37,7 +37,6 @@
"main": "out/node/entry.js",
"devDependencies": {
"@schemastore/package": "^0.0.6",
"@types/body-parser": "^1.19.0",
"@types/browserify": "^12.0.36",
"@types/compression": "^1.7.0",
"@types/cookie-parser": "^1.4.2",
@ -50,8 +49,7 @@
"@types/safe-compare": "^1.1.0",
"@types/semver": "^7.1.0",
"@types/split2": "^3.2.0",
"@types/tar-fs": "^2.0.0",
"@types/tar-stream": "^2.1.0",
"@types/trusted-types": "^2.0.2",
"@types/ws": "^7.2.6",
"@typescript-eslint/eslint-plugin": "^4.7.0",
"@typescript-eslint/parser": "^4.7.0",
@ -70,7 +68,7 @@
"stylelint": "^13.0.0",
"stylelint-config-recommended": "^5.0.0",
"ts-node": "^10.0.0",
"typescript": "^4.1.3"
"typescript": "^4.4.0-dev.20210528"
},
"resolutions": {
"normalize-package-data": "^3.0.0",
@ -86,7 +84,6 @@
"dependencies": {
"@coder/logger": "1.1.16",
"argon2": "^0.28.0",
"body-parser": "^1.19.0",
"compression": "^1.7.4",
"cookie-parser": "^1.4.5",
"env-paths": "^2.2.0",
@ -104,7 +101,6 @@
"safe-compare": "^1.1.4",
"semver": "^7.1.3",
"split2": "^3.2.2",
"tar-fs": "^2.0.0",
"ws": "^8.0.0",
"xdg-basedir": "^4.0.0",
"yarn": "^1.22.4"

View File

@ -30,7 +30,7 @@
<div class="content">
<form class="login-form" method="post">
<input class="user" type="text" autocomplete="username" />
<input id="base" type="hidden" name="base" value="/" />
<input id="base" type="hidden" name="base" value="{{BASE}}" />
<div class="field">
<input
required

View File

@ -1,8 +1 @@
import { getOptions } from "../../common/util"
import "../register"
const options = getOptions()
const el = document.getElementById("base") as HTMLInputElement
if (el) {
el.value = options.base
}

View File

@ -17,11 +17,6 @@
<!-- 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}}" />
<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="{{CS_STATIC_BASE}}/src/browser/media/favicon-dark-support.svg" />
<link rel="alternate icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" />

View File

@ -1,24 +1,9 @@
import { getOptions, Options } from "../../common/util"
import { getClientConfiguration } from "../../common/util"
import { WORKBENCH_WEB_CONFIG_ID } from "../../node/constants"
import "../register"
// TODO@jsjoeio: Add proper types.
type FixMeLater = any
// NOTE@jsjoeio
// This lives here ../../../lib/vscode/src/vs/base/common/platform.ts#L106
export const nlsConfigElementId = "vscode-remote-nls-configuration"
type NlsConfiguration = {
locale: string
availableLanguages: { [key: string]: string } | {}
_languagePackId?: string
_translationsConfigFile?: string
_cacheRoot?: string
_resolvedLanguagePackCoreLocation?: string
_corruptedFile?: string
_languagePackSupport?: boolean
loadBundle?: FixMeLater
}
type NLSConfigurationWeb = CodeServerLib.NLSConfigurationWeb
type ClientConfiguration = CodeServerLib.ClientConfiguration
/**
* Helper function to create the path to the bundle
@ -41,26 +26,29 @@ export function createBundlePath(_resolvedLanguagePackCoreLocation: string, bund
*
* Make sure to wrap this in a try/catch block when you call it.
**/
export function getNlsConfiguration(_document: Document, base: string) {
export function getNlsConfiguration<T extends NLSConfigurationWeb = NLSConfigurationWeb>(
_document: Document,
base: string,
): T {
const errorMsgPrefix = "[vscode]"
const nlsConfigElement = _document?.getElementById(nlsConfigElementId)
const dataSettings = nlsConfigElement?.getAttribute("data-settings")
if (!nlsConfigElement) {
throw new Error(
`${errorMsgPrefix} Could not parse NLS configuration. Could not find nlsConfigElement with id: ${nlsConfigElementId}`,
)
const workbenchElement = _document.getElementById(WORKBENCH_WEB_CONFIG_ID)
if (!workbenchElement) {
throw new Error(`${errorMsgPrefix} Could not find Workbench element`)
}
if (!dataSettings) {
throw new Error(
`${errorMsgPrefix} Could not parse NLS configuration. Found nlsConfigElement but missing data-settings attribute.`,
)
const rawWorkbenchConfig = workbenchElement.getAttribute("data-settings")
if (!rawWorkbenchConfig) {
throw new Error(`${errorMsgPrefix} Could not find Workbench data`)
}
const nlsConfig = JSON.parse(dataSettings) as NlsConfiguration
const nlsConfiguration: T = JSON.parse(rawWorkbenchConfig).nlsConfiguration
if (nlsConfig._resolvedLanguagePackCoreLocation) {
if (!nlsConfiguration) {
throw new Error(`${errorMsgPrefix} Could not parse NLS config from Workbench configuration`)
}
if ("_resolvedLanguagePackCoreLocation" in nlsConfiguration) {
// NOTE@jsjoeio
// Not sure why we use Object.create(null) instead of {}
// They are not the same
@ -70,15 +58,13 @@ export function getNlsConfiguration(_document: Document, base: string) {
[key: string]: string
} = Object.create(null)
type LoadBundleCallback = (_: undefined, result?: string) => void
nlsConfig.loadBundle = (bundle: string, _language: string, cb: LoadBundleCallback): void => {
nlsConfiguration.loadBundle = (bundle, _language, cb): void => {
const result = bundles[bundle]
if (result) {
return cb(undefined, result)
}
// FIXME: Only works if path separators are /.
const path = createBundlePath(nlsConfig._resolvedLanguagePackCoreLocation || "", bundle)
const path = createBundlePath(nlsConfiguration._resolvedLanguagePackCoreLocation || "", bundle)
fetch(`${base}/vscode/resource/?path=${encodeURIComponent(path)}`)
.then((response) => response.json())
.then((json) => {
@ -89,12 +75,12 @@ export function getNlsConfiguration(_document: Document, base: string) {
}
}
return nlsConfig
return nlsConfiguration
}
type GetLoaderParams = {
nlsConfig: NlsConfiguration
options: Options
nlsConfiguration: NLSConfigurationWeb
options: ClientConfiguration
_window: Window
}
@ -105,12 +91,11 @@ type GetLoaderParams = {
type Loader = {
baseUrl: string
recordStats: boolean
// TODO@jsjoeio: There don't appear to be any types for trustedTypes yet.
trustedTypesPolicy: FixMeLater
trustedTypesPolicy: TrustedTypePolicy | undefined
paths: {
[key: string]: string
}
"vs/nls": NlsConfiguration
"vs/nls": NLSConfigurationWeb
}
/**
@ -135,16 +120,18 @@ export function _createScriptURL(value: string, origin: string): string {
* We extracted the logic into a function so that
* it's easier to test.
**/
export function getConfigurationForLoader({ nlsConfig, options, _window }: GetLoaderParams) {
export function getConfigurationForLoader({ nlsConfiguration, options, _window }: GetLoaderParams) {
const trustedPolicyOptions: TrustedTypePolicyOptions = {
createScriptURL(value: string): string {
return _createScriptURL(value, window.location.origin)
},
}
const loader: Loader = {
// Without the full URL VS Code will try to load file://.
baseUrl: `${window.location.origin}${options.csStaticBase}/lib/vscode/out`,
recordStats: true,
trustedTypesPolicy: (_window as FixMeLater).trustedTypes?.createPolicy("amdLoader", {
createScriptURL(value: string): string {
return _createScriptURL(value, window.location.origin)
},
}),
trustedTypesPolicy: _window.trustedTypes?.createPolicy("amdLoader", trustedPolicyOptions),
paths: {
"vscode-textmate": `../node_modules/vscode-textmate/release/main`,
"vscode-oniguruma": `../node_modules/vscode-oniguruma/release/main`,
@ -156,7 +143,7 @@ export function getConfigurationForLoader({ nlsConfig, options, _window }: GetLo
"iconv-lite-umd": `../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js`,
jschardet: `../node_modules/jschardet/dist/jschardet.min.js`,
},
"vs/nls": nlsConfig,
"vs/nls": nlsConfiguration,
}
return loader
@ -229,11 +216,11 @@ export function main(_document: Document | undefined, _window: Window | undefine
throw new Error(`localStorage is undefined.`)
}
const options = getOptions()
const options = getClientConfiguration()
const nlsConfig = getNlsConfiguration(_document, options.base)
const loader = getConfigurationForLoader({
nlsConfig,
nlsConfiguration: nlsConfig,
options,
_window,
})

View File

@ -1,9 +1,8 @@
import { logger } from "@coder/logger"
import { getOptions, normalize, logError } from "../common/util"
import { getClientConfiguration, normalize, logError } from "../common/util"
export async function registerServiceWorker(): Promise<void> {
const options = getOptions()
logger.level = options.logLevel
const options = getClientConfiguration()
const path = normalize(`${options.csStaticBase}/out/browser/serviceWorker.js`)
try {
@ -11,8 +10,8 @@ export async function registerServiceWorker(): Promise<void> {
scope: options.base + "/",
})
logger.info(`[Service Worker] registered`)
} catch (error) {
logError(logger, `[Service Worker] registration`, error)
} catch (error: unknown) {
logError(logger, `[Service Worker] registration`, error as Error)
}
}

View File

@ -46,7 +46,7 @@ export class Emitter<T> {
this.listeners.map(async (cb) => {
try {
await cb(value, promise)
} catch (error) {
} catch (error: any) {
logger.error(error.message)
}
}),

View File

@ -1,19 +1,3 @@
/*
* This file exists in two locations:
* - src/common/util.ts
* - lib/vscode/src/vs/server/common/util.ts
* The second is a symlink to the first.
*/
/**
* Base options included on every page.
*/
export interface Options {
base: string
csStaticBase: string
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.
@ -67,14 +51,14 @@ export const resolveBase = (base?: string): string => {
}
/**
* Get options embedded in the HTML or query params.
* Get client-side configuration embedded in the HTML or query params.
*/
export const getOptions = <T extends Options>(): T => {
let options: T
export const getClientConfiguration = <T extends CodeServerLib.ClientConfiguration>(): T => {
let config: T
try {
options = JSON.parse(document.getElementById("coder-options")!.getAttribute("data-settings")!)
config = JSON.parse(document.getElementById("coder-options")!.getAttribute("data-settings")!)
} catch (error) {
options = {} as T
config = {} as T
}
// You can also pass options in stringified form to the options query
@ -83,16 +67,16 @@ export const getOptions = <T extends Options>(): T => {
const params = new URLSearchParams(location.search)
const queryOpts = params.get("options")
if (queryOpts) {
options = {
...options,
config = {
...config,
...JSON.parse(queryOpts),
}
}
options.base = resolveBase(options.base)
options.csStaticBase = resolveBase(options.csStaticBase)
config.base = resolveBase(config.base)
config.csStaticBase = resolveBase(config.csStaticBase)
return options
return config
}
/**
@ -109,17 +93,6 @@ export const arrayify = <T>(value?: T | T[]): T[] => {
return [value]
}
/**
* Get the first string. If there's no string return undefined.
*/
export const getFirstString = (value: string | string[] | object | undefined): string | undefined => {
if (Array.isArray(value)) {
return value[0]
}
return typeof value === "string" ? value : undefined
}
// TODO: Might make sense to add Error handling to the logger itself.
export function logError(logger: { error: (msg: string) => void }, prefix: string, err: Error | string): void {
if (err instanceof Error) {

View File

@ -8,6 +8,36 @@ import * as util from "../common/util"
import { DefaultedArgs } from "./cli"
import { handleUpgrade } from "./wsRouter"
type ListenOptions = Pick<DefaultedArgs, "socket" | "port" | "host">
const listen = (server: http.Server, { host, port, socket }: ListenOptions) => {
return new Promise<void>(async (resolve, reject) => {
server.on("error", reject)
const onListen = () => {
// Promise resolved earlier so this is an unrelated error.
server.off("error", reject)
server.on("error", (err) => util.logError(logger, "http server error", err))
resolve()
}
if (socket) {
try {
await fs.unlink(socket)
} catch (error: any) {
if (error.code !== "ENOENT") {
logger.error(error.message)
}
}
server.listen(socket, onListen)
} else {
// [] is the correct format when using :: but Node errors with them.
server.listen(port, host.replace(/^\[|\]$/g, ""), onListen)
}
})
}
/**
* Create an Express app and an HTTP/S server to serve it.
*/
@ -26,35 +56,7 @@ export const createApp = async (args: DefaultedArgs): Promise<[Express, Express,
)
: http.createServer(app)
let resolved = false
await new Promise<void>(async (resolve2, reject) => {
const resolve = () => {
resolved = true
resolve2()
}
server.on("error", (err) => {
if (!resolved) {
reject(err)
} else {
// Promise resolved earlier so this is an unrelated error.
util.logError(logger, "http server error", err)
}
})
if (args.socket) {
try {
await fs.unlink(args.socket)
} catch (error) {
if (error.code !== "ENOENT") {
logger.error(error.message)
}
}
server.listen(args.socket, resolve)
} else {
// [] is the correct format when using :: but Node errors with them.
server.listen(args.port, args.host.replace(/^\[|\]$/g, ""), resolve)
}
})
await listen(server, args)
const wsApp = express()
handleUpgrade(wsApp, server)

View File

@ -3,12 +3,11 @@ import { promises as fs } from "fs"
import yaml from "js-yaml"
import * as os from "os"
import * as path from "path"
import { Args as VsArgs } from "../../typings/ipc"
import { canConnect, generateCertificate, generatePassword, humanPath, paths } from "./util"
export enum Feature {
/** Web socket compression. */
PermessageDeflate = "permessage-deflate",
// No current experimental features!
Placeholder = "placeholder",
}
export enum AuthType {
@ -30,7 +29,21 @@ export enum LogLevel {
export class OptionalString extends Optional<string> {}
export interface Args extends VsArgs {
export interface Args
extends Pick<
CodeServerLib.NativeParsedArgs,
| "user-data-dir"
| "enable-proposed-api"
| "extensions-dir"
| "builtin-extensions-dir"
| "extra-extensions-dir"
| "extra-builtin-extensions-dir"
| "ignore-last-opened"
| "locale"
| "log"
| "verbose"
| "_"
> {
config?: string
auth?: AuthType
password?: string
@ -56,8 +69,6 @@ export interface Args extends VsArgs {
"show-versions"?: boolean
"uninstall-extension"?: string[]
"proxy-domain"?: string[]
locale?: string
_: string[]
"reuse-window"?: boolean
"new-window"?: boolean
@ -534,7 +545,7 @@ export async function readConfigFile(configPath?: string): Promise<ConfigArgs> {
flag: "wx", // wx means to fail if the path exists.
})
logger.info(`Wrote default config file to ${humanPath(configPath)}`)
} catch (error) {
} catch (error: any) {
// EEXIST is fine; we don't want to overwrite existing configurations.
if (error.code !== "EEXIST") {
throw error
@ -643,7 +654,7 @@ export const shouldOpenInExistingInstance = async (args: Args): Promise<string |
const readSocketPath = async (): Promise<string | undefined> => {
try {
return await fs.readFile(path.join(os.tmpdir(), "vscode-ipc"), "utf8")
} catch (error) {
} catch (error: any) {
if (error.code !== "ENOENT") {
throw error
}

View File

@ -3,11 +3,13 @@ import { JSONSchemaForNPMPackageJsonFiles } from "@schemastore/package"
import * as os from "os"
import * as path from "path"
export const WORKBENCH_WEB_CONFIG_ID = "vscode-workbench-web-configuration"
export function getPackageJson(relativePath: string): JSONSchemaForNPMPackageJsonFiles {
let pkg = {}
try {
pkg = require(relativePath)
} catch (error) {
} catch (error: any) {
logger.warn(error.message)
}
@ -19,5 +21,6 @@ const pkg = getPackageJson("../../package.json")
export const version = pkg.version || "development"
export const commit = pkg.commit || "development"
export const rootPath = path.resolve(__dirname, "../..")
export const vsRootPath = path.join(rootPath, "lib/vscode")
export const tmpdir = path.join(os.tmpdir(), "code-server")
export const isDevMode = commit === "development"

View File

@ -3,11 +3,11 @@ import * as express from "express"
import * as expressCore from "express-serve-static-core"
import qs from "qs"
import { HttpCode, HttpError } from "../common/http"
import { normalize, Options } from "../common/util"
import { normalize } from "../common/util"
import { AuthType, DefaultedArgs } from "./cli"
import { commit, rootPath } from "./constants"
import { commit, rootPath, version as codeServerVersion } from "./constants"
import { Heart } from "./heart"
import { getPasswordMethod, IsCookieValidArgs, isCookieValid, sanitizeString, escapeHtml } from "./util"
import { getPasswordMethod, IsCookieValidArgs, isCookieValid, sanitizeString, escapeHtml, escapeJSON } from "./util"
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
@ -19,6 +19,16 @@ declare global {
}
}
export const createClientConfiguration = (req: express.Request): CodeServerLib.ClientConfiguration => {
const base = relativeRoot(req)
return {
base,
csStaticBase: base + "/static/" + commit + rootPath,
codeServerVersion,
}
}
/**
* Replace common variable strings in HTML templates.
*/
@ -27,18 +37,16 @@ export const replaceTemplates = <T extends object>(
content: string,
extraOpts?: Omit<T, "base" | "csStaticBase" | "logLevel">,
): string => {
const base = relativeRoot(req)
const options: Options = {
base,
csStaticBase: base + "/static/" + commit + rootPath,
logLevel: logger.level,
const serverOptions: CodeServerLib.ClientConfiguration = {
...createClientConfiguration(req),
...extraOpts,
}
return content
.replace(/{{TO}}/g, (typeof req.query.to === "string" && escapeHtml(req.query.to)) || "/")
.replace(/{{BASE}}/g, options.base)
.replace(/{{CS_STATIC_BASE}}/g, options.csStaticBase)
.replace(/"{{OPTIONS}}"/, `'${JSON.stringify(options)}'`)
.replace(/{{BASE}}/g, serverOptions.base)
.replace(/{{CS_STATIC_BASE}}/g, serverOptions.csStaticBase)
.replace("{{OPTIONS}}", () => escapeJSON(serverOptions))
}
/**

View File

@ -1,49 +1,41 @@
import { field, logger } from "@coder/logger"
import * as cp from "child_process"
import http from "http"
import * as path from "path"
import { CliMessage, OpenCommandPipeArgs } from "../../typings/ipc"
import path from "path"
import { plural } from "../common/util"
import { createApp, ensureAddress } from "./app"
import { AuthType, DefaultedArgs, Feature } from "./cli"
import { coderCloudBind } from "./coder_cloud"
import { commit, version } from "./constants"
import { register } from "./routes"
import { humanPath, isFile, open } from "./util"
import { humanPath, isFile, loadAMDModule, open } from "./util"
export const runVsCodeCli = (args: DefaultedArgs): void => {
logger.debug("forking vs code cli...")
const vscode = cp.fork(path.resolve(__dirname, "../../lib/vscode/out/vs/server/fork"), [], {
env: {
...process.env,
CODE_SERVER_PARENT_PID: process.pid.toString(),
},
})
vscode.once("message", (message: any) => {
logger.debug("got message from VS Code", field("message", message))
if (message.type !== "ready") {
logger.error("Unexpected response waiting for ready response", field("type", message.type))
process.exit(1)
}
const send: CliMessage = { type: "cli", args }
vscode.send(send)
})
vscode.once("error", (error) => {
/**
* This is useful when an CLI arg should be passed to VS Code directly,
* such as when managing extensions.
* @deprecated This should be removed when code-server merges with lib/vscode.
*/
export const runVsCodeCli = async (args: DefaultedArgs): Promise<void> => {
logger.debug("Running VS Code CLI")
const cliProcessMain = await loadAMDModule<CodeServerLib.IMainCli["main"]>("vs/code/node/cliProcessMain", "main")
try {
await cliProcessMain(args)
} catch (error) {
logger.error("Got error from VS Code", field("error", error))
process.exit(1)
})
vscode.on("exit", (code) => process.exit(code || 0))
}
process.exit(0)
}
export const openInExistingInstance = async (args: DefaultedArgs, socketPath: string): Promise<void> => {
const pipeArgs: OpenCommandPipeArgs & { fileURIs: string[] } = {
const pipeArgs: CodeServerLib.OpenCommandPipeArgs & { fileURIs: string[] } = {
type: "open",
folderURIs: [],
fileURIs: [],
forceReuseWindow: args["reuse-window"],
forceNewWindow: args["new-window"],
}
for (let i = 0; i < args._.length; i++) {
const fp = path.resolve(args._[i])
if (await isFile(fp)) {
@ -52,17 +44,14 @@ export const openInExistingInstance = async (args: DefaultedArgs, socketPath: st
pipeArgs.folderURIs.push(fp)
}
}
if (pipeArgs.forceNewWindow && pipeArgs.fileURIs.length > 0) {
logger.error("--new-window can only be used with folder paths")
process.exit(1)
}
if (pipeArgs.folderURIs.length === 0 && pipeArgs.fileURIs.length === 0) {
logger.error("Please specify at least one file or folder")
process.exit(1)
}
const vscode = http.request(
{
path: "/",

View File

@ -172,9 +172,9 @@ export class PluginAPI {
}
await this.loadPlugin(path.join(dir, ent.name))
}
} catch (err) {
if (err.code !== "ENOENT") {
this.logger.warn(`failed to load plugins from ${q(dir)}: ${err.message}`)
} catch (error: any) {
if (error.code !== "ENOENT") {
this.logger.warn(`failed to load plugins from ${q(dir)}: ${error.message}`)
}
}
}
@ -195,9 +195,9 @@ export class PluginAPI {
}
const p = this._loadPlugin(dir, packageJSON)
this.plugins.set(p.name, p)
} catch (err) {
if (err.code !== "ENOENT") {
this.logger.warn(`failed to load plugin: ${err.stack}`)
} catch (error: any) {
if (error.code !== "ENOENT") {
this.logger.warn(`failed to load plugin: ${error.stack}`)
}
}
}
@ -278,7 +278,7 @@ export class PluginAPI {
}
try {
await p.deinit()
} catch (error) {
} catch (error: any) {
this.logger.error("plugin failed to deinit", field("name", p.name), field("error", error.message))
}
}),

View File

@ -1,5 +1,4 @@
import { logger } from "@coder/logger"
import bodyParser from "body-parser"
import cookieParser from "cookie-parser"
import * as express from "express"
import { promises as fs } from "fs"
@ -10,7 +9,7 @@ import * as pluginapi from "../../../typings/pluginapi"
import { HttpCode, HttpError } from "../../common/http"
import { plural } from "../../common/util"
import { AuthType, DefaultedArgs } from "../cli"
import { rootPath } from "../constants"
import { isDevMode, rootPath } from "../constants"
import { Heart } from "../heart"
import { ensureAuthenticated, redirect, replaceTemplates } from "../http"
import { PluginAPI } from "../plugin"
@ -25,7 +24,7 @@ import * as pathProxy from "./pathProxy"
// static is a reserved keyword.
import * as _static from "./static"
import * as update from "./update"
import * as vscode from "./vscode"
import { createVSServerRouter } from "./vscode"
/**
* Register all routes and middleware.
@ -124,13 +123,10 @@ export const register = async (
wrapper.onDispose(() => pluginApi.dispose())
}
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.use("/", vscode.router)
wsApp.use("/", vscode.wsRouter.router)
app.use("/vscode", vscode.router)
wsApp.use("/vscode", vscode.wsRouter.router)
app.use("/static", _static.router)
app.use("/healthz", health.router)
wsApp.use("/healthz", health.wsRouter.router)
@ -143,9 +139,22 @@ export const register = async (
app.all("/logout", (req, res) => redirect(req, res, "/", {}))
}
app.use("/static", _static.router)
app.use("/update", update.router)
try {
const vscode = await createVSServerRouter(args)
app.use("/", vscode.router)
wsApp.use("/", vscode.wsRouter.router)
app.use("/vscode", vscode.router)
wsApp.use("/vscode", vscode.wsRouter.router)
} catch (error: any) {
if (isDevMode) {
logger.warn("VS Server router may still be compiling.")
}
throw error
}
app.use(() => {
throw new HttpError("Not Found", HttpCode.NotFound)
})

View File

@ -111,7 +111,7 @@ router.post("/", async (req, res) => {
)
throw new Error("Incorrect password")
} catch (error) {
} catch (error: any) {
const renderedHtml = await getRoot(req, error)
res.send(renderedHtml)
}

View File

@ -1,45 +1,22 @@
import { field, logger } from "@coder/logger"
import { Router } from "express"
import { promises as fs } from "fs"
import * as path from "path"
import { Readable } from "stream"
import * as tarFs from "tar-fs"
import * as zlib from "zlib"
import { HttpCode, HttpError } from "../../common/http"
import { getFirstString } from "../../common/util"
import { rootPath } from "../constants"
import { authenticated, ensureAuthenticated, replaceTemplates } from "../http"
import { getMediaMime, pathToFsPath } from "../util"
import { authenticated } from "../http"
import { getMediaMime } from "../util"
export const router = Router()
// The commit is for caching.
router.get("/(:commit)(/*)?", async (req, res) => {
// Used by VS Code to load extensions into the web worker.
const tar = getFirstString(req.query.tar)
if (tar) {
await ensureAuthenticated(req)
let stream: Readable = tarFs.pack(pathToFsPath(tar))
if (req.headers["accept-encoding"] && req.headers["accept-encoding"].includes("gzip")) {
logger.debug("gzipping tar", field("path", tar))
const compress = zlib.createGzip()
stream.pipe(compress)
stream.on("error", (error) => compress.destroy(error))
stream.on("close", () => compress.end())
stream = compress
res.header("content-encoding", "gzip")
}
res.set("Content-Type", "application/x-tar")
stream.on("close", () => res.end())
return stream.pipe(res)
}
// If not a tar use the remainder of the path to load the resource.
if (!req.params[0]) {
throw new HttpError("Not Found", HttpCode.NotFound)
}
router.get("/(/*)?", async (req, res) => {
const resourcePath = path.resolve(req.params[0])
console.log(">>>", resourcePath)
// console.log(">>", resourcePath)
// if (req.params.commit.startsWith("out") || req.params.commit.startsWith("remote")) {
// resourcePath = path.join(vsRootPath, req.params.commit, resourcePath)
// // console.log("rewrite >>", resourcePath)
// }
// Make sure it's in code-server if you aren't authenticated. This lets
// unauthenticated users load the login assets.
@ -48,11 +25,11 @@ router.get("/(:commit)(/*)?", async (req, res) => {
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
}
// Don't cache during development. - can also be used if you want to make a
// static request without caching.
if (req.params.commit !== "development" && req.params.commit !== "-") {
res.header("Cache-Control", "public, max-age=31536000")
}
// // Don't cache during development. - can also be used if you want to make a
// // static request without caching.
// if (commit !== "development" && req.params.commit !== "-") {
// res.header("Cache-Control", "public, max-age=31536000")
// }
// Without this the default is to use the directory the script loaded from.
if (req.headers["service-worker"]) {
@ -61,11 +38,7 @@ router.get("/(:commit)(/*)?", async (req, res) => {
res.set("Content-Type", getMediaMime(resourcePath))
if (resourcePath.endsWith("manifest.json")) {
const content = await fs.readFile(resourcePath, "utf8")
return res.send(replaceTemplates(req, content))
}
const content = await fs.readFile(resourcePath)
return res.send(content)
})

View File

@ -1,232 +1,34 @@
import * as crypto from "crypto"
import { Request, Router } from "express"
import { promises as fs } from "fs"
import * as path from "path"
import qs from "qs"
import * as ipc from "../../../typings/ipc"
import { Emitter } from "../../common/emitter"
import { HttpCode, HttpError } from "../../common/http"
import { getFirstString } from "../../common/util"
import { Feature } from "../cli"
import { isDevMode, rootPath, version } from "../constants"
import { authenticated, ensureAuthenticated, redirect, replaceTemplates } from "../http"
import { getMediaMime, pathToFsPath } from "../util"
import { VscodeProvider } from "../vscode"
import * as express from "express"
import { AuthType, DefaultedArgs } from "../cli"
import { version as codeServerVersion } from "../constants"
import { ensureAuthenticated } from "../http"
import { loadAMDModule } from "../util"
import { Router as WsRouter } from "../wsRouter"
export const router = Router()
export const createVSServerRouter = async (args: DefaultedArgs) => {
const vscodeServerMain = await loadAMDModule<CodeServerLib.CreateVSServer>("vs/server/entry", "createVSServer")
const vscode = new VscodeProvider()
router.get("/", async (req, res) => {
const isAuthenticated = await authenticated(req)
if (!isAuthenticated) {
return redirect(req, res, "login", {
// req.baseUrl can be blank if already at the root.
to: req.baseUrl && req.baseUrl !== "/" ? req.baseUrl : undefined,
})
}
const [content, options] = await Promise.all([
await fs.readFile(path.join(rootPath, "src/browser/pages/vscode.html"), "utf8"),
(async () => {
try {
return await vscode.initialize({ args: req.args, remoteAuthority: req.headers.host || "" }, req.query)
} catch (error) {
const devMessage = isDevMode ? "It might not have finished compiling." : ""
throw new Error(`VS Code failed to load. ${devMessage} ${error.message}`)
}
})(),
])
options.productConfiguration.codeServerVersion = version
res.send(
replaceTemplates<ipc.Options>(
req,
// Uncomment prod blocks if not in development. TODO: Would this be
// better as a build step? Or maintain two HTML files again?
!isDevMode ? content.replace(/<!-- PROD_ONLY/g, "").replace(/END_PROD_ONLY -->/g, "") : content,
{
authed: req.args.auth !== "none",
disableUpdateCheck: !!req.args["disable-update-check"],
},
)
.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)}'`),
)
})
/**
* TODO: Might currently be unused.
*/
router.get("/resource(/*)?", ensureAuthenticated, async (req, res) => {
const path = getFirstString(req.query.path)
if (path) {
res.set("Content-Type", getMediaMime(path))
res.send(await fs.readFile(pathToFsPath(path)))
}
})
/**
* Used by VS Code to load files.
*/
router.get("/vscode-remote-resource(/*)?", ensureAuthenticated, async (req, res) => {
const path = getFirstString(req.query.path)
if (path) {
res.set("Content-Type", getMediaMime(path))
res.send(await fs.readFile(pathToFsPath(path)))
}
})
/**
* VS Code webviews use these paths to load files and to load webview assets
* like HTML and JavaScript.
*/
router.get("/webview/*", ensureAuthenticated, async (req, res) => {
res.set("Content-Type", getMediaMime(req.path))
if (/^vscode-resource/.test(req.params[0])) {
return res.send(await fs.readFile(req.params[0].replace(/^vscode-resource(\/file)?/, "")))
}
return res.send(
await fs.readFile(path.join(vscode.vsRootPath, "out/vs/workbench/contrib/webview/browser/pre", req.params[0])),
)
})
interface Callback {
uri: {
scheme: string
authority?: string
path?: string
query?: string
fragment?: string
}
timeout: NodeJS.Timeout
}
const callbacks = new Map<string, Callback>()
const callbackEmitter = new Emitter<{ id: string; callback: Callback }>()
/**
* Get vscode-requestId from the query and throw if it's missing or invalid.
*/
const getRequestId = (req: Request): string => {
if (!req.query["vscode-requestId"]) {
throw new HttpError("vscode-requestId is missing", HttpCode.BadRequest)
}
if (typeof req.query["vscode-requestId"] !== "string") {
throw new HttpError("vscode-requestId is not a string", HttpCode.BadRequest)
}
return req.query["vscode-requestId"]
}
// Matches VS Code's fetch timeout.
const fetchTimeout = 5 * 60 * 1000
// The callback endpoints are used during authentication. A URI is stored on
// /callback and then fetched later on /fetch-callback.
// See ../../../lib/vscode/resources/web/code-web.js
router.get("/callback", ensureAuthenticated, async (req, res) => {
const uriKeys = [
"vscode-requestId",
"vscode-scheme",
"vscode-authority",
"vscode-path",
"vscode-query",
"vscode-fragment",
]
const id = getRequestId(req)
// Move any query variables that aren't URI keys into the URI's query
// (importantly, this will include the code for oauth).
const query: qs.ParsedQs = {}
for (const key in req.query) {
if (!uriKeys.includes(key)) {
query[key] = req.query[key]
}
}
const callback = {
uri: {
scheme: getFirstString(req.query["vscode-scheme"]) || "code-oss",
authority: getFirstString(req.query["vscode-authority"]),
path: getFirstString(req.query["vscode-path"]),
query: (getFirstString(req.query.query) || "") + "&" + qs.stringify(query),
fragment: getFirstString(req.query["vscode-fragment"]),
},
// Make sure the map doesn't leak if nothing fetches this URI.
timeout: setTimeout(() => callbacks.delete(id), fetchTimeout),
}
callbacks.set(id, callback)
callbackEmitter.emit({ id, callback })
res.sendFile(path.join(rootPath, "lib/vscode/resources/web/callback.html"))
})
router.get("/fetch-callback", ensureAuthenticated, async (req, res) => {
const id = getRequestId(req)
const send = (callback: Callback) => {
clearTimeout(callback.timeout)
callbacks.delete(id)
res.json(callback.uri)
}
const callback = callbacks.get(id)
if (callback) {
return send(callback)
}
// VS Code will try again if the route returns no content but it seems more
// efficient to just wait on this request for as long as possible?
const handler = callbackEmitter.event(({ id: emitId, callback }) => {
if (id === emitId) {
handler.dispose()
send(callback)
}
const serverUrl = new URL(`${args.cert ? "https" : "http"}://${args.host}:${args.port}`)
const vscodeServer = await vscodeServerMain({
codeServerVersion,
serverUrl,
args,
authed: args.auth !== AuthType.None,
disableUpdateCheck: !!args["disable-update-check"],
})
// If the client closes the connection.
req.on("close", () => handler.dispose())
})
const router = express.Router()
const wsRouter = WsRouter()
export const wsRouter = WsRouter()
router.all("*", ensureAuthenticated, (req, res) => {
vscodeServer.emit("request", req, res)
})
wsRouter.ws("/", ensureAuthenticated, async (req) => {
const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
const reply = crypto
.createHash("sha1")
.update(req.headers["sec-websocket-key"] + magic)
.digest("base64")
wsRouter.ws("/", ensureAuthenticated, (req) => {
vscodeServer.emit("upgrade", req, req.socket, req.head)
const responseHeaders = [
"HTTP/1.1 101 Switching Protocols",
"Upgrade: websocket",
"Connection: Upgrade",
`Sec-WebSocket-Accept: ${reply}`,
]
req.socket.resume()
})
// See if the browser reports it supports web socket compression.
// TODO: Parse this header properly.
const extensions = req.headers["sec-websocket-extensions"]
const isCompressionSupported = extensions ? extensions.includes("permessage-deflate") : false
// TODO: For now we only use compression if the user enables it.
const isCompressionEnabled = !!req.args.enable?.includes(Feature.PermessageDeflate)
const useCompression = isCompressionEnabled && isCompressionSupported
if (useCompression) {
// This response header tells the browser the server supports compression.
responseHeaders.push("Sec-WebSocket-Extensions: permessage-deflate; server_max_window_bits=15")
}
req.ws.write(responseHeaders.join("\r\n") + "\r\n\r\n")
await vscode.sendWebsocket(req.ws, req.query, useCompression)
})
return { router, wsRouter }
}

View File

@ -20,7 +20,7 @@ export class SettingsProvider<T> {
try {
const raw = (await fs.readFile(this.settingsPath, "utf8")).trim()
return raw ? JSON.parse(raw) : {}
} catch (error) {
} catch (error: any) {
if (error.code !== "ENOENT") {
logger.warn(error.message)
}
@ -37,7 +37,7 @@ export class SettingsProvider<T> {
const oldSettings = await this.read()
const nextSettings = { ...oldSettings, ...settings }
await fs.writeFile(this.settingsPath, JSON.stringify(nextSettings, null, 2))
} catch (error) {
} catch (error: any) {
logger.warn(error.message)
}
}

View File

@ -60,7 +60,7 @@ export class UpdateProvider {
}
logger.debug("got latest version", field("latest", update.version))
return update
} catch (error) {
} catch (error: any) {
logger.error("Failed to get latest version", field("error", error.message))
return {
checked: now,

View File

@ -1,66 +0,0 @@
// In a bit of a hack, this file is stored in two places
// - src/node/uri_transformer.ts
// - lib/vscode/src/vs/server/uriTransformer.ts
// The reason for this is that we need a CommonJS-compiled
// version of this file to supply as a command line argument
// to extensionHostProcessSetup.ts; but we also need to include
// it ourselves cleanly in `lib/vscode/src/vs/server`.
// @oxy: Could not figure out how to compile as a CommonJS module
// in the same tree as VSCode, which is why I came up with the solution
// of storing it in two places.
// NOTE: copied over from lib/vscode/src/vs/common/uriIpc.ts
// remember to update this for proper type checks!
interface UriParts {
scheme: string
authority?: string
path?: string
}
interface IRawURITransformer {
transformIncoming(uri: UriParts): UriParts
transformOutgoing(uri: UriParts): UriParts
transformOutgoingScheme(scheme: string): string
}
// Using `export =` is deliberate.
// See lib/vscode/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts;
// they include the file directly with a node require and expect a function as `module.exports`.
// `export =` in TypeScript is equivalent to `module.exports =` in vanilla JS.
export = function rawURITransformerFactory(authority: string) {
return new RawURITransformer(authority)
}
class RawURITransformer implements IRawURITransformer {
constructor(private readonly authority: string) {}
transformIncoming(uri: UriParts): UriParts {
switch (uri.scheme) {
case "vscode-remote":
return { scheme: "file", path: uri.path }
default:
return uri
}
}
transformOutgoing(uri: UriParts): UriParts {
switch (uri.scheme) {
case "file":
return { scheme: "vscode-remote", authority: this.authority, path: uri.path }
default:
return uri
}
}
transformOutgoingScheme(scheme: string): string {
switch (scheme) {
case "file":
return "vscode-remote"
default:
return scheme
}
}
}

View File

@ -10,7 +10,7 @@ import * as path from "path"
import safeCompare from "safe-compare"
import * as util from "util"
import xdgBasedir from "xdg-basedir"
import { getFirstString } from "../common/util"
import { vsRootPath } from "./constants"
export interface Paths {
data: string
@ -157,7 +157,7 @@ export const generatePassword = async (length = 24): Promise<string> => {
export const hash = async (password: string): Promise<string> => {
try {
return await argon2.hash(password)
} catch (error) {
} catch (error: any) {
logger.error(error)
return ""
}
@ -172,7 +172,7 @@ export const isHashMatch = async (password: string, hash: string) => {
}
try {
return await argon2.verify(hash, password)
} catch (error) {
} catch (error: any) {
throw new Error(error)
}
}
@ -439,55 +439,6 @@ export const isObject = <T extends object>(obj: T): obj is T => {
return !Array.isArray(obj) && typeof obj === "object" && obj !== null
}
/**
* Taken from vs/base/common/charCode.ts. Copied for now instead of importing so
* we don't have to set up a `vs` alias to be able to import with types (since
* the alternative is to directly import from `out`).
*/
enum CharCode {
Slash = 47,
A = 65,
Z = 90,
a = 97,
z = 122,
Colon = 58,
}
/**
* Compute `fsPath` for the given uri.
* Taken from vs/base/common/uri.ts. It's not imported to avoid also importing
* everything that file imports.
*/
export function pathToFsPath(path: string, keepDriveLetterCasing = false): string {
const isWindows = process.platform === "win32"
const uri = { authority: undefined, path: getFirstString(path) || "", scheme: "file" }
let value: string
if (uri.authority && uri.path.length > 1 && uri.scheme === "file") {
// unc path: file://shares/c$/far/boo
value = `//${uri.authority}${uri.path}`
} else if (
uri.path.charCodeAt(0) === CharCode.Slash &&
((uri.path.charCodeAt(1) >= CharCode.A && uri.path.charCodeAt(1) <= CharCode.Z) ||
(uri.path.charCodeAt(1) >= CharCode.a && uri.path.charCodeAt(1) <= CharCode.z)) &&
uri.path.charCodeAt(2) === CharCode.Colon
) {
if (!keepDriveLetterCasing) {
// windows drive letter: file:///c:/far/boo
value = uri.path[1].toLowerCase() + uri.path.substr(2)
} else {
value = uri.path.substr(1)
}
} else {
// other path
value = uri.path
}
if (isWindows) {
value = value.replace(/\//g, "\\")
}
return value
}
/**
* Return a promise that resolves with whether the socket path is active.
*/
@ -524,3 +475,30 @@ export function escapeHtml(unsafe: string): string {
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;")
}
// TODO: Replace with proper templating system.
export const escapeJSON = (value: cp.Serializable) => JSON.stringify(value).replace(/"/g, "&quot;")
type AMDModule<T> = { [exportName: string]: T }
const AMDModules = new Map<string, AMDModule<unknown>>()
/**
* Loads AMD module, typically from a compiled VSCode bundle.
*
* @deprecated This should be gradually phased out as code-server migrates to lib/vscode
* @param amdPath Path to module relative to lib/vscode
* @param exportName Given name of export in the file
*/
export const loadAMDModule = async <T>(amdPath: string, exportName: string): Promise<T> => {
let module = AMDModules.get(amdPath)
if (!module) {
module = await new Promise<AMDModule<T>>((resolve, reject) => {
require(path.join(vsRootPath, "out/bootstrap-amd")).load(amdPath, resolve, reject)
})
AMDModules.set(amdPath, module)
}
return module[exportName] as T
}

View File

@ -1,168 +0,0 @@
import { logger } from "@coder/logger"
import * as cp from "child_process"
import * as net from "net"
import * as path from "path"
import * as ipc from "../../typings/ipc"
import { arrayify, generateUuid } from "../common/util"
import { rootPath } from "./constants"
import { settings } from "./settings"
import { SocketProxyProvider } from "./socket"
import { isFile } from "./util"
import { onMessage, wrapper } from "./wrapper"
export class VscodeProvider {
public readonly serverRootPath: string
public readonly vsRootPath: string
private _vscode?: Promise<cp.ChildProcess>
private readonly socketProvider = new SocketProxyProvider()
public constructor() {
this.vsRootPath = path.resolve(rootPath, "lib/vscode")
this.serverRootPath = path.join(this.vsRootPath, "out/vs/server")
wrapper.onDispose(() => this.dispose())
}
public async dispose(): Promise<void> {
this.socketProvider.stop()
if (this._vscode) {
const vscode = await this._vscode
vscode.removeAllListeners()
vscode.kill()
this._vscode = undefined
}
}
public async initialize(
options: Omit<ipc.VscodeOptions, "startPath">,
query: ipc.Query,
): Promise<ipc.WorkbenchOptions> {
const { lastVisited } = await settings.read()
let startPath = await this.getFirstPath([
{ url: query.workspace, workspace: true },
{ url: query.folder, workspace: false },
options.args._ && options.args._.length > 0
? { url: path.resolve(options.args._[options.args._.length - 1]) }
: undefined,
!options.args["ignore-last-opened"] ? lastVisited : undefined,
])
if (query.ew) {
startPath = undefined
}
settings.write({
lastVisited: startPath,
query,
})
const id = generateUuid()
const vscode = await this.fork()
logger.debug("setting up vs code...")
this.send(
{
type: "init",
id,
options: {
...options,
startPath,
},
},
vscode,
)
const message = await onMessage<ipc.VscodeMessage, ipc.OptionsMessage>(
vscode,
(message): message is ipc.OptionsMessage => {
// There can be parallel initializations so wait for the right ID.
return message.type === "options" && message.id === id
},
)
return message.options
}
private fork(): Promise<cp.ChildProcess> {
if (this._vscode) {
return this._vscode
}
logger.debug("forking vs code...")
const vscode = cp.fork(path.join(this.serverRootPath, "fork"))
const dispose = () => {
vscode.removeAllListeners()
vscode.kill()
this._vscode = undefined
}
vscode.on("error", (error: Error) => {
logger.error(error.message)
if (error.stack) {
logger.debug(error.stack)
}
dispose()
})
vscode.on("exit", (code) => {
logger.error(`VS Code exited unexpectedly with code ${code}`)
dispose()
})
this._vscode = onMessage<ipc.VscodeMessage, ipc.ReadyMessage>(vscode, (message): message is ipc.ReadyMessage => {
return message.type === "ready"
}).then(() => vscode)
return this._vscode
}
/**
* VS Code expects a raw socket. It will handle all the web socket frames.
*/
public async sendWebsocket(socket: net.Socket, query: ipc.Query, permessageDeflate: boolean): Promise<void> {
const vscode = await this._vscode
// TLS sockets cannot be transferred to child processes so we need an
// in-between. Non-TLS sockets will be returned as-is.
const socketProxy = await this.socketProvider.createProxy(socket)
this.send({ type: "socket", query, permessageDeflate }, vscode, socketProxy)
}
private send(message: ipc.CodeServerMessage, vscode?: cp.ChildProcess, socket?: net.Socket): void {
if (!vscode || vscode.killed) {
throw new Error("vscode is not running")
}
vscode.send(message, socket)
}
/**
* Choose the first non-empty path from the provided array.
*
* Each array item consists of `url` and an optional `workspace` boolean that
* indicates whether that url is for a workspace.
*
* `url` can be a fully qualified URL or just the path portion.
*
* `url` can also be a query object to make it easier to pass in query
* variables directly but anything that isn't a string or string array is not
* valid and will be ignored.
*/
private async getFirstPath(
startPaths: Array<{ url?: string | string[] | ipc.Query | ipc.Query[]; workspace?: boolean } | undefined>,
): Promise<ipc.StartPath | undefined> {
for (let i = 0; i < startPaths.length; ++i) {
const startPath = startPaths[i]
const url = arrayify(startPath && startPath.url).find((p) => !!p)
if (startPath && url && typeof url === "string") {
return {
url,
// The only time `workspace` is undefined is for the command-line
// argument, in which case it's a path (not a URL) so we can stat it
// without having to parse it.
workspace: typeof startPath.workspace !== "undefined" ? startPath.workspace : await isFile(url),
}
}
}
return undefined
}
}

View File

@ -267,7 +267,7 @@ export class ParentProcess extends Process {
try {
this.started = this._start()
await this.started
} catch (error) {
} catch (error: any) {
this.logger.error(error.message)
this.exit(typeof error.code === "number" ? error.code : 1)
}

View File

@ -4,7 +4,7 @@
"devDependencies": {
"@playwright/test": "^1.12.1",
"@types/jest": "^26.0.20",
"@types/jsdom": "^16.2.6",
"@types/jsdom": "^16.2.13",
"@types/node-fetch": "^2.5.8",
"@types/supertest": "^2.0.10",
"argon2": "^0.28.0",

View File

@ -4,13 +4,27 @@
import { JSDOM } from "jsdom"
import {
getNlsConfiguration,
nlsConfigElementId,
getConfigurationForLoader,
setBodyBackgroundToThemeBackgroundColor,
_createScriptURL,
main,
createBundlePath,
} from "../../../../src/browser/pages/vscode"
import { WORKBENCH_WEB_CONFIG_ID, version as codeServerVersion } from "../../../../src/node/constants"
interface MockWorkbenchConfig {
nlsConfiguration: Pick<CodeServerLib.NLSConfigurationWeb, "locale" | "availableLanguages">
}
const createMockDataSettings = (): MockWorkbenchConfig => ({
nlsConfiguration: {
locale: "en",
availableLanguages: {
en: "English",
de: "German",
},
},
})
describe("vscode", () => {
describe("getNlsConfiguration", () => {
@ -22,9 +36,9 @@ describe("vscode", () => {
_document = _window.document
})
it("should throw an error if no nlsConfigElement", () => {
it("should throw an error if no Workbench element", () => {
const errorMsgPrefix = "[vscode]"
const errorMessage = `${errorMsgPrefix} Could not parse NLS configuration. Could not find nlsConfigElement with id: ${nlsConfigElementId}`
const errorMessage = `${errorMsgPrefix} Could not find Workbench element`
expect(() => {
getNlsConfiguration(_document, "")
@ -32,11 +46,11 @@ describe("vscode", () => {
})
it("should throw an error if no nlsConfig", () => {
const mockElement = _document.createElement("div")
mockElement.setAttribute("id", nlsConfigElementId)
mockElement.setAttribute("id", WORKBENCH_WEB_CONFIG_ID)
_document.body.appendChild(mockElement)
const errorMsgPrefix = "[vscode]"
const errorMessage = `${errorMsgPrefix} Could not parse NLS configuration. Found nlsConfigElement but missing data-settings attribute.`
const errorMessage = `${errorMsgPrefix} Could not find Workbench data`
expect(() => {
getNlsConfiguration(_document, "")
@ -46,32 +60,28 @@ describe("vscode", () => {
})
it("should return the correct configuration", () => {
const mockElement = _document.createElement("div")
const dataSettings = {
first: "Jane",
last: "Doe",
}
const dataSettings = createMockDataSettings()
mockElement.setAttribute("id", nlsConfigElementId)
mockElement.setAttribute("id", WORKBENCH_WEB_CONFIG_ID)
mockElement.setAttribute("data-settings", JSON.stringify(dataSettings))
_document.body.appendChild(mockElement)
const actual = getNlsConfiguration(_document, "")
expect(actual).toStrictEqual(dataSettings)
expect(actual).toStrictEqual(dataSettings.nlsConfiguration)
_document.body.removeChild(mockElement)
})
it("should return have loadBundle property if _resolvedLangaugePackCoreLocation", () => {
const mockElement = _document.createElement("div")
const dataSettings = {
locale: "en",
availableLanguages: ["en", "de"],
_resolvedLanguagePackCoreLocation: "./",
}
mockElement.setAttribute("id", nlsConfigElementId)
const dataSettings = createMockDataSettings()
;(dataSettings.nlsConfiguration as CodeServerLib.InternalNLSConfiguration)._resolvedLanguagePackCoreLocation =
"./"
mockElement.setAttribute("id", WORKBENCH_WEB_CONFIG_ID)
mockElement.setAttribute("data-settings", JSON.stringify(dataSettings))
_document.body.appendChild(mockElement)
const nlsConfig = getNlsConfiguration(_document, "")
const nlsConfig = getNlsConfiguration<CodeServerLib.InternalNLSConfiguration>(_document, "")
expect(nlsConfig._resolvedLanguagePackCoreLocation).not.toBe(undefined)
expect(nlsConfig.loadBundle).not.toBe(undefined)
@ -196,21 +206,18 @@ describe("vscode", () => {
_window = __window
})
it("should return a loader object (with undefined trustedTypesPolicy)", () => {
const options = {
const options: CodeServerLib.ClientConfiguration = {
base: ".",
csStaticBase: "/",
logLevel: 1,
}
const nlsConfig = {
first: "Jane",
last: "Doe",
locale: "en",
availableLanguages: {},
codeServerVersion,
}
const { nlsConfiguration } = createMockDataSettings()
const loader = getConfigurationForLoader({
options,
_window,
nlsConfig: nlsConfig,
nlsConfiguration,
})
expect(loader).toStrictEqual({
@ -234,20 +241,11 @@ describe("vscode", () => {
// maybe extract function into function
// and test manually
trustedTypesPolicy: undefined,
"vs/nls": {
availableLanguages: {},
first: "Jane",
last: "Doe",
locale: "en",
},
"vs/nls": nlsConfiguration,
})
})
it("should return a loader object with trustedTypesPolicy", () => {
interface PolicyOptions {
createScriptUrl: (url: string) => string
}
function mockCreatePolicy(policyName: string, options: PolicyOptions) {
function mockCreatePolicy(policyName: string, options: TrustedTypePolicyOptions) {
return {
name: policyName,
...options,
@ -256,16 +254,17 @@ describe("vscode", () => {
const mockFn = jest.fn(mockCreatePolicy)
// @ts-expect-error we are adding a custom property to window
_window.trustedTypes = {
createPolicy: mockFn,
_window.trustedTypes = <TrustedTypePolicyFactory>{
createPolicy: mockFn as TrustedTypePolicyFactory["createPolicy"],
}
const options = {
base: "/",
csStaticBase: "/",
logLevel: 1,
codeServerVersion,
}
const nlsConfig = {
first: "Jane",
last: "Doe",
@ -275,11 +274,11 @@ describe("vscode", () => {
const loader = getConfigurationForLoader({
options,
_window,
nlsConfig: nlsConfig,
nlsConfiguration: nlsConfig,
})
expect(loader.trustedTypesPolicy).not.toBe(undefined)
expect(loader.trustedTypesPolicy.name).toBe("amdLoader")
expect(loader.trustedTypesPolicy!.name).toBe("amdLoader")
})
})
describe("_createScriptURL", () => {
@ -311,12 +310,9 @@ describe("vscode", () => {
_localStorage = __window.localStorage
const mockElement = _document.createElement("div")
const dataSettings = {
first: "Jane",
last: "Doe",
}
const dataSettings = createMockDataSettings()
mockElement.setAttribute("id", nlsConfigElementId)
mockElement.setAttribute("id", WORKBENCH_WEB_CONFIG_ID)
mockElement.setAttribute("data-settings", JSON.stringify(dataSettings))
_document.body.appendChild(mockElement)

View File

@ -131,7 +131,7 @@ describe("util", () => {
})
it("should return options with base and cssStaticBase even if it doesn't exist", () => {
expect(util.getOptions()).toStrictEqual({
expect(util.getClientConfiguration()).toStrictEqual({
base: "",
csStaticBase: "",
})
@ -151,7 +151,7 @@ describe("util", () => {
// it returns the element
spy.mockImplementation(() => mockElement)
expect(util.getOptions()).toStrictEqual({
expect(util.getClientConfiguration()).toStrictEqual({
base: "",
csStaticBase: "/static/development/Users/jp/Dev/code-server",
disableUpdateCheck: false,
@ -167,7 +167,7 @@ describe("util", () => {
// spreads the original options
// then parses the queryOpts
location.search = '?options={"logLevel":2}'
expect(util.getOptions()).toStrictEqual({
expect(util.getClientConfiguration()).toStrictEqual({
base: "",
csStaticBase: "",
logLevel: 2,

View File

@ -1,12 +1,12 @@
import bodyParser from "body-parser"
import * as express from "express"
import * as nodeFetch from "node-fetch"
import * as http from "http"
import * as nodeFetch from "node-fetch"
import { HttpCode } from "../../../src/common/http"
import { proxy } from "../../../src/node/proxy"
import { getAvailablePort } from "../../utils/helpers"
import * as httpserver from "../../utils/httpserver"
import * as integration from "../../utils/integration"
import { getAvailablePort } from "../../utils/helpers"
describe("proxy", () => {
const nhooyrDevServer = new httpserver.HttpServer()

View File

@ -1,7 +1,7 @@
import { promises as fs } from "fs"
import * as net from "net"
import * as os from "os"
import * as path from "path"
import * as net from "net"
/**
* Return a mock of @coder/logger.

View File

@ -1101,10 +1101,10 @@
jest-diff "^26.0.0"
pretty-format "^26.0.0"
"@types/jsdom@^16.2.6":
version "16.2.6"
resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-16.2.6.tgz#9ddf0521e49be5365797e690c3ba63148e562c29"
integrity sha512-yQA+HxknGtW9AkRTNyiSH3OKW5V+WzO8OPTdne99XwJkYC+KYxfNIcoJjeiSqP3V00PUUpFP6Myoo9wdIu78DQ==
"@types/jsdom@^16.2.13":
version "16.2.13"
resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-16.2.13.tgz#126c8b7441b159d6234610a48de77b6066f1823f"
integrity sha512-8JQCjdeAidptSsOcRWk2iTm9wCcwn9l+kRG6k5bzUacrnm1ezV4forq0kWjUih/tumAeoG+OspOvQEbbRucBTw==
dependencies:
"@types/node" "*"
"@types/parse5" "*"

View File

@ -15,9 +15,14 @@
"sourceMap": true,
"tsBuildInfoFile": "./.cache/tsbuildinfo",
"incremental": true,
"typeRoots": ["./node_modules/@types", "./typings", "./test/node_modules/@types"],
"downlevelIteration": true
"typeRoots": [
"./node_modules/@types",
"./typings",
"./test/node_modules/@types",
"./vendor/modules/code-oss-dev/src/vs/server/@types"
],
"downlevelIteration": true,
},
"include": ["./src/**/*.ts"],
"include": ["./src/**/*"],
"exclude": ["/test", "/lib", "/ci", "/doc"]
}

137
typings/ipc.d.ts vendored
View File

@ -1,137 +0,0 @@
/**
* External interfaces for integration into code-server over IPC.
* This file exists in two locations:
* - typings/ipc.d.ts
* - lib/vscode/src/typings/ipc.d.ts
* The second is a symlink to the first.
*/
export interface Options {
authed: boolean
base: string
csStaticBase: string
disableUpdateCheck: boolean
logLevel: number
}
export interface InitMessage {
type: "init"
id: string
options: VscodeOptions
}
export type Query = { [key: string]: string | string[] | undefined | Query | Query[] }
export interface SocketMessage {
type: "socket"
query: Query
permessageDeflate: boolean
}
export interface CliMessage {
type: "cli"
args: Args
}
export interface OpenCommandPipeArgs {
type: "open"
fileURIs?: string[]
folderURIs: string[]
forceNewWindow?: boolean
diffMode?: boolean
addMode?: boolean
gotoLineMode?: boolean
forceReuseWindow?: boolean
waitMarkerFilePath?: string
}
export type CodeServerMessage = InitMessage | SocketMessage | CliMessage
export interface ReadyMessage {
type: "ready"
}
export interface OptionsMessage {
id: string
type: "options"
options: WorkbenchOptions
}
export type VscodeMessage = ReadyMessage | OptionsMessage
export interface StartPath {
url: string
workspace: boolean
}
export interface Args {
"user-data-dir"?: string
"enable-proposed-api"?: string[]
"extensions-dir"?: string
"builtin-extensions-dir"?: string
"extra-extensions-dir"?: string[]
"extra-builtin-extensions-dir"?: string[]
"ignore-last-opened"?: boolean
locale?: string
log?: string
verbose?: boolean
_: string[]
}
export interface VscodeOptions {
readonly args: Args
readonly remoteAuthority: string
readonly startPath?: StartPath
}
export interface VscodeOptionsMessage extends VscodeOptions {
readonly id: string
}
export interface UriComponents {
readonly scheme: string
readonly authority: string
readonly path: string
readonly query: string
readonly fragment: string
}
export interface NLSConfiguration {
locale: string
availableLanguages: {
[key: string]: string
}
pseudo?: boolean
_languagePackSupport?: boolean
}
export interface WorkbenchOptions {
readonly workbenchWebConfiguration: {
readonly remoteAuthority?: string
readonly folderUri?: UriComponents
readonly workspaceUri?: UriComponents
readonly logLevel?: number
readonly workspaceProvider?: {
payload: [["userDataPath", string], ["enableProposedApi", string]]
}
}
readonly remoteUserDataUri: UriComponents
readonly productConfiguration: {
codeServerVersion?: string
readonly extensionsGallery?: {
readonly serviceUrl: string
readonly itemUrl: string
readonly controlUrl: string
readonly recommendationsUrl: string
}
}
readonly nlsConfiguration: NLSConfiguration
readonly commit: string
}
export interface WorkbenchOptionsMessage {
id: string
}

View File

@ -339,7 +339,7 @@
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.1.tgz#a6ca6a9a0ff366af433f42f5f0e124794ff6b8f1"
integrity sha512-FTgBI767POY/lKNDNbIzgAX6miIDBs6NTCbdlDb8TrWovHsSvaVIZDlTqym29C6UqhzwcJx4CYr+AlrMywA0cA==
"@types/body-parser@*", "@types/body-parser@^1.19.0":
"@types/body-parser@*":
version "1.19.1"
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.1.tgz#0c0174c42a7d017b818303d4b5d969cb0b75929c"
integrity sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==
@ -520,6 +520,11 @@
dependencies:
"@types/node" "*"
"@types/trusted-types@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756"
integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==
"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
@ -893,7 +898,7 @@ bn.js@^5.0.0, bn.js@^5.1.1:
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002"
integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==
body-parser@1.19.0, body-parser@^1.19.0:
body-parser@1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
@ -5074,10 +5079,10 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript@^4.1.3:
version "4.3.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4"
integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==
typescript@^4.4.0-dev.20210528:
version "4.4.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.2.tgz#6d618640d430e3569a1dfb44f7d7e600ced3ee86"
integrity sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==
umd@^3.0.0:
version "3.0.3"
@ -5404,9 +5409,9 @@ write-file-atomic@^3.0.3:
typedarray-to-buffer "^3.1.5"
ws@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.0.0.tgz#550605d13dfc1437c9ec1396975709c6d7ffc57d"
integrity sha512-6AcSIXpBlS0QvCVKk+3cWnWElLsA6SzC0lkQ43ciEglgXJXiCWK3/CGFEJ+Ybgp006CMibamAsqOlxE9s4AvYA==
version "8.2.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.0.tgz#0b738cd484bfc9303421914b11bb4011e07615bb"
integrity sha512-uYhVJ/m9oXwEI04iIVmgLmugh2qrZihkywG9y5FfZV2ATeLIzHf93qs+tUNqlttbQK957/VX3mtwAS+UfIwA4g==
x-is-string@^0.1.0:
version "0.1.0"