mirror of
https://github.com/coder/code-server.git
synced 2024-12-26 11:12:07 +03:00
Prefer matching editor sessions when opening files. (#6191)
Signed-off-by: Sean Lee <freshdried@gmail.com>
This commit is contained in:
parent
ccb0d3a34f
commit
fb73742b2b
@ -1,4 +1,4 @@
|
|||||||
Store a static reference to the IPC socket
|
Store the IPC socket with workspace metadata.
|
||||||
|
|
||||||
This lets us use it to open files inside code-server from outside of
|
This lets us use it to open files inside code-server from outside of
|
||||||
code-server.
|
code-server.
|
||||||
@ -9,6 +9,8 @@ To test this:
|
|||||||
|
|
||||||
It should open in your existing code-server instance.
|
It should open in your existing code-server instance.
|
||||||
|
|
||||||
|
When the extension host is terminated, the socket is unregistered.
|
||||||
|
|
||||||
Index: code-server/lib/vscode/src/vs/workbench/api/node/extHostExtensionService.ts
|
Index: code-server/lib/vscode/src/vs/workbench/api/node/extHostExtensionService.ts
|
||||||
===================================================================
|
===================================================================
|
||||||
--- code-server.orig/lib/vscode/src/vs/workbench/api/node/extHostExtensionService.ts
|
--- code-server.orig/lib/vscode/src/vs/workbench/api/node/extHostExtensionService.ts
|
||||||
@ -18,20 +20,114 @@ Index: code-server/lib/vscode/src/vs/workbench/api/node/extHostExtensionService.
|
|||||||
* 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 { promises as fs } from 'fs';
|
+import * as os from 'os';
|
||||||
+import * as os from 'os'
|
+import * as _http from 'http';
|
||||||
+import * as path from 'vs/base/common/path';
|
+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';
|
||||||
@@ -72,6 +74,10 @@ export class ExtHostExtensionService ext
|
@@ -17,6 +19,7 @@ import { ExtensionRuntime } from 'vs/wor
|
||||||
if (this._initData.remote.isRemote && this._initData.remote.authority) {
|
import { CLIServer } from 'vs/workbench/api/node/extHostCLIServer';
|
||||||
const cliServer = this._instaService.createInstance(CLIServer);
|
import { realpathSync } from 'vs/base/node/extpath';
|
||||||
process.env['VSCODE_IPC_HOOK_CLI'] = cliServer.ipcHandlePath;
|
import { ExtHostConsoleForwarder } from 'vs/workbench/api/node/extHostConsoleForwarder';
|
||||||
+
|
+import { IExtHostWorkspace } from '../common/extHostWorkspace';
|
||||||
+ fs.writeFile(path.join(os.tmpdir(), 'vscode-ipc'), cliServer.ipcHandlePath).catch((error) => {
|
|
||||||
+ this._logService.error(error);
|
|
||||||
+ });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Module loading tricks
|
class NodeModuleRequireInterceptor extends RequireInterceptor {
|
||||||
|
|
||||||
|
@@ -79,6 +82,52 @@ export class ExtHostExtensionService ext
|
||||||
|
await interceptor.install();
|
||||||
|
performance.mark('code/extHost/didInitAPI');
|
||||||
|
|
||||||
|
+ (async () => {
|
||||||
|
+ const socketPath = process.env['VSCODE_IPC_HOOK_CLI'];
|
||||||
|
+ if (!socketPath) {
|
||||||
|
+ return;
|
||||||
|
+ }
|
||||||
|
+ const workspace = this._instaService.invokeFunction((accessor) => {
|
||||||
|
+ const workspaceService = accessor.get(IExtHostWorkspace);
|
||||||
|
+ return workspaceService.workspace;
|
||||||
|
+ });
|
||||||
|
+ const entry = {
|
||||||
|
+ workspace,
|
||||||
|
+ socketPath
|
||||||
|
+ };
|
||||||
|
+ const message = JSON.stringify({entry});
|
||||||
|
+ const codeServerSocketPath = path.join(os.tmpdir(), 'code-server-ipc.sock');
|
||||||
|
+ await new Promise<void>((resolve, reject) => {
|
||||||
|
+ const opts: _http.RequestOptions = {
|
||||||
|
+ path: '/add-session',
|
||||||
|
+ socketPath: codeServerSocketPath,
|
||||||
|
+ method: 'POST',
|
||||||
|
+ headers: {
|
||||||
|
+ 'content-type': 'application/json',
|
||||||
|
+ }
|
||||||
|
+ };
|
||||||
|
+ const req = _http.request(opts, (res) => {
|
||||||
|
+ res.on('error', reject);
|
||||||
|
+ res.on('end', () => {
|
||||||
|
+ try {
|
||||||
|
+ if (res.statusCode === 200) {
|
||||||
|
+ resolve();
|
||||||
|
+ } else {
|
||||||
|
+ reject(new Error('Unexpected status code: ' + res.statusCode));
|
||||||
|
+ }
|
||||||
|
+ } catch (e: unknown) {
|
||||||
|
+ reject(e);
|
||||||
|
+ }
|
||||||
|
+ });
|
||||||
|
+ });
|
||||||
|
+ req.on('error', reject);
|
||||||
|
+ req.write(message);
|
||||||
|
+ req.end();
|
||||||
|
+ });
|
||||||
|
+ })().catch(error => {
|
||||||
|
+ this._logService.error(error);
|
||||||
|
+ });
|
||||||
|
+
|
||||||
|
// Do this when extension service exists, but extensions are not being activated yet.
|
||||||
|
const configProvider = await this._extHostConfiguration.getConfigProvider();
|
||||||
|
await connectProxyResolver(this._extHostWorkspace, configProvider, this, this._logService, this._mainThreadTelemetryProxy, this._initData);
|
||||||
|
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/lib/vscode/src/vs/workbench/api/node/extensionHostProcess.ts
|
||||||
|
@@ -3,6 +3,9 @@
|
||||||
|
* 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 path from 'vs/base/common/path';
|
||||||
|
import * as nativeWatchdog from 'native-watchdog';
|
||||||
|
import * as net from 'net';
|
||||||
|
import * as minimist from 'minimist';
|
||||||
|
@@ -400,7 +403,28 @@ async function startExtensionHostProcess
|
||||||
|
);
|
||||||
|
|
||||||
|
// rewrite onTerminate-function to be a proper shutdown
|
||||||
|
- onTerminate = (reason: string) => extensionHostMain.terminate(reason);
|
||||||
|
+ onTerminate = (reason: string) => {
|
||||||
|
+ extensionHostMain.terminate(reason);
|
||||||
|
+
|
||||||
|
+ const socketPath = process.env['VSCODE_IPC_HOOK_CLI'];
|
||||||
|
+ if (!socketPath) {
|
||||||
|
+ return;
|
||||||
|
+ }
|
||||||
|
+ const message = JSON.stringify({socketPath});
|
||||||
|
+ const codeServerSocketPath = path.join(os.tmpdir(), 'code-server-ipc.sock');
|
||||||
|
+ const opts: _http.RequestOptions = {
|
||||||
|
+ path: '/delete-session',
|
||||||
|
+ socketPath: codeServerSocketPath,
|
||||||
|
+ method: 'POST',
|
||||||
|
+ headers: {
|
||||||
|
+ 'content-type': 'application/json',
|
||||||
|
+ 'accept': 'application/json'
|
||||||
|
+ }
|
||||||
|
+ };
|
||||||
|
+ const req = _http.request(opts);
|
||||||
|
+ req.write(message);
|
||||||
|
+ req.end();
|
||||||
|
+ };
|
||||||
|
}
|
||||||
|
|
||||||
|
startExtensionHostProcess().catch((err) => console.log(err));
|
||||||
|
@ -9,9 +9,11 @@ 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 { handleUpgrade } from "./wsRouter"
|
import { handleUpgrade } from "./wsRouter"
|
||||||
|
|
||||||
type ListenOptions = Pick<DefaultedArgs, "socket-mode" | "socket" | "port" | "host">
|
type SocketOptions = { socket: string; "socket-mode"?: string }
|
||||||
|
type ListenOptions = DefaultedArgs | SocketOptions
|
||||||
|
|
||||||
export interface App extends Disposable {
|
export interface App extends Disposable {
|
||||||
/** Handles regular HTTP requests. */
|
/** Handles regular HTTP requests. */
|
||||||
@ -20,12 +22,18 @@ export interface App extends Disposable {
|
|||||||
wsRouter: Express
|
wsRouter: Express
|
||||||
/** The underlying HTTP server. */
|
/** The underlying HTTP server. */
|
||||||
server: http.Server
|
server: http.Server
|
||||||
|
/** Handles requests to the editor session management API. */
|
||||||
|
editorSessionManagerServer: http.Server
|
||||||
}
|
}
|
||||||
|
|
||||||
export const listen = async (server: http.Server, { host, port, socket, "socket-mode": mode }: ListenOptions) => {
|
const isSocketOpts = (opts: ListenOptions): opts is SocketOptions => {
|
||||||
if (socket) {
|
return !!(opts as SocketOptions).socket || !(opts as DefaultedArgs).host
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listen = async (server: http.Server, opts: ListenOptions) => {
|
||||||
|
if (isSocketOpts(opts)) {
|
||||||
try {
|
try {
|
||||||
await fs.unlink(socket)
|
await fs.unlink(opts.socket)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
handleArgsSocketCatchError(error)
|
handleArgsSocketCatchError(error)
|
||||||
}
|
}
|
||||||
@ -38,18 +46,20 @@ export const listen = async (server: http.Server, { host, port, socket, "socket-
|
|||||||
server.on("error", (err) => util.logError(logger, "http server error", err))
|
server.on("error", (err) => util.logError(logger, "http server error", err))
|
||||||
resolve()
|
resolve()
|
||||||
}
|
}
|
||||||
if (socket) {
|
if (isSocketOpts(opts)) {
|
||||||
server.listen(socket, onListen)
|
server.listen(opts.socket, onListen)
|
||||||
} else {
|
} else {
|
||||||
// [] is the correct format when using :: but Node errors with them.
|
// [] is the correct format when using :: but Node errors with them.
|
||||||
server.listen(port, host.replace(/^\[|\]$/g, ""), onListen)
|
server.listen(opts.port, opts.host.replace(/^\[|\]$/g, ""), onListen)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// NOTE@jsjoeio: we need to chmod after the server is finished
|
// NOTE@jsjoeio: we need to chmod after the server is finished
|
||||||
// listening. Otherwise, the socket may not have been created yet.
|
// listening. Otherwise, the socket may not have been created yet.
|
||||||
if (socket && mode) {
|
if (isSocketOpts(opts)) {
|
||||||
await fs.chmod(socket, mode)
|
if (opts["socket-mode"]) {
|
||||||
|
await fs.chmod(opts.socket, opts["socket-mode"])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,14 +80,22 @@ export const createApp = async (args: DefaultedArgs): Promise<App> => {
|
|||||||
)
|
)
|
||||||
: http.createServer(router)
|
: http.createServer(router)
|
||||||
|
|
||||||
const dispose = disposer(server)
|
const disposeServer = disposer(server)
|
||||||
|
|
||||||
await listen(server, args)
|
await listen(server, args)
|
||||||
|
|
||||||
const wsRouter = express()
|
const wsRouter = express()
|
||||||
handleUpgrade(wsRouter, server)
|
handleUpgrade(wsRouter, server)
|
||||||
|
|
||||||
return { router, wsRouter, server, dispose }
|
const editorSessionManager = new EditorSessionManager()
|
||||||
|
const editorSessionManagerServer = await makeEditorSessionManagerServer(DEFAULT_SOCKET_PATH, editorSessionManager)
|
||||||
|
const disposeEditorSessionManagerServer = disposer(editorSessionManagerServer)
|
||||||
|
|
||||||
|
const dispose = async () => {
|
||||||
|
await Promise.all([disposeServer(), disposeEditorSessionManagerServer()])
|
||||||
|
}
|
||||||
|
|
||||||
|
return { router, wsRouter, server, dispose, editorSessionManagerServer }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3,17 +3,8 @@ import { promises as fs } from "fs"
|
|||||||
import { load } from "js-yaml"
|
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 {
|
import { generateCertificate, generatePassword, humanPath, paths, splitOnFirstEquals } from "./util"
|
||||||
canConnect,
|
import { DEFAULT_SOCKET_PATH, EditorSessionManagerClient } from "./vscodeSocket"
|
||||||
generateCertificate,
|
|
||||||
generatePassword,
|
|
||||||
humanPath,
|
|
||||||
paths,
|
|
||||||
isNodeJSErrnoException,
|
|
||||||
splitOnFirstEquals,
|
|
||||||
} from "./util"
|
|
||||||
|
|
||||||
const DEFAULT_SOCKET_PATH = path.join(os.tmpdir(), "vscode-ipc")
|
|
||||||
|
|
||||||
export enum Feature {
|
export enum Feature {
|
||||||
// No current experimental features!
|
// No current experimental features!
|
||||||
@ -591,9 +582,7 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config
|
|||||||
}
|
}
|
||||||
args["proxy-domain"] = finalProxies
|
args["proxy-domain"] = finalProxies
|
||||||
|
|
||||||
if (typeof args._ === "undefined") {
|
args._ = getResolvedPathsFromArgs(args)
|
||||||
args._ = []
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...args,
|
...args,
|
||||||
@ -602,6 +591,10 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config
|
|||||||
} as DefaultedArgs // TODO: Technically no guarantee this is fulfilled.
|
} as DefaultedArgs // TODO: Technically no guarantee this is fulfilled.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getResolvedPathsFromArgs(args: UserProvidedArgs): string[] {
|
||||||
|
return (args._ ?? []).map((p) => path.resolve(p))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to return the default config file.
|
* Helper function to return the default config file.
|
||||||
*
|
*
|
||||||
@ -741,27 +734,6 @@ function bindAddrFromAllSources(...argsConfig: UserProvidedArgs[]): Addr {
|
|||||||
return addr
|
return addr
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the socketPath based on path passed in.
|
|
||||||
*
|
|
||||||
* The one usually passed in is the DEFAULT_SOCKET_PATH.
|
|
||||||
*
|
|
||||||
* If it can't read the path, it throws an error and returns undefined.
|
|
||||||
*/
|
|
||||||
export async function readSocketPath(path: string): Promise<string | undefined> {
|
|
||||||
try {
|
|
||||||
return await fs.readFile(path, "utf8")
|
|
||||||
} catch (error) {
|
|
||||||
// If it doesn't exist, we don't care.
|
|
||||||
// But if it fails for some reason, we should throw.
|
|
||||||
// We want to surface that to the user.
|
|
||||||
if (!isNodeJSErrnoException(error) || error.code !== "ENOENT") {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if it looks like the user is trying to open a file or folder in an
|
* Determine if it looks like the user is trying to open a file or folder in an
|
||||||
* existing instance. The arguments here should be the arguments the user
|
* existing instance. The arguments here should be the arguments the user
|
||||||
@ -774,6 +746,14 @@ export const shouldOpenInExistingInstance = async (args: UserProvidedArgs): Prom
|
|||||||
return process.env.VSCODE_IPC_HOOK_CLI
|
return process.env.VSCODE_IPC_HOOK_CLI
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const paths = getResolvedPathsFromArgs(args)
|
||||||
|
const client = new EditorSessionManagerClient(DEFAULT_SOCKET_PATH)
|
||||||
|
|
||||||
|
// 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.
|
||||||
const openInFlagCount = ["reuse-window", "new-window"].reduce((prev, cur) => {
|
const openInFlagCount = ["reuse-window", "new-window"].reduce((prev, cur) => {
|
||||||
@ -781,7 +761,7 @@ export const shouldOpenInExistingInstance = async (args: UserProvidedArgs): Prom
|
|||||||
}, 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 readSocketPath(DEFAULT_SOCKET_PATH)
|
return await client.getConnectedSocketPath(paths[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
@ -790,8 +770,8 @@ export const shouldOpenInExistingInstance = async (args: UserProvidedArgs): Prom
|
|||||||
// 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.
|
||||||
if (Object.keys(args).length === 1 && typeof args._ !== "undefined" && args._.length > 0) {
|
if (Object.keys(args).length === 1 && typeof args._ !== "undefined" && args._.length > 0) {
|
||||||
const socketPath = await readSocketPath(DEFAULT_SOCKET_PATH)
|
const socketPath = await client.getConnectedSocketPath(paths[0])
|
||||||
if (socketPath && (await canConnect(socketPath))) {
|
if (socketPath) {
|
||||||
logger.debug("Found existing code-server socket")
|
logger.debug("Found existing code-server socket")
|
||||||
return socketPath
|
return socketPath
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { field, logger } from "@coder/logger"
|
import { field, logger } from "@coder/logger"
|
||||||
import http from "http"
|
import http from "http"
|
||||||
import * as os from "os"
|
import * as os from "os"
|
||||||
import path from "path"
|
|
||||||
import { Disposable } from "../common/emitter"
|
import { Disposable } from "../common/emitter"
|
||||||
import { plural } from "../common/util"
|
import { plural } from "../common/util"
|
||||||
import { createApp, ensureAddress } from "./app"
|
import { createApp, ensureAddress } from "./app"
|
||||||
@ -70,9 +69,8 @@ export const openInExistingInstance = async (args: DefaultedArgs, socketPath: st
|
|||||||
forceNewWindow: args["new-window"],
|
forceNewWindow: args["new-window"],
|
||||||
gotoLineMode: true,
|
gotoLineMode: true,
|
||||||
}
|
}
|
||||||
const paths = args._ || []
|
for (let i = 0; i < args._.length; i++) {
|
||||||
for (let i = 0; i < paths.length; i++) {
|
const fp = args._[i]
|
||||||
const fp = path.resolve(paths[i])
|
|
||||||
if (await isDirectory(fp)) {
|
if (await isDirectory(fp)) {
|
||||||
pipeArgs.folderURIs.push(fp)
|
pipeArgs.folderURIs.push(fp)
|
||||||
} else {
|
} else {
|
||||||
@ -123,10 +121,12 @@ 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")
|
||||||
|
206
src/node/vscodeSocket.ts
Normal file
206
src/node/vscodeSocket.ts
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
import { logger } from "@coder/logger"
|
||||||
|
import express from "express"
|
||||||
|
import * as http from "http"
|
||||||
|
import * as os from "os"
|
||||||
|
import * as path from "path"
|
||||||
|
import { HttpCode } from "../common/http"
|
||||||
|
import { listen } from "./app"
|
||||||
|
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 {
|
||||||
|
workspace: {
|
||||||
|
id: string
|
||||||
|
folders: {
|
||||||
|
uri: {
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
socketPath: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteSessionRequest {
|
||||||
|
socketPath: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AddSessionRequest {
|
||||||
|
entry: EditorSessionEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetSessionResponse {
|
||||||
|
socketPath?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function makeEditorSessionManagerServer(
|
||||||
|
codeServerSocketPath: string,
|
||||||
|
editorSessionManager: EditorSessionManager,
|
||||||
|
): Promise<http.Server> {
|
||||||
|
const router = express()
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-named-as-default-member
|
||||||
|
router.use(express.json())
|
||||||
|
|
||||||
|
router.get("/session", async (req, res) => {
|
||||||
|
const filePath = req.query.filePath as string
|
||||||
|
if (!filePath) {
|
||||||
|
res.status(HttpCode.BadRequest).send("filePath is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const socketPath = await editorSessionManager.getConnectedSocketPath(filePath)
|
||||||
|
const response: GetSessionResponse = { socketPath }
|
||||||
|
res.json(response)
|
||||||
|
} catch (error: unknown) {
|
||||||
|
res.status(HttpCode.ServerError).send(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
router.post("/add-session", async (req, res) => {
|
||||||
|
const request = req.body as AddSessionRequest
|
||||||
|
if (!request.entry) {
|
||||||
|
res.status(400).send("entry is required")
|
||||||
|
}
|
||||||
|
editorSessionManager.addSession(request.entry)
|
||||||
|
res.status(200).send()
|
||||||
|
})
|
||||||
|
|
||||||
|
router.post("/delete-session", async (req, res) => {
|
||||||
|
const request = req.body as DeleteSessionRequest
|
||||||
|
if (!request.socketPath) {
|
||||||
|
res.status(400).send("socketPath is required")
|
||||||
|
}
|
||||||
|
editorSessionManager.deleteSession(request.socketPath)
|
||||||
|
res.status(200).send()
|
||||||
|
})
|
||||||
|
|
||||||
|
const server = http.createServer(router)
|
||||||
|
await listen(server, { socket: codeServerSocketPath })
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EditorSessionManager {
|
||||||
|
// Map from socket path to EditorSessionEntry.
|
||||||
|
private entries = new Map<string, EditorSessionEntry>()
|
||||||
|
|
||||||
|
addSession(entry: EditorSessionEntry): void {
|
||||||
|
logger.debug(`Adding session to session registry: ${entry.socketPath}`)
|
||||||
|
this.entries.set(entry.socketPath, entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
getCandidatesForFile(filePath: string): EditorSessionEntry[] {
|
||||||
|
const matchCheckResults = new Map<string, boolean>()
|
||||||
|
|
||||||
|
const checkMatch = (entry: EditorSessionEntry): boolean => {
|
||||||
|
if (matchCheckResults.has(entry.socketPath)) {
|
||||||
|
return matchCheckResults.get(entry.socketPath)!
|
||||||
|
}
|
||||||
|
const result = entry.workspace.folders.some((folder) => filePath.startsWith(folder.uri.path + path.sep))
|
||||||
|
matchCheckResults.set(entry.socketPath, result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(this.entries.values())
|
||||||
|
.reverse() // Most recently registered first.
|
||||||
|
.sort((a, b) => {
|
||||||
|
// Matches first.
|
||||||
|
const aMatch = checkMatch(a)
|
||||||
|
const bMatch = checkMatch(b)
|
||||||
|
if (aMatch === bMatch) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if (aMatch) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteSession(socketPath: string): void {
|
||||||
|
logger.debug(`Deleting session from session registry: ${socketPath}`)
|
||||||
|
this.entries.delete(socketPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the best socket path that we can connect to.
|
||||||
|
* We also delete any sockets that we can't connect to.
|
||||||
|
*/
|
||||||
|
async getConnectedSocketPath(filePath: string): Promise<string | undefined> {
|
||||||
|
const candidates = this.getCandidatesForFile(filePath)
|
||||||
|
let match: EditorSessionEntry | undefined = undefined
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
if (await canConnect(candidate.socketPath)) {
|
||||||
|
match = candidate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
this.deleteSession(candidate.socketPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return match?.socketPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EditorSessionManagerClient {
|
||||||
|
constructor(private codeServerSocketPath: string) {}
|
||||||
|
|
||||||
|
async canConnect() {
|
||||||
|
return canConnect(this.codeServerSocketPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getConnectedSocketPath(filePath: string): Promise<string | undefined> {
|
||||||
|
const response = await new Promise<GetSessionResponse>((resolve, reject) => {
|
||||||
|
const opts = {
|
||||||
|
path: "/session?filePath=" + encodeURIComponent(filePath),
|
||||||
|
socketPath: this.codeServerSocketPath,
|
||||||
|
method: "GET",
|
||||||
|
}
|
||||||
|
const req = http.request(opts, (res) => {
|
||||||
|
let rawData = ""
|
||||||
|
res.setEncoding("utf8")
|
||||||
|
res.on("data", (chunk) => {
|
||||||
|
rawData += chunk
|
||||||
|
})
|
||||||
|
res.on("end", () => {
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(rawData)
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
resolve(obj)
|
||||||
|
} else {
|
||||||
|
reject(new Error("Unexpected status code: " + res.statusCode))
|
||||||
|
}
|
||||||
|
} catch (e: unknown) {
|
||||||
|
reject(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
req.on("error", reject)
|
||||||
|
req.end()
|
||||||
|
})
|
||||||
|
return response.socketPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// Currently only used for tests.
|
||||||
|
async addSession(request: AddSessionRequest): Promise<void> {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
const opts = {
|
||||||
|
path: "/add-session",
|
||||||
|
socketPath: this.codeServerSocketPath,
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
accept: "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const req = http.request(opts, () => {
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
req.on("error", reject)
|
||||||
|
req.write(JSON.stringify(request))
|
||||||
|
req.end()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -17,7 +17,7 @@ describe("createApp", () => {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
mockLogger()
|
mockLogger()
|
||||||
|
|
||||||
const testName = "unlink-socket"
|
const testName = "app"
|
||||||
await clean(testName)
|
await clean(testName)
|
||||||
tmpDirPath = await tmpdir(testName)
|
tmpDirPath = await tmpdir(testName)
|
||||||
tmpFilePath = path.join(tmpDirPath, "unlink-socket-file")
|
tmpFilePath = path.join(tmpDirPath, "unlink-socket-file")
|
||||||
@ -103,7 +103,7 @@ describe("createApp", () => {
|
|||||||
|
|
||||||
const app = await createApp(defaultArgs)
|
const app = await createApp(defaultArgs)
|
||||||
|
|
||||||
expect(unlinkSpy).toHaveBeenCalledTimes(1)
|
expect(unlinkSpy).toHaveBeenCalledWith(tmpFilePath)
|
||||||
app.dispose()
|
app.dispose()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
import { Level, logger } from "@coder/logger"
|
import { Level, logger } from "@coder/logger"
|
||||||
import { promises as fs } from "fs"
|
import { promises as fs } from "fs"
|
||||||
import * as net from "net"
|
|
||||||
import * as os from "os"
|
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import {
|
import {
|
||||||
UserProvidedArgs,
|
UserProvidedArgs,
|
||||||
bindAddrFromArgs,
|
bindAddrFromArgs,
|
||||||
defaultConfigFile,
|
defaultConfigFile,
|
||||||
parse,
|
parse,
|
||||||
readSocketPath,
|
|
||||||
setDefaults,
|
setDefaults,
|
||||||
shouldOpenInExistingInstance,
|
shouldOpenInExistingInstance,
|
||||||
toCodeArgs,
|
toCodeArgs,
|
||||||
@ -20,7 +17,13 @@ import {
|
|||||||
} from "../../../src/node/cli"
|
} from "../../../src/node/cli"
|
||||||
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 { clean, useEnv, tmpdir } from "../../utils/helpers"
|
import {
|
||||||
|
DEFAULT_SOCKET_PATH,
|
||||||
|
EditorSessionManager,
|
||||||
|
EditorSessionManagerClient,
|
||||||
|
makeEditorSessionManagerServer,
|
||||||
|
} from "../../../src/node/vscodeSocket"
|
||||||
|
import { clean, useEnv, tmpdir, listenOn } from "../../utils/helpers"
|
||||||
|
|
||||||
// The parser should not set any defaults so the caller can determine what
|
// The parser should not set any defaults so the caller can determine what
|
||||||
// values the user actually set. These are only set after explicitly calling
|
// values the user actually set. These are only set after explicitly calling
|
||||||
@ -487,7 +490,7 @@ describe("parser", () => {
|
|||||||
|
|
||||||
describe("cli", () => {
|
describe("cli", () => {
|
||||||
const testName = "cli"
|
const testName = "cli"
|
||||||
const vscodeIpcPath = path.join(os.tmpdir(), "vscode-ipc")
|
let tmpDirPath: string
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await clean(testName)
|
await clean(testName)
|
||||||
@ -495,7 +498,7 @@ describe("cli", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
delete process.env.VSCODE_IPC_HOOK_CLI
|
delete process.env.VSCODE_IPC_HOOK_CLI
|
||||||
await fs.rm(vscodeIpcPath, { force: true, recursive: true })
|
tmpDirPath = await tmpdir(testName)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should use existing if inside code-server", async () => {
|
it("should use existing if inside code-server", async () => {
|
||||||
@ -509,54 +512,152 @@ describe("cli", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
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 args: UserProvidedArgs = {}
|
const args: UserProvidedArgs = {}
|
||||||
args["reuse-window"] = true
|
args["reuse-window"] = true
|
||||||
await expect(shouldOpenInExistingInstance(args)).resolves.toStrictEqual(undefined)
|
await expect(shouldOpenInExistingInstance(args)).resolves.toStrictEqual(undefined)
|
||||||
|
|
||||||
await fs.writeFile(vscodeIpcPath, "test")
|
const socketPath = path.join(tmpDirPath, "socket")
|
||||||
await expect(shouldOpenInExistingInstance(args)).resolves.toStrictEqual("test")
|
const client = new EditorSessionManagerClient(DEFAULT_SOCKET_PATH)
|
||||||
|
await client.addSession({
|
||||||
|
entry: {
|
||||||
|
workspace: {
|
||||||
|
id: "aaa",
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
uri: {
|
||||||
|
path: "/aaa",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
socketPath,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const vscodeSockets = listenOn(socketPath)
|
||||||
|
|
||||||
|
await expect(shouldOpenInExistingInstance(args)).resolves.toStrictEqual(socketPath)
|
||||||
|
|
||||||
args.port = 8081
|
args.port = 8081
|
||||||
await expect(shouldOpenInExistingInstance(args)).resolves.toStrictEqual("test")
|
await expect(shouldOpenInExistingInstance(args)).resolves.toStrictEqual(socketPath)
|
||||||
|
|
||||||
|
server.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 args: UserProvidedArgs = {}
|
const args: UserProvidedArgs = {}
|
||||||
args["new-window"] = true
|
args["new-window"] = true
|
||||||
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined)
|
await expect(shouldOpenInExistingInstance(args)).resolves.toStrictEqual(undefined)
|
||||||
|
|
||||||
await fs.writeFile(vscodeIpcPath, "test")
|
const socketPath = path.join(tmpDirPath, "socket")
|
||||||
expect(await shouldOpenInExistingInstance(args)).toStrictEqual("test")
|
const client = new EditorSessionManagerClient(DEFAULT_SOCKET_PATH)
|
||||||
|
await client.addSession({
|
||||||
|
entry: {
|
||||||
|
workspace: {
|
||||||
|
id: "aaa",
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
uri: {
|
||||||
|
path: "/aaa",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
socketPath,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const vscodeSockets = listenOn(socketPath)
|
||||||
|
|
||||||
|
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(socketPath)
|
||||||
|
|
||||||
args.port = 8081
|
args.port = 8081
|
||||||
expect(await shouldOpenInExistingInstance(args)).toStrictEqual("test")
|
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(socketPath)
|
||||||
|
|
||||||
|
server.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 args: UserProvidedArgs = {}
|
const args: UserProvidedArgs = {}
|
||||||
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined)
|
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined)
|
||||||
|
|
||||||
args._ = ["./file"]
|
args._ = ["./file"]
|
||||||
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined)
|
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined)
|
||||||
|
|
||||||
const testDir = await tmpdir(testName)
|
const client = new EditorSessionManagerClient(DEFAULT_SOCKET_PATH)
|
||||||
const socketPath = path.join(testDir, "socket")
|
const socketPath = path.join(tmpDirPath, "socket")
|
||||||
await fs.writeFile(vscodeIpcPath, socketPath)
|
await client.addSession({
|
||||||
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined)
|
entry: {
|
||||||
|
workspace: {
|
||||||
await new Promise((resolve) => {
|
id: "aaa",
|
||||||
const server = net.createServer(() => {
|
folders: [
|
||||||
// Close after getting the first connection.
|
{
|
||||||
server.close()
|
uri: {
|
||||||
})
|
path: "/aaa",
|
||||||
server.once("listening", () => resolve(server))
|
},
|
||||||
server.listen(socketPath)
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
socketPath,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
const vscodeSockets = listenOn(socketPath)
|
||||||
|
|
||||||
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(socketPath)
|
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(socketPath)
|
||||||
|
|
||||||
args.port = 8081
|
args.port = 8081
|
||||||
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined)
|
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined)
|
||||||
|
|
||||||
|
server.close()
|
||||||
|
vscodeSockets.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should prefer matching sessions for only the first path", async () => {
|
||||||
|
const server = await makeEditorSessionManagerServer(DEFAULT_SOCKET_PATH, new EditorSessionManager())
|
||||||
|
const client = new EditorSessionManagerClient(DEFAULT_SOCKET_PATH)
|
||||||
|
await client.addSession({
|
||||||
|
entry: {
|
||||||
|
workspace: {
|
||||||
|
id: "aaa",
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
uri: {
|
||||||
|
path: "/aaa",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
socketPath: `${tmpDirPath}/vscode-ipc-aaa.sock`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await client.addSession({
|
||||||
|
entry: {
|
||||||
|
workspace: {
|
||||||
|
id: "bbb",
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
uri: {
|
||||||
|
path: "/bbb",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
socketPath: `${tmpDirPath}/vscode-ipc-bbb.sock`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
listenOn(`${tmpDirPath}/vscode-ipc-aaa.sock`, `${tmpDirPath}/vscode-ipc-bbb.sock`)
|
||||||
|
|
||||||
|
const args: UserProvidedArgs = {}
|
||||||
|
args._ = ["/aaa/file", "/bbb/file"]
|
||||||
|
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(`${tmpDirPath}/vscode-ipc-aaa.sock`)
|
||||||
|
|
||||||
|
server.close()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -729,44 +830,6 @@ cert: false`)
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("readSocketPath", () => {
|
|
||||||
const fileContents = "readSocketPath file contents"
|
|
||||||
let tmpDirPath: string
|
|
||||||
let tmpFilePath: string
|
|
||||||
|
|
||||||
const testName = "readSocketPath"
|
|
||||||
beforeAll(async () => {
|
|
||||||
await clean(testName)
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
tmpDirPath = await tmpdir(testName)
|
|
||||||
tmpFilePath = path.join(tmpDirPath, "readSocketPath.txt")
|
|
||||||
await fs.writeFile(tmpFilePath, fileContents)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should throw an error if it can't read the file", async () => {
|
|
||||||
// TODO@jsjoeio - implement
|
|
||||||
// Test it on a directory.... ESDIR
|
|
||||||
// TODO@jsjoeio - implement
|
|
||||||
expect(() => readSocketPath(tmpDirPath)).rejects.toThrow("EISDIR")
|
|
||||||
})
|
|
||||||
it("should return undefined if it can't read the file", async () => {
|
|
||||||
// TODO@jsjoeio - implement
|
|
||||||
const socketPath = await readSocketPath(path.join(tmpDirPath, "not-a-file"))
|
|
||||||
expect(socketPath).toBeUndefined()
|
|
||||||
})
|
|
||||||
it("should return the file contents", async () => {
|
|
||||||
const contents = await readSocketPath(tmpFilePath)
|
|
||||||
expect(contents).toBe(fileContents)
|
|
||||||
})
|
|
||||||
it("should return the same file contents for two different calls", async () => {
|
|
||||||
const contents1 = await readSocketPath(tmpFilePath)
|
|
||||||
const contents2 = await readSocketPath(tmpFilePath)
|
|
||||||
expect(contents2).toBe(contents1)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("toCodeArgs", () => {
|
describe("toCodeArgs", () => {
|
||||||
const vscodeDefaults = {
|
const vscodeDefaults = {
|
||||||
...defaults,
|
...defaults,
|
||||||
|
243
test/unit/node/vscodeSocket.test.ts
Normal file
243
test/unit/node/vscodeSocket.test.ts
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
import { EditorSessionManager } from "../../../src/node/vscodeSocket"
|
||||||
|
import { clean, tmpdir, listenOn } from "../../utils/helpers"
|
||||||
|
|
||||||
|
describe("EditorSessionManager", () => {
|
||||||
|
let tmpDirPath: string
|
||||||
|
|
||||||
|
const testName = "esm"
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clean(testName)
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
tmpDirPath = await tmpdir(testName)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getCandidatesForFile", () => {
|
||||||
|
it("should prefer the last added socket path for a matching path", async () => {
|
||||||
|
const manager = new EditorSessionManager()
|
||||||
|
manager.addSession({
|
||||||
|
workspace: {
|
||||||
|
id: "aaa",
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
uri: {
|
||||||
|
path: "/aaa",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
socketPath: `${tmpDirPath}/vscode-ipc-aaa-1.sock`,
|
||||||
|
})
|
||||||
|
manager.addSession({
|
||||||
|
workspace: {
|
||||||
|
id: "aaa",
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
uri: {
|
||||||
|
path: "/aaa",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
socketPath: `${tmpDirPath}/vscode-ipc-aaa-2.sock`,
|
||||||
|
})
|
||||||
|
manager.addSession({
|
||||||
|
workspace: {
|
||||||
|
id: "bbb",
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
uri: {
|
||||||
|
path: "/bbb",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
socketPath: `${tmpDirPath}/vscode-ipc-bbb.sock`,
|
||||||
|
})
|
||||||
|
const socketPaths = manager.getCandidatesForFile("/aaa/some-file:1:1")
|
||||||
|
expect(socketPaths.map((x) => x.socketPath)).toEqual([
|
||||||
|
// Matches
|
||||||
|
`${tmpDirPath}/vscode-ipc-aaa-2.sock`,
|
||||||
|
`${tmpDirPath}/vscode-ipc-aaa-1.sock`,
|
||||||
|
// Non-matches
|
||||||
|
`${tmpDirPath}/vscode-ipc-bbb.sock`,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return the last added socketPath if there are no matches", async () => {
|
||||||
|
const manager = new EditorSessionManager()
|
||||||
|
manager.addSession({
|
||||||
|
workspace: {
|
||||||
|
id: "aaa",
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
uri: {
|
||||||
|
path: "/aaa",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
socketPath: `${tmpDirPath}/vscode-ipc-aaa.sock`,
|
||||||
|
})
|
||||||
|
manager.addSession({
|
||||||
|
workspace: {
|
||||||
|
id: "bbb",
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
uri: {
|
||||||
|
path: "/bbb",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
socketPath: `${tmpDirPath}/vscode-ipc-bbb.sock`,
|
||||||
|
})
|
||||||
|
const socketPaths = manager.getCandidatesForFile("/ccc/some-file:1:1")
|
||||||
|
expect(socketPaths.map((x) => x.socketPath)).toEqual([
|
||||||
|
`${tmpDirPath}/vscode-ipc-bbb.sock`,
|
||||||
|
`${tmpDirPath}/vscode-ipc-aaa.sock`,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("does not just directly do a substring match", async () => {
|
||||||
|
const manager = new EditorSessionManager()
|
||||||
|
manager.addSession({
|
||||||
|
workspace: {
|
||||||
|
id: "aaa",
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
uri: {
|
||||||
|
path: "/aaa",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
socketPath: `${tmpDirPath}/vscode-ipc-aaa.sock`,
|
||||||
|
})
|
||||||
|
manager.addSession({
|
||||||
|
workspace: {
|
||||||
|
id: "bbb",
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
uri: {
|
||||||
|
path: "/bbb",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
socketPath: `${tmpDirPath}/vscode-ipc-bbb.sock`,
|
||||||
|
})
|
||||||
|
const entries = manager.getCandidatesForFile("/aaaxxx/some-file:1:1")
|
||||||
|
expect(entries.map((x) => x.socketPath)).toEqual([
|
||||||
|
`${tmpDirPath}/vscode-ipc-bbb.sock`,
|
||||||
|
`${tmpDirPath}/vscode-ipc-aaa.sock`,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getConnectedSocketPath", () => {
|
||||||
|
it("should return socket path if socket is active", async () => {
|
||||||
|
listenOn(`${tmpDirPath}/vscode-ipc-aaa.sock`).once()
|
||||||
|
const manager = new EditorSessionManager()
|
||||||
|
manager.addSession({
|
||||||
|
workspace: {
|
||||||
|
id: "aaa",
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
uri: {
|
||||||
|
path: "/aaa",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
socketPath: `${tmpDirPath}/vscode-ipc-aaa.sock`,
|
||||||
|
})
|
||||||
|
const socketPath = await manager.getConnectedSocketPath("/aaa/some-file:1:1")
|
||||||
|
expect(socketPath).toBe(`${tmpDirPath}/vscode-ipc-aaa.sock`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return undefined if socket is inactive", async () => {
|
||||||
|
const manager = new EditorSessionManager()
|
||||||
|
manager.addSession({
|
||||||
|
workspace: {
|
||||||
|
id: "aaa",
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
uri: {
|
||||||
|
path: "/aaa",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
socketPath: `${tmpDirPath}/vscode-ipc-aaa.sock`,
|
||||||
|
})
|
||||||
|
const socketPath = await manager.getConnectedSocketPath("/aaa/some-file:1:1")
|
||||||
|
expect(socketPath).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return undefined given no matching active sockets", async () => {
|
||||||
|
const vscodeSockets = listenOn(`${tmpDirPath}/vscode-ipc-bbb.sock`)
|
||||||
|
const manager = new EditorSessionManager()
|
||||||
|
manager.addSession({
|
||||||
|
workspace: {
|
||||||
|
id: "aaa",
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
uri: {
|
||||||
|
path: "/aaa",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
socketPath: `${tmpDirPath}/vscode-ipc-aaa.sock`,
|
||||||
|
})
|
||||||
|
const socketPath = await manager.getConnectedSocketPath("/aaa/some-file:1:1")
|
||||||
|
expect(socketPath).toBeUndefined()
|
||||||
|
vscodeSockets.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return undefined if there are no entries", async () => {
|
||||||
|
const manager = new EditorSessionManager()
|
||||||
|
const socketPath = await manager.getConnectedSocketPath("/aaa/some-file:1:1")
|
||||||
|
expect(socketPath).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return most recently used socket path available", async () => {
|
||||||
|
listenOn(`${tmpDirPath}/vscode-ipc-aaa-1.sock`).once()
|
||||||
|
const manager = new EditorSessionManager()
|
||||||
|
manager.addSession({
|
||||||
|
workspace: {
|
||||||
|
id: "aaa",
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
uri: {
|
||||||
|
path: "/aaa",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
socketPath: `${tmpDirPath}/vscode-ipc-aaa-1.sock`,
|
||||||
|
})
|
||||||
|
manager.addSession({
|
||||||
|
workspace: {
|
||||||
|
id: "aaa",
|
||||||
|
folders: [
|
||||||
|
{
|
||||||
|
uri: {
|
||||||
|
path: "/aaa",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
socketPath: `${tmpDirPath}/vscode-ipc-aaa-2.sock`,
|
||||||
|
})
|
||||||
|
|
||||||
|
const socketPath = await manager.getConnectedSocketPath("/aaa/some-file:1:1")
|
||||||
|
expect(socketPath).toBe(`${tmpDirPath}/vscode-ipc-aaa-1.sock`)
|
||||||
|
// Failed sockets should be removed from the entries.
|
||||||
|
expect((manager as any).entries.has(`${tmpDirPath}/vscode-ipc-aaa-2.sock`)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -150,3 +150,52 @@ export function getMaybeProxiedPathname(url: URL): string {
|
|||||||
|
|
||||||
return url.pathname
|
return url.pathname
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FakeVscodeSockets {
|
||||||
|
/* If called, closes all servers after the first connection. */
|
||||||
|
once(): FakeVscodeSockets
|
||||||
|
|
||||||
|
/* Manually close all servers. */
|
||||||
|
close(): Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates servers for each socketPath specified.
|
||||||
|
*/
|
||||||
|
export function listenOn(...socketPaths: string[]): FakeVscodeSockets {
|
||||||
|
let once = false
|
||||||
|
const servers = socketPaths.map((socketPath) => {
|
||||||
|
const server = net.createServer(() => {
|
||||||
|
if (once) {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
server.listen(socketPath)
|
||||||
|
return server
|
||||||
|
})
|
||||||
|
|
||||||
|
async function close() {
|
||||||
|
await Promise.all(
|
||||||
|
servers.map(
|
||||||
|
(server) =>
|
||||||
|
new Promise<void>((resolve, reject) => {
|
||||||
|
server.close((err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const fakeVscodeSockets = {
|
||||||
|
close,
|
||||||
|
once: () => {
|
||||||
|
once = true
|
||||||
|
return fakeVscodeSockets
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fakeVscodeSockets
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user