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

Merged
fioan89 merged 10 commits intomainfromimpl-progress-while-downloading-cli
Jun 19, 2025
Merged
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
29 changes: 15 additions & 14 deletionssrc/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,10 +9,10 @@ import com.coder.toolbox.util.CoderProtocolHandler
import com.coder.toolbox.util.DialogUi
import com.coder.toolbox.util.withPath
import com.coder.toolbox.views.Action
import com.coder.toolbox.views.AuthWizardPage
import com.coder.toolbox.views.CoderCliSetupWizardPage
import com.coder.toolbox.views.CoderSettingsPage
import com.coder.toolbox.views.NewEnvironmentPage
import com.coder.toolbox.views.state.AuthWizardState
import com.coder.toolbox.views.state.CoderCliSetupWizardState
import com.coder.toolbox.views.state.WizardStep
import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon
import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon.IconType
Expand DownExpand Up@@ -242,7 +242,7 @@ class CoderRemoteProvider(
environments.value = LoadableState.Value(emptyList())
isInitialized.update { false }
client = null
AuthWizardState.resetSteps()
CoderCliSetupWizardState.resetSteps()
}

override val svgIcon: SvgIcon =
Expand DownExpand Up@@ -301,7 +301,7 @@ class CoderRemoteProvider(
*/
override suspend fun handleUri(uri: URI) {
linkHandler.handle(
uri,shouldDoAutoLogin(),
uri,shouldDoAutoSetup(),
{
coderHeaderPage.isBusyCreatingNewEnvironment.update {
true
Expand DownExpand Up@@ -343,17 +343,17 @@ class CoderRemoteProvider(
* list.
*/
override fun getOverrideUiPage(): UiPage? {
// Showsign in page if we have not configured the client yet.
// Showthe setup page if we have not configured the client yet.
if (client == null) {
val errorBuffer = mutableListOf<Throwable>()
// When coming back to the application,authenticate immediately.
valautologin =shouldDoAutoLogin()
// When coming back to the application,initializeSession immediately.
valautoSetup =shouldDoAutoSetup()
context.secrets.lastToken.let { lastToken ->
context.secrets.lastDeploymentURL.let { lastDeploymentURL ->
if (autologin && lastDeploymentURL.isNotBlank() && (lastToken.isNotBlank() || !settings.requireTokenAuth)) {
if (autoSetup && lastDeploymentURL.isNotBlank() && (lastToken.isNotBlank() || !settings.requireTokenAuth)) {
try {
AuthWizardState.goToStep(WizardStep.LOGIN)
returnAuthWizardPage(context, settingsPage, visibilityState, true, ::onConnect)
CoderCliSetupWizardState.goToStep(WizardStep.CONNECT)
returnCoderCliSetupWizardPage(context, settingsPage, visibilityState, true, ::onConnect)
} catch (ex: Exception) {
errorBuffer.add(ex)
}
Expand All@@ -363,18 +363,19 @@ class CoderRemoteProvider(
firstRun = false

// Login flow.
val authWizard = AuthWizardPage(context, settingsPage, visibilityState, onConnect = ::onConnect)
val setupWizardPage =
CoderCliSetupWizardPage(context, settingsPage, visibilityState, onConnect = ::onConnect)
// We might have navigated here due to a polling error.
errorBuffer.forEach {
authWizard.notify("Error encountered", it)
setupWizardPage.notify("Error encountered", it)
}
// and now reset the errors, otherwise we show it every time on the screen
returnauthWizard
returnsetupWizardPage
}
return null
}

private funshouldDoAutoLogin(): Boolean = firstRun && context.secrets.rememberMe == true
private funshouldDoAutoSetup(): Boolean = firstRun && context.secrets.rememberMe == true

private suspend fun onConnect(client: CoderRestClient, cli: CoderCLIManager) {
// Store the URL and token for use next time.
Expand Down
61 changes: 48 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(buildVersion, 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(buildVersion, 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(buildVersion: String, showTextProgress: (String) -> Unit): Boolean {
val eTag = getBinaryETag()
val conn = remoteBinaryURL.openConnection() as HttpURLConnection
if (!settings.headerCommand.isNullOrBlank()) {
Expand All@@ -162,13 +167,27 @@ class CoderCLIManager(
when (conn.responseCode) {
HttpURLConnection.HTTP_OK -> {
logger.info("Downloading binary to $localBinaryPath")
Files.deleteIfExists(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("${settings.defaultCliBinaryNameByOsAndArch} $buildVersion - ${totalRead.toHumanReadableSize()} downloaded")
}
}
}
if (getOS() != OS.WINDOWS) {
localBinaryPath.toFile().setExecutable(true)
Expand All@@ -178,6 +197,7 @@ class CoderCLIManager(

HttpURLConnection.HTTP_NOT_MODIFIED -> {
logger.info("Using cached binary at $localBinaryPath")
showTextProgress("Using cached binary")
return false
}
}
Expand All@@ -190,6 +210,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 +238,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
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,8 +5,8 @@ import com.coder.toolbox.cli.CoderCLIManager
import com.coder.toolbox.sdk.CoderRestClient
import com.coder.toolbox.sdk.ex.APIResponseException
import com.coder.toolbox.util.toURL
import com.coder.toolbox.views.state.AuthContext
import com.coder.toolbox.views.state.AuthWizardState
import com.coder.toolbox.views.state.CoderCliSetupContext
import com.coder.toolbox.views.state.CoderCliSetupWizardState
import com.coder.toolbox.views.state.WizardStep
import com.jetbrains.toolbox.api.remoteDev.ProviderVisibilityState
import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription
Expand All@@ -16,26 +16,26 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.util.UUID

classAuthWizardPage(
classCoderCliSetupWizardPage(
private val context: CoderToolboxContext,
private val settingsPage: CoderSettingsPage,
private val visibilityState: MutableStateFlow<ProviderVisibilityState>,
initialAutoLogin: Boolean = false,
initialAutoSetup: Boolean = false,
onConnect: suspend (
client: CoderRestClient,
cli: CoderCLIManager,
) -> Unit,
) : CoderPage(context.i18n.ptrl("Authenticate to Coder"), false) {
private valshouldAutoLogin = MutableStateFlow(initialAutoLogin)
) : CoderPage(context.i18n.ptrl("Setting up Coder"), false) {
private valshouldAutoSetup = MutableStateFlow(initialAutoSetup)
private val settingsAction = Action(context.i18n.ptrl("Settings"), actionBlock = {
context.ui.showUiPage(settingsPage)
})

private valsignInStep =SignInStep(context, this::notify)
private valdeploymentUrlStep =DeploymentUrlStep(context, this::notify)
private val tokenStep = TokenStep(context)
private val connectStep = ConnectStep(
context,
shouldAutoLogin,
shouldAutoSetup,
this::notify,
this::displaySteps,
onConnect
Expand All@@ -50,9 +50,9 @@ class AuthWizardPage(
private val errorBuffer = mutableListOf<Throwable>()

init {
if (shouldAutoLogin.value) {
AuthContext.url = context.secrets.lastDeploymentURL.toURL()
AuthContext.token = context.secrets.lastToken
if (shouldAutoSetup.value) {
CoderCliSetupContext.url = context.secrets.lastDeploymentURL.toURL()
CoderCliSetupContext.token = context.secrets.lastToken
}
}

Expand All@@ -67,22 +67,22 @@ class AuthWizardPage(
}

private fun displaySteps() {
when (AuthWizardState.currentStep()) {
when (CoderCliSetupWizardState.currentStep()) {
WizardStep.URL_REQUEST -> {
fields.update {
listOf(signInStep.panel)
listOf(deploymentUrlStep.panel)
}
actionButtons.update {
listOf(
Action(context.i18n.ptrl("Sign In"), closesPage = false, actionBlock = {
if (signInStep.onNext()) {
Action(context.i18n.ptrl("Next"), closesPage = false, actionBlock = {
if (deploymentUrlStep.onNext()) {
displaySteps()
}
}),
settingsAction
)
}
signInStep.onVisible()
deploymentUrlStep.onVisible()
}

WizardStep.TOKEN_REQUEST -> {
Expand All@@ -106,7 +106,7 @@ class AuthWizardPage(
tokenStep.onVisible()
}

WizardStep.LOGIN -> {
WizardStep.CONNECT -> {
fields.update {
listOf(connectStep.panel)
}
Expand All@@ -115,7 +115,7 @@ class AuthWizardPage(
settingsAction,
Action(context.i18n.ptrl("Back"), closesPage = false, actionBlock = {
connectStep.onBack()
shouldAutoLogin.update {
shouldAutoSetup.update {
false
}
displaySteps()
Expand DownExpand Up@@ -150,7 +150,7 @@ class AuthWizardPage(
context.cs.launch {
context.ui.showSnackbar(
UUID.randomUUID().toString(),
context.i18n.ptrl("Error encounteredduring authentication"),
context.i18n.ptrl("Error encounteredwhile setting up Coder"),
context.i18n.pnotr(textError ?: ""),
context.i18n.ptrl("Dismiss")
)
Expand Down
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp