@@ -69,6 +69,7 @@ import kotlinx.coroutines.delay
69
69
import kotlinx.coroutines.isActive
70
70
import kotlinx.coroutines.launch
71
71
import kotlinx.coroutines.withContext
72
+ import org.zeroturnaround.exec.InvalidExitValueException
72
73
import java.awt.Component
73
74
import java.awt.Dimension
74
75
import java.awt.event.MouseEvent
@@ -99,6 +100,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
99
100
private val cs = CoroutineScope (Dispatchers .Main )
100
101
private var localWizardModel = CoderWorkspacesWizardModel ()
101
102
private val clientService: CoderRestClientService = service()
103
+ private var cliManager: CoderCLIManager ? = null
102
104
private val iconDownloader: TemplateIconDownloader = service()
103
105
private val settings: CoderSettingsState = service()
104
106
@@ -339,6 +341,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
339
341
}
340
342
341
343
override fun onInit (wizardModel : CoderWorkspacesWizardModel ) {
344
+ cliManager = null
342
345
tableOfWorkspaces.listTableModel.items = emptyList()
343
346
if (localWizardModel.coderURL.isNotBlank() && localWizardModel.token != null ) {
344
347
triggerWorkspacePolling(true )
@@ -443,6 +446,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
443
446
onAuthFailure : (() -> Unit )? = null,
444
447
): Job {
445
448
// Clear out old deployment details.
449
+ cliManager = null
446
450
poller?.cancel()
447
451
tableOfWorkspaces.setEmptyState(" Connecting to $deploymentURL ..." )
448
452
tableOfWorkspaces.listTableModel.items = emptyList()
@@ -454,12 +458,13 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
454
458
canBeCancelled = false ,
455
459
isIndeterminate = true
456
460
) {
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)
463
468
try {
464
469
this .indicator.text = " Authenticating client..."
465
470
authenticate(deploymentURL, token.first)
@@ -472,23 +477,42 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
472
477
// supported if the binary is downloaded from alternate sources.
473
478
// For CLIs without the JSON output flag we will fall back to
474
479
// the 304 method.
475
- if (! cliManager .matchesVersion(clientService.buildVersion)) {
480
+ if (! cli .matchesVersion(clientService.buildVersion)) {
476
481
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
+ }
478
497
}
479
498
480
499
this .indicator.text = " Authenticating Coder CLI..."
481
- cliManager .login(token.first)
500
+ cli .login(token.first)
482
501
483
502
this .indicator.text = " Retrieving workspaces..."
484
503
loadWorkspaces()
485
504
486
505
updateWorkspaceActions()
487
506
triggerWorkspacePolling(false )
488
507
508
+ cliManager = cli
489
509
tableOfWorkspaces.setEmptyState(" Connected to $deploymentURL " )
490
510
} 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.message} "
513
+ is InvalidExitValueException -> " CLI exited unexpectedly with ${e.exitValue} "
514
+ else -> e.message ? : " No reason was provided"
515
+ }
492
516
var msg = CoderGatewayBundle .message(
493
517
" gateway.connector.view.workspaces.connect.failed" ,
494
518
deploymentURL,
@@ -513,7 +537,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
513
537
is ResponseException , is ConnectException -> {
514
538
msg = CoderGatewayBundle .message(
515
539
" gateway.connector.view.workspaces.connect.download-failed" ,
516
- cliManager .remoteBinaryURL,
540
+ cli .remoteBinaryURL,
517
541
errorSummary,
518
542
)
519
543
}
@@ -700,29 +724,30 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
700
724
token = localWizardModel.token
701
725
}
702
726
727
+ // These being null would be a developer error.
703
728
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
+ }
716
737
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()
721
740
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
726
751
}
727
752
728
753
override fun dispose () {
0 commit comments