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