PageRenderTime 30ms CodeModel.GetById 2ms app.highlight 22ms RepoModel.GetById 1ms app.codeStats 1ms

/Plugins/BuildServerIntegration/TeamCityIntegration/TeamCityAdapter.cs

https://github.com/PKRoma/gitextensions
C# | 622 lines | 530 code | 87 blank | 5 comment | 56 complexity | 624b413dc3d575cf31bee9ba1787a208 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#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks (The task is completed)
362                                 task => GetStreamFromHttpResponseAsync(task, restServicePath, cancellationToken),
363#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks
364                                 cancellationToken,
365                                 TaskContinuationOptions.AttachedToParent,
366                                 TaskScheduler.Current)
367                             .Unwrap();
368        }
369
370        private Task<Stream> GetStreamFromHttpResponseAsync(Task<HttpResponseMessage> task, string restServicePath, CancellationToken cancellationToken)
371        {
372            if (!task.IsCompleted)
373            {
374                throw new InvalidOperationException($"Task in state '{task.Status}' was expected to be completed.");
375            }
376
377            bool retry = task.IsCanceled && !cancellationToken.IsCancellationRequested;
378            bool unauthorized = task.Status == TaskStatus.RanToCompletion &&
379                                (task.CompletedResult().StatusCode == HttpStatusCode.Unauthorized || task.CompletedResult().StatusCode == HttpStatusCode.Forbidden);
380
381            if (!retry)
382            {
383                if (task.CompletedResult().IsSuccessStatusCode)
384                {
385                    var httpContent = task.CompletedResult().Content;
386
387                    if (httpContent.Headers.ContentType.MediaType == "text/html")
388                    {
389                        // TeamCity responds with an HTML login page when guest access is denied.
390                        unauthorized = true;
391                    }
392                    else
393                    {
394                        return httpContent.ReadAsStreamAsync();
395                    }
396                }
397            }
398
399            if (retry)
400            {
401                return GetStreamAsync(restServicePath, cancellationToken);
402            }
403
404            if (unauthorized)
405            {
406                var buildServerCredentials = _buildServerWatcher.GetBuildServerCredentials(this, true);
407                var useBuildServerCredentials = buildServerCredentials != null
408                                                && !buildServerCredentials.UseGuestAccess
409                                                && (string.IsNullOrWhiteSpace(buildServerCredentials.Username) && string.IsNullOrWhiteSpace(buildServerCredentials.Password));
410                if (useBuildServerCredentials)
411                {
412                    UpdateHttpClientOptionsCredentialsAuth(buildServerCredentials);
413                    return GetStreamAsync(restServicePath, cancellationToken);
414                }
415                else
416                {
417                    UpdateHttpClientOptionsNtlmAuth(buildServerCredentials);
418                    return GetStreamAsync(restServicePath, cancellationToken);
419                }
420            }
421
422            throw new HttpRequestException(task.CompletedResult().ReasonPhrase);
423        }
424
425        public void UpdateHttpClientOptionsNtlmAuth(IBuildServerCredentials buildServerCredentials)
426        {
427            try
428            {
429                _httpClient.Dispose();
430                _httpClientHandler.Dispose();
431
432                _httpClientHostSuffix = "httpAuth";
433                CreateNewHttpClient(HostName);
434                _httpClientHandler.CookieContainer = GetTeamCityNtlmAuthCookie(_httpClient.BaseAddress.AbsoluteUri, buildServerCredentials);
435            }
436            catch (Exception exception)
437            {
438                Console.WriteLine(exception);
439                throw;
440            }
441        }
442
443        public void UpdateHttpClientOptionsGuestAuth()
444        {
445            _httpClientHostSuffix = "guestAuth";
446            _httpClient.DefaultRequestHeaders.Authorization = null;
447        }
448
449        private void UpdateHttpClientOptionsCredentialsAuth(IBuildServerCredentials buildServerCredentials)
450        {
451            _httpClientHostSuffix = "httpAuth";
452            _httpClient.DefaultRequestHeaders.Authorization = CreateBasicHeader(buildServerCredentials.Username, buildServerCredentials.Password);
453        }
454
455        private static AuthenticationHeaderValue CreateBasicHeader(string username, string password)
456        {
457            byte[] byteArray = Encoding.UTF8.GetBytes(string.Format("{0}:{1}", username, password));
458            return new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));
459        }
460
461        private Task<XDocument> GetXmlResponseAsync(string relativePath, CancellationToken cancellationToken)
462        {
463            var getStreamTask = GetStreamAsync(relativePath, cancellationToken);
464
465            return getStreamTask.ContinueWith(
466                task =>
467                    {
468                        using (var responseStream = task.Result)
469                        {
470                            return XDocument.Load(responseStream);
471                        }
472                    },
473                cancellationToken,
474                TaskContinuationOptions.AttachedToParent,
475                TaskScheduler.Current);
476        }
477
478        private Uri FormatRelativePath(string restServicePath)
479        {
480            return new Uri(string.Format("{0}/app/rest/{1}", _httpClientHostSuffix, restServicePath), UriKind.Relative);
481        }
482
483        private Task<XDocument> GetBuildFromIdXmlResponseAsync(string buildId, CancellationToken cancellationToken)
484        {
485            return GetXmlResponseAsync(string.Format("builds/id:{0}", buildId), cancellationToken);
486        }
487
488        private Task<XDocument> GetBuildTypeFromIdXmlResponseAsync(string buildId, CancellationToken cancellationToken)
489        {
490            return GetXmlResponseAsync(string.Format("buildTypes/id:{0}", buildId), cancellationToken);
491        }
492
493        private Task<XDocument> GetProjectFromNameXmlResponseAsync(string projectName, CancellationToken cancellationToken)
494        {
495            return GetXmlResponseAsync(string.Format("projects/{0}", projectName), cancellationToken);
496        }
497
498        private Task<XDocument> GetProjectsResponseAsync(CancellationToken cancellationToken)
499        {
500            return GetXmlResponseAsync("projects", cancellationToken);
501        }
502
503        private Task<XDocument> GetFilteredBuildsXmlResponseAsync(string buildTypeId, CancellationToken cancellationToken, DateTime? sinceDate = null, bool? running = null)
504        {
505            var values = new List<string> { "branch:(default:any)" };
506
507            if (sinceDate.HasValue)
508            {
509                values.Add(string.Format("sinceDate:{0}", FormatJsonDate(sinceDate.Value)));
510            }
511
512            if (running.HasValue)
513            {
514                values.Add(string.Format("running:{0}", running.Value.ToString(CultureInfo.InvariantCulture)));
515            }
516
517            string buildLocator = string.Join(",", values);
518            var url = string.Format("buildTypes/id:{0}/builds/?locator={1}", buildTypeId, buildLocator);
519            var filteredBuildsXmlResponseTask = GetXmlResponseAsync(url, cancellationToken);
520
521            return filteredBuildsXmlResponseTask;
522        }
523
524        private static DateTime DecodeJsonDateTime(string dateTimeString)
525        {
526            var dateTime = new DateTime(
527                    int.Parse(dateTimeString.Substring(0, 4)),
528                    int.Parse(dateTimeString.Substring(4, 2)),
529                    int.Parse(dateTimeString.Substring(6, 2)),
530                    int.Parse(dateTimeString.Substring(9, 2)),
531                    int.Parse(dateTimeString.Substring(11, 2)),
532                    int.Parse(dateTimeString.Substring(13, 2)),
533                    DateTimeKind.Utc)
534                .AddHours(int.Parse(dateTimeString.Substring(15, 3)))
535                .AddMinutes(int.Parse(dateTimeString.Substring(15, 1) + dateTimeString.Substring(18, 2)));
536
537            return dateTime;
538        }
539
540        private static string FormatJsonDate(DateTime dateTime)
541        {
542            return dateTime.ToUniversalTime().ToString("yyyyMMdd'T'HHmmss-0000", CultureInfo.InvariantCulture).Replace(":", string.Empty);
543        }
544
545        public void Dispose()
546        {
547            GC.SuppressFinalize(this);
548
549            _httpClient?.Dispose();
550        }
551
552        [CanBeNull]
553        public Project GetProjectsTree()
554        {
555            var projectsRootElement = ThreadHelper.JoinableTaskFactory.Run(() => GetProjectsResponseAsync(CancellationToken.None));
556            var projects = projectsRootElement.Root.Elements().Where(e => (string)e.Attribute("archived") != "true").Select(e => new Project
557            {
558                Id = (string)e.Attribute("id"),
559                Name = (string)e.Attribute("name"),
560                ParentProject = (string)e.Attribute("parentProjectId"),
561                SubProjects = new List<Project>()
562            }).ToList();
563
564            var projectDictionary = projects.ToDictionary(p => p.Id, p => p);
565
566            Project rootProject = null;
567            foreach (var project in projects)
568            {
569                if (project.ParentProject != null)
570                {
571                    projectDictionary[project.ParentProject].SubProjects.Add(project);
572                }
573                else
574                {
575                    rootProject = project;
576                }
577            }
578
579            return rootProject;
580        }
581
582        public List<Build> GetProjectBuilds(string projectId)
583        {
584            var projectsRootElement = ThreadHelper.JoinableTaskFactory.Run(() => GetProjectFromNameXmlResponseAsync(projectId, CancellationToken.None));
585            return projectsRootElement.Root.Element("buildTypes").Elements().Select(e => new Build
586            {
587                Id = (string)e.Attribute("id"),
588                Name = (string)e.Attribute("name"),
589                ParentProject = (string)e.Attribute("projectId")
590            }).ToList();
591        }
592
593        public Build GetBuildType(string buildId)
594        {
595            var projectsRootElement = ThreadHelper.JoinableTaskFactory.Run(() => GetBuildTypeFromIdXmlResponseAsync(buildId, CancellationToken.None));
596            var buildType = projectsRootElement.Root;
597            return new Build
598            {
599                Id = buildId,
600                Name = (string)buildType.Attribute("name"),
601                ParentProject = (string)buildType.Attribute("projectId")
602            };
603        }
604    }
605
606    public class Project
607    {
608        public string Id { get; set; }
609        public string Name { get; set; }
610        public string ParentProject { get; set; }
611        public IList<Project> SubProjects { get; set; }
612        public IList<Build> Builds { get; set; }
613    }
614
615    public class Build
616    {
617        public string ParentProject { get; set; }
618        public string Id { get; set; }
619        public string Name { get; set; }
620        public string DisplayName => Name + " (" + Id + ")";
621    }
622}