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.telnet.term.processor;
021    
022    import org.crsh.cli.impl.completion.CompletionMatch;
023    import org.crsh.cli.impl.line.LineParser;
024    import org.crsh.cli.impl.line.MultiLineVisitor;
025    import org.crsh.cli.spi.Completion;
026    import org.crsh.cli.impl.Delimiter;
027    import org.crsh.shell.Shell;
028    import org.crsh.shell.ShellProcess;
029    import org.crsh.telnet.term.Term;
030    import org.crsh.telnet.term.TermEvent;
031    import org.crsh.text.Screenable;
032    import org.crsh.text.ScreenContext;
033    import org.crsh.text.Style;
034    import org.crsh.util.CloseableList;
035    import org.crsh.util.Utils;
036    
037    import java.io.Closeable;
038    import java.io.IOException;
039    import java.util.Iterator;
040    import java.util.LinkedList;
041    import java.util.Map;
042    import java.util.logging.Level;
043    import java.util.logging.Logger;
044    
045    public 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    }