v1.3.3: fix SSH terminal no-output bug (critical)

Three bugs fixed in SshTerminalManager:

1. connect()/connectWithKey() never returned — readOutput() was called
   with withContext(Dispatchers.IO), creating a blocking infinite loop
   that prevented the coroutine from completing. Fixed by launching
   readOutput as a separate coroutine via GlobalScope.launch().

2. \r handling in readOutput() silently ate all terminal output —
   \r cleared currentLine, then the subsequent \n found an empty
   line and skipped it. Fixed: \r now finalizes currentLine instead
   of clearing it, properly handling \r\n line endings.

3. Added initial newline on connect to trigger the shell prompt so
   users see output immediately after connection.
This commit is contained in:
茂之钳
2026-05-26 07:31:30 +00:00
parent eb03204347
commit 902a4c5572
@@ -8,6 +8,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import java.io.File
@@ -16,6 +17,7 @@ import java.io.OutputStream
import java.util.Vector
import java.util.concurrent.atomic.AtomicBoolean
@Serializable
data class SshConfig(
val host: String = "",
@@ -115,6 +117,7 @@ class SshTerminalManager(
val connectionState: StateFlow<SshConnectionState> = _connectionState.asStateFlow()
private val running = AtomicBoolean(false)
private var readOutputJob: kotlinx.coroutines.Job? = null
var fingerprintCallback: ((HostKeyInfo) -> Boolean)? = null
private var lastConfig: SshConfig? = null
private var lastKeyPath: String? = null
@@ -149,14 +152,20 @@ class SshTerminalManager(
currentLine.clear()
running.set(true)
// Send initial newline to trigger shell prompt
try {
outputStream?.write("\n".toByteArray(Charsets.UTF_8))
outputStream?.flush()
} catch (_: Exception) {}
_connectionState.value = SshConnectionState(
connected = true,
host = config.host,
displayName = config.displayName.ifEmpty { config.host }
)
// Start reading output
withContext(Dispatchers.IO) {
// Start reading output in a separate coroutine (don't block connect())
readOutputJob = kotlinx.coroutines.GlobalScope.launch(Dispatchers.IO) {
readOutput()
}
@@ -218,6 +227,12 @@ class SshTerminalManager(
currentLine.clear()
running.set(true)
// Send initial newline to trigger shell prompt
try {
outputStream?.write("\n".toByteArray(Charsets.UTF_8))
outputStream?.flush()
} catch (_: Exception) {}
_connectionState.value = SshConnectionState(
connected = true,
host = config.host,
@@ -225,7 +240,7 @@ class SshTerminalManager(
authMode = "key"
)
withContext(Dispatchers.IO) {
readOutputJob = kotlinx.coroutines.GlobalScope.launch(Dispatchers.IO) {
readOutput()
}
@@ -254,14 +269,19 @@ class SshTerminalManager(
for (ch in text) {
when (ch) {
'\n' -> {
// Newline: finalize current line (ignore empty if already cleared by \r)
if (currentLine.isNotEmpty()) {
lineBuffer.add(currentLine.toString())
currentLine.clear()
}
}
'\r' -> {
// Bug 3 fix: carriage return — clear current line for overwrite
currentLine.clear()
// Carriage return: in \r\n pairs, the next \n handles the line.
// If currentLine has content (no preceding \r), finalize it.
if (currentLine.isNotEmpty()) {
lineBuffer.add(currentLine.toString())
currentLine.clear()
}
}
else -> currentLine.append(ch)
}
@@ -394,6 +414,8 @@ class SshTerminalManager(
fun disconnect() {
running.set(false)
readOutputJob?.cancel()
readOutputJob = null
closeSftpChannel()
try { channel?.disconnect() } catch (_: Exception) {}
try { session?.disconnect() } catch (_: Exception) {}