@@ -3,7 +3,7 @@ import { Api } from "coder/site/src/api/api"
33import { getErrorMessage } from "coder/site/src/api/errors"
44import { User , Workspace , WorkspaceAgent } from "coder/site/src/api/typesGenerated"
55import { lookup } from "dns"
6- import { inRange } from "range_check "
6+ import ipRangeCheck from "ip-range-check "
77import { promisify } from "util"
88import * as vscode from "vscode"
99import { makeCoderSdk , needToken } from "./api"
@@ -396,26 +396,14 @@ export class Commands {
396396if ( ! baseUrl ) {
397397throw new Error ( "You are not logged in" )
398398}
399-
400- try {
401- await openWorkspace (
402- this . restClient ,
403- baseUrl ,
404- treeItem . workspaceOwner ,
405- treeItem . workspaceName ,
406- treeItem . workspaceAgent ,
407- treeItem . workspaceFolderPath ,
408- true ,
409- )
410- } catch ( err ) {
411- const message = getErrorMessage ( err , "no response from the server" )
412- this . storage . writeToCoderOutputChannel ( `Failed to open workspace:${ message } ` )
413- this . vscodeProposed . window . showErrorMessage ( "Failed to open workspace" , {
414- detail :message ,
415- modal :true ,
416- useCustom :true ,
417- } )
418- }
399+ await this . openWorkspace (
400+ baseUrl ,
401+ treeItem . workspaceOwner ,
402+ treeItem . workspaceName ,
403+ treeItem . workspaceAgent ,
404+ treeItem . workspaceFolderPath ,
405+ true ,
406+ )
419407} else {
420408// If there is no tree item, then the user manually ran this command.
421409// Default to the regular open instead.
@@ -513,15 +501,7 @@ export class Commands {
513501}
514502
515503try {
516- await openWorkspace (
517- this . restClient ,
518- baseUrl ,
519- workspaceOwner ,
520- workspaceName ,
521- workspaceAgent ,
522- folderPath ,
523- openRecent ,
524- )
504+ await this . openWorkspace ( baseUrl , workspaceOwner , workspaceName , workspaceAgent , folderPath , openRecent )
525505} catch ( err ) {
526506const message = getErrorMessage ( err , "no response from the server" )
527507this . storage . writeToCoderOutputChannel ( `Failed to open workspace:${ message } ` )
@@ -574,32 +554,112 @@ export class Commands {
574554await this . workspaceRestClient . updateWorkspaceVersion ( this . workspace )
575555}
576556}
577- }
578557
579- /**
580- * Given a workspace, build the host name, find a directory to open, and pass
581- * both to the Remote SSH plugin in the form of a remote authority URI.
582- */
583- async function openWorkspace (
584- restClient :Api ,
585- baseUrl :string ,
586- workspaceOwner :string ,
587- workspaceName :string ,
588- workspaceAgent :string | undefined ,
589- folderPath :string | undefined ,
590- openRecent :boolean | undefined ,
591- ) {
592- let remoteAuthority = toRemoteAuthority ( baseUrl , workspaceOwner , workspaceName , workspaceAgent )
558+ /**
559+ * Given a workspace, build the host name, find a directory to open, and pass
560+ * both to the Remote SSH plugin in the form of a remote authority URI.
561+ */
562+ private async openWorkspace (
563+ baseUrl :string ,
564+ workspaceOwner :string ,
565+ workspaceName :string ,
566+ workspaceAgent :string | undefined ,
567+ folderPath :string | undefined ,
568+ openRecent :boolean | undefined ,
569+ ) {
570+ let remoteAuthority = toRemoteAuthority ( baseUrl , workspaceOwner , workspaceName , workspaceAgent )
571+
572+ // When called from `openFromSidebar`, the workspaceAgent will only not be set
573+ // if the workspace is stopped, in which case we can't use Coder Connect
574+ // When called from `open`, the workspaceAgent will always be set.
575+ if ( workspaceAgent ) {
576+ let hostnameSuffix = "coder"
577+ try {
578+ // If the field was undefined, it's an older server, and always 'coder'
579+ hostnameSuffix = ( await this . fetchHostnameSuffix ( ) ) ?? hostnameSuffix
580+ } catch ( error ) {
581+ const message = getErrorMessage ( error , "no response from the server" )
582+ this . storage . writeToCoderOutputChannel ( `Failed to open workspace:${ message } ` )
583+ this . vscodeProposed . window . showErrorMessage ( "Failed to open workspace" , {
584+ detail :message ,
585+ modal :true ,
586+ useCustom :true ,
587+ } )
588+ }
593589
594- // When called from `openFromSidebar`, the workspaceAgent will only not be set
595- // if the workspace is stopped, in which case we can't use Coder Connect
596- // When called from `open`, the workspaceAgent will always be set.
597- if ( workspaceAgent ) {
598- let hostnameSuffix = "coder"
590+ const coderConnectAddr = await maybeCoderConnectAddr (
591+ workspaceAgent ,
592+ workspaceName ,
593+ workspaceOwner ,
594+ hostnameSuffix ,
595+ )
596+ if ( coderConnectAddr ) {
597+ remoteAuthority = `ssh-remote+${ coderConnectAddr } `
598+ }
599+ }
600+
601+ let newWindow = true
602+ // Open in the existing window if no workspaces are open.
603+ if ( ! vscode . workspace . workspaceFolders ?. length ) {
604+ newWindow = false
605+ }
606+
607+ // If a folder isn't specified or we have been asked to open the most recent,
608+ // we can try to open a recently opened folder/workspace.
609+ if ( ! folderPath || openRecent ) {
610+ const output :{
611+ workspaces :{ folderUri :vscode . Uri ; remoteAuthority :string } [ ]
612+ } = await vscode . commands . executeCommand ( "_workbench.getRecentlyOpened" )
613+ const opened = output . workspaces . filter (
614+ // Remove recents that do not belong to this connection. The remote
615+ // authority maps to a workspace or workspace/agent combination (using the
616+ // SSH host name). This means, at the moment, you can have a different
617+ // set of recents for a workspace versus workspace/agent combination, even
618+ // if that agent is the default for the workspace.
619+ ( opened ) => opened . folderUri ?. authority === remoteAuthority ,
620+ )
621+
622+ // openRecent will always use the most recent. Otherwise, if there are
623+ // multiple we ask the user which to use.
624+ if ( opened . length === 1 || ( opened . length > 1 && openRecent ) ) {
625+ folderPath = opened [ 0 ] . folderUri . path
626+ } else if ( opened . length > 1 ) {
627+ const items = opened . map ( ( f ) => f . folderUri . path )
628+ folderPath = await vscode . window . showQuickPick ( items , {
629+ title :"Select a recently opened folder" ,
630+ } )
631+ if ( ! folderPath ) {
632+ // User aborted.
633+ return
634+ }
635+ }
636+ }
637+
638+ if ( folderPath ) {
639+ await vscode . commands . executeCommand (
640+ "vscode.openFolder" ,
641+ vscode . Uri . from ( {
642+ scheme :"vscode-remote" ,
643+ authority :remoteAuthority ,
644+ path :folderPath ,
645+ } ) ,
646+ // Open this in a new window!
647+ newWindow ,
648+ )
649+ return
650+ }
651+
652+ // This opens the workspace without an active folder opened.
653+ await vscode . commands . executeCommand ( "vscode.newWindow" , {
654+ remoteAuthority :remoteAuthority ,
655+ reuseWindow :! newWindow ,
656+ } )
657+ }
658+
659+ private async fetchHostnameSuffix ( ) :Promise < string | undefined > {
599660try {
600- const sshConfig = await restClient . getDeploymentSSHConfig ( )
601- // If the field is undefined, it's an older server, and always 'coder'
602- hostnameSuffix = sshConfig . hostname_suffix ?? hostnameSuffix
661+ const sshConfig = await this . restClient . getDeploymentSSHConfig ( )
662+ return sshConfig . hostname_suffix
603663} catch ( error ) {
604664if ( ! isAxiosError ( error ) ) {
605665throw error
@@ -609,75 +669,11 @@ async function openWorkspace(
609669// Likely a very old deployment, just use the default.
610670break
611671}
612- case 401 :{
613- throw error
614- }
615672default :
616673throw error
617674}
618675}
619- const coderConnectAddr = await maybeCoderConnectAddr ( workspaceAgent , workspaceName , workspaceOwner , hostnameSuffix )
620- if ( coderConnectAddr ) {
621- remoteAuthority = `ssh-remote+${ coderConnectAddr } `
622- }
623- }
624-
625- let newWindow = true
626- // Open in the existing window if no workspaces are open.
627- if ( ! vscode . workspace . workspaceFolders ?. length ) {
628- newWindow = false
629676}
630-
631- // If a folder isn't specified or we have been asked to open the most recent,
632- // we can try to open a recently opened folder/workspace.
633- if ( ! folderPath || openRecent ) {
634- const output :{
635- workspaces :{ folderUri :vscode . Uri ; remoteAuthority :string } [ ]
636- } = await vscode . commands . executeCommand ( "_workbench.getRecentlyOpened" )
637- const opened = output . workspaces . filter (
638- // Remove recents that do not belong to this connection. The remote
639- // authority maps to a workspace or workspace/agent combination (using the
640- // SSH host name). This means, at the moment, you can have a different
641- // set of recents for a workspace versus workspace/agent combination, even
642- // if that agent is the default for the workspace.
643- ( opened ) => opened . folderUri ?. authority === remoteAuthority ,
644- )
645-
646- // openRecent will always use the most recent. Otherwise, if there are
647- // multiple we ask the user which to use.
648- if ( opened . length === 1 || ( opened . length > 1 && openRecent ) ) {
649- folderPath = opened [ 0 ] . folderUri . path
650- } else if ( opened . length > 1 ) {
651- const items = opened . map ( ( f ) => f . folderUri . path )
652- folderPath = await vscode . window . showQuickPick ( items , {
653- title :"Select a recently opened folder" ,
654- } )
655- if ( ! folderPath ) {
656- // User aborted.
657- return
658- }
659- }
660- }
661-
662- if ( folderPath ) {
663- await vscode . commands . executeCommand (
664- "vscode.openFolder" ,
665- vscode . Uri . from ( {
666- scheme :"vscode-remote" ,
667- authority :remoteAuthority ,
668- path :folderPath ,
669- } ) ,
670- // Open this in a new window!
671- newWindow ,
672- )
673- return
674- }
675-
676- // This opens the workspace without an active folder opened.
677- await vscode . commands . executeCommand ( "vscode.newWindow" , {
678- remoteAuthority :remoteAuthority ,
679- reuseWindow :! newWindow ,
680- } )
681677}
682678
683679async function maybeCoderConnectAddr (
@@ -689,7 +685,7 @@ async function maybeCoderConnectAddr(
689685const coderConnectHostname = `${ agent } .${ workspace } .${ owner } .${ hostnameSuffix } `
690686try {
691687const res = await promisify ( lookup ) ( coderConnectHostname )
692- return res . family === 6 && inRange ( res . address , "fd60:627a:a42b::/48" ) ?coderConnectHostname :undefined
688+ return res . family === 6 && ipRangeCheck ( res . address , "fd60:627a:a42b::/48" ) ?coderConnectHostname :undefined
693689} catch {
694690return undefined
695691}