agent/src/jetbrains/buildServer/xmlReportPlugin/XmlReportPlugin.java (559 lines of code) (raw):

package jetbrains.buildServer.xmlReportPlugin; import java.io.File; import java.util.*; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import jetbrains.buildServer.BuildProblemData; import jetbrains.buildServer.ExtensionsProvider; import jetbrains.buildServer.agent.*; import jetbrains.buildServer.agent.duplicates.DuplicatesReporter; import jetbrains.buildServer.agent.impl.MessageTweakingSupport; import jetbrains.buildServer.util.*; import jetbrains.buildServer.util.executors.ExecutorsFactory; import jetbrains.buildServer.util.impl.Lazy; import jetbrains.buildServer.util.positioning.PositionAware; import jetbrains.buildServer.util.positioning.PositionConstraint; import jetbrains.buildServer.xmlReportPlugin.duplicates.DuplicationReporter; import jetbrains.buildServer.xmlReportPlugin.duplicates.TeamCityDuplicationReporter; import jetbrains.buildServer.xmlReportPlugin.inspections.InspectionReporter; import jetbrains.buildServer.xmlReportPlugin.inspections.TeamCityInspectionReporter; import jetbrains.buildServer.xmlReportPlugin.tests.TeamCityTestReporter; import jetbrains.buildServer.xmlReportPlugin.tests.TestReporter; import jetbrains.buildServer.xmlReportPlugin.utils.LoggingUtils; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.springframework.util.AntPathMatcher; import static jetbrains.buildServer.xmlReportPlugin.XmlReportPluginUtil.*; public class XmlReportPlugin extends AgentLifeCycleAdapter implements RulesProcessor, PositionAware { private static final Pattern SPLIT_RULES = Pattern.compile(XmlReportPluginConstants.SPLIT_REGEX); @NotNull private final jetbrains.buildServer.agent.inspections.InspectionReporter myInspectionReporter; @NotNull private final DuplicatesReporter myDuplicatesReporter; @NotNull private final ExtensionsProvider myExtensionProvider; @Nullable private AgentRunningBuild myBuild; @NotNull private final ExecutorService myParseExecutor; @NotNull private final Lazy<Map<String, ParserFactory>> myParserFactoryMap = new Lazy<Map<String, ParserFactory>>() { @NotNull @Override protected Map<String, ParserFactory> createValue() { final Collection<ParserFactory> factories = myExtensionProvider.getExtensions(ParserFactory.class); final Map<String, ParserFactory> map = new HashMap<String, ParserFactory>((int)(factories.size() / 0.75f) + 1); for (ParserFactory factory : factories) { map.put(factory.getType(), factory); } return map; } }; @NotNull private final BuildAgentConfiguration myConfiguration; @Nullable private ProcessingContext myBuildProcessingContext; @Nullable private ProcessingContext myStepProcessingContext; private boolean myQuietMode; public XmlReportPlugin(@NotNull ExtensionsProvider extensionsProvider, @NotNull EventDispatcher<AgentLifeCycleListener> agentDispatcher, @NotNull jetbrains.buildServer.agent.inspections.InspectionReporter inspectionReporter, @NotNull DuplicatesReporter duplicatesReporter, @NotNull BuildAgentConfiguration configuration) { myExtensionProvider = extensionsProvider; myConfiguration = configuration; agentDispatcher.addListener(this); myInspectionReporter = inspectionReporter; myDuplicatesReporter = duplicatesReporter; myParseExecutor = createExecutor(); } @Override public void buildStarted(@NotNull AgentRunningBuild runningBuild) { myBuild = runningBuild; initBuildProcessingContext(runningBuild); } private void initBuildProcessingContext(final @NotNull AgentRunningBuild runningBuild) { myBuildProcessingContext = new ProcessingContext(new ArrayList<RulesContext>()); final Collection<AgentBuildFeature> features = getBuild().getBuildFeaturesOfType("xml-report-plugin"); if (features.isEmpty()) return; for (AgentBuildFeature feature : features) { final Map<String, String> params = feature.getParameters(); params.putAll(runningBuild.getSharedConfigParameters()); getBuildProcessingContext().rulesContexts.add(createRulesContext(new RulesData(getRules(params), params, getBuildProcessingContext().startTime))); } } @Override public synchronized void beforeRunnerStart(@NotNull BuildRunnerContext runner) { myQuietMode = PropertiesUtil.getBoolean(runner.getRunnerParameters().get(XmlReportPluginConstants.QUIET_MODE)); startProcessing(getBuildProcessingContext()); myStepProcessingContext = new ProcessingContext(new CopyOnWriteArrayList<RulesContext>()); } @Override public synchronized void processRules(@NotNull File rulesFile, @NotNull Map<String, String> params) { final ProcessingContext stepContext = getStepProcessingContext(); if (stepContext == null) { throw new IllegalStateException("Step processing context is null"); } if (stepContext.finished) return; // here we check if this path is already monitored for reports of this type // we also don't support processing two inspections type during one build final String newType = getReportType(params); for (RulesContext context : stepContext.rulesContexts) { final String existingType = context.getRulesData().getType(); if (existingType.equals(newType)) { final Collection<File> paths = context.getRulesData().getRules().getPaths(); if (paths.size() == 1) { if (paths.contains(rulesFile)) { LoggingUtils.LOG.info("Skip monitoring " + rulesFile + " (already monitoring)"); return; } } } if (isInspectionType(existingType) && newType != null && isInspectionType(newType)) { LoggingUtils .warn(String.format("Two different inspections can not be processed during one build, skip %s reports", getReportTypeName( newType)), getBuild().getBuildLogger()); return; } } final RulesData rulesData = new RulesData(getRules(rulesFile, params), params, stepContext.startTime); stepContext.rulesContexts.add(createRulesContext(rulesData)); startProcessing(stepContext); } @Override public synchronized void runnerFinished(@NotNull BuildRunnerContext runner, @NotNull BuildFinishedStatus status) { final ProcessingContext stepContext = getStepProcessingContext(); if (stepContext == null) return; // if beforeRunnerStart was not called finishProcessing(stepContext, true); finishProcessing(getBuildProcessingContext(), false); startProcessing(getBuildProcessingContext()); myStepProcessingContext = null; } @Override public void beforeBuildFinish(@NotNull final AgentRunningBuild build, @NotNull final BuildFinishedStatus buildStatus) { if (myBuildProcessingContext == null) return; finishProcessing(getBuildProcessingContext(), true); myBuild = null; myBuildProcessingContext = null; } @Override public void agentShutdown() { shutdownExecutor(myParseExecutor); } private RulesContext createRulesContext(@NotNull final RulesData rulesData) { final RulesState fileStateHolder = new RulesState(); final ParserFactory parserFactory = getParserFactory(rulesData.getType()); final RulesContext rulesContext = new RulesContext(rulesData, fileStateHolder); switch (parserFactory.getParsingStage()) { case BEFORE_FINISH: rulesContext.addParseFactory(parserFactory); break; case RUNTIME: rulesContext.setMonitorRulesCommand(new MonitorRulesCommand(rulesData.getMonitorRulesParameters(), rulesContext.getRulesState(), myQuietMode, new MonitorRulesCommand.MonitorRulesListener() { @Override public void modificationDetected(@NotNull File file) { submitParsing(file, rulesContext, parserFactory); } })); break; } return rulesContext; } private void startProcessing(@NotNull final ProcessingContext processingContext) { Thread monitor = processingContext.monitorThread; if (isStarted(monitor)) return; if (isRulesEmpty(processingContext)) return; processingContext.finished = false; monitor = new Thread(new Runnable() { public void run() { while (!processingContext.finished) { processAllRules(processingContext); try { Thread.sleep(500L); } catch (InterruptedException e) { getBuild().getBuildLogger().exception(e); } } } }); (processingContext.monitorThread = monitor).start(); } private boolean isRulesEmpty(final @NotNull ProcessingContext processingContext) { return processingContext.rulesContexts.isEmpty(); } @Contract("null -> false") private boolean isStarted(@Nullable final Thread monitor) { return monitor != null; } private void processAllRules(final @NotNull ProcessingContext processingContext) { for (RulesContext rulesContext : processingContext.rulesContexts) { final MonitorRulesCommand monitorRules = rulesContext.getMonitorRulesCommand(); if (monitorRules != null) monitorRules.run(); } } private void finishProcessing(@NotNull final ProcessingContext processingContext, boolean fullFinish) { Thread monitor = processingContext.monitorThread; if (!isStarted(monitor) && isRulesEmpty(processingContext)) return; if (!isStarted(monitor)) { // process all rules even if we do not have build steps processAllRules(processingContext); } processingContext.finished = true; try { monitor = processingContext.monitorThread; processingContext.monitorThread = null; if (isStarted(monitor)) { monitor.join(); } for (RulesContext rulesContext : processingContext.rulesContexts) { rulesContext.waitRuntimeParsing(); rulesContext.clearRuntimeParseTasks(); final MonitorRulesCommand monitorRules = rulesContext.getMonitorRulesCommand(); if (monitorRules != null) monitorRules.run(); if (fullFinish) rulesContext.finish(); else rulesContext.waitRuntimeParsing(); if (fullFinish && !myQuietMode) logStatistics(rulesContext); } } catch (Exception e) { LoggingUtils.logError("Exception occurred while finishing rules monitoring", e, getBuild().getBuildLogger(), false); } } private void submitParsing(@NotNull File file, @NotNull final RulesContext rulesContext, @NotNull ParserFactory parserFactory) { final ParseReportCommand parseReportCommand = new ParseReportCommand(file, rulesContext.getRulesData().getParseReportParameters(), rulesContext.getRulesState(), parserFactory); rulesContext.addParseTask(myParseExecutor, parseReportCommand); } private void shutdownExecutor(@NotNull ExecutorService executor) { executor.shutdown(); try { executor.awaitTermination(5, TimeUnit.SECONDS); if (!executor.isTerminated()) { LoggingUtils.LOG.warn("Waiting for one of xml-report-plugin executors to complete"); } executor.shutdownNow(); executor.awaitTermination(30, TimeUnit.SECONDS); } catch (InterruptedException e) { LoggingUtils.LOG.warn(e.toString()); LoggingUtils.LOG.debug(e.getMessage(), e); } if (!executor.isTerminated()) { final File dump = DiagnosticUtil.threadDumpToDirectory(myConfiguration.getAgentLogsDirectory(), new DiagnosticUtil.ThreadDumpData() .withSummary("Stopped waiting for one xml-report-plugin executors to complete, it is still running")); LoggingUtils.LOG.warn("Stopped waiting for one xml-report-plugin executors to complete, it is still running. Thread dump is saved to " + dump.getAbsolutePath()); } } private static ExecutorService createExecutor() { return ExecutorsFactory.newFixedDaemonExecutor("xml-report-plugin", 1); } @SuppressWarnings("ConstantConditions") private Rules getRules(@NotNull Map<String, String> parameters) { return getRules(getXmlReportPaths(parameters)); } @NotNull @SuppressWarnings("ConstantConditions") private Rules getRules(@Nullable File rulesFile, @NotNull Map<String, String> parameters) { final String rulesStr = rulesFile == null ? getXmlReportPaths(parameters) : rulesFile.getAbsolutePath(); return getRules(rulesStr); } @NotNull private Rules getRules(@NotNull String rulesStr) { final List<String> rules = Arrays.asList(SPLIT_RULES.split(rulesStr)); final File baseDir = getBuild().getCheckoutDirectory(); if (rules.size() == 1) { final String rule = rules.get(0); if (isFilePath(rule)) { return new FileRules(new File(resolveRule(rule, baseDir))); } } return new OptimizingIncludeExcludeRules(baseDir, rules); } @NotNull private String resolveRule(@NotNull String rule, @NotNull File baseDir) { if (rule.startsWith("+:") || rule.startsWith("-:")) { rule = rule.substring(2); } return FileUtil.normalizeAbsolutePath(FileUtil.resolvePath(baseDir, rule).getAbsolutePath()); } private boolean isFilePath(@NotNull String rule) { return !new AntPathMatcher().isPattern(rule); } private void logStatistics(@NotNull final RulesContext rulesContext) { final BuildProgressLogger logger = getBuild().getBuildLogger(); final Map<File, ParsingResult> succeeded = rulesContext.getRulesState().getProcessedFiles(); final Map<File, ParsingResult> failedToParse = rulesContext.getRulesState().getFailedToProcessFiles(); final List<File> outOfDate = rulesContext.getRulesState().getOutOfDateFiles(); final int processedFileCount = succeeded.size() + failedToParse.size(); final LogAction summaryLogAction = processedFileCount == 0 ? rulesContext.getRulesData().getWhenNoDataPublished() : LogAction.INFO; if (summaryLogAction == LogAction.DO_NOTHING) return; LoggingUtils.logInTarget(LoggingUtils.getTypeDisplayName(rulesContext.getRulesData().getType()) + " report watcher", new Runnable() { public void run() { final int totalFileCount = processedFileCount + outOfDate.size(); summaryLogAction.doLogAction( totalFileCount == 0 ? "No reports found for paths:" : totalFileCount + " " + StringUtil.pluralize("report", totalFileCount) + " found for paths:", logger); final Collection<String> rules = rulesContext.getRulesData().getRules().getBody(); if (rules.isEmpty()) { LoggingUtils.warn("<no paths>", logger); } for (String rule : rules) { summaryLogAction.doLogAction(rule, logger); } final ParsingResult result = getParserFactory(rulesContext.getRulesData().getType()).createEmptyResult(); if (!failedToParse.isEmpty()) { LoggingUtils.logInTarget("Parsing errors", new Runnable() { public void run() { LoggingUtils .error("Failed to parse " + failedToParse.size() + " " + StringUtil.pluralize("report", failedToParse.size()), logger); for (Map.Entry<File, ParsingResult> parsedFile : failedToParse.entrySet()) { final ParsingResult parsingResult = parsedFile.getValue(); final File file = parsedFile.getKey(); final Throwable problem = getProblem(parsingResult); String path = getPathInCheckoutDir(file); if (problem == null) path = path + ": Report is incomplete or has unexpected structure"; else if (StringUtil.isNotEmpty(problem.getMessage())) path = path + ": " + problem.getMessage(); LoggingUtils.logError(path, problem, logger, rulesContext.getRulesData().isVerbose() || failedToParse.size() == 1); result.accumulate(parsingResult); } } }, logger); if (rulesContext.getRulesData().failBuildIfParsingFailed()) { logger.logBuildProblem(createBuildProblem(rulesContext.getRulesData().getType(), failedToParse.keySet())); } } if (!succeeded.isEmpty()) { LoggingUtils.logInTarget("Successfully parsed", new Runnable() { public void run() { LoggingUtils .message(succeeded.size() + " " + StringUtil.pluralize("report", succeeded.size()), logger); for (Map.Entry<File, ParsingResult> parsedFile : succeeded.entrySet()) { final ParsingResult parsingResult = parsedFile.getValue(); final File file = parsedFile.getKey(); final String path = getPathInCheckoutDir(file); if (rulesContext.getRulesData().isVerbose() || succeeded.size() == 1) { LoggingUtils.message(path, logger); } else { LoggingUtils.LOG.debug(path); } result.accumulate(parsingResult); } } }, logger); } if (!outOfDate.isEmpty()) { LoggingUtils.logInTarget("Skipped as out-of-date", new Runnable() { @Override public void run() { LoggingUtils.verbose("Processing start time is: [" + rulesContext.getRulesData().getMonitorRulesParameters().getStartTime() + "]", logger); summaryLogAction.doLogAction(outOfDate.size() + " " + StringUtil.pluralize("report", outOfDate.size()), logger); for (File outOfDateFile : outOfDate) { final String path = getPathInCheckoutDir(outOfDateFile); final String details = path + " has last modified timestamp [" + outOfDateFile.lastModified() + "]"; if (rulesContext.getRulesData().isVerbose() || outOfDate.size() == 1 || processedFileCount == 0) { summaryLogAction.doLogAction(path, logger); } LoggingUtils.verbose(details, logger); } } }, logger); } result.logAsTotalResult(rulesContext.getRulesData().getParseReportParameters()); } }, logger); } private String getPathInCheckoutDir(@NotNull File file) { String relativePath = null; if (FileUtil.isAncestor(getBuild().getCheckoutDirectory(), file, false)) { relativePath = FileUtil.getRelativePath(getBuild().getCheckoutDirectory(), file); } return relativePath == null ? file.getAbsolutePath() : relativePath; } @Nullable Throwable getProblem(@NotNull ParsingResult parsingResult) { @SuppressWarnings({"ThrowableResultOfMethodCallIgnored"}) final Throwable problem = parsingResult.getProblem(); if (problem == null) return null; assert problem instanceof ParsingException; return problem.getCause(); } @SuppressWarnings({"NullableProblems"}) @NotNull private AgentRunningBuild getBuild() { if (myBuild == null) { throw new IllegalStateException("Build is null"); } return myBuild; } @NotNull private ParserFactory getParserFactory(@NotNull String type) { final Map<String, ParserFactory> map = myParserFactoryMap.getValue(); if (!map.containsKey(type)) throw new IllegalArgumentException("No factory for " + type); return map.get(type); } @SuppressWarnings({"NullableProblems"}) @NotNull private ProcessingContext getBuildProcessingContext() { if (myBuildProcessingContext == null) { throw new IllegalStateException("Build processing context is null"); } return myBuildProcessingContext; } @Nullable private synchronized ProcessingContext getStepProcessingContext() { return myStepProcessingContext; } @NotNull @Override public String getOrderId() { return "XmlReportPlugin"; } @NotNull @Override public PositionConstraint getConstraint() { return PositionConstraint.before("InspectionsReporter", "DuplicatesReporter"); } public class RulesData { @NotNull private final Rules myRules; @NotNull private final Map<String, String> myParameters; private final long myStartTime; public RulesData(@NotNull Rules rules, @NotNull Map<String, String> parameters, long startTime) { myRules = rules; myParameters = parameters; myStartTime = startTime; } @NotNull public Rules getRules() { return myRules; } @SuppressWarnings("ConstantConditions") @NotNull public String getType() { return getReportType(myParameters); } public boolean isVerbose() { return isOutputVerbose(myParameters); } @NotNull public LogAction getWhenNoDataPublished() { return LogAction.getAction(whenNoDataPublished(myParameters)); } public boolean failBuildIfParsingFailed() { return isFailBuildIfParsingFailed(myParameters); } @NotNull public MonitorRulesCommand.MonitorRulesParameters getMonitorRulesParameters() { return new MonitorRulesCommand.MonitorRulesParameters() { @NotNull @Override public Rules getRules() { return myRules; } @SuppressWarnings("ConstantConditions") @NotNull @Override public String getType() { return getReportType(myParameters); } @Override public boolean isParseOutOfDate() { return isParseOutOfDateReports(myParameters); } @Override public long getStartTime() { return myStartTime; } @NotNull @Override public BuildProgressLogger getThreadLogger() { return getBuild().getBuildLogger().getThreadLogger(); } @Override public boolean isReparseUpdated() { return isReparseUpdatedReports(myParameters); } }; } @NotNull public ParseParameters getParseReportParameters() { return new ParseParameters() { @Override public boolean isVerbose() { return isOutputVerbose(myParameters); } @NotNull @Override public BuildProgressLogger getThreadLogger() { return getBuild().getBuildLogger().getThreadLogger(); } @NotNull private BuildProgressLogger getInternalizingThreadLogger() { return isLogAsInternal() ? ((MessageTweakingSupport) getThreadLogger()).getTweakedLogger(MessageInternalizer.MESSAGE_INTERNALIZER) : getThreadLogger(); } private boolean isLogAsInternal() { return isLogIsInternal(myParameters); } @NotNull @Override public InspectionReporter getInspectionReporter() { return new TeamCityInspectionReporter(myInspectionReporter, getBuild().getBuildLogger(), getCheckoutDir(), getBuildProblemType(getType(), "InspectFailure")); } @NotNull @Override public DuplicationReporter getDuplicationReporter() { return new TeamCityDuplicationReporter(myDuplicatesReporter, getBuild().getBuildLogger(), getCheckoutDir().getAbsolutePath(), getBuildProblemType(getType(), "DupFailure")); } @NotNull @Override public TestReporter getTestReporter() { return new TeamCityTestReporter(getInternalizingThreadLogger(), getBuildProblemType(getType(), "TestFailure"), getCheckoutDir().getAbsolutePath()); } @NotNull @Override public Map<String, String> getParameters() { return Collections.unmodifiableMap(myParameters); } @SuppressWarnings("ConstantConditions") @NotNull @Override public String getType() { return getReportType(myParameters); } @NotNull @Override public File getCheckoutDir() { return getBuild().getCheckoutDirectory(); } }; } } @NotNull private BuildProblemData createBuildProblem(@NotNull String type, @NotNull Collection<File> failedToParse) { return BuildProblemData.createBuildProblem(String.valueOf(failedToParse.hashCode()), getBuildProblemType(type, "ParsingFailure"), "Failed to parse xml " + StringUtil.pluralize("report", failedToParse.size())); } @NotNull private String getBuildProblemType(@NotNull String type, @NotNull String suffix) { return StringUtil.truncateStringValue(XmlReportPluginConstants.BUILD_PROBLEM_TYPE + StringUtil.capitalize(type) + suffix, BuildProblemData.MAX_TYPE_LENGTH); } private final class ProcessingContext { private final long startTime; private volatile boolean finished; @Nullable private volatile Thread monitorThread; @NotNull private final List<RulesContext> rulesContexts; private ProcessingContext(@NotNull List<RulesContext> rulesContexts) { this.rulesContexts = rulesContexts; startTime = new Date().getTime()/1000*1000; finished = false; } } }