Add documentation
Tails developers
9 years ago
43 | 43 | logging.getLogger('stem').setLevel(logging.WARNING) |
44 | 44 | |
45 | 45 | class TorMonitorWindow(Gtk.ApplicationWindow): |
46 | """Tor Monitor main window | |
47 | ||
48 | This class contains all the UI and the logic to update. | |
49 | """ | |
46 | 50 | |
47 | 51 | # Constants used to distinguish circuits and streams in our TreeStore |
48 | 52 | TYPE_CIRC = 0 |
49 | 53 | TYPE_STREAM = 1 |
50 | 54 | |
51 | 55 | def __init__(self, app): |
56 | """Create a new TorMonitorWindow | |
57 | ||
58 | :var Gtk.Application app: the application which own the window | |
59 | """ | |
52 | 60 | Gtk.Window.__init__(self, application=app) |
53 | 61 | |
54 | 62 | self._listeners_initialized = False |
70 | 78 | self._infobar.show() |
71 | 79 | |
72 | 80 | def _create_ui(self): |
81 | """Creates the user interface | |
82 | """ | |
73 | 83 | self.set_default_size(600, 400) |
74 | 84 | self.set_icon_name('tormonitor') |
75 | 85 | self.connect('delete-event', self.delete_event_cb) |
129 | 139 | self.show_all() |
130 | 140 | |
131 | 141 | def delete_event_cb(self, widget, event, data=None): |
142 | """Hide the window immediately on close. | |
143 | ||
144 | This callback is connected to window's 'delete-event' and forces Gtk to | |
145 | hide the window while waiting for all threads to close. | |
146 | """ | |
132 | 147 | logging.info("quitting, waiting all threads to return...") |
133 | 148 | self.hide() |
134 | 149 | while Gtk.events_pending(): |
139 | 154 | # ===================== |
140 | 155 | |
141 | 156 | def init_listeners(self): |
142 | # Connect our handlers to Tor event listeners. Our handlers won't be | |
143 | # executed in the main thread. | |
157 | """Connect our handlers to Tor event listeners | |
158 | """ | |
159 | # These handlers won't be executed in the main thread, they will have to | |
160 | # do the real work in another method executed in the main thread by | |
161 | # GLib.idle_add in order not to make Gtk crazy. | |
144 | 162 | if not self._listeners_initialized: |
145 | 163 | self.controller.add_event_listener(self.update_circ_handler, |
146 | 164 | stem.control.EventType.CIRC) |
150 | 168 | self._listeners_initialized = True |
151 | 169 | |
152 | 170 | def update_circ_handler(self, circ_event): |
171 | """Handler for stem.control.EventType.CIRC | |
172 | """ | |
153 | 173 | # Handle the event in main thread |
154 | 174 | GLib.idle_add(self.update_circ_cb, circ_event) |
155 | 175 | |
156 | 176 | def update_stream_handler(self, stream_event): |
177 | """Handler for stem.control.EventType.STREAM | |
178 | """ | |
157 | 179 | # Handle the event in main thread |
158 | 180 | GLib.idle_add(self.update_stream_cb, stream_event) |
159 | 181 | |
160 | 182 | def update_status_handler(self, controller, state, timestamp): |
183 | """Handler for stem.control.BaseController.add_status_listener | |
184 | """ | |
161 | 185 | if state == stem.control.State.CLOSED: |
162 | 186 | GLib.idle_add(self.connection_closed_cb) |
163 | 187 | GLib.timeout_add_seconds(1, self.controller_reconnect_cb) |
168 | 192 | # ======================= |
169 | 193 | |
170 | 194 | def connection_closed_cb(self): |
195 | """Update the UI after we lost conection to the Tor daemon | |
196 | ||
197 | This callback is called when we lost connection with the Tor daemon. | |
198 | ||
199 | :returns: **False** | |
200 | """ | |
171 | 201 | logging.debug("Controller connection closed") |
172 | 202 | self._hbox.set_sensitive(False) |
173 | 203 | self._infobar_label.set_text(_("Lost connection to the Tor daemon. " |
174 | 204 | "Tor Monitor will try to reconnect...")) |
175 | 205 | self._infobar.set_message_type(Gtk.MessageType.ERROR) |
176 | 206 | self._infobar.show() |
207 | self._treestore.clear() | |
177 | 208 | return False |
178 | 209 | |
179 | 210 | def connection_init_cb(self): |
211 | """Update the UI after a (re)connection to the Tor daemon | |
212 | ||
213 | This callback is called when we (re)connect to the Tor daemon. | |
214 | ||
215 | :returns: **False** | |
216 | """ | |
180 | 217 | logging.debug("Controller initialized") |
181 | 218 | self._hbox.set_sensitive(True) |
182 | 219 | self._infobar_label.set_text(_("Reconnected to the Tor daemon! " |
189 | 226 | return False |
190 | 227 | |
191 | 228 | def controller_reconnect_cb(self): |
229 | """Try to reconnect to the Tor daemon | |
230 | ||
231 | This callback is called regularly by self.update_status_handler if the | |
232 | connection to the Tor daemon is lost. It calls self.connection_init_cb | |
233 | after a successful reconnection. | |
234 | ||
235 | :returns: **bool** that's **False** if we reconnected successfully and | |
236 | **True** if we failed to reconnect (so that GLib.timeout_add will call | |
237 | the method again). | |
238 | """ | |
192 | 239 | logging.debug("Trying to reconnect the controller") |
193 | 240 | try: |
194 | 241 | self.controller.connect() |
199 | 246 | return False |
200 | 247 | |
201 | 248 | def controller_connect_cb(self): |
249 | """Try to connect to the Tor daemon for the 1st time | |
250 | ||
251 | This callback is called regularly by self.__init__ if there is no | |
252 | connection to the Tor daemon at startup. It calls | |
253 | self.connection_init_cb after a successful connection. | |
254 | ||
255 | :returns: **bool** that's **False** if we connected successfully and | |
256 | **True** if we failed to connect (so that GLib.timeout_add will call | |
257 | the method again). | |
258 | """ | |
202 | 259 | logging.debug("Trying to connect the controller") |
203 | 260 | controller = self.get_application().connect_controller() |
204 | 261 | if controller: |
212 | 269 | # ========================= |
213 | 270 | |
214 | 271 | def remove_treeiter(self, treeiter): |
272 | """Remove a treeiter from our circuits/streams list if it is valid | |
273 | ||
274 | :var Gtk.TreeIter treeiter: the treeiter to remove | |
275 | ||
276 | :returns: **False** | |
277 | """ | |
215 | 278 | if self._treestore.iter_is_valid(treeiter): |
216 | 279 | self._treestore.remove(treeiter) |
217 | 280 | else: |
226 | 289 | |
227 | 290 | @staticmethod |
228 | 291 | def circuit_label(circuit): |
292 | """Returns a label for a circuit | |
293 | ||
294 | :var stem.response.events.CircuitEvent circuit: the circuit | |
295 | ||
296 | :returns: **str** representing the circuit | |
297 | """ | |
229 | 298 | if circuit.path: |
230 | 299 | circ_str = _(', ').join([nick for fp, nick in circuit.path]) |
231 | 300 | else: |
233 | 302 | return circ_str |
234 | 303 | |
235 | 304 | def add_circuit(self, circuit): |
305 | """Adds a circuit to our circuits/streams list | |
306 | ||
307 | :var stem.response.events.CircuitEvent circuit: the circuit | |
308 | ||
309 | :returns: the :class:`Gtk.TreeIter` corresponding to the circuit | |
310 | """ | |
236 | 311 | circ_iter = self._treestore.append(None, |
237 | 312 | [self.TYPE_CIRC, |
238 | 313 | circuit.id, |
242 | 317 | return circ_iter |
243 | 318 | |
244 | 319 | def update_circuit(self, circuit): |
320 | """Updates a circuit in our circuits/streams list | |
321 | ||
322 | :var stem.response.events.CircuitEvent circuit: the circuit | |
323 | """ | |
245 | 324 | logging.debug("updating circuit %s" % circuit) |
246 | 325 | if circuit.reason: |
247 | 326 | status = _("%s: %s") % (str(circuit.status).capitalize(), |
254 | 333 | 3, status) |
255 | 334 | |
256 | 335 | def remove_circuit(self, circuit): |
336 | """Remove a circuit from our circuits/streams list | |
337 | ||
338 | :var stem.response.events.CircuitEvent circuit: the circuit | |
339 | """ | |
257 | 340 | self._treestore.remove(self._circ_to_iter[circuit.id]) |
258 | 341 | del self._circ_to_iter[circuit.id] |
259 | 342 | |
260 | 343 | def remove_circuit_delayed(self, circuit): |
344 | """Remove a circuit from our circuits/streams list after a delay | |
345 | ||
346 | The delay gives the user time to read the reason of the removal. | |
347 | ||
348 | :var stem.response.events.CircuitEvent circuit: the circuit | |
349 | """ | |
261 | 350 | circ_iter = self._circ_to_iter[circuit.id] |
262 | 351 | del self._circ_to_iter[circuit.id] |
263 | 352 | GLib.timeout_add_seconds(5, self.remove_treeiter, circ_iter) |
264 | 353 | |
265 | 354 | def update_circ_cb(self, circ_event): |
355 | """Updates the circuits/streams list in response to a the | |
356 | :class:`stem.response.events.CircuitEvent` | |
357 | ||
358 | :var stem.response.events.CircuitEvent circ_event: the circuit event | |
359 | """ | |
266 | 360 | if circ_event.id not in self._circ_to_iter: |
267 | 361 | self.add_circuit(circ_event) |
268 | 362 | else: |
276 | 370 | |
277 | 371 | @staticmethod |
278 | 372 | def stream_label(stream): |
373 | """Returns a label for a stream | |
374 | ||
375 | :var stem.response.events.StreamEvent stream: the stream | |
376 | ||
377 | :returns: **str** representing the stream | |
378 | """ | |
279 | 379 | return "%s" % stream.target |
280 | 380 | |
281 | 381 | def add_stream(self, stream): |
382 | """Adds a circuit to our circuits/streams list | |
383 | ||
384 | :var stem.response.events.StreamEvent stream: the stream | |
385 | ||
386 | :returns: the :class:`Gtk.TreeIter` corresponding to the stream | |
387 | """ | |
282 | 388 | if not stream.circ_id: |
283 | 389 | return None |
284 | 390 | circ_iter = self._circ_to_iter[stream.circ_id] |
295 | 401 | return stream_iter |
296 | 402 | |
297 | 403 | def update_stream(self, stream): |
404 | """Updates a stream in our circuits/streams list | |
405 | ||
406 | :var stem.response.events.StreamEvent stream: the stream | |
407 | """ | |
298 | 408 | stream_iter = self._stream_to_iter[stream.id] |
299 | 409 | if stream.circ_id != self._treestore.get_value(stream_iter, 1): |
300 | 410 | # The stream doesn't belong its parent circuit anymore. Remove it. |
309 | 419 | self._treestore.set(stream_iter, 3, str(stream.status).capitalize()) |
310 | 420 | |
311 | 421 | def remove_stream(self, stream): |
422 | """Remove a stream from our circuits/streams list | |
423 | ||
424 | :var stem.response.events.StreamEvent stream: the stream | |
425 | """ | |
312 | 426 | self._treestore.remove(self._stream_to_iter[stream.id]) |
313 | 427 | del self._stream_to_iter[stream.id] |
314 | 428 | |
315 | 429 | def remove_stream_delayed(self, stream): |
430 | """Remove a stream from our circuits/streams list after a delay | |
431 | ||
432 | The delay gives the user time to read the reason of the removal. | |
433 | ||
434 | :var stem.response.events.StreamEvent stream: the stream | |
435 | """ | |
316 | 436 | stream_iter = self._stream_to_iter[stream.id] |
317 | 437 | if stream_iter: |
318 | 438 | del self._stream_to_iter[stream.id] |
319 | 439 | GLib.timeout_add_seconds(5, self.remove_treeiter, stream_iter) |
320 | 440 | |
321 | 441 | def update_stream_cb(self, stream_event): |
442 | """Updates the circuits/streams list in response to a the | |
443 | :class:`stem.response.events.StreamEvent` | |
444 | ||
445 | :var stem.response.events.StreamEvent stream_event: the stream event | |
446 | """ | |
322 | 447 | if stream_event.id not in self._stream_to_iter: |
323 | 448 | self.add_stream(stream_event) |
324 | 449 | else: |
329 | 454 | self.remove_stream_delayed(stream_event) |
330 | 455 | |
331 | 456 | def populate_treeview(self): |
457 | """Synchronize the circuits/streams list with the Tor daemon | |
458 | """ | |
332 | 459 | self._treestore.clear() |
333 | 460 | self._circ_to_iter = {} |
334 | 461 | self._stream_to_iter = {} |
343 | 470 | # =============== |
344 | 471 | |
345 | 472 | def cb_treeselection_changed(self, treeselection, data=None): |
473 | """Handle selection change in the circuits/streams list | |
474 | ||
475 | Display details for the circuit selected in the circuits/streams list | |
476 | ||
477 | :var Gtk.TreeSelection treeselection: the selection | |
478 | ||
479 | :returns: **True** | |
480 | """ | |
346 | 481 | (model, selected_iter) = treeselection.get_selected() |
347 | 482 | if not selected_iter: |
348 | 483 | return False |
362 | 497 | return False |
363 | 498 | |
364 | 499 | def show_circuit_details(self, circuit): |
500 | """Display details for a circuit | |
501 | ||
502 | :var stem.response.events.CircuitEvent circuit: the circuit | |
503 | """ | |
365 | 504 | logging.debug("looking up details for %s" % circuit) |
366 | 505 | |
367 | 506 | # Replace the old content of _path by a fresh ListBox. |
375 | 514 | self._path.show_all() |
376 | 515 | |
377 | 516 | def display_node(self, status_entry): |
517 | """Display details for a node | |
518 | ||
519 | :var stem.descriptor.router_status_entry.RouterStatusEntryMicroV3 | |
520 | status_entry: the status entry for the node | |
521 | """ | |
378 | 522 | country = self.controller.get_info("ip-to-country/%s" % status_entry.address) |
379 | 523 | if pycountry: |
380 | 524 | country = pycountry.countries.get(alpha2=country.upper()).name |
407 | 551 | grid.show_all() |
408 | 552 | |
409 | 553 | class TorMonitorApplication(Gtk.Application): |
554 | """Tor Monitor application | |
555 | ||
556 | :var stem.control.Controller controller: a controller to the Tor daemon | |
557 | """ | |
410 | 558 | |
411 | 559 | def __init__(self): |
412 | 560 | Gtk.Application.__init__(self) |
414 | 562 | self.connect_controller() |
415 | 563 | |
416 | 564 | def connect_controller(self): |
565 | """Connects the controller to the Tor daemon. | |
566 | """ | |
417 | 567 | self.controller = stem.connection.connect_socket_file() |
418 | 568 | return self.controller |
419 | 569 |