From 12f38a1d9ce1186b96e2c9ec490bfba0066a2c7d Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Fri, 21 Feb 2025 13:09:29 -0800 Subject: [PATCH 1/8] Control automatic port forwarding with a devcontainer.json file devcontainer.json has a system for specifying default behavior for forwarding ports, and also behavior for specific ports and ranges of ports. Its schema is essentially identical to the settings VS Code uses to control port forwarding, so supporting this format for port settings keeps things consistent between VS Code and JetBrains. See https://containers.dev/implementors/json_reference/ for the spec. As an example, this will turn off automatic port forwarding except for ports 7123 and 8100-8150: { "otherPortsAttributes": { "onAutoForward": "ignore" }, "portsAttributes": { "7123": { "onAutoForward": "notify" }, "8100-8150": { "onAutoForward": "notify" } } } Fixes: #38 --- build.gradle.kts | 1 + .../coder/jetbrains/matcher/PortMatcher.kt | 50 ++++++++++++++++++ .../services/CoderPortForwardService.kt | 52 +++++++++++++++++-- .../jetbrains/matcher/PortMatcherTest.kt | 43 +++++++++++++++ 4 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/com/coder/jetbrains/matcher/PortMatcher.kt create mode 100644 src/test/kotlin/com/coder/jetbrains/matcher/PortMatcherTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 7cc0417..b19191c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,6 +24,7 @@ repositories { // Dependencies are managed with Gradle version catalog - read more: https://docs.gradle.org/current/userguide/platforms.html#sub:version-catalog dependencies { // implementation(libs.annotations) + implementation("org.json:json:20210307") } // Set the JVM language level used to build the project. diff --git a/src/main/kotlin/com/coder/jetbrains/matcher/PortMatcher.kt b/src/main/kotlin/com/coder/jetbrains/matcher/PortMatcher.kt new file mode 100644 index 0000000..8d3f093 --- /dev/null +++ b/src/main/kotlin/com/coder/jetbrains/matcher/PortMatcher.kt @@ -0,0 +1,50 @@ +package com.coder.jetbrains.matcher + +class PortMatcher(private val rule: String) { + private sealed class MatchRule { + data class SinglePort(val port: Int) : MatchRule() + data class PortRange(val start: Int, val end: Int) : MatchRule() + data class RegexPort(val pattern: Regex) : MatchRule() + } + + private val parsedRule: MatchRule + + init { + parsedRule = parseRule(rule) + } + + fun matches(port: Int): Boolean { + return when (parsedRule) { + is MatchRule.SinglePort -> port == parsedRule.port + is MatchRule.PortRange -> port in parsedRule.start..parsedRule.end + is MatchRule.RegexPort -> parsedRule.pattern.matches(port.toString()) + } + } + + private fun parseRule(rule: String): MatchRule { + // Remove host part if present (e.g., "localhost:3000" -> "3000") + val portPart = rule.substringAfter(':').takeIf { ':' in rule } ?: rule + + return when { + // Try parsing as single port + portPart.all { it.isDigit() } -> { + MatchRule.SinglePort(portPart.toInt()) + } + // Try parsing as port range (e.g., "40000-55000") + portPart.matches("^\\d+-\\d+$".toRegex()) -> { + val (start, end) = portPart.split('-') + .map { it.trim().toInt() } + require(start <= end) { "Invalid port range: start must be less than or equal to end" } + MatchRule.PortRange(start, end) + } + // If not a single port or range, treat as regex + else -> { + try { + MatchRule.RegexPort(portPart.toRegex()) + } catch (e: Exception) { + throw IllegalArgumentException("Invalid port rule format: $rule") + } + } + } + } +} diff --git a/src/main/kotlin/com/coder/jetbrains/services/CoderPortForwardService.kt b/src/main/kotlin/com/coder/jetbrains/services/CoderPortForwardService.kt index 84ed196..21a7ffc 100644 --- a/src/main/kotlin/com/coder/jetbrains/services/CoderPortForwardService.kt +++ b/src/main/kotlin/com/coder/jetbrains/services/CoderPortForwardService.kt @@ -1,5 +1,6 @@ package com.coder.jetbrains.services +import com.coder.jetbrains.matcher.PortMatcher import com.coder.jetbrains.scanner.listeningPorts import com.intellij.openapi.Disposable import com.intellij.openapi.components.serviceOrNull @@ -15,6 +16,8 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.io.File +import org.json.JSONObject /** * Automatically forward ports that have something listening on them by scanning @@ -29,15 +32,20 @@ class CoderPortForwardService( private val logger = thisLogger() private var poller: Job? = null - // TODO: Make customizable. + private data class PortRule( + val matcher: PortMatcher, + val autoForward: Boolean + ) // TODO: I also see 63342, 57675, and 56830 for JetBrains. Are they static? // TODO: If you have multiple IDEs, you will see 5991. 5992, etc. Can we // detect all of these and exclude them? - private val ignoreList = setOf( - 22, // SSH - 5990, // JetBrains Gateway port. + private val rules = mutableListOf( + PortRule(PortMatcher("22"), false), + PortRule(PortMatcher("5990"), false), ) + private var defaultForward = true + init { logger.info("initializing port forwarding service") start() @@ -48,12 +56,46 @@ class CoderPortForwardService( } private fun start() { + // TODO: make path configurable? + val devcontainerFile = File(System.getProperty("user.home"), ".cache/JetBrains/devcontainer.json") + if (devcontainerFile.exists()) { + try { + val json = devcontainerFile.readText() + val obj = JSONObject(json) + + val portsAttributes = obj.optJSONObject("portsAttributes") ?: JSONObject() + portsAttributes.keys().forEach { spec -> + portsAttributes.optJSONObject(spec)?.let { attrs -> + val onAutoForward = attrs.optString("onAutoForward") + if (onAutoForward == "ignore") { + logger.info("found ignored port specification $spec in devcontainer.json") + rules.add(0, PortRule(PortMatcher(spec), false)) + } else if (onAutoForward != "") { + logger.info("found auto-forward port specification $spec in devcontainer.json") + rules.add(0, PortRule(PortMatcher(spec), true)) + } + } + } + + val otherPortsAttributes = obj.optJSONObject("otherPortsAttributes") ?: JSONObject() + if (otherPortsAttributes.optString("onAutoForward") == "ignore") { + logger.info("found ignored setting for otherPortsAttributes in devcontainer.json") + defaultForward = false + } + } catch (e: Exception) { + logger.warn("Failed to parse devcontainer.json", e) + } + } + logger.info("starting port scanner") poller = cs.launch { while (isActive) { logger.debug("scanning for ports") val listeningPorts = withContext(Dispatchers.IO) { - listeningPorts().subtract(ignoreList) + listeningPorts().filter { port -> + val matchedRule = rules.firstOrNull { it.matcher.matches(port) } + matchedRule?.autoForward ?: defaultForward + }.toSet() } application.invokeLater { val manager = serviceOrNull() diff --git a/src/test/kotlin/com/coder/jetbrains/matcher/PortMatcherTest.kt b/src/test/kotlin/com/coder/jetbrains/matcher/PortMatcherTest.kt new file mode 100644 index 0000000..a1ad62c --- /dev/null +++ b/src/test/kotlin/com/coder/jetbrains/matcher/PortMatcherTest.kt @@ -0,0 +1,43 @@ +package com.coder.jetbrains.matcher + +import org.junit.Test +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue + +class PortMatcherTest { + + @Test + fun `test single port`() { + val matcher = PortMatcher("3000") + assertTrue(matcher.matches(3000)) + assertFalse(matcher.matches(2999)) + assertFalse(matcher.matches(3001)) + } + + @Test + fun `test host colon port`() { + val matcher = PortMatcher("localhost:3000") + assertTrue(matcher.matches(3000)) + assertFalse(matcher.matches(3001)) + } + + @Test + fun `test port range`() { + val matcher = PortMatcher("40000-55000") + assertFalse(matcher.matches(39999)) + assertTrue(matcher.matches(40000)) + assertTrue(matcher.matches(50000)) + assertTrue(matcher.matches(55000)) + assertFalse(matcher.matches(55001)) + } + + @Test + fun `test regex`() { + val matcher = PortMatcher("800[1-9]") + assertFalse(matcher.matches(8000)) + assertTrue(matcher.matches(8001)) + assertTrue(matcher.matches(8005)) + assertTrue(matcher.matches(8009)) + assertFalse(matcher.matches(8010)) + } +} From ae1df2f2eaf898fba7d011488b8cc37c0053f82f Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Tue, 25 Feb 2025 13:51:03 -0800 Subject: [PATCH 2/8] Validate single port or range endpoint is between 0 and 65535 --- .../coder/jetbrains/matcher/PortMatcher.kt | 14 ++++++++++++- .../jetbrains/matcher/PortMatcherTest.kt | 21 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/coder/jetbrains/matcher/PortMatcher.kt b/src/main/kotlin/com/coder/jetbrains/matcher/PortMatcher.kt index 8d3f093..fb571fd 100644 --- a/src/main/kotlin/com/coder/jetbrains/matcher/PortMatcher.kt +++ b/src/main/kotlin/com/coder/jetbrains/matcher/PortMatcher.kt @@ -28,12 +28,16 @@ class PortMatcher(private val rule: String) { return when { // Try parsing as single port portPart.all { it.isDigit() } -> { - MatchRule.SinglePort(portPart.toInt()) + val port = portPart.toInt() + validatePort(port) + MatchRule.SinglePort(port) } // Try parsing as port range (e.g., "40000-55000") portPart.matches("^\\d+-\\d+$".toRegex()) -> { val (start, end) = portPart.split('-') .map { it.trim().toInt() } + validatePort(start) + validatePort(end) require(start <= end) { "Invalid port range: start must be less than or equal to end" } MatchRule.PortRange(start, end) } @@ -47,4 +51,12 @@ class PortMatcher(private val rule: String) { } } } + + private fun validatePort(port: Int) { + require(port in 0..65535) { "Port number must be between 0 and 65535, got: $port" } + } + + companion object { + const val MAX_PORT = 65535 + } } diff --git a/src/test/kotlin/com/coder/jetbrains/matcher/PortMatcherTest.kt b/src/test/kotlin/com/coder/jetbrains/matcher/PortMatcherTest.kt index a1ad62c..c4d7575 100644 --- a/src/test/kotlin/com/coder/jetbrains/matcher/PortMatcherTest.kt +++ b/src/test/kotlin/com/coder/jetbrains/matcher/PortMatcherTest.kt @@ -3,6 +3,7 @@ package com.coder.jetbrains.matcher import org.junit.Test import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue +import org.junit.Assert.assertThrows class PortMatcherTest { @@ -40,4 +41,24 @@ class PortMatcherTest { assertTrue(matcher.matches(8009)) assertFalse(matcher.matches(8010)) } + + @Test + fun `test invalid port numbers`() { + assertThrows(IllegalArgumentException::class.java) { PortMatcher("65536") } + assertThrows(IllegalArgumentException::class.java) { PortMatcher("0-65536") } + assertThrows(IllegalArgumentException::class.java) { PortMatcher("70000") } + } + + @Test + fun `test edge case port numbers`() { + // These should work + PortMatcher("0") + PortMatcher("65535") + PortMatcher("0-65535") + + // These combinations should work + val matcher = PortMatcher("0-65535") + assertTrue(matcher.matches(0)) + assertTrue(matcher.matches(65535)) + } } From 933e1d4711e4e0049f9942af679c6fc764500849 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Tue, 25 Feb 2025 13:56:18 -0800 Subject: [PATCH 3/8] Split devcontainer.json file path out to a settings module --- .../jetbrains/services/CoderPortForwardService.kt | 4 ++-- .../coder/jetbrains/settings/CoderBackendSettings.kt | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/com/coder/jetbrains/settings/CoderBackendSettings.kt diff --git a/src/main/kotlin/com/coder/jetbrains/services/CoderPortForwardService.kt b/src/main/kotlin/com/coder/jetbrains/services/CoderPortForwardService.kt index 21a7ffc..4a607ce 100644 --- a/src/main/kotlin/com/coder/jetbrains/services/CoderPortForwardService.kt +++ b/src/main/kotlin/com/coder/jetbrains/services/CoderPortForwardService.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File import org.json.JSONObject +import com.coder.jetbrains.settings.CoderBackendSettings /** * Automatically forward ports that have something listening on them by scanning @@ -56,8 +57,7 @@ class CoderPortForwardService( } private fun start() { - // TODO: make path configurable? - val devcontainerFile = File(System.getProperty("user.home"), ".cache/JetBrains/devcontainer.json") + val devcontainerFile = CoderBackendSettings.getDevcontainerFile() if (devcontainerFile.exists()) { try { val json = devcontainerFile.readText() diff --git a/src/main/kotlin/com/coder/jetbrains/settings/CoderBackendSettings.kt b/src/main/kotlin/com/coder/jetbrains/settings/CoderBackendSettings.kt new file mode 100644 index 0000000..dd8e015 --- /dev/null +++ b/src/main/kotlin/com/coder/jetbrains/settings/CoderBackendSettings.kt @@ -0,0 +1,10 @@ +package com.coder.jetbrains.settings + +import java.io.File + +object CoderBackendSettings { + fun getDevcontainerFile(): File { + // TODO: make path configurable? + return File(System.getProperty("user.home"), ".cache/JetBrains/devcontainer.json") + } +} From cf25cbe9a9c3c8a6ca56d2f571851ccc9c69a8cf Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Tue, 25 Feb 2025 14:01:50 -0800 Subject: [PATCH 4/8] Add validation that onAutoForward is a string --- .../services/CoderPortForwardService.kt | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/com/coder/jetbrains/services/CoderPortForwardService.kt b/src/main/kotlin/com/coder/jetbrains/services/CoderPortForwardService.kt index 4a607ce..adc002e 100644 --- a/src/main/kotlin/com/coder/jetbrains/services/CoderPortForwardService.kt +++ b/src/main/kotlin/com/coder/jetbrains/services/CoderPortForwardService.kt @@ -56,6 +56,8 @@ class CoderPortForwardService( poller?.cancel() } + class InvalidJsonTypeException(message: String) : Exception(message) + private fun start() { val devcontainerFile = CoderBackendSettings.getDevcontainerFile() if (devcontainerFile.exists()) { @@ -66,11 +68,15 @@ class CoderPortForwardService( val portsAttributes = obj.optJSONObject("portsAttributes") ?: JSONObject() portsAttributes.keys().forEach { spec -> portsAttributes.optJSONObject(spec)?.let { attrs -> - val onAutoForward = attrs.optString("onAutoForward") - if (onAutoForward == "ignore") { + val onAutoForward = attrs.opt("onAutoForward") + if (!isValidString(onAutoForward)) { + throw InvalidJsonTypeException("onAutoForward for port $spec is not a string value") + } + val onAutoForwardStr = onAutoForward as String + if (onAutoForwardStr == "ignore") { logger.info("found ignored port specification $spec in devcontainer.json") rules.add(0, PortRule(PortMatcher(spec), false)) - } else if (onAutoForward != "") { + } else if (onAutoForwardStr != "") { logger.info("found auto-forward port specification $spec in devcontainer.json") rules.add(0, PortRule(PortMatcher(spec), true)) } @@ -78,7 +84,11 @@ class CoderPortForwardService( } val otherPortsAttributes = obj.optJSONObject("otherPortsAttributes") ?: JSONObject() - if (otherPortsAttributes.optString("onAutoForward") == "ignore") { + val otherPortsAutoForward = otherPortsAttributes.opt("onAutoForward") + if (!isValidString(otherPortsAutoForward)) { + throw InvalidJsonTypeException("otherPortsAttributes.onAutoForward is not a string value") + } + if ((otherPortsAutoForward as String) == "ignore") { logger.info("found ignored setting for otherPortsAttributes in devcontainer.json") defaultForward = false } @@ -141,4 +151,8 @@ class CoderPortForwardService( } } } + + private fun isValidString(value: Any?): Boolean { + return value != null && value is String + } } From 69432d7b318f26ddf85b05358b5e1270b304a96d Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Tue, 25 Feb 2025 16:00:32 -0800 Subject: [PATCH 5/8] PortMatcher code review feedback --- .../com/coder/jetbrains/matcher/PortMatcher.kt | 18 +++++++----------- .../coder/jetbrains/matcher/PortMatcherTest.kt | 7 +++++++ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/com/coder/jetbrains/matcher/PortMatcher.kt b/src/main/kotlin/com/coder/jetbrains/matcher/PortMatcher.kt index fb571fd..99ffe7c 100644 --- a/src/main/kotlin/com/coder/jetbrains/matcher/PortMatcher.kt +++ b/src/main/kotlin/com/coder/jetbrains/matcher/PortMatcher.kt @@ -7,11 +7,7 @@ class PortMatcher(private val rule: String) { data class RegexPort(val pattern: Regex) : MatchRule() } - private val parsedRule: MatchRule - - init { - parsedRule = parseRule(rule) - } + private val parsedRule: MatchRule = parseRule(rule) fun matches(port: Int): Boolean { return when (parsedRule) { @@ -33,12 +29,10 @@ class PortMatcher(private val rule: String) { MatchRule.SinglePort(port) } // Try parsing as port range (e.g., "40000-55000") - portPart.matches("^\\d+-\\d+$".toRegex()) -> { + portPart.matches("^\\d+\\s*-\\s*\\d+$".toRegex()) -> { val (start, end) = portPart.split('-') .map { it.trim().toInt() } - validatePort(start) - validatePort(end) - require(start <= end) { "Invalid port range: start must be less than or equal to end" } + validatePortRange(start, end) MatchRule.PortRange(start, end) } // If not a single port or range, treat as regex @@ -56,7 +50,9 @@ class PortMatcher(private val rule: String) { require(port in 0..65535) { "Port number must be between 0 and 65535, got: $port" } } - companion object { - const val MAX_PORT = 65535 + private fun validatePortRange(start: Int, end: Int) { + validatePort(start) + validatePort(end) + require(start <= end) { "Invalid port range: start must be less than or equal to end" } } } diff --git a/src/test/kotlin/com/coder/jetbrains/matcher/PortMatcherTest.kt b/src/test/kotlin/com/coder/jetbrains/matcher/PortMatcherTest.kt index c4d7575..6134c03 100644 --- a/src/test/kotlin/com/coder/jetbrains/matcher/PortMatcherTest.kt +++ b/src/test/kotlin/com/coder/jetbrains/matcher/PortMatcherTest.kt @@ -32,6 +32,13 @@ class PortMatcherTest { assertFalse(matcher.matches(55001)) } + @Test + fun `test port range with whitespace`() { + val matcher = PortMatcher("20021 - 20024") + assertFalse(matcher.matches(20000)) + assertTrue(matcher.matches(20022)) + } + @Test fun `test regex`() { val matcher = PortMatcher("800[1-9]") From 3433d28c2ccf9c0e4e86d6fa6764731dab3fccf0 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Tue, 25 Feb 2025 16:05:03 -0800 Subject: [PATCH 6/8] Use kotlinx.serialization for JSON deserialization --- build.gradle.kts | 3 +- .../services/CoderPortForwardService.kt | 51 +++++++++---------- .../jetbrains/services/DevContainerConfig.kt | 24 +++++++++ 3 files changed, 51 insertions(+), 27 deletions(-) create mode 100644 src/main/kotlin/com/coder/jetbrains/services/DevContainerConfig.kt diff --git a/build.gradle.kts b/build.gradle.kts index b19191c..6d40774 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,6 +11,7 @@ plugins { alias(libs.plugins.changelog) // Gradle Changelog Plugin alias(libs.plugins.qodana) // Gradle Qodana Plugin alias(libs.plugins.kover) // Gradle Kover Plugin + kotlin("plugin.serialization") version "2.1.10" } group = properties("pluginGroup").get() @@ -24,7 +25,7 @@ repositories { // Dependencies are managed with Gradle version catalog - read more: https://docs.gradle.org/current/userguide/platforms.html#sub:version-catalog dependencies { // implementation(libs.annotations) - implementation("org.json:json:20210307") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0") } // Set the JVM language level used to build the project. diff --git a/src/main/kotlin/com/coder/jetbrains/services/CoderPortForwardService.kt b/src/main/kotlin/com/coder/jetbrains/services/CoderPortForwardService.kt index adc002e..8a1e376 100644 --- a/src/main/kotlin/com/coder/jetbrains/services/CoderPortForwardService.kt +++ b/src/main/kotlin/com/coder/jetbrains/services/CoderPortForwardService.kt @@ -17,7 +17,8 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File -import org.json.JSONObject +import kotlinx.serialization.json.Json +import kotlinx.serialization.ExperimentalSerializationApi import com.coder.jetbrains.settings.CoderBackendSettings /** @@ -56,41 +57,43 @@ class CoderPortForwardService( poller?.cancel() } - class InvalidJsonTypeException(message: String) : Exception(message) + companion object { + @OptIn(ExperimentalSerializationApi::class) + private val json = Json { + ignoreUnknownKeys = true + allowTrailingComma = true + allowComments = true + } + } private fun start() { val devcontainerFile = CoderBackendSettings.getDevcontainerFile() if (devcontainerFile.exists()) { try { - val json = devcontainerFile.readText() - val obj = JSONObject(json) + val jsonContent = devcontainerFile.readText() + val config = json.decodeFromString(jsonContent) - val portsAttributes = obj.optJSONObject("portsAttributes") ?: JSONObject() - portsAttributes.keys().forEach { spec -> - portsAttributes.optJSONObject(spec)?.let { attrs -> - val onAutoForward = attrs.opt("onAutoForward") - if (!isValidString(onAutoForward)) { - throw InvalidJsonTypeException("onAutoForward for port $spec is not a string value") - } - val onAutoForwardStr = onAutoForward as String - if (onAutoForwardStr == "ignore") { + // Process port attributes + config.portsAttributes.forEach { (spec, attrs) -> + when (attrs.onAutoForward) { + "ignore" -> { logger.info("found ignored port specification $spec in devcontainer.json") rules.add(0, PortRule(PortMatcher(spec), false)) - } else if (onAutoForwardStr != "") { + } + "" -> {} + else -> { logger.info("found auto-forward port specification $spec in devcontainer.json") rules.add(0, PortRule(PortMatcher(spec), true)) } } } - val otherPortsAttributes = obj.optJSONObject("otherPortsAttributes") ?: JSONObject() - val otherPortsAutoForward = otherPortsAttributes.opt("onAutoForward") - if (!isValidString(otherPortsAutoForward)) { - throw InvalidJsonTypeException("otherPortsAttributes.onAutoForward is not a string value") - } - if ((otherPortsAutoForward as String) == "ignore") { - logger.info("found ignored setting for otherPortsAttributes in devcontainer.json") - defaultForward = false + // Process other ports attributes + config.otherPortsAttributes?.let { + if (it.onAutoForward == "ignore") { + logger.info("found ignored setting for otherPortsAttributes in devcontainer.json") + defaultForward = false + } } } catch (e: Exception) { logger.warn("Failed to parse devcontainer.json", e) @@ -151,8 +154,4 @@ class CoderPortForwardService( } } } - - private fun isValidString(value: Any?): Boolean { - return value != null && value is String - } } diff --git a/src/main/kotlin/com/coder/jetbrains/services/DevContainerConfig.kt b/src/main/kotlin/com/coder/jetbrains/services/DevContainerConfig.kt new file mode 100644 index 0000000..a187572 --- /dev/null +++ b/src/main/kotlin/com/coder/jetbrains/services/DevContainerConfig.kt @@ -0,0 +1,24 @@ +package com.coder.jetbrains.services + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class DevContainerConfig( + @SerialName("portsAttributes") + val portsAttributes: Map = mapOf(), + @SerialName("otherPortsAttributes") + val otherPortsAttributes: OtherPortsAttributes? = null +) + +@Serializable +data class PortAttributes( + @SerialName("onAutoForward") + val onAutoForward: String = "" +) + +@Serializable +data class OtherPortsAttributes( + @SerialName("onAutoForward") + val onAutoForward: String = "" +) From 2da09089621828cfb6a5d47fee6ad6c516ceac96 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Wed, 26 Feb 2025 07:48:08 -0800 Subject: [PATCH 7/8] Add comment about switching kotlinx-serialization-json to compileOnly --- build.gradle.kts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 6d40774..e9ad197 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,6 +25,11 @@ repositories { // Dependencies are managed with Gradle version catalog - read more: https://docs.gradle.org/current/userguide/platforms.html#sub:version-catalog dependencies { // implementation(libs.annotations) + // We need kotlinx-serialization-json >= 1.7.0 to support allowComments, as + // devcontainer.json files often have comments and therefore use + // nonstandard JSON syntax. This dependency should be marked compileOnly + // once the minimum IDE version we support (pluginSinceBuild in + // gradle.properties) is at least 251, which includes version 1.7.2. implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0") } From fa0df632acc8b5741981ada8f064f49b7408663d Mon Sep 17 00:00:00 2001 From: Benjamin Date: Thu, 27 Feb 2025 14:20:57 -0600 Subject: [PATCH 8/8] changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 482b7fd..2790a67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ ## [Unreleased] +### Added + +- Allow specifying port forwarding behavior in devcontainer.json + ## [0.0.4] - 2025-02-21 ### Added