Skip to content

Commit fb73742

Browse files
authored
Prefer matching editor sessions when opening files. (#6191)
Signed-off-by: Sean Lee <[email protected]>
1 parent ccb0d3a commit fb73742

File tree

9 files changed

+785
-130
lines changed

9 files changed

+785
-130
lines changed

patches/store-socket.diff

+108-12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Store a static reference to the IPC socket
1+
Store the IPC socket with workspace metadata.
22

33
This lets us use it to open files inside code-server from outside of
44
code-server.
@@ -9,6 +9,8 @@ To test this:
99

1010
It should open in your existing code-server instance.
1111

12+
When the extension host is terminated, the socket is unregistered.
13+
1214
Index: code-server/lib/vscode/src/vs/workbench/api/node/extHostExtensionService.ts
1315
===================================================================
1416
--- 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.
1820
* Licensed under the MIT License. See License.txt in the project root for license information.
1921
*--------------------------------------------------------------------------------------------*/
2022
-
21-
+import { promises as fs } from 'fs';
22-
+import * as os from 'os'
23+
+import * as os from 'os';
24+
+import * as _http from 'http';
2325
+import * as path from 'vs/base/common/path';
2426
import * as performance from 'vs/base/common/performance';
2527
import { createApiFactoryAndRegisterActors } from 'vs/workbench/api/common/extHost.api.impl';
2628
import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor';
27-
@@ -72,6 +74,10 @@ export class ExtHostExtensionService ext
28-
if (this._initData.remote.isRemote && this._initData.remote.authority) {
29-
const cliServer = this._instaService.createInstance(CLIServer);
30-
process.env['VSCODE_IPC_HOOK_CLI'] = cliServer.ipcHandlePath;
31-
+
32-
+ fs.writeFile(path.join(os.tmpdir(), 'vscode-ipc'), cliServer.ipcHandlePath).catch((error) => {
33-
+ this._logService.error(error);
29+
@@ -17,6 +19,7 @@ import { ExtensionRuntime } from 'vs/wor
30+
import { CLIServer } from 'vs/workbench/api/node/extHostCLIServer';
31+
import { realpathSync } from 'vs/base/node/extpath';
32+
import { ExtHostConsoleForwarder } from 'vs/workbench/api/node/extHostConsoleForwarder';
33+
+import { IExtHostWorkspace } from '../common/extHostWorkspace';
34+
35+
class NodeModuleRequireInterceptor extends RequireInterceptor {
36+
37+
@@ -79,6 +82,52 @@ export class ExtHostExtensionService ext
38+
await interceptor.install();
39+
performance.mark('code/extHost/didInitAPI');
40+
41+
+ (async () => {
42+
+ const socketPath = process.env['VSCODE_IPC_HOOK_CLI'];
43+
+ if (!socketPath) {
44+
+ return;
45+
+ }
46+
+ const workspace = this._instaService.invokeFunction((accessor) => {
47+
+ const workspaceService = accessor.get(IExtHostWorkspace);
48+
+ return workspaceService.workspace;
49+
+ });
50+
+ const entry = {
51+
+ workspace,
52+
+ socketPath
53+
+ };
54+
+ const message = JSON.stringify({entry});
55+
+ const codeServerSocketPath = path.join(os.tmpdir(), 'code-server-ipc.sock');
56+
+ await new Promise<void>((resolve, reject) => {
57+
+ const opts: _http.RequestOptions = {
58+
+ path: '/add-session',
59+
+ socketPath: codeServerSocketPath,
60+
+ method: 'POST',
61+
+ headers: {
62+
+ 'content-type': 'application/json',
63+
+ }
64+
+ };
65+
+ const req = _http.request(opts, (res) => {
66+
+ res.on('error', reject);
67+
+ res.on('end', () => {
68+
+ try {
69+
+ if (res.statusCode === 200) {
70+
+ resolve();
71+
+ } else {
72+
+ reject(new Error('Unexpected status code: ' + res.statusCode));
73+
+ }
74+
+ } catch (e: unknown) {
75+
+ reject(e);
76+
+ }
77+
+ });
78+
+ });
79+
+ req.on('error', reject);
80+
+ req.write(message);
81+
+ req.end();
3482
+ });
35-
}
83+
+ })().catch(error => {
84+
+ this._logService.error(error);
85+
+ });
86+
+
87+
// Do this when extension service exists, but extensions are not being activated yet.
88+
const configProvider = await this._extHostConfiguration.getConfigProvider();
89+
await connectProxyResolver(this._extHostWorkspace, configProvider, this, this._logService, this._mainThreadTelemetryProxy, this._initData);
90+
Index: code-server/lib/vscode/src/vs/workbench/api/node/extensionHostProcess.ts
91+
===================================================================
92+
--- code-server.orig/lib/vscode/src/vs/workbench/api/node/extensionHostProcess.ts
93+
+++ code-server/lib/vscode/src/vs/workbench/api/node/extensionHostProcess.ts
94+
@@ -3,6 +3,9 @@
95+
* Licensed under the MIT License. See License.txt in the project root for license information.
96+
*--------------------------------------------------------------------------------------------*/
97+
98+
+import * as os from 'os';
99+
+import * as _http from 'http';
100+
+import * as path from 'vs/base/common/path';
101+
import * as nativeWatchdog from 'native-watchdog';
102+
import * as net from 'net';
103+
import * as minimist from 'minimist';
104+
@@ -400,7 +403,28 @@ async function startExtensionHostProcess
105+
);
106+
107+
// rewrite onTerminate-function to be a proper shutdown
108+
- onTerminate = (reason: string) => extensionHostMain.terminate(reason);
109+
+ onTerminate = (reason: string) => {
110+
+ extensionHostMain.terminate(reason);
111+
+
112+
+ const socketPath = process.env['VSCODE_IPC_HOOK_CLI'];
113+
+ if (!socketPath) {
114+
+ return;
115+
+ }
116+
+ const message = JSON.stringify({socketPath});
117+
+ const codeServerSocketPath = path.join(os.tmpdir(), 'code-server-ipc.sock');
118+
+ const opts: _http.RequestOptions = {
119+
+ path: '/delete-session',
120+
+ socketPath: codeServerSocketPath,
121+
+ method: 'POST',
122+
+ headers: {
123+
+ 'content-type': 'application/json',
124+
+ 'accept': 'application/json'
125+
+ }
126+
+ };
127+
+ const req = _http.request(opts);
128+
+ req.write(message);
129+
+ req.end();
130+
+ };
131+
}
36132

37-
// Module loading tricks
133+
startExtensionHostProcess().catch((err) => console.log(err));

src/node/app.ts

+29-11
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import * as util from "../common/util"
99
import { DefaultedArgs } from "./cli"
1010
import { disposer } from "./http"
1111
import { isNodeJSErrnoException } from "./util"
12+
import { DEFAULT_SOCKET_PATH, EditorSessionManager, makeEditorSessionManagerServer } from "./vscodeSocket"
1213
import { handleUpgrade } from "./wsRouter"
1314

14-
type ListenOptions = Pick<DefaultedArgs, "socket-mode" | "socket" | "port" | "host">
15+
type SocketOptions = { socket: string; "socket-mode"?: string }
16+
type ListenOptions = DefaultedArgs | SocketOptions
1517

1618
export interface App extends Disposable {
1719
/** Handles regular HTTP requests. */
@@ -20,12 +22,18 @@ export interface App extends Disposable {
2022
wsRouter: Express
2123
/** The underlying HTTP server. */
2224
server: http.Server
25+
/** Handles requests to the editor session management API. */
26+
editorSessionManagerServer: http.Server
2327
}
2428

25-
export const listen = async (server: http.Server, { host, port, socket, "socket-mode": mode }: ListenOptions) => {
26-
if (socket) {
29+
const isSocketOpts = (opts: ListenOptions): opts is SocketOptions => {
30+
return !!(opts as SocketOptions).socket || !(opts as DefaultedArgs).host
31+
}
32+
33+
export const listen = async (server: http.Server, opts: ListenOptions) => {
34+
if (isSocketOpts(opts)) {
2735
try {
28-
await fs.unlink(socket)
36+
await fs.unlink(opts.socket)
2937
} catch (error: any) {
3038
handleArgsSocketCatchError(error)
3139
}
@@ -38,18 +46,20 @@ export const listen = async (server: http.Server, { host, port, socket, "socket-
3846
server.on("error", (err) => util.logError(logger, "http server error", err))
3947
resolve()
4048
}
41-
if (socket) {
42-
server.listen(socket, onListen)
49+
if (isSocketOpts(opts)) {
50+
server.listen(opts.socket, onListen)
4351
} else {
4452
// [] is the correct format when using :: but Node errors with them.
45-
server.listen(port, host.replace(/^\[|\]$/g, ""), onListen)
53+
server.listen(opts.port, opts.host.replace(/^\[|\]$/g, ""), onListen)
4654
}
4755
})
4856

4957
// NOTE@jsjoeio: we need to chmod after the server is finished
5058
// listening. Otherwise, the socket may not have been created yet.
51-
if (socket && mode) {
52-
await fs.chmod(socket, mode)
59+
if (isSocketOpts(opts)) {
60+
if (opts["socket-mode"]) {
61+
await fs.chmod(opts.socket, opts["socket-mode"])
62+
}
5363
}
5464
}
5565

@@ -70,14 +80,22 @@ export const createApp = async (args: DefaultedArgs): Promise<App> => {
7080
)
7181
: http.createServer(router)
7282

73-
const dispose = disposer(server)
83+
const disposeServer = disposer(server)
7484

7585
await listen(server, args)
7686

7787
const wsRouter = express()
7888
handleUpgrade(wsRouter, server)
7989

80-
return { router, wsRouter, server, dispose }
90+
const editorSessionManager = new EditorSessionManager()
91+
const editorSessionManagerServer = await makeEditorSessionManagerServer(DEFAULT_SOCKET_PATH, editorSessionManager)
92+
const disposeEditorSessionManagerServer = disposer(editorSessionManagerServer)
93+
94+
const dispose = async () => {
95+
await Promise.all([disposeServer(), disposeEditorSessionManagerServer()])
96+
}
97+
98+
return { router, wsRouter, server, dispose, editorSessionManagerServer }
8199
}
82100

83101
/**

src/node/cli.ts

+18-38
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,8 @@ import { promises as fs } from "fs"
33
import { load } from "js-yaml"
44
import * as os from "os"
55
import * as path from "path"
6-
import {
7-
canConnect,
8-
generateCertificate,
9-
generatePassword,
10-
humanPath,
11-
paths,
12-
isNodeJSErrnoException,
13-
splitOnFirstEquals,
14-
} from "./util"
15-
16-
const DEFAULT_SOCKET_PATH = path.join(os.tmpdir(), "vscode-ipc")
6+
import { generateCertificate, generatePassword, humanPath, paths, splitOnFirstEquals } from "./util"
7+
import { DEFAULT_SOCKET_PATH, EditorSessionManagerClient } from "./vscodeSocket"
178

189
export enum Feature {
1910
// No current experimental features!
@@ -591,9 +582,7 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config
591582
}
592583
args["proxy-domain"] = finalProxies
593584

594-
if (typeof args._ === "undefined") {
595-
args._ = []
596-
}
585+
args._ = getResolvedPathsFromArgs(args)
597586

598587
return {
599588
...args,
@@ -602,6 +591,10 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config
602591
} as DefaultedArgs // TODO: Technically no guarantee this is fulfilled.
603592
}
604593

594+
export function getResolvedPathsFromArgs(args: UserProvidedArgs): string[] {
595+
return (args._ ?? []).map((p) => path.resolve(p))
596+
}
597+
605598
/**
606599
* Helper function to return the default config file.
607600
*
@@ -741,27 +734,6 @@ function bindAddrFromAllSources(...argsConfig: UserProvidedArgs[]): Addr {
741734
return addr
742735
}
743736

744-
/**
745-
* Reads the socketPath based on path passed in.
746-
*
747-
* The one usually passed in is the DEFAULT_SOCKET_PATH.
748-
*
749-
* If it can't read the path, it throws an error and returns undefined.
750-
*/
751-
export async function readSocketPath(path: string): Promise<string | undefined> {
752-
try {
753-
return await fs.readFile(path, "utf8")
754-
} catch (error) {
755-
// If it doesn't exist, we don't care.
756-
// But if it fails for some reason, we should throw.
757-
// We want to surface that to the user.
758-
if (!isNodeJSErrnoException(error) || error.code !== "ENOENT") {
759-
throw error
760-
}
761-
}
762-
return undefined
763-
}
764-
765737
/**
766738
* Determine if it looks like the user is trying to open a file or folder in an
767739
* existing instance. The arguments here should be the arguments the user
@@ -774,14 +746,22 @@ export const shouldOpenInExistingInstance = async (args: UserProvidedArgs): Prom
774746
return process.env.VSCODE_IPC_HOOK_CLI
775747
}
776748

749+
const paths = getResolvedPathsFromArgs(args)
750+
const client = new EditorSessionManagerClient(DEFAULT_SOCKET_PATH)
751+
752+
// If we can't connect to the socket then there's no existing instance.
753+
if (!(await client.canConnect())) {
754+
return undefined
755+
}
756+
777757
// If these flags are set then assume the user is trying to open in an
778758
// existing instance since these flags have no effect otherwise.
779759
const openInFlagCount = ["reuse-window", "new-window"].reduce((prev, cur) => {
780760
return args[cur as keyof UserProvidedArgs] ? prev + 1 : prev
781761
}, 0)
782762
if (openInFlagCount > 0) {
783763
logger.debug("Found --reuse-window or --new-window")
784-
return readSocketPath(DEFAULT_SOCKET_PATH)
764+
return await client.getConnectedSocketPath(paths[0])
785765
}
786766

787767
// 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
790770
// 2. That a file or directory was passed.
791771
// 3. That the socket is active.
792772
if (Object.keys(args).length === 1 && typeof args._ !== "undefined" && args._.length > 0) {
793-
const socketPath = await readSocketPath(DEFAULT_SOCKET_PATH)
794-
if (socketPath && (await canConnect(socketPath))) {
773+
const socketPath = await client.getConnectedSocketPath(paths[0])
774+
if (socketPath) {
795775
logger.debug("Found existing code-server socket")
796776
return socketPath
797777
}

src/node/main.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { field, logger } from "@coder/logger"
22
import http from "http"
33
import * as os from "os"
4-
import path from "path"
54
import { Disposable } from "../common/emitter"
65
import { plural } from "../common/util"
76
import { createApp, ensureAddress } from "./app"
@@ -70,9 +69,8 @@ export const openInExistingInstance = async (args: DefaultedArgs, socketPath: st
7069
forceNewWindow: args["new-window"],
7170
gotoLineMode: true,
7271
}
73-
const paths = args._ || []
74-
for (let i = 0; i < paths.length; i++) {
75-
const fp = path.resolve(paths[i])
72+
for (let i = 0; i < args._.length; i++) {
73+
const fp = args._[i]
7674
if (await isDirectory(fp)) {
7775
pipeArgs.folderURIs.push(fp)
7876
} else {
@@ -123,10 +121,12 @@ export const runCodeServer = async (
123121
const app = await createApp(args)
124122
const protocol = args.cert ? "https" : "http"
125123
const serverAddress = ensureAddress(app.server, protocol)
124+
const sessionServerAddress = app.editorSessionManagerServer.address()
126125
const disposeRoutes = await register(app, args)
127126

128127
logger.info(`Using config file ${humanPath(os.homedir(), args.config)}`)
129128
logger.info(`${protocol.toUpperCase()} server listening on ${serverAddress.toString()}`)
129+
logger.info(`Session server listening on ${sessionServerAddress?.toString()}`)
130130

131131
if (args.auth === AuthType.Password) {
132132
logger.info(" - Authentication is enabled")

0 commit comments

Comments
 (0)