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