From d54e9dd0582481cd96313d18239990f1fd75a7e2 Mon Sep 17 00:00:00 2001 From: stud_i_sram Date: Fri, 15 May 2026 13:44:15 +0300 Subject: [PATCH] =?UTF-8?q?-=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20=D1=81=20?= =?UTF-8?q?=D1=82=D0=B5=D1=80=D0=BC=D0=B8=D0=BD=D0=B0=D0=BB=D0=BE=D0=BC=20?= =?UTF-8?q?=D0=BF=D0=BE=20ssh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 1 + .../plugin/OpenRemoteTerminalAction.java | 88 +++++++++++++++++++ .../example/plugin/SftpSessionManager.java | 51 ++++++++--- .../com/example/plugin/SshTtyConnector.java | 79 +++++++++++++++++ src/main/resources/META-INF/plugin.xml | 7 +- 5 files changed, 212 insertions(+), 14 deletions(-) create mode 100644 src/main/java/com/example/plugin/OpenRemoteTerminalAction.java create mode 100644 src/main/java/com/example/plugin/SshTtyConnector.java diff --git a/build.gradle.kts b/build.gradle.kts index bd33d38..235eba8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { // IntelliJ Platform Gradle Plugin Dependencies Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-dependencies-extension.html intellijPlatform { intellijIdeaCommunity("2024.1.7") + bundledPlugin("org.jetbrains.plugins.terminal") testFramework(TestFrameworkType.Platform) } } diff --git a/src/main/java/com/example/plugin/OpenRemoteTerminalAction.java b/src/main/java/com/example/plugin/OpenRemoteTerminalAction.java new file mode 100644 index 0000000..e7c2539 --- /dev/null +++ b/src/main/java/com/example/plugin/OpenRemoteTerminalAction.java @@ -0,0 +1,88 @@ +package com.example.plugin; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowManager; +import com.intellij.terminal.JBTerminalWidget; +import com.intellij.ui.content.Content; +import com.intellij.ui.content.ContentFactory; +import com.jcraft.jsch.ChannelShell; +import com.jediterm.terminal.TtyConnector; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.terminal.JBTerminalSystemSettingsProvider; + +import java.util.Optional; + +public class OpenRemoteTerminalAction extends AnAction { + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + Project project = e.getProject(); + if (project == null) return; + + String serverId = ProjectServerMapping.getInstance() + .getServerIdForProject(project.getBasePath()); + + if (serverId == null) { + Messages.showErrorDialog("No server associated with this project.", "Error"); + return; + } + + Optional maybeServer = + SshServerManager.getInstance().findServer(serverId); + + if (maybeServer.isEmpty()) { + Messages.showErrorDialog("Server not found.", "Error"); + return; + } + + SshServer server = maybeServer.get(); + + try { + ChannelShell shellChannel = + SftpSessionManager.getInstance(project) + .getShellChannel(server); + + TtyConnector connector = new SshTtyConnector(shellChannel); + + ToolWindow terminalToolWindow = + ToolWindowManager.getInstance(project).getToolWindow("Terminal"); + + if (terminalToolWindow == null) { + Messages.showErrorDialog("Terminal tool window not found.", "Error"); + return; + } + + Disposable disposable = + Disposer.newDisposable(project, "Remote SSH Terminal"); + + JBTerminalWidget widget = + new JBTerminalWidget( + project, + new JBTerminalSystemSettingsProvider(), + disposable + ); + + Content content = + ContentFactory.getInstance() + .createContent(widget, "Remote: " + server.name, false); + + content.setDisposer(disposable); + terminalToolWindow.getContentManager().addContent(content); + terminalToolWindow.getContentManager().setSelectedContent(content, true); + + terminalToolWindow.activate(() -> widget.start(connector), true); + + } catch (Exception ex) { + Messages.showErrorDialog( + "Failed to open terminal: " + ex.getMessage(), + "Error" + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/plugin/SftpSessionManager.java b/src/main/java/com/example/plugin/SftpSessionManager.java index 8064f36..7434353 100644 --- a/src/main/java/com/example/plugin/SftpSessionManager.java +++ b/src/main/java/com/example/plugin/SftpSessionManager.java @@ -4,11 +4,11 @@ import com.intellij.openapi.project.Project; import com.jcraft.jsch.*; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicReference; public class SftpSessionManager { private final ConcurrentHashMap channels = new ConcurrentHashMap<>(); private final ConcurrentHashMap sessions = new ConcurrentHashMap<>(); + private final ConcurrentHashMap shellChannels = new ConcurrentHashMap<>(); private final Object lock = new Object(); public ChannelSftp getChannel(SshServer server) throws Exception { @@ -20,32 +20,59 @@ public class SftpSessionManager { channel = channels.get(server.id); if (channel != null && channel.isConnected() && !channel.isClosed()) return channel; - JSch jsch = new JSch(); - Session session = jsch.getSession(server.user, server.host, server.port); - session.setPassword(SshServerManager.getInstance().getPassword(server.id)); - java.util.Properties config = new java.util.Properties(); - config.put("StrictHostKeyChecking", "no"); - session.setConfig(config); - session.connect(5000); - + Session session = getSession(server); ChannelSftp newChannel = (ChannelSftp) session.openChannel("sftp"); newChannel.connect(3000); - - sessions.put(server.id, session); channels.put(server.id, newChannel); return newChannel; } } + public ChannelShell getShellChannel(SshServer server) throws Exception { + ChannelShell shell = shellChannels.get(server.id); + if (shell != null && shell.isConnected() && !shell.isClosed()) { + return shell; + } + synchronized (lock) { + shell = shellChannels.get(server.id); + if (shell != null && shell.isConnected() && !shell.isClosed()) return shell; + + Session session = getSession(server); + ChannelShell newShell = (ChannelShell) session.openChannel("shell"); + newShell.connect(5000); + shellChannels.put(server.id, newShell); + return newShell; + } + } + + private Session getSession(SshServer server) throws JSchException { + Session session = sessions.get(server.id); + if (session == null || !session.isConnected()) { + JSch jsch = new JSch(); + session = jsch.getSession(server.user, server.host, server.port); + session.setPassword(SshServerManager.getInstance().getPassword(server.id)); + java.util.Properties config = new java.util.Properties(); + config.put("StrictHostKeyChecking", "no"); + session.setConfig(config); + session.connect(5000); + sessions.put(server.id, session); + } + return session; + } + public void disconnect() { synchronized (lock) { + for (ChannelShell sh : shellChannels.values()) { + if (sh.isConnected()) sh.disconnect(); + } + shellChannels.clear(); for (ChannelSftp ch : channels.values()) { if (ch.isConnected()) ch.disconnect(); } + channels.clear(); for (Session s : sessions.values()) { if (s.isConnected()) s.disconnect(); } - channels.clear(); sessions.clear(); } } diff --git a/src/main/java/com/example/plugin/SshTtyConnector.java b/src/main/java/com/example/plugin/SshTtyConnector.java new file mode 100644 index 0000000..418201d --- /dev/null +++ b/src/main/java/com/example/plugin/SshTtyConnector.java @@ -0,0 +1,79 @@ +package com.example.plugin; + +import com.jcraft.jsch.ChannelShell; +import com.jediterm.terminal.TtyConnector; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.io.*; +import java.nio.charset.StandardCharsets; + +public class SshTtyConnector implements TtyConnector { + private final ChannelShell channel; + private final InputStream inputStream; + private final OutputStream outputStream; + private final Reader reader; + + public SshTtyConnector(ChannelShell channel) { + this.channel = channel; + try { + this.inputStream = channel.getInputStream(); + this.outputStream = channel.getOutputStream(); + this.reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean isConnected() { + return channel.isConnected() && !channel.isClosed(); + } + + @Override + public void close() { + if (channel.isConnected()) { + channel.disconnect(); + } + } + + @Override + public @Nullable String getName() { + return "SSH Remote"; + } + + @Override + public int read(char[] buf, int offset, int length) throws IOException { + return reader.read(buf, offset, length); + } + + @Override + public void write(String string) throws IOException { + write(string.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public void write(byte[] bytes) throws IOException { + outputStream.write(bytes); + outputStream.flush(); + } + + @Override + public void resize(@NotNull Dimension size) { + channel.setPtySize(size.width, size.height, 0, 0); + } + + @Override + public int waitFor() throws InterruptedException { + while (isConnected()) { + Thread.sleep(100); + } + return channel.getExitStatus(); + } + + @Override + public boolean ready() throws IOException { + return reader.ready(); + } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index c171ba4..47aefda 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -28,15 +28,14 @@ ]]> com.intellij.modules.platform + org.jetbrains.plugins.terminal - - @@ -61,6 +60,10 @@ class="com.example.plugin.RemoteCommandAction" text="Run Remote Command" description="Execute command on the associated server"/> +