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 String p = prompt == null ? "% " : prompt;
312 StringBuilder sb = new StringBuilder(p);
313 CharSequence buffer = term.getBuffer();
314 if (buffer != null) {
315 sb.append(buffer);
316 }
317 term.provide(Text.create(sb));
318 term.flush();
319 } catch (IOException e) {
320 // Todo : improve that
321 e.printStackTrace();
322 }
323 }
324
325 private void complete(CharSequence prefix) {
326 log.log(Level.FINE, "About to get completions for " + prefix);
327 CompletionMatch completion = shell.complete(prefix.toString());
328 Completion completions = completion.getValue();
329 log.log(Level.FINE, "Completions for " + prefix + " are " + completions);
330
331 //
332 Delimiter delimiter = completion.getDelimiter();
333
334 try {
335 // Try to find the greatest prefix among all the results
336 if (completions.getSize() == 0) {
337 // Do nothing
338 } else if (completions.getSize() == 1) {
339 Map.Entry<String, Boolean> entry = completions.iterator().next();
340 Appendable buffer = term.getDirectBuffer();
341 String insert = entry.getKey();
342 term.getDirectBuffer().append(delimiter.escape(insert));
343 if (entry.getValue()) {
344 buffer.append(completion.getDelimiter().getValue());
345 }
346 } else {
347 String commonCompletion = Strings.findLongestCommonPrefix(completions.getValues());
348
349 // Format stuff
350 int width = term.getWidth();
351
352 //
353 String completionPrefix = completions.getPrefix();
354
355 // Get the max length
356 int max = 0;
357 for (String suffix : completions.getValues()) {
358 max = Math.max(max, completionPrefix.length() + suffix.length());
359 }
360
361 // Separator : use two whitespace like in BASH
362 max += 2;
363
364 //
365 StringBuilder sb = new StringBuilder().append('\n');
366 if (max < width) {
367 int columns = width / max;
368 int index = 0;
369 for (String suffix : completions.getValues()) {
370 sb.append(completionPrefix).append(suffix);
371 for (int l = completionPrefix.length() + suffix.length();l < max;l++) {
372 sb.append(' ');
373 }
374 if (++index >= columns) {
375 index = 0;
376 sb.append('\n');
377 }
378 }
379 if (index > 0) {
380 sb.append('\n');
381 }
382 } else {
383 for (Iterator<String> i = completions.getValues().iterator();i.hasNext();) {
384 String suffix = i.next();
385 sb.append(commonCompletion).append(suffix);
386 if (i.hasNext()) {
387 sb.append('\n');
388 }
389 }
390 sb.append('\n');
391 }
392
393 // We propose
394 term.provide(Text.create(sb.toString()));
395
396 // Rewrite prompt
397 writePromptFlush();
398
399 // If we have common completion we append it now
400 if (commonCompletion.length() > 0) {
401 term.getDirectBuffer().append(delimiter.escape(commonCompletion));
402 }
403 }
404 }
405 catch (IOException e) {
406 log.log(Level.SEVERE, "Could not write completion", e);
407 }
408 }
409 }