Package: debtorrent
Version: 0.1.9
Severity: wishlist
(This is a TODO for something that's been discussed on IRC)
It would be good to have more information on the local status page, as
mocked up on
http://wiki.debian.org/DebTorrent/UserInterfaceIdeas?action=recall&rev=3
It also gives a bit of insight in to DebTorrent's performance; such as how
much of the software that you download also interests other users (of
interest for the sharing of large packages such as TexLive and Open Arena).
Steve
Attached patch:
A prototype based on SVN revision 411, which still needs polishing. This
is implemented as SVG images; but should also be good base for adding PNG
images.
Images are only added if the user follows the "Click here to show" link, so
this doesn't slow down the normal status page.
diff --git a/DebTorrent/BT1/AptListener.py b/DebTorrent/BT1/AptListener.py
index a2c92ea..86b8b73 100644
--- a/DebTorrent/BT1/AptListener.py
+++ b/DebTorrent/BT1/AptListener.py
@@ -25,7 +25,7 @@ from cStringIO import StringIO
from time import time, gmtime, strftime
from DebTorrent.clock import clock
from sha import sha
-from binascii import b2a_hex
+from binascii import b2a_hex, a2b_hex
from makemetafile import TorrentCreator
from DebTorrent.HTTPCache import HTTPCache
import os, logging
@@ -230,11 +230,14 @@ class AptListener:
self.connection_update(open_conns)
- def get_infopage(self):
+ def get_infopage(self, show_piecemaps):
"""Format the info page to display for normal browsers.
Formats the currently downloading torrents into a table in human-readable
format to display in a browser window.
+
+ @type show_piecemaps: C{string}
+ @param show_piecemaps: "svg" to include SVG piecemaps. "png" may be supported in future
@rtype: (C{int}, C{string}, C{dictionary}, C{string})
@return: the HTTP status code, status message, headers, and message body
@@ -324,6 +327,26 @@ class AptListener:
'<li><em>distributed copies:</em> the number of copies of the complete torrent seen in non-seeding peers</li>\n' \
'</ul>\n')
+ # Draw the piece maps
+ s.write('<h3>Piece maps</h3>\n')
+ if show_piecemaps == 'svg':
+ for x in data:
+ ( name, hash, status, progress, peers, seeds, seedsmsg, dist,
+ uprate, dnrate, upamt, dnamt, httpdnamt, size, t, msg ) = x
+ s.write('<h4>%s (%s)</h4>\n' % (name, b2a_hex(hash)))
+ s.write('<a href="piecemap.svg?info_hash=%s"><object data="piecemap.svg?info_hash=%s" width="100%%" height="%d"></object></a>\n'
+ % (b2a_hex(hash), b2a_hex(hash), 10*(peers+1)))
+ #TODO use an accurate size for the image
+
+ s.write('<ul>\n' \
+ '<li><em>blue:</em> You have this piece</li>\n' \
+ '<li><em>purple:</em> A peer has this piece</li>\n' \
+ '<li><em>white:</em> You/Peer does not have this piece, but does have a later piece</li>\n' \
+ '<li><em>grey:</em> You/Peer does not have this piece, and does not have any later piece</li>\n' \
+ '</ul>\n')
+ else:
+ s.write('<p><a href="index.html?piecemaps=svg">Click here to show</a></p>')
+
s.write('</body>\n' \
'</html>\n')
return (200, 'OK', {'Server': VERSION, 'Content-Type': 'text/html; charset=iso-8859-1'}, s.getvalue())
@@ -331,6 +354,135 @@ class AptListener:
logger.exception('Error returning info_page')
return (500, 'Internal Server Error', {'Server': VERSION, 'Content-Type': 'text/html; charset=iso-8859-1'}, 'Server Error')
+ def get_piecemap_for_torrent_svg(self, info_hash):
+ """Shows the piecemap (which peers have what) for a given torrent.
+
+ The HTTP request should include the appropriate torrent hash ( http://.../piecemap.svg?info_hash=... )
+
+ @type info_hash: C{string}
+ @param info_hash: the info_hash passed to the HTTP query
+
+ @rtype: (C{int}, C{string}, C{dictionary}, C{string})
+ @return: the HTTP status code, status message, headers, and message body
+
+ """
+
+ try:
+ if not self.config['show_infopage']:
+ return (404, 'Not Found', {'Server': VERSION, 'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, alas)
+
+ if info_hash == None:
+ return (400, 'Bad request', {'Server': VERSION, 'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, 'No hash specified in request')
+ hash = a2b_hex(info_hash);
+ piecelist_list = self.handler.get_piecemap(hash)
+ if piecelist_list == None:
+ return (404, 'Not Found', {'Server': VERSION, 'Content-Type': 'text/plain', 'Pragma': 'no-cache'}, 'Unknown hash')
+
+ s = StringIO()
+ self.svg_draw_piecemap(s, piecelist_list, ["blue", "purple"])
+
+ return (200, 'OK', {'Server': VERSION, 'Content-Type': 'image/svg+xml; charset=utf-8'}, s.getvalue())
+ except:
+ logger.exception('Error returning info_page')
+ return (500, 'Internal Server Error', {'Server': VERSION, 'Content-Type': 'text/html; charset=iso-8859-1'}, 'Server Error')
+
+ def svg_draw_piecemap(self, s, piecelist_list, color_list):
+ """Draw (in SVG) a map of which pieces are present in the piecelist
+
+ @type s: C{StringIO}
+ @param s: Output stream for data that will be sent to the webbrowser.
+ @type piecelist_list: C{List} of C{Bitfield}
+ @param piecelist_list: The bitfields representing which pieces are present.
+ @type color_list: C{List} of C{String}
+ @param color_list: List of colors for drawing the pieces. color_list[i] is used for piecelist_list[i]; if there are more piecelists than colors then the last color is reused.
+
+ """
+
+ # Constants for the size of the drawing
+ # Vertically, each line is of total height lineheight, with padding at the bottom.
+ markwidth=1
+ lineheight=7
+ linepadding=1
+ markheight=lineheight - linepadding
+ fittowindow = True
+
+ # Since some piecelists have more pieces than others, find the longest.
+ mostpieces = 0
+ for piecelist in piecelist_list:
+ if (len(piecelist) > mostpieces):
+ mostpieces = len(piecelist)
+
+ # The bitfields have a lot of whitespace. Compress the drawing by showing only the pieces held by at least one peer.
+ # piecelist[piecenumber] will be drawn at pixellist[piecenumber]
+ pixellist = [0] * mostpieces
+ y = 0
+ for x in xrange(0, mostpieces):
+ someonehas = False
+ pixellist[x] = y
+ for i in xrange(0, len(piecelist_list)):
+ if (piecelist_list[i][x]):
+ someonehas = True
+ if someonehas:
+ y += 1
+
+ # And this is the width of the compressed drawing
+ imagewidth = y
+
+ if (fittowindow):
+ s.write('<svg:svg xmlns:svg="http://www.w3.org/2000/svg" width="100%%" height="%spx" viewBox="0 0 %s %s" preserveAspectRatio="none" shape-rendering="crispEdges">\n'
+ % (lineheight * len(piecelist_list) - linepadding, markwidth * imagewidth, lineheight * len(piecelist_list) - linepadding))
+ else:
+ s.write('<svg:svg xmlns:svg="http://www.w3.org/2000/svg" width="%spx" height="%spx" shape-rendering="crispEdges">\n'
+ % (markwidth * imagewidth, lineheight * len(piecelist_list) - linepadding))
+
+ # Draw the background in one go.
+ s.write('<svg:rect x="0" y="0" width="%spx" height="%spx" fill="black" />\n'
+ % (markwidth * imagewidth, lineheight * len(piecelist_list)))
+
+ color = "blue"; # Default which should be overridden by color_list[0]
+
+ for i in xrange(0, len(piecelist_list)):
+ piecelist = piecelist_list[i]
+ y = i * lineheight # the y-coordinate that we're drawing at
+
+ # This makes each row white, on a black grid (the previous background showing through)
+ s.write('<svg:rect x="0" y="%spx" width="%spx" height="%spx" fill="white" />\n'
+ % (y, imagewidth * markwidth, markheight))
+
+ # Set default drawing color
+ if (i < len(color_list)):
+ color = color_list[i]
+ s.write('<svg:g fill="%s">\n' % (color))
+
+ # Clump the data together into runs of "we have this piece" and "we lack this piece"
+ runstart = 0
+ runtype = piecelist[0]
+ for piece in xrange (1, len(piecelist)):
+ if (piecelist[piece] != runtype):
+ # Just past the end of the current run. Draw it if it was a "we have" run.
+ if (runtype):
+ s.write('<svg:rect x="%spx" y="%spx" width="%spx" height="%spx" title="%s-%s" />\n'
+ % (pixellist[runstart] * markwidth, y, (pixellist[piece]-pixellist[runstart]) * markwidth, markheight,
+ runstart, piece-1))
+
+ # Now start counting the new run.
+ runstart = piece
+ runtype = piecelist[piece]
+
+ # Reached the end of the data, draw the final run
+ if (runtype):
+ s.write('<svg:rect x="%spx" y="%spx" width="%spx" height="%spx" title="%s-%s" />\n'
+ % (pixellist[runstart] * markwidth, y, (pixellist[piece]-pixellist[runstart]) * markwidth, markheight,
+ runstart, piece-1))
+ else:
+ # Show how far the "no pieces" bit is
+ s.write('<svg:rect x="%spx" y="%spx" width="%spx" height="%spx" fill="grey" />\n'
+ % (pixellist[runstart] * markwidth, y, (pixellist[piece]-pixellist[runstart]) * markwidth, markheight))
+
+ s.write("</svg:g>\n\n")
+
+ s.write('</svg:svg>')
+
def get_meow(self):
return (200, 'OK', {'Server': VERSION, 'Content-Type': 'text/html; charset=iso-8859-1'}, """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n<html><head><title>Meow</title>\n</head>\n<body style="color: rgb(255, 255, 255); background-color: rgb(0, 0, 0);">\n<div><big style="font-weight: bold;"><big><big><span style="font-family: arial,helvetica,sans-serif;">I IZ TAKIN BRAKE</span></big></big></big><br></div>\n<pre><b><tt> .-o=o-.<br> , /=o=o=o=\ .--.<br> _|\|=o=O=o=O=| \<br> __.' a`\=o=o=o=(`\ /<br> '. a 4/`|.-""'`\ \ ;'`) .---.<br> \ .' / .--' |_.' / .-._)<br> `) _.' / /`-.__.' /<br> `'-.____; /'-.___.-'<br> `\"""`</tt></b></pre>\n<div><big style="font-weight: bold;"><big><big><span style="font-family: arial,helvetica,sans-serif;">FRM GETIN UR PACKAGES</span></big></big></big><br></div>\n</body>\n</html>""")
@@ -787,9 +939,12 @@ class AptListener:
kw = unquote(s[:i])
paramslist.setdefault(kw, [])
paramslist[kw] += [unquote(s[i+1:])]
+ logger.debug('paramslist['+str(kw)+'] =='+str(paramslist[kw]))
if path == '' or path == 'index.html':
- return self.get_infopage()
+ return self.get_infopage(params('piecemaps'))
+ if path == 'piecemap.svg':
+ return self.get_piecemap_for_torrent_svg(params('info_hash'))
if path == 'meow':
return self.get_meow()
if path == 'favicon.ico':
diff --git a/DebTorrent/launchmanycore.py b/DebTorrent/launchmanycore.py
index a1bdf3b..c60b5c7 100644
--- a/DebTorrent/launchmanycore.py
+++ b/DebTorrent/launchmanycore.py
@@ -35,6 +35,7 @@ from cStringIO import StringIO
import logging
from DebTorrent.BT1.AptListener import AptListener
from DebTorrent.HTTPHandler import HTTPHandler
+from binascii import b2a_hex, a2b_hex
logger = logging.getLogger('DebTorrent.launchmanycore')
@@ -497,6 +498,38 @@ class LaunchMany:
return data
+ def get_piecemap(self, id):
+ """Return the piecemap (which peers have what) for a given torrent.
+
+ @type id: C{string}
+ @param id: Info hash for the torrent, as provided by gather_stats.
+
+ Internal: The "id" argument should match the identifier that
+ gather_stats provides in data[][1]. As of revision 373 it provides the
+ long-lived torrent ID, not the hash.
+
+ @rtype: C{list}
+ @return: List of piecemaps. The first will be for the local peer, then the other peers in undefined order.
+
+ """
+
+ hash = None
+ for search_hash in self.torrent_list:
+ if self.torrent_cache[search_hash]['metainfo'].get('identifier', search_hash) == id:
+ hash = search_hash
+
+ if hash == None:
+ logger.warning('Could not find hash for id %s\n' % b2a_hex(id))
+ return None
+
+ piecelist_list = []
+ piecelist_list.append(self.downloads[hash].d.storagewrapper.have)
+ if (self.downloads[hash].d.downloader != None):
+ for peer in self.downloads[hash].d.downloader.downloads:
+ piecelist_list.append(peer.have)
+
+ return piecelist_list
+
def remove(self, hash):
"""Stop and remove a running torrent.