diff --git a/paramiko/channel.py b/paramiko/channel.py index 3c9fa35..db85aac 100644 --- a/paramiko/channel.py +++ b/paramiko/channel.py @@ -88,6 +88,9 @@ class Channel (object): self.combine_stderr = False self.exit_status = -1 + def __del__(self): + self.close() + def __repr__(self): """ Return a string representation of this object, for debugging. @@ -455,11 +458,7 @@ class Channel (object): Close the channel. All future read/write operations on the channel will fail. The remote end will receive no more data (after queued data is flushed). Channels are automatically closed when their L{Transport} - is closed. - - Note that because of peculiarities with the way python's garbage - collection works on cycles, channels will B{not} be automatically - closed by the garbage collector. + is closed or when they are garbage collected. """ self.lock.acquire() try: diff --git a/paramiko/sftp_client.py b/paramiko/sftp_client.py index a7afdb0..84e32fe 100644 --- a/paramiko/sftp_client.py +++ b/paramiko/sftp_client.py @@ -63,6 +63,9 @@ class SFTPClient (BaseSFTP): self.sock.get_name() + '.sftp') self.ultra_debug = transport.get_hexdump() self._send_version() + + def __del__(self): + self.close() def from_transport(selfclass, t): """ diff --git a/paramiko/sftp_file.py b/paramiko/sftp_file.py index 51c7d07..a359510 100644 --- a/paramiko/sftp_file.py +++ b/paramiko/sftp_file.py @@ -41,6 +41,9 @@ class SFTPFile (BufferedFile): self.handle = handle BufferedFile._set_mode(self, mode, bufsize) + def __del__(self): + self.close() + def close(self): BufferedFile.close(self) try: diff --git a/paramiko/transport.py b/paramiko/transport.py index 86e33ed..9ad8b45 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -21,6 +21,7 @@ L{BaseTransport} handles the core SSH2 protocol. """ import sys, os, string, threading, socket, struct, time +import weakref from common import * from ssh_exception import SSHException @@ -205,7 +206,7 @@ class BaseTransport (threading.Thread): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((hostname, port)) # okay, normal socket-ish flow here... - threading.Thread.__init__(self, target=self._run) + threading.Thread.__init__(self) self.randpool = randpool self.sock = sock # Python < 2.3 doesn't have the settimeout method - RogerB @@ -229,7 +230,7 @@ class BaseTransport (threading.Thread): self.initial_kex_done = False self.in_kex = False self.lock = threading.Lock() # synchronization (always higher level than write_lock) - self.channels = { } # (id -> Channel) + self.channels = weakref.WeakValueDictionary() # (id -> Channel) self.channel_events = { } # (id -> Event) self.channel_counter = 1 self.window_size = 65536 @@ -249,6 +250,9 @@ class BaseTransport (threading.Thread): self.server_accept_cv = threading.Condition(self.lock) self.subsystem_table = { } + def __del__(self): + self.close() + def __repr__(self): """ Returns a string representation of this object, for debugging. @@ -969,7 +973,12 @@ class BaseTransport (threading.Thread): raise SSHException('Unknown client cipher ' + name) return self._cipher_info[name]['class'].new(key, self._cipher_info[name]['mode'], iv) - def _run(self): + def run(self): + # (use the exposed "run" method, because if we specify a thread target + # of a private method, threading.Thread will keep a reference to it + # indefinitely, creating a GC cycle and not letting Transport ever be + # GC'd. it's a bug in Thread.) + # active=True occurs before the thread is launched, to avoid a race _active_threads.append(self) if self.server_mode: @@ -1281,6 +1290,7 @@ class BaseTransport (threading.Thread): # can also free a bunch of stuff here self.local_kex_init = self.remote_kex_init = None self.K = None + self.kex_engine = None if not self.initial_kex_done: # this was the first key exchange self.initial_kex_done = True