From 31d4609a3b8e8d0967402bbc752ff970b3fcd265 Mon Sep 17 00:00:00 2001 From: HelloZeroNet Date: Mon, 23 Feb 2015 23:33:31 +0100 Subject: [PATCH] version 0.2.4, peerPing and peerGetFile commands, old content update bugfix, new network code and protocol, connection share between sites, connection reuse, dont retry bad file more than 3 times in 20 min, multi threaded include file download, shuffle peers before publish, simple internal stats page, dont retry on failed peers, more than 10 peers publish bugfix --- src/Config.py | 15 +- src/Connection/Connection.py | 234 ++++++++++++++++++++++++++ src/Connection/ConnectionBenchmark.py | 136 +++++++++++++++ src/Connection/ConnectionServer.py | 231 +++++++++++++++++++++++++ src/Connection/__init__.py | 2 + src/Content/ContentManager.py | 5 +- src/File/FileRequest.py | 42 +++-- src/File/FileServer.py | 58 ++----- src/Peer/Peer.py | 69 ++++---- src/Site/Site.py | 54 ++++-- src/Ui/UiRequest.py | 24 ++- src/Ui/UiServer.py | 3 +- src/Worker/Worker.py | 3 +- src/Worker/WorkerManager.py | 15 +- src/main.py | 26 ++- src/util/Event.py | 2 +- tools/upnpc/upnpc-static.exe | Bin 29696 -> 0 bytes zeronet.py | 1 + 18 files changed, 790 insertions(+), 130 deletions(-) create mode 100644 src/Connection/Connection.py create mode 100644 src/Connection/ConnectionBenchmark.py create mode 100644 src/Connection/ConnectionServer.py create mode 100644 src/Connection/__init__.py delete mode 100644 tools/upnpc/upnpc-static.exe diff --git a/src/Config.py b/src/Config.py index 2b0b0538..af277082 100644 --- a/src/Config.py +++ b/src/Config.py @@ -3,7 +3,7 @@ import ConfigParser class Config(object): def __init__(self): - self.version = "0.2.3" + self.version = "0.2.4" self.parser = self.createArguments() argv = sys.argv[:] # Copy command line arguments argv = self.parseConfig(argv) # Add arguments from config file @@ -52,6 +52,19 @@ class Config(object): action = subparsers.add_parser("siteVerify", help='Verify site files using sha512: address') action.add_argument('address', help='Site to verify') + # PeerPing + action = subparsers.add_parser("peerPing", help='Send Ping command to peer') + action.add_argument('peer_ip', help='Peer ip') + action.add_argument('peer_port', help='Peer port') + + # PeerGetFile + action = subparsers.add_parser("peerGetFile", help='Request and print a file content from peer') + action.add_argument('peer_ip', help='Peer ip') + action.add_argument('peer_port', help='Peer port') + action.add_argument('site', help='Site address') + action.add_argument('filename', help='File name to request') + + # Config parameters parser.add_argument('--debug', help='Debug mode', action='store_true') diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py new file mode 100644 index 00000000..33dd1566 --- /dev/null +++ b/src/Connection/Connection.py @@ -0,0 +1,234 @@ +import logging, socket, time +from cStringIO import StringIO +import gevent, msgpack +from Config import config +from Debug import Debug +try: + import zmq.green as zmq +except: + zmq = None + +class Connection: + def __init__(self, server, ip, port, sock=None): + self.sock = sock + self.ip = ip + self.port = port + self.peer_id = None # Bittorrent style peer id (not used yet) + self.id = server.last_connection_id + self.protocol = "?" + server.last_connection_id += 1 + + self.server = server + self.log = logging.getLogger(str(self)) + self.unpacker = msgpack.Unpacker() # Stream incoming socket messages here + self.req_id = 0 # Last request id + self.handshake = None # Handshake info got from peer + self.event_handshake = gevent.event.AsyncResult() # Solves on handshake received + self.closed = False + + self.zmq_sock = None # Zeromq sock if outgoing connection + self.zmq_queue = [] # Messages queued to send + self.zmq_working = False # Zmq currently working, just add to queue + self.forward_thread = None # Zmq forwarder thread + + self.waiting_requests = {} # Waiting sent requests + if not sock: self.connect() # Not an incoming connection, connect to peer + + + def __str__(self): + return "Conn#%2s %-12s [%s]" % (self.id, self.ip, self.protocol) + + def __repr__(self): + return "<%s>" % self.__str__() + + + # Open connection to peer and wait for handshake + def connect(self): + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.connect((self.ip, self.port)) + # Detect protocol + self.send({"cmd": "handshake", "req_id": 0, "params": self.handshakeInfo()}) + gevent.spawn(self.messageLoop) + return self.event_handshake.get() # Wait for handshake + + + + # Handle incoming connection + def handleIncomingConnection(self, sock): + firstchar = sock.recv(1) # Find out if pure socket or zeromq + if firstchar == "\xff": # Backward compatiblity: forward data to zmq + if config.debug_socket: self.log.debug("Fallback incoming connection to ZeroMQ") + + self.protocol = "zeromq" + self.log.name = str(self) + self.event_handshake.set(self.protocol) + + if self.server.zmq_running: + zmq_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + zmq_sock.connect(("127.0.0.1", self.server.zmq_port)) + zmq_sock.send(firstchar) + + self.forward_thread = gevent.spawn(self.server.forward, self, zmq_sock, sock) + self.server.forward(self, sock, zmq_sock) + self.close() # Forward ended close connection + else: + self.config.debug("ZeroMQ Server not running, exiting!") + else: # Normal socket + self.messageLoop(firstchar) + + + # Message loop for connection + def messageLoop(self, firstchar=None): + sock = self.sock + if not firstchar: firstchar = sock.recv(1) + if firstchar == "\xff": # Backward compatibility to zmq + self.sock.close() # Close normal socket + if zmq: + if config.debug_socket: self.log.debug("Connecting as ZeroMQ") + self.protocol = "zeromq" + self.log.name = str(self) + self.event_handshake.set(self.protocol) # Mark handshake as done + + try: + context = zmq.Context() + zmq_sock = context.socket(zmq.REQ) + zmq_sock.hwm = 1 + zmq_sock.setsockopt(zmq.RCVTIMEO, 50000) # Wait for data arrive + zmq_sock.setsockopt(zmq.SNDTIMEO, 5000) # Wait for data send + zmq_sock.setsockopt(zmq.LINGER, 500) # Wait for zmq_sock close + zmq_sock.connect('tcp://%s:%s' % (self.ip, self.port)) + self.zmq_sock = zmq_sock + except Exception, err: + self.log.debug("Socket error: %s" % Debug.formatException(err)) + else: + return False # No zeromq connection supported + else: # Normal socket + self.protocol = "v2" + self.log.name = str(self) + self.event_handshake.set(self.protocol) # Mark handshake as done + + unpacker = self.unpacker + unpacker.feed(firstchar) # Feed the first char we already requested + try: + while True: + buff = sock.recv(16*1024) + if not buff: break # Connection closed + unpacker.feed(buff) + for message in unpacker: + self.handleMessage(message) + except Exception, err: + self.log.debug("Socket error: %s" % Debug.formatException(err)) + self.close() # MessageLoop ended, close connection + + + # Read one line (not used) + def recvLine(self): + sock = self.sock + data = sock.recv(16*1024) + if not data: return + if not data.endswith("\n"): # Multipart, read until \n + buff = StringIO() + buff.write(data) + while not data.endswith("\n"): + data = sock.recv(16*1024) + if not data: break + buff.write(data) + return buff.getvalue().strip("\n") + + return data.strip("\n") + + + # My handshake info + def handshakeInfo(self): + return { + "version": config.version, + "protocol": "v2", + "peer_id": self.server.peer_id, + "fileserver_port": config.fileserver_port + } + + + # Handle incoming message + def handleMessage(self, message): + if message.get("cmd") == "response": # New style response + if message["to"] in self.waiting_requests: + self.waiting_requests[message["to"]].set(message) # Set the response to event + del self.waiting_requests[message["to"]] + elif message["to"] == 0: # Other peers handshake + if config.debug_socket: self.log.debug("Got handshake response: %s" % message) + self.handshake = message + self.port = message["fileserver_port"] # Set peer fileserver port + else: + self.log.debug("Unknown response: %s" % message) + elif message.get("cmd"): # Handhsake request + if message["cmd"] == "handshake": + self.handshake = message["params"] + self.port = self.handshake["fileserver_port"] # Set peer fileserver port + if config.debug_socket: self.log.debug("Handshake request: %s" % message) + data = self.handshakeInfo() + data["cmd"] = "response" + data["to"] = message["req_id"] + self.send(data) + else: + self.server.handleRequest(self, message) + else: # Old style response, no req_id definied + if config.debug_socket: self.log.debug("Old style response, waiting: %s" % self.waiting_requests.keys()) + last_req_id = min(self.waiting_requests.keys()) # Get the oldest waiting request and set it true + self.waiting_requests[last_req_id].set(message) + del self.waiting_requests[last_req_id] # Remove from waiting request + + + + # Send data to connection + def send(self, data): + if config.debug_socket: self.log.debug("Send: %s" % data.get("cmd")) + if self.protocol == "zeromq": + if self.zmq_sock: # Outgoing connection + self.zmq_queue.append(data) + if self.zmq_working: + self.log.debug("ZeroMQ already working...") + return + while self.zmq_queue: + self.zmq_working = True + data = self.zmq_queue.pop(0) + self.zmq_sock.send(msgpack.packb(data)) + self.handleMessage(msgpack.unpackb(self.zmq_sock.recv())) + self.zmq_working = False + + else: # Incoming request + self.server.zmq_sock.send(msgpack.packb(data)) + else: # Normal connection + self.sock.sendall(msgpack.packb(data)) + + + # Create and send a request to peer + def request(self, cmd, params={}): + self.req_id += 1 + data = {"cmd": cmd, "req_id": self.req_id, "params": params} + event = gevent.event.AsyncResult() # Create new event for response + self.waiting_requests[self.req_id] = event + self.send(data) # Send request + res = event.get() # Wait until event solves + + return res + + + # Close connection + def close(self): + if self.closed: return False # Already closed + self.closed = True + if config.debug_socket: self.log.debug("Closing connection, waiting_requests: %s..." % len(self.waiting_requests)) + for request in self.waiting_requests.values(): # Mark pending requests failed + request.set(False) + self.waiting_requests = {} + self.server.removeConnection(self) # Remove connection from server registry + try: + if self.forward_thread: + self.forward_thread.kill(exception=Debug.Notify("Closing connection")) + if self.zmq_sock: + self.zmq_sock.close() + if self.sock: + self.sock.shutdown(gevent.socket.SHUT_WR) + self.sock.close() + except Exception, err: + if config.debug_socket: self.log.debug("Close error: %s" % Debug.formatException(err)) diff --git a/src/Connection/ConnectionBenchmark.py b/src/Connection/ConnectionBenchmark.py new file mode 100644 index 00000000..5605398d --- /dev/null +++ b/src/Connection/ConnectionBenchmark.py @@ -0,0 +1,136 @@ +import time, socket, msgpack +from cStringIO import StringIO + +print "Connecting..." +sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +sock.connect(("localhost", 1234)) + + +print "1 Threaded: Send, receive 10000 ping request...", +s = time.time() +for i in range(10000): + sock.sendall(msgpack.packb({"cmd": "Ping"})) + req = sock.recv(16*1024) +print time.time()-s, repr(req), time.time()-s + + +print "1 Threaded: Send, receive, decode 10000 ping request...", +s = time.time() +unpacker = msgpack.Unpacker() +reqs = 0 +for i in range(10000): + sock.sendall(msgpack.packb({"cmd": "Ping"})) + unpacker.feed(sock.recv(16*1024)) + for req in unpacker: + reqs += 1 +print "Found:", req, "x", reqs, time.time()-s + + +print "1 Threaded: Send, receive, decode, reconnect 1000 ping request...", +s = time.time() +unpacker = msgpack.Unpacker() +reqs = 0 +for i in range(1000): + sock.sendall(msgpack.packb({"cmd": "Ping"})) + unpacker.feed(sock.recv(16*1024)) + for req in unpacker: + reqs += 1 + sock.close() + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(("localhost", 1234)) +print "Found:", req, "x", reqs, time.time()-s + + +print "1 Threaded: Request, receive, decode 10000 x 10k data request...", +s = time.time() +unpacker = msgpack.Unpacker() +reqs = 0 +for i in range(10000): + sock.sendall(msgpack.packb({"cmd": "Bigdata"})) + + """buff = StringIO() + data = sock.recv(16*1024) + buff.write(data) + if not data: + break + while not data.endswith("\n"): + data = sock.recv(16*1024) + if not data: break + buff.write(data) + req = msgpack.unpackb(buff.getvalue().strip("\n")) + reqs += 1""" + + req_found = False + while not req_found: + buff = sock.recv(16*1024) + unpacker.feed(buff) + for req in unpacker: + reqs += 1 + req_found = True + break # Only process one request +print "Found:", len(req["res"]), "x", reqs, time.time()-s + + +print "10 Threaded: Request, receive, decode 10000 x 10k data request...", +import gevent +s = time.time() +reqs = 0 +req = None +def requester(): + global reqs, req + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(("localhost", 1234)) + unpacker = msgpack.Unpacker() + for i in range(1000): + sock.sendall(msgpack.packb({"cmd": "Bigdata"})) + + req_found = False + while not req_found: + buff = sock.recv(16*1024) + unpacker.feed(buff) + for req in unpacker: + reqs += 1 + req_found = True + break # Only process one request + +threads = [] +for i in range(10): + threads.append(gevent.spawn(requester)) +gevent.joinall(threads) +print "Found:", len(req["res"]), "x", reqs, time.time()-s + + +print "1 Threaded: ZeroMQ Send, receive 1000 ping request...", +s = time.time() +import zmq.green as zmq +c = zmq.Context() +zmq_sock = c.socket(zmq.REQ) +zmq_sock.connect('tcp://127.0.0.1:1234') +for i in range(1000): + zmq_sock.send(msgpack.packb({"cmd": "Ping"})) + req = zmq_sock.recv(16*1024) +print "Found:", req, time.time()-s + + +print "1 Threaded: ZeroMQ Send, receive 1000 x 10k data request...", +s = time.time() +import zmq.green as zmq +c = zmq.Context() +zmq_sock = c.socket(zmq.REQ) +zmq_sock.connect('tcp://127.0.0.1:1234') +for i in range(1000): + zmq_sock.send(msgpack.packb({"cmd": "Bigdata"})) + req = msgpack.unpackb(zmq_sock.recv(1024*1024)) +print "Found:", len(req["res"]), time.time()-s + + +print "1 Threaded: direct ZeroMQ Send, receive 1000 x 10k data request...", +s = time.time() +import zmq.green as zmq +c = zmq.Context() +zmq_sock = c.socket(zmq.REQ) +zmq_sock.connect('tcp://127.0.0.1:1233') +for i in range(1000): + zmq_sock.send(msgpack.packb({"cmd": "Bigdata"})) + req = msgpack.unpackb(zmq_sock.recv(1024*1024)) +print "Found:", len(req["res"]), time.time()-s \ No newline at end of file diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py new file mode 100644 index 00000000..e8fcb22f --- /dev/null +++ b/src/Connection/ConnectionServer.py @@ -0,0 +1,231 @@ +from gevent.server import StreamServer +from gevent.pool import Pool +import socket, os, logging, random, string +import gevent, msgpack +import cStringIO as StringIO +from Debug import Debug +from Connection import Connection +from Config import config + + +class ConnectionServer: + def __init__(self, ip=None, port=None, request_handler=None): + self.ip = ip + self.port = port + self.last_connection_id = 1 # Connection id incrementer + self.log = logging.getLogger(__name__) + + self.connections = [] # Connections + self.ips = {} # Connection by ip + self.peer_ids = {} # Connections by peer_ids + + self.running = True + self.zmq_running = False + self.zmq_last_connection = None # Last incoming message client + + self.peer_id = "-ZN0"+config.version.replace(".", "")+"-"+''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(12)) # Bittorrent style peerid + + if port: # Listen server on a port + self.zmq_port = port-1 + self.pool = Pool(1000) # do not accept more than 1000 connections + self.stream_server = StreamServer((ip.replace("*", ""), port), self.handleIncomingConnection, spawn=self.pool, backlog=100) + if request_handler: self.handleRequest = request_handler + gevent.spawn(self.zmqServer) # Start ZeroMQ server for backward compatibility + + + + def start(self): + self.running = True + try: + self.log.debug("Binding to: %s:%s" % (self.ip, self.port)) + self.stream_server.serve_forever() # Start normal connection server + except Exception, err: + self.log.info("StreamServer bind error, must be running already: %s" % err) + + + def stop(self): + self.running = False + self.stream_server.stop() + + + def handleIncomingConnection(self, sock, addr): + ip, port = addr + connection = Connection(self, ip, port, sock) + self.connections.append(connection) + self.ips[ip] = connection + connection.handleIncomingConnection(sock) + + + + def connect(self, ip=None, port=None, peer_id=None): + if peer_id and peer_id in self.peer_ids: # Find connection by peer id + return self.peer_ids.get(peer_id) + if ip in self.ips: # Find connection by ip + return self.ips[ip] + # No connection found yet + try: + connection = Connection(self, ip, port) + self.ips[ip] = connection + self.connections.append(connection) + except Exception, err: + self.log.debug("%s Connect error: %s" % (ip, Debug.formatException(err))) + raise err + return connection + + + + def removeConnection(self, connection): + if self.ips.get(connection.ip) == connection: # Delete if same as in registry + del self.ips[connection.ip] + if connection in self.connections: + self.connections.remove(connection) + if connection.peer_id and self.peer_ids.get(connection.peer_id) == connection: # Delete if same as in registry + del self.peer_ids[connection.peer_id] + + + def zmqServer(self): + self.log.debug("Starting ZeroMQ on: tcp://127.0.0.1:%s..." % self.zmq_port) + try: + import zmq.green as zmq + context = zmq.Context() + self.zmq_sock = context.socket(zmq.REP) + self.zmq_sock.bind("tcp://127.0.0.1:%s" % self.zmq_port) + self.zmq_sock.hwm = 1 + self.zmq_sock.setsockopt(zmq.RCVTIMEO, 5000) # Wait for data receive + self.zmq_sock.setsockopt(zmq.SNDTIMEO, 50000) # Wait for data send + self.zmq_running = True + except Exception, err: + self.log.debug("ZeroMQ start error: %s" % Debug.formatException(err)) + return False + + while True: + try: + data = self.zmq_sock.recv() + if not data: break + message = msgpack.unpackb(data) + self.zmq_last_connection.handleMessage(message) + except Exception, err: + self.log.debug("ZMQ Server error: %s" % Debug.formatException(err)) + self.zmq_sock.send(msgpack.packb({"error": "%s" % err}, use_bin_type=True)) + + + # Forward incoming data to other socket + def forward(self, connection, source, dest): + data = True + try: + while data: + data = source.recv(16*1024) + self.zmq_last_connection = connection + if data: + dest.sendall(data) + else: + source.shutdown(socket.SHUT_RD) + dest.shutdown(socket.SHUT_WR) + except Exception, err: + self.log.debug("%s ZMQ forward error: %s" % (connection.ip, Debug.formatException(err))) + connection.close() + + +# -- TESTING -- + +def testCreateServer(): + global server + server = ConnectionServer("127.0.0.1", 1234, testRequestHandler) + server.start() + + +def testRequestHandler(connection, req): + print req + if req["cmd"] == "Bigdata": + connection.send({"res": "HelloWorld"*1024}) + else: + connection.send({"res": "pong"}) + + +def testClient(num): + time.sleep(1) + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect(("localhost", 1234)) + for i in range(10): + print "[C%s] send..." % num + s.sendall(msgpack.packb({"cmd": "[C] Ping"})) + print "[C%s] recv..." % num + print "[C%s] %s" % (num, repr(s.recv(1024))) + time.sleep(1) + + +def testSlowClient(num): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect(("localhost", 1234)) + for i in range(1): + print "[C%s] send..." % num + s.sendall(msgpack.packb({"cmd": "Bigdata"})) + print "[C%s] recv..." % num + gevent.spawn_later(1, lambda s: s.send(msgpack.packb({"cmd": "[Z] Ping"})), s) + while 1: + data = s.recv(1000) + if not data: break + print "[C%s] %s" % (num, data) + time.sleep(1) + #s.sendall(msgpack.packb({"cmd": "[C] Ping"})) + + +def testZmqClient(num): + import zmq.green as zmq + c = zmq.Context(1) + for i in range(10): + s = c.socket(zmq.REQ) + s.connect('tcp://127.0.0.1:1234') + print "[Z%s] send..." % num + s.send(msgpack.packb({"cmd": "[Z] Ping %s" % i})) + print "[Z%s] recv..." % num + print "[Z%s] %s" % (num, s.recv(1024)) + s.close() + time.sleep(1) + + +def testZmqSlowClient(num): + import zmq.green as zmq + c = zmq.Context(1) + s = c.socket(zmq.REQ) + for i in range(1): + s.connect('tcp://127.0.0.1:1234') + print "[Z%s] send..." % num + s.send(msgpack.packb({"cmd": "Bigdata"})) + print "[Z%s] recv..." % num + #gevent.spawn_later(1, lambda s: s.send(msgpack.packb({"cmd": "[Z] Ping"})), s) + while 1: + data = s.recv(1024*1024) + if not data: break + print "[Z%s] %s" % (num, data) + time.sleep(1) + s.send(msgpack.packb({"cmd": "[Z] Ping"})) + + +def testConnection(): + global server + connection = server.connect("127.0.0.1", 1234) + connection.send({"res": "Sending: Hello!"}) + print connection + + +def greenletsNum(): + from greenlet import greenlet + import gc + while 1: + print len([ob for ob in gc.get_objects() if isinstance(ob, greenlet)]) + time.sleep(1) + +if __name__ == "__main__": + from gevent import monkey; monkey.patch_all(thread=False) + import sys, time + logging.getLogger().setLevel(logging.DEBUG) + + gevent.spawn(testZmqClient, 1) + gevent.spawn(greenletsNum) + #gevent.spawn(testClient, 1) + #gevent.spawn_later(1, testConnection) + print "Running server..." + server = None + testCreateServer() + diff --git a/src/Connection/__init__.py b/src/Connection/__init__.py new file mode 100644 index 00000000..8f47108e --- /dev/null +++ b/src/Connection/__init__.py @@ -0,0 +1,2 @@ +from ConnectionServer import ConnectionServer +from Connection import Connection \ No newline at end of file diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 2e69252d..7286eacb 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -1,4 +1,4 @@ -import json, time, re, os +import json, time, re, os, gevent from Debug import Debug from Crypt import CryptHash from Config import config @@ -42,7 +42,7 @@ class ContentManager: new_hash = info[hash_type] if old_content and old_content["files"].get(relative_path): # We have the file in the old content - old_hash = old_content["files"][relative_path][hash_type] + old_hash = old_content["files"][relative_path].get(hash_type) else: # The file is not in the old content old_hash = None if old_hash != new_hash: changed.append(content_dir+relative_path) @@ -293,6 +293,7 @@ class ContentManager: return None elif old_content["modified"] > new_content["modified"]: # We have newer self.log.debug("We have newer %s (Our: %s, Sent: %s)" % (inner_path, old_content["modified"], new_content["modified"])) + gevent.spawn(self.site.publish, inner_path=inner_path) # Try to fix the broken peers return False if new_content["modified"] > time.time()+60*60*24: # Content modified in the far future (allow 1 day window) self.log.error("%s modify is in the future!" % inner_path) diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index 02b134e1..682b9911 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -1,5 +1,4 @@ import os, msgpack, shutil, gevent -from Site import SiteManager from cStringIO import StringIO from Debug import Debug from Config import config @@ -8,21 +7,30 @@ FILE_BUFF = 1024*512 # Request from me class FileRequest: - def __init__(self, server = None): - if server: - self.server = server - self.log = server.log - self.sites = SiteManager.list() + def __init__(self, server, connection): + self.server = server + self.connection = connection + + self.req_id = None + self.sites = self.server.sites + self.log = server.log def send(self, msg): + self.connection.send(msg) + + + def response(self, msg): if not isinstance(msg, dict): # If msg not a dict create a {"body": msg} msg = {"body": msg} - self.server.socket.send(msgpack.packb(msg, use_bin_type=True)) + msg["cmd"] = "response" + msg["to"] = self.req_id + self.send(msg) # Route file requests - def route(self, cmd, params): + def route(self, cmd, req_id, params): + self.req_id = req_id if cmd == "getFile": self.actionGetFile(params) elif cmd == "update": @@ -37,7 +45,7 @@ class FileRequest: def actionUpdate(self, params): site = self.sites.get(params["site"]) if not site or not site.settings["serving"]: # Site unknown or not serving - self.send({"error": "Unknown site"}) + self.response({"error": "Unknown site"}) return False if site.settings["own"] and params["inner_path"].endswith("content.json"): self.log.debug("Someone trying to push a file to own site %s, reload local %s first" % (site.address, params["inner_path"])) @@ -61,7 +69,7 @@ class FileRequest: lambda: site.downloadContent(params["inner_path"], peer=peer) ) # Load new content file and download changed files in new thread - self.send({"ok": "Thanks, file %s updated!" % params["inner_path"]}) + self.response({"ok": "Thanks, file %s updated!" % params["inner_path"]}) elif valid == None: # Not changed peer = site.addPeer(*params["peer"], return_peer = True) # Add or get peer @@ -70,18 +78,18 @@ class FileRequest: for task in site.worker_manager.tasks: # New peer add to every ongoing task if task["peers"]: site.needFile(task["inner_path"], peer=peer, update=True, blocking=False) # Download file from this peer too if its peer locked - self.send({"ok": "File not changed"}) + self.response({"ok": "File not changed"}) else: # Invalid sign or sha1 hash self.log.debug("Update for %s is invalid" % params["inner_path"]) - self.send({"error": "File invalid"}) + self.response({"error": "File invalid"}) # Send file content request def actionGetFile(self, params): site = self.sites.get(params["site"]) if not site or not site.settings["serving"]: # Site unknown or not serving - self.send({"error": "Unknown site"}) + self.response({"error": "Unknown site"}) return False try: file_path = site.getPath(params["inner_path"]) @@ -93,18 +101,18 @@ class FileRequest: back["location"] = file.tell() back["size"] = os.fstat(file.fileno()).st_size if config.debug_socket: self.log.debug("Sending file %s from position %s to %s" % (file_path, params["location"], back["location"])) - self.send(back) + self.response(back) if config.debug_socket: self.log.debug("File %s sent" % file_path) except Exception, err: - self.send({"error": "File read error: %s" % Debug.formatException(err)}) + self.response({"error": "File read error: %s" % Debug.formatException(err)}) return False # Send a simple Pong! answer def actionPing(self): - self.send("Pong!") + self.response("Pong!") # Unknown command def actionUnknown(self, cmd, params): - self.send({"error": "Unknown command: %s" % cmd}) + self.response({"error": "Unknown command: %s" % cmd}) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index 5ae5b732..d5f2d763 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -5,30 +5,28 @@ from Config import config from FileRequest import FileRequest from Site import SiteManager from Debug import Debug +from Connection import ConnectionServer -class FileServer: +class FileServer(ConnectionServer): def __init__(self): - self.ip = config.fileserver_ip - self.port = config.fileserver_port - self.log = logging.getLogger(__name__) + ConnectionServer.__init__(self, config.fileserver_ip, config.fileserver_port, self.handleRequest) if config.ip_external: # Ip external definied in arguments self.port_opened = True SiteManager.peer_blacklist.append((config.ip_external, self.port)) # Add myself to peer blacklist else: self.port_opened = None # Is file server opened on router self.sites = SiteManager.list() - self.running = True # Handle request to fileserver - def handleRequest(self, msg): - if "params" in msg: - self.log.debug("FileRequest: %s %s %s" % (msg["cmd"], msg["params"].get("site"), msg["params"].get("inner_path"))) + def handleRequest(self, connection, message): + if "params" in message: + self.log.debug("FileRequest: %s %s %s" % (message["cmd"], message["params"].get("site"), message["params"].get("inner_path"))) else: - self.log.debug("FileRequest: %s" % msg["cmd"]) - req = FileRequest(self) - req.route(msg["cmd"], msg.get("params")) + self.log.debug("FileRequest: %s" % req["cmd"]) + req = FileRequest(self, connection) + req.route(message["cmd"], message.get("req_id"), message.get("params")) # Reload the FileRequest class to prevent restarts in debug mode @@ -124,13 +122,15 @@ class FileServer: time.sleep(2) # Prevent too quick request - # Announce sites every 10 min + # Announce sites every 20 min def announceSites(self): while 1: time.sleep(20*60) # Announce sites every 20 min for address, site in self.sites.items(): if site.settings["serving"]: site.announce() # Announce site to tracker + for inner_path in site.bad_files: # Reset bad file retry counter + site.bad_files[inner_path] = 0 time.sleep(2) # Prevent too quick request @@ -155,40 +155,14 @@ class FileServer: from Debug import DebugReloader DebugReloader(self.reload) - self.context = zmq.Context() - socket = self.context.socket(zmq.REP) - self.socket = socket - self.socket.setsockopt(zmq.RCVTIMEO, 5000) # Wait for data receive - self.socket.setsockopt(zmq.SNDTIMEO, 50000) # Wait for data send - self.log.info("Binding to tcp://%s:%s" % (self.ip, self.port)) - try: - self.socket.bind('tcp://%s:%s' % (self.ip, self.port)) - except Exception, err: - self.log.error("Can't bind, FileServer must be running already") - return if check_sites: # Open port, Update sites, Check files integrity gevent.spawn(self.checkSites) thread_announce_sites = gevent.spawn(self.announceSites) thread_wakeup_watcher = gevent.spawn(self.wakeupWatcher) - while self.running: - try: - ret = {} - req = msgpack.unpackb(socket.recv()) - self.handleRequest(req) - except Exception, err: - self.log.error(err) - if self.running: self.socket.send(msgpack.packb({"error": "%s" % Debug.formatException(err)}, use_bin_type=True)) - if config.debug: # Raise exception - import sys - sys.modules["src.main"].DebugHook.handleError() - thread_wakeup_watcher.kill(exception=Debug.Notify("Stopping FileServer")) - thread_announce_sites.kill(exception=Debug.Notify("Stopping FileServer")) + ConnectionServer.start(self) + + # thread_wakeup_watcher.kill(exception=Debug.Notify("Stopping FileServer")) + # thread_announce_sites.kill(exception=Debug.Notify("Stopping FileServer")) self.log.debug("Stopped.") - - - def stop(self): - self.running = False - self.socket.close() - diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index b13f7b5c..0c4cc8a5 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -1,21 +1,20 @@ -import os, logging, gevent, time, msgpack +import os, logging, gevent, time, msgpack, sys import zmq.green as zmq from cStringIO import StringIO from Config import config from Debug import Debug -context = zmq.Context() - # Communicate remote peers class Peer: - def __init__(self, ip, port, site): + def __init__(self, ip, port, site=None): self.ip = ip self.port = port self.site = site self.key = "%s:%s" % (ip, port) self.log = None + self.connection_server = sys.modules["src.main"].file_server - self.socket = None + self.connection = None self.last_found = None # Time of last found in the torrent tracker self.last_response = None # Time of last successfull response from peer self.last_ping = None # Last response time for ping @@ -29,19 +28,22 @@ class Peer: # Connect to host def connect(self): + if self.connection: self.connection.close() + self.connection = None if not self.log: self.log = logging.getLogger("Peer:%s:%s" % (self.ip, self.port)) - if self.socket: self.socket.close() - self.socket = context.socket(zmq.REQ) - self.socket.setsockopt(zmq.RCVTIMEO, 50000) # Wait for data arrive - self.socket.setsockopt(zmq.SNDTIMEO, 5000) # Wait for data send - self.socket.setsockopt(zmq.LINGER, 500) # Wait for socket close - # self.socket.setsockopt(zmq.TCP_KEEPALIVE, 1) # Enable keepalive - # self.socket.setsockopt(zmq.TCP_KEEPALIVE_IDLE, 4*60) # Send after 4 minute idle - # self.socket.setsockopt(zmq.TCP_KEEPALIVE_INTVL, 15) # Wait 15 sec to response - # self.socket.setsockopt(zmq.TCP_KEEPALIVE_CNT, 4) # 4 Probes - self.socket.connect('tcp://%s:%s' % (self.ip, self.port)) + self.log.debug("Connecting...") + try: + self.connection = self.connection_server.connect(self.ip, self.port) + except Exception, err: + self.log.debug("Connecting error: %s" % Debug.formatException(err)) + self.onConnectionError() + + def __str__(self): + return "Peer %-12s" % self.ip + def __repr__(self): + return "<%s>" % self.__str__() # Found a peer on tracker def found(self): @@ -49,18 +51,20 @@ class Peer: # Send a command to peer - def sendCmd(self, cmd, params = {}): - if not self.socket: self.connect() + def request(self, cmd, params = {}): + if not self.connection or self.connection.closed: + self.connect() + if not self.connection: return None # Connection failed + if cmd != "ping" and self.last_response and time.time() - self.last_response > 20*60: # If last response if older than 20 minute, ping first to see if still alive if not self.ping(): return None for retry in range(1,3): # Retry 3 times - if config.debug_socket: self.log.debug("sendCmd: %s %s" % (cmd, params.get("inner_path"))) + #if config.debug_socket: self.log.debug("sendCmd: %s %s" % (cmd, params.get("inner_path"))) try: - self.socket.send(msgpack.packb({"cmd": cmd, "params": params}, use_bin_type=True)) - if config.debug_socket: self.log.debug("Sent command: %s" % cmd) - response = msgpack.unpackb(self.socket.recv()) - if config.debug_socket: self.log.debug("Got response to: %s" % cmd) + response = self.connection.request(cmd, params) + if not response: raise Exception("Send error") + #if config.debug_socket: self.log.debug("Got response to: %s" % cmd) if "error" in response: self.log.debug("%s error: %s" % (cmd, response["error"])) self.onConnectionError() @@ -69,13 +73,14 @@ class Peer: self.last_response = time.time() return response except Exception, err: - self.onConnectionError() - self.log.debug("%s (connection_error: %s, hash_failed: %s, retry: %s)" % (Debug.formatException(err), self.connection_error, self.hash_failed, retry)) - time.sleep(1*retry) - self.connect() - if type(err).__name__ == "Notify" and err.message == "Worker stopped": # Greenlet kill by worker - self.log.debug("Peer worker got killed, aborting cmd: %s" % cmd) + if type(err).__name__ == "Notify": # Greenlet kill by worker + self.log.debug("Peer worker got killed: %s, aborting cmd: %s" % (err.message, cmd)) break + else: + self.onConnectionError() + self.log.debug("%s (connection_error: %s, hash_failed: %s, retry: %s)" % (Debug.formatException(err), self.connection_error, self.hash_failed, retry)) + time.sleep(1*retry) + self.connect() return None # Failed after 4 retry @@ -85,7 +90,7 @@ class Peer: buff = StringIO() s = time.time() while 1: # Read in 512k parts - back = self.sendCmd("getFile", {"site": site, "inner_path": inner_path, "location": location}) # Get file content from last location + back = self.request("getFile", {"site": site, "inner_path": inner_path, "location": location}) # Get file content from last location if not back or "body" not in back: # Error return False @@ -106,7 +111,8 @@ class Peer: for retry in range(1,3): # Retry 3 times s = time.time() with gevent.Timeout(10.0, False): # 10 sec timeout, dont raise exception - response = self.sendCmd("ping") + response = self.request("ping") + if response and "body" in response and response["body"] == "Pong!": response_time = time.time()-s break # All fine, exit from for loop @@ -126,7 +132,8 @@ class Peer: def remove(self): self.log.debug("Removing peer...Connection error: %s, Hash failed: %s" % (self.connection_error, self.hash_failed)) if self.key in self.site.peers: del(self.site.peers[self.key]) - self.socket.close() + if self.connection: + self.connection.close() # - EVENTS - diff --git a/src/Site/Site.py b/src/Site/Site.py index 590ed856..df71ff6e 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -27,7 +27,7 @@ class Site: self.peer_blacklist = SiteManager.peer_blacklist # Ignore this peers (eg. myself) self.last_announce = 0 # Last announce time to tracker self.worker_manager = WorkerManager(self) # Handle site download from other peers - self.bad_files = {} # SHA512 check failed files, need to redownload + self.bad_files = {} # SHA512 check failed files, need to redownload {"inner.content": 1} (key: file, value: failed accept) self.content_updated = None # Content.js update time self.last_downloads = [] # Files downloaded in run of self.download() self.notifications = [] # Pending notifications displayed once on page load [error|ok|info, message, timeout] @@ -115,28 +115,39 @@ class Site: changed = self.content_manager.loadContent(inner_path, load_includes=False) # Start download files - evts = [] + file_threads = [] if download_files: for file_relative_path in self.content_manager.contents[inner_path].get("files", {}).keys(): file_inner_path = content_inner_dir+file_relative_path res = self.needFile(file_inner_path, blocking=False, update=self.bad_files.get(file_inner_path), peer=peer) # No waiting for finish, return the event if res != True: # Need downloading self.last_downloads.append(file_inner_path) - evts.append(res) # Append evt + file_threads.append(res) # Append evt # Wait for includes download + include_threads = [] for file_relative_path in self.content_manager.contents[inner_path].get("includes", {}).keys(): file_inner_path = content_inner_dir+file_relative_path - self.downloadContent(file_inner_path, download_files=download_files, peer=peer) + include_thread = gevent.spawn(self.downloadContent, file_inner_path, download_files=download_files, peer=peer) + include_threads.append(include_thread) + self.log.debug("%s: Downloading %s includes..." % (inner_path, len(include_threads))) + gevent.joinall(include_threads) self.log.debug("%s: Includes downloaded" % inner_path) - self.log.debug("%s: Downloading %s files..." % (inner_path, len(evts))) - gevent.joinall(evts) + + self.log.debug("%s: Downloading %s files..." % (inner_path, len(file_threads))) + gevent.joinall(file_threads) self.log.debug("%s: All file downloaded in %.2fs" % (inner_path, time.time()-s)) return True + # Return bad files with less than 3 retry + def getReachableBadFiles(self): + if not self.bad_files: return False + return [bad_file for bad_file, retry in self.bad_files.iteritems() if retry < 3] + + # Download all files of the site @util.Noparallel(blocking=False) def download(self, check_size=False): @@ -163,7 +174,7 @@ class Site: changed = self.content_manager.loadContent("content.json") if changed: for changed_file in changed: - self.bad_files[changed_file] = True + self.bad_files[changed_file] = self.bad_files.get(changed_file, 0)+1 if not self.settings["own"]: self.checkFiles(quick_check=True) # Quick check files based on file size if self.bad_files: self.download() @@ -178,16 +189,19 @@ class Site: if not peers or len(published) >= limit: break # All peers done, or published engouht peer = peers.pop(0) result = {"exception": "Timeout"} - try: - with gevent.Timeout(timeout, False): - result = peer.sendCmd("update", { - "site": self.address, - "inner_path": inner_path, - "body": open(self.getPath(inner_path), "rb").read(), - "peer": (config.ip_external, config.fileserver_port) - }) - except Exception, err: - result = {"exception": Debug.formatException(err)} + + for retry in range(2): + try: + with gevent.Timeout(timeout, False): + result = peer.request("update", { + "site": self.address, + "inner_path": inner_path, + "body": open(self.getPath(inner_path), "rb").read(), + "peer": (config.ip_external, config.fileserver_port) + }) + if result: break + except Exception, err: + result = {"exception": Debug.formatException(err)} if result and "ok" in result: published.append(peer) @@ -202,6 +216,8 @@ class Site: published = [] # Successfuly published (Peer) publishers = [] # Publisher threads peers = self.peers.values() + + random.shuffle(peers) for i in range(limit): publisher = gevent.spawn(self.publisher, inner_path, peers, published, limit) publishers.append(publisher) @@ -303,7 +319,7 @@ class Site: bad_files = self.verifyFiles(quick_check) if bad_files: for bad_file in bad_files: - self.bad_files[bad_file] = True + self.bad_files[bad_file] = self.bad_files.get("bad_file", 0)+1 def deleteFiles(self): @@ -387,6 +403,8 @@ class Site: if inner_path == "content.json": self.content_updated = False self.log.error("Can't update content.json") + if inner_path in self.bad_files: + self.bad_files[inner_path] = self.bad_files.get(inner_path, 0)+1 self.updateWebsocket(file_failed=inner_path) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index ce648ebf..91f4cfdd 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -46,6 +46,8 @@ class UiRequest: return self.actionDebug() elif path == "/Console" and config.debug: return self.actionConsole() + elif path == "/Stats": + return self.actionStats() # Test elif path == "/Test/Websocket": return self.actionFile("Data/temp/ws_test.html") @@ -114,7 +116,7 @@ class UiRequest: if not inner_path: inner_path = "index.html" # If inner path defaults to index.html site = self.server.sites.get(match.group("site")) - if site and site.content_manager.contents.get("content.json") and (not site.bad_files or site.settings["own"]): # Its downloaded or own + if site and site.content_manager.contents.get("content.json") and (not site.getReachableBadFiles() or site.settings["own"]): # Its downloaded or own title = site.content_manager.contents["content.json"]["title"] else: title = "Loading %s..." % match.group("site") @@ -268,10 +270,30 @@ class UiRequest: # Just raise an error to get console def actionConsole(self): + import sys sites = self.server.sites + main = sys.modules["src.main"] raise Exception("Here is your console") + def actionStats(self): + import gc, sys + from greenlet import greenlet + greenlets = [obj for obj in gc.get_objects() if isinstance(obj, greenlet)] + self.sendHeader() + main = sys.modules["src.main"] + + yield "
"
+		yield "Connections (%s):
" % len(main.file_server.connections) + for connection in main.file_server.connections: + yield "%s: %s %s
" % (connection.protocol, connection.ip, connection.zmq_sock) + + yield "Greenlets (%s):
" % len(greenlets) + for thread in greenlets: + yield " - %s
" % cgi.escape(repr(thread)) + yield "
" + + # - Tests - def actionTestStream(self): diff --git a/src/Ui/UiServer.py b/src/Ui/UiServer.py index 0592a019..5fa00690 100644 --- a/src/Ui/UiServer.py +++ b/src/Ui/UiServer.py @@ -94,11 +94,12 @@ class UiServer: browser = webbrowser.get(config.open_browser) browser.open("http://%s:%s" % (config.ui_ip, config.ui_port), new=2) - self.server = WSGIServer((self.ip, self.port), handler, handler_class=UiWSGIHandler, log=self.log) + self.server = WSGIServer((self.ip.replace("*", ""), self.port), handler, handler_class=UiWSGIHandler, log=self.log) self.server.sockets = {} self.server.serve_forever() self.log.debug("Stopped.") + def stop(self): # Close WS sockets for client in self.server.clients.values(): diff --git a/src/Worker/Worker.py b/src/Worker/Worker.py index df651e55..325ebb48 100644 --- a/src/Worker/Worker.py +++ b/src/Worker/Worker.py @@ -53,7 +53,8 @@ class Worker: self.manager.doneTask(task) self.task = None else: # Hash failed - self.manager.log.debug("%s: Hash failed: %s" % (self.key, task["inner_path"])) + self.manager.log.debug("%s: Hash failed: %s, failed peers: %s" % (self.key, task["inner_path"], len(task["failed"]))) + task["failed"].append(self.key) self.task = None self.peer.hash_failed += 1 if self.peer.hash_failed >= 3: # Broken peer diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index e5bf3915..02e1c1d4 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -20,8 +20,8 @@ class WorkerManager: time.sleep(15) # Check every 15 sec # Clean up workers - if not self.tasks and self.workers: # No task but workers still running - for worker in self.workers.values(): worker.stop() + for worker in self.workers.values(): + if worker.task and worker.task["done"]: worker.stop() # Stop workers with task done if not self.tasks: continue tasks = self.tasks[:] # Copy it so removing elements wont cause any problem @@ -38,7 +38,7 @@ class WorkerManager: elif (task["time_started"] and time.time() >= task["time_started"]+15) or not self.workers: # Task started more than 15 sec ago or no workers self.log.debug("Task taking more than 15 secs, find more peers: %s" % task["inner_path"]) task["site"].announce() # Find more peers - if task["peers"]: # Release the peer olck + if task["peers"]: # Release the peer lock self.log.debug("Task peer lock release: %s" % task["inner_path"]) task["peers"] = [] self.startWorkers() @@ -62,6 +62,7 @@ class WorkerManager: self.tasks.sort(key=self.taskSorter, reverse=True) # Sort tasks by priority and worker numbers for task in self.tasks: # Find a task if task["peers"] and peer not in task["peers"]: continue # This peer not allowed to pick this task + if peer.key in task["failed"]: continue # Peer already tried to solve this, but failed return task @@ -85,9 +86,9 @@ class WorkerManager: # Start workers to process tasks def startWorkers(self, peers=None): - if len(self.workers) >= MAX_WORKERS and not peers: return False # Workers number already maxed + if len(self.workers) >= MAX_WORKERS and not peers: return False # Workers number already maxed and no starting peers definied if not self.tasks: return False # No task for workers - peers = self.site.peers.values() + if not peers: peers = self.site.peers.values() # No peers definied, use any from site random.shuffle(peers) for peer in peers: # One worker for every peer if peers and peer not in peers: continue # If peers definied and peer not valid @@ -139,7 +140,7 @@ class WorkerManager: peers = [peer] # Only download from this peer else: peers = None - task = {"evt": evt, "workers_num": 0, "site": self.site, "inner_path": inner_path, "done": False, "time_added": time.time(), "time_started": None, "peers": peers, "priority": priority} + task = {"evt": evt, "workers_num": 0, "site": self.site, "inner_path": inner_path, "done": False, "time_added": time.time(), "time_started": None, "peers": peers, "priority": priority, "failed": []} self.tasks.append(task) self.log.debug("New task: %s, peer lock: %s, priority: %s" % (task["inner_path"], peers, priority)) self.startWorkers(peers) @@ -168,5 +169,5 @@ class WorkerManager: self.tasks.remove(task) # Remove from queue self.site.onFileDone(task["inner_path"]) task["evt"].set(True) - if not self.tasks: self.site.onComplete() # No more task trigger site compelte + if not self.tasks: self.site.onComplete() # No more task trigger site complete diff --git a/src/main.py b/src/main.py index f1385eb2..d5bc2f8e 100644 --- a/src/main.py +++ b/src/main.py @@ -150,8 +150,10 @@ def siteNeedFile(address, inner_path): def sitePublish(address, peer_ip=None, peer_port=15441, inner_path="content.json"): + global file_server from Site import Site from File import FileServer # We need fileserver to handle incoming file requests + logging.info("Creating FileServer....") file_server = FileServer() file_server_thread = gevent.spawn(file_server.start, check_sites=False) # Dont check every site integrity @@ -184,10 +186,15 @@ def cryptoPrivatekeyToAddress(privatekey=None): # Peer -def peerPing(ip, port): +def peerPing(peer_ip, peer_port): + logging.info("Opening a simple connection server") + global file_server + from Connection import ConnectionServer + file_server = ConnectionServer("127.0.0.1", 1234) + from Peer import Peer - logging.info("Pinging 5 times peer: %s:%s..." % (ip, port)) - peer = Peer(ip, port) + logging.info("Pinging 5 times peer: %s:%s..." % (peer_ip, peer_port)) + peer = Peer(peer_ip, peer_port) for i in range(5): s = time.time() print peer.ping(), @@ -195,12 +202,15 @@ def peerPing(ip, port): time.sleep(1) -def peerGetFile(ip, port, site, filename=None): +def peerGetFile(peer_ip, peer_port, site, filename): + logging.info("Opening a simple connection server") + global file_server + from Connection import ConnectionServer + file_server = ConnectionServer() + from Peer import Peer - if not site: site = config.homepage - if not filename: filename = "content.json" - logging.info("Getting %s/%s from peer: %s:%s..." % (site, filename, ip, port)) - peer = Peer(ip, port) + logging.info("Getting %s/%s from peer: %s:%s..." % (site, filename, peer_ip, peer_port)) + peer = Peer(peer_ip, peer_port) s = time.time() print peer.getFile(site, filename).read() print "Response time: %.3fs" % (time.time()-s) diff --git a/src/util/Event.py b/src/util/Event.py index 664daae9..4b4e7c96 100644 --- a/src/util/Event.py +++ b/src/util/Event.py @@ -3,7 +3,7 @@ class Event(list): def __call__(self, *args, **kwargs): for f in self[:]: - if "once" in dir(f): + if "once" in dir(f) and f in self: self.remove(f) f(*args, **kwargs) diff --git a/tools/upnpc/upnpc-static.exe b/tools/upnpc/upnpc-static.exe deleted file mode 100644 index 7b3159901bf52a4d4c60337d3fdd6201c0e88e19..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29696 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�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 CB-H)@ diff --git a/zeronet.py b/zeronet.py index e8156209..4dcceb73 100644 --- a/zeronet.py +++ b/zeronet.py @@ -35,3 +35,4 @@ def main(): if __name__ == '__main__': main() +