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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -16,6 +17,7 @@ import java.io.OutputStream
|
|||||||
import java.util.Vector
|
import java.util.Vector
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SshConfig(
|
data class SshConfig(
|
||||||
val host: String = "",
|
val host: String = "",
|
||||||
@@ -115,6 +117,7 @@ class SshTerminalManager(
|
|||||||
val connectionState: StateFlow<SshConnectionState> = _connectionState.asStateFlow()
|
val connectionState: StateFlow<SshConnectionState> = _connectionState.asStateFlow()
|
||||||
|
|
||||||
private val running = AtomicBoolean(false)
|
private val running = AtomicBoolean(false)
|
||||||
|
private var readOutputJob: kotlinx.coroutines.Job? = null
|
||||||
var fingerprintCallback: ((HostKeyInfo) -> Boolean)? = null
|
var fingerprintCallback: ((HostKeyInfo) -> Boolean)? = null
|
||||||
private var lastConfig: SshConfig? = null
|
private var lastConfig: SshConfig? = null
|
||||||
private var lastKeyPath: String? = null
|
private var lastKeyPath: String? = null
|
||||||
@@ -149,14 +152,20 @@ class SshTerminalManager(
|
|||||||
currentLine.clear()
|
currentLine.clear()
|
||||||
running.set(true)
|
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(
|
_connectionState.value = SshConnectionState(
|
||||||
connected = true,
|
connected = true,
|
||||||
host = config.host,
|
host = config.host,
|
||||||
displayName = config.displayName.ifEmpty { config.host }
|
displayName = config.displayName.ifEmpty { config.host }
|
||||||
)
|
)
|
||||||
|
|
||||||
// Start reading output
|
// Start reading output in a separate coroutine (don't block connect())
|
||||||
withContext(Dispatchers.IO) {
|
readOutputJob = kotlinx.coroutines.GlobalScope.launch(Dispatchers.IO) {
|
||||||
readOutput()
|
readOutput()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,6 +227,12 @@ class SshTerminalManager(
|
|||||||
currentLine.clear()
|
currentLine.clear()
|
||||||
running.set(true)
|
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(
|
_connectionState.value = SshConnectionState(
|
||||||
connected = true,
|
connected = true,
|
||||||
host = config.host,
|
host = config.host,
|
||||||
@@ -225,7 +240,7 @@ class SshTerminalManager(
|
|||||||
authMode = "key"
|
authMode = "key"
|
||||||
)
|
)
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
readOutputJob = kotlinx.coroutines.GlobalScope.launch(Dispatchers.IO) {
|
||||||
readOutput()
|
readOutput()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,15 +269,20 @@ class SshTerminalManager(
|
|||||||
for (ch in text) {
|
for (ch in text) {
|
||||||
when (ch) {
|
when (ch) {
|
||||||
'\n' -> {
|
'\n' -> {
|
||||||
|
// Newline: finalize current line (ignore empty if already cleared by \r)
|
||||||
if (currentLine.isNotEmpty()) {
|
if (currentLine.isNotEmpty()) {
|
||||||
lineBuffer.add(currentLine.toString())
|
lineBuffer.add(currentLine.toString())
|
||||||
currentLine.clear()
|
currentLine.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'\r' -> {
|
'\r' -> {
|
||||||
// Bug 3 fix: carriage return — clear current line for overwrite
|
// 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()
|
currentLine.clear()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else -> currentLine.append(ch)
|
else -> currentLine.append(ch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -394,6 +414,8 @@ class SshTerminalManager(
|
|||||||
|
|
||||||
fun disconnect() {
|
fun disconnect() {
|
||||||
running.set(false)
|
running.set(false)
|
||||||
|
readOutputJob?.cancel()
|
||||||
|
readOutputJob = null
|
||||||
closeSftpChannel()
|
closeSftpChannel()
|
||||||
try { channel?.disconnect() } catch (_: Exception) {}
|
try { channel?.disconnect() } catch (_: Exception) {}
|
||||||
try { session?.disconnect() } catch (_: Exception) {}
|
try { session?.disconnect() } catch (_: Exception) {}
|
||||||
|
|||||||
Reference in New Issue
Block a user