22 type Workspace ,
33 type WorkspaceAgent ,
44} from "coder/site/src/api/typesGenerated" ;
5- import { spawn } from "node:child_process" ;
65import * as fs from "node:fs/promises" ;
76import * as path from "node:path" ;
87import * as semver from "semver" ;
@@ -14,8 +13,8 @@ import {
1413 workspaceStatusLabel ,
1514} from "./api/api-helper" ;
1615import { type CoderApi } from "./api/coderApi" ;
16+ import * as cliExec from "./core/cliExec" ;
1717import { type CliManager } from "./core/cliManager" ;
18- import * as cliUtils from "./core/cliUtils" ;
1918import { type ServiceContainer } from "./core/container" ;
2019import { type MementoManager } from "./core/mementoManager" ;
2120import { type PathResolver } from "./core/pathResolver" ;
@@ -32,12 +31,8 @@ import {
3231 RECOMMENDED_SSH_SETTINGS ,
3332 applySettingOverrides ,
3433} from "./remote/sshOverrides" ;
35- import {
36- getGlobalFlags ,
37- getGlobalShellFlags ,
38- resolveCliAuth ,
39- } from "./settings/cli" ;
40- import { escapeCommandArg , toRemoteAuthority , toSafeHost } from "./util" ;
34+ import { resolveCliAuth } from "./settings/cli" ;
35+ import { toRemoteAuthority , toSafeHost } from "./util" ;
4136import { vscodeProposed } from "./vscodeProposed" ;
4237import {
4338 AgentTreeItem ,
@@ -172,17 +167,17 @@ export class Commands {
172167 }
173168
174169 /**
175- * Run a speed test against the currently connected workspace and display the
176- * results in a new editor document .
170+ * Run a speed test against a workspace and display the results in a new
171+ * editor document. Can be triggered from the sidebar or command palette .
177172 */
178- public async speedTest ( ) : Promise < void > {
179- const workspace = this . workspace ;
180- const client = this . remoteWorkspaceClient ;
181- if ( ! workspace || ! client ) {
182- vscode . window . showInformationMessage ( "No workspace connected." ) ;
173+ public async speedTest ( item ?: OpenableTreeItem ) : Promise < void > {
174+ const resolved = await this . resolveClientAndWorkspace ( item ) ;
175+ if ( ! resolved ) {
183176 return ;
184177 }
185178
179+ const { client, workspaceId } = resolved ;
180+
186181 const duration = await vscode . window . showInputBox ( {
187182 title : "Speed Test Duration" ,
188183 prompt : "Duration for the speed test (e.g., 5s, 10s, 1m)" ,
@@ -194,24 +189,8 @@ export class Commands {
194189
195190 const result = await withCancellableProgress (
196191 async ( { signal } ) => {
197- const baseUrl = client . getAxiosInstance ( ) . defaults . baseURL ;
198- if ( ! baseUrl ) {
199- throw new Error ( "No deployment URL for the connected workspace" ) ;
200- }
201- const safeHost = toSafeHost ( baseUrl ) ;
202- const binary = await this . cliManager . fetchBinary ( client ) ;
203- const version = semver . parse ( await cliUtils . version ( binary ) ) ;
204- const featureSet = featureSetForVersion ( version ) ;
205- const configDir = this . pathResolver . getGlobalConfigDir ( safeHost ) ;
206- const configs = vscode . workspace . getConfiguration ( ) ;
207- const auth = resolveCliAuth ( configs , featureSet , baseUrl , configDir ) ;
208- const globalFlags = getGlobalFlags ( configs , auth ) ;
209- const workspaceName = createWorkspaceIdentifier ( workspace ) ;
210-
211- return cliUtils . speedtest ( binary , globalFlags , workspaceName , {
212- signal,
213- duration : duration . trim ( ) ,
214- } ) ;
192+ const env = await this . resolveCliEnv ( client ) ;
193+ return cliExec . speedtest ( env , workspaceId , duration . trim ( ) , signal ) ;
215194 } ,
216195 {
217196 location : vscode . ProgressLocation . Notification ,
@@ -568,17 +547,8 @@ export class Commands {
568547 title : `Connecting to AI Agent...` ,
569548 } ,
570549 async ( ) => {
571- const { binary, globalFlags } = await this . resolveCliEnv (
572- this . extensionClient ,
573- ) ;
574-
575- const terminal = vscode . window . createTerminal ( app . name ) ;
576- terminal . sendText (
577- `${ escapeCommandArg ( binary ) } ${ globalFlags . join ( " " ) } ssh ${ app . workspace_name } ` ,
578- ) ;
579- await new Promise ( ( resolve ) => setTimeout ( resolve , 5000 ) ) ;
580- terminal . sendText ( app . command ?? "" ) ;
581- terminal . show ( false ) ;
550+ const env = await this . resolveCliEnv ( this . extensionClient ) ;
551+ await cliExec . openAppStatusTerminal ( env , app ) ;
582552 } ,
583553 ) ;
584554 }
@@ -729,175 +699,72 @@ export class Commands {
729699 }
730700
731701 public async pingWorkspace ( item ?: OpenableTreeItem ) : Promise < void > {
732- let client : CoderApi ;
733- let workspaceId : string ;
734-
735- if ( item ) {
736- client = this . extensionClient ;
737- workspaceId = createWorkspaceIdentifier ( item . workspace ) ;
738- } else if ( this . workspace && this . remoteWorkspaceClient ) {
739- client = this . remoteWorkspaceClient ;
740- workspaceId = createWorkspaceIdentifier ( this . workspace ) ;
741- } else {
742- client = this . extensionClient ;
743- const workspace = await this . pickWorkspace ( {
744- title : "Ping a running workspace" ,
745- initialValue : "owner:me status:running " ,
746- placeholder : "Search running workspaces..." ,
747- filter : ( w ) => w . latest_build . status === "running" ,
748- } ) ;
749- if ( ! workspace ) {
750- return ;
751- }
752- workspaceId = createWorkspaceIdentifier ( workspace ) ;
702+ const resolved = await this . resolveClientAndWorkspace ( item ) ;
703+ if ( ! resolved ) {
704+ return ;
753705 }
754706
755- return this . spawnPing ( client , workspaceId ) ;
756- }
757-
758- private spawnPing ( client : CoderApi , workspaceId : string ) : Thenable < void > {
707+ const { client, workspaceId } = resolved ;
759708 return withProgress (
760709 {
761710 location : vscode . ProgressLocation . Notification ,
762711 title : `Starting ping for ${ workspaceId } ...` ,
763712 } ,
764713 async ( ) => {
765- const { binary, globalFlags } = await this . resolveCliEnv ( client ) ;
766-
767- const writeEmitter = new vscode . EventEmitter < string > ( ) ;
768- const closeEmitter = new vscode . EventEmitter < number | void > ( ) ;
769-
770- const args = [ ...globalFlags , "ping" , escapeCommandArg ( workspaceId ) ] ;
771- const cmd = `${ escapeCommandArg ( binary ) } ${ args . join ( " " ) } ` ;
772- // On Unix, spawn in a new process group so we can signal the
773- // entire group (shell + coder binary) on Ctrl+C. On Windows,
774- // detached opens a visible console window and negative-PID kill
775- // is unsupported, so we fall back to proc.kill().
776- const useProcessGroup = process . platform !== "win32" ;
777- const proc = spawn ( cmd , {
778- shell : true ,
779- detached : useProcessGroup ,
780- } ) ;
781-
782- let closed = false ;
783- let exited = false ;
784- let forceKillTimer : ReturnType < typeof setTimeout > | undefined ;
785-
786- const sendSignal = ( sig : "SIGINT" | "SIGKILL" ) => {
787- try {
788- if ( useProcessGroup && proc . pid ) {
789- process . kill ( - proc . pid , sig ) ;
790- } else {
791- proc . kill ( sig ) ;
792- }
793- } catch {
794- // Process already exited.
795- }
796- } ;
797-
798- const gracefulKill = ( ) => {
799- sendSignal ( "SIGINT" ) ;
800- // Escalate to SIGKILL if the process doesn't exit promptly.
801- forceKillTimer = setTimeout ( ( ) => sendSignal ( "SIGKILL" ) , 5000 ) ;
802- } ;
803-
804- const terminal = vscode . window . createTerminal ( {
805- name : `Coder Ping: ${ workspaceId } ` ,
806- pty : {
807- onDidWrite : writeEmitter . event ,
808- onDidClose : closeEmitter . event ,
809- open : ( ) => {
810- writeEmitter . fire ( "Press Ctrl+C (^C) to stop.\r\n" ) ;
811- writeEmitter . fire ( "─" . repeat ( 40 ) + "\r\n" ) ;
812- } ,
813- close : ( ) => {
814- closed = true ;
815- clearTimeout ( forceKillTimer ) ;
816- sendSignal ( "SIGKILL" ) ;
817- writeEmitter . dispose ( ) ;
818- closeEmitter . dispose ( ) ;
819- } ,
820- handleInput : ( data : string ) => {
821- if ( exited ) {
822- closeEmitter . fire ( ) ;
823- } else if ( data === "\x03" ) {
824- if ( forceKillTimer ) {
825- // Second Ctrl+C: force kill immediately.
826- clearTimeout ( forceKillTimer ) ;
827- sendSignal ( "SIGKILL" ) ;
828- } else {
829- if ( ! closed ) {
830- writeEmitter . fire ( "\r\nStopping...\r\n" ) ;
831- }
832- gracefulKill ( ) ;
833- }
834- }
835- } ,
836- } ,
837- } ) ;
838-
839- const fireLines = ( data : Buffer ) => {
840- if ( closed ) {
841- return ;
842- }
843- const lines = data
844- . toString ( )
845- . split ( / \r * \n / )
846- . filter ( ( line ) => line !== "" ) ;
847- for ( const line of lines ) {
848- writeEmitter . fire ( line + "\r\n" ) ;
849- }
850- } ;
851-
852- proc . stdout ?. on ( "data" , fireLines ) ;
853- proc . stderr ?. on ( "data" , fireLines ) ;
854- proc . on ( "error" , ( err ) => {
855- exited = true ;
856- clearTimeout ( forceKillTimer ) ;
857- if ( closed ) {
858- return ;
859- }
860- writeEmitter . fire ( `\r\nFailed to start: ${ err . message } \r\n` ) ;
861- writeEmitter . fire ( "Press any key to close.\r\n" ) ;
862- } ) ;
863- proc . on ( "close" , ( code , signal ) => {
864- exited = true ;
865- clearTimeout ( forceKillTimer ) ;
866- if ( closed ) {
867- return ;
868- }
869- let reason : string ;
870- if ( signal === "SIGKILL" ) {
871- reason = "Ping force killed (SIGKILL)" ;
872- } else if ( signal ) {
873- reason = "Ping stopped" ;
874- } else {
875- reason = `Process exited with code ${ code } ` ;
876- }
877- writeEmitter . fire ( `\r\n${ reason } . Press any key to close.\r\n` ) ;
878- } ) ;
879-
880- terminal . show ( false ) ;
714+ const env = await this . resolveCliEnv ( client ) ;
715+ cliExec . ping ( env , workspaceId ) ;
881716 } ,
882717 ) ;
883718 }
884719
885- private async resolveCliEnv (
886- client : CoderApi ,
887- ) : Promise < { binary : string ; globalFlags : string [ ] } > {
720+ /**
721+ * Resolve the API client and workspace identifier from a sidebar item,
722+ * the currently connected workspace, or by prompting the user to pick one.
723+ * Returns undefined if the user cancels the picker.
724+ */
725+ private async resolveClientAndWorkspace (
726+ item ?: OpenableTreeItem ,
727+ ) : Promise < { client : CoderApi ; workspaceId : string } | undefined > {
728+ if ( item ) {
729+ return {
730+ client : this . extensionClient ,
731+ workspaceId : createWorkspaceIdentifier ( item . workspace ) ,
732+ } ;
733+ }
734+ if ( this . workspace && this . remoteWorkspaceClient ) {
735+ return {
736+ client : this . remoteWorkspaceClient ,
737+ workspaceId : createWorkspaceIdentifier ( this . workspace ) ,
738+ } ;
739+ }
740+ const workspace = await this . pickWorkspace ( {
741+ title : "Select a running workspace" ,
742+ initialValue : "owner:me status:running " ,
743+ placeholder : "Search running workspaces..." ,
744+ filter : ( w ) => w . latest_build . status === "running" ,
745+ } ) ;
746+ if ( ! workspace ) {
747+ return undefined ;
748+ }
749+ return {
750+ client : this . extensionClient ,
751+ workspaceId : createWorkspaceIdentifier ( workspace ) ,
752+ } ;
753+ }
754+
755+ private async resolveCliEnv ( client : CoderApi ) : Promise < cliExec . CliEnv > {
888756 const baseUrl = client . getAxiosInstance ( ) . defaults . baseURL ;
889757 if ( ! baseUrl ) {
890758 throw new Error ( "You are not logged in" ) ;
891759 }
892760 const safeHost = toSafeHost ( baseUrl ) ;
893761 const binary = await this . cliManager . fetchBinary ( client ) ;
894- const version = semver . parse ( await cliUtils . version ( binary ) ) ;
762+ const version = semver . parse ( await cliExec . version ( binary ) ) ;
895763 const featureSet = featureSetForVersion ( version ) ;
896764 const configDir = this . pathResolver . getGlobalConfigDir ( safeHost ) ;
897765 const configs = vscode . workspace . getConfiguration ( ) ;
898766 const auth = resolveCliAuth ( configs , featureSet , baseUrl , configDir ) ;
899- const globalFlags = getGlobalShellFlags ( configs , auth ) ;
900- return { binary, globalFlags } ;
767+ return { binary, configs, auth } ;
901768 }
902769
903770 /**
0 commit comments