001    /*
002     * Copyright (C) 2010 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;
021    
022    import org.crsh.shell.Shell;
023    import org.crsh.shell.ShellProcess;
024    import org.crsh.shell.ShellProcessContext;
025    import org.crsh.shell.ShellResponse;
026    import org.crsh.term.Term;
027    import org.crsh.term.TermEvent;
028    import org.crsh.util.FutureListener;
029    import org.crsh.util.LatchedFuture;
030    import org.crsh.util.Strings;
031    import org.slf4j.Logger;
032    import org.slf4j.LoggerFactory;
033    
034    import java.io.IOException;
035    import java.util.ArrayList;
036    import java.util.Iterator;
037    import java.util.List;
038    import java.util.Map;
039    import java.util.concurrent.ExecutionException;
040    import java.util.concurrent.atomic.AtomicReference;
041    
042    /**
043     * @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a>
044     * @version $Revision$
045     */
046    public class Processor implements Runnable {
047    
048      public static enum State {
049    
050        INITIAL,
051    
052        OPEN,
053    
054        WANT_CLOSE,
055    
056        CLOSED;
057    
058        /** . */
059        public final Status available;
060    
061        /** . */
062        public final Status busy;
063    
064        State() {
065          this.available = new Status(this, true);
066          this.busy = new Status(this, false);
067        }
068      }
069    
070      public static class Status {
071    
072        /** . */
073        private final State state;
074    
075        /** . */
076        private final boolean available;
077    
078        private Status(State state, boolean available) {
079          this.state = state;
080          this.available = available;
081        }
082    
083        public State getState() {
084          return state;
085        }
086    
087        public boolean isAvailable() {
088          return available;
089        }
090    
091        public boolean isBusy() {
092          return !available;
093        }
094    
095        @Override
096        public boolean equals(Object o) {
097          if (o == this) {
098            return true;
099          } else if (o instanceof Status) {
100            Status that = (Status)o;
101            return state == that.state && available == that.available;
102          } else {
103            return false;
104          }
105        }
106      }
107    
108      public interface Result {
109    
110        State getState();
111    
112      }
113    
114      /** . */
115      private final Logger log = LoggerFactory.getLogger(Processor.class);
116    
117      /** . */
118      private final Term term;
119    
120      /** . */
121      private final AtomicReference<Status> status;
122    
123      /** . */
124      private final Shell shell;
125    
126      /** The current process being executed. */
127      private volatile ShellProcess process;
128    
129      /** . */
130      private final List<ProcessorListener> listeners;
131    
132      public Processor(Term term, Shell shell) {
133        this.term = term;
134        this.status = new AtomicReference<Status>(State.INITIAL.available);
135        this.shell = shell;
136        this.process = null;
137        this.listeners = new ArrayList<ProcessorListener>();
138      }
139    
140      public void run() {
141        while (true) {
142          Result result = execute();
143          State state = result.getState();
144          if (state == State.CLOSED) {
145            break;
146          }
147        }
148      }
149    
150      public boolean isAvailable() {
151        return process == null;
152      }
153    
154      public State getState() {
155        return status.get().getState();
156      }
157    
158      private static abstract class Task {
159    
160        protected abstract LatchedFuture<State> execute();
161    
162      }
163    
164      public Result execute() {
165    
166        //
167        final Status _status = status.get();
168    
169        Task task = null;
170    
171        if (_status == State.INITIAL.available) {
172    
173          task = new Task() {
174            @Override
175            protected LatchedFuture<State> execute() {
176              try {
177                String welcome = shell.getWelcome();
178                log.debug("Writing welcome message to term");
179                term.write(welcome);
180                log.debug("Wrote welcome message to term");
181                writePrompt();
182              }
183              catch (IOException e) {
184                e.printStackTrace();
185              }
186              return new LatchedFuture<State>(State.OPEN);
187            }
188          };
189    
190        } else if (_status == State.OPEN.available) {
191    
192          task = new Task() {
193            @Override
194            protected LatchedFuture<State> execute() {
195              //
196              TermEvent event = null;
197    
198              try {
199                log.debug("About to read next term event");
200                event = term.read();
201                log.debug("Read next term event " + event);
202              } catch (IOException e) {
203                if (status.get() == State.OPEN.available) {
204                  log.error("Could not read term data", e);
205                } else {
206                  log.debug("Exception but term is considered as closed", e);
207                  // We continue it will lead to getting out of the loop
208                }
209              }
210    
211              //
212              if (event == null) {
213                // Do nothing wait until next event
214                return new LatchedFuture<State>(State.OPEN);
215              } else if (event instanceof TermEvent.ReadLine) {
216                String line = ((TermEvent.ReadLine)event).getLine().toString();
217                log.debug("Submitting command " + line);
218    
219                //
220                ShellInvoker invoker = new ShellInvoker();
221    
222                // Process
223                process = shell.createProcess(((TermEvent.ReadLine) event).getLine().toString());
224    
225                //
226                process.execute(invoker);
227    
228                //
229                if (line.length() > 0) {
230                  term.addToHistory(line);
231                }
232    
233                //
234                return invoker.result;
235              } else if (event instanceof TermEvent.Break) {
236                if (process != null) {
237                  process.cancel();
238                } else {
239                  log.debug("Ignoring action " + event);
240                  writePrompt();
241                }
242                return new LatchedFuture<State>(State.OPEN);
243              } else if (event instanceof TermEvent.Complete) {
244                TermEvent.Complete complete = (TermEvent.Complete)event;
245                String prefix = complete.getLine().toString();
246                log.debug("About to get completions for " + prefix);
247                Map<String, String> completions = shell.complete(prefix);
248                log.debug("Completions for " + prefix + " are " + completions);
249    
250                // Try to find the greatest prefix among all the results
251                String commonCompletion;
252                if (completions.size() == 0) {
253                  commonCompletion = "";
254                } else if (completions.size() == 1) {
255                  Map.Entry<String, String> entry = completions.entrySet().iterator().next();
256                  commonCompletion = entry.getKey() + entry.getValue();
257                } else {
258                  commonCompletion = Strings.findLongestCommonPrefix(completions.keySet());
259                }
260    
261                //
262                if (commonCompletion.length() > 0) {
263                  try {
264                    term.bufferInsert(commonCompletion);
265                  }
266                  catch (IOException e) {
267                    e.printStackTrace();
268                  }
269                } else {
270                  if (completions.size() > 1) {
271                    // We propose
272                    StringBuilder sb = new StringBuilder("\n");
273                    for (Iterator<String> i = completions.keySet().iterator();i.hasNext();) {
274                      String completion = i.next();
275                      sb.append(completion);
276                      if (i.hasNext()) {
277                        sb.append(" ");
278                      }
279                    }
280                    sb.append("\n");
281                    try {
282                      term.write(sb.toString());
283                    }
284                    catch (IOException e) {
285                      e.printStackTrace();
286                    }
287                    writePrompt();
288                  }
289                }
290                return new LatchedFuture<State>(State.OPEN);
291              } else if (event instanceof TermEvent.Close) {
292                return new LatchedFuture<State>(State.WANT_CLOSE);
293              } else {
294                return new LatchedFuture<State>(State.OPEN);
295              }
296            }
297          };
298        } else if (_status == State.WANT_CLOSE.available) {
299    
300          task = new Task() {
301            @Override
302            protected LatchedFuture<State> execute() {
303              //
304              log.debug("Closing processor");
305    
306              // Make a copy
307              ArrayList<ProcessorListener> listeners;
308              synchronized (Processor.this.listeners) {
309                listeners = new ArrayList<ProcessorListener>(Processor.this.listeners);
310              }
311    
312              // Status to closed, we won't process any further request
313              // it's important to set the status before closing anything to
314              // avoid a race condition because closing a listener may
315              // cause an interrupted exception
316              status.set(State.CLOSED.available);
317    
318              //
319              for (ProcessorListener listener : listeners) {
320                try {
321                  log.debug("Closing " + listener.getClass().getSimpleName());
322                  listener.closed();
323                }
324                catch (Exception e) {
325                  e.printStackTrace();
326                }
327              }
328    
329              //
330              return new LatchedFuture<State>(State.CLOSED);
331            }
332          };
333        } else {
334          //
335        }
336    
337        //
338        if (task != null) {
339    
340          // Add listener to update our state
341          final LatchedFuture<State> futureState = task.execute();
342    
343          // It will update the status
344          futureState.addListener(new FutureListener<State>() {
345            public void completed(State value) {
346              status.set(value.available);
347            }
348          });
349    
350          //
351          return new Result() {
352            public State getState() {
353              try {
354                return futureState.get();
355              } catch (InterruptedException e) {
356                return status.get().getState();
357              } catch (ExecutionException e) {
358                return status.get().getState();
359              }
360            }
361          };
362        } else {
363          return new Result() {
364            public State getState() {
365              return _status.getState();
366            }
367          };
368        }
369      }
370    
371      private void writePrompt() {
372        String prompt = shell.getPrompt();
373        try {
374          String p = prompt == null ? "% " : prompt;
375          term.write("\r\n");
376          term.write(p);
377          term.write(term.getBuffer());
378        } catch (IOException e) {
379          e.printStackTrace();
380        }
381      }
382    
383      private class ShellInvoker implements ShellProcessContext {
384    
385        /** . */
386        private final LatchedFuture<State> result = new LatchedFuture<State>();
387    
388        public int getWidth() {
389          return term.getWidth();
390        }
391    
392        public String readLine(String msg, boolean echo) {
393          try {
394            term.setEcho(echo);
395            term.write(msg);
396            TermEvent action = term.read();
397            CharSequence line = null;
398            if (action instanceof TermEvent.ReadLine) {
399              line = ((TermEvent.ReadLine)action).getLine();
400              log.debug("Read from console");
401            }
402            else {
403              log.debug("Ignoring action " + action + " returning null");
404            }
405            term.write("\r\n");
406            return line.toString();
407          }
408          catch (Exception e) {
409            log.error("Reading from console failed", e);
410            return null;
411          }
412          finally {
413            term.setEcho(true);
414          }
415        }
416    
417        public void end(ShellResponse response) {
418          try {
419    
420            //
421            if (response instanceof ShellResponse.Close) {
422              System.out.println("received close response");
423              result.set(State.WANT_CLOSE);
424            } else {
425              if (response instanceof ShellResponse.Cancelled) {
426                result.set(State.OPEN);
427              } else {
428                String ret = response.getText();
429                log.debug("Command completed with result " + ret);
430                try {
431                  term.write(ret);
432                }
433                catch (IOException e) {
434                  log.error("Write to term failure", e);
435                }
436                process = null;
437              }
438    
439              //
440              writePrompt();
441    
442              //
443              result.set(State.OPEN);
444            }
445          }
446          finally {
447            process = null;
448          }
449        }
450      }
451    
452      public void addListener(ProcessorListener listener) {
453        if (listener == null) {
454          throw new NullPointerException();
455        }
456        synchronized (listeners) {
457          if (listeners.contains(listener)) {
458            throw new IllegalStateException("Already listening");
459          }
460          listeners.add(listener);
461        }
462      }
463    }