Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: lib/browser.js

Issue 9615013: Crawler, first version (Closed)
Patch Set: Created March 6, 2013, 4:05 a.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 let {Logger} = require( "logger" );
2
3 //-------------------------------------------------------
4 // Tabbed_Browser
5 //-------------------------------------------------------
6 /**
7 * A single OS-level window of a multiple-tab Firefox browser. This is the objec t referred to by the global 'gBrowser'.
8 *
9 * @param {Window} window
10 * @param {Number} max_requests
11 * The maximum number of simultaneous requests this object may have.
12 * @constructor
13 */
14 var Tabbed_Browser = function( window, max_requests )
15 {
16 /**
17 * Browser window through which we access the global browser object.
18 * @type {Window}
19 */
20 this.window = window;
21
22 /**
23 * A browser object that can hold multiple individual tabbed browser panes.
24 */
25 this.tabbed_browser = this.window.gBrowser;
26
27 /**
28 * The current number of pending requests in child tabs of this object.
29 * @type {Number}
30 */
31 this.n_requests = 0;
32
33 /**
34 * The maximum number of simultaneous requests this object may have.
35 * @type {Number}
36 */
37 this.max_requests = max_requests;
38
39 /**
40 * The heart of the dispatcher for both handling progress events and trackin g block activity is this map from
41 * browser objects to Browser_Tab ones.
42 * @type {Map}
43 */
44 this.map_browser_to_child = new Map();
45
46 /**
47 * Every object in the range of the WeakMap has this object as its prototype . This enables map values to have
48 * sane defaults.
49 *
50 * @type {Object}
51 */
52 //this.map_range_prototype = { browser: null };
53
54 /**
55 * A transient set for allocated requests that have not started their load c ycle.
56 * @type {Set}
57 */
58 this.allocated_not_loaded = new Set();
59
60 this.listener = { onStateChange: this._progress.bind( this ) };
61 this.tabbed_browser.addTabsProgressListener( this.listener );
62
63 this.logger = new Logger( "Tabbed_Browser" );
64 };
65
66 /**
67 * Release resources held by this object. This includes event handlers. We also close all the child tabs, since they
68 * won't work right after our progress event handler is no longer registered.
69 */
70 Tabbed_Browser.prototype.close = function()
71 {
72 var log = this.logger.make_log( "close" );
73 log( "Tabbed_Browser.close", false );
74 if ( this.listener )
75 {
76 this.tabbed_browser.removeTabsProgressListener( this.listener );
77 this.listener = null;
78 }
79
80 let pair = null;
81 for ( pair of this.map_browser_to_child )
82 {
83 let [ key, value ] = pair;
84 value.child.close();
85 this.map_browser_to_child.delete( key );
86 }
87 };
88
89 /**
90 * Predicate "is there an open request slot?"
91 */
92 Tabbed_Browser.prototype.available = function()
93 {
94 return this.n_requests < this.max_requests;
95 };
96
97 /**
98 * Predicate: "Are there no open tabs?"
99 * @return {boolean}
100 */
101 Tabbed_Browser.prototype.quiescent = function()
102 {
103 return this.n_requests == 0;
104 };
105
106 /**
107 * @param {Boolean} [leave_open=false]
108 * Leave the tab open in the browser after closing the present object
109 */
110 Tabbed_Browser.prototype.make_tab = function( leave_open )
111 {
112 return new Browser_Tab( this, leave_open );
113 };
114
115 /**
116 * Request an allocation of available HTTP requests. Allocates one if available.
117 * <p/>
118 * HAZARD: This request is made when the asynchronous action is created, which i s strictly before it is launched. If
119 * the caller does not either launch the action or close it, there will be an in ternal resource leak here.
120 *
121 * @param child
122 * @return {Boolean}
123 */
124 Tabbed_Browser.prototype.request_load = function( child )
125 {
126 if ( !this.available() )
127 {
128 return false;
129 }
130 ++this.n_requests;
131 this.allocated_not_loaded.add( child );
132 return true;
133 };
134
135 /**
136 * Notification that a child tab is loading a page. This constitutes a change in the number of unallocated requests.
137 *
138 * @param {Browser_Tab} child
139 */
140 Tabbed_Browser.prototype.notify_load_begin = function( child )
141 {
142 if ( this.allocated_not_loaded.has( child ) )
143 {
144 this.allocated_not_loaded.delete( child );
145 }
146 else
147 {
148 Cu.reportError( "notice_load_begin: child not found" );
149 throw "notice_load_begin: child not found";
150 }
151 let value = { child: child };
152 this.map_browser_to_child.set( child.browser, value );
153 };
154
155 /**
156 * Notification that a child tab is loading a page. This constitutes a change in the number of unallocated requests.
157 * <p/>
158 * The child must only call this function once, since it acts as a resource deal locator, freeing up a request slot.
159 */
160 Tabbed_Browser.prototype.notify_load_end = function()
161 {
162 if ( this.n_requests <= 0 )
163 {
164 throw "Tabbed_Browser.notify_load_end: n_requests <= 0";
165 }
166 --this.n_requests;
167 };
168
169 /**
170 * Notification that a child tab is closing. We leave the tab present in our map of active children until the tab is
171 * closed. This allows us to handle events that occur after the document has loa ded, which typically arise from
172 * scripts on the page.
173 *
174 * @param child
175 */
176 Tabbed_Browser.prototype.notify_close = function( child )
177 {
178 if ( this.map_browser_to_child.has( child.browser ) )
179 {
180 this.map_browser_to_child.delete( child.browser );
181 }
182 else
183 {
184 // If we're getting this notice, it really should be in our map
185 Cu.reportError( "Child browser not found in map during 'notice_close()'" );
186 }
187 };
188
189 //noinspection JSUnusedLocalSymbols
190 /**
191 * Progress event handler. It looks only for STOP states on the present tab. Whe n that happens, it determines the
192 * success status and calls the landing function.
193 *
194 * @param {*} browser
195 * @param {nsIWebProgress} controller
196 * The control object for progress monitoring that dispatches the event.
197 * @param {nsIRequest} browse_request
198 * The request object generated by the called to addTab(), which loads a pa ge.
199 * @param state
200 * The progress state, represented as flags.
201 * @param stop_status
202 * Status code for success or failure if the argument state is a STOP state .
203 */
204 Tabbed_Browser.prototype._progress = function( browser, controller, browse_reque st, state, stop_status )
205 {
206 /*
207 * We only care about STOP states. We're not tracking redirects, which is on e of the progress states possible.
208 * We may want to in the future, though, in case redirect behavior is involv ed with ad delivery in some way.
209 *
210 * As a point of warning, traces on these messages shows that the START mess age is delivered to the present
211 * function _before_ 'notify_load_begin' is called, which seems to mean that the JS interpreter is doing something
212 * fishy, either using a second thread or dispatching during a function invo cation or return. Regardless, this
213 * event come in before it's possible that 'map_browser_to_child' has the 'b rowser' element of a new tab as a key.
214 * Thus, a warning that trapping any other progress state here should happen only after thoroughly tracing the
215 * event sequence to determine the actual behavior.
216 */
217 //noinspection JSBitwiseOperatorUsage
218 if ( !(state & Ci.nsIWebProgressListener.STATE_STOP) )
219 return;
220
221 /*
222 * This handler receives events for all the tabs present in a tabbrowser ele ment, even ones that we didn't
223 * add ourselves. It's not an error to receive such events.
224 */
225 if ( !this.map_browser_to_child.has( browser ) )
226 {
227 return;
228 }
229
230 var {child} = this.map_browser_to_child.get( browser );
231 child._stop( stop_status );
232
233 var log = this.logger.make_log( "_progress" );
234 log( "request name = " + browse_request.name, false );
235 };
236
237 //-------------------------------------------------------
238 // Browser_Tab
239 //-------------------------------------------------------
240 /**
241 * A single browser tab that can asynchronously load a web page.
242 *
243 * @param {Tabbed_Browser} parent
244 * @param {Boolean} [leave_open=false]
245 * Leave the tab open in the browser after closing the present object
246 * @constructor
247 */
248 var Browser_Tab = function( parent, leave_open )
249 {
250 /**
251 * The parent tabbed browser in whose tab set this tab is a member.
252 * @type {Tabbed_Browser}
253 */
254 this.parent = parent;
255
256 /**
257 * Leave the tab open in the browser after the crawler exits. The reason to do this is to allow manual inspection
258 * of the window as the crawler loaded it.
259 * <p/>
260 * It's necessary to call 'close()' on any instance of this object in order to ensure event handlers are released.
261 * This is true whether or not the tab remains open afterwards.
262 *
263 * @type {Boolean}
264 */
265 this.leave_open = (arguments.length >= 2) ? leave_open : false;
266
267 /**
268 * A browser object that can hold multiple individual tabbed browser panes.
269 */
270 this.tabbed_browser = this.parent.tabbed_browser;
271
272 /**
273 * Our tab within the tabbed browser. This is the "external" view of browser pane, the one that allows us to
274 * control loading. The tab must have a URL associated with it, so it's not displayed at the outset
275 * <p/>
276 * FUTURE: Might it be useful to load the tab with a empty page but descript ive title at construction time?
277 */
278 this.tab = null;
279
280 /**
281 * The function to be run upon completion of an asynchronous load.
282 * @type {Function}
283 */
284 this.finally_f = null;
285
286 /**
287 * The function to be run if there's an exceptional termination to an asynch ronous load.
288 * @type {Function}
289 */
290 this.catch_f = null;
291
292 /**
293 *
294 * @type {*}
295 */
296 this.browser = null;
297
298 /**
299 * STATE
300 */
301 this.state = Browser_Tab.STATE.CREATED;
302 };
303
304 Browser_Tab.STATE = {
305 // Initial state
306 CREATED: 0,
307 // Nonterminal states
308 LOADING: 1,
309 // Terminal states
310 DISPLAYED: 2,
311 ERROR: 3,
312 CLOSED: 4
313 };
314
315 /**
316 * Predicate "is the Browser_Tab in an initial state for its page load?"
317 *
318 * @return {Boolean}
319 */
320 Browser_Tab.prototype.in_initial_state = function()
321 {
322 return this.state == Browser_Tab.STATE.CREATED;
323 };
324
325 /**
326 * Predicate "is this object in a final state for its page load?"
327 * <p/>
328 * The CLOSED state is considered a final state, although it's present to implem ent the moral equivalent of a
329 * destructor correctly.
330 *
331 * @return {Boolean}
332 */
333 Browser_Tab.prototype.in_final_state = function()
334 {
335 return this.state >= Browser_Tab.STATE.DISPLAYED;
336 };
337
338 /**
339 * Close function destroys our allocated host resources, such as tabs, listeners , requests, etc.
340 */
341 Browser_Tab.prototype.close = function()
342 {
343 if ( this.state == Browser_Tab.STATE.CLOSED )
344 return;
345
346 if ( this.tab )
347 {
348 if ( !this.leave_open )
349 {
350 this.tabbed_browser.removeTab( this.tab );
351 }
352 this.tab = null;
353 /*
354 * Kill the map from our associated browser to this object. This is the point at which we can no longer
355 * locate this object with a 'browser' or 'window' object.
356 */
357 this.parent.notify_close( this );
358 this.browser = null;
359 }
360 /*
361 * FUTURE: Cancel any pending page load here.
362 */
363 this.state = Browser_Tab.STATE.CLOSED;
364 };
365
366 /**
367 * Return an asynchronous action that loads a target into a new tab.
368 */
369 Browser_Tab.prototype.load = function( target )
370 {
371 if ( !this.parent.request_load( this ) )
372 {
373 // Should not reach. The caller should be calling available() on the Tab bed_Browser first.
374 return null;
375 }
376 return {
377 go: function( finally_f, catch_f )
378 {
379 this.finally_f = finally_f;
380 this.catch_f = catch_f;
381 this._show( target );
382 }.bind( this )
383 };
384 };
385
386 /**
387 * Show the tab by loading a URL target into it.
388 *
389 * @param {String} target
390 */
391 Browser_Tab.prototype._show = function( target )
392 {
393 if ( !this.in_initial_state() )
394 return;
395 try
396 {
397 this.tab = this.tabbed_browser.addTab( target );
398 this.browser = this.tabbed_browser.getBrowserForTab( this.tab );
399 this.parent.notify_load_begin( this );
400 }
401 catch ( e )
402 {
403 this.state = Browser_Tab.STATE.ERROR;
404 Cu.reportError( "Unexpected exception in Browser_Tab.show(): " + e.toStr ing() );
405 if ( this.catch_f ) this.catch_f( e );
406 if ( this.finally_f ) this.finally_f( false, undefined );
407 }
408 };
409
410 /**
411 * Stop event handler. It receives only STOP events on the present tab. When tha t happens, it determines the
412 * success status and calls the landing function.
413 *
414 * @param stop_status
415 * Status code for success or failure if the argument state is a STOP state .
416 */
417 Browser_Tab.prototype._stop = function( stop_status )
418 {
419 /*
420 * This check ensures that we only call 'finally_f' once. The browser will s end multiple STOP events when the user
421 * focuses on a tab window by clicking on its tab. Since we set a final stat e below, checking for a final state
422 * ensures that we act idempotently.
423 *
424 * This check also forestalls a race condition where a request completes and schedules a progress event while we are
425 * closing the object.
426 */
427 if ( this.in_final_state() )
428 return;
429
430 /*
431 * This notice back to the parent must happen after the check for being in a final state. Since multiple STOP
432 * events may arrive on a tab (they're not all for the original document), w e send this notice just once, which
433 * means that we need the state in this Browser_Tab instance. The choice is to make a call back to the parent
434 * instead of exposing this object's internal state.
435 */
436 this.parent.notify_load_end();
437
438 var success = ( stop_status == 0 );
439 if ( success )
440 {
441 this.state = Browser_Tab.STATE.DISPLAYED;
442 }
443 else
444 {
445 this.state = Browser_Tab.STATE.ERROR;
446 /**
447 * This argument is an XPCOM 'nsresult' value. It could be examined if t he cause of the failure to load needs
448 * to be diagnosed. For example, NS_ERROR_OFFLINE would be useful for su spending operation of the crawler while
449 * internet connectivity comes back. NS_ERROR_MALFORMED_URI would be use ful for notifing the user of a typo.
450 */
451 this.error_code = stop_status;
452 }
453 if ( this.finally_f ) this.finally_f( success, this.error_code );
454 };
455
456 exports.Tabbed_Browser = Tabbed_Browser;
457 exports.Browser_Tab = Browser_Tab;
OLDNEW
« .hgignore ('K') | « chrome/skin/text-x-source.png ('k') | lib/client.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld