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 }