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 break; 140 } 141 } 142 } 143 144 boolean iterate() throws InterruptedException, IOException { 145 146 // 147 Runnable runnable; 148 synchronized (lock) { 149 switch (status) { 150 case AVAILABLE: 151 runnable = peekProcess(); 152 if (runnable != null) { 153 break; 154 } 155 case PROCESSING: 156 case CANCELLING: 157 runnable = READ_TERM; 158 break; 159 case CLOSED: 160 return false; 161 default: 162 throw new AssertionError(); 163 } 164 } 165 166 // 167 runnable.run(); 168 169 // 170 return true; 171 } 172 173 // We assume this is called under lock synchronization 174 ProcessContext peekProcess() { 175 while (true) { 176 synchronized (lock) { 177 if (status == Status.AVAILABLE) { 178 if (queue.size() > 0) { 179 TermEvent event = queue.removeFirst(); 180 if (event instanceof TermEvent.Complete) { 181 complete(((TermEvent.Complete)event).getLine()); 182 } else { 183 String line = ((TermEvent.ReadLine)event).getLine().toString(); 184 if (line.length() > 0) { 185 term.addToHistory(line); 186 } 187 ShellProcess process = shell.createProcess(line); 188 current = new ProcessContext(this, process); 189 status = Status.PROCESSING; 190 return current; 191 } 192 } else { 193 break; 194 } 195 } else { 196 break; 197 } 198 } 199 } 200 return null; 201 } 202 203 /** . */ 204 private final Object termLock = new Object(); 205 206 private boolean reading = false; 207 208 void readTerm() { 209 210 // 211 synchronized (termLock) { 212 if (reading) { 213 try { 214 termLock.wait(); 215 return; 216 } 217 catch (InterruptedException e) { 218 throw new AssertionError(e); 219 } 220 } else { 221 reading = true; 222 } 223 } 224 225 // 226 try { 227 TermEvent event = term.read(); 228 229 // 230 Runnable runnable; 231 if (event instanceof TermEvent.Break) { 232 synchronized (lock) { 233 queue.clear(); 234 if (status == Status.PROCESSING) { 235 status = Status.CANCELLING; 236 runnable = new Runnable() { 237 ProcessContext context = current; 238 public void run() { 239 context.process.cancel(); 240 } 241 }; 242 } 243 else if (status == Status.AVAILABLE) { 244 runnable = WRITE_PROMPT; 245 } else { 246 runnable = NOOP; 247 } 248 } 249 } else if (event instanceof TermEvent.Close) { 250 synchronized (lock) { 251 queue.clear(); 252 if (status == Status.PROCESSING) { 253 runnable = new Runnable() { 254 ProcessContext context = current; 255 public void run() { 256 context.process.cancel(); 257 close(); 258 } 259 }; 260 } else if (status != Status.CLOSED) { 261 runnable = CLOSE; 262 } else { 263 runnable = NOOP; 264 } 265 status = Status.CLOSED; 266 } 267 } else { 268 synchronized (queue) { 269 queue.addLast(event); 270 runnable = NOOP; 271 } 272 } 273 274 // 275 runnable.run(); 276 } 277 catch (IOException e) { 278 log.log(Level.SEVERE, "Error when reading term", e); 279 } 280 finally { 281 synchronized (termLock) { 282 reading = false; 283 termLock.notifyAll(); 284 } 285 } 286 } 287 288 void close() { 289 listeners.close(); 290 } 291 292 public void addListener(Closeable listener) { 293 listeners.add(listener); 294 } 295 296 public Class<Chunk> getConsumedType() { 297 return Chunk.class; 298 } 299 300 public void provide(Chunk element) throws IOException { 301 term.write(element); 302 } 303 304 public void flush() throws IOException { 305 throw new UnsupportedOperationException("what does it mean?"); 306 } 307 308 void writePromptFlush() { 309 String prompt = shell.getPrompt(); 310 try { 311 StringBuilder sb = new StringBuilder("\r\n"); 312 String p = prompt == null ? "% " : prompt; 313 sb.append(p); 314 CharSequence buffer = term.getBuffer(); 315 if (buffer != null) { 316 sb.append(buffer); 317 } 318 term.write(Text.create(sb)); 319 term.flush(); 320 } catch (IOException e) { 321 // Todo : improve that 322 e.printStackTrace(); 323 } 324 } 325 326 private void complete(CharSequence prefix) { 327 log.log(Level.FINE, "About to get completions for " + prefix); 328 CompletionMatch completion = shell.complete(prefix.toString()); 329 Completion completions = completion.getValue(); 330 log.log(Level.FINE, "Completions for " + prefix + " are " + completions); 331 332 // 333 Delimiter delimiter = completion.getDelimiter(); 334 335 try { 336 // Try to find the greatest prefix among all the results 337 if (completions.getSize() == 0) { 338 // Do nothing 339 } else if (completions.getSize() == 1) { 340 Map.Entry<String, Boolean> entry = completions.iterator().next(); 341 Appendable buffer = term.getDirectBuffer(); 342 String insert = entry.getKey(); 343 term.getDirectBuffer().append(delimiter.escape(insert)); 344 if (entry.getValue()) { 345 buffer.append(completion.getDelimiter().getValue()); 346 } 347 } else { 348 String commonCompletion = Strings.findLongestCommonPrefix(completions.getValues()); 349 350 // Format stuff 351 int width = term.getWidth(); 352 353 // 354 String completionPrefix = completions.getPrefix(); 355 356 // Get the max length 357 int max = 0; 358 for (String suffix : completions.getValues()) { 359 max = Math.max(max, completionPrefix.length() + suffix.length()); 360 } 361 362 // Separator : use two whitespace like in BASH 363 max += 2; 364 365 // 366 StringBuilder sb = new StringBuilder().append('\n'); 367 if (max < width) { 368 int columns = width / max; 369 int index = 0; 370 for (String suffix : completions.getValues()) { 371 sb.append(completionPrefix).append(suffix); 372 for (int l = completionPrefix.length() + suffix.length();l < max;l++) { 373 sb.append(' '); 374 } 375 if (++index >= columns) { 376 index = 0; 377 sb.append('\n'); 378 } 379 } 380 if (index > 0) { 381 sb.append('\n'); 382 } 383 } else { 384 for (Iterator<String> i = completions.getValues().iterator();i.hasNext();) { 385 String suffix = i.next(); 386 sb.append(commonCompletion).append(suffix); 387 if (i.hasNext()) { 388 sb.append('\n'); 389 } 390 } 391 sb.append('\n'); 392 } 393 394 // We propose 395 term.write(Text.create(sb.toString())); 396 397 // Rewrite prompt 398 writePromptFlush(); 399 400 // If we have common completion we append it now 401 if (commonCompletion.length() > 0) { 402 term.getDirectBuffer().append(delimiter.escape(commonCompletion)); 403 } 404 } 405 } 406 catch (IOException e) { 407 log.log(Level.SEVERE, "Could not write completion", e); 408 } 409 } 410}