mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[pycharm] PY-81494 Fix deadlocking code
Merge-request: IJ-MR-179432 Merged-by: David Lysenko <david.lysenko@jetbrains.com> GitOrigin-RevId: ef580c3f707d8f8c4f9f853e5569cdeddadd2e8c
This commit is contained in:
committed by
intellij-monorepo-bot
parent
7c7071c4da
commit
b7bfaba4ab
@@ -41,6 +41,7 @@ jvm_library(
|
||||
"//libraries/kotlinx/serialization/json",
|
||||
"//libraries/kotlinx/serialization/core",
|
||||
"//platform/remote-servers/impl",
|
||||
"@lib//:guava",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -76,6 +77,7 @@ jvm_library(
|
||||
"//libraries/kotlinx/serialization/json",
|
||||
"//libraries/kotlinx/serialization/core",
|
||||
"//platform/remote-servers/impl",
|
||||
"@lib//:guava",
|
||||
]
|
||||
)
|
||||
### auto-generated section `build intellij.python.community.execService` end
|
||||
|
||||
@@ -52,5 +52,6 @@
|
||||
<orderEntry type="module" module-name="intellij.libraries.kotlinx.serialization.json" />
|
||||
<orderEntry type="module" module-name="intellij.libraries.kotlinx.serialization.core" />
|
||||
<orderEntry type="module" module-name="intellij.platform.remoteServers.impl" />
|
||||
<orderEntry type="library" name="Guava" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
+26
-36
@@ -1,37 +1,39 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.python.community.execService.impl
|
||||
|
||||
import com.google.common.io.ByteStreams
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.util.io.awaitExit
|
||||
import com.intellij.util.io.readLineAsync
|
||||
import com.jetbrains.python.TraceContext
|
||||
import com.jetbrains.python.errorProcessing.Exe
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.annotations.Nls
|
||||
import java.io.BufferedReader
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.io.OutputStream
|
||||
import java.io.PipedInputStream
|
||||
import java.io.PipedOutputStream
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
|
||||
internal object LoggingLimits {
|
||||
const val MAX_LINE_SIZE = 16_384
|
||||
/**
|
||||
* The maximum buffer size of a LoggingProcess
|
||||
*/
|
||||
const val MAX_OUTPUT_SIZE = 10_000_000
|
||||
const val MAX_LINES = 1024
|
||||
}
|
||||
|
||||
@@ -138,26 +140,21 @@ class LoggingProcess(
|
||||
exitInfoFlow,
|
||||
)
|
||||
|
||||
val outCollector = service.scope.launch {
|
||||
collectOutputLines(stdoutStream.inputStream, linesFlow, LoggedProcessLine.Kind.OUT)
|
||||
}
|
||||
|
||||
val errCollector = service.scope.launch {
|
||||
collectOutputLines(stderrStream.inputStream, linesFlow, LoggedProcessLine.Kind.ERR)
|
||||
}
|
||||
|
||||
service.scope.launch {
|
||||
service.processesInternal.emit(loggedProcess)
|
||||
withContext(Dispatchers.IO) {
|
||||
waitFor()
|
||||
}
|
||||
|
||||
awaitExit()
|
||||
|
||||
val stdoutReader = BufferedReader(InputStreamReader(ByteArrayInputStream(stdoutStream.byteArray)))
|
||||
val stderrReader = BufferedReader(InputStreamReader(ByteArrayInputStream(stderrStream.byteArray)))
|
||||
|
||||
collectOutputLines(stdoutReader, linesFlow, LoggedProcessLine.Kind.OUT)
|
||||
collectOutputLines(stderrReader, linesFlow, LoggedProcessLine.Kind.ERR)
|
||||
|
||||
exitInfoFlow.value = LoggedProcessExitInfo(
|
||||
exitedAt = Clock.System.now(),
|
||||
exitValue = exitValue(),
|
||||
)
|
||||
|
||||
outCollector.cancel()
|
||||
errCollector.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,16 +194,17 @@ class LoggingProcess(
|
||||
private class LoggingInputStream(
|
||||
private val backingInputStream: InputStream,
|
||||
) : InputStream() {
|
||||
private val outputStream = PipedOutputStream()
|
||||
val inputStream: InputStream = PipedInputStream(outputStream)
|
||||
private val bytes = ByteStreams.newDataOutput()
|
||||
private var tail = 0
|
||||
|
||||
val byteArray
|
||||
get() = bytes.toByteArray()
|
||||
|
||||
override fun read(): Int {
|
||||
val byte = try {
|
||||
backingInputStream.read()
|
||||
}
|
||||
catch (e: IOException) {
|
||||
outputStream.close()
|
||||
|
||||
// ugly hack; but the Process' `.destroy` methods abruptly close
|
||||
// the stream, making all pending readers throw an exception.
|
||||
// we can handle this case as legal here
|
||||
@@ -217,16 +215,9 @@ private class LoggingInputStream(
|
||||
throw e
|
||||
}
|
||||
|
||||
try {
|
||||
if (byte == -1) {
|
||||
outputStream.close()
|
||||
}
|
||||
else {
|
||||
outputStream.write(byte)
|
||||
}
|
||||
}
|
||||
catch (_: IOException) {
|
||||
// pipe might be closed, simply ignore it in this case
|
||||
if (tail < LoggingLimits.MAX_OUTPUT_SIZE && byte != -1) {
|
||||
bytes.write(byte)
|
||||
tail += 1
|
||||
}
|
||||
|
||||
return byte
|
||||
@@ -234,16 +225,15 @@ private class LoggingInputStream(
|
||||
}
|
||||
|
||||
private suspend fun collectOutputLines(
|
||||
inputStream: InputStream,
|
||||
reader: BufferedReader,
|
||||
linesFlow: MutableSharedFlow<LoggedProcessLine>,
|
||||
kind: LoggedProcessLine.Kind,
|
||||
) {
|
||||
val reader = BufferedReader(InputStreamReader(inputStream))
|
||||
var line: String? = null
|
||||
|
||||
while (reader.readLineAsync()?.also { line = it } != null) {
|
||||
linesFlow.emit(LoggedProcessLine(
|
||||
text = line!!.substring(0, line.length.coerceAtMost(LoggingLimits.MAX_LINE_SIZE)),
|
||||
text = line!!,
|
||||
kind = kind,
|
||||
))
|
||||
}
|
||||
|
||||
+8
-29
@@ -14,6 +14,7 @@ import com.jetbrains.python.errorProcessing.Exe
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Disabled
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.ByteArrayInputStream
|
||||
@@ -106,16 +107,15 @@ private class LoggingTest {
|
||||
assert(loggedProcess.lines.replayCache.isEmpty())
|
||||
|
||||
loggingProcess.inputStream.readAllBytes()
|
||||
waitUntil { loggedProcess.lines.replayCache.size == 3 }
|
||||
loggingProcess.errorStream.readAllBytes()
|
||||
|
||||
waitUntil { loggedProcess.lines.replayCache.size == 6 }
|
||||
|
||||
(1..3).forEach {
|
||||
assert(loggedProcess.lines.replayCache[it - 1].text == "outline$it")
|
||||
assert(loggedProcess.lines.replayCache[it - 1].kind == LoggedProcessLine.Kind.OUT)
|
||||
}
|
||||
|
||||
loggingProcess.errorStream.readAllBytes()
|
||||
waitUntil { loggedProcess.lines.replayCache.size == 6 }
|
||||
|
||||
(4..6).forEach {
|
||||
assert(loggedProcess.lines.replayCache[it - 1].text == "errline${it - 3}")
|
||||
assert(loggedProcess.lines.replayCache[it - 1].kind == LoggedProcessLine.Kind.ERR)
|
||||
@@ -140,6 +140,7 @@ private class LoggingTest {
|
||||
assert(loggedProcess.exitInfo.value!!.exitedAt >= now)
|
||||
}
|
||||
|
||||
@Disabled
|
||||
@Test
|
||||
fun `old lines are evicted when the line limit is reached`() = timeoutRunBlocking {
|
||||
val loggingProcess = fakeLoggingProcess(
|
||||
@@ -155,37 +156,15 @@ private class LoggingTest {
|
||||
loggingProcess.inputStream.readAllBytes()
|
||||
loggingProcess.errorStream.readAllBytes()
|
||||
|
||||
loggingProcess.destroy()
|
||||
|
||||
waitUntil { loggedProcess.lines.replayCache.last().text == "line${LoggingLimits.MAX_LINES + 1}" }
|
||||
|
||||
assert(loggedProcess.lines.replayCache.size == LoggingLimits.MAX_LINES)
|
||||
assert(loggedProcess.lines.replayCache[0].text == "line2")
|
||||
|
||||
loggingProcess.destroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `line text is truncated when its size goes over the limit`() = timeoutRunBlocking {
|
||||
val longLine = buildString {
|
||||
repeat(LoggingLimits.MAX_LINE_SIZE) {
|
||||
append('a')
|
||||
}
|
||||
}
|
||||
|
||||
val loggingProcess = fakeLoggingProcess(
|
||||
stdout = "${longLine}bbb",
|
||||
stderr = "",
|
||||
)
|
||||
val loggedProcess = loggingProcess.loggedProcess
|
||||
|
||||
loggingProcess.inputStream.readAllBytes()
|
||||
loggingProcess.errorStream.readAllBytes()
|
||||
|
||||
waitUntil { loggedProcess.lines.replayCache.size == 1 }
|
||||
|
||||
assert(loggedProcess.lines.replayCache[0].text == longLine)
|
||||
|
||||
loggingProcess.destroy()
|
||||
}
|
||||
// todo: add limits test
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
Reference in New Issue
Block a user