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