/* * The MIT License * * 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.model; import java.io.BufferedReader; import java.io.IOException; import java.io.PrintStream; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Scanner; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.google.common.base.Joiner; import com.sonyericsson.jenkins.plugins.bfa.model.indication.FoundIndication; import com.sonyericsson.jenkins.plugins.bfa.model.indication.Indication; import hudson.Util; import hudson.console.ConsoleNote; import hudson.model.AbstractBuild; import hudson.model.Run; import org.codehaus.jackson.annotate.JsonIgnoreType; /** * Reader used to find indications of a failure cause. * * @author Tomas Westling <tomas.westling@sonymobile.com> */ @JsonIgnoreType public abstract class FailureReader { private static final Logger logger = Logger.getLogger(FailureReader.class.getName()); private static final long TIMEOUT_BLOCK = 2000; private static final long TIMEOUT_FILE = 10000; private static final long TIMEOUT_LINE = 1000; private static final long SLEEPTIME = 200; /** * Overlapping bytes when moving the sliding window searching area. * A value of 5000 essentially means that a regular expression can span * 5000 bytes (~50 lines) anywhere in the buildlog and still get a match. * Used when scanning for * {@link com.sonyericsson.jenkins.plugins.bfa.model.indication.MultilineBuildLogIndication}. * * Can never be larger than BUF_SIZE_BYTES. */ private static final int OVERLAP_BYTES = 5000; /** * The read buffer size for scanMultiLineOneFile(). This is also the size * of the total "search area" when moving the sliding window through * the buildlog. Used when scanning for * {@link com.sonyericsson.jenkins.plugins.bfa.model.indication.MultilineBuildLogIndication}. */ private static final int BUF_SIZE_BYTES = 15000; /** The indication we are looking for. */ protected Indication indication; /** * Standard constructor. * @param indication the indication to look for. */ public FailureReader(Indication indication) { this.indication = indication; } /** * Scans a build log. * * @param build - the build whose log should be scanned. * @return a FoundIndication if the pattern given by this FailureReader * is found in the log of the given build; return null otherwise. * @throws IOException if so. * @deprecated use {@link #scan(hudson.model.Run)}. */ @Deprecated public FoundIndication scan(AbstractBuild build) throws IOException { if (Util.isOverridden(FailureReader.class, getClass(), "scan", Run.class)) { return scan((Run)build); } return null; } /** * Scans a build log. * * @param build - the build whose log should be scanned. * @return a FoundIndication if the pattern given by this FailureReader * is found in the log of the given build; return null otherwise. * @throws IOException if so. */ public FoundIndication scan(Run build) throws IOException { if (Util.isOverridden(FailureReader.class, getClass(), "scan", AbstractBuild.class)) { return scan((AbstractBuild)build); } return null; } /** * Scans for indications of a failure cause. * @param build the build to scan for indications. * @param buildLog the log of the build. * @return a FoundIndication if something was found, null if not. * @deprecated Use {@link #scan(hudson.model.Run, java.io.PrintStream)}. */ @Deprecated public FoundIndication scan(AbstractBuild build, PrintStream buildLog) { if (Util.isOverridden(FailureReader.class, getClass(), "scan", Run.class, PrintStream.class)) { return scan((Run)build, buildLog); } return null; } /** * Scans for indications of a failure cause. * @param build the build to scan for indications. * @param buildLog the log of the build. * @return a FoundIndication if something was found, null if not. */ public FoundIndication scan(Run build, PrintStream buildLog) { if (Util.isOverridden(FailureReader.class, getClass(), "scan", AbstractBuild.class, PrintStream.class)) { return scan((AbstractBuild)build, buildLog); } return null; } /** * Checks all patterns one-by-one for entire file. * * @param causes list of failure causes that we a looking for. * @param build current build. * @param reader file reader. * @param currentFile file name. * @return found indications. * @throws IOException Exception. */ public static List<FoundFailureCause> scanSingleLinePatterns(List<FailureCause> causes, Run build, BufferedReader reader, String currentFile) throws IOException { TimerThread timerThread = new TimerThread(Thread.currentThread(), TIMEOUT_LINE); final long adjustedFileTimeout = TIMEOUT_FILE * getTotalNumberOfPatterns(causes); Map<FailureCause, List<FoundIndication>> resultMap = new HashMap<FailureCause, List<FoundIndication>>(); Map<FailureCause, List<Indication>> firstOccurrences = new HashMap<FailureCause, List<Indication>>(); timerThread.start(); try { long startTime = System.currentTimeMillis(); int currentLine = 1; String line; while ((line = reader.readLine()) != null) { for (FailureCause cause : causes) { for (Indication indication : cause.getIndications()) { try { List<Indication> wasBefore = firstOccurrences.get(cause); if (wasBefore == null || !wasBefore.contains(indication)) { if (processIndication(build, currentFile, resultMap, line, cause, indication)) { wasBefore = new ArrayList<Indication>(); wasBefore.add(indication); firstOccurrences.put(cause, wasBefore); } } } catch (RuntimeException e) { if (e.getCause() instanceof InterruptedException) { logger.warning("Timeout scanning for indication '" + indication.toString() + "'" + " for file " + currentFile + ":" + currentLine); } else { // This is not a timeout exception throw e; } } currentLine++; timerThread.touch(); if (System.currentTimeMillis() - startTime > adjustedFileTimeout) { logger.warning("File timeout scanning for indication '" + indication.toString() + "'" + " for file " + currentFile + ":" + currentLine); return convertToFoundFailureCauses(resultMap); } } } } return convertToFoundFailureCauses(resultMap); } finally { timerThread.requestStop(); timerThread.interrupt(); try { timerThread.join(); //CS IGNORE EmptyBlock FOR NEXT 2 LINES. REASON: unimportant exception } catch (InterruptedException eIgnore) { } // reset the interrupt Thread.interrupted(); } } /** * Calculates total number of patterns in list of causes. * * @param causes list of failure causes that we a looking for. * @return total number of patterns. */ private static int getTotalNumberOfPatterns(List<FailureCause> causes) { int total = 0; for (FailureCause cause : causes) { total += cause.getIndications().size(); } return total; } /** * * Updates map of found failure causes if pattern matches the line * * @param build current build * @param currentFile current file * @param causeIndicationsMap result map * @param line line with content * @param cause current cause * @param indication indication that should be checked * @return true if new indication was found */ private static boolean processIndication(Run build, String currentFile, Map<FailureCause, List<FoundIndication>> causeIndicationsMap, String line, FailureCause cause, Indication indication) { Pattern pattern = indication.getPattern(); if (pattern.matcher(new InterruptibleCharSequence(line)).matches()) { FoundIndication foundIndication = new FoundIndication( build, pattern.toString(), currentFile, ConsoleNote.removeNotes(line)); putToMapWithList(causeIndicationsMap, cause, foundIndication); return true; } return false; } /** * Put FoundIndication to List of according FailureCause * @param causeIndicationsMap result map * @param cause Failure cause that would be used as key * @param foundIndication Found indication that would be pushed to map */ private static void putToMapWithList(Map<FailureCause, List<FoundIndication>> causeIndicationsMap, FailureCause cause, FoundIndication foundIndication) { if (causeIndicationsMap.containsKey(cause)) { causeIndicationsMap.get(cause).add(foundIndication); } else { List<FoundIndication> foundIndications = new ArrayList<FoundIndication>(); foundIndications.add(foundIndication); causeIndicationsMap.put(cause, foundIndications); } } /** * Converts from a map with a FailureCause as key * and a list of FoundIndications as value * to a list of FoundFailureCauses * * @param causes input data * @return List of FoundFailureCauses that was generated from input data */ private static List<FoundFailureCause> convertToFoundFailureCauses(Map<FailureCause, List<FoundIndication>> causes) { List<FoundFailureCause> foundFailureCauses = new ArrayList<FoundFailureCause>(causes.size()); for (FailureCause failureCause : causes.keySet()) { foundFailureCauses.add(new FoundFailureCause(failureCause, causes.get(failureCause))); } return foundFailureCauses; } /** * Scans one file for the required multi-line pattern. * @param build the build we are processing. * @param reader the reader to read from. * @param currentFile the file path of the file we want to scan. * @return a FoundIndication if we find the pattern, null if not. * @throws IOException if problems occur in the reader handling. */ protected FoundIndication scanMultiLineOneFile(Run build, BufferedReader reader, String currentFile) throws IOException { TimerThread timerThread = new TimerThread(Thread.currentThread(), TIMEOUT_BLOCK); FoundIndication foundIndication = null; final Pattern pattern = indication.getPattern(); timerThread.start(); try { long startTime = System.currentTimeMillis(); char[] buf = new char[BUF_SIZE_BYTES]; StringBuilder searchBuffer = new StringBuilder(); int read; boolean firstRead = true; //CS IGNORE AvoidInlineConditionals FOR NEXT 1 LINES. REASON: Split up makes code less reasable. while ((read = reader.read(buf, 0, BUF_SIZE_BYTES - (firstRead ? 0 : OVERLAP_BYTES))) != -1) { try { firstRead = false; searchBuffer.append(buf, 0, read); Matcher matcher = pattern.matcher(new InterruptibleCharSequence(searchBuffer.toString())); if (matcher.find()) { foundIndication = new FoundIndication(build, pattern.pattern(), currentFile, removeConsoleNotes(matcher.group())); break; } searchBuffer.delete(0, BUF_SIZE_BYTES - OVERLAP_BYTES); } catch (RuntimeException e) { if (e.getCause() instanceof InterruptedException) { logger.warning("Timeout scanning for indication '" + indication.toString() + "' for file " + currentFile); } else { // This is not a timeout exception throw e; } } timerThread.touch(); if (System.currentTimeMillis() - startTime > TIMEOUT_FILE) { logger.warning("File timeout scanning for indication '" + indication.toString() + "' for file " + currentFile); break; } } return foundIndication; } finally { timerThread.requestStop(); timerThread.interrupt(); try { timerThread.join(); //CS IGNORE EmptyBlock FOR NEXT 2 LINES. REASON: unimportant exception } catch (InterruptedException eIgnore) { } // reset the interrupt Thread.interrupted(); } } /** * @param input the input string from which to remove any console notes * @return the input string less console notes. Note the returned string may not contain the same line endings * as the input string. */ private String removeConsoleNotes(final String input) { final List<String> cleanLines = new LinkedList<String>(); final Scanner lineTokenizer = new Scanner(input); try { lineTokenizer.useDelimiter(Pattern.compile("[\\n\\r]")); while (lineTokenizer.hasNext()) { cleanLines.add(ConsoleNote.removeNotes(lineTokenizer.next())); } } finally { lineTokenizer.close(); } return Joiner.on('\n').join(cleanLines); } /** * CharSequence that notices thread interrupts -- as might be necessary * to recover from a loose regex on unexpected challenging input. */ public static class InterruptibleCharSequence implements CharSequence { CharSequence inner; /** * Standard constructor. * @param inner the CharSequence to be able to interrupt. */ public InterruptibleCharSequence(CharSequence inner) { super(); this.inner = inner.toString(); } @Override public char charAt(int index) { if (Thread.interrupted()) { // clears flag if set throw new RuntimeException(new InterruptedException()); } return inner.charAt(index); } @Override public int length() { return inner.length(); } @Override public CharSequence subSequence(int start, int end) { return new InterruptibleCharSequence(inner.subSequence(start, end)); } @Override public String toString() { return inner.toString(); } } /** * TimerThread interrupting a monitored thread unless TimerThread is touched within the * specified timeout value */ static class TimerThread extends Thread { private Thread monitorThread; private boolean stop = false; private long timeout; private long lastTouched; /** * Standard constructor. * @param monitorThread The thread to monitor and interrupt after the timeout. * @param timeout The timeout in ms. */ TimerThread(Thread monitorThread, long timeout) { this.monitorThread = monitorThread; this.timeout = timeout; } @Override public void run() { lastTouched = System.currentTimeMillis(); while (!stop) { try { Thread.sleep(SLEEPTIME); if (System.currentTimeMillis() - lastTouched >= timeout) { monitorThread.interrupt(); // timeout met, interrupt the launcherThread } //CS IGNORE EmptyBlock FOR NEXT 5 LINES. REASON: timeout exception } catch (InterruptedException eRestartSleep) { // My thread was interrupted so continue loop and // check if I'm stopped and otherwise just restart sleep } } } /** * Touch, i.e. reset countdown timer. */ public void touch() { lastTouched = System.currentTimeMillis(); } /** * Set stop flag to stop executing */ public void requestStop() { stop = true; } } }