commit c1abfa73fc24528713be233347cbd0f8796c537e Author: Damian Johnson atagar@torproject.org Date: Wed Mar 13 09:09:54 2013 -0700
'Tortoise and the Hare' tutorial
Adding a tutorial for event listening. This has the most substantial example yet, making a curses graph for tor's bandwidth usage. --- docs/_static/bandwidth_graph_output.png | Bin 0 -> 8702 bytes .../label/resources/tortoise_and_the_hare.xcf | Bin 0 -> 8081 bytes docs/_static/label/tortoise_and_the_hare.png | Bin 0 -> 3750 bytes docs/_static/section/tutorial/tortoise.png | Bin 0 -> 14700 bytes docs/tutorial/tortoise_and_the_hare.rst | 204 ++++++++++++++++++++ docs/tutorials.rst | 19 ++- 6 files changed, 221 insertions(+), 2 deletions(-)
diff --git a/docs/_static/bandwidth_graph_output.png b/docs/_static/bandwidth_graph_output.png new file mode 100644 index 0000000..9fa31e6 Binary files /dev/null and b/docs/_static/bandwidth_graph_output.png differ diff --git a/docs/_static/label/resources/tortoise_and_the_hare.xcf b/docs/_static/label/resources/tortoise_and_the_hare.xcf new file mode 100644 index 0000000..4a97fc0 Binary files /dev/null and b/docs/_static/label/resources/tortoise_and_the_hare.xcf differ diff --git a/docs/_static/label/tortoise_and_the_hare.png b/docs/_static/label/tortoise_and_the_hare.png new file mode 100644 index 0000000..e215db1 Binary files /dev/null and b/docs/_static/label/tortoise_and_the_hare.png differ diff --git a/docs/_static/section/tutorial/tortoise.png b/docs/_static/section/tutorial/tortoise.png new file mode 100644 index 0000000..52b5da0 Binary files /dev/null and b/docs/_static/section/tutorial/tortoise.png differ diff --git a/docs/tutorial/tortoise_and_the_hare.rst b/docs/tutorial/tortoise_and_the_hare.rst new file mode 100644 index 0000000..f575853 --- /dev/null +++ b/docs/tutorial/tortoise_and_the_hare.rst @@ -0,0 +1,204 @@ +Tortoise and the Hare +===================== + +Controllers have two methods of talking with Tor... + +* **Synchronous** - Most commonly you make a request to Tor then receive its + reply. The :func:`~stem.control.Controller.get_info` calls in the `first + tutorial <the_little_relay_that_could.html>`_ are an example of this. + +* **Asynchronous** - Controllers can subscribe to be notified when various + kinds of events occur within Tor (see the :data:`~stem.control.EventType`). + Stem's users provide a callback function to + :func:`~stem.control.Controller.add_event_listener` which is then notified + when the event occurs. + +Try to avoid lengthy operations within event callbacks. They're notified by a +single dedicated event thread, and blocking this thread will prevent the +delivery of further events. + +With that out of the way lets see an example. The following is a `curses +http://docs.python.org/2/howto/curses.html`_ application that graphs the +bandwidth usage of Tor... + +.. image:: /_static/bandwidth_graph_output.png + +To do this it listens to **BW events** +(the class for which is a :class:`~stem.response.events.BandwidthEvent`). These +are events that Tor emits each second saying the number of bytes downloaded and +uploaded. + +:: + + import curses + import functools + + from stem.control import EventType, Controller + from stem.util import str_tools + + # colors that curses can handle + + COLOR_LIST = { + "red": curses.COLOR_RED, + "green": curses.COLOR_GREEN, + "yellow": curses.COLOR_YELLOW, + "blue": curses.COLOR_BLUE, + "cyan": curses.COLOR_CYAN, + "magenta": curses.COLOR_MAGENTA, + "black": curses.COLOR_BLACK, + "white": curses.COLOR_WHITE, + } + + GRAPH_WIDTH = 40 + GRAPH_HEIGHT = 8 + + DOWNLOAD_COLOR = "green" + UPLOAD_COLOR = "blue" + + def main(): + with Controller.from_port(port = 9051) as controller: + controller.authenticate() + + try: + curses.wrapper(drawBandwidthGraph, controller) + except KeyboardInterrupt: + pass # the user hit ctrl+c + + def drawBandwidthGraph(stdscr, controller): + window = Window(stdscr) + + # (downloaded, uploaded) tuples for the last 40 seconds + + bandwidth_rates = [(0, 0)] * GRAPH_WIDTH + + # Making a partial that wraps the window and bandwidth_rates with a function + # for Tor to call when it gets a BW event. + + bw_event_handler = functools.partial(_handle_bandwidth_event, window, bandwidth_rates) + + # Registering this listener with Tor. Tor reports a BW event each second. + + controller.add_event_listener(bw_event_handler, EventType.BW) + + # Pause the main thread until the user hits any key... and no, don't you dare + # ask where the 'any' key is. :P + + stdscr.getch() + + def _handle_bandwidth_event(window, bandwidth_rates, event): + # callback for when tor provides us with a BW event + + bandwidth_rates.insert(0, (event.read, event.written)) + bandwidth_rates = bandwidth_rates[:GRAPH_WIDTH] # truncate old values + _render_graph(window, bandwidth_rates) + + def _render_graph(window, bandwidth_rates): + window.erase() + + download_rates = [entry[0] for entry in bandwidth_rates] + upload_rates = [entry[1] for entry in bandwidth_rates] + + # show the latest values at the top + + label = "Downloaded (%s/s):" % str_tools.get_size_label(download_rates[0], 1) + window.addstr(0, 1, label, DOWNLOAD_COLOR, curses.A_BOLD) + + label = "Uploaded (%s/s):" % str_tools.get_size_label(upload_rates[0], 1) + window.addstr(0, GRAPH_WIDTH + 7, label, UPLOAD_COLOR, curses.A_BOLD) + + # draw the graph bounds in KB + + max_download_rate = max(download_rates) + max_upload_rate = max(upload_rates) + + window.addstr(1, 1, "%4i" % (max_download_rate / 1024), DOWNLOAD_COLOR) + window.addstr(GRAPH_HEIGHT, 1, " 0", DOWNLOAD_COLOR) + + window.addstr(1, GRAPH_WIDTH + 7, "%4i" % (max_upload_rate / 1024), UPLOAD_COLOR) + window.addstr(GRAPH_HEIGHT, GRAPH_WIDTH + 7, " 0", UPLOAD_COLOR) + + # draw the graph + + for col in xrange(GRAPH_WIDTH): + col_height = GRAPH_HEIGHT * download_rates[col] / max(max_download_rate, 1) + + for row in xrange(col_height): + window.addstr(GRAPH_HEIGHT - row, col + 6, " ", DOWNLOAD_COLOR, curses.A_STANDOUT) + + col_height = GRAPH_HEIGHT * upload_rates[col] / max(max_upload_rate, 1) + + for row in xrange(col_height): + window.addstr(GRAPH_HEIGHT - row, col + GRAPH_WIDTH + 12, " ", UPLOAD_COLOR, curses.A_STANDOUT) + + window.refresh() + + class Window(object): + """ + Simple wrapper for the curses standard screen object. + """ + + def __init__(self, stdscr): + self._stdscr = stdscr + + # Mappings of names to the curses color attribute. Initially these all + # reference black text, but if the terminal can handle color then + # they're set with that foreground color. + + self._colors = dict([(color, 0) for color in COLOR_LIST]) + + # allows for background transparency + + try: + curses.use_default_colors() + except curses.error: + pass + + # makes the cursor invisible + + try: + curses.curs_set(0) + except curses.error: + pass + + # initializes colors if the terminal can handle them + + try: + if curses.has_colors(): + color_pair = 1 + + for name, foreground in COLOR_LIST.items(): + background = -1 # allows for default (possibly transparent) background + curses.init_pair(color_pair, foreground, background) + self._colors[name] = curses.color_pair(color_pair) + color_pair += 1 + except curses.error: + pass + + def addstr(self, y, x, msg, color = None, attr = curses.A_NORMAL): + # Curses throws an error if we try to draw a message that spans out of the + # window's bounds (... seriously?), so doing our best to avoid that. + + if color is not None: + if color not in self._colors: + recognized_colors = ", ".join(self._colors.keys()) + raise ValueError("The '%s' color isn't recognized: %s" % (color, recognized_colors)) + + attr |= self._colors[color] + + max_y, max_x = self._stdscr.getmaxyx() + + if max_x > x and max_y > y: + try: + self._stdscr.addstr(y, x, msg[:max_x - x], attr) + except: + pass # maybe an edge case while resizing the window + + def erase(self): + self._stdscr.erase() + + def refresh(self): + self._stdscr.refresh() + + if __name__ == '__main__': + main() + diff --git a/docs/tutorials.rst b/docs/tutorials.rst index 28ea3d5..646af20 100644 --- a/docs/tutorials.rst +++ b/docs/tutorials.rst @@ -1,5 +1,5 @@ -Tutorials -========= +Tutorial +========
.. Image Sources:
@@ -15,6 +15,11 @@ Tutorials License: Public Domain (not a subject of copyright according the Russian civil code) Alternate: https://openclipart.org/detail/85555/communist-sabbatarian-ribbon-by-rones-8...
+ * Tortoise and the Hare - tortoise.png + Source: https://openclipart.org/detail/27911/green-tortoise-%28cartoon%29-by-arking-... + Author: arking + License: Public Domain + * Mirror Mirror On The Wall - mirror.png Source: https://openclipart.org/detail/152155/mirror-frame-by-gsagri04 Author: Unknown (gsagri04?) @@ -52,6 +57,16 @@ feet wet by jumping straight in with some tutorials... tutorial we'll programmatically start Tor then use it to read a site through mother Russia!
+ * - .. image:: /_static/section/tutorial/tortoise.png + :target: tutorial/tortoise_and_the_hare.html + + - .. image:: /_static/label/tortoise_and_the_hare.png + :target: tutorial/tortoise_and_the_hare.html + + As Tor runs it generates a variety of **events** that controllers can + subscribe to be notified of. In this tutorial we'll do just that, + writing a curses application that graphs the bandwidth usage of Tor. + * - .. image:: /_static/section/tutorial/mirror.png :target: tutorial/mirror_mirror_on_the_wall.html
tor-commits@lists.torproject.org