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.cmdline.spi.Completion;
027 import org.crsh.command.CommandContext;
028 import org.crsh.cmdline.CommandCompletion;
029 import org.crsh.cmdline.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 CommandCompletion 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 CommandCompletion 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 CommandCompletion(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 CommandCompletion(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 CommandCompletion(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 }