@@ -2,16 +2,15 @@ import {
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" ;
98import * as vscode from "vscode" ;
109
1110import { createWorkspaceIdentifier , extractAgents } from "./api/api-helper" ;
1211import { type CoderApi } from "./api/coderApi" ;
12+ import * as cliExec from "./core/cliExec" ;
1313import { type CliManager } from "./core/cliManager" ;
14- import * as cliUtils from "./core/cliUtils" ;
1514import { type ServiceContainer } from "./core/container" ;
1615import { type MementoManager } from "./core/mementoManager" ;
1716import { type PathResolver } from "./core/pathResolver" ;
@@ -28,12 +27,8 @@ import {
2827 RECOMMENDED_SSH_SETTINGS ,
2928 applySettingOverrides ,
3029} from "./remote/sshOverrides" ;
31- import {
32- getGlobalFlags ,
33- getGlobalShellFlags ,
34- resolveCliAuth ,
35- } from "./settings/cli" ;
36- import { escapeCommandArg , toRemoteAuthority , toSafeHost } from "./util" ;
30+ import { resolveCliAuth } from "./settings/cli" ;
31+ import { toRemoteAuthority , toSafeHost } from "./util" ;
3732import { vscodeProposed } from "./vscodeProposed" ;
3833import {
3934 AgentTreeItem ,
@@ -168,17 +163,17 @@ export class Commands {
168163 }
169164
170165 /**
171- * Run a speed test against the currently connected workspace and display the
172- * results in a new editor document .
166+ * Run a speed test against a workspace and display the results in a new
167+ * editor document. Can be triggered from the sidebar or command palette .
173168 */
174- public async speedTest ( ) : Promise < void > {
175- const workspace = this . workspace ;
176- const client = this . remoteWorkspaceClient ;
177- if ( ! workspace || ! client ) {
178- vscode . window . showInformationMessage ( "No workspace connected." ) ;
169+ public async speedTest ( item ?: OpenableTreeItem ) : Promise < void > {
170+ const resolved = await this . resolveClientAndWorkspace ( item ) ;
171+ if ( ! resolved ) {
179172 return ;
180173 }
181174
175+ const { client, workspaceId } = resolved ;
176+
182177 const duration = await vscode . window . showInputBox ( {
183178 title : "Speed Test Duration" ,
184179 prompt : "Duration for the speed test (e.g., 5s, 10s, 1m)" ,
@@ -190,24 +185,8 @@ export class Commands {
190185
191186 const result = await withCancellableProgress (
192187 async ( { signal } ) => {
193- const baseUrl = client . getAxiosInstance ( ) . defaults . baseURL ;
194- if ( ! baseUrl ) {
195- throw new Error ( "No deployment URL for the connected workspace" ) ;
196- }
197- const safeHost = toSafeHost ( baseUrl ) ;
198- const binary = await this . cliManager . fetchBinary ( client ) ;
199- const version = semver . parse ( await cliUtils . version ( binary ) ) ;
200- const featureSet = featureSetForVersion ( version ) ;
201- const configDir = this . pathResolver . getGlobalConfigDir ( safeHost ) ;
202- const configs = vscode . workspace . getConfiguration ( ) ;
203- const auth = resolveCliAuth ( configs , featureSet , baseUrl , configDir ) ;
204- const globalFlags = getGlobalFlags ( configs , auth ) ;
205- const workspaceName = createWorkspaceIdentifier ( workspace ) ;
206-
207- return cliUtils . speedtest ( binary , globalFlags , workspaceName , {
208- signal,
209- duration : duration . trim ( ) ,
210- } ) ;
188+ const env = await this . resolveCliEnv ( client ) ;
189+ return cliExec . speedtest ( env , workspaceId , duration . trim ( ) , signal ) ;
211190 } ,
212191 {
213192 location : vscode . ProgressLocation . Notification ,
@@ -564,17 +543,8 @@ export class Commands {
564543 title : `Connecting to AI Agent...` ,
565544 } ,
566545 async ( ) => {
567- const { binary, globalFlags } = await this . resolveCliEnv (
568- this . extensionClient ,
569- ) ;
570-
571- const terminal = vscode . window . createTerminal ( app . name ) ;
572- terminal . sendText (
573- `${ escapeCommandArg ( binary ) } ${ globalFlags . join ( " " ) } ssh ${ app . workspace_name } ` ,
574- ) ;
575- await new Promise ( ( resolve ) => setTimeout ( resolve , 5000 ) ) ;
576- terminal . sendText ( app . command ?? "" ) ;
577- terminal . show ( false ) ;
546+ const env = await this . resolveCliEnv ( this . extensionClient ) ;
547+ await cliExec . openAppStatusTerminal ( env , app ) ;
578548 } ,
579549 ) ;
580550 }
@@ -725,175 +695,72 @@ export class Commands {
725695 }
726696
727697 public async pingWorkspace ( item ?: OpenableTreeItem ) : Promise < void > {
728- let client : CoderApi ;
729- let workspaceId : string ;
730-
731- if ( item ) {
732- client = this . extensionClient ;
733- workspaceId = createWorkspaceIdentifier ( item . workspace ) ;
734- } else if ( this . workspace && this . remoteWorkspaceClient ) {
735- client = this . remoteWorkspaceClient ;
736- workspaceId = createWorkspaceIdentifier ( this . workspace ) ;
737- } else {
738- client = this . extensionClient ;
739- const workspace = await this . pickWorkspace ( {
740- title : "Ping a running workspace" ,
741- initialValue : "owner:me status:running " ,
742- placeholder : "Search running workspaces..." ,
743- filter : ( w ) => w . latest_build . status === "running" ,
744- } ) ;
745- if ( ! workspace ) {
746- return ;
747- }
748- workspaceId = createWorkspaceIdentifier ( workspace ) ;
698+ const resolved = await this . resolveClientAndWorkspace ( item ) ;
699+ if ( ! resolved ) {
700+ return ;
749701 }
750702
751- return this . spawnPing ( client , workspaceId ) ;
752- }
753-
754- private spawnPing ( client : CoderApi , workspaceId : string ) : Thenable < void > {
703+ const { client, workspaceId } = resolved ;
755704 return withProgress (
756705 {
757706 location : vscode . ProgressLocation . Notification ,
758707 title : `Starting ping for ${ workspaceId } ...` ,
759708 } ,
760709 async ( ) => {
761- const { binary, globalFlags } = await this . resolveCliEnv ( client ) ;
762-
763- const writeEmitter = new vscode . EventEmitter < string > ( ) ;
764- const closeEmitter = new vscode . EventEmitter < number | void > ( ) ;
765-
766- const args = [ ...globalFlags , "ping" , escapeCommandArg ( workspaceId ) ] ;
767- const cmd = `${ escapeCommandArg ( binary ) } ${ args . join ( " " ) } ` ;
768- // On Unix, spawn in a new process group so we can signal the
769- // entire group (shell + coder binary) on Ctrl+C. On Windows,
770- // detached opens a visible console window and negative-PID kill
771- // is unsupported, so we fall back to proc.kill().
772- const useProcessGroup = process . platform !== "win32" ;
773- const proc = spawn ( cmd , {
774- shell : true ,
775- detached : useProcessGroup ,
776- } ) ;
777-
778- let closed = false ;
779- let exited = false ;
780- let forceKillTimer : ReturnType < typeof setTimeout > | undefined ;
781-
782- const sendSignal = ( sig : "SIGINT" | "SIGKILL" ) => {
783- try {
784- if ( useProcessGroup && proc . pid ) {
785- process . kill ( - proc . pid , sig ) ;
786- } else {
787- proc . kill ( sig ) ;
788- }
789- } catch {
790- // Process already exited.
791- }
792- } ;
793-
794- const gracefulKill = ( ) => {
795- sendSignal ( "SIGINT" ) ;
796- // Escalate to SIGKILL if the process doesn't exit promptly.
797- forceKillTimer = setTimeout ( ( ) => sendSignal ( "SIGKILL" ) , 5000 ) ;
798- } ;
799-
800- const terminal = vscode . window . createTerminal ( {
801- name : `Coder Ping: ${ workspaceId } ` ,
802- pty : {
803- onDidWrite : writeEmitter . event ,
804- onDidClose : closeEmitter . event ,
805- open : ( ) => {
806- writeEmitter . fire ( "Press Ctrl+C (^C) to stop.\r\n" ) ;
807- writeEmitter . fire ( "─" . repeat ( 40 ) + "\r\n" ) ;
808- } ,
809- close : ( ) => {
810- closed = true ;
811- clearTimeout ( forceKillTimer ) ;
812- sendSignal ( "SIGKILL" ) ;
813- writeEmitter . dispose ( ) ;
814- closeEmitter . dispose ( ) ;
815- } ,
816- handleInput : ( data : string ) => {
817- if ( exited ) {
818- closeEmitter . fire ( ) ;
819- } else if ( data === "\x03" ) {
820- if ( forceKillTimer ) {
821- // Second Ctrl+C: force kill immediately.
822- clearTimeout ( forceKillTimer ) ;
823- sendSignal ( "SIGKILL" ) ;
824- } else {
825- if ( ! closed ) {
826- writeEmitter . fire ( "\r\nStopping...\r\n" ) ;
827- }
828- gracefulKill ( ) ;
829- }
830- }
831- } ,
832- } ,
833- } ) ;
834-
835- const fireLines = ( data : Buffer ) => {
836- if ( closed ) {
837- return ;
838- }
839- const lines = data
840- . toString ( )
841- . split ( / \r * \n / )
842- . filter ( ( line ) => line !== "" ) ;
843- for ( const line of lines ) {
844- writeEmitter . fire ( line + "\r\n" ) ;
845- }
846- } ;
847-
848- proc . stdout ?. on ( "data" , fireLines ) ;
849- proc . stderr ?. on ( "data" , fireLines ) ;
850- proc . on ( "error" , ( err ) => {
851- exited = true ;
852- clearTimeout ( forceKillTimer ) ;
853- if ( closed ) {
854- return ;
855- }
856- writeEmitter . fire ( `\r\nFailed to start: ${ err . message } \r\n` ) ;
857- writeEmitter . fire ( "Press any key to close.\r\n" ) ;
858- } ) ;
859- proc . on ( "close" , ( code , signal ) => {
860- exited = true ;
861- clearTimeout ( forceKillTimer ) ;
862- if ( closed ) {
863- return ;
864- }
865- let reason : string ;
866- if ( signal === "SIGKILL" ) {
867- reason = "Ping force killed (SIGKILL)" ;
868- } else if ( signal ) {
869- reason = "Ping stopped" ;
870- } else {
871- reason = `Process exited with code ${ code } ` ;
872- }
873- writeEmitter . fire ( `\r\n${ reason } . Press any key to close.\r\n` ) ;
874- } ) ;
875-
876- terminal . show ( false ) ;
710+ const env = await this . resolveCliEnv ( client ) ;
711+ cliExec . ping ( env , workspaceId ) ;
877712 } ,
878713 ) ;
879714 }
880715
881- private async resolveCliEnv (
882- client : CoderApi ,
883- ) : Promise < { binary : string ; globalFlags : string [ ] } > {
716+ /**
717+ * Resolve the API client and workspace identifier from a sidebar item,
718+ * the currently connected workspace, or by prompting the user to pick one.
719+ * Returns undefined if the user cancels the picker.
720+ */
721+ private async resolveClientAndWorkspace (
722+ item ?: OpenableTreeItem ,
723+ ) : Promise < { client : CoderApi ; workspaceId : string } | undefined > {
724+ if ( item ) {
725+ return {
726+ client : this . extensionClient ,
727+ workspaceId : createWorkspaceIdentifier ( item . workspace ) ,
728+ } ;
729+ }
730+ if ( this . workspace && this . remoteWorkspaceClient ) {
731+ return {
732+ client : this . remoteWorkspaceClient ,
733+ workspaceId : createWorkspaceIdentifier ( this . workspace ) ,
734+ } ;
735+ }
736+ const workspace = await this . pickWorkspace ( {
737+ title : "Select a running workspace" ,
738+ initialValue : "owner:me status:running " ,
739+ placeholder : "Search running workspaces..." ,
740+ filter : ( w ) => w . latest_build . status === "running" ,
741+ } ) ;
742+ if ( ! workspace ) {
743+ return undefined ;
744+ }
745+ return {
746+ client : this . extensionClient ,
747+ workspaceId : createWorkspaceIdentifier ( workspace ) ,
748+ } ;
749+ }
750+
751+ private async resolveCliEnv ( client : CoderApi ) : Promise < cliExec . CliEnv > {
884752 const baseUrl = client . getAxiosInstance ( ) . defaults . baseURL ;
885753 if ( ! baseUrl ) {
886754 throw new Error ( "You are not logged in" ) ;
887755 }
888756 const safeHost = toSafeHost ( baseUrl ) ;
889757 const binary = await this . cliManager . fetchBinary ( client ) ;
890- const version = semver . parse ( await cliUtils . version ( binary ) ) ;
758+ const version = semver . parse ( await cliExec . version ( binary ) ) ;
891759 const featureSet = featureSetForVersion ( version ) ;
892760 const configDir = this . pathResolver . getGlobalConfigDir ( safeHost ) ;
893761 const configs = vscode . workspace . getConfiguration ( ) ;
894762 const auth = resolveCliAuth ( configs , featureSet , baseUrl , configDir ) ;
895- const globalFlags = getGlobalShellFlags ( configs , auth ) ;
896- return { binary, globalFlags } ;
763+ return { binary, auth } ;
897764 }
898765
899766 /**
0 commit comments