Exemple #1
0
        /// <summary>
        /// Adds a new build history for a stream
        /// </summary>
        /// <param name="Issue">The issue to add a build to</param>
        /// <param name="InputJob">The job containing the error</param>
        /// <param name="InputJobStep">The job step containing the error</param>
        /// <param name="InputErrorUrl">Url of the error</param>
        /// <param name="State">Current persistent state. Used to find previous build history.</param>
        void AddFailureToIssue(BuildHealthIssue Issue, InputJob InputJob, InputJobStep InputJobStep, string InputErrorUrl, BuildHealthState State)
        {
            // Find or add a step name to history mapping
            Dictionary <string, BuildHealthJobHistory> StepNameToHistory;

            if (!Issue.Streams.TryGetValue(InputJob.Stream, out StepNameToHistory))
            {
                StepNameToHistory = new Dictionary <string, BuildHealthJobHistory>();
                Issue.Streams.Add(InputJob.Stream, StepNameToHistory);
            }

            // Find or add a history for this step
            BuildHealthJobHistory History;

            if (!StepNameToHistory.TryGetValue(InputJobStep.Name, out History))
            {
                History = new BuildHealthJobHistory(State.FindBuildBefore(InputJob.Stream, InputJob.Change, InputJobStep.Name));
                StepNameToHistory.Add(InputJobStep.Name, History);
            }

            // Add the new build
            History.AddFailedBuild(CreateBuildForJobStep(InputJob, InputJobStep, InputErrorUrl));
        }
