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

Commitd5b1c3b

Browse files
authored
impl: support for displaying network latency (#108)
Under the "Additional environment information". Unfortunately it was notpossible any other way. The description property is modifiable howeverToolbox renders the description label only as long as the SSH connectionis not established. As soon as an ssh connection is running thedescription label is used as mechanism to notify users about availableIDE updates.It also appears that we can't have any other extra tab, other than"Tools", "Projects" and "Settings". There is a secondary informationattribute API, but it is not usable to show recurring metrics infobecause it can only beconfigured once, it is not a mutable field.The best effort was to add the information in the Settings page, and itis worth highlighting that the metrics are only refreshed when usereither:- switches between tabs- expands/collapses the "Additional environment information" section.There is no programmatic mechanism to notify the information in theSettings page that latency changed.The network metrics are loaded from the pid files created by the sshcommand. Toolbox spawns a native process running the SSH client. The sshclient then spawns another process which is associated to the coderproxy command. SSH network metrics are saved into json files with thename equal to the pid of the ssh command (not to be confused with theproxy command's name).-resolves#100 -resolves#101
1 parent0dfd81c commitd5b1c3b

File tree

35 files changed

+320
-31
lines changed

35 files changed

+320
-31
lines changed

‎CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
##Unreleased
44

5+
###Added
6+
7+
- render network status in the Settings tab, under`Additional environment information` section.
8+
59
##0.2.1 - 2025-05-05
610

711
###Changed

‎src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@ package com.coder.toolbox
22

33
importcom.coder.toolbox.browser.BrowserUtil
44
importcom.coder.toolbox.cli.CoderCLIManager
5+
importcom.coder.toolbox.cli.SshCommandProcessHandle
56
importcom.coder.toolbox.models.WorkspaceAndAgentStatus
67
importcom.coder.toolbox.sdk.CoderRestClient
78
importcom.coder.toolbox.sdk.ex.APIResponseException
9+
importcom.coder.toolbox.sdk.v2.models.NetworkMetrics
810
importcom.coder.toolbox.sdk.v2.models.Workspace
911
importcom.coder.toolbox.sdk.v2.models.WorkspaceAgent
1012
importcom.coder.toolbox.util.waitForFalseWithTimeout
1113
importcom.coder.toolbox.util.withPath
1214
importcom.coder.toolbox.views.Action
1315
importcom.coder.toolbox.views.EnvironmentView
16+
importcom.jetbrains.toolbox.api.localization.LocalizableString
1417
importcom.jetbrains.toolbox.api.remoteDev.AfterDisconnectHook
1518
importcom.jetbrains.toolbox.api.remoteDev.BeforeConnectionHook
1619
importcom.jetbrains.toolbox.api.remoteDev.DeleteEnvironmentConfirmationParams
@@ -20,15 +23,21 @@ import com.jetbrains.toolbox.api.remoteDev.environments.EnvironmentContentsView
2023
importcom.jetbrains.toolbox.api.remoteDev.states.EnvironmentDescription
2124
importcom.jetbrains.toolbox.api.remoteDev.states.RemoteEnvironmentState
2225
importcom.jetbrains.toolbox.api.ui.actions.ActionDescription
26+
importcom.squareup.moshi.Moshi
27+
importkotlinx.coroutines.Job
2328
importkotlinx.coroutines.delay
2429
importkotlinx.coroutines.flow.MutableStateFlow
2530
importkotlinx.coroutines.flow.update
2631
importkotlinx.coroutines.isActive
2732
importkotlinx.coroutines.launch
2833
importkotlinx.coroutines.withTimeout
34+
importjava.io.File
35+
importjava.nio.file.Path
2936
importkotlin.time.Duration.Companion.minutes
3037
importkotlin.time.Duration.Companion.seconds
3138

