1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5// TODO(wittman): Convert this extension to event pages once they work with 6// the notifications API. Currently it's not possible to restore the 7// Notification object when event pages get reloaded. See 8// http://crbug.com/165276. 9 10(function() { 11 12var statusURL = "http://chromium-status.appspot.com/current?format=raw"; 13var statusHistoryURL = 14 "http://chromium-status.appspot.com/allstatus?limit=20&format=json"; 15var pollFrequencyInMs = 30000; 16var tryPollFrequencyInMs = 30000; 17 18var prefs = new buildbot.PrefStore; 19 20function updateBadgeOnErrorStatus() { 21 chrome.browserAction.setBadgeText({text:"?"}); 22 chrome.browserAction.setBadgeBackgroundColor({color:[0,0,255,255]}); 23} 24 25var lastNotification = null; 26function notifyStatusChange(treeState, status) { 27 if (lastNotification) 28 lastNotification.cancel(); 29 30 var notification = webkitNotifications.createNotification( 31 chrome.extension.getURL("icon.png"), "Tree is " + treeState, status); 32 lastNotification = notification; 33 notification.show(); 34} 35 36// The type parameter should be "open", "closed", or "throttled". 37function getLastStatusTime(callback, type) { 38 buildbot.requestURL(statusHistoryURL, "text", function(text) { 39 var entries = JSON.parse(text); 40 41 for (var i = 0; i < entries.length; i++) { 42 if (entries[i].general_state == type) { 43 callback(new Date(entries[i].date + " UTC")); 44 return; 45 } 46 } 47 }, updateBadgeOnErrorStatus); 48} 49 50function updateTimeBadge(timeDeltaInMs) { 51 var secondsSinceChangeEvent = Math.round(timeDeltaInMs / 1000); 52 var minutesSinceChangeEvent = Math.round(secondsSinceChangeEvent / 60); 53 var hoursSinceChangeEvent = Math.round(minutesSinceChangeEvent / 60); 54 var daysSinceChangeEvent = Math.round(hoursSinceChangeEvent / 24); 55 56 var text; 57 if (secondsSinceChangeEvent < 60) { 58 text = "<1m"; 59 } else if (minutesSinceChangeEvent < 57.5) { 60 if (minutesSinceChangeEvent < 30) { 61 text = minutesSinceChangeEvent + "m"; 62 } else { 63 text = Math.round(minutesSinceChangeEvent / 5) * 5 + "m"; 64 } 65 } else if (minutesSinceChangeEvent < 5 * 60) { 66 var halfHours = Math.round(minutesSinceChangeEvent / 30); 67 text = Math.floor(halfHours / 2) + (halfHours % 2 ? ".5" : "") + "h"; 68 } else if (hoursSinceChangeEvent < 23.5) { 69 text = hoursSinceChangeEvent + "h"; 70 } else { 71 text = daysSinceChangeEvent + "d"; 72 } 73 74 chrome.browserAction.setBadgeText({text: text}); 75} 76 77var lastState; 78var lastChangeTime; 79function updateStatus(status) { 80 var badgeState = { 81 open: {color: [0,255,0,255], defaultText: "\u2022"}, 82 closed: {color: [255,0,0,255], defaultText: "\u00D7"}, 83 throttled: {color: [255,255,0,255], defaultText: "!"} 84 }; 85 86 chrome.browserAction.setTitle({title:status}); 87 var treeState = (/open/i).exec(status) ? "open" : 88 (/throttled/i).exec(status) ? "throttled" : "closed"; 89 90 if (lastState && lastState != treeState) { 91 prefs.getUseNotifications(function(useNotifications) { 92 if (useNotifications) 93 notifyStatusChange(treeState, status); 94 }); 95 } 96 97 chrome.browserAction.setBadgeBackgroundColor( 98 {color: badgeState[treeState].color}); 99 100 if (lastChangeTime === undefined) { 101 chrome.browserAction.setBadgeText( 102 {text: badgeState[treeState].defaultText}); 103 lastState = treeState; 104 getLastStatusTime(function(time) { 105 lastChangeTime = time; 106 updateTimeBadge(Date.now() - lastChangeTime); 107 }, treeState); 108 } else { 109 if (treeState != lastState) { 110 lastState = treeState; 111 // The change event will occur 1/2 the polling frequency before we 112 // are aware of it, on average. 113 lastChangeTime = Date.now() - pollFrequencyInMs / 2; 114 } 115 updateTimeBadge(Date.now() - lastChangeTime); 116 } 117} 118 119function requestStatus() { 120 buildbot.requestURL(statusURL, 121 "text", 122 updateStatus, 123 updateBadgeOnErrorStatus); 124 setTimeout(requestStatus, pollFrequencyInMs); 125} 126 127// Record of the last defunct build number we're aware of on each builder. If 128// the build number is less than or equal to this number, the buildbot 129// information is not available and a request will return a 404. 130var lastDefunctTryJob = {}; 131 132function fetchTryJobResults(fullPatchset, builder, buildnumber, completed) { 133 var tryJobURL = 134 "http://build.chromium.org/p/tryserver.chromium/json/builders/" + 135 builder + "/builds/" + buildnumber; 136 137 if (lastDefunctTryJob.hasOwnProperty(builder) && 138 buildnumber <= lastDefunctTryJob[builder]) { 139 completed(); 140 return; 141 } 142 143 buildbot.requestURL(tryJobURL, "json", function(tryJobResult) { 144 if (!fullPatchset.full_try_job_results) 145 fullPatchset.full_try_job_results = {}; 146 147 var key = builder + "-" + buildnumber; 148 fullPatchset.full_try_job_results[key] = tryJobResult; 149 150 completed(); 151 }, function(errorStatus) { 152 if (errorStatus == 404) { 153 lastDefunctTryJob[builder] = 154 Math.max(lastDefunctTryJob[builder] || 0, buildnumber); 155 } 156 completed(); 157 }); 158} 159 160// Enums corresponding to how much state has been loaded for an issue. 161var PATCHES_COMPLETE = 0; 162var TRY_JOBS_COMPLETE = 1; 163 164function fetchPatches(issue, updatedCallback) { 165 // Notify updated once after receiving all patchsets, and a second time after 166 // receiving all try job results. 167 var patchsetsRetrieved = 0; 168 var tryJobResultsOutstanding = 0; 169 issue.patchsets.forEach(function(patchset) { 170 var patchURL = "https://codereview.chromium.org/api/" + issue.issue + 171 "/" + patchset; 172 173 buildbot.requestURL(patchURL, "json", function(patch) { 174 if (!issue.full_patchsets) 175 issue.full_patchsets = {}; 176 177 issue.full_patchsets[patch.patchset] = patch; 178 179 // TODO(wittman): Revise to reduce load on the try servers. Repeatedly 180 // loading old try results increases the size of the working set of try 181 // jobs on the try servers, causing them to become disk-bound. 182 // patch.try_job_results.forEach(function(results) { 183 // if (results.buildnumber) { 184 // tryJobResultsOutstanding++; 185 186 // fetchTryJobResults(patch, results.builder, results.buildnumber, 187 // function() { 188 // if (--tryJobResultsOutstanding == 0) 189 // updatedCallback(TRY_JOBS_COMPLETE); 190 // }); 191 // } 192 // }); 193 194 if (++patchsetsRetrieved == issue.patchsets.length) { 195 updatedCallback(PATCHES_COMPLETE); 196 // TODO(wittman): Remove once we revise the try job fetching code. 197 updatedCallback(TRY_JOBS_COMPLETE); 198 } 199 }); 200 }); 201} 202 203function updateTryStatus(status) { 204 var seen = {}; 205 var activeIssues = buildbot.getActiveIssues(); 206 status.results.forEach(function(result) { 207 var issueURL = "https://codereview.chromium.org/api/" + result.issue; 208 209 buildbot.requestURL(issueURL, "json", function(issue) { 210 fetchPatches(issue, function(state) { 211 // If the issue already exists, wait until all the issue state has 212 // loaded before updating the issue so we don't lose try job information 213 // from the display. 214 if (activeIssues.getIssue(issue.issue)) { 215 if (state == TRY_JOBS_COMPLETE) 216 activeIssues.updateIssue(issue); 217 } else { 218 activeIssues.updateIssue(issue); 219 } 220 }); 221 }); 222 223 seen[result.issue] = true; 224 }); 225 226 activeIssues.forEach(function(issue) { 227 if (!seen[issue.issue]) 228 activeIssues.removeIssue(issue); 229 }); 230} 231 232function fetchTryStatus(username) { 233 if (!username) 234 return; 235 236 var url = "https://codereview.chromium.org/search" + 237 // commit=2 is CLs with commit bit set, commit=3 is CLs with commit 238 // bit cleared, commit=1 is either. 239 "?closed=3&commit=1&limit=100&order=-modified&format=json&owner=" + 240 username.trim(); 241 buildbot.requestURL(url, "json", updateTryStatus); 242} 243 244function requestTryStatus() { 245 var searchBaseURL = "https://codereview.chromium.org/search"; 246 247 prefs.getTryJobUsername(function(username) { 248 if (username == null) { 249 var usernameScrapingURL = "https://codereview.chromium.org/search"; 250 // Try scraping username from Rietveld if unset. 251 buildbot.requestURL(usernameScrapingURL, "text", function(text) { 252 var match = /([^<>\s]+@\S+)\s+\(.+\)/.exec(text); 253 if (match) { 254 username = match[1]; 255 prefs.setTryJobUsername(username); 256 fetchTryStatus(username); 257 } 258 }); 259 } else { 260 fetchTryStatus(username); 261 } 262 263 setTimeout(requestTryStatus, tryPollFrequencyInMs); 264 }); 265} 266 267function main() { 268 requestStatus(); 269 requestTryStatus(); 270} 271 272main(); 273 274})(); 275