@@ -69,6 +69,7 @@ import kotlinx.coroutines.delay
6969import kotlinx.coroutines.isActive
7070import kotlinx.coroutines.launch
7171import kotlinx.coroutines.withContext
72+ import org.zeroturnaround.exec.InvalidExitValueException
7273import java.awt.Component
7374import java.awt.Dimension
7475import java.awt.event.MouseEvent
@@ -99,6 +100,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
99100 private val cs = CoroutineScope (Dispatchers .Main )
100101 private var localWizardModel = CoderWorkspacesWizardModel ()
101102 private val clientService: CoderRestClientService = service()
103+ private var cliManager: CoderCLIManager ? = null
102104 private val iconDownloader: TemplateIconDownloader = service()
103105 private val settings: CoderSettingsState = service()
104106
@@ -339,6 +341,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
339341 }
340342
341343 override fun onInit (wizardModel : CoderWorkspacesWizardModel ) {
344+ cliManager = null
342345 tableOfWorkspaces.listTableModel.items = emptyList()
343346 if (localWizardModel.coderURL.isNotBlank() && localWizardModel.token != null ) {
344347 triggerWorkspacePolling(true )
@@ -443,6 +446,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
443446 onAuthFailure : (() -> Unit )? = null,
444447 ): Job {
445448 // Clear out old deployment details.
449+ cliManager = null
446450 poller?.cancel()
447451 tableOfWorkspaces.setEmptyState(" Connecting to $deploymentURL ..." )
448452 tableOfWorkspaces.listTableModel.items = emptyList()
@@ -454,12 +458,13 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
454458 canBeCancelled = false ,
455459 isIndeterminate = true
456460 ) {
457- val cliManager = CoderCLIManager (
458- deploymentURL,
459- if (settings.binaryDestination.isNotBlank()) Path .of(settings.binaryDestination)
460- else CoderCLIManager .getDataDir(),
461- settings.binarySource,
462- )
461+ val dataDir =
462+ if (settings.dataDirectory.isBlank()) CoderCLIManager .getDataDir()
463+ else Path .of(settings.dataDirectory).toAbsolutePath()
464+ val cliDir =
465+ if (settings.binaryDirectory.isBlank()) null
466+ else Path .of(settings.binaryDirectory).toAbsolutePath()
467+ var cli = CoderCLIManager (deploymentURL, dataDir, cliDir, settings.binarySource)
463468 try {
464469 this .indicator.text = " Authenticating client..."
465470 authenticate(deploymentURL, token.first)
@@ -472,23 +477,42 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
472477 // supported if the binary is downloaded from alternate sources.
473478 // For CLIs without the JSON output flag we will fall back to
474479 // the 304 method.
475- if (! cliManager .matchesVersion(clientService.buildVersion)) {
480+ if (! cli .matchesVersion(clientService.buildVersion)) {
476481 this .indicator.text = " Downloading Coder CLI..."
477- cliManager.downloadCLI()
482+ try {
483+ cli.downloadCLI()
484+ } catch (e: java.nio.file.AccessDeniedException ) {
485+ // Try the data directory instead.
486+ if (cliDir != null && ! cliDir.startsWith(dataDir)) {
487+ val oldPath = cli.localBinaryPath
488+ cli = CoderCLIManager (deploymentURL, dataDir, null , settings.binarySource )
489+ logger.info(" Cannot write to $oldPath , falling back to ${cli.localBinaryPath} " )
490+ if (! cli.matchesVersion(clientService.buildVersion)) {
491+ cli.downloadCLI()
492+ }
493+ } else {
494+ throw e
495+ }
496+ }
478497 }
479498
480499 this .indicator.text = " Authenticating Coder CLI..."
481- cliManager .login(token.first)
500+ cli .login(token.first)
482501
483502 this .indicator.text = " Retrieving workspaces..."
484503 loadWorkspaces()
485504
486505 updateWorkspaceActions()
487506 triggerWorkspacePolling(false )
488507
508+ cliManager = cli
489509 tableOfWorkspaces.setEmptyState(" Connected to $deploymentURL " )
490510 } catch (e: Exception ) {
491- val errorSummary = e.message ? : " No reason was provided"
511+ val errorSummary = when (e) {
512+ is java.nio.file.AccessDeniedException -> " Access denied to ${e.file} "
513+ is InvalidExitValueException -> " CLI exited unexpectedly with ${e.exitValue} "
514+ else -> e.message ? : " No reason was provided"
515+ }
492516 var msg = CoderGatewayBundle .message(
493517 " gateway.connector.view.workspaces.connect.failed" ,
494518 deploymentURL,
@@ -513,7 +537,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
513537 is ResponseException , is ConnectException -> {
514538 msg = CoderGatewayBundle .message(
515539 " gateway.connector.view.workspaces.connect.download-failed" ,
516- cliManager .remoteBinaryURL,
540+ cli .remoteBinaryURL,
517541 errorSummary,
518542 )
519543 }
@@ -700,29 +724,30 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
700724 token = localWizardModel.token
701725 }
702726
727+ // These being null would be a developer error.
703728 val workspace = tableOfWorkspaces.selectedObject
704- if (workspace != null ) {
705- wizardModel.selectedWorkspace = workspace
706- poller?.cancel()
707-
708- logger.info(" Configuring Coder CLI..." )
709- val cliManager = CoderCLIManager (
710- wizardModel.coderURL.toURL(),
711- if (settings.binaryDestination.isNotBlank()) Path .of(settings.binaryDestination)
712- else CoderCLIManager .getDataDir(),
713- settings.binarySource,
714- )
715- cliManager.configSsh(tableOfWorkspaces.items)
729+ val cli = cliManager
730+ if (workspace == null ) {
731+ logger.error(" No selected workspace" )
732+ return false
733+ } else if (cli == null ) {
734+ logger.error(" No configured CLI" )
735+ return false
736+ }
716737
717- // The config directory can be used to pull the URL and token in
718- // order to query this workspace's status in other flows, for
719- // example from the recent connections screen.
720- wizardModel.configDirectory = cliManager.coderConfigPath.toString()
738+ wizardModel.selectedWorkspace = workspace
739+ poller?.cancel()
721740
722- logger.info(" Opening IDE and Project Location window for ${workspace.name} " )
723- return true
724- }
725- return false
741+ logger.info(" Configuring Coder CLI..." )
742+ cli.configSsh(tableOfWorkspaces.items)
743+
744+ // The config directory can be used to pull the URL and token in
745+ // order to query this workspace's status in other flows, for
746+ // example from the recent connections screen.
747+ wizardModel.configDirectory = cli.coderConfigPath.toString()
748+
749+ logger.info(" Opening IDE and Project Location window for ${workspace.name} " )
750+ return true
726751 }
727752
728753 override fun dispose () {
0 commit comments