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.io.Consumer;
027 import org.crsh.cli.impl.Delimiter;
028 import org.crsh.shell.Shell;
029 import org.crsh.shell.ShellProcess;
030 import org.crsh.telnet.term.Term;
031 import org.crsh.telnet.term.TermEvent;
032 import org.crsh.text.Chunk;
033 import org.crsh.text.Text;
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, Consumer<Chunk> {
046
047 /** . */
048 private static final Text CONTINUE_PROMPT = Text.create("> ");
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.write(Text.create(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.write(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 public Class<Chunk> getConsumedType() {
325 return Chunk.class;
326 }
327
328 public void provide(Chunk element) throws IOException {
329 term.write(element);
330 }
331
332 public void flush() throws IOException {
333 throw new UnsupportedOperationException("what does it mean?");
334 }
335
336 void writePromptFlush() {
337 String prompt = shell.getPrompt();
338 try {
339 StringBuilder sb = new StringBuilder("\r\n");
340 String p = prompt == null ? "% " : prompt;
341 sb.append(p);
342 CharSequence buffer = term.getBuffer();
343 if (buffer != null) {
344 sb.append(buffer);
345 }
346 term.write(Text.create(sb));
347 term.flush();
348 } catch (IOException e) {
349 // Todo : improve that
350 e.printStackTrace();
351 }
352 }
353
354 private void complete(CharSequence prefix) {
355 log.log(Level.FINE, "About to get completions for " + prefix);
356 CompletionMatch completion = shell.complete(prefix.toString());
357 Completion completions = completion.getValue();
358 log.log(Level.FINE, "Completions for " + prefix + " are " + completions);
359
360 //
361 Delimiter delimiter = completion.getDelimiter();
362
363 try {
364 // Try to find the greatest prefix among all the results
365 if (completions.getSize() == 0) {
366 // Do nothing
367 } else if (completions.getSize() == 1) {
368 Map.Entry<String, Boolean> entry = completions.iterator().next();
369 Appendable buffer = term.getDirectBuffer();
370 String insert = entry.getKey();
371 term.getDirectBuffer().append(delimiter.escape(insert));
372 if (entry.getValue()) {
373 buffer.append(completion.getDelimiter().getValue());
374 }
375 } else {
376 String commonCompletion = Utils.findLongestCommonPrefix(completions.getValues());
377
378 // Format stuff
379 int width = term.getWidth();
380
381 //
382 String completionPrefix = completions.getPrefix();
383
384 // Get the max length
385 int max = 0;
386 for (String suffix : completions.getValues()) {
387 max = Math.max(max, completionPrefix.length() + suffix.length());
388 }
389
390 // Separator : use two whitespace like in BASH
391 max += 2;
392
393 //
394 StringBuilder sb = new StringBuilder().append('\n');
395 if (max < width) {
396 int columns = width / max;
397 int index = 0;
398 for (String suffix : completions.getValues()) {
399 sb.append(completionPrefix).append(suffix);
400 for (int l = completionPrefix.length() + suffix.length();l < max;l++) {
401 sb.append(' ');
402 }
403 if (++index >= columns) {
404 index = 0;
405 sb.append('\n');
406 }
407 }
408 if (index > 0) {
409 sb.append('\n');
410 }
411 } else {
412 for (Iterator<String> i = completions.getValues().iterator();i.hasNext();) {
413 String suffix = i.next();
414 sb.append(commonCompletion).append(suffix);
415 if (i.hasNext()) {
416 sb.append('\n');
417 }
418 }
419 sb.append('\n');
420 }
421
422 // We propose
423 term.write(Text.create(sb.toString()));
424
425 // Rewrite prompt
426 writePromptFlush();
427
428 // If we have common completion we append it now
429 if (commonCompletion.length() > 0) {
430 term.getDirectBuffer().append(delimiter.escape(commonCompletion));
431 }
432 }
433 }
434 catch (IOException e) {
435 log.log(Level.SEVERE, "Could not write completion", e);
436 }
437 }
438 }