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