Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

impl: visual text progress during Coder CLI downloading#130

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Draft
fioan89 wants to merge3 commits intomain
base:main
Choose a base branch
Loading
fromimpl-progress-while-downloading-cli
Draft
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletionsCHANGELOG.md
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,10 @@

## Unreleased

### Added

- visual text progress during Coder CLI downloading

### Changed

- the plugin will now remember the SSH connection state for each workspace, and it will try to automatically
Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -346,7 +346,7 @@ class CoderRemoteProvider(
// Show sign in page if we have not configured the client yet.
if (client == null) {
val errorBuffer = mutableListOf<Throwable>()
// When coming back to the application,authenticate immediately.
// When coming back to the application,initializeSession immediately.
val autologin = shouldDoAutoLogin()
context.secrets.lastToken.let { lastToken ->
context.secrets.lastDeploymentURL.let { lastDeploymentURL ->
Expand Down
60 changes: 47 additions & 13 deletionssrc/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,7 +32,7 @@ import java.net.HttpURLConnection
import java.net.URL
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.nio.file.StandardOpenOption
import java.util.zip.GZIPInputStream
import javax.net.ssl.HttpsURLConnection

Expand All@@ -44,6 +44,8 @@ internal data class Version(
@Json(name = "version") val version: String,
)

private const val DOWNLOADING_CODER_CLI = "Downloading Coder CLI..."

/**
* Do as much as possible to get a valid, up-to-date CLI.
*
Expand All@@ -60,6 +62,7 @@ fun ensureCLI(
context: CoderToolboxContext,
deploymentURL: URL,
buildVersion: String,
showTextProgress: (String) -> Unit
): CoderCLIManager {
val settings = context.settingsStore.readOnly()
val cli = CoderCLIManager(deploymentURL, context.logger, settings)
Expand All@@ -76,9 +79,10 @@ fun ensureCLI(

// If downloads are enabled download the new version.
if (settings.enableDownloads) {
context.logger.info("Downloading Coder CLI...")
context.logger.info(DOWNLOADING_CODER_CLI)
showTextProgress(DOWNLOADING_CODER_CLI)
try {
cli.download()
cli.download(showTextProgress)
return cli
} catch (e: java.nio.file.AccessDeniedException) {
// Might be able to fall back to the data directory.
Expand All@@ -98,8 +102,9 @@ fun ensureCLI(
}

if (settings.enableDownloads) {
context.logger.info("Downloading Coder CLI...")
dataCLI.download()
context.logger.info(DOWNLOADING_CODER_CLI)
showTextProgress(DOWNLOADING_CODER_CLI)
dataCLI.download(showTextProgress)
return dataCLI
}

Expand DownExpand Up@@ -137,7 +142,7 @@ class CoderCLIManager(
/**
* Download the CLI from the deployment if necessary.
*/
fun download(): Boolean {
fun download(showTextProgress: (String) -> Unit): Boolean {
val eTag = getBinaryETag()
val conn = remoteBinaryURL.openConnection() as HttpURLConnection
if (!settings.headerCommand.isNullOrBlank()) {
Expand All@@ -163,12 +168,25 @@ class CoderCLIManager(
HttpURLConnection.HTTP_OK -> {
logger.info("Downloading binary to $localBinaryPath")
Files.createDirectories(localBinaryPath.parent)
conn.inputStream.use {
Files.copy(
if (conn.contentEncoding == "gzip") GZIPInputStream(it) else it,
localBinaryPath,
StandardCopyOption.REPLACE_EXISTING,
)
val outputStream = Files.newOutputStream(
localBinaryPath,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING
)
val sourceStream = if (conn.isGzip()) GZIPInputStream(conn.inputStream) else conn.inputStream

val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytesRead: Int
var totalRead = 0L

sourceStream.use { source ->
outputStream.use { sink ->
while (source.read(buffer).also { bytesRead = it } != -1) {
sink.write(buffer, 0, bytesRead)
totalRead += bytesRead
showTextProgress("Downloaded ${totalRead.toHumanReadableSize()}...")
}
}
}
if (getOS() != OS.WINDOWS) {
localBinaryPath.toFile().setExecutable(true)
Expand All@@ -178,6 +196,7 @@ class CoderCLIManager(

HttpURLConnection.HTTP_NOT_MODIFIED -> {
logger.info("Using cached binary at $localBinaryPath")
showTextProgress("Using cached binary")
return false
}
}
Expand All@@ -190,6 +209,21 @@ class CoderCLIManager(
throw ResponseException("Unexpected response from $remoteBinaryURL", conn.responseCode)
}

private fun HttpURLConnection.isGzip(): Boolean = this.contentEncoding.equals("gzip", ignoreCase = true)

fun Long.toHumanReadableSize(): String {
if (this < 1024) return "$this B"

val kb = this / 1024.0
if (kb < 1024) return String.format("%.1f KB", kb)

val mb = kb / 1024.0
if (mb < 1024) return String.format("%.1f MB", mb)

val gb = mb / 1024.0
return String.format("%.1f GB", gb)
}

/**
* Return the entity tag for the binary on disk, if any.
*/
Expand All@@ -203,7 +237,7 @@ class CoderCLIManager(
}

/**
* Use the provided token toauthenticate the CLI.
* Use the provided token toinitializeSession the CLI.
*/
fun login(token: String): String {
logger.info("Storing CLI credentials in $coderConfigPath")
Expand Down
12 changes: 8 additions & 4 deletionssrc/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -131,12 +131,11 @@ open class CoderRestClient(
}

/**
* Authenticate and load information about the current user and the build
* version.
* Load information about the current user and the build version.
*
* @throws [APIResponseException].
*/
suspend funauthenticate(): User {
suspend funinitializeSession(): User {
me = me()
buildVersion = buildInfo().version
return me
Expand All@@ -149,7 +148,12 @@ open class CoderRestClient(
suspend fun me(): User {
val userResponse = retroRestClient.me()
if (!userResponse.isSuccessful) {
throw APIResponseException("authenticate", url, userResponse.code(), userResponse.parseErrorBody(moshi))
throw APIResponseException(
"initializeSession",
url,
userResponse.code(),
userResponse.parseErrorBody(moshi)
)
}

return userResponse.body()!!
Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ import kotlin.time.Duration.Companion.seconds
import kotlin.time.toJavaDuration

private const val CAN_T_HANDLE_URI_TITLE = "Can't handle URI"
private val noOpTextProgress: (String) -> Unit = { _ -> }

@Suppress("UnstableApiUsage")
open class CoderProtocolHandler(
Expand DownExpand Up@@ -143,7 +144,7 @@ open class CoderProtocolHandler(
if (settings.requireTokenAuth) token else null,
PluginManager.pluginInfo.version
)
client.authenticate()
client.initializeSession()
return client
}

Expand DownExpand Up@@ -304,7 +305,8 @@ open class CoderProtocolHandler(
val cli = ensureCLI(
context,
deploymentURL.toURL(),
restClient.buildInfo().version
restClient.buildInfo().version,
noOpTextProgress
)

// We only need to log in if we are using token-based auth.
Expand Down
14 changes: 9 additions & 5 deletionssrc/main/kotlin/com/coder/toolbox/views/ConnectStep.kt
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,7 +77,6 @@ class ConnectStep(
signInJob?.cancel()
signInJob = context.cs.launch {
try {
statusField.textState.update { (context.i18n.ptrl("Authenticating to ${AuthContext.url!!.host}...")) }
val client = CoderRestClient(
context,
AuthContext.url!!,
Expand All@@ -86,12 +85,17 @@ class ConnectStep(
)
// allows interleaving with the back/cancel action
yield()
client.authenticate()
statusField.textState.update { (context.i18n.ptrl("Checking Coder binary...")) }
val cli = ensureCLI(context, client.url, client.buildVersion)
client.initializeSession()
statusField.textState.update { (context.i18n.ptrl("Checking Coder CLI...")) }
val cli = ensureCLI(
context, client.url,
client.buildVersion
) { progress ->
statusField.textState.update { (context.i18n.pnotr(progress)) }
}
// We only need to log in if we are using token-based auth.
if (client.token != null) {
statusField.textState.update { (context.i18n.ptrl("Configuring CLI...")) }
statusField.textState.update { (context.i18n.ptrl("ConfiguringCoderCLI...")) }
// allows interleaving with the back/cancel action
yield()
cli.login(client.token)
Expand Down
36 changes: 19 additions & 17 deletionssrc/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,6 +62,8 @@ import kotlin.test.assertFalse
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue

private val noOpTextProgress: (String) -> Unit = { _ -> }

internal class CoderCLIManagerTest {
private val context = CoderToolboxContext(
mockk<ToolboxUi>(),
Expand DownExpand Up@@ -145,7 +147,7 @@ internal class CoderCLIManagerTest {
val ex =
assertFailsWith(
exceptionClass = ResponseException::class,
block = { ccm.download() },
block = { ccm.download(noOpTextProgress) },
)
assertEquals(HttpURLConnection.HTTP_INTERNAL_ERROR, ex.code)

Expand DownExpand Up@@ -200,7 +202,7 @@ internal class CoderCLIManagerTest {

assertFailsWith(
exceptionClass = AccessDeniedException::class,
block = { ccm.download() },
block = { ccm.download(noOpTextProgress) },
)

srv.stop(0)
Expand DownExpand Up@@ -229,11 +231,11 @@ internal class CoderCLIManagerTest {
).readOnly(),
)

assertTrue(ccm.download())
assertTrue(ccm.download(noOpTextProgress))
assertDoesNotThrow { ccm.version() }

// It should skip the second attempt.
assertFalse(ccm.download())
assertFalse(ccm.download(noOpTextProgress))

// Make sure login failures propagate.
assertFailsWith(
Expand All@@ -258,11 +260,11 @@ internal class CoderCLIManagerTest {
).readOnly(),
)

assertEquals(true, ccm.download())
assertEquals(true, ccm.download(noOpTextProgress))
assertEquals(SemVer(url.port.toLong(), 0, 0), ccm.version())

// It should skip the second attempt.
assertEquals(false, ccm.download())
assertEquals(false, ccm.download(noOpTextProgress))

// Should use the source override.
ccm = CoderCLIManager(
Expand All@@ -278,7 +280,7 @@ internal class CoderCLIManagerTest {
).readOnly(),
)

assertEquals(true, ccm.download())
assertEquals(true, ccm.download(noOpTextProgress))
assertContains(ccm.localBinaryPath.toFile().readText(), "0.0.0")

srv.stop(0)
Expand DownExpand Up@@ -326,7 +328,7 @@ internal class CoderCLIManagerTest {
assertEquals("cli", ccm.localBinaryPath.toFile().readText())
assertEquals(0, ccm.localBinaryPath.toFile().lastModified())

assertTrue(ccm.download())
assertTrue(ccm.download(noOpTextProgress))

assertNotEquals("cli", ccm.localBinaryPath.toFile().readText())
assertNotEquals(0, ccm.localBinaryPath.toFile().lastModified())
Expand All@@ -351,8 +353,8 @@ internal class CoderCLIManagerTest {
val ccm1 = CoderCLIManager(url1, context.logger, settings)
val ccm2 = CoderCLIManager(url2, context.logger, settings)

assertTrue(ccm1.download())
assertTrue(ccm2.download())
assertTrue(ccm1.download(noOpTextProgress))
assertTrue(ccm2.download(noOpTextProgress))

srv1.stop(0)
srv2.stop(0)
Expand DownExpand Up@@ -883,12 +885,12 @@ internal class CoderCLIManagerTest {
Result.ERROR -> {
assertFailsWith(
exceptionClass = AccessDeniedException::class,
block = { ensureCLI(localContext, url, it.buildVersion) },
block = { ensureCLI(localContext, url, it.buildVersion, noOpTextProgress) },
)
}

Result.NONE -> {
val ccm = ensureCLI(localContext, url, it.buildVersion)
val ccm = ensureCLI(localContext, url, it.buildVersion, noOpTextProgress)
assertEquals(settings.binPath(url), ccm.localBinaryPath)
assertFailsWith(
exceptionClass = ProcessInitException::class,
Expand All@@ -897,25 +899,25 @@ internal class CoderCLIManagerTest {
}

Result.DL_BIN -> {
val ccm = ensureCLI(localContext, url, it.buildVersion)
val ccm = ensureCLI(localContext, url, it.buildVersion, noOpTextProgress)
assertEquals(settings.binPath(url), ccm.localBinaryPath)
assertEquals(SemVer(url.port.toLong(), 0, 0), ccm.version())
}

Result.DL_DATA -> {
val ccm = ensureCLI(localContext, url, it.buildVersion)
val ccm = ensureCLI(localContext, url, it.buildVersion, noOpTextProgress)
assertEquals(settings.binPath(url, true), ccm.localBinaryPath)
assertEquals(SemVer(url.port.toLong(), 0, 0), ccm.version())
}

Result.USE_BIN -> {
val ccm = ensureCLI(localContext, url, it.buildVersion)
val ccm = ensureCLI(localContext, url, it.buildVersion, noOpTextProgress)
assertEquals(settings.binPath(url), ccm.localBinaryPath)
assertEquals(SemVer.parse(it.version ?: ""), ccm.version())
}

Result.USE_DATA -> {
val ccm = ensureCLI(localContext, url, it.buildVersion)
val ccm = ensureCLI(localContext, url, it.buildVersion, noOpTextProgress)
assertEquals(settings.binPath(url, true), ccm.localBinaryPath)
assertEquals(SemVer.parse(it.fallbackVersion ?: ""), ccm.version())
}
Expand DownExpand Up@@ -955,7 +957,7 @@ internal class CoderCLIManagerTest {
context.logger,
).readOnly(),
)
assertEquals(true, ccm.download())
assertEquals(true, ccm.download(noOpTextProgress))
assertEquals(it.second, ccm.features, "version: ${it.first}")

srv.stop(0)
Expand Down
Loading

[8]ページ先頭

©2009-2025 Movatter.jp