summarylogtreecommitdiffstats
path: root/aw-snap-reloader-background.js
blob: d7324e7c97bc8ac7e3b8d0524b2d7469dffca8a0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
// Inspiration for this extension:
// http://zerodeveloper.tumblr.com/post/67664299242/chrome-extension-reload-tab-after-crash
// and
// https://github.com/unclespode/ohnoyoudidnt

// Different ways to test crashes and other problems in Chrome/Chromium:
// In Chrome/Chromium see chrome://about under "For debug", currenlty
// the following options are available:
// chrome://badcastcrash/
// chrome://crash/
// chrome://crashdump/
// chrome://kill/
// chrome://hang/
// chrome://shorthang
// chrome://gpuclean/
// chrome://gpucrash/
// chrome://gpuhang/
// chrome://memory-exhaust/
// chrome://ppapiflashcrash/
// chrome://ppapiflashhang/
// chrome://quit/
// chrome://restart/
//
// Another option is to open the developer tools on a tab that should be
// crashed and in the JavaScript console type:
//
// var memoryHog = "more"; while(true) {memoryHog = memoryHog + "andMore";}
//
// This code will consume so much memory that the tab will crash

// About reloading of tabs; see: https://developer.chrome.com/extensions/tabs
// chrome.tabs.reload(integer tabId, object reloadProperties, function callback)
//   tabId (integer optional): The ID of the tab to reload;
//         defaults to the selected tab of the current window.
//   reloadProperties (object optional): bypassCache (boolean optional): Whether
//         using any local cache. Default is false.
//   callback (function optional): If you specify the callback parameter,
//         it should be a function that looks like this: function() {...};
//
// For the use case of reloading a kiosk app or tabs that the user is using
// the default of calling chrome.tabs.reload() is best

var tabSuccessCount = {}; // store of succesful probe calls
var tabUnresponsiveCount = {}; // store of probe calls that got stuck in limbo
// Interval between checks, do not make this too short (
// less than 1 second seems unwise, better no less than
// 2 seconds) since it is also used to determine if a
// tab is unresponsive
var checkInterval = 10000; // in milliseconds
var tabsChecked = {}; // store for arrays of tab-ids that were checked
var checkIndex = 0; // index of current array of checked tab-ids
var nrTabs = 0; // current number of tabs

function reloadTabIfNeeded(tab) {
  return function(result) {
    if (tabCrashed()) {
      console.log("Crashed tab: title=" + (tab.title || "") + " id=" + tab.id +
                  " index=" + tab.index.toString() + " windowId=" + tab.windowId.toString() +
                  " sessionId=" + (tab.sessionId || "").toString() +
                  " highlighted=" + tab.highlighted.toString() + " active=" + tab.active.toString());
      if (tabShouldBeReloaded(tab)) {
        console.log("Reload tab:" + tab.id.toString());
        chrome.tabs.reload(tab.id);
      }
    } else {
      registerSuccessfulNoOp(tab);
    }
    tabsChecked[checkIndex].push(tab.id);
  };
}

function tabShouldBeReloaded(tab) {
  // Reload if at least one sucessful no-op has occurred.
  // This might be too causious but ensures the tab was working
  // before it crashed
  // this also ensures that we do not reload a tab that takes a long
  // time to load (being unresponsive whilst doing so)
  return tabSuccessCount[tab.id] > 0;
}

function registerSuccessfulNoOp(tab) {
  if ((tabSuccessCount[tab.id] || null) === null) {
    tabSuccessCount[tab.id] = 0;
  }
  tabSuccessCount[tab.id] += 1;
  tabUnresponsiveCount[tab.id] = 0;
}

function registerUnresponsive(tab) {
  if ((tabUnresponsiveCount[tab.id] || null) === null) {
    tabUnresponsiveCount[tab.id] = 0;
  }
  tabUnresponsiveCount[tab.id] += 1;
}

