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    }