001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2008 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2008 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041: package org.netbeans.modules.ruby.platform.execution;
042:
043: import java.awt.event.ActionEvent;
044: import java.io.File;
045: import java.io.IOException;
046: import java.util.ArrayList;
047: import java.util.Arrays;
048: import java.util.HashSet;
049: import java.util.List;
050: import java.util.Map;
051: import java.util.Map.Entry;
052: import java.util.Set;
053: import java.util.logging.Level;
054: import java.util.logging.Logger;
055:
056: import javax.swing.AbstractAction;
057: import javax.swing.Action;
058:
059: import org.netbeans.api.progress.ProgressHandle;
060: import org.netbeans.api.progress.ProgressHandleFactory;
061: import org.netbeans.modules.ruby.platform.RubyExecution;
062: import org.netbeans.modules.ruby.platform.Util;
063: import org.netbeans.modules.ruby.platform.spi.RubyDebuggerImplementation;
064: import org.openide.ErrorManager;
065: import org.openide.execution.ExecutionEngine;
066: import org.openide.execution.ExecutorTask;
067: import org.openide.util.Cancellable;
068: import org.openide.util.Exceptions;
069: import org.openide.util.Lookup;
070: import org.openide.util.NbBundle;
071: import org.openide.util.RequestProcessor;
072: import org.openide.util.Task;
073: import org.openide.util.TaskListener;
074: import org.openide.util.Utilities;
075: import org.openide.windows.IOProvider;
076: import org.openide.windows.InputOutput;
077:
078: /**
079: * <p>An ExecutionService takes an {@link ExecutionDescriptor} and executes it.
080: * It will execute the program with an associated I/O window, with stop and
081: * restart buttons. It will also obey various descriptor properties such as
082: * whether or not to show a progress bar.
083: * <p>
084: * All launched processes will be killed on exit. Possibly I could make this
085: * optional or at least ask the user.
086: * </p>
087: *
088: * @todo Add a Restart button which accomplishes both a stop and a restart
089: *
090: * @author Tor Norbye
091: */
092: public class ExecutionService {
093:
094: public static final Logger LOGGER = Logger
095: .getLogger(ExecutionService.class.getName());
096:
097: static {
098: Thread t = new Thread() {
099: @Override
100: public void run() {
101: ExecutionService.killAll();
102: }
103: };
104:
105: Runtime.getRuntime().addShutdownHook(t);
106: }
107:
108: /** Display names of currently active processes. */
109: private static final Set<String> ACTIVE_DISPLAY_NAMES = new HashSet<String>();
110:
111: /** Set of currently active processes. */
112: private final static Set<ExecutionService> RUNNING_PROCESSES = new HashSet<ExecutionService>();
113:
114: private InputOutput io;
115: private StopAction stopAction;
116: private RerunAction rerunAction;
117: protected ExecutionDescriptor descriptor;
118: private String displayName; // May be tweaked from descriptor to deal with duplicate running same-name processes
119:
120: private boolean rerun;
121:
122: public ExecutionService(ExecutionDescriptor descriptor) {
123: this .descriptor = descriptor;
124: }
125:
126: public void setupProcessEnvironment(Map<String, String> env) {
127: String path = descriptor.getCmd().getParent();
128: if (!Utilities.isWindows()) {
129: path = path.replace(" ", "\\ "); // NOI18N
130: }
131:
132: // Find PATH environment variable - on Windows it can be some other
133: // case and we should use whatever it has.
134: String pathName = "PATH"; // NOI18N
135:
136: if (Utilities.isWindows()) {
137: pathName = "Path"; // NOI18N
138:
139: for (String key : env.keySet()) {
140: if ("PATH".equals(key.toUpperCase())) { // NOI18N
141: pathName = key;
142:
143: break;
144: }
145: }
146: }
147:
148: String currentPath = env.get(pathName);
149:
150: if (currentPath == null) {
151: currentPath = "";
152: }
153:
154: currentPath = path + File.pathSeparator + currentPath;
155:
156: if (descriptor.getAppendJdkToPath()) {
157: String javaHome = System.getProperty("jruby.java.home"); // NOI18N
158:
159: if (javaHome == null) {
160: javaHome = System.getProperty("java.home"); // NOI18N
161: }
162:
163: if (javaHome != null) {
164: javaHome = javaHome + File.separator + "bin"; // NOI18N
165: if (!Utilities.isWindows()) {
166: javaHome = javaHome.replace(" ", "\\ "); // NOI18N
167: }
168: currentPath = currentPath + File.pathSeparator
169: + javaHome;
170: }
171: }
172:
173: env.put(pathName, currentPath); // NOI18N
174: }
175:
176: public void kill() {
177: if (stopAction != null) {
178: stopAction.actionPerformed(null);
179: if (stopAction.process != null) {
180: stopAction.process.destroy();
181: }
182: }
183: }
184:
185: public static void killAll() {
186: for (ExecutionService service : RUNNING_PROCESSES) {
187: service.kill();
188: }
189: }
190:
191: Task rerun() {
192: try {
193: io.getOut().reset();
194: } catch (IOException ex) {
195: Exceptions.printStackTrace(ex);
196: }
197: rerun = true;
198: return run();
199: }
200:
201: /**
202: * Retruns list of default arguments and options from the descriptor's
203: * <code>initialArgs</code>, <code>script</code> and
204: * <code>additionalArgs</code> in that order.
205: */
206: protected List<? extends String> buildArgs() {
207: List<String> argvList = new ArrayList<String>();
208: File cmd = descriptor.cmd;
209: assert cmd != null;
210:
211: if (descriptor.getInitialArgs() != null) {
212: argvList.addAll(Arrays.asList(descriptor.getInitialArgs()));
213: }
214:
215: if (descriptor.script != null) {
216: argvList.add(descriptor.script);
217: }
218:
219: if (descriptor.getAdditionalArgs() != null) {
220: argvList.addAll(Arrays.asList(descriptor
221: .getAdditionalArgs()));
222: }
223: return argvList;
224: }
225:
226: public Task run() {
227: if (!rerun) {
228: // try to find free output windows
229: synchronized (this ) {
230: if (io == null) {
231: Entry<InputOutput, String> entry = FreeIOHandler
232: .findFreeIO(descriptor.getDisplayName(),
233: descriptor.frontWindow);
234: if (entry != null) {
235: io = entry.getKey();
236: displayName = entry.getValue();
237: }
238: }
239: }
240:
241: if (io == null || stopAction == null) { // free IO was not found
242: displayName = getNonActiveDisplayName(descriptor
243: .getDisplayName());
244:
245: stopAction = new StopAction();
246: rerunAction = new RerunAction(this , descriptor
247: .getFileObject());
248:
249: if (io == null) {
250: io = IOProvider.getDefault().getIO(displayName,
251: new Action[] { rerunAction, stopAction });
252:
253: try {
254: io.getOut().reset();
255: } catch (IOException exc) {
256: ErrorManager.getDefault().notify(exc);
257: }
258:
259: // Note - do this AFTER the reset() call above; if not, weird bugs occur
260: io.setErrSeparated(false);
261:
262: // Open I/O window now. This should probably be configurable.
263: if (descriptor.frontWindow) {
264: io.select();
265: }
266: }
267: }
268: }
269:
270: ACTIVE_DISPLAY_NAMES.add(displayName);
271: io.setInputVisible(descriptor.inputVisible);
272:
273: //io.getErr().println(NbBundle.getMessage(RubyExecutionService.class, "RunStarting"));
274: Runnable runnable = new Runnable() {
275: public void run() {
276: File cmd = descriptor.cmd;
277: try {
278: Process process = null;
279: if (descriptor.debug) {
280: RubyDebuggerImplementation debugger = Lookup
281: .getDefault()
282: .lookup(
283: RubyDebuggerImplementation.class);
284: if (debugger != null) {
285: process = debugger.debug(descriptor);
286: }
287: if (process == null) {
288: return;
289: }
290: } else {
291: List<String> commandL = new ArrayList<String>();
292: if (!cmd.getName().startsWith("jruby")
293: || RubyExecution.LAUNCH_JRUBY_SCRIPT) { // NOI18N
294: commandL.add(cmd.getPath());
295: }
296:
297: List<? extends String> args = buildArgs();
298: commandL.addAll(args);
299: String[] command = commandL
300: .toArray(new String[commandL.size()]);
301:
302: if ((command != null) && Utilities.isWindows()) {
303: for (int i = 0; i < command.length; i++) {
304: if ((command[i] != null)
305: && (command[i].indexOf(' ') != -1)
306: && (command[i].indexOf('"') == -1)) { // NOI18N
307: command[i] = '"' + command[i] + '"'; // NOI18N
308: }
309: }
310: }
311: ProcessBuilder pb = new ProcessBuilder(command);
312: pb.directory(descriptor.pwd);
313:
314: Map<String, String> env = pb.environment();
315: // set up custom environment configuration
316: env.putAll(descriptor
317: .getAdditionalEnvironment());
318: if (descriptor.addBinPath) {
319: setupProcessEnvironment(env);
320: }
321: Util.adjustProxy(pb);
322: ExecutionService.logProcess(pb);
323: process = pb.start();
324: }
325:
326: RUNNING_PROCESSES.add(ExecutionService.this );
327: stopAction.process = process;
328: runIO(stopAction, process, io, descriptor
329: .getFileLocator(),
330: descriptor.outputRecognizers);
331:
332: process.waitFor();
333: } catch (IOException ex) {
334: ErrorManager.getDefault().notify(ex);
335: } catch (InterruptedException ex) {
336: ErrorManager.getDefault().notify(ex);
337: }
338: }
339: };
340:
341: final ProgressHandle handle;
342:
343: if (descriptor.showProgress || descriptor.showSuspended) {
344: handle = ProgressHandleFactory.createHandle(displayName,
345: new Cancellable() {
346: public boolean cancel() {
347: stopAction.actionPerformed(null);
348:
349: return true;
350: }
351: }, new AbstractAction() {
352: public void actionPerformed(ActionEvent e) {
353: io.select();
354: }
355: });
356: handle.start();
357: handle.switchToIndeterminate();
358:
359: if (descriptor.showSuspended) {
360: handle.suspend(NbBundle.getMessage(
361: ExecutionService.class, "Running"));
362: }
363: } else {
364: handle = null;
365: }
366:
367: stopAction.setEnabled(true);
368: rerunAction.setEnabled(false);
369:
370: ExecutorTask task = ExecutionEngine.getDefault().execute(null,
371: runnable, InputOutput.NULL);
372:
373: task.addTaskListener(new TaskListener() {
374: public void taskFinished(Task task) {
375: RUNNING_PROCESSES.remove(ExecutionService.this );
376:
377: if (io != null) {
378: FreeIOHandler.addFreeIO(io, displayName);
379: }
380:
381: ACTIVE_DISPLAY_NAMES.remove(displayName);
382:
383: //if (task instanceof ExecutorTask) {
384: // result = ((ExecutorTask)task).result();
385: //}
386: //
387: // if (result == 0) {
388: // System.err.println(NbBundle.getMessage(RubyExecutionService.class,
389: // "RunCompleted"));
390: // } else {
391: // System.err.println(NbBundle.getMessage(RubyExecutionService.class,
392: // "RunFailed", result));
393: // }
394: if (descriptor.postBuildAction != null) {
395: descriptor.postBuildAction.run();
396: }
397:
398: if (handle != null) {
399: handle.finish();
400: }
401:
402: stopAction.setEnabled(false);
403: rerunAction.setEnabled(true);
404:
405: if (stopAction.process != null) {
406: stopAction.process.destroy();
407: stopAction.process = null;
408: }
409: }
410: });
411:
412: return task;
413: }
414:
415: private static void runIO(final StopAction sa, Process process,
416: InputOutput ioput, FileLocator fileLocator,
417: List<OutputRecognizer> recognizers) {
418: try {
419: InputForwarder in = new InputForwarder(process
420: .getOutputStream(), ioput.getIn());
421: OutputForwarder out = new OutputForwarder(process
422: .getInputStream(), ioput.getOut(), fileLocator,
423: recognizers, sa);
424: OutputForwarder err = new OutputForwarder(process
425: .getErrorStream(), ioput.getErr(), fileLocator,
426: recognizers, sa);
427:
428: RequestProcessor PROCESSOR = new RequestProcessor(
429: "Process Execution Stream Handler", 3, true); // NOI18N
430:
431: TaskListener tl = new TaskListener() {
432: public void taskFinished(Task task) {
433: sa.notifyDone((RequestProcessor.Task) task);
434: }
435: };
436:
437: RequestProcessor.Task outTask = PROCESSOR.post(out);
438: RequestProcessor.Task errTask = PROCESSOR.post(err);
439: RequestProcessor.Task inTask = PROCESSOR.post(in);
440:
441: outTask.addTaskListener(tl);
442: errTask.addTaskListener(tl);
443: inTask.addTaskListener(tl);
444:
445: sa.processorTasks.add(outTask);
446: sa.processorTasks.add(errTask);
447: sa.processorTasks.add(inTask);
448:
449: process.waitFor();
450: sa.process = null;
451:
452: in.cancel();
453: outTask.waitFinished();
454: errTask.waitFinished();
455: inTask.waitFinished();
456:
457: PROCESSOR.stop();
458: } catch (InterruptedException exc) {
459: // XXX Uhm... why do we log this? Isn't this a good thing?
460: // This happens if we try to cancel the process for example
461: ErrorManager.getDefault().notify(
462: ErrorManager.INFORMATIONAL, exc);
463: }
464: }
465:
466: static boolean isAppropriateName(String base, String toMatch) {
467: if (!toMatch.startsWith(base)) {
468: return false;
469: }
470: return toMatch.substring(base.length()).matches(
471: "^(\\ #[0-9]+)?$"); // NOI18N
472: }
473:
474: private static String getNonActiveDisplayName(
475: final String displayNameBase) {
476: String nonActiveDN = displayNameBase;
477: if (ACTIVE_DISPLAY_NAMES.contains(nonActiveDN)) {
478: // Uniquify: "prj (targ) #2", "prj (targ) #3", etc.
479: int i = 2;
480: String testdn;
481:
482: do {
483: testdn = NbBundle.getMessage(ExecutionService.class,
484: "Uniquified", nonActiveDN, i++);
485: } while (ACTIVE_DISPLAY_NAMES.contains(testdn));
486:
487: nonActiveDN = testdn;
488: }
489: assert !ACTIVE_DISPLAY_NAMES.contains(nonActiveDN);
490: return nonActiveDN;
491: }
492:
493: /** Just helper method for logging. */
494: public static void logProcess(final ProcessBuilder pb) {
495: if (LOGGER.isLoggable(Level.FINE)) {
496: File dir = pb.directory();
497: String basedir = dir == null ? "" : "(basedir: "
498: + dir.getAbsolutePath() + ") ";
499: LOGGER.fine("Running: " + basedir + '"'
500: + getProcessAsString(pb.command()) + '"');
501: LOGGER.fine("Environment: " + pb.environment());
502: }
503: }
504:
505: /** Just helper method for logging. */
506: private static String getProcessAsString(
507: List<? extends String> process) {
508:
509: StringBuilder sb = new StringBuilder();
510: for (String arg : process) {
511: sb.append(arg).append(' ');
512: }
513: return sb.toString().trim();
514: }
515:
516: }
|