diff --git a/buildSrc/src/main/kotlin/CI.kt b/buildSrc/src/main/kotlin/CI.kt index 5e8995271c5..8b27caedf2d 100644 --- a/buildSrc/src/main/kotlin/CI.kt +++ b/buildSrc/src/main/kotlin/CI.kt @@ -58,16 +58,19 @@ fun Project.configureTestTasksOnCi() { */ private fun KotlinJvmTest.applyTestRetryCompatibilityWorkaround() { if (targetName == null) return + val originalTargetName = targetName val executeTestsActionIndex = taskActions.indexOfLast { it.displayName == "Execute executeTests" } check(executeTestsActionIndex != -1) { "Action executeTests not found" } // Add the workaround action and then move it to the correct position right before tests execution. - doFirst("workaround for compatibility with testRetry") { - targetName = null - } + doFirst("workaround for compatibility with testRetry") { targetName = null } val injectedAction = taskActions.removeFirst() taskActions.add(executeTestsActionIndex, injectedAction) + + // Restore targetName value as other plugins might rely on it. + // For example, kover uses it to find test tasks by target name + doLast("restore targetName") { targetName = originalTargetName } } // Docs: https://docs.gradle.com/develocity/gradle-plugin/current/#test_retry diff --git a/buildSrc/src/main/kotlin/JsConfig.kt b/buildSrc/src/main/kotlin/JsConfig.kt index 0cf581fd03c..161e05c452e 100644 --- a/buildSrc/src/main/kotlin/JsConfig.kt +++ b/buildSrc/src/main/kotlin/JsConfig.kt @@ -52,7 +52,6 @@ internal fun Project.configureJsTestTasks(target: String) { val shouldRunJsBrowserTest = !hasProperty("teamcity") || hasProperty("enable-js-tests") if (shouldRunJsBrowserTest) return - val capitalizedTarget = target.replaceFirstChar { it.titlecase() } - tasks.maybeNamed("clean${capitalizedTarget}BrowserTest") { onlyIf { false } } + tasks.maybeNamed("clean${target.capitalized()}BrowserTest") { onlyIf { false } } tasks.maybeNamed("${target}BrowserTest") { onlyIf { false } } } diff --git a/buildSrc/src/main/kotlin/Publication.kt b/buildSrc/src/main/kotlin/Publication.kt index 4020f60ed6c..07de40a7c29 100644 --- a/buildSrc/src/main/kotlin/Publication.kt +++ b/buildSrc/src/main/kotlin/Publication.kt @@ -2,72 +2,73 @@ * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ +import internal.* import org.gradle.api.* import org.gradle.api.publish.* import org.gradle.api.publish.maven.* import org.gradle.api.publish.maven.tasks.* +import org.gradle.api.publish.plugins.* import org.gradle.jvm.tasks.* import org.gradle.kotlin.dsl.* import org.gradle.plugins.signing.* import java.util.concurrent.locks.* -fun isAvailableForPublication(publication: Publication): Boolean { - val name = publication.name - if (name == "maven") return true - - var result = false - val jvmAndCommon = setOf( - "jvm", - "androidRelease", - "androidDebug", - "js", - "wasmJs", - "metadata", - "kotlinMultiplatform" - ) - result = result || name in jvmAndCommon - result = result || (HOST_NAME == "linux" && (name == "linuxX64" || name == "linuxArm64")) - result = result || (HOST_NAME == "windows" && name == "mingwX64") - val macPublications = setOf( - "iosX64", - "iosArm64", - "iosSimulatorArm64", - - "watchosX64", - "watchosArm32", - "watchosArm64", - "watchosSimulatorArm64", - "watchosDeviceArm64", - - "tvosX64", - "tvosArm64", - "tvosSimulatorArm64", - - "macosX64", - "macosArm64" - ) - - result = result || (HOST_NAME == "macos" && name in macPublications) - - // can be published from any host - val androidNativePublication = setOf( - "androidNativeArm32", - "androidNativeArm64", - "androidNativeX64", - "androidNativeX86" - ) - - result = result || name in androidNativePublication - - return result -} +private val jvmAndCommonTargets = setOf( + "jvm", + "androidRelease", + "androidDebug", + "metadata", + "kotlinMultiplatform", + "maven", +) + +private val jsTargets = setOf( + "js", + "wasmJs", +) + +private val linuxTargets = setOf( + "linuxX64", + "linuxArm64", +) + +private val windowsTargets = setOf( + "mingwX64", +) + +private val darwinTargets = setOf( + "iosX64", + "iosArm64", + "iosSimulatorArm64", + + "watchosX64", + "watchosArm32", + "watchosArm64", + "watchosSimulatorArm64", + "watchosDeviceArm64", + + "tvosX64", + "tvosArm64", + "tvosSimulatorArm64", + + "macosX64", + "macosArm64", +) + +private val androidNativeTargets = setOf( + "androidNativeArm32", + "androidNativeArm64", + "androidNativeX64", + "androidNativeX86", +) fun Project.configurePublication() { apply(plugin = "maven-publish") tasks.withType().configureEach { - onlyIf { isAvailableForPublication(publication) } + onlyIf { publication.isAvailableForPublication() } } + configureAggregatingTasks() val publishingUser: String? = System.getenv("PUBLISHING_USER") val publishingPassword: String? = System.getenv("PUBLISHING_PASSWORD") @@ -99,7 +100,7 @@ fun Project.configurePublication() { } maven { name = "testLocal" - setUrl("${rootProject.layout.buildDirectory.get().asFile}/m2") + setUrl(rootProject.layout.buildDirectory.dir("m2")) } } @@ -108,7 +109,8 @@ fun Project.configurePublication() { pom { name = project.name - description = project.description?.takeIf { it.isNotEmpty() } ?: "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort." + description = project.description.orEmpty() + .ifEmpty { "Ktor is a framework for quickly creating web applications in Kotlin with minimal effort." } url = "https://github.com/ktorio/ktor" licenses { license { @@ -147,6 +149,36 @@ fun Project.configurePublication() { configureJavadocArtifact() } +private fun Publication.isAvailableForPublication(): Boolean { + val name = name + + var result = name in jvmAndCommonTargets || name in jsTargets || name in androidNativeTargets + result = result || (HOST_NAME == "linux" && name in linuxTargets) + result = result || (HOST_NAME == "windows" && name in windowsTargets) + result = result || (HOST_NAME == "macos" && name in darwinTargets) + + return result +} + +private fun Project.configureAggregatingTasks() { + registerAggregatingTask("JvmAndCommon", jvmAndCommonTargets) + if (hasJs || hasWasmJs) registerAggregatingTask("Js", jsTargets) + if (hasLinux) registerAggregatingTask("Linux", linuxTargets) + if (hasWindows) registerAggregatingTask("Windows", windowsTargets) + if (hasDarwin) registerAggregatingTask("Darwin", darwinTargets) + if (hasAndroidNative) registerAggregatingTask("AndroidNative", androidNativeTargets) +} + +private fun Project.registerAggregatingTask(name: String, targets: Set) { + tasks.register("publish${name}Publications") { + group = PublishingPlugin.PUBLISH_TASK_GROUP + val targetsTasks = targets.mapNotNull { target -> + tasks.maybeNamed("publish${target.capitalized()}PublicationToMavenRepository") + } + dependsOn(targetsTasks) + } +} + private fun Project.configureSigning() { extra["signing.gnupg.keyName"] = (System.getenv("SIGN_KEY_ID") ?: return) extra["signing.gnupg.passphrase"] = (System.getenv("SIGN_KEY_PASSPHRASE") ?: return) diff --git a/buildSrc/src/main/kotlin/TargetsConfig.kt b/buildSrc/src/main/kotlin/TargetsConfig.kt index 269526b7aab..d8f652d5b04 100644 --- a/buildSrc/src/main/kotlin/TargetsConfig.kt +++ b/buildSrc/src/main/kotlin/TargetsConfig.kt @@ -5,13 +5,11 @@ @file:OptIn(ExperimentalKotlinGradlePluginApi::class) import org.gradle.api.* -import org.gradle.api.tasks.testing.* import org.gradle.kotlin.dsl.* import org.jetbrains.kotlin.gradle.* import org.jetbrains.kotlin.gradle.plugin.* import org.jetbrains.kotlin.gradle.plugin.mpp.* import org.jetbrains.kotlin.konan.target.* -import org.jetbrains.kotlin.gradle.tasks.* import java.io.* private val Project.files: Array get() = project.projectDir.listFiles() ?: emptyArray() @@ -27,7 +25,7 @@ val Project.hasAndroidNative: Boolean get() = hasPosix || files.any { it.name == val Project.hasWindows: Boolean get() = hasPosix || files.any { it.name == "windows" } val Project.hasJsAndWasmShared: Boolean get() = files.any { it.name == "jsAndWasmShared" } val Project.hasJs: Boolean get() = hasCommon || files.any { it.name == "js" } || hasJsAndWasmShared -val Project.hasWasm: Boolean get() = hasCommon || files.any { it.name == "wasmJs" } || hasJsAndWasmShared +val Project.hasWasmJs: Boolean get() = hasCommon || files.any { it.name == "wasmJs" } || hasJsAndWasmShared val Project.hasJvm: Boolean get() = hasCommon || hasJvmAndNix || hasJvmAndPosix || files.any { it.name == "jvm" } val Project.hasExplicitNative: Boolean @@ -42,7 +40,7 @@ fun Project.configureTargets() { if (hasJvm) configureJvm() if (hasJs) configureJs() - if (hasWasm) configureWasm() + if (hasWasmJs) configureWasm() if (hasPosix) posixTargets() if (hasNix) nixTargets() diff --git a/buildSrc/src/main/kotlin/internal/String.kt b/buildSrc/src/main/kotlin/internal/String.kt new file mode 100644 index 00000000000..8cb2abb155d --- /dev/null +++ b/buildSrc/src/main/kotlin/internal/String.kt @@ -0,0 +1,7 @@ +/* + * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package internal + +internal fun String.capitalized() = replaceFirstChar { it.uppercase() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 65db47b7bf8..6b8fb7ffdbd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ serialization = "1.7.3" ktlint = "3.15.0" kotlinx-browser = "0.2" -netty = "4.1.114.Final" +netty = "4.1.115.Final" netty-tcnative = "2.0.69.Final" jetty = "9.4.56.v20240826" diff --git a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/plugins/ServerSentEventsTest.kt b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/plugins/ServerSentEventsTest.kt index 91e0905f9d9..e74cefacb28 100644 --- a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/plugins/ServerSentEventsTest.kt +++ b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/plugins/ServerSentEventsTest.kt @@ -25,6 +25,7 @@ import kotlinx.serialization.* import kotlinx.serialization.json.* import kotlin.coroutines.* import kotlin.test.* +import kotlin.test.assertFailsWith import kotlin.time.Duration.Companion.minutes class ServerSentEventsTest : ClientLoader(2.minutes) { diff --git a/ktor-io/api/ktor-io.api b/ktor-io/api/ktor-io.api index 147672b699b..715e6bfc794 100644 --- a/ktor-io/api/ktor-io.api +++ b/ktor-io/api/ktor-io.api @@ -174,7 +174,8 @@ public abstract interface class io/ktor/utils/io/ChannelJob { } public final class io/ktor/utils/io/ConcurrentIOException : java/lang/IllegalStateException { - public fun (Ljava/lang/String;)V + public fun (Ljava/lang/String;Ljava/lang/Throwable;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V } public final class io/ktor/utils/io/CountedByteReadChannel : io/ktor/utils/io/ByteReadChannel { diff --git a/ktor-io/api/ktor-io.klib.api b/ktor-io/api/ktor-io.klib.api index b56b03ef723..2f3e0666a3f 100644 --- a/ktor-io/api/ktor-io.klib.api +++ b/ktor-io/api/ktor-io.klib.api @@ -219,7 +219,7 @@ final class io.ktor.utils.io/ByteChannel : io.ktor.utils.io/BufferedByteWriteCha } final class io.ktor.utils.io/ConcurrentIOException : kotlin/IllegalStateException { // io.ktor.utils.io/ConcurrentIOException|null[0] - constructor (kotlin/String) // io.ktor.utils.io/ConcurrentIOException.|(kotlin.String){}[0] + constructor (kotlin/String, kotlin/Throwable? = ...) // io.ktor.utils.io/ConcurrentIOException.|(kotlin.String;kotlin.Throwable?){}[0] } final class io.ktor.utils.io/CountedByteReadChannel : io.ktor.utils.io/ByteReadChannel { // io.ktor.utils.io/CountedByteReadChannel|null[0] diff --git a/ktor-io/common/src/io/ktor/utils/io/ByteChannel.kt b/ktor-io/common/src/io/ktor/utils/io/ByteChannel.kt index 42a302c178e..822cf1542b1 100644 --- a/ktor-io/common/src/io/ktor/utils/io/ByteChannel.kt +++ b/ktor-io/common/src/io/ktor/utils/io/ByteChannel.kt @@ -12,6 +12,7 @@ import kotlin.concurrent.Volatile import kotlin.coroutines.* import kotlin.jvm.* +internal expect val DEVELOPMENT_MODE: Boolean internal const val CHANNEL_MAX_SIZE: Int = 1024 * 1024 /** @@ -189,13 +190,16 @@ public class ByteChannel(public val autoFlush: Boolean = false) : ByteReadChanne // Resume the previous task when (previous) { is TaskType -> - previous.resume(ConcurrentIOException(slot.taskName())) + previous.resume(ConcurrentIOException(slot.taskName(), previous.created)) + is Slot.Task -> previous.resume() + is Slot.Closed -> { slot.resume(previous.cause) return } + Slot.Empty -> {} } @@ -219,6 +223,8 @@ public class ByteChannel(public val autoFlush: Boolean = false) : ByteReadChanne data class Closed(val cause: Throwable?) : Slot sealed interface Task : Slot { + val created: Throwable? + val continuation: Continuation fun taskName(): String @@ -231,10 +237,30 @@ public class ByteChannel(public val autoFlush: Boolean = false) : ByteReadChanne } class Read(override val continuation: Continuation) : Task { + override var created: Throwable? = null + + init { + if (DEVELOPMENT_MODE) { + created = Throwable("ReadTask 0x${continuation.hashCode().toString(16)}").also { + it.stackTraceToString() + } + } + } + override fun taskName(): String = "read" } class Write(override val continuation: Continuation) : Task { + override var created: Throwable? = null + + init { + if (DEVELOPMENT_MODE) { + created = Throwable("WriteTask 0x${continuation.hashCode().toString(16)}").also { + it.stackTraceToString() + } + } + } + override fun taskName(): String = "write" } } @@ -243,4 +269,8 @@ public class ByteChannel(public val autoFlush: Boolean = false) : ByteReadChanne /** * Thrown when a coroutine awaiting I/O is replaced by another. */ -public class ConcurrentIOException(taskName: String) : IllegalStateException("Concurrent $taskName attempts") +public class ConcurrentIOException( + taskName: String, + cause: Throwable? = null +) : IllegalStateException("Concurrent $taskName attempts", cause) { +} diff --git a/ktor-io/jvm/src/io/ktor/utils/io/ByteChannel.jvm.kt b/ktor-io/jvm/src/io/ktor/utils/io/ByteChannel.jvm.kt new file mode 100644 index 00000000000..3ae91d7c702 --- /dev/null +++ b/ktor-io/jvm/src/io/ktor/utils/io/ByteChannel.jvm.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package io.ktor.utils.io + +private const val DEVELOPMENT_MODE_KEY: String = "io.ktor.development" + +internal actual val DEVELOPMENT_MODE: Boolean + get() = System.getProperty(DEVELOPMENT_MODE_KEY)?.toBoolean() == true diff --git a/ktor-io/posix/src/io/ktor/utils/io/ByteChannel.posix.kt b/ktor-io/posix/src/io/ktor/utils/io/ByteChannel.posix.kt new file mode 100644 index 00000000000..4194ed962b5 --- /dev/null +++ b/ktor-io/posix/src/io/ktor/utils/io/ByteChannel.posix.kt @@ -0,0 +1,8 @@ +/* + * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package io.ktor.utils.io + +internal actual val DEVELOPMENT_MODE: Boolean + get() = false diff --git a/ktor-io/src/jsAndWasmSharedMain/kotlin/io/ktor/utils/io/ByteChannel.jsAndWasmShared.kt b/ktor-io/src/jsAndWasmSharedMain/kotlin/io/ktor/utils/io/ByteChannel.jsAndWasmShared.kt new file mode 100644 index 00000000000..4194ed962b5 --- /dev/null +++ b/ktor-io/src/jsAndWasmSharedMain/kotlin/io/ktor/utils/io/ByteChannel.jsAndWasmShared.kt @@ -0,0 +1,8 @@ +/* + * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package io.ktor.utils.io + +internal actual val DEVELOPMENT_MODE: Boolean + get() = false