--- Begin Message ---
- To: Debian Bug Tracking System <submit@bugs.debian.org>
 
- Subject: bittorrent: Patch for four bugs
 
- From: Daniel Dickinson <cshore@fionavar.ca>
 
- Date: Tue, 01 Jul 2008 19:55:48 -0400
 
- Message-id: <20080701235548.7513.47966.reportbug@brennin.fionavar.dd>
 
Package: bittorrent
Version: 3.4.2-11.1
Severity: normal
Tags: patch
The attached patch fixes four bugs.  From my changelog entries:
bittorrent (3.4.2-11.2dfd06~005) unstable; urgency=low
	
  * Modify global upload throttle so that it uses full bandwidth, except
    for a small cushion for inactive torrents to start downloading
 -- Daniel Dickinson <cshore@fionavar.ca>  Mon, 30 Jun 2008 22:46:45 -0500	
bittorrent (3.4.2-11.2dfd05~015) unstable; urgency=low
	
  * Add global upload rate throttle that keeps a cushion for new
    sessions sessions under max to grow, but otherwise allocates full max
    global upload rate to sessions that want to upload at a rate greater
    than the per-session maximum upload rate (calculated as global
    maximum divided by number of active uploads) (Closes: #481276)
  * Fix deadfile handling and checking locking.  (Closes: #478758)
  * Fix exception handling (or rather lack thereof)
  * Comment a bunch of code
  * Change startup disk check to be one torrent at a time. (Closes: #482478)
  * Add outgoing port range limiting in order to play well with strict 
    firewalls.  (Closes: #481276)
 -- Daniel Dickinson <cshore@fionavar.ca>  Sun, 01 Jun 2008 02:09:55 -0500
-- System Information:
Debian Release: lenny/sid
  APT prefers testing
  APT policy: (500, 'testing'), (500, 'stable')
Architecture: i386 (i686)
Kernel: Linux 2.6.24-1-686 (SMP w/1 CPU core)
Locale: LANG=en_CA.UTF-8, LC_CTYPE=en_CA.UTF-8 (charmap=UTF-8)
Shell: /bin/sh linked to /bin/bash
diff -Naur bittorrent_3.4.2-11.1/BitTorrent/Connecter.py bittorrent-3.4.2/BitTorrent/Connecter.py
--- bittorrent_3.4.2-11.1/BitTorrent/Connecter.py	2008-05-31 19:44:44.000000000 -0400
+++ bittorrent-3.4.2/BitTorrent/Connecter.py	2008-06-01 06:48:51.000000000 -0400
@@ -90,7 +90,7 @@
 
 class Connecter:
     def __init__(self, make_upload, downloader, choker, numpieces,
-            totalup, max_upload_rate = 0, sched = None):
+            totalup, max_upload_rate = 0, sched = None, global_cap = None):
         self.downloader = downloader
         self.make_upload = make_upload
         self.choker = choker
@@ -98,11 +98,26 @@
         self.max_upload_rate = max_upload_rate
         self.sched = sched
         self.totalup = totalup
+        self.global_cap = global_cap
         self.rate_capped = False
         self.connections = {}
 
     def _update_upload_rate(self, amount):
         self.totalup.update_rate(amount)
+        # If we have defined the global_cap dictionary, check if there is a
+        # global cap to enforce (and do so if necessary)
+        if self.global_cap:
+            global_max_upload_rate = self.global_cap['global_max_upload_rate']
+            global_rate = self.global_cap['global_rate']
+            global_slice = self.global_cap['global_slice']
+            # Only do global rate throttling if the global maximum and the 
+            # current global rate are not None (undefined)
+            if global_max_upload_rate != None and global_rate != None:
+                # If we have a global throttle limit
+                if global_max_upload_rate > 0:
+                    # Set the per-torrent maximum as calculated by the 
+                    # multi-downloader
+                    self.max_upload_rate = global_slice
         if self.max_upload_rate > 0 and self.totalup.get_rate_noupdate() > self.max_upload_rate:
             self.rate_capped = True
             self.sched(self._uncap, self.totalup.time_until_rate(self.max_upload_rate))
diff -Naur bittorrent_3.4.2-11.1/BitTorrent/download.py bittorrent-3.4.2/BitTorrent/download.py
--- bittorrent_3.4.2-11.1/BitTorrent/download.py	2008-05-31 19:44:44.000000000 -0400
+++ bittorrent-3.4.2/BitTorrent/download.py	2008-06-01 02:01:11.000000000 -0400
@@ -92,9 +92,13 @@
         "the number of uploads to fill out to with extra optimistic unchokes"),
     ('report_hash_failures', 0,
         "whether to inform the user that hash failures occur. They're non-fatal."),
+    ('min_outgoing_port', 1024,
+        "lowest port from which we are allowed to connect"),
+    ('max_outgoing_port', 65535,
+        "highest port from which we are allowed to connect"),
     ]
 
-def download(params, filefunc, statusfunc, finfunc, errorfunc, doneflag, cols, pathFunc = None, paramfunc = None, spewflag = Event()):
+def download(params, filefunc, statusfunc, finfunc, errorfunc, doneflag, cols, pathFunc = None, paramfunc = None, spewflag = Event(), global_cap = None):
     if len(params) == 0:
         errorfunc('arguments are -\n' + formatDefinitions(defaults, cols))
         return
@@ -197,7 +201,7 @@
         doneflag.set()
         if reason is not None:
             errorfunc(reason)
-    rawserver = RawServer(doneflag, config['timeout_check_interval'], config['timeout'], errorfunc = errorfunc, maxconnects = config['max_allow_in'])
+    rawserver = RawServer(doneflag, config['timeout_check_interval'], config['timeout'], errorfunc = errorfunc, maxconnects = config['max_allow_in'], min_outgoing_port = config['min_outgoing_port'], max_outgoing_port = config['max_outgoing_port'] )
     try:
         try:
             storage = Storage(files, open, path.exists, path.getsize)
@@ -242,6 +246,13 @@
         errorfunc("Couldn't listen - " + str(e))
         return
 
+    if config['min_outgoing_port'] < 1024 or config['max_outgoing_port'] > 65535:
+        errorfunc("We can only connect to peers using ports between 1024 and 65535")
+        return
+    if config['min_outgoing_port'] > config['max_outgoing_port']:
+        errorfunc("max_outgoing_port less than min_outgoing_port; can't connect")
+        return
+
     choker = Choker(config['max_uploads'], rawserver.add_task, finflag.isSet, 
         config['min_uploads'])
     upmeasure = Measure(config['max_rate_period'], 
@@ -265,7 +276,7 @@
         len(pieces), downmeasure, config['snub_time'], 
         ratemeasure.data_came_in)
     connecter = Connecter(make_upload, downloader, choker,
-        len(pieces), upmeasure, config['max_upload_rate'] * 1024, rawserver.add_task)
+        len(pieces), upmeasure, config['max_upload_rate'] * 1024, rawserver.add_task, global_cap)
     infohash = sha(bencode(info)).digest()
     encoder = Encoder(connecter, rawserver, 
         myid, config['max_message_length'], rawserver.add_task, 
diff -Naur bittorrent_3.4.2-11.1/BitTorrent/RawServer.py bittorrent-3.4.2/BitTorrent/RawServer.py
--- bittorrent_3.4.2-11.1/BitTorrent/RawServer.py	2008-05-31 19:44:44.000000000 -0400
+++ bittorrent-3.4.2/BitTorrent/RawServer.py	2008-06-01 02:00:45.000000000 -0400
@@ -80,7 +80,7 @@
 
 class RawServer:
     def __init__(self, doneflag, timeout_check_interval, timeout, noisy = True,
-            errorfunc = default_error_handler, maxconnects = 55):
+            errorfunc = default_error_handler, maxconnects = 55, min_outgoing_port = 1024, max_outgoing_port = 65535):
         self.timeout_check_interval = timeout_check_interval
         self.timeout = timeout
         self.poll = poll()
