PageRenderTime 21ms CodeModel.GetById 2ms app.highlight 14ms RepoModel.GetById 1ms app.codeStats 0ms

/Plugins/BuildServerIntegration/TeamCityIntegration/TeamCityAdapter.cs

https://gitlab.com/Rockyspade/gitextensions
C# | 469 lines | 393 code | 71 blank | 5 comment | 45 complexity | 54a333cb2803a6d4225b374c21ee3f46 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.Settings;
 20using GitCommands.Utils;
 21using GitUIPluginInterfaces;
 22using GitUIPluginInterfaces.BuildServerIntegration;
 23using Microsoft.Win32;
 24
 25namespace TeamCityIntegration
 26{
 27
 28    [MetadataAttribute]
 29    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
 30    public class TeamCityIntegrationMetadata : BuildServerAdapterMetadataAttribute
 31    {
 32        public TeamCityIntegrationMetadata(string buildServerType)
 33            : base(buildServerType)
 34        {
 35        }
 36
 37        public override string CanBeLoaded
 38        {
 39            get
 40            {
 41                if (EnvUtils.IsNet4FullOrHigher())
 42                    return null;
 43                else
 44                    return ".Net 4 full framework required";
 45            }
 46        }
 47    }
 48
 49
 50
 51    [Export(typeof(IBuildServerAdapter))]
 52    [TeamCityIntegrationMetadata("TeamCity")]
 53    [PartCreationPolicy(CreationPolicy.NonShared)]
 54    internal class TeamCityAdapter : IBuildServerAdapter
 55    {
 56        private IBuildServerWatcher buildServerWatcher;
 57
 58        private HttpClient httpClient;
 59
 60        private string httpClientHostSuffix;
 61
 62        private List<Task<IEnumerable<string>>> getBuildTypesTask = new List<Task<IEnumerable<string>>>();
 63
 64        private string[] ProjectNames { get; set; }
 65
 66        private Regex BuildIdFilter { get; set; }
 67
 68        public void Initialize(IBuildServerWatcher buildServerWatcher, ISettingsSource config)
 69        {
 70            if (this.buildServerWatcher != null)
 71                throw new InvalidOperationException("Already initialized");
 72
 73            this.buildServerWatcher = buildServerWatcher;
 74
 75            ProjectNames = config.GetString("ProjectName", "").Split(new char[]{'|'}, StringSplitOptions.RemoveEmptyEntries);
 76            BuildIdFilter = new Regex(config.GetString("BuildIdFilter", ""), RegexOptions.Compiled);
 77            var hostName = config.GetString("BuildServerUrl", null);
 78            if (!string.IsNullOrEmpty(hostName))
 79            {
 80                httpClient = new HttpClient
 81                    {
 82                        Timeout = TimeSpan.FromMinutes(2),
 83                        BaseAddress = hostName.Contains("://")
 84                                          ? new Uri(hostName, UriKind.Absolute)
 85                                          : new Uri(string.Format("{0}://{1}", Uri.UriSchemeHttp, hostName), UriKind.Absolute)
 86                    };
 87                httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
 88
 89                var buildServerCredentials = buildServerWatcher.GetBuildServerCredentials(this, true);
 90
 91                UpdateHttpClientOptions(buildServerCredentials);
 92
 93                if (ProjectNames.Length > 0)
 94                {
 95                    getBuildTypesTask.Clear();
 96                    foreach (var name in ProjectNames)
 97                    {
 98                        getBuildTypesTask.Add(
 99                            GetProjectFromNameXmlResponseAsync(name, CancellationToken.None)
100                            .ContinueWith(
101                            task => from element in task.Result.XPathSelectElements("/project/buildTypes/buildType")
102                                   select element.Attribute("id").Value,
103                           TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.AttachedToParent));
104                    }
105
106                }
107            }
108        }
109
110        /// <summary>
111        /// Gets a unique key which identifies this build server.
112        /// </summary>
113        public string UniqueKey
114        {
115            get { return httpClient.BaseAddress.Host; }
116        }
117
118        public IObservable<BuildInfo> GetFinishedBuildsSince(IScheduler scheduler, DateTime? sinceDate = null)
119        {
120            return GetBuilds(scheduler, sinceDate, false);
121        }
122
123        public IObservable<BuildInfo> GetRunningBuilds(IScheduler scheduler)
124        {
125            return GetBuilds(scheduler, null, true);
126        }
127
128        public IObservable<BuildInfo> GetBuilds(IScheduler scheduler, DateTime? sinceDate = null, bool? running = null)
129        {
130            if (httpClient == null || httpClient.BaseAddress == null || ProjectNames.Length == 0)
131            {
132                return Observable.Empty<BuildInfo>(scheduler);
133            }
134
135            return Observable.Create<BuildInfo>((observer, cancellationToken) =>
136                Task<IDisposable>.Factory.StartNew(
137                    () => scheduler.Schedule(() => ObserveBuilds(sinceDate, running, observer, cancellationToken))));
138        }
139
140        private void ObserveBuilds(DateTime? sinceDate, bool? running, IObserver<BuildInfo> observer, CancellationToken cancellationToken)
141        {
142            try
143            {
144                if (getBuildTypesTask.Any(task => PropagateTaskAnomalyToObserver(task, observer)))
145                {
146                    return;
147                }
148
149                var localObserver = observer;
150                var buildTypes = getBuildTypesTask.SelectMany(t => t.Result).Where(id => BuildIdFilter.IsMatch(id));
151                var buildIdTasks = buildTypes.Select(buildTypeId => GetFilteredBuildsXmlResponseAsync(buildTypeId, cancellationToken, sinceDate, running)).ToArray();
152
153                Task.Factory
154                    .ContinueWhenAll(
155                        buildIdTasks,
156                        completedTasks =>
157                            {
158                                var buildIds = completedTasks.Where(task => task.Status == TaskStatus.RanToCompletion)
159                                                             .SelectMany(
160                                                                 buildIdTask =>
161                                                                 buildIdTask.Result
162                                                                            .XPathSelectElements("/builds/build")
163                                                                            .Select(x => x.Attribute("id").Value))
164                                                             .ToArray();
165
166                                NotifyObserverOfBuilds(buildIds, observer, cancellationToken);
167                            },
168                        cancellationToken,
169                        TaskContinuationOptions.AttachedToParent | TaskContinuationOptions.ExecuteSynchronously,
170                        TaskScheduler.Current)
171                    .ContinueWith(
172                        task => localObserver.OnError(task.Exception),
173                        CancellationToken.None,
174                        TaskContinuationOptions.AttachedToParent | TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnFaulted,
175                        TaskScheduler.Current);
176            }
177            catch (OperationCanceledException)
178            {
179                // Do nothing, the observer is already stopped
180            }
181            catch (Exception ex)
182            {
183                observer.OnError(ex);
184            }
185        }
186
187        private void NotifyObserverOfBuilds(string[] buildIds, IObserver<BuildInfo> observer, CancellationToken cancellationToken)
188        {
189            var tasks = new List<Task>(8);
190            var buildsLeft = buildIds.Length;
191
192            foreach (var buildId in buildIds.OrderByDescending(int.Parse))
193            {
194                var notifyObserverTask =
195                    GetBuildFromIdXmlResponseAsync(buildId, cancellationToken)
196                        .ContinueWith(
197                            task =>
198                                {
199                                    if (task.Status == TaskStatus.RanToCompletion)
200                                    {
201                                        var buildDetails = task.Result;
202                                        var buildInfo = CreateBuildInfo(buildDetails);
203                                        if (buildInfo.CommitHashList.Any())
204                                        {
205                                            observer.OnNext(buildInfo);
206                                        }
207                                    }
208                                },
209                            cancellationToken,
210                            TaskContinuationOptions.AttachedToParent | TaskContinuationOptions.ExecuteSynchronously,
211                            TaskScheduler.Current);
212                
213                tasks.Add(notifyObserverTask);
214                --buildsLeft;
215
216                if (tasks.Count == tasks.Capacity || buildsLeft == 0)
217                {
218                    var batchTasks = tasks.ToArray();
219                    tasks.Clear();
220
221                    try
222                    {
223                        Task.WaitAll(batchTasks, cancellationToken);
224                    }
225                    catch (Exception e)
226                    {
227                        observer.OnError(e);
228                        return;
229                    }
230                }
231            }
232
233            observer.OnCompleted();
234        }
235
236        private static bool PropagateTaskAnomalyToObserver(Task task, IObserver<BuildInfo> observer)
237        {
238            if (task.IsCanceled)
239            {
240                observer.OnCompleted();
241                return true;
242            }
243
244            if (task.IsFaulted)
245            {
246                Debug.Assert(task.Exception != null);
247
248                observer.OnError(task.Exception);
249                return true;
250            }
251
252            return false;
253        }
254
255        private static BuildInfo CreateBuildInfo(XDocument buildXmlDocument)
256        {
257            var buildXElement = buildXmlDocument.Element("build");
258            var idValue = buildXElement.Attribute("id").Value;
259            var statusValue = buildXElement.Attribute("status").Value;
260            var startDateText = buildXElement.Element("startDate").Value;
261            var statusText = buildXElement.Element("statusText").Value;
262            var webUrl = buildXElement.Attribute("webUrl").Value;
263            var revisionsElements = buildXElement.XPathSelectElements("revisions/revision");
264            var commitHashList = revisionsElements.Select(x => x.Attribute("version").Value).ToArray();
265            var runningAttribute = buildXElement.Attribute("running");
266
267            if (runningAttribute != null && Convert.ToBoolean(runningAttribute.Value))
268            {
269                var runningInfoXElement = buildXElement.Element("running-info");
270                var currentStageText = runningInfoXElement.Attribute("currentStageText").Value;
271
272                statusText = currentStageText;
273            }
274
275            var buildInfo = new BuildInfo
276                {
277                    Id = idValue,
278                    StartDate = DecodeJsonDateTime(startDateText),
279                    Status = ParseBuildStatus(statusValue),
280                    Description = statusText,
281                    CommitHashList = commitHashList,
282                    Url = webUrl
283                };
284            return buildInfo;
285        }
286
287        private static AuthenticationHeaderValue CreateBasicHeader(string username, string password)
288        {
289            byte[] byteArray = Encoding.UTF8.GetBytes(string.Format("{0}:{1}", username, password));
290            return new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));
291        }
292
293        private static BuildInfo.BuildStatus ParseBuildStatus(string statusValue)
294        {
295            switch (statusValue)
296            {
297                case "SUCCESS":
298                    return BuildInfo.BuildStatus.Success;
299                case "FAILURE":
300                    return BuildInfo.BuildStatus.Failure;
301                default:
302                    return BuildInfo.BuildStatus.Unknown;
303            }
304        }
305
306        private Task<Stream> GetStreamAsync(string restServicePath, CancellationToken cancellationToken)
307        {
308            cancellationToken.ThrowIfCancellationRequested();
309
310            return httpClient.GetAsync(FormatRelativePath(restServicePath), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
311                             .ContinueWith(
312                                 task => GetStreamFromHttpResponseAsync(task, restServicePath, cancellationToken),
313                                 cancellationToken,
314                                 TaskContinuationOptions.AttachedToParent,
315                                 TaskScheduler.Current)
316                             .Unwrap();
317        }
318
319        private Task<Stream> GetStreamFromHttpResponseAsync(Task<HttpResponseMessage> task, string restServicePath, CancellationToken cancellationToken)
320        {
321#if !__MonoCS__
322            bool retry = task.IsCanceled && !cancellationToken.IsCancellationRequested;
323            bool unauthorized = task.Status == TaskStatus.RanToCompletion &&
324                                task.Result.StatusCode == HttpStatusCode.Unauthorized;
325
326            if (!retry)
327            {
328                if (task.Result.IsSuccessStatusCode)
329                {
330                    var httpContent = task.Result.Content;
331
332                    if (httpContent.Headers.ContentType.MediaType == "text/html")
333                    {
334                        // TeamCity responds with an HTML login page when guest access is denied.
335                        unauthorized = true;
336                    }
337                    else
338                    {
339                        return httpContent.ReadAsStreamAsync();
340                    }
341                }
342            }
343
344            if (retry)
345            {
346                return GetStreamAsync(restServicePath, cancellationToken);
347            }
348
349            if (unauthorized)
350            {
351                var buildServerCredentials = buildServerWatcher.GetBuildServerCredentials(this, false);
352
353                if (buildServerCredentials != null)
354                {
355                    UpdateHttpClientOptions(buildServerCredentials);
356
357                    return GetStreamAsync(restServicePath, cancellationToken);
358                }
359
360                throw new OperationCanceledException(task.Result.ReasonPhrase);
361            }
362
363            throw new HttpRequestException(task.Result.ReasonPhrase);
364#else
365            return null;
366#endif
367        }
368
369        private void UpdateHttpClientOptions(IBuildServerCredentials buildServerCredentials)
370        {
371            var useGuestAccess = buildServerCredentials == null || buildServerCredentials.UseGuestAccess;
372
373            if (useGuestAccess)
374            {
375                httpClientHostSuffix = "guestAuth";
376                httpClient.DefaultRequestHeaders.Authorization = null;
377            }
378            else
379            {
380                httpClientHostSuffix = "httpAuth";
381                httpClient.DefaultRequestHeaders.Authorization = CreateBasicHeader(buildServerCredentials.Username, buildServerCredentials.Password);
382            }
383        }
384
385        private Task<XDocument> GetXmlResponseAsync(string relativePath, CancellationToken cancellationToken)
386        {
387            var getStreamTask = GetStreamAsync(relativePath, cancellationToken);
388
389            return getStreamTask.ContinueWith(
390                task =>
391                    {
392                        using (var responseStream = task.Result)
393                        {
394                            return XDocument.Load(responseStream);
395                        }
396                    },
397                cancellationToken, 
398                TaskContinuationOptions.AttachedToParent, 
399                TaskScheduler.Current);
400        }
401
402        private Uri FormatRelativePath(string restServicePath)
403        {
404            return new Uri(string.Format("{0}/app/rest/{1}", httpClientHostSuffix, restServicePath), UriKind.Relative);
405        }
406
407        private Task<XDocument> GetBuildFromIdXmlResponseAsync(string buildId, CancellationToken cancellationToken)
408        {
409            return GetXmlResponseAsync(string.Format("builds/id:{0}", buildId), cancellationToken);
410        }
411
412        private Task<XDocument> GetProjectFromNameXmlResponseAsync(string projectName, CancellationToken cancellationToken)
413        {
414            return GetXmlResponseAsync(string.Format("projects/{0}", projectName), cancellationToken);
415        }
416
417        private Task<XDocument> GetFilteredBuildsXmlResponseAsync(string buildTypeId, CancellationToken cancellationToken, DateTime? sinceDate = null, bool? running = null)
418        {
419            var values = new List<string> { "branch:(default:any)" };
420
421            if (sinceDate.HasValue)
422            {
423                values.Add(string.Format("sinceDate:{0}", FormatJsonDate(sinceDate.Value)));
424            }
425
426            if (running.HasValue)
427            {
428                values.Add(string.Format("running:{0}", running.Value.ToString(CultureInfo.InvariantCulture)));
429            }
430
431            string buildLocator = string.Join(",", values);
432            var url = string.Format("buildTypes/id:{0}/builds/?locator={1}", buildTypeId, buildLocator);
433            var filteredBuildsXmlResponseTask = GetXmlResponseAsync(url, cancellationToken);
434
435            return filteredBuildsXmlResponseTask;
436        }
437
438        private static DateTime DecodeJsonDateTime(string dateTimeString)
439        {
440            var dateTime = new DateTime(
441                    int.Parse(dateTimeString.Substring(0, 4)),
442                    int.Parse(dateTimeString.Substring(4, 2)),
443                    int.Parse(dateTimeString.Substring(6, 2)),
444                    int.Parse(dateTimeString.Substring(9, 2)),
445                    int.Parse(dateTimeString.Substring(11, 2)),
446                    int.Parse(dateTimeString.Substring(13, 2)),
447                    DateTimeKind.Utc)
448                .AddHours(int.Parse(dateTimeString.Substring(15, 3)))
449                .AddMinutes(int.Parse(dateTimeString.Substring(15, 1) + dateTimeString.Substring(18, 2)));
450
451            return dateTime;
452        }
453
454        private static string FormatJsonDate(DateTime dateTime)
455        {
456            return dateTime.ToUniversalTime().ToString("yyyyMMdd'T'HHmmss-0000", CultureInfo.InvariantCulture).Replace(":", string.Empty);
457        }
458
459        public void Dispose()
460        {
461            GC.SuppressFinalize(this);
462
463            if (httpClient != null)
464            {
465                httpClient.Dispose();
466            }
467        }
468    }
469}