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