Compare commits

..

5 Commits

Author SHA1 Message Date
Asher
5c19962930 Set session socket into environment variable (#6282)
* Avoid spawning code-server with --reuse-window and --new-window

These flags mean the user explicitly wants to open in an existing
instance so if the socket is down it should error rather than try to
spawn code-server normally.

* Set session socket into environment variable

While I was at it I added a CLI flag to override the default.  I also
swapped the default to --user-data-dir.

The value is set on an environment variable so it can be used by the
extension host similar to VSCODE_IPC_HOOK_CLI.

* Add e2e test for opening files externally
2023-06-21 22:47:01 -08:00
Asher
56d10d82bf Enforce Node binary permissions
In some cases it may have extra write permissions.
2023-06-21 11:23:40 -08:00
Asher
cef2aa22dc Move session server log after HTTP server logs
The indented logs technically apply to the HTTP server so move the
session server log afterward to avoid making them look like they apply
to the session server.
2023-06-21 10:59:02 -08:00
Sean Lee
b5a9ef80e7 Use unique socket per user for managing editor sessions (#6278)
Also warn if editor session manager socket cannot be created rather than failing.
2023-06-21 13:39:25 -05:00
Asher
5d3c9edce4 Release v4.14.0 2023-06-16 13:38:21 -08:00
15 changed files with 232 additions and 96 deletions

View File

@@ -22,7 +22,31 @@ Code v99.99.999
## Unreleased ## Unreleased
Code v1.78.2 Code v1.79.2
### Fixed
- Fix being unable to launch multiple instances of code-server for different
users.
### Added
- `--session-socket` CLI flag to configure the location of the session socket.
By default it will be placed in `--user-data-dir`.
## [4.14.0](https://github.com/coder/code-server/releases/tag/v4.14.0) - 2023-06-16
Code v1.79.2
### Added
- `--domain-proxy` now supports `{{port}}` and `{{host}}` template variables.
### Changed
- Updated to Code 1.79.2
- Files opened from an external terminal will now open in the most closely
related window rather than in the last opened window.
## [4.13.0](https://github.com/coder/code-server/releases/tag/v4.13.0) - 2023-05-19 ## [4.13.0](https://github.com/coder/code-server/releases/tag/v4.13.0) - 2023-05-19

View File

@@ -24,6 +24,8 @@ main() {
rsync ./ci/build/code-server.sh "$RELEASE_PATH/bin/code-server" rsync ./ci/build/code-server.sh "$RELEASE_PATH/bin/code-server"
rsync "$node_path" "$RELEASE_PATH/lib/node" rsync "$node_path" "$RELEASE_PATH/lib/node"
chmod 755 "$RELEASE_PATH/lib/node"
pushd "$RELEASE_PATH" pushd "$RELEASE_PATH"
npm install --unsafe-perm --omit=dev npm install --unsafe-perm --omit=dev
popd popd

View File

@@ -15,9 +15,9 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes # This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version. # to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/) # Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 3.9.0 version: 3.10.0
# This is the version number of the application being deployed. This version number should be # This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to # incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using. # follow Semantic Versioning. They should reflect the version the application is using.
appVersion: 4.13.0 appVersion: 4.14.0

View File

@@ -6,7 +6,7 @@ replicaCount: 1
image: image:
repository: codercom/code-server repository: codercom/code-server
tag: '4.13.0' tag: '4.14.0'
pullPolicy: Always pullPolicy: Always
# Specifies one or more secrets to be used when pulling images from a # Specifies one or more secrets to be used when pulling images from a

View File

@@ -15,18 +15,16 @@ Index: code-server/lib/vscode/src/vs/workbench/api/node/extHostExtensionService.
=================================================================== ===================================================================
--- code-server.orig/lib/vscode/src/vs/workbench/api/node/extHostExtensionService.ts --- code-server.orig/lib/vscode/src/vs/workbench/api/node/extHostExtensionService.ts
+++ code-server/lib/vscode/src/vs/workbench/api/node/extHostExtensionService.ts +++ code-server/lib/vscode/src/vs/workbench/api/node/extHostExtensionService.ts
@@ -2,7 +2,9 @@ @@ -2,7 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved. * Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information. * Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
- -
+import * as os from 'os';
+import * as _http from 'http'; +import * as _http from 'http';
+import * as path from 'vs/base/common/path';
import * as performance from 'vs/base/common/performance'; import * as performance from 'vs/base/common/performance';
import { createApiFactoryAndRegisterActors } from 'vs/workbench/api/common/extHost.api.impl'; import { createApiFactoryAndRegisterActors } from 'vs/workbench/api/common/extHost.api.impl';
import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor'; import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor';
@@ -17,6 +19,7 @@ import { ExtensionRuntime } from 'vs/wor @@ -17,6 +17,7 @@ import { ExtensionRuntime } from 'vs/wor
import { CLIServer } from 'vs/workbench/api/node/extHostCLIServer'; import { CLIServer } from 'vs/workbench/api/node/extHostCLIServer';
import { realpathSync } from 'vs/base/node/extpath'; import { realpathSync } from 'vs/base/node/extpath';
import { ExtHostConsoleForwarder } from 'vs/workbench/api/node/extHostConsoleForwarder'; import { ExtHostConsoleForwarder } from 'vs/workbench/api/node/extHostConsoleForwarder';
@@ -34,13 +32,14 @@ Index: code-server/lib/vscode/src/vs/workbench/api/node/extHostExtensionService.
import { ExtHostDiskFileSystemProvider } from 'vs/workbench/api/node/extHostDiskFileSystemProvider'; import { ExtHostDiskFileSystemProvider } from 'vs/workbench/api/node/extHostDiskFileSystemProvider';
class NodeModuleRequireInterceptor extends RequireInterceptor { class NodeModuleRequireInterceptor extends RequireInterceptor {
@@ -83,6 +86,52 @@ export class ExtHostExtensionService ext @@ -83,6 +84,52 @@ export class ExtHostExtensionService ext
await interceptor.install(); await interceptor.install();
performance.mark('code/extHost/didInitAPI'); performance.mark('code/extHost/didInitAPI');
+ (async () => { + (async () => {
+ const socketPath = process.env['VSCODE_IPC_HOOK_CLI']; + const socketPath = process.env['VSCODE_IPC_HOOK_CLI'];
+ if (!socketPath) { + const codeServerSocketPath = process.env['CODE_SERVER_SESSION_SOCKET']
+ if (!socketPath || !codeServerSocketPath) {
+ return; + return;
+ } + }
+ const workspace = this._instaService.invokeFunction((accessor) => { + const workspace = this._instaService.invokeFunction((accessor) => {
@@ -52,7 +51,6 @@ Index: code-server/lib/vscode/src/vs/workbench/api/node/extHostExtensionService.
+ socketPath + socketPath
+ }; + };
+ const message = JSON.stringify({entry}); + const message = JSON.stringify({entry});
+ const codeServerSocketPath = path.join(os.tmpdir(), 'code-server-ipc.sock');
+ await new Promise<void>((resolve, reject) => { + await new Promise<void>((resolve, reject) => {
+ const opts: _http.RequestOptions = { + const opts: _http.RequestOptions = {
+ path: '/add-session', + path: '/add-session',
@@ -91,17 +89,15 @@ Index: code-server/lib/vscode/src/vs/workbench/api/node/extensionHostProcess.ts
=================================================================== ===================================================================
--- code-server.orig/lib/vscode/src/vs/workbench/api/node/extensionHostProcess.ts --- code-server.orig/lib/vscode/src/vs/workbench/api/node/extensionHostProcess.ts
+++ code-server/lib/vscode/src/vs/workbench/api/node/extensionHostProcess.ts +++ code-server/lib/vscode/src/vs/workbench/api/node/extensionHostProcess.ts
@@ -3,6 +3,9 @@ @@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information. * Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
+import * as os from 'os';
+import * as _http from 'http'; +import * as _http from 'http';
+import * as path from 'vs/base/common/path';
import * as nativeWatchdog from 'native-watchdog'; import * as nativeWatchdog from 'native-watchdog';
import * as net from 'net'; import * as net from 'net';
import * as minimist from 'minimist'; import * as minimist from 'minimist';
@@ -400,7 +403,28 @@ async function startExtensionHostProcess @@ -400,7 +401,28 @@ async function startExtensionHostProcess
); );
// rewrite onTerminate-function to be a proper shutdown // rewrite onTerminate-function to be a proper shutdown
@@ -110,11 +106,11 @@ Index: code-server/lib/vscode/src/vs/workbench/api/node/extensionHostProcess.ts
+ extensionHostMain.terminate(reason); + extensionHostMain.terminate(reason);
+ +
+ const socketPath = process.env['VSCODE_IPC_HOOK_CLI']; + const socketPath = process.env['VSCODE_IPC_HOOK_CLI'];
+ if (!socketPath) { + const codeServerSocketPath = process.env['CODE_SERVER_SESSION_SOCKET']
+ if (!socketPath || !codeServerSocketPath) {
+ return; + return;
+ } + }
+ const message = JSON.stringify({socketPath}); + const message = JSON.stringify({socketPath});
+ const codeServerSocketPath = path.join(os.tmpdir(), 'code-server-ipc.sock');
+ const opts: _http.RequestOptions = { + const opts: _http.RequestOptions = {
+ path: '/delete-session', + path: '/delete-session',
+ socketPath: codeServerSocketPath, + socketPath: codeServerSocketPath,

View File

@@ -9,7 +9,7 @@ import * as util from "../common/util"
import { DefaultedArgs } from "./cli" import { DefaultedArgs } from "./cli"
import { disposer } from "./http" import { disposer } from "./http"
import { isNodeJSErrnoException } from "./util" import { isNodeJSErrnoException } from "./util"
import { DEFAULT_SOCKET_PATH, EditorSessionManager, makeEditorSessionManagerServer } from "./vscodeSocket" import { EditorSessionManager, makeEditorSessionManagerServer } from "./vscodeSocket"
import { handleUpgrade } from "./wsRouter" import { handleUpgrade } from "./wsRouter"
type SocketOptions = { socket: string; "socket-mode"?: string } type SocketOptions = { socket: string; "socket-mode"?: string }
@@ -88,7 +88,7 @@ export const createApp = async (args: DefaultedArgs): Promise<App> => {
handleUpgrade(wsRouter, server) handleUpgrade(wsRouter, server)
const editorSessionManager = new EditorSessionManager() const editorSessionManager = new EditorSessionManager()
const editorSessionManagerServer = await makeEditorSessionManagerServer(DEFAULT_SOCKET_PATH, editorSessionManager) const editorSessionManagerServer = await makeEditorSessionManagerServer(args["session-socket"], editorSessionManager)
const disposeEditorSessionManagerServer = disposer(editorSessionManagerServer) const disposeEditorSessionManagerServer = disposer(editorSessionManagerServer)
const dispose = async () => { const dispose = async () => {

View File

@@ -4,7 +4,7 @@ import { load } from "js-yaml"
import * as os from "os" import * as os from "os"
import * as path from "path" import * as path from "path"
import { generateCertificate, generatePassword, humanPath, paths, splitOnFirstEquals } from "./util" import { generateCertificate, generatePassword, humanPath, paths, splitOnFirstEquals } from "./util"
import { DEFAULT_SOCKET_PATH, EditorSessionManagerClient } from "./vscodeSocket" import { EditorSessionManagerClient } from "./vscodeSocket"
export enum Feature { export enum Feature {
// No current experimental features! // No current experimental features!
@@ -51,6 +51,7 @@ export interface UserProvidedCodeArgs {
"disable-file-downloads"?: boolean "disable-file-downloads"?: boolean
"disable-workspace-trust"?: boolean "disable-workspace-trust"?: boolean
"disable-getting-started-override"?: boolean "disable-getting-started-override"?: boolean
"session-socket"?: string
} }
/** /**
@@ -160,6 +161,9 @@ export const options: Options<Required<UserProvidedArgs>> = {
"Disable update check. Without this flag, code-server checks every 6 hours against the latest github release and \n" + "Disable update check. Without this flag, code-server checks every 6 hours against the latest github release and \n" +
"then notifies you once every week that a new release is available.", "then notifies you once every week that a new release is available.",
}, },
"session-socket": {
type: "string",
},
"disable-file-downloads": { "disable-file-downloads": {
type: "boolean", type: "boolean",
description: description:
@@ -459,6 +463,7 @@ export interface DefaultedArgs extends ConfigArgs {
usingEnvHashedPassword: boolean usingEnvHashedPassword: boolean
"extensions-dir": string "extensions-dir": string
"user-data-dir": string "user-data-dir": string
"session-socket": string
/* Positional arguments. */ /* Positional arguments. */
_: string[] _: string[]
} }
@@ -479,6 +484,11 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config
args["extensions-dir"] = path.join(args["user-data-dir"], "extensions") args["extensions-dir"] = path.join(args["user-data-dir"], "extensions")
} }
if (!args["session-socket"]) {
args["session-socket"] = path.join(args["user-data-dir"], "code-server-ipc.sock")
}
process.env.CODE_SERVER_SESSION_SOCKET = args["session-socket"]
// --verbose takes priority over --log and --log takes priority over the // --verbose takes priority over --log and --log takes priority over the
// environment variable. // environment variable.
if (args.verbose) { if (args.verbose) {
@@ -739,7 +749,10 @@ function bindAddrFromAllSources(...argsConfig: UserProvidedArgs[]): Addr {
* existing instance. The arguments here should be the arguments the user * existing instance. The arguments here should be the arguments the user
* explicitly passed on the command line, *NOT DEFAULTS* or the configuration. * explicitly passed on the command line, *NOT DEFAULTS* or the configuration.
*/ */
export const shouldOpenInExistingInstance = async (args: UserProvidedArgs): Promise<string | undefined> => { export const shouldOpenInExistingInstance = async (
args: UserProvidedArgs,
sessionSocket: string,
): Promise<string | undefined> => {
// Always use the existing instance if we're running from VS Code's terminal. // Always use the existing instance if we're running from VS Code's terminal.
if (process.env.VSCODE_IPC_HOOK_CLI) { if (process.env.VSCODE_IPC_HOOK_CLI) {
logger.debug("Found VSCODE_IPC_HOOK_CLI") logger.debug("Found VSCODE_IPC_HOOK_CLI")
@@ -747,21 +760,22 @@ export const shouldOpenInExistingInstance = async (args: UserProvidedArgs): Prom
} }
const paths = getResolvedPathsFromArgs(args) const paths = getResolvedPathsFromArgs(args)
const client = new EditorSessionManagerClient(DEFAULT_SOCKET_PATH) const client = new EditorSessionManagerClient(sessionSocket)
// If we can't connect to the socket then there's no existing instance.
if (!(await client.canConnect())) {
return undefined
}
// If these flags are set then assume the user is trying to open in an // If these flags are set then assume the user is trying to open in an
// existing instance since these flags have no effect otherwise. // existing instance since these flags have no effect otherwise. That means
// if there is no existing instance we should error rather than falling back
// to spawning code-server normally.
const openInFlagCount = ["reuse-window", "new-window"].reduce((prev, cur) => { const openInFlagCount = ["reuse-window", "new-window"].reduce((prev, cur) => {
return args[cur as keyof UserProvidedArgs] ? prev + 1 : prev return args[cur as keyof UserProvidedArgs] ? prev + 1 : prev
}, 0) }, 0)
if (openInFlagCount > 0) { if (openInFlagCount > 0) {
logger.debug("Found --reuse-window or --new-window") logger.debug("Found --reuse-window or --new-window")
return await client.getConnectedSocketPath(paths[0]) const socketPath = await client.getConnectedSocketPath(paths[0])
if (!socketPath) {
throw new Error(`No opened code-server instances found to handle ${paths[0]}`)
}
return socketPath
} }
// It's possible the user is trying to spawn another instance of code-server. // It's possible the user is trying to spawn another instance of code-server.
@@ -769,7 +783,11 @@ export const shouldOpenInExistingInstance = async (args: UserProvidedArgs): Prom
// code-server is invoked exactly like this: `code-server my-file`). // code-server is invoked exactly like this: `code-server my-file`).
// 2. That a file or directory was passed. // 2. That a file or directory was passed.
// 3. That the socket is active. // 3. That the socket is active.
// 4. That an instance exists to handle the path (implied by #3).
if (Object.keys(args).length === 1 && typeof args._ !== "undefined" && args._.length > 0) { if (Object.keys(args).length === 1 && typeof args._ !== "undefined" && args._.length > 0) {
if (!(await client.canConnect())) {
return undefined
}
const socketPath = await client.getConnectedSocketPath(paths[0]) const socketPath = await client.getConnectedSocketPath(paths[0])
if (socketPath) { if (socketPath) {
logger.debug("Found existing code-server socket") logger.debug("Found existing code-server socket")

View File

@@ -51,7 +51,7 @@ async function entry(): Promise<void> {
return runCodeCli(args) return runCodeCli(args)
} }
const socketPath = await shouldOpenInExistingInstance(cliArgs) const socketPath = await shouldOpenInExistingInstance(cliArgs, args["session-socket"])
if (socketPath) { if (socketPath) {
logger.debug("Trying to open in existing instance") logger.debug("Trying to open in existing instance")
return openInExistingInstance(args, socketPath) return openInExistingInstance(args, socketPath)

View File

@@ -121,13 +121,10 @@ export const runCodeServer = async (
const app = await createApp(args) const app = await createApp(args)
const protocol = args.cert ? "https" : "http" const protocol = args.cert ? "https" : "http"
const serverAddress = ensureAddress(app.server, protocol) const serverAddress = ensureAddress(app.server, protocol)
const sessionServerAddress = app.editorSessionManagerServer.address()
const disposeRoutes = await register(app, args) const disposeRoutes = await register(app, args)
logger.info(`Using config file ${humanPath(os.homedir(), args.config)}`) logger.info(`Using config file ${humanPath(os.homedir(), args.config)}`)
logger.info(`${protocol.toUpperCase()} server listening on ${serverAddress.toString()}`) logger.info(`${protocol.toUpperCase()} server listening on ${serverAddress.toString()}`)
logger.info(`Session server listening on ${sessionServerAddress?.toString()}`)
if (args.auth === AuthType.Password) { if (args.auth === AuthType.Password) {
logger.info(" - Authentication is enabled") logger.info(" - Authentication is enabled")
if (args.usingEnvPassword) { if (args.usingEnvPassword) {
@@ -155,6 +152,11 @@ export const runCodeServer = async (
logger.info(`Using proxy URI in PORTS tab: ${process.env.VSCODE_PROXY_URI}`) logger.info(`Using proxy URI in PORTS tab: ${process.env.VSCODE_PROXY_URI}`)
} }
const sessionServerAddress = app.editorSessionManagerServer.address()
if (sessionServerAddress) {
logger.info(`Session server listening on ${sessionServerAddress.toString()}`)
}
if (args.enable && args.enable.length > 0) { if (args.enable && args.enable.length > 0) {
logger.info("Enabling the following experimental features:") logger.info("Enabling the following experimental features:")
args.enable.forEach((feature) => { args.enable.forEach((feature) => {

View File

@@ -1,15 +1,11 @@
import { logger } from "@coder/logger" import { logger } from "@coder/logger"
import express from "express" import express from "express"
import * as http from "http" import * as http from "http"
import * as os from "os"
import * as path from "path" import * as path from "path"
import { HttpCode } from "../common/http" import { HttpCode } from "../common/http"
import { listen } from "./app" import { listen } from "./app"
import { canConnect } from "./util" import { canConnect } from "./util"
// Socket path of the daemonized code-server instance.
export const DEFAULT_SOCKET_PATH = path.join(os.tmpdir(), "code-server-ipc.sock")
export interface EditorSessionEntry { export interface EditorSessionEntry {
workspace: { workspace: {
id: string id: string
@@ -78,7 +74,11 @@ export async function makeEditorSessionManagerServer(
}) })
const server = http.createServer(router) const server = http.createServer(router)
try {
await listen(server, { socket: codeServerSocketPath }) await listen(server, { socket: codeServerSocketPath })
} catch (e) {
logger.warn(`Could not create socket at ${codeServerSocketPath}`)
}
return server return server
} }

View File

@@ -117,15 +117,8 @@ export class CodeServer {
* directories. * directories.
*/ */
private async spawn(): Promise<CodeServerProcess> { private async spawn(): Promise<CodeServerProcess> {
// This will be used both as the workspace and data directory to ensure
// instances don't bleed into each other.
const dir = await this.createWorkspace() const dir = await this.createWorkspace()
const args = await this.argsWithDefaults([
return new Promise((resolve, reject) => {
const args = [
this.entry,
"--extensions-dir",
path.join(dir, "extensions"),
"--auth", "--auth",
"none", "none",
// The workspace to open. // The workspace to open.
@@ -134,23 +127,16 @@ export class CodeServer {
// Using port zero will spawn on a random port. // Using port zero will spawn on a random port.
"--bind-addr", "--bind-addr",
"127.0.0.1:0", "127.0.0.1:0",
// Setting the XDG variables would be easier and more thorough but the ])
// modules we import ignores those variables for non-Linux operating return new Promise((resolve, reject) => {
// systems so use these flags instead.
"--config",
path.join(dir, "config.yaml"),
"--user-data-dir",
dir,
]
this.logger.debug("spawning `node " + args.join(" ") + "`") this.logger.debug("spawning `node " + args.join(" ") + "`")
const proc = cp.spawn("node", args, { const proc = cp.spawn("node", args, {
cwd: path.join(__dirname, "../../.."), cwd: path.join(__dirname, "../../.."),
env: { env: {
...process.env, ...process.env,
...this.env, ...this.env,
// Set to empty string to prevent code-server from // Prevent code-server from using the existing instance when running
// using the existing instance when running the e2e tests // the e2e tests from an integrated terminal.
// from an integrated terminal.
VSCODE_IPC_HOOK_CLI: "", VSCODE_IPC_HOOK_CLI: "",
PASSWORD, PASSWORD,
}, },
@@ -173,11 +159,15 @@ export class CodeServer {
reject(error) reject(error)
}) })
// Tracks when the HTTP and session servers are ready.
let httpAddress: string | undefined
let sessionAddress: string | undefined
let resolved = false let resolved = false
proc.stdout.setEncoding("utf8") proc.stdout.setEncoding("utf8")
onLine(proc, (line) => { onLine(proc, (line) => {
// As long as we are actively getting input reset the timer. If we stop // As long as we are actively getting input reset the timer. If we stop
// getting input and still have not found the address the timer will // getting input and still have not found the addresses the timer will
// reject. // reject.
timer.reset() timer.reset()
@@ -186,20 +176,69 @@ export class CodeServer {
if (resolved) { if (resolved) {
return return
} }
const match = line.trim().match(/HTTPS? server listening on (https?:\/\/[.:\d]+)\/?$/)
let match = line.trim().match(/HTTPS? server listening on (https?:\/\/[.:\d]+)\/?$/)
if (match) { if (match) {
// Cookies don't seem to work on IP address so swap to localhost. // Cookies don't seem to work on IP addresses so swap to localhost.
// TODO: Investigate whether this is a bug with code-server. // TODO: Investigate whether this is a bug with code-server.
const address = match[1].replace("127.0.0.1", "localhost") httpAddress = match[1].replace("127.0.0.1", "localhost")
this.logger.debug(`spawned on ${address}`) }
match = line.trim().match(/Session server listening on (.+)$/)
if (match) {
sessionAddress = match[1]
}
if (typeof httpAddress !== "undefined" && typeof sessionAddress !== "undefined") {
resolved = true resolved = true
timer.dispose() timer.dispose()
resolve({ process: proc, address }) this.logger.debug(`code-server is ready: ${httpAddress} ${sessionAddress}`)
resolve({ process: proc, address: httpAddress })
} }
}) })
}) })
} }
/**
* Execute a short-lived command.
*/
async run(args: string[]): Promise<void> {
args = await this.argsWithDefaults(args)
this.logger.debug("executing `node " + args.join(" ") + "`")
await util.promisify(cp.exec)("node " + args.join(" "), {
cwd: path.join(__dirname, "../../.."),
env: {
...process.env,
...this.env,
// Prevent code-server from using the existing instance when running
// the e2e tests from an integrated terminal.
VSCODE_IPC_HOOK_CLI: "",
},
})
}
/**
* Combine arguments with defaults.
*/
private async argsWithDefaults(args: string[]): Promise<string[]> {
// This will be used both as the workspace and data directory to ensure
// instances don't bleed into each other.
const dir = await this.workspaceDir
return [
this.entry,
"--extensions-dir",
path.join(dir, "extensions"),
...args,
// Setting the XDG variables would be easier and more thorough but the
// modules we import ignores those variables for non-Linux operating
// systems so use these flags instead.
"--config",
path.join(dir, "config.yaml"),
"--user-data-dir",
dir,
]
}
/** /**
* Close the code-server process. * Close the code-server process.
*/ */
@@ -364,6 +403,13 @@ export class CodeServerPage {
await this.waitForTab(file) await this.waitForTab(file)
} }
/**
* Open a file through an external command.
*/
async openFileExternally(file: string) {
await this.codeServer.run(["--reuse-window", file])
}
/** /**
* Wait for a tab to open for the specified file. * Wait for a tab to open for the specified file.
*/ */

View File

@@ -35,13 +35,19 @@ describe("Integrated Terminal", ["--disable-workspace-trust"], {}, () => {
const tmpFolderPath = await tmpdir(testName) const tmpFolderPath = await tmpdir(testName)
const tmpFile = path.join(tmpFolderPath, "test-file") const tmpFile = path.join(tmpFolderPath, "test-file")
await fs.writeFile(tmpFile, "test") await fs.writeFile(tmpFile, "test")
const fileName = path.basename(tmpFile)
await codeServerPage.focusTerminal() await codeServerPage.focusTerminal()
await codeServerPage.page.keyboard.type(`code-server ${tmpFile}`) await codeServerPage.page.keyboard.type(`code-server ${tmpFile}`)
await codeServerPage.page.keyboard.press("Enter") await codeServerPage.page.keyboard.press("Enter")
await codeServerPage.waitForTab(fileName) await codeServerPage.waitForTab(path.basename(tmpFile))
const externalTmpFile = path.join(tmpFolderPath, "test-external-file")
await fs.writeFile(externalTmpFile, "foobar")
await codeServerPage.openFileExternally(externalTmpFile)
await codeServerPage.waitForTab(path.basename(externalTmpFile))
}) })
}) })

View File

@@ -18,7 +18,6 @@ import {
import { shouldSpawnCliProcess } from "../../../src/node/main" import { shouldSpawnCliProcess } from "../../../src/node/main"
import { generatePassword, paths } from "../../../src/node/util" import { generatePassword, paths } from "../../../src/node/util"
import { import {
DEFAULT_SOCKET_PATH,
EditorSessionManager, EditorSessionManager,
EditorSessionManagerClient, EditorSessionManagerClient,
makeEditorSessionManagerServer, makeEditorSessionManagerServer,
@@ -37,6 +36,7 @@ const defaults = {
usingEnvHashedPassword: false, usingEnvHashedPassword: false,
"extensions-dir": path.join(paths.data, "extensions"), "extensions-dir": path.join(paths.data, "extensions"),
"user-data-dir": paths.data, "user-data-dir": paths.data,
"session-socket": path.join(paths.data, "code-server-ipc.sock"),
_: [], _: [],
} }
@@ -103,6 +103,8 @@ describe("parser", () => {
"--disable-getting-started-override", "--disable-getting-started-override",
["--session-socket", "/tmp/override-code-server-ipc-socket"],
["--host", "0.0.0.0"], ["--host", "0.0.0.0"],
"4", "4",
"--", "--",
@@ -136,6 +138,7 @@ describe("parser", () => {
"welcome-text": "welcome to code", "welcome-text": "welcome to code",
version: true, version: true,
"bind-addr": "192.169.0.1:8080", "bind-addr": "192.169.0.1:8080",
"session-socket": "/tmp/override-code-server-ipc-socket",
}) })
}) })
@@ -504,22 +507,23 @@ describe("cli", () => {
it("should use existing if inside code-server", async () => { it("should use existing if inside code-server", async () => {
process.env.VSCODE_IPC_HOOK_CLI = "test" process.env.VSCODE_IPC_HOOK_CLI = "test"
const args: UserProvidedArgs = {} const args: UserProvidedArgs = {}
expect(await shouldOpenInExistingInstance(args)).toStrictEqual("test") expect(await shouldOpenInExistingInstance(args, "")).toStrictEqual("test")
args.port = 8081 args.port = 8081
args._ = ["./file"] args._ = ["./file"]
expect(await shouldOpenInExistingInstance(args)).toStrictEqual("test") expect(await shouldOpenInExistingInstance(args, "")).toStrictEqual("test")
}) })
it("should use existing if --reuse-window is set", async () => { it("should use existing if --reuse-window is set", async () => {
const server = await makeEditorSessionManagerServer(DEFAULT_SOCKET_PATH, new EditorSessionManager()) const sessionSocket = path.join(tmpDirPath, "session-socket")
const server = await makeEditorSessionManagerServer(sessionSocket, new EditorSessionManager())
const args: UserProvidedArgs = {} const args: UserProvidedArgs = {}
args["reuse-window"] = true args["reuse-window"] = true
await expect(shouldOpenInExistingInstance(args)).resolves.toStrictEqual(undefined) await expect(shouldOpenInExistingInstance(args, sessionSocket)).rejects.toThrow()
const socketPath = path.join(tmpDirPath, "socket") const socketPath = path.join(tmpDirPath, "socket")
const client = new EditorSessionManagerClient(DEFAULT_SOCKET_PATH) const client = new EditorSessionManagerClient(sessionSocket)
await client.addSession({ await client.addSession({
entry: { entry: {
workspace: { workspace: {
@@ -537,24 +541,25 @@ describe("cli", () => {
}) })
const vscodeSockets = listenOn(socketPath) const vscodeSockets = listenOn(socketPath)
await expect(shouldOpenInExistingInstance(args)).resolves.toStrictEqual(socketPath) await expect(shouldOpenInExistingInstance(args, sessionSocket)).resolves.toStrictEqual(socketPath)
args.port = 8081 args.port = 8081
await expect(shouldOpenInExistingInstance(args)).resolves.toStrictEqual(socketPath) await expect(shouldOpenInExistingInstance(args, sessionSocket)).resolves.toStrictEqual(socketPath)
server.close() server.close()
vscodeSockets.close() vscodeSockets.close()
}) })
it("should use existing if --new-window is set", async () => { it("should use existing if --new-window is set", async () => {
const server = await makeEditorSessionManagerServer(DEFAULT_SOCKET_PATH, new EditorSessionManager()) const sessionSocket = path.join(tmpDirPath, "session-socket")
const server = await makeEditorSessionManagerServer(sessionSocket, new EditorSessionManager())
const args: UserProvidedArgs = {} const args: UserProvidedArgs = {}
args["new-window"] = true args["new-window"] = true
await expect(shouldOpenInExistingInstance(args)).resolves.toStrictEqual(undefined) await expect(shouldOpenInExistingInstance(args, sessionSocket)).rejects.toThrow()
const socketPath = path.join(tmpDirPath, "socket") const socketPath = path.join(tmpDirPath, "socket")
const client = new EditorSessionManagerClient(DEFAULT_SOCKET_PATH) const client = new EditorSessionManagerClient(sessionSocket)
await client.addSession({ await client.addSession({
entry: { entry: {
workspace: { workspace: {
@@ -572,25 +577,26 @@ describe("cli", () => {
}) })
const vscodeSockets = listenOn(socketPath) const vscodeSockets = listenOn(socketPath)
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(socketPath) expect(await shouldOpenInExistingInstance(args, sessionSocket)).toStrictEqual(socketPath)
args.port = 8081 args.port = 8081
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(socketPath) expect(await shouldOpenInExistingInstance(args, sessionSocket)).toStrictEqual(socketPath)
server.close() server.close()
vscodeSockets.close() vscodeSockets.close()
}) })
it("should use existing if no unrelated flags are set, has positional, and socket is active", async () => { it("should use existing if no unrelated flags are set, has positional, and socket is active", async () => {
const server = await makeEditorSessionManagerServer(DEFAULT_SOCKET_PATH, new EditorSessionManager()) const sessionSocket = path.join(tmpDirPath, "session-socket")
const server = await makeEditorSessionManagerServer(sessionSocket, new EditorSessionManager())
const args: UserProvidedArgs = {} const args: UserProvidedArgs = {}
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined) expect(await shouldOpenInExistingInstance(args, sessionSocket)).toStrictEqual(undefined)
args._ = ["./file"] args._ = ["./file"]
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined) expect(await shouldOpenInExistingInstance(args, sessionSocket)).toStrictEqual(undefined)
const client = new EditorSessionManagerClient(DEFAULT_SOCKET_PATH) const client = new EditorSessionManagerClient(sessionSocket)
const socketPath = path.join(tmpDirPath, "socket") const socketPath = path.join(tmpDirPath, "socket")
await client.addSession({ await client.addSession({
entry: { entry: {
@@ -609,18 +615,19 @@ describe("cli", () => {
}) })
const vscodeSockets = listenOn(socketPath) const vscodeSockets = listenOn(socketPath)
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(socketPath) expect(await shouldOpenInExistingInstance(args, sessionSocket)).toStrictEqual(socketPath)
args.port = 8081 args.port = 8081
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined) expect(await shouldOpenInExistingInstance(args, sessionSocket)).toStrictEqual(undefined)
server.close() server.close()
vscodeSockets.close() vscodeSockets.close()
}) })
it("should prefer matching sessions for only the first path", async () => { it("should prefer matching sessions for only the first path", async () => {
const server = await makeEditorSessionManagerServer(DEFAULT_SOCKET_PATH, new EditorSessionManager()) const sessionSocket = path.join(tmpDirPath, "session-socket")
const client = new EditorSessionManagerClient(DEFAULT_SOCKET_PATH) const server = await makeEditorSessionManagerServer(sessionSocket, new EditorSessionManager())
const client = new EditorSessionManagerClient(sessionSocket)
await client.addSession({ await client.addSession({
entry: { entry: {
workspace: { workspace: {
@@ -655,7 +662,7 @@ describe("cli", () => {
const args: UserProvidedArgs = {} const args: UserProvidedArgs = {}
args._ = ["/aaa/file", "/bbb/file"] args._ = ["/aaa/file", "/bbb/file"]
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(`${tmpDirPath}/vscode-ipc-aaa.sock`) expect(await shouldOpenInExistingInstance(args, sessionSocket)).toStrictEqual(`${tmpDirPath}/vscode-ipc-aaa.sock`)
server.close() server.close()
}) })

View File

@@ -43,6 +43,7 @@ describe("plugin", () => {
usingEnvHashedPassword: false, usingEnvHashedPassword: false,
"extensions-dir": "", "extensions-dir": "",
"user-data-dir": "", "user-data-dir": "",
"session-socket": "",
} }
next() next()
} }

View File

@@ -1,5 +1,39 @@
import { EditorSessionManager } from "../../../src/node/vscodeSocket" import { logger } from "@coder/logger"
import { clean, tmpdir, listenOn } from "../../utils/helpers" import * as app from "../../../src/node/app"
import { EditorSessionManager, makeEditorSessionManagerServer } from "../../../src/node/vscodeSocket"
import { clean, tmpdir, listenOn, mockLogger } from "../../utils/helpers"
describe("makeEditorSessionManagerServer", () => {
let tmpDirPath: string
const testName = "mesms"
beforeAll(async () => {
jest.clearAllMocks()
mockLogger()
await clean(testName)
})
afterAll(() => {
jest.resetModules()
})
beforeEach(async () => {
tmpDirPath = await tmpdir(testName)
})
it("warns if socket cannot be created", async () => {
jest.spyOn(app, "listen").mockImplementation(() => {
throw new Error()
})
const server = await makeEditorSessionManagerServer(
`${tmpDirPath}/code-server-ipc.sock`,
new EditorSessionManager(),
)
expect(logger.warn).toHaveBeenCalledWith(`Could not create socket at ${tmpDirPath}/code-server-ipc.sock`)
server.close()
})
})
describe("EditorSessionManager", () => { describe("EditorSessionManager", () => {
let tmpDirPath: string let tmpDirPath: string