Skip to content

Commit b61e5d6

Browse files
committed
Prefer matching editor sessions when opening files.
1 parent 3f7db15 commit b61e5d6

File tree

6 files changed

+439
-106
lines changed

6 files changed

+439
-106
lines changed

patches/store-socket.diff

+32-10
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,36 @@ Index: code-server/lib/vscode/src/vs/workbench/api/node/extHostExtensionService.
2424
import * as performance from 'vs/base/common/performance';
2525
import { createApiFactoryAndRegisterActors } from 'vs/workbench/api/common/extHost.api.impl';
2626
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);
34-
+ });
35-
}
27+
@@ -17,6 +19,7 @@ import { ExtensionRuntime } from 'vs/wor
28+
import { CLIServer } from 'vs/workbench/api/node/extHostCLIServer';
29+
import { realpathSync } from 'vs/base/node/extpath';
30+
import { ExtHostConsoleForwarder } from 'vs/workbench/api/node/extHostConsoleForwarder';
31+
+import { IExtHostWorkspace } from '../common/extHostWorkspace';
32+
33+
class NodeModuleRequireInterceptor extends RequireInterceptor {
3634

37-
// Module loading tricks
35+
@@ -79,6 +82,24 @@ export class ExtHostExtensionService ext
36+
await interceptor.install();
37+
performance.mark('code/extHost/didInitAPI');
38+
39+
+ (async () => {
40+
+ let socketPath = process.env['VSCODE_IPC_HOOK_CLI'];
41+
+ if (!socketPath) {
42+
+ return
43+
+ }
44+
+ const workspace = this._instaService.invokeFunction((accessor) => {
45+
+ const workspaceService = accessor.get(IExtHostWorkspace);
46+
+ return workspaceService.workspace;
47+
+ });
48+
+ const entry = {
49+
+ workspace,
50+
+ socketPath
51+
+ };
52+
+ fs.appendFile(path.join(os.tmpdir(), 'vscode-ipc'), '\n' + JSON.stringify(entry), 'utf-8');
53+
+ })().catch(error => {
54+
+ this._logService.error(error);
55+
+ });
56+
+
57+
// Do this when extension service exists, but extensions are not being activated yet.
58+
const configProvider = await this._extHostConfiguration.getConfigProvider();
59+
await connectProxyResolver(this._extHostWorkspace, configProvider, this, this._logService, this._mainThreadTelemetryProxy, this._initData);

src/node/cli.ts

+11-35
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,9 @@ 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, VscodeSocketResolver } from "./vscodeSocket"
8+
import { getResolvedPathsFromArgs } from "./main"
179

