diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dd84b7..5013b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +### Fixed + +- snackbar dismissal no longer cancels the calling coroutine, so error popups during URI handling don't leave the page + stuck in a busy state + ### Changed - skip the Coder TLS alternate hostname when fetching IDE metadata from JetBrains diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt index 972497e..debc80c 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt @@ -360,15 +360,18 @@ class CoderRemoteProvider( context.logger.info("Handling $uri...") val newUrl = resolveDeploymentUrl(params)?.toURL() ?: return val newToken = if (context.settingsStore.requiresMTlsAuth) null else resolveToken(params) ?: return - coderHeaderPage.isBusy.update { true } if (sameUrl(newUrl, client?.url)) { - if (context.settingsStore.requiresTokenAuth) { - newToken?.let { - refreshSession(newUrl, it) + coderHeaderPage.isBusy.update { true } + try { + if (context.settingsStore.requiresTokenAuth) { + newToken?.let { + refreshSession(newUrl, it) + } } + linkHandler.handle(params, newUrl, this.client!!, this.cli!!) + } finally { + coderHeaderPage.isBusy.update { false } } - linkHandler.handle(params, newUrl, this.client!!, this.cli!!) - coderHeaderPage.isBusy.update { false } } else { // Different URL - we need a new connection. Tear down any // in-flight wizard, install a fresh one on the router, and let @@ -378,10 +381,7 @@ class CoderRemoteProvider( context, settingsPage, visibilityState, url = newUrl, credentials = credentials, - onConnect = onConnect.andThen(deferredLinkHandler(params, newUrl)) - .andThen { _, _ -> - coderHeaderPage.isBusy.update { false } - }, + onConnect = onConnect.andThen(deferredLinkHandler(params, newUrl)), onTokenRefreshed = ::onTokenRefreshed, ) router.navigate(wizard) @@ -397,7 +397,6 @@ class CoderRemoteProvider( "Error encountered while handling Coder URI", textError ?: "" ) - coderHeaderPage.isBusy.update { false } } finally { firstRun = false } @@ -654,6 +653,7 @@ class CoderRemoteProvider( deploymentUrl: URL, ): SuspendBiConsumer = SuspendBiConsumer { client, cli -> context.cs.launch(CoroutineName("Deferred Link Handler")) { + coderHeaderPage.isBusy.update { true } try { linkHandler.handle(params, deploymentUrl, client, cli) } catch (ex: Exception) { @@ -661,6 +661,8 @@ class CoderRemoteProvider( "Error handling deferred link", ex.message ?: "" ) + } finally { + coderHeaderPage.isBusy.update { false } } } } diff --git a/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt b/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt index 5308067..1b14622 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt @@ -13,7 +13,10 @@ import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager import com.jetbrains.toolbox.api.ui.ToolboxUi +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import java.net.URL import java.util.UUID @@ -49,44 +52,53 @@ data class CoderToolboxContext( ?: settingsStore.defaultURL.toURL() } - suspend fun logAndShowError(title: String, error: String) { + fun logAndShowError(title: String, error: String) { logger.error(error) - ui.showSnackbar( - UUID.randomUUID().toString(), - i18n.pnotr(title), - i18n.pnotr(error), - i18n.ptrl("OK") - ) + showSnackbar(title, error) } - suspend fun logAndShowError(title: String, error: String, exception: Exception) { + fun logAndShowError(title: String, error: String, exception: Exception) { logger.error(exception, error) - ui.showSnackbar( - UUID.randomUUID().toString(), - i18n.pnotr(title), - i18n.pnotr(error), - i18n.ptrl("OK") - ) + showSnackbar(title, error) } - suspend fun logAndShowWarning(title: String, warning: String) { + fun logAndShowWarning(title: String, warning: String) { logger.warn(warning) - ui.showSnackbar( - UUID.randomUUID().toString(), - i18n.pnotr(title), - i18n.pnotr(warning), - i18n.ptrl("OK") - ) + showSnackbar(title, warning) } - suspend fun logAndShowInfo(title: String, info: String) { + fun logAndShowInfo(title: String, info: String) { logger.info(info) - ui.showSnackbar( - UUID.randomUUID().toString(), - i18n.pnotr(title), - i18n.pnotr(info), - i18n.ptrl("OK") - ) + showSnackbar(title, info) + } + + /** + * Displays a snackbar on a child of the plugin coroutine scope rather than on the + * caller's coroutine, without waiting for it. + * + * Toolbox keeps the [ToolboxUi.showSnackbar] coroutine suspended for the entire lifetime + * of the snackbar and cancels it (rather than resuming it) when the snackbar goes away. + * Calling it directly on the caller's coroutine would therefore either block the caller + * until the snackbar is gone or, on dismissal, abruptly cancel the caller (e.g. the URI + * handler) - skipping any code that runs after the error is shown, such as resetting the + * busy state. Launching it fire-and-forget on the plugin scope lets the caller continue + * immediately while the snackbar lives independently. + */ + fun showSnackbar(title: String, text: String) { + cs.launch(CoroutineName("snackbar")) { + try { + ui.showSnackbar( + UUID.randomUUID().toString(), + i18n.pnotr(title), + i18n.pnotr(text), + i18n.ptrl("OK") + ) + } catch (_: CancellationException) { + // Expected when the snackbar is dismissed or the plugin scope shuts down. + } catch (ex: Exception) { + logger.error(ex, "Failed to display snackbar with title '$title'") + } + } } fun popupPluginMainPage() { diff --git a/src/main/kotlin/com/coder/toolbox/views/ErrorReporter.kt b/src/main/kotlin/com/coder/toolbox/views/ErrorReporter.kt index e6d6ac5..1549bfd 100644 --- a/src/main/kotlin/com/coder/toolbox/views/ErrorReporter.kt +++ b/src/main/kotlin/com/coder/toolbox/views/ErrorReporter.kt @@ -4,8 +4,6 @@ import com.coder.toolbox.CoderToolboxContext import com.coder.toolbox.util.prettify import com.jetbrains.toolbox.api.remoteDev.ProviderVisibilityState import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import java.util.UUID sealed class ErrorReporter { @@ -47,15 +45,7 @@ private class ErrorReporterImpl( private fun showError(ex: Throwable) { val textError = ex.prettify() - - context.cs.launch { - context.ui.showSnackbar( - UUID.randomUUID().toString(), - context.i18n.ptrl("Error encountered while setting up Coder"), - context.i18n.pnotr(textError), - context.i18n.ptrl("Dismiss") - ) - } + context.showSnackbar("Error encountered while setting up Coder", textError) } override fun flush() {