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