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 org.crsh.cli.Argument;
028    import org.crsh.cli.Command;
029    import org.crsh.cli.Option;
030    import org.crsh.cli.Usage;
031    import org.crsh.cli.descriptor.CommandDescriptor;
032    import org.crsh.cli.impl.Delimiter;
033    import org.crsh.cli.impl.descriptor.IntrospectionException;
034    import org.crsh.cli.impl.invocation.InvocationMatch;
035    import org.crsh.cli.impl.invocation.InvocationMatcher;
036    import org.crsh.cli.impl.lang.CommandFactory;
037    import org.crsh.cli.impl.lang.Instance;
038    import org.crsh.cli.impl.lang.Util;
039    import org.crsh.console.jline.JLineProcessor;
040    import org.crsh.plugin.ResourceManager;
041    import org.crsh.shell.Shell;
042    import org.crsh.shell.ShellFactory;
043    import org.crsh.shell.impl.remoting.RemoteServer;
044    import org.crsh.util.CloseableList;
045    import org.crsh.util.InterruptHandler;
046    import org.crsh.util.Utils;
047    import org.crsh.vfs.FS;
048    import org.crsh.vfs.Path;
049    import org.crsh.vfs.Resource;
050    import org.fusesource.jansi.AnsiConsole;
051    
052    import java.io.ByteArrayInputStream;
053    import java.io.Closeable;
054    import java.io.File;
055    import java.io.FileDescriptor;
056    import java.io.FileInputStream;
057    import java.io.FileOutputStream;
058    import java.io.IOException;
059    import java.io.PrintStream;
060    import java.util.List;
061    import java.util.Properties;
062    import java.util.jar.Attributes;
063    import java.util.jar.JarOutputStream;
064    import java.util.jar.Manifest;
065    import java.util.logging.Level;
066    import java.util.logging.Logger;
067    import java.util.regex.Pattern;
068    
069    public 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    }