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}