[terminal] IJPL-176473 fix race when cancelling content updates might leave the last output pending/unapplied

The problem is that `TerminalOutputChangesTracker` state is already updated, so `collectChangedOutputOrNull` sees the updated state, and therefore the pending/unapplied output is lost.

`BlockTerminalCommandExecutionTest.commands are executed in order` is fixed now. Previously, it was flacky and the failure could be reproduced when running with "Repeat: Until failure".

GitOrigin-RevId: 2f2431f930e15d5ef54ef62f1b9b13b90df72ae9
This commit is contained in:
Sergey Simonchik
2025-02-01 12:16:43 +00:00
committed by intellij-monorepo-bot
parent b84fe0eece
commit 2d341e0ccc
4 changed files with 25 additions and 10 deletions
@@ -63,6 +63,10 @@ internal class TerminalOutputChangesTracker(
private val changeListeners: MutableList<() -> Unit> = CopyOnWriteArrayList()
@Volatile
var pendingOutput: PartialCommandOutput? = null
private set
init {
val listener = object : TextBufferChangesListener {
override fun linesChanged(fromIndex: Int) = textBuffer.withLock {
@@ -175,7 +179,9 @@ internal class TerminalOutputChangesTracker(
isAnyLineChanged = false
isChangesDiscarded = false
return PartialCommandOutput(output.text, output.styleRanges, logicalLineIndex, textBuffer.width, anyDiscarded)
return PartialCommandOutput(output.text, output.styleRanges, logicalLineIndex, textBuffer.width, anyDiscarded).also {
pendingOutput = it
}
}
/**
@@ -190,4 +196,8 @@ internal class TerminalOutputChangesTracker(
}
return count
}
internal fun onOutputApplied() {
pendingOutput = null
}
}
@@ -56,6 +56,7 @@ internal class TerminalOutputContentUpdatesScheduler(
val partialChange = tracker.collectChangedOutputOrWait()
scheduleChangeApplying(partialChange).join()
tracker.onOutputApplied()
}
}
@@ -73,13 +74,16 @@ internal class TerminalOutputContentUpdatesScheduler(
}
}
fun finishUpdating(): PartialCommandOutput? = textBuffer.withLock {
fun finishUpdating(): List<PartialCommandOutput> = textBuffer.withLock {
val tracker = changesTracker ?: error("Finish updating called before start updating")
changesTracker = null
updatingJob?.cancel()
finished = true
tracker.collectChangedOutputOrNull()
// Not-null `tracker.pendingOutput` means that it was either not applied due to
// cancellation or is being applied right now on EDT.
// If the latter, it won't hurt to apply it twice.
return listOfNotNull(tracker.pendingOutput, tracker.collectChangedOutputOrNull())
}
private val metricTextInBufferToTextVisible = ActionCoordinator<Unit, TimeMark>(
@@ -145,7 +145,7 @@ internal class TerminalOutputController(
private fun scheduleLastOutputUpdate() {
val contentUpdatesScheduler = runningCommandInteractivity?.contentUpdatesScheduler
val lastOutput: PartialCommandOutput? = if (contentUpdatesScheduler?.finished == false) {
val lastOutput: List<PartialCommandOutput> = if (contentUpdatesScheduler?.finished == false) {
contentUpdatesScheduler.finishUpdating()
}
else {
@@ -155,18 +155,19 @@ internal class TerminalOutputController(
val (output, terminalWidth) = session.model.withContentLock {
ShellCommandOutputScraperImpl.scrapeOutput(session) to session.model.width
}
PartialCommandOutput(
listOf(PartialCommandOutput(
output.text,
output.styleRanges,
logicalLineIndex = 0,
terminalWidth,
isChangesDiscarded = false,
)
))
}
if (lastOutput != null) {
if (lastOutput.isNotEmpty()) {
invokeLater(editor.getDisposed(), ModalityState.any()) {
updateCommandOutput(lastOutput)
for (output in lastOutput) {
updateCommandOutput(output)
}
}
}
}
@@ -64,7 +64,7 @@ internal class BlockTerminalCommandExecutionTest(private val shellPath: Path) {
expected.forEach {
session.commandExecutionManager.sendCommandToExecute(it.command)
}
awaitBlocksFinalized(view.outputView.controller.outputModel, count)
awaitBlocksFinalized(view.outputView.controller.outputModel, count, 60.seconds)
val actual = view.outputView.controller.outputModel.collectCommandResults()
Assert.assertEquals(expected, actual)
}