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