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
020 package org.crsh.standalone;
021
022 import com.sun.tools.attach.VirtualMachine;
023 import org.crsh.cli.impl.descriptor.CommandDescriptorImpl;
024 import jline.Terminal;
025 import jline.TerminalFactory;
026 import jline.console.ConsoleReader;
027 import org.crsh.cli.impl.Delimiter;
028 import org.crsh.cli.impl.descriptor.IntrospectionException;
029 import org.crsh.cli.Argument;
030 import org.crsh.cli.Command;
031 import org.crsh.cli.Option;
032 import org.crsh.cli.Usage;
033 import org.crsh.cli.impl.lang.CommandFactory;
034 import org.crsh.cli.impl.invocation.InvocationMatch;
035 import org.crsh.cli.impl.invocation.InvocationMatcher;
036 import org.crsh.processor.jline.JLineProcessor;
037 import org.crsh.shell.Shell;
038 import org.crsh.shell.ShellFactory;
039 import org.crsh.shell.impl.remoting.RemoteServer;
040 import org.crsh.util.CloseableList;
041 import org.crsh.util.IO;
042 import org.crsh.util.InterruptHandler;
043 import org.crsh.util.Safe;
044 import org.crsh.vfs.FS;
045 import org.crsh.vfs.Path;
046 import org.crsh.vfs.Resource;
047 import org.fusesource.jansi.AnsiConsole;
048
049 import java.io.ByteArrayInputStream;
050 import java.io.Closeable;
051 import java.io.File;
052 import java.io.FileDescriptor;
053 import java.io.FileInputStream;
054 import java.io.FileOutputStream;
055 import java.io.IOException;
056 import java.io.PrintWriter;
057 import java.util.List;
058 import java.util.Properties;
059 import java.util.jar.Attributes;
060 import java.util.jar.JarOutputStream;
061 import java.util.jar.Manifest;
062 import java.util.logging.Level;
063 import java.util.logging.Logger;
064 import java.util.regex.Pattern;
065
066 public class CRaSH {
067
068 /** . */
069 private static Logger log = Logger.getLogger(CRaSH.class.getName());
070
071 /** . */
072 private final CommandDescriptorImpl<CRaSH> descriptor;
073
074 public CRaSH() throws IntrospectionException {
075 this.descriptor = CommandFactory.DEFAULT.create(CRaSH.class);
076 }
077
078 private void copy(org.crsh.vfs.File src, File dst) throws IOException {
079 if (src.isDir()) {
080 if (!dst.exists()) {
081 if (dst.mkdir()) {
082 log.fine("Could not create dir " + dst.getCanonicalPath());
083 }
084 }
085 if (dst.exists() && dst.isDirectory()) {
086 for (org.crsh.vfs.File child : src.children()) {
087 copy(child, new File(dst, child.getName()));
088 }
089 }
090 } else {
091 if (!dst.exists()) {
092 Resource resource = src.getResource();
093 if (resource != null) {
094 log.info("Copied resource " + src.getPath().getValue() + " to " + dst.getCanonicalPath());
095 IO.copy(new ByteArrayInputStream(resource.getContent()), new FileOutputStream(dst));
096 }
097 }
098 }
099 }
100
101 @Command
102 public void main(
103 @Option(names= {"non-interactive"})
104 @Usage("non interactive mode, the JVM io will not be used")
105 Boolean nonInteractive,
106 @Option(names={"c","cmd"})
107 @Usage("adds a dir to the command path")
108 List<String> cmds,
109 @Option(names={"conf"})
110 @Usage("adds a dir to the conf path")
111 List<String> confs,
112 @Option(names={"p","property"})
113 @Usage("set a property of the form a=b")
114 List<String> properties,
115 @Option(names = {"cmd-mode"})
116 @Usage("the cmd mode (read or copy), copy mode requires at least one cmd path to be specified")
117 ResourceMode cmdMode,
118 @Option(names = {"conf-mode"})
119 @Usage("the conf mode (read of copy), copy mode requires at least one conf path to be specified")
120 ResourceMode confMode,
121 @Argument(name = "pid")
122 @Usage("the optional list of JVM process id to attach to")
123 List<Integer> pids) throws Exception {
124
125 //
126 boolean copyCmd = cmdMode != ResourceMode.read && cmds != null && cmds.size() > 0;
127 boolean copyConf = confMode != ResourceMode.read && confs != null && confs.size() > 0;
128 boolean interactive = nonInteractive == null || !nonInteractive;
129
130 //
131 if (copyCmd) {
132 File dst = new File(cmds.get(0));
133 if (!dst.isDirectory()) {
134 throw new Exception("Directory " + dst.getAbsolutePath() + " does not exist");
135 }
136 FS fs = new FS();
137 fs.mount(Thread.currentThread().getContextClassLoader(), Path.get("/crash/commands/"));
138 org.crsh.vfs.File f = fs.get(Path.get("/"));
139 log.info("Copying command classpath resources");
140 copy(f, dst);
141 }
142
143 //
144 if (copyConf) {
145 File dst = new File(confs.get(0));
146 if (!dst.isDirectory()) {
147 throw new Exception("Directory " + dst.getAbsolutePath() + " does not exist");
148 }
149 FS fs = new FS();
150 fs.mount(Thread.currentThread().getContextClassLoader(), Path.get("/crash/"));
151 org.crsh.vfs.File f = fs.get(Path.get("/"));
152 log.info("Copying conf classpath resources");
153 for (org.crsh.vfs.File child : f.children()) {
154 if (!child.isDir()) {
155 copy(child, new File(dst, child.getName()));
156 }
157 }
158 }
159
160 //
161 CloseableList closeable = new CloseableList();
162 Shell shell;
163 if (pids != null && pids.size() > 0) {
164
165 //
166 if (interactive && pids.size() > 1) {
167 throw new Exception("Cannot attach to more than one JVM in interactive mode");
168 }
169
170 // Compute classpath
171 String classpath = System.getProperty("java.class.path");
172 String sep = System.getProperty("path.separator");
173 StringBuilder buffer = new StringBuilder();
174 for (String path : classpath.split(Pattern.quote(sep))) {
175 File file = new File(path);
176 if (file.exists()) {
177 if (buffer.length() > 0) {
178 buffer.append(' ');
179 }
180 buffer.append(file.getCanonicalPath());
181 }
182 }
183
184 // Create manifest
185 Manifest manifest = new Manifest();
186 Attributes attributes = manifest.getMainAttributes();
187 attributes.putValue("Agent-Class", Agent.class.getName());
188 attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0");
189 attributes.put(Attributes.Name.CLASS_PATH, buffer.toString());
190
191 // Create jar file
192 File agentFile = File.createTempFile("agent", ".jar");
193 agentFile.deleteOnExit();
194 JarOutputStream out = new JarOutputStream(new FileOutputStream(agentFile), manifest);
195 out.close();
196 log.log(Level.INFO, "Created agent jar " + agentFile.getCanonicalPath());
197
198 // Build the options
199 StringBuilder sb = new StringBuilder();
200
201 // Rewrite canonical path
202 if (copyCmd) {
203 sb.append("--cmd-mode copy ");
204 } else {
205 sb.append("--cmd-mode read ");
206 }
207 if (cmds != null) {
208 for (String cmd : cmds) {
209 File cmdPath = new File(cmd);
210 if (cmdPath.exists()) {
211 sb.append("--cmd ");
212 Delimiter.EMPTY.escape(cmdPath.getCanonicalPath(), sb);
213 sb.append(' ');
214 }
215 }
216 }
217
218 // Rewrite canonical path
219 if (copyCmd) {
220 sb.append("--conf-mode copy ");
221 } else {
222 sb.append("--conf-mode read ");
223 }
224 if (confs != null) {
225 for (String conf : confs) {
226 File confPath = new File(conf);
227 if (confPath.exists()) {
228 sb.append("--conf ");
229 Delimiter.EMPTY.escape(confPath.getCanonicalPath(), sb);
230 sb.append(' ');
231 }
232 }
233 }
234
235 // Propagate canonical config
236 if (properties != null) {
237 for (String property : properties) {
238 sb.append("--property ");
239 Delimiter.EMPTY.escape(property, sb);
240 sb.append(' ');
241 }
242 }
243
244 //
245 if (interactive) {
246 RemoteServer server = new RemoteServer(0);
247 int port = server.bind();
248 log.log(Level.INFO, "Callback server set on port " + port);
249 sb.append(port);
250 String options = sb.toString();
251 Integer pid = pids.get(0);
252 final VirtualMachine vm = VirtualMachine.attach("" + pid);
253 log.log(Level.INFO, "Loading agent with command " + options + " as agent " + agentFile.getCanonicalPath());
254 vm.loadAgent(agentFile.getCanonicalPath(), options);
255 server.accept();
256 shell = server.getShell();
257 closeable.add(new Closeable() {
258 public void close() throws IOException {
259 vm.detach();
260 }
261 });
262 } else {
263 for (Integer pid : pids) {
264 log.log(Level.INFO, "Attaching to remote process " + pid);
265 VirtualMachine vm = VirtualMachine.attach("" + pid);
266 String options = sb.toString();
267 log.log(Level.INFO, "Loading agent with command " + options + " as agent " + agentFile.getCanonicalPath());
268 vm.loadAgent(agentFile.getCanonicalPath(), options);
269 }
270 shell = null;
271 }
272 } else {
273 final Bootstrap bootstrap = new Bootstrap(Thread.currentThread().getContextClassLoader());
274
275 //
276 if (!copyCmd) {
277 bootstrap.addToCmdPath(Path.get("/crash/commands/"));
278 }
279 if (cmds != null) {
280 for (String cmd : cmds) {
281 File cmdPath = new File(cmd);
282 bootstrap.addToCmdPath(cmdPath);
283 }
284 }
285
286 //
287 if (!copyConf) {
288 bootstrap.addToConfPath(Path.get("/crash/"));
289 }
290 if (confs != null) {
291 for (String conf : confs) {
292 File confPath = new File(conf);
293 bootstrap.addToConfPath(confPath);
294 }
295 }
296
297 //
298 if (properties != null) {
299 Properties config = new Properties();
300 for (String property : properties) {
301 int index = property.indexOf('=');
302 if (index == -1) {
303 config.setProperty(property, "");
304 } else {
305 config.setProperty(property.substring(0, index), property.substring(index + 1));
306 }
307 }
308 bootstrap.setConfig(config);
309 }
310
311 // Register shutdown hook
312 Runtime.getRuntime().addShutdownHook(new Thread() {
313 @Override
314 public void run() {
315 // Should trigger some kind of run interruption
316 }
317 });
318
319 // Do bootstrap
320 bootstrap.bootstrap();
321 Runtime.getRuntime().addShutdownHook(new Thread(){
322 @Override
323 public void run() {
324 bootstrap.shutdown();
325 }
326 });
327
328 //
329 if (interactive) {
330 ShellFactory factory = bootstrap.getContext().getPlugin(ShellFactory.class);
331 shell = factory.create(null);
332 } else {
333 shell = null;
334 }
335 closeable = null;
336 }
337
338 //
339 if (shell != null) {
340
341 // Start crash for this command line
342 final Terminal term = TerminalFactory.create();
343 term.init();
344 ConsoleReader reader = new ConsoleReader(null, new FileInputStream(FileDescriptor.in), System.out, term);
345 Runtime.getRuntime().addShutdownHook(new Thread(){
346 @Override
347 public void run() {
348 try {
349 term.restore();
350 }
351 catch (Exception ignore) {
352 }
353 }
354 });
355
356 AnsiConsole.systemInstall();
357
358 final PrintWriter out = new PrintWriter(AnsiConsole.out);
359 final JLineProcessor processor = new JLineProcessor(
360 shell,
361 reader,
362 out
363 );
364 reader.addCompleter(processor);
365
366 // Install signal handler
367 InterruptHandler ih = new InterruptHandler(new Runnable() {
368 public void run() {
369 processor.cancel();
370 }
371 });
372 ih.install();
373
374 //
375 try {
376 processor.run();
377 }
378 catch (Throwable t) {
379 t.printStackTrace();
380 }
381 finally {
382
383 //
384 AnsiConsole.systemUninstall();
385
386 //
387 if (closeable != null) {
388 Safe.close(closeable);
389 }
390
391 // Force exit
392 System.exit(0);
393 }
394 }
395 }
396
397 public static void main(String[] args) throws Exception {
398
399 StringBuilder line = new StringBuilder();
400 for (int i = 0;i < args.length;i++) {
401 if (i > 0) {
402 line.append(' ');
403 }
404 Delimiter.EMPTY.escape(args[i], line);
405 }
406
407 //
408 CRaSH main = new CRaSH();
409 InvocationMatcher<CRaSH> matcher = main.descriptor.invoker("main");
410 InvocationMatch<CRaSH> match = matcher.match(line.toString());
411 match.invoke(new CRaSH());
412 }
413 }