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:
@@ -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) {}
|
||||
|
||||
Reference in New Issue
Block a user