Skip to content

fix: socket connection timeout #53

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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 2, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
29 changes: 22 additions & 7 deletions src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,20 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.onTimeout
import kotlinx.coroutines.selects.select
import okhttp3.OkHttpClient
import java.net.SocketTimeoutException
import java.net.URI
import java.net.URL
import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration.Companion.seconds
import kotlin.time.TimeSource
import com.jetbrains.toolbox.api.ui.components.AccountDropdownField as DropDownMenu
import com.jetbrains.toolbox.api.ui.components.AccountDropdownField as dropDownFactory

private val POLL_INTERVAL = 5.seconds

@OptIn(ExperimentalCoroutinesApi::class)
class CoderRemoteProvider(
private val context: CoderToolboxContext,
private val httpClient: OkHttpClient,
) : RemoteProvider("Coder") {
// Current polling job.
private var pollJob: Job? = null
Expand All @@ -66,7 +68,7 @@ class CoderRemoteProvider(
private var firstRun = true
private val isInitialized: MutableStateFlow<Boolean> = MutableStateFlow(false)
private var coderHeaderPage = NewEnvironmentPage(context, context.i18n.pnotr(getDeploymentURL()?.first ?: ""))
private val linkHandler = CoderProtocolHandler(context, httpClient, dialogUi, isInitialized)
private val linkHandler = CoderProtocolHandler(context, dialogUi, isInitialized)
override val environments: MutableStateFlow<LoadableState<List<RemoteProviderEnvironment>>> = MutableStateFlow(
LoadableState.Value(emptyList())
)
Expand All @@ -77,6 +79,7 @@ class CoderRemoteProvider(
* first time).
*/
private fun poll(client: CoderRestClient, cli: CoderCLIManager): Job = context.cs.launch {
var lastPollTime = TimeSource.Monotonic.markNow()
while (isActive) {
try {
context.logger.debug("Fetching workspace agents from ${client.url}")
Expand Down Expand Up @@ -134,16 +137,28 @@ class CoderRemoteProvider(
} catch (_: CancellationException) {
context.logger.debug("${client.url} polling loop canceled")
break
} catch (ex: SocketTimeoutException) {
val elapsed = lastPollTime.elapsedNow()
if (elapsed > POLL_INTERVAL * 2) {
context.logger.info("wake-up from an OS sleep was detected, going to re-initialize the http client...")
client.setupSession()
} else {
context.logger.error(ex, "workspace polling error encountered")
pollError = ex
logout()
break
}
} catch (ex: Exception) {
context.logger.info(ex, "workspace polling error encountered")
context.logger.error(ex, "workspace polling error encountered")
pollError = ex
logout()
break
}

// TODO: Listening on a web socket might be better?
select<Unit> {
onTimeout(5.seconds) {
context.logger.trace("workspace poller waked up by the 5 seconds timeout")
onTimeout(POLL_INTERVAL) {
context.logger.trace("workspace poller waked up by the $POLL_INTERVAL timeout")
}
triggerSshConfig.onReceive { shouldTrigger ->
if (shouldTrigger) {
Expand All @@ -152,6 +167,7 @@ class CoderRemoteProvider(
}
}
}
lastPollTime = TimeSource.Monotonic.markNow()
}
}

Expand Down Expand Up @@ -329,7 +345,6 @@ class CoderRemoteProvider(
context,
deploymentURL,
token,
httpClient,
::goToEnvironmentsPage,
) { client, cli ->
// Store the URL and token for use next time.
Expand Down
4 changes: 1 addition & 3 deletions src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ 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.CoroutineScope
import okhttp3.OkHttpClient

/**
* Entry point into the extension.
Expand All @@ -35,8 +34,7 @@ class CoderToolboxExtension : RemoteDevExtension {
serviceLocator.getService(LocalizableStringFactory::class.java),
CoderSettingsStore(serviceLocator.getService(PluginSettingsStore::class.java), Environment(), logger),
CoderSecretsStore(serviceLocator.getService(PluginSecretStore::class.java)),
),
OkHttpClient(),
)
)
}
}
12 changes: 8 additions & 4 deletions src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,19 @@ open class CoderRestClient(
val token: String?,
private val proxyValues: ProxyValues? = null,
private val pluginVersion: String = "development",
existingHttpClient: OkHttpClient? = null,
) {
private val settings = context.settingsStore.readOnly()
private val httpClient: OkHttpClient
private val retroRestClient: CoderV2RestFacade
private lateinit var httpClient: OkHttpClient
private lateinit var retroRestClient: CoderV2RestFacade

lateinit var me: User
lateinit var buildVersion: String

init {
setupSession()
}

fun setupSession() {
val moshi =
Moshi.Builder()
.add(ArchConverter())
Expand All @@ -73,7 +76,7 @@ open class CoderRestClient(

val socketFactory = coderSocketFactory(settings.tls)
val trustManagers = coderTrustManagers(settings.tls.caPath)
var builder = existingHttpClient?.newBuilder() ?: OkHttpClient.Builder()
var builder = OkHttpClient.Builder()

if (proxyValues != null) {
builder =
Expand Down Expand Up @@ -103,6 +106,7 @@ open class CoderRestClient(
builder
.sslSocketFactory(socketFactory, trustManagers[0] as X509TrustManager)
.hostnameVerifier(CoderHostnameVerifier(settings.tls.altHostname))
.retryOnConnectionFailure(true)
.addInterceptor {
it.proceed(
it.request().newBuilder().addHeader(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.time.withTimeout
import okhttp3.OkHttpClient
import java.net.HttpURLConnection
import java.net.URI
import java.net.URL
Expand All @@ -26,7 +25,6 @@ import kotlin.time.toJavaDuration

open class CoderProtocolHandler(
private val context: CoderToolboxContext,
private val httpClient: OkHttpClient?,
private val dialogUi: DialogUi,
private val isInitialized: StateFlow<Boolean>,
) {
Expand Down Expand Up @@ -230,8 +228,7 @@ open class CoderProtocolHandler(
deploymentURL.toURL(),
token,
proxyValues = null, // TODO - not sure the above comment applies as we are creating our own http client
PluginManager.pluginInfo.version,
httpClient
PluginManager.pluginInfo.version
)
client.authenticate()
return client
Expand Down
3 changes: 0 additions & 3 deletions src/main/kotlin/com/coder/toolbox/views/ConnectPage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import java.net.URL

/**
Expand All @@ -24,7 +23,6 @@ class ConnectPage(
private val context: CoderToolboxContext,
private val url: URL,
private val token: String?,
private val httpClient: OkHttpClient,
private val onCancel: () -> Unit,
private val onConnect: (
client: CoderRestClient,
Expand Down Expand Up @@ -95,7 +93,6 @@ class ConnectPage(
token,
proxyValues = null,
PluginManager.pluginInfo.version,
httpClient
)
client.authenticate()
updateStatus(context.i18n.ptrl("Checking Coder binary..."), error = null)
Expand Down
Loading