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