/*******************************************************************************
* Copyright (c) 2008, 2011 Thomas Holland (thomas@innot.de) and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Thomas Holland - initial API and implementation
*******************************************************************************/
package de.innot.avreclipse.core.targets.tools;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.ui.console.IOConsoleOutputStream;
import org.eclipse.ui.console.MessageConsole;
import de.innot.avreclipse.AVRPlugin;
import de.innot.avreclipse.core.avrdude.AVRDudeException;
import de.innot.avreclipse.core.avrdude.AVRDudeException.Reason;
import de.innot.avreclipse.core.targets.ITargetConfigConstants;
import de.innot.avreclipse.core.targets.ITargetConfiguration;
import de.innot.avreclipse.core.targets.ToolManager;
import de.innot.avreclipse.core.toolinfo.ExternalCommandLauncher;
import de.innot.avreclipse.core.toolinfo.ICommandOutputListener;
/**
* @author Thomas Holland
* @since
*
*/
public abstract class AbstractTool {
private final ITargetConfiguration fHC;
protected AbstractTool(ITargetConfiguration hc) {
fHC = hc;
}
protected abstract String getName();
protected abstract String getId();
/**
* Returns the value of the command attribute.
* <p>
* This is used to get the name of the executable for the tool. The command can be either just
* the command name (e.g. 'avrdude') or a absolute path (e.g. '/usr/bin/avrdude')
* </p>
*
* @return String with the command
*/
protected abstract String getCommand();
protected abstract ICommandOutputListener getOutputListener();
protected ITargetConfiguration getHardwareConfig() {
return fHC;
}
/**
* Runs the tool with the given arguments.
* <p>
* The Output of stdout and stderr are merged and returned in a <code>List<String></code>.
* </p>
* <p>
* If the command fails to execute an entry is written to the log and an
* {@link AVRDudeException} with the reason is thrown.
* </p>
*
* @param arguments
* Zero or more arguments for avrdude
* @return A list of all output lines, or <code>null</code> if the command could not be
* launched.
* @throws AVRDudeException
* when avrdude cannot be started or when avrdude returned an
*/
public List<String> runCommand(String... arguments) throws AVRDudeException {
List<String> arglist = new ArrayList<String>(1);
for (String arg : arguments) {
arglist.add(arg);
}
return runCommand(arglist, new NullProgressMonitor(), false, null);
}
/**
* Runs tool with the given arguments.
* <p>
* The Output of stdout and stderr are merged and returned in a <code>List<String></code>.
* If the "use Console" flag is set in the Preferences, the complete output is shown on a
* Console as well.
* </p>
* <p>
* If the command fails to execute an entry is written to the log and an
* {@link AVRDudeException} with the reason is thrown.
* </p>
*
* @param arguments
* <code>List<String></code> with the arguments
* @param monitor
* <code>IProgressMonitor</code> to cancel the running process.
* @param forceconsole
* If <code>true</code> all output is copied to the console, regardless of the "use
* console" flag.
* @param cwd
* <code>IPath</code> with a current working directory or <code>null</code> to use
* the default working directory (usually the one defined with the system property
* <code>user.dir</code). May not be empty.
* @return A list of all output lines, or <code>null</code> if the command could not be
* launched.
* @throws AVRDudeException
* when the tool cannot be started or when it returns with an error.
*/
public List<String> runCommand(List<String> arglist, IProgressMonitor monitor,
boolean forceconsole, IPath cwd) throws AVRDudeException {
try {
monitor.beginTask("Running " + getName(), 100);
// Check if the CWD is valid
if (cwd != null && cwd.isEmpty()) {
throw new AVRDudeException(Reason.INVALID_CWD,
"CWD does not point to a valid directory.");
}
// TODO: resolve variables in the path
String command = getCommand();
// Set up the External Command
ExternalCommandLauncher launcher = new ExternalCommandLauncher(command, arglist, cwd);
launcher.redirectErrorStream(true);
// Set the Console (if requested by the user for the target configuratio)
MessageConsole console = null;
String consoleattr = getId() + ".useconsole";
boolean useconsole = fHC.getBooleanAttribute(consoleattr);
if (useconsole || forceconsole) {
console = AVRPlugin.getDefault().getConsole("External Tools");
launcher.setConsole(console);
}
ICommandOutputListener outputlistener = getOutputListener();
outputlistener.init(monitor);
launcher.setCommandOutputListener(outputlistener);
// USB devices:
// This will delay the actual call if the previous call finished less than the
// user provided time in milliseconds
avrdudeInvocationDelay(console, new SubProgressMonitor(monitor, 10));
// Run avrdude
try {
int result = launcher.launch(new SubProgressMonitor(monitor, 80));
// Test if launch was aborted
Reason abortReason = outputlistener.getAbortReason();
if (abortReason != null) {
throw new AVRDudeException(abortReason, outputlistener.getAbortLine());
}
if (result == -1) {
throw new AVRDudeException(Reason.USER_CANCEL, "");
}
} catch (IOException e) {
// Something didn't work while running the external command
throw new AVRDudeException(Reason.NO_AVRDUDE_FOUND,
"Cannot run AVRDude executable. Please check the AVR path preferences.", e);
}
// Everything was fine: get the ooutput from avrdude and return it
// to the caller
List<String> stdout = launcher.getStdOut();
monitor.worked(10);
return stdout;
} finally {
monitor.done();
String progport = fHC.getAttribute(ITargetConfigConstants.ATTR_PROGRAMMER_PORT);
ToolManager.getDefault().setLastAccess(progport, System.currentTimeMillis());
}
}
/**
* Delay for the user specified invocation delay time.
* <p>
* This method will take the user supplied delay value from the given ProgrammerConfig, check
* how much time has passed since the last tool invocation finished and - if actually required -
* wait for the remaining milliseconds.
* </p>
* <p>
* While sleeping this method will wake up every 10 ms to check if the user has cancelled, in
* which case an {@link AVRDudeException} with {@link Reason#USER_CANCEL} is thrown.
* </p>
*
* @param console
* If not <code>null</code>, then the start and end of the delay is logged on the
* console.
* @param monitor
* polled for user cancel event.
* @throws AVRDudeException
* when the user cancels the delay.
*/
private void avrdudeInvocationDelay(MessageConsole console, IProgressMonitor monitor)
throws AVRDudeException {
// Get the (optional) invocation delay value
String delayattr = ITargetConfigConstants.ATTR_USB_DELAY;
String delayvalue = fHC.getAttribute(delayattr);
if (delayvalue == null || delayvalue.length() == 0) {
return;
}
final int delay = Integer.decode(delayvalue);
if (delay == 0) {
return;
}
IOConsoleOutputStream ostream = null;
if (console != null) {
ostream = console.newOutputStream();
}
String programmerport = fHC.getAttribute(ITargetConfigConstants.ATTR_PROGRAMMER_PORT);
long lastaccess = ToolManager.getDefault().getLastAccess(programmerport);
final long targetmillis = lastaccess + delay;
// Quick exit if the delay has already expired
long targetdelay = targetmillis - System.currentTimeMillis();
if (targetdelay < 1L) {
return;
}
try {
monitor.beginTask("delay", (int) (targetdelay / 10));
writeOutput(ostream, "\n>>> " + getName() + " invocation delay: " + targetdelay
+ " milliseconds\n");
// delay for specified amount of milliseconds
// To allow user cancel during long delays we check the monitor every 10
// milliseconds.
// This is the fix for Bug 2071415
while (System.currentTimeMillis() < targetmillis) {
if (monitor.isCanceled()) {
writeOutput(ostream, ">>> " + getName() + " invocation delay: cancelled\n");
throw new AVRDudeException(Reason.USER_CANCEL, "User cancelled");
}
Thread.sleep(10);
monitor.worked(1);
}
writeOutput(ostream, ">>> " + getName() + " invocation delay: finished\n");
} catch (InterruptedException e) {
throw new AVRDudeException(Reason.USER_CANCEL, "System interrupt");
} catch (IOException e) {
// ignore exception
} finally {
if (ostream != null) {
try {
ostream.close();
} catch (IOException e) {
// ignore exception
}
}
monitor.done();
}
}
/**
* Convenience method to print a message to the given stream. This method checks that the stream
* exists.
*
* @param ostream
* @param message
* @throws IOException
*/
private void writeOutput(IOConsoleOutputStream ostream, String message) throws IOException {
if (ostream != null) {
ostream.write(message);
}
}
}