@@ -92,6 +92,8 @@
         self.errorfunc = errorfunc
         self.maxconnects = maxconnects
         self.funcs = []
+        self.min_outgoing_port = min_outgoing_port
+        self.max_outgoing_port = max_outgoing_port
         self.unscheduled_tasks = []
         self.add_task(self.scan_for_timeouts, timeout_check_interval)
 
@@ -133,7 +135,14 @@
             sock.setsockopt(socket.IPPROTO_IP, socket.IP_TOS, 32)
         except:
             pass
-        sock.bind((self.bindaddr, 0))
+        for out_port in xrange(max_outgoing_port, min_outgoing_port - 1, -1):
+            was_bound = True
+            try:
+                sock.bind((self.bindaddr, out_port))
+            except:
+                was_bound = False
+            if was_bound:
+                break
         try:
             sock.connect_ex(dns)
         except socket.error:
diff -Naur bittorrent_3.4.2-11.1/btlaunchmany.py bittorrent-3.4.2/btlaunchmany.py
--- bittorrent_3.4.2-11.1/btlaunchmany.py	2008-05-31 19:44:44.000000000 -0400
+++ bittorrent-3.4.2/btlaunchmany.py	2008-07-01 18:10:02.000000000 -0400
@@ -12,134 +12,355 @@
 from threading import Thread, Event, Lock
 from os import listdir
 from os.path import abspath, join, exists, getsize
-from sys import argv, stdout, exit
+from sys import argv, stdout, exit, stderr
 from time import sleep
 import traceback
+from BitTorrent import Connecter
+import locale
+import time
 
 def dummy(*args, **kwargs):
     pass
 
 threads = {}
+global_max_upload_rate = 0
 ext = '.torrent'
 print 'btlaunchmany starting..'
