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 fb73742

Browse files
authored
Prefer matching editor sessions when opening files. (#6191)
Signed-off-by: Sean Lee <freshdried@gmail.com>
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‎

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

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

Lines changed: 18 additions & 38 deletions
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 awaitclient.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&&(awaitcanConnect(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‎

Lines changed: 4 additions & 4 deletions
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)

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