import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNullContents;
import static org.eclipse.tracecompass.common.core.NonNullUtils.nullToEmptyString;
-import java.io.BufferedReader;
import java.io.File;
-import java.io.IOException;
-import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.EnumSet;
import java.util.List;
import java.util.Map;
+import java.util.WeakHashMap;
import java.util.function.Predicate;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
-import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.common.core.log.TraceCompassLog;
+import org.eclipse.tracecompass.common.core.process.ProcessUtils;
+import org.eclipse.tracecompass.common.core.process.ProcessUtils.OutputReaderFunction;
import org.eclipse.tracecompass.internal.analysis.lami.core.Activator;
import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.LamiStrings;
+import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.ShellUtils;
import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiDurationAspect;
import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiEmptyAspect;
import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiGenericAspect;
* LAMI protocol.
*
* @author Alexandre Montplaisir
+ * @author Philippe Proulx
*/
public class LamiAnalysis implements IOnDemandAnalysis {
private static final Logger LOGGER = TraceCompassLog.getLogger(LamiAnalysis.class);
-
- /**
- * Maximum major version of the LAMI protocol we support.
- *
- * Currently only 0.x/unversioned MI, outputted by lttng-analyses 0.4.x
- */
- private static final int MAX_SUPPORTED_MAJOR_VERSION = 0;
-
private static final String DOUBLE_QUOTES = "\""; //$NON-NLS-1$
/* Flags passed to the analysis scripts */
+ private static final String MI_VERSION_FLAG = "--mi-version"; //$NON-NLS-1$
+ private static final String TEST_COMPATIBILITY_FLAG = "--test-compatibility"; //$NON-NLS-1$
private static final String METADATA_FLAG = "--metadata"; //$NON-NLS-1$
private static final String PROGRESS_FLAG = "--output-progress"; //$NON-NLS-1$
private static final String BEGIN_FLAG = "--begin"; //$NON-NLS-1$
private static final String END_FLAG = "--end"; //$NON-NLS-1$
+ /* Log messages */
+ private static final String LOG_RUNNING_MESSAGE = "[LamiAnalysis:RunningCommand] "; //$NON-NLS-1$
+ private static final String LOG_NO_MI_VERSION_FMT = "[LamiAnalysis:InvalidMIVersionReport] Command \"%s\" reports no specific or invalid MI version"; //$NON-NLS-1$
+
+ /* Tokens of the complete command */
private final List<String> fScriptCommand;
- /**
- * The LAMI analysis is considered initialized after we have read the
- * script's --metadata once. This will assign the fields below.
- */
- private boolean fInitialized = false;
+ /* 0 means pre-1.0 LAMI protocol */
+ private int fMiVersion = 0;
- private boolean fIsAvailable;
+ /* Custom name given to this analysis. */
private final String fName;
+
+ /*
+ * True if this analysis is defined by a user, that is, not
+ * provided by TC.
+ */
private final boolean fIsUserDefined;
+
+ /*
+ * The predicate to use to determine if a given trace applies
+ * to this analysis, that is, the given trace's type is compatible
+ * with this analysis a priori. The analysis might not be able to
+ * execute for a given trace, but this is returned by canExecute().
+ */
private final Predicate<ITmfTrace> fAppliesTo;
/* Data defined by the analysis's metadata */
private @Nullable String fAnalysisTitle;
private @Nullable Map<String, LamiTableClass> fTableClasses;
- private boolean fUseProgressOutput;
+
+ /* Cache: true if the initialization process took place already. */
+ private boolean fInitialized = false;
+
+ /*
+ * Cache: assigns a boolean to a trace which indicates if this analysis can
+ * be executed on the given trace. Presence in the map indicates the
+ * compatibility test has already run, so "false" is different than "absent"
+ * here.
+ *
+ * Uses weak keys, so we do not hold references to trace objects and prevent
+ * them from being disposed.
+ */
+ private final Map<ITmfTrace, Boolean> fTraceCompatibilityCache = new WeakHashMap<>();
+
+ /**
+ * Available features.
+ */
+ private enum Features {
+ /** The MI version of the analysis is supported. */
+ VERSION_IS_SUPPORTED,
+
+ /** The analysis is supported at all. */
+ SUPPORTED,
+
+ /** The analysis supports a progress indication output. */
+ OUTPUT_PROGRESS,
+
+ /** The analysis supports testing a given trace for compatibility. */
+ TEST_COMPATIBILITY
+ }
+
+ /* Available features depending on the MI version */
+ private final EnumSet<Features> fFeatures = checkNotNull(EnumSet.noneOf(Features.class));
/**
* Constructor. To be called by implementing classes.
return fAppliesTo.test(trace);
}
+ private boolean testCompatibility(ITmfTrace trace) {
+ final @NonNull String tracePath = checkNotNull(trace.getPath());
+
+ final List<String> commandLine = ImmutableList.<@NonNull String> builder()
+ .addAll(fScriptCommand)
+ .add(TEST_COMPATIBILITY_FLAG)
+ .add(tracePath)
+ .build();
+ final boolean isCompatible = (getOutputFromCommand(commandLine) != null);
+
+ /* Add this result to the compatibility cache. */
+ fTraceCompatibilityCache.put(trace, isCompatible);
+
+ return isCompatible;
+ }
+
+ private boolean isSupported() {
+ initialize();
+
+ return fFeatures.contains(Features.SUPPORTED);
+ }
+
@Override
public boolean canExecute(ITmfTrace trace) {
- initialize();
- return fIsAvailable;
+ /* Make sure this analysis is supported at all. */
+ if (!isSupported()) {
+ return false;
+ }
+
+ if (!fFeatures.contains(Features.TEST_COMPATIBILITY)) {
+ /*
+ * No support for dynamic compatibility testing: suppose this
+ * analysis can run on any trace.
+ */
+ return true;
+ }
+
+ /* Check if this trace is already registered in the cache. */
+ if (fTraceCompatibilityCache.getOrDefault(trace, false)) {
+ return true;
+ }
+
+ /*
+ * Test compatibility since it's supported.
+ */
+ return testCompatibility(trace);
+ }
+
+ private void setFeatures() {
+ if (fMiVersion == 0) {
+ // Pre-1.0 LAMI protocol: supported for backward compatibility
+ fFeatures.add(Features.VERSION_IS_SUPPORTED);
+ return;
+ }
+
+ if (fMiVersion >= 100 && fMiVersion < 200) {
+ // LAMI 1.x
+ fFeatures.add(Features.VERSION_IS_SUPPORTED);
+ fFeatures.add(Features.OUTPUT_PROGRESS);
+ fFeatures.add(Features.TEST_COMPATIBILITY);
+ }
+ }
+
+ private void readVersion() {
+ final String command = fScriptCommand.get(0);
+ final List<String> commandLine = ImmutableList.<@NonNull String> builder()
+ .add(command).add(MI_VERSION_FLAG).build();
+ final String output = getOutputFromCommand(commandLine);
+
+ if (output == null) {
+ LOGGER.info(() -> String.format(LOG_NO_MI_VERSION_FMT, command));
+ return;
+ }
+
+ final String versionString = output.trim();
+
+ if (!versionString.matches("\\d{1,3}\\.\\d{1,3}")) { //$NON-NLS-1$
+ LOGGER.info(() -> String.format(LOG_NO_MI_VERSION_FMT, command));
+ return;
+ }
+
+ LOGGER.info(() -> String.format("[LamiAnalysis:MIVersionReport] Command \"%s\" reports MI version %s", //$NON-NLS-1$
+ command, versionString));
+
+ final String[] parts = versionString.split("\\."); //$NON-NLS-1$
+ final int major = Integer.valueOf(parts[0]);
+ final int minor = Integer.valueOf(parts[1]);
+
+ fMiVersion = major * 100 + minor;
+ }
+
+ private static boolean executableExists(String name) {
+ if (name.contains(File.separator)) {
+ /* This seems like a path, not just an executable name */
+ return Files.isExecutable(Paths.get(name));
+ }
+
+ /* Check if this name is found in the PATH environment variable */
+ final String pathEnv = System.getenv("PATH"); //$NON-NLS-1$
+ final String[] exeDirs = pathEnv.split(checkNotNull(Pattern.quote(File.pathSeparator)));
+
+ return Stream.of(exeDirs)
+ .map(Paths::get)
+ .anyMatch(path -> Files.isExecutable(path.resolve(name)));
}
/**
@VisibleForTesting
protected synchronized void initialize() {
if (fInitialized) {
+ /* Already initialized */
return;
}
- /* Do the analysis's initialization */
+ fInitialized = true;
- /* Check if the script's expected executable is on the PATH */
- String executable = fScriptCommand.get(0);
- boolean exists = Stream.of(System.getenv("PATH").split(checkNotNull(Pattern.quote(File.pathSeparator)))) //$NON-NLS-1$
- .map(Paths::get)
- .anyMatch(path -> Files.exists(path.resolve(executable)));
- if (!exists) {
+ /* Step 1: Check if the script's expected executable is on the PATH. */
+ final String executable = fScriptCommand.get(0);
+ final boolean executableExists = executableExists(executable);
+
+ if (!executableExists) {
/* Script is not found */
- fIsAvailable = false;
- fInitialized = true;
return;
}
- fIsAvailable = checkMetadata();
- fInitialized = true;
+ /*
+ * Step 2: Read the version, which also gives us an indication as
+ * to whether or not this executable supports LAMI 1.0.
+ */
+ readVersion();
+
+ /*
+ * Set the available features according to the MI version.
+ */
+ setFeatures();
+
+ if (!fFeatures.contains(Features.VERSION_IS_SUPPORTED)) {
+ /* Unsupported LAMI version */
+ return;
+ }
+
+ /*
+ * Step 3: Check the metadata. This determines if the analysis is
+ * supported at all or not.
+ */
+ if (checkMetadata()) {
+ fFeatures.add(Features.SUPPORTED);
+ }
}
/**
* Verify that this script returns valid metadata.
*
- * This will populate all remaining non-final fields of this class.
- *
- * @return If the metadata is valid or not
+ * @return True if the command outputs a valid metadata object
*/
@VisibleForTesting
protected boolean checkMetadata() {
/*
* The initialize() phase of the analysis will be used to check the
* script's metadata. Actual runs of the script will use the execute()
- * method below.
+ * method.
*/
List<String> command = ImmutableList.<@NonNull String> builder()
.addAll(fScriptCommand).add(METADATA_FLAG).build();
-
- LOGGER.info(() -> "[LamiAnalysis:RunningMetadataCommand] command=" + command.toString()); //$NON-NLS-1$
-
+ LOGGER.info(() -> "[LamiAnalysis:RunningMetadataCommand] " + command.toString()); //$NON-NLS-1$
String output = getOutputFromCommand(command);
if (output == null || output.isEmpty()) {
return false;
JSONObject obj = new JSONObject(output);
fAnalysisTitle = obj.getString(LamiStrings.TITLE);
- /* Very early scripts may not contain the "mi-version" */
- JSONObject miVersion = obj.optJSONObject(LamiStrings.MI_VERSION);
- if (miVersion == null) {
- /* Before version 0.1 */
- fUseProgressOutput = false;
- } else {
- int majorVersion = miVersion.getInt(LamiStrings.MAJOR);
- if (majorVersion <= MAX_SUPPORTED_MAJOR_VERSION) {
- fUseProgressOutput = true;
- } else {
- /* Unknown version, we do not support it */
- return false;
- }
- }
-
JSONObject tableClasses = obj.getJSONObject(LamiStrings.TABLE_CLASSES);
@NonNull String[] tableClassNames = checkNotNullContents(JSONObject.getNames(tableClasses));
} catch (JSONException e) {
/* Error in the parsing of the JSON, script is broken? */
LOGGER.severe(() -> "[LamiAnalysis:ErrorParsingMetadata] msg=" + e.getMessage()); //$NON-NLS-1$
- Activator.instance().logError(e.getMessage());
return false;
}
return true;
if (range != null) {
begin = range.getStartTime().getValue();
end = range.getEndTime().getValue();
+ if (range.getStartTime().compareTo(range.getEndTime()) > 0) {
+ begin = range.getEndTime().getValue();
+ end = range.getStartTime().getValue();
+ }
}
ImmutableList.Builder<String> builder = ImmutableList.builder();
builder.addAll(fScriptCommand);
- if (fUseProgressOutput) {
+ if (fFeatures.contains(Features.OUTPUT_PROGRESS)) {
builder.add(PROGRESS_FLAG);
}
return builder;
}
- /**
- * Call the currently defined LAMI script with the given arguments.
- *
- * @param timeRange
- * The time range. Null for the whole trace.
- * @param monitor
- * The progress monitor used to report progress
- * @return The script's output, formatted into {@link LamiTableEntry}'s.
- * @throws CoreException
- * If execution did not terminate normally
- */
@Override
public List<LamiResultTable> execute(ITmfTrace trace, @Nullable TmfTimeRange timeRange,
- String extraParams, IProgressMonitor monitor) throws CoreException {
+ String extraParamsString, IProgressMonitor monitor) throws CoreException {
/* Should have been called already, but in case it was not */
initialize();
final @NonNull String tracePath = checkNotNull(trace.getPath());
- final @NonNull String[] splitParams = extraParams.trim().split(" "); //$NON-NLS-1$
+ final @NonNull String trimmedExtraParamsString = checkNotNull(extraParamsString.trim());
+ final List<String> extraParams = ShellUtils.commandStringToArgs(trimmedExtraParamsString);
ImmutableList.Builder<String> builder = getBaseCommand(timeRange);
- if (!extraParams.trim().equals("")) { //$NON-NLS-1$
- builder.addAll(Arrays.asList(splitParams));
- }
+ builder.addAll(extraParams);
builder.add(tracePath);
List<String> command = builder.build();
-
- LOGGER.info(() -> "[LamiAnalysis:RunningExecuteCommand] command=" + command.toString()); //$NON-NLS-1$
+ LOGGER.info(() -> "[LamiAnalysis:RunningExecuteCommand] " + command.toString()); //$NON-NLS-1$
String output = getResultsFromCommand(command, monitor);
if (output.isEmpty()) {
JSONObject result = results.getJSONObject(i);
/* Parse the time-range */
- JSONObject trObject = result.getJSONObject(LamiStrings.TIME_RANGE);
- long start = trObject.getLong(LamiStrings.BEGIN);
- long end = trObject.getLong(LamiStrings.END);
- LamiTimeRange tr = new LamiTimeRange(start, end);
+ JSONObject trObject = checkNotNull(result.getJSONObject(LamiStrings.TIME_RANGE));
+ LamiData trData = LamiData.createFromObject(trObject);
+ if (!(trData instanceof LamiTimeRange)) {
+ throw new JSONException("Time range did not have expected class type."); //$NON-NLS-1$
+ }
+ LamiTimeRange tr = (LamiTimeRange) trData;
/* Parse the table's class */
LamiTableClass tableClass;
*/
@VisibleForTesting
protected @Nullable String getOutputFromCommand(List<String> command) {
- try {
- ProcessBuilder builder = new ProcessBuilder(command);
- builder.redirectErrorStream(true);
-
- Process p = builder.start();
- try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));) {
- int ret = p.waitFor();
- String output = br.lines().collect(Collectors.joining());
-
- return (ret == 0 ? output : null);
- }
- } catch (IOException | InterruptedException e) {
+ LOGGER.info(() -> LOG_RUNNING_MESSAGE + ' ' + command.toString());
+ List<String> lines = ProcessUtils.getOutputFromCommand(command);
+ if (lines == null) {
return null;
}
+ return String.join("", lines); //$NON-NLS-1$
}
/**
@VisibleForTesting
protected String getResultsFromCommand(List<String> command, IProgressMonitor monitor)
throws CoreException {
+ List<String> lines = ProcessUtils.getOutputFromCommandCancellable(command, monitor, nullToEmptyString(Messages.LamiAnalysis_MainTaskName), OUTPUT_READER);
+ return checkNotNull(String.join("", lines)); //$NON-NLS-1$
+ }
- final int scale = 1000;
+ private static final OutputReaderFunction OUTPUT_READER = (reader, monitor) -> {
double workedSoFar = 0.0;
- ProcessCanceller cancellerRunnable = null;
- Thread cancellerThread = null;
-
- try {
- monitor.beginTask(Messages.LamiAnalysis_MainTaskName, scale);
-
- ProcessBuilder builder = new ProcessBuilder(command);
- builder.redirectErrorStream(false);
-
- Process p = checkNotNull(builder.start());
-
- cancellerRunnable = new ProcessCanceller(p, monitor);
- cancellerThread = new Thread(cancellerRunnable);
- cancellerThread.start();
-
- List<String> results = new ArrayList<>();
-
- try (BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));) {
- String line = in.readLine();
- while (line != null && !line.matches("\\s*\\{.*")) { //$NON-NLS-1$
- /*
- * This is a line indicating progress, it has the form:
- *
- * 0.123 3000 of 5000 events processed
- *
- * The first part indicates the estimated fraction (out of
- * 1.0) of work done. The second part is status text.
- */
-
- // Trim the line first to make sure the first character is
- // significant
- line = line.trim();
-
- // Split at the first space
- String[] elems = line.split(" ", 2); //$NON-NLS-1$
-
- if (elems[0].matches("\\d.*")) { //$NON-NLS-1$
- // It looks like we have a progress indication
- try {
- // Try parsing the number
- double cumulativeWork = Double.parseDouble(elems[0]) * scale;
- double workedThisLoop = cumulativeWork - workedSoFar;
-
- // We're going backwards? Do not update the
- // monitor's value
- if (workedThisLoop > 0) {
- monitor.internalWorked(workedThisLoop);
- workedSoFar = cumulativeWork;
- }
-
- // There is a message: update the monitor's task name
- if (elems.length >= 2) {
- monitor.setTaskName(elems[1].trim());
- }
- } catch (NumberFormatException e) {
- // Continue reading progress lines anyway
- }
+ String line = reader.readLine();
+ while (line != null && !line.matches("\\s*\\{.*")) { //$NON-NLS-1$
+ /*
+ * This is a line indicating progress, it has the form:
+ *
+ * 0.123 3000 of 5000 events processed
+ *
+ * The first part indicates the estimated fraction (out of 1.0) of
+ * work done. The second part is status text.
+ */
+
+ // Trim the line first to make sure the first character is
+ // significant
+ line = line.trim();
+
+ // Split at the first space
+ String[] elems = line.split(" ", 2); //$NON-NLS-1$
+
+ if (elems[0].matches("\\d.*")) { //$NON-NLS-1$
+ // It looks like we have a progress indication
+ try {
+ // Try parsing the number
+ double cumulativeWork = Double.parseDouble(elems[0]) * 1000;
+ double workedThisLoop = cumulativeWork - workedSoFar;
+
+ // We're going backwards? Do not update the
+ // monitor's value
+ if (workedThisLoop > 0) {
+ monitor.internalWorked(workedThisLoop);
+ workedSoFar = cumulativeWork;
}
- line = in.readLine();
- }
- while (line != null) {
- /*
- * We have seen the first line containing a '{', this is our
- * JSON output!
- */
- results.add(line);
- line = in.readLine();
- }
- }
- int ret = p.waitFor();
-
- if (monitor.isCanceled()) {
- /* We were interrupted by the canceller thread. */
- IStatus status = new Status(IStatus.CANCEL, Activator.instance().getPluginId(), null);
- throw new CoreException(status);
- }
-
- if (ret != 0) {
- /*
- * Something went wrong running the external script. We will
- * gather the stderr and report it to the user.
- */
- BufferedReader br = new BufferedReader(new InputStreamReader(p.getErrorStream()));
- List<String> stdErrOutput = br.lines().collect(Collectors.toList());
-
- MultiStatus status = new MultiStatus(Activator.instance().getPluginId(),
- IStatus.ERROR, Messages.LamiAnalysis_ErrorDuringExecution, null);
- for (String str : stdErrOutput) {
- status.add(new Status(IStatus.ERROR, Activator.instance().getPluginId(), str));
- }
- if (stdErrOutput.isEmpty()) {
- /*
- * At least say "no output", so an error message actually
- * shows up.
- */
- status.add(new Status(IStatus.ERROR, Activator.instance().getPluginId(), Messages.LamiAnalysis_ErrorNoOutput));
- }
- throw new CoreException(status);
- }
-
- /* External script ended successfully, all is fine! */
- String resultsStr = results.stream().collect(Collectors.joining());
- return checkNotNull(resultsStr);
-
- } catch (IOException | InterruptedException e) {
- IStatus status = new Status(IStatus.ERROR, Activator.instance().getPluginId(), Messages.LamiAnalysis_ExecutionInterrupted, e);
- throw new CoreException(status);
-
- } finally {
- if (cancellerRunnable != null) {
- cancellerRunnable.setFinished();
- }
- if (cancellerThread != null) {
- try {
- cancellerThread.join();
- } catch (InterruptedException e) {
+ // There is a message: update the monitor's task name
+ if (elems.length >= 2) {
+ monitor.setTaskName(elems[1].trim());
+ }
+ } catch (NumberFormatException e) {
+ // Continue reading progress lines anyway
}
}
- monitor.done();
+ line = reader.readLine();
}
- }
-
- private static class ProcessCanceller implements Runnable {
-
- private final Process fProcess;
- private final IProgressMonitor fMonitor;
-
- private boolean fIsFinished = false;
- public ProcessCanceller(Process process, IProgressMonitor monitor) {
- fProcess = process;
- fMonitor = monitor;
+ List<String> results = new ArrayList<>();
+ while (line != null) {
+ /*
+ * We have seen the first line containing a '{', this is our JSON
+ * output!
+ */
+ results.add(line);
+ line = reader.readLine();
}
+ return results;
+ };
- public void setFinished() {
- fIsFinished = true;
- }
-
- @Override
- public void run() {
- try {
- while (!fIsFinished) {
- Thread.sleep(500);
- if (fMonitor.isCanceled()) {
- fProcess.destroy();
- return;
- }
- }
- } catch (InterruptedException e) {
- }
- }
-
- }
@Override
public @NonNull String getName() {