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