/* * Copyright 2000-2018 JetBrains s.r.o. * * 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. */ 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.Future; import java.util.concurrent.TimeUnit; import jetbrains.buildServer.BuildProblemData; import jetbrains.buildServer.ExtensionsProvider; import jetbrains.buildServer.agent.*; import jetbrains.buildServer.agent.impl.MessageTweakingSupport; import jetbrains.buildServer.util.DiagnosticUtil; import jetbrains.buildServer.util.EventDispatcher; import jetbrains.buildServer.util.FileUtil; import jetbrains.buildServer.util.StringUtil; 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.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 { @NotNull private final jetbrains.buildServer.agent.inspections.InspectionReporter myInspectionReporter; @NotNull private final jetbrains.buildServer.agent.duplicates.DuplicatesReporter myDuplicatesReporter; @NotNull private final ExtensionsProvider myExtensionProvider; @Nullable private AgentRunningBuild myBuild; @NotNull private final ExecutorService myParseExecutor; @NotNull private final Lazy> myParserFactoryMap = new Lazy>() { @Nullable @Override protected Map createValue() { final Map map = new HashMap(); final Collection factories = myExtensionProvider.getExtensions(ParserFactory.class); for (ParserFactory factory : factories) { map.put(factory.getType(), factory); } return map; } }; @NotNull private final BuildAgentConfiguration myConfiguration; @Nullable private ProcessingContext myBuildProcessingContext; @Nullable private ProcessingContext myStepProcessingContext; public XmlReportPlugin(@NotNull ExtensionsProvider extensionsProvider, @NotNull EventDispatcher agentDispatcher, @NotNull jetbrains.buildServer.agent.inspections.InspectionReporter inspectionReporter, @NotNull jetbrains.buildServer.agent.duplicates.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()); final Collection features = getBuild().getBuildFeaturesOfType("xml-report-plugin"); if (features.isEmpty()) return; for (AgentBuildFeature feature : features) { final Map 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) { startProcessing(getBuildProcessingContext()); myStepProcessingContext = new ProcessingContext(new CopyOnWriteArrayList()); } public synchronized void processRules(@NotNull File rulesFile, @NotNull Map params) { if (getNotNullStepProcessingContext().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 : getNotNullStepProcessingContext().rulesContexts) { final String existingType = context.getRulesData().getType(); if (existingType.equals(newType)) { final Collection paths = context.getRulesData().getRules().getPaths(); if (paths.size() == 1) { if (paths.contains(rulesFile)) { LoggingUtils.LOG.info("Skip monitoring " + rulesFile + " (already monitoring)"); return; } } } if (XmlReportPluginUtil.isInspectionType(existingType) && XmlReportPluginUtil.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, getNotNullStepProcessingContext().startTime); getNotNullStepProcessingContext().rulesContexts.add(createRulesContext(rulesData)); startProcessing(getNotNullStepProcessingContext()); } @Override public synchronized void runnerFinished(@NotNull BuildRunnerContext runner, @NotNull BuildFinishedStatus status) { if (getStepProcessingContext() == null) return; // if beforeRunnerStart was not called finishProcessing(getNotNullStepProcessingContext(), 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); final MonitorRulesCommand monitorRulesCommand = new MonitorRulesCommand(rulesData.getMonitorRulesParameters(), rulesContext.getRulesState(), new MonitorRulesCommand.MonitorRulesListener() { public void modificationDetected(@NotNull File file) { submitParsing(file, rulesContext, parserFactory); } }); rulesContext.setMonitorRulesCommand(monitorRulesCommand); return rulesContext; } private void startProcessing(@NotNull final ProcessingContext processingContext) { if (isStarted(processingContext)) return; if (!rulesExist(processingContext)) return; processingContext.finished = false; processingContext.monitorThread = 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.start(); } private boolean rulesExist(final @NotNull ProcessingContext processingContext) { return processingContext.rulesContexts.size() > 0; } private boolean isStarted(final @NotNull ProcessingContext processingContext) { return processingContext.monitorThread != null; } private void processAllRules(final @NotNull ProcessingContext processingContext) { for (RulesContext rulesContext : processingContext.rulesContexts) { rulesContext.getMonitorRulesCommand().run(); } } private void finishProcessing(@NotNull final ProcessingContext processingContext, boolean logStatistics) { if (!isStarted(processingContext) && !rulesExist(processingContext)) return; if (!isStarted(processingContext)) { // process all rules even if we do not have build steps processAllRules(processingContext); } processingContext.finished = true; try { if (isStarted(processingContext)) { processingContext.monitorThread.join(); } processingContext.monitorThread = null; for (RulesContext rulesContext : processingContext.rulesContexts) { for (Future future : rulesContext.getParseTasks()) { future.get(); } rulesContext.clearParseTasks(); rulesContext.getMonitorRulesCommand().run(); for (Future future : rulesContext.getParseTasks()) { future.get(); } if (logStatistics) 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); synchronized (myParseExecutor) { rulesContext.addParseTask(myParseExecutor.submit(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); } private Rules getRules(@NotNull Map parameters) { return getRules(getXmlReportPaths(parameters)); } private Rules getRules(@Nullable File rulesFile, @NotNull Map parameters) { final String rulesStr = rulesFile == null ? getXmlReportPaths(parameters) : rulesFile.getAbsolutePath(); return getRules(rulesStr); } @NotNull private Rules getRules(@NotNull String rulesStr) { final List rules = Arrays.asList(rulesStr.split(XmlReportPluginConstants.SPLIT_REGEX)); 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 succeeded = rulesContext.getRulesState().getProcessedFiles(); final Map failedToParse = rulesContext.getRulesState().getFailedToProcessFiles(); final List 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 + " report" + getEnding(totalFileCount) + " found for paths:", logger); final Collection rules = rulesContext.getRulesData().getRules().getBody(); if (rules.size() == 0) { LoggingUtils.warn("", logger); } else { for (String r : rules) { summaryLogAction.doLogAction(r, 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() + " report" + getEnding(failedToParse.size()), logger); for (Map.Entry e : failedToParse.entrySet()) { final Throwable p = getProblem(e.getValue()); String m = getPathInCheckoutDir(e.getKey()); if (p == null) m = m + ": Report is incomplete or has unexpected structure"; else if (StringUtil.isNotEmpty(p.getMessage())) m = m + ": " + p.getMessage(); LoggingUtils.logError(m, p, logger, rulesContext.getRulesData().isVerbose() || failedToParse.size() == 1); result.accumulate(e.getValue()); } } }, 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() + " report" + getEnding(succeeded.size()), logger); for (Map.Entry e : succeeded.entrySet()) { final String m = getPathInCheckoutDir(e.getKey()); if (rulesContext.getRulesData().isVerbose() || succeeded.size() == 1) { LoggingUtils.message(m, logger); } else { LoggingUtils.LOG.debug(m); } result.accumulate(e.getValue()); } } }, 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() + " report" + getEnding(outOfDate.size()), logger); for (File f : outOfDate) { final String m = getPathInCheckoutDir(f); final String details = m + " has last modified timestamp [" + f.lastModified() + "]"; if (rulesContext.getRulesData().isVerbose() || outOfDate.size() == 1 || processedFileCount == 0) { summaryLogAction.doLogAction(m, 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(); } @NotNull private static String getEnding(int count) { return count == 1 ? "" : "s"; } @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 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 private synchronized ProcessingContext getNotNullStepProcessingContext() { final ProcessingContext context = getStepProcessingContext(); if (context == null) { throw new IllegalStateException("Step processing context is null"); } return context; } @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 myParameters; private final long myStartTime; public RulesData(@NotNull Rules rules, @NotNull Map parameters, long startTime) { myRules = rules; myParameters = parameters; myStartTime = startTime; } @NotNull public Rules getRules() { return myRules; } @NotNull public String getType() { return XmlReportPluginUtil.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 public Rules getRules() { return myRules; } @NotNull public String getType() { return getReportType(myParameters); } public boolean isParseOutOfDate() { return isParseOutOfDateReports(myParameters); } public long getStartTime() { return myStartTime; } @NotNull public BuildProgressLogger getThreadLogger() { return getBuild().getBuildLogger().getThreadLogger(); } @Override public boolean isReparseUpdated() { return isReparseUpdatedReports(myParameters); } }; } @NotNull public ParseParameters getParseReportParameters() { return new ParseParameters() { public boolean isVerbose() { return isOutputVerbose(myParameters); } @NotNull 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 public InspectionReporter getInspectionReporter() { return new TeamCityInspectionReporter(myInspectionReporter, getBuild().getBuildLogger(), getCheckoutDir(), getBuildProblemType(getType(), "InspectFailure")); } @NotNull public DuplicationReporter getDuplicationReporter() { return new TeamCityDuplicationReporter(myDuplicatesReporter, getBuild().getBuildLogger(), getCheckoutDir().getAbsolutePath(), getBuildProblemType(getType(), "DupFailure")); } @NotNull public TestReporter getTestReporter() { return new TeamCityTestReporter(getInternalizingThreadLogger(), getBuildProblemType(getType(), "TestFailure"), getCheckoutDir().getAbsolutePath()); } @NotNull public Map getParameters() { return Collections.unmodifiableMap(myParameters); } @NotNull public String getType() { return getReportType(myParameters); } @NotNull public File getCheckoutDir() { return getBuild().getCheckoutDirectory(); } }; } } @NotNull private BuildProblemData createBuildProblem(@NotNull String type, @NotNull Collection 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 rulesContexts; private ProcessingContext(@NotNull List rulesContexts) { this.rulesContexts = rulesContexts; startTime = new Date().getTime()/1000*1000; finished = false; } } }