-filecheck = Lock()
+logfile = stderr
+debug_level = 0
+display_interval = 1
+
+class Filecheck:
+    def __init__(self):
+        self.locked = False
+        self.lock = Lock()
+    def acquire(self, num):
+        self.locked = True
+        # Acquire a lock if possible, return success/failure of acquire
+        return self.lock.acquire(num)
+
+    def release(self):
+        # If we're locked
+        if self.locked:
+            # Release the lock
+            return self.lock.release()
+        else:
+            # Tell caller we weren't locked, which means logic is bad
+            return False
+
+filecheck = Filecheck()
+
+def log(mesg):
+    global logfile
+    date = time.strftime("%Y-%m-%d %H:%M:%S")
+    if logfile:
+        try:
+            logfile.write("%s btlaunchmany.py: %s\n" % (date, mesg))            
+        except EnvironmentError:
+            stderr.write("Error writing logfile")
+            stderr.flush()
+            sys.exit(1)
+        
+        logfile.flush()
 
 def dropdir_mainloop(d, params):
+    global filecheck
+    global global_max_upload_rate
     deadfiles = []
     global threads, status
     while 1:
-        files = listdir(d)
-        # new files
-        for file in files: 
-            if file[-len(ext):] == ext:
-                if file not in threads.keys() + deadfiles:
-                    threads[file] = {'kill': Event(), 'try': 1}
-                    print 'New torrent: %s' % file
-                    stdout.flush()
-                    threads[file]['thread'] = Thread(target = StatusUpdater(join(d, file), params, file).download, name = file)
-                    threads[file]['thread'].start()
-        # files with multiple tries
-        for file, threadinfo in threads.items():
-            if threadinfo.get('timeout') == 0:
-                # Zero seconds left, try and start the thing again.
-                threadinfo['try'] = threadinfo['try'] + 1
-                threadinfo['thread'] = Thread(target = StatusUpdater(join(d, file), params, file).download, name = file)
-                threadinfo['thread'].start()
+        def start_torrent(trynum = 1):
+            # If we can obtain a lock (no other file is doing
+            # intial checking)
+            if filecheck.acquire(0):
+                # For this thread, set an event that will tell the
+                # thread when we want it to die, and the number of
+                # attempts we have made to start the thread 
+                threads[file]['kill'] = Event()
+                threads[file]['try'] = trynum
+                threads[file]['global_cap'] = { 'global_max_upload_rate': global_max_upload_rate, 'global_rate': 0, 'global_slice': 0 }
+                print 'New torrent: %s' % file
+                stdout.flush()
+                # Create a download (bittorrent) instance for the file
+                status_updater = StatusUpdater(join(d, file), params, file, 1, threads[file]['global_cap'])
+                # Create a thread from the instance
+                threads[file]['thread'] = Thread(target = status_updater.download, name = file)
+                # And start it
+                threads[file]['thread'].start()
                 threadinfo['timeout'] = -1
-            elif threadinfo.get('timeout') > 0: 
-                # Decrement our counter by 1
-                threadinfo['timeout'] = threadinfo['timeout'] - 1
-            elif not threadinfo['thread'].isAlive():
-                # died without permission
-                # if it was checking the file, it isn't anymore.
-                if threadinfo.get('checking', None):
-                    filecheck.release()
-                if threadinfo.get('try') == 6: 
-                    # Died on the sixth try? You're dead.
-                    deadfiles.append(file)
-                    print '%s died 6 times, added to dead list' % fil
+            # If we can't obtain a lock
+            else:
+                threadinfo['status'] = 'disk wait'
+
+        try:
+            # Find files in download directory
+            files = listdir(d)
+            # For each file in the download directory
+            for file in files: 
+                # for a .torrent
+                if file[-len(ext):] == ext:                
+                    # If file does not already have a table entry and it 
+                    # didn't fail to start before
+                    if file not in threads.keys() + deadfiles:
+                        # Create a table entry that has never tried and which
+                        # is done wait for retry (i.e. will start immediately)
+                        threads[file] = { 'try': 0 , 'timeout': 0 }
+                        threads[file]['global_cap'] = { 'global_max_upload_rate': global_max_upload_rate, 'global_rate': 0, 'global_slice': 0 }
+            # For existing table entries
+            for file, threadinfo in threads.items():
+                # If we're done waiting for a retry (zero seconds left)
+                if threadinfo.get('timeout') == 0:
+                    start_torrent(threadinfo['try'] + 1)
+                # Otherwise if there is a timeout
+                elif threadinfo.get('timeout') > 0: 
+                    # Update out timeout (reduce time left to wait)
+                    threadinfo['timeout'] = threadinfo['timeout'] - 1
+                # Otherwise, if the thread is dead
+                elif not threadinfo['thread'].isAlive():
+                    # died without permission
+                    # if it was checking the file, it isn't anymore.
+                    if threadinfo.get('checking', None):
+                        # Relase the lock
+                        filecheck.release()
+                        if threadinfo.get('try') == 6: 
+                            # Died on the sixth try? You're dead.
+                            deadfiles.append(file)
+                            print '%s died 6 times, added to dead list' % file
+                            stdout.flush()
+                            # Don't try again (remove table entry)
+                            del threads[file]
+                        else:
+                            # Remove the thread information (not table)
+                            del threadinfo['thread']
+                            # And set timeout so it will try again in 120s
+                            threadinfo['timeout'] = 120
+                # dealing with files that dissapear
+                if file not in files:
+                    print 'Torrent file dissapeared, killing %s' % file
                     stdout.flush()
