analysis.lami: check if command is executable, and support relative paths
[deliverable/tracecompass.git] / analysis / org.eclipse.tracecompass.analysis.lami.core / src / org / eclipse / tracecompass / internal / provisional / analysis / lami / core / module / LamiAnalysis.java
CommitLineData
4208b510
AM
1/*******************************************************************************
2 * Copyright (c) 2015, 2016 EfficiOS Inc., Alexandre Montplaisir
3 *
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 *******************************************************************************/
9
10package org.eclipse.tracecompass.internal.provisional.analysis.lami.core.module;
11
12import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;
13import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNullContents;
14import static org.eclipse.tracecompass.common.core.NonNullUtils.nullToEmptyString;
15
16import java.io.BufferedReader;
17import java.io.File;
18import java.io.IOException;
19import java.io.InputStreamReader;
20import java.nio.file.Files;
21import java.nio.file.Paths;
22import java.util.ArrayList;
23import java.util.Arrays;
24import java.util.Collection;
25import java.util.Collections;
4208b510
AM
26import java.util.List;
27import java.util.Map;
ae5bf609
AM
28import java.util.function.Predicate;
29import java.util.logging.Logger;
4208b510
AM
30import java.util.regex.Pattern;
31import java.util.stream.Collectors;
32import java.util.stream.Stream;
33
46f0c09c 34import org.eclipse.core.runtime.CoreException;
4208b510 35import org.eclipse.core.runtime.IProgressMonitor;
46f0c09c
AM
36import org.eclipse.core.runtime.IStatus;
37import org.eclipse.core.runtime.MultiStatus;
38import org.eclipse.core.runtime.Status;
4208b510
AM
39import org.eclipse.jdt.annotation.NonNull;
40import org.eclipse.jdt.annotation.Nullable;
ae5bf609 41import org.eclipse.tracecompass.common.core.log.TraceCompassLog;
4208b510
AM
42import org.eclipse.tracecompass.internal.analysis.lami.core.Activator;
43import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.LamiStrings;
ae5bf609
AM
44import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiDurationAspect;
45import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiEmptyAspect;
4208b510
AM
46import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiGenericAspect;
47import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiIRQNameAspect;
48import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiIRQNumberAspect;
49import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiIRQTypeAspect;
50import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiMixedAspect;
51import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiProcessNameAspect;
52import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiProcessPIDAspect;
53import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiProcessTIDAspect;
4208b510
AM
54import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiTableEntryAspect;
55import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiTimeRangeBeginAspect;
56import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiTimeRangeDurationAspect;
57import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiTimeRangeEndAspect;
58import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiTimestampAspect;
59import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.types.LamiData;
60import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.types.LamiData.DataType;
61import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.types.LamiTimeRange;
62import org.eclipse.tracecompass.tmf.core.analysis.ondemand.IOnDemandAnalysis;
4208b510
AM
63import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimeRange;
64import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
65import org.json.JSONArray;
66import org.json.JSONException;
67import org.json.JSONObject;
68
ace6413c 69import com.google.common.annotations.VisibleForTesting;
4208b510
AM
70import com.google.common.collect.ImmutableList;
71import com.google.common.collect.ImmutableMap;
72import com.google.common.collect.ImmutableMultimap;
73import com.google.common.collect.Multimap;
74
75/**
76 * Base class for analysis modules that call external scripts implementing the
77 * LAMI protocol.
78 *
79 * @author Alexandre Montplaisir
80 */
81public class LamiAnalysis implements IOnDemandAnalysis {
82
ae5bf609
AM
83 private static final Logger LOGGER = TraceCompassLog.getLogger(LamiAnalysis.class);
84
1b3ebb09
AM
85 /**
86 * Maximum major version of the LAMI protocol we support.
87 *
88 * Currently only 0.x/unversioned MI, outputted by lttng-analyses 0.4.x
89 */
90 private static final int MAX_SUPPORTED_MAJOR_VERSION = 0;
4208b510
AM
91
92 private static final String DOUBLE_QUOTES = "\""; //$NON-NLS-1$
93
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$
99
4208b510
AM
100 private final List<String> fScriptCommand;
101
102 /**
103 * The LAMI analysis is considered initialized after we have read the
104 * script's --metadata once. This will assign the fields below.
105 */
106 private boolean fInitialized = false;
107
108 private boolean fIsAvailable;
109 private final String fName;
110 private final boolean fIsUserDefined;
111 private final Predicate<ITmfTrace> fAppliesTo;
112
113 /* Data defined by the analysis's metadata */
114 private @Nullable String fAnalysisTitle;
115 private @Nullable Map<String, LamiTableClass> fTableClasses;
116 private boolean fUseProgressOutput;
117
118 /**
119 * Constructor. To be called by implementing classes.
120 *
121 * @param name
122 * Name of this analysis
123 * @param isUserDefined
124 * {@code true} if this is a user-defined analysis
125 * @param appliesTo
126 * Predicate to use to check whether or not this analysis applies
127 * to a given trace
128 * @param args
129 * Analysis arguments, including the executable name (first
130 * argument)
131 */
132 public LamiAnalysis(String name, boolean isUserDefined, Predicate<ITmfTrace> appliesTo,
133 List<String> args) {
134 fScriptCommand = ImmutableList.copyOf(args);
135 fName = name;
136 fIsUserDefined = isUserDefined;
137 fAppliesTo = appliesTo;
138 }
139
140 /**
141 * Map of pre-defined charts, for every table class names.
142 *
143 * If a table class is not in this map then it means that table has no
144 * predefined charts.
145 *
146 * @return The chart models, per table class names
147 */
148 protected Multimap<String, LamiChartModel> getPredefinedCharts() {
149 return ImmutableMultimap.of();
150 }
151
152 @Override
ace6413c 153 public final boolean appliesTo(ITmfTrace trace) {
4208b510
AM
154 return fAppliesTo.test(trace);
155 }
156
157 @Override
158 public boolean canExecute(ITmfTrace trace) {
159 initialize();
160 return fIsAvailable;
161 }
162
5f7c6b84
PP
163 private static boolean executableExists(String name) {
164 if (name.contains(File.separator)) {
165 // This seems like a path, not just an executable name
166 return Files.isExecutable(Paths.get(name));
167 }
168
169 // Check if this name is found in the PATH environment variable
170 final String pathEnv = System.getenv("PATH"); //$NON-NLS-1$
171 final String[] exeDirs = pathEnv.split(checkNotNull(Pattern.quote(File.pathSeparator)));
172
173 return Stream.of(exeDirs)
174 .map(Paths::get)
175 .anyMatch(path -> Files.isExecutable(path.resolve(name)));
176 }
177
ace6413c
MJ
178 /**
179 * Perform initialization of the LAMI script. This means verifying that it
180 * is actually present on disk, and that it returns correct --metadata.
181 */
182 @VisibleForTesting
183 protected synchronized void initialize() {
4208b510
AM
184 if (fInitialized) {
185 return;
186 }
187
188 /* Do the analysis's initialization */
189
190 /* Check if the script's expected executable is on the PATH */
5f7c6b84
PP
191 final String executable = fScriptCommand.get(0);
192 final boolean executableExists = executableExists(executable);
193
194 if (!executableExists) {
4208b510
AM
195 /* Script is not found */
196 fIsAvailable = false;
197 fInitialized = true;
198 return;
199 }
200
201 fIsAvailable = checkMetadata();
202 fInitialized = true;
203 }
204
ace6413c
MJ
205 /**
206 * Verify that this script returns valid metadata.
207 *
208 * This will populate all remaining non-final fields of this class.
209 *
210 * @return If the metadata is valid or not
211 */
212 @VisibleForTesting
213 protected boolean checkMetadata() {
4208b510
AM
214 /*
215 * The initialize() phase of the analysis will be used to check the
216 * script's metadata. Actual runs of the script will use the execute()
217 * method below.
218 */
219 List<String> command = ImmutableList.<@NonNull String> builder()
220 .addAll(fScriptCommand).add(METADATA_FLAG).build();
221
ae5bf609 222 LOGGER.info(() -> "[LamiAnalysis:RunningMetadataCommand] command=" + command.toString()); //$NON-NLS-1$
4208b510
AM
223
224 String output = getOutputFromCommand(command);
225 if (output == null || output.isEmpty()) {
226 return false;
227 }
228
229 /*
230 *
231 * Metadata should look this this:
232 *
233 * {
234 * "version": [1, 5, 2, "dev"],
235 * "title": "I/O latency statistics",
236 * "authors": [
237 * "Julien Desfossez",
238 * "Antoine Busque"
239 * ],
240 * "description": "Provides statistics about the latency involved in various I/O operations.",
241 * "url": "https://github.com/lttng/lttng-analyses",
242 * "tags": [
243 * "io",
244 * "stats",
245 * "linux-kernel",
246 * "lttng-analyses"
247 * ],
248 * "table-classes": {
249 * "syscall-latency": {
250 * "title": "System calls latency statistics",
251 * "column-descriptions": [
252 * {"title": "System call", "type": "syscall"},
253 * {"title": "Count", "type": "int", "unit": "operations"},
254 * {"title": "Minimum duration", "type": "duration"},
255 * {"title": "Average duration", "type": "duration"},
256 * {"title": "Maximum duration", "type": "duration"},
257 * {"title": "Standard deviation", "type": "duration"}
258 * ]
259 * },
260 * "disk-latency": {
261 * "title": "Disk latency statistics",
262 * "column-descriptions": [
263 * {"title": "Disk name", "type": "disk"},
264 * {"title": "Count", "type": "int", "unit": "operations"},
265 * {"title": "Minimum duration", "type": "duration"},
266 * {"title": "Average duration", "type": "duration"},
267 * {"title": "Maximum duration", "type": "duration"},
268 * {"title": "Standard deviation", "type": "duration"}
269 * ]
270 * }
271 * }
272 * }
273 *
274 */
275
276 try {
277 JSONObject obj = new JSONObject(output);
278 fAnalysisTitle = obj.getString(LamiStrings.TITLE);
279
280 /* Very early scripts may not contain the "mi-version" */
281 JSONObject miVersion = obj.optJSONObject(LamiStrings.MI_VERSION);
282 if (miVersion == null) {
283 /* Before version 0.1 */
284 fUseProgressOutput = false;
285 } else {
286 int majorVersion = miVersion.getInt(LamiStrings.MAJOR);
287 if (majorVersion <= MAX_SUPPORTED_MAJOR_VERSION) {
288 fUseProgressOutput = true;
289 } else {
290 /* Unknown version, we do not support it */
291 return false;
292 }
293 }
294
295 JSONObject tableClasses = obj.getJSONObject(LamiStrings.TABLE_CLASSES);
296 @NonNull String[] tableClassNames = checkNotNullContents(JSONObject.getNames(tableClasses));
297
298 ImmutableMap.Builder<String, LamiTableClass> tablesBuilder = ImmutableMap.builder();
299 for (String tableClassName : tableClassNames) {
300 JSONObject tableClass = tableClasses.getJSONObject(tableClassName);
301
302 final String tableTitle = checkNotNull(tableClass.getString(LamiStrings.TITLE));
303 @NonNull JSONArray columnDescriptions = checkNotNull(tableClass.getJSONArray(LamiStrings.COLUMN_DESCRIPTIONS));
304
305 List<LamiTableEntryAspect> aspects = getAspectsFromColumnDescriptions(columnDescriptions);
306 Collection<LamiChartModel> chartModels = getPredefinedCharts().get(tableClassName);
307
308 tablesBuilder.put(tableClassName, new LamiTableClass(tableClassName, tableTitle, aspects, chartModels));
309 }
310
311 try {
312 fTableClasses = tablesBuilder.build();
313 } catch (IllegalArgumentException e) {
314 /*
315 * This is thrown if there are duplicate keys in the map
316 * builder.
317 */
318 throw new JSONException("Duplicate table class entry in " + fAnalysisTitle); //$NON-NLS-1$
319 }
320
321 } catch (JSONException e) {
322 /* Error in the parsing of the JSON, script is broken? */
ae5bf609 323 LOGGER.severe(() -> "[LamiAnalysis:ErrorParsingMetadata] msg=" + e.getMessage()); //$NON-NLS-1$
4208b510
AM
324 Activator.instance().logError(e.getMessage());
325 return false;
326 }
327 return true;
328 }
329
330 private static List<LamiTableEntryAspect> getAspectsFromColumnDescriptions(JSONArray columnDescriptions) throws JSONException {
331 ImmutableList.Builder<LamiTableEntryAspect> aspectsBuilder = new ImmutableList.Builder<>();
332 for (int j = 0; j < columnDescriptions.length(); j++) {
333 JSONObject column = columnDescriptions.getJSONObject(j);
334 DataType columnDataType;
335 String columnClass = column.optString(LamiStrings.CLASS, null);
336
337 if (columnClass == null) {
338 columnDataType = DataType.MIXED;
339 } else {
340 columnDataType = getDataTypeFromString(columnClass);
341 }
342
343 String columnTitle = column.optString(LamiStrings.TITLE, null);
344
345 if (columnTitle == null) {
346 columnTitle = String.format("%s #%d", columnDataType.getTitle(), j + 1); //$NON-NLS-1$
347 }
348
349 final int colIndex = j;
350 switch (columnDataType) {
351 case TIME_RANGE:
352 /*
353 * We will add 3 aspects, to represent the start, end and
354 * duration of this time range.
355 */
356 aspectsBuilder.add(new LamiTimeRangeBeginAspect(columnTitle, colIndex));
357 aspectsBuilder.add(new LamiTimeRangeEndAspect(columnTitle, colIndex));
358 aspectsBuilder.add(new LamiTimeRangeDurationAspect(columnTitle, colIndex));
359 break;
360
361 case TIMESTAMP:
362 aspectsBuilder.add(new LamiTimestampAspect(columnTitle, colIndex));
363 break;
364
365 case PROCESS:
366 aspectsBuilder.add(new LamiProcessNameAspect(columnTitle, colIndex));
367 aspectsBuilder.add(new LamiProcessPIDAspect(columnTitle, colIndex));
368 aspectsBuilder.add(new LamiProcessTIDAspect(columnTitle, colIndex));
369 break;
370
371 case IRQ:
372 aspectsBuilder.add(new LamiIRQTypeAspect(columnTitle, colIndex));
373 aspectsBuilder.add(new LamiIRQNameAspect(columnTitle, colIndex));
374 aspectsBuilder.add(new LamiIRQNumberAspect(columnTitle, colIndex));
375 break;
376
377 case DURATION:
378 aspectsBuilder.add(new LamiDurationAspect(columnTitle, colIndex));
379 break;
380
381 case MIXED:
382 aspectsBuilder.add(new LamiMixedAspect(columnTitle, colIndex));
383 break;
384
385 // $CASES-OMITTED$
386 default:
387 String units = column.optString(LamiStrings.UNIT, null);
388
389 if (units == null) {
390 units = columnDataType.getUnits();
391 }
392
393 /* We will add only one aspect representing the element */
394 LamiTableEntryAspect aspect = new LamiGenericAspect(columnTitle,
395 units, colIndex, columnDataType.isContinuous(), false);
396 aspectsBuilder.add(aspect);
397 break;
398 }
399 }
400 /*
401 * SWT quirk : we need an empty column at the end or else the last data
402 * column will clamp to the right edge of the view if it is
403 * right-aligned.
404 */
405 aspectsBuilder.add(LamiEmptyAspect.INSTANCE);
406
407 return aspectsBuilder.build();
408 }
409
410 private static DataType getDataTypeFromString(String value) throws JSONException {
411 try {
412 return DataType.fromString(value);
413 } catch (IllegalArgumentException e) {
414 throw new JSONException("Unrecognized data type: " + value); //$NON-NLS-1$
415 }
416 }
417
418 /**
419 * Get the title of this analysis, as read from the script's metadata.
420 *
421 * @return The analysis title. Should not be null after the initialization
422 * completed successfully.
423 */
424 public @Nullable String getAnalysisTitle() {
425 return fAnalysisTitle;
426 }
427
428 /**
429 * Get the result table classes defined by this analysis, as read from the
430 * script's metadata.
431 *
432 * @return The analysis' result table classes. Should not be null after the
433 * execution completed successfully.
434 */
435 public @Nullable Map<String, LamiTableClass> getTableClasses() {
436 return fTableClasses;
437 }
438
439 /**
440 * Print the full command that will be run when calling {@link #execute},
441 * with the exception of the 'extraParams' that will be passed to execute().
442 *
443 * This can be used to display the command in the UI before it is actually
444 * run.
445 *
446 * @param trace
447 * The trace on which to run the analysis
448 * @param range
449 * The time range to specify. Null will not specify a time range,
450 * which means the whole trace will be taken.
451 * @return The command as a single, space-separated string
452 */
453 public String getFullCommandAsString(ITmfTrace trace, @Nullable TmfTimeRange range) {
454 String tracePath = checkNotNull(trace.getPath());
455
456 ImmutableList.Builder<String> builder = getBaseCommand(range);
457 /*
458 * We can add double-quotes around the trace path, which could contain
459 * spaces, so that the resulting command can be easily copy-pasted into
460 * a shell.
461 */
462 builder.add(DOUBLE_QUOTES + tracePath + DOUBLE_QUOTES);
463 List<String> list = builder.build();
464 String ret = list.stream().collect(Collectors.joining(" ")); //$NON-NLS-1$
465 return checkNotNull(ret);
466 }
467
468 /**
469 * Get the base part of the command that will be executed to run this
470 * analysis, supplying the given time range. Base part meaning:
471 *
472 * <pre>
473 * [script executable] [statically-defined parameters] [--begin/--end (if applicable)]
474 * </pre>
475 *
476 * Note that it does not include the path to the trace, that is to be added
477 * separately.
478 *
479 * @param range
480 * The time range that will be passed
481 * @return The elements of the command
482 */
483 private ImmutableList.Builder<String> getBaseCommand(@Nullable TmfTimeRange range) {
484 long begin = 0;
485 long end = 0;
486 if (range != null) {
487 begin = range.getStartTime().getValue();
488 end = range.getEndTime().getValue();
489 }
490
491 ImmutableList.Builder<String> builder = ImmutableList.builder();
492 builder.addAll(fScriptCommand);
493
494 if (fUseProgressOutput) {
495 builder.add(PROGRESS_FLAG);
496 }
497
498 if (range != null) {
499 builder.add(BEGIN_FLAG).add(String.valueOf(begin));
500 builder.add(END_FLAG).add(String.valueOf(end));
501 }
502 return builder;
503 }
504
505 /**
506 * Call the currently defined LAMI script with the given arguments.
507 *
508 * @param timeRange
509 * The time range. Null for the whole trace.
510 * @param monitor
511 * The progress monitor used to report progress
512 * @return The script's output, formatted into {@link LamiTableEntry}'s.
46f0c09c 513 * @throws CoreException
4208b510
AM
514 * If execution did not terminate normally
515 */
516 @Override
517 public List<LamiResultTable> execute(ITmfTrace trace, @Nullable TmfTimeRange timeRange,
46f0c09c 518 String extraParams, IProgressMonitor monitor) throws CoreException {
4208b510
AM
519 /* Should have been called already, but in case it was not */
520 initialize();
521
522 final @NonNull String tracePath = checkNotNull(trace.getPath());
523 final @NonNull String[] splitParams = extraParams.trim().split(" "); //$NON-NLS-1$
524
525 ImmutableList.Builder<String> builder = getBaseCommand(timeRange);
526
527 if (!extraParams.trim().equals("")) { //$NON-NLS-1$
528 builder.addAll(Arrays.asList(splitParams));
529 }
530 builder.add(tracePath);
531 List<String> command = builder.build();
532
ae5bf609 533 LOGGER.info(() -> "[LamiAnalysis:RunningExecuteCommand] command=" + command.toString()); //$NON-NLS-1$
4208b510
AM
534 String output = getResultsFromCommand(command, monitor);
535
536 if (output.isEmpty()) {
46f0c09c
AM
537 IStatus status = new Status(IStatus.INFO, Activator.instance().getPluginId(), Messages.LamiAnalysis_NoResults);
538 throw new CoreException(status);
4208b510
AM
539 }
540
541 /*
542 * {
543 * "results": [
544 * {
545 * "time-range": {
546 * "type": "time-range",
547 * "begin": 1444334398154194201,
548 * "end": 1444334425194487548
549 * },
550 * "class": "syscall-latency",
551 * "data": [
552 * [
553 * {"type": "syscall", "name": "open"},
554 * 45,
555 * {"type": "duration", "value": 5562},
556 * {"type": "duration", "value": 13835},
557 * {"type": "duration", "value": 77683},
558 * {"type": "duration", "value": 15263}
559 * ],
560 * [
561 * {"type": "syscall", "name": "read"},
562 * 109,
563 * {"type": "duration", "value": 316},
564 * {"type": "duration", "value": 5774},
565 * {"type": "duration", "value": 62569},
566 * {"type": "duration", "value": 9277}
567 * ]
568 * ]
569 * },
570 * {
571 * "time-range": {
572 * "type": "time-range",
573 * "begin": 1444334425194487549,
574 * "end": 1444334425254887190
575 * },
576 * "class": "syscall-latency",
577 * "data": [
578 * [
579 * {"type": "syscall", "name": "open"},
580 * 45,
581 * {"type": "duration", "value": 1578},
582 * {"type": "duration", "value": 16648},
583 * {"type": "duration", "value": 15444},
584 * {"type": "duration", "value": 68540}
585 * ],
586 * [
587 * {"type": "syscall", "name": "read"},
588 * 109,
589 * {"type": "duration", "value": 78},
590 * {"type": "duration", "value": 1948},
591 * {"type": "duration", "value": 11184},
592 * {"type": "duration", "value": 94670}
593 * ]
594 * ]
595 * }
596 * ]
597 * }
598 *
599 */
600
601 ImmutableList.Builder<LamiResultTable> resultsBuilder = new ImmutableList.Builder<>();
602
603 try {
604 JSONObject obj = new JSONObject(output);
605 JSONArray results = obj.getJSONArray(LamiStrings.RESULTS);
606
607 if (results.length() == 0) {
608 /*
609 * No results were reported. This may be normal, but warn the
610 * user why a report won't be created.
611 */
46f0c09c
AM
612 IStatus status = new Status(IStatus.INFO, Activator.instance().getPluginId(), Messages.LamiAnalysis_NoResults);
613 throw new CoreException(status);
4208b510
AM
614 }
615
616 for (int i = 0; i < results.length(); i++) {
617 JSONObject result = results.getJSONObject(i);
618
619 /* Parse the time-range */
9415b799
PP
620 JSONObject trObject = checkNotNull(result.getJSONObject(LamiStrings.TIME_RANGE));
621 LamiData trData = LamiData.createFromObject(trObject);
622 if (!(trData instanceof LamiTimeRange)) {
623 throw new JSONException("Time range did not have expected class type."); //$NON-NLS-1$
624 }
625 LamiTimeRange tr = (LamiTimeRange) trData;
4208b510
AM
626
627 /* Parse the table's class */
628 LamiTableClass tableClass;
629 JSONObject tableClassObject = result.optJSONObject(LamiStrings.CLASS);
630 if (tableClassObject == null) {
631 /*
632 * "class" is just a standard string, indicating we use a
633 * metadata-defined table class as-is
634 */
635 @NonNull String tableClassName = checkNotNull(result.getString(LamiStrings.CLASS));
636 tableClass = getTableClassFromName(tableClassName);
637
638 // FIXME Rest will become more generic eventually in the LAMI format.
639 } else if (tableClassObject.has(LamiStrings.INHERIT)) {
640 /*
641 * Dynamic title: We reuse an existing table class but
642 * override the title.
643 */
644 String baseTableName = checkNotNull(tableClassObject.getString(LamiStrings.INHERIT));
645 LamiTableClass baseTableClass = getTableClassFromName(baseTableName);
646 String newTitle = checkNotNull(tableClassObject.getString(LamiStrings.TITLE));
647
648 tableClass = new LamiTableClass(baseTableClass, newTitle);
649 } else {
650 /*
651 * Dynamic column descriptions: we implement a new table
652 * class entirely.
653 */
654 String title = checkNotNull(tableClassObject.getString(LamiStrings.TITLE));
655 JSONArray columnDescriptions = checkNotNull(tableClassObject.getJSONArray(LamiStrings.COLUMN_DESCRIPTIONS));
656 List<LamiTableEntryAspect> aspects = getAspectsFromColumnDescriptions(columnDescriptions);
657
658 tableClass = new LamiTableClass(nullToEmptyString(Messages.LamiAnalysis_DefaultDynamicTableName), title, aspects, Collections.EMPTY_SET);
659 }
660
661 /* Parse the "data", which is the array of rows */
662 JSONArray data = result.getJSONArray(LamiStrings.DATA);
663 ImmutableList.Builder<LamiTableEntry> dataBuilder = new ImmutableList.Builder<>();
664
665 for (int j = 0; j < data.length(); j++) {
666 /* A row is an array of cells */
667 JSONArray row = data.getJSONArray(j);
668 ImmutableList.Builder<LamiData> rowBuilder = ImmutableList.builder();
669
670 for (int k = 0; k < row.length(); k++) {
671 Object cellObject = checkNotNull(row.get(k));
672 LamiData cellValue = LamiData.createFromObject(cellObject);
673 rowBuilder.add(cellValue);
674
675 }
676 dataBuilder.add(new LamiTableEntry(rowBuilder.build()));
677 }
678 resultsBuilder.add(new LamiResultTable(tr, tableClass, dataBuilder.build()));
679 }
680
681 } catch (JSONException e) {
ae5bf609 682 LOGGER.severe(() -> "[LamiAnalysis:ErrorParsingExecutionOutput] msg=" + e.getMessage()); //$NON-NLS-1$
46f0c09c
AM
683 IStatus status = new Status(IStatus.ERROR, Activator.instance().getPluginId(), e.getMessage(), e);
684 throw new CoreException(status);
4208b510
AM
685 }
686
687 return resultsBuilder.build();
688 }
689
690 private LamiTableClass getTableClassFromName(String tableClassName) throws JSONException {
691 Map<String, LamiTableClass> map = checkNotNull(fTableClasses);
692 LamiTableClass tableClass = map.get(tableClassName);
693 if (tableClass == null) {
694 throw new JSONException("Table class " + tableClassName + //$NON-NLS-1$
695 " was not declared in the metadata"); //$NON-NLS-1$
696 }
697 return tableClass;
698 }
699
700 /**
701 * Get the output of an external command, used for getting the metadata.
702 * Cannot be cancelled, and will not report errors, simply returns null if
703 * the process ended abnormally.
ace6413c
MJ
704 *
705 * @param command
706 * The parameters of the command, passed to
707 * {@link ProcessBuilder}
708 * @return The command output as a string
4208b510 709 */
ace6413c
MJ
710 @VisibleForTesting
711 protected @Nullable String getOutputFromCommand(List<String> command) {
4208b510
AM
712 try {
713 ProcessBuilder builder = new ProcessBuilder(command);
714 builder.redirectErrorStream(true);
715
716 Process p = builder.start();
717 try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));) {
718 int ret = p.waitFor();
719 String output = br.lines().collect(Collectors.joining());
720
721 return (ret == 0 ? output : null);
722 }
723 } catch (IOException | InterruptedException e) {
724 return null;
725 }
726 }
727
728 /**
729 * Get the results of invoking the specified command.
730 *
ace6413c
MJ
731 * The result should start with '{"results":...', as specified by the LAMI
732 * JSON protocol. The JSON itself may be split over multiple lines.
4208b510
AM
733 *
734 * @param command
735 * The command to run (program and its arguments)
ace6413c
MJ
736 * @param monitor
737 * The progress monitor
4208b510 738 * @return The analysis results
46f0c09c 739 * @throws CoreException
ace6413c
MJ
740 * If the command ended abnormally, and normal results were not
741 * returned
4208b510 742 */
ace6413c
MJ
743 @VisibleForTesting
744 protected String getResultsFromCommand(List<String> command, IProgressMonitor monitor)
46f0c09c 745 throws CoreException {
4208b510
AM
746
747 final int scale = 1000;
748 double workedSoFar = 0.0;
749
750 ProcessCanceller cancellerRunnable = null;
751 Thread cancellerThread = null;
752
753 try {
754 monitor.beginTask(Messages.LamiAnalysis_MainTaskName, scale);
755
756 ProcessBuilder builder = new ProcessBuilder(command);
757 builder.redirectErrorStream(false);
758
759 Process p = checkNotNull(builder.start());
760
761 cancellerRunnable = new ProcessCanceller(p, monitor);
762 cancellerThread = new Thread(cancellerRunnable);
763 cancellerThread.start();
764
765 List<String> results = new ArrayList<>();
766
767 try (BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));) {
768 String line = in.readLine();
769 while (line != null && !line.matches("\\s*\\{.*")) { //$NON-NLS-1$
770 /*
771 * This is a line indicating progress, it has the form:
772 *
773 * 0.123 3000 of 5000 events processed
774 *
775 * The first part indicates the estimated fraction (out of
776 * 1.0) of work done. The second part is status text.
777 */
778
779 // Trim the line first to make sure the first character is
780 // significant
781 line = line.trim();
782
783 // Split at the first space
784 String[] elems = line.split(" ", 2); //$NON-NLS-1$
785
786 if (elems[0].matches("\\d.*")) { //$NON-NLS-1$
787 // It looks like we have a progress indication
788 try {
789 // Try parsing the number
790 double cumulativeWork = Double.parseDouble(elems[0]) * scale;
791 double workedThisLoop = cumulativeWork - workedSoFar;
792
793 // We're going backwards? Do not update the
794 // monitor's value
795 if (workedThisLoop > 0) {
796 monitor.internalWorked(workedThisLoop);
797 workedSoFar = cumulativeWork;
798 }
799
800 // There is a message: update the monitor's task name
801 if (elems.length >= 2) {
802 monitor.setTaskName(elems[1].trim());
803 }
804 } catch (NumberFormatException e) {
805 // Continue reading progress lines anyway
806 }
807 }
808
809 line = in.readLine();
810 }
811 while (line != null) {
812 /*
813 * We have seen the first line containing a '{', this is our
814 * JSON output!
815 */
816 results.add(line);
817 line = in.readLine();
818 }
819 }
820 int ret = p.waitFor();
821
822 if (monitor.isCanceled()) {
823 /* We were interrupted by the canceller thread. */
46f0c09c
AM
824 IStatus status = new Status(IStatus.CANCEL, Activator.instance().getPluginId(), null);
825 throw new CoreException(status);
4208b510
AM
826 }
827
828 if (ret != 0) {
829 /*
830 * Something went wrong running the external script. We will
831 * gather the stderr and report it to the user.
832 */
833 BufferedReader br = new BufferedReader(new InputStreamReader(p.getErrorStream()));
46f0c09c
AM
834 List<String> stdErrOutput = br.lines().collect(Collectors.toList());
835
836 MultiStatus status = new MultiStatus(Activator.instance().getPluginId(),
837 IStatus.ERROR, Messages.LamiAnalysis_ErrorDuringExecution, null);
838 for (String str : stdErrOutput) {
839 status.add(new Status(IStatus.ERROR, Activator.instance().getPluginId(), str));
840 }
841 if (stdErrOutput.isEmpty()) {
842 /*
843 * At least say "no output", so an error message actually
844 * shows up.
845 */
846 status.add(new Status(IStatus.ERROR, Activator.instance().getPluginId(), Messages.LamiAnalysis_ErrorNoOutput));
847 }
848 throw new CoreException(status);
4208b510
AM
849 }
850
851 /* External script ended successfully, all is fine! */
852 String resultsStr = results.stream().collect(Collectors.joining());
853 return checkNotNull(resultsStr);
854
855 } catch (IOException | InterruptedException e) {
46f0c09c
AM
856 IStatus status = new Status(IStatus.ERROR, Activator.instance().getPluginId(), Messages.LamiAnalysis_ExecutionInterrupted, e);
857 throw new CoreException(status);
4208b510
AM
858
859 } finally {
860 if (cancellerRunnable != null) {
861 cancellerRunnable.setFinished();
862 }
863 if (cancellerThread != null) {
864 try {
865 cancellerThread.join();
866 } catch (InterruptedException e) {
867 }
868 }
869
870 monitor.done();
871 }
872 }
873
874 private static class ProcessCanceller implements Runnable {
875
876 private final Process fProcess;
877 private final IProgressMonitor fMonitor;
878
879 private boolean fIsFinished = false;
880
881 public ProcessCanceller(Process process, IProgressMonitor monitor) {
882 fProcess = process;
883 fMonitor = monitor;
884 }
885
886 public void setFinished() {
887 fIsFinished = true;
888 }
889
890 @Override
891 public void run() {
892 try {
893 while (!fIsFinished) {
894 Thread.sleep(500);
895 if (fMonitor.isCanceled()) {
896 fProcess.destroy();
897 return;
898 }
899 }
900 } catch (InterruptedException e) {
901 }
902 }
903
904 }
905
906 @Override
907 public @NonNull String getName() {
908 return fName;
909 }
910
911 @Override
912 public boolean isUserDefined() {
913 return fIsUserDefined;
914 }
915
916}
This page took 0.084104 seconds and 5 git commands to generate.