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