package org.dcache.services.ssh2; import jline.TerminalSupport; import jline.console.ConsoleReader; import jline.console.history.FileHistory; import jline.console.history.MemoryHistory; import jline.console.history.PersistentHistory; import org.apache.sshd.server.Command; import org.apache.sshd.server.Environment; import org.apache.sshd.server.ExitCallback; import org.fusesource.jansi.Ansi; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; import java.io.OutputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; import diskCacheV111.admin.UserAdminShell; import dmg.cells.nucleus.NoRouteToCellException; import dmg.cells.nucleus.SerializationException; import dmg.util.CommandException; import dmg.util.CommandExitException; import dmg.util.CommandPanicException; import dmg.util.CommandSyntaxException; import org.dcache.util.Strings; import static org.fusesource.jansi.Ansi.Color.CYAN; import static org.fusesource.jansi.Ansi.Color.RED; /** * This class implements the Command Interface, which is part of the sshd-core * library allowing to access input and output stream of the ssh2Server. This * class is also the point of connecting the ssh2 streams to the * userAdminShell's input and output streams. The run() method of the thread * takes care of handling the user input. It lets the userAdminShell execute the * commands entered by the user, waits for the answer and outputs the answer to * the terminal of the user. * @author bernardt */ public class AnsiTerminalCommand implements Command, Runnable { private static final Logger _logger = LoggerFactory.getLogger(AnsiTerminalCommand.class); private final UserAdminShell _userAdminShell; private ExitCallback _exitCallback; private InputStream _in; private OutputStream _out; private Thread _adminShellThread; private ConsoleReader _console; private MemoryHistory _history; private final boolean _useColors; private PipedOutputStream _pipedOut; private PipedInputStream _pipedIn; private Thread _pipeThread; public AnsiTerminalCommand(File historyFile, int historySize, boolean useColor, UserAdminShell shell) { _useColors = useColor; _userAdminShell = shell; if (historyFile != null && (!historyFile.exists() || historyFile.isFile())) { try { _history = new FileHistory(historyFile); _history.setMaxSize(historySize); } catch (IOException e) { _logger.warn("History creation failed: " + e.getMessage()); } } } @Override public void destroy() { Thread thread = _pipeThread; if (thread != null) { thread.interrupt(); } thread = _adminShellThread; if (thread != null) { thread.interrupt(); } } @Override public void setErrorStream(OutputStream err) { } @Override public void setExitCallback(ExitCallback callback) { _exitCallback = callback; } @Override public void setInputStream(InputStream in) { _in = in; } @Override public void setOutputStream(OutputStream out) { _out = new SshOutputStream(out); } @Override public void start(Environment env) throws IOException { _pipedOut = new PipedOutputStream(); _pipedIn = new PipedInputStream(_pipedOut); _userAdminShell.setUser(env.getEnv().get(Environment.ENV_USER)); _console = new ConsoleReader(_pipedIn, _out, new ConsoleReaderTerminal(env)) { @Override public void print(CharSequence s) throws IOException { /* See https://github.com/jline/jline2/issues/205 */ getOutput().append(s); } }; _adminShellThread = new Thread(this); _adminShellThread.start(); _pipeThread = new Thread(new Pipe()); _pipeThread.start(); } @Override public void run() { try { initAdminShell(); runAsciiMode(); } catch (IOException e) { _logger.warn(e.getMessage()); } finally { try { cleanUp(); } catch (IOException e) { _logger.warn("Failed to shutdown console cleanly: " + e.getMessage()); } _exitCallback.onExit(0); } } private void initAdminShell() throws IOException { if (_history != null) { _console.setHistory(_history); } _console.addCompleter(_userAdminShell); _console.println(_userAdminShell.getHello()); _console.flush(); } private void runAsciiMode() throws IOException { Ansi.setEnabled(_useColors); while (true) { String prompt = Ansi.ansi().bold().a(_userAdminShell.getPrompt()).boldOff().toString(); Object result; try { String str = _console.readLine(prompt); try { if (str == null) { throw new CommandExitException(); } result = _userAdminShell.executeCommand(str); } catch (IllegalArgumentException e) { result = e.toString(); } catch (SerializationException e) { result = "There is a bug here, please report to support@dcache.org"; _logger.error("This must be a bug, please report to support@dcache.org.", e); } catch (CommandSyntaxException e) { result = e; } catch (CommandExitException e) { break; } catch (CommandPanicException e) { result = "Command '" + str + "' triggered a bug (" + e.getTargetException() + "); the service log file contains additional information. Please " + "contact support@dcache.org."; } catch (CommandException e) { result = e.getMessage(); } catch (NoRouteToCellException e) { result = "Cell name does not exist or cell is not started: " + e.getMessage(); _logger.warn("The cell the command was sent to is no " + "longer there: {}", e.getMessage()); } catch (RuntimeException e) { result = String.format("Command '%s' triggered a bug (%s); please" + " locate this message in the log file of the admin service and" + " send an email to support@dcache.org with this line and the" + " following stack-trace", str, e); _logger.error((String) result, e); } } catch (InterruptedIOException e) { _console.getCursorBuffer().clear(); _console.println(); result = null; } catch (InterruptedException e) { _console.println("^C"); _console.flush(); _console.getCursorBuffer().clear(); result = null; } catch (IOException e) { throw e; } catch (Exception e) { result = e.getMessage(); if(result == null) { result = e.getClass().getSimpleName() + ": (null)"; } } if (result != null) { if (result instanceof CommandSyntaxException) { CommandSyntaxException e = (CommandSyntaxException) result; Ansi sb = Ansi.ansi(); sb.fg(RED).a("Syntax error: ").a(e.getMessage()).newline(); String help = e.getHelpText(); if (help != null) { sb.fg(CYAN); sb.a("Help : ").newline(); sb.a(help); } _console.println(sb.reset().toString()); } else { String s; s = Strings.toMultilineString(result); if (!s.isEmpty()) { _console.println(s); _console.flush(); } } } _console.flush(); } } private void cleanUp() throws IOException { if (_history instanceof PersistentHistory) { ((PersistentHistory) _history).flush(); } _console.println(); _console.flush(); } private static class ConsoleReaderTerminal extends TerminalSupport { private final Environment _env; private ConsoleReaderTerminal(Environment env) { super(true); _env = env; setAnsiSupported(true); setEchoEnabled(false); } @Override public int getHeight() { String h = _env.getEnv().get(Environment.ENV_LINES); if (h != null) { try { /* The SSH client may report 0 if forced to allocate a pseudo TTY * even when it got no local TTY. */ int i = Integer.parseInt(h); return i == 0 ? Integer.MAX_VALUE : i; } catch(NumberFormatException ignored) { } } return super.getHeight(); } @Override public int getWidth() { String w = _env.getEnv().get(Environment.ENV_COLUMNS); if (w != null) { try { /* The SSH client may report 0 if forced to allocate a pseudo TTY * even when it got no local TTY. */ int i = Integer.parseInt(w); return i == 0 ? Integer.MAX_VALUE : i; } catch(NumberFormatException ignored) { } } return super.getWidth(); } } private static class SshOutputStream extends FilterOutputStream { public SshOutputStream(OutputStream out) { super(out); } @Override public void write(int c) throws IOException { if (c == '\n') { super.write(0xa); super.write(0xd); } else { super.write(c); } } } private class Pipe implements Runnable { public static final int CTRL_C = 3; public void run() { try { while (!Thread.interrupted()) { try { int c = _in.read(); if (c == -1) { return; } else if (c == CTRL_C) { _adminShellThread.interrupt(); } _pipedOut.write(c); _pipedOut.flush(); } catch (Throwable t) { return; } } } finally { try { _pipedOut.close(); } catch (IOException ignored) { } } } } }