/*
* The MIT License
*
* Copyright 2012 Sony Ericsson Mobile Communications. All rights reserved.
* Copyright 2012 Sony Mobile Communications AB. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.sonyericsson.jenkins.plugins.bfa;
import com.sonyericsson.jenkins.plugins.bfa.db.KnowledgeBase;
import com.sonyericsson.jenkins.plugins.bfa.db.LocalFileKnowledgeBase;
import com.sonyericsson.jenkins.plugins.bfa.model.FailureCause;
import com.sonyericsson.jenkins.plugins.bfa.model.ScannerJobProperty;
import com.sonyericsson.jenkins.plugins.bfa.sod.ScanOnDemandQueue;
import com.sonyericsson.jenkins.plugins.bfa.sod.ScanOnDemandVariables;
import hudson.ExtensionList;
import hudson.Plugin;
import hudson.PluginManager;
import hudson.PluginWrapper;
import hudson.model.Descriptor;
import hudson.model.Hudson;
import hudson.model.Job;
import hudson.model.Result;
import hudson.model.Run;
import hudson.security.Permission;
import hudson.security.PermissionGroup;
import hudson.util.CopyOnWriteList;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.StaplerRequest;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* The main thing.
*
* @author Robert Sandell <robert.sandell@sonyericsson.com>
*/
public class PluginImpl extends Plugin {
private static final Logger logger = Logger.getLogger(PluginImpl.class.getName());
/**
* Convenience constant for the 24x24 icon size. used for {@link #getImageUrl(String, String)}.
*/
public static final String DEFAULT_ICON_SIZE = "24x24";
/**
* Convenience constant for the default icon size. used for {@link #getImageUrl(String, String)}.
*/
public static final String DEFAULT_ICON_NAME = "information.png";
/**
* Default number of concurrent scan threads.
*/
public static final int DEFAULT_NR_OF_SCAN_THREADS = 3;
/**
* Default max size of log to be scanned ('0' disables check).
*/
public static final int DEFAULT_MAX_LOG_SIZE = 0;
private static final int BYTES_IN_MEGABYTE = 1024 * 1024;
/**
* The permission group for all permissions related to this plugin.
*/
public static final PermissionGroup PERMISSION_GROUP =
new PermissionGroup(PluginImpl.class, Messages._PermissionGroup_Title());
/**
* Permission to update the causes. E.e. Access {@link CauseManagement}.
*/
public static final Permission UPDATE_PERMISSION =
new Permission(PERMISSION_GROUP, "UpdateCauses",
Messages._PermissionUpdate_Description(), Hudson.ADMINISTER);
/**
* Permission to view the causes. E.e. Access {@link CauseManagement}.
*/
public static final Permission VIEW_PERMISSION =
new Permission(PERMISSION_GROUP, "ViewCauses",
Messages._PermissionView_Description(), UPDATE_PERMISSION);
/**
* Permission to remove causes.
*/
public static final Permission REMOVE_PERMISSION =
new Permission(PERMISSION_GROUP, "RemoveCause",
Messages._PermissionRemove_Description(), Hudson.ADMINISTER);
private static final String DEFAULT_NO_CAUSES_MESSAGE = "No problems were identified. "
+ "If you know why this problem occurred, please add a suitable Cause for it.";
private static String staticResourcesBase = null;
/**
* Minimum allowed value for {@link #nrOfScanThreads}.
*/
protected static final int MINIMUM_NR_OF_SCAN_THREADS = 1;
private String noCausesMessage;
private Boolean globalEnabled;
private boolean doNotAnalyzeAbortedJob;
private Boolean gerritTriggerEnabled;
private transient CopyOnWriteList<FailureCause> causes;
private KnowledgeBase knowledgeBase;
private int nrOfScanThreads;
private int maxLogSize;
private Boolean graphsEnabled;
private Boolean testResultParsingEnabled;
private String testResultCategories;
/**
* ScanOnDemandVariable instance.
*/
private ScanOnDemandVariables sodVariables;
@Override
public void start() throws Exception {
super.start();
logger.finer("[BFA] Starting...");
load();
if (noCausesMessage == null) {
noCausesMessage = DEFAULT_NO_CAUSES_MESSAGE;
}
if (testResultCategories == null) {
testResultCategories = "";
}
if (nrOfScanThreads < 1) {
nrOfScanThreads = DEFAULT_NR_OF_SCAN_THREADS;
}
sodVariables = new ScanOnDemandVariables();
if (sodVariables.getMinimumSodWorkerThreads() < 1) {
sodVariables.setMinimumSodWorkerThreads(ScanOnDemandVariables.
DEFAULT_MINIMUM_SOD_WORKER_THREADS);
}
if (sodVariables.getMaximumSodWorkerThreads() < 1) {
sodVariables.setMaximumSodWorkerThreads(ScanOnDemandVariables.
DEFAULT_MAXIMUM_SOD_WORKER_THREADS);
}
if (sodVariables.getSodThreadKeepAliveTime() < 1) {
sodVariables.setSodThreadKeepAliveTime(ScanOnDemandVariables.
DEFAULT_SOD_THREADS_KEEP_ALIVE_TIME);
}
if (sodVariables.getSodWaitForJobShutdownTimeout() < 1) {
sodVariables.setSodWaitForJobShutdownTimeout(ScanOnDemandVariables.
DEFAULT_SOD_WAIT_FOR_JOBS_SHUTDOWN_TIMEOUT);
}
if (sodVariables.getSodCorePoolNumberOfThreads() < 1) {
sodVariables.setSodCorePoolNumberOfThreads(ScanOnDemandVariables.
DEFAULT_SOD_COREPOOL_THREADS);
}
if (knowledgeBase == null) {
if (causes == null) {
knowledgeBase = new LocalFileKnowledgeBase();
} else {
//Migrate old data.
knowledgeBase = new LocalFileKnowledgeBase(causes);
//No reason to keep it in memory right?
causes = null;
}
}
try {
knowledgeBase.start();
logger.fine("[BFA] Started!");
} catch (Exception e) {
logger.log(Level.SEVERE, "Could not initialize the knowledge base: ", e);
}
}
@Override
public void stop() throws Exception {
super.stop();
ScanOnDemandQueue.shutdown();
knowledgeBase.stop();
}
/**
* Returns the base relative URI for static resources packaged in webapp.
*
* @return the base URI.
*/
public static String getStaticResourcesBase() {
if (staticResourcesBase == null) {
PluginManager pluginManager = Jenkins.getInstance().getPluginManager();
if (pluginManager != null) {
PluginWrapper wrapper = pluginManager.getPlugin(PluginImpl.class);
if (wrapper != null) {
staticResourcesBase = "/plugin/" + wrapper.getShortName();
}
}
//Did we really find it?
if (staticResourcesBase == null) {
//This is not the preferred way since the module name could change,
//But in some unit test cases we cannot reach the plug-in info.
return "/plugin/build-failure-analyzer";
}
}
return staticResourcesBase;
}
/**
* Getter sodVariable.
*
* @return the message.
*/
public ScanOnDemandVariables getSodVariables() {
return sodVariables;
}
/**
* Returns the base relative URI for static images packaged in webapp.
*
* @return the images directory.
*
* @see #getStaticResourcesBase()
*/
public static String getStaticImagesBase() {
return getStaticResourcesBase() + "/images";
}
/**
* Provides a Jenkins relative url to a plugin internal image.
*
* @param size the size of the image (the sub directory of images).
* @param name the name of the image file.
* @return a URL to the image.
*/
public static String getImageUrl(String size, String name) {
return getStaticImagesBase() + "/" + size + "/" + name;
}
/**
* Get the full url to an image, including rootUrl and context path.
*
* @param size the size of the image (the sub directory of images).
* @param name the name of the image file.
* @return a URL to the image.
*/
public static String getFullImageUrl(String size, String name) {
return Jenkins.getInstance().getRootUrl() + getImageUrl(size, name);
}
/**
* Provides a Jenkins relative url to a plugin internal image of {@link #DEFAULT_ICON_SIZE} size.
*
* @param name the name of the image.
* @return a URL to the image.
*
* @see #getImageUrl(String, String)
*/
public static String getImageUrl(String name) {
return getImageUrl(DEFAULT_ICON_SIZE, name);
}
/**
* The default icon to be used throughout this plugin.
*
* @return the relative URL to the image.
*
* @see #getImageUrl(String)
* @see #getImageUrl(String, String)
*/
public static String getDefaultIcon() {
return getImageUrl(DEFAULT_ICON_NAME);
}
/**
* Returns the singleton instance.
*
* @return the one.
*/
@Nonnull
public static PluginImpl getInstance() {
Jenkins jenkins = Jenkins.getInstance();
if (jenkins == null) {
throw new AssertionError("Jenkins is not here yet.");
}
PluginImpl plugin = jenkins.getPlugin(PluginImpl.class);
if (plugin == null) {
throw new AssertionError("Not here yet.");
}
return plugin;
}
/**
* Getter for the no causes message.
*
* @return the message.
*/
public String getNoCausesMessage() {
return noCausesMessage;
}
/**
* If this feature is enabled or not. When on all unsuccessful builds will be scanned. None when off.
*
* @return true if on.
*/
public boolean isGlobalEnabled() {
if (globalEnabled == null) {
return true;
} else {
return globalEnabled;
}
}
/**
* If this feature is enabled or not. When on all aborted builds will be ignored.
*
* @return true if on.
*/
public boolean isDoNotAnalyzeAbortedJob() {
return doNotAnalyzeAbortedJob;
}
/**
* If graphs are enabled or not. Links to graphs and graphs will not be displayed when disabled.
* It can be enabled only if the knowledgeBase has support for it.
* @return True if enabled.
*/
public boolean isGraphsEnabled() {
if (graphsEnabled == null || knowledgeBase == null) {
return false;
} else {
return knowledgeBase.isStatisticsEnabled() && graphsEnabled;
}
}
/**
* If failed test cases should be represented as failure causes.
*
* @return True if enabled.
*/
public boolean isTestResultParsingEnabled() {
if (testResultParsingEnabled == null) {
return false;
} else {
return testResultParsingEnabled;
}
}
/**
* Get categories to be assigned to failure causes representing failed test cases.
*
* @return the categories.
*/
public String getTestResultCategories() {
return testResultCategories;
}
/**
* Sets if this feature is enabled or not. When on all aborted builds will be ignored.
*
* @param doNotAnalyzeAbortedJob on or off.
*/
public void setDoNotAnalyzeAbortedJob(boolean doNotAnalyzeAbortedJob) {
this.doNotAnalyzeAbortedJob = doNotAnalyzeAbortedJob;
}
/**
* Sets if this feature is enabled or not. When on all unsuccessful builds will be scanned. None when off.
*
* @param globalEnabled on or off. null == on.
*/
public void setGlobalEnabled(Boolean globalEnabled) {
this.globalEnabled = globalEnabled;
}
/**
* Sets if failed test cases should be represented as failure causes or not.
*
* @param testResultParsingEnabled on or off. null == off.
*/
public void setTestResultParsingEnabled(Boolean testResultParsingEnabled) {
this.testResultParsingEnabled = testResultParsingEnabled;
}
/**
* Set categories to be assigned to failure causes representing failed test cases.
*
* @param testResultCategories Space-separated string with categories
*/
public void setTestResultCategories(String testResultCategories) {
this.testResultCategories = testResultCategories;
}
/**
* Send notifications to Gerrit-Trigger-plugin.
*
* @return true if on.
*/
public boolean isGerritTriggerEnabled() {
if (gerritTriggerEnabled == null) {
return true;
} else {
return gerritTriggerEnabled;
}
}
/**
* Sets if this feature is enabled or not. When on, cause descriptions will be forwarded to Gerrit-Trigger-Plugin.
*
* @param gerritTriggerEnabled on or off. null == on.
*/
public void setGerritTriggerEnabled(Boolean gerritTriggerEnabled) {
this.gerritTriggerEnabled = gerritTriggerEnabled;
}
/**
* The number of threads to have in the pool for each build. Used by the {@link BuildFailureScanner}.
* Will return nothing less than {@link #MINIMUM_NR_OF_SCAN_THREADS}.
*
* @return the number of scan threads.
*/
public int getNrOfScanThreads() {
if (nrOfScanThreads < MINIMUM_NR_OF_SCAN_THREADS) {
nrOfScanThreads = DEFAULT_NR_OF_SCAN_THREADS;
}
return nrOfScanThreads;
}
/**
* The number of threads to have in the pool for each build. Used by the {@link BuildFailureScanner}.
* Will throw an {@link IllegalArgumentException} if the parameter is less than {@link #MINIMUM_NR_OF_SCAN_THREADS}.
*
* @param nrOfScanThreads the number of scan threads.
*/
public void setNrOfScanThreads(int nrOfScanThreads) {
if (nrOfScanThreads < MINIMUM_NR_OF_SCAN_THREADS) {
throw new IllegalArgumentException("Minimum nrOfScanThreads is " + MINIMUM_NR_OF_SCAN_THREADS);
}
this.nrOfScanThreads = nrOfScanThreads;
}
/**
* Set the maximum log size that should be scanned.
*
* @param maxLogSize value
*/
public void setMaxLogSize(int maxLogSize) {
this.maxLogSize = maxLogSize;
}
/**
* Returns the maximum log size that should be scanned.
*
* @return value
*/
public int getMaxLogSize() {
return maxLogSize;
}
/**
* Checks if the build with certain result should be analyzed or not.
*
* @param result the result
* @return true if it should be analyzed.
*/
public static boolean needToAnalyze(Result result) {
if (getInstance().isDoNotAnalyzeAbortedJob()) {
return result != Result.SUCCESS && result != Result.ABORTED;
} else {
return result != Result.SUCCESS;
}
}
/**
* Checks if the specified build should be scanned or not.
*
* @param build the build
* @return true if it should be scanned.
* @see {@link #shouldScan(Job)}
*/
public static boolean shouldScan(Run build) {
return shouldScan(build.getParent());
}
/**
* Checks that log size is in limits.
*
* @param build the build
* @return true if size is in limit.
*/
public static boolean isSizeInLimit(Run build) {
return getInstance().getMaxLogSize() == 0
|| getInstance().getMaxLogSize() > (build.getLogFile().length() / BYTES_IN_MEGABYTE);
}
/**
* Checks if the specified project should be scanned or not. Determined by {@link #isGlobalEnabled()} and if the
* project has {@link com.sonyericsson.jenkins.plugins.bfa.model.ScannerJobProperty#isDoNotScan()}.
*
* @param project the project
* @return true if it should be scanned.
*/
public static boolean shouldScan(Job project) {
if (getInstance().isGlobalEnabled()) {
ScannerJobProperty property = (ScannerJobProperty)project.getProperty(ScannerJobProperty.class);
if (property != null) {
return !property.isDoNotScan();
} else {
return true;
}
} else {
return false;
}
}
/**
* The knowledge base containing all causes.
*
* @return all the base.
*/
public KnowledgeBase getKnowledgeBase() {
return knowledgeBase;
}
/**
* Convenience method to reach the list from jelly.
*
* @return the list of registered KnowledgeBaseDescriptors
*/
public ExtensionList<KnowledgeBase.KnowledgeBaseDescriptor> getKnowledgeBaseDescriptors() {
return KnowledgeBase.KnowledgeBaseDescriptor.all();
}
/**
* Gets the KnowledgeBaseDescriptor that matches the name descString.
*
* @param descString either name of a KnowledgeBaseDescriptor or the fully qualified name.
* @return The matching KnowledgeBaseDescriptor or null if none is found.
*/
public KnowledgeBase.KnowledgeBaseDescriptor getKnowledgeBaseDescriptor(String descString) {
for (KnowledgeBase.KnowledgeBaseDescriptor desc : getKnowledgeBaseDescriptors()) {
if (desc.getClass().toString().contains(descString)) {
return desc;
}
}
return null;
}
@Override
public void configure(StaplerRequest req, JSONObject o) throws Descriptor.FormException, IOException {
noCausesMessage = o.getString("noCausesMessage");
globalEnabled = o.getBoolean("globalEnabled");
doNotAnalyzeAbortedJob = o.optBoolean("doNotAnalyzeAbortedJob", false);
gerritTriggerEnabled = o.getBoolean("gerritTriggerEnabled");
graphsEnabled = o.getBoolean("graphsEnabled");
testResultParsingEnabled = o.getBoolean("testResultParsingEnabled");
testResultCategories = o.getString("testResultCategories");
maxLogSize = o.optInt("maxLogSize");
int scanThreads = o.getInt("nrOfScanThreads");
int minSodWorkerThreads = o.getInt("minimumNumberOfWorkerThreads");
int maxSodWorkerThreads = o.getInt("maximumNumberOfWorkerThreads");
int thrkeepAliveTime = o.getInt("maximumNumberOfWorkerThreads");
int jobShutdownTimeWait = o.getInt("waitForJobShutdownTime");
int corePoolNumberOfThreads = o.getInt("corePoolNumberOfThreads");
if (scanThreads < MINIMUM_NR_OF_SCAN_THREADS) {
nrOfScanThreads = DEFAULT_NR_OF_SCAN_THREADS;
} else {
nrOfScanThreads = scanThreads;
}
if (maxLogSize < 0) {
maxLogSize = DEFAULT_MAX_LOG_SIZE;
}
if (corePoolNumberOfThreads < ScanOnDemandVariables.DEFAULT_SOD_COREPOOL_THREADS) {
sodVariables.setSodCorePoolNumberOfThreads(ScanOnDemandVariables.DEFAULT_SOD_COREPOOL_THREADS);
} else {
sodVariables.setSodCorePoolNumberOfThreads(corePoolNumberOfThreads);
}
if (jobShutdownTimeWait < ScanOnDemandVariables.DEFAULT_SOD_WAIT_FOR_JOBS_SHUTDOWN_TIMEOUT) {
sodVariables.setSodWaitForJobShutdownTimeout(ScanOnDemandVariables.
DEFAULT_SOD_WAIT_FOR_JOBS_SHUTDOWN_TIMEOUT);
} else {
sodVariables.setSodWaitForJobShutdownTimeout(jobShutdownTimeWait);
}
if (thrkeepAliveTime < ScanOnDemandVariables.DEFAULT_SOD_THREADS_KEEP_ALIVE_TIME) {
sodVariables.setSodThreadKeepAliveTime(ScanOnDemandVariables.DEFAULT_SOD_THREADS_KEEP_ALIVE_TIME);
} else {
sodVariables.setSodThreadKeepAliveTime(thrkeepAliveTime);
}
if (minSodWorkerThreads < ScanOnDemandVariables.DEFAULT_MINIMUM_SOD_WORKER_THREADS) {
sodVariables.setMinimumSodWorkerThreads(ScanOnDemandVariables.DEFAULT_MINIMUM_SOD_WORKER_THREADS);
} else {
sodVariables.setMinimumSodWorkerThreads(minSodWorkerThreads);
}
if (maxSodWorkerThreads < ScanOnDemandVariables.DEFAULT_MAXIMUM_SOD_WORKER_THREADS) {
sodVariables.setMaximumSodWorkerThreads(ScanOnDemandVariables.DEFAULT_MAXIMUM_SOD_WORKER_THREADS);
} else {
sodVariables.setMaximumSodWorkerThreads(maxSodWorkerThreads);
}
if (maxSodWorkerThreads < ScanOnDemandVariables.DEFAULT_MAXIMUM_SOD_WORKER_THREADS) {
sodVariables.setMaximumSodWorkerThreads(ScanOnDemandVariables.DEFAULT_MAXIMUM_SOD_WORKER_THREADS);
} else {
sodVariables.setMaximumSodWorkerThreads(maxSodWorkerThreads);
}
KnowledgeBase base = req.bindJSON(KnowledgeBase.class, o.getJSONObject("knowledgeBase"));
if (base != null && !knowledgeBase.equals(base)) {
try {
base.start();
} catch (Exception e) {
logger.log(Level.SEVERE, "Could not start new knowledge base, reverting ", e);
save();
return;
}
if (o.getBoolean("convertOldKb")) {
try {
base.convertFrom(knowledgeBase);
} catch (Exception e) {
logger.log(Level.SEVERE, "Could not convert knowledge base ", e);
}
}
knowledgeBase.stop();
knowledgeBase = base;
}
save();
}
}