PageRenderTime 30ms CodeModel.GetById 10ms app.highlight 14ms RepoModel.GetById 2ms app.codeStats 0ms

/Plugins/BuildServerIntegration/JenkinsIntegration/JenkinsAdapter.cs

https://gitlab.com/Rockyspade/gitextensions
C# | 365 lines | 307 code | 53 blank | 5 comment | 43 complexity | 5912dad88e15524e07a8e543a82b98fa MD5 | raw file
  1using System;
  2using System.Collections.Generic;
  3using System.ComponentModel.Composition;
  4using System.Diagnostics;
  5using System.IO;
  6using System.Linq;
  7using System.Net;
  8using System.Net.Http;
  9using System.Net.Http.Headers;
 10using System.Reactive.Concurrency;
 11using System.Reactive.Linq;
 12using System.Text;
 13using System.Threading;
 14using System.Threading.Tasks;
 15using GitCommands.Settings;
 16using GitCommands.Utils;
 17using GitUIPluginInterfaces;
 18using GitUIPluginInterfaces.BuildServerIntegration;
 19using Newtonsoft.Json.Linq;
 20
 21namespace JenkinsIntegration
 22{
 23    [MetadataAttribute]
 24    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
 25    public class JenkinsIntegrationMetadata : BuildServerAdapterMetadataAttribute
 26    {
 27        public JenkinsIntegrationMetadata(string buildServerType)
 28            : base(buildServerType) { }
 29
 30        public override string CanBeLoaded
 31        {
 32            get
 33            {
 34                if (EnvUtils.IsNet4FullOrHigher())
 35                    return null;
 36                return ".Net 4 full framework required";
 37            }
 38        }
 39    }
 40
 41    [Export(typeof(IBuildServerAdapter))]
 42    [JenkinsIntegrationMetadata("Jenkins")]
 43    [PartCreationPolicy(CreationPolicy.NonShared)]
 44    internal class JenkinsAdapter : IBuildServerAdapter
 45    {
 46        private IBuildServerWatcher _buildServerWatcher;
 47
 48        private HttpClient _httpClient;
 49
 50        private IList<Task<IEnumerable<string>>> _getBuildUrls;
 51
 52        public void Initialize(IBuildServerWatcher buildServerWatcher, ISettingsSource config)
 53        {
 54            if (_buildServerWatcher != null)
 55                throw new InvalidOperationException("Already initialized");
 56
 57            _buildServerWatcher = buildServerWatcher;
 58
 59            var projectName = config.GetString("ProjectName", null);
 60            var hostName = config.GetString("BuildServerUrl", null);
 61
 62            if (!string.IsNullOrEmpty(hostName) && !string.IsNullOrEmpty(projectName))
 63            {
 64                var baseAdress = hostName.Contains("://")
 65                                     ? new Uri(hostName, UriKind.Absolute)
 66                                     : new Uri(string.Format("{0}://{1}:8080", Uri.UriSchemeHttp, hostName), UriKind.Absolute);
 67
 68                _httpClient = new HttpClient
 69                    {
 70                        Timeout = TimeSpan.FromMinutes(2),
 71                        BaseAddress = baseAdress
 72                    };
 73
 74                var buildServerCredentials = buildServerWatcher.GetBuildServerCredentials(this, true);
 75
 76                UpdateHttpClientOptions(buildServerCredentials);
 77
 78                _getBuildUrls = new List<Task<IEnumerable<string>>>();
 79
 80                string[] projectUrls = projectName.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
 81                foreach (var projectUrl in projectUrls.Select(s => baseAdress + "job/" + s.Trim() + "/"))
 82                {
 83                    AddGetBuildUrl(projectUrl);
 84                }
 85            }
 86        }
 87
 88        private void AddGetBuildUrl(string projectUrl)
 89        {
 90            _getBuildUrls.Add(GetResponseAsync(FormatToGetJson(projectUrl), CancellationToken.None)
 91                .ContinueWith(
 92                    task =>
 93                    {
 94                        JObject jobDescription = JObject.Parse(task.Result);
 95                        return jobDescription["builds"].Select(b => b["url"].ToObject<string>());
 96                    },
 97                    TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.AttachedToParent));
 98        }
 99
100        /// <summary>
101        /// Gets a unique key which identifies this build server.
102        /// </summary>
103        public string UniqueKey
104        {
105            get { return _httpClient.BaseAddress.Host; }
106        }
107
108        public IObservable<BuildInfo> GetFinishedBuildsSince(IScheduler scheduler, DateTime? sinceDate = null)
109        {
110            return GetBuilds(scheduler, sinceDate, false);
111        }
112
113        public IObservable<BuildInfo> GetRunningBuilds(IScheduler scheduler)
114        {
115            return GetBuilds(scheduler, null, true);
116        }
117
118        public IObservable<BuildInfo> GetBuilds(IScheduler scheduler, DateTime? sinceDate = null, bool? running = null)
119        {
120            if (_getBuildUrls == null || _getBuildUrls.Count() == 0)
121            {
122                return Observable.Empty<BuildInfo>(scheduler);
123            }
124
125            return Observable.Create<BuildInfo>((observer, cancellationToken) =>
126                Task<IDisposable>.Factory.StartNew(
127                    () => scheduler.Schedule(() => ObserveBuilds(sinceDate, running, observer, cancellationToken))));
128        }
129
130        private void ObserveBuilds(DateTime? sinceDate, bool? running, IObserver<BuildInfo> observer, CancellationToken cancellationToken)
131        {
132            try
133            {
134                if (_getBuildUrls.All(t => t.IsCanceled))
135                {
136                    observer.OnCompleted();
137                    return;
138                }
139
140                foreach (var currentGetBuildUrls in _getBuildUrls)
141                {
142                    if (currentGetBuildUrls.IsFaulted)
143                    {
144                        Debug.Assert(currentGetBuildUrls.Exception != null);
145
146                        observer.OnError(currentGetBuildUrls.Exception);
147                        continue;
148                    }
149
150                    var buildContents = currentGetBuildUrls.Result
151                        .Select(buildUrl => GetResponseAsync(FormatToGetJson(buildUrl), cancellationToken).Result)
152                        .Where(s => !string.IsNullOrEmpty(s)).ToArray();
153
154                    foreach (var buildDetails in buildContents)
155                    {
156                        JObject buildDescription = JObject.Parse(buildDetails);
157                        var startDate = TimestampToDateTime(buildDescription["timestamp"].ToObject<long>());
158                        var isRunning = buildDescription["building"].ToObject<bool>();
159
160                        if (sinceDate.HasValue && sinceDate.Value > startDate)
161                            continue;
162
163                        if (running.HasValue && running.Value != isRunning)
164                            continue;
165
166                        var buildInfo = CreateBuildInfo(buildDescription);
167                        if (buildInfo.CommitHashList.Any())
168                        {
169                            observer.OnNext(buildInfo);
170                        }
171                    }
172                }
173            }
174            catch (OperationCanceledException)
175            {
176                // Do nothing, the observer is already stopped
177            }
178            catch (Exception ex)
179            {
180                observer.OnError(ex);
181            }
182        }
183
184        private static BuildInfo CreateBuildInfo(JObject buildDescription)
185        {
186            var idValue = buildDescription["number"].ToObject<string>();
187            var statusValue = buildDescription["result"].ToObject<string>();
188            var startDateTicks = buildDescription["timestamp"].ToObject<long>();
189            var displayName = buildDescription["fullDisplayName"].ToObject<string>();
190            var webUrl = buildDescription["url"].ToObject<string>();
191            var action = buildDescription["actions"];
192            var commitHashList = new List<string>();
193            int nbTests = 0;
194            int nbFailedTests = 0;
195            int nbSkippedTests = 0;
196            foreach (var element in action)
197            {
198                if (element["lastBuiltRevision"] != null)
199                    commitHashList.Add(element["lastBuiltRevision"]["SHA1"].ToObject<string>());
200                if (element["totalCount"] != null)
201                {
202                    nbTests = element["totalCount"].ToObject<int>();
203                    nbFailedTests = element["failCount"].ToObject<int>();
204                    nbSkippedTests = element["skipCount"].ToObject<int>();
205                }
206            }
207
208            string testResults = string.Empty;
209            if (nbTests != 0)
210            {
211                testResults = String.Format(" : {0} tests ( {1} failed, {2} skipped )", nbTests, nbFailedTests, nbSkippedTests);
212            }
213
214            var isRunning = buildDescription["building"].ToObject<bool>();
215
216            var status = ParseBuildStatus(statusValue);
217            var statusText = isRunning ? string.Empty : status.ToString("G");
218            var buildInfo = new BuildInfo
219                {
220                    Id = idValue,
221                    StartDate = TimestampToDateTime(startDateTicks),
222                    Status = isRunning ? BuildInfo.BuildStatus.InProgress : status,
223                    Description = displayName + " " + statusText + testResults,
224                    CommitHashList = commitHashList.ToArray(),
225                    Url = webUrl
226                };
227            return buildInfo;
228        }
229
230        public static DateTime TimestampToDateTime(long timestamp)
231        {
232            return new DateTime(1970, 1, 1, 0, 0, 0, DateTime.Now.Kind).AddMilliseconds(timestamp);
233        }
234
235        private static AuthenticationHeaderValue CreateBasicHeader(string username, string password)
236        {
237            byte[] byteArray = Encoding.UTF8.GetBytes(string.Format("{0}:{1}", username, password));
238            return new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));
239        }
240
241        private static BuildInfo.BuildStatus ParseBuildStatus(string statusValue)
242        {
243            switch (statusValue)
244            {
245                case "SUCCESS":
246                    return BuildInfo.BuildStatus.Success;
247                case "FAILURE":
248                    return BuildInfo.BuildStatus.Failure;
249                case "UNSTABLE":
250                    return BuildInfo.BuildStatus.Unstable;
251                case "ABORTED":
252                    return BuildInfo.BuildStatus.Stopped;
253                default:
254                    return BuildInfo.BuildStatus.Unknown;
255            }
256        }
257
258        private Task<Stream> GetStreamAsync(string restServicePath, CancellationToken cancellationToken)
259        {
260            cancellationToken.ThrowIfCancellationRequested();
261
262            return _httpClient.GetAsync(restServicePath, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
263                             .ContinueWith(
264                                 task => GetStreamFromHttpResponseAsync(task, restServicePath, cancellationToken),
265                                 cancellationToken,
266                                 TaskContinuationOptions.AttachedToParent,
267                                 TaskScheduler.Current)
268                             .Unwrap();
269        }
270
271        private Task<Stream> GetStreamFromHttpResponseAsync(Task<HttpResponseMessage> task, string restServicePath, CancellationToken cancellationToken)
272        {
273#if !__MonoCS__
274            bool retry = task.IsCanceled && !cancellationToken.IsCancellationRequested;
275            bool unauthorized = task.Status == TaskStatus.RanToCompletion &&
276                                task.Result.StatusCode == HttpStatusCode.Unauthorized;
277
278            if (!retry)
279            {
280                if (task.Result.IsSuccessStatusCode)
281                {
282                    var httpContent = task.Result.Content;
283
284                    if (httpContent.Headers.ContentType.MediaType == "text/html")
285                    {
286                        // Jenkins responds with an HTML login page when guest access is denied.
287                        unauthorized = true;
288                    }
289                    else
290                    {
291                        return httpContent.ReadAsStreamAsync();
292                    }
293                }
294            }
295
296            if (retry)
297            {
298                return GetStreamAsync(restServicePath, cancellationToken);
299            }
300
301            if (unauthorized)
302            {
303                var buildServerCredentials = _buildServerWatcher.GetBuildServerCredentials(this, false);
304
305                if (buildServerCredentials != null)
306                {
307                    UpdateHttpClientOptions(buildServerCredentials);
308
309                    return GetStreamAsync(restServicePath, cancellationToken);
310                }
311
312                throw new OperationCanceledException(task.Result.ReasonPhrase);
313            }
314
315            throw new HttpRequestException(task.Result.ReasonPhrase);
316#else
317            return null;
318#endif
319        }
320
321        private void UpdateHttpClientOptions(IBuildServerCredentials buildServerCredentials)
322        {
323            var useGuestAccess = buildServerCredentials == null || buildServerCredentials.UseGuestAccess;
324
325            _httpClient.DefaultRequestHeaders.Authorization = useGuestAccess
326                ? null : CreateBasicHeader(buildServerCredentials.Username, buildServerCredentials.Password);
327        }
328
329        private Task<string> GetResponseAsync(string relativePath, CancellationToken cancellationToken)
330        {
331            var getStreamTask = GetStreamAsync(relativePath, cancellationToken);
332
333            return getStreamTask.ContinueWith(
334                task =>
335                {
336                    if (task.Status != TaskStatus.RanToCompletion)
337                        return string.Empty;
338                    using (var responseStream = task.Result)
339                    {
340                        return new StreamReader(responseStream).ReadToEnd();
341                    }
342                },
343                cancellationToken,
344                TaskContinuationOptions.AttachedToParent,
345                TaskScheduler.Current);
346        }
347
348        private string FormatToGetJson(string restServicePath)
349        {
350            if (!restServicePath.EndsWith("/"))
351                restServicePath += "/";
352            return restServicePath + "api/json";
353        }
354
355        public void Dispose()
356        {
357            GC.SuppressFinalize(this);
358
359            if (_httpClient != null)
360            {
361                _httpClient.Dispose();
362            }
363        }
364    }
365}