feat: support clone session

This commit is contained in:
hstyi
2025-07-05 11:57:06 +08:00
committed by hstyi
parent d54671757e
commit a32838dad6
19 changed files with 691 additions and 190 deletions

View File

@@ -9,10 +9,8 @@ import app.termora.findeverywhere.BasicFilterFindEverywhereProvider
import app.termora.findeverywhere.FindEverywhereProvider
import app.termora.findeverywhere.FindEverywhereProviderExtension
import app.termora.findeverywhere.FindEverywhereResult
import app.termora.plugin.ExtensionManager
import app.termora.plugin.internal.extension.DynamicExtensionHandler
import app.termora.plugin.internal.sftppty.SFTPPtyProtocolProvider
import app.termora.plugin.internal.sftppty.SFTPPtyTerminalTab
import app.termora.plugin.internal.ssh.SSHProtocolProvider
import app.termora.terminal.DataKey
import com.formdev.flatlaf.FlatLaf
import com.formdev.flatlaf.extras.components.FlatPopupMenu
@@ -24,7 +22,6 @@ import java.awt.event.ActionEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.beans.PropertyChangeListener
import java.util.*
import javax.swing.*
import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT
import kotlin.math.min
@@ -211,6 +208,15 @@ class TerminalTabbed(
private fun showContextMenu(tabIndex: Int, e: MouseEvent) {
val c = tabbedPane.getComponentAt(tabIndex) as JComponent
val tab = tabs[tabIndex]
val extensions = ExtensionManager.getInstance().getExtensions(TerminalTabbedContextMenuExtension::class.java)
val menuItems = mutableListOf<JMenuItem>()
for (extension in extensions) {
try {
menuItems.add(extension.createJMenuItem(windowScope, tab))
} catch (_: UnsupportedOperationException) {
continue
}
}
val popupMenu = FlatPopupMenu()
@@ -232,7 +238,7 @@ class TerminalTabbed(
}
// 克隆
val clone = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.clone"))
val clone = popupMenu.add(I18n.getString("termora.copy"))
clone.addActionListener { evt ->
if (tab is HostTerminalTab) {
actionManager
@@ -284,14 +290,10 @@ class TerminalTabbed(
}
})
if (tab is HostTerminalTab) {
val openHostAction = actionManager.getAction(OpenHostAction.OPEN_HOST)
if (openHostAction != null) {
if (tab.host.protocol == SSHProtocolProvider.PROTOCOL || tab.host.protocol == SFTPPtyProtocolProvider.PROTOCOL) {
if (menuItems.isNotEmpty()) {
popupMenu.addSeparator()
val sftpCommand = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.sftp-command"))
sftpCommand.addActionListener { openSFTPPtyTab(tab, openHostAction, it) }
}
for (item in menuItems) {
popupMenu.add(item)
}
}
@@ -383,36 +385,6 @@ class TerminalTabbed(
}
private fun openSFTPPtyTab(tab: HostTerminalTab, openHostAction: Action, evt: EventObject) {
if (!SFTPPtyTerminalTab.canSupports) {
OptionPane.showMessageDialog(
SwingUtilities.getWindowAncestor(this),
I18n.getString("termora.tabbed.contextmenu.sftp-not-install"),
messageType = JOptionPane.ERROR_MESSAGE
)
return
}
var host = tab.host
if (host.protocol == SSHProtocolProvider.PROTOCOL) {
val envs = tab.host.options.envs().toMutableMap()
val currentDir = tab.getData(DataProviders.Terminal)?.getTerminalModel()
?.getData(DataKey.CurrentDir, StringUtils.EMPTY) ?: StringUtils.EMPTY
if (currentDir.isNotBlank()) {
envs["CurrentDir"] = currentDir
}
host = host.copy(
protocol = SFTPPtyProtocolProvider.PROTOCOL,
options = host.options.copy(env = envs.toPropertiesString())
)
}
openHostAction.actionPerformed(OpenHostActionEvent(this, host, evt))
}
/**
* 对着 ToolBar 右键
*/

View File

@@ -0,0 +1,12 @@
package app.termora
import app.termora.plugin.Extension
import javax.swing.JMenuItem
interface TerminalTabbedContextMenuExtension : Extension {
/**
* 抛出 [UnsupportedOperationException] 表示不支持
*/
fun createJMenuItem(windowScope: WindowScope, tab: TerminalTab): JMenuItem
}

View File

@@ -2,6 +2,7 @@ package app.termora.keymgr
import app.termora.*
import app.termora.keyboardinteractive.TerminalUserInteraction
import app.termora.plugin.internal.ssh.SshClients
import app.termora.terminal.ControlCharacters
import app.termora.terminal.DataKey
import app.termora.terminal.PtyConnectorDelegate

View File

@@ -4,6 +4,7 @@ import app.termora.*
import app.termora.database.DatabaseManager
import app.termora.keymgr.KeyManager
import app.termora.keymgr.OhKeyPairKeyPairProvider
import app.termora.plugin.internal.ssh.SshClients
import app.termora.terminal.*
import com.formdev.flatlaf.util.SystemInfo
import org.apache.commons.io.Charsets

View File

@@ -0,0 +1,44 @@
package app.termora.plugin.internal.ssh
import app.termora.I18n
import app.termora.TerminalTab
import app.termora.TerminalTabbedContextMenuExtension
import app.termora.WindowScope
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.actions.DataProviders
import javax.swing.JMenuItem
class CloneSessionTerminalTabbedContextMenuExtension private constructor() : TerminalTabbedContextMenuExtension {
companion object {
val instance = CloneSessionTerminalTabbedContextMenuExtension()
}
override fun createJMenuItem(
windowScope: WindowScope,
tab: TerminalTab
): JMenuItem {
if (tab is SSHTerminalTab) {
if (tab.host.protocol == SSHProtocolProvider.PROTOCOL) {
val cloneSession = JMenuItem(I18n.getString("termora.tabbed.contextmenu.clone-session"))
val c = tab.getData(SSHTerminalTab.MySshHandler)
cloneSession.isEnabled = c?.channel?.isOpen == true
if (c != null) {
cloneSession.addActionListener(object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) {
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
val handler = c.copy(channel = null)
val newTab = SSHTerminalTab(windowScope, tab.host, handler)
terminalTabbedManager.addTerminalTab(newTab)
newTab.start()
}
})
}
return cloneSession
}
}
throw UnsupportedOperationException()
}
}

View File

@@ -1,5 +1,6 @@
package app.termora.plugin.internal.ssh
import app.termora.TerminalTabbedContextMenuExtension
import app.termora.plugin.Extension
import app.termora.plugin.InternalPlugin
import app.termora.protocol.ProtocolHostPanelExtension
@@ -9,6 +10,8 @@ internal class SSHInternalPlugin : InternalPlugin() {
init {
support.addExtension(ProtocolProviderExtension::class.java) { SSHProtocolProviderExtension.instance }
support.addExtension(ProtocolHostPanelExtension::class.java) { SSHProtocolHostPanelExtension.instance }
support.addExtension(TerminalTabbedContextMenuExtension::class.java) { SftpCommandTerminalTabbedContextMenuExtension.instance }
support.addExtension(TerminalTabbedContextMenuExtension::class.java) { CloneSessionTerminalTabbedContextMenuExtension.instance }
}
override fun getName(): String {

View File

@@ -10,41 +10,36 @@ import app.termora.keymap.KeymapManager
import app.termora.terminal.ControlCharacters
import app.termora.terminal.DataKey
import app.termora.terminal.PtyConnector
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.withContext
import org.apache.commons.io.Charsets
import org.apache.commons.lang3.StringUtils
import org.apache.sshd.client.SshClient
import org.apache.sshd.client.channel.ChannelShell
import org.apache.sshd.client.session.ClientSession
import org.apache.sshd.common.SshConstants
import org.apache.sshd.common.channel.Channel
import org.apache.sshd.common.channel.ChannelListener
import org.apache.sshd.common.session.Session
import org.apache.sshd.common.session.SessionListener
import org.apache.sshd.common.future.CloseFuture
import org.apache.sshd.common.future.SshFutureListener
import org.slf4j.LoggerFactory
import java.nio.charset.StandardCharsets
import javax.swing.Icon
import javax.swing.JComponent
import javax.swing.SwingUtilities
import kotlin.time.Duration.Companion.milliseconds
class SSHTerminalTab(
windowScope: WindowScope, host: Host,
private val handler: SshHandler = SshHandler()
) : PtyHostTerminalTab(windowScope, host) {
class SSHTerminalTab(windowScope: WindowScope, host: Host) :
PtyHostTerminalTab(windowScope, host) {
companion object {
val SSHSession = DataKey(ClientSession::class)
internal val MySshHandler = DataKey(SshHandler::class)
private val log = LoggerFactory.getLogger(SSHTerminalTab::class.java)
}
private val mutex = Mutex()
private val tab = this
private var sshClient: SshClient? = null
private var sshSession: ClientSession? = null
private var sshChannelShell: ChannelShell? = null
private val owner get() = SwingUtilities.getWindowAncestor(terminalPanel)
private val tab get() = this
init {
terminalPanel.dropFiles = false
@@ -55,12 +50,10 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
return terminalPanel
}
override fun canReconnect(): Boolean {
return !mutex.isLocked
return mutex.isLocked.not()
}
override suspend fun openPtyConnector(): PtyConnector {
if (mutex.tryLock()) {
try {
@@ -82,74 +75,32 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
// hide cursor
terminalModel.setData(DataKey.Companion.ShowCursor, false)
// print
terminal.write("SSH client is opening...\r\n")
terminal.write("Connecting to remote server ")
}
val owner = SwingUtilities.getWindowAncestor(terminalPanel)
val client = SshClients.openClient(host, owner).also { sshClient = it }
val sessionListener = MySessionListener()
val channelListener = MyChannelListener()
withContext(Dispatchers.Swing) { terminal.write("SSH client opened successfully.\r\n\r\n") }
client.addSessionListener(sessionListener)
client.addChannelListener(channelListener)
val (session, channel) = try {
val session = SshClients.openSession(host, client).also { sshSession = it }
val channel = SshClients.openShell(
host,
terminalPanel.winSize(),
session
).also { sshChannelShell = it }
Pair(session, channel)
} finally {
client.removeSessionListener(sessionListener)
client.removeChannelListener(channelListener)
}
// newline
withContext(Dispatchers.Swing) {
terminal.write("\r\n")
}
channel.addChannelListener(object : ChannelListener {
private val reconnectShortcut
get() = KeymapManager.Companion.getInstance().getActiveKeymap()
.getShortcut(TabReconnectAction.Companion.RECONNECT_TAB).firstOrNull()
override fun channelClosed(channel: Channel, reason: Throwable?) {
coroutineScope.launch(Dispatchers.Swing) {
terminal.write("\r\n\r\n${ControlCharacters.Companion.ESC}[31m")
terminal.write(I18n.getString("termora.terminal.channel-disconnected"))
if (reconnectShortcut is KeyShortcut) {
terminal.write(
I18n.getString(
"termora.terminal.channel-reconnect",
reconnectShortcut.toString()
)
)
}
terminal.write("\r\n")
terminal.write("${ControlCharacters.Companion.ESC}[0m")
terminalModel.setData(DataKey.Companion.ShowCursor, false)
if (DatabaseManager.getInstance().terminal.autoCloseTabWhenDisconnected) {
terminalTabbedManager?.let { manager ->
SwingUtilities.invokeLater {
manager.closeTerminalTab(tab, true)
}
val loading = coroutineScope.launch(Dispatchers.Swing) {
var c = 0
while (isActive) {
if (++c > 6) c = 1
terminal.write("${ControlCharacters.ESC}[1;32m")
terminal.write(".".repeat(c))
terminal.write(" ".repeat(6 - c))
terminal.write("${ControlCharacters.ESC}[0m")
delay(350.milliseconds)
terminal.write("${ControlCharacters.BS}".repeat(6))
}
}
// stop
stop()
}
}
})
val channel: ChannelShell
try {
val client = openClient()
val session = openSession(client)
channel = openChannel(session)
// 打开隧道
openTunnelings(session, host)
} finally {
loading.cancel()
}
// 隐藏提示
withContext(Dispatchers.Swing) {
@@ -194,10 +145,68 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
}
}
private fun openClient(): SshClient {
val client = handler.client
if (client != null) return client
return SshClients.openClient(host, owner).also { handler.client = it }
}
private fun openSession(client: SshClient): ClientSession {
val session = handler.session
if (session != null) return SshSessionPool.register(session, client)
return SshClients.openSession(host, client).also { handler.session = SshSessionPool.register(it, client) }
}
private fun openChannel(session: ClientSession): ChannelShell {
val channel = SshClients.openShell(host, terminalPanel.winSize(), session)
handler.channel = channel
channel.addCloseFutureListener(object : SshFutureListener<CloseFuture> {
private val reconnectShortcut
get() = KeymapManager.Companion.getInstance().getActiveKeymap()
.getShortcut(TabReconnectAction.Companion.RECONNECT_TAB).firstOrNull()
private val autoCloseTabWhenDisconnected get() = DatabaseManager.getInstance().terminal.autoCloseTabWhenDisconnected
override fun operationComplete(future: CloseFuture) {
coroutineScope.launch(Dispatchers.Swing) {
terminal.write("\r\n\r\n${ControlCharacters.Companion.ESC}[31m")
terminal.write(I18n.getString("termora.terminal.channel-disconnected"))
if (reconnectShortcut is KeyShortcut) {
terminal.write(
I18n.getString(
"termora.terminal.channel-reconnect",
reconnectShortcut.toString()
)
)
}
terminal.write("\r\n")
terminal.write("${ControlCharacters.Companion.ESC}[0m")
terminalModel.setData(DataKey.Companion.ShowCursor, false)
if (autoCloseTabWhenDisconnected) {
terminalTabbedManager?.let { manager ->
SwingUtilities.invokeLater {
manager.closeTerminalTab(tab, true)
}
}
}
// stop
stop()
}
}
})
return channel
}
@Suppress("UNCHECKED_CAST")
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
if (dataKey == SSHSession) {
return sshSession as T?
return handler.session as T?
}
if (dataKey == MySshHandler) {
return handler as T?
}
return super.getData(dataKey)
}
@@ -206,16 +215,7 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
if (mutex.tryLock()) {
try {
super.stop()
sshChannelShell?.close(true)
sshSession?.disableSessionHeartbeat()
sshSession?.disconnect(SshConstants.SSH2_DISCONNECT_BY_APPLICATION, StringUtils.EMPTY)
sshSession?.close(true)
sshClient?.close(true)
sshChannelShell = null
sshSession = null
sshClient = null
handler.close()
} finally {
mutex.unlock()
}
@@ -231,36 +231,4 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
terminalPanel.storeVisualWindows(host.id)
}
private inner class MySessionListener : SessionListener, Disposable {
override fun sessionEvent(session: Session, event: SessionListener.Event) {
coroutineScope.launch {
when (event) {
SessionListener.Event.KeyEstablished -> terminal.write("Session Key exchange successful.\r\n")
SessionListener.Event.Authenticated -> terminal.write("Session authentication successful.\r\n\r\n")
SessionListener.Event.KexCompleted -> terminal.write("Session KEX negotiation successful.\r\n")
}
}
}
override fun sessionEstablished(session: Session) {
coroutineScope.launch { terminal.write("Session established.\r\n") }
}
override fun sessionCreated(session: Session?) {
coroutineScope.launch { terminal.write("Session created.\r\n") }
}
}
private inner class MyChannelListener : ChannelListener, Disposable {
override fun channelOpenSuccess(channel: Channel) {
coroutineScope.launch { terminal.write("Channel shell opened successfully.\r\n") }
}
override fun channelInitialized(channel: Channel) {
coroutineScope.launch { terminal.write("Channel shell initialization successful.\r\n") }
}
}
}

View File

@@ -0,0 +1,72 @@
package app.termora.plugin.internal.ssh
import app.termora.*
import app.termora.actions.*
import app.termora.plugin.internal.sftppty.SFTPPtyProtocolProvider
import app.termora.plugin.internal.sftppty.SFTPPtyTerminalTab
import app.termora.terminal.DataKey
import org.apache.commons.lang3.StringUtils
import java.util.*
import javax.swing.Action
import javax.swing.JMenuItem
import javax.swing.JOptionPane
class SftpCommandTerminalTabbedContextMenuExtension private constructor() : TerminalTabbedContextMenuExtension {
companion object {
val instance = SftpCommandTerminalTabbedContextMenuExtension()
}
private val actionManager = ActionManager.getInstance()
override fun createJMenuItem(
windowScope: WindowScope,
tab: TerminalTab
): JMenuItem {
if (tab is HostTerminalTab) {
val openHostAction = actionManager.getAction(OpenHostAction.OPEN_HOST)
if (openHostAction != null) {
if (tab.host.protocol == SSHProtocolProvider.PROTOCOL || tab.host.protocol == SFTPPtyProtocolProvider.PROTOCOL) {
val sftpCommand = JMenuItem(I18n.getString("termora.tabbed.contextmenu.sftp-command"))
sftpCommand.addActionListener(object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) {
openSFTPPtyTab(tab, openHostAction, evt)
}
})
return sftpCommand
}
}
}
throw UnsupportedOperationException()
}
private fun openSFTPPtyTab(tab: HostTerminalTab, openHostAction: Action, evt: EventObject) {
if (SFTPPtyTerminalTab.canSupports.not()) {
OptionPane.showMessageDialog(
tab.windowScope.window,
I18n.getString("termora.tabbed.contextmenu.sftp-not-install"),
messageType = JOptionPane.ERROR_MESSAGE
)
return
}
var host = tab.host
if (host.protocol == SSHProtocolProvider.PROTOCOL) {
val envs = tab.host.options.envs().toMutableMap()
val currentDir = tab.getData(DataProviders.Terminal)?.getTerminalModel()
?.getData(DataKey.CurrentDir, StringUtils.EMPTY) ?: StringUtils.EMPTY
if (currentDir.isNotBlank()) {
envs["CurrentDir"] = currentDir
}
host = host.copy(
protocol = SFTPPtyProtocolProvider.PROTOCOL,
options = host.options.copy(env = envs.toPropertiesString())
)
}
openHostAction.actionPerformed(OpenHostActionEvent(evt.source, host, evt))
}
}

View File

@@ -1,5 +1,6 @@
package app.termora
package app.termora.plugin.internal.ssh
import app.termora.*
import app.termora.keyboardinteractive.TerminalUserInteraction
import app.termora.keymgr.OhKeyPairKeyPairProvider
import app.termora.terminal.TerminalSize
@@ -29,7 +30,6 @@ import org.apache.sshd.client.session.ClientSession
import org.apache.sshd.client.session.ClientSessionImpl
import org.apache.sshd.client.session.SessionFactory
import org.apache.sshd.common.AttributeRepository
import org.apache.sshd.common.SshConstants
import org.apache.sshd.common.SshException
import org.apache.sshd.common.channel.ChannelFactory
import org.apache.sshd.common.channel.PtyChannelConfiguration
@@ -63,7 +63,7 @@ import org.eclipse.jgit.internal.transport.sshd.proxy.AbstractClientProxyConnect
import org.eclipse.jgit.internal.transport.sshd.proxy.HttpClientConnector
import org.eclipse.jgit.internal.transport.sshd.proxy.Socks5ClientConnector
import org.eclipse.jgit.transport.CredentialsProvider
import org.eclipse.jgit.transport.SshConstants.IDENTITY_AGENT
import org.eclipse.jgit.transport.SshConstants
import org.eclipse.jgit.transport.sshd.IdentityPasswordProvider
import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory
import org.slf4j.LoggerFactory
@@ -89,7 +89,7 @@ object SshClients {
val HOST_KEY = AttributeRepository.AttributeKey<Host>()
private val timeout = Duration.ofSeconds(30)
private val hostManager get() = HostManager.getInstance()
private val hostManager get() = HostManager.Companion.getInstance()
private val log by lazy { LoggerFactory.getLogger(SshClients::class.java) }
/**
@@ -166,7 +166,7 @@ object SshClients {
}
val jumpHosts = mutableListOf<Host>()
val hosts = HostManager.getInstance().hosts().associateBy { it.id }
val hosts = HostManager.Companion.getInstance().hosts().associateBy { it.id }
for (jumpHostId in h.options.jumpHosts) {
val e = hosts[jumpHostId]
if (e == null) {
@@ -235,16 +235,16 @@ object SshClients {
if (SystemInfo.isMacOS) {
val file = FileUtils.getFile(Application.getBaseDataDir(), "config", "ssh-agent.sock")
if (file.exists()) {
entry.setProperty(IDENTITY_AGENT, file.absolutePath)
entry.setProperty(SshConstants.IDENTITY_AGENT, file.absolutePath)
}
}
if (entry.getProperty(IDENTITY_AGENT).isNullOrBlank()) {
if (entry.getProperty(SshConstants.IDENTITY_AGENT).isNullOrBlank()) {
if (host.authentication.password.isNotBlank())
entry.setProperty(IDENTITY_AGENT, host.authentication.password)
entry.setProperty(SshConstants.IDENTITY_AGENT, host.authentication.password)
else if (SystemInfo.isWindows)
entry.setProperty(IDENTITY_AGENT, PageantConnector.DESCRIPTOR.identityAgent)
entry.setProperty(SshConstants.IDENTITY_AGENT, PageantConnector.DESCRIPTOR.identityAgent)
else
entry.setProperty(IDENTITY_AGENT, UnixDomainSocketConnector.DESCRIPTOR.identityAgent)
entry.setProperty(SshConstants.IDENTITY_AGENT, UnixDomainSocketConnector.DESCRIPTOR.identityAgent)
}
}
@@ -272,7 +272,7 @@ object SshClients {
throw SshException("Authentication failed")
}
} catch (e: Exception) {
if (e !is SshException || e.disconnectCode != SshConstants.SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) throw e
if (e !is SshException || e.disconnectCode != org.apache.sshd.common.SshConstants.SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) throw e
val owner = client.properties["owner"] as Window? ?: throw e
val askUserInfo = ask(host, entry, owner) ?: throw e
if (askUserInfo.authentication.type == AuthenticationType.No) throw e
@@ -383,7 +383,7 @@ object SshClients {
val channelFactories = mutableListOf<ChannelFactory>()
channelFactories.addAll(ClientBuilder.DEFAULT_CHANNEL_FACTORIES)
channelFactories.add(X11ChannelFactory.INSTANCE)
channelFactories.add(X11ChannelFactory.Companion.INSTANCE)
builder.channelFactories(channelFactories)
val sshClient = builder.build() as JGitSshClient
@@ -726,4 +726,3 @@ object SshClients {
}
}

View File

@@ -0,0 +1,27 @@
package app.termora.plugin.internal.ssh
import org.apache.sshd.client.SshClient
import org.apache.sshd.client.session.ClientSession
import org.apache.sshd.common.channel.Channel
data class SshHandler(
var client: SshClient? = null,
var session: ClientSession? = null,
var channel: Channel? = null
) : AutoCloseable {
override fun close() {
channel?.close(true)?.await()
session?.close(true)?.await()
channel = null
session = null
// client 由 SshSessionPool 负责关闭
if (client?.isClosing == true || client?.isClosed == true) {
client = null
}
}
}

View File

@@ -0,0 +1,394 @@
package app.termora.plugin.internal.ssh
import org.apache.sshd.client.SshClient
import org.apache.sshd.client.channel.ChannelExec
import org.apache.sshd.client.channel.ChannelShell
import org.apache.sshd.client.session.ClientSession
import org.apache.sshd.client.session.forward.DynamicPortForwardingTracker
import org.apache.sshd.client.session.forward.ExplicitPortForwardingTracker
import org.apache.sshd.common.AttributeRepository
import org.apache.sshd.common.channel.Channel
import org.apache.sshd.common.channel.PtyChannelConfigurationHolder
import org.apache.sshd.common.channel.throttle.ChannelStreamWriter
import org.apache.sshd.common.channel.throttle.ChannelStreamWriterResolver
import org.apache.sshd.common.future.CloseFuture
import org.apache.sshd.common.future.DefaultCloseFuture
import org.apache.sshd.common.io.IoWriteFuture
import org.apache.sshd.common.session.SessionHeartbeatController
import org.apache.sshd.common.util.buffer.Buffer
import org.apache.sshd.common.util.net.SshdSocketAddress
import java.io.OutputStream
import java.net.SocketAddress
import java.nio.charset.Charset
import java.time.Duration
import java.util.*
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import java.util.function.Function
internal object SshSessionPool {
private val map = WeakHashMap<ClientSession, MyClientSession>()
fun register(session: ClientSession, client: SshClient): ClientSession {
if (session is MyClientSession) {
session.refCount.incrementAndGet()
return session
}
synchronized(session) {
val delegate = map[session] ?: MyClientSession(client, session)
map[session] = delegate
delegate.refCount.incrementAndGet()
return delegate
}
}
private class MyClientSession(
private val client: SshClient,
private val delegate: ClientSession
) : ClientSession by delegate {
val refCount = AtomicInteger(0)
override fun createShellChannel(): ChannelShell? {
return delegate.createShellChannel()
}
override fun createExecChannel(command: String?): ChannelExec? {
return delegate.createExecChannel(command)
}
override fun createExecChannel(
command: String?,
ptyConfig: PtyChannelConfigurationHolder?,
env: Map<String?, *>?
): ChannelExec? {
return delegate.createExecChannel(command, ptyConfig, env)
}
override fun executeRemoteCommand(command: String?): String? {
return delegate.executeRemoteCommand(command)
}
override fun executeRemoteCommand(
command: String?,
stderr: OutputStream?,
charset: Charset?
): String? {
return delegate.executeRemoteCommand(command, stderr, charset)
}
override fun executeRemoteCommand(
command: String?,
stdout: OutputStream?,
stderr: OutputStream?,
charset: Charset?
) {
delegate.executeRemoteCommand(command, stdout, stderr, charset)
}
override fun createLocalPortForwardingTracker(
localPort: Int,
remote: SshdSocketAddress?
): ExplicitPortForwardingTracker? {
return delegate.createLocalPortForwardingTracker(localPort, remote)
}
override fun createLocalPortForwardingTracker(
local: SshdSocketAddress?,
remote: SshdSocketAddress?
): ExplicitPortForwardingTracker? {
return delegate.createLocalPortForwardingTracker(local, remote)
}
override fun createRemotePortForwardingTracker(
remote: SshdSocketAddress?,
local: SshdSocketAddress?
): ExplicitPortForwardingTracker? {
return delegate.createRemotePortForwardingTracker(remote, local)
}
override fun createDynamicPortForwardingTracker(local: SshdSocketAddress?): DynamicPortForwardingTracker? {
return delegate.createDynamicPortForwardingTracker(local)
}
override fun waitFor(
mask: Collection<ClientSession.ClientSessionEvent?>?,
timeout: Duration?
): Set<ClientSession.ClientSessionEvent?>? {
return delegate.waitFor(mask, timeout)
}
override fun createBuffer(cmd: Byte): Buffer? {
return delegate.createBuffer(cmd)
}
override fun writePacket(
buffer: Buffer?,
timeout: Duration?
): IoWriteFuture? {
return delegate.writePacket(buffer, timeout)
}
override fun writePacket(
buffer: Buffer?,
maxWaitMillis: Long
): IoWriteFuture? {
return delegate.writePacket(buffer, maxWaitMillis)
}
override fun request(
request: String?,
buffer: Buffer?,
timeout: Long,
unit: TimeUnit?
): Buffer? {
return delegate.request(request, buffer, timeout, unit)
}
override fun request(
request: String?,
buffer: Buffer?,
timeout: Duration?
): Buffer? {
return delegate.request(request, buffer, timeout)
}
override fun getLocalAddress(): SocketAddress? {
return delegate.getLocalAddress()
}
override fun getRemoteAddress(): SocketAddress? {
return delegate.getRemoteAddress()
}
override fun <T : Any?> resolveAttribute(key: AttributeRepository.AttributeKey<T?>?): T? {
return delegate.resolveAttribute(key)
}
override fun getSessionHeartbeatType(): SessionHeartbeatController.HeartbeatType? {
return delegate.getSessionHeartbeatType()
}
override fun getSessionHeartbeatInterval(): Duration? {
return delegate.getSessionHeartbeatInterval()
}
override fun disableSessionHeartbeat() {
delegate.disableSessionHeartbeat()
}
override fun setSessionHeartbeat(
type: SessionHeartbeatController.HeartbeatType?,
unit: TimeUnit?,
count: Long
) {
delegate.setSessionHeartbeat(type, unit, count)
}
override fun setSessionHeartbeat(
type: SessionHeartbeatController.HeartbeatType?,
interval: Duration?
) {
delegate.setSessionHeartbeat(type, interval)
}
override fun isEmpty(): Boolean {
return delegate.isEmpty()
}
override fun getLongProperty(name: String?, def: Long): Long {
return delegate.getLongProperty(name, def)
}
override fun getLong(name: String?): Long? {
return delegate.getLong(name)
}
override fun getIntProperty(name: String?, def: Int): Int {
return delegate.getIntProperty(name, def)
}
override fun getInteger(name: String?): Int? {
return delegate.getInteger(name)
}
override fun getBooleanProperty(name: String?, def: Boolean): Boolean {
return delegate.getBooleanProperty(name, def)
}
override fun getBoolean(name: String?): Boolean? {
return delegate.getBoolean(name)
}
override fun getStringProperty(name: String?, def: String?): String? {
return delegate.getStringProperty(name, def)
}
override fun getString(name: String?): String? {
return delegate.getString(name)
}
override fun getObject(name: String?): Any? {
return delegate.getObject(name)
}
override fun getCharset(
name: String?,
defaultValue: Charset?
): Charset? {
return delegate.getCharset(name, defaultValue)
}
override fun <T : Any?> computeAttributeIfAbsent(
key: AttributeRepository.AttributeKey<T?>?,
resolver: Function<in AttributeRepository.AttributeKey<T>, out T?>?
): T? {
return delegate.computeAttributeIfAbsent(key, resolver)
}
override fun close() {
close(true)?.await()
}
override fun close(immediately: Boolean): CloseFuture? {
synchronized(delegate) {
if (refCount.decrementAndGet() < 1) {
delegate.close(immediately).await()
return client.close(immediately)
}
}
return DefaultCloseFuture(this, this).apply { setClosed() }
}
override fun isOpen(): Boolean {
return delegate.isOpen()
}
override fun getCipherFactoriesNameList(): String? {
return delegate.getCipherFactoriesNameList()
}
override fun getCipherFactoriesNames(): List<String?>? {
return delegate.getCipherFactoriesNames()
}
override fun setCipherFactoriesNameList(names: String?) {
delegate.setCipherFactoriesNameList(names)
}
override fun setCipherFactoriesNames(vararg names: String?) {
delegate.setCipherFactoriesNames(*names)
}
override fun setCipherFactoriesNames(names: Collection<String?>?) {
delegate.setCipherFactoriesNames(names)
}
override fun getCompressionFactoriesNameList(): String? {
return delegate.getCompressionFactoriesNameList()
}
override fun getCompressionFactoriesNames(): List<String?>? {
return delegate.getCompressionFactoriesNames()
}
override fun setCompressionFactoriesNameList(names: String?) {
delegate.setCompressionFactoriesNameList(names)
}
override fun setCompressionFactoriesNames(vararg names: String?) {
delegate.setCompressionFactoriesNames(*names)
}
override fun setCompressionFactoriesNames(names: Collection<String?>?) {
delegate.setCompressionFactoriesNames(names)
}
override fun getMacFactoriesNameList(): String? {
return delegate.getMacFactoriesNameList()
}
override fun getMacFactoriesNames(): List<String?>? {
return delegate.getMacFactoriesNames()
}
override fun setMacFactoriesNameList(names: String?) {
delegate.setMacFactoriesNameList(names)
}
override fun setMacFactoriesNames(vararg names: String?) {
delegate.setMacFactoriesNames(*names)
}
override fun setMacFactoriesNames(names: Collection<String?>?) {
delegate.setMacFactoriesNames(names)
}
override fun setSignatureFactoriesNameList(names: String?) {
delegate.setSignatureFactoriesNameList(names)
}
override fun setSignatureFactoriesNames(vararg names: String?) {
delegate.setSignatureFactoriesNames(*names)
}
override fun setSignatureFactoriesNames(names: Collection<String?>?) {
delegate.setSignatureFactoriesNames(names)
}
override fun getSignatureFactoriesNameList(): String? {
return delegate.getSignatureFactoriesNameList()
}
override fun getSignatureFactoriesNames(): List<String?>? {
return delegate.getSignatureFactoriesNames()
}
override fun resolveChannelStreamWriterResolver(): ChannelStreamWriterResolver? {
return delegate.resolveChannelStreamWriterResolver()
}
override fun resolveChannelStreamWriter(
channel: Channel?,
cmd: Byte
): ChannelStreamWriter? {
return delegate.resolveChannelStreamWriter(channel, cmd)
}
override fun isLocalPortForwardingStartedForPort(port: Int): Boolean {
return delegate.isLocalPortForwardingStartedForPort(port)
}
override fun isRemotePortForwardingStartedForPort(port: Int): Boolean {
return delegate.isRemotePortForwardingStartedForPort(port)
}
override fun setUserAuthFactoriesNames(names: Collection<String?>?) {
delegate.setUserAuthFactoriesNames(names)
}
override fun setUserAuthFactoriesNames(vararg names: String?) {
delegate.setUserAuthFactoriesNames(*names)
}
override fun getUserAuthFactoriesNameList(): String? {
return delegate.getUserAuthFactoriesNameList()
}
override fun getUserAuthFactoriesNames(): List<String?>? {
return delegate.getUserAuthFactoriesNames()
}
override fun setUserAuthFactoriesNameList(names: String?) {
delegate.setUserAuthFactoriesNameList(names)
}
override fun startLocalPortForwarding(
localPort: Int,
remote: SshdSocketAddress?
): SshdSocketAddress? {
return delegate.startLocalPortForwarding(localPort, remote)
}
}
}

View File

@@ -3,8 +3,8 @@ package app.termora.terminal.panel.vw
import app.termora.Disposer
import app.termora.I18n
import app.termora.Icons
import app.termora.SshClients
import app.termora.plugin.internal.ssh.SSHTerminalTab
import app.termora.plugin.internal.ssh.SshClients
import com.formdev.flatlaf.extras.FlatSVGIcon
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
@@ -28,7 +28,7 @@ import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.xpath.XPathFactory
import kotlin.time.Duration.Companion.milliseconds
class NvidiaSMIVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
internal class NvidiaSMIVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
SSHVisualWindow(tab, "NVIDIA-SMI", visualWindowManager) {
companion object {

View File

@@ -1,7 +1,11 @@
package app.termora.terminal.panel.vw
import app.termora.*
import app.termora.Disposer
import app.termora.DynamicColor
import app.termora.I18n
import app.termora.formatBytes
import app.termora.plugin.internal.ssh.SSHTerminalTab
import app.termora.plugin.internal.ssh.SshClients
import com.jgoodies.forms.builder.FormBuilder
import com.jgoodies.forms.layout.FormLayout
import kotlinx.coroutines.Dispatchers
@@ -16,7 +20,7 @@ import javax.swing.table.DefaultTableCellRenderer
import javax.swing.table.DefaultTableModel
class SystemInformationVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
internal class SystemInformationVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
SSHVisualWindow(tab, "SystemInformation", visualWindowManager) {
companion object {

View File

@@ -38,7 +38,7 @@ import kotlin.reflect.cast
import kotlin.time.Duration.Companion.milliseconds
class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
SSHVisualWindow(tab, "Transfer", visualWindowManager) {
companion object {

View File

@@ -1,6 +1,6 @@
package app.termora.transfer.internal.sftp
import app.termora.SshClients
import app.termora.plugin.internal.ssh.SshClients
import app.termora.protocol.PathHandler
import app.termora.protocol.PathHandlerRequest
import app.termora.protocol.TransferProtocolProvider

View File

@@ -258,6 +258,7 @@ termora.tabbed.contextmenu.rename=Rename
termora.tabbed.contextmenu.sftp-command=SFTP Command
termora.tabbed.contextmenu.sftp-not-install=SFTP programme not found, please install and try again
termora.tabbed.contextmenu.clone=Clone
termora.tabbed.contextmenu.clone-session=Clone Session
termora.tabbed.contextmenu.open-in-new-window=Open in New Window
termora.tabbed.contextmenu.close=Close
termora.tabbed.contextmenu.close-other-tabs=Close Other Tabs

View File

@@ -253,6 +253,7 @@ termora.tabbed.contextmenu.rename=重命名
termora.tabbed.contextmenu.sftp-command=SFTP 终端
termora.tabbed.contextmenu.sftp-not-install=没有找到 SFTP 程序,请安装后重试
termora.tabbed.contextmenu.clone=克隆
termora.tabbed.contextmenu.clone-session=克隆会话
termora.tabbed.contextmenu.open-in-new-window=在新窗口打开
termora.tabbed.contextmenu.close=关闭
termora.tabbed.contextmenu.close-other-tabs=关闭其他标签页

View File

@@ -248,6 +248,7 @@ termora.tabbed.contextmenu.rename=重新命名
termora.tabbed.contextmenu.sftp-command=SFTP 終端
termora.tabbed.contextmenu.sftp-not-install=沒有找到 SFTP 程序,請安裝後重試
termora.tabbed.contextmenu.clone=克隆
termora.tabbed.contextmenu.clone-session=克隆會話
termora.tabbed.contextmenu.open-in-new-window=在新視窗打開
termora.tabbed.contextmenu.close=關閉
termora.tabbed.contextmenu.close-other-tabs=關閉其他標籤頁

View File

@@ -1,5 +1,6 @@
package app.termora
import app.termora.plugin.internal.ssh.SshClients
import org.apache.sshd.client.session.ClientSession
import org.testcontainers.containers.GenericContainer
import kotlin.test.AfterTest