Movatterモバイル変換


[0]ホーム

URL:


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

Commite120cb5

Browse files
committed
Extract SSH process discovery and network status display into a
dedicated SshProcessMonitor class. Add centralized Remote SSH extensiondetection to better support Cursor, Windsurf, and other VS Code forks.Key changes:- Extract SSH monitoring logic from remote.ts into sshProcess.ts- Add sshExtension.ts to detect installed Remote SSH extension- Use createRequire instead of private module._load API- Fix port detection to find most recent port (handles reconnects)- Add Cursor's "Socks port:" log format to port regex
1 parent91d481e commite120cb5

File tree

5 files changed

+579
-221
lines changed

5 files changed

+579
-221
lines changed

‎src/extension.ts‎

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
importaxios,{isAxiosError}from"axios";
44
import{getErrorMessage}from"coder/site/src/api/errors";
5-
import*asmodulefrom"module";
5+
import{createRequire}from"node:module";
6+
import*aspathfrom"node:path";
67
import*asvscodefrom"vscode";
78

89
import{errToStr}from"./api/api-helper";
@@ -14,6 +15,7 @@ import { AuthAction } from "./core/secretsManager";
1415
import{CertificateError,getErrorDetail}from"./error";
1516
import{maybeAskUrl}from"./promptUtils";
1617
import{Remote}from"./remote/remote";
18+
import{getRemoteSshExtension}from"./remote/sshExtension";
1719
import{toSafeHost}from"./util";
1820
import{
1921
WorkspaceProvider,
@@ -33,30 +35,21 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
3335
// Cursor and VSCode are covered by ms remote, and the only other is windsurf for now
3436
// Means that vscodium is not supported by this for now
3537

36-
constremoteSSHExtension=
37-
vscode.extensions.getExtension("jeanp413.open-remote-ssh")||
38-
vscode.extensions.getExtension("codeium.windsurf-remote-openssh")||
39-
vscode.extensions.getExtension("anysphere.remote-ssh")||
40-
vscode.extensions.getExtension("ms-vscode-remote.remote-ssh")||
41-
vscode.extensions.getExtension("google.antigravity-remote-openssh");
38+
constremoteSshExtension=getRemoteSshExtension();
4239

4340
letvscodeProposed:typeofvscode=vscode;
4441

45-
if(!remoteSSHExtension){
42+
if(remoteSshExtension){
43+
constextensionRequire=createRequire(
44+
path.join(remoteSshExtension.extensionPath,"package.json"),
45+
);
46+
vscodeProposed=extensionRequire("vscode");
47+
}else{
4648
vscode.window.showErrorMessage(
4749
"Remote SSH extension not found, this may not work as expected.\n"+
4850
// NB should we link to documentation or marketplace?
4951
"Please install your choice of Remote SSH extension from the VS Code Marketplace.",
5052
);
51-
}else{
52-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
53-
vscodeProposed=(moduleasany)._load(
54-
"vscode",
55-
{
56-
filename:remoteSSHExtension.extensionPath,
57-
},
58-
false,
59-
);
6053
}
6154

6255
constserviceContainer=newServiceContainer(ctx,vscodeProposed);
@@ -366,11 +359,12 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
366359
// after the Coder extension is installed, instead of throwing a fatal error
367360
// (this would require the user to uninstall the Coder extension and
368361
// reinstall after installing the remote SSH extension, which is annoying)
369-
if(remoteSSHExtension&&vscodeProposed.env.remoteAuthority){
362+
if(remoteSshExtension&&vscodeProposed.env.remoteAuthority){
370363
try{
371364
constdetails=awaitremote.setup(
372365
vscodeProposed.env.remoteAuthority,
373366
isFirstConnect,
367+
remoteSshExtension.id,
374368
);
375369
if(details){
376370
ctx.subscriptions.push(details);

‎src/remote/remote.ts‎

Lines changed: 41 additions & 191 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@ import {
44
typeWorkspace,
55
typeWorkspaceAgent,
66
}from"coder/site/src/api/typesGenerated";
7-
importfindfrom"find-process";
87
import*asjsoncfrom"jsonc-parser";
98
import*asfsfrom"node:fs/promises";
109
import*asosfrom"node:os";
1110
import*aspathfrom"node:path";
12-
importprettyBytesfrom"pretty-bytes";
1311
import*assemverfrom"semver";
1412
import*asvscodefrom"vscode";
1513

@@ -36,12 +34,12 @@ import {
3634
AuthorityPrefix,
3735
escapeCommandArg,
3836
expandPath,
39-
findPort,
4037
parseRemoteAuthority,
4138
}from"../util";
4239
import{WorkspaceMonitor}from"../workspace/workspaceMonitor";
4340

4441
import{SSHConfig,typeSSHValues,mergeSSHConfigValues}from"./sshConfig";
42+
import{SshProcessMonitor}from"./sshProcess";
4543
import{computeSSHProperties,sshSupportsSetEnv}from"./sshSupport";
4644
import{WorkspaceStateMachine}from"./workspaceStateMachine";
4745

@@ -109,6 +107,7 @@ export class Remote {
109107
publicasyncsetup(
110108
remoteAuthority:string,
111109
firstConnect:boolean,
110+
remoteSshExtensionId:string,
112111
):Promise<RemoteDetails|undefined>{
113112
constparts=parseRemoteAuthority(remoteAuthority);
114113
if(!parts){
@@ -148,15 +147,15 @@ export class Remote {
148147
]);
149148

150149
if(result.type==="login"){
151-
returnthis.setup(remoteAuthority,firstConnect);
150+
returnthis.setup(remoteAuthority,firstConnect,remoteSshExtensionId);
152151
}elseif(!result.userChoice){
153152
// User declined to log in.
154153
awaitthis.closeRemote();
155154
return;
156155
}else{
157156
// Log in then try again.
158157
awaitthis.commands.login({url:baseUrlRaw,label:parts.label});
159-
returnthis.setup(remoteAuthority,firstConnect);
158+
returnthis.setup(remoteAuthority,firstConnect,remoteSshExtensionId);
160159
}
161160
};
162161

@@ -485,30 +484,26 @@ export class Remote {
485484
throwerror;
486485
}
487486

488-
// TODO: This needs to be reworked; it fails to pick up reconnects.
489-
this.findSSHProcessID().then(async(pid)=>{
490-
if(!pid){
491-
// TODO: Show an error here!
492-
return;
493-
}
494-
disposables.push(this.showNetworkUpdates(pid));
495-
if(logDir){
496-
constlogFiles=awaitfs.readdir(logDir);
497-
constlogFileName=logFiles
498-
.reverse()
499-
.find(
500-
(file)=>file===`${pid}.log`||file.endsWith(`-${pid}.log`),
501-
);
502-
this.commands.workspaceLogPath=logFileName
503-
?path.join(logDir,logFileName)
504-
:undefined;
505-
}else{
506-
this.commands.workspaceLogPath=undefined;
507-
}
487+
// Monitor SSH process and display network status
488+
constsshMonitor=SshProcessMonitor.start({
489+
sshHost:parts.host,
490+
networkInfoPath:this.pathResolver.getNetworkInfoPath(),
491+
proxyLogDir:logDir||undefined,
492+
logger:this.logger,
493+
codeLogDir:this.pathResolver.getCodeLogDir(),
494+
remoteSshExtensionId,
508495
});
496+
disposables.push(sshMonitor);
497+
498+
this.commands.workspaceLogPath=sshMonitor.getLogFilePath();
509499

510-
// Register the label formatter again because SSH overrides it!
511500
disposables.push(
501+
sshMonitor.onLogFilePathChange((newPath)=>{
502+
this.commands.workspaceLogPath=newPath;
503+
}),
504+
// Watch for logDir configuration changes
505+
this.watchLogDirSetting(logDir,featureSet),
506+
// Register the label formatter again because SSH overrides it!
512507
vscode.extensions.onDidChange(()=>{
513508
// Dispose previous label formatter
514509
labelFormatterDisposable.dispose();
@@ -741,172 +736,27 @@ export class Remote {
741736
return`${args.join(" ")}`;
742737
}
743738

744-
// showNetworkUpdates finds the SSH process ID that is being used by this
745-
// workspace and reads the file being created by the Coder CLI.
746-
privateshowNetworkUpdates(sshPid:number):vscode.Disposable{
747-
constnetworkStatus=vscode.window.createStatusBarItem(
748-
vscode.StatusBarAlignment.Left,
749-
1000,
750-
);
751-
constnetworkInfoFile=path.join(
752-
this.pathResolver.getNetworkInfoPath(),
753-
`${sshPid}.json`,
754-
);
755-
756-
constupdateStatus=(network:{
757-
p2p:boolean;
758-
latency:number;
759-
preferred_derp:string;
760-
derp_latency:{[key:string]:number};
761-
upload_bytes_sec:number;
762-
download_bytes_sec:number;
763-
using_coder_connect:boolean;
764-
})=>{
765-
letstatusText="$(globe) ";
766-
767-
// Coder Connect doesn't populate any other stats
768-
if(network.using_coder_connect){
769-
networkStatus.text=statusText+"Coder Connect ";
770-
networkStatus.tooltip="You're connected using Coder Connect.";
771-
networkStatus.show();
772-
return;
773-
}
774-
775-
if(network.p2p){
776-
statusText+="Direct ";
777-
networkStatus.tooltip="You're connected peer-to-peer ✨.";
778-
}else{
779-
statusText+=network.preferred_derp+" ";
780-
networkStatus.tooltip=
781-
"You're connected through a relay 🕵.\nWe'll switch over to peer-to-peer when available.";
782-
}
783-
networkStatus.tooltip+=
784-
"\n\nDownload ↓ "+
785-
prettyBytes(network.download_bytes_sec,{
786-
bits:true,
787-
})+
788-
"/s • Upload ↑ "+
789-
prettyBytes(network.upload_bytes_sec,{
790-
bits:true,
791-
})+
792-
"/s\n";
793-
794-
if(!network.p2p){
795-
constderpLatency=network.derp_latency[network.preferred_derp];
796-
797-
networkStatus.tooltip+=`You ↔${derpLatency.toFixed(2)}ms ↔${network.preferred_derp}${(network.latency-derpLatency).toFixed(2)}ms ↔ Workspace`;
798-
799-
letfirst=true;
800-
Object.keys(network.derp_latency).forEach((region)=>{
801-
if(region===network.preferred_derp){
802-
return;
803-
}
804-
if(first){
805-
networkStatus.tooltip+=`\n\nOther regions:`;
806-
first=false;
807-
}
808-
networkStatus.tooltip+=`\n${region}:${Math.round(network.derp_latency[region]*100)/100}ms`;
809-
});
810-
}
811-
812-
statusText+="("+network.latency.toFixed(2)+"ms)";
813-
networkStatus.text=statusText;
814-
networkStatus.show();
815-
};
816-
letdisposed=false;
817-
constperiodicRefresh=()=>{
818-
if(disposed){
819-
return;
820-
}
821-
fs.readFile(networkInfoFile,"utf8")
822-
.then((content)=>{
823-
returnJSON.parse(content);
824-
})
825-
.then((parsed)=>{
826-
try{
827-
updateStatus(parsed);
828-
}catch{
829-
// Ignore
830-
}
831-
})
832-
.catch(()=>{
833-
// TODO: Log a failure here!
834-
})
835-
.finally(()=>{
836-
// This matches the write interval of `coder vscodessh`.
837-
setTimeout(periodicRefresh,3000);
838-
});
839-
};
840-
periodicRefresh();
841-
842-
return{
843-
dispose:()=>{
844-
disposed=true;
845-
networkStatus.dispose();
846-
},
847-
};
848-
}
849-
850-
// findSSHProcessID returns the currently active SSH process ID that is
851-
// powering the remote SSH connection.
852-
privateasyncfindSSHProcessID(timeout=15000):Promise<number|undefined>{
853-
constsearch=async(logPath:string):Promise<number|undefined>=>{
854-
// This searches for the socksPort that Remote SSH is connecting to. We do
855-
// this to find the SSH process that is powering this connection. That SSH
856-
// process will be logging network information periodically to a file.
857-
consttext=awaitfs.readFile(logPath,"utf8");
858-
constport=findPort(text);
859-
if(!port){
860-
return;
861-
}
862-
constprocesses=awaitfind("port",port);
863-
if(processes.length<1){
864-
return;
865-
}
866-
constprocess=processes[0];
867-
returnprocess.pid;
868-
};
869-
conststart=Date.now();
870-
constloop=async():Promise<number|undefined>=>{
871-
if(Date.now()-start>timeout){
872-
returnundefined;
873-
}
874-
// Loop until we find the remote SSH log for this window.
875-
constfilePath=awaitthis.getRemoteSSHLogPath();
876-
if(!filePath){
877-
returnnewPromise((resolve)=>setTimeout(()=>resolve(loop()),500));
878-
}
879-
// Then we search the remote SSH log until we find the port.
880-
constresult=awaitsearch(filePath);
881-
if(!result){
882-
returnnewPromise((resolve)=>setTimeout(()=>resolve(loop()),500));
739+
privatewatchLogDirSetting(
740+
currentLogDir:string,
741+
featureSet:FeatureSet,
742+
):vscode.Disposable{
743+
returnvscode.workspace.onDidChangeConfiguration((e)=>{
744+
if(e.affectsConfiguration("coder.proxyLogDirectory")){
745+
constnewLogDir=this.getLogDir(featureSet);
746+
if(newLogDir!==currentLogDir){
747+
vscode.window
748+
.showInformationMessage(
749+
"Log directory configuration changed. Reload window to apply.",
750+
"Reload",
751+
)
752+
.then((action)=>{
753+
if(action==="Reload"){
754+
vscode.commands.executeCommand("workbench.action.reloadWindow");
755+
}
756+
});
757+
}
883758
}
884-
returnresult;
885-
};
886-
returnloop();
887-
}
888-
889-
/**
890-
* Returns the log path for the "Remote - SSH" output panel. There is no VS
891-
* Code API to get the contents of an output panel. We use this to get the
892-
* active port so we can display network information.
893-
*/
894-
privateasyncgetRemoteSSHLogPath():Promise<string|undefined>{
895-
constupperDir=path.dirname(this.pathResolver.getCodeLogDir());
896-
// Node returns these directories sorted already!
897-
constdirs=awaitfs.readdir(upperDir);
898-
constlatestOutput=dirs
899-
.reverse()
900-
.filter((dir)=>dir.startsWith("output_logging_"));
901-
if(latestOutput.length===0){
902-
returnundefined;
903-
}
904-
constdir=awaitfs.readdir(path.join(upperDir,latestOutput[0]));
905-
constremoteSSH=dir.filter((file)=>file.indexOf("Remote - SSH")!==-1);
906-
if(remoteSSH.length===0){
907-
returnundefined;
908-
}
909-
returnpath.join(upperDir,latestOutput[0],remoteSSH[0]);
759+
});
910760
}
911761

912762
/**

‎src/remote/sshExtension.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import*asvscodefrom"vscode";
2+
3+
exportconstREMOTE_SSH_EXTENSION_IDS=[
4+
"jeanp413.open-remote-ssh",
5+
"codeium.windsurf-remote-openssh",
6+
"anysphere.remote-ssh",
7+
"ms-vscode-remote.remote-ssh",
8+
"google.antigravity-remote-openssh",
9+
]asconst;
10+
11+
exporttypeRemoteSshExtensionId=(typeofREMOTE_SSH_EXTENSION_IDS)[number];
12+
13+
typeRemoteSshExtension=vscode.Extension<unknown>&{
14+
id:RemoteSshExtensionId;
15+
};
16+
17+
exportfunctiongetRemoteSshExtension():RemoteSshExtension|undefined{
18+
for(constidofREMOTE_SSH_EXTENSION_IDS){
19+
constextension=vscode.extensions.getExtension(id);
20+
if(extension){
21+
returnextensionasRemoteSshExtension;
22+
}
23+
}
24+
returnundefined;
25+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp