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