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