Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit ea70a42

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

File tree

6 files changed

+439
-106
lines changed

6 files changed

+439
-106
lines changed

‎patches/store-socket.diff‎

Lines changed: 32 additions & 10 deletions
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‎

Lines changed: 11 additions & 35 deletions
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‎

Lines changed: 7 additions & 2 deletions
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‎

Lines changed: 142 additions & 0 deletions
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 EditorSessionEntry {
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 EditorSessionEntry.
28+
private entries = new Map<string, { index: number; entry: EditorSessionEntry }>()
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 EditorSessionEntry)
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: EditorSessionEntry[]): 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)

AltStyle によって変換されたページ (->オリジナル) /