mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
feat: support clone session
This commit is contained in:
@@ -9,10 +9,8 @@ import app.termora.findeverywhere.BasicFilterFindEverywhereProvider
|
|||||||
import app.termora.findeverywhere.FindEverywhereProvider
|
import app.termora.findeverywhere.FindEverywhereProvider
|
||||||
import app.termora.findeverywhere.FindEverywhereProviderExtension
|
import app.termora.findeverywhere.FindEverywhereProviderExtension
|
||||||
import app.termora.findeverywhere.FindEverywhereResult
|
import app.termora.findeverywhere.FindEverywhereResult
|
||||||
|
import app.termora.plugin.ExtensionManager
|
||||||
import app.termora.plugin.internal.extension.DynamicExtensionHandler
|
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 app.termora.terminal.DataKey
|
||||||
import com.formdev.flatlaf.FlatLaf
|
import com.formdev.flatlaf.FlatLaf
|
||||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
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.MouseAdapter
|
||||||
import java.awt.event.MouseEvent
|
import java.awt.event.MouseEvent
|
||||||
import java.beans.PropertyChangeListener
|
import java.beans.PropertyChangeListener
|
||||||
import java.util.*
|
|
||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT
|
import javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@@ -211,6 +208,15 @@ class TerminalTabbed(
|
|||||||
private fun showContextMenu(tabIndex: Int, e: MouseEvent) {
|
private fun showContextMenu(tabIndex: Int, e: MouseEvent) {
|
||||||
val c = tabbedPane.getComponentAt(tabIndex) as JComponent
|
val c = tabbedPane.getComponentAt(tabIndex) as JComponent
|
||||||
val tab = tabs[tabIndex]
|
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()
|
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 ->
|
clone.addActionListener { evt ->
|
||||||
if (tab is HostTerminalTab) {
|
if (tab is HostTerminalTab) {
|
||||||
actionManager
|
actionManager
|
||||||
@@ -284,14 +290,10 @@ class TerminalTabbed(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (tab is HostTerminalTab) {
|
if (menuItems.isNotEmpty()) {
|
||||||
val openHostAction = actionManager.getAction(OpenHostAction.OPEN_HOST)
|
|
||||||
if (openHostAction != null) {
|
|
||||||
if (tab.host.protocol == SSHProtocolProvider.PROTOCOL || tab.host.protocol == SFTPPtyProtocolProvider.PROTOCOL) {
|
|
||||||
popupMenu.addSeparator()
|
popupMenu.addSeparator()
|
||||||
val sftpCommand = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.sftp-command"))
|
for (item in menuItems) {
|
||||||
sftpCommand.addActionListener { openSFTPPtyTab(tab, openHostAction, it) }
|
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 右键
|
* 对着 ToolBar 右键
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package app.termora.keymgr
|
|||||||
|
|
||||||
import app.termora.*
|
import app.termora.*
|
||||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||||
|
import app.termora.plugin.internal.ssh.SshClients
|
||||||
import app.termora.terminal.ControlCharacters
|
import app.termora.terminal.ControlCharacters
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
import app.termora.terminal.PtyConnectorDelegate
|
import app.termora.terminal.PtyConnectorDelegate
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import app.termora.*
|
|||||||
import app.termora.database.DatabaseManager
|
import app.termora.database.DatabaseManager
|
||||||
import app.termora.keymgr.KeyManager
|
import app.termora.keymgr.KeyManager
|
||||||
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
||||||
|
import app.termora.plugin.internal.ssh.SshClients
|
||||||
import app.termora.terminal.*
|
import app.termora.terminal.*
|
||||||
import com.formdev.flatlaf.util.SystemInfo
|
import com.formdev.flatlaf.util.SystemInfo
|
||||||
import org.apache.commons.io.Charsets
|
import org.apache.commons.io.Charsets
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora.plugin.internal.ssh
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
|
import app.termora.TerminalTabbedContextMenuExtension
|
||||||
import app.termora.plugin.Extension
|
import app.termora.plugin.Extension
|
||||||
import app.termora.plugin.InternalPlugin
|
import app.termora.plugin.InternalPlugin
|
||||||
import app.termora.protocol.ProtocolHostPanelExtension
|
import app.termora.protocol.ProtocolHostPanelExtension
|
||||||
@@ -9,6 +10,8 @@ internal class SSHInternalPlugin : InternalPlugin() {
|
|||||||
init {
|
init {
|
||||||
support.addExtension(ProtocolProviderExtension::class.java) { SSHProtocolProviderExtension.instance }
|
support.addExtension(ProtocolProviderExtension::class.java) { SSHProtocolProviderExtension.instance }
|
||||||
support.addExtension(ProtocolHostPanelExtension::class.java) { SSHProtocolHostPanelExtension.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 {
|
override fun getName(): String {
|
||||||
|
|||||||
@@ -10,41 +10,36 @@ import app.termora.keymap.KeymapManager
|
|||||||
import app.termora.terminal.ControlCharacters
|
import app.termora.terminal.ControlCharacters
|
||||||
import app.termora.terminal.DataKey
|
import app.termora.terminal.DataKey
|
||||||
import app.termora.terminal.PtyConnector
|
import app.termora.terminal.PtyConnector
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.swing.Swing
|
import kotlinx.coroutines.swing.Swing
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.apache.commons.io.Charsets
|
import org.apache.commons.io.Charsets
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import org.apache.sshd.client.SshClient
|
import org.apache.sshd.client.SshClient
|
||||||
import org.apache.sshd.client.channel.ChannelShell
|
import org.apache.sshd.client.channel.ChannelShell
|
||||||
import org.apache.sshd.client.session.ClientSession
|
import org.apache.sshd.client.session.ClientSession
|
||||||
import org.apache.sshd.common.SshConstants
|
import org.apache.sshd.common.future.CloseFuture
|
||||||
import org.apache.sshd.common.channel.Channel
|
import org.apache.sshd.common.future.SshFutureListener
|
||||||
import org.apache.sshd.common.channel.ChannelListener
|
|
||||||
import org.apache.sshd.common.session.Session
|
|
||||||
import org.apache.sshd.common.session.SessionListener
|
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import javax.swing.Icon
|
import javax.swing.Icon
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
import javax.swing.SwingUtilities
|
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 {
|
companion object {
|
||||||
val SSHSession = DataKey(ClientSession::class)
|
val SSHSession = DataKey(ClientSession::class)
|
||||||
|
internal val MySshHandler = DataKey(SshHandler::class)
|
||||||
private val log = LoggerFactory.getLogger(SSHTerminalTab::class.java)
|
private val log = LoggerFactory.getLogger(SSHTerminalTab::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
private val tab = this
|
private val owner get() = SwingUtilities.getWindowAncestor(terminalPanel)
|
||||||
|
private val tab get() = this
|
||||||
private var sshClient: SshClient? = null
|
|
||||||
private var sshSession: ClientSession? = null
|
|
||||||
private var sshChannelShell: ChannelShell? = null
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
terminalPanel.dropFiles = false
|
terminalPanel.dropFiles = false
|
||||||
@@ -55,12 +50,10 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
return terminalPanel
|
return terminalPanel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun canReconnect(): Boolean {
|
override fun canReconnect(): Boolean {
|
||||||
return !mutex.isLocked
|
return mutex.isLocked.not()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override suspend fun openPtyConnector(): PtyConnector {
|
override suspend fun openPtyConnector(): PtyConnector {
|
||||||
if (mutex.tryLock()) {
|
if (mutex.tryLock()) {
|
||||||
try {
|
try {
|
||||||
@@ -82,74 +75,32 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
// hide cursor
|
// hide cursor
|
||||||
terminalModel.setData(DataKey.Companion.ShowCursor, false)
|
terminalModel.setData(DataKey.Companion.ShowCursor, false)
|
||||||
// print
|
// print
|
||||||
terminal.write("SSH client is opening...\r\n")
|
terminal.write("Connecting to remote server ")
|
||||||
}
|
}
|
||||||
|
|
||||||
val owner = SwingUtilities.getWindowAncestor(terminalPanel)
|
val loading = coroutineScope.launch(Dispatchers.Swing) {
|
||||||
val client = SshClients.openClient(host, owner).also { sshClient = it }
|
var c = 0
|
||||||
val sessionListener = MySessionListener()
|
while (isActive) {
|
||||||
val channelListener = MyChannelListener()
|
if (++c > 6) c = 1
|
||||||
|
terminal.write("${ControlCharacters.ESC}[1;32m")
|
||||||
withContext(Dispatchers.Swing) { terminal.write("SSH client opened successfully.\r\n\r\n") }
|
terminal.write(".".repeat(c))
|
||||||
|
terminal.write(" ".repeat(6 - c))
|
||||||
client.addSessionListener(sessionListener)
|
terminal.write("${ControlCharacters.ESC}[0m")
|
||||||
client.addChannelListener(channelListener)
|
delay(350.milliseconds)
|
||||||
|
terminal.write("${ControlCharacters.BS}".repeat(6))
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// stop
|
val channel: ChannelShell
|
||||||
stop()
|
try {
|
||||||
}
|
val client = openClient()
|
||||||
}
|
val session = openSession(client)
|
||||||
})
|
channel = openChannel(session)
|
||||||
|
|
||||||
// 打开隧道
|
// 打开隧道
|
||||||
openTunnelings(session, host)
|
openTunnelings(session, host)
|
||||||
|
} finally {
|
||||||
|
loading.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
// 隐藏提示
|
// 隐藏提示
|
||||||
withContext(Dispatchers.Swing) {
|
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")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||||
if (dataKey == SSHSession) {
|
if (dataKey == SSHSession) {
|
||||||
return sshSession as T?
|
return handler.session as T?
|
||||||
|
}
|
||||||
|
if (dataKey == MySshHandler) {
|
||||||
|
return handler as T?
|
||||||
}
|
}
|
||||||
return super.getData(dataKey)
|
return super.getData(dataKey)
|
||||||
}
|
}
|
||||||
@@ -206,16 +215,7 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
if (mutex.tryLock()) {
|
if (mutex.tryLock()) {
|
||||||
try {
|
try {
|
||||||
super.stop()
|
super.stop()
|
||||||
|
handler.close()
|
||||||
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
|
|
||||||
} finally {
|
} finally {
|
||||||
mutex.unlock()
|
mutex.unlock()
|
||||||
}
|
}
|
||||||
@@ -231,36 +231,4 @@ class SSHTerminalTab(windowScope: WindowScope, host: Host) :
|
|||||||
terminalPanel.storeVisualWindows(host.id)
|
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") }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora
|
package app.termora.plugin.internal.ssh
|
||||||
|
|
||||||
|
import app.termora.*
|
||||||
import app.termora.keyboardinteractive.TerminalUserInteraction
|
import app.termora.keyboardinteractive.TerminalUserInteraction
|
||||||
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
import app.termora.keymgr.OhKeyPairKeyPairProvider
|
||||||
import app.termora.terminal.TerminalSize
|
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.ClientSessionImpl
|
||||||
import org.apache.sshd.client.session.SessionFactory
|
import org.apache.sshd.client.session.SessionFactory
|
||||||
import org.apache.sshd.common.AttributeRepository
|
import org.apache.sshd.common.AttributeRepository
|
||||||
import org.apache.sshd.common.SshConstants
|
|
||||||
import org.apache.sshd.common.SshException
|
import org.apache.sshd.common.SshException
|
||||||
import org.apache.sshd.common.channel.ChannelFactory
|
import org.apache.sshd.common.channel.ChannelFactory
|
||||||
import org.apache.sshd.common.channel.PtyChannelConfiguration
|
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.HttpClientConnector
|
||||||
import org.eclipse.jgit.internal.transport.sshd.proxy.Socks5ClientConnector
|
import org.eclipse.jgit.internal.transport.sshd.proxy.Socks5ClientConnector
|
||||||
import org.eclipse.jgit.transport.CredentialsProvider
|
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.IdentityPasswordProvider
|
||||||
import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory
|
import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -89,7 +89,7 @@ object SshClients {
|
|||||||
val HOST_KEY = AttributeRepository.AttributeKey<Host>()
|
val HOST_KEY = AttributeRepository.AttributeKey<Host>()
|
||||||
|
|
||||||
private val timeout = Duration.ofSeconds(30)
|
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) }
|
private val log by lazy { LoggerFactory.getLogger(SshClients::class.java) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -166,7 +166,7 @@ object SshClients {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val jumpHosts = mutableListOf<Host>()
|
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) {
|
for (jumpHostId in h.options.jumpHosts) {
|
||||||
val e = hosts[jumpHostId]
|
val e = hosts[jumpHostId]
|
||||||
if (e == null) {
|
if (e == null) {
|
||||||
@@ -235,16 +235,16 @@ object SshClients {
|
|||||||
if (SystemInfo.isMacOS) {
|
if (SystemInfo.isMacOS) {
|
||||||
val file = FileUtils.getFile(Application.getBaseDataDir(), "config", "ssh-agent.sock")
|
val file = FileUtils.getFile(Application.getBaseDataDir(), "config", "ssh-agent.sock")
|
||||||
if (file.exists()) {
|
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())
|
if (host.authentication.password.isNotBlank())
|
||||||
entry.setProperty(IDENTITY_AGENT, host.authentication.password)
|
entry.setProperty(SshConstants.IDENTITY_AGENT, host.authentication.password)
|
||||||
else if (SystemInfo.isWindows)
|
else if (SystemInfo.isWindows)
|
||||||
entry.setProperty(IDENTITY_AGENT, PageantConnector.DESCRIPTOR.identityAgent)
|
entry.setProperty(SshConstants.IDENTITY_AGENT, PageantConnector.DESCRIPTOR.identityAgent)
|
||||||
else
|
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")
|
throw SshException("Authentication failed")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} 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 owner = client.properties["owner"] as Window? ?: throw e
|
||||||
val askUserInfo = ask(host, entry, owner) ?: throw e
|
val askUserInfo = ask(host, entry, owner) ?: throw e
|
||||||
if (askUserInfo.authentication.type == AuthenticationType.No) throw e
|
if (askUserInfo.authentication.type == AuthenticationType.No) throw e
|
||||||
@@ -383,7 +383,7 @@ object SshClients {
|
|||||||
|
|
||||||
val channelFactories = mutableListOf<ChannelFactory>()
|
val channelFactories = mutableListOf<ChannelFactory>()
|
||||||
channelFactories.addAll(ClientBuilder.DEFAULT_CHANNEL_FACTORIES)
|
channelFactories.addAll(ClientBuilder.DEFAULT_CHANNEL_FACTORIES)
|
||||||
channelFactories.add(X11ChannelFactory.INSTANCE)
|
channelFactories.add(X11ChannelFactory.Companion.INSTANCE)
|
||||||
builder.channelFactories(channelFactories)
|
builder.channelFactories(channelFactories)
|
||||||
|
|
||||||
val sshClient = builder.build() as JGitSshClient
|
val sshClient = builder.build() as JGitSshClient
|
||||||
@@ -726,4 +726,3 @@ object SshClients {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,8 @@ package app.termora.terminal.panel.vw
|
|||||||
import app.termora.Disposer
|
import app.termora.Disposer
|
||||||
import app.termora.I18n
|
import app.termora.I18n
|
||||||
import app.termora.Icons
|
import app.termora.Icons
|
||||||
import app.termora.SshClients
|
|
||||||
import app.termora.plugin.internal.ssh.SSHTerminalTab
|
import app.termora.plugin.internal.ssh.SSHTerminalTab
|
||||||
|
import app.termora.plugin.internal.ssh.SshClients
|
||||||
import com.formdev.flatlaf.extras.FlatSVGIcon
|
import com.formdev.flatlaf.extras.FlatSVGIcon
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
import com.jgoodies.forms.layout.FormLayout
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
@@ -28,7 +28,7 @@ import javax.xml.parsers.DocumentBuilderFactory
|
|||||||
import javax.xml.xpath.XPathFactory
|
import javax.xml.xpath.XPathFactory
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
class NvidiaSMIVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
|
internal class NvidiaSMIVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
|
||||||
SSHVisualWindow(tab, "NVIDIA-SMI", visualWindowManager) {
|
SSHVisualWindow(tab, "NVIDIA-SMI", visualWindowManager) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package app.termora.terminal.panel.vw
|
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.SSHTerminalTab
|
||||||
|
import app.termora.plugin.internal.ssh.SshClients
|
||||||
import com.jgoodies.forms.builder.FormBuilder
|
import com.jgoodies.forms.builder.FormBuilder
|
||||||
import com.jgoodies.forms.layout.FormLayout
|
import com.jgoodies.forms.layout.FormLayout
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -16,7 +20,7 @@ import javax.swing.table.DefaultTableCellRenderer
|
|||||||
import javax.swing.table.DefaultTableModel
|
import javax.swing.table.DefaultTableModel
|
||||||
|
|
||||||
|
|
||||||
class SystemInformationVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
|
internal class SystemInformationVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
|
||||||
SSHVisualWindow(tab, "SystemInformation", visualWindowManager) {
|
SSHVisualWindow(tab, "SystemInformation", visualWindowManager) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import kotlin.reflect.cast
|
|||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
|
||||||
class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
|
internal class TransferVisualWindow(tab: SSHTerminalTab, visualWindowManager: VisualWindowManager) :
|
||||||
SSHVisualWindow(tab, "Transfer", visualWindowManager) {
|
SSHVisualWindow(tab, "Transfer", visualWindowManager) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package app.termora.transfer.internal.sftp
|
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.PathHandler
|
||||||
import app.termora.protocol.PathHandlerRequest
|
import app.termora.protocol.PathHandlerRequest
|
||||||
import app.termora.protocol.TransferProtocolProvider
|
import app.termora.protocol.TransferProtocolProvider
|
||||||
|
|||||||
@@ -258,6 +258,7 @@ termora.tabbed.contextmenu.rename=Rename
|
|||||||
termora.tabbed.contextmenu.sftp-command=SFTP Command
|
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.sftp-not-install=SFTP programme not found, please install and try again
|
||||||
termora.tabbed.contextmenu.clone=Clone
|
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.open-in-new-window=Open in New Window
|
||||||
termora.tabbed.contextmenu.close=Close
|
termora.tabbed.contextmenu.close=Close
|
||||||
termora.tabbed.contextmenu.close-other-tabs=Close Other Tabs
|
termora.tabbed.contextmenu.close-other-tabs=Close Other Tabs
|
||||||
|
|||||||
@@ -253,6 +253,7 @@ termora.tabbed.contextmenu.rename=重命名
|
|||||||
termora.tabbed.contextmenu.sftp-command=SFTP 终端
|
termora.tabbed.contextmenu.sftp-command=SFTP 终端
|
||||||
termora.tabbed.contextmenu.sftp-not-install=没有找到 SFTP 程序,请安装后重试
|
termora.tabbed.contextmenu.sftp-not-install=没有找到 SFTP 程序,请安装后重试
|
||||||
termora.tabbed.contextmenu.clone=克隆
|
termora.tabbed.contextmenu.clone=克隆
|
||||||
|
termora.tabbed.contextmenu.clone-session=克隆会话
|
||||||
termora.tabbed.contextmenu.open-in-new-window=在新窗口打开
|
termora.tabbed.contextmenu.open-in-new-window=在新窗口打开
|
||||||
termora.tabbed.contextmenu.close=关闭
|
termora.tabbed.contextmenu.close=关闭
|
||||||
termora.tabbed.contextmenu.close-other-tabs=关闭其他标签页
|
termora.tabbed.contextmenu.close-other-tabs=关闭其他标签页
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ termora.tabbed.contextmenu.rename=重新命名
|
|||||||
termora.tabbed.contextmenu.sftp-command=SFTP 終端
|
termora.tabbed.contextmenu.sftp-command=SFTP 終端
|
||||||
termora.tabbed.contextmenu.sftp-not-install=沒有找到 SFTP 程序,請安裝後重試
|
termora.tabbed.contextmenu.sftp-not-install=沒有找到 SFTP 程序,請安裝後重試
|
||||||
termora.tabbed.contextmenu.clone=克隆
|
termora.tabbed.contextmenu.clone=克隆
|
||||||
|
termora.tabbed.contextmenu.clone-session=克隆會話
|
||||||
termora.tabbed.contextmenu.open-in-new-window=在新視窗打開
|
termora.tabbed.contextmenu.open-in-new-window=在新視窗打開
|
||||||
termora.tabbed.contextmenu.close=關閉
|
termora.tabbed.contextmenu.close=關閉
|
||||||
termora.tabbed.contextmenu.close-other-tabs=關閉其他標籤頁
|
termora.tabbed.contextmenu.close-other-tabs=關閉其他標籤頁
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.termora
|
package app.termora
|
||||||
|
|
||||||
|
import app.termora.plugin.internal.ssh.SshClients
|
||||||
import org.apache.sshd.client.session.ClientSession
|
import org.apache.sshd.client.session.ClientSession
|
||||||
import org.testcontainers.containers.GenericContainer
|
import org.testcontainers.containers.GenericContainer
|
||||||
import kotlin.test.AfterTest
|
import kotlin.test.AfterTest
|
||||||
|
|||||||
Reference in New Issue
Block a user