Exemple #2
0
        /// <summary>
        /// Main command entry point
        /// </summary>
        /// <param name="Arguments">The command line arguments</param>
        public override void Exec(CommandLineArguments Arguments)
        {
            // Parse the arguments
            bool               bClean           = Arguments.HasOption("-Clean");
            string             PerforcePort     = Arguments.GetStringOrDefault("-P4Port=", null);
            string             PerforceUser     = Arguments.GetStringOrDefault("-P4User="******"-InputFile=", null);
            FileReference      StateFile        = Arguments.GetFileReference("-StateFile=");
            string             ServerUrl        = Arguments.GetStringOrDefault("-Server=", null);
            bool               bKeepHistory     = Arguments.HasOption("-KeepHistory");
            bool               bReadOnly        = Arguments.HasOption("-ReadOnly");
            DirectoryReference SaveUnmatchedDir = Arguments.GetDirectoryReferenceOrDefault("-SaveUnmatched=", null);

            Arguments.CheckAllArgumentsUsed();

            // Build a mapping from category to matching
            Dictionary <string, PatternMatcher> CategoryNameToMatcher = new Dictionary <string, PatternMatcher>();

            foreach (PatternMatcher Matcher in Matchers)
            {
                CategoryNameToMatcher[Matcher.Category] = Matcher;
            }

            // Complete any interrupted operation to update the state file
            CompleteStateTransaction(StateFile);

            // Read the persistent data file
            BuildHealthState State;

            if (!bClean && FileReference.Exists(StateFile))
            {
                Log.TraceInformation("Reading persistent data from {0}", StateFile);
                State = DeserializeJson <BuildHealthState>(StateFile);
            }
            else
            {
                Log.TraceInformation("Creating new persistent data");
                State = new BuildHealthState();
            }

            // Fixup any issues loaded from disk
            foreach (BuildHealthIssue Issue in State.Issues)
            {
                if (Issue.References == null)
                {
                    Issue.References = new SortedSet <string>();
                }
            }

            // Create the Perforce connection
            PerforceConnection Perforce = new PerforceConnection(PerforcePort, PerforceUser, null);

            // Process the input data
            if (InputFile != null)
            {
                // Parse the input file
                Log.TraceInformation("Reading build results from {0}", InputFile);
                InputData InputData = DeserializeJson <InputData>(InputFile);

                // Parse all the builds and add them to the persistent data
                List <InputJob> InputJobs = InputData.Jobs.OrderBy(x => x.Change).ThenBy(x => x.Stream).ToList();
                Stopwatch       Timer     = Stopwatch.StartNew();
                foreach (InputJob InputJob in InputJobs)
                {
                    // Add a new build for each job step
                    foreach (InputJobStep InputJobStep in InputJob.Steps)
                    {
                        BuildHealthJobStep NewBuild = new BuildHealthJobStep(InputJob.Change, InputJob.Name, InputJob.Url, InputJobStep.Name, InputJobStep.Url, null);
                        State.AddBuild(InputJob.Stream, NewBuild);
                    }

                    // Add all the job steps
                    List <InputJobStep> InputJobSteps = InputJob.Steps.OrderBy(x => x.Name).ToList();
                    foreach (InputJobStep InputJobStep in InputJobSteps)
                    {
                        if (InputJobStep.Diagnostics != null && InputJobStep.Diagnostics.Count > 0)
                        {
                            AddStep(Perforce, State, InputJob, InputJobStep);
                        }
                    }

                    // Remove any steps which are empty
                    InputJob.Steps.RemoveAll(x => x.Diagnostics == null || x.Diagnostics.Count == 0);
                }
                InputJobs.RemoveAll(x => x.Steps.Count == 0);
                Log.TraceInformation("Added jobs in {0}s", Timer.Elapsed.TotalSeconds);

                // If there are any unmatched issues, save out the current state and remaining input
                if (SaveUnmatchedDir != null && InputJobs.Count > 0)
                {
                    DirectoryReference.CreateDirectory(SaveUnmatchedDir);
                    if (FileReference.Exists(StateFile))
                    {
                        FileReference.Copy(StateFile, FileReference.Combine(SaveUnmatchedDir, "State.json"), true);
                    }
                    SerializeJson(FileReference.Combine(SaveUnmatchedDir, "Input.json"), InputData);
                }

                // Try to find the next successful build for each stream, so we can close it as part of updating the server
                for (int Idx = 0; Idx < State.Issues.Count; Idx++)
                {
                    BuildHealthIssue Issue = State.Issues[Idx];
                    foreach (string Stream in Issue.Streams.Keys)
                    {
                        Dictionary <string, BuildHealthJobHistory> StepNameToHistory = Issue.Streams[Stream];
                        foreach (string StepName in StepNameToHistory.Keys)
                        {
                            BuildHealthJobHistory IssueHistory = StepNameToHistory[StepName];
                            if (IssueHistory.FailedBuilds.Count > 0 && IssueHistory.NextSuccessfulBuild == null)
                            {
                                // Find the successful build after this change
                                BuildHealthJobStep LastFailedBuild = IssueHistory.FailedBuilds[IssueHistory.FailedBuilds.Count - 1];
                                IssueHistory.NextSuccessfulBuild = State.FindBuildAfter(Stream, LastFailedBuild.Change, StepName);
                            }
                        }
                    }
                }

                // Find the change two days before the latest change being added
                if (InputData.Jobs.Count > 0 && !bKeepHistory)
                {
                    // Find all the unique change numbers for each stream
                    SortedSet <int> ChangeNumbers = new SortedSet <int>();
                    foreach (List <BuildHealthJobStep> Builds in State.Streams.Values)
                    {
                        ChangeNumbers.UnionWith(Builds.Select(x => x.Change));
                    }

                    // Get the latest change record
                    int          LatestChangeNumber = InputData.Jobs.Min(x => x.Change);
                    ChangeRecord LatestChangeRecord = Perforce.GetChange(GetChangeOptions.None, LatestChangeNumber).Data;

                    // Step forward through all the changelists until we get to one we don't want to delete
                    int DeleteChangeNumber = -1;
                    foreach (int ChangeNumber in ChangeNumbers)
                    {
                        ChangeRecord ChangeRecord = Perforce.GetChange(GetChangeOptions.None, ChangeNumber).Data;
                        if (ChangeRecord.Date > LatestChangeRecord.Date - TimeSpan.FromDays(2))
                        {
                            break;
                        }
                        DeleteChangeNumber = ChangeNumber;
                    }

                    // Remove any builds we no longer want to track
                    foreach (List <BuildHealthJobStep> Builds in State.Streams.Values)
                    {
                        Builds.RemoveAll(x => x.Change <= DeleteChangeNumber);
                    }
                }
            }

            // Mark any issues as resolved
            foreach (BuildHealthIssue Issue in State.Issues)
            {
                if (Issue.IsResolved())
                {
                    if (!Issue.ResolvedAt.HasValue)
                    {
                        Issue.ResolvedAt = DateTime.UtcNow;
                    }
                }
                else
                {
                    if (Issue.ResolvedAt.HasValue)
                    {
                        Issue.ResolvedAt = null;
                    }
                }
            }

            // If we're in read-only mode, don't write anything out
            if (bReadOnly)
            {
                return;
            }

            // Save the persistent data
            Log.TraceInformation("Writing persistent data to {0}", StateFile);
            DirectoryReference.CreateDirectory(StateFile.Directory);
            WriteState(StateFile, State);

            // Synchronize with the server
            if (ServerUrl != null)
            {
                // Post any issue updates
                foreach (BuildHealthIssue Issue in State.Issues)
                {
                    PatternMatcher Matcher;
                    if (!CategoryNameToMatcher.TryGetValue(Issue.Category, out Matcher))
                    {
                        continue;
                    }

                    string Summary = Matcher.GetSummary(Issue);
                    if (Issue.Id == -1)
                    {
                        Log.TraceInformation("Adding issue: {0}", Issue);

                        if (Issue.PendingWatchers.Count == 0)
                        {
                            Log.TraceWarning("(No possible causers)");
                        }

                        CommandTypes.AddIssue IssueBody = new CommandTypes.AddIssue();
                        IssueBody.Project = Issue.Project;
                        IssueBody.Summary = Summary;

                        if (Issue.PendingWatchers.Count == 1)
                        {
                            IssueBody.Owner = Issue.PendingWatchers.First();
                        }

                        using (HttpWebResponse Response = SendHttpRequest(String.Format("{0}/api/issues", ServerUrl), "POST", IssueBody))
                        {
                            int ResponseCode = (int)Response.StatusCode;
                            if (!(ResponseCode >= 200 && ResponseCode <= 299))
                            {
                                throw new Exception("Unable to add issue");
                            }
                            Issue.Id = ParseHttpResponse <CommandTypes.AddIssueResponse>(Response).Id;
                        }

                        Issue.PostedSummary = Summary;
                        WriteState(StateFile, State);
                    }
                    else if (Issue.PostedSummary == null || !String.Equals(Issue.PostedSummary, Summary, StringComparison.Ordinal))
                    {
                        Log.TraceInformation("Updating issue {0}", Issue.Id);

                        CommandTypes.UpdateIssue IssueBody = new CommandTypes.UpdateIssue();
                        IssueBody.Summary = Summary;

                        using (HttpWebResponse Response = SendHttpRequest(String.Format("{0}/api/issues/{1}", ServerUrl, Issue.Id), "PUT", IssueBody))
                        {
                            int ResponseCode = (int)Response.StatusCode;
                            if (!(ResponseCode >= 200 && ResponseCode <= 299))
                            {
                                throw new Exception("Unable to add issue");
                            }
                        }

                        Issue.PostedSummary = Summary;
                        WriteState(StateFile, State);
                    }
                }

                // Add any new builds associated with issues
                Dictionary <string, long> JobStepUrlToId = new Dictionary <string, long>(StringComparer.Ordinal);
                foreach (BuildHealthIssue Issue in State.Issues)
                {
                    foreach (KeyValuePair <string, Dictionary <string, BuildHealthJobHistory> > StreamPair in Issue.Streams)
                    {
                        foreach (BuildHealthJobHistory StreamHistory in StreamPair.Value.Values)
                        {
                            foreach (BuildHealthJobStep Build in StreamHistory.Builds)
                            {
                                if (!Build.bPostedToServer)
                                {
                                    Log.TraceInformation("Adding {0} to issue {1}", Build.JobStepUrl, Issue.Id);

                                    CommandTypes.AddBuild AddBuild = new CommandTypes.AddBuild();
                                    AddBuild.Stream      = StreamPair.Key;
                                    AddBuild.Change      = Build.Change;
                                    AddBuild.JobName     = Build.JobName;
                                    AddBuild.JobUrl      = Build.JobUrl;
                                    AddBuild.JobStepName = Build.JobStepName;
                                    AddBuild.JobStepUrl  = Build.JobStepUrl;
                                    AddBuild.ErrorUrl    = Build.ErrorUrl;
                                    AddBuild.Outcome     = (Build == StreamHistory.PrevSuccessfulBuild || Build == StreamHistory.NextSuccessfulBuild)? CommandTypes.Outcome.Success : CommandTypes.Outcome.Error;

                                    using (HttpWebResponse Response = SendHttpRequest(String.Format("{0}/api/issues/{1}/builds", ServerUrl, Issue.Id), "POST", AddBuild))
                                    {
                                        int ResponseCode = (int)Response.StatusCode;
                                        if (!(ResponseCode >= 200 && ResponseCode <= 299))
                                        {
                                            throw new Exception("Unable to add build");
                                        }
                                        Build.Id = ParseHttpResponse <CommandTypes.AddBuildResponse>(Response).Id;
                                    }

                                    Build.bPostedToServer = true;
                                    WriteState(StateFile, State);
                                }
                                if (Build.Id != -1)
                                {
                                    JobStepUrlToId[Build.JobStepUrl] = Build.Id;
                                }
                            }
                        }
                    }
                }

                // Add any new diagnostics
                foreach (BuildHealthIssue Issue in State.Issues)
                {
                    foreach (BuildHealthDiagnostic Diagnostic in Issue.Diagnostics)
                    {
                        if (!Diagnostic.bPostedToServer)
                        {
                            string Summary = Diagnostic.Message;

                            const int MaxLength = 40;
                            if (Summary.Length > MaxLength)
                            {
                                Summary = Summary.Substring(0, MaxLength).TrimEnd();
                            }

                            Log.TraceInformation("Adding diagnostic '{0}' to issue {1}", Summary, Issue.Id);

                            CommandTypes.AddDiagnostic AddDiagnostic = new CommandTypes.AddDiagnostic();

                            long BuildId;
                            if (Diagnostic.JobStepUrl != null && JobStepUrlToId.TryGetValue(Diagnostic.JobStepUrl, out BuildId))
                            {
                                AddDiagnostic.BuildId = BuildId;
                            }
                            else
                            {
                                Console.WriteLine("ERROR");
                            }

                            AddDiagnostic.Message = Diagnostic.Message;
                            AddDiagnostic.Url     = Diagnostic.ErrorUrl;

                            using (HttpWebResponse Response = SendHttpRequest(String.Format("{0}/api/issues/{1}/diagnostics", ServerUrl, Issue.Id), "POST", AddDiagnostic))
                            {
                                int ResponseCode = (int)Response.StatusCode;
                                if (!(ResponseCode >= 200 && ResponseCode <= 299))
                                {
                                    throw new Exception("Unable to add build");
                                }
                            }

                            Diagnostic.bPostedToServer = true;
                            WriteState(StateFile, State);
                        }
                    }
                }

                // Close any issues which are complete
                for (int Idx = 0; Idx < State.Issues.Count; Idx++)
                {
                    BuildHealthIssue Issue = State.Issues[Idx];
                    if (Issue.ResolvedAt.HasValue != Issue.bPostedResolved)
                    {
                        Log.TraceInformation("Setting issue {0} resolved flag to {1}", Issue.Id, Issue.ResolvedAt.HasValue);

                        CommandTypes.UpdateIssue UpdateBody = new CommandTypes.UpdateIssue();
                        UpdateBody.Resolved = Issue.ResolvedAt.HasValue;

                        using (HttpWebResponse Response = SendHttpRequest(String.Format("{0}/api/issues/{1}", ServerUrl, Issue.Id), "PUT", UpdateBody))
                        {
                            int ResponseCode = (int)Response.StatusCode;
                            if (!(ResponseCode >= 200 && ResponseCode <= 299))
                            {
                                throw new Exception("Unable to delete issue");
                            }
                        }

                        Issue.bPostedResolved = Issue.ResolvedAt.HasValue;
                        WriteState(StateFile, State);
                    }
                }

                // Update watchers on any open builds
                foreach (BuildHealthIssue Issue in State.Issues)
                {
                    while (Issue.PendingWatchers.Count > 0)
                    {
                        CommandTypes.Watcher Watcher = new CommandTypes.Watcher();
                        Watcher.UserName = Issue.PendingWatchers.First();

                        using (HttpWebResponse Response = SendHttpRequest(String.Format("{0}/api/issues/{1}/watchers", ServerUrl, Issue.Id), "POST", Watcher))
                        {
                            int ResponseCode = (int)Response.StatusCode;
                            if (!(ResponseCode >= 200 && ResponseCode <= 299))
                            {
                                throw new Exception("Unable to add watcher");
                            }
                        }

                        Issue.PendingWatchers.Remove(Watcher.UserName);
                        Issue.Watchers.Add(Watcher.UserName);

                        WriteState(StateFile, State);
                    }
                }
            }

            // Remove any issues which have been resolved for 24 hours. We have to keep information about issues that have been fixed for some time; we may be updating the same job
            // multiple times while other steps are running, and we don't want to keep opening new issues for it. Also, it can take time for changes to propagate between streams.
            DateTime RemoveIssueTime = DateTime.UtcNow - TimeSpan.FromHours(24.0);

            for (int Idx = 0; Idx < State.Issues.Count; Idx++)
            {
                BuildHealthIssue Issue = State.Issues[Idx];
                if (Issue.ResolvedAt.HasValue && Issue.ResolvedAt.Value < RemoveIssueTime)
                {
                    State.Issues.RemoveAt(Idx--);
                    WriteState(StateFile, State);
                    continue;
                }
            }

            // TODO: VERIFY ISSUES ARE CLOSED
        }