From 22010e157ae9f38483d03121edd2f20537bf6ec3 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Wed, 7 May 2025 23:03:03 +0300 Subject: [PATCH 01/10] impl: configurable network info location Adds support for a configurable path where ssh network info stats are going to be saved. --- .../com/coder/toolbox/settings/ReadOnlyCoderSettings.kt | 6 ++++++ .../kotlin/com/coder/toolbox/store/CoderSettingsStore.kt | 5 +++++ src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt | 2 ++ 3 files changed, 13 insertions(+) diff --git a/src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt b/src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt index 25568d3..478fdd1 100644 --- a/src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt +++ b/src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt @@ -110,6 +110,12 @@ interface ReadOnlyCoderSettings { */ val sshConfigOptions: String? + + /** + * The path where network information for SSH hosts are stored + */ + val networkInfoDir: String + /** * The default URL to show in the connection window. */ diff --git a/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt b/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt index 92c08d0..c70a3f1 100644 --- a/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt +++ b/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt @@ -65,6 +65,11 @@ class CoderSettingsStore( override val sshLogDirectory: String? get() = store[SSH_LOG_DIR] override val sshConfigOptions: String? get() = store[SSH_CONFIG_OPTIONS].takeUnless { it.isNullOrEmpty() } ?: env.get(CODER_SSH_CONFIG_OPTIONS) + override val networkInfoDir: String + get() = store[NETWORK_INFO_DIR].takeUnless { it.isNullOrEmpty() } ?: getDefaultGlobalDataDir() + .resolve("network-info") + .normalize() + .toString() /** * The default URL to show in the connection window. diff --git a/src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt b/src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt index 35040e3..e34436f 100644 --- a/src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt +++ b/src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt @@ -38,3 +38,5 @@ internal const val SSH_LOG_DIR = "sshLogDir" internal const val SSH_CONFIG_OPTIONS = "sshConfigOptions" +internal const val NETWORK_INFO_DIR = "networkInfoDir" + From 22d74a14826f0edb293f20267bcd8ba93433f4cd Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Wed, 7 May 2025 23:17:31 +0300 Subject: [PATCH 02/10] chore: remove dead code Remnant from the ssh background connection support --- src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt b/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt index a1b06d6..4bae4fb 100644 --- a/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt +++ b/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt @@ -277,8 +277,6 @@ class CoderCLIManager( if (!settings.sshLogDirectory.isNullOrBlank()) escape(settings.sshLogDirectory!!) else null, if (feats.reportWorkspaceUsage) "--usage-app=jetbrains" else null, ) - val backgroundProxyArgs = - baseArgs + listOfNotNull(if (feats.reportWorkspaceUsage) "--usage-app=disable" else null) val extraConfig = if (!settings.sshConfigOptions.isNullOrBlank()) { "\n" + settings.sshConfigOptions!!.prependIndent(" ") From b96a188f8bc060ec1c9e03df0d34dd33217b0d9d Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Wed, 7 May 2025 23:54:48 +0300 Subject: [PATCH 03/10] impl: download ssh network stats --- src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt b/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt index 4bae4fb..2898179 100644 --- a/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt +++ b/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt @@ -271,6 +271,7 @@ class CoderCLIManager( "ssh", "--stdio", if (settings.disableAutostart && feats.disableAutostart) "--disable-autostart" else null, + "--network-info-dir ${escape(settings.networkInfoDir)}" ) val proxyArgs = baseArgs + listOfNotNull( if (!settings.sshLogDirectory.isNullOrBlank()) "--log-dir" else null, From 129a0f5d3d57811bf027a69d4d40ef88d5b00ef4 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Thu, 8 May 2025 00:14:31 +0300 Subject: [PATCH 04/10] impl: allow network info dir to be configurable This patch provides UI support for the network inf dir where ssh metrics will be saved. Users can configure the default location for this place. --- src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt | 4 ++++ src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt | 4 ++++ src/main/resources/localization/defaultMessages.po | 3 +++ 3 files changed, 11 insertions(+) diff --git a/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt b/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt index c70a3f1..7429fdb 100644 --- a/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt +++ b/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt @@ -237,6 +237,10 @@ class CoderSettingsStore( store[SSH_LOG_DIR] = path } + fun updateNetworkInfoDir(path: String) { + store[NETWORK_INFO_DIR] = path + } + fun updateSshConfigOptions(options: String) { store[SSH_CONFIG_OPTIONS] = options } diff --git a/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt b/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt index ff86c42..f888c3d 100644 --- a/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt @@ -56,6 +56,8 @@ class CoderSettingsPage(context: CoderToolboxContext, triggerSshConfig: Channel< TextField(context.i18n.ptrl("Extra SSH options"), settings.sshConfigOptions ?: "", TextType.General) private val sshLogDirField = TextField(context.i18n.ptrl("SSH proxy log directory"), settings.sshLogDirectory ?: "", TextType.General) + private val networkInfoDirField = + TextField(context.i18n.ptrl("SSH network metrics directory"), settings.networkInfoDir, TextType.General) override val fields: StateFlow> = MutableStateFlow( @@ -73,6 +75,7 @@ class CoderSettingsPage(context: CoderToolboxContext, triggerSshConfig: Channel< disableAutostartField, enableSshWildCardConfig, sshLogDirField, + networkInfoDirField, sshExtraArgs, ) ) @@ -104,6 +107,7 @@ class CoderSettingsPage(context: CoderToolboxContext, triggerSshConfig: Channel< } } context.settingsStore.updateSshLogDir(sshLogDirField.textState.value) + context.settingsStore.updateNetworkInfoDir(networkInfoDirField.textState.value) context.settingsStore.updateSshConfigOptions(sshExtraArgs.textState.value) } ) diff --git a/src/main/resources/localization/defaultMessages.po b/src/main/resources/localization/defaultMessages.po index b38b2a6..27ec0b1 100644 --- a/src/main/resources/localization/defaultMessages.po +++ b/src/main/resources/localization/defaultMessages.po @@ -128,4 +128,7 @@ msgid "Extra SSH options" msgstr "" msgid "SSH proxy log directory" +msgstr "" + +msgid "SSH network metrics directory" msgstr "" \ No newline at end of file From 46a58bd01e4b69a53b548cabc0da0886799efd89 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Fri, 9 May 2025 01:22:55 +0300 Subject: [PATCH 05/10] fix: update UTs to include the network metrics configuration Also, the default directory is renamed to ssh-network-metrics for a better description. --- .../com/coder/toolbox/store/CoderSettingsStore.kt | 2 +- .../com/coder/toolbox/cli/CoderCLIManagerTest.kt | 11 ++++++++++- .../fixtures/outputs/append-blank-newlines.conf | 2 +- src/test/resources/fixtures/outputs/append-blank.conf | 2 +- .../resources/fixtures/outputs/append-no-blocks.conf | 2 +- .../resources/fixtures/outputs/append-no-newline.conf | 2 +- .../fixtures/outputs/append-no-related-blocks.conf | 2 +- .../resources/fixtures/outputs/disable-autostart.conf | 2 +- src/test/resources/fixtures/outputs/extra-config.conf | 2 +- .../fixtures/outputs/header-command-windows.conf | 2 +- .../resources/fixtures/outputs/header-command.conf | 2 +- src/test/resources/fixtures/outputs/log-dir.conf | 2 +- .../resources/fixtures/outputs/multiple-agents.conf | 4 ++-- .../resources/fixtures/outputs/multiple-users.conf | 4 ++-- .../fixtures/outputs/multiple-workspaces.conf | 4 ++-- .../fixtures/outputs/no-disable-autostart.conf | 2 +- .../resources/fixtures/outputs/no-report-usage.conf | 2 +- .../fixtures/outputs/replace-end-no-newline.conf | 2 +- src/test/resources/fixtures/outputs/replace-end.conf | 2 +- .../outputs/replace-middle-ignore-unrelated.conf | 2 +- .../resources/fixtures/outputs/replace-middle.conf | 2 +- src/test/resources/fixtures/outputs/replace-only.conf | 2 +- .../resources/fixtures/outputs/replace-start.conf | 2 +- src/test/resources/fixtures/outputs/url.conf | 2 +- src/test/resources/fixtures/outputs/wildcard.conf | 2 +- 25 files changed, 37 insertions(+), 28 deletions(-) diff --git a/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt b/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt index 7429fdb..50d6c25 100644 --- a/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt +++ b/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt @@ -67,7 +67,7 @@ class CoderSettingsStore( get() = store[SSH_CONFIG_OPTIONS].takeUnless { it.isNullOrEmpty() } ?: env.get(CODER_SSH_CONFIG_OPTIONS) override val networkInfoDir: String get() = store[NETWORK_INFO_DIR].takeUnless { it.isNullOrEmpty() } ?: getDefaultGlobalDataDir() - .resolve("network-info") + .resolve("ssh-network-metrics") .normalize() .toString() diff --git a/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt b/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt index d612000..b8dc145 100644 --- a/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt +++ b/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt @@ -18,6 +18,7 @@ import com.coder.toolbox.store.DISABLE_AUTOSTART import com.coder.toolbox.store.ENABLE_BINARY_DIR_FALLBACK import com.coder.toolbox.store.ENABLE_DOWNLOADS import com.coder.toolbox.store.HEADER_COMMAND +import com.coder.toolbox.store.NETWORK_INFO_DIR import com.coder.toolbox.store.SSH_CONFIG_OPTIONS import com.coder.toolbox.store.SSH_CONFIG_PATH import com.coder.toolbox.store.SSH_LOG_DIR @@ -510,7 +511,10 @@ internal class CoderCLIManagerTest { HEADER_COMMAND to it.headerCommand, SSH_CONFIG_PATH to tmpdir.resolve(it.input + "_to_" + it.output + ".conf").toString(), SSH_CONFIG_OPTIONS to it.extraConfig, - SSH_LOG_DIR to (it.sshLogDirectory?.toString() ?: "") + SSH_LOG_DIR to (it.sshLogDirectory?.toString() ?: ""), + NETWORK_INFO_DIR to tmpdir.parent.resolve("coder-toolbox") + .resolve("ssh-network-metrics") + .normalize().toString() ), env = it.env, context.logger, @@ -531,6 +535,7 @@ internal class CoderCLIManagerTest { // Output is the configuration we expect to have after configuring. val coderConfigPath = ccm.localBinaryPath.parent.resolve("config") + val networkMetricsPath = tmpdir.parent.resolve("coder-toolbox").resolve("ssh-network-metrics") val expectedConf = Path.of("src/test/resources/fixtures/outputs/").resolve(it.output + ".conf").toFile().readText() .replace(newlineRe, System.lineSeparator()) @@ -539,6 +544,10 @@ internal class CoderCLIManagerTest { "/tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64", escape(ccm.localBinaryPath.toString()) ) + .replace( + "/tmp/coder-toolbox/ssh-network-metrics", + escape(networkMetricsPath.toString()) + ) .let { conf -> if (it.sshLogDirectory != null) { conf.replace("/tmp/coder-toolbox/test.coder.invalid/logs", it.sshLogDirectory.toString()) diff --git a/src/test/resources/fixtures/outputs/append-blank-newlines.conf b/src/test/resources/fixtures/outputs/append-blank-newlines.conf index 6a3fa9d..51d1d75 100644 --- a/src/test/resources/fixtures/outputs/append-blank-newlines.conf +++ b/src/test/resources/fixtures/outputs/append-blank-newlines.conf @@ -4,7 +4,7 @@ # --- START CODER JETBRAINS TOOLBOX test.coder.invalid Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --usage-app=jetbrains owner/foo.agent1 + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --network-info-dir /tmp/coder-toolbox/ssh-network-metrics --usage-app=jetbrains owner/foo.agent1 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null diff --git a/src/test/resources/fixtures/outputs/append-blank.conf b/src/test/resources/fixtures/outputs/append-blank.conf index 4c1ac2b..f2f1c8b 100644 --- a/src/test/resources/fixtures/outputs/append-blank.conf +++ b/src/test/resources/fixtures/outputs/append-blank.conf @@ -1,6 +1,6 @@ # --- START CODER JETBRAINS TOOLBOX test.coder.invalid Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --usage-app=jetbrains owner/foo.agent1 + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --network-info-dir /tmp/coder-toolbox/ssh-network-metrics --usage-app=jetbrains owner/foo.agent1 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null diff --git a/src/test/resources/fixtures/outputs/append-no-blocks.conf b/src/test/resources/fixtures/outputs/append-no-blocks.conf index fbcd928..0c34e44 100644 --- a/src/test/resources/fixtures/outputs/append-no-blocks.conf +++ b/src/test/resources/fixtures/outputs/append-no-blocks.conf @@ -5,7 +5,7 @@ Host test2 # --- START CODER JETBRAINS TOOLBOX test.coder.invalid Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --usage-app=jetbrains owner/foo.agent1 + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --network-info-dir /tmp/coder-toolbox/ssh-network-metrics --usage-app=jetbrains owner/foo.agent1 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null diff --git a/src/test/resources/fixtures/outputs/append-no-newline.conf b/src/test/resources/fixtures/outputs/append-no-newline.conf index f31936a..c25a062 100644 --- a/src/test/resources/fixtures/outputs/append-no-newline.conf +++ b/src/test/resources/fixtures/outputs/append-no-newline.conf @@ -4,7 +4,7 @@ Host test2 Port 443 # --- START CODER JETBRAINS TOOLBOX test.coder.invalid Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --usage-app=jetbrains owner/foo.agent1 + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --network-info-dir /tmp/coder-toolbox/ssh-network-metrics --usage-app=jetbrains owner/foo.agent1 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null diff --git a/src/test/resources/fixtures/outputs/append-no-related-blocks.conf b/src/test/resources/fixtures/outputs/append-no-related-blocks.conf index 6578ea9..53f964e 100644 --- a/src/test/resources/fixtures/outputs/append-no-related-blocks.conf +++ b/src/test/resources/fixtures/outputs/append-no-related-blocks.conf @@ -11,7 +11,7 @@ some jetbrains config # --- START CODER JETBRAINS TOOLBOX test.coder.invalid Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --usage-app=jetbrains owner/foo.agent1 + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --network-info-dir /tmp/coder-toolbox/ssh-network-metrics --usage-app=jetbrains owner/foo.agent1 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null diff --git a/src/test/resources/fixtures/outputs/disable-autostart.conf b/src/test/resources/fixtures/outputs/disable-autostart.conf index 64f4126..27c6986 100644 --- a/src/test/resources/fixtures/outputs/disable-autostart.conf +++ b/src/test/resources/fixtures/outputs/disable-autostart.conf @@ -1,6 +1,6 @@ # --- START CODER JETBRAINS TOOLBOX test.coder.invalid Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --disable-autostart --usage-app=jetbrains owner/foo.agent1 + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --disable-autostart --network-info-dir /tmp/coder-toolbox/ssh-network-metrics --usage-app=jetbrains owner/foo.agent1 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null diff --git a/src/test/resources/fixtures/outputs/extra-config.conf b/src/test/resources/fixtures/outputs/extra-config.conf index 75bd083..6abe1f0 100644 --- a/src/test/resources/fixtures/outputs/extra-config.conf +++ b/src/test/resources/fixtures/outputs/extra-config.conf @@ -1,6 +1,6 @@ # --- START CODER JETBRAINS TOOLBOX test.coder.invalid Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --usage-app=jetbrains owner/foo.agent1 + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --network-info-dir /tmp/coder-toolbox/ssh-network-metrics --usage-app=jetbrains owner/foo.agent1 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null diff --git a/src/test/resources/fixtures/outputs/header-command-windows.conf b/src/test/resources/fixtures/outputs/header-command-windows.conf index 700032c..4d3b49c 100644 --- a/src/test/resources/fixtures/outputs/header-command-windows.conf +++ b/src/test/resources/fixtures/outputs/header-command-windows.conf @@ -1,6 +1,6 @@ # --- START CODER JETBRAINS TOOLBOX test.coder.invalid Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid --header-command "\"C:\Program Files\My Header Command\HeaderCommand.exe\" --url=\"%%CODER_URL%%\" --test=\"foo bar\"" ssh --stdio --usage-app=jetbrains owner/foo.agent1 + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid --header-command "\"C:\Program Files\My Header Command\HeaderCommand.exe\" --url=\"%%CODER_URL%%\" --test=\"foo bar\"" ssh --stdio --network-info-dir /tmp/coder-toolbox/ssh-network-metrics --usage-app=jetbrains owner/foo.agent1 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null diff --git a/src/test/resources/fixtures/outputs/header-command.conf b/src/test/resources/fixtures/outputs/header-command.conf index b8d6e14..4d27aaa 100644 --- a/src/test/resources/fixtures/outputs/header-command.conf +++ b/src/test/resources/fixtures/outputs/header-command.conf @@ -1,6 +1,6 @@ # --- START CODER JETBRAINS TOOLBOX test.coder.invalid Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid --header-command 'my-header-command --url="$CODER_URL" --test="foo bar" --literal='\''$CODER_URL'\''' ssh --stdio --usage-app=jetbrains owner/foo.agent1 + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid --header-command 'my-header-command --url="$CODER_URL" --test="foo bar" --literal='\''$CODER_URL'\''' ssh --stdio --network-info-dir /tmp/coder-toolbox/ssh-network-metrics --usage-app=jetbrains owner/foo.agent1 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null diff --git a/src/test/resources/fixtures/outputs/log-dir.conf b/src/test/resources/fixtures/outputs/log-dir.conf index e47b5be..0050661 100644 --- a/src/test/resources/fixtures/outputs/log-dir.conf +++ b/src/test/resources/fixtures/outputs/log-dir.conf @@ -1,6 +1,6 @@ # --- START CODER JETBRAINS TOOLBOX test.coder.invalid Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --log-dir /tmp/coder-toolbox/test.coder.invalid/logs --usage-app=jetbrains owner/foo.agent1 + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --network-info-dir /tmp/coder-toolbox/ssh-network-metrics --log-dir /tmp/coder-toolbox/test.coder.invalid/logs --usage-app=jetbrains owner/foo.agent1 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null diff --git a/src/test/resources/fixtures/outputs/multiple-agents.conf b/src/test/resources/fixtures/outputs/multiple-agents.conf index c0cbffe..d26e398 100644 --- a/src/test/resources/fixtures/outputs/multiple-agents.conf +++ b/src/test/resources/fixtures/outputs/multiple-agents.conf @@ -1,6 +1,6 @@ # --- START CODER JETBRAINS TOOLBOX test.coder.invalid Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --usage-app=jetbrains owner/foo.agent1 + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --network-info-dir /tmp/coder-toolbox/ssh-network-metrics --usage-app=jetbrains owner/foo.agent1 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null @@ -8,7 +8,7 @@ Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid SetEnv CODER_SSH_SESSION_TYPE=JetBrains Host coder-jetbrains-toolbox--owner--foo.agent2--test.coder.invalid - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --usage-app=jetbrains owner/foo.agent2 + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --network-info-dir /tmp/coder-toolbox/ssh-network-metrics --usage-app=jetbrains owner/foo.agent2 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null diff --git a/src/test/resources/fixtures/outputs/multiple-users.conf b/src/test/resources/fixtures/outputs/multiple-users.conf index ed34415..13801b9 100644 --- a/src/test/resources/fixtures/outputs/multiple-users.conf +++ b/src/test/resources/fixtures/outputs/multiple-users.conf @@ -1,6 +1,6 @@ # --- START CODER JETBRAINS TOOLBOX test.coder.invalid Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --usage-app=jetbrains owner/foo.agent1 + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --network-info-dir /tmp/coder-toolbox/ssh-network-metrics --usage-app=jetbrains owner/foo.agent1 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null @@ -8,7 +8,7 @@ Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid SetEnv CODER_SSH_SESSION_TYPE=JetBrains Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --usage-app=jetbrains owner/foo.agent1 + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --network-info-dir /tmp/coder-toolbox/ssh-network-metrics --usage-app=jetbrains owner/foo.agent1 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null diff --git a/src/test/resources/fixtures/outputs/multiple-workspaces.conf b/src/test/resources/fixtures/outputs/multiple-workspaces.conf index 9e308e7..d912d26 100644 --- a/src/test/resources/fixtures/outputs/multiple-workspaces.conf +++ b/src/test/resources/fixtures/outputs/multiple-workspaces.conf @@ -1,6 +1,6 @@ # --- START CODER JETBRAINS TOOLBOX test.coder.invalid Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --usage-app=jetbrains owner/foo.agent1 + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --network-info-dir /tmp/coder-toolbox/ssh-network-metrics --usage-app=jetbrains owner/foo.agent1 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null @@ -8,7 +8,7 @@ Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid SetEnv CODER_SSH_SESSION_TYPE=JetBrains Host coder-jetbrains-toolbox--owner--bar.agent1--test.coder.invalid - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --usage-app=jetbrains owner/bar.agent1 + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --network-info-dir /tmp/coder-toolbox/ssh-network-metrics --usage-app=jetbrains owner/bar.agent1 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null diff --git a/src/test/resources/fixtures/outputs/no-disable-autostart.conf b/src/test/resources/fixtures/outputs/no-disable-autostart.conf index 4c1ac2b..f2f1c8b 100644 --- a/src/test/resources/fixtures/outputs/no-disable-autostart.conf +++ b/src/test/resources/fixtures/outputs/no-disable-autostart.conf @@ -1,6 +1,6 @@ # --- START CODER JETBRAINS TOOLBOX test.coder.invalid Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --usage-app=jetbrains owner/foo.agent1 + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --network-info-dir /tmp/coder-toolbox/ssh-network-metrics --usage-app=jetbrains owner/foo.agent1 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null diff --git a/src/test/resources/fixtures/outputs/no-report-usage.conf b/src/test/resources/fixtures/outputs/no-report-usage.conf index 2bdfd47..3f2311c 100644 --- a/src/test/resources/fixtures/outputs/no-report-usage.conf +++ b/src/test/resources/fixtures/outputs/no-report-usage.conf @@ -1,6 +1,6 @@ # --- START CODER JETBRAINS TOOLBOX test.coder.invalid Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio owner/foo.agent1 + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --network-info-dir /tmp/coder-toolbox/ssh-network-metrics owner/foo.agent1 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null diff --git a/src/test/resources/fixtures/outputs/replace-end-no-newline.conf b/src/test/resources/fixtures/outputs/replace-end-no-newline.conf index 36b8380..7e64e33 100644 --- a/src/test/resources/fixtures/outputs/replace-end-no-newline.conf +++ b/src/test/resources/fixtures/outputs/replace-end-no-newline.conf @@ -3,7 +3,7 @@ Host test Host test2 Port 443 # --- START CODER JETBRAINS TOOLBOX test.coder.invalid Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --usage-app=jetbrains owner/foo.agent1 + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --network-info-dir /tmp/coder-toolbox/ssh-network-metrics --usage-app=jetbrains owner/foo.agent1 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null diff --git a/src/test/resources/fixtures/outputs/replace-end.conf b/src/test/resources/fixtures/outputs/replace-end.conf index f31936a..c25a062 100644 --- a/src/test/resources/fixtures/outputs/replace-end.conf +++ b/src/test/resources/fixtures/outputs/replace-end.conf @@ -4,7 +4,7 @@ Host test2 Port 443 # --- START CODER JETBRAINS TOOLBOX test.coder.invalid Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --usage-app=jetbrains owner/foo.agent1 + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --network-info-dir /tmp/coder-toolbox/ssh-network-metrics --usage-app=jetbrains owner/foo.agent1 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null diff --git a/src/test/resources/fixtures/outputs/replace-middle-ignore-unrelated.conf b/src/test/resources/fixtures/outputs/replace-middle-ignore-unrelated.conf index 80cd717..f4f7f16 100644 --- a/src/test/resources/fixtures/outputs/replace-middle-ignore-unrelated.conf +++ b/src/test/resources/fixtures/outputs/replace-middle-ignore-unrelated.conf @@ -5,7 +5,7 @@ some coder config # ------------END-CODER------------ # --- START CODER JETBRAINS TOOLBOX test.coder.invalid Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --usage-app=jetbrains owner/foo.agent1 + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --network-info-dir /tmp/coder-toolbox/ssh-network-metrics --usage-app=jetbrains owner/foo.agent1 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null diff --git a/src/test/resources/fixtures/outputs/replace-middle.conf b/src/test/resources/fixtures/outputs/replace-middle.conf index 5c74b95..8d6fadc 100644 --- a/src/test/resources/fixtures/outputs/replace-middle.conf +++ b/src/test/resources/fixtures/outputs/replace-middle.conf @@ -2,7 +2,7 @@ Host test Port 80 # --- START CODER JETBRAINS TOOLBOX test.coder.invalid Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --usage-app=jetbrains owner/foo.agent1 + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --network-info-dir /tmp/coder-toolbox/ssh-network-metrics --usage-app=jetbrains owner/foo.agent1 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null diff --git a/src/test/resources/fixtures/outputs/replace-only.conf b/src/test/resources/fixtures/outputs/replace-only.conf index 4c1ac2b..f2f1c8b 100644 --- a/src/test/resources/fixtures/outputs/replace-only.conf +++ b/src/test/resources/fixtures/outputs/replace-only.conf @@ -1,6 +1,6 @@ # --- START CODER JETBRAINS TOOLBOX test.coder.invalid Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --usage-app=jetbrains owner/foo.agent1 + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --network-info-dir /tmp/coder-toolbox/ssh-network-metrics --usage-app=jetbrains owner/foo.agent1 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null diff --git a/src/test/resources/fixtures/outputs/replace-start.conf b/src/test/resources/fixtures/outputs/replace-start.conf index c99a993..dfc2151 100644 --- a/src/test/resources/fixtures/outputs/replace-start.conf +++ b/src/test/resources/fixtures/outputs/replace-start.conf @@ -1,6 +1,6 @@ # --- START CODER JETBRAINS TOOLBOX test.coder.invalid Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --usage-app=jetbrains owner/foo.agent1 + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --network-info-dir /tmp/coder-toolbox/ssh-network-metrics --usage-app=jetbrains owner/foo.agent1 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null diff --git a/src/test/resources/fixtures/outputs/url.conf b/src/test/resources/fixtures/outputs/url.conf index 4d06a17..d028507 100644 --- a/src/test/resources/fixtures/outputs/url.conf +++ b/src/test/resources/fixtures/outputs/url.conf @@ -1,6 +1,6 @@ # --- START CODER JETBRAINS TOOLBOX test.coder.invalid Host coder-jetbrains-toolbox--owner--foo.agent1--test.coder.invalid - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid?foo=bar&baz=qux ssh --stdio --usage-app=jetbrains owner/foo.agent1 + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid?foo=bar&baz=qux ssh --stdio --network-info-dir /tmp/coder-toolbox/ssh-network-metrics --usage-app=jetbrains owner/foo.agent1 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null diff --git a/src/test/resources/fixtures/outputs/wildcard.conf b/src/test/resources/fixtures/outputs/wildcard.conf index e7c55b1..86d4d97 100644 --- a/src/test/resources/fixtures/outputs/wildcard.conf +++ b/src/test/resources/fixtures/outputs/wildcard.conf @@ -1,6 +1,6 @@ # --- START CODER JETBRAINS TOOLBOX test.coder.invalid Host coder-jetbrains-toolbox-test.coder.invalid--* - ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --ssh-host-prefix coder-jetbrains-toolbox-test.coder.invalid-- %h + ProxyCommand /tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-toolbox/test.coder.invalid/config --url https://test.coder.invalid ssh --stdio --network-info-dir /tmp/coder-toolbox/ssh-network-metrics --ssh-host-prefix coder-jetbrains-toolbox-test.coder.invalid-- %h ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null From c5c1850995e3dac712dd51bb2da5bdf1945cf33f Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Fri, 9 May 2025 23:51:09 +0300 Subject: [PATCH 06/10] impl: identify the PID for the SSH command Toolbox spawns a native process running the SSH client. The ssh client then spawns another process which associated to the coder proxy command. SSH network metrics are saved into json file with the name equal to the pid of the ssh command (not to be confused with the proxy command's name) --- .../toolbox/cli/SshCommandProcessHandle.kt | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/main/kotlin/com/coder/toolbox/cli/SshCommandProcessHandle.kt diff --git a/src/main/kotlin/com/coder/toolbox/cli/SshCommandProcessHandle.kt b/src/main/kotlin/com/coder/toolbox/cli/SshCommandProcessHandle.kt new file mode 100644 index 0000000..1d36e4f --- /dev/null +++ b/src/main/kotlin/com/coder/toolbox/cli/SshCommandProcessHandle.kt @@ -0,0 +1,42 @@ +package com.coder.toolbox.cli + +import com.coder.toolbox.CoderToolboxContext +import com.coder.toolbox.sdk.v2.models.Workspace +import com.coder.toolbox.sdk.v2.models.WorkspaceAgent +import kotlin.jvm.optionals.getOrNull + +/** + * Identifies the PID for the SSH Coder command spawned by Toolbox. + */ +class SshCommandProcessHandle(private val ctx: CoderToolboxContext) { + + /** + * Finds the PID of a Coder (not the proxy command) ssh cmd associated with the specified workspace and agent. + * Null is returned when no ssh command process was found. + * + * Implementation Notes: + * An iterative DFS approach where we start with Toolbox's direct children, grep the command + * and if nothing is found we continue with the processes children. Toolbox spawns an ssh command + * as a separate command which in turns spawns another child for the proxy command. + */ + fun findByWorkspaceAndAgent(ws: Workspace, agent: WorkspaceAgent): Long? { + val stack = ArrayDeque(ProcessHandle.current().children().toList()) + while (stack.isNotEmpty()) { + val processHandle = stack.removeLast() + val cmdLine = processHandle.info().commandLine().getOrNull() + ctx.logger.debug("SSH command PID: ${processHandle.pid()} Command: $cmdLine") + if (cmdLine != null && cmdLine.isSshCommandFor(ws, agent)) { + ctx.logger.debug("SSH command with PID: ${processHandle.pid()} and Command: $cmdLine matches ${ws.name}.${agent.name}") + return processHandle.pid() + } else { + stack.addAll(processHandle.children().toList()) + } + } + return null + } + + private fun String.isSshCommandFor(ws: Workspace, agent: WorkspaceAgent): Boolean { + // usage-app is present only in the ProxyCommand + return !this.contains("--usage-app=jetbrains") && this.contains("${ws.name}.${agent.name}") + } +} \ No newline at end of file From 8c2271c4f36abf86f0247ed9f3c1b220ecab68a2 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Sat, 10 May 2025 00:14:51 +0300 Subject: [PATCH 07/10] impl: network metrics poll job When ssh is connected a poll job is created that searches for the SSH command pid, and then determines the network metrics json file. The pid can change multiple times in a Toolbox session, especially when the OS goes to sleep and comes back - the ssh is respawned. This is the reason we don't cache the pid, instead always search for it. The poll job activates every 5 settings and it is cancelled when the user stops the ssh connection. --- .../coder/toolbox/CoderRemoteEnvironment.kt | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt index adafeb0..1d6f5f4 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt @@ -2,6 +2,7 @@ package com.coder.toolbox import com.coder.toolbox.browser.BrowserUtil import com.coder.toolbox.cli.CoderCLIManager +import com.coder.toolbox.cli.SshCommandProcessHandle import com.coder.toolbox.models.WorkspaceAndAgentStatus import com.coder.toolbox.sdk.CoderRestClient import com.coder.toolbox.sdk.ex.APIResponseException @@ -20,15 +21,20 @@ import com.jetbrains.toolbox.api.remoteDev.environments.EnvironmentContentsView import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentDescription import com.jetbrains.toolbox.api.remoteDev.states.RemoteEnvironmentState import com.jetbrains.toolbox.api.ui.actions.ActionDescription +import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout +import java.io.File +import java.nio.file.Path import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds +private val POLL_INTERVAL = 5.seconds + /** * Represents an agent and workspace combination. * @@ -55,6 +61,9 @@ class CoderRemoteEnvironment( override val actionsList: MutableStateFlow> = MutableStateFlow(getAvailableActions()) + private val proxyCommandHandle = SshCommandProcessHandle(context) + private var pollJob: Job? = null + fun asPairOfWorkspaceAndAgent(): Pair = Pair(workspace, agent) private fun getAvailableActions(): List { @@ -141,9 +150,37 @@ class CoderRemoteEnvironment( override fun beforeConnection() { context.logger.info("Connecting to $id...") isConnected.update { true } + + pollJob = pollNetworkMetrics() + } + + private fun pollNetworkMetrics(): Job = context.cs.launch { + context.logger.info("Starting the network metrics poll job for $id") + while (isActive) { + context.logger.debug("Searching SSH command's PID for workspace $id...") + val pid = proxyCommandHandle.findByWorkspaceAndAgent(workspace, agent) + if (pid == null) { + context.logger.debug("No SSH command PID was found for workspace $id") + delay(POLL_INTERVAL) + continue + } + + val metricsFile = Path.of(context.settingsStore.networkInfoDir, "$pid.json").toFile() + if (metricsFile.doesNotExists()) { + context.logger.debug("No metrics file found at ${metricsFile.absolutePath} for $id") + delay(POLL_INTERVAL) + continue + } + context.logger.debug("Loading metrics from ${metricsFile.absolutePath} for $id") + delay(POLL_INTERVAL) + } } + private fun File.doesNotExists(): Boolean = !this.exists() + override fun afterDisconnect() { + context.logger.info("Stopping the network metrics poll job for $id") + pollJob?.cancel() this.connectionRequest.update { false } isConnected.update { false } context.logger.info("Disconnected from $id") From 5e30b60abdfb5edb14c4a89764054071e1eced4d Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Sat, 10 May 2025 00:53:43 +0300 Subject: [PATCH 08/10] impl: load ssh metrics from json This commit contains the data class necessary to unmarshall the ssh network metrics and also the necessary moshi plumbing for converting the json file. --- .../coder/toolbox/CoderRemoteEnvironment.kt | 12 ++++++- .../toolbox/sdk/v2/models/NetworkMetrics.kt | 33 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/com/coder/toolbox/sdk/v2/models/NetworkMetrics.kt diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt index 1d6f5f4..7544d4f 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt @@ -6,6 +6,7 @@ import com.coder.toolbox.cli.SshCommandProcessHandle import com.coder.toolbox.models.WorkspaceAndAgentStatus import com.coder.toolbox.sdk.CoderRestClient import com.coder.toolbox.sdk.ex.APIResponseException +import com.coder.toolbox.sdk.v2.models.NetworkMetrics import com.coder.toolbox.sdk.v2.models.Workspace import com.coder.toolbox.sdk.v2.models.WorkspaceAgent import com.coder.toolbox.util.waitForFalseWithTimeout @@ -21,6 +22,7 @@ import com.jetbrains.toolbox.api.remoteDev.environments.EnvironmentContentsView import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentDescription import com.jetbrains.toolbox.api.remoteDev.states.RemoteEnvironmentState import com.jetbrains.toolbox.api.ui.actions.ActionDescription +import com.squareup.moshi.Moshi import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -61,6 +63,7 @@ class CoderRemoteEnvironment( override val actionsList: MutableStateFlow> = MutableStateFlow(getAvailableActions()) + private val networkMetricsMarshaller = Moshi.Builder().build().adapter(NetworkMetrics::class.java) private val proxyCommandHandle = SshCommandProcessHandle(context) private var pollJob: Job? = null @@ -150,7 +153,6 @@ class CoderRemoteEnvironment( override fun beforeConnection() { context.logger.info("Connecting to $id...") isConnected.update { true } - pollJob = pollNetworkMetrics() } @@ -172,6 +174,14 @@ class CoderRemoteEnvironment( continue } context.logger.debug("Loading metrics from ${metricsFile.absolutePath} for $id") + try { + context.logger.debug("$id metrics: ${networkMetricsMarshaller.fromJson(metricsFile.readText())}") + } catch (e: Exception) { + context.logger.error( + e, + "Error encountered while trying to load network metrics from ${metricsFile.absolutePath} for $id" + ) + } delay(POLL_INTERVAL) } } diff --git a/src/main/kotlin/com/coder/toolbox/sdk/v2/models/NetworkMetrics.kt b/src/main/kotlin/com/coder/toolbox/sdk/v2/models/NetworkMetrics.kt new file mode 100644 index 0000000..0f9032a --- /dev/null +++ b/src/main/kotlin/com/coder/toolbox/sdk/v2/models/NetworkMetrics.kt @@ -0,0 +1,33 @@ +package com.coder.toolbox.sdk.v2.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Coder ssh network metrics. All properties are optional + * because Coder Connect only populates `using_coder_connect` + * while p2p doesn't populate this property. + */ +@JsonClass(generateAdapter = true) +data class NetworkMetrics( + @Json(name = "p2p") + val p2p: Boolean?, + + @Json(name = "latency") + val latency: Double?, + + @Json(name = "preferred_derp") + val preferredDerp: String?, + + @Json(name = "derp_latency") + val derpLatency: Map?, + + @Json(name = "upload_bytes_sec") + val uploadBytesSec: Long?, + + @Json(name = "download_bytes_sec") + val downloadBytesSec: Long?, + + @Json(name = "using_coder_connect") + val usingCoderConnect: Boolean? +) From 3f2ab4f3937c7aeb8c37dd34047ec920c80aaf88 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Tue, 13 May 2025 00:40:05 +0300 Subject: [PATCH 09/10] impl: show ssh network metrics in the Settings tab Under the "Additional environment information". Unfortunately it was not possible any other way. The description property is modifiable however Toolbox renders the description label only as long as the SSH connection is not established. As soon as an ssh connection is running the description label is used as mechanism to notify users about available IDE updates. It also appears that we can't have any other extra tab, other than "Tools", "Projects" and "Settings". There is a secondary information attribute API, but it is not usable to show recurring metrics info because it can only be configured once, it is not a mutable field. The best effort was to add the information in the Settings page, and it is worth highlighting that the metrics are only refreshed when user either: - switches between tabs - expands/collapses teh "Additional environment information" section. There is no programmatic mechanism to notify the information in the Settings page changed --- .../com/coder/toolbox/CoderRemoteEnvironment.kt | 11 ++++++++--- .../com/coder/toolbox/sdk/v2/models/NetworkMetrics.kt | 10 +++++++++- src/main/resources/localization/defaultMessages.po | 3 +++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt index 7544d4f..7c812e4 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt @@ -13,6 +13,7 @@ import com.coder.toolbox.util.waitForFalseWithTimeout import com.coder.toolbox.util.withPath import com.coder.toolbox.views.Action import com.coder.toolbox.views.EnvironmentView +import com.jetbrains.toolbox.api.localization.LocalizableString import com.jetbrains.toolbox.api.remoteDev.AfterDisconnectHook import com.jetbrains.toolbox.api.remoteDev.BeforeConnectionHook import com.jetbrains.toolbox.api.remoteDev.DeleteEnvironmentConfirmationParams @@ -52,7 +53,6 @@ class CoderRemoteEnvironment( private var wsRawStatus = WorkspaceAndAgentStatus.from(workspace, agent) override var name: String = "${workspace.name}.${agent.name}" - private var isConnected: MutableStateFlow = MutableStateFlow(false) override val connectionRequest: MutableStateFlow = MutableStateFlow(false) @@ -60,7 +60,7 @@ class CoderRemoteEnvironment( MutableStateFlow(wsRawStatus.toRemoteEnvironmentState(context)) override val description: MutableStateFlow = MutableStateFlow(EnvironmentDescription.General(context.i18n.pnotr(workspace.templateDisplayName))) - + override val additionalEnvironmentInformation: MutableMap = mutableMapOf() override val actionsList: MutableStateFlow> = MutableStateFlow(getAvailableActions()) private val networkMetricsMarshaller = Moshi.Builder().build().adapter(NetworkMetrics::class.java) @@ -175,7 +175,12 @@ class CoderRemoteEnvironment( } context.logger.debug("Loading metrics from ${metricsFile.absolutePath} for $id") try { - context.logger.debug("$id metrics: ${networkMetricsMarshaller.fromJson(metricsFile.readText())}") + val metrics = networkMetricsMarshaller.fromJson(metricsFile.readText()) + if (metrics == null) { + return@launch + } + context.logger.debug("$id metrics: $metrics") + additionalEnvironmentInformation.put(context.i18n.ptrl("Network Metrics"), metrics.toPretty()) } catch (e: Exception) { context.logger.error( e, diff --git a/src/main/kotlin/com/coder/toolbox/sdk/v2/models/NetworkMetrics.kt b/src/main/kotlin/com/coder/toolbox/sdk/v2/models/NetworkMetrics.kt index 0f9032a..2512fec 100644 --- a/src/main/kotlin/com/coder/toolbox/sdk/v2/models/NetworkMetrics.kt +++ b/src/main/kotlin/com/coder/toolbox/sdk/v2/models/NetworkMetrics.kt @@ -30,4 +30,12 @@ data class NetworkMetrics( @Json(name = "using_coder_connect") val usingCoderConnect: Boolean? -) +) { + fun toPretty(): String { + return if (p2p == true) { + "Direct (${latency}ms) \u00B7 Download \u2193 $downloadBytesSec b/s \u00B7 Upload \u2191 $uploadBytesSec b/s" + } else { + "$preferredDerp (${latency}ms) \u00B7 Download \u2193 $downloadBytesSec b/s \u00B7 Upload \u2191 $uploadBytesSec b/s" + } + } +} diff --git a/src/main/resources/localization/defaultMessages.po b/src/main/resources/localization/defaultMessages.po index 27ec0b1..cd44044 100644 --- a/src/main/resources/localization/defaultMessages.po +++ b/src/main/resources/localization/defaultMessages.po @@ -131,4 +131,7 @@ msgid "SSH proxy log directory" msgstr "" msgid "SSH network metrics directory" +msgstr "" + +msgid "Network Metrics" msgstr "" \ No newline at end of file From 100fb9db9d30e25841e00024f46b335207916f27 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Wed, 14 May 2025 23:31:27 +0300 Subject: [PATCH 10/10] impl: show ssh network metrics in the Settings tab (2) Discarded the download/upload stats, changed the text status, and I've added support for Coder Connect. UTs covering the status generation were also added. --- CHANGELOG.md | 4 + .../coder/toolbox/CoderRemoteEnvironment.kt | 2 +- .../toolbox/sdk/v2/models/NetworkMetrics.kt | 12 +- .../resources/localization/defaultMessages.po | 2 +- .../sdk/v2/models/NetworkMetricsTest.kt | 107 ++++++++++++++++++ 5 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 src/test/kotlin/com/coder/toolbox/sdk/v2/models/NetworkMetricsTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 32831c5..588d65a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added + +- render network status in the Settings tab, under `Additional environment information` section. + ## 0.2.1 - 2025-05-05 ### Changed diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt index 7c812e4..ad11c30 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt @@ -180,7 +180,7 @@ class CoderRemoteEnvironment( return@launch } context.logger.debug("$id metrics: $metrics") - additionalEnvironmentInformation.put(context.i18n.ptrl("Network Metrics"), metrics.toPretty()) + additionalEnvironmentInformation.put(context.i18n.ptrl("Network Status"), metrics.toPretty()) } catch (e: Exception) { context.logger.error( e, diff --git a/src/main/kotlin/com/coder/toolbox/sdk/v2/models/NetworkMetrics.kt b/src/main/kotlin/com/coder/toolbox/sdk/v2/models/NetworkMetrics.kt index 2512fec..cb7d235 100644 --- a/src/main/kotlin/com/coder/toolbox/sdk/v2/models/NetworkMetrics.kt +++ b/src/main/kotlin/com/coder/toolbox/sdk/v2/models/NetworkMetrics.kt @@ -2,6 +2,9 @@ package com.coder.toolbox.sdk.v2.models import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import java.text.DecimalFormat + +private val formatter = DecimalFormat("#.00") /** * Coder ssh network metrics. All properties are optional @@ -32,10 +35,15 @@ data class NetworkMetrics( val usingCoderConnect: Boolean? ) { fun toPretty(): String { + if (usingCoderConnect == true) { + return "You're connected using Coder Connect" + } return if (p2p == true) { - "Direct (${latency}ms) \u00B7 Download \u2193 $downloadBytesSec b/s \u00B7 Upload \u2191 $uploadBytesSec b/s" + "Direct (${formatter.format(latency)}ms). You're connected peer-to-peer" } else { - "$preferredDerp (${latency}ms) \u00B7 Download \u2193 $downloadBytesSec b/s \u00B7 Upload \u2191 $uploadBytesSec b/s" + val derpLatency = derpLatency!![preferredDerp] + val workspaceLatency = latency!!.minus(derpLatency!!) + "You ↔ $preferredDerp (${formatter.format(derpLatency)}ms) ↔ Workspace (${formatter.format(workspaceLatency)}ms). You are connected through a relay" } } } diff --git a/src/main/resources/localization/defaultMessages.po b/src/main/resources/localization/defaultMessages.po index cd44044..fddd131 100644 --- a/src/main/resources/localization/defaultMessages.po +++ b/src/main/resources/localization/defaultMessages.po @@ -133,5 +133,5 @@ msgstr "" msgid "SSH network metrics directory" msgstr "" -msgid "Network Metrics" +msgid "Network Status" msgstr "" \ No newline at end of file diff --git a/src/test/kotlin/com/coder/toolbox/sdk/v2/models/NetworkMetricsTest.kt b/src/test/kotlin/com/coder/toolbox/sdk/v2/models/NetworkMetricsTest.kt new file mode 100644 index 0000000..08b98df --- /dev/null +++ b/src/test/kotlin/com/coder/toolbox/sdk/v2/models/NetworkMetricsTest.kt @@ -0,0 +1,107 @@ +package com.coder.toolbox.sdk.v2.models + +import kotlin.test.Test +import kotlin.test.assertEquals + +class NetworkMetricsTest { + + @Test + fun `toPretty should return message for Coder Connect`() { + val metrics = NetworkMetrics( + p2p = null, + latency = null, + preferredDerp = null, + derpLatency = null, + uploadBytesSec = null, + downloadBytesSec = null, + usingCoderConnect = true + ) + + val expected = "You're connected using Coder Connect" + assertEquals(expected, metrics.toPretty()) + } + + @Test + fun `toPretty should return message for P2P connection`() { + val metrics = NetworkMetrics( + p2p = true, + latency = 35.526, + preferredDerp = null, + derpLatency = null, + uploadBytesSec = null, + downloadBytesSec = null, + usingCoderConnect = false + ) + + val expected = "Direct (35.53ms). You're connected peer-to-peer" + assertEquals(expected, metrics.toPretty()) + } + + @Test + fun `toPretty should round latency with more than two decimals correctly for P2P`() { + val metrics = NetworkMetrics( + p2p = true, + latency = 42.6789, + preferredDerp = null, + derpLatency = null, + uploadBytesSec = null, + downloadBytesSec = null, + usingCoderConnect = false + ) + + val expected = "Direct (42.68ms). You're connected peer-to-peer" + assertEquals(expected, metrics.toPretty()) + } + + @Test + fun `toPretty should pad latency with one decimal correctly for P2P`() { + val metrics = NetworkMetrics( + p2p = true, + latency = 12.5, + preferredDerp = null, + derpLatency = null, + uploadBytesSec = null, + downloadBytesSec = null, + usingCoderConnect = false + ) + + val expected = "Direct (12.50ms). You're connected peer-to-peer" + assertEquals(expected, metrics.toPretty()) + } + + @Test + fun `toPretty should return message for DERP relay connection`() { + val metrics = NetworkMetrics( + p2p = false, + latency = 80.0, + preferredDerp = "derp1", + derpLatency = mapOf("derp1" to 30.0), + uploadBytesSec = null, + downloadBytesSec = null, + usingCoderConnect = false + ) + + val expected = "You ↔ derp1 (30.00ms) ↔ Workspace (50.00ms). You are connected through a relay" + assertEquals(expected, metrics.toPretty()) + } + + @Test + fun `toPretty should round and pad latencies correctly for DERP`() { + val metrics = NetworkMetrics( + p2p = false, + latency = 78.1267, + preferredDerp = "derp2", + derpLatency = mapOf("derp2" to 23.5), + uploadBytesSec = null, + downloadBytesSec = null, + usingCoderConnect = false + ) + + // Total latency: 78.1267 + // DERP latency: 23.5 → formatted as 23.50 + // Workspace latency: 78.1267 - 23.5 = 54.6267 → formatted as 54.63 + + val expected = "You ↔ derp2 (23.50ms) ↔ Workspace (54.63ms). You are connected through a relay" + assertEquals(expected, metrics.toPretty()) + } +} \ No newline at end of file