@@ -4,12 +4,10 @@ import {
44type Workspace ,
55type WorkspaceAgent ,
66} from "coder/site/src/api/typesGenerated" ;
7- import find from "find-process" ;
87import * as jsonc from "jsonc-parser" ;
98import * as fs from "node:fs/promises" ;
109import * as os from "node:os" ;
1110import * as path from "node:path" ;
12- import prettyBytes from "pretty-bytes" ;
1311import * as semver from "semver" ;
1412import * as vscode from "vscode" ;
1513
@@ -36,12 +34,12 @@ import {
3634AuthorityPrefix ,
3735escapeCommandArg ,
3836expandPath ,
39- findPort ,
4037parseRemoteAuthority ,
4138} from "../util" ;
4239import { WorkspaceMonitor } from "../workspace/workspaceMonitor" ;
4340
4441import { SSHConfig , type SSHValues , mergeSSHConfigValues } from "./sshConfig" ;
42+ import { SshProcessMonitor } from "./sshProcess" ;
4543import { computeSSHProperties , sshSupportsSetEnv } from "./sshSupport" ;
4644import { WorkspaceStateMachine } from "./workspaceStateMachine" ;
4745
@@ -109,6 +107,7 @@ export class Remote {
109107public async setup (
110108remoteAuthority :string ,
111109firstConnect :boolean ,
110+ remoteSshExtensionId :string ,
112111) :Promise < RemoteDetails | undefined > {
113112const parts = parseRemoteAuthority ( remoteAuthority ) ;
114113if ( ! parts ) {
@@ -148,15 +147,15 @@ export class Remote {
148147] ) ;
149148
150149if ( result . type === "login" ) {
151- return this . setup ( remoteAuthority , firstConnect ) ;
150+ return this . setup ( remoteAuthority , firstConnect , remoteSshExtensionId ) ;
152151} else if ( ! result . userChoice ) {
153152// User declined to log in.
154153await this . closeRemote ( ) ;
155154return ;
156155} else {
157156// Log in then try again.
158157await this . commands . login ( { url :baseUrlRaw , label :parts . label } ) ;
159- return this . setup ( remoteAuthority , firstConnect ) ;
158+ return this . setup ( remoteAuthority , firstConnect , remoteSshExtensionId ) ;
160159}
161160} ;
162161
@@ -485,30 +484,26 @@ export class Remote {
485484throw error ;
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- const logFiles = await fs . readdir ( logDir ) ;
497- const logFileName = 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+ const sshMonitor = 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!
511500disposables . 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!
512507vscode . extensions . onDidChange ( ( ) => {
513508// Dispose previous label formatter
514509labelFormatterDisposable . dispose ( ) ;
@@ -741,172 +736,27 @@ export class Remote {
741736return `${ 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- private showNetworkUpdates ( sshPid :number ) :vscode . Disposable {
747- const networkStatus = vscode . window . createStatusBarItem (
748- vscode . StatusBarAlignment . Left ,
749- 1000 ,
750- ) ;
751- const networkInfoFile = path . join (
752- this . pathResolver . getNetworkInfoPath ( ) ,
753- `${ sshPid } .json` ,
754- ) ;
755-
756- const updateStatus = ( 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- let statusText = "$(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- const derpLatency = 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- let first = 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- let disposed = false ;
817- const periodicRefresh = ( ) => {
818- if ( disposed ) {
819- return ;
820- }
821- fs . readFile ( networkInfoFile , "utf8" )
822- . then ( ( content ) => {
823- return JSON . 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- private async findSSHProcessID ( timeout = 15000 ) :Promise < number | undefined > {
853- const search = 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- const text = await fs . readFile ( logPath , "utf8" ) ;
858- const port = findPort ( text ) ;
859- if ( ! port ) {
860- return ;
861- }
862- const processes = await find ( "port" , port ) ;
863- if ( processes . length < 1 ) {
864- return ;
865- }
866- const process = processes [ 0 ] ;
867- return process . pid ;
868- } ;
869- const start = Date . now ( ) ;
870- const loop = async ( ) :Promise < number | undefined > => {
871- if ( Date . now ( ) - start > timeout ) {
872- return undefined ;
873- }
874- // Loop until we find the remote SSH log for this window.
875- const filePath = await this . getRemoteSSHLogPath ( ) ;
876- if ( ! filePath ) {
877- return new Promise ( ( resolve ) => setTimeout ( ( ) => resolve ( loop ( ) ) , 500 ) ) ;
878- }
879- // Then we search the remote SSH log until we find the port.
880- const result = await search ( filePath ) ;
881- if ( ! result ) {
882- return new Promise ( ( resolve ) => setTimeout ( ( ) => resolve ( loop ( ) ) , 500 ) ) ;
739+ private watchLogDirSetting (
740+ currentLogDir :string ,
741+ featureSet :FeatureSet ,
742+ ) :vscode . Disposable {
743+ return vscode . workspace . onDidChangeConfiguration ( ( e ) => {
744+ if ( e . affectsConfiguration ( "coder.proxyLogDirectory" ) ) {
745+ const newLogDir = 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- return result ;
885- } ;
886- return loop ( ) ;
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- private async getRemoteSSHLogPath ( ) :Promise < string | undefined > {
895- const upperDir = path . dirname ( this . pathResolver . getCodeLogDir ( ) ) ;
896- // Node returns these directories sorted already!
897- const dirs = await fs . readdir ( upperDir ) ;
898- const latestOutput = dirs
899- . reverse ( )
900- . filter ( ( dir ) => dir . startsWith ( "output_logging_" ) ) ;
901- if ( latestOutput . length === 0 ) {
902- return undefined ;
903- }
904- const dir = await fs . readdir ( path . join ( upperDir , latestOutput [ 0 ] ) ) ;
905- const remoteSSH = dir . filter ( ( file ) => file . indexOf ( "Remote - SSH" ) !== - 1 ) ;
906- if ( remoteSSH . length === 0 ) {
907- return undefined ;
908- }
909- return path . join ( upperDir , latestOutput [ 0 ] , remoteSSH [ 0 ] ) ;
759+ } ) ;
910760}
911761
912762/**