/*******************************************************************************
* Copyright (c) 2017 Synopsys, Inc
* 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
*
* Contributors:
* Synopsys, Inc - initial implementation and documentation
*******************************************************************************/
package jenkins.plugins.coverity;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.*;
import hudson.EnvVars;
import hudson.model.Queue;
import hudson.remoting.Channel;
import hudson.remoting.VirtualChannel;
import hudson.util.ArgumentListBuilder;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import org.apache.tools.ant.types.Commandline;
import java.io.*;
import java.lang.reflect.Array;
import java.util.*;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class CoverityUtils {
private static final Logger logger = Logger.getLogger(CoverityUtils.class.getName());
/**
* Evaluates an environment variable using the specified parser. The result is an interpolated string.
*/
public static String evaluateEnvVars(String input, EnvVars environment, boolean useAdvancedParser)throws RuntimeException{
try{
if(useAdvancedParser){
String interpolated = EnvParser.interpolateRecursively(input, 1, environment);
return interpolated;
} else {
return environment.expand(input);
}
}catch(Exception e){
throw new RuntimeException("Error trying to evaluate environment variable: " + input);
}
}
public static void checkDir(VirtualChannel channel, String home) throws Exception {
Validate.notNull(channel, VirtualChannel.class.getName() + " object can't be null");
Validate.notNull(home, String.class.getName() + " object can't be null");
FilePath homePath = new FilePath(channel, home);
if(!homePath.exists()){
throw new Exception("Directory: " + home + " doesn't exist.");
}
}
/**
* getCovBuild
*
* Retrieves the location of cov-build executable/sh from the system and returns the string of the
* path
* @return string of cov-build's path
*/
public static String getCovBuild(TaskListener listener, Node node) {
AbstractBuild build = getBuild();
AbstractProject project = build.getProject();
CoverityPublisher publisher = (CoverityPublisher) project.getPublishersList().get(CoverityPublisher.class);
InvocationAssistance invocationAssistance = publisher.getInvocationAssistance();
if(listener == null){
try{
throw new Exception("Listener used by getCovBuild() is null.");
} catch (Exception e){
e.printStackTrace();
return null;
}
}
String covBuild = "cov-build";
String home = null;
try {
home = publisher.getDescriptor().getHome(node, build.getEnvironment(listener));
} catch(Exception e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
listener.getLogger().println("[Error] " + pw.toString());
e.printStackTrace();
}
if(invocationAssistance != null){
if(invocationAssistance.getSaOverride() != null) {
try {
home = new CoverityInstallation(invocationAssistance.getSaOverride()).forEnvironment(build.getEnvironment(listener)).getHome();
CoverityUtils.checkDir(node.getChannel(), home);
} catch(IOException e) {
e.printStackTrace();
} catch(InterruptedException e) {
e.printStackTrace();
} catch(Exception e) {
e.printStackTrace();
}
}
}
if(home != null) {
covBuild = new FilePath(node.getChannel(), home).child("bin").child(covBuild).getRemote();
}
return covBuild;
}
/**
* Calls intepolate() in order to evaluate environment variables. In the case that a substitution took place, it
* tokenize the result and it calls itself on each token in case further evaluations are needed.
*
* In the case of a recursive definition (ex: VAR1=$VAR2, VAR2=$VAR1) an exception is thrown.
*/
public static List<String> expand(String input, EnvVars environment) throws ParseException {
/**
* Interpolates environment
*/
List<String> output = new ArrayList<>();
String interpolated = EnvParser.interpolateRecursively(input, 1, environment);
output.addAll(EnvParser.tokenize(interpolated));
return output;
}
/**
* Evaluates environment variables on a command represented by a list of tokens.
*/
public static List<String> evaluateEnvVars(List<String> input, EnvVars environment){
List<String> output = new ArrayList<String>();
try {
for(String arg : input){
output.addAll(expand(arg, environment));
}
} catch(ParseException e){
throw new RuntimeException(e.getMessage());
}
return output;
}
/**
* Gets the stacktrace from an exception, so that this exception can be handled.
*/
public static String getStackTrace(Exception e){
StringWriter writer = new StringWriter();
PrintWriter printWriter = new PrintWriter( writer );
e.printStackTrace(printWriter);
printWriter.flush();
String stackTrace = writer.toString();
try {
writer.close();
printWriter.close();
} catch (IOException e1) {
}
return stackTrace;
}
public static void handleException(String message, AbstractBuild<?, ?> build, BuildListener listener, Exception exception){
listener.getLogger().println(message);
listener.getLogger().println("Stacktrace: \n" + CoverityUtils.getStackTrace(exception));
build.setResult(Result.FAILURE);
}
public static void handleException(String message, AbstractBuild<?, ?> build, TaskListener listener, Exception exception){
listener.getLogger().println(message);
listener.getLogger().println("Stacktrace: \n" + CoverityUtils.getStackTrace(exception));
build.setResult(Result.FAILURE);
}
/**
* Prepares command according with the specified parsing mechanism. If "useAdvancedParser" is set to true, the plugin
* will evaluate environment variables with its custom mechanism. If not, environment variable substitution will be
* handled by Jenkins in the standard way.
*/
public static List<String> prepareCmds(List<String> input, String[] envVarsArray, boolean useAdvancedParser){
if(useAdvancedParser){
EnvVars envVars = new EnvVars(arrayToMap(envVarsArray));
return CoverityUtils.evaluateEnvVars(input, envVars);
} else {
return input;
}
}
public static List<String> prepareCmds(List<String> input, EnvVars envVars, boolean useAdvancedParser){
if(useAdvancedParser){
return CoverityUtils.evaluateEnvVars(input, envVars);
} else {
return input;
}
}
/**
* Jenkins API ProcStarter.envs() returns an array of environment variables where each element is a string "key=value".
* However the constructor for EnvVars accepts only arrays of the format [key1, value1, key2, value2]. Because of
* this, we need to transform that array into a map and use a constructor that accepts that map.
*/
public static Map<String, String> arrayToMap(String[] input){
Map<String, String> result = new HashMap<String, String>();
for(int i=0; i<input.length; i++){
List<String> keyValuePair = splitKeyValue(input[i]);
if(keyValuePair != null && !keyValuePair.isEmpty()){
String key = keyValuePair.get(0);
String value = keyValuePair.get(1);
result.put(key, value);
}
}
return result;
}
/**
* Split string of the form key=value into an array [key, value]
*/
public static List<String> splitKeyValue(String input){
if(input == null){
return null;
}
List<String> result = new ArrayList<>();
int index = input.indexOf('=');
if(index == 0){
logger.warning("Could not parse environment variable \"" + input + "\" because its key is empty.");
} else if (index < 0){
logger.warning("Could not parse environment variable \"" + input + "\" because no value for it has been defined.");
} else if(index == input.length() - 1){
logger.warning("Could not parse environment variable \"" + input + "\" because the value for it is empty.");
} else {
String key = input.substring(0, index);
String value = input.substring(index + 1);
result.add(key);
result.add(value);
}
return result;
}
/**
* Returns the InvocationAssistance for a given build.
*/
public static InvocationAssistance getInvocationAssistance(AbstractBuild<?, ?> build){
AbstractProject project = build.getProject();
CoverityPublisher publisher = (CoverityPublisher) project.getPublishersList().get(CoverityPublisher.class);
return publisher.getInvocationAssistance();
}
/**
* Returns the InvocationAssistance on the current thread. This can be used when an "AbstractBuild" object is not
* available, for example while decorating the launcher.
*/
public static InvocationAssistance getInvocationAssistance(){
AbstractBuild build = getBuild();
return getInvocationAssistance(build);
}
/**
* Collects environment variables from an array and an EnvVars object and returns an updated EnvVars object.
* This is useful for updating the environment variables on a ProcStarter with the variables from the listener.
*/
public static String[] addEnvVars(String[] envVarsArray, EnvVars envVars){
// All variables are stored on a map, the ones from ProcStarter will take precedence.
EnvVars resultMap = new EnvVars(envVars);
resultMap.putAll(arrayToMap(envVarsArray));
String[] r = new String[resultMap.size()];
int idx=0;
for (Map.Entry<String,String> e : resultMap.entrySet()) {
r[idx++] = e.getKey() + '=' + e.getValue();
}
return r;
}
public static int runCmd(List<String> cmd, AbstractBuild<?, ?> build, Launcher launcher, TaskListener listener,
EnvVars envVars, boolean useAdvancedParser) throws IOException, InterruptedException {
/**
* Get environment variables from a launcher, add custom environment environment variables if needed,
* then call join() to starts the launcher process.
*/
String[] launcherEnvVars = launcher.launch().envs();
launcherEnvVars = CoverityUtils.addEnvVars(launcherEnvVars, envVars);
cmd = prepareCmds(cmd, launcherEnvVars, useAdvancedParser);
int result = launcher.
launch().
cmds(new ArgumentListBuilder(cmd.toArray(new String[cmd.size()]))).
pwd(build.getWorkspace()).
stdout(listener).
stderr(listener.getLogger()).
envs(launcherEnvVars).
join();
return result;
}
public static AbstractBuild getBuild(){
Executor executor = Executor.currentExecutor();
Queue.Executable exec = executor.getCurrentExecutable();
AbstractBuild build = (AbstractBuild) exec;
return build;
}
public static String doubleQuote(String input){
return "\"" + input + "\"";
}
/**
* Coverity's parser remove double/single quotes but Jenkins parser does not. When dealing (for instance) with
* streams with spaces, we would expect [--stream, My Stream]. In order to do this the token "My Stream" must be
* quoted if using out parser, but not if using Jenkins.
*/
public static String doubleQuote(String input, boolean useAdvancedParser){
if(useAdvancedParser){
return "\"" + input + "\"";
} else {
return input;
}
}
/**
* Gets environment variables from the build.
*/
public static EnvVars getBuildEnvVars(TaskListener listener){
AbstractBuild build = CoverityUtils.getBuild();
EnvVars envVars = null;
try {
envVars = build.getEnvironment(listener);
} catch (Exception e) {
CoverityUtils.handleException(e.getMessage(), build, listener, e);
}
return envVars;
}
/**
* Gets environment variables from the given build
*/
public static EnvVars getBuildEnvVars(AbstractBuild build, TaskListener listener){
EnvVars envVars = null;
try {
envVars = build.getEnvironment(listener);
} catch (Exception e) {
CoverityUtils.handleException(e.getMessage(), build, listener, e);
}
return envVars;
}
public static Collection<File> listFiles(
File directory,
FilenameFilter filter,
boolean recurse) {
Vector<File> files = new Vector<File>();
File[] entries = directory.listFiles();
if (entries == null) {
return files;
}
for(File entry : entries) {
if(filter == null || filter.accept(directory, entry.getName())) {
files.add(entry);
}
if(recurse && entry.isDirectory()) {
files.addAll(listFiles(entry, filter, recurse));
}
}
return files;
}
public static File[] listFilesAsArray(
File directory,
FilenameFilter filter,
boolean recurse) {
Collection<File> files = listFiles(directory, filter, recurse);
File[] arr = new File[files.size()];
return files.toArray(arr);
}
}