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