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