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