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