-                    del threads[file]
+                    # If thread was active
+                    if threadinfo.get('timeout', -1) == -1:
+                        # Kill the thread, by setting even that tells it to die
+                        threadinfo['kill'].set()
+                        if threadinfo['thread']:
+                            # And attach to thread until it stops
+                            threadinfo['thread'].join()
+                    # if this thread was filechecking
+                    if threadinfo.get('checking', None): 
+                        # Release lock
+                        filecheck.release()
+                        # Remove table etnry
+                        del threads[file]
+                        # Check the list of dead files
+                        for file in deadfiles:
+                            # If the file no longer exists in the download dir
+                            if file not in files: 
+                                # Remove that file from the list of dead files
+                                deadfiles.remove(file)
+        # If there is a file or OS error
+        except EnvironmentError:
+            # Print a stack trace
+            traceback.print_exc()
+            # and log it
+            log(traceback.format_exc())
+            # But keep on going
+            pass
+        sleep(1)
+
+def calc_slice(calckiller):
+    """Calculate bandwith cap for each active torrent"""
+    global threads
+    global global_max_upload_rate
+    debug_sec = 0
+
+    # Keep going until main program terminates this thread
+    while not calckiller.isSet():
+        try:
+            # Base calculation only on torrents that actually have traffic
+            active_uploads = 0
+            # Total upload rate
+            totalup = 0
+
+            # If there is an upload cap and there are torrents to cap
+            if global_max_upload_rate > 0 and len(threads) > 0:
+                # For each torrent
+                for file, threadinfo in threads.items():
+                    uprate = threadinfo.get('uprate', 0)
+                    # add torrents upload rate to the total
+                    totalup += uprate
+
+                    # If the torrent is uploading it is considered active, 
+                    # otherwise ignored for per-torrent rate cap calculation
+                    if uprate > 0:
+                        active_uploads += 1
+                
+                
+                # Minimum by which torrent upload rates can increase 
+                cushion = global_max_upload_rate * 0.05
+                
+                # If there is unused bandwidth greater than the cushion
+                if global_max_upload_rate - totalup > cushion:
+                    # Set the cushion to the amount of unused bandwidth
+                    cushion = global_max_upload_rate - totalup
+                
+                # cushion per torrent
+                cushion_slice = cushion / len(threads)
+
+                # max non-cushion bandwith for active torrents
+                active_max = global_max_upload_rate - cushion
+
+                # amount over max active bandwidth
+                reduce_by = totalup - active_max
+
+                # For each torrent
+                for file, threadinfo in threads.items():
+                    # Get the upload rate for this torrent
+                    uprate = threadinfo.get('uprate', 0)
+                    # This tells Connecter the current total upload rate
+                    threadinfo['global_cap']['global_rate'] = totalup
+                    # And the maxium (always one less than totalup so that torrents are always capped)
+                    threadinfo['global_cap']['global_max_upload_rate'] = totalup - 1
+                    # If not active
+                    if uprate <= 0:
+                        # cap is cushion (per torrent)
+                        threadinfo['global_cap']['global_slice'] = cushion_slice
+                    # Otherwise, if torrent is active
+                    else:
+                        # active cushion slice starts as normal slice
+                        active_cushion_slice = cushion_slice 
+
+                        # Calculate amount to reduce usage
+                        reduce_by_slice = uprate / totalup * reduce_by
+
+                        # a single upload just gets the entire active cushion
+                        if active_uploads > 1:
+                            if uprate > totalup / active_uploads:
+                                active_cushion_slice = cushion_slice - cushion_slice / active_uploads
+                            elif uprate < totalup / active_uploads:
+                                active_cushion_slice = cushion_slice + cushion_slice / active_uploads
+
+                        # Calculate new slice
+                        threadinfo['global_cap']['global_slice'] = uprate - reduce_by_slice + active_cushion_slice
+
+                    if debug_level >= 3 and debug_sec > 60:
+                        if uprate <= 0:
+                            reduce_by_slice = 0
+
+                        downrate = threadinfo.get('downrate', 0)
+                        log("%s: slice: %.0f, uprate: %.0f, downrate: %.0f, reduce_by: %.0f, cushion_slice: %0.f" % (file, threadinfo['global_cap']['global_slice'], uprate, downrate, reduce_by_slice, cushion_slice))
+                if debug_level >= 2 and debug_sec > 60:
+                    log("Summary: active_max: %.0f, totalup: %.0f, threads: %s, active: %s, reduce_by: %0.f, cushion: %0.f" % (active_max, totalup, len(threads), active_uploads, reduce_by, cushion))
+                if debug_sec <= 60:
+                    debug_sec += 1
                 else:
