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