Skip to content

Commit cd2abb4

Browse files
committed
impl: rework the URI handler
With this commit we split the responsibilities. `CoderProtocolHander` is now only responsible searching the workspace and agent and orchestrating the IDE installation and launching. The code around initializing the http client and cli with a new URL and a new token, plus cleaning up the old resources like the polling loop and the list of environments. There are two major benefits to this approach: - allows us to easily share/reuse the logic around cleaning up resources and re-initializing the http client and cli without passing so many callbacks to CoderProtocolHandler (less coupling, code that is cleaner and easier to read, easier to maintain and test) - provides a nice and easy way to check whether the URI url is the same as the one from current deployment and properly react if they differ.
1 parent 6390b42 commit cd2abb4

File tree

6 files changed

+163
-216
lines changed

6 files changed

+163
-216
lines changed

src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt

Lines changed: 104 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ import com.coder.toolbox.sdk.ex.APIResponseException
88
import com.coder.toolbox.sdk.v2.models.WorkspaceStatus
99
import com.coder.toolbox.util.CoderProtocolHandler
1010
import com.coder.toolbox.util.DialogUi
11+
import com.coder.toolbox.util.TOKEN
12+
import com.coder.toolbox.util.URL
13+
import com.coder.toolbox.util.WebUrlValidationResult.Invalid
14+
import com.coder.toolbox.util.toQueryParameters
15+
import com.coder.toolbox.util.toURL
16+
import com.coder.toolbox.util.token
17+
import com.coder.toolbox.util.url
18+
import com.coder.toolbox.util.validateStrictWebUrl
1119
import com.coder.toolbox.util.waitForTrue
1220
import com.coder.toolbox.util.withPath
1321
import com.coder.toolbox.views.Action
@@ -46,6 +54,7 @@ import com.jetbrains.toolbox.api.ui.components.AccountDropdownField as DropDownM
4654
import com.jetbrains.toolbox.api.ui.components.AccountDropdownField as dropDownFactory
4755

4856
private val POLL_INTERVAL = 5.seconds
57+
private const val CAN_T_HANDLE_URI_TITLE = "Can't handle URI"
4958

