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 }