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 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.provide(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.provide(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 String p = prompt == null ? "% " : prompt; 312 StringBuilder sb = new StringBuilder("\r\n").append(p); 313 CharSequence buffer = term.getBuffer(); 314 if (buffer != null) { 315 sb.append(buffer); 316 } 317 term.provide(Text.create(sb)); 318 term.flush(); 319 } catch (IOException e) { 320 // Todo : improve that 321 e.printStackTrace(); 322 } 323 } 324 325 private void complete(CharSequence prefix) { 326 log.log(Level.FINE, "About to get completions for " + prefix); 327 CompletionMatch completion = shell.complete(prefix.toString()); 328 Completion completions = completion.getValue(); 329 log.log(Level.FINE, "Completions for " + prefix + " are " + completions); 330 331 // 332 Delimiter delimiter = completion.getDelimiter(); 333 334 try { 335 // Try to find the greatest prefix among all the results 336 if (completions.getSize() == 0) { 337 // Do nothing 338 } else if (completions.getSize() == 1) { 339 Map.Entry<String, Boolean> entry = completions.iterator().next(); 340 Appendable buffer = term.getDirectBuffer(); 341 String insert = entry.getKey(); 342 term.getDirectBuffer().append(delimiter.escape(insert)); 343 if (entry.getValue()) { 344 buffer.append(completion.getDelimiter().getValue()); 345 } 346 } else { 347 String commonCompletion = Strings.findLongestCommonPrefix(completions.getValues()); 348 if (commonCompletion.length() > 0) { 349 term.getDirectBuffer().append(delimiter.escape(commonCompletion)); 350 } else { 351 // Format stuff 352 int width = term.getWidth(); 353 354 // 355 String completionPrefix = completions.getPrefix(); 356 357 // Get the max length 358 int max = 0; 359 for (String suffix : completions.getValues()) { 360 max = Math.max(max, completionPrefix.length() + suffix.length()); 361 } 362 363 // Separator : use two whitespace like in BASH 364 max += 2; 365 366 // 367 StringBuilder sb = new StringBuilder().append('\n'); 368 if (max < width) { 369 int columns = width / max; 370 int index = 0; 371 for (String suffix : completions.getValues()) { 372 sb.append(completionPrefix).append(suffix); 373 for (int l = completionPrefix.length() + suffix.length();l < max;l++) { 374 sb.append(' '); 375 } 376 if (++index >= columns) { 377 index = 0; 378 sb.append('\n'); 379 } 380 } 381 if (index > 0) { 382 sb.append('\n'); 383 } 384 } else { 385 for (Iterator<String> i = completions.getValues().iterator();i.hasNext();) { 386 String suffix = i.next(); 387 sb.append(commonCompletion).append(suffix); 388 if (i.hasNext()) { 389 sb.append('\n'); 390 } 391 } 392 sb.append('\n'); 393 } 394 395 // We propose 396 term.provide(Text.create(sb.toString())); 397 writePromptFlush(); 398 } 399 } 400 } 401 catch (IOException e) { 402 log.log(Level.SEVERE, "Could not write completion", e); 403 } 404 } 405 }