1 /*******************************************************************************
2 * Copyright (c) 2015, 2016 EfficiOS Inc., Alexandre Montplaisir
4 * All rights reserved. This program and the accompanying materials are
5 * made available under the terms of the Eclipse Public License v1.0 which
6 * accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
8 *******************************************************************************/
10 package org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
;
12 import static org
.eclipse
.tracecompass
.common
.core
.NonNullUtils
.checkNotNull
;
13 import static org
.eclipse
.tracecompass
.common
.core
.NonNullUtils
.checkNotNullContents
;
14 import static org
.eclipse
.tracecompass
.common
.core
.NonNullUtils
.nullToEmptyString
;
16 import java
.io
.BufferedReader
;
18 import java
.io
.IOException
;
19 import java
.io
.InputStreamReader
;
20 import java
.nio
.file
.Files
;
21 import java
.nio
.file
.Paths
;
22 import java
.util
.ArrayList
;
23 import java
.util
.Arrays
;
24 import java
.util
.Collection
;
25 import java
.util
.Collections
;
26 import java
.util
.List
;
28 import java
.util
.function
.Predicate
;
29 import java
.util
.logging
.Logger
;
30 import java
.util
.regex
.Pattern
;
31 import java
.util
.stream
.Collectors
;
32 import java
.util
.stream
.Stream
;
34 import org
.eclipse
.core
.runtime
.CoreException
;
35 import org
.eclipse
.core
.runtime
.IProgressMonitor
;
36 import org
.eclipse
.core
.runtime
.IStatus
;
37 import org
.eclipse
.core
.runtime
.MultiStatus
;
38 import org
.eclipse
.core
.runtime
.Status
;
39 import org
.eclipse
.jdt
.annotation
.NonNull
;
40 import org
.eclipse
.jdt
.annotation
.Nullable
;
41 import org
.eclipse
.tracecompass
.common
.core
.log
.TraceCompassLog
;
42 import org
.eclipse
.tracecompass
.internal
.analysis
.lami
.core
.Activator
;
43 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.LamiStrings
;
44 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiDurationAspect
;
45 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiEmptyAspect
;
46 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiGenericAspect
;
47 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiIRQNameAspect
;
48 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiIRQNumberAspect
;
49 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiIRQTypeAspect
;
50 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiMixedAspect
;
51 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiProcessNameAspect
;
52 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiProcessPIDAspect
;
53 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiProcessTIDAspect
;
54 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiTableEntryAspect
;
55 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiTimeRangeBeginAspect
;
56 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiTimeRangeDurationAspect
;
57 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiTimeRangeEndAspect
;
58 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiTimestampAspect
;
59 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.types
.LamiData
;
60 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.types
.LamiData
.DataType
;
61 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.types
.LamiTimeRange
;
62 import org
.eclipse
.tracecompass
.tmf
.core
.analysis
.ondemand
.IOnDemandAnalysis
;
63 import org
.eclipse
.tracecompass
.tmf
.core
.timestamp
.TmfTimeRange
;
64 import org
.eclipse
.tracecompass
.tmf
.core
.trace
.ITmfTrace
;
65 import org
.json
.JSONArray
;
66 import org
.json
.JSONException
;
67 import org
.json
.JSONObject
;
69 import com
.google
.common
.annotations
.VisibleForTesting
;
70 import com
.google
.common
.collect
.ImmutableList
;
71 import com
.google
.common
.collect
.ImmutableMap
;
72 import com
.google
.common
.collect
.ImmutableMultimap
;
73 import com
.google
.common
.collect
.Multimap
;
76 * Base class for analysis modules that call external scripts implementing the
79 * @author Alexandre Montplaisir
81 public class LamiAnalysis
implements IOnDemandAnalysis
{
83 private static final Logger LOGGER
= TraceCompassLog
.getLogger(LamiAnalysis
.class);
86 * Maximum major version of the LAMI protocol we support.
88 * Currently only 0.x/unversioned MI, outputted by lttng-analyses 0.4.x
90 private static final int MAX_SUPPORTED_MAJOR_VERSION
= 0;
92 private static final String DOUBLE_QUOTES
= "\""; //$NON-NLS-1$
94 /* Flags passed to the analysis scripts */
95 private static final String METADATA_FLAG
= "--metadata"; //$NON-NLS-1$
96 private static final String PROGRESS_FLAG
= "--output-progress"; //$NON-NLS-1$
97 private static final String BEGIN_FLAG
= "--begin"; //$NON-NLS-1$
98 private static final String END_FLAG
= "--end"; //$NON-NLS-1$
100 private final List
<String
> fScriptCommand
;
103 * The LAMI analysis is considered initialized after we have read the
104 * script's --metadata once. This will assign the fields below.
106 private boolean fInitialized
= false;
108 private boolean fIsAvailable
;
109 private final String fName
;
110 private final boolean fIsUserDefined
;
111 private final Predicate
<ITmfTrace
> fAppliesTo
;
113 /* Data defined by the analysis's metadata */
114 private @Nullable String fAnalysisTitle
;
115 private @Nullable Map
<String
, LamiTableClass
> fTableClasses
;
116 private boolean fUseProgressOutput
;
119 * Constructor. To be called by implementing classes.
122 * Name of this analysis
123 * @param isUserDefined
124 * {@code true} if this is a user-defined analysis
126 * Predicate to use to check whether or not this analysis applies
129 * Analysis arguments, including the executable name (first
132 public LamiAnalysis(String name
, boolean isUserDefined
, Predicate
<ITmfTrace
> appliesTo
,
134 fScriptCommand
= ImmutableList
.copyOf(args
);
136 fIsUserDefined
= isUserDefined
;
137 fAppliesTo
= appliesTo
;
141 * Map of pre-defined charts, for every table class names.
143 * If a table class is not in this map then it means that table has no
146 * @return The chart models, per table class names
148 protected Multimap
<String
, LamiChartModel
> getPredefinedCharts() {
149 return ImmutableMultimap
.of();
153 public final boolean appliesTo(ITmfTrace trace
) {
154 return fAppliesTo
.test(trace
);
158 public boolean canExecute(ITmfTrace trace
) {
164 * Perform initialization of the LAMI script. This means verifying that it
165 * is actually present on disk, and that it returns correct --metadata.
168 protected synchronized void initialize() {
173 /* Do the analysis's initialization */
175 /* Check if the script's expected executable is on the PATH */
176 String executable
= fScriptCommand
.get(0);
177 boolean exists
= Stream
.of(System
.getenv("PATH").split(checkNotNull(Pattern
.quote(File
.pathSeparator
)))) //$NON-NLS-1$
179 .anyMatch(path
-> Files
.exists(path
.resolve(executable
)));
181 /* Script is not found */
182 fIsAvailable
= false;
187 fIsAvailable
= checkMetadata();
192 * Verify that this script returns valid metadata.
194 * This will populate all remaining non-final fields of this class.
196 * @return If the metadata is valid or not
199 protected boolean checkMetadata() {
201 * The initialize() phase of the analysis will be used to check the
202 * script's metadata. Actual runs of the script will use the execute()
205 List
<String
> command
= ImmutableList
.<@NonNull String
> builder()
206 .addAll(fScriptCommand
).add(METADATA_FLAG
).build();
208 LOGGER
.info(() -> "[LamiAnalysis:RunningMetadataCommand] command=" + command
.toString()); //$NON-NLS-1$
210 String output
= getOutputFromCommand(command
);
211 if (output
== null || output
.isEmpty()) {
217 * Metadata should look this this:
220 * "version": [1, 5, 2, "dev"],
221 * "title": "I/O latency statistics",
223 * "Julien Desfossez",
226 * "description": "Provides statistics about the latency involved in various I/O operations.",
227 * "url": "https://github.com/lttng/lttng-analyses",
235 * "syscall-latency": {
236 * "title": "System calls latency statistics",
237 * "column-descriptions": [
238 * {"title": "System call", "type": "syscall"},
239 * {"title": "Count", "type": "int", "unit": "operations"},
240 * {"title": "Minimum duration", "type": "duration"},
241 * {"title": "Average duration", "type": "duration"},
242 * {"title": "Maximum duration", "type": "duration"},
243 * {"title": "Standard deviation", "type": "duration"}
247 * "title": "Disk latency statistics",
248 * "column-descriptions": [
249 * {"title": "Disk name", "type": "disk"},
250 * {"title": "Count", "type": "int", "unit": "operations"},
251 * {"title": "Minimum duration", "type": "duration"},
252 * {"title": "Average duration", "type": "duration"},
253 * {"title": "Maximum duration", "type": "duration"},
254 * {"title": "Standard deviation", "type": "duration"}
263 JSONObject obj
= new JSONObject(output
);
264 fAnalysisTitle
= obj
.getString(LamiStrings
.TITLE
);
266 /* Very early scripts may not contain the "mi-version" */
267 JSONObject miVersion
= obj
.optJSONObject(LamiStrings
.MI_VERSION
);
268 if (miVersion
== null) {
269 /* Before version 0.1 */
270 fUseProgressOutput
= false;
272 int majorVersion
= miVersion
.getInt(LamiStrings
.MAJOR
);
273 if (majorVersion
<= MAX_SUPPORTED_MAJOR_VERSION
) {
274 fUseProgressOutput
= true;
276 /* Unknown version, we do not support it */
281 JSONObject tableClasses
= obj
.getJSONObject(LamiStrings
.TABLE_CLASSES
);
282 @NonNull String
[] tableClassNames
= checkNotNullContents(JSONObject
.getNames(tableClasses
));
284 ImmutableMap
.Builder
<String
, LamiTableClass
> tablesBuilder
= ImmutableMap
.builder();
285 for (String tableClassName
: tableClassNames
) {
286 JSONObject tableClass
= tableClasses
.getJSONObject(tableClassName
);
288 final String tableTitle
= checkNotNull(tableClass
.getString(LamiStrings
.TITLE
));
289 @NonNull JSONArray columnDescriptions
= checkNotNull(tableClass
.getJSONArray(LamiStrings
.COLUMN_DESCRIPTIONS
));
291 List
<LamiTableEntryAspect
> aspects
= getAspectsFromColumnDescriptions(columnDescriptions
);
292 Collection
<LamiChartModel
> chartModels
= getPredefinedCharts().get(tableClassName
);
294 tablesBuilder
.put(tableClassName
, new LamiTableClass(tableClassName
, tableTitle
, aspects
, chartModels
));
298 fTableClasses
= tablesBuilder
.build();
299 } catch (IllegalArgumentException e
) {
301 * This is thrown if there are duplicate keys in the map
304 throw new JSONException("Duplicate table class entry in " + fAnalysisTitle
); //$NON-NLS-1$
307 } catch (JSONException e
) {
308 /* Error in the parsing of the JSON, script is broken? */
309 LOGGER
.severe(() -> "[LamiAnalysis:ErrorParsingMetadata] msg=" + e
.getMessage()); //$NON-NLS-1$
310 Activator
.instance().logError(e
.getMessage());
316 private static List
<LamiTableEntryAspect
> getAspectsFromColumnDescriptions(JSONArray columnDescriptions
) throws JSONException
{
317 ImmutableList
.Builder
<LamiTableEntryAspect
> aspectsBuilder
= new ImmutableList
.Builder
<>();
318 for (int j
= 0; j
< columnDescriptions
.length(); j
++) {
319 JSONObject column
= columnDescriptions
.getJSONObject(j
);
320 DataType columnDataType
;
321 String columnClass
= column
.optString(LamiStrings
.CLASS
, null);
323 if (columnClass
== null) {
324 columnDataType
= DataType
.MIXED
;
326 columnDataType
= getDataTypeFromString(columnClass
);
329 String columnTitle
= column
.optString(LamiStrings
.TITLE
, null);
331 if (columnTitle
== null) {
332 columnTitle
= String
.format("%s #%d", columnDataType
.getTitle(), j
+ 1); //$NON-NLS-1$
335 final int colIndex
= j
;
336 switch (columnDataType
) {
339 * We will add 3 aspects, to represent the start, end and
340 * duration of this time range.
342 aspectsBuilder
.add(new LamiTimeRangeBeginAspect(columnTitle
, colIndex
));
343 aspectsBuilder
.add(new LamiTimeRangeEndAspect(columnTitle
, colIndex
));
344 aspectsBuilder
.add(new LamiTimeRangeDurationAspect(columnTitle
, colIndex
));
348 aspectsBuilder
.add(new LamiTimestampAspect(columnTitle
, colIndex
));
352 aspectsBuilder
.add(new LamiProcessNameAspect(columnTitle
, colIndex
));
353 aspectsBuilder
.add(new LamiProcessPIDAspect(columnTitle
, colIndex
));
354 aspectsBuilder
.add(new LamiProcessTIDAspect(columnTitle
, colIndex
));
358 aspectsBuilder
.add(new LamiIRQTypeAspect(columnTitle
, colIndex
));
359 aspectsBuilder
.add(new LamiIRQNameAspect(columnTitle
, colIndex
));
360 aspectsBuilder
.add(new LamiIRQNumberAspect(columnTitle
, colIndex
));
364 aspectsBuilder
.add(new LamiDurationAspect(columnTitle
, colIndex
));
368 aspectsBuilder
.add(new LamiMixedAspect(columnTitle
, colIndex
));
373 String units
= column
.optString(LamiStrings
.UNIT
, null);
376 units
= columnDataType
.getUnits();
379 /* We will add only one aspect representing the element */
380 LamiTableEntryAspect aspect
= new LamiGenericAspect(columnTitle
,
381 units
, colIndex
, columnDataType
.isContinuous(), false);
382 aspectsBuilder
.add(aspect
);
387 * SWT quirk : we need an empty column at the end or else the last data
388 * column will clamp to the right edge of the view if it is
391 aspectsBuilder
.add(LamiEmptyAspect
.INSTANCE
);
393 return aspectsBuilder
.build();
396 private static DataType
getDataTypeFromString(String value
) throws JSONException
{
398 return DataType
.fromString(value
);
399 } catch (IllegalArgumentException e
) {
400 throw new JSONException("Unrecognized data type: " + value
); //$NON-NLS-1$
405 * Get the title of this analysis, as read from the script's metadata.
407 * @return The analysis title. Should not be null after the initialization
408 * completed successfully.
410 public @Nullable String
getAnalysisTitle() {
411 return fAnalysisTitle
;
415 * Get the result table classes defined by this analysis, as read from the
418 * @return The analysis' result table classes. Should not be null after the
419 * execution completed successfully.
421 public @Nullable Map
<String
, LamiTableClass
> getTableClasses() {
422 return fTableClasses
;
426 * Print the full command that will be run when calling {@link #execute},
427 * with the exception of the 'extraParams' that will be passed to execute().
429 * This can be used to display the command in the UI before it is actually
433 * The trace on which to run the analysis
435 * The time range to specify. Null will not specify a time range,
436 * which means the whole trace will be taken.
437 * @return The command as a single, space-separated string
439 public String
getFullCommandAsString(ITmfTrace trace
, @Nullable TmfTimeRange range
) {
440 String tracePath
= checkNotNull(trace
.getPath());
442 ImmutableList
.Builder
<String
> builder
= getBaseCommand(range
);
444 * We can add double-quotes around the trace path, which could contain
445 * spaces, so that the resulting command can be easily copy-pasted into
448 builder
.add(DOUBLE_QUOTES
+ tracePath
+ DOUBLE_QUOTES
);
449 List
<String
> list
= builder
.build();
450 String ret
= list
.stream().collect(Collectors
.joining(" ")); //$NON-NLS-1$
451 return checkNotNull(ret
);
455 * Get the base part of the command that will be executed to run this
456 * analysis, supplying the given time range. Base part meaning:
459 * [script executable] [statically-defined parameters] [--begin/--end (if applicable)]
462 * Note that it does not include the path to the trace, that is to be added
466 * The time range that will be passed
467 * @return The elements of the command
469 private ImmutableList
.Builder
<String
> getBaseCommand(@Nullable TmfTimeRange range
) {
473 begin
= range
.getStartTime().getValue();
474 end
= range
.getEndTime().getValue();
477 ImmutableList
.Builder
<String
> builder
= ImmutableList
.builder();
478 builder
.addAll(fScriptCommand
);
480 if (fUseProgressOutput
) {
481 builder
.add(PROGRESS_FLAG
);
485 builder
.add(BEGIN_FLAG
).add(String
.valueOf(begin
));
486 builder
.add(END_FLAG
).add(String
.valueOf(end
));
492 * Call the currently defined LAMI script with the given arguments.
495 * The time range. Null for the whole trace.
497 * The progress monitor used to report progress
498 * @return The script's output, formatted into {@link LamiTableEntry}'s.
499 * @throws CoreException
500 * If execution did not terminate normally
503 public List
<LamiResultTable
> execute(ITmfTrace trace
, @Nullable TmfTimeRange timeRange
,
504 String extraParams
, IProgressMonitor monitor
) throws CoreException
{
505 /* Should have been called already, but in case it was not */
508 final @NonNull String tracePath
= checkNotNull(trace
.getPath());
509 final @NonNull String
[] splitParams
= extraParams
.trim().split(" "); //$NON-NLS-1$
511 ImmutableList
.Builder
<String
> builder
= getBaseCommand(timeRange
);
513 if (!extraParams
.trim().equals("")) { //$NON-NLS-1$
514 builder
.addAll(Arrays
.asList(splitParams
));
516 builder
.add(tracePath
);
517 List
<String
> command
= builder
.build();
519 LOGGER
.info(() -> "[LamiAnalysis:RunningExecuteCommand] command=" + command
.toString()); //$NON-NLS-1$
520 String output
= getResultsFromCommand(command
, monitor
);
522 if (output
.isEmpty()) {
523 IStatus status
= new Status(IStatus
.INFO
, Activator
.instance().getPluginId(), Messages
.LamiAnalysis_NoResults
);
524 throw new CoreException(status
);
532 * "type": "time-range",
533 * "begin": 1444334398154194201,
534 * "end": 1444334425194487548
536 * "class": "syscall-latency",
539 * {"type": "syscall", "name": "open"},
541 * {"type": "duration", "value": 5562},
542 * {"type": "duration", "value": 13835},
543 * {"type": "duration", "value": 77683},
544 * {"type": "duration", "value": 15263}
547 * {"type": "syscall", "name": "read"},
549 * {"type": "duration", "value": 316},
550 * {"type": "duration", "value": 5774},
551 * {"type": "duration", "value": 62569},
552 * {"type": "duration", "value": 9277}
558 * "type": "time-range",
559 * "begin": 1444334425194487549,
560 * "end": 1444334425254887190
562 * "class": "syscall-latency",
565 * {"type": "syscall", "name": "open"},
567 * {"type": "duration", "value": 1578},
568 * {"type": "duration", "value": 16648},
569 * {"type": "duration", "value": 15444},
570 * {"type": "duration", "value": 68540}
573 * {"type": "syscall", "name": "read"},
575 * {"type": "duration", "value": 78},
576 * {"type": "duration", "value": 1948},
577 * {"type": "duration", "value": 11184},
578 * {"type": "duration", "value": 94670}
587 ImmutableList
.Builder
<LamiResultTable
> resultsBuilder
= new ImmutableList
.Builder
<>();
590 JSONObject obj
= new JSONObject(output
);
591 JSONArray results
= obj
.getJSONArray(LamiStrings
.RESULTS
);
593 if (results
.length() == 0) {
595 * No results were reported. This may be normal, but warn the
596 * user why a report won't be created.
598 IStatus status
= new Status(IStatus
.INFO
, Activator
.instance().getPluginId(), Messages
.LamiAnalysis_NoResults
);
599 throw new CoreException(status
);
602 for (int i
= 0; i
< results
.length(); i
++) {
603 JSONObject result
= results
.getJSONObject(i
);
605 /* Parse the time-range */
606 JSONObject trObject
= result
.getJSONObject(LamiStrings
.TIME_RANGE
);
607 long start
= trObject
.getLong(LamiStrings
.BEGIN
);
608 long end
= trObject
.getLong(LamiStrings
.END
);
609 LamiTimeRange tr
= new LamiTimeRange(start
, end
);
611 /* Parse the table's class */
612 LamiTableClass tableClass
;
613 JSONObject tableClassObject
= result
.optJSONObject(LamiStrings
.CLASS
);
614 if (tableClassObject
== null) {
616 * "class" is just a standard string, indicating we use a
617 * metadata-defined table class as-is
619 @NonNull String tableClassName
= checkNotNull(result
.getString(LamiStrings
.CLASS
));
620 tableClass
= getTableClassFromName(tableClassName
);
622 // FIXME Rest will become more generic eventually in the LAMI format.
623 } else if (tableClassObject
.has(LamiStrings
.INHERIT
)) {
625 * Dynamic title: We reuse an existing table class but
626 * override the title.
628 String baseTableName
= checkNotNull(tableClassObject
.getString(LamiStrings
.INHERIT
));
629 LamiTableClass baseTableClass
= getTableClassFromName(baseTableName
);
630 String newTitle
= checkNotNull(tableClassObject
.getString(LamiStrings
.TITLE
));
632 tableClass
= new LamiTableClass(baseTableClass
, newTitle
);
635 * Dynamic column descriptions: we implement a new table
638 String title
= checkNotNull(tableClassObject
.getString(LamiStrings
.TITLE
));
639 JSONArray columnDescriptions
= checkNotNull(tableClassObject
.getJSONArray(LamiStrings
.COLUMN_DESCRIPTIONS
));
640 List
<LamiTableEntryAspect
> aspects
= getAspectsFromColumnDescriptions(columnDescriptions
);
642 tableClass
= new LamiTableClass(nullToEmptyString(Messages
.LamiAnalysis_DefaultDynamicTableName
), title
, aspects
, Collections
.EMPTY_SET
);
645 /* Parse the "data", which is the array of rows */
646 JSONArray data
= result
.getJSONArray(LamiStrings
.DATA
);
647 ImmutableList
.Builder
<LamiTableEntry
> dataBuilder
= new ImmutableList
.Builder
<>();
649 for (int j
= 0; j
< data
.length(); j
++) {
650 /* A row is an array of cells */
651 JSONArray row
= data
.getJSONArray(j
);
652 ImmutableList
.Builder
<LamiData
> rowBuilder
= ImmutableList
.builder();
654 for (int k
= 0; k
< row
.length(); k
++) {
655 Object cellObject
= checkNotNull(row
.get(k
));
656 LamiData cellValue
= LamiData
.createFromObject(cellObject
);
657 rowBuilder
.add(cellValue
);
660 dataBuilder
.add(new LamiTableEntry(rowBuilder
.build()));
662 resultsBuilder
.add(new LamiResultTable(tr
, tableClass
, dataBuilder
.build()));
665 } catch (JSONException e
) {
666 LOGGER
.severe(() -> "[LamiAnalysis:ErrorParsingExecutionOutput] msg=" + e
.getMessage()); //$NON-NLS-1$
667 IStatus status
= new Status(IStatus
.ERROR
, Activator
.instance().getPluginId(), e
.getMessage(), e
);
668 throw new CoreException(status
);
671 return resultsBuilder
.build();
674 private LamiTableClass
getTableClassFromName(String tableClassName
) throws JSONException
{
675 Map
<String
, LamiTableClass
> map
= checkNotNull(fTableClasses
);
676 LamiTableClass tableClass
= map
.get(tableClassName
);
677 if (tableClass
== null) {
678 throw new JSONException("Table class " + tableClassName
+ //$NON-NLS-1$
679 " was not declared in the metadata"); //$NON-NLS-1$
685 * Get the output of an external command, used for getting the metadata.
686 * Cannot be cancelled, and will not report errors, simply returns null if
687 * the process ended abnormally.
690 * The parameters of the command, passed to
691 * {@link ProcessBuilder}
692 * @return The command output as a string
695 protected @Nullable String
getOutputFromCommand(List
<String
> command
) {
697 ProcessBuilder builder
= new ProcessBuilder(command
);
698 builder
.redirectErrorStream(true);
700 Process p
= builder
.start();
701 try (BufferedReader br
= new BufferedReader(new InputStreamReader(p
.getInputStream()));) {
702 int ret
= p
.waitFor();
703 String output
= br
.lines().collect(Collectors
.joining());
705 return (ret
== 0 ? output
: null);
707 } catch (IOException
| InterruptedException e
) {
713 * Get the results of invoking the specified command.
715 * The result should start with '{"results":...', as specified by the LAMI
716 * JSON protocol. The JSON itself may be split over multiple lines.
719 * The command to run (program and its arguments)
721 * The progress monitor
722 * @return The analysis results
723 * @throws CoreException
724 * If the command ended abnormally, and normal results were not
728 protected String
getResultsFromCommand(List
<String
> command
, IProgressMonitor monitor
)
729 throws CoreException
{
731 final int scale
= 1000;
732 double workedSoFar
= 0.0;
734 ProcessCanceller cancellerRunnable
= null;
735 Thread cancellerThread
= null;
738 monitor
.beginTask(Messages
.LamiAnalysis_MainTaskName
, scale
);
740 ProcessBuilder builder
= new ProcessBuilder(command
);
741 builder
.redirectErrorStream(false);
743 Process p
= checkNotNull(builder
.start());
745 cancellerRunnable
= new ProcessCanceller(p
, monitor
);
746 cancellerThread
= new Thread(cancellerRunnable
);
747 cancellerThread
.start();
749 List
<String
> results
= new ArrayList
<>();
751 try (BufferedReader in
= new BufferedReader(new InputStreamReader(p
.getInputStream()));) {
752 String line
= in
.readLine();
753 while (line
!= null && !line
.matches("\\s*\\{.*")) { //$NON-NLS-1$
755 * This is a line indicating progress, it has the form:
757 * 0.123 3000 of 5000 events processed
759 * The first part indicates the estimated fraction (out of
760 * 1.0) of work done. The second part is status text.
763 // Trim the line first to make sure the first character is
767 // Split at the first space
768 String
[] elems
= line
.split(" ", 2); //$NON-NLS-1$
770 if (elems
[0].matches("\\d.*")) { //$NON-NLS-1$
771 // It looks like we have a progress indication
773 // Try parsing the number
774 double cumulativeWork
= Double
.parseDouble(elems
[0]) * scale
;
775 double workedThisLoop
= cumulativeWork
- workedSoFar
;
777 // We're going backwards? Do not update the
779 if (workedThisLoop
> 0) {
780 monitor
.internalWorked(workedThisLoop
);
781 workedSoFar
= cumulativeWork
;
784 // There is a message: update the monitor's task name
785 if (elems
.length
>= 2) {
786 monitor
.setTaskName(elems
[1].trim());
788 } catch (NumberFormatException e
) {
789 // Continue reading progress lines anyway
793 line
= in
.readLine();
795 while (line
!= null) {
797 * We have seen the first line containing a '{', this is our
801 line
= in
.readLine();
804 int ret
= p
.waitFor();
806 if (monitor
.isCanceled()) {
807 /* We were interrupted by the canceller thread. */
808 IStatus status
= new Status(IStatus
.CANCEL
, Activator
.instance().getPluginId(), null);
809 throw new CoreException(status
);
814 * Something went wrong running the external script. We will
815 * gather the stderr and report it to the user.
817 BufferedReader br
= new BufferedReader(new InputStreamReader(p
.getErrorStream()));
818 List
<String
> stdErrOutput
= br
.lines().collect(Collectors
.toList());
820 MultiStatus status
= new MultiStatus(Activator
.instance().getPluginId(),
821 IStatus
.ERROR
, Messages
.LamiAnalysis_ErrorDuringExecution
, null);
822 for (String str
: stdErrOutput
) {
823 status
.add(new Status(IStatus
.ERROR
, Activator
.instance().getPluginId(), str
));
825 if (stdErrOutput
.isEmpty()) {
827 * At least say "no output", so an error message actually
830 status
.add(new Status(IStatus
.ERROR
, Activator
.instance().getPluginId(), Messages
.LamiAnalysis_ErrorNoOutput
));
832 throw new CoreException(status
);
835 /* External script ended successfully, all is fine! */
836 String resultsStr
= results
.stream().collect(Collectors
.joining());
837 return checkNotNull(resultsStr
);
839 } catch (IOException
| InterruptedException e
) {
840 IStatus status
= new Status(IStatus
.ERROR
, Activator
.instance().getPluginId(), Messages
.LamiAnalysis_ExecutionInterrupted
, e
);
841 throw new CoreException(status
);
844 if (cancellerRunnable
!= null) {
845 cancellerRunnable
.setFinished();
847 if (cancellerThread
!= null) {
849 cancellerThread
.join();
850 } catch (InterruptedException e
) {
858 private static class ProcessCanceller
implements Runnable
{
860 private final Process fProcess
;
861 private final IProgressMonitor fMonitor
;
863 private boolean fIsFinished
= false;
865 public ProcessCanceller(Process process
, IProgressMonitor monitor
) {
870 public void setFinished() {
877 while (!fIsFinished
) {
879 if (fMonitor
.isCanceled()) {
884 } catch (InterruptedException e
) {
891 public @NonNull String
getName() {
896 public boolean isUserDefined() {
897 return fIsUserDefined
;