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