5059
@OptIn(ExperimentalCoroutinesApi::class)
5160
class CoderRemoteProvider(
@@ -63,6 +72,7 @@ class CoderRemoteProvider(
6372

6473
// The REST client, if we are signed in
6574
private var client: CoderRestClient? = null
75+
private var cli: CoderCLIManager? = null
6676

6777
// On the first load, automatically log in if we can.
6878
private var firstRun = true
@@ -84,7 +94,7 @@ class CoderRemoteProvider(
8494
providerVisible = false
8595
)
8696
)
87-
private val linkHandler = CoderProtocolHandler(context, dialogUi, settingsPage, visibilityState, isInitialized)
97+
private val linkHandler = CoderProtocolHandler(context)
8898

8999
override val loadingEnvironmentsDescription: LocalizableString = context.i18n.ptrl("Loading workspaces...")
90100
override val environments: MutableStateFlow<LoadableState<List<CoderRemoteEnvironment>>> = MutableStateFlow(
@@ -258,6 +268,7 @@ class CoderRemoteProvider(
258268
override fun close() {
259269
softClose()
260270
client = null
271+
cli = null
261272
lastEnvironments.clear()
262273
environments.value = LoadableState.Value(emptyList())
263274
isInitialized.update { false }
@@ -337,13 +348,50 @@ class CoderRemoteProvider(
337348
*/
338349
override suspend fun handleUri(uri: URI) {
339350
try {
340-
linkHandler.handle(
341-
uri,
342-
client?.url,
343-
shouldDoAutoSetup(),
344-
::refreshSession,
345-
::performReLogin
346-
)
351+
val params = uri.toQueryParameters()
352+
if (params.isEmpty()) {
353+
// probably a plugin installation scenario
354+
context.logAndShowInfo("URI will not be handled", "No query parameters were provided")
355+
return
356+
}
357+
// this switches to the main plugin screen, even
358+
// if last opened provider was not Coder
359+
context.envPageManager.showPluginEnvironmentsPage()
360+
coderHeaderPage.isBusy.update { true }
361+
if (shouldDoAutoSetup()) {
362+
isInitialized.waitForTrue()
363+
}
364+
context.logger.info("Handling $uri...")
365+
val newUrl = resolveDeploymentUrl(params)?.toURL() ?: return
366+
val newToken = if (context.settingsStore.requiresMTlsAuth) null else resolveToken(params) ?: return
367+
if (sameUrl(newUrl, client?.url)) {
368+
if (context.settingsStore.requiresTokenAuth) {
369+
newToken?.let {
370+
refreshSession(newUrl, it)
371+
}
372+
}
373+
} else {
374+
CoderCliSetupContext.apply {
375+
url = newUrl
376+
token = newToken
377+
}
378+
CoderCliSetupWizardState.goToStep(WizardStep.CONNECT)
379+
CoderCliSetupWizardPage(
380+
context, settingsPage, visibilityState,
381+
initialAutoSetup = true,
382+
jumpToMainPageOnError = true,
383+
connectSynchronously = true,
384+
onConnect = ::onConnect
385+
).apply {
386+
beforeShow()
387+
}
388+
}
389+
// TODO - do I really need these two lines? I'm anyway doing a workspace call
390+
triggerProviderVisible.send(true)
391+
isInitialized.waitForTrue()
392+
393+
linkHandler.handle(params, newUrl, this.client!!, this.cli!!)
394+
coderHeaderPage.isBusy.update { false }
347395
} catch (ex: Exception) {
348396
val textError = if (ex is APIResponseException) {
349397
if (!ex.reason.isNullOrBlank()) {
@@ -355,50 +403,61 @@ class CoderRemoteProvider(
355403
textError ?: ""
356404
)
357405
context.envPageManager.showPluginEnvironmentsPage()
406+
} finally {
407+
coderHeaderPage.isBusy.update { false }
358408
}
359409
}
360410

361-
private suspend fun refreshSession(url: URL, token: String): Pair<CoderRestClient, CoderCLIManager> {
362-
coderHeaderPage.isBusyCreatingNewEnvironment.update { true }
363-
try {
364-
context.logger.info("Stopping workspace polling and re-initializing the http client and cli with a new token")
365-
softClose()
366-
val restClient = CoderRestClient(
367-
context,
368-
url,
369-
token,
370-
PluginManager.pluginInfo.version,
371-
).apply { initializeSession() }
372-
val cli = CoderCLIManager(context, url).apply {
373-
login(token)
374-
}
375-
this.client = restClient
376-
pollJob = poll(restClient, cli)
377-
triggerProviderVisible.send(true)
378-
context.logger.info("Workspace poll job with name ${pollJob.toString()} was created while handling URI")
379-
return restClient to cli
380-
} finally {
381-
coderHeaderPage.isBusyCreatingNewEnvironment.update { false }
411+
private suspend fun resolveDeploymentUrl(params: Map<String, String>): String? {
412+
val deploymentURL = params.url() ?: askUrl()
413+
if (deploymentURL.isNullOrBlank()) {
414+
context.logAndShowError(CAN_T_HANDLE_URI_TITLE, "Query parameter \"${URL}\" is missing from URI")
415+
return null
416+
}
417+
val validationResult = deploymentURL.validateStrictWebUrl()
418+
if (validationResult is Invalid) {
419+
context.logAndShowError(CAN_T_HANDLE_URI_TITLE, "\"$URL\" is invalid: ${validationResult.reason}")
420+
return null
382421
}
422+
return deploymentURL
383423
}
384424

385-
private suspend fun performReLogin(restClient: CoderRestClient, cli: CoderCLIManager) {
386-
context.logger.info("Stopping workspace polling and de-initializing resources")
387-
close()
388-
isInitialized.update {
389-
false
425+
private suspend fun resolveToken(params: Map<String, String>): String? {
426+
val token = params.token()
427+
if (token.isNullOrBlank()) {
428+
context.logAndShowError(CAN_T_HANDLE_URI_TITLE, "Query parameter \"$TOKEN\" is missing from URI")
429+
return null
390430
}
391-
context.logger.info("Starting initialization with the new settings")
392-
this@CoderRemoteProvider.client = restClient
393-
if (context.settingsStore.useAppNameAsTitle) {
394-
coderHeaderPage.setTitle(context.i18n.pnotr(restClient.appName))
395-
} else {
396-
coderHeaderPage.setTitle(context.i18n.pnotr(restClient.url.toString()))
431+
return token
432+
}
433+
434+
private fun sameUrl(first: URL, second: URL?): Boolean = first.toURI().normalize() == second?.toURI()?.normalize()
435+
436+
private suspend fun refreshSession(url: URL, token: String): Pair<CoderRestClient, CoderCLIManager> {
437+
context.logger.info("Stopping workspace polling and re-initializing the http client and cli with a new token")
438+
softClose()
439+
val newRestClient = CoderRestClient(
440+
context,
441+
url,
442+
token,
443+
PluginManager.pluginInfo.version,
444+
).apply { initializeSession() }
445+
val newCli = CoderCLIManager(context, url).apply {
446+
login(token)
397447
}
398-
environments.showLoadingMessage()
399-
pollJob = poll(restClient, cli)
448+
this.client = newRestClient
449+
this.cli = newCli
450+
pollJob = poll(newRestClient, newCli)
400451
context.logger.info("Workspace poll job with name ${pollJob.toString()} was created while handling URI")
401-
isInitialized.waitForTrue()
452+
return newRestClient to newCli
453+
}
454+
455+
private suspend fun askUrl(): String? {
456+
context.popupPluginMainPage()
457+
return dialogUi.ask(
458+
context.i18n.ptrl("Deployment URL"),
459+
context.i18n.ptrl("Enter the full URL of your Coder deployment")
460+
)
402461
}
403462

404463
/**
@@ -455,6 +514,7 @@ class CoderRemoteProvider(
455514

456515
private fun onConnect(client: CoderRestClient, cli: CoderCLIManager) {
457516
// Store the URL and token for use next time.
517+
close()
458518
context.settingsStore.updateLastUsedUrl(client.url)
459519
if (context.settingsStore.requiresTokenAuth) {
460520
context.secrets.storeTokenFor(client.url, client.token ?: "")
@@ -463,10 +523,7 @@ class CoderRemoteProvider(
463523
context.logger.info("Deployment URL was stored and will be available for automatic connection")
464524
}
465525
this.client = client
466-
pollJob?.let {
467-
it.cancel()
468-
context.logger.info("Cancelled workspace poll job ${pollJob.toString()} in order to start a new one")
469-
}
526+
this.cli = cli
470527
environments.showLoadingMessage()
471528
if (context.settingsStore.useAppNameAsTitle) {
472529
coderHeaderPage.setTitle(context.i18n.pnotr(client.appName))

0 commit comments

Comments
 (0)