common: Add a ProcessUtils for external process launching
[deliverable/tracecompass.git] / analysis / org.eclipse.tracecompass.analysis.lami.core / src / org / eclipse / tracecompass / internal / provisional / analysis / lami / core / module / LamiAnalysis.java
index 5cd62122c629a7dced5b3b8f1cc395d8698f1775..019b206caabebdadae52fdb0bf5e8d41b328dafe 100644 (file)
@@ -13,28 +13,36 @@ import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;
 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.function.Predicate;
+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 java.util.stream.Collectors;
 import java.util.stream.Stream;
 
+import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+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;
 import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiIRQNameAspect;
 import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiIRQNumberAspect;
@@ -43,8 +51,6 @@ import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.L
 import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiProcessNameAspect;
 import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiProcessPIDAspect;
 import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiProcessTIDAspect;
-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.LamiTableEntryAspect;
 import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiTimeRangeBeginAspect;
 import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiTimeRangeDurationAspect;
@@ -54,7 +60,6 @@ import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.types.La
 import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.types.LamiData.DataType;
 import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.types.LamiTimeRange;
 import org.eclipse.tracecompass.tmf.core.analysis.ondemand.IOnDemandAnalysis;
-import org.eclipse.tracecompass.tmf.core.analysis.ondemand.OnDemandAnalysisException;
 import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimeRange;
 import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
 import org.json.JSONArray;
@@ -72,40 +77,85 @@ import com.google.common.collect.Multimap;
  * LAMI protocol.
  *
  * @author Alexandre Montplaisir
+ * @author Philippe Proulx
  */
 public class LamiAnalysis implements IOnDemandAnalysis {
 
-    /** Maximum major version of the LAMI protocol we support */
-    private static final int MAX_SUPPORTED_MAJOR_VERSION = 1;
-
+    private static final Logger LOGGER = TraceCompassLog.getLogger(LamiAnalysis.class);
     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 message for commands being run */
-    private static final String RUNNING_MESSAGE = "Running command:"; //$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.
@@ -146,10 +196,110 @@ public class LamiAnalysis implements IOnDemandAnalysis {
         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)));
     }
 
     /**
@@ -159,46 +309,61 @@ public class LamiAnalysis implements IOnDemandAnalysis {
     @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();
-
-        Activator.instance().logInfo(RUNNING_MESSAGE + ' ' + command.toString());
-
+        LOGGER.info(() -> "[LamiAnalysis:RunningMetadataCommand] " + command.toString()); //$NON-NLS-1$
         String output = getOutputFromCommand(command);
         if (output == null || output.isEmpty()) {
             return false;
@@ -255,21 +420,6 @@ public class LamiAnalysis implements IOnDemandAnalysis {
             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));
 
@@ -298,7 +448,7 @@ public class LamiAnalysis implements IOnDemandAnalysis {
 
         } catch (JSONException e) {
             /* Error in the parsing of the JSON, script is broken? */
-            Activator.instance().logError(e.getMessage());
+            LOGGER.severe(() -> "[LamiAnalysis:ErrorParsingMetadata] msg=" + e.getMessage()); //$NON-NLS-1$
             return false;
         }
         return true;
@@ -463,12 +613,16 @@ public class LamiAnalysis implements IOnDemandAnalysis {
         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);
         }
 
@@ -479,39 +633,27 @@ public class LamiAnalysis implements IOnDemandAnalysis {
         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 OnDemandAnalysisException
-     *             If execution did not terminate normally
-     */
     @Override
     public List<LamiResultTable> execute(ITmfTrace trace, @Nullable TmfTimeRange timeRange,
-            String extraParams, IProgressMonitor monitor) throws OnDemandAnalysisException {
+            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();
-
-        Activator.instance().logInfo(RUNNING_MESSAGE + ' ' + command.toString());
+        LOGGER.info(() -> "[LamiAnalysis:RunningExecuteCommand] " + command.toString()); //$NON-NLS-1$
         String output = getResultsFromCommand(command, monitor);
 
         if (output.isEmpty()) {
-            throw new OnDemandAnalysisException(Messages.LamiAnalysis_NoResults);
+            IStatus status = new Status(IStatus.INFO, Activator.instance().getPluginId(), Messages.LamiAnalysis_NoResults);
+            throw new CoreException(status);
         }
 
         /*
@@ -585,17 +727,20 @@ public class LamiAnalysis implements IOnDemandAnalysis {
                  * No results were reported. This may be normal, but warn the
                  * user why a report won't be created.
                  */
-                throw new OnDemandAnalysisException(Messages.LamiAnalysis_NoResults);
+                IStatus status = new Status(IStatus.INFO, Activator.instance().getPluginId(), Messages.LamiAnalysis_NoResults);
+                throw new CoreException(status);
             }
 
             for (int i = 0; i < results.length(); i++) {
                 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;
@@ -652,7 +797,9 @@ public class LamiAnalysis implements IOnDemandAnalysis {
             }
 
         } catch (JSONException e) {
-            throw new OnDemandAnalysisException(e.getMessage());
+            LOGGER.severe(() -> "[LamiAnalysis:ErrorParsingExecutionOutput] msg=" + e.getMessage()); //$NON-NLS-1$
+            IStatus status = new Status(IStatus.ERROR, Activator.instance().getPluginId(), e.getMessage(), e);
+            throw new CoreException(status);
         }
 
         return resultsBuilder.build();
@@ -680,20 +827,12 @@ public class LamiAnalysis implements IOnDemandAnalysis {
      */
     @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$
     }
 
     /**
@@ -707,157 +846,76 @@ public class LamiAnalysis implements IOnDemandAnalysis {
      * @param monitor
      *            The progress monitor
      * @return The analysis results
-     * @throws OnDemandAnalysisException
+     * @throws CoreException
      *             If the command ended abnormally, and normal results were not
      *             returned
      */
     @VisibleForTesting
     protected String getResultsFromCommand(List<String> command, IProgressMonitor monitor)
-            throws OnDemandAnalysisException {
+            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. */
-                throw new OnDemandAnalysisException(null);
-            }
-
-            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()));
-                String stdErrOutput = br.lines().collect(Collectors.joining("\n")); //$NON-NLS-1$
-                throw new OnDemandAnalysisException(stdErrOutput);
-            }
-
-            /* External script ended successfully, all is fine! */
-            String resultsStr = results.stream().collect(Collectors.joining());
-            return checkNotNull(resultsStr);
-
-        } catch (IOException | InterruptedException e) {
-            throw new OnDemandAnalysisException(Messages.LamiAnalysis_ExecutionInterrupted);
-
-        } 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();
-        }
-    }
-
-    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;
+            line = reader.readLine();
         }
 
-        public void setFinished() {
-            fIsFinished = true;
+        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;
+    };
 
-        @Override
-        public void run() {
-            try {
-                while (!fIsFinished) {
-                    Thread.sleep(500);
-                    if (fMonitor.isCanceled()) {
-                        fProcess.destroy();
-                        return;
-                    }
-                }
-            } catch (InterruptedException e) {
-            }
-        }
-
-    }
 
     @Override
     public @NonNull String getName() {
This page took 0.033879 seconds and 5 git commands to generate.