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 020package org.crsh.standalone; 021 022import com.sun.tools.attach.VirtualMachine; 023import jline.AnsiWindowsTerminal; 024import jline.Terminal; 025import jline.TerminalFactory; 026import jline.console.ConsoleReader; 027import jline.internal.Configuration; 028import org.crsh.cli.Argument; 029import org.crsh.cli.Command; 030import org.crsh.cli.Named; 031import org.crsh.cli.Option; 032import org.crsh.cli.Usage; 033import org.crsh.cli.descriptor.CommandDescriptor; 034import org.crsh.cli.impl.Delimiter; 035import org.crsh.cli.impl.descriptor.IntrospectionException; 036import org.crsh.cli.impl.invocation.InvocationMatch; 037import org.crsh.cli.impl.invocation.InvocationMatcher; 038import org.crsh.cli.impl.lang.CommandFactory; 039import org.crsh.cli.impl.lang.Instance; 040import org.crsh.cli.impl.lang.Util; 041import org.crsh.console.jline.JLineProcessor; 042import org.crsh.plugin.ResourceManager; 043import org.crsh.shell.Shell; 044import org.crsh.shell.ShellFactory; 045import org.crsh.shell.impl.remoting.RemoteServer; 046import org.crsh.util.CloseableList; 047import org.crsh.util.InterruptHandler; 048import org.crsh.util.Utils; 049import org.crsh.vfs.FS; 050import org.crsh.vfs.Path; 051import org.crsh.vfs.Resource; 052import org.crsh.vfs.spi.Mount; 053import org.crsh.vfs.spi.file.FileMountFactory; 054import org.crsh.vfs.spi.url.ClassPathMountFactory; 055import org.fusesource.jansi.AnsiConsole; 056 057import java.io.BufferedOutputStream; 058import java.io.ByteArrayInputStream; 059import java.io.Closeable; 060import java.io.File; 061import java.io.FileDescriptor; 062import java.io.FileInputStream; 063import java.io.FileOutputStream; 064import java.io.IOException; 065import java.io.PrintStream; 066import java.util.List; 067import java.util.Properties; 068import java.util.jar.Attributes; 069import java.util.jar.JarOutputStream; 070import java.util.jar.Manifest; 071import java.util.logging.Level; 072import java.util.logging.Logger; 073import java.util.regex.Pattern; 074 075@Named("crash") 076public 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 String fileName = file.getCanonicalPath(); 231 if(fileName.charAt(0) != '/' && fileName.charAt(1) == ':') { 232 // On window, the value of Class-Path in Manifest file must in form: /C:/path/lib/abc.jar 233 fileName = fileName.replace(File.separatorChar, '/'); 234 buffer.append("/").append(fileName); 235 236 } else { 237 buffer.append(file.getCanonicalPath()); 238 } 239 } 240 } 241 242 // Create manifest 243 Manifest manifest = new Manifest(); 244 Attributes attributes = manifest.getMainAttributes(); 245 attributes.putValue("Agent-Class", Agent.class.getName()); 246 attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0"); 247 attributes.put(Attributes.Name.CLASS_PATH, buffer.toString()); 248 249 // Create jar file 250 File agentFile = File.createTempFile("agent", ".jar"); 251 agentFile.deleteOnExit(); 252 JarOutputStream out = new JarOutputStream(new FileOutputStream(agentFile), manifest); 253 out.close(); 254 log.log(Level.INFO, "Created agent jar " + agentFile.getCanonicalPath()); 255 256 // Build the options 257 StringBuilder sb = new StringBuilder(); 258 259 // Path configuration 260 sb.append("--cmd "); 261 Delimiter.EMPTY.escape(toString(cmdBuilder), sb); 262 sb.append(' '); 263 sb.append("--conf "); 264 Delimiter.EMPTY.escape(toString(confBuilder), sb); 265 sb.append(' '); 266 267 // Propagate canonical config 268 if (properties != null) { 269 for (String property : properties) { 270 sb.append("--property "); 271 Delimiter.EMPTY.escape(property, sb); 272 sb.append(' '); 273 } 274 } 275 276 // 277 if (interactive) { 278 RemoteServer server = new RemoteServer(0); 279 int port = server.bind(); 280 log.log(Level.INFO, "Callback server set on port " + port); 281 sb.append(port); 282 String options = sb.toString(); 283 Integer pid = pids.get(0); 284 final VirtualMachine vm = VirtualMachine.attach("" + pid); 285 log.log(Level.INFO, "Loading agent with command " + options + " as agent " + agentFile.getCanonicalPath()); 286 vm.loadAgent(agentFile.getCanonicalPath(), options); 287 server.accept(); 288 shell = server.getShell(); 289 closeable.add(new Closeable() { 290 public void close() throws IOException { 291 vm.detach(); 292 } 293 }); 294 } else { 295 for (Integer pid : pids) { 296 log.log(Level.INFO, "Attaching to remote process " + pid); 297 VirtualMachine vm = VirtualMachine.attach("" + pid); 298 String options = sb.toString(); 299 log.log(Level.INFO, "Loading agent with command " + options + " as agent " + agentFile.getCanonicalPath()); 300 vm.loadAgent(agentFile.getCanonicalPath(), options); 301 } 302 shell = null; 303 } 304 } else { 305 final Bootstrap bootstrap = new Bootstrap( 306 Thread.currentThread().getContextClassLoader(), 307 confBuilder.build(), 308 cmdBuilder.build()); 309 310 // 311 if (properties != null) { 312 Properties config = new Properties(); 313 for (String property : properties) { 314 int index = property.indexOf('='); 315 if (index == -1) { 316 config.setProperty(property, ""); 317 } else { 318 config.setProperty(property.substring(0, index), property.substring(index + 1)); 319 } 320 } 321 bootstrap.setConfig(config); 322 } 323 324 // Register shutdown hook 325 Runtime.getRuntime().addShutdownHook(new Thread() { 326 @Override 327 public void run() { 328 // Should trigger some kind of run interruption 329 } 330 }); 331 332 // Do bootstrap 333 bootstrap.bootstrap(); 334 Runtime.getRuntime().addShutdownHook(new Thread(){ 335 @Override 336 public void run() { 337 bootstrap.shutdown(); 338 } 339 }); 340 341 // 342 if (interactive) { 343 ShellFactory factory = bootstrap.getContext().getPlugin(ShellFactory.class); 344 shell = factory.create(null); 345 } else { 346 shell = null; 347 } 348 closeable = null; 349 } 350 351 // 352 if (shell != null) { 353 354 // 355 final Terminal term = TerminalFactory.create(); 356 357 // 358 Runtime.getRuntime().addShutdownHook(new Thread() { 359 @Override 360 public void run() { 361 try { 362 term.restore(); 363 } 364 catch (Exception ignore) { 365 } 366 } 367 }); 368 369 // 370 String encoding = Configuration.getEncoding(); 371 372 // Use AnsiConsole only if term doesn't support Ansi 373 PrintStream out; 374 PrintStream err; 375 boolean ansi; 376 if (term.isAnsiSupported()) { 377 out = new PrintStream(new BufferedOutputStream(term.wrapOutIfNeeded(new FileOutputStream(FileDescriptor.out)), 16384), false, encoding); 378 err = new PrintStream(new BufferedOutputStream(term.wrapOutIfNeeded(new FileOutputStream(FileDescriptor.err)), 16384), false, encoding); 379 ansi = true; 380 } else { 381 out = AnsiConsole.out; 382 err = AnsiConsole.err; 383 ansi = false; 384 } 385 386 // 387 FileInputStream in = new FileInputStream(FileDescriptor.in); 388 ConsoleReader reader = new ConsoleReader(null, in, out, term); 389 390 // 391 final JLineProcessor processor = new JLineProcessor(ansi, shell, reader, out); 392 393 // 394 InterruptHandler interruptHandler = new InterruptHandler(new Runnable() { 395 @Override 396 public void run() { 397 processor.interrupt(); 398 } 399 }); 400 interruptHandler.install(); 401 402 // 403 Thread thread = new Thread(processor); 404 thread.setDaemon(true); 405 thread.start(); 406 407 // 408 try { 409 processor.closed(); 410 } 411 catch (Throwable t) { 412 t.printStackTrace(); 413 } 414 finally { 415 416 // 417 if (closeable != null) { 418 Utils.close(closeable); 419 } 420 421 // Force exit 422 System.exit(0); 423 } 424 } 425 } 426 427 public static void main(String[] args) throws Exception { 428 429 StringBuilder line = new StringBuilder(); 430 for (int i = 0;i < args.length;i++) { 431 if (i > 0) { 432 line.append(' '); 433 } 434 Delimiter.EMPTY.escape(args[i], line); 435 } 436 437 // 438 CRaSH main = new CRaSH(); 439 InvocationMatcher<Instance<CRaSH>> matcher = main.descriptor.matcher(); 440 InvocationMatch<Instance<CRaSH>> match = matcher.parse(line.toString()); 441 match.invoke(Util.wrap(main)); 442 } 443}