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 020 package org.crsh.telnet.term.processor; 021 022 import org.crsh.cli.impl.completion.CompletionMatch; 023 import org.crsh.cli.impl.line.LineParser; 024 import org.crsh.cli.impl.line.MultiLineVisitor; 025 import org.crsh.cli.spi.Completion; 026 import org.crsh.cli.impl.Delimiter; 027 import org.crsh.shell.Shell; 028 import org.crsh.shell.ShellProcess; 029 import org.crsh.telnet.term.Term; 030 import org.crsh.telnet.term.TermEvent; 031 import org.crsh.text.Screenable; 032 import org.crsh.text.ScreenContext; 033 import org.crsh.text.Style; 034 import org.crsh.util.CloseableList; 035 import org.crsh.util.Utils; 036 037 import java.io.Closeable; 038 import java.io.IOException; 039 import java.util.Iterator; 040 import java.util.LinkedList; 041 import java.util.Map; 042 import java.util.logging.Level; 043 import java.util.logging.Logger; 044 045 public final class Processor implements Runnable, ScreenContext { 046 047 /** . */ 048 private static final String CONTINUE_PROMPT = "> "; 049 050 /** . */ 051 static final Runnable NOOP = new Runnable() { 052 public void run() { 053 } 054 }; 055 056 /** . */ 057 final Runnable WRITE_PROMPT_TASK = new Runnable() { 058 public void run() { 059 writePromptFlush(); 060 } 061 }; 062 063 /** . */ 064 final Runnable CLOSE_TASK = new Runnable() { 065 public void run() { 066 close(); 067 } 068 }; 069 070 /** . */ 071 private final Runnable READ_TERM_TASK = new Runnable() { 072 public void run() { 073 readTerm(); 074 } 075 }; 076 077 /** . */ 078 final Logger log = Logger.getLogger(Processor.class.getName()); 079 080 /** . */ 081 final Term term; 082 083 /** . */ 084 final Shell shell; 085 086 /** . */ 087 final LinkedList<TermEvent> queue; 088 089 /** . */ 090 final Object lock; 091 092 /** . */ 093 ProcessContext current; 094 095 /** . */ 096 Status status; 097 098 /** A flag useful for unit testing to know when the thread is reading. */ 099 volatile boolean waitingEvent; 100 101 /** . */ 102 private final CloseableList listeners; 103 104 /** . */ 105 private final LineParser lineBuffer; 106 107 /** . */ 108 private final MultiLineVisitor lineVisitor; 109 110 public Processor(Term term, Shell shell) { 111 this.term = term; 112 this.shell = shell; 113 this.queue = new LinkedList<TermEvent>(); 114 this.lock = new Object(); 115 this.status = Status.AVAILABLE; 116 this.listeners = new CloseableList(); 117 this.waitingEvent = false; 118 this.lineVisitor = new MultiLineVisitor(); 119 this.lineBuffer = new LineParser(lineVisitor); 120 } 121 122 public boolean isWaitingEvent() { 123 return waitingEvent; 124 } 125 126 public void run() { 127 128 129 // Display initial stuff 130 try { 131 String welcome = shell.getWelcome(); 132 log.log(Level.FINE, "Writing welcome message to term"); 133 term.append(welcome); 134 log.log(Level.FINE, "Wrote welcome message to term"); 135 writePromptFlush(); 136 } 137 catch (IOException e) { 138 e.printStackTrace(); 139 } 140 141 // 142 while (true) { 143 try { 144 if (!iterate()) { 145 break; 146 } 147 } 148 catch (IOException e) { 149 e.printStackTrace(); 150 } 151 catch (InterruptedException e) { 152 Thread.currentThread().interrupt(); 153 break; 154 } 155 } 156 } 157 158 boolean iterate() throws InterruptedException, IOException { 159 160 // 161 Runnable runnable; 162 synchronized (lock) { 163 switch (status) { 164 case AVAILABLE: 165 runnable = peekProcess(); 166 if (runnable != null) { 167 break; 168 } 169 case PROCESSING: 170 case CANCELLING: 171 runnable = READ_TERM_TASK; 172 break; 173 case CLOSED: 174 return false; 175 default: 176 throw new AssertionError(); 177 } 178 } 179 180 // 181 runnable.run(); 182 183 // 184 return true; 185 } 186 187 ProcessContext peekProcess() { 188 while (true) { 189 synchronized (lock) { 190 if (status == Status.AVAILABLE) { 191 if (queue.size() > 0) { 192 TermEvent event = queue.removeFirst(); 193 if (event instanceof TermEvent.Complete) { 194 complete(((TermEvent.Complete)event).getLine()); 195 } else { 196 String line = ((TermEvent.ReadLine)event).getLine().toString(); 197 lineBuffer.append(line); 198 if (!lineBuffer.crlf()) { 199 try { 200 term.append(CONTINUE_PROMPT); 201 term.flush(); 202 } 203 catch (IOException e) { 204 e.printStackTrace(); 205 } 206 } else { 207 String command = lineVisitor.getRaw(); 208 lineBuffer.reset(); 209 if (command.length() > 0) { 210 term.addToHistory(command); 211 } 212 ShellProcess process = shell.createProcess(command); 213 current = new ProcessContext(this, process); 214 status = Status.PROCESSING; 215 return current; 216 } 217 } 218 } else { 219 break; 220 } 221 } else { 222 break; 223 } 224 } 225 } 226 return null; 227 } 228 229 /** . */ 230 private final Object termLock = new Object(); 231 232 /** . */ 233 private boolean termReading = false; 234 235 void readTerm() { 236 237 // 238 synchronized (termLock) { 239 if (termReading) { 240 try { 241 termLock.wait(); 242 return; 243 } 244 catch (InterruptedException e) { 245 Thread.currentThread().interrupt(); 246 throw new AssertionError(e); 247 } 248 } else { 249 termReading = true; 250 } 251 } 252 253 // 254 try { 255 TermEvent event = term.read(); 256 257 // 258 Runnable runnable; 259 if (event instanceof TermEvent.Break) { 260 synchronized (lock) { 261 queue.clear(); 262 if (status == Status.PROCESSING) { 263 status = Status.CANCELLING; 264 runnable = new Runnable() { 265 ProcessContext context = current; 266 public void run() { 267 context.process.cancel(); 268 } 269 }; 270 } 271 else if (status == Status.AVAILABLE) { 272 runnable = WRITE_PROMPT_TASK; 273 } else { 274 runnable = NOOP; 275 } 276 } 277 } else if (event instanceof TermEvent.Close) { 278 synchronized (lock) { 279 queue.clear(); 280 if (status == Status.PROCESSING) { 281 runnable = new Runnable() { 282 ProcessContext context = current; 283 public void run() { 284 context.process.cancel(); 285 close(); 286 } 287 }; 288 } else if (status != Status.CLOSED) { 289 runnable = CLOSE_TASK; 290 } else { 291 runnable = NOOP; 292 } 293 status = Status.CLOSED; 294 } 295 } else { 296 synchronized (queue) { 297 queue.addLast(event); 298 runnable = NOOP; 299 } 300 } 301 302 // 303 runnable.run(); 304 } 305 catch (IOException e) { 306 log.log(Level.SEVERE, "Error when reading term", e); 307 } 308 finally { 309 synchronized (termLock) { 310 termReading = false; 311 termLock.notifyAll(); 312 } 313 } 314 } 315 316 void close() { 317 listeners.close(); 318 } 319 320 public void addListener(Closeable listener) { 321 listeners.add(listener); 322 } 323 324 @Override 325 public int getWidth() { 326 return term.getWidth(); 327 } 328 329 @Override 330 public int getHeight() { 331 return term.getHeight(); 332 } 333 334 @Override 335 public Appendable append(char c) throws IOException { 336 term.append(c); 337 return this; 338 } 339 340 @Override 341 public Appendable append(CharSequence s) throws IOException { 342 term.append(s); 343 return this; 344 } 345 346 @Override 347 public Appendable append(CharSequence csq, int start, int end) throws IOException { 348 term.append(csq, start, end); 349 return this; 350 } 351 352 @Override 353 public Screenable append(Style style) throws IOException { 354 term.append(style); 355 return this; 356 } 357 358 @Override 359 public Screenable cls() throws IOException { 360 term.cls(); 361 return this; 362 } 363 364 public void flush() throws IOException { 365 throw new UnsupportedOperationException("what does it mean?"); 366 } 367 368 void writePromptFlush() { 369 String prompt = shell.getPrompt(); 370 try { 371 StringBuilder sb = new StringBuilder("\r\n"); 372 String p = prompt == null ? "% " : prompt; 373 sb.append(p); 374 CharSequence buffer = term.getBuffer(); 375 if (buffer != null) { 376 sb.append(buffer); 377 } 378 term.append(sb); 379 term.flush(); 380 } catch (IOException e) { 381 // Todo : improve that 382 e.printStackTrace(); 383 } 384 } 385 386 private void complete(CharSequence prefix) { 387 log.log(Level.FINE, "About to get completions for " + prefix); 388 CompletionMatch completion = shell.complete(prefix.toString()); 389 Completion completions = completion.getValue(); 390 log.log(Level.FINE, "Completions for " + prefix + " are " + completions); 391 392 // 393 Delimiter delimiter = completion.getDelimiter(); 394 395 try { 396 // Try to find the greatest prefix among all the results 397 if (completions.getSize() == 0) { 398 // Do nothing 399 } else if (completions.getSize() == 1) { 400 Map.Entry<String, Boolean> entry = completions.iterator().next(); 401 Appendable buffer = term.getDirectBuffer(); 402 String insert = entry.getKey(); 403 term.getDirectBuffer().append(delimiter.escape(insert)); 404 if (entry.getValue()) { 405 buffer.append(completion.getDelimiter().getValue()); 406 } 407 } else { 408 String commonCompletion = Utils.findLongestCommonPrefix(completions.getValues()); 409 410 // Format stuff 411 int width = term.getWidth(); 412 413 // 414 String completionPrefix = completions.getPrefix(); 415 416 // Get the max length 417 int max = 0; 418 for (String suffix : completions.getValues()) { 419 max = Math.max(max, completionPrefix.length() + suffix.length()); 420 } 421 422 // Separator : use two whitespace like in BASH 423 max += 2; 424 425 // 426 StringBuilder sb = new StringBuilder().append('\n'); 427 if (max < width) { 428 int columns = width / max; 429 int index = 0; 430 for (String suffix : completions.getValues()) { 431 sb.append(completionPrefix).append(suffix); 432 for (int l = completionPrefix.length() + suffix.length();l < max;l++) { 433 sb.append(' '); 434 } 435 if (++index >= columns) { 436 index = 0; 437 sb.append('\n'); 438 } 439 } 440 if (index > 0) { 441 sb.append('\n'); 442 } 443 } else { 444 for (Iterator<String> i = completions.getValues().iterator();i.hasNext();) { 445 String suffix = i.next(); 446 sb.append(commonCompletion).append(suffix); 447 if (i.hasNext()) { 448 sb.append('\n'); 449 } 450 } 451 sb.append('\n'); 452 } 453 454 // We propose 455 term.append(sb); 456 457 // Rewrite prompt 458 writePromptFlush(); 459 460 // If we have common completion we append it now 461 if (commonCompletion.length() > 0) { 462 term.getDirectBuffer().append(delimiter.escape(commonCompletion)); 463 } 464 } 465 } 466 catch (IOException e) { 467 log.log(Level.SEVERE, "Could not write completion", e); 468 } 469 } 470 }