From 4bd7cc77f5bfded3639180560fcdaf081675e4ce Mon Sep 17 00:00:00 2001 From: Alexandre Montplaisir Date: Mon, 24 Oct 2016 20:32:36 -0400 Subject: [PATCH] common: Add a ProcessUtils for external process launching Consolidate the existing external-process-launching utility methods into a new common class. Right now this includes the FileOffsetMapper, which calls addr2line for debug-info symbol resolution, as well as the LamiAnalysis class which calls the LAMI analysis scripts. Bug: 508406 Change-Id: I685fb461a93cd6726575b5df771233f37e423e5f Signed-off-by: Alexandre Montplaisir Reviewed-on: https://git.eclipse.org/r/85973 Reviewed-by: Hudson CI --- .../lami/core/module/LamiAnalysis.java | 214 ++++------------ .../META-INF/MANIFEST.MF | 1 + .../common/core/process/Messages.java | 37 +++ .../common/core/process/ProcessUtils.java | 238 ++++++++++++++++++ .../common/core/process/messages.properties | 13 + .../common/core/process/package-info.java | 11 + .../analysis/debuginfo/FileOffsetMapper.java | 26 +- 7 files changed, 356 insertions(+), 184 deletions(-) create mode 100644 common/org.eclipse.tracecompass.common.core/src/org/eclipse/tracecompass/common/core/process/Messages.java create mode 100644 common/org.eclipse.tracecompass.common.core/src/org/eclipse/tracecompass/common/core/process/ProcessUtils.java create mode 100644 common/org.eclipse.tracecompass.common.core/src/org/eclipse/tracecompass/common/core/process/messages.properties create mode 100644 common/org.eclipse.tracecompass.common.core/src/org/eclipse/tracecompass/common/core/process/package-info.java diff --git a/analysis/org.eclipse.tracecompass.analysis.lami.core/src/org/eclipse/tracecompass/internal/provisional/analysis/lami/core/module/LamiAnalysis.java b/analysis/org.eclipse.tracecompass.analysis.lami.core/src/org/eclipse/tracecompass/internal/provisional/analysis/lami/core/module/LamiAnalysis.java index b71391edaa..019b206caa 100644 --- a/analysis/org.eclipse.tracecompass.analysis.lami.core/src/org/eclipse/tracecompass/internal/provisional/analysis/lami/core/module/LamiAnalysis.java +++ b/analysis/org.eclipse.tracecompass.analysis.lami.core/src/org/eclipse/tracecompass/internal/provisional/analysis/lami/core/module/LamiAnalysis.java @@ -13,10 +13,7 @@ import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull; import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNullContents; import static org.eclipse.tracecompass.common.core.NonNullUtils.nullToEmptyString; -import java.io.BufferedReader; import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; @@ -35,11 +32,12 @@ import java.util.stream.Stream; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.MultiStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.tracecompass.common.core.log.TraceCompassLog; +import org.eclipse.tracecompass.common.core.process.ProcessUtils; +import org.eclipse.tracecompass.common.core.process.ProcessUtils.OutputReaderFunction; import org.eclipse.tracecompass.internal.analysis.lami.core.Activator; import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.LamiStrings; import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.ShellUtils; @@ -830,21 +828,11 @@ public class LamiAnalysis implements IOnDemandAnalysis { @VisibleForTesting protected @Nullable String getOutputFromCommand(List command) { LOGGER.info(() -> LOG_RUNNING_MESSAGE + ' ' + command.toString()); - - try { - ProcessBuilder builder = new ProcessBuilder(command); - builder.redirectErrorStream(true); - - Process p = builder.start(); - try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));) { - int ret = p.waitFor(); - String output = br.lines().collect(Collectors.joining()); - - return (ret == 0 ? output : null); - } - } catch (IOException | InterruptedException e) { + List lines = ProcessUtils.getOutputFromCommand(command); + if (lines == null) { return null; } + return String.join("", lines); //$NON-NLS-1$ } /** @@ -865,165 +853,69 @@ public class LamiAnalysis implements IOnDemandAnalysis { @VisibleForTesting protected String getResultsFromCommand(List command, IProgressMonitor monitor) throws CoreException { + List lines = ProcessUtils.getOutputFromCommandCancellable(command, monitor, nullToEmptyString(Messages.LamiAnalysis_MainTaskName), OUTPUT_READER); + return checkNotNull(String.join("", lines)); //$NON-NLS-1$ + } - final int scale = 1000; + private static final OutputReaderFunction OUTPUT_READER = (reader, monitor) -> { double workedSoFar = 0.0; - ProcessCanceller cancellerRunnable = null; - Thread cancellerThread = null; - - try { - monitor.beginTask(Messages.LamiAnalysis_MainTaskName, scale); - - ProcessBuilder builder = new ProcessBuilder(command); - builder.redirectErrorStream(false); - - Process p = checkNotNull(builder.start()); - - cancellerRunnable = new ProcessCanceller(p, monitor); - cancellerThread = new Thread(cancellerRunnable); - cancellerThread.start(); + String line = reader.readLine(); + while (line != null && !line.matches("\\s*\\{.*")) { //$NON-NLS-1$ + /* + * This is a line indicating progress, it has the form: + * + * 0.123 3000 of 5000 events processed + * + * The first part indicates the estimated fraction (out of 1.0) of + * work done. The second part is status text. + */ - List results = new ArrayList<>(); + // Trim the line first to make sure the first character is + // significant + line = line.trim(); - try (BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));) { - String line = in.readLine(); - while (line != null && !line.matches("\\s*\\{.*")) { //$NON-NLS-1$ - /* - * This is a line indicating progress, it has the form: - * - * 0.123 3000 of 5000 events processed - * - * The first part indicates the estimated fraction (out of - * 1.0) of work done. The second part is status text. - */ + // Split at the first space + String[] elems = line.split(" ", 2); //$NON-NLS-1$ - // Trim the line first to make sure the first character is - // significant - line = line.trim(); - - // Split at the first space - String[] elems = line.split(" ", 2); //$NON-NLS-1$ - - if (elems[0].matches("\\d.*")) { //$NON-NLS-1$ - // It looks like we have a progress indication - try { - // Try parsing the number - double cumulativeWork = Double.parseDouble(elems[0]) * scale; - double workedThisLoop = cumulativeWork - workedSoFar; - - // We're going backwards? Do not update the - // monitor's value - if (workedThisLoop > 0) { - monitor.internalWorked(workedThisLoop); - workedSoFar = cumulativeWork; - } - - // There is a message: update the monitor's task name - if (elems.length >= 2) { - monitor.setTaskName(elems[1].trim()); - } - } catch (NumberFormatException e) { - // Continue reading progress lines anyway - } + if (elems[0].matches("\\d.*")) { //$NON-NLS-1$ + // It looks like we have a progress indication + try { + // Try parsing the number + double cumulativeWork = Double.parseDouble(elems[0]) * 1000; + double workedThisLoop = cumulativeWork - workedSoFar; + + // We're going backwards? Do not update the + // monitor's value + if (workedThisLoop > 0) { + monitor.internalWorked(workedThisLoop); + workedSoFar = cumulativeWork; } - line = in.readLine(); - } - while (line != null) { - /* - * We have seen the first line containing a '{', this is our - * JSON output! - */ - results.add(line); - line = in.readLine(); - } - } - int ret = p.waitFor(); - - if (monitor.isCanceled()) { - /* We were interrupted by the canceller thread. */ - IStatus status = new Status(IStatus.CANCEL, Activator.instance().getPluginId(), null); - throw new CoreException(status); - } - - if (ret != 0) { - /* - * Something went wrong running the external script. We will - * gather the stderr and report it to the user. - */ - BufferedReader br = new BufferedReader(new InputStreamReader(p.getErrorStream())); - List stdErrOutput = br.lines().collect(Collectors.toList()); - - MultiStatus status = new MultiStatus(Activator.instance().getPluginId(), - IStatus.ERROR, Messages.LamiAnalysis_ErrorDuringExecution, null); - for (String str : stdErrOutput) { - status.add(new Status(IStatus.ERROR, Activator.instance().getPluginId(), str)); - } - if (stdErrOutput.isEmpty()) { - /* - * At least say "no output", so an error message actually - * shows up. - */ - status.add(new Status(IStatus.ERROR, Activator.instance().getPluginId(), Messages.LamiAnalysis_ErrorNoOutput)); - } - throw new CoreException(status); - } - - /* External script ended successfully, all is fine! */ - String resultsStr = results.stream().collect(Collectors.joining()); - return checkNotNull(resultsStr); - - } catch (IOException | InterruptedException e) { - IStatus status = new Status(IStatus.ERROR, Activator.instance().getPluginId(), Messages.LamiAnalysis_ExecutionInterrupted, e); - throw new CoreException(status); - - } finally { - if (cancellerRunnable != null) { - cancellerRunnable.setFinished(); - } - if (cancellerThread != null) { - try { - cancellerThread.join(); - } catch (InterruptedException e) { + // There is a message: update the monitor's task name + if (elems.length >= 2) { + monitor.setTaskName(elems[1].trim()); + } + } catch (NumberFormatException e) { + // Continue reading progress lines anyway } } - monitor.done(); - } - } - - private static class ProcessCanceller implements Runnable { - - private final Process fProcess; - private final IProgressMonitor fMonitor; - - private boolean fIsFinished = false; - - public ProcessCanceller(Process process, IProgressMonitor monitor) { - fProcess = process; - fMonitor = monitor; - } - - public void setFinished() { - fIsFinished = true; + line = reader.readLine(); } - @Override - public void run() { - try { - while (!fIsFinished) { - Thread.sleep(500); - if (fMonitor.isCanceled()) { - fProcess.destroy(); - return; - } - } - } catch (InterruptedException e) { - } + List results = new ArrayList<>(); + while (line != null) { + /* + * We have seen the first line containing a '{', this is our JSON + * output! + */ + results.add(line); + line = reader.readLine(); } + return results; + }; - } @Override public @NonNull String getName() { diff --git a/common/org.eclipse.tracecompass.common.core/META-INF/MANIFEST.MF b/common/org.eclipse.tracecompass.common.core/META-INF/MANIFEST.MF index eca20a17c8..afb10aff6d 100644 --- a/common/org.eclipse.tracecompass.common.core/META-INF/MANIFEST.MF +++ b/common/org.eclipse.tracecompass.common.core/META-INF/MANIFEST.MF @@ -15,5 +15,6 @@ Export-Package: org.eclipse.tracecompass.common.core, org.eclipse.tracecompass.common.core.format, org.eclipse.tracecompass.common.core.log, org.eclipse.tracecompass.common.core.math, + org.eclipse.tracecompass.common.core.process, org.eclipse.tracecompass.internal.common.core;x-internal:=true Import-Package: com.google.common.collect diff --git a/common/org.eclipse.tracecompass.common.core/src/org/eclipse/tracecompass/common/core/process/Messages.java b/common/org.eclipse.tracecompass.common.core/src/org/eclipse/tracecompass/common/core/process/Messages.java new file mode 100644 index 0000000000..4953b247db --- /dev/null +++ b/common/org.eclipse.tracecompass.common.core/src/org/eclipse/tracecompass/common/core/process/Messages.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * Copyright (c) 2016 EfficiOS Inc., Alexandre Montplaisir + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v1.0 which + * accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ + +package org.eclipse.tracecompass.common.core.process; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.osgi.util.NLS; + +/** + * Message bundle for the package + * + * @noreference Messages class + */ +@NonNullByDefault({}) +@SuppressWarnings("javadoc") +public class Messages extends NLS { + + private static final String BUNDLE_NAME = Messages.class.getPackage().getName() + ".messages"; //$NON-NLS-1$ + +// public static String LamiAnalysis_MainTaskName; +// public static String LamiAnalysis_NoResults; + public static String ProcessUtils_ErrorDuringExecution; + public static String ProcessUtils_ErrorNoOutput; + public static String ProcessUtils_ExecutionInterrupted; + + static { + NLS.initializeMessages(BUNDLE_NAME, Messages.class); + } + + private Messages() {} +} diff --git a/common/org.eclipse.tracecompass.common.core/src/org/eclipse/tracecompass/common/core/process/ProcessUtils.java b/common/org.eclipse.tracecompass.common.core/src/org/eclipse/tracecompass/common/core/process/ProcessUtils.java new file mode 100644 index 0000000000..479988c68f --- /dev/null +++ b/common/org.eclipse.tracecompass.common.core/src/org/eclipse/tracecompass/common/core/process/ProcessUtils.java @@ -0,0 +1,238 @@ +/******************************************************************************* + * Copyright (c) 2016 EfficiOS Inc., Alexandre Montplaisir + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ + +package org.eclipse.tracecompass.common.core.process; + +import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.MultiStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.tracecompass.internal.common.core.Activator; + +/** + * Common utility methods for launching external processes and retrieving their + * output. + * + * @author Alexandre Montplaisir + * @since 2.2 + */ +public final class ProcessUtils { + + private ProcessUtils() {} + + /** + * Simple output-getting command. Cannot be cancelled, and will return null + * if the external process exits with a non-zero return code. + * + * @param command + * The command (executable + arguments) to launch + * @return The process's standard output upon completion + */ + public static @Nullable List getOutputFromCommand(List command) { + try { + ProcessBuilder builder = new ProcessBuilder(command); + builder.redirectErrorStream(true); + + Process p = builder.start(); + try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));) { + List output = new LinkedList<>(); + + /* + * We must consume the output before calling Process.waitFor(), + * or else the buffers might fill and block the external program + * if there is a lot of output. + */ + String line = br.readLine(); + while (line != null) { + output.add(line); + line = br.readLine(); + } + + int ret = p.waitFor(); + return (ret == 0 ? output : null); + } + } catch (IOException | InterruptedException e) { + return null; + } + } + + /** + * Interface defining what do to with a process's output. For use with + * {@link #getOutputFromCommandCancellable}. + */ + @FunctionalInterface + public static interface OutputReaderFunction { + + /** + * Handle the output of the process. This can include reporting progress + * to the monitor, and pre-processing the returned output. + * + * @param reader + * A buffered reader to the process's standard output. + * Managed internally, so you do not need to + * {@link BufferedReader#close()} it. + * @param monitor + * The progress monitor. Implementation should check + * periodically if it is cancelled to end processing early. + * The monitor's start and end will be managed, but progress + * can be reported via the {@link IProgressMonitor#worked} + * method. The total is 1000 work units. + * @return The process's output + * @throws IOException + * If there was a read error. Letting throw all exception + * from the {@link BufferedReader} is recommended. + */ + List readOutput(BufferedReader reader, IProgressMonitor monitor) throws IOException; + } + + /** + * Cancellable output-getting command. The processing, as well as the + * external process itself, can be stopped by cancelling the passed progress + * monitor. + * + * @param command + * The command (executable + arguments) to execute + * @param monitor + * The progress monitor to check for cancellation and optionally + * progress + * @param mainTaskName + * The main task name of the job + * @param readerFunction + * What to do with the output. See {@link OutputReaderFunction}. + * @return The process's standard output, upon normal completion + * @throws CoreException + * If a problem happened with the execution of the external + * process. It can be reported to the user with the help of an + * ErrorDialog. + */ + public static List getOutputFromCommandCancellable(List command, + IProgressMonitor monitor, + String mainTaskName, + OutputReaderFunction readerFunction) + throws CoreException { + + CancellableRunnable cancellerRunnable = null; + Thread cancellerThread = null; + + try { + monitor.beginTask(mainTaskName, 1000); + + ProcessBuilder builder = new ProcessBuilder(command); + builder.redirectErrorStream(false); + + Process p = checkNotNull(builder.start()); + + cancellerRunnable = new CancellableRunnable(p, monitor); + cancellerThread = new Thread(cancellerRunnable); + cancellerThread.start(); + + try (BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(p.getInputStream()));) { + + List lines = readerFunction.readOutput(stdoutReader, monitor); + + int ret = p.waitFor(); + + if (monitor.isCanceled()) { + /* We were interrupted by the canceller thread. */ + IStatus status = new Status(IStatus.CANCEL, Activator.instance().getPluginId(), null); + throw new CoreException(status); + } + + if (ret != 0) { + /* + * Something went wrong running the external process. We + * will gather the stderr and report it to the user. + */ + BufferedReader stderrReader = new BufferedReader(new InputStreamReader(p.getErrorStream())); + List stderrOutput = stderrReader.lines().collect(Collectors.toList()); + + MultiStatus status = new MultiStatus(Activator.instance().getPluginId(), + IStatus.ERROR, Messages.ProcessUtils_ErrorDuringExecution, null); + for (String str : stderrOutput) { + status.add(new Status(IStatus.ERROR, Activator.instance().getPluginId(), str)); + } + if (stderrOutput.isEmpty()) { + /* + * At least say "no output", so an error message actually + * shows up. + */ + status.add(new Status(IStatus.ERROR, Activator.instance().getPluginId(), Messages.ProcessUtils_ErrorNoOutput)); + } + throw new CoreException(status); + } + + return lines; + } + + } catch (IOException | InterruptedException e) { + IStatus status = new Status(IStatus.ERROR, Activator.instance().getPluginId(), Messages.ProcessUtils_ExecutionInterrupted, e); + throw new CoreException(status); + + } finally { + if (cancellerRunnable != null) { + cancellerRunnable.setFinished(); + } + if (cancellerThread != null) { + try { + cancellerThread.join(); + } catch (InterruptedException e) { + } + } + + monitor.done(); + } + } + + /** + * Internal wrapper class that allows forcibly stopping a {@link Process} + * when its corresponding progress monitor is cancelled. + */ + private static class CancellableRunnable implements Runnable { + + private final Process fProcess; + private final IProgressMonitor fMonitor; + + private boolean fIsFinished = false; + + public CancellableRunnable(Process process, IProgressMonitor monitor) { + fProcess = process; + fMonitor = monitor; + } + + public void setFinished() { + fIsFinished = true; + } + + @Override + public void run() { + try { + while (!fIsFinished) { + Thread.sleep(500); + if (fMonitor.isCanceled()) { + fProcess.destroy(); + return; + } + } + } catch (InterruptedException e) { + } + } + + } +} diff --git a/common/org.eclipse.tracecompass.common.core/src/org/eclipse/tracecompass/common/core/process/messages.properties b/common/org.eclipse.tracecompass.common.core/src/org/eclipse/tracecompass/common/core/process/messages.properties new file mode 100644 index 0000000000..c6078d9f1c --- /dev/null +++ b/common/org.eclipse.tracecompass.common.core/src/org/eclipse/tracecompass/common/core/process/messages.properties @@ -0,0 +1,13 @@ +############################################################################### +# Copyright (c) 2016 EfficiOS Inc. and others +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +############################################################################### + +ProcessUtils_NoResults = No results were returned. +ProcessUtils_ErrorDuringExecution = Error during execution of the script. +ProcessUtils_ErrorNoOutput = (No output) +ProcessUtils_ExecutionInterrupted = Execution was interrupted. diff --git a/common/org.eclipse.tracecompass.common.core/src/org/eclipse/tracecompass/common/core/process/package-info.java b/common/org.eclipse.tracecompass.common.core/src/org/eclipse/tracecompass/common/core/process/package-info.java new file mode 100644 index 0000000000..a93108b15e --- /dev/null +++ b/common/org.eclipse.tracecompass.common.core/src/org/eclipse/tracecompass/common/core/process/package-info.java @@ -0,0 +1,11 @@ +/******************************************************************************* + * Copyright (c) 2016 EfficiOS Inc., Alexandre Montplaisir + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ + +@org.eclipse.jdt.annotation.NonNullByDefault +package org.eclipse.tracecompass.common.core.process; diff --git a/lttng/org.eclipse.tracecompass.lttng2.ust.core/src/org/eclipse/tracecompass/internal/lttng2/ust/core/analysis/debuginfo/FileOffsetMapper.java b/lttng/org.eclipse.tracecompass.lttng2.ust.core/src/org/eclipse/tracecompass/internal/lttng2/ust/core/analysis/debuginfo/FileOffsetMapper.java index d6700046d2..8b5b939682 100644 --- a/lttng/org.eclipse.tracecompass.lttng2.ust.core/src/org/eclipse/tracecompass/internal/lttng2/ust/core/analysis/debuginfo/FileOffsetMapper.java +++ b/lttng/org.eclipse.tracecompass.lttng2.ust.core/src/org/eclipse/tracecompass/internal/lttng2/ust/core/analysis/debuginfo/FileOffsetMapper.java @@ -11,21 +11,18 @@ package org.eclipse.tracecompass.internal.lttng2.ust.core.analysis.debuginfo; import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull; -import java.io.BufferedReader; import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; import java.nio.file.Files; import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.logging.Logger; -import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.tracecompass.common.core.log.TraceCompassLog; +import org.eclipse.tracecompass.common.core.process.ProcessUtils; import org.eclipse.tracecompass.tmf.core.event.lookup.TmfCallsite; import com.google.common.base.Objects; @@ -235,8 +232,8 @@ public final class FileOffsetMapper { List callsites = new LinkedList<>(); // FIXME Could eventually use CDT's Addr2line class once it implements --inlines - List output = getOutputFromCommand(Arrays.asList( - ADDR2LINE_EXECUTABLE, "-i", "-f", "-C", "-e", filePath, "0x" + Long.toHexString(offset))); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ + List command = Arrays.asList(ADDR2LINE_EXECUTABLE, "-i", "-f", "-C", "-e", filePath, "0x" + Long.toHexString(offset)); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ + List output = ProcessUtils.getOutputFromCommand(command); if (output == null) { /* Command returned an error */ @@ -283,21 +280,4 @@ public final class FileOffsetMapper { return callsites; } - - private static @Nullable List getOutputFromCommand(List command) { - try { - ProcessBuilder builder = new ProcessBuilder(command); - builder.redirectErrorStream(true); - - Process p = builder.start(); - try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));) { - int ret = p.waitFor(); - List lines = br.lines().collect(Collectors.toList()); - - return (ret == 0 ? lines : null); - } - } catch (IOException | InterruptedException e) { - return null; - } - } } -- 2.34.1