-                    del threadinfo['thread']
-                    threadinfo['timeout'] = 10
-            # dealing with files that dissapear
-            if file not in files:
-                print 'Torrent file dissapeared, killing %s' % file
-                stdout.flush()
-                if threadinfo.get('timeout', -1) == -1:
-                    threadinfo['kill'].set()
-                    threadinfo['thread'].join()
-                # if this thread was filechecking, open it up
-                if threadinfo.get('checking', None): 
-                    filecheck.release()
-                del threads[file]
-        for file in deadfiles:
-            # if the file dissapears, remove it from our dead list
-            if file not in files: 
-                deadfiles.remove(file)
+                    debug_sec = 0
+        except:
+            # Print a stack trace
+            traceback.print_exc()
+            # and log it
+            log(traceback.format_exc())
+            # But keep on going
+            pass
         sleep(1)
 
 def display_thread(displaykiller):
-    interval = 1.0
+    global display_interval
+    interval = display_interval
+    hoursec = 0
+    houruprate = 0
+    hourdownrate = 0
+    houruptotal = 0
+    hourdowntotal = 0
     global threads, status
     while 1:
-        # display file info
-        if (displaykiller.isSet()): 
-            break
-        totalup = 0
-        totaldown = 0
-        totaluptotal = 0.0
-        totaldowntotal = 0.0
-        tdis = threads.items()
-        tdis.sort()
-        for file, threadinfo in tdis: 
-            uprate = threadinfo.get('uprate', 0)
-            downrate = threadinfo.get('downrate', 0)
-            uptxt = fmtsize(uprate, padded = 0)
-            downtxt = fmtsize(downrate, padded = 0)
-            uptotal = threadinfo.get('uptotal', 0.0)
-            downtotal = threadinfo.get('downtotal', 0.0)
-            uptotaltxt = fmtsize(uptotal, baseunit = 2, padded = 0)
-            downtotaltxt = fmtsize(downtotal, baseunit = 2, padded = 0)
-            filename = threadinfo.get('savefile', file)
-            if threadinfo.get('timeout', 0) > 0:
-                trys = threadinfo.get('try', 1)
-                timeout = threadinfo.get('timeout')
-                print '%s: try %d died, retry in %d' % (filename, trys, timeout)
+        try:
+            # display file info
+            if (displaykiller.isSet()): 
+                break
+            totalup = 0
+            totaldown = 0
+            totaluptotal = 0.0
+            totaldowntotal = 0.0
+            tdis = threads.items()
+            tdis.sort()
+            for file, threadinfo in tdis: 
+                uprate = threadinfo.get('uprate', 0)
+                downrate = threadinfo.get('downrate', 0)
+                uptxt = fmtsize(uprate, padded = 0)
+                downtxt = fmtsize(downrate, padded = 0)
+                uptotal = threadinfo.get('uptotal', 0.0)
+                downtotal = threadinfo.get('downtotal', 0.0)
+                uptotaltxt = fmtsize(uptotal, baseunit = 2, padded = 0)
+                downtotaltxt = fmtsize(downtotal, baseunit = 2, padded = 0)
+                filename = threadinfo.get('savefile', file)
+                if threadinfo.get('timeout', 0) > 0:
+                    trys = threadinfo.get('try', 1)
+                    timeout = threadinfo.get('timeout')
+                    print '%s: try %d died, retry in %d' % (filename, trys, timeout)
+                else:
+                    status = threadinfo.get('status','')
+                    print '%s: Spd: %s/s:%s/s Tot: %s:%s [%s]' % (filename, uptxt, downtxt, uptotaltxt, downtotaltxt, status)
+                totalup += uprate
+                totaldown += downrate
+                totaluptotal += uptotal
+                totaldowntotal += downtotal
+            # display totals line
+            houruprate += totalup
+            hourdownrate + totaldown
+            totaluptxt = fmtsize(totalup, padded = 0)
+            totaldowntxt = fmtsize(totaldown, padded = 0)
+            totaluptotaltxt = fmtsize(totaluptotal, baseunit = 2, padded = 0)
+            totaldowntotaltxt = fmtsize(totaldowntotal, baseunit = 2, padded = 0)
+            print 'All: Spd: %s/s :%s/s, Tot: %s :%s' % (totaluptxt, totaldowntxt, totaluptotaltxt, totaldowntotaltxt)
+            print
+            stdout.flush()
+            if hoursec < 3600 / interval:
+                hoursec +=1
             else:
