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 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.BaseRuntimeContext;
029    import org.crsh.command.RuntimeContext;
030    import org.crsh.cli.impl.Delimiter;
031    import org.crsh.command.CommandInvoker;
032    import org.crsh.command.GroovyScript;
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, RuntimeContext {
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 GroovyScript getLifeCycle(String name) throws NoSuchCommandException, NullPointerException {
087        Class<? extends GroovyScript> scriptClass = crash.scriptManager.getClass(name);
088        if (scriptClass != null) {
089          GroovyScript script = (GroovyScript)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          GroovyScript login = getLifeCycle("login");
109          if (login != null) {
110            login.setContext(this);
111            login.run();
112          }
113        }
114        catch (NoSuchCommandException e) {
115          e.printStackTrace();
116        }
117    
118      }
119    
120      public Map<String, Object> getSession() {
121        return this;
122      }
123    
124      public Map<String, Object> getAttributes() {
125        return crash.context.getAttributes();
126      }
127    
128      public void close() {
129        ClassLoader previous = setCRaSHLoader();
130        try {
131          GroovyScript logout = getLifeCycle("logout");
132          if (logout != null) {
133            logout.setContext(this);
134            logout.run();
135          }
136        }
137        catch (NoSuchCommandException e) {
138          e.printStackTrace();
139        }
140        finally {
141          setPreviousLoader(previous);
142        }
143      }
144    
145      // Shell implementation **********************************************************************************************
146    
147      private String eval(String name, String def) {
148        ClassLoader previous = setCRaSHLoader();
149        try {
150          GroovyShell shell = getGroovyShell();
151          Object ret = shell.getContext().getVariable(name);
152          if (ret instanceof Closure) {
153            log.log(Level.FINEST, "Invoking " + name + " closure");
154            Closure c = (Closure)ret;
155            ret = c.call();
156          } else if (ret == null) {
157            log.log(Level.FINEST, "No " + name + " will use empty");
158            return def;
159          }
160          return String.valueOf(ret);
161        }
162        catch (Exception e) {
163          log.log(Level.SEVERE, "Could not get a " + name + " message, will use empty", e);
164          return def;
165        }
166        finally {
167          setPreviousLoader(previous);
168        }
169      }
170    
171      public String getWelcome() {
172        return eval("welcome", "");
173      }
174    
175      public String getPrompt() {
176        return eval("prompt", "% ");
177      }
178    
179      public ShellProcess createProcess(String request) {
180        log.log(Level.FINE, "Invoking request " + request);
181        final ShellResponse response;
182        if ("bye".equals(request) || "exit".equals(request)) {
183          response = ShellResponse.close();
184        } else {
185          // Create pipeline from request
186          PipeLineParser parser = new PipeLineParser(request);
187          final PipeLineFactory factory = parser.parse();
188          if (factory != null) {
189            try {
190              final CommandInvoker<Void, Chunk> pipeLine = factory.create(this);
191              return new CRaSHProcess(this, request) {
192    
193                @Override
194                ShellResponse doInvoke(final ShellProcessContext context) throws InterruptedException {
195                  CRaSHProcessContext invocationContext = new CRaSHProcessContext(CRaSHSession.this, context);
196                  try {
197                    pipeLine.open(invocationContext);
198                    pipeLine.flush();
199                    return ShellResponse.ok();
200                  }
201                  catch (ScriptException e) {
202                    return build(e);
203                  } catch (Throwable t) {
204                    return build(t);
205                  } finally {
206                    Safe.close(pipeLine);
207                    Safe.close(invocationContext);
208                  }
209                }
210    
211                private ShellResponse.Error build(Throwable throwable) {
212                  ErrorType errorType;
213                  if (throwable instanceof ScriptException) {
214                    errorType = ErrorType.EVALUATION;
215                    Throwable cause = throwable.getCause();
216                    if (cause != null) {
217                      throwable = cause;
218                    }
219                  } else {
220                    errorType = ErrorType.INTERNAL;
221                  }
222                  String result;
223                  String msg = throwable.getMessage();
224                  if (throwable instanceof ScriptException) {
225                    if (msg == null) {
226                      result = request + ": failed";
227                    } else {
228                      result = request + ": " + msg;
229                    }
230                    return ShellResponse.error(errorType, result, throwable);
231                  } else {
232                    if (msg == null) {
233                      msg = throwable.getClass().getSimpleName();
234                    }
235                    if (throwable instanceof RuntimeException) {
236                      result = request + ": exception: " + msg;
237                    } else if (throwable instanceof Exception) {
238                      result = request + ": exception: " + msg;
239                    } else if (throwable instanceof java.lang.Error) {
240                      result = request + ": error: " + msg;
241                    } else {
242                      result = request + ": unexpected throwable: " + msg;
243                    }
244                    return ShellResponse.error(errorType, result, throwable);
245                  }
246                }
247              };
248            }
249            catch (NoSuchCommandException e) {
250              response = ShellResponse.unknownCommand(e.getCommandName());
251            }
252          } else {
253            response = ShellResponse.noCommand();
254          }
255        }
256    
257        //
258        return new CRaSHProcess(this, request) {
259          @Override
260          ShellResponse doInvoke(ShellProcessContext context) throws InterruptedException {
261            return response;
262          }
263        };
264      }
265    
266      /**
267       * For now basic implementation
268       */
269      public CompletionMatch complete(final String prefix) {
270        ClassLoader previous = setCRaSHLoader();
271        try {
272          log.log(Level.FINE, "Want prefix of " + prefix);
273          PipeLineFactory ast = new PipeLineParser(prefix).parse();
274          String termPrefix;
275          if (ast != null) {
276            PipeLineFactory last = ast.getLast();
277            termPrefix = Utils.trimLeft(last.getLine());
278          } else {
279            termPrefix = "";
280          }
281    
282          //
283          log.log(Level.FINE, "Retained term prefix is " + prefix);
284          CompletionMatch completion;
285          int pos = termPrefix.indexOf(' ');
286          if (pos == -1) {
287            Completion.Builder builder = Completion.builder(prefix);
288            for (String resourceId : crash.context.listResourceId(ResourceKind.COMMAND)) {
289              if (resourceId.startsWith(termPrefix)) {
290                builder.add(resourceId.substring(termPrefix.length()), true);
291              }
292            }
293            completion = new CompletionMatch(Delimiter.EMPTY, builder.build());
294          } else {
295            String commandName = termPrefix.substring(0, pos);
296            termPrefix = termPrefix.substring(pos);
297            try {
298              ShellCommand command = crash.getCommand(commandName);
299              if (command != null) {
300                completion = command.complete(new BaseRuntimeContext(this, crash.context.getAttributes()), termPrefix);
301              } else {
302                completion = new CompletionMatch(Delimiter.EMPTY, Completion.create());
303              }
304            }
305            catch (NoSuchCommandException e) {
306              log.log(Level.FINE, "Could not create command for completion of " + prefix, e);
307              completion = new CompletionMatch(Delimiter.EMPTY, Completion.create());
308            }
309          }
310    
311          //
312          log.log(Level.FINE, "Found completions for " + prefix + ": " + completion);
313          return completion;
314        }
315        finally {
316          setPreviousLoader(previous);
317        }
318      }
319    
320      ClassLoader setCRaSHLoader() {
321        Thread thread = Thread.currentThread();
322        ClassLoader previous = thread.getContextClassLoader();
323        thread.setContextClassLoader(crash.context.getLoader());
324        return previous;
325      }
326    
327      void setPreviousLoader(ClassLoader previous) {
328        Thread.currentThread().setContextClassLoader(previous);
329      }
330    }