PageRenderTime 3ms CodeModel.GetById 2ms app.highlight 42ms RepoModel.GetById 1ms app.codeStats 1ms

/Plugins/BuildServerIntegration/TeamCityIntegration/TeamCityAdapter.cs

http://github.com/spdr870/gitextensions
C# | 620 lines | 528 code | 87 blank | 5 comment | 56 complexity | d1918a2b69a899c4d5f0cd2ee9faa614 MD5 | raw file
  1using System;
  2using System.Collections.Generic;
  3using System.ComponentModel.Composition;
  4using System.Diagnostics;
  5using System.Globalization;
  6using System.IO;
  7using System.Linq;
  8using System.Net;
  9using System.Net.Http;
 10using System.Net.Http.Headers;
 11using System.Reactive.Concurrency;
 12using System.Reactive.Linq;
 13using System.Text;
 14using System.Text.RegularExpressions;
 15using System.Threading;
 16using System.Threading.Tasks;
 17using System.Xml.Linq;
 18using System.Xml.XPath;
 19using GitCommands.Utils;
 20using GitUI;
 21using GitUIPluginInterfaces;
 22using GitUIPluginInterfaces.BuildServerIntegration;
 23using JetBrains.Annotations;
 24using Microsoft.VisualStudio.Threading;
 25
 26namespace TeamCityIntegration
 27{
 28    [MetadataAttribute]
 29    [AttributeUsage(AttributeTargets.Class)]
 30    public class TeamCityIntegrationMetadataAttribute : BuildServerAdapterMetadataAttribute
 31    {
 32        public TeamCityIntegrationMetadataAttribute(string buildServerType)
 33            : base(buildServerType)
 34        {
 35        }
 36
 37        public override string CanBeLoaded
 38        {
 39            get
 40            {
 41                if (EnvUtils.IsNet4FullOrHigher())
 42                {
 43                    return null;
 44                }
 45                else
 46                {
 47                    return ".Net 4 full framework required";
 48                }
 49            }
 50        }
 51    }
 52
 53    [Export(typeof(IBuildServerAdapter))]
 54    [TeamCityIntegrationMetadata(PluginName)]
 55    [PartCreationPolicy(CreationPolicy.NonShared)]
 56    internal class TeamCityAdapter : IBuildServerAdapter
 57    {
 58        public const string PluginName = "TeamCity";
 59        private IBuildServerWatcher _buildServerWatcher;
 60
 61        private HttpClientHandler _httpClientHandler;
 62        private HttpClient _httpClient;
 63
 64        private string _httpClientHostSuffix;
 65
 66        private readonly List<JoinableTask<IEnumerable<string>>> _getBuildTypesTask = new List<JoinableTask<IEnumerable<string>>>();
 67
 68        private CookieContainer _teamCityNtlmAuthCookie;
 69
 70        private string HostName { get; set; }
 71
 72        private string[] ProjectNames { get; set; }
 73
 74        private Regex BuildIdFilter { get; set; }
 75
 76        private CookieContainer GetTeamCityNtlmAuthCookie(string serverUrl, IBuildServerCredentials buildServerCredentials)
 77        {
 78            if (_teamCityNtlmAuthCookie != null)
 79            {
 80                return _teamCityNtlmAuthCookie;
 81            }
 82
 83            string url = serverUrl + "ntlmLogin.html";
 84            var cookieContainer = new CookieContainer();
 85            var request = (HttpWebRequest)WebRequest.Create(url);
 86            request.CookieContainer = cookieContainer;
 87
 88            if (buildServerCredentials != null
 89                && !string.IsNullOrEmpty(buildServerCredentials.Username)
 90                && !string.IsNullOrEmpty(buildServerCredentials.Password))
 91            {
 92                request.Credentials = new NetworkCredential(buildServerCredentials.Username, buildServerCredentials.Password);
 93            }
 94            else
 95            {
 96                request.Credentials = CredentialCache.DefaultCredentials;
 97            }
 98
 99            request.PreAuthenticate = true;
100            request.GetResponse();
101
102            _teamCityNtlmAuthCookie = cookieContainer;
103            return _teamCityNtlmAuthCookie;
104        }
105
106        public string LogAsGuestUrlParameter { get; set; }
107
108        public void Initialize(IBuildServerWatcher buildServerWatcher, ISettingsSource config, Func<ObjectId, bool> isCommitInRevisionGrid = null)
109        {
110            if (_buildServerWatcher != null)
111            {
112                throw new InvalidOperationException("Already initialized");
113            }
114
115            _buildServerWatcher = buildServerWatcher;
116
117            ProjectNames = buildServerWatcher.ReplaceVariables(config.GetString("ProjectName", ""))
118                .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
119
120            var buildIdFilerSetting = config.GetString("BuildIdFilter", "");
121            if (!BuildServerSettingsHelper.IsRegexValid(buildIdFilerSetting))
122            {
123                return;
124            }
125
126            BuildIdFilter = new Regex(buildIdFilerSetting, RegexOptions.Compiled);
127            HostName = config.GetString("BuildServerUrl", null);
128            LogAsGuestUrlParameter = config.GetBool("LogAsGuest", false) ? "&guest=1" : string.Empty;
129
130            if (!string.IsNullOrEmpty(HostName))
131            {
132                InitializeHttpClient(HostName);
133                if (ProjectNames.Length > 0)
134                {
135                    _getBuildTypesTask.Clear();
136                    foreach (var name in ProjectNames)
137                    {
138                        _getBuildTypesTask.Add(ThreadHelper.JoinableTaskFactory.RunAsync(async () =>
139                        {
140                            var response = await GetProjectFromNameXmlResponseAsync(name, CancellationToken.None).ConfigureAwait(false);
141                            return from element in response.XPathSelectElements("/project/buildTypes/buildType")
142                                   select element.Attribute("id").Value;
143                        }));
144                    }
145                }
146            }
147        }
148
149        public void InitializeHttpClient(string hostname)
150        {
151            CreateNewHttpClient(hostname);
152            UpdateHttpClientOptionsGuestAuth();
153        }
154
155        private void CreateNewHttpClient(string hostName)
156        {
157            _httpClientHandler = new HttpClientHandler();
158            _httpClient = new HttpClient(_httpClientHandler)
159            {
160                Timeout = TimeSpan.FromMinutes(2),
161                BaseAddress = hostName.Contains("://")
162                    ? new Uri(hostName, UriKind.Absolute)
163                    : new Uri(string.Format("{0}://{1}", Uri.UriSchemeHttp, hostName), UriKind.Absolute)
164            };
165            _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
166        }
167
168        /// <summary>
169        /// Gets a unique key which identifies this build server.
170        /// </summary>
171        public string UniqueKey => _httpClient.BaseAddress.Host;
172
173        public IObservable<BuildInfo> GetFinishedBuildsSince(IScheduler scheduler, DateTime? sinceDate = null)
174        {
175            return GetBuilds(scheduler, sinceDate, false);
176        }
177
178        public IObservable<BuildInfo> GetRunningBuilds(IScheduler scheduler)
179        {
180            return GetBuilds(scheduler, null, true);
181        }
182
183        public IObservable<BuildInfo> GetBuilds(IScheduler scheduler, DateTime? sinceDate = null, bool? running = null)
184        {
185            if (_httpClient == null || _httpClient.BaseAddress == null || ProjectNames.Length == 0)
186            {
187                return Observable.Empty<BuildInfo>(scheduler);
188            }
189
190            return Observable.Create<BuildInfo>((observer, cancellationToken) =>
191                Task.Run(
192                    () => scheduler.Schedule(() => ObserveBuilds(sinceDate, running, observer, cancellationToken))));
193        }
194
195        private void ObserveBuilds(DateTime? sinceDate, bool? running, IObserver<BuildInfo> observer, CancellationToken cancellationToken)
196        {
197            try
198            {
199                if (_getBuildTypesTask.Any(task => PropagateTaskAnomalyToObserver(task.Task, observer)))
200                {
201                    return;
202                }
203
204                var localObserver = observer;
205                var buildTypes = _getBuildTypesTask.SelectMany(t => t.Join()).Where(id => BuildIdFilter.IsMatch(id));
206                var buildIdTasks = buildTypes.Select(buildTypeId => GetFilteredBuildsXmlResponseAsync(buildTypeId, cancellationToken, sinceDate, running)).ToArray();
207
208                Task.Factory
209                    .ContinueWhenAll(
210                        buildIdTasks,
211                        completedTasks =>
212                            {
213                                var buildIds = completedTasks.Where(task => task.Status == TaskStatus.RanToCompletion)
214                                                             .SelectMany(
215                                                                 buildIdTask =>
216                                                                 buildIdTask.CompletedResult()
217                                                                            .XPathSelectElements("/builds/build")
218                                                                            .Select(x => x.Attribute("id").Value))
219                                                             .ToArray();
220
221                                NotifyObserverOfBuilds(buildIds, observer, cancellationToken);
222                            },
223                        cancellationToken,
224                        TaskContinuationOptions.AttachedToParent | TaskContinuationOptions.ExecuteSynchronously,
225                        TaskScheduler.Current)
226                    .ContinueWith(
227                        task => localObserver.OnError(task.Exception),
228                        CancellationToken.None,
229                        TaskContinuationOptions.AttachedToParent | TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnFaulted,
230                        TaskScheduler.Current);
231            }
232            catch (OperationCanceledException)
233            {
234                // Do nothing, the observer is already stopped
235            }
236            catch (Exception ex)
237            {
238                observer.OnError(ex);
239            }
240        }
241
242        private void NotifyObserverOfBuilds(string[] buildIds, IObserver<BuildInfo> observer, CancellationToken cancellationToken)
243        {
244            var tasks = new List<Task>(8);
245            var buildsLeft = buildIds.Length;
246
247            foreach (var buildId in buildIds.OrderByDescending(int.Parse))
248            {
249                var notifyObserverTask =
250                    GetBuildFromIdXmlResponseAsync(buildId, cancellationToken)
251                        .ContinueWith(
252                            task =>
253                                {
254                                    if (task.Status == TaskStatus.RanToCompletion)
255                                    {
256                                        var buildDetails = task.CompletedResult();
257                                        var buildInfo = CreateBuildInfo(buildDetails);
258                                        if (buildInfo.CommitHashList.Any())
259                                        {
260                                            observer.OnNext(buildInfo);
261                                        }
262                                    }
263                                },
264                            cancellationToken,
265                            TaskContinuationOptions.AttachedToParent | TaskContinuationOptions.ExecuteSynchronously,
266                            TaskScheduler.Current);
267
268                tasks.Add(notifyObserverTask);
269                --buildsLeft;
270
271                if (tasks.Count == tasks.Capacity || buildsLeft == 0)
272                {
273                    var batchTasks = tasks.ToArray();
274                    tasks.Clear();
275
276                    try
277                    {
278                        Task.WaitAll(batchTasks, cancellationToken);
279                    }
280                    catch (Exception e)
281                    {
282                        observer.OnError(e);
283                        return;
284                    }
285                }
286            }
287
288            observer.OnCompleted();
289        }
290
291        private static bool PropagateTaskAnomalyToObserver(Task task, IObserver<BuildInfo> observer)
292        {
293            if (task.IsCanceled)
294            {
295                observer.OnCompleted();
296                return true;
297            }
298
299            if (task.IsFaulted)
300            {
301                Debug.Assert(task.Exception != null, "task.Exception != null");
302
303                observer.OnError(task.Exception);
304                return true;
305            }
306
307            return false;
308        }
309
310        private BuildInfo CreateBuildInfo(XDocument buildXmlDocument)
311        {
312            var buildXElement = buildXmlDocument.Element("build");
313            var idValue = buildXElement.Attribute("id").Value;
314            var statusValue = buildXElement.Attribute("status").Value;
315            var startDateText = buildXElement.Element("startDate").Value;
316            var statusText = buildXElement.Element("statusText").Value;
317            var webUrl = buildXElement.Attribute("webUrl").Value + LogAsGuestUrlParameter;
318            var revisionsElements = buildXElement.XPathSelectElements("revisions/revision");
319            var commitHashList = revisionsElements.Select(x => ObjectId.Parse(x.Attribute("version").Value)).ToList();
320            var runningAttribute = buildXElement.Attribute("running");
321
322            if (runningAttribute != null && Convert.ToBoolean(runningAttribute.Value))
323            {
324                var runningInfoXElement = buildXElement.Element("running-info");
325                var currentStageText = runningInfoXElement.Attribute("currentStageText").Value;
326
327                statusText = currentStageText;
328            }
329
330            var buildInfo = new BuildInfo
331            {
332                Id = idValue,
333                StartDate = DecodeJsonDateTime(startDateText),
334                Status = ParseBuildStatus(statusValue),
335                Description = statusText,
336                CommitHashList = commitHashList,
337                Url = webUrl
338            };
339            return buildInfo;
340        }
341
342        private static BuildInfo.BuildStatus ParseBuildStatus(string statusValue)
343        {
344            switch (statusValue)
345            {
346                case "SUCCESS":
347                    return BuildInfo.BuildStatus.Success;
348                case "FAILURE":
349                    return BuildInfo.BuildStatus.Failure;
350                default:
351                    return BuildInfo.BuildStatus.Unknown;
352            }
353        }
354
355        private Task<Stream> GetStreamAsync(string restServicePath, CancellationToken cancellationToken)
356        {
357            cancellationToken.ThrowIfCancellationRequested();
358
359            return _httpClient.GetAsync(FormatRelativePath(restServicePath), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
360                             .ContinueWith(
361                                 task => GetStreamFromHttpResponseAsync(task, restServicePath, cancellationToken),
362                                 cancellationToken,
363                                 TaskContinuationOptions.AttachedToParent,
364                                 TaskScheduler.Current)
365                             .Unwrap();
366        }
367
368        private Task<Stream> GetStreamFromHttpResponseAsync(Task<HttpResponseMessage> task, string restServicePath, CancellationToken cancellationToken)
369        {
370            if (!task.IsCompleted)
371            {
372                throw new InvalidOperationException($"Task in state '{task.Status}' was expected to be completed.");
373            }
374
375            bool retry = task.IsCanceled && !cancellationToken.IsCancellationRequested;
376            bool unauthorized = task.Status == TaskStatus.RanToCompletion &&
377                                (task.CompletedResult().StatusCode == HttpStatusCode.Unauthorized || task.CompletedResult().StatusCode == HttpStatusCode.Forbidden);
378
379            if (!retry)
380            {
381                if (task.CompletedResult().IsSuccessStatusCode)
382                {
383                    var httpContent = task.CompletedResult().Content;
384
385                    if (httpContent.Headers.ContentType.MediaType == "text/html")
386                    {
387                        // TeamCity responds with an HTML login page when guest access is denied.
388                        unauthorized = true;
389                    }
390                    else
391                    {
392                        return httpContent.ReadAsStreamAsync();
393                    }
394                }
395            }
396
397            if (retry)
398            {
399                return GetStreamAsync(restServicePath, cancellationToken);
400            }
401
402            if (unauthorized)
403            {
404                var buildServerCredentials = _buildServerWatcher.GetBuildServerCredentials(this, true);
405                var useBuildServerCredentials = buildServerCredentials != null
406                                                && !buildServerCredentials.UseGuestAccess
407                                                && (string.IsNullOrWhiteSpace(buildServerCredentials.Username) && string.IsNullOrWhiteSpace(buildServerCredentials.Password));
408                if (useBuildServerCredentials)
409                {
410                    UpdateHttpClientOptionsCredentialsAuth(buildServerCredentials);
411                    return GetStreamAsync(restServicePath, cancellationToken);
412                }
413                else
414                {
415                    UpdateHttpClientOptionsNtlmAuth(buildServerCredentials);
416                    return GetStreamAsync(restServicePath, cancellationToken);
417                }
418            }
419
420            throw new HttpRequestException(task.CompletedResult().ReasonPhrase);
421        }
422
423        public void UpdateHttpClientOptionsNtlmAuth(IBuildServerCredentials buildServerCredentials)
424        {
425            try
426            {
427                _httpClient.Dispose();
428                _httpClientHandler.Dispose();
429
430                _httpClientHostSuffix = "httpAuth";
431                CreateNewHttpClient(HostName);
432                _httpClientHandler.CookieContainer = GetTeamCityNtlmAuthCookie(_httpClient.BaseAddress.AbsoluteUri, buildServerCredentials);
433            }
434            catch (Exception exception)
435            {
436                Console.WriteLine(exception);
437                throw;
438            }
439        }
440
441        public void UpdateHttpClientOptionsGuestAuth()
442        {
443            _httpClientHostSuffix = "guestAuth";
444            _httpClient.DefaultRequestHeaders.Authorization = null;
445        }
446
447        private void UpdateHttpClientOptionsCredentialsAuth(IBuildServerCredentials buildServerCredentials)
448        {
449            _httpClientHostSuffix = "httpAuth";
450            _httpClient.DefaultRequestHeaders.Authorization = CreateBasicHeader(buildServerCredentials.Username, buildServerCredentials.Password);
451        }
452
453        private static AuthenticationHeaderValue CreateBasicHeader(string username, string password)
454        {
455            byte[] byteArray = Encoding.UTF8.GetBytes(string.Format("{0}:{1}", username, password));
456            return new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));
457        }
458
459        private Task<XDocument> GetXmlResponseAsync(string relativePath, CancellationToken cancellationToken)
460        {
461            var getStreamTask = GetStreamAsync(relativePath, cancellationToken);
462
463            return getStreamTask.ContinueWith(
464                task =>
465                    {
466                        using (var responseStream = task.Result)
467                        {
468                            return XDocument.Load(responseStream);
469                        }
470                    },
471                cancellationToken,
472                TaskContinuationOptions.AttachedToParent,
473                TaskScheduler.Current);
474        }
475
476        private Uri FormatRelativePath(string restServicePath)
477        {
478            return new Uri(string.Format("{0}/app/rest/{1}", _httpClientHostSuffix, restServicePath), UriKind.Relative);
479        }
480
481        private Task<XDocument> GetBuildFromIdXmlResponseAsync(string buildId, CancellationToken cancellationToken)
482        {
483            return GetXmlResponseAsync(string.Format("builds/id:{0}", buildId), cancellationToken);
484        }
485
486        private Task<XDocument> GetBuildTypeFromIdXmlResponseAsync(string buildId, CancellationToken cancellationToken)
487        {
488            return GetXmlResponseAsync(string.Format("buildTypes/id:{0}", buildId), cancellationToken);
489        }
490
491        private Task<XDocument> GetProjectFromNameXmlResponseAsync(string projectName, CancellationToken cancellationToken)
492        {
493            return GetXmlResponseAsync(string.Format("projects/{0}", projectName), cancellationToken);
494        }
495
496        private Task<XDocument> GetProjectsResponseAsync(CancellationToken cancellationToken)
497        {
498            return GetXmlResponseAsync("projects", cancellationToken);
499        }
500
501        private Task<XDocument> GetFilteredBuildsXmlResponseAsync(string buildTypeId, CancellationToken cancellationToken, DateTime? sinceDate = null, bool? running = null)
502        {
503            var values = new List<string> { "branch:(default:any)" };
504
505            if (sinceDate.HasValue)
506            {
507                values.Add(string.Format("sinceDate:{0}", FormatJsonDate(sinceDate.Value)));
508            }
509
510            if (running.HasValue)
511            {
512                values.Add(string.Format("running:{0}", running.Value.ToString(CultureInfo.InvariantCulture)));
513            }
514
515            string buildLocator = string.Join(",", values);
516            var url = string.Format("buildTypes/id:{0}/builds/?locator={1}", buildTypeId, buildLocator);
517            var filteredBuildsXmlResponseTask = GetXmlResponseAsync(url, cancellationToken);
518
519            return filteredBuildsXmlResponseTask;
520        }
521
522        private static DateTime DecodeJsonDateTime(string dateTimeString)
523        {
524            var dateTime = new DateTime(
525                    int.Parse(dateTimeString.Substring(0, 4)),
526                    int.Parse(dateTimeString.Substring(4, 2)),
527                    int.Parse(dateTimeString.Substring(6, 2)),
528                    int.Parse(dateTimeString.Substring(9, 2)),
529                    int.Parse(dateTimeString.Substring(11, 2)),
530                    int.Parse(dateTimeString.Substring(13, 2)),
531                    DateTimeKind.Utc)
532                .AddHours(int.Parse(dateTimeString.Substring(15, 3)))
533                .AddMinutes(int.Parse(dateTimeString.Substring(15, 1) + dateTimeString.Substring(18, 2)));
534
535            return dateTime;
536        }
537
538        private static string FormatJsonDate(DateTime dateTime)
539        {
540            return dateTime.ToUniversalTime().ToString("yyyyMMdd'T'HHmmss-0000", CultureInfo.InvariantCulture).Replace(":", string.Empty);
541        }
542
543        public void Dispose()
544        {
545            GC.SuppressFinalize(this);
546
547            _httpClient?.Dispose();
548        }
549
550        [CanBeNull]
551        public Project GetProjectsTree()
552        {
553            var projectsRootElement = ThreadHelper.JoinableTaskFactory.Run(() => GetProjectsResponseAsync(CancellationToken.None));
554            var projects = projectsRootElement.Root.Elements().Where(e => (string)e.Attribute("archived") != "true").Select(e => new Project
555            {
556                Id = (string)e.Attribute("id"),
557                Name = (string)e.Attribute("name"),
558                ParentProject = (string)e.Attribute("parentProjectId"),
559                SubProjects = new List<Project>()
560            }).ToList();
561
562            var projectDictionary = projects.ToDictionary(p => p.Id, p => p);
563
564            Project rootProject = null;
565            foreach (var project in projects)
566            {
567                if (project.ParentProject != null)
568                {
569                    projectDictionary[project.ParentProject].SubProjects.Add(project);
570                }
571                else
572                {
573                    rootProject = project;
574                }
575            }
576
577            return rootProject;
578        }
579
580        public List<Build> GetProjectBuilds(string projectId)
581        {
582            var projectsRootElement = ThreadHelper.JoinableTaskFactory.Run(() => GetProjectFromNameXmlResponseAsync(projectId, CancellationToken.None));
583            return projectsRootElement.Root.Element("buildTypes").Elements().Select(e => new Build
584            {
585                Id = (string)e.Attribute("id"),
586                Name = (string)e.Attribute("name"),
587                ParentProject = (string)e.Attribute("projectId")
588            }).ToList();
589        }
590
591        public Build GetBuildType(string buildId)
592        {
593            var projectsRootElement = ThreadHelper.JoinableTaskFactory.Run(() => GetBuildTypeFromIdXmlResponseAsync(buildId, CancellationToken.None));
594            var buildType = projectsRootElement.Root;
595            return new Build
596            {
597                Id = buildId,
598                Name = (string)buildType.Attribute("name"),
599                ParentProject = (string)buildType.Attribute("projectId")
600            };
601        }
602    }
603
604    public class Project
605    {
606        public string Id { get; set; }
607        public string Name { get; set; }
608        public string ParentProject { get; set; }
609        public IList<Project> SubProjects { get; set; }
610        public IList<Build> Builds { get; set; }
611    }
612
613    public class Build
614    {
615        public string ParentProject { get; set; }
616        public string Id { get; set; }
617        public string Name { get; set; }
618        public string DisplayName => Name + " (" + Id + ")";
619    }
620}