-                status = threadinfo.get('status','')
-                print '%s: Spd: %s/s:%s/s Tot: %s:%s [%s]' % (filename, uptxt, downtxt, uptotaltxt, downtotaltxt, status)
-            totalup += uprate
-            totaldown += downrate
-            totaluptotal += uptotal
-            totaldowntotal += downtotal
-        # display totals line
-        totaluptxt = fmtsize(totalup, padded = 0)
-        totaldowntxt = fmtsize(totaldown, padded = 0)
-        totaluptotaltxt = fmtsize(totaluptotal, baseunit = 2, padded = 0)
-        totaldowntotaltxt = fmtsize(totaldowntotal, baseunit = 2, padded = 0)
-        print 'All: Spd: %s/s:%s/s Tot: %s:%s' % (totaluptxt, totaldowntxt, totaluptotaltxt, totaldowntotaltxt)
-        print
-        stdout.flush()
+                hoursec = 0
+                hourupratetxt = fmtsize(houruprate / 3600 / interval, padded = 0)
+                hourdownratetxt = fmtsize(hourdownrate / 3600 / interval , padded = 0)
+
+                log('Speed: %s/s : %s/s, Total: %s :%s' % (hourupratetxt, hourdownratetxt, totaluptotaltxt, totaldowntotaltxt))
+                houruprate = 0
+                hourdownrate = 0
+        except:
+            # Print a stack trace
+            traceback.print_exc()
+            # and log it
+            log(traceback.format_exc())
+            # But keep on going
+            pass
         sleep(interval)
 
+
 class StatusUpdater:
-    def __init__(self, file, params, name):
+    def __init__(self, file, params, name, checking = 0, global_cap = None):
         self.file = file
         self.params = params
         self.name = name
         self.myinfo = threads[name]
         self.done = 0
-        self.checking = 0
+        self.checking = checking
+        self.global_cap = global_cap
+        self.myinfo['checking'] = checking
         self.activity = 'starting'
-        self.display()
+        self.myinfo['status'] = self.activity
         self.myinfo['errors'] = []
 
     def download(self): 
-        download(self.params + ['--responsefile', self.file], self.choose, self.display, self.finished, self.err, self.myinfo['kill'], 80)
-        print 'Torrent %s stopped' % self.file
-        stdout.flush()
+        try:
+            # Initial bittorrent session for file (80 is 80-column display)
+            download(self.params + ['--responsefile', self.file], self.choose, self.display, self.finished, self.err, self.myinfo['kill'], 80, global_cap = self.global_cap)
+            print 'Torrent %s stopped' % self.file
+            stdout.flush()
+        except:
+            # Print a stack trace
+            traceback.print_exc()
+            # and log it
+            log(traceback.format_exc())
+            # But keep on going
+            pass
 
     def finished(self): 
         self.done = 1
@@ -157,74 +378,185 @@
 
     def choose(self, default, size, saveas, dir):
         global filecheck
-        self.myinfo['downfile'] = default
-        self.myinfo['filesize'] = fmtsize(size)
-        if saveas == '': 
-            saveas = default
-        # it asks me where I want to save it before checking the file.. 
-        if exists(self.file[:-len(ext)]) and (getsize(self.file[:-len(ext)]) > 0):
-            # file will get checked
-            while (not filecheck.acquire(0) and not self.myinfo['kill'].isSet()):
-                self.myinfo['status'] = 'disk wait'
-                sleep(0.1)
-            if not self.myinfo['kill'].isSet():
-                self.checking = 1
-                self.myinfo['checking'] = 1
-        self.myinfo['savefile'] = self.file[:-len(ext)]
+        try:
+            # Save file to the default location and same name as torrent, 
+            # without the .torrent
+            self.myinfo['downfile'] = default
+            self.myinfo['filesize'] = fmtsize(size)
+            if saveas == '': 
+                saveas = default
+                self.myinfo['savefile'] = self.file[:-len(ext)]
+        except:
+            # Print a stack trace
+            traceback.print_exc()
+            # and log it
+            log(traceback.format_exc())
+            # But keep on going
+            pass
+
         return self.file[:-len(ext)]
     
     def display(self, dict = {}):
-        fractionDone = dict.get('fractionDone', None)
-        timeEst = dict.get('timeEst', None)
-        activity = dict.get('activity', None) 
-        global status
-        if activity is not None and not self.done: 
-            if activity == 'checking existing file':
-                self.activity = 'disk check'
-            elif activity == 'connecting to peers':
-                self.activity = 'connecting'
+        try:
+            # Percent done
+            fractionDone = dict.get('fractionDone', None)
+            # Estimated time remaining
+            timeEst = dict.get('timeEst', None)
+            # What the torrent is doing
+            activity = dict.get('activity', None) 
+            global status
+            # If torrent is not done non-download tasks
+            if activity is not None and not self.done: 
+                if activity == 'checking existing file':
+                    self.activity = 'disk check'
+                elif activity == 'connecting to peers':
+                    self.activity = 'connecting'
+                else:
+                    self.activity = activity
+            # Otherwise, if downloading
+            elif timeEst is not None: 
+                # Set display to time remaing for download to complete
+                self.activity = fmttime(timeEst, 1)
+            # If a task is partially done
+            if fractionDone is not None: 
+                # Display task and percent done
+                self.myinfo['status'] = '%s %.0f%%' % (self.activity, fractionDone * 100)
+            # Otherwise, for non-partial tasks
             else:
