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
020package org.crsh.shell.impl.async;
021
022import org.crsh.console.KeyHandler;
023import org.crsh.shell.ShellProcess;
024import org.crsh.shell.ShellProcessContext;
025import org.crsh.shell.ShellResponse;
026import org.crsh.text.Chunk;
027
028import java.io.IOException;
029import java.util.concurrent.Callable;
030
031public class AsyncProcess implements ShellProcess {
032
033
034  /** . */
035  private final String request;
036
037  /** . */
038  private ShellProcessContext caller;
039
040  /** . */
041  private ShellProcess callee;
042
043  /** . */
044  private AsyncShell shell;
045
046  /** . */
047  private Status status;
048
049  /** . */
050  private final Object lock;
051
052  AsyncProcess(AsyncShell shell, String request) {
053    this.shell = shell;
054    this.request = request;
055    this.callee = null;
056    this.status = Status.CONSTRUCTED;
057    this.lock = new Object();
058  }
059
060  public Status getStatus() {
061    return status;
062  }
063
064  /** . */
065  private final ShellProcessContext context = new ShellProcessContext() {
066    public int getWidth() {
067      return caller.getWidth();
068    }
069
070    public int getHeight() {
071      return caller.getHeight();
072    }
073
074    public String getProperty(String name) {
075      return caller.getProperty(name);
076    }
077
078    public boolean takeAlternateBuffer() throws IOException {
079      return caller.takeAlternateBuffer();
080    }
081
082    public boolean releaseAlternateBuffer() throws IOException {
083      return caller.releaseAlternateBuffer();
084    }
085
086    public String readLine(String msg, boolean echo) throws IOException, InterruptedException {
087      return caller.readLine(msg, echo);
088    }
089
090    public Class<Chunk> getConsumedType() {
091      return Chunk.class;
092    }
093
094    public void write(Chunk chunk) throws IOException {
095      caller.write(chunk);
096    }
097
098    public void flush() throws IOException {
099      caller.flush();
100    }
101
102    public void end(ShellResponse response) {
103      // Always leave the status in terminated status if the method succeeds
104      // Cancelled -> Terminated
105      // Evaluating -> Terminated
106      // Terminated -> Terminated
107      synchronized (lock) {
108        switch (status) {
109          case CONSTRUCTED:
110          case QUEUED:
111            throw new AssertionError("Should not happen");
112          case CANCELED:
113            // We substitute the response
114            response = ShellResponse.cancelled();
115            status = Status.TERMINATED;
116            break;
117          case EVALUATING:
118            status = Status.TERMINATED;
119            break;
120          case TERMINATED:
121            throw new IllegalStateException("Cannot end a process already terminated");
122        }
123      }
124
125      //
126      caller.end(response);
127    }
128  };
129
130  @Override
131  public KeyHandler getKeyHandler() {
132    synchronized (lock) {
133      if (status != Status.EVALUATING) {
134        throw new IllegalStateException();
135      }
136    }
137    // Should it be synchronous or not ????
138    // no clue for now :-)
139    return callee.getKeyHandler();
140  }
141
142  public void execute(ShellProcessContext processContext) {
143
144    // Constructed -> Queued
145    synchronized (lock) {
146      if (status != Status.CONSTRUCTED) {
147        throw new IllegalStateException("State was " + status);
148      }
149
150      // Update state
151      status = Status.QUEUED;
152      callee = shell.shell.createProcess(request);
153      caller = processContext;
154    }
155
156    // Create the task
157    Callable<AsyncProcess> task = new Callable<AsyncProcess>() {
158      public AsyncProcess call() throws Exception {
159        try {
160          // Cancelled -> Cancelled
161          // Queued -> Evaluating
162          ShellResponse response;
163          synchronized (lock) {
164            switch (status) {
165              case CANCELED:
166                // Do nothing it was canceled in the mean time
167                response = ShellResponse.cancelled();
168                break;
169              case QUEUED:
170                // Ok we are going to run it
171                status = Status.EVALUATING;
172                response = null;
173                break;
174              default:
175                // Just in case but this can only be called by the queue
176                throw new AssertionError();
177            }
178          }
179
180          // Execute the process if we are in evalating state
181          if (response == null) {
182            // Here the status could already be in status cancelled
183            // it is a race condition, execution still happens
184            // but the callback of the callee to the end method will make the process
185            // terminate and use a cancel response
186            try {
187              callee.execute(context);
188              response = ShellResponse.ok();
189            }
190            catch (Throwable t) {
191              response = ShellResponse.internalError("Unexpected throwable when executing process", t);
192            }
193          }
194
195          // Make the callback
196          // Calling terminated twice will have no effect
197          try {
198            context.end(response);
199          }
200          catch (Throwable t) {
201            // Log it
202          }
203
204          // We return this but we don't really care for now
205          return AsyncProcess.this;
206        }
207        finally {
208          synchronized (shell.lock) {
209            shell.processes.remove(AsyncProcess.this);
210          }
211        }
212      }
213    };
214
215    //
216    synchronized (shell.lock) {
217      if (!shell.closed) {
218        shell.executor.submit(task);
219        shell.processes.add(this);
220      } else {
221        boolean invokeEnd;
222        synchronized (lock) {
223          invokeEnd = status != Status.TERMINATED;
224          status = Status.TERMINATED;
225        }
226        if (invokeEnd) {
227          caller.end(ShellResponse.cancelled());
228        }
229      }
230    }
231  }
232
233  public void cancel() {
234    // Construcuted -> ISE
235    // Evaluating -> Canceled
236    // Queued -> Canceled
237    // Cancelled -> Cancelled
238    // Terminated -> Terminated
239    boolean cancel;
240    synchronized (lock) {
241      switch (status) {
242        case CONSTRUCTED:
243          throw new IllegalStateException("Cannot call cancel on process that was not scheduled for execution yet");
244        case QUEUED:
245          status = Status.CANCELED;
246          cancel = false;
247          break;
248        case EVALUATING:
249          status = Status.CANCELED;
250          cancel = true;
251          break;
252        default:
253          cancel = false;
254          break;
255      }
256    }
257
258    //
259    if (cancel) {
260      callee.cancel();
261    }
262  }
263}