[terminal console] WEB-75932 convert LF to CRLF correctly for processes without PTY

GitOrigin-RevId: 0bda77d5c4686ef70967fb35cbc2053849b3df9c
This commit is contained in:
Sergey Simonchik
2025-12-22 23:08:21 +00:00
committed by intellij-monorepo-bot
parent ad2307b5df
commit 38d2b04dfb
3 changed files with 41 additions and 25 deletions
@@ -268,12 +268,12 @@ public class TerminalExecutionConsole implements ConsoleView, ObservableConsoleV
myTerminalWidget.createTerminalSession(ttyConnector);
myTerminalWidget.start();
if (attachToProcessOutput) {
boolean convertLfToCrlf = shouldConvertLfToCrlf(processHandler);
boolean isProcessWithPty = isProcessWithPty(processHandler);
if (processHandler instanceof ColoredProcessHandler coloredProcessHandler) {
coloredProcessHandler.addRawTextListener(new ColoredProcessHandler.RawTextListener() {
@Override
public void onRawTextAvailable(@NotNull String text, @NotNull Key<?> outputType) {
processProcessOutputText(text, outputType, convertLfToCrlf);
processProcessOutputText(text, outputType, isProcessWithPty);
}
});
}
@@ -281,7 +281,7 @@ public class TerminalExecutionConsole implements ConsoleView, ObservableConsoleV
processHandler.addProcessListener(new ProcessListener() {
@Override
public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType) {
processProcessOutputText(event.getText(), outputType, convertLfToCrlf);
processProcessOutputText(event.getText(), outputType, isProcessWithPty);
}
});
}
@@ -297,7 +297,7 @@ public class TerminalExecutionConsole implements ConsoleView, ObservableConsoleV
});
}
private void processProcessOutputText(@NotNull String text, @NotNull Key<?> outputType, boolean convertLfToCrlf) {
private void processProcessOutputText(@NotNull String text, @NotNull Key<?> outputType, boolean isProcessWithPty) {
try {
ConsoleViewContentType contentType = null;
if (outputType != ProcessOutputTypes.STDOUT) {
@@ -306,7 +306,7 @@ public class TerminalExecutionConsole implements ConsoleView, ObservableConsoleV
if (outputType == ProcessOutputTypes.SYSTEM) {
text = StringUtil.convertLineSeparators(text, LineSeparator.CRLF.getSeparatorString());
}
else if (convertLfToCrlf) {
else if (!isProcessWithPty && myConvertLfToCrlfForNonPtyProcess) {
text = convertTextToCRLF(text);
}
ConsoleViewContentType notNullContentType = ObjectUtils.notNull(contentType, ConsoleViewContentType.NORMAL_OUTPUT);
@@ -327,16 +327,13 @@ public class TerminalExecutionConsole implements ConsoleView, ObservableConsoleV
}
}
private boolean shouldConvertLfToCrlf(@NotNull ProcessHandler processHandler) {
return myConvertLfToCrlfForNonPtyProcess && isNonPtyProcess(processHandler);
}
private static boolean isNonPtyProcess(@NotNull ProcessHandler processHandler) {
if (processHandler instanceof BaseProcessHandler) {
Process process = ((BaseProcessHandler<?>)processHandler).getProcess();
return !(process instanceof PtyProcess);
private static boolean isProcessWithPty(@NotNull ProcessHandler processHandler) {
if (processHandler instanceof BaseProcessHandler<?> baseProcessHandler) {
Process process = baseProcessHandler.getProcess();
return process instanceof PtyProcess ||
(process instanceof PtyBasedProcess ptyBasedProcess && ptyBasedProcess.hasPty());
}
return true;
return false;
}
@Override
@@ -415,12 +412,7 @@ public class TerminalExecutionConsole implements ConsoleView, ObservableConsoleV
}
public static boolean isAcceptable(@NotNull ProcessHandler processHandler) {
if (processHandler instanceof BaseProcessHandler<?> baseProcessHandler) {
Process process = baseProcessHandler.getProcess();
return process instanceof PtyProcess ||
(process instanceof PtyBasedProcess && ((PtyBasedProcess)process).hasPty());
}
return false;
return isProcessWithPty(processHandler);
}
private final class ConsoleTerminalWidget extends JBTerminalWidget {
@@ -54,14 +54,37 @@ class TerminalExecutionConsoleTest : BasePlatformTestCase() {
return threadInfos.find { it.threadName.startsWith("TerminalEmulator-") }
}
fun `test convert LF to CRLF for processes without PTY`(): Unit = timeoutRunBlocking(DEFAULT_TEST_TIMEOUT) {
val processHandler = OSProcessHandler(MockPtyBasedProcess(false), "my command", Charsets.UTF_8)
val console = withContext(Dispatchers.UI) {
TerminalExecutionConsole(project, processHandler)
}
console.withConvertLfToCrlfForNonPtyProcess(true)
TestProcessTerminationMessage.attach(processHandler)
processHandler.startNotify()
processHandler.notifyTextAvailable("Foo\nBar\nBaz", ProcessOutputTypes.STDOUT)
processHandler.destroyProcess()
console.awaitOutputContainsSubstring(substringToFind = TestProcessTerminationMessage.getMessage(MockPtyBasedProcess.EXIT_CODE))
val output = TerminalOutput.collect(console.terminalWidget)
output.assertLinesAre(listOf(
"my command",
"Foo",
"Bar",
"Baz",
TestProcessTerminationMessage.getMessage(MockPtyBasedProcess.EXIT_CODE)
))
withContext(Dispatchers.UI) {
Disposer.dispose(console)
}
}
fun `test support ColoredProcessHandler`(): Unit = timeoutRunBlockingWithConsole { console ->
val processHandler = ColoredProcessHandler(MockPtyBasedProcess(), "my command line", Charsets.UTF_8)
val processHandler = ColoredProcessHandler(MockPtyBasedProcess(true), "my command line", Charsets.UTF_8)
assertTrue(TerminalExecutionConsole.isAcceptable(processHandler))
console.attachToProcess(processHandler)
processHandler.startNotify()
processHandler.notifyTextAvailable("\u001b[0m", ProcessOutputTypes.STDOUT)
processHandler.notifyTextAvailable("\u001b[32mFoo\u001b[0m", ProcessOutputTypes.STDOUT)
processHandler.setShouldDestroyProcessRecursively(false)
TestProcessTerminationMessage.attach(processHandler)
processHandler.destroyProcess()
console.awaitOutputContainsSubstring(substringToFind = TestProcessTerminationMessage.getMessage(MockPtyBasedProcess.EXIT_CODE))
@@ -75,7 +98,7 @@ class TerminalExecutionConsoleTest : BasePlatformTestCase() {
}
fun `test support OSProcessHandler`(): Unit = timeoutRunBlockingWithConsole { console ->
val processHandler = OSProcessHandler(MockPtyBasedProcess(), "command line", Charsets.UTF_8)
val processHandler = OSProcessHandler(MockPtyBasedProcess(true), "command line", Charsets.UTF_8)
assertTrue(TerminalExecutionConsole.isAcceptable(processHandler))
console.attachToProcess(processHandler)
processHandler.startNotify()
@@ -5,6 +5,7 @@ import com.intellij.execution.process.KillableProcessHandler
import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.process.ProcessTerminatedListener
import com.intellij.execution.process.PtyBasedProcess
import com.intellij.execution.process.SelfKiller
import com.intellij.platform.eel.EelExecApi.Pty
import com.intellij.platform.eel.ExecuteProcessException
import com.intellij.util.io.BaseDataReader
@@ -43,7 +44,7 @@ internal fun ProcessHandler.writeToStdinAndHitEnter(input: String) {
}
}
internal class MockPtyBasedProcess : Process(), PtyBasedProcess {
internal class MockPtyBasedProcess(private val withPty: Boolean) : Process(), PtyBasedProcess, SelfKiller {
private val exitCodeFuture: CompletableFuture<Int> = CompletableFuture()
@@ -61,7 +62,7 @@ internal class MockPtyBasedProcess : Process(), PtyBasedProcess {
override fun getErrorStream(): InputStream = InputStream.nullInputStream()
override fun getInputStream(): InputStream = InputStream.nullInputStream()
override fun hasPty(): Boolean = true
override fun hasPty(): Boolean = withPty
override fun setWindowSize(columns: Int, rows: Int) {}