-                self.activity = activity
-        elif timeEst is not None: 
-            self.activity = fmttime(timeEst, 1)
-        if fractionDone is not None: 
-            self.myinfo['status'] = '%s %.0f%%' % (self.activity, fractionDone * 100)
-        else:
-            self.myinfo['status'] = self.activity
-        if self.activity != 'checking existing file' and self.checking:
-            # we finished checking our files. 
-            filecheck.release()
-            self.checking = 0
-            self.myinfo['checking'] = 0
-        if dict.has_key('upRate'):
-            self.myinfo['uprate'] = dict['upRate']
-        if dict.has_key('downRate'):
-            self.myinfo['downrate'] = dict['downRate']
-        if dict.has_key('upTotal'):
-            self.myinfo['uptotal'] = dict['upTotal']
-        if dict.has_key('downTotal'):
-            self.myinfo['downtotal'] = dict['downTotal']
+                # Display task without percent done
+                self.myinfo['status'] = self.activity
+            # If we think we're checking but we're actually done
+            if self.activity != 'checking existing file' and self.activity != 'disk check' and self.checking:
+                # we finished checking our files, so release the filecheck lock
+                filecheck.release()
+                # and tell ourselves we're not checking anymore
+                self.checking = 0
+                self.myinfo['checking'] = 0
+            # Record upload rate from torrent thread to our own variables
+            if dict.has_key('upRate'):
+                self.myinfo['uprate'] = dict['upRate']
+            # Record download rate from torrent thread to our own variables
+            if dict.has_key('downRate'):
+                self.myinfo['downrate'] = dict['downRate']
+            # Record upload total from torrent thread to our own variables
+            if dict.has_key('upTotal'):
+                self.myinfo['uptotal'] = dict['upTotal']
+            # Record download total from torrent thread to our own variables
+            if dict.has_key('downTotal'):
+                self.myinfo['downtotal'] = dict['downTotal']
+        except:
+            # Print a stack trace
+            traceback.print_exc()
+            # and log it
+            log(traceback.format_exc())
+            # But keep on going
+            pass
+
 
 if __name__ == '__main__':
+    def kill_torrents():
+        # Kill all torrents
+        for file, threadinfo in threads.items(): 
+            status = 'Killing torrent %s' % file
+            # If the thread is still active
+            if threadinfo.get('thread'):
+                # Set the kill event; tells thread that it should die
+                threadinfo['kill'].set() 
+                # Attach to thread until it stops
+                threadinfo['thread'].join() 
+            # Remove thread from list of active threads
+            del threads[file]
+        # Kill display thread by setting even that tells it that it should die
+        displaykiller.set()
+        # And attach to the thread until it exits
+        displaythread.join()
+        # Kill slice calcuation thread by setting event that tells thread to die
+        calckiller.set()
+        # And attach to thread until it exits
+        calcslicethread.join()
+        # and exit
     if (len(argv) < 2):
         print """Usage: btlaunchmany.py <directory> <global options>
   <directory> - directory to look for .torrent files (non-recursive)
   <global options> - options to be applied to all torrents (see btdownloadheadless.py)
+   --global_max_upload_rate <rate> - combined maximum upload rate in kB/s (optional)
+   --logfile <filename> - file to which to log errors and debugging
+   --display_interval display_rate - number of seconds between display updates
+   --debug_level <#> - level of verbosity for debugging
 """
         exit(-1)
+
+    # We need to determine the global_upload_rate 
+    # So parse the command line after the torrent directory URL
+     
+    logfilename = None
+    # If we have global maximum upload rate
+    for i in xrange(4):
+        if len(argv) > 3:
+            if argv[2] == "--global_max_upload_rate":
+                # convert the rate to an integer representing KB/s
+                global_max_upload_rate = locale.atoi(argv[3]) * 1024
+                # Remove --global_max_upload_rate rate from the argument list
+                # so that download.py (the bittorrent downloader, which is used by
+                # btlaunchmany to do the actual downloads) doesn't get this
+                # parameter, which it doesn't understand
+                del argv[3]
+                del argv[2]
+            elif argv[2] == "--logfile":
+                logfilename = argv[3]
+                del argv[3]
+                del argv[2]
+            elif argv[2] == "--display_interval":
+                display_interval = locale.atoi(argv[3])
+                del argv[3]
+                del argv[2]
+            elif argv[2] == "--debug_level":
+                debug_level = locale.atoi(argv[3])
+                del argv[3]
+                del argv[2]
+
+    try:
+        if logfilename != None:
+            print logfilename
+            logfile = open(logfilename, 'a')
+    except EnvironmentError:
+        print("btlaunchmany.py died trying to open the logfile")
+        sys.exit(1)
+    log("bittorrent started")
+    log("Parameters: global_max_upload_rate: %s, logfile: %s, interval %s" % (global_max_upload_rate, logfilename, display_interval))
+
+
+
     try:
