001/* 002 * Copyright (C) 2012 eXo Platform SAS. 003 * 004 * This is free software; you can redistribute it and/or modify it 005 * under the terms of the GNU Lesser General Public License as 006 * published by the Free Software Foundation; either version 2.1 of 007 * the License, or (at your option) any later version. 008 * 009 * This software is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * You should have received a copy of the GNU Lesser General Public 015 * License along with this software; if not, write to the Free 016 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 017 * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 018 */ 019 020package org.crsh.processor.jline; 021 022import jline.Terminal; 023import jline.console.ConsoleReader; 024import jline.console.completer.Completer; 025import org.crsh.cli.impl.Delimiter; 026import org.crsh.cli.impl.completion.CompletionMatch; 027import org.crsh.cli.spi.Completion; 028import org.crsh.shell.Shell; 029import org.crsh.shell.ShellProcess; 030import org.crsh.shell.ShellResponse; 031 032import java.io.IOException; 033import java.io.InputStream; 034import java.io.InterruptedIOException; 035import java.io.PrintStream; 036import java.io.PrintWriter; 037import java.util.List; 038import java.util.Map; 039import java.util.concurrent.ArrayBlockingQueue; 040import java.util.concurrent.BlockingQueue; 041import java.util.concurrent.atomic.AtomicReference; 042 043public class JLineProcessor implements Runnable, Completer { 044 045 /** . */ 046 private final Shell shell; 047 048 /** . */ 049 final ConsoleReader reader; 050 051 /** . */ 052 final PrintWriter writer; 053 054 /** . */ 055 final AtomicReference<ShellProcess> current; 056 057 /** Whether or not we switched on the alternate screen. */ 058 boolean useAlternate; 059 060 // ********* 061 062 private BlockingQueue<Integer> queue; 063 private boolean interrupt; 064 private Thread pipe; 065 volatile private boolean running; 066 volatile private boolean eof; 067 private InputStream consoleInput; 068 private InputStream in; 069 private PrintStream out; 070 private PrintStream err; 071 private Thread thread; 072 public static final String IGNORE_INTERRUPTS = "karaf.ignoreInterrupts"; 073 074 public JLineProcessor(Shell shell, InputStream in, 075 PrintStream out, 076 PrintStream err, 077 Terminal term) throws IOException { 078 079 // 080 this.consoleInput = new ConsoleInputStream(); 081 this.in = in; 082 this.out = out; 083 this.err = err; 084 this.queue = new ArrayBlockingQueue<Integer>(1024); 085 this.pipe = new Thread(new Pipe()); 086 pipe.setName("gogo shell pipe thread"); 087 pipe.setDaemon(true); 088 089 // 090 ConsoleReader reader = new ConsoleReader(null, consoleInput, out, term); 091 reader.addCompleter(this); 092 093 // 094 this.shell = shell; 095 this.reader = reader; 096 this.writer = new PrintWriter(out); 097 this.current = new AtomicReference<ShellProcess>(); 098 this.useAlternate = false; 099 } 100 101 private boolean getBoolean(String name) { 102 if (name.equals(IGNORE_INTERRUPTS)) { 103 return false; 104 } 105 else { 106 throw new UnsupportedOperationException(); 107 } 108 } 109 110 private void checkInterrupt() throws IOException { 111 if (Thread.interrupted() || interrupt) { 112 interrupt = false; 113 throw new InterruptedIOException("Keyboard interruption"); 114 } 115 } 116 117 private void interrupt() { 118 interrupt = true; 119// thread.interrupt(); 120 cancel(); 121 } 122 123 private class ConsoleInputStream extends InputStream { 124 private int read(boolean wait) throws IOException { 125 if (!running) { 126 return -1; 127 } 128 checkInterrupt(); 129 if (eof && queue.isEmpty()) { 130 return -1; 131 } 132 Integer i; 133 if (wait) { 134 try { 135 i = queue.take(); 136 } 137 catch (InterruptedException e) { 138 throw new InterruptedIOException(); 139 } 140 checkInterrupt(); 141 } 142 else { 143 i = queue.poll(); 144 } 145 if (i == null) { 146 return -1; 147 } 148 return i; 149 } 150 151 @Override 152 public int read() throws IOException { 153 return read(true); 154 } 155 156 @Override 157 public int read(byte b[], int off, int len) throws IOException { 158 if (b == null) { 159 throw new NullPointerException(); 160 } 161 else if (off < 0 || len < 0 || len > b.length - off) { 162 throw new IndexOutOfBoundsException(); 163 } 164 else if (len == 0) { 165 return 0; 166 } 167 168 int nb = 1; 169 int i = read(true); 170 if (i < 0) { 171 return -1; 172 } 173 b[off++] = (byte)i; 174 while (nb < len) { 175 i = read(false); 176 if (i < 0) { 177 return nb; 178 } 179 b[off++] = (byte)i; 180 nb++; 181 } 182 return nb; 183 } 184 185 @Override 186 public int available() throws IOException { 187 return queue.size(); 188 } 189 } 190 191 private class Pipe implements Runnable { 192 public void run() { 193 try { 194 while (running) { 195 try { 196 int c = in.read(); 197 if (c == -1) { 198 return; 199 } 200 else if (c == 4 && !getBoolean(IGNORE_INTERRUPTS)) { 201 err.println("^D"); 202 return; 203 } 204 else if (c == 3 && !getBoolean(IGNORE_INTERRUPTS)) { 205 err.println("^C"); 206 reader.getCursorBuffer().clear(); 207 interrupt(); 208 } 209 queue.put(c); 210 } 211 catch (Throwable t) { 212 return; 213 } 214 } 215 } 216 finally { 217 eof = true; 218 try { 219 queue.put(-1); 220 } 221 catch (InterruptedException e) { 222 } 223 } 224 } 225 } 226 227 private String readAndParseCommand() throws IOException { 228 String command = null; 229 boolean loop = true; 230 boolean first = true; 231 while (loop) { 232 checkInterrupt(); 233 String line = reader.readLine(first ? getPrompt() : "> "); 234 if (line == null) { 235 break; 236 } 237 if (command == null) { 238 command = line; 239 } 240 else { 241 command += " " + line; 242 } 243 if (reader.getHistory().size() == 0) { 244 reader.getHistory().add(command); 245 } 246 else { 247 // jline doesn't add blank lines to the history so we don't 248 // need to replace the command in jline's console history with 249 // an indented one 250 if (command.length() > 0 && !" ".equals(command)) { 251 reader.getHistory().replace(command); 252 } 253 } 254 try { 255 loop = false; 256 } 257 catch (Exception e) { 258 loop = true; 259 first = false; 260 } 261 } 262 return command; 263 } 264 265 // ***** 266 267 public void run() { 268 running = true; 269 pipe.start(); 270 String welcome = shell.getWelcome(); 271 writer.println(welcome); 272 writer.flush(); 273 loop(); 274 } 275 276/* 277 private String readLine() { 278 StringBuilder buffer = new StringBuilder(); 279 String prompt = getPrompt(); 280 writer.println(); 281 writer.flush(); 282 while (true) { 283 try { 284 String chunk; 285 if ((chunk = reader.readLine(prompt)) == null) { 286 return null; 287 } 288 if (chunk.length() > 0 && chunk.charAt(chunk.length() - 1) == '\\') { 289 prompt = "> "; 290 buffer.append(chunk, 0, chunk.length() - 1); 291 } else { 292 buffer.append(chunk); 293 return buffer.toString(); 294 } 295 } 296 catch (IOException e) { 297 // What should we do other than that ? 298 return null; 299 } 300 } 301 } 302*/ 303 304 private void loop() { 305 while (true) { 306 307 // 308 String line = null; 309 try { 310 line = readAndParseCommand(); 311 } 312 catch (InterruptedIOException e) { 313 continue; 314 } 315 catch (IOException e) { 316 e.printStackTrace(); 317 } 318 319 if (line == null) { 320 break; 321 } 322 323 // 324 ShellProcess process = shell.createProcess(line); 325 JLineProcessContext context = new JLineProcessContext(this); 326 current.set(process); 327 try { 328 process.execute(context); 329 try { 330 context.latch.await(); 331 } 332 catch (InterruptedException ignore) { 333 // At the moment 334 } 335 } 336 finally { 337 current.set(null); 338 } 339 340 // 341 ShellResponse response = context.resp.get(); 342 343 // Write message 344 String msg = response.getMessage(); 345 if (msg.length() > 0) { 346 writer.write(msg); 347 } 348 writer.println(); 349 writer.flush(); 350 351 // 352 if (response instanceof ShellResponse.Cancelled) { 353 // Do nothing 354 } 355 else if (response instanceof ShellResponse.Close) { 356 break; 357 } 358 } 359 } 360 361 public int complete(String buffer, int cursor, List<CharSequence> candidates) { 362 String prefix = buffer.substring(0, cursor); 363 CompletionMatch completion = shell.complete(prefix); 364 Completion vc = completion.getValue(); 365 if (vc.isEmpty()) { 366 return -1; 367 } 368 Delimiter delimiter = completion.getDelimiter(); 369 for (Map.Entry<String, Boolean> entry : vc) { 370 StringBuilder sb = new StringBuilder(); 371 sb.append(vc.getPrefix()); 372 try { 373 delimiter.escape(entry.getKey(), sb); 374 if (entry.getValue()) { 375 sb.append(completion.getDelimiter().getValue()); 376 } 377 candidates.add(sb.toString()); 378 } 379 catch (IOException ignore) { 380 } 381 } 382 return cursor - vc.getPrefix().length(); 383 } 384 385 public void cancel() { 386 ShellProcess process = current.get(); 387 if (process != null) { 388 process.cancel(); 389 } 390 else { 391 // Do nothing 392 } 393 } 394 395 String getPrompt() { 396 String prompt = shell.getPrompt(); 397 return prompt == null ? "% " : prompt; 398 } 399}