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.Pipe;
023 import org.crsh.cmdline.CommandCompletion;
024 import org.crsh.cmdline.Delimiter;
025 import org.crsh.cmdline.spi.ValueCompletion;
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 import org.slf4j.Logger;
035 import org.slf4j.LoggerFactory;
036
037 import java.io.Closeable;
038 import java.io.IOException;
039 import java.util.Iterator;
040 import java.util.LinkedList;
041 import java.util.Map;
042
043 public final class Processor implements Runnable, Pipe<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 = LoggerFactory.getLogger(Processor.class);
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.debug("Writing welcome message to term");
120 term.provide(new Text(welcome));
121 log.debug("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.error("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 void provide(Chunk element) throws IOException {
297 term.provide(element);
298 }
299
300 public void flush() throws IOException {
301 throw new UnsupportedOperationException("what does it mean?");
302 }
303
304 void writePromptFlush() {
305 String prompt = shell.getPrompt();
306 try {
307 String p = prompt == null ? "% " : prompt;
308 StringBuilder sb = new StringBuilder("\r\n").append(p);
309 CharSequence buffer = term.getBuffer();
310 if (buffer != null) {
311 sb.append(buffer);
312 }
313 term.provide(new Text(sb));
314 term.flush();
315 } catch (IOException e) {
316 // Todo : improve that
317 e.printStackTrace();
318 }
319 }
320
321 private void complete(CharSequence prefix) {
322 log.debug("About to get completions for " + prefix);
323 CommandCompletion completion = shell.complete(prefix.toString());
324 ValueCompletion completions = completion.getValue();
325 log.debug("Completions for " + prefix + " are " + completions);
326
327 //
328 Delimiter delimiter = completion.getDelimiter();
329
330 try {
331 // Try to find the greatest prefix among all the results
332 if (completions.getSize() == 0) {
333 // Do nothing
334 } else if (completions.getSize() == 1) {
335 Map.Entry<String, Boolean> entry = completions.iterator().next();
336 Appendable buffer = term.getInsertBuffer();
337 String insert = entry.getKey();
338 term.getInsertBuffer().append(delimiter.escape(insert));
339 if (entry.getValue()) {
340 buffer.append(completion.getDelimiter().getValue());
341 }
342 } else {
343 String commonCompletion = Strings.findLongestCommonPrefix(completions.getSuffixes());
344 if (commonCompletion.length() > 0) {
345 term.getInsertBuffer().append(delimiter.escape(commonCompletion));
346 } else {
347 // Format stuff
348 int width = term.getWidth();
349
350 //
351 String completionPrefix = completions.getPrefix();
352
353 // Get the max length
354 int max = 0;
355 for (String suffix : completions.getSuffixes()) {
356 max = Math.max(max, completionPrefix.length() + suffix.length());
357 }
358
359 // Separator : use two whitespace like in BASH
360 max += 2;
361
362 //
363 StringBuilder sb = new StringBuilder().append('\n');
364 if (max < width) {
365 int columns = width / max;
366 int index = 0;
367 for (String suffix : completions.getSuffixes()) {
368 sb.append(completionPrefix).append(suffix);
369 for (int l = completionPrefix.length() + suffix.length();l < max;l++) {
370 sb.append(' ');
371 }
372 if (++index >= columns) {
373 index = 0;
374 sb.append('\n');
375 }
376 }
377 if (index > 0) {
378 sb.append('\n');
379 }
380 } else {
381 for (Iterator<String> i = completions.getSuffixes().iterator();i.hasNext();) {
382 String suffix = i.next();
383 sb.append(commonCompletion).append(suffix);
384 if (i.hasNext()) {
385 sb.append('\n');
386 }
387 }
388 sb.append('\n');
389 }
390
391 // We propose
392 term.provide(new Text(sb.toString()));
393 writePromptFlush();
394 }
395 }
396 }
397 catch (IOException e) {
398 log.error("Could not write completion", e);
399 }
400 }
401 }