1810
export enum Feature {
1911
// No current experimental features!
@@ -732,27 +724,6 @@ function bindAddrFromAllSources(...argsConfig: UserProvidedArgs[]): Addr {
732724
return addr
733725
}
734726

735-
/**
736-
* Reads the socketPath based on path passed in.
737-
*
738-
* The one usually passed in is the DEFAULT_SOCKET_PATH.
739-
*
740-
* If it can't read the path, it throws an error and returns undefined.
741-
*/
742-
export async function readSocketPath(path: string): Promise<string | undefined> {
743-
try {
744-
return await fs.readFile(path, "utf8")
745-
} catch (error) {
746-
// If it doesn't exist, we don't care.
747-
// But if it fails for some reason, we should throw.
748-
// We want to surface that to the user.
749-
if (!isNodeJSErrnoException(error) || error.code !== "ENOENT") {
750-
throw error
751-
}
752-
}
753-
return undefined
754-
}
755-
756727
/**
757728
* Determine if it looks like the user is trying to open a file or folder in an
758729
* existing instance. The arguments here should be the arguments the user
@@ -765,14 +736,18 @@ export const shouldOpenInExistingInstance = async (args: UserProvidedArgs): Prom
765736
return process.env.VSCODE_IPC_HOOK_CLI
766737
}
767738

739+
const paths = getResolvedPathsFromArgs(args)
740+
const resolver = new VscodeSocketResolver(DEFAULT_SOCKET_PATH)
741+
768742
// If these flags are set then assume the user is trying to open in an
769743
// existing instance since these flags have no effect otherwise.
770744
const openInFlagCount = ["reuse-window", "new-window"].reduce((prev, cur) => {
771745
return args[cur as keyof UserProvidedArgs] ? prev + 1 : prev
772746
}, 0)
773747
if (openInFlagCount > 0) {
774748
logger.debug("Found --reuse-window or --new-window")
775-
return readSocketPath(DEFAULT_SOCKET_PATH)
749+
await resolver.load()
750+
return await resolver.getConnectedSocketPath(paths)
776751
}
777752

778753
// It's possible the user is trying to spawn another instance of code-server.
@@ -781,8 +756,9 @@ export const shouldOpenInExistingInstance = async (args: UserProvidedArgs): Prom
781756
// 2. That a file or directory was passed.
782757
// 3. That the socket is active.
783758
if (Object.keys(args).length === 1 && typeof args._ !== "undefined" && args._.length > 0) {
784-
const socketPath = await readSocketPath(DEFAULT_SOCKET_PATH)
785-
if (socketPath && (await canConnect(socketPath))) {
759+
await resolver.load()
760+
const socketPath = await resolver.getConnectedSocketPath(paths)
761+
if (socketPath) {
786762
logger.debug("Found existing code-server socket")
787763
return socketPath
788764
}

src/node/main.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ export const runCodeCli = async (args: DefaultedArgs): Promise<void> => {
6161
process.exit(0)
6262
}
6363

64+
export const getResolvedPathsFromArgs = (args: UserProvidedArgs): string[] => {
65+
const paths = args._ || []
66+
return paths.map((p) => path.resolve(p))
67+
}
68+
6469
export const openInExistingInstance = async (args: DefaultedArgs, socketPath: string): Promise<void> => {
6570
const pipeArgs: OpenCommandPipeArgs & { fileURIs: string[] } = {
6671
type: "open",
@@ -70,9 +75,9 @@ export const openInExistingInstance = async (args: DefaultedArgs, socketPath: st
7075
forceNewWindow: args["new-window"],
7176
gotoLineMode: true,
7277
}
73-
const paths = args._ || []
78+
const paths = getResolvedPathsFromArgs(args)
7479
for (let i = 0; i < paths.length; i++) {
75-
const fp = path.resolve(paths[i])
80+
const fp = paths[i]
7681
if (await isDirectory(fp)) {
7782
pipeArgs.folderURIs.push(fp)
7883
} else {

src/node/vscodeSocket.ts

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { logger } from "@coder/logger"
2+
import * as os from "os"
3+
import * as path from "path"
4+
import { promises as fs } from "fs"
5+
import { isNodeJSErrnoException } from "./util"
6+
import { canConnect } from "./util"
7+
8+
export const DEFAULT_SOCKET_PATH = path.join(os.tmpdir(), "vscode-ipc")
9+
10+
export interface VscodeSocket {
11+
workspace: {
12+
id: string
13+
folders: {
14+
uri: {
15+
path: string
16+
}
17+
}[]
18+
}
19+
20+
socketPath: string
21+
}
22+
23+
export class VscodeSocketResolver {
24+
// Map from folder path to socket path.
25+
private pathMap = new Map<string, string[]>()
26+
27+
// Map from socket path to VscodeSocket.
28+
private entries = new Map<string, { index: number; entry: VscodeSocket }>()
29+
30+
constructor(private readonly socketRegistryFilePath: string) {}
31+
32+
async load(): Promise<void> {
33+
try {
34+
const data = await fs.readFile(this.socketRegistryFilePath, "utf8")
35+
const lines = data.split("\n")
36+
let entries = []
37+
for (const line of lines) {
38+
if (!line) {
39+
continue
40+
}
41+
entries.push(JSON.parse(line) as VscodeSocket)
42+
}
43+
this.loadFromEntries(entries)
44+
} catch (error) {
45+
// If it doesn't exist, we don't care.
46+
// But if it fails for some reason, we should throw.
47+
// We want to surface that to the user.
48+
if (!isNodeJSErrnoException(error) || error.code !== "ENOENT") {
49+
throw error
50+
}
51+
}
52+
}
53+
54+
private loadFromEntries(entries: VscodeSocket[]): void {
55+
for (let i = 0; i < entries.length; i++) {
56+
const entry = entries[i]
57+
this.entries.set(entry.socketPath, { index: i, entry })
58+
for (const folder of entry.workspace.folders) {
59+
if (this.pathMap.has(folder.uri.path)) {
60+
this.pathMap.get(folder.uri.path)!.push(entry.socketPath)
61+
} else {
62+
this.pathMap.set(folder.uri.path, [entry.socketPath])
63+
}
64+
}
65+
}
66+
}
67+
68+
/**
69+
* Returns matching and then non-matching socket paths, sorted by most recently registered.
70+
*/
71+
getSocketPaths(paths: string[] = []): string[] {
72+
const matches = new Set<string>()
73+
for (const socketPath of paths.flatMap((p) => this.getMatchingSocketPaths(p))) {
74+
matches.add(socketPath)
75+
}
76+
77+
// Sort by most recently registered, preferring matches.
78+
return Array.from(this.entries.values())
79+
.sort((a, b) => {
80+
if (matches.has(a.entry.socketPath) && matches.has(b.entry.socketPath)) {
81+
return b.index - a.index
82+
}
83+
if (matches.has(a.entry.socketPath)) {
84+
return -1
85+
}
86+
if (matches.has(b.entry.socketPath)) {
87+
return 1
88+
}
89+
return b.index - a.index
90+
})
91+
.map(({ entry }) => entry.socketPath)
92+
}
93+
94+
/**
95+
* Returns the best socket path that we can connect to.
96+
* See getSocketPaths for the order of preference.
97+
* We also delete any sockets that we can't connect to.
98+
*/
99+
async getConnectedSocketPath(paths: string[] = []): Promise<string | undefined> {
100+
const socketPaths = this.getSocketPaths(paths)
101+
let ret = undefined
102+
103+
const toDelete = new Set<string>()
104+
// Connect to the most recently registered socket first.
105+
for (let i = 0; i < socketPaths.length; i++) {
106+
const socketPath = socketPaths[i]
107+
if (await canConnect(socketPath)) {
108+
ret = socketPath
109+
break
110+
}
111+
toDelete.add(socketPath)
112+
}
113+
114+
if (toDelete.size === 0) {
115+
return ret
116+
}
117+
118+
// Remove any sockets that we couldn't connect to.
119+
for (const socketPath of toDelete) {
120+
logger.debug(`Deleting stale socket from socket registry: ${socketPath}`)
121+
this.entries.delete(socketPath)
122+
}
123+
const newEntries = Array.from(this.entries.values()).map(({ entry }) => entry)
124+
this.pathMap = new Map()
125+
this.entries = new Map()
126+
this.loadFromEntries(newEntries)
127+
await this.save()
128+
129+
return ret
130+
}
131+
132+
private getMatchingSocketPaths(p: string): string[] {
133+
return Array.from(this.pathMap.entries())
134+
.filter(([folder]) => p.startsWith(folder))
135+
.flatMap(([, socketPaths]) => socketPaths)
136+
}
137+
138+
async save(): Promise<void> {
139+
const lines = Array.from(this.entries.values()).map(({ entry }) => JSON.stringify(entry))
140+
return fs.writeFile(this.socketRegistryFilePath, lines.join("\n"))
141+
}
142+
}

0 commit comments

Comments
 (0)