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