diff --git a/build.gradle.kts b/build.gradle.kts index e0ca598..4c89011 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -39,7 +39,6 @@ jvmWrapper { dependencies { compileOnly(libs.bundles.toolbox.plugin.api) implementation(libs.slf4j) - implementation(libs.tinylog) implementation(libs.bundles.serialization) implementation(libs.coroutines.core) implementation(libs.okhttp) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bed6d8a..fc96a97 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,17 +1,16 @@ [versions] -toolbox-plugin-api = "0.6.2.6.0.37447" -kotlin = "2.0.10" -coroutines = "1.7.3" -serialization = "1.5.0" +toolbox-plugin-api = "0.7.2.6.0.38311" +kotlin = "2.1.0" +coroutines = "1.10.1" +serialization = "1.8.0" okhttp = "4.10.0" slf4j = "2.0.3" -tinylog = "2.7.0" dependency-license-report = "2.5" marketplace-client = "2.0.38" gradle-wrapper = "0.14.0" exec = "1.12" moshi = "1.15.1" -ksp = "2.0.10-1.0.24" +ksp = "2.1.0-1.0.29" retrofit = "2.8.2" [libraries] @@ -24,7 +23,6 @@ serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-jso serialization-json-okio = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-okio", version.ref = "serialization" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } -tinylog = {module = "org.tinylog:slf4j-tinylog", version.ref = "tinylog"} exec = { module = "org.zeroturnaround:zt-exec", version.ref = "exec" } moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi"} moshi-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi"} diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt index 07a9000..c3e5f64 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt @@ -16,7 +16,6 @@ import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateConsumer import com.jetbrains.toolbox.api.ui.ToolboxUi import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import java.util.concurrent.CompletableFuture /** * Represents an agent and workspace combination. @@ -29,12 +28,12 @@ class CoderRemoteEnvironment( private var workspace: Workspace, private var agent: WorkspaceAgent, private var cs: CoroutineScope, -) : AbstractRemoteProviderEnvironment() { +) : AbstractRemoteProviderEnvironment("${workspace.name}.${agent.name}") { private var status = WorkspaceAndAgentStatus.from(workspace, agent) private val ui: ToolboxUi = serviceLocator.getService(ToolboxUi::class.java) - override fun getId(): String = "${workspace.name}.${agent.name}" - override fun getName(): String = "${workspace.name}.${agent.name}" + + override var name: String = "${workspace.name}.${agent.name}" init { actionsList.add( @@ -105,12 +104,11 @@ class CoderRemoteEnvironment( * The contents are provided by the SSH view provided by Toolbox, all we * have to do is provide it a host name. */ - override fun getContentsView(): CompletableFuture = - CompletableFuture.completedFuture(EnvironmentView(client.url, workspace, agent)) + override suspend fun getContentsView(): EnvironmentContentsView = EnvironmentView(client.url, workspace, agent) /** - * Does nothing. In theory we could do something like start the workspace - * when you click into the workspace but you would still need to press + * Does nothing. In theory, we could do something like start the workspace + * when you click into the workspace, but you would still need to press * "connect" anyway before the content is populated so there does not seem * to be much value. */ @@ -140,12 +138,12 @@ class CoderRemoteEnvironment( if (other == null) return false if (this === other) return true // Note the triple === if (other !is CoderRemoteEnvironment) return false - if (getId() != other.getId()) return false + if (id != other.id) return false return true } /** * Companion to equals, for sets. */ - override fun hashCode(): Int = getId().hashCode() + override fun hashCode(): Int = id.hashCode() } diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt index 54556f1..a360229 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt @@ -1,6 +1,7 @@ package com.coder.toolbox import com.coder.toolbox.cli.CoderCLIManager +import com.coder.toolbox.logger.CoderLoggerFactory import com.coder.toolbox.sdk.CoderRestClient import com.coder.toolbox.sdk.v2.models.WorkspaceStatus import com.coder.toolbox.services.CoderSecretsService @@ -34,7 +35,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import okhttp3.OkHttpClient -import org.slf4j.LoggerFactory import java.net.URI import java.net.URL import kotlin.coroutines.cancellation.CancellationException @@ -43,8 +43,8 @@ import kotlin.time.Duration.Companion.seconds class CoderRemoteProvider( private val serviceLocator: ServiceLocator, private val httpClient: OkHttpClient, -) : RemoteProvider { - private val logger = LoggerFactory.getLogger(javaClass) +) : RemoteProvider("Coder") { + private val logger = CoderLoggerFactory.getLogger(javaClass) private val ui: ToolboxUi = serviceLocator.getService(ToolboxUi::class.java) private val consumer: RemoteEnvironmentConsumer = serviceLocator.getService(RemoteEnvironmentConsumer::class.java) @@ -185,18 +185,18 @@ class CoderRemoteProvider( consumer.consumeEnvironments(emptyList(), true) } - override fun getName(): String = "Coder" - override fun getSvgIcon(): SvgIcon = + override val svgIcon: SvgIcon = SvgIcon(this::class.java.getResourceAsStream("/icon.svg")?.readAllBytes() ?: byteArrayOf()) - override fun getNoEnvironmentsSvgIcon(): ByteArray = - this::class.java.getResourceAsStream("/icon.svg")?.readAllBytes() ?: byteArrayOf() + override val noEnvironmentsSvgIcon: SvgIcon? = + SvgIcon(this::class.java.getResourceAsStream("/icon.svg")?.readAllBytes() ?: byteArrayOf()) /** * TODO@JB: It would be nice to show "loading workspaces" at first but it * appears to be only called once. */ - override fun getNoEnvironmentsDescription(): String = "No workspaces yet" + override val noEnvironmentsDescription: String? = "No workspaces yet" + /** * TODO@JB: Supposedly, setting this to false causes the new environment @@ -205,7 +205,7 @@ class CoderRemoteProvider( * this changes it would be nice to have a new spot to show the * URL. */ - override fun canCreateNewEnvironments(): Boolean = false + override val canCreateNewEnvironments: Boolean = false /** * Just displays the deployment URL at the moment, but we could use this as @@ -216,7 +216,7 @@ class CoderRemoteProvider( /** * We always show a list of environments. */ - override fun isSingleEnvironment(): Boolean = false + override val isSingleEnvironment: Boolean = false /** * TODO: Possibly a good idea to start/stop polling based on visibility, at @@ -241,9 +241,11 @@ class CoderRemoteProvider( */ override fun handleUri(uri: URI) { val params = uri.toQueryParameters() - val name = linkHandler.handle(params) - // TODO@JB: Now what? How do we actually connect this workspace? - logger.debug("External request for {}: {}", name, uri) + coroutineScope.launch { + val name = linkHandler.handle(params) + // TODO@JB: Now what? How do we actually connect this workspace? + logger.debug("External request for {}: {}", name, uri) + } } /** diff --git a/src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt b/src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt index f7e6cd1..7875cf7 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt @@ -1,6 +1,8 @@ package com.coder.toolbox +import com.coder.toolbox.logger.CoderLoggerFactory import com.jetbrains.toolbox.api.core.ServiceLocator +import com.jetbrains.toolbox.api.core.diagnostics.Logger import com.jetbrains.toolbox.api.remoteDev.RemoteDevExtension import com.jetbrains.toolbox.api.remoteDev.RemoteProvider import okhttp3.OkHttpClient @@ -11,6 +13,9 @@ import okhttp3.OkHttpClient class CoderToolboxExtension : RemoteDevExtension { // All services must be passed in here and threaded as necessary. override fun createRemoteProviderPluginInstance(serviceLocator: ServiceLocator): RemoteProvider { + // initialize logger factory + CoderLoggerFactory.tLogger = serviceLocator.getService(Logger::class.java) + return CoderRemoteProvider( serviceLocator, OkHttpClient(), diff --git a/src/main/kotlin/com/coder/toolbox/browser/BrowserUtil.kt b/src/main/kotlin/com/coder/toolbox/browser/BrowserUtil.kt index 57de42f..000263c 100644 --- a/src/main/kotlin/com/coder/toolbox/browser/BrowserUtil.kt +++ b/src/main/kotlin/com/coder/toolbox/browser/BrowserUtil.kt @@ -6,7 +6,7 @@ import org.zeroturnaround.exec.ProcessExecutor class BrowserUtil { companion object { - fun browse(url: String, errorHandler: (BrowserException) -> Unit) { + suspend fun browse(url: String, errorHandler: suspend (BrowserException) -> Unit) { val os = getOS() if (os == null) { errorHandler(BrowserException("Failed to open the URL because we can't detect the OS")) @@ -19,7 +19,7 @@ class BrowserUtil { } } - private fun linuxBrowse(url: String, errorHandler: (BrowserException) -> Unit) { + private suspend fun linuxBrowse(url: String, errorHandler: suspend (BrowserException) -> Unit) { try { if (OS.LINUX.getDesktopEnvironment()?.uppercase()?.contains("GNOME") == true) { exec("gnome-open", url) @@ -36,7 +36,7 @@ class BrowserUtil { } } - private fun macBrowse(url: String, errorHandler: (BrowserException) -> Unit) { + private suspend fun macBrowse(url: String, errorHandler: suspend (BrowserException) -> Unit) { try { exec("open", url) } catch (e: Exception) { @@ -44,7 +44,7 @@ class BrowserUtil { } } - private fun windowsBrowse(url: String, errorHandler: (BrowserException) -> Unit) { + private suspend fun windowsBrowse(url: String, errorHandler: suspend (BrowserException) -> Unit) { try { exec("cmd", "start \"$url\"") } catch (e: Exception) { diff --git a/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt b/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt index e62cd95..707cb5b 100644 --- a/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt +++ b/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt @@ -3,6 +3,7 @@ package com.coder.toolbox.cli import com.coder.toolbox.cli.ex.MissingVersionException import com.coder.toolbox.cli.ex.ResponseException import com.coder.toolbox.cli.ex.SSHConfigFormatException +import com.coder.toolbox.logger.CoderLoggerFactory import com.coder.toolbox.settings.CoderSettings import com.coder.toolbox.settings.CoderSettingsState import com.coder.toolbox.util.CoderHostnameVerifier @@ -20,7 +21,6 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonDataException import com.squareup.moshi.Moshi -import org.slf4j.LoggerFactory import org.zeroturnaround.exec.ProcessExecutor import java.io.EOFException import java.io.FileInputStream @@ -126,7 +126,7 @@ class CoderCLIManager( // manager to download to the data directory instead. forceDownloadToData: Boolean = false, ) { - private val logger = LoggerFactory.getLogger(javaClass) + private val logger = CoderLoggerFactory.getLogger(javaClass) val remoteBinaryURL: URL = settings.binSource(deploymentURL) val localBinaryPath: Path = settings.binPath(deploymentURL, forceDownloadToData) diff --git a/src/main/kotlin/com/coder/toolbox/logger/CoderLoggerFactory.kt b/src/main/kotlin/com/coder/toolbox/logger/CoderLoggerFactory.kt new file mode 100644 index 0000000..58b7fb4 --- /dev/null +++ b/src/main/kotlin/com/coder/toolbox/logger/CoderLoggerFactory.kt @@ -0,0 +1,12 @@ +package com.coder.toolbox.logger + +import org.slf4j.ILoggerFactory +import org.slf4j.Logger +import com.jetbrains.toolbox.api.core.diagnostics.Logger as ToolboxLogger + +object CoderLoggerFactory : ILoggerFactory { + var tLogger: ToolboxLogger? = null + + fun getLogger(clazz: Class): Logger = getLogger(clazz.name) + override fun getLogger(clazzName: String): Logger = LoggerImpl(clazzName, tLogger) +} \ No newline at end of file diff --git a/src/main/kotlin/com/coder/toolbox/logger/LoggerImpl.kt b/src/main/kotlin/com/coder/toolbox/logger/LoggerImpl.kt new file mode 100644 index 0000000..a476666 --- /dev/null +++ b/src/main/kotlin/com/coder/toolbox/logger/LoggerImpl.kt @@ -0,0 +1,235 @@ +package com.coder.toolbox.logger + +import org.slf4j.Logger +import org.slf4j.Marker +import com.jetbrains.toolbox.api.core.diagnostics.Logger as ToolboxLogger + +class LoggerImpl(private val clazzName: String, private val tLogger: ToolboxLogger?) : Logger { + override fun getName(): String = clazzName + + override fun isTraceEnabled(): Boolean = true + + override fun trace(message: String) { + tLogger?.trace(message) + } + + override fun trace(message: String, arg: Any?) { + extractThrowable(arg)?.let { tLogger?.trace(it, message) } ?: tLogger?.trace(message) + } + + override fun trace(message: String, firstArg: Any?, secondArg: Any?) { + extractThrowable(firstArg, secondArg)?.let { tLogger?.trace(it, message) } ?: tLogger?.trace(message) + } + + override fun trace(message: String, vararg args: Any?) { + extractThrowable(args)?.let { tLogger?.trace(it, message) } ?: tLogger?.trace(message) + } + + override fun trace(message: String, exception: Throwable) { + tLogger?.trace(exception, message) + } + + override fun isTraceEnabled(marker: Marker): Boolean = true + + override fun trace(marker: Marker, message: String) { + tLogger?.trace(message) + } + + override fun trace(marker: Marker, message: String, arg: Any) { + extractThrowable(arg)?.let { tLogger?.trace(it, message) } ?: tLogger?.trace(message) + } + + override fun trace(marker: Marker, message: String, firstArg: Any?, secondArg: Any?) { + extractThrowable(firstArg, secondArg)?.let { tLogger?.trace(it, message) } ?: tLogger?.trace(message) + } + + override fun trace(marker: Marker, message: String, vararg args: Any?) { + extractThrowable(args)?.let { tLogger?.trace(it, message) } ?: tLogger?.trace(message) + } + + override fun trace(marker: Marker, message: String, exception: Throwable) { + tLogger?.trace(exception, message) + } + + override fun isDebugEnabled(): Boolean = true + + override fun debug(message: String) { + tLogger?.debug(message) + } + + override fun debug(message: String, arg: Any?) { + extractThrowable(arg)?.let { tLogger?.debug(it, message) } ?: tLogger?.debug(message) + } + + override fun debug(message: String, firstArg: Any?, secondArg: Any?) { + extractThrowable(firstArg, secondArg)?.let { tLogger?.debug(it, message) } ?: tLogger?.debug(message) + } + + override fun debug(message: String, vararg args: Any?) { + extractThrowable(args)?.let { tLogger?.debug(it, message) } ?: tLogger?.debug(message) + } + + override fun debug(message: String, exception: Throwable) { + tLogger?.debug(exception, message) + } + + override fun isDebugEnabled(marker: Marker): Boolean = true + + override fun debug(marker: Marker, message: String) { + tLogger?.debug(message) + } + + override fun debug(marker: Marker, message: String, arg: Any?) { + extractThrowable(arg)?.let { tLogger?.debug(it, message) } ?: tLogger?.debug(message) + } + + override fun debug(marker: Marker, message: String, firstArg: Any?, secondArg: Any?) { + extractThrowable(firstArg, secondArg)?.let { tLogger?.debug(it, message) } ?: tLogger?.debug(message) + } + + override fun debug(marker: Marker, message: String, vararg args: Any?) { + extractThrowable(args)?.let { tLogger?.debug(it, message) } ?: tLogger?.debug(message) + } + + override fun debug(marker: Marker, message: String, exception: Throwable) { + tLogger?.debug(exception, message) + } + + override fun isInfoEnabled(): Boolean = true + + override fun info(message: String) { + tLogger?.info(message) + } + + override fun info(message: String, arg: Any?) { + extractThrowable(arg)?.let { tLogger?.info(it, message) } ?: tLogger?.info(message) + } + + override fun info(message: String, firstArg: Any?, secondArg: Any?) { + extractThrowable(firstArg, secondArg)?.let { tLogger?.info(it, message) } ?: tLogger?.info(message) + } + + override fun info(message: String, vararg args: Any?) { + extractThrowable(args)?.let { tLogger?.info(it, message) } ?: tLogger?.info(message) + } + + override fun info(message: String, exception: Throwable) { + tLogger?.info(exception, message) + } + + override fun isInfoEnabled(marker: Marker): Boolean = true + + override fun info(marker: Marker, message: String) { + tLogger?.info(message) + } + + override fun info(marker: Marker, message: String, arg: Any?) { + extractThrowable(arg)?.let { tLogger?.info(it, message) } ?: tLogger?.info(message) + } + + override fun info(marker: Marker, message: String, firstArg: Any?, secondArg: Any?) { + extractThrowable(firstArg, secondArg)?.let { tLogger?.info(it, message) } ?: tLogger?.info(message) + } + + override fun info(marker: Marker, message: String, vararg args: Any?) { + extractThrowable(args)?.let { tLogger?.info(it, message) } ?: tLogger?.info(message) + } + + override fun info(marker: Marker, message: String, exception: Throwable) { + tLogger?.info(exception, message) + } + + override fun isWarnEnabled(): Boolean = true + + override fun warn(message: String) { + tLogger?.warn(message) + } + + override fun warn(message: String, arg: Any?) { + extractThrowable(arg)?.let { tLogger?.warn(it, message) } ?: tLogger?.warn(message) + } + + override fun warn(message: String, vararg args: Any?) { + extractThrowable(args)?.let { tLogger?.warn(it, message) } ?: tLogger?.warn(message) + } + + override fun warn(message: String, firstArg: Any?, secondArg: Any?) { + extractThrowable(firstArg, secondArg)?.let { tLogger?.warn(it, message) } ?: tLogger?.warn(message) + } + + override fun warn(message: String, exception: Throwable) { + tLogger?.warn(exception, message) + } + + override fun isWarnEnabled(marker: Marker): Boolean = true + + override fun warn(marker: Marker, message: String) { + tLogger?.warn(message) + } + + override fun warn(marker: Marker, message: String, arg: Any?) { + extractThrowable(arg)?.let { tLogger?.warn(it, message) } ?: tLogger?.warn(message) + } + + override fun warn(marker: Marker, message: String, firstArg: Any?, secondArg: Any?) { + extractThrowable(firstArg, secondArg)?.let { tLogger?.warn(it, message) } ?: tLogger?.warn(message) + } + + override fun warn(marker: Marker, message: String, vararg args: Any?) { + extractThrowable(args)?.let { tLogger?.warn(it, message) } ?: tLogger?.warn(message) + } + + override fun warn(marker: Marker, message: String, exception: Throwable) { + tLogger?.warn(exception, message) + } + + override fun isErrorEnabled(): Boolean = true + + override fun error(message: String) { + tLogger?.error(message) + } + + override fun error(message: String, arg: Any?) { + extractThrowable(arg)?.let { tLogger?.error(it, message) } ?: tLogger?.error(message) + } + + override fun error(message: String, firstArg: Any?, secondArg: Any?) { + extractThrowable(firstArg, secondArg)?.let { tLogger?.error(it, message) } ?: tLogger?.error(message) + } + + override fun error(message: String, vararg args: Any?) { + extractThrowable(args)?.let { tLogger?.error(it, message) } ?: tLogger?.error(message) + } + + override fun error(message: String, exception: Throwable) { + tLogger?.error(exception, message) + } + + override fun isErrorEnabled(marker: Marker): Boolean = true + + override fun error(marker: Marker, message: String) { + tLogger?.error(message) + } + + override fun error(marker: Marker, message: String, arg: Any?) { + extractThrowable(arg)?.let { tLogger?.error(it, message) } ?: tLogger?.error(message) + } + + override fun error(marker: Marker, message: String, firstArg: Any?, secondArg: Any?) { + extractThrowable(firstArg, secondArg)?.let { tLogger?.error(it, message) } ?: tLogger?.error(message) + } + + override fun error(marker: Marker, message: String, vararg args: Any?) { + extractThrowable(args)?.let { tLogger?.error(it, message) } ?: tLogger?.error(message) + } + + override fun error(marker: Marker, message: String, exception: Throwable) { + tLogger?.error(exception, message) + } + + companion object { + fun extractThrowable(vararg args: Any?): Throwable? = args.firstOrNull { it is Throwable } as? Throwable + + fun extractThrowable(arg: Any?): Throwable? = arg as? Throwable + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/coder/toolbox/settings/CoderSettings.kt b/src/main/kotlin/com/coder/toolbox/settings/CoderSettings.kt index 209fd55..ddcd269 100644 --- a/src/main/kotlin/com/coder/toolbox/settings/CoderSettings.kt +++ b/src/main/kotlin/com/coder/toolbox/settings/CoderSettings.kt @@ -1,5 +1,6 @@ package com.coder.toolbox.settings +import com.coder.toolbox.logger.CoderLoggerFactory import com.coder.toolbox.util.Arch import com.coder.toolbox.util.OS import com.coder.toolbox.util.expand @@ -8,7 +9,6 @@ import com.coder.toolbox.util.getOS import com.coder.toolbox.util.safeHost import com.coder.toolbox.util.toURL import com.coder.toolbox.util.withPath -import org.slf4j.LoggerFactory import java.net.URL import java.nio.file.Files import java.nio.file.Path @@ -127,7 +127,7 @@ open class CoderSettings( // Overrides the default binary name (for tests). private val binaryName: String? = null, ) { - private val logger = LoggerFactory.getLogger(javaClass) + private val logger = CoderLoggerFactory.getLogger(javaClass) val tls = CoderTLSSettings(state) diff --git a/src/main/kotlin/com/coder/toolbox/util/Dialogs.kt b/src/main/kotlin/com/coder/toolbox/util/Dialogs.kt index 886ce45..8414e9d 100644 --- a/src/main/kotlin/com/coder/toolbox/util/Dialogs.kt +++ b/src/main/kotlin/com/coder/toolbox/util/Dialogs.kt @@ -16,12 +16,11 @@ class DialogUi( private val settings: CoderSettings, private val ui: ToolboxUi, ) { - fun confirm(title: String, description: String): Boolean { - val f = ui.showOkCancelPopup(title, description, "Yes", "No") - return f.get() + suspend fun confirm(title: String, description: String): Boolean { + return ui.showOkCancelPopup(title, description, "Yes", "No") } - fun ask( + suspend fun ask( title: String, description: String, placeholder: String? = null, @@ -30,12 +29,10 @@ class DialogUi( isError: Boolean = false, link: Pair? = null, ): String? { - val f = ui.showTextInputPopup(title, description, placeholder, TextType.General, "OK", "Cancel") - return f.get() + return ui.showTextInputPopup(title, description, placeholder, TextType.General, "OK", "Cancel") } - private fun openUrl(url: URL) { - // TODO - check this later + private suspend fun openUrl(url: URL) { BrowserUtil.browse(url.toString()) { ui.showErrorInfoPopup(it) } @@ -53,7 +50,7 @@ class DialogUi( * other existing token) unless this is a retry to avoid clobbering the * token that just failed. */ - fun askToken( + suspend fun askToken( url: URL, token: Pair?, useExisting: Boolean, diff --git a/src/main/kotlin/com/coder/toolbox/util/LinkHandler.kt b/src/main/kotlin/com/coder/toolbox/util/LinkHandler.kt index 128c26d..9c6342e 100644 --- a/src/main/kotlin/com/coder/toolbox/util/LinkHandler.kt +++ b/src/main/kotlin/com/coder/toolbox/util/LinkHandler.kt @@ -26,11 +26,12 @@ open class LinkHandler( * Throw if required arguments are not supplied or the workspace is not in a * connectable state. */ - fun handle( + suspend fun handle( parameters: Map, indicator: ((t: String) -> Unit)? = null, ): String { - val deploymentURL = parameters.url() ?: dialogUi.ask("Deployment URL", "Enter the full URL of your Coder deployment") + val deploymentURL = + parameters.url() ?: dialogUi.ask("Deployment URL", "Enter the full URL of your Coder deployment") if (deploymentURL.isNullOrBlank()) { throw MissingArgumentException("Query parameter \"$URL\" is missing") } @@ -44,11 +45,12 @@ open class LinkHandler( val client = try { authenticate(deploymentURL, queryToken) } catch (ex: MissingArgumentException) { - throw MissingArgumentException("Query parameter \"$TOKEN\" is missing") + throw MissingArgumentException("Query parameter \"$TOKEN\" is missing", ex) } // TODO: Show a dropdown and ask for the workspace if missing. - val workspaceName = parameters.workspace() ?: throw MissingArgumentException("Query parameter \"$WORKSPACE\" is missing") + val workspaceName = + parameters.workspace() ?: throw MissingArgumentException("Query parameter \"$WORKSPACE\" is missing") val workspaces = client.workspaces() val workspace = @@ -60,19 +62,28 @@ open class LinkHandler( WorkspaceStatus.PENDING, WorkspaceStatus.STARTING -> // TODO: Wait for the workspace to turn on. throw IllegalArgumentException( - "The workspace \"$workspaceName\" is ${workspace.latestBuild.status.toString().lowercase()}; please wait then try again", + "The workspace \"$workspaceName\" is ${ + workspace.latestBuild.status.toString().lowercase() + }; please wait then try again", ) + WorkspaceStatus.STOPPING, WorkspaceStatus.STOPPED, WorkspaceStatus.CANCELING, WorkspaceStatus.CANCELED, - -> + -> // TODO: Turn on the workspace. throw IllegalArgumentException( - "The workspace \"$workspaceName\" is ${workspace.latestBuild.status.toString().lowercase()}; please start the workspace and try again", + "The workspace \"$workspaceName\" is ${ + workspace.latestBuild.status.toString().lowercase() + }; please start the workspace and try again", ) + WorkspaceStatus.FAILED, WorkspaceStatus.DELETING, WorkspaceStatus.DELETED -> throw IllegalArgumentException( - "The workspace \"$workspaceName\" is ${workspace.latestBuild.status.toString().lowercase()}; unable to connect", + "The workspace \"$workspaceName\" is ${ + workspace.latestBuild.status.toString().lowercase() + }; unable to connect", ) + WorkspaceStatus.RUNNING -> Unit // All is well } @@ -83,10 +94,16 @@ open class LinkHandler( if (status.pending()) { // TODO: Wait for the agent to be ready. throw IllegalArgumentException( - "The agent \"${agent.name}\" has a status of \"${status.toString().lowercase()}\"; please wait then try again", + "The agent \"${agent.name}\" has a status of \"${ + status.toString().lowercase() + }\"; please wait then try again", ) } else if (!status.ready()) { - throw IllegalArgumentException("The agent \"${agent.name}\" has a status of \"${status.toString().lowercase()}\"; unable to connect") + throw IllegalArgumentException( + "The agent \"${agent.name}\" has a status of \"${ + status.toString().lowercase() + }\"; unable to connect" + ) } val cli = @@ -120,7 +137,7 @@ open class LinkHandler( * Throw MissingArgumentException if the user aborts. Any network or invalid * token error may also be thrown. */ - private fun authenticate( + private suspend fun authenticate( deploymentURL: String, tryToken: Pair?, error: String? = null, @@ -172,7 +189,7 @@ open class LinkHandler( /** * Check that the link is allowlisted. If not, confirm with the user. */ - private fun verifyDownloadLink(parameters: Map) { + private suspend fun verifyDownloadLink(parameters: Map) { val link = parameters.ideDownloadLink() if (link.isNullOrBlank()) { return // Nothing to verify @@ -233,7 +250,7 @@ private fun isAllowlisted(url: URL): Triple { val allowlisted = domainAllowlist.any { url.host == it || url.host.endsWith(".$it") } && - domainAllowlist.any { finalUrl.host == it || finalUrl.host.endsWith(".$it") } + domainAllowlist.any { finalUrl.host == it || finalUrl.host.endsWith(".$it") } val https = url.protocol == "https" && finalUrl.protocol == "https" return Triple(allowlisted, https, linkWithRedirect) } @@ -308,4 +325,4 @@ internal fun getMatchingAgent( return agent } -class MissingArgumentException(message: String) : IllegalArgumentException(message) +class MissingArgumentException(message: String, ex: Throwable? = null) : IllegalArgumentException(message, ex) diff --git a/src/main/kotlin/com/coder/toolbox/util/TLS.kt b/src/main/kotlin/com/coder/toolbox/util/TLS.kt index 9c38350..0d17560 100644 --- a/src/main/kotlin/com/coder/toolbox/util/TLS.kt +++ b/src/main/kotlin/com/coder/toolbox/util/TLS.kt @@ -1,8 +1,8 @@ package com.coder.toolbox.util +import com.coder.toolbox.logger.CoderLoggerFactory import com.coder.toolbox.settings.CoderTLSSettings import okhttp3.internal.tls.OkHostnameVerifier -import org.slf4j.LoggerFactory import java.io.File import java.io.FileInputStream import java.net.InetAddress @@ -182,7 +182,7 @@ class AlternateNameSSLSocketFactory(private val delegate: SSLSocketFactory, priv } class CoderHostnameVerifier(private val alternateName: String) : HostnameVerifier { - private val logger = LoggerFactory.getLogger(javaClass) + private val logger = CoderLoggerFactory.getLogger(javaClass) override fun verify( host: String, diff --git a/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt b/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt index 59b19d4..f2ce937 100644 --- a/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt @@ -1,11 +1,11 @@ package com.coder.toolbox.views +import com.coder.toolbox.logger.CoderLoggerFactory import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription import com.jetbrains.toolbox.api.ui.components.UiField import com.jetbrains.toolbox.api.ui.components.UiPage import com.jetbrains.toolbox.api.ui.components.ValidationErrorField -import org.slf4j.LoggerFactory import java.util.function.Consumer /** @@ -19,9 +19,10 @@ import java.util.function.Consumer * to use the mouse. */ abstract class CoderPage( - private val showIcon: Boolean = true, -) : UiPage { - private val logger = LoggerFactory.getLogger(javaClass) + title: String, + showIcon: Boolean = true, +) : UiPage(title) { + private val logger = CoderLoggerFactory.getLogger(javaClass) /** * An error to display on the page. @@ -44,12 +45,10 @@ abstract class CoderPage( * * This seems to only work on the first page. */ - override fun getSvgIcon(): SvgIcon { - return if (showIcon) { - SvgIcon(this::class.java.getResourceAsStream("/icon.svg")?.readAllBytes() ?: byteArrayOf()) - } else { - SvgIcon(byteArrayOf()) - } + override val svgIcon: SvgIcon? = if (showIcon) { + SvgIcon(this::class.java.getResourceAsStream("/icon.svg")?.readAllBytes() ?: byteArrayOf()) + } else { + SvgIcon(byteArrayOf()) } /** @@ -87,14 +86,14 @@ abstract class CoderPage( * An action that simply runs the provided callback. */ class Action( - private val label: String, - private val closesPage: Boolean = false, - private val enabled: () -> Boolean = { true }, + description: String, + closesPage: Boolean = false, + enabled: () -> Boolean = { true }, private val actionBlock: () -> Unit, ) : RunnableActionDescription { - override fun getLabel(): String = label - override fun getShouldClosePage(): Boolean = closesPage - override fun isEnabled(): Boolean = enabled() + override val label: String = description + override val shouldClosePage: Boolean = closesPage + override val isEnabled: Boolean = enabled() override fun run() { actionBlock() } diff --git a/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt b/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt index 8b49275..a4d7f19 100644 --- a/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt @@ -14,7 +14,7 @@ import com.jetbrains.toolbox.api.ui.components.UiField * TODO@JB: There is no scroll, and our settings do not fit. As a consequence, * I have not been able to test this page. */ -class CoderSettingsPage(private val settings: CoderSettingsService) : CoderPage(false) { +class CoderSettingsPage(private val settings: CoderSettingsService) : CoderPage("Coder Settings", false) { // TODO: Copy over the descriptions, holding until I can test this page. private val binarySourceField = TextField("Binary source", settings.binarySource, TextType.General) private val binaryDirectoryField = TextField("Binary directory", settings.binaryDirectory, TextType.General) @@ -30,7 +30,7 @@ class CoderSettingsPage(private val settings: CoderSettingsService) : CoderPage( TextField("TLS alternate hostname", settings.tlsAlternateHostname, TextType.General) private val disableAutostartField = CheckboxField(settings.disableAutostart, "Disable autostart") - override fun getFields(): MutableList = mutableListOf( + override val fields: MutableList = mutableListOf( binarySourceField, enableDownloadsField, binaryDirectoryField, @@ -44,9 +44,7 @@ class CoderSettingsPage(private val settings: CoderSettingsService) : CoderPage( disableAutostartField, ) - override fun getTitle(): String = "Coder Settings" - - override fun getActionButtons(): MutableList = mutableListOf( + override val actionButtons: MutableList = mutableListOf( Action("Save", closesPage = true) { settings.binarySource = binarySourceField.text.value settings.binaryDirectory = binaryDirectoryField.text.value diff --git a/src/main/kotlin/com/coder/toolbox/views/ConnectPage.kt b/src/main/kotlin/com/coder/toolbox/views/ConnectPage.kt index fcf51b1..5270578 100644 --- a/src/main/kotlin/com/coder/toolbox/views/ConnectPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/ConnectPage.kt @@ -29,13 +29,12 @@ class ConnectPage( client: CoderRestClient, cli: CoderCLIManager, ) -> Unit, -) : CoderPage() { +) : CoderPage("Connecting to Coder") { private var signInJob: Job? = null private var statusField = LabelField("Connecting to ${url.host}...") - override fun getTitle(): String = "Connecting to Coder" - override fun getDescription(): String = "Please wait while we configure Toolbox for ${url.host}." + override val description: String = "Please wait while we configure Toolbox for ${url.host}." init { connect() @@ -46,7 +45,7 @@ class ConnectPage( * * TODO@JB: This looks kinda sparse. A centered spinner would be welcome. */ - override fun getFields(): MutableList = listOfNotNull( + override val fields: MutableList = listOfNotNull( statusField, errorField, ).toMutableList() @@ -54,7 +53,7 @@ class ConnectPage( /** * Show a retry button on error. */ - override fun getActionButtons(): MutableList = listOfNotNull( + override val actionButtons: MutableList = listOfNotNull( if (errorField != null) Action("Retry", closesPage = false) { retry() } else null, if (errorField != null) Action("Cancel", closesPage = false) { onCancel() } else null, ).toMutableList() diff --git a/src/main/kotlin/com/coder/toolbox/views/EnvironmentView.kt b/src/main/kotlin/com/coder/toolbox/views/EnvironmentView.kt index 89a0916..ebee9fe 100644 --- a/src/main/kotlin/com/coder/toolbox/views/EnvironmentView.kt +++ b/src/main/kotlin/com/coder/toolbox/views/EnvironmentView.kt @@ -6,7 +6,6 @@ import com.coder.toolbox.sdk.v2.models.WorkspaceAgent import com.jetbrains.toolbox.api.remoteDev.environments.SshEnvironmentContentsView import com.jetbrains.toolbox.api.remoteDev.ssh.SshConnectionInfo import java.net.URL -import java.util.concurrent.CompletableFuture /** * A view for a single environment. It displays the projects and IDEs. @@ -21,20 +20,21 @@ class EnvironmentView( private val workspace: Workspace, private val agent: WorkspaceAgent, ) : SshEnvironmentContentsView { - override fun getConnectionInfo(): CompletableFuture = CompletableFuture.completedFuture(object : SshConnectionInfo { + override suspend fun getConnectionInfo(): SshConnectionInfo = object : SshConnectionInfo { /** * The host name generated by the cli manager for this workspace. */ - override fun getHost() = CoderCLIManager.getHostName(url, "${workspace.name}.${agent.name}") + override val host: String = CoderCLIManager.getHostName(url, "${workspace.name}.${agent.name}") /** * The port is ignored by the Coder proxy command. */ - override fun getPort() = 22 + override val port: Int = 22 /** * The username is ignored by the Coder proxy command. */ - override fun getUserName() = "coder" - }) + override val userName: String? = "coder" + + } } diff --git a/src/main/kotlin/com/coder/toolbox/views/NewEnvironmentPage.kt b/src/main/kotlin/com/coder/toolbox/views/NewEnvironmentPage.kt index f9f6f44..efe4279 100644 --- a/src/main/kotlin/com/coder/toolbox/views/NewEnvironmentPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/NewEnvironmentPage.kt @@ -10,7 +10,6 @@ import com.jetbrains.toolbox.api.ui.components.UiField * For now we just use this to display the deployment URL since we do not * support creating environments from the plugin. */ -class NewEnvironmentPage(private val deploymentURL: String?) : CoderPage() { - override fun getFields(): MutableList = mutableListOf() - override fun getTitle(): String = deploymentURL ?: "" +class NewEnvironmentPage(private val deploymentURL: String?) : CoderPage(deploymentURL ?: "") { + override val fields: MutableList = mutableListOf() } diff --git a/src/main/kotlin/com/coder/toolbox/views/SignInPage.kt b/src/main/kotlin/com/coder/toolbox/views/SignInPage.kt index b45de84..2fdbf60 100644 --- a/src/main/kotlin/com/coder/toolbox/views/SignInPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/SignInPage.kt @@ -17,18 +17,16 @@ import java.net.URL class SignInPage( private val deploymentURL: Pair?, private val onSignIn: (deploymentURL: URL) -> Unit, -) : CoderPage() { +) : CoderPage("Sign In to Coder") { private val urlField = TextField("Deployment URL", deploymentURL?.first ?: "", TextType.General) - override fun getTitle(): String = "Sign In to Coder" - /** * Fields for this page, displayed in order. * * TODO@JB: Fields are reset when you navigate back. * Ideally they remember what the user entered. */ - override fun getFields(): MutableList = listOfNotNull( + override val fields: MutableList = listOfNotNull( urlField, deploymentURL?.let { LabelField(deploymentURL.second.description("URL")) }, errorField, @@ -37,7 +35,7 @@ class SignInPage( /** * Buttons displayed at the bottom of the page. */ - override fun getActionButtons(): MutableList = mutableListOf( + override val actionButtons: MutableList = mutableListOf( Action("Sign In", closesPage = false) { submit() }, ) diff --git a/src/main/kotlin/com/coder/toolbox/views/TokenPage.kt b/src/main/kotlin/com/coder/toolbox/views/TokenPage.kt index 16f4231..d0da1fc 100644 --- a/src/main/kotlin/com/coder/toolbox/views/TokenPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/TokenPage.kt @@ -17,21 +17,19 @@ import java.net.URL * enter their own. */ class TokenPage( - private val deploymentURL: URL, - private val token: Pair?, + deploymentURL: URL, + token: Pair?, private val onToken: ((token: String) -> Unit), -) : CoderPage() { +) : CoderPage("Enter your token") { private val tokenField = TextField("Token", token?.first ?: "", TextType.General) - override fun getTitle(): String = "Enter your token" - /** * Fields for this page, displayed in order. * * TODO@JB: Fields are reset when you navigate back. * Ideally they remember what the user entered. */ - override fun getFields(): MutableList = listOfNotNull( + override val fields: MutableList = listOfNotNull( tokenField, LabelField( token?.second?.description("token") @@ -45,7 +43,7 @@ class TokenPage( /** * Buttons displayed at the bottom of the page. */ - override fun getActionButtons(): MutableList = mutableListOf( + override val actionButtons: MutableList = mutableListOf( Action("Connect", closesPage = false) { submit(tokenField.text.value) }, )