11package com.coder.gateway.sdk
22
33import com.coder.gateway.models.WorkspaceAgentModel
4+ import com.coder.gateway.services.CoderSettingsState
45import com.coder.gateway.views.steps.CoderWorkspacesStepView
56import com.google.gson.Gson
7+ import com.google.gson.JsonSyntaxException
68import com.intellij.openapi.diagnostic.Logger
9+ import com.intellij.openapi.progress.ProgressIndicator
710import org.zeroturnaround.exec.ProcessExecutor
811import java.io.BufferedInputStream
912import java.io.FileInputStream
@@ -26,7 +29,8 @@ import javax.xml.bind.annotation.adapters.HexBinaryAdapter
2629 */
2730class CoderCLIManager @JvmOverloads constructor(
2831 private val deploymentURL : URL ,
29- destinationDir : Path ,
32+ dataDir : Path ,
33+ cliDir : Path ? = null ,
3034 remoteBinaryURLOverride : String? = null ,
3135 private val sshConfigPath : Path = Path .of(System .getProperty("user.home")).resolve(".ssh/config"),
3236) {
@@ -52,8 +56,8 @@ class CoderCLIManager @JvmOverloads constructor(
5256 }
5357 val host = getSafeHost(deploymentURL)
5458 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()
5761 }
5862
5963 /* *
@@ -293,26 +297,47 @@ class CoderCLIManager @JvmOverloads constructor(
293297 val raw = exec(" version" , " --output" , " json" )
294298 val json = Gson ().fromJson(raw, Version ::class .java)
295299 if (json?.version == null ) {
296- throw InvalidVersionException (" No version found in output" )
300+ throw MissingVersionException (" No version found in output" )
297301 }
298302 return CoderSemVer .parse(json.version)
299303 }
300304
301305 /* *
302306 * 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.
305310 */
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()
312314 } 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
315336 }
337+
338+ val matches = cliVersion == buildVersion
339+ logger.info(" $localBinaryPath version $cliVersion matches $buildVersion : $matches " )
340+ return matches
316341 }
317342
318343 private fun exec (vararg args : String ): String {
@@ -404,6 +429,68 @@ class CoderCLIManager @JvmOverloads constructor(
404429 fun getHostName (url : URL , ws : WorkspaceAgentModel ): String {
405430 return " coder-jetbrains--${ws.name} --${getSafeHost(url)} "
406431 }
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+ }
407494 }
408495}
409496
@@ -418,5 +505,5 @@ class Environment(private val env: Map<String, String> = emptyMap()) {
418505}
419506
420507class ResponseException (message : String , val code : Int ) : Exception(message)
421-
422508class SSHConfigFormatException (message : String ) : Exception(message)
509+ class MissingVersionException (message : String ) : Exception(message)
0 commit comments