PageRenderTime 51ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/Plugins/BuildServerIntegration/TeamCityIntegration/TeamCityAdapter.cs

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