39+
privatevalPOLL_INTERVAL=5.seconds
40+
3241
/**
3342
* Represents an agent and workspace combination.
3443
*
@@ -44,17 +53,20 @@ class CoderRemoteEnvironment(
4453
privatevar wsRawStatus=WorkspaceAndAgentStatus.from(workspace, agent)
4554

4655
overridevar name:String="${workspace.name}.${agent.name}"
47-
4856
privatevar isConnected:MutableStateFlow<Boolean>=MutableStateFlow(false)
4957
overrideval connectionRequest:MutableStateFlow<Boolean>=MutableStateFlow(false)
5058

5159
overrideval state:MutableStateFlow<RemoteEnvironmentState>=
5260
MutableStateFlow(wsRawStatus.toRemoteEnvironmentState(context))
5361
overrideval description:MutableStateFlow<EnvironmentDescription>=
5462
MutableStateFlow(EnvironmentDescription.General(context.i18n.pnotr(workspace.templateDisplayName)))
55-
63+
overrideval additionalEnvironmentInformation:MutableMap<LocalizableString,String>=mutableMapOf()
5664
overrideval actionsList:MutableStateFlow<List<ActionDescription>>=MutableStateFlow(getAvailableActions())
5765

66+
privateval networkMetricsMarshaller=Moshi.Builder().build().adapter(NetworkMetrics::class.java)
67+
privateval proxyCommandHandle=SshCommandProcessHandle(context)
68+
privatevar pollJob:Job?=null
69+
5870
funasPairOfWorkspaceAndAgent():Pair<Workspace,WorkspaceAgent>=Pair(workspace, agent)
5971

6072
privatefungetAvailableActions():List<ActionDescription> {
@@ -141,9 +153,49 @@ class CoderRemoteEnvironment(
141153
overridefunbeforeConnection() {
142154
context.logger.info("Connecting to$id...")
143155
isConnected.update {true }
156+
pollJob= pollNetworkMetrics()
157+
}
158+
159+
privatefunpollNetworkMetrics():Job= context.cs.launch {
160+
context.logger.info("Starting the network metrics poll job for$id")
161+
while (isActive) {
162+
context.logger.debug("Searching SSH command's PID for workspace$id...")
163+
val pid= proxyCommandHandle.findByWorkspaceAndAgent(workspace, agent)
164+
if (pid==null) {
165+
context.logger.debug("No SSH command PID was found for workspace$id")
166+
delay(POLL_INTERVAL)
167+
continue
168+
}
169+
170+
val metricsFile=Path.of(context.settingsStore.networkInfoDir,"$pid.json").toFile()
171+
if (metricsFile.doesNotExists()) {
172+
context.logger.debug("No metrics file found at${metricsFile.absolutePath} for$id")
173+
delay(POLL_INTERVAL)
174+
continue
175+
}
176+
context.logger.debug("Loading metrics from${metricsFile.absolutePath} for$id")
177+
try {
178+
val metrics= networkMetricsMarshaller.fromJson(metricsFile.readText())
179+
if (metrics==null) {
180+
return@launch
181+
}
182+
context.logger.debug("$id metrics:$metrics")
183+
additionalEnvironmentInformation.put(context.i18n.ptrl("Network Status"), metrics.toPretty())
184+
}catch (e:Exception) {
185+
context.logger.error(
186+
e,
187+
"Error encountered while trying to load network metrics from${metricsFile.absolutePath} for$id"
188+
)
189+
}
190+
delay(POLL_INTERVAL)
191+
}
144192
}
145193

194+
privatefun File.doesNotExists():Boolean=!this.exists()
195+
146196
overridefunafterDisconnect() {
197+
context.logger.info("Stopping the network metrics poll job for$id")
198+
pollJob?.cancel()
147199
this.connectionRequest.update {false }
148200
isConnected.update {false }
149201
context.logger.info("Disconnected from$id")

‎src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,14 +271,13 @@ class CoderCLIManager(
271271
"ssh",
272272
"--stdio",
273273
if (settings.disableAutostart&& feats.disableAutostart)"--disable-autostart"elsenull,
274+
"--network-info-dir${escape(settings.networkInfoDir)}"
274275
)
275276
val proxyArgs= baseArgs+ listOfNotNull(
276277
if (!settings.sshLogDirectory.isNullOrBlank())"--log-dir"elsenull,
277278
if (!settings.sshLogDirectory.isNullOrBlank()) escape(settings.sshLogDirectory!!)elsenull,
278279
if (feats.reportWorkspaceUsage)"--usage-app=jetbrains"elsenull,
279280
)
280-
val backgroundProxyArgs=
281-
baseArgs+ listOfNotNull(if (feats.reportWorkspaceUsage)"--usage-app=disable"elsenull)
282281
val extraConfig=
283282
if (!settings.sshConfigOptions.isNullOrBlank()) {
284283
"\n"+ settings.sshConfigOptions!!.prependIndent("")
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
packagecom.coder.toolbox.cli
2+
3+
importcom.coder.toolbox.CoderToolboxContext
4+
importcom.coder.toolbox.sdk.v2.models.Workspace
5+
importcom.coder.toolbox.sdk.v2.models.WorkspaceAgent
6+
importkotlin.jvm.optionals.getOrNull
7+
8+
/**
9+
* Identifies the PID for the SSH Coder command spawned by Toolbox.
10+
*/
11+
classSshCommandProcessHandle(privatevalctx:CoderToolboxContext) {
12+
13+
/**
14+
* Finds the PID of a Coder (not the proxy command) ssh cmd associated with the specified workspace and agent.
15+
* Null is returned when no ssh command process was found.
16+
*
17+
* Implementation Notes:
18+
* An iterative DFS approach where we start with Toolbox's direct children, grep the command
19+
* and if nothing is found we continue with the processes children. Toolbox spawns an ssh command
20+
* as a separate command which in turns spawns another child for the proxy command.
21+
*/
22+
funfindByWorkspaceAndAgent(ws:Workspace,agent:WorkspaceAgent):Long? {
23+
val stack=ArrayDeque<ProcessHandle>(ProcessHandle.current().children().toList())
24+
while (stack.isNotEmpty()) {
25+
val processHandle= stack.removeLast()
26+
val cmdLine= processHandle.info().commandLine().getOrNull()
27+
ctx.logger.debug("SSH command PID:${processHandle.pid()} Command:$cmdLine")
28+
if (cmdLine!=null&& cmdLine.isSshCommandFor(ws, agent)) {
29+
ctx.logger.debug("SSH command with PID:${processHandle.pid()} and Command:$cmdLine matches${ws.name}.${agent.name}")
30+
return processHandle.pid()
31+
}else {
32+
stack.addAll(processHandle.children().toList())
33+
}
34+
}
35+
returnnull
36+
}
37+
38+
privatefun String.isSshCommandFor(ws:Workspace,agent:WorkspaceAgent):Boolean {
39+
// usage-app is present only in the ProxyCommand
40+
return!this.contains("--usage-app=jetbrains")&&this.contains("${ws.name}.${agent.name}")
41+
}
42+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
packagecom.coder.toolbox.sdk.v2.models
2+
3+
importcom.squareup.moshi.Json
4+
importcom.squareup.moshi.JsonClass
5+
importjava.text.DecimalFormat
6+
7+
privateval formatter=DecimalFormat("#.00")
8+
9+
/**
10+
* Coder ssh network metrics. All properties are optional
11+
* because Coder Connect only populates `using_coder_connect`
12+
* while p2p doesn't populate this property.
13+
*/
14+
@JsonClass(generateAdapter=true)
15+
data classNetworkMetrics(
16+
@Json(name="p2p")
17+
valp2p:Boolean?,
18+
19+
@Json(name="latency")
20+
vallatency:Double?,
21+
22+
@Json(name="preferred_derp")
23+
valpreferredDerp:String?,
24+
25+
@Json(name="derp_latency")
26+
valderpLatency:Map<String,Double>?,
27+
28+
@Json(name="upload_bytes_sec")
29+
valuploadBytesSec:Long?,
30+
31+
@Json(name="download_bytes_sec")
32+
valdownloadBytesSec:Long?,
33+
34+
@Json(name="using_coder_connect")
35+
valusingCoderConnect:Boolean?
36+
) {
37+
funtoPretty():String {
38+
if (usingCoderConnect==true) {
39+
return"You're connected using Coder Connect"
40+
}
41+
returnif (p2p==true) {
42+
"Direct (${formatter.format(latency)}ms). You're connected peer-to-peer"
43+
}else {
44+
val derpLatency= derpLatency!![preferredDerp]
45+
val workspaceLatency= latency!!.minus(derpLatency!!)
46+
"You ↔$preferredDerp (${formatter.format(derpLatency)}ms) ↔ Workspace (${formatter.format(workspaceLatency)}ms). You are connected through a relay"
47+
}
48+
}
49+
}

‎src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ interface ReadOnlyCoderSettings {
110110
*/
111111
val sshConfigOptions:String?
112112

113+
114+
/**
115+
* The path where network information for SSH hosts are stored
116+
*/
117+
val networkInfoDir:String
118+
113119
/**
114120
* The default URL to show in the connection window.
115121
*/

‎src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ class CoderSettingsStore(
6565
overrideval sshLogDirectory:String? get()= store[SSH_LOG_DIR]
6666
overrideval sshConfigOptions:String?
6767
get()= store[SSH_CONFIG_OPTIONS].takeUnless { it.isNullOrEmpty() }?: env.get(CODER_SSH_CONFIG_OPTIONS)
68+
overrideval networkInfoDir:String
69+
get()= store[NETWORK_INFO_DIR].takeUnless { it.isNullOrEmpty() }?: getDefaultGlobalDataDir()
70+
.resolve("ssh-network-metrics")
71+
.normalize()
72+
.toString()
6873

6974
/**
7075
* The default URL to show in the connection window.
@@ -232,6 +237,10 @@ class CoderSettingsStore(
232237
store[SSH_LOG_DIR]= path
233238
}
234239

240+
funupdateNetworkInfoDir(path:String) {
241+
store[NETWORK_INFO_DIR]= path
242+
}
243+
235244
funupdateSshConfigOptions(options:String) {
236245
store[SSH_CONFIG_OPTIONS]= options
237246
}

‎src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,5 @@ internal const val SSH_LOG_DIR = "sshLogDir"
3838

3939
internalconstvalSSH_CONFIG_OPTIONS="sshConfigOptions"
4040

41+
internalconstvalNETWORK_INFO_DIR="networkInfoDir"
42+

‎src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ class CoderSettingsPage(context: CoderToolboxContext, triggerSshConfig: Channel<
5656
TextField(context.i18n.ptrl("Extra SSH options"), settings.sshConfigOptions?:"",TextType.General)
5757
privateval sshLogDirField=
5858
TextField(context.i18n.ptrl("SSH proxy log directory"), settings.sshLogDirectory?:"",TextType.General)
59+
privateval networkInfoDirField=
60+
TextField(context.i18n.ptrl("SSH network metrics directory"), settings.networkInfoDir,TextType.General)
5961

6062

6163
overrideval fields:StateFlow<List<UiField>>=MutableStateFlow(
@@ -73,6 +75,7 @@ class CoderSettingsPage(context: CoderToolboxContext, triggerSshConfig: Channel<
7375
disableAutostartField,
7476
enableSshWildCardConfig,
7577
sshLogDirField,
78+
networkInfoDirField,
7679
sshExtraArgs,
7780
)
7881
)
@@ -104,6 +107,7 @@ class CoderSettingsPage(context: CoderToolboxContext, triggerSshConfig: Channel<
104107
}
105108
}
106109
context.settingsStore.updateSshLogDir(sshLogDirField.textState.value)
110+
context.settingsStore.updateNetworkInfoDir(networkInfoDirField.textState.value)
107111
context.settingsStore.updateSshConfigOptions(sshExtraArgs.textState.value)
108112
}
109113
)

‎src/main/resources/localization/defaultMessages.po

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,4 +128,10 @@ msgid "Extra SSH options"
128128
msgstr""
129129

130130
msgid"SSH proxy log directory"
131+
msgstr""
132+
133+
msgid"SSH network metrics directory"
134+
msgstr""
135+
136+
msgid"Network Status"
131137
msgstr""

‎src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import com.coder.toolbox.store.DISABLE_AUTOSTART
1818
importcom.coder.toolbox.store.ENABLE_BINARY_DIR_FALLBACK
1919
importcom.coder.toolbox.store.ENABLE_DOWNLOADS
2020
importcom.coder.toolbox.store.HEADER_COMMAND
21+
importcom.coder.toolbox.store.NETWORK_INFO_DIR
2122
importcom.coder.toolbox.store.SSH_CONFIG_OPTIONS
2223
importcom.coder.toolbox.store.SSH_CONFIG_PATH
2324
importcom.coder.toolbox.store.SSH_LOG_DIR
@@ -510,7 +511,10 @@ internal class CoderCLIManagerTest {
510511
HEADER_COMMAND to it.headerCommand,
511512
SSH_CONFIG_PATH to tmpdir.resolve(it.input+"_to_"+ it.output+".conf").toString(),
512513
SSH_CONFIG_OPTIONS to it.extraConfig,
513-
SSH_LOG_DIR to (it.sshLogDirectory?.toString()?:"")
514+
SSH_LOG_DIR to (it.sshLogDirectory?.toString()?:""),
515+
NETWORK_INFO_DIR to tmpdir.parent.resolve("coder-toolbox")
516+
.resolve("ssh-network-metrics")
517+
.normalize().toString()
514518
),
515519
env= it.env,
516520
context.logger,
@@ -531,6 +535,7 @@ internal class CoderCLIManagerTest {
531535

532536
// Output is the configuration we expect to have after configuring.
533537
val coderConfigPath= ccm.localBinaryPath.parent.resolve("config")
538+
val networkMetricsPath= tmpdir.parent.resolve("coder-toolbox").resolve("ssh-network-metrics")
534539
val expectedConf=
535540
Path.of("src/test/resources/fixtures/outputs/").resolve(it.output+".conf").toFile().readText()
536541
.replace(newlineRe,System.lineSeparator())
@@ -539,6 +544,10 @@ internal class CoderCLIManagerTest {
539544
"/tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64",
540545
escape(ccm.localBinaryPath.toString())
541546
)
547+
.replace(
548+
"/tmp/coder-toolbox/ssh-network-metrics",
549+
escape(networkMetricsPath.toString())
550+
)
542551
.let { conf->
543552
if (it.sshLogDirectory!=null) {
544553
conf.replace("/tmp/coder-toolbox/test.coder.invalid/logs", it.sshLogDirectory.toString())

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp