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