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    package org.crsh.shell.impl.command;
020    
021    import groovy.lang.Binding;
022    import groovy.lang.Closure;
023    import groovy.lang.GroovyShell;
024    import groovy.lang.Script;
025    import org.codehaus.groovy.control.CompilerConfiguration;
026    import org.codehaus.groovy.runtime.InvokerHelper;
027    import org.crsh.cli.impl.completion.CompletionMatch;
028    import org.crsh.cli.spi.Completion;
029    import org.crsh.command.CommandContext;
030    import org.crsh.cli.impl.Delimiter;
031    import org.crsh.command.BaseCommandContext;
032    import org.crsh.command.CommandInvoker;
033    import org.crsh.command.NoSuchCommandException;
034    import org.crsh.command.GroovyScriptCommand;
035    import org.crsh.command.ScriptException;
036    import org.crsh.command.ShellCommand;
037    import org.crsh.plugin.ResourceKind;
038    import org.crsh.shell.ErrorType;
039    import org.crsh.shell.Shell;
040    import org.crsh.shell.ShellProcess;
041    import org.crsh.shell.ShellProcessContext;
042    import org.crsh.shell.ShellResponse;
043    import org.crsh.text.Chunk;
044    import org.crsh.util.Safe;
045    import org.crsh.util.Utils;
046    
047    import java.io.Closeable;
048    import java.security.Principal;
049    import java.util.HashMap;
050    import java.util.Map;
051    import java.util.logging.Level;
052    import java.util.logging.Logger;
053    
054    public class CRaSHSession extends HashMap<String, Object> implements Shell, Closeable, CommandContext {
055    
056      /** . */
057      static final Logger log = Logger.getLogger(CRaSHSession.class.getName());
058    
059      /** . */
060      static final Logger accessLog = Logger.getLogger("org.crsh.shell.access");
061    
062      /** . */
063      private GroovyShell groovyShell;
064    
065      /** . */
066      final CRaSH crash;
067    
068      /** . */
069      final Principal user;
070    
071      /**
072       * Used for testing purposes.
073       *
074       * @return a groovy shell operating on the session attributes
075       */
076      public GroovyShell getGroovyShell() {
077        if (groovyShell == null) {
078          CompilerConfiguration config = new CompilerConfiguration();
079          config.setRecompileGroovySource(true);
080          config.setScriptBaseClass(GroovyScriptCommand.class.getName());
081          groovyShell = new GroovyShell(crash.context.getLoader(), new Binding(this), config);
082        }
083        return groovyShell;
084      }
085    
086      public Script getLifeCycle(String name) throws NoSuchCommandException, NullPointerException {
087        Class<? extends Script> scriptClass = crash.lifecycles.getClass(name);
088        if (scriptClass != null) {
089          Script script = InvokerHelper.createScript(scriptClass, new Binding(this));
090          script.setBinding(new Binding(this));
091          return script;
092        } else {
093          return null;
094        }
095      }
096    
097      CRaSHSession(final CRaSH crash, Principal user) {
098        // Set variable available to all scripts
099        put("crash", crash);
100    
101        //
102        this.groovyShell = null;
103        this.crash = crash;
104        this.user = user;
105    
106        //
107        try {
108          Script login = getLifeCycle("login");
109          if (login instanceof CommandInvoker) {
110            ((CommandInvoker)login).setSession(this);
111          }
112          if (login != null) {
113            login.run();
114          }
115        }
116        catch (NoSuchCommandException e) {
117          e.printStackTrace();
118        }
119    
120      }
121    
122      public Map<String, Object> getSession() {
123        return this;
124      }
125    
126      public Map<String, Object> getAttributes() {
127        return crash.context.getAttributes();
128      }
129    
130      public void close() {
131        ClassLoader previous = setCRaSHLoader();
132        try {
133          Script logout = getLifeCycle("logout");
134          if (logout instanceof CommandInvoker) {
135            ((CommandInvoker)logout).setSession(this);
136          }
137          if (logout != null) {
138            logout.run();
139          }
140        }
141        catch (NoSuchCommandException e) {
142          e.printStackTrace();
143        }
144        finally {
145          setPreviousLoader(previous);
146        }
147      }
148    
149      // Shell implementation **********************************************************************************************
150    
151      private String eval(String name, String def) {
152        ClassLoader previous = setCRaSHLoader();
153        try {
154          GroovyShell shell = getGroovyShell();
155          Object ret = shell.evaluate("return " + name + ";");
156          if (ret instanceof Closure) {
157            log.log(Level.FINEST, "Invoking " + name + " closure");
158            Closure c = (Closure)ret;
159            ret = c.call();
160          } else if (ret == null) {
161            log.log(Level.FINEST, "No " + name + " will use empty");
162            return def;
163          }
164          return String.valueOf(ret);
165        }
166        catch (Exception e) {
167          log.log(Level.SEVERE, "Could not get a " + name + " message, will use empty", e);
168          return def;
169        }
170        finally {
171          setPreviousLoader(previous);
172        }
173      }
174    
175      public String getWelcome() {
176        return eval("welcome", "");
177      }
178    
179      public String getPrompt() {
180        return eval("prompt", "% ");
181      }
182    
183      public ShellProcess createProcess(String request) {
184        log.log(Level.FINE, "Invoking request " + request);
185        final ShellResponse response;
186        if ("bye".equals(request) || "exit".equals(request)) {
187          response = ShellResponse.close();
188        } else {
189          // Create pipeline from request
190          PipeLineParser parser = new PipeLineParser(request);
191          final PipeLineFactory factory = parser.parse();
192          if (factory != null) {
193            try {
194              final CommandInvoker<Void, Chunk> pipeLine = factory.create(this);
195              return new CRaSHProcess(this, request) {
196    
197                @Override
198                ShellResponse doInvoke(final ShellProcessContext context) throws InterruptedException {
199                  CRaSHProcessContext invocationContext = new CRaSHProcessContext(CRaSHSession.this, context);
200                  try {
201                    pipeLine.open(invocationContext);
202                    pipeLine.flush();
203                    return ShellResponse.ok();
204                  }
205                  catch (ScriptException e) {
206                    return build(e);
207                  } catch (Throwable t) {
208                    return build(t);
209                  } finally {
210                    Safe.close(pipeLine);
211                    Safe.close(invocationContext);
212                  }
213                }
214    
215                private ShellResponse.Error build(Throwable throwable) {
216                  ErrorType errorType;
217                  if (throwable instanceof ScriptException) {
218                    errorType = ErrorType.EVALUATION;
219                    Throwable cause = throwable.getCause();
220                    if (cause != null) {
221                      throwable = cause;
222                    }
223                  } else {
224                    errorType = ErrorType.INTERNAL;
225                  }
226                  String result;
227                  String msg = throwable.getMessage();
228                  if (throwable instanceof ScriptException) {
229                    if (msg == null) {
230                      result = request + ": failed";
231                    } else {
232                      result = request + ": " + msg;
233                    }
234                    return ShellResponse.error(errorType, result, throwable);
235                  } else {
236                    if (msg == null) {
237                      msg = throwable.getClass().getSimpleName();
238                    }
239                    if (throwable instanceof RuntimeException) {
240                      result = request + ": exception: " + msg;
241                    } else if (throwable instanceof Exception) {
242                      result = request + ": exception: " + msg;
243                    } else if (throwable instanceof java.lang.Error) {
244                      result = request + ": error: " + msg;
245                    } else {
246                      result = request + ": unexpected throwable: " + msg;
247                    }
248                    return ShellResponse.error(errorType, result, throwable);
249                  }
250                }
251              };
252            }
253            catch (NoSuchCommandException e) {
254              response = ShellResponse.unknownCommand(e.getCommandName());
255            }
256          } else {
257            response = ShellResponse.noCommand();
258          }
259        }
260    
261        //
262        return new CRaSHProcess(this, request) {
263          @Override
264          ShellResponse doInvoke(ShellProcessContext context) throws InterruptedException {
265            return response;
266          }
267        };
268      }
269    
270      /**
271       * For now basic implementation
272       */
273      public CompletionMatch complete(final String prefix) {
274        ClassLoader previous = setCRaSHLoader();
275        try {
276          log.log(Level.FINE, "Want prefix of " + prefix);
277          PipeLineFactory ast = new PipeLineParser(prefix).parse();
278          String termPrefix;
279          if (ast != null) {
280            PipeLineFactory last = ast.getLast();
281            termPrefix = Utils.trimLeft(last.getLine());
282          } else {
283            termPrefix = "";
284          }
285    
286          //
287          log.log(Level.FINE, "Retained term prefix is " + prefix);
288          CompletionMatch completion;
289          int pos = termPrefix.indexOf(' ');
290          if (pos == -1) {
291            Completion.Builder builder = Completion.builder(prefix);
292            for (String resourceId : crash.context.listResourceId(ResourceKind.COMMAND)) {
293              if (resourceId.startsWith(termPrefix)) {
294                builder.add(resourceId.substring(termPrefix.length()), true);
295              }
296            }
297            completion = new CompletionMatch(Delimiter.EMPTY, builder.build());
298          } else {
299            String commandName = termPrefix.substring(0, pos);
300            termPrefix = termPrefix.substring(pos);
301            try {
302              ShellCommand command = crash.getCommand(commandName);
303              if (command != null) {
304                completion = command.complete(new BaseCommandContext(this, crash.context.getAttributes()), termPrefix);
305              } else {
306                completion = new CompletionMatch(Delimiter.EMPTY, Completion.create());
307              }
308            }
309            catch (NoSuchCommandException e) {
310              log.log(Level.FINE, "Could not create command for completion of " + prefix, e);
311              completion = new CompletionMatch(Delimiter.EMPTY, Completion.create());
312            }
313          }
314    
315          //
316          log.log(Level.FINE, "Found completions for " + prefix + ": " + completion);
317          return completion;
318        }
319        finally {
320          setPreviousLoader(previous);
321        }
322      }
323    
324      ClassLoader setCRaSHLoader() {
325        Thread thread = Thread.currentThread();
326        ClassLoader previous = thread.getContextClassLoader();
327        thread.setContextClassLoader(crash.context.getLoader());
328        return previous;
329      }
330    
331      void setPreviousLoader(ClassLoader previous) {
332        Thread.currentThread().setContextClassLoader(previous);
333      }
334    }