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