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