Skip to content

Commit a8d4d66

Browse files
darthorimarintellij-monorepo-bot
authored andcommitted
[lsp] fix lsp server freeze when a client disconnects during channel read
- Using ktor here seems to help here - Also, introduce a multiclient mode flag to stope the server after a client disconnects (according to the LSP spec) LSP-166 GitOrigin-RevId: a739d097e02156828d68973865cacc484e38ff81
1 parent b2fb034 commit a8d4d66

File tree

7 files changed

+181
-30
lines changed

7 files changed

+181
-30
lines changed

kotlin-lsp/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ jvm_library(
5151
"@community//fleet/rhizomedb",
5252
"@community//fleet/kernel",
5353
"@community//platform/service-container",
54+
"@community//libraries/io",
55+
"@lib//:io-ktor-io",
56+
"@lib//:ktor-network-tls",
5457
]
5558
)
5659
### auto-generated section `build language-server.kotlin-lsp` end

kotlin-lsp/language-server.kotlin-lsp.iml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,7 @@
5858
<orderEntry type="module" module-name="fleet.rhizomedb" />
5959
<orderEntry type="module" module-name="fleet.kernel" />
6060
<orderEntry type="module" module-name="intellij.platform.serviceContainer" />
61+
<orderEntry type="library" name="ktor-network-tls" level="project" />
62+
<orderEntry type="library" name="kotlinx-io-core" level="project" />
6163
</component>
6264
</module>

kotlin-lsp/src/com/jetbrains/ls/kotlinLsp/KotlinLspServer.kt

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ import com.jetbrains.ls.kotlinLsp.util.addKotlinStdlib
2222
import com.jetbrains.ls.kotlinLsp.util.logSystemInfo
2323
import com.jetbrains.ls.snapshot.api.impl.core.createServerStarterAnalyzerImpl
2424
import com.jetbrains.lsp.implementation.*
25-
import kotlinx.coroutines.*
25+
import kotlinx.coroutines.CompletableDeferred
26+
import kotlinx.coroutines.Dispatchers
27+
import kotlinx.coroutines.awaitCancellation
28+
import kotlinx.coroutines.runBlocking
2629
import org.jetbrains.kotlin.idea.base.plugin.artifacts.KotlinArtifacts
2730
import org.jetbrains.kotlin.idea.compiler.configuration.KotlinPluginLayoutMode
2831
import org.jetbrains.kotlin.idea.compiler.configuration.KotlinPluginLayoutModeProvider
2932
import org.jetbrains.kotlin.idea.compiler.configuration.isRunningFromSources
30-
import java.io.InputStream
31-
import java.io.OutputStream
32-
import java.net.Socket
3333
import kotlin.io.path.absolutePathString
3434
import kotlin.io.path.createTempDirectory
3535