function tabCrashed() {
  // The crux of finding a crashed tab:
  // If an operation (even a no-op) is executed on a crashed tab an error
  // is reported which is available as the chrome.runtime.lastError
  // The error incorrectly reports the tab was closed instead of the fact that the
  // tab does not respond.
  return chrome.runtime.lastError && chrome.runtime.lastError.message === "The tab was closed.";
}

function checkTab(thisTab) {
  if (relevantTab(thisTab)) {
    // Perform a no-op as a probe to find if the tab reponds
    chrome.tabs.executeScript(thisTab.id, {
      // To find crashed tabs probing with a no-op is enough
      // code: "null;"
      // To find unresponsive tabs probing with some operation
      // that takes CPU-cycles is needed
      code: "1 + 1;"
    }, reloadTabIfNeeded(thisTab));
  }
}

function relevantTab(tab){
  // Only check tabs that have finished loading
  // and that use the http or https protocol.
  // This ignores tabs like chrome://...
  // return tab.url.substring(0, 4) == "http" && tab.status == "complete";
  // This makes testing this extension more difficult, to test use
  // the line below
  return tab.status == "complete";
}

function reloadUnresposiveTabs(index, nrTabs, tabs) {
  if (nrTabs === tabs.length && nrTabs > tabsChecked[index].length) {
    var nrTabsToFind = nrTabs - tabsChecked[index].length;
    console.log("Found " + nrTabsToFind.toString() + " unresponsive tabs");
    for (var j = 0; j < nrTabs && nrTabsToFind > 0; j += 1) {
      if (tabsChecked[index].indexOf(tabs[j].id) == -1) {
        registerUnresponsive(tabs[j]);
        if (tabShouldBeReloaded(tabs[j])) {
          console.log("Reload unresponsive tab:" + tabs[j].id.toString());
          // Reloading an unresponsive tab does not work
          // chrome.tabs.reload(tabs[j].id);
          // Therefore a new tab is created with the url of the old tab
          // and the unresponsive tab is removed.
          // Setting the new tab as active is mainly aimed at kiosk-like applications
          chrome.tabs.create({url: tabs[j].url, active: true}, function(tab){
            console.log("Created new tab: id=" + tab.id.toString() + " title=" + (tab.title || "") + " url=" + (tab.url || ""));
          });
          chrome.tabs.remove(tabs[j].id, function(){
            console.log("Removed unresponsive tab:" + tabs[j].id.toString());
          });
        }
        nrTabsToFind -= 1;
      }
    }
  }
}

function checkTabs(tabs) {
  // check for unresponsive tabs by checking the results of
  // the previous round of checkTab calls
  // NOTE: it is assumed all tabs have been checked (all callbacks
  // initiated in the previous checkTabs call have ended (apart
  // form the ones that were done on unresponsive tabs)). The
  // checkInterval has to be long enough to make this a "certainty"
  if (nrTabs > 0) {
    reloadUnresposiveTabs(checkIndex, nrTabs, tabs);
  }

  // roll the checkIndex around after 10 iterations
  checkIndex = checkIndex > 9 ? 0 : (checkIndex + 1);
  nrTabs = tabs.length;
  tabsChecked[checkIndex] = [];
  for (var i = 0; i < tabs.length; i += 1) {
    checkTab(tabs[i]);
  }
}

// Reset the count for tabs that are closed or that change
function tabChanged(tabId, changeInfo, tab) {
  console.log("Resetting Stats for tab: id=" + tabId.toString() + " title=" +
              (tab !== undefined ? tab.title : ""));
  tabSuccessCount[tabId] = 0;
  tabUnresponsiveCount[tabId] = 0;
}

setInterval(function() {
  chrome.tabs.query({}, checkTabs);
}, checkInterval);

// If the tab reloads, reset stats
chrome.tabs.onUpdated.addListener(tabChanged);
chrome.tabs.onRemoved.addListener(tabChanged);