io.knowledgelinks.cicd.semantic.versioning.commandline.ShellRunner Maven / Gradle / Ivy
package io.knowledgelinks.cicd.semantic.versioning.commandline;
/*-
* #%L
* Semantic Versioning
* %%
* Copyright (C) 2022 - 2023 Knowledgelinks
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Container for running shell commands. It stores the error code and collects the data and error
* streams for retrieval.
*
* @author mstabile75
*
*/
public class ShellRunner implements Runnable {
private String[] command;
private Map environment;
List data = new ArrayList<>();
List errors = new ArrayList<>();
int exitCode;
private File directory = null;
public ShellRunner(String... command) {
this(null, command);
}
public ShellRunner(Map environment, String... command) {
this.command = command.clone();
this.environment = environment;
}
@Override
public void run() {
ProcessBuilder builder = new ProcessBuilder();
builder.command(command);
builder.directory(directory);
if (environment != null) {
Map builderEnv = builder.environment();
builderEnv.putAll(environment);
}
Process process = null;
ExecutorService service = null;
Thread dataStreamConsumer = null;
Thread errorStreamConsumer = null;
try {
process = builder.start();
dataStreamConsumer = new Thread(new StreamConsumer(process.getInputStream(), data::add));
errorStreamConsumer = new Thread(new StreamConsumer(process.getErrorStream(), errors::add));
dataStreamConsumer.start();
errorStreamConsumer.start();
service = Executors.newSingleThreadExecutor();
exitCode = process.waitFor();
} catch (IOException | InterruptedException e) {
exitCode = 1;
errors.add(0, String.format("%s: %s | running command: %s", e.getClass().getName(),
e.getMessage(), String.join(" ", command)));
} finally {
// ensure all streams, services and processes are closed
// all errors thrown here are ignored
try {
dataStreamConsumer.join();
} catch (NullPointerException | InterruptedException e) {
// ignore error
}
try {
errorStreamConsumer.join();
} catch (NullPointerException | InterruptedException e) {
// ignore error
}
try {
service.shutdown();
} catch (NullPointerException e) {
// ignore error
}
try {
process.destroy();
} catch (NullPointerException e) {
// ignore error
}
}
if (exitCode != 0) {
errors.add(0, "exitCode=" + exitCode);
}
}
public Map getEnvironment() {
return environment;
}
/**
* Set additional environment variables that will be added to the runtime.
*
* Note: this does not replace the environment that was in place when the JVM started.
*
* @param environment the environment to use in the shell call
*/
public void setEnvironment(Map environment) {
this.environment = environment;
}
public String[] getCommand() {
return command.clone();
}
/**
* Retrieve the data from the execution.
*
* @return the list of data
*/
public List getData() {
return data;
}
/**
* Retrieve the error output, if there was any.
*
* Note: Occasionally, this may contain data even if the program ran without an error. Use a
* combination of the errorCode and this field to determine if there was an error encountered for
* the specific command that was executed.
*
* @return the list of errors
*/
public List getErrors() {
return errors;
}
/**
* Returns the error code for the executed command. This is program dependent, however 0 typically
* indicates no errors were encountered.
*
* @return the exitCode from the shell command
*/
public int getExitCode() {
return exitCode;
}
public File getDirectory() {
return directory;
}
/**
* Set the working directory for the shell command to run.
*
* @param directory the directory to run the command
*/
public void setDirectory(File directory) {
this.directory = directory;
}
}