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.shell.impl.async;
021    
022    import org.crsh.console.KeyHandler;
023    import org.crsh.shell.ShellProcess;
024    import org.crsh.shell.ShellProcessContext;
025    import org.crsh.shell.ShellResponse;
026    import org.crsh.text.Chunk;
027    
028    import java.io.IOException;
029    import java.util.concurrent.Callable;
030    
031    public 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    }