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.telnet.term.processor; 021 022import org.crsh.cli.impl.completion.CompletionMatch; 023import org.crsh.cli.impl.line.LineParser; 024import org.crsh.cli.impl.line.MultiLineVisitor; 025import org.crsh.cli.spi.Completion; 026import org.crsh.io.Consumer; 027import org.crsh.cli.impl.Delimiter; 028import org.crsh.shell.Shell; 029import org.crsh.shell.ShellProcess; 030import org.crsh.telnet.term.Term; 031import org.crsh.telnet.term.TermEvent; 032import org.crsh.text.Chunk; 033import org.crsh.text.Text; 034import org.crsh.util.CloseableList; 035import org.crsh.util.Utils; 036 037import java.io.Closeable; 038import java.io.IOException; 039import java.util.Iterator; 040import java.util.LinkedList; 041import java.util.Map; 042import java.util.logging.Level; 043import java.util.logging.Logger; 044 045public final class Processor implements Runnable, Consumer<Chunk> { 046 047 /** . */ 048 private static final Text CONTINUE_PROMPT = Text.create("> "); 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.write(Text.create(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.write(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 public Class<Chunk> getConsumedType() { 325 return Chunk.class; 326 } 327 328 public void provide(Chunk element) throws IOException { 329 term.write(element); 330 } 331 332 public void flush() throws IOException { 333 throw new UnsupportedOperationException("what does it mean?"); 334 } 335 336 void writePromptFlush() { 337 String prompt = shell.getPrompt(); 338 try { 339 StringBuilder sb = new StringBuilder("\r\n"); 340 String p = prompt == null ? "% " : prompt; 341 sb.append(p); 342 CharSequence buffer = term.getBuffer(); 343 if (buffer != null) { 344 sb.append(buffer); 345 } 346 term.write(Text.create(sb)); 347 term.flush(); 348 } catch (IOException e) { 349 // Todo : improve that 350 e.printStackTrace(); 351 } 352 } 353 354 private void complete(CharSequence prefix) { 355 log.log(Level.FINE, "About to get completions for " + prefix); 356 CompletionMatch completion = shell.complete(prefix.toString()); 357 Completion completions = completion.getValue(); 358 log.log(Level.FINE, "Completions for " + prefix + " are " + completions); 359 360 // 361 Delimiter delimiter = completion.getDelimiter(); 362 363 try { 364 // Try to find the greatest prefix among all the results 365 if (completions.getSize() == 0) { 366 // Do nothing 367 } else if (completions.getSize() == 1) { 368 Map.Entry<String, Boolean> entry = completions.iterator().next(); 369 Appendable buffer = term.getDirectBuffer(); 370 String insert = entry.getKey(); 371 term.getDirectBuffer().append(delimiter.escape(insert)); 372 if (entry.getValue()) { 373 buffer.append(completion.getDelimiter().getValue()); 374 } 375 } else { 376 String commonCompletion = Utils.findLongestCommonPrefix(completions.getValues()); 377 378 // Format stuff 379 int width = term.getWidth(); 380 381 // 382 String completionPrefix = completions.getPrefix(); 383 384 // Get the max length 385 int max = 0; 386 for (String suffix : completions.getValues()) { 387 max = Math.max(max, completionPrefix.length() + suffix.length()); 388 } 389 390 // Separator : use two whitespace like in BASH 391 max += 2; 392 393 // 394 StringBuilder sb = new StringBuilder().append('\n'); 395 if (max < width) { 396 int columns = width / max; 397 int index = 0; 398 for (String suffix : completions.getValues()) { 399 sb.append(completionPrefix).append(suffix); 400 for (int l = completionPrefix.length() + suffix.length();l < max;l++) { 401 sb.append(' '); 402 } 403 if (++index >= columns) { 404 index = 0; 405 sb.append('\n'); 406 } 407 } 408 if (index > 0) { 409 sb.append('\n'); 410 } 411 } else { 412 for (Iterator<String> i = completions.getValues().iterator();i.hasNext();) { 413 String suffix = i.next(); 414 sb.append(commonCompletion).append(suffix); 415 if (i.hasNext()) { 416 sb.append('\n'); 417 } 418 } 419 sb.append('\n'); 420 } 421 422 // We propose 423 term.write(Text.create(sb.toString())); 424 425 // Rewrite prompt 426 writePromptFlush(); 427 428 // If we have common completion we append it now 429 if (commonCompletion.length() > 0) { 430 term.getDirectBuffer().append(delimiter.escape(commonCompletion)); 431 } 432 } 433 } 434 catch (IOException e) { 435 log.log(Level.SEVERE, "Could not write completion", e); 436 } 437 } 438}