001 package org.crsh.term.processor; 002 003 import org.crsh.cmdline.CommandCompletion; 004 import org.crsh.cmdline.Delimiter; 005 import org.crsh.cmdline.spi.ValueCompletion; 006 import org.crsh.shell.Shell; 007 import org.crsh.shell.ShellProcess; 008 import org.crsh.term.Term; 009 import org.crsh.term.TermEvent; 010 import org.crsh.util.Strings; 011 import org.slf4j.Logger; 012 import org.slf4j.LoggerFactory; 013 014 import java.io.Closeable; 015 import java.io.IOException; 016 import java.util.ArrayList; 017 import java.util.Iterator; 018 import java.util.LinkedList; 019 import java.util.List; 020 import java.util.Map; 021 022 /** @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a> */ 023 public final class Processor implements Runnable { 024 025 /** . */ 026 static final Runnable NOOP = new Runnable() { 027 public void run() { 028 } 029 }; 030 031 /** . */ 032 final Runnable WRITE_PROMPT = new Runnable() { 033 public void run() { 034 writePrompt(); 035 } 036 }; 037 038 /** . */ 039 final Runnable CLOSE = new Runnable() { 040 public void run() { 041 close(); 042 } 043 }; 044 045 /** . */ 046 private final Runnable READ_TERM = new Runnable() { 047 public void run() { 048 readTerm(); 049 } 050 }; 051 052 /** . */ 053 final Logger log = LoggerFactory.getLogger(Processor.class); 054 055 /** . */ 056 final Term term; 057 058 /** . */ 059 final Shell shell; 060 061 /** . */ 062 final LinkedList<TermEvent> queue; 063 064 /** . */ 065 final Object lock; 066 067 /** . */ 068 ProcessContext current; 069 070 /** . */ 071 Status status; 072 073 /** A flag useful for unit testing to know when the thread is reading. */ 074 volatile boolean waitingEvent; 075 076 /** . */ 077 private final List<Closeable> listeners; 078 079 public Processor(Term term, Shell shell) { 080 this.term = term; 081 this.shell = shell; 082 this.queue = new LinkedList<TermEvent>(); 083 this.lock = new Object(); 084 this.status = Status.AVAILABLE; 085 this.listeners = new ArrayList<Closeable>(); 086 this.waitingEvent = false; 087 } 088 089 public boolean isWaitingEvent() { 090 return waitingEvent; 091 } 092 093 public void run() { 094 095 096 // Display initial stuff 097 try { 098 String welcome = shell.getWelcome(); 099 log.debug("Writing welcome message to term"); 100 term.write(welcome); 101 log.debug("Wrote welcome message to term"); 102 writePrompt(); 103 } 104 catch (IOException e) { 105 e.printStackTrace(); 106 } 107 108 // 109 while (true) { 110 try { 111 if (!iterate()) { 112 break; 113 } 114 } 115 catch (IOException e) { 116 e.printStackTrace(); 117 } 118 catch (InterruptedException e) { 119 break; 120 } 121 } 122 } 123 124 boolean iterate() throws InterruptedException, IOException { 125 126 // 127 Runnable runnable; 128 synchronized (lock) { 129 switch (status) { 130 case AVAILABLE: 131 runnable = peekProcess(); 132 if (runnable != null) { 133 break; 134 } 135 case PROCESSING: 136 case CANCELLING: 137 runnable = READ_TERM; 138 break; 139 case CLOSED: 140 return false; 141 default: 142 throw new AssertionError(); 143 } 144 } 145 146 // 147 runnable.run(); 148 149 // 150 return true; 151 } 152 153 // We assume this is called under lock synchronization 154 ProcessContext peekProcess() { 155 while (true) { 156 synchronized (lock) { 157 if (status == Status.AVAILABLE) { 158 if (queue.size() > 0) { 159 TermEvent event = queue.removeFirst(); 160 if (event instanceof TermEvent.Complete) { 161 complete(((TermEvent.Complete)event).getLine()); 162 } else { 163 String line = ((TermEvent.ReadLine)event).getLine().toString(); 164 if (line.length() > 0) { 165 term.addToHistory(line); 166 } 167 ShellProcess process = shell.createProcess(line); 168 current = new ProcessContext(this, process); 169 status = Status.PROCESSING; 170 return current; 171 } 172 } else { 173 break; 174 } 175 } else { 176 break; 177 } 178 } 179 } 180 return null; 181 } 182 183 /** . */ 184 private final Object termLock = new Object(); 185 186 private boolean reading = false; 187 188 void readTerm() { 189 190 // 191 synchronized (termLock) { 192 if (reading) { 193 try { 194 termLock.wait(); 195 return; 196 } 197 catch (InterruptedException e) { 198 throw new AssertionError(e); 199 } 200 } else { 201 reading = true; 202 } 203 } 204 205 // 206 try { 207 TermEvent event = term.read(); 208 209 // 210 Runnable runnable; 211 if (event instanceof TermEvent.Break) { 212 synchronized (lock) { 213 queue.clear(); 214 if (status == Status.PROCESSING) { 215 status = Status.CANCELLING; 216 runnable = new Runnable() { 217 ProcessContext context = current; 218 public void run() { 219 context.process.cancel(); 220 } 221 }; 222 } 223 else if (status == Status.AVAILABLE) { 224 runnable = WRITE_PROMPT; 225 } else { 226 runnable = NOOP; 227 } 228 } 229 } else if (event instanceof TermEvent.Close) { 230 synchronized (lock) { 231 queue.clear(); 232 if (status == Status.PROCESSING) { 233 runnable = new Runnable() { 234 ProcessContext context = current; 235 public void run() { 236 context.process.cancel(); 237 close(); 238 } 239 }; 240 } else if (status != Status.CLOSED) { 241 runnable = CLOSE; 242 } else { 243 runnable = NOOP; 244 } 245 status = Status.CLOSED; 246 } 247 } else { 248 synchronized (queue) { 249 queue.addLast(event); 250 runnable = NOOP; 251 } 252 } 253 254 // 255 runnable.run(); 256 } 257 catch (IOException e) { 258 log.error("Error when reading term", e); 259 } 260 finally { 261 synchronized (termLock) { 262 reading = false; 263 termLock.notifyAll(); 264 } 265 } 266 } 267 268 void close() { 269 // Make a copy 270 ArrayList<Closeable> listeners; 271 synchronized (Processor.this.listeners) { 272 listeners = new ArrayList<Closeable>(Processor.this.listeners); 273 } 274 275 // 276 for (Closeable listener : listeners) { 277 try { 278 log.debug("Closing " + listener.getClass().getSimpleName()); 279 listener.close(); 280 } 281 catch (Exception e) { 282 e.printStackTrace(); 283 } 284 } 285 } 286 287 public void addListener(Closeable listener) { 288 if (listener == null) { 289 throw new NullPointerException(); 290 } 291 synchronized (listeners) { 292 if (listeners.contains(listener)) { 293 throw new IllegalStateException("Already listening"); 294 } 295 listeners.add(listener); 296 } 297 } 298 299 void write(String text) { 300 try { 301 term.write(text); 302 } 303 catch (IOException e) { 304 log.error("Write to term failure", e); 305 } 306 } 307 308 void writePrompt() { 309 String prompt = shell.getPrompt(); 310 try { 311 String p = prompt == null ? "% " : prompt; 312 term.write("\r\n"); 313 term.write(p); 314 term.write(term.getBuffer()); 315 } catch (IOException e) { 316 e.printStackTrace(); 317 } 318 } 319 320 private void complete(CharSequence prefix) { 321 log.debug("About to get completions for " + prefix); 322 CommandCompletion completion = shell.complete(prefix.toString()); 323 ValueCompletion completions = completion.getValue(); 324 log.debug("Completions for " + prefix + " are " + completions); 325 326 // 327 Delimiter delimiter = completion.getDelimiter(); 328 329 try { 330 // Try to find the greatest prefix among all the results 331 if (completions.getSize() == 0) { 332 // Do nothing 333 } else if (completions.getSize() == 1) { 334 Map.Entry<String, Boolean> entry = completions.iterator().next(); 335 Appendable buffer = term.getInsertBuffer(); 336 String insert = entry.getKey(); 337 delimiter.escape(insert, term.getInsertBuffer()); 338 if (entry.getValue()) { 339 buffer.append(completion.getDelimiter().getValue()); 340 } 341 } else { 342 String commonCompletion = Strings.findLongestCommonPrefix(completions.getSuffixes()); 343 if (commonCompletion.length() > 0) { 344 delimiter.escape(commonCompletion, term.getInsertBuffer()); 345 } else { 346 // Format stuff 347 int width = term.getWidth(); 348 349 // 350 String completionPrefix = completions.getPrefix(); 351 352 // Get the max length 353 int max = 0; 354 for (String suffix : completions.getSuffixes()) { 355 max = Math.max(max, completionPrefix.length() + suffix.length()); 356 } 357 358 // Separator : use two whitespace like in BASH 359 max += 2; 360 361 // 362 StringBuilder sb = new StringBuilder().append('\n'); 363 if (max < width) { 364 int columns = width / max; 365 int index = 0; 366 for (String suffix : completions.getSuffixes()) { 367 sb.append(completionPrefix).append(suffix); 368 for (int l = completionPrefix.length() + suffix.length();l < max;l++) { 369 sb.append(' '); 370 } 371 if (++index >= columns) { 372 index = 0; 373 sb.append('\n'); 374 } 375 } 376 if (index > 0) { 377 sb.append('\n'); 378 } 379 } else { 380 for (Iterator<String> i = completions.getSuffixes().iterator();i.hasNext();) { 381 String suffix = i.next(); 382 sb.append(commonCompletion).append(suffix); 383 if (i.hasNext()) { 384 sb.append('\n'); 385 } 386 } 387 sb.append('\n'); 388 } 389 390 // We propose 391 term.write(sb.toString()); 392 writePrompt(); 393 } 394 } 395 } 396 catch (IOException e) { 397 log.error("Could not write completion", e); 398 } 399 } 400 }