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.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) {
|
||||
popupMenu.addSeparator()
|
||||
val sftpCommand = popupMenu.add(I18n.getString("termora.tabbed.contextmenu.sftp-command"))
|
||||
sftpCommand.addActionListener { openSFTPPtyTab(tab, openHostAction, it) }
|
||||
}
|
||||
if (menuItems.isNotEmpty()) {
|
||||
popupMenu.addSeparator()
|
||||
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 右键
|
||||
*/
|
||||
|
||||
@@ -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.keyboardinteractive.TerminalUserInteraction
|
||||
import app.termora.plugin.internal.ssh.SshClients
|
||||
import app.termora.terminal.ControlCharacters
|
||||
import app.termora.terminal.DataKey
|
||||
import app.termora.terminal.PtyConnectorDelegate
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stop
|
||||
stop()
|
||||
}
|
||||
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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 打开隧道
|
||||
openTunnelings(session, host)
|
||||
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") }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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.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 {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=关闭其他标签页
|
||||
|
||||
@@ -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=關閉其他標籤頁
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user