+        # Add event that allows us to cancel updating display when
+        # we want to exit this program
         displaykiller = Event()
+        # Create display updating thread and start it
         displaythread = Thread(target = display_thread, name = 'display', args = [displaykiller])
         displaythread.start()
+        # Add event that allows us to cancel global slice calculation when
+        # we want to exit this program
+        calckiller = Event()
+        # Create global slice calculation thread ands start it
+        calcslicethread = Thread(target = calc_slice, name='calc_slice', args = [calckiller])
+        calcslicethread.start()
+        # Execute the main action loop, which checks for new torrent files and
+        # initiates a bittorrent transfer for it or removes a bittorrent
+        # transfer thread for files that have been removed
         dropdir_mainloop(argv[1], argv[2:])
+        # Never exits, unless there is an exception
+    # Until interrupted by Ctrl-C or kill -INT
     except KeyboardInterrupt: 
         print '^C caught! Killing torrents..'
-        for file, threadinfo in threads.items(): 
-            status = 'Killing torrent %s' % file
-            threadinfo['kill'].set() 
-            threadinfo['thread'].join() 
-            del threads[file]
-        displaykiller.set()
-        displaythread.join()
+        # kill off torrents
+        kill_torrents()
+        # and exit
+    # Or there is an uncaught exception
     except:
+        # print a stack trace
         traceback.print_exc()
+        # and log it
+        log(traceback.format_exc())
+        # kill off torrents
+        kill_torrents()
+        # and exit
diff -Naur bittorrent_3.4.2-11.1/debian/bittorrent-downloader.bittorrent.1 bittorrent-3.4.2/debian/bittorrent-downloader.bittorrent.1
--- bittorrent_3.4.2-11.1/debian/bittorrent-downloader.bittorrent.1	2008-05-31 19:44:44.000000000 -0400
+++ bittorrent-3.4.2/debian/bittorrent-downloader.bittorrent.1	2008-06-01 02:00:45.000000000 -0400
@@ -117,6 +117,16 @@
 .B \-\-rarest_first_priority_cutoff \fInum\fP
 the number of peers which need to have a piece before other partials take
 priority over rarest first (default 3)
+.TP
+.B \-\-min_outgoing_port \fIportnum\fP
+set \fIportnum\fP as the minimum port from which we are allowed to connect
+(default 1024).  Useful to set this for strict (outgoing blocking)
+firewalls that allow bittorrent out only from specific ports.
+.TP
+.B \-\-max_outgoing_port \fIportnum\fP
+set \fIportnum\fP as the maximum port from which we are allowed to
+connect (default 65535).  Useful to set this for strict (outgoing blocking)
+firewalls that allow bittorrent out only from specific ports.
 
 .SH SEE ALSO
 .BR bttrack (1),
diff -Naur bittorrent_3.4.2-11.1/debian/changelog bittorrent-3.4.2/debian/changelog
--- bittorrent_3.4.2-11.1/debian/changelog	2008-05-31 19:44:44.000000000 -0400
+++ bittorrent-3.4.2/debian/changelog	2008-07-01 10:55:23.000000000 -0400
@@ -1,3 +1,26 @@
+bittorrent (3.4.2-11.2dfd06~005) unstable; urgency=low
+	
+  * Modify global upload throttle so that it uses full bandwidth, except
+    for a small cushion for inactive torrents to start downloading
+
+ -- Daniel Dickinson <cshore@fionavar.ca>  Mon, 30 Jun 2008 22:46:45 -0500	
+
+bittorrent (3.4.2-11.2dfd05~015) unstable; urgency=low
+	
+  * Add global upload rate throttle that keeps a cushion for new
+    sessions sessions under max to grow, but otherwise allocates full max
+    global upload rate to sessions that want to upload at a rate greater
+    than the per-session maximum upload rate (calculated as global
+    maximum divided by number of active uploads) (Closes: #481276)
+  * Fix deadfile handling and checking locking.  (Closes: #478758)
+  * Fix exception handling (or rather lack thereof)
+  * Comment a bunch of code
+  * Change startup disk check to be one torrent at a time. (Closes: #482478)
+  * Add outgoing port range limiting in order to play well with strict 
+    firewalls.  (Closes: #481276)
+
+ -- Daniel Dickinson <cshore@fionavar.ca>  Sun, 01 Jun 2008 02:09:55 -0500
+	
 bittorrent (3.4.2-11.1) unstable; urgency=low
 
   * Non-maintainer upload.
--- End Message ---