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