PageRenderTime 68ms CodeModel.GetById 28ms RepoModel.GetById 1ms app.codeStats 0ms

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