1
1
package com.coder.gateway.sdk
2
2
3
3
import com.coder.gateway.models.WorkspaceAgentModel
4
+ import com.coder.gateway.services.CoderSettingsState
4
5
import com.coder.gateway.views.steps.CoderWorkspacesStepView
5
6
import com.google.gson.Gson
7
+ import com.google.gson.JsonSyntaxException
6
8
import com.intellij.openapi.diagnostic.Logger
9
+ import com.intellij.openapi.progress.ProgressIndicator
7
10
import org.zeroturnaround.exec.ProcessExecutor
8
11
import java.io.BufferedInputStream
9
12
import java.io.FileInputStream
@@ -26,7 +29,8 @@ import javax.xml.bind.annotation.adapters.HexBinaryAdapter
26
29
*/
27
30
class CoderCLIManager @JvmOverloads constructor(
28
31
private val deploymentURL : URL ,
29
- destinationDir : Path ,
32
+ dataDir : Path ,
33
+ cliDir : Path ? = null ,
30
34
remoteBinaryURLOverride : String? = null ,
31
35
private val sshConfigPath : Path = Path .of(System .getProperty("user.home")).resolve(".ssh/config"),
32
36
) {
@@ -52,8 +56,8 @@ class CoderCLIManager @JvmOverloads constructor(
52
56
}
53
57
val host = getSafeHost(deploymentURL)
54
58
val subdir = if (deploymentURL.port > 0 ) " ${host} -${deploymentURL.port} " else host
55
- localBinaryPath = destinationDir .resolve(subdir).resolve(binaryName).toAbsolutePath()
56
- coderConfigPath = destinationDir .resolve(subdir).resolve(" config" ).toAbsolutePath()
59
+ localBinaryPath = (cliDir ? : dataDir) .resolve(subdir).resolve(binaryName).toAbsolutePath()
60
+ coderConfigPath = dataDir .resolve(subdir).resolve(" config" ).toAbsolutePath()
57
61
}
58
62
59
63
/* *
@@ -293,26 +297,47 @@ class CoderCLIManager @JvmOverloads constructor(
293
297
val raw = exec(" version" , " --output" , " json" )
294
298
val json = Gson ().fromJson(raw, Version ::class .java)
295
299
if (json?.version == null ) {
296
- throw InvalidVersionException (" No version found in output" )
300
+ throw MissingVersionException (" No version found in output" )
297
301
}
298
302
return CoderSemVer .parse(json.version)
299
303
}
300
304
301
305
/* *
302
306
* Returns true if the CLI has the same major/minor/patch version as the
303
- * provided version and false if it does not match or the CLI version could
304
- * not be determined or the provided version is invalid.
307
+ * provided version, false if it does not match or either version is
308
+ * invalid, or null if the CLI version could not be determined because the
309
+ * binary could not be executed.
305
310
*/
306
- fun matchesVersion (buildVersion : String ): Boolean {
307
- return try {
308
- val cliVersion = version()
309
- val matches = cliVersion == CoderSemVer .parse(buildVersion)
310
- logger.info(" $localBinaryPath version $cliVersion matches $buildVersion : $matches " )
311
- matches
311
+ fun matchesVersion (rawBuildVersion : String ): Boolean? {
312
+ val cliVersion = try {
313
+ version()
312
314
} catch (e: Exception ) {
313
- logger.info(" Unable to determine $localBinaryPath version: ${e.message} " )
314
- false
315
+ when (e) {
316
+ is JsonSyntaxException ,
317
+ is IllegalArgumentException -> {
318
+ logger.info(" Got invalid version from $localBinaryPath : ${e.message} " )
319
+ return false
320
+ }
321
+ else -> {
322
+ // An error here most likely means the CLI does not exist or
323
+ // it executed successfully but output no version which
324
+ // suggests it is not the right binary.
325
+ logger.info(" Unable to determine $localBinaryPath version: ${e.message} " )
326
+ return null
327
+ }
328
+ }
329
+ }
330
+
331
+ val buildVersion = try {
332
+ CoderSemVer .parse(rawBuildVersion)
333
+ } catch (e: IllegalArgumentException ) {
334
+ logger.info(" Got invalid build version: $rawBuildVersion " )
335
+ return false
315
336
}
337
+
338
+ val matches = cliVersion == buildVersion
339
+ logger.info(" $localBinaryPath version $cliVersion matches $buildVersion : $matches " )
340
+ return matches
316
341
}
317
342
318
343
private fun exec (vararg args : String ): String {
@@ -404,6 +429,68 @@ class CoderCLIManager @JvmOverloads constructor(
404
429
fun getHostName (url : URL , ws : WorkspaceAgentModel ): String {
405
430
return " coder-jetbrains--${ws.name} --${getSafeHost(url)} "
406
431
}
432
+
433
+ /* *
434
+ * Do as much as possible to get a valid, up-to-date CLI.
435
+ */
436
+ @JvmStatic
437
+ @JvmOverloads
438
+ fun ensureCLI (
439
+ deploymentURL : URL ,
440
+ buildVersion : String ,
441
+ settings : CoderSettingsState ,
442
+ indicator : ProgressIndicator ? = null,
443
+ ): CoderCLIManager {
444
+ val dataDir =
445
+ if (settings.dataDirectory.isBlank()) getDataDir()
446
+ else Path .of(settings.dataDirectory).toAbsolutePath()
447
+ val binDir =
448
+ if (settings.binaryDirectory.isBlank()) null
449
+ else Path .of(settings.binaryDirectory).toAbsolutePath()
450
+
451
+ val cli = CoderCLIManager (deploymentURL, dataDir, binDir, settings.binarySource)
452
+
453
+ // Short-circuit if we already have the expected version. This
454
+ // lets us bypass the 304 which is slower and may not be
455
+ // supported if the binary is downloaded from alternate sources.
456
+ // For CLIs without the JSON output flag we will fall back to
457
+ // the 304 method.
458
+ val cliMatches = cli.matchesVersion(buildVersion)
459
+ if (cliMatches == true ) {
460
+ return cli
461
+ }
462
+
463
+ // If downloads are enabled download the new version.
464
+ if (settings.enableDownloads) {
465
+ indicator?.text = " Downloading Coder CLI..."
466
+ try {
467
+ cli.downloadCLI()
468
+ return cli
469
+ } catch (e: java.nio.file.AccessDeniedException ) {
470
+ // Might be able to fall back.
471
+ if (binDir == null || binDir == dataDir || ! settings.enableBinaryDirectoryFallback) {
472
+ throw e
473
+ }
474
+ }
475
+ }
476
+
477
+ // Try falling back to the data directory.
478
+ val dataCLI = CoderCLIManager (deploymentURL, dataDir, null , settings.binarySource)
479
+ val dataCLIMatches = dataCLI.matchesVersion(buildVersion)
480
+ if (dataCLIMatches == true ) {
481
+ return dataCLI
482
+ }
483
+
484
+ if (settings.enableDownloads) {
485
+ indicator?.text = " Downloading Coder CLI..."
486
+ dataCLI.downloadCLI()
487
+ return dataCLI
488
+ }
489
+
490
+ // Prefer the binary directory unless the data directory has a
491
+ // working binary and the binary directory does not.
492
+ return if (cliMatches == null && dataCLIMatches != null ) dataCLI else cli
493
+ }
407
494
}
408
495
}
409
496
@@ -418,5 +505,5 @@ class Environment(private val env: Map<String, String> = emptyMap()) {
418
505
}
419
506
420
507
class ResponseException (message : String , val code : Int ) : Exception(message)
421
-
422
508
class SSHConfigFormatException (message : String ) : Exception(message)
509
+ class MissingVersionException (message : String ) : Exception(message)
0 commit comments