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 }