001 /*
002 * Copyright (C) 2010 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;
021
022 import org.crsh.shell.Shell;
023 import org.crsh.shell.ShellProcess;
024 import org.crsh.shell.ShellProcessContext;
025 import org.crsh.shell.ShellResponse;
026 import org.crsh.term.Term;
027 import org.crsh.term.TermEvent;
028 import org.crsh.util.FutureListener;
029 import org.crsh.util.LatchedFuture;
030 import org.crsh.util.Strings;
031 import org.slf4j.Logger;
032 import org.slf4j.LoggerFactory;
033
034 import java.io.IOException;
035 import java.util.ArrayList;
036 import java.util.Iterator;
037 import java.util.List;
038 import java.util.Map;
039 import java.util.concurrent.ExecutionException;
040 import java.util.concurrent.atomic.AtomicReference;
041
042 /**
043 * @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a>
044 * @version $Revision$
045 */
046 public class Processor implements Runnable {
047
048 public static enum State {
049
050 INITIAL,
051
052 OPEN,
053
054 WANT_CLOSE,
055
056 CLOSED;
057
058 /** . */
059 public final Status available;
060
061 /** . */
062 public final Status busy;
063
064 State() {
065 this.available = new Status(this, true);
066 this.busy = new Status(this, false);
067 }
068 }
069
070 public static class Status {
071
072 /** . */
073 private final State state;
074
075 /** . */
076 private final boolean available;
077
078 private Status(State state, boolean available) {
079 this.state = state;
080 this.available = available;
081 }
082
083 public State getState() {
084 return state;
085 }
086
087 public boolean isAvailable() {
088 return available;
089 }
090
091 public boolean isBusy() {
092 return !available;
093 }
094
095 @Override
096 public boolean equals(Object o) {
097 if (o == this) {
098 return true;
099 } else if (o instanceof Status) {
100 Status that = (Status)o;
101 return state == that.state && available == that.available;
102 } else {
103 return false;
104 }
105 }
106 }
107
108 public interface Result {
109
110 State getState();
111
112 }
113
114 /** . */
115 private final Logger log = LoggerFactory.getLogger(Processor.class);
116
117 /** . */
118 private final Term term;
119
120 /** . */
121 private final AtomicReference<Status> status;
122
123 /** . */
124 private final Shell shell;
125
126 /** The current process being executed. */
127 private volatile ShellProcess process;
128
129 /** . */
130 private final List<ProcessorListener> listeners;
131
132 public Processor(Term term, Shell shell) {
133 this.term = term;
134 this.status = new AtomicReference<Status>(State.INITIAL.available);
135 this.shell = shell;
136 this.process = null;
137 this.listeners = new ArrayList<ProcessorListener>();
138 }
139
140 public void run() {
141 while (true) {
142 Result result = execute();
143 State state = result.getState();
144 if (state == State.CLOSED) {
145 break;
146 }
147 }
148 }
149
150 public boolean isAvailable() {
151 return process == null;
152 }
153
154 public State getState() {
155 return status.get().getState();
156 }
157
158 private static abstract class Task {
159
160 protected abstract LatchedFuture<State> execute();
161
162 }
163
164 public Result execute() {
165
166 //
167 final Status _status = status.get();
168
169 Task task = null;
170
171 if (_status == State.INITIAL.available) {
172
173 task = new Task() {
174 @Override
175 protected LatchedFuture<State> execute() {
176 try {
177 String welcome = shell.getWelcome();
178 log.debug("Writing welcome message to term");
179 term.write(welcome);
180 log.debug("Wrote welcome message to term");
181 writePrompt();
182 }
183 catch (IOException e) {
184 e.printStackTrace();
185 }
186 return new LatchedFuture<State>(State.OPEN);
187 }
188 };
189
190 } else if (_status == State.OPEN.available) {
191
192 task = new Task() {
193 @Override
194 protected LatchedFuture<State> execute() {
195 //
196 TermEvent event = null;
197
198 try {
199 log.debug("About to read next term event");
200 event = term.read();
201 log.debug("Read next term event " + event);
202 } catch (IOException e) {
203 if (status.get() == State.OPEN.available) {
204 log.error("Could not read term data", e);
205 } else {
206 log.debug("Exception but term is considered as closed", e);
207 // We continue it will lead to getting out of the loop
208 }
209 }
210
211 //
212 if (event == null) {
213 // Do nothing wait until next event
214 return new LatchedFuture<State>(State.OPEN);
215 } else if (event instanceof TermEvent.ReadLine) {
216 String line = ((TermEvent.ReadLine)event).getLine().toString();
217 log.debug("Submitting command " + line);
218
219 //
220 ShellInvoker invoker = new ShellInvoker();
221
222 // Process
223 process = shell.createProcess(((TermEvent.ReadLine) event).getLine().toString());
224
225 //
226 process.execute(invoker);
227
228 //
229 if (line.length() > 0) {
230 term.addToHistory(line);
231 }
232
233 //
234 return invoker.result;
235 } else if (event instanceof TermEvent.Break) {
236 if (process != null) {
237 process.cancel();
238 } else {
239 log.debug("Ignoring action " + event);
240 writePrompt();
241 }
242 return new LatchedFuture<State>(State.OPEN);
243 } else if (event instanceof TermEvent.Complete) {
244 TermEvent.Complete complete = (TermEvent.Complete)event;
245 String prefix = complete.getLine().toString();
246 log.debug("About to get completions for " + prefix);
247 Map<String, String> completions = shell.complete(prefix);
248 log.debug("Completions for " + prefix + " are " + completions);
249
250 // Try to find the greatest prefix among all the results
251 String commonCompletion;
252 if (completions.size() == 0) {
253 commonCompletion = "";
254 } else if (completions.size() == 1) {
255 Map.Entry<String, String> entry = completions.entrySet().iterator().next();
256 commonCompletion = entry.getKey() + entry.getValue();
257 } else {
258 commonCompletion = Strings.findLongestCommonPrefix(completions.keySet());
259 }
260
261 //
262 if (commonCompletion.length() > 0) {
263 try {
264 term.bufferInsert(commonCompletion);
265 }
266 catch (IOException e) {
267 e.printStackTrace();
268 }
269 } else {
270 if (completions.size() > 1) {
271 // We propose
272 StringBuilder sb = new StringBuilder("\n");
273 for (Iterator<String> i = completions.keySet().iterator();i.hasNext();) {
274 String completion = i.next();
275 sb.append(completion);
276 if (i.hasNext()) {
277 sb.append(" ");
278 }
279 }
280 sb.append("\n");
281 try {
282 term.write(sb.toString());
283 }
284 catch (IOException e) {
285 e.printStackTrace();
286 }
287 writePrompt();
288 }
289 }
290 return new LatchedFuture<State>(State.OPEN);
291 } else if (event instanceof TermEvent.Close) {
292 return new LatchedFuture<State>(State.WANT_CLOSE);
293 } else {
294 return new LatchedFuture<State>(State.OPEN);
295 }
296 }
297 };
298 } else if (_status == State.WANT_CLOSE.available) {
299
300 task = new Task() {
301 @Override
302 protected LatchedFuture<State> execute() {
303 //
304 log.debug("Closing processor");
305
306 // Make a copy
307 ArrayList<ProcessorListener> listeners;
308 synchronized (Processor.this.listeners) {
309 listeners = new ArrayList<ProcessorListener>(Processor.this.listeners);
310 }
311
312 // Status to closed, we won't process any further request
313 // it's important to set the status before closing anything to
314 // avoid a race condition because closing a listener may
315 // cause an interrupted exception
316 status.set(State.CLOSED.available);
317
318 //
319 for (ProcessorListener listener : listeners) {
320 try {
321 log.debug("Closing " + listener.getClass().getSimpleName());
322 listener.closed();
323 }
324 catch (Exception e) {
325 e.printStackTrace();
326 }
327 }
328
329 //
330 return new LatchedFuture<State>(State.CLOSED);
331 }
332 };
333 } else {
334 //
335 }
336
337 //
338 if (task != null) {
339
340 // Add listener to update our state
341 final LatchedFuture<State> futureState = task.execute();
342
343 // It will update the status
344 futureState.addListener(new FutureListener<State>() {
345 public void completed(State value) {
346 status.set(value.available);
347 }
348 });
349
350 //
351 return new Result() {
352 public State getState() {
353 try {
354 return futureState.get();
355 } catch (InterruptedException e) {
356 return status.get().getState();
357 } catch (ExecutionException e) {
358 return status.get().getState();
359 }
360 }
361 };
362 } else {
363 return new Result() {
364 public State getState() {
365 return _status.getState();
366 }
367 };
368 }
369 }
370
371 private void writePrompt() {
372 String prompt = shell.getPrompt();
373 try {
374 String p = prompt == null ? "% " : prompt;
375 term.write("\r\n");
376 term.write(p);
377 term.write(term.getBuffer());
378 } catch (IOException e) {
379 e.printStackTrace();
380 }
381 }
382
383 private class ShellInvoker implements ShellProcessContext {
384
385 /** . */
386 private final LatchedFuture<State> result = new LatchedFuture<State>();
387
388 public int getWidth() {
389 return term.getWidth();
390 }
391
392 public String getProperty(String name) {
393 return term.getProperty(name);
394 }
395
396 public String readLine(String msg, boolean echo) {
397 try {
398 term.setEcho(echo);
399 term.write(msg);
400 TermEvent action = term.read();
401 CharSequence line = null;
402 if (action instanceof TermEvent.ReadLine) {
403 line = ((TermEvent.ReadLine)action).getLine();
404 log.debug("Read from console");
405 }
406 else {
407 log.debug("Ignoring action " + action + " returning null");
408 }
409 term.write("\r\n");
410 return line.toString();
411 }
412 catch (Exception e) {
413 log.error("Reading from console failed", e);
414 return null;
415 }
416 finally {
417 term.setEcho(true);
418 }
419 }
420
421 public void end(ShellResponse response) {
422 try {
423
424 //
425 if (response instanceof ShellResponse.Close) {
426 System.out.println("received close response");
427 result.set(State.WANT_CLOSE);
428 } else {
429 if (response instanceof ShellResponse.Cancelled) {
430 result.set(State.OPEN);
431 } else {
432 String ret = response.getText();
433 log.debug("Command completed with result " + ret);
434 try {
435 term.write(ret);
436 }
437 catch (IOException e) {
438 log.error("Write to term failure", e);
439 }
440 process = null;
441 }
442
443 //
444 writePrompt();
445
446 //
447 result.set(State.OPEN);
448 }
449 }
450 finally {
451 process = null;
452 }
453 }
454 }
455
456 public void addListener(ProcessorListener listener) {
457 if (listener == null) {
458 throw new NullPointerException();
459 }
460 synchronized (listeners) {
461 if (listeners.contains(listener)) {
462 throw new IllegalStateException("Already listening");
463 }
464 listeners.add(listener);
465 }
466 }
467 }