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.jline;
021
022import jline.Terminal;
023import jline.console.ConsoleReader;
024import jline.console.completer.Completer;
025import org.crsh.cli.impl.Delimiter;
026import org.crsh.cli.impl.completion.CompletionMatch;
027import org.crsh.cli.spi.Completion;
028import org.crsh.shell.Shell;
029import org.crsh.shell.ShellProcess;
030import org.crsh.shell.ShellResponse;
031
032import java.io.IOException;
033import java.io.InputStream;
034import java.io.InterruptedIOException;
035import java.io.PrintStream;
036import java.io.PrintWriter;
037import java.util.List;
038import java.util.Map;
039import java.util.concurrent.ArrayBlockingQueue;
040import java.util.concurrent.BlockingQueue;
041import java.util.concurrent.atomic.AtomicReference;
042
043public class JLineProcessor implements Runnable, Completer {
044
045  /** . */
046  private final Shell shell;
047
048  /** . */
049  final ConsoleReader reader;
050
051  /** . */
052  final PrintWriter writer;
053
054  /** . */
055  final AtomicReference<ShellProcess> current;
056
057  /** Whether or not we switched on the alternate screen. */
058  boolean useAlternate;
059
060  // *********
061
062  private BlockingQueue<Integer> queue;
063  private boolean interrupt;
064  private Thread pipe;
065  volatile private boolean running;
066  volatile private boolean eof;
067  private InputStream consoleInput;
068  private InputStream in;
069  private PrintStream out;
070  private PrintStream err;
071  private Thread thread;
072  public static final String IGNORE_INTERRUPTS = "karaf.ignoreInterrupts";
073
074  public JLineProcessor(Shell shell, InputStream in,
075                        PrintStream out,
076                        PrintStream err,
077                        Terminal term) throws IOException {
078
079    //
080    this.consoleInput = new ConsoleInputStream();
081    this.in = in;
082    this.out = out;
083    this.err = err;
084    this.queue = new ArrayBlockingQueue<Integer>(1024);
085    this.pipe = new Thread(new Pipe());
086    pipe.setName("gogo shell pipe thread");
087    pipe.setDaemon(true);
088
089    //
090    ConsoleReader reader = new ConsoleReader(null, consoleInput, out, term);
091    reader.addCompleter(this);
092
093    //
094    this.shell = shell;
095    this.reader = reader;
096    this.writer = new PrintWriter(out);
097    this.current = new AtomicReference<ShellProcess>();
098    this.useAlternate = false;
099  }
100
101  private boolean getBoolean(String name) {
102    if (name.equals(IGNORE_INTERRUPTS)) {
103      return false;
104    }
105    else {
106      throw new UnsupportedOperationException();
107    }
108  }
109
110  private void checkInterrupt() throws IOException {
111    if (Thread.interrupted() || interrupt) {
112      interrupt = false;
113      throw new InterruptedIOException("Keyboard interruption");
114    }
115  }
116
117  private void interrupt() {
118    interrupt = true;
119//    thread.interrupt();
120    cancel();
121  }
122
123  private class ConsoleInputStream extends InputStream {
124    private int read(boolean wait) throws IOException {
125      if (!running) {
126        return -1;
127      }
128      checkInterrupt();
129      if (eof && queue.isEmpty()) {
130        return -1;
131      }
132      Integer i;
133      if (wait) {
134        try {
135          i = queue.take();
136        }
137        catch (InterruptedException e) {
138          throw new InterruptedIOException();
139        }
140        checkInterrupt();
141      }
142      else {
143        i = queue.poll();
144      }
145      if (i == null) {
146        return -1;
147      }
148      return i;
149    }
150
151    @Override
152    public int read() throws IOException {
153      return read(true);
154    }
155
156    @Override
157    public int read(byte b[], int off, int len) throws IOException {
158      if (b == null) {
159        throw new NullPointerException();
160      }
161      else if (off < 0 || len < 0 || len > b.length - off) {
162        throw new IndexOutOfBoundsException();
163      }
164      else if (len == 0) {
165        return 0;
166      }
167
168      int nb = 1;
169      int i = read(true);
170      if (i < 0) {
171        return -1;
172      }
173      b[off++] = (byte)i;
174      while (nb < len) {
175        i = read(false);
176        if (i < 0) {
177          return nb;
178        }
179        b[off++] = (byte)i;
180        nb++;
181      }
182      return nb;
183    }
184
185    @Override
186    public int available() throws IOException {
187      return queue.size();
188    }
189  }
190
191  private class Pipe implements Runnable {
192    public void run() {
193      try {
194        while (running) {
195          try {
196            int c = in.read();
197            if (c == -1) {
198              return;
199            }
200            else if (c == 4 && !getBoolean(IGNORE_INTERRUPTS)) {
201              err.println("^D");
202              return;
203            }
204            else if (c == 3 && !getBoolean(IGNORE_INTERRUPTS)) {
205              err.println("^C");
206              reader.getCursorBuffer().clear();
207              interrupt();
208            }
209            queue.put(c);
210          }
211          catch (Throwable t) {
212            return;
213          }
214        }
215      }
216      finally {
217        eof = true;
218        try {
219          queue.put(-1);
220        }
221        catch (InterruptedException e) {
222        }
223      }
224    }
225  }
226
227  private String readAndParseCommand() throws IOException {
228    String command = null;
229    boolean loop = true;
230    boolean first = true;
231    while (loop) {
232      checkInterrupt();
233      String line = reader.readLine(first ? getPrompt() : "> ");
234      if (line == null) {
235        break;
236      }
237      if (command == null) {
238        command = line;
239      }
240      else {
241        command += " " + line;
242      }
243      if (reader.getHistory().size() == 0) {
244        reader.getHistory().add(command);
245      }
246      else {
247        // jline doesn't add blank lines to the history so we don't
248        // need to replace the command in jline's console history with
249        // an indented one
250        if (command.length() > 0 && !" ".equals(command)) {
251          reader.getHistory().replace(command);
252        }
253      }
254      try {
255        loop = false;
256      }
257      catch (Exception e) {
258        loop = true;
259        first = false;
260      }
261    }
262    return command;
263  }
264
265  // *****
266
267  public void run() {
268    running = true;
269    pipe.start();
270    String welcome = shell.getWelcome();
271    writer.println(welcome);
272    writer.flush();
273    loop();
274  }
275
276/*
277  private String readLine() {
278    StringBuilder buffer = new StringBuilder();
279    String prompt = getPrompt();
280    writer.println();
281    writer.flush();
282    while (true) {
283      try {
284        String chunk;
285        if ((chunk = reader.readLine(prompt)) == null) {
286          return null;
287        }
288        if (chunk.length() > 0 && chunk.charAt(chunk.length() - 1) == '\\') {
289          prompt = "> ";
290          buffer.append(chunk, 0, chunk.length() - 1);
291        } else {
292          buffer.append(chunk);
293          return buffer.toString();
294        }
295      }
296      catch (IOException e) {
297        // What should we do other than that ?
298        return null;
299      }
300    }
301  }
302*/
303
304  private void loop() {
305    while (true) {
306
307      //
308      String line = null;
309      try {
310        line = readAndParseCommand();
311      }
312      catch (InterruptedIOException e) {
313        continue;
314      }
315      catch (IOException e) {
316        e.printStackTrace();
317      }
318
319      if (line == null) {
320        break;
321      }
322
323      //
324      ShellProcess process = shell.createProcess(line);
325      JLineProcessContext context = new JLineProcessContext(this);
326      current.set(process);
327      try {
328        process.execute(context);
329        try {
330          context.latch.await();
331        }
332        catch (InterruptedException ignore) {
333          // At the moment
334        }
335      }
336      finally {
337        current.set(null);
338      }
339
340      //
341      ShellResponse response = context.resp.get();
342
343      // Write message
344      String msg = response.getMessage();
345      if (msg.length() > 0) {
346        writer.write(msg);
347      }
348      writer.println();
349      writer.flush();
350
351      //
352      if (response instanceof ShellResponse.Cancelled) {
353        // Do nothing
354      }
355      else if (response instanceof ShellResponse.Close) {
356        break;
357      }
358    }
359  }
360
361  public int complete(String buffer, int cursor, List<CharSequence> candidates) {
362    String prefix = buffer.substring(0, cursor);
363    CompletionMatch completion = shell.complete(prefix);
364    Completion vc = completion.getValue();
365    if (vc.isEmpty()) {
366      return -1;
367    }
368    Delimiter delimiter = completion.getDelimiter();
369    for (Map.Entry<String, Boolean> entry : vc) {
370      StringBuilder sb = new StringBuilder();
371      sb.append(vc.getPrefix());
372      try {
373        delimiter.escape(entry.getKey(), sb);
374        if (entry.getValue()) {
375          sb.append(completion.getDelimiter().getValue());
376        }
377        candidates.add(sb.toString());
378      }
379      catch (IOException ignore) {
380      }
381    }
382    return cursor - vc.getPrefix().length();
383  }
384
385  public void cancel() {
386    ShellProcess process = current.get();
387    if (process != null) {
388      process.cancel();
389    }
390    else {
391      // Do nothing
392    }
393  }
394
395  String getPrompt() {
396    String prompt = shell.getPrompt();
397    return prompt == null ? "% " : prompt;
398  }
399}