OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 import 'dart:io'; |
| 6 import 'dart:async'; |
| 7 import 'dart:convert'; |
| 8 import 'package:http/http.dart' as http; |
| 9 import 'package:html/parser.dart' show parse; |
| 10 import 'try.dart'; |
| 11 import 'cache_new.dart'; |
| 12 |
| 13 const String LUCI_HOST = "luci-milo.appspot.com"; |
| 14 |
| 15 typedef void ModifyRequestFunction(HttpClientRequest request); |
| 16 |
| 17 /// Base class for communicating with [Luci] |
| 18 /// Some information is found through the api |
| 19 /// <https://docs.google.com/document/d/1HbPp7Sy7ofC |
| 20 /// U7C9USqcE91VubGg_IIET2GUj9iknev4/edit#> |
| 21 /// and some information is found via screen-scraping. |
| 22 class LuciApi { |
| 23 final HttpClient _client = new HttpClient(); |
| 24 |
| 25 LuciApi(); |
| 26 |
| 27 /// [getBuildBots] fetches all build bots from luci (we cannot |
| 28 /// get this from the api). The format is: |
| 29 /// <li> |
| 30 /// <a href="/buildbot/client.crashpad/crashpad_win_x86_wow64_rel"> |
| 31 /// crashpad_win_x86_wow64_rel</a> |
| 32 /// </li> |
| 33 /// <h3> client.dart </h3> |
| 34 /// <li> |
| 35 /// <a href="/buildbot/client.dart/analyze-linux-be">analyze-linux-be</a> |
| 36 /// </li> |
| 37 /// <li> |
| 38 /// <a href="/buildbot/client.dart/analyze-linux-stable"> |
| 39 /// analyze-linux-stable</a> |
| 40 /// </li> |
| 41 /// <li> |
| 42 /// <a href="/buildbot/client.dart/analyzer-linux-release-be"> |
| 43 /// analyzer-linux-release-be</a> |
| 44 /// </li> |
| 45 /// |
| 46 /// We look for the section header matching clients, then |
| 47 /// if we are in the right section, we take the <li> element |
| 48 /// and transform to a build bot |
| 49 /// |
| 50 Future<Try<List<LuciBuildBot>>> getAllBuildBots( |
| 51 String client, WithCacheFunction withCache) async { |
| 52 return await tryStartAsync(() => withCache( |
| 53 () => _makeGetRequest( |
| 54 new Uri(scheme: 'https', host: LUCI_HOST, path: "/")), |
| 55 "all_buildbots")) |
| 56 .then((Try<String> tryRes) => tryRes.bind(parse).bind((htmlDoc) { |
| 57 // This is really dirty, but the structure of |
| 58 // the document is not really suited for anything else. |
| 59 var takeSection = false; |
| 60 return htmlDoc.body.children.where((node) { |
| 61 if (node.localName == "li") return takeSection; |
| 62 if (node.localName != "h3") { |
| 63 takeSection = false; |
| 64 return false; |
| 65 } |
| 66 // Current node is <h3>. |
| 67 takeSection = client == node.text.trim(); |
| 68 return false; |
| 69 }); |
| 70 }).bind((elements) { |
| 71 // Here we hold an iterable of buildbot elements |
| 72 // <li> |
| 73 // <a href="/buildbot/client.dart/analyzer-linux-release-be"> |
| 74 // analyzer-linux-release-be</a> |
| 75 // </li> |
| 76 return elements.map((element) { |
| 77 var name = element.children[0].text; |
| 78 var url = element.children[0].attributes['href']; |
| 79 return new LuciBuildBot(client, name, url); |
| 80 }).toList(); |
| 81 })); |
| 82 } |
| 83 |
| 84 /// [getPrimaryBuilders] fetches all primary builders |
| 85 /// (the ones usually used by gardeners) by not including buildbots with |
| 86 /// the name -dev, -stable or -integration. |
| 87 Future<Try<List<LuciBuildBot>>> getPrimaryBuilders( |
| 88 String client, WithCacheFunction withCache) async { |
| 89 return await getAllBuildBots(client, withCache) |
| 90 .then((Try<List<LuciBuildBot>> tryRes) { |
| 91 return tryRes |
| 92 .bind((buildBots) => buildBots.where((LuciBuildBot buildBot) { |
| 93 return !(buildBot.name.contains("-dev") || |
| 94 buildBot.name.contains("-stable") || |
| 95 buildBot.name.contains("-integration")); |
| 96 })); |
| 97 }); |
| 98 } |
| 99 |
| 100 /// Calling the Milo Api to get latest builds for this bot, |
| 101 /// where the field [amount] is the number of recent builds to fetch. |
| 102 Future<Try<List<BuildDetail>>> getBuildBotDetails( |
| 103 String client, String botName, WithCacheFunction withCache, |
| 104 [int amount = 20]) async { |
| 105 var uri = new Uri( |
| 106 scheme: "https", |
| 107 host: LUCI_HOST, |
| 108 path: "prpc/milo.Buildbot/GetBuildbotBuildsJSON"); |
| 109 var body = { |
| 110 "master": client, |
| 111 "builder": botName, |
| 112 "limit": amount, |
| 113 "includeCurrent": true |
| 114 }; |
| 115 var result = await tryStartAsync(() => withCache( |
| 116 () => _makePostRequest(uri, JSON.encode(body), { |
| 117 HttpHeaders.CONTENT_TYPE: "application/json", |
| 118 HttpHeaders.ACCEPT: "application/json" |
| 119 }), |
| 120 '${uri.path}_${botName}_$amount')); |
| 121 return result.bind(JSON.decode).bind((json) { |
| 122 return json["builds"].map((b) { |
| 123 var build = JSON.decode(UTF8.decode(BASE64.decode(b["data"]))); |
| 124 return getBuildDetailFromJson(client, botName, build); |
| 125 }).toList(); |
| 126 }); |
| 127 } |
| 128 |
| 129 /// Calling the Milo Api to get information about a specific build |
| 130 /// where the field [buildNumber] is the build number to fetch. |
| 131 Future<Try<BuildDetail>> getBuildBotBuildDetails(String client, |
| 132 String botName, int buildNumber, WithCacheFunction withCache) async { |
| 133 var uri = new Uri( |
| 134 scheme: "https", |
| 135 host: LUCI_HOST, |
| 136 path: "prpc/milo.Buildbot/GetBuildbotBuildJSON"); |
| 137 var body = {"master": client, "builder": botName, "buildNum": buildNumber}; |
| 138 print(body); |
| 139 var result = await tryStartAsync(() => withCache( |
| 140 () => _makePostRequest(uri, JSON.encode(body), { |
| 141 HttpHeaders.CONTENT_TYPE: "application/json", |
| 142 HttpHeaders.ACCEPT: "application/json" |
| 143 }), |
| 144 '${uri.path}_${botName}_$buildNumber')); |
| 145 return result.bind(JSON.decode).bind((json) { |
| 146 var build = JSON.decode(UTF8.decode(BASE64.decode(json["data"]))); |
| 147 return getBuildDetailFromJson(client, botName, build); |
| 148 }); |
| 149 } |
| 150 |
| 151 /// [_makeGetRequest] performs a get request to [uri]. |
| 152 Future<String> _makeGetRequest(Uri uri) async { |
| 153 var request = await _client.getUrl(uri); |
| 154 var response = await request.close(); |
| 155 if (response.statusCode != 200) { |
| 156 response.drain(); |
| 157 throw new HttpException(response.reasonPhrase, uri: uri); |
| 158 } |
| 159 return response.transform(UTF8.decoder).join(); |
| 160 } |
| 161 |
| 162 /// [_makeGetRequest] performs a post request to [uri], where the posted |
| 163 /// body is the string representation of [body]. For adding custom headers |
| 164 /// use the map [headers]. |
| 165 Future<String> _makePostRequest( |
| 166 Uri uri, Object body, Map<String, String> headers) async { |
| 167 var response = await http.post(uri, body: body, headers: headers); |
| 168 if (response.statusCode != 200) { |
| 169 throw new HttpException(response.reasonPhrase, uri: uri); |
| 170 } |
| 171 // Prpc outputs a prefix to combat vulnerability. |
| 172 if (response.body.startsWith(")]}'")) { |
| 173 return response.body.substring(4); |
| 174 } |
| 175 return response.body; |
| 176 } |
| 177 |
| 178 /// Closes the Http client connection |
| 179 void close() { |
| 180 _client.close(); |
| 181 } |
| 182 } |
| 183 |
| 184 /// [getBuildDetailFromJson] parses json [build] to a class [BuildDetail] |
| 185 BuildDetail getBuildDetailFromJson( |
| 186 String client, String botName, dynamic build) { |
| 187 List<GitCommit> changes = build["sourceStamp"]["changes"].map((change) { |
| 188 return new GitCommit(change["revision"], change["revLink"], change["who"], |
| 189 change["comments"], change["files"].map((file) => file["name"])); |
| 190 }).toList(); |
| 191 |
| 192 List<BuildProperty> properties = build["properties"].map((prop) { |
| 193 return new BuildProperty(prop[0], prop[1].toString(), prop[2]); |
| 194 }).toList(); |
| 195 |
| 196 List<BuildStep> steps = build["steps"].map((step) { |
| 197 var start = |
| 198 new DateTime.fromMillisecondsSinceEpoch(step["times"][0] * 1000); |
| 199 DateTime end = null; |
| 200 if (step["times"][1] != null) { |
| 201 end = new DateTime.fromMillisecondsSinceEpoch(step["times"][1] * 1000); |
| 202 } |
| 203 return new BuildStep( |
| 204 step["name"], |
| 205 step["text"], |
| 206 step["results"].toString(), |
| 207 start, |
| 208 end, |
| 209 step["step_number"], |
| 210 step["isStarted"], |
| 211 step["isFinished"], |
| 212 step["logs"].map((log) => new BuildLog(log[0], log[1]))); |
| 213 }).toList(); |
| 214 |
| 215 DateTime end = null; |
| 216 if (build["times"][1] != null) { |
| 217 end = new DateTime.fromMillisecondsSinceEpoch(build["times"][1] * 1000); |
| 218 } |
| 219 |
| 220 Timing timing = new Timing( |
| 221 new DateTime.fromMillisecondsSinceEpoch(build["times"][0] * 1000), end); |
| 222 |
| 223 return new BuildDetail( |
| 224 client, |
| 225 botName, |
| 226 build["number"], |
| 227 build["text"].join(' '), |
| 228 build["finished"], |
| 229 steps, |
| 230 properties, |
| 231 build["blame"], |
| 232 timing, |
| 233 changes); |
| 234 } |
| 235 |
| 236 // Structured classes to relay information from api and web pages |
| 237 |
| 238 /// [LuciBuildBot] holds information about a build bot |
| 239 class LuciBuildBot { |
| 240 final String client; |
| 241 final String name; |
| 242 final String url; |
| 243 |
| 244 LuciBuildBot(this.client, this.name, this.url); |
| 245 |
| 246 @override |
| 247 String toString() { |
| 248 return "LuciBuildBot { client: $client, name: $name, url: $url }"; |
| 249 } |
| 250 } |
| 251 |
| 252 /// [BuildDetail] holds data detailing a specific build |
| 253 class BuildDetail { |
| 254 final String client; |
| 255 final String botName; |
| 256 final int buildNumber; |
| 257 final String results; |
| 258 final bool finished; |
| 259 final List<BuildStep> steps; |
| 260 final List<BuildProperty> buildProperties; |
| 261 final List<String> blameList; |
| 262 final Timing timing; |
| 263 final List<GitCommit> allChanges; |
| 264 |
| 265 BuildDetail( |
| 266 this.client, |
| 267 this.botName, |
| 268 this.buildNumber, |
| 269 this.results, |
| 270 this.finished, |
| 271 this.steps, |
| 272 this.buildProperties, |
| 273 this.blameList, |
| 274 this.timing, |
| 275 this.allChanges); |
| 276 |
| 277 @override |
| 278 String toString() { |
| 279 StringBuffer buffer = new StringBuffer(); |
| 280 buffer.writeln("--------------------------------------"); |
| 281 buffer.writeln(results); |
| 282 buffer.writeln(timing); |
| 283 buffer.writeln("----------------STEPS-----------------"); |
| 284 if (steps != null) steps.forEach(buffer.writeln); |
| 285 buffer.writeln("----------BUILD PROPERTIES------------"); |
| 286 if (buildProperties != null) buildProperties.forEach(buffer.writeln); |
| 287 buffer.writeln("-------------BLAME LIST---------------"); |
| 288 if (blameList != null) blameList.forEach(buffer.writeln); |
| 289 buffer.writeln("------------ALL CHANGES---------------"); |
| 290 if (allChanges != null) allChanges.forEach(buffer.writeln); |
| 291 return buffer.toString(); |
| 292 } |
| 293 } |
| 294 |
| 295 /// [BuildStep] holds data detailing a specific build |
| 296 class BuildStep { |
| 297 final String name; |
| 298 final String description; |
| 299 final String result; |
| 300 final DateTime start; |
| 301 final DateTime end; |
| 302 final int number; |
| 303 final bool isStarted; |
| 304 final bool isFinished; |
| 305 final List<BuildLog> logs; |
| 306 |
| 307 BuildStep(this.name, this.description, this.result, this.start, this.end, |
| 308 this.number, this.isStarted, this.isFinished, this.logs); |
| 309 |
| 310 @override |
| 311 String toString() { |
| 312 StringBuffer buffer = new StringBuffer(); |
| 313 buffer.writeln("${result == '[0, []]' ? 'SUCCESS' : result}: " |
| 314 "$name - $description ($start, $end)"); |
| 315 logs.forEach((subLink) { |
| 316 buffer.writeln("\t${subLink}"); |
| 317 }); |
| 318 return buffer.toString(); |
| 319 } |
| 320 } |
| 321 |
| 322 /// [BuildLog] holds log-information for a specific build. |
| 323 class BuildLog { |
| 324 final String name; |
| 325 final String url; |
| 326 |
| 327 BuildLog(this.name, this.url); |
| 328 |
| 329 @override |
| 330 String toString() { |
| 331 return "$name | $url"; |
| 332 } |
| 333 } |
| 334 |
| 335 /// [BuildProperty] descibes build properties of a specific build. |
| 336 class BuildProperty { |
| 337 final String name; |
| 338 final String value; |
| 339 final String source; |
| 340 |
| 341 BuildProperty(this.name, this.value, this.source); |
| 342 |
| 343 @override |
| 344 String toString() { |
| 345 return "$name\t$value\t$source"; |
| 346 } |
| 347 } |
| 348 |
| 349 /// [Timing] is a class to hold timing information for builds and steps. |
| 350 class Timing { |
| 351 final DateTime start; |
| 352 final DateTime end; |
| 353 |
| 354 Timing(this.start, this.end); |
| 355 |
| 356 @override |
| 357 String toString() { |
| 358 return "start: $start\tend: $end"; |
| 359 } |
| 360 } |
| 361 |
| 362 /// [GitCommit] holds data about a specific commit. |
| 363 class GitCommit { |
| 364 final String revision; |
| 365 final String commitUrl; |
| 366 final String changedBy; |
| 367 final String comments; |
| 368 final List<String> changedFiles; |
| 369 |
| 370 GitCommit(this.revision, this.commitUrl, this.changedBy, this.comments, |
| 371 this.changedFiles); |
| 372 |
| 373 @override |
| 374 String toString() { |
| 375 StringBuffer buffer = new StringBuffer(); |
| 376 buffer.writeln("revision: $revision"); |
| 377 buffer.writeln("commitUrl: $commitUrl"); |
| 378 buffer.writeln("changedBy: $changedBy"); |
| 379 buffer.write("\n"); |
| 380 buffer.writeln(comments); |
| 381 buffer.write("\nfiles:\n"); |
| 382 changedFiles.forEach(buffer.writeln); |
| 383 return buffer.toString(); |
| 384 } |
| 385 } |
OLD | NEW |