@@ -45,11 +45,19 @@ private class RunKotlinLspCommand : CliktCommand(name = "kotlin-lsp") {
4545
.help("Whether the Kotlin LSP server is used in client mode. If not set, server mode will be used with a port specified by `${::socket.name}`")
4646
.validate { if (it && stdio) fail("Can't use stdio mode with client mode") }
4747

48+
val multiclient: Boolean by option().flag()
49+
.help("Whether the Kotlin LSP server is used in multiclient mode. If not set, server will be shut down after the first client disconnects.`")
50+
.validate {
51+
if (it && stdio) fail("Stdio mode doesn't support multiclient mode")
52+
if (it && client) fail("Client mode doesn't support multiclient mode")
53+
}
54+
55+
4856
private fun createRunConfig(): KotlinLspServerRunConfig {
4957
val mode = when {
5058
stdio -> KotlinLspServerMode.Stdio
51-
client -> KotlinLspServerMode.Socket.Client(socket)
52-
else -> KotlinLspServerMode.Socket.Server(socket)
59+
client -> KotlinLspServerMode.Socket(TcpConnectionConfig.Client(port = socket))
60+
else -> KotlinLspServerMode.Socket(TcpConnectionConfig.Server(port = socket, isMulticlient = multiclient))
5361
}
5462
return KotlinLspServerRunConfig(mode)
5563
}
@@ -76,16 +84,17 @@ private fun run(runConfig: KotlinLspServerRunConfig) {
7684
KotlinLspServerMode.Stdio -> {
7785
val stdout = System.out
7886
System.setOut(System.err)
79-
handleRequests(System.`in`, stdout, config, mode)
87+
stdioConnection(System.`in`, stdout) { connection ->
88+
handleRequests(connection, config, mode)
89+
}
8090
}
8191

8292
is KotlinLspServerMode.Socket -> {
8393
logSystemInfo()
8494
tcpConnection(
85-
clientMode = mode is KotlinLspServerMode.Socket.Client,
86-
port = mode.port,
95+
mode.config,
8796
) { connection ->
88-
handleRequests(connection.inputStream, connection.outputStream, config, mode)
97+
handleRequests(connection, config, mode)
8998
}
9099
}
91100
}
@@ -94,11 +103,19 @@ private fun run(runConfig: KotlinLspServerRunConfig) {
94103
}
95104

96105
context(LSServerContext)
97-
private suspend fun handleRequests(input: InputStream, output: OutputStream, config: LSConfiguration, mode: KotlinLspServerMode) {
98-
withBaseProtocolFraming(input, output) { incoming, outgoing ->
106+
private suspend fun handleRequests(connection: LspConnection, config: LSConfiguration, mode: KotlinLspServerMode) {
107+
val shutdownOnExitSignal = when (mode) {
108+
is KotlinLspServerMode.Socket -> when (val tcpConfig = mode.config) {
109+
is TcpConnectionConfig.Client -> true
110+
is TcpConnectionConfig.Server -> !tcpConfig.isMulticlient
111+
}
112+
KotlinLspServerMode.Stdio -> true
113+
}
114+
val exitSignal = if (shutdownOnExitSignal) CompletableDeferred<Unit>() else null
115+
116+
withBaseProtocolFraming(connection, exitSignal) { incoming, outgoing ->
99117
withServer {
100-
val exitSignal = CompletableDeferred<Unit>()
101-
val handler = createLspHandlers(config, exitSignal, clientMode = mode is KotlinLspServerMode.Socket.Client)
118+
val handler = createLspHandlers(config, exitSignal)
102119

103120
withLsp(
104121
incoming,
@@ -108,7 +125,11 @@ private suspend fun handleRequests(input: InputStream, output: OutputStream, con
108125
Client.contextElement(lspClient)
109126
},
110127
) { lsp ->
111-
exitSignal.await()
128+
if (exitSignal != null) {
129+
exitSignal.await()
130+
} else {
131+
awaitCancellation()
132+
}
112133
}
113134
}
114135
}
@@ -164,12 +185,12 @@ fun createConfiguration(
164185
}
165186

166187
context(LSServer)
167-
fun createLspHandlers(config: LSConfiguration, exitSignal: CompletableDeferred<Unit>, clientMode: Boolean = false): LspHandlers {
188+
fun createLspHandlers(config: LSConfiguration, exitSignal: CompletableDeferred<Unit>?): LspHandlers {
168189
with(config) {
169190
return lspHandlers {
170191
initializeRequest()
171192
setTraceNotification()
172-
shutdownRequest(clientMode, exitSignal)
193+
shutdownRequest(exitSignal)
173194
fileUpdateRequests()
174195
features()
175196
}
Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
package com.jetbrains.ls.kotlinLsp
22

3+
import com.jetbrains.lsp.implementation.TcpConnectionConfig
4+
35
class KotlinLspServerRunConfig(
46
val mode: KotlinLspServerMode
57
)
68

79
sealed interface KotlinLspServerMode {
810
object Stdio : KotlinLspServerMode
911

10-
sealed interface Socket : KotlinLspServerMode {
11-
val port: Int
12-
13-
class Server(override val port: Int) : Socket
14-
class Client(override val port: Int) : Socket
15-
}
16-
}
12+
data class Socket(val config: TcpConnectionConfig) : KotlinLspServerMode
13+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
@file:Suppress("INVISIBLE_REFERENCE")
6+
/*
7+
Copied from https://github.com/ktorio/ktor/pull/4594 until ktor is not updated in monorepo
8+
*/
9+
10+
package com.jetbrains.ls.kotlinLsp.ktorHack
11+
12+
import io.ktor.utils.io.ByteWriteChannel
13+
import io.ktor.utils.io.CLOSED
14+
import io.ktor.utils.io.CloseToken
15+
import io.ktor.utils.io.InternalAPI
16+
import kotlinx.io.IOException
17+
import kotlinx.io.RawSink
18+
import kotlinx.io.Sink
19+
import kotlinx.io.asSink
20+
import kotlinx.io.buffered
21+
import java.io.OutputStream
22+
23+
/**
24+
* Creates a [io.ktor.utils.io.ByteWriteChannel] that writes to this [Sink].
25+
*
26+
* Example usage:
27+
* ```kotlin
28+
* suspend fun writeMessage(raw: RawSink) {
29+
* val channel = raw.asByteWriteChannel()
30+
* channel.writeByte(42)
31+
* channel.flushAndClose()
32+
* }
33+
*
34+
* val buffer = Buffer()
35+
* writeMessage(buffer)
36+
* buffer.readByte() // 42
37+
* ```
38+
*
39+
* Please note that the channel will be buffered even if the sink is not.
40+
*/
41+
public fun RawSink.asByteWriteChannel(): ByteWriteChannel = SinkByteWriteChannel(this)
42+
43+
@Suppress("INVISIBLE_REFERENCE")
44+
internal class SinkByteWriteChannel(origin: RawSink) : ByteWriteChannel {
45+
@Volatile
46+
var closed: CloseToken? = null
47+
private val buffer = origin.buffered()
48+
49+
override val isClosedForWrite: Boolean
50+
get() = closed != null
51+
52+
override val closedCause: Throwable?
53+
get() = closed?.cause
54+
55+
@InternalAPI
56+
override val writeBuffer: Sink
57+
get() {
58+
if (isClosedForWrite) throw closedCause ?: IOException("Channel is closed for write")
59+
return buffer
60+
}
61+
62+
@OptIn(InternalAPI::class)
63+
override suspend fun flush() {
64+
writeBuffer.flush()
65+
}
66+
67+
@OptIn(InternalAPI::class)
68+
override suspend fun flushAndClose() {
69+
writeBuffer.flush()
70+
closed = CLOSED
71+
}
72+
73+
@OptIn(InternalAPI::class)
74+
override fun cancel(cause: Throwable?) {
75+
val token = if (cause == null) CLOSED else CloseToken(cause)
76+
closed = token
77+
}
78+
}
79+
80+
/**
81+
* Converts this [OutputStream] into a [ByteWriteChannel], enabling asynchronous writing of byte sequences.
82+
*
83+
* ```kotlin
84+
* val outputStream: OutputStream = FileOutputStream("file.txt")
85+
* val channel: ByteWriteChannel = outputStream.asByteWriteChannel()
86+
* channel.writeFully("Hello, World!".toByteArray())
87+
* channel.flushAndClose() // Ensure the data is written to the OutputStream
88+
* ```
89+
*
90+
* All operations on the [ByteWriteChannel] are buffered: the underlying [OutputStream] will be receiving bytes
91+
* when the [ByteWriteChannel.flush] happens.
92+
*/
93+
public fun OutputStream.asByteWriteChannel(): ByteWriteChannel = asSink().asByteWriteChannel()

kotlin-lsp/src/com/jetbrains/ls/kotlinLsp/requests/core/shutdown.kt

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,12 @@ import com.jetbrains.lsp.protocol.Shutdown
99
import kotlinx.coroutines.CompletableDeferred
1010

1111
context(LSServer, LSConfiguration)
12-
internal fun LspHandlersBuilder.shutdownRequest(clientMode: Boolean, exitSignal: CompletableDeferred<Unit>) {
12+
internal fun LspHandlersBuilder.shutdownRequest(exitSignal: CompletableDeferred<Unit>?) {
1313
request(Shutdown) {
1414
// TODO All the requests and notifications after this one should return InvalidRequest
1515
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#shutdown
1616
}
1717
notification(ExitNotificationType) {
18-
if (clientMode) {
19-
exitSignal.complete(Unit)
20-
} else {
21-
// do nothing, keep the server running
22-
}
18+
exitSignal?.complete(Unit)
2319
}
2420
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
2+
package com.jetbrains.ls.kotlinLsp
3+
4+
import com.jetbrains.lsp.implementation.LspConnection
5+
import io.ktor.utils.io.ByteWriteChannel
6+
import com.jetbrains.ls.kotlinLsp.ktorHack.asByteWriteChannel
7+
import io.ktor.utils.io.cancel
8+
import io.ktor.utils.io.jvm.javaio.toByteReadChannel
9+
import kotlinx.coroutines.CoroutineScope
10+
import kotlinx.coroutines.coroutineScope
11+
import java.io.InputStream
12+
import java.io.OutputStream
13+
14+
suspend fun stdioConnection(
15+
inputStream: InputStream,
16+
outputStream: OutputStream,
17+
body: suspend CoroutineScope.(LspConnection) -> Unit
18+
) {
19+
coroutineScope {
20+
body(StdioConnection(inputStream, outputStream))
21+
}
22+
}
23+
24+
private class StdioConnection(
25+
inputStream: InputStream,
26+
outputStream: OutputStream,
27+
) : LspConnection {
28+
override val input = inputStream.toByteReadChannel()
29+
override val output: ByteWriteChannel = outputStream.asByteWriteChannel()
30+
31+
override fun close() {
32+
input.cancel()
33+
output.cancel(kotlinx.io.IOException("cancelled"))
34+
}
35+
36+
override fun isAlive(): Boolean {
37+
return !input.isClosedForRead || !output.isClosedForWrite
38+
}
39+
}

0 commit comments

Comments
 (0)