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