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 }