Repository: karaf Updated Branches: refs/heads/master 218732544 -> 23668b681
[KARAF-3954] Convey terminal size changes through SSH Project: http://git-wip-us.apache.org/repos/asf/karaf/repo Commit: http://git-wip-us.apache.org/repos/asf/karaf/commit/23668b68 Tree: http://git-wip-us.apache.org/repos/asf/karaf/tree/23668b68 Diff: http://git-wip-us.apache.org/repos/asf/karaf/diff/23668b68 Branch: refs/heads/master Commit: 23668b681c8dd6173385d45924952292eef0acef Parents: 2187325 Author: Guillaume Nodet <[email protected]> Authored: Mon Aug 24 12:04:08 2015 +0200 Committer: Guillaume Nodet <[email protected]> Committed: Mon Aug 24 12:04:08 2015 +0200 ---------------------------------------------------------------------- .../main/java/org/apache/karaf/client/Main.java | 137 +++++++++++++++++- .../org/apache/karaf/shell/ssh/SshAction.java | 145 +++++++++++++++++-- .../org/apache/karaf/shell/ssh/SshTerminal.java | 4 + 3 files changed, 270 insertions(+), 16 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/karaf/blob/23668b68/client/src/main/java/org/apache/karaf/client/Main.java ---------------------------------------------------------------------- diff --git a/client/src/main/java/org/apache/karaf/client/Main.java b/client/src/main/java/org/apache/karaf/client/Main.java index 4c368d7..3fe2e1f 100644 --- a/client/src/main/java/org/apache/karaf/client/Main.java +++ b/client/src/main/java/org/apache/karaf/client/Main.java @@ -17,10 +17,16 @@ package org.apache.karaf.client; import java.io.*; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; import java.net.URL; import java.security.KeyPair; import java.nio.charset.Charset; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; @@ -29,6 +35,7 @@ import jline.Terminal; import jline.TerminalFactory; import jline.UnixTerminal; +import jline.internal.TerminalLineSettings; import org.apache.sshd.ClientChannel; import org.apache.sshd.ClientSession; import org.apache.sshd.SshClient; @@ -37,9 +44,14 @@ import org.apache.sshd.agent.local.AgentImpl; import org.apache.sshd.agent.local.LocalAgentFactory; import org.apache.sshd.client.UserInteraction; import org.apache.sshd.client.channel.ChannelShell; +import org.apache.sshd.client.channel.PtyCapableChannelSession; import org.apache.sshd.client.future.ConnectFuture; +import org.apache.sshd.common.PtyMode; import org.apache.sshd.common.RuntimeSshException; +import org.apache.sshd.common.Session; +import org.apache.sshd.common.SshConstants; import org.apache.sshd.common.keyprovider.FileKeyPairProvider; +import org.apache.sshd.common.util.Buffer; import org.fusesource.jansi.AnsiConsole; import org.slf4j.impl.SimpleLogger; @@ -131,8 +143,60 @@ public class Main { ConsoleInputStream in = new ConsoleInputStream(terminal.wrapInIfNeeded(System.in)); new Thread(in).start(); channel.setIn(in); - ((ChannelShell) channel).setPtyColumns(terminal != null ? terminal.getWidth() : 80); - ((ChannelShell) channel).setupSensibleDefaultPty(); + if (terminal instanceof UnixTerminal) { + TerminalLineSettings settings = ((UnixTerminal) terminal).getSettings(); + Map<PtyMode, Integer> modes = new HashMap<>(); + // Control chars + modes.put(PtyMode.VINTR, settings.getProperty("vintr")); + modes.put(PtyMode.VQUIT, settings.getProperty("vquit")); + modes.put(PtyMode.VERASE, settings.getProperty("verase")); + modes.put(PtyMode.VKILL, settings.getProperty("vkill")); + modes.put(PtyMode.VEOF, settings.getProperty("veof")); + modes.put(PtyMode.VEOL, settings.getProperty("veol")); + modes.put(PtyMode.VEOL2, settings.getProperty("veol2")); + modes.put(PtyMode.VSTART, settings.getProperty("vstart")); + modes.put(PtyMode.VSTOP, settings.getProperty("vstop")); + modes.put(PtyMode.VSUSP, settings.getProperty("vsusp")); + modes.put(PtyMode.VDSUSP, settings.getProperty("vdusp")); + modes.put(PtyMode.VREPRINT, settings.getProperty("vreprint")); + modes.put(PtyMode.VWERASE, settings.getProperty("vwerase")); + modes.put(PtyMode.VLNEXT, settings.getProperty("vlnext")); + modes.put(PtyMode.VSTATUS, settings.getProperty("vstatus")); + modes.put(PtyMode.VDISCARD, settings.getProperty("vdiscard")); + // Input flags + modes.put(PtyMode.IGNPAR, getFlag(settings, PtyMode.IGNPAR)); + modes.put(PtyMode.PARMRK, getFlag(settings, PtyMode.PARMRK)); + modes.put(PtyMode.INPCK, getFlag(settings, PtyMode.INPCK)); + modes.put(PtyMode.ISTRIP, getFlag(settings, PtyMode.ISTRIP)); + modes.put(PtyMode.INLCR, getFlag(settings, PtyMode.INLCR)); + modes.put(PtyMode.IGNCR, getFlag(settings, PtyMode.IGNCR)); + modes.put(PtyMode.ICRNL, getFlag(settings, PtyMode.ICRNL)); + modes.put(PtyMode.IXON, getFlag(settings, PtyMode.IXON)); + modes.put(PtyMode.IXANY, getFlag(settings, PtyMode.IXANY)); + modes.put(PtyMode.IXOFF, getFlag(settings, PtyMode.IXOFF)); + // Local flags + modes.put(PtyMode.ISIG, getFlag(settings, PtyMode.ISIG)); + modes.put(PtyMode.ICANON, getFlag(settings, PtyMode.ICANON)); + modes.put(PtyMode.ECHO, getFlag(settings, PtyMode.ECHO)); + modes.put(PtyMode.ECHOE, getFlag(settings, PtyMode.ECHOE)); + modes.put(PtyMode.ECHOK, getFlag(settings, PtyMode.ECHOK)); + modes.put(PtyMode.ECHONL, getFlag(settings, PtyMode.ECHONL)); + modes.put(PtyMode.NOFLSH, getFlag(settings, PtyMode.NOFLSH)); + modes.put(PtyMode.TOSTOP, getFlag(settings, PtyMode.TOSTOP)); + modes.put(PtyMode.IEXTEN, getFlag(settings, PtyMode.IEXTEN)); + // Output flags + modes.put(PtyMode.OPOST, getFlag(settings, PtyMode.OPOST)); + modes.put(PtyMode.OLCUC, getFlag(settings, PtyMode.OLCUC)); + modes.put(PtyMode.ONLCR, getFlag(settings, PtyMode.ONLCR)); + modes.put(PtyMode.OCRNL, getFlag(settings, PtyMode.OCRNL)); + modes.put(PtyMode.ONOCR, getFlag(settings, PtyMode.ONOCR)); + modes.put(PtyMode.ONLRET, getFlag(settings, PtyMode.ONLRET)); + ((ChannelShell) channel).setPtyModes(modes); + } else { + ((ChannelShell) channel).setupSensibleDefaultPty(); + } + ((ChannelShell) channel).setPtyColumns(terminal.getWidth()); + ((ChannelShell) channel).setPtyLines(terminal.getHeight()); ((ChannelShell) channel).setAgentForwarding(true); String ctype = System.getenv("LC_CTYPE"); if (ctype == null) { @@ -143,7 +207,10 @@ public class Main { } channel.setOut(AnsiConsole.wrapOutputStream(System.out)); channel.setErr(AnsiConsole.wrapOutputStream(System.err)); - channel.open(); + channel.open().verify(); + if (channel instanceof PtyCapableChannelSession) { + registerSignalHandler(terminal, (PtyCapableChannelSession) channel); + } channel.waitFor(ClientChannel.CLOSED, 0); if (channel.getExitStatus() != null) { exitStatus = channel.getExitStatus(); @@ -170,6 +237,11 @@ public class Main { System.exit(exitStatus); } + private static int getFlag(TerminalLineSettings settings, PtyMode mode) { + String name = mode.toString().toLowerCase(); + return (settings.getPropertyAsString(name) != null) ? 1 : 0; + } + private static void setupAgent(String user, String keyFile, SshClient client) { SshAgent agent; URL builtInPrivateKey = Main.class.getClassLoader().getResource("karaf.key"); @@ -250,6 +322,65 @@ public class Main { return sb.toString(); } + private static void registerSignalHandler(final Terminal terminal, final PtyCapableChannelSession channel) { + try { + Class<?> signalClass = Class.forName("sun.misc.Signal"); + Class<?> signalHandlerClass = Class.forName("sun.misc.SignalHandler"); + // Implement signal handler + Object signalHandler = Proxy.newProxyInstance(Main.class.getClassLoader(), + new Class<?>[]{signalHandlerClass}, new InvocationHandler() { + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Ugly hack to force the jline unix terminal to retrieve the width/height of the terminal + // because results are cached for 1 second. + try { + Field field = terminal.getClass().getSuperclass().getDeclaredField("settings"); + field.setAccessible(true); + Object settings = field.get(terminal); + field = settings.getClass().getDeclaredField("configLastFetched"); + field.setAccessible(true); + field.setLong(settings, 0L); + } catch (Throwable t) { + // Ignore + } + // TODO: replace with PtyCapableChannelSession#sendWindowChange + Session session = channel.getSession(); + Buffer buffer = session.createBuffer(SshConstants.SSH_MSG_CHANNEL_REQUEST); + buffer.putInt(channel.getRecipient()); + buffer.putString("window-change"); + buffer.putBoolean(false); + buffer.putInt(terminal.getWidth()); + buffer.putInt(terminal.getHeight()); + buffer.putInt(0); + buffer.putInt(0); + session.writePacket(buffer); + return null; + } + } + ); + // Register the signal handler, this code is equivalent to: + // Signal.handle(new Signal("CONT"), signalHandler); + signalClass.getMethod("handle", signalClass, signalHandlerClass).invoke(null, signalClass.getConstructor(String.class).newInstance("WINCH"), signalHandler); + } catch (Exception e) { + // Ignore this exception, if the above failed, the signal API is incompatible with what we're expecting + + } + } + + private static void unregisterSignalHandler() { + try { + Class<?> signalClass = Class.forName("sun.misc.Signal"); + Class<?> signalHandlerClass = Class.forName("sun.misc.SignalHandler"); + + Object signalHandler = signalHandlerClass.getField("SIG_DFL").get(null); + // Register the signal handler, this code is equivalent to: + // Signal.handle(new Signal("CONT"), signalHandler); + signalClass.getMethod("handle", signalClass, signalHandlerClass).invoke(null, signalClass.getConstructor(String.class).newInstance("WINCH"), signalHandler); + } catch (Exception e) { + // Ignore this exception, if the above failed, the signal API is incompatible with what we're expecting + + } + } + private static class ConsoleInputStream extends InputStream implements Runnable { private InputStream in; http://git-wip-us.apache.org/repos/asf/karaf/blob/23668b68/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/SshAction.java ---------------------------------------------------------------------- diff --git a/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/SshAction.java b/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/SshAction.java index 81d6829..444f09e 100644 --- a/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/SshAction.java +++ b/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/SshAction.java @@ -19,10 +19,15 @@ package org.apache.karaf.shell.ssh; import java.io.*; +import java.lang.reflect.Field; import java.net.URL; import java.security.KeyPair; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import jline.UnixTerminal; +import jline.internal.TerminalLineSettings; import org.apache.karaf.shell.api.action.Action; import org.apache.karaf.shell.api.action.Argument; import org.apache.karaf.shell.api.action.Command; @@ -30,6 +35,8 @@ import org.apache.karaf.shell.api.action.Option; import org.apache.karaf.shell.api.action.lifecycle.Reference; import org.apache.karaf.shell.api.action.lifecycle.Service; import org.apache.karaf.shell.api.console.Session; +import org.apache.karaf.shell.api.console.Signal; +import org.apache.karaf.shell.api.console.SignalListener; import org.apache.karaf.shell.api.console.Terminal; import org.apache.sshd.ClientChannel; import org.apache.sshd.ClientSession; @@ -41,7 +48,11 @@ import org.apache.sshd.client.ServerKeyVerifier; import org.apache.sshd.client.UserInteraction; import org.apache.sshd.client.channel.ChannelShell; import org.apache.sshd.client.future.ConnectFuture; +import org.apache.sshd.common.PtyMode; +import org.apache.sshd.common.SshConstants; +import org.apache.sshd.common.channel.AbstractChannel; import org.apache.sshd.common.keyprovider.FileKeyPairProvider; +import org.apache.sshd.common.util.Buffer; import org.apache.sshd.common.util.NoCloseInputStream; import org.apache.sshd.common.util.NoCloseOutputStream; import org.slf4j.Logger; @@ -152,25 +163,123 @@ public class SshAction implements Action { sb.append(cmd); } } - ClientChannel channel; if (sb.length() > 0) { - channel = sshSession.createChannel("exec", sb.append("\n").toString()); + ClientChannel channel = sshSession.createChannel("exec", sb.append("\n").toString()); channel.setIn(new ByteArrayInputStream(new byte[0])); - } else { - channel = sshSession.createChannel("shell"); - channel.setIn(new NoCloseInputStream(System.in)); - ((ChannelShell) channel).setPtyColumns(getTermWidth()); - ((ChannelShell) channel).setupSensibleDefaultPty(); - ((ChannelShell) channel).setAgentForwarding(true); + channel.setOut(new NoCloseOutputStream(System.out)); + channel.setErr(new NoCloseOutputStream(System.err)); + channel.open().verify(); + channel.waitFor(ClientChannel.CLOSED, 0); + } else if (session.getTerminal() != null) { + final ChannelShell channel = sshSession.createShellChannel(); + final jline.Terminal jlineTerminal = (jline.Terminal) session.get(".jline.terminal"); + if (jlineTerminal instanceof UnixTerminal) { + TerminalLineSettings settings = ((UnixTerminal) jlineTerminal).getSettings(); + Map<PtyMode, Integer> modes = new HashMap<>(); + // Control chars + modes.put(PtyMode.VINTR, settings.getProperty("vintr")); + modes.put(PtyMode.VQUIT, settings.getProperty("vquit")); + modes.put(PtyMode.VERASE, settings.getProperty("verase")); + modes.put(PtyMode.VKILL, settings.getProperty("vkill")); + modes.put(PtyMode.VEOF, settings.getProperty("veof")); + modes.put(PtyMode.VEOL, settings.getProperty("veol")); + modes.put(PtyMode.VEOL2, settings.getProperty("veol2")); + modes.put(PtyMode.VSTART, settings.getProperty("vstart")); + modes.put(PtyMode.VSTOP, settings.getProperty("vstop")); + modes.put(PtyMode.VSUSP, settings.getProperty("vsusp")); + modes.put(PtyMode.VDSUSP, settings.getProperty("vdusp")); + modes.put(PtyMode.VREPRINT, settings.getProperty("vreprint")); + modes.put(PtyMode.VWERASE, settings.getProperty("vwerase")); + modes.put(PtyMode.VLNEXT, settings.getProperty("vlnext")); + modes.put(PtyMode.VSTATUS, settings.getProperty("vstatus")); + modes.put(PtyMode.VDISCARD, settings.getProperty("vdiscard")); + // Input flags + modes.put(PtyMode.IGNPAR, getFlag(settings, PtyMode.IGNPAR)); + modes.put(PtyMode.PARMRK, getFlag(settings, PtyMode.PARMRK)); + modes.put(PtyMode.INPCK, getFlag(settings, PtyMode.INPCK)); + modes.put(PtyMode.ISTRIP, getFlag(settings, PtyMode.ISTRIP)); + modes.put(PtyMode.INLCR, getFlag(settings, PtyMode.INLCR)); + modes.put(PtyMode.IGNCR, getFlag(settings, PtyMode.IGNCR)); + modes.put(PtyMode.ICRNL, getFlag(settings, PtyMode.ICRNL)); + modes.put(PtyMode.IXON, getFlag(settings, PtyMode.IXON)); + modes.put(PtyMode.IXANY, getFlag(settings, PtyMode.IXANY)); + modes.put(PtyMode.IXOFF, getFlag(settings, PtyMode.IXOFF)); + // Local flags + modes.put(PtyMode.ISIG, getFlag(settings, PtyMode.ISIG)); + modes.put(PtyMode.ICANON, getFlag(settings, PtyMode.ICANON)); + modes.put(PtyMode.ECHO, getFlag(settings, PtyMode.ECHO)); + modes.put(PtyMode.ECHOE, getFlag(settings, PtyMode.ECHOE)); + modes.put(PtyMode.ECHOK, getFlag(settings, PtyMode.ECHOK)); + modes.put(PtyMode.ECHONL, getFlag(settings, PtyMode.ECHONL)); + modes.put(PtyMode.NOFLSH, getFlag(settings, PtyMode.NOFLSH)); + modes.put(PtyMode.TOSTOP, getFlag(settings, PtyMode.TOSTOP)); + modes.put(PtyMode.IEXTEN, getFlag(settings, PtyMode.IEXTEN)); + // Output flags + modes.put(PtyMode.OPOST, getFlag(settings, PtyMode.OPOST)); + modes.put(PtyMode.OLCUC, getFlag(settings, PtyMode.OLCUC)); + modes.put(PtyMode.ONLCR, getFlag(settings, PtyMode.ONLCR)); + modes.put(PtyMode.OCRNL, getFlag(settings, PtyMode.OCRNL)); + modes.put(PtyMode.ONOCR, getFlag(settings, PtyMode.ONOCR)); + modes.put(PtyMode.ONLRET, getFlag(settings, PtyMode.ONLRET)); + channel.setPtyModes(modes); + } else if (session.getTerminal() instanceof SshTerminal) { + channel.setPtyModes(((SshTerminal) session.getTerminal()).getEnvironment().getPtyModes()); + } else { + channel.setupSensibleDefaultPty(); + } + channel.setPtyColumns(getTermWidth()); + channel.setPtyLines(getTermHeight()); + channel.setAgentForwarding(true); + channel.setEnv("TERM", session.getTerminal().getType()); Object ctype = session.get("LC_CTYPE"); if (ctype != null) { - ((ChannelShell) channel).setEnv("LC_CTYPE", ctype.toString()); + channel.setEnv("LC_CTYPE", ctype.toString()); + } + channel.setIn(new NoCloseInputStream(System.in)); + channel.setOut(new NoCloseOutputStream(System.out)); + channel.setErr(new NoCloseOutputStream(System.err)); + channel.open().verify(); + SignalListener signalListener = new SignalListener() { + @Override + public void signal(Signal signal) { + try { + // Ugly hack to force the jline unix terminal to retrieve the width/height of the terminal + // because results are cached for 1 second. + try { + Field field = jlineTerminal.getClass().getSuperclass().getDeclaredField("settings"); + field.setAccessible(true); + Object settings = field.get(jlineTerminal); + field = settings.getClass().getDeclaredField("configLastFetched"); + field.setAccessible(true); + field.setLong(settings, 0L); + } catch (Throwable t) { + // Ignore + } + // TODO: replace with PtyCapableChannelSession#sendWindowChange + org.apache.sshd.common.Session sshSession = ((AbstractChannel) channel).getSession(); + Buffer buffer = sshSession.createBuffer(SshConstants.SSH_MSG_CHANNEL_REQUEST); + buffer.putInt(channel.getRecipient()); + buffer.putString("window-change"); + buffer.putBoolean(false); + buffer.putInt(session.getTerminal().getWidth()); + buffer.putInt(session.getTerminal().getHeight()); + buffer.putInt(0); + buffer.putInt(0); + sshSession.writePacket(buffer); + } catch (IOException e) { + // Ignore + } + } + }; + session.getTerminal().addSignalListener(signalListener, Signal.WINCH); + try { + channel.waitFor(ClientChannel.CLOSED, 0); + } finally { + session.getTerminal().removeSignalListener(signalListener); } + } else { + throw new IllegalStateException("No terminal for interactive ssh session"); } - channel.setOut(new NoCloseOutputStream(System.out)); - channel.setErr(new NoCloseOutputStream(System.err)); - channel.open().verify(); - channel.waitFor(ClientChannel.CLOSED, 0); } finally { session.put(Session.IGNORE_INTERRUPTS, oldIgnoreInterrupts); sshSession.close(false); @@ -182,11 +291,21 @@ public class SshAction implements Action { return null; } + private int getFlag(TerminalLineSettings settings, PtyMode mode) { + String name = mode.toString().toLowerCase(); + return (settings.getPropertyAsString(name) != null) ? 1 : 0; + } + private int getTermWidth() { Terminal term = session.getTerminal(); return term != null ? term.getWidth() : 80; } + private int getTermHeight() { + Terminal term = session.getTerminal(); + return term != null ? term.getHeight() : 25; + } + private void setupAgent(String user, String keyFile, SshClient client) { SshAgent agent; URL url = getClass().getClassLoader().getResource("karaf.key"); http://git-wip-us.apache.org/repos/asf/karaf/blob/23668b68/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/SshTerminal.java ---------------------------------------------------------------------- diff --git a/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/SshTerminal.java b/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/SshTerminal.java index 155335d..6c1213a 100644 --- a/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/SshTerminal.java +++ b/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/SshTerminal.java @@ -82,4 +82,8 @@ public class SshTerminal extends SignalSupport implements Terminal { echo = enabled; } + public Environment getEnvironment() { + return environment; + } + }
