From 7bd05e9b164b9fad1e7c01aca8b2c099638d44f6 Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Sat, 30 Jul 2016 17:09:15 -0400 Subject: [PATCH 01/81] Handle creation of settings directory in windows --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index ea9124d57..c1b45205d 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -171,6 +171,11 @@ class LBRYDaemon(jsonrpc.JSONRPC): from lbrynet.winhelpers.knownpaths import get_path, FOLDERID, UserHandle default_download_directory = get_path(FOLDERID.Downloads, UserHandle.current) self.db_dir = os.path.join(get_path(FOLDERID.RoamingAppData, UserHandle.current), "lbrynet") + try: + os.makedirs(self.db_dir) + except OSError: + if not os.path.isdir(self.db_dir): + raise elif sys.platform == "darwin": default_download_directory = os.path.join(os.path.expanduser("~"), 'Downloads') self.db_dir = user_data_dir("LBRY") From 3f95c1a9d59768ed68a638e7e195733cb5217b74 Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Sat, 30 Jul 2016 17:09:38 -0400 Subject: [PATCH 02/81] Skip db migration in windows --- lbrynet/db_migrator/dbmigrator.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lbrynet/db_migrator/dbmigrator.py b/lbrynet/db_migrator/dbmigrator.py index d7644f6cd..78e4152ee 100644 --- a/lbrynet/db_migrator/dbmigrator.py +++ b/lbrynet/db_migrator/dbmigrator.py @@ -1,9 +1,12 @@ import logging +import os def migrate_db(db_dir, start, end): current = start old_dirs = [] + if os.name == "nt": + return old_dirs while current < end: if current == 0: from lbrynet.db_migrator.migrate0to1 import do_migration From bbfbcacc6a270b31564c4919306df68a3964b203 Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Sat, 30 Jul 2016 22:10:30 -0400 Subject: [PATCH 03/81] Add windows icons for packaging --- packaging/windows/icons/lbry128.ico | Bin 0 -> 99678 bytes packaging/windows/icons/lbry256.ico | Bin 0 -> 370070 bytes packaging/windows/icons/lbry32.ico | Bin 0 -> 5430 bytes packaging/windows/icons/lbry48.ico | Bin 0 -> 15086 bytes packaging/windows/icons/lbry96.ico | Bin 0 -> 32038 bytes 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 packaging/windows/icons/lbry128.ico create mode 100644 packaging/windows/icons/lbry256.ico create mode 100644 packaging/windows/icons/lbry32.ico create mode 100644 packaging/windows/icons/lbry48.ico create mode 100644 packaging/windows/icons/lbry96.ico diff --git a/packaging/windows/icons/lbry128.ico b/packaging/windows/icons/lbry128.ico new file mode 100644 index 0000000000000000000000000000000000000000..3cb6f992d76dd670ce5368579bc5e9e2c9b6cfaa GIT binary patch literal 99678 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
T-wOEr(M638+YE^*X;bB*|`7P^uMpGwe2Py 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 HG4THb(T5N= literal 0 HcmV?d00001 diff --git a/packaging/windows/icons/lbry32.ico b/packaging/windows/icons/lbry32.ico new file mode 100644 index 0000000000000000000000000000000000000000..6a6219b501c6755fa1e530cbeec4becef701ca89 GIT binary patch literal 5430 zcmeHLdr-}J6u)-YnEhjC7_+mpkJK*fHS9xrB9XLFE3{*YC2!MYBT3Rr6GcQ+^z`VV zO@v*NM=U*TVk@&Wsjf|t9;j4QK6}nLKizKY>dNa6XXf1R<2--g@A-bu`Cgf>@E!pA^WU~I;(`p^>qxsD=H;{(R!qeTvuS{l4^U46A+gpg;$tSz>=T7F=)zwB& zzjr5n{E+w@PoMguV&#hY*t07bPRr+l$24!NLWht$k#e|vrQopW*)fyV&5n8X-Y` zc=)gp7c&3EisiO&TW!Z=@6a9oD8F5Xogo{sHDE0&EAQ}p2guF6DApo9?KHCfIuCoh z8Av;Ssw#T#4zeF9baxO^n&7pFjR}R9yMwSv84EdAmsS_l-4?a(?&OJ5A;D}V*^cfg!=jih#`Mhtz3vRsmZ0p!Acb4*X;78%tGFC zik+tlYHIEyA}j!|E=!P}mU@E-iwL_b$x3>9(06*r#e}tZy8nvc?Y{7ETZ&8B83n{Y z`x!EltYSMrF28;(CnxIy$xK&6PEp4z@5^LBSCH=+Eka{L>7MZvcN0x0z}x!$@BC_a zeqMRJskz?0h@bEOY(gJ3WxMh%oNuBCaW*v>PLvZ$OaJ9z6`?s{S9GhA)g;}Cgq*W5 z&Dt1-dRmw>Ya+FpbT_VJ62lR~j|f%iQirBVx(*S7av-dy7-59I7UdmR+`CuJ)8x-l z{9yVWEX=hP?^b6bsbfc(SCiy9hv0Z}5~f;>z(|9y;J;xFYHIHBbUWbu~7iYiMqNPBDDeAJb8kbyA9;>0?eLi zj!}kv5fvFI5%@cL5UR2}i0I;bj~tH2Bnv$l8TP~0Eo)K#;DMw^v`x)T2oDXw*wF(; z49uJ{9z})u?RX9zh$i|VOr1OuIe%v{t%YzNA^X7jP2)+Q$KrdF4#vWAf)0#E^h40L zb!cp;CsG@5jls2s@u>coU^Wc<6LyQ5!f}}`^mR1gx86n6Dv|`}YR-@P!!<;H_wPZ`^?U?w;SvjZgu=e=u+MPi3cJ<@b+t9v?C&A=lJ|r6<#`Z7{`0w9 zywDouj_ix+V$A-(EZ~}Zy^phqPiLz8YiUoFKrHSo8#9Wpc4t2Z^u(6S*UmjG^yNPRbD@%PzsbpVj`Hn(vPL{a4_?&d++)2uw)s|6? z_HATD`C40gsz0@=v9}O_YwQz!o`0Fe69#M!P*n5tLsJi`;x5_gdAKpuhH&{ U`uk4q^~FY~*IMDPfH%_p4<`lk$N&HU literal 0 HcmV?d00001 diff --git a/packaging/windows/icons/lbry48.ico b/packaging/windows/icons/lbry48.ico new file mode 100644 index 0000000000000000000000000000000000000000..95c0947fae6093fed4743f97f14f0ffed281c65b GIT binary patch literal 15086 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 literal 0 HcmV?d00001 diff --git a/packaging/windows/icons/lbry96.ico b/packaging/windows/icons/lbry96.ico new file mode 100644 index 0000000000000000000000000000000000000000..25572bc9cb7cb9e8d5517467aa29357b82fc60a1 GIT binary patch literal 32038 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 X9*FQjga Date: Sat, 30 Jul 2016 22:52:49 -0400 Subject: [PATCH 04/81] Set cacert environ path for windows distribution --- lbrynet/lbrynet_daemon/LBRYDaemonControl.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemonControl.py b/lbrynet/lbrynet_daemon/LBRYDaemonControl.py index 1bfa62eab..a81dcb78d 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemonControl.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemonControl.py @@ -32,6 +32,9 @@ log = logging.getLogger(__name__) REMOTE_SERVER = "www.google.com" +if getattr(sys, 'frozen', False): + os.environ["REQUESTS_CA_BUNDLE"] = os.path.join(os.getcwd(), "cacert.pem") + def test_internet_connection(): try: From dd631328e0e77b6ee893dd7ef6ca3ab5917eb4b1 Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Sat, 30 Jul 2016 22:54:51 -0400 Subject: [PATCH 05/81] Update windows distribution setup --- setup_win32.py | 80 +++++++++++++++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 30 deletions(-) diff --git a/setup_win32.py b/setup_win32.py index 1fc0f0d58..0e9b4982e 100644 --- a/setup_win32.py +++ b/setup_win32.py @@ -9,6 +9,10 @@ import sys from cx_Freeze import setup, Executable import requests.certs +from lbrynet import __version__ + +base_dir = os.path.abspath(os.path.dirname(__file__)) + def find_data_file(filename): if getattr(sys, 'frozen', False): # The application is frozen @@ -19,16 +23,28 @@ def find_data_file(filename): data_dir = os.path.dirname(__file__) return os.path.join(data_dir, filename) +console_scripts = ['lbrynet-stdin-uploader = lbrynet.lbrynet_console.LBRYStdinUploader:launch_stdin_uploader', + 'lbrynet-stdout-downloader = lbrynet.lbrynet_console.LBRYStdoutDownloader:launch_stdout_downloader', + 'lbrynet-create-network = lbrynet.create_network:main', + 'lbrynet-launch-node = lbrynet.dht.node:main', + 'lbrynet-launch-rpc-node = lbrynet.rpc_node:main', + 'lbrynet-rpc-node-cli = lbrynet.node_rpc_cli:main', + 'lbrynet-lookup-hosts-for-hash = lbrynet.dht_scripts:get_hosts_for_hash_in_dht', + 'lbrynet-announce_hash_to_dht = lbrynet.dht_scripts:announce_hash_to_dht', + 'lbrynet-daemon = lbrynet.lbrynet_daemon.LBRYDaemonControl:start', + 'stop-lbrynet-daemon = lbrynet.lbrynet_daemon.LBRYDaemonControl:stop', + 'lbrynet-cli = lbrynet.lbrynet_daemon.LBRYDaemonCLI:main'] + shortcut_table = [ ('DesktopShortcut', # Shortcut 'DesktopFolder', # Directory - 'LBRY', # Name + 'lbrynet', # Name 'TARGETDIR', # Component - '[TARGETDIR]\LBRY.exe', # Target + '[TARGETDIR]\lbrynet.exe', # Target None, # Arguments None, # Description None, # Hotkey - os.path.join('lbrynet', 'lbrynet_gui', 'lbry-dark-icon.ico'), # Icon + os.path.join('lbry-dark-icon.ico'), # Icon None, # IconIndex None, # ShowCmd 'TARGETDIR', # WkDir @@ -39,52 +55,56 @@ shortcut_table = [ msi_data = {'Shortcut': shortcut_table} bdist_msi_options = { - 'upgrade_code': '{66620F3A-DC3A-11E2-B341-002219E9B01F}', + # 'upgrade_code': '{66620F3A-DC3A-11E2-B341-002219E9B01F}', 'add_to_path': False, - 'initial_target_dir': r'[LocalAppDataFolder]\LBRY', + 'initial_target_dir': r'[LocalAppDataFolder]\lbrynet', 'data': msi_data, } build_exe_options = { 'include_msvcr': True, 'includes': [], - 'packages': ['six', 'os', 'twisted', 'miniupnpc', 'unqlite', 'seccure', - 'requests', 'bitcoinrpc', 'txjsonrpc', 'win32api', 'Crypto', - 'gmpy', 'yapsy', 'lbryum', 'google.protobuf'], - 'excludes': ['zope.interface._zope_interface_coptimizations'], - 'include_files': [os.path.join('lbrynet', 'lbrynet_gui', 'close.gif'), - os.path.join('lbrynet', 'lbrynet_gui', 'close1.png'), - os.path.join('lbrynet', 'lbrynet_gui', 'close2.gif'), - os.path.join('lbrynet', 'lbrynet_gui', 'drop_down.gif'), - os.path.join('lbrynet', 'lbrynet_gui', 'hide_options.gif'), - os.path.join('lbrynet', 'lbrynet_gui', 'lbry-dark-242x80.gif'), - os.path.join('lbrynet', 'lbrynet_gui', 'lbry-dark-icon.ico'), - os.path.join('lbrynet', 'lbrynet_gui', 'lbry-dark-icon.xbm'), - os.path.join('lbrynet', 'lbrynet_gui', 'show_options.gif'), - os.path.join('lbrycrdd.exe'), # Not included in repo - os.path.join('lbrycrd-cli.exe'), # Not included in repo - (requests.certs.where(), 'cacert.pem'), - ], + 'packages': ['Crypto', 'twisted', 'miniupnpc', 'yapsy', 'seccure', + 'bitcoinrpc', 'txjsonrpc', 'requests', 'unqlite', 'lbryum', + 'jsonrpc', 'simplejson', 'appdirs', 'six', 'base58', 'googlefinance', + 'ecdsa', 'pbkdf2', 'qrcode', 'jsonrpclib', + 'os', 'cython', 'win32api', 'pkg_resources', 'zope.interface', + 'argparse', 'colorama', 'certifi' + # 'gmpy', 'wsgiref', 'slowaes', 'dnspython', 'protobuf', 'google', 'google.protobuf' + ], + 'excludes': ['collections.sys', 'collections._weakref', 'tkinter', 'tk', 'tcl' + 'zope.interface._zope_interface_coptimizations', 'matplotlib', 'numpy', 'pillow', 'pandas'], + 'include_files': [(requests.certs.where(), 'cacert.pem')], 'namespace_packages': ['zope']} exe = Executable( - script=os.path.join('lbrynet', 'lbrynet_gui', 'gui.py'), - base='Win32GUI', - icon=os.path.join('lbrynet', 'lbrynet_gui', 'lbry-dark-icon.ico'), + script=os.path.join('lbrynet', 'lbrynet_daemon', 'LBRYDaemonControl.py'), + # base='Win32GUI', + icon=os.path.join('packaging', 'windows', 'icons', 'lbry256.ico'), + # icon=os.path.join('lbry-dark-icon.ico'), compress=True, - shortcutName='LBRY', + shortcutName='lbrynet', shortcutDir='DesktopFolder', - targetName='LBRY.exe' + targetName='lbrynet.exe' # targetDir="LocalAppDataFolder" ) setup( - name='LBRY', - version='0.0.4', - description='A fully decentralized network for distributing data', + name='lbrynet', + version=__version__, + description='A decentralized media library and marketplace', url='lbry.io', author='', keywords='LBRY', + # entry_points={'console_scripts': console_scripts}, + data_files=[ + ('lbrynet/lbrynet_console/plugins', + [ + os.path.join(base_dir, 'lbrynet', 'lbrynet_console', 'plugins', + 'blindrepeater.yapsy-plugin') + ] + ), + ], options={'build_exe': build_exe_options, 'bdist_msi': bdist_msi_options}, executables=[exe], From 7d535ef7d19de5e1eeb02e0b97f24f455dc2b2b6 Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Wed, 3 Aug 2016 10:16:14 -0400 Subject: [PATCH 06/81] Make ca_path checks for frozen windows only --- lbrynet/lbrynet_daemon/LBRYDaemonControl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemonControl.py b/lbrynet/lbrynet_daemon/LBRYDaemonControl.py index a81dcb78d..d3f992592 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemonControl.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemonControl.py @@ -32,7 +32,7 @@ log = logging.getLogger(__name__) REMOTE_SERVER = "www.google.com" -if getattr(sys, 'frozen', False): +if getattr(sys, 'frozen', False) and os.name == "nt": os.environ["REQUESTS_CA_BUNDLE"] = os.path.join(os.getcwd(), "cacert.pem") From 191f661b357f4993115149cbe51e9da8232036a4 Mon Sep 17 00:00:00 2001 From: Jimmy Kiselak Date: Wed, 3 Aug 2016 23:03:14 -0400 Subject: [PATCH 07/81] skeleton for reflector protocol --- lbrynet/reflector/__init__.py | 0 lbrynet/reflector/client/__init__.py | 0 lbrynet/reflector/client/client.py | 140 +++++++++++++++++++++++++++ lbrynet/reflector/server/__init__.py | 0 lbrynet/reflector/server/server.py | 86 ++++++++++++++++ 5 files changed, 226 insertions(+) create mode 100644 lbrynet/reflector/__init__.py create mode 100644 lbrynet/reflector/client/__init__.py create mode 100644 lbrynet/reflector/client/client.py create mode 100644 lbrynet/reflector/server/__init__.py create mode 100644 lbrynet/reflector/server/server.py diff --git a/lbrynet/reflector/__init__.py b/lbrynet/reflector/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lbrynet/reflector/client/__init__.py b/lbrynet/reflector/client/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lbrynet/reflector/client/client.py b/lbrynet/reflector/client/client.py new file mode 100644 index 000000000..940dc7a27 --- /dev/null +++ b/lbrynet/reflector/client/client.py @@ -0,0 +1,140 @@ +""" +The reflector protocol (all dicts encoded in json): + +Client Handshake (sent once per connection, at the start of the connection): + +{ + 'version': 0, +} + + +Server Handshake (sent once per connection, after receiving the client handshake): + +{ + 'version': 0, +} + + +Client Info Request: + +{ + 'blob_hash': "", + 'blob_size': +} + + +Server Info Response (sent in response to Client Info Request): + +{ + 'response': ['YES', 'NO'] +} + +If response is 'YES', client may send a Client Blob Request or a Client Info Request. +If response is 'NO', client may only send a Client Info Request + + +Client Blob Request: + +{} # Yes, this is an empty dictionary, in case something needs to go here in the future + # this blob data must match the info sent in the most recent Client Info Request + + +Server Blob Response (sent in response to Client Blob Request): +{ + 'received': True +} + +Client may now send another Client Info Request + +""" +import json +import logging +from twisted.internet.protocol import Protocol, ClientFactory + + +log = logging.getLogger(__name__) + + +class IncompleteResponseError(Exception): + pass + + +class LBRYFileReflectorClient(Protocol): + def connectionMade(self): + self.peer = self.factory.peer + self.response_buff = '' + self.outgoing_buff = '' + self.blob_hashes_to_send = [] + self.next_blob_to_send = None + self.received_handshake_response = False + d = self.get_blobs_to_send(self.factory.stream_info_manager, self.factory.stream_hash) + d.addCallback(lambda _: self.send_handshake()) + + def dataReceived(self, data): + self.response_buff += data + try: + msg = self.parse_response(self.response_buff) + except IncompleteResponseError: + pass + else: + self.response_buff = '' + d = self.handle_response(msg) + d.addCallbacks(lambda _: self.send_next_request(), self.response_failure_handler) + + def connectionLost(self, reason): + pass + + def get_blobs_to_send(self, stream_info_manager, stream_hash): + d = stream_info_manager.get_blobs_for_stream(stream_hash) + + def set_blobs(blob_hashes): + for blob_hash, position, iv, length in blob_hashes: + self.blob_hashes_to_send.append(blob_hash) + + d.addCallback(set_blobs) + return d + + def parse_response(self, buff): + try: + return json.loads(buff) + except ValueError: + raise IncompleteResponseError() + + def handle_response(self, response_dict): + if self.received_handshake_response is False: + self.handle_handshake_response(response_dict) + else: + self.handle_normal_response(response_dict) + + def handle_handshake_response(self, response_dict): + pass + + def handle_normal_response(self, response_dict): + pass + + def send_next_request(self): + if self.next_blob_to_send is not None: + # send the blob + pass + elif self.blobs_to_send: + # send the server the next blob hash + length + pass + else: + # close connection + pass + + +class LBRYFileReflectorClientFactory(ClientFactory): + protocol = LBRYFileReflectorClient + + def __init__(self, stream_info_manager, peer, stream_hash): + self.peer = peer + self.stream_info_manager = stream_info_manager + self.stream_hash = stream_hash + self.p = None + + def buildProtocol(self, addr): + p = self.protocol() + p.factory = self + self.p = p + return p \ No newline at end of file diff --git a/lbrynet/reflector/server/__init__.py b/lbrynet/reflector/server/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lbrynet/reflector/server/server.py b/lbrynet/reflector/server/server.py new file mode 100644 index 000000000..ca43e224b --- /dev/null +++ b/lbrynet/reflector/server/server.py @@ -0,0 +1,86 @@ +import logging +from twisted.python import failure +from twisted.internet import error +from twisted.internet.protocol import Protocol, ServerFactory +import json + + +log = logging.getLogger(__name__) + + +class IncompleteMessageError(Exception): + pass + + +class ReflectorServer(Protocol): + """ + """ + + def connectionMade(self): + peer_info = self.transport.getPeer() + self.peer = self.factory.peer_manager.get_peer(peer_info.host, peer_info.port) + self.received_handshake = False + self.peer_version = None + self.receiving_blob = False + self.blob_write = None + self.blob_finished_d = None + self.request_buff = "" + + def connectionLost(self, reason=failure.Failure(error.ConnectionDone())): + pass + + def dataReceived(self, data): + if self.receiving_blob is False: + self.request_buff += data + try: + msg = self.parse_request(self.request_buff) + except IncompleteMessageError: + pass + else: + self.request_buff = '' + d = self.handle_request(msg) + d.addCallbacks(self.send_response, self.handle_error) + else: + self.blob_write(data) + + def parse_request(self, buff): + try: + return json.loads(buff) + except ValueError: + raise IncompleteMessageError() + + def handle_request(self, request_dict): + if self.received_handshake is False: + return self.handle_handshake(request_dict) + else: + return self.handle_normal_request(request_dict) + + def handle_handshake(self, request_dict): + pass + + def handle_normal_request(self, request_dict): + if self.blob_write is None: + # we haven't opened a blob yet, meaning we must be waiting for the + # next message containing a blob hash and a length. this message + # should be it. if it's one we want, open the blob for writing, and + # return a nice response dict (in a Deferred) saying go ahead + pass + else: + # we have a blob open already, so this message should have nothing + # important in it. to the deferred that fires when the blob is done, + # add a callback which returns a nice response dict saying to keep + # sending, and then return that deferred + pass + + def send_response(self, response_dict): + pass + + def handle_error(self, err): + pass + + +class ReflectorServerFactory(ServerFactory): + protocol = ReflectorServer + + def __init__(self, peer_manager): + self.peer_manager = peer_manager \ No newline at end of file From 1dea20a358864dc392268d70554a6f1711fbf852 Mon Sep 17 00:00:00 2001 From: Jimmy Kiselak Date: Wed, 3 Aug 2016 23:11:29 -0400 Subject: [PATCH 08/81] include sd blob hashes in list of blob hashes to send via reflector --- lbrynet/reflector/client/client.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lbrynet/reflector/client/client.py b/lbrynet/reflector/client/client.py index 940dc7a27..379a86a96 100644 --- a/lbrynet/reflector/client/client.py +++ b/lbrynet/reflector/client/client.py @@ -92,6 +92,13 @@ class LBRYFileReflectorClient(Protocol): self.blob_hashes_to_send.append(blob_hash) d.addCallback(set_blobs) + + d.addCallback(lambda _: stream_info_manager.get_sd_blob_hashes_for_stream) + + def set_sd_blobs(sd_blob_hashes): + for sd_blob_hash, in sd_blob_hashes: + self.blob_hashes_to_send.append(sd_blob_hash) + d.addCallback(set_sd_blobs) return d def parse_response(self, buff): From b7e2e87ac148c0807e47e3f5ce107b91e07ca3ec Mon Sep 17 00:00:00 2001 From: Jimmy Kiselak Date: Sun, 7 Aug 2016 22:33:40 -0400 Subject: [PATCH 09/81] fill in much of the skeleton for the reflector protocol --- lbrynet/reflector/client/client.py | 99 +++++++++++++++++++++++++----- lbrynet/reflector/server/server.py | 89 ++++++++++++++++++++------- 2 files changed, 152 insertions(+), 36 deletions(-) diff --git a/lbrynet/reflector/client/client.py b/lbrynet/reflector/client/client.py index 379a86a96..2bccb4fe3 100644 --- a/lbrynet/reflector/client/client.py +++ b/lbrynet/reflector/client/client.py @@ -26,7 +26,7 @@ Client Info Request: Server Info Response (sent in response to Client Info Request): { - 'response': ['YES', 'NO'] + 'send_blob': True|False } If response is 'YES', client may send a Client Blob Request or a Client Info Request. @@ -41,7 +41,7 @@ Client Blob Request: Server Blob Response (sent in response to Client Blob Request): { - 'received': True + 'received_blob': True } Client may now send another Client Info Request @@ -49,6 +49,7 @@ Client may now send another Client Info Request """ import json import logging +from twisted.protocols.basic import FileSender from twisted.internet.protocol import Protocol, ClientFactory @@ -60,13 +61,19 @@ class IncompleteResponseError(Exception): class LBRYFileReflectorClient(Protocol): + + # Protocol stuff + def connectionMade(self): - self.peer = self.factory.peer + self.blob_manager = self.factory.blob_manager self.response_buff = '' self.outgoing_buff = '' self.blob_hashes_to_send = [] self.next_blob_to_send = None + self.blob_read_handle = None self.received_handshake_response = False + self.protocol_version = None + self.file_sender = None d = self.get_blobs_to_send(self.factory.stream_info_manager, self.factory.stream_hash) d.addCallback(lambda _: self.send_handshake()) @@ -84,6 +91,17 @@ class LBRYFileReflectorClient(Protocol): def connectionLost(self, reason): pass + # IConsumer stuff + + def registerProducer(self, producer, streaming): + assert streaming is True + + def unregisterProducer(self): + self.transport.loseConnection() + + def write(self, data): + self.transport.write(data) + def get_blobs_to_send(self, stream_info_manager, stream_hash): d = stream_info_manager.get_blobs_for_stream(stream_hash) @@ -109,33 +127,86 @@ class LBRYFileReflectorClient(Protocol): def handle_response(self, response_dict): if self.received_handshake_response is False: - self.handle_handshake_response(response_dict) + return self.handle_handshake_response(response_dict) else: - self.handle_normal_response(response_dict) + return self.handle_normal_response(response_dict) + + def set_not_uploading(self): + if self.currently_uploading is not None: + self.currently_uploading.close_read_handle(self.read_handle) + self.read_handle = None + self.currently_uploading = None + self.file_sender = None + + def start_transfer(self): + self.write(json.dumps("{}")) + log.info("Starting the file upload") + assert self.read_handle is not None, "self.read_handle was None when trying to start the transfer" + d = self.file_sender.beginFileTransfer(self.read_handle, self) + return d def handle_handshake_response(self, response_dict): - pass + if 'version' not in response_dict: + raise ValueError("Need protocol version number!") + self.protocol_version = int(response_dict['version']) + if self.protocol_version != 0: + raise ValueError("I can't handle protocol version {}!".format(self.protocol_version)) + self.received_handshake_response = True def handle_normal_response(self, response_dict): - pass + if self.next_blob_to_send is not None: # Expecting Server Info Response + if 'send_blob' not in response_dict: + raise ValueError("I don't know whether to send the blob or not!") + if response_dict['send_blob'] is True: + self.file_sender = FileSender() + return True + else: + return self.set_not_uploading() + else: # Expecting Server Blob Response + if 'received_blob' not in response_dict: + raise ValueError("I don't know if the blob made it to the intended destination!") + else: + return self.set_not_uploading() + + def open_blob_for_reading(self, blob): + if blob.is_validated(): + read_handle = blob.open_for_reading() + if read_handle is not None: + self.next_blob_to_send = blob + self.read_handle = read_handle + return None + raise ValueError("Couldn't open that blob for some reason") + + def send_blob_info(self): + assert self.next_blob_to_send is not None, "need to have a next blob to send at this point" + self.write(json.dumps({ + 'blob_hash': self.next_blob_to_send.blob_hash, + 'blob_length': self.next_blob_to_send.length + })) def send_next_request(self): - if self.next_blob_to_send is not None: + if self.file_sender is not None: # send the blob - pass - elif self.blobs_to_send: + return self.start_transfer() + elif self.blob_hashes_to_send: + # open the next blob to send + blob_hash = self.blob_hashes_to_send[0] + self.blob_hashes_to_send = self.blob_hashes_to_send[1:] + d = self.blob_manager.get_blob(blob_hash, True) + d.addCallback(self.open_blob_for_reading) # send the server the next blob hash + length - pass + d.addCallback(lambda _: self.send_blob_info()) + return d else: # close connection - pass + self.transport.loseConnection() class LBRYFileReflectorClientFactory(ClientFactory): protocol = LBRYFileReflectorClient - def __init__(self, stream_info_manager, peer, stream_hash): - self.peer = peer + def __init__(self, blob_manager, stream_info_manager, stream_hash): + self.blob_manager = blob_manager self.stream_info_manager = stream_info_manager self.stream_hash = stream_hash self.p = None diff --git a/lbrynet/reflector/server/server.py b/lbrynet/reflector/server/server.py index ca43e224b..f0ee24f87 100644 --- a/lbrynet/reflector/server/server.py +++ b/lbrynet/reflector/server/server.py @@ -4,26 +4,25 @@ from twisted.internet import error from twisted.internet.protocol import Protocol, ServerFactory import json +from lbrynet.core.utils import is_valid_blobhash + log = logging.getLogger(__name__) -class IncompleteMessageError(Exception): - pass - - class ReflectorServer(Protocol): - """ - """ def connectionMade(self): peer_info = self.transport.getPeer() self.peer = self.factory.peer_manager.get_peer(peer_info.host, peer_info.port) + self.blob_manager = self.factory.blob_manager self.received_handshake = False self.peer_version = None self.receiving_blob = False + self.incoming_blob = None self.blob_write = None self.blob_finished_d = None + self.cancel_write = None self.request_buff = "" def connectionLost(self, reason=failure.Failure(error.ConnectionDone())): @@ -32,22 +31,34 @@ class ReflectorServer(Protocol): def dataReceived(self, data): if self.receiving_blob is False: self.request_buff += data - try: - msg = self.parse_request(self.request_buff) - except IncompleteMessageError: - pass - else: + msg, extra_data = self._get_valid_response(self.request_buff) + if msg is not None: self.request_buff = '' d = self.handle_request(msg) d.addCallbacks(self.send_response, self.handle_error) + if self.receiving_blob is True and len(extra_data) != 0: + self.blob_write(extra_data) else: self.blob_write(data) - def parse_request(self, buff): - try: - return json.loads(buff) - except ValueError: - raise IncompleteMessageError() + def _get_valid_response(self, response_msg): + extra_data = None + response = None + curr_pos = 0 + while 1: + next_close_paren = response_msg.find('}', curr_pos) + if next_close_paren != -1: + curr_pos = next_close_paren + 1 + try: + response = json.loads(response_msg[:curr_pos]) + except ValueError: + pass + else: + extra_data = response_msg[curr_pos:] + break + else: + break + return response, extra_data def handle_request(self, request_dict): if self.received_handshake is False: @@ -56,7 +67,26 @@ class ReflectorServer(Protocol): return self.handle_normal_request(request_dict) def handle_handshake(self, request_dict): - pass + if 'version' not in request_dict: + raise ValueError("Client should send version") + self.peer_version = int(request_dict['version']) + if self.peer_version != 0: + raise ValueError("I don't know that version!") + return {'version': 0} + + def determine_blob_needed(self, blob): + if blob.is_validated(): + return {'send_blob': False} + else: + self.incoming_blob = blob + self.blob_finished_d, self.blob_write, self.cancel_write = blob.open_for_writing(self.peer) + return {'send_blob': True} + + def close_blob(self): + self.blob_finished_d = None + self.blob_write = None + self.cancel_write = None + self.incoming_blob = None def handle_normal_request(self, request_dict): if self.blob_write is None: @@ -64,16 +94,30 @@ class ReflectorServer(Protocol): # next message containing a blob hash and a length. this message # should be it. if it's one we want, open the blob for writing, and # return a nice response dict (in a Deferred) saying go ahead - pass + if not 'blob_hash' in request_dict or not 'blob_size' in request_dict: + raise ValueError("Expected a blob hash and a blob size") + if not is_valid_blobhash(request_dict['blob_hash']): + raise ValueError("Got a bad blob hash: {}".format(request_dict['blob_hash'])) + d = self.blob_manager.get_blob( + request_dict['blob_hash'], + True, + int(request_dict['blob_size']) + ) + d.addCallback(self.determine_blob_needed) else: # we have a blob open already, so this message should have nothing # important in it. to the deferred that fires when the blob is done, # add a callback which returns a nice response dict saying to keep # sending, and then return that deferred - pass + self.receiving_blob = True + d = self.blob_finished_d + d.addCallback(lambda _: self.close_blob()) + d.addCallback(lambda _: {'received_blob': True}) + d.addCallback(self.send_response) + return d def send_response(self, response_dict): - pass + self.write(json.dumps(response_dict)) def handle_error(self, err): pass @@ -82,5 +126,6 @@ class ReflectorServer(Protocol): class ReflectorServerFactory(ServerFactory): protocol = ReflectorServer - def __init__(self, peer_manager): - self.peer_manager = peer_manager \ No newline at end of file + def __init__(self, peer_manager, blob_manager): + self.peer_manager = peer_manager + self.blob_manager = blob_manager \ No newline at end of file From 7e2ad58eddb65ebaeef325ca30df4052e5341cbc Mon Sep 17 00:00:00 2001 From: Jimmy Kiselak Date: Tue, 9 Aug 2016 00:59:50 -0400 Subject: [PATCH 10/81] get reflector client and server working; add func test to prove it --- lbrynet/reflector/client/client.py | 57 +++++++---- lbrynet/reflector/server/server.py | 9 +- tests/functional_tests.py | 149 ++++++++++++++++++++++++++++- 3 files changed, 194 insertions(+), 21 deletions(-) diff --git a/lbrynet/reflector/client/client.py b/lbrynet/reflector/client/client.py index 2bccb4fe3..ce2102f95 100644 --- a/lbrynet/reflector/client/client.py +++ b/lbrynet/reflector/client/client.py @@ -51,6 +51,7 @@ import json import logging from twisted.protocols.basic import FileSender from twisted.internet.protocol import Protocol, ClientFactory +from twisted.internet import defer, error log = logging.getLogger(__name__) @@ -74,8 +75,11 @@ class LBRYFileReflectorClient(Protocol): self.received_handshake_response = False self.protocol_version = None self.file_sender = None + self.producer = None + self.streaming = False d = self.get_blobs_to_send(self.factory.stream_info_manager, self.factory.stream_hash) d.addCallback(lambda _: self.send_handshake()) + d.addErrback(lambda err: log.warning("An error occurred immediately: %s", err.getTraceback())) def dataReceived(self, data): self.response_buff += data @@ -86,45 +90,64 @@ class LBRYFileReflectorClient(Protocol): else: self.response_buff = '' d = self.handle_response(msg) - d.addCallbacks(lambda _: self.send_next_request(), self.response_failure_handler) + d.addCallback(lambda _: self.send_next_request()) + d.addErrback(self.response_failure_handler) def connectionLost(self, reason): - pass + if reason.check(error.ConnectionDone): + self.factory.finished_deferred.callback(True) + else: + self.factory.finished_deferred.callback(reason) # IConsumer stuff def registerProducer(self, producer, streaming): - assert streaming is True + self.producer = producer + self.streaming = streaming + if self.streaming is False: + from twisted.internet import reactor + reactor.callLater(0, self.producer.resumeProducing) def unregisterProducer(self): - self.transport.loseConnection() + self.producer = None def write(self, data): self.transport.write(data) + if self.producer is not None and self.streaming is False: + from twisted.internet import reactor + reactor.callLater(0, self.producer.resumeProducing) def get_blobs_to_send(self, stream_info_manager, stream_hash): d = stream_info_manager.get_blobs_for_stream(stream_hash) def set_blobs(blob_hashes): for blob_hash, position, iv, length in blob_hashes: - self.blob_hashes_to_send.append(blob_hash) + if blob_hash is not None: + self.blob_hashes_to_send.append(blob_hash) d.addCallback(set_blobs) - d.addCallback(lambda _: stream_info_manager.get_sd_blob_hashes_for_stream) + d.addCallback(lambda _: stream_info_manager.get_sd_blob_hashes_for_stream(stream_hash)) def set_sd_blobs(sd_blob_hashes): - for sd_blob_hash, in sd_blob_hashes: + for sd_blob_hash in sd_blob_hashes: self.blob_hashes_to_send.append(sd_blob_hash) + d.addCallback(set_sd_blobs) return d + def send_handshake(self): + self.write(json.dumps({'version': 0})) + def parse_response(self, buff): try: return json.loads(buff) except ValueError: raise IncompleteResponseError() + def response_failure_handler(self, err): + log.warning("An error occurred handling the response: %s", err.getTraceback()) + def handle_response(self, response_dict): if self.received_handshake_response is False: return self.handle_handshake_response(response_dict) @@ -132,15 +155,15 @@ class LBRYFileReflectorClient(Protocol): return self.handle_normal_response(response_dict) def set_not_uploading(self): - if self.currently_uploading is not None: - self.currently_uploading.close_read_handle(self.read_handle) + if self.next_blob_to_send is not None: + self.next_blob_to_send.close_read_handle(self.read_handle) self.read_handle = None - self.currently_uploading = None + self.next_blob_to_send = None self.file_sender = None + return defer.succeed(None) def start_transfer(self): - self.write(json.dumps("{}")) - log.info("Starting the file upload") + self.write(json.dumps({})) assert self.read_handle is not None, "self.read_handle was None when trying to start the transfer" d = self.file_sender.beginFileTransfer(self.read_handle, self) return d @@ -152,14 +175,15 @@ class LBRYFileReflectorClient(Protocol): if self.protocol_version != 0: raise ValueError("I can't handle protocol version {}!".format(self.protocol_version)) self.received_handshake_response = True + return defer.succeed(True) def handle_normal_response(self, response_dict): - if self.next_blob_to_send is not None: # Expecting Server Info Response + if self.file_sender is None: # Expecting Server Info Response if 'send_blob' not in response_dict: raise ValueError("I don't know whether to send the blob or not!") if response_dict['send_blob'] is True: self.file_sender = FileSender() - return True + return defer.succeed(True) else: return self.set_not_uploading() else: # Expecting Server Blob Response @@ -175,13 +199,13 @@ class LBRYFileReflectorClient(Protocol): self.next_blob_to_send = blob self.read_handle = read_handle return None - raise ValueError("Couldn't open that blob for some reason") + raise ValueError("Couldn't open that blob for some reason. blob_hash: {}".format(blob.blob_hash)) def send_blob_info(self): assert self.next_blob_to_send is not None, "need to have a next blob to send at this point" self.write(json.dumps({ 'blob_hash': self.next_blob_to_send.blob_hash, - 'blob_length': self.next_blob_to_send.length + 'blob_size': self.next_blob_to_send.length })) def send_next_request(self): @@ -210,6 +234,7 @@ class LBRYFileReflectorClientFactory(ClientFactory): self.stream_info_manager = stream_info_manager self.stream_hash = stream_hash self.p = None + self.finished_deferred = defer.Deferred() def buildProtocol(self, addr): p = self.protocol() diff --git a/lbrynet/reflector/server/server.py b/lbrynet/reflector/server/server.py index f0ee24f87..a8f36ae22 100644 --- a/lbrynet/reflector/server/server.py +++ b/lbrynet/reflector/server/server.py @@ -1,6 +1,6 @@ import logging from twisted.python import failure -from twisted.internet import error +from twisted.internet import error, defer from twisted.internet.protocol import Protocol, ServerFactory import json @@ -72,7 +72,8 @@ class ReflectorServer(Protocol): self.peer_version = int(request_dict['version']) if self.peer_version != 0: raise ValueError("I don't know that version!") - return {'version': 0} + self.received_handshake = True + return defer.succeed({'version': 0}) def determine_blob_needed(self, blob): if blob.is_validated(): @@ -87,6 +88,7 @@ class ReflectorServer(Protocol): self.blob_write = None self.cancel_write = None self.incoming_blob = None + self.receiving_blob = False def handle_normal_request(self, request_dict): if self.blob_write is None: @@ -113,11 +115,10 @@ class ReflectorServer(Protocol): d = self.blob_finished_d d.addCallback(lambda _: self.close_blob()) d.addCallback(lambda _: {'received_blob': True}) - d.addCallback(self.send_response) return d def send_response(self, response_dict): - self.write(json.dumps(response_dict)) + self.transport.write(json.dumps(response_dict)) def handle_error(self, err): pass diff --git a/tests/functional_tests.py b/tests/functional_tests.py index 76af2b5ed..c9e062d1c 100644 --- a/tests/functional_tests.py +++ b/tests/functional_tests.py @@ -26,7 +26,7 @@ from lbrynet.core.StreamDescriptor import download_sd_blob from lbrynet.lbryfilemanager.LBRYFileCreator import create_lbry_file from lbrynet.lbryfile.client.LBRYFileOptions import add_lbry_file_to_sd_identifier from lbrynet.lbryfile.StreamDescriptor import get_sd_info -from twisted.internet import defer, threads, task +from twisted.internet import defer, threads, task, error from twisted.trial.unittest import TestCase from twisted.python.failure import Failure import os @@ -38,6 +38,10 @@ from lbrynet.core.server.ServerProtocol import ServerProtocolFactory from lbrynet.lbrylive.server.LiveBlobInfoQueryHandler import CryptBlobInfoQueryHandlerFactory from lbrynet.lbrylive.client.LiveStreamOptions import add_live_stream_to_sd_identifier from lbrynet.lbrylive.client.LiveStreamDownloader import add_full_live_stream_downloader_to_sd_identifier +from lbrynet.core.BlobManager import TempBlobManager +from lbrynet.reflector.client.client import LBRYFileReflectorClientFactory +from lbrynet.reflector.server.server import ReflectorServerFactory +from lbrynet.lbryfile.StreamDescriptor import publish_sd_blob log_format = "%(funcName)s(): %(message)s" @@ -1369,4 +1373,147 @@ class TestStreamify(TestCase): d.addCallback(lambda _: self.lbry_file_manager.setup()) d.addCallback(lambda _: create_stream()) d.addCallback(combine_stream) + return d + + +class TestReflector(TestCase): + + def setUp(self): + self.session = None + self.stream_info_manager = None + self.lbry_file_manager = None + self.server_blob_manager = None + self.reflector_port = None + self.addCleanup(self.take_down_env) + + def take_down_env(self): + + d = defer.succeed(True) + if self.lbry_file_manager is not None: + d.addCallback(lambda _: self.lbry_file_manager.stop()) + if self.session is not None: + d.addCallback(lambda _: self.session.shut_down()) + if self.stream_info_manager is not None: + d.addCallback(lambda _: self.stream_info_manager.stop()) + if self.server_blob_manager is not None: + d.addCallback(lambda _: self.server_blob_manager.stop()) + if self.reflector_port is not None: + d.addCallback(lambda _: self.reflector_port.stopListening()) + + def delete_test_env(): + shutil.rmtree('client') + + d.addCallback(lambda _: threads.deferToThread(delete_test_env)) + return d + + def test_reflector(self): + + wallet = FakeWallet() + peer_manager = PeerManager() + peer_finder = FakePeerFinder(5553, peer_manager, 2) + hash_announcer = FakeAnnouncer() + rate_limiter = DummyRateLimiter() + sd_identifier = StreamDescriptorIdentifier() + + self.expected_blobs = [ + ('dc4708f76a5e7af0f1cae0ee96b824e2ed9250c9346c093b' + '441f0a20d3607c17948b6fcfb4bc62020fe5286693d08586', 2097152), + ('f4067522c1b49432a2a679512e3917144317caa1abba0c04' + '1e0cd2cf9f635d4cf127ce1824fa04189b63916174951f70', 2097152), + ('305486c434260484fcb2968ce0e963b72f81ba56c11b08b1' + 'af0789b55b44d78422600f9a38e3cf4f2e9569897e5646a9', 1015056), + ] + + db_dir = "client" + os.mkdir(db_dir) + + self.session = LBRYSession(MIN_BLOB_DATA_PAYMENT_RATE, db_dir=db_dir, lbryid="abcd", + peer_finder=peer_finder, hash_announcer=hash_announcer, + blob_dir=None, peer_port=5553, + use_upnp=False, rate_limiter=rate_limiter, wallet=wallet) + + self.stream_info_manager = TempLBRYFileMetadataManager() + + self.lbry_file_manager = LBRYFileManager(self.session, self.stream_info_manager, sd_identifier) + + self.server_blob_manager = TempBlobManager(hash_announcer) + + d = self.session.setup() + d.addCallback(lambda _: self.stream_info_manager.setup()) + d.addCallback(lambda _: add_lbry_file_to_sd_identifier(sd_identifier)) + d.addCallback(lambda _: self.lbry_file_manager.setup()) + d.addCallback(lambda _: self.server_blob_manager.setup()) + + def verify_equal(sd_info): + self.assertEqual(sd_info, test_create_stream_sd_file) + + def save_sd_blob_hash(sd_hash): + self.expected_blobs.append((sd_hash, 923)) + + def verify_stream_descriptor_file(stream_hash): + d = get_sd_info(self.lbry_file_manager.stream_info_manager, stream_hash, True) + d.addCallback(verify_equal) + d.addCallback(lambda _: publish_sd_blob(self.lbry_file_manager.stream_info_manager, self.session.blob_manager, stream_hash)) + d.addCallback(save_sd_blob_hash) + d.addCallback(lambda _: stream_hash) + return d + + def iv_generator(): + iv = 0 + while 1: + iv += 1 + yield "%016d" % iv + + def create_stream(): + test_file = GenFile(5209343, b''.join([chr(i + 3) for i in xrange(0, 64, 6)])) + d = create_lbry_file(self.session, self.lbry_file_manager, "test_file", test_file, + key="0123456701234567", iv_generator=iv_generator()) + return d + + def start_server(): + server_factory = ReflectorServerFactory(peer_manager, self.server_blob_manager) + from twisted.internet import reactor + port = 8943 + while self.reflector_port is None: + try: + self.reflector_port = reactor.listenTCP(port, server_factory) + except error.CannotListenError: + port += 1 + return defer.succeed(port) + + def send_to_server(port, stream_hash): + factory = LBRYFileReflectorClientFactory( + self.session.blob_manager, + self.stream_info_manager, + stream_hash + ) + + from twisted.internet import reactor + reactor.connectTCP('localhost', port, factory) + return factory.finished_deferred + + def verify_blob_completed(blob, blob_size): + self.assertTrue(blob.is_validated()) + self.assertEqual(blob_size, blob.length) + + def verify_have_blob(blob_hash, blob_size): + d = self.server_blob_manager.get_blob(blob_hash, True) + d.addCallback(lambda blob: verify_blob_completed(blob, blob_size)) + return d + + def verify_data_on_reflector(): + check_blob_ds = [] + for blob_hash, blob_size in self.expected_blobs: + check_blob_ds.append(verify_have_blob(blob_hash, blob_size)) + return defer.DeferredList(check_blob_ds) + + def upload_to_reflector(stream_hash): + d = start_server() + d.addCallback(lambda port: send_to_server(port, stream_hash)) + d.addCallback(lambda _: verify_data_on_reflector()) + return d + + d.addCallback(lambda _: create_stream()) + d.addCallback(verify_stream_descriptor_file) + d.addCallback(upload_to_reflector) return d \ No newline at end of file From 14a0252819d74ef55e958c2e6bbc41222b0eada8 Mon Sep 17 00:00:00 2001 From: Jack Date: Tue, 9 Aug 2016 12:07:26 -0400 Subject: [PATCH 11/81] hooking up reflector plumbing --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 30 +++++++++++++++++++++++-- lbrynet/lbrynet_daemon/LBRYPublisher.py | 8 +++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index 40f75156f..a1671505e 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -56,6 +56,7 @@ from lbrynet.core.PTCWallet import PTCWallet from lbrynet.core.LBRYWallet import LBRYcrdWallet, LBRYumWallet from lbrynet.lbryfilemanager.LBRYFileManager import LBRYFileManager from lbrynet.lbryfile.LBRYFileMetadataManager import DBLBRYFileMetadataManager, TempLBRYFileMetadataManager +from lbrynet.reflector.server import ReflectorServerFactory # from lbryum import LOG_PATH as lbryum_log @@ -203,6 +204,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): 'delete_blobs_on_remove': True, 'peer_port': 3333, 'dht_node_port': 4444, + 'reflector_port': 5566, 'use_upnp': True, 'start_lbrycrdd': True, 'requested_first_run_credits': False, @@ -291,6 +293,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): #### self.delete_blobs_on_remove = self.session_settings['delete_blobs_on_remove'] self.peer_port = self.session_settings['peer_port'] + self.reflector_port = self.session_settings['reflector_port'] self.dht_node_port = self.session_settings['dht_node_port'] self.use_upnp = self.session_settings['use_upnp'] self.start_lbrycrdd = self.session_settings['start_lbrycrdd'] @@ -670,10 +673,10 @@ class LBRYDaemon(jsonrpc.JSONRPC): def _start_server(self): if self.peer_port is not None: - server_factory = ServerProtocolFactory(self.session.rate_limiter, self.query_handlers, self.session.peer_manager) + try: self.lbry_server_port = reactor.listenTCP(self.peer_port, server_factory) except error.CannotListenError as e: @@ -682,6 +685,27 @@ class LBRYDaemon(jsonrpc.JSONRPC): raise ValueError("%s lbrynet may already be running on your computer.", str(e)) return defer.succeed(True) + def _start_reflector(self): + if self.reflector_port is not None: + reflector_factory = ReflectorServerFactory(self.session.peer_manager, self.session.blob_manager) + try: + self.reflector_server_port = reactor.listenTCP(self.reflector_port, reflector_factory) + except error.CannotListenError as e: + import traceback + log.error("Couldn't bind reflector to port %d. %s", self.reflector, traceback.format_exc()) + raise ValueError("%s lbrynet may already be running on your computer.", str(e)) + return defer.succeed(True) + + def _stop_reflector(self): + try: + if self.reflector_server_port is not None: + self.reflector_server_port, p = None, self.reflector_server_port + return defer.maybeDeferred(p.stopListening) + else: + return defer.succeed(True) + except AttributeError: + return defer.succeed(True) + def _stop_server(self): try: if self.lbry_server_port is not None: @@ -695,7 +719,8 @@ class LBRYDaemon(jsonrpc.JSONRPC): def _setup_server(self): def restore_running_status(running): if running is True: - return self._start_server() + d = self._start_server() + d.addCallback(lambda _: self._start_reflector()) return defer.succeed(True) self.startup_status = STARTUP_STAGES[4] @@ -793,6 +818,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d = self._upload_log(log_type="close", exclude_previous=False if self.first_run else True) d.addCallback(lambda _: self._stop_server()) + d.addCallback(lambda _: self._stop_reflector()) d.addErrback(lambda err: True) d.addCallback(lambda _: self.lbry_file_manager.stop()) d.addErrback(lambda err: True) diff --git a/lbrynet/lbrynet_daemon/LBRYPublisher.py b/lbrynet/lbrynet_daemon/LBRYPublisher.py index 3e1b78325..7584d91fe 100644 --- a/lbrynet/lbrynet_daemon/LBRYPublisher.py +++ b/lbrynet/lbrynet_daemon/LBRYPublisher.py @@ -12,6 +12,7 @@ from lbrynet.lbryfile.StreamDescriptor import publish_sd_blob from lbrynet.core.PaymentRateManager import PaymentRateManager from lbrynet.core.LBRYMetadata import Metadata, CURRENT_METADATA_VERSION from lbrynet.lbryfilemanager.LBRYFileDownloader import ManagedLBRYFileDownloader +from lbrynet.reflector.client import LBRYFileReflectorClientFactory from lbrynet.conf import LOG_FILE_NAME from twisted.internet import threads, defer @@ -41,6 +42,7 @@ class Publisher(object): self.lbry_file = None self.txid = None self.stream_hash = None + self.reflector_client = None self.metadata = {} def start(self, name, file_path, bid, metadata, old_txid): @@ -62,10 +64,16 @@ class Publisher(object): d.addCallback(lambda _: self._create_sd_blob()) d.addCallback(lambda _: self._claim_name()) d.addCallback(lambda _: self.set_status()) + d.addCallback(lambda _: self.start_reflector()) d.addCallbacks(lambda _: _show_result(), self._show_publish_error) return d + def start_reflector(self): + self.reflector_client = LBRYFileReflectorClientFactory(self.session.blob_manager, + self.lbry_file_manager.stream_info_manager, + self.stream_hash) + def _check_file_path(self, file_path): def check_file_threaded(): f = open(file_path) From 82a4fea81a7e0ea24c688d15d0eddf899a73a462 Mon Sep 17 00:00:00 2001 From: Jack Date: Tue, 9 Aug 2016 12:12:40 -0400 Subject: [PATCH 12/81] fix log statement --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index a1671505e..43cedfbbf 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -692,7 +692,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): self.reflector_server_port = reactor.listenTCP(self.reflector_port, reflector_factory) except error.CannotListenError as e: import traceback - log.error("Couldn't bind reflector to port %d. %s", self.reflector, traceback.format_exc()) + log.error("Couldn't bind reflector to port %d. %s", self.reflector_port, traceback.format_exc()) raise ValueError("%s lbrynet may already be running on your computer.", str(e)) return defer.succeed(True) From ef6fe3d1d9b9be3092e72653642562f58de6c06f Mon Sep 17 00:00:00 2001 From: Jack Date: Tue, 9 Aug 2016 12:18:46 -0400 Subject: [PATCH 13/81] add variable to enable running reflector server --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 34 ++++++++++++++++------------ 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index 43cedfbbf..bbaaa98c0 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -170,6 +170,9 @@ class LBRYDaemon(jsonrpc.JSONRPC): self.first_run_after_update = False self.uploaded_temp_files = [] + # change this to enable reflector server + self.run_reflector_server = False + if os.name == "nt": from lbrynet.winhelpers.knownpaths import get_path, FOLDERID, UserHandle default_download_directory = get_path(FOLDERID.Downloads, UserHandle.current) @@ -686,25 +689,26 @@ class LBRYDaemon(jsonrpc.JSONRPC): return defer.succeed(True) def _start_reflector(self): - if self.reflector_port is not None: - reflector_factory = ReflectorServerFactory(self.session.peer_manager, self.session.blob_manager) - try: - self.reflector_server_port = reactor.listenTCP(self.reflector_port, reflector_factory) - except error.CannotListenError as e: - import traceback - log.error("Couldn't bind reflector to port %d. %s", self.reflector_port, traceback.format_exc()) - raise ValueError("%s lbrynet may already be running on your computer.", str(e)) + if self.run_reflector_server: + if self.reflector_port is not None: + reflector_factory = ReflectorServerFactory(self.session.peer_manager, self.session.blob_manager) + try: + self.reflector_server_port = reactor.listenTCP(self.reflector_port, reflector_factory) + except error.CannotListenError as e: + import traceback + log.error("Couldn't bind reflector to port %d. %s", self.reflector_port, traceback.format_exc()) + raise ValueError("%s lbrynet may already be running on your computer.", str(e)) return defer.succeed(True) def _stop_reflector(self): - try: - if self.reflector_server_port is not None: - self.reflector_server_port, p = None, self.reflector_server_port - return defer.maybeDeferred(p.stopListening) - else: + if self.run_reflector_server: + try: + if self.reflector_server_port is not None: + self.reflector_server_port, p = None, self.reflector_server_port + return defer.maybeDeferred(p.stopListening) + except AttributeError: return defer.succeed(True) - except AttributeError: - return defer.succeed(True) + return defer.succeed(True) def _stop_server(self): try: From 0733d885fb1461b6d9925c98ab04d317dc5378cf Mon Sep 17 00:00:00 2001 From: Jack Date: Tue, 9 Aug 2016 12:26:34 -0400 Subject: [PATCH 14/81] start reflector upload in publish --- lbrynet/conf.py | 2 ++ lbrynet/lbrynet_daemon/LBRYPublisher.py | 13 ++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lbrynet/conf.py b/lbrynet/conf.py index 466c477ea..fe37dd499 100644 --- a/lbrynet/conf.py +++ b/lbrynet/conf.py @@ -28,6 +28,8 @@ SEARCH_SERVERS = ["http://lighthouse1.lbry.io:50005", "http://lighthouse2.lbry.io:50005", "http://lighthouse3.lbry.io:50005"] +REFLECTOR_SERVERS = [("http://reflector.lbry.io", 5566)] + LOG_FILE_NAME = "lbrynet.log" LOG_POST_URL = "https://lbry.io/log-upload" diff --git a/lbrynet/lbrynet_daemon/LBRYPublisher.py b/lbrynet/lbrynet_daemon/LBRYPublisher.py index 7584d91fe..1fe6da991 100644 --- a/lbrynet/lbrynet_daemon/LBRYPublisher.py +++ b/lbrynet/lbrynet_daemon/LBRYPublisher.py @@ -2,9 +2,9 @@ import logging import mimetypes import os import sys +import random from appdirs import user_data_dir -from datetime import datetime from lbrynet.core.Error import InsufficientFundsError from lbrynet.lbryfilemanager.LBRYFileCreator import create_lbry_file @@ -13,8 +13,8 @@ from lbrynet.core.PaymentRateManager import PaymentRateManager from lbrynet.core.LBRYMetadata import Metadata, CURRENT_METADATA_VERSION from lbrynet.lbryfilemanager.LBRYFileDownloader import ManagedLBRYFileDownloader from lbrynet.reflector.client import LBRYFileReflectorClientFactory -from lbrynet.conf import LOG_FILE_NAME -from twisted.internet import threads, defer +from lbrynet.conf import LOG_FILE_NAME, REFLECTOR_SERVERS +from twisted.internet import threads, defer, reactor if sys.platform != "darwin": log_dir = os.path.join(os.path.expanduser("~"), ".lbrynet") @@ -42,7 +42,8 @@ class Publisher(object): self.lbry_file = None self.txid = None self.stream_hash = None - self.reflector_client = None + reflector_server = random.choice(REFLECTOR_SERVERS) + self.reflector_server, self.reflector_port = reflector_server[0], reflector_server[1] self.metadata = {} def start(self, name, file_path, bid, metadata, old_txid): @@ -70,9 +71,11 @@ class Publisher(object): return d def start_reflector(self): - self.reflector_client = LBRYFileReflectorClientFactory(self.session.blob_manager, + factory = LBRYFileReflectorClientFactory(self.session.blob_manager, self.lbry_file_manager.stream_info_manager, self.stream_hash) + reactor.connectTCP(self.reflector_server, self.reflector_port, factory) + return factory.finished_deferred def _check_file_path(self, file_path): def check_file_threaded(): From 0cd92a96c003a8dfe3fde49fa05db05d30765f63 Mon Sep 17 00:00:00 2001 From: Jack Date: Tue, 9 Aug 2016 17:46:25 -0400 Subject: [PATCH 15/81] add reflector files to tests directory --- tests/lbrynet/lbrynet/reflector/__init__.py | 0 .../lbrynet/reflector/client/__init__.py | 0 .../lbrynet/reflector/client/client.py | 243 ++++++++++++++++++ .../lbrynet/reflector/server/__init__.py | 0 .../lbrynet/reflector/server/server.py | 132 ++++++++++ 5 files changed, 375 insertions(+) create mode 100644 tests/lbrynet/lbrynet/reflector/__init__.py create mode 100644 tests/lbrynet/lbrynet/reflector/client/__init__.py create mode 100644 tests/lbrynet/lbrynet/reflector/client/client.py create mode 100644 tests/lbrynet/lbrynet/reflector/server/__init__.py create mode 100644 tests/lbrynet/lbrynet/reflector/server/server.py diff --git a/tests/lbrynet/lbrynet/reflector/__init__.py b/tests/lbrynet/lbrynet/reflector/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/lbrynet/lbrynet/reflector/client/__init__.py b/tests/lbrynet/lbrynet/reflector/client/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/lbrynet/lbrynet/reflector/client/client.py b/tests/lbrynet/lbrynet/reflector/client/client.py new file mode 100644 index 000000000..ce2102f95 --- /dev/null +++ b/tests/lbrynet/lbrynet/reflector/client/client.py @@ -0,0 +1,243 @@ +""" +The reflector protocol (all dicts encoded in json): + +Client Handshake (sent once per connection, at the start of the connection): + +{ + 'version': 0, +} + + +Server Handshake (sent once per connection, after receiving the client handshake): + +{ + 'version': 0, +} + + +Client Info Request: + +{ + 'blob_hash': "", + 'blob_size': +} + + +Server Info Response (sent in response to Client Info Request): + +{ + 'send_blob': True|False +} + +If response is 'YES', client may send a Client Blob Request or a Client Info Request. +If response is 'NO', client may only send a Client Info Request + + +Client Blob Request: + +{} # Yes, this is an empty dictionary, in case something needs to go here in the future + # this blob data must match the info sent in the most recent Client Info Request + + +Server Blob Response (sent in response to Client Blob Request): +{ + 'received_blob': True +} + +Client may now send another Client Info Request + +""" +import json +import logging +from twisted.protocols.basic import FileSender +from twisted.internet.protocol import Protocol, ClientFactory +from twisted.internet import defer, error + + +log = logging.getLogger(__name__) + + +class IncompleteResponseError(Exception): + pass + + +class LBRYFileReflectorClient(Protocol): + + # Protocol stuff + + def connectionMade(self): + self.blob_manager = self.factory.blob_manager + self.response_buff = '' + self.outgoing_buff = '' + self.blob_hashes_to_send = [] + self.next_blob_to_send = None + self.blob_read_handle = None + self.received_handshake_response = False + self.protocol_version = None + self.file_sender = None + self.producer = None + self.streaming = False + d = self.get_blobs_to_send(self.factory.stream_info_manager, self.factory.stream_hash) + d.addCallback(lambda _: self.send_handshake()) + d.addErrback(lambda err: log.warning("An error occurred immediately: %s", err.getTraceback())) + + def dataReceived(self, data): + self.response_buff += data + try: + msg = self.parse_response(self.response_buff) + except IncompleteResponseError: + pass + else: + self.response_buff = '' + d = self.handle_response(msg) + d.addCallback(lambda _: self.send_next_request()) + d.addErrback(self.response_failure_handler) + + def connectionLost(self, reason): + if reason.check(error.ConnectionDone): + self.factory.finished_deferred.callback(True) + else: + self.factory.finished_deferred.callback(reason) + + # IConsumer stuff + + def registerProducer(self, producer, streaming): + self.producer = producer + self.streaming = streaming + if self.streaming is False: + from twisted.internet import reactor + reactor.callLater(0, self.producer.resumeProducing) + + def unregisterProducer(self): + self.producer = None + + def write(self, data): + self.transport.write(data) + if self.producer is not None and self.streaming is False: + from twisted.internet import reactor + reactor.callLater(0, self.producer.resumeProducing) + + def get_blobs_to_send(self, stream_info_manager, stream_hash): + d = stream_info_manager.get_blobs_for_stream(stream_hash) + + def set_blobs(blob_hashes): + for blob_hash, position, iv, length in blob_hashes: + if blob_hash is not None: + self.blob_hashes_to_send.append(blob_hash) + + d.addCallback(set_blobs) + + d.addCallback(lambda _: stream_info_manager.get_sd_blob_hashes_for_stream(stream_hash)) + + def set_sd_blobs(sd_blob_hashes): + for sd_blob_hash in sd_blob_hashes: + self.blob_hashes_to_send.append(sd_blob_hash) + + d.addCallback(set_sd_blobs) + return d + + def send_handshake(self): + self.write(json.dumps({'version': 0})) + + def parse_response(self, buff): + try: + return json.loads(buff) + except ValueError: + raise IncompleteResponseError() + + def response_failure_handler(self, err): + log.warning("An error occurred handling the response: %s", err.getTraceback()) + + def handle_response(self, response_dict): + if self.received_handshake_response is False: + return self.handle_handshake_response(response_dict) + else: + return self.handle_normal_response(response_dict) + + def set_not_uploading(self): + if self.next_blob_to_send is not None: + self.next_blob_to_send.close_read_handle(self.read_handle) + self.read_handle = None + self.next_blob_to_send = None + self.file_sender = None + return defer.succeed(None) + + def start_transfer(self): + self.write(json.dumps({})) + assert self.read_handle is not None, "self.read_handle was None when trying to start the transfer" + d = self.file_sender.beginFileTransfer(self.read_handle, self) + return d + + def handle_handshake_response(self, response_dict): + if 'version' not in response_dict: + raise ValueError("Need protocol version number!") + self.protocol_version = int(response_dict['version']) + if self.protocol_version != 0: + raise ValueError("I can't handle protocol version {}!".format(self.protocol_version)) + self.received_handshake_response = True + return defer.succeed(True) + + def handle_normal_response(self, response_dict): + if self.file_sender is None: # Expecting Server Info Response + if 'send_blob' not in response_dict: + raise ValueError("I don't know whether to send the blob or not!") + if response_dict['send_blob'] is True: + self.file_sender = FileSender() + return defer.succeed(True) + else: + return self.set_not_uploading() + else: # Expecting Server Blob Response + if 'received_blob' not in response_dict: + raise ValueError("I don't know if the blob made it to the intended destination!") + else: + return self.set_not_uploading() + + def open_blob_for_reading(self, blob): + if blob.is_validated(): + read_handle = blob.open_for_reading() + if read_handle is not None: + self.next_blob_to_send = blob + self.read_handle = read_handle + return None + raise ValueError("Couldn't open that blob for some reason. blob_hash: {}".format(blob.blob_hash)) + + def send_blob_info(self): + assert self.next_blob_to_send is not None, "need to have a next blob to send at this point" + self.write(json.dumps({ + 'blob_hash': self.next_blob_to_send.blob_hash, + 'blob_size': self.next_blob_to_send.length + })) + + def send_next_request(self): + if self.file_sender is not None: + # send the blob + return self.start_transfer() + elif self.blob_hashes_to_send: + # open the next blob to send + blob_hash = self.blob_hashes_to_send[0] + self.blob_hashes_to_send = self.blob_hashes_to_send[1:] + d = self.blob_manager.get_blob(blob_hash, True) + d.addCallback(self.open_blob_for_reading) + # send the server the next blob hash + length + d.addCallback(lambda _: self.send_blob_info()) + return d + else: + # close connection + self.transport.loseConnection() + + +class LBRYFileReflectorClientFactory(ClientFactory): + protocol = LBRYFileReflectorClient + + def __init__(self, blob_manager, stream_info_manager, stream_hash): + self.blob_manager = blob_manager + self.stream_info_manager = stream_info_manager + self.stream_hash = stream_hash + self.p = None + self.finished_deferred = defer.Deferred() + + def buildProtocol(self, addr): + p = self.protocol() + p.factory = self + self.p = p + return p \ No newline at end of file diff --git a/tests/lbrynet/lbrynet/reflector/server/__init__.py b/tests/lbrynet/lbrynet/reflector/server/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/lbrynet/lbrynet/reflector/server/server.py b/tests/lbrynet/lbrynet/reflector/server/server.py new file mode 100644 index 000000000..a8f36ae22 --- /dev/null +++ b/tests/lbrynet/lbrynet/reflector/server/server.py @@ -0,0 +1,132 @@ +import logging +from twisted.python import failure +from twisted.internet import error, defer +from twisted.internet.protocol import Protocol, ServerFactory +import json + +from lbrynet.core.utils import is_valid_blobhash + + +log = logging.getLogger(__name__) + + +class ReflectorServer(Protocol): + + def connectionMade(self): + peer_info = self.transport.getPeer() + self.peer = self.factory.peer_manager.get_peer(peer_info.host, peer_info.port) + self.blob_manager = self.factory.blob_manager + self.received_handshake = False + self.peer_version = None + self.receiving_blob = False + self.incoming_blob = None + self.blob_write = None + self.blob_finished_d = None + self.cancel_write = None + self.request_buff = "" + + def connectionLost(self, reason=failure.Failure(error.ConnectionDone())): + pass + + def dataReceived(self, data): + if self.receiving_blob is False: + self.request_buff += data + msg, extra_data = self._get_valid_response(self.request_buff) + if msg is not None: + self.request_buff = '' + d = self.handle_request(msg) + d.addCallbacks(self.send_response, self.handle_error) + if self.receiving_blob is True and len(extra_data) != 0: + self.blob_write(extra_data) + else: + self.blob_write(data) + + def _get_valid_response(self, response_msg): + extra_data = None + response = None + curr_pos = 0 + while 1: + next_close_paren = response_msg.find('}', curr_pos) + if next_close_paren != -1: + curr_pos = next_close_paren + 1 + try: + response = json.loads(response_msg[:curr_pos]) + except ValueError: + pass + else: + extra_data = response_msg[curr_pos:] + break + else: + break + return response, extra_data + + def handle_request(self, request_dict): + if self.received_handshake is False: + return self.handle_handshake(request_dict) + else: + return self.handle_normal_request(request_dict) + + def handle_handshake(self, request_dict): + if 'version' not in request_dict: + raise ValueError("Client should send version") + self.peer_version = int(request_dict['version']) + if self.peer_version != 0: + raise ValueError("I don't know that version!") + self.received_handshake = True + return defer.succeed({'version': 0}) + + def determine_blob_needed(self, blob): + if blob.is_validated(): + return {'send_blob': False} + else: + self.incoming_blob = blob + self.blob_finished_d, self.blob_write, self.cancel_write = blob.open_for_writing(self.peer) + return {'send_blob': True} + + def close_blob(self): + self.blob_finished_d = None + self.blob_write = None + self.cancel_write = None + self.incoming_blob = None + self.receiving_blob = False + + def handle_normal_request(self, request_dict): + if self.blob_write is None: + # we haven't opened a blob yet, meaning we must be waiting for the + # next message containing a blob hash and a length. this message + # should be it. if it's one we want, open the blob for writing, and + # return a nice response dict (in a Deferred) saying go ahead + if not 'blob_hash' in request_dict or not 'blob_size' in request_dict: + raise ValueError("Expected a blob hash and a blob size") + if not is_valid_blobhash(request_dict['blob_hash']): + raise ValueError("Got a bad blob hash: {}".format(request_dict['blob_hash'])) + d = self.blob_manager.get_blob( + request_dict['blob_hash'], + True, + int(request_dict['blob_size']) + ) + d.addCallback(self.determine_blob_needed) + else: + # we have a blob open already, so this message should have nothing + # important in it. to the deferred that fires when the blob is done, + # add a callback which returns a nice response dict saying to keep + # sending, and then return that deferred + self.receiving_blob = True + d = self.blob_finished_d + d.addCallback(lambda _: self.close_blob()) + d.addCallback(lambda _: {'received_blob': True}) + return d + + def send_response(self, response_dict): + self.transport.write(json.dumps(response_dict)) + + def handle_error(self, err): + pass + + +class ReflectorServerFactory(ServerFactory): + protocol = ReflectorServer + + def __init__(self, peer_manager, blob_manager): + self.peer_manager = peer_manager + self.blob_manager = blob_manager \ No newline at end of file From 871e6e6f64d0e95a69637a4a0252a96680b51ada Mon Sep 17 00:00:00 2001 From: Jack Date: Tue, 9 Aug 2016 17:53:34 -0400 Subject: [PATCH 16/81] remove files that didn't fix test --- tests/lbrynet/lbrynet/reflector/__init__.py | 0 .../lbrynet/reflector/client/__init__.py | 0 .../lbrynet/reflector/client/client.py | 243 ------------------ .../lbrynet/reflector/server/__init__.py | 0 .../lbrynet/reflector/server/server.py | 132 ---------- 5 files changed, 375 deletions(-) delete mode 100644 tests/lbrynet/lbrynet/reflector/__init__.py delete mode 100644 tests/lbrynet/lbrynet/reflector/client/__init__.py delete mode 100644 tests/lbrynet/lbrynet/reflector/client/client.py delete mode 100644 tests/lbrynet/lbrynet/reflector/server/__init__.py delete mode 100644 tests/lbrynet/lbrynet/reflector/server/server.py diff --git a/tests/lbrynet/lbrynet/reflector/__init__.py b/tests/lbrynet/lbrynet/reflector/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/lbrynet/lbrynet/reflector/client/__init__.py b/tests/lbrynet/lbrynet/reflector/client/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/lbrynet/lbrynet/reflector/client/client.py b/tests/lbrynet/lbrynet/reflector/client/client.py deleted file mode 100644 index ce2102f95..000000000 --- a/tests/lbrynet/lbrynet/reflector/client/client.py +++ /dev/null @@ -1,243 +0,0 @@ -""" -The reflector protocol (all dicts encoded in json): - -Client Handshake (sent once per connection, at the start of the connection): - -{ - 'version': 0, -} - - -Server Handshake (sent once per connection, after receiving the client handshake): - -{ - 'version': 0, -} - - -Client Info Request: - -{ - 'blob_hash': "", - 'blob_size': -} - - -Server Info Response (sent in response to Client Info Request): - -{ - 'send_blob': True|False -} - -If response is 'YES', client may send a Client Blob Request or a Client Info Request. -If response is 'NO', client may only send a Client Info Request - - -Client Blob Request: - -{} # Yes, this is an empty dictionary, in case something needs to go here in the future - # this blob data must match the info sent in the most recent Client Info Request - - -Server Blob Response (sent in response to Client Blob Request): -{ - 'received_blob': True -} - -Client may now send another Client Info Request - -""" -import json -import logging -from twisted.protocols.basic import FileSender -from twisted.internet.protocol import Protocol, ClientFactory -from twisted.internet import defer, error - - -log = logging.getLogger(__name__) - - -class IncompleteResponseError(Exception): - pass - - -class LBRYFileReflectorClient(Protocol): - - # Protocol stuff - - def connectionMade(self): - self.blob_manager = self.factory.blob_manager - self.response_buff = '' - self.outgoing_buff = '' - self.blob_hashes_to_send = [] - self.next_blob_to_send = None - self.blob_read_handle = None - self.received_handshake_response = False - self.protocol_version = None - self.file_sender = None - self.producer = None - self.streaming = False - d = self.get_blobs_to_send(self.factory.stream_info_manager, self.factory.stream_hash) - d.addCallback(lambda _: self.send_handshake()) - d.addErrback(lambda err: log.warning("An error occurred immediately: %s", err.getTraceback())) - - def dataReceived(self, data): - self.response_buff += data - try: - msg = self.parse_response(self.response_buff) - except IncompleteResponseError: - pass - else: - self.response_buff = '' - d = self.handle_response(msg) - d.addCallback(lambda _: self.send_next_request()) - d.addErrback(self.response_failure_handler) - - def connectionLost(self, reason): - if reason.check(error.ConnectionDone): - self.factory.finished_deferred.callback(True) - else: - self.factory.finished_deferred.callback(reason) - - # IConsumer stuff - - def registerProducer(self, producer, streaming): - self.producer = producer - self.streaming = streaming - if self.streaming is False: - from twisted.internet import reactor - reactor.callLater(0, self.producer.resumeProducing) - - def unregisterProducer(self): - self.producer = None - - def write(self, data): - self.transport.write(data) - if self.producer is not None and self.streaming is False: - from twisted.internet import reactor - reactor.callLater(0, self.producer.resumeProducing) - - def get_blobs_to_send(self, stream_info_manager, stream_hash): - d = stream_info_manager.get_blobs_for_stream(stream_hash) - - def set_blobs(blob_hashes): - for blob_hash, position, iv, length in blob_hashes: - if blob_hash is not None: - self.blob_hashes_to_send.append(blob_hash) - - d.addCallback(set_blobs) - - d.addCallback(lambda _: stream_info_manager.get_sd_blob_hashes_for_stream(stream_hash)) - - def set_sd_blobs(sd_blob_hashes): - for sd_blob_hash in sd_blob_hashes: - self.blob_hashes_to_send.append(sd_blob_hash) - - d.addCallback(set_sd_blobs) - return d - - def send_handshake(self): - self.write(json.dumps({'version': 0})) - - def parse_response(self, buff): - try: - return json.loads(buff) - except ValueError: - raise IncompleteResponseError() - - def response_failure_handler(self, err): - log.warning("An error occurred handling the response: %s", err.getTraceback()) - - def handle_response(self, response_dict): - if self.received_handshake_response is False: - return self.handle_handshake_response(response_dict) - else: - return self.handle_normal_response(response_dict) - - def set_not_uploading(self): - if self.next_blob_to_send is not None: - self.next_blob_to_send.close_read_handle(self.read_handle) - self.read_handle = None - self.next_blob_to_send = None - self.file_sender = None - return defer.succeed(None) - - def start_transfer(self): - self.write(json.dumps({})) - assert self.read_handle is not None, "self.read_handle was None when trying to start the transfer" - d = self.file_sender.beginFileTransfer(self.read_handle, self) - return d - - def handle_handshake_response(self, response_dict): - if 'version' not in response_dict: - raise ValueError("Need protocol version number!") - self.protocol_version = int(response_dict['version']) - if self.protocol_version != 0: - raise ValueError("I can't handle protocol version {}!".format(self.protocol_version)) - self.received_handshake_response = True - return defer.succeed(True) - - def handle_normal_response(self, response_dict): - if self.file_sender is None: # Expecting Server Info Response - if 'send_blob' not in response_dict: - raise ValueError("I don't know whether to send the blob or not!") - if response_dict['send_blob'] is True: - self.file_sender = FileSender() - return defer.succeed(True) - else: - return self.set_not_uploading() - else: # Expecting Server Blob Response - if 'received_blob' not in response_dict: - raise ValueError("I don't know if the blob made it to the intended destination!") - else: - return self.set_not_uploading() - - def open_blob_for_reading(self, blob): - if blob.is_validated(): - read_handle = blob.open_for_reading() - if read_handle is not None: - self.next_blob_to_send = blob - self.read_handle = read_handle - return None - raise ValueError("Couldn't open that blob for some reason. blob_hash: {}".format(blob.blob_hash)) - - def send_blob_info(self): - assert self.next_blob_to_send is not None, "need to have a next blob to send at this point" - self.write(json.dumps({ - 'blob_hash': self.next_blob_to_send.blob_hash, - 'blob_size': self.next_blob_to_send.length - })) - - def send_next_request(self): - if self.file_sender is not None: - # send the blob - return self.start_transfer() - elif self.blob_hashes_to_send: - # open the next blob to send - blob_hash = self.blob_hashes_to_send[0] - self.blob_hashes_to_send = self.blob_hashes_to_send[1:] - d = self.blob_manager.get_blob(blob_hash, True) - d.addCallback(self.open_blob_for_reading) - # send the server the next blob hash + length - d.addCallback(lambda _: self.send_blob_info()) - return d - else: - # close connection - self.transport.loseConnection() - - -class LBRYFileReflectorClientFactory(ClientFactory): - protocol = LBRYFileReflectorClient - - def __init__(self, blob_manager, stream_info_manager, stream_hash): - self.blob_manager = blob_manager - self.stream_info_manager = stream_info_manager - self.stream_hash = stream_hash - self.p = None - self.finished_deferred = defer.Deferred() - - def buildProtocol(self, addr): - p = self.protocol() - p.factory = self - self.p = p - return p \ No newline at end of file diff --git a/tests/lbrynet/lbrynet/reflector/server/__init__.py b/tests/lbrynet/lbrynet/reflector/server/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/lbrynet/lbrynet/reflector/server/server.py b/tests/lbrynet/lbrynet/reflector/server/server.py deleted file mode 100644 index a8f36ae22..000000000 --- a/tests/lbrynet/lbrynet/reflector/server/server.py +++ /dev/null @@ -1,132 +0,0 @@ -import logging -from twisted.python import failure -from twisted.internet import error, defer -from twisted.internet.protocol import Protocol, ServerFactory -import json - -from lbrynet.core.utils import is_valid_blobhash - - -log = logging.getLogger(__name__) - - -class ReflectorServer(Protocol): - - def connectionMade(self): - peer_info = self.transport.getPeer() - self.peer = self.factory.peer_manager.get_peer(peer_info.host, peer_info.port) - self.blob_manager = self.factory.blob_manager - self.received_handshake = False - self.peer_version = None - self.receiving_blob = False - self.incoming_blob = None - self.blob_write = None - self.blob_finished_d = None - self.cancel_write = None - self.request_buff = "" - - def connectionLost(self, reason=failure.Failure(error.ConnectionDone())): - pass - - def dataReceived(self, data): - if self.receiving_blob is False: - self.request_buff += data - msg, extra_data = self._get_valid_response(self.request_buff) - if msg is not None: - self.request_buff = '' - d = self.handle_request(msg) - d.addCallbacks(self.send_response, self.handle_error) - if self.receiving_blob is True and len(extra_data) != 0: - self.blob_write(extra_data) - else: - self.blob_write(data) - - def _get_valid_response(self, response_msg): - extra_data = None - response = None - curr_pos = 0 - while 1: - next_close_paren = response_msg.find('}', curr_pos) - if next_close_paren != -1: - curr_pos = next_close_paren + 1 - try: - response = json.loads(response_msg[:curr_pos]) - except ValueError: - pass - else: - extra_data = response_msg[curr_pos:] - break - else: - break - return response, extra_data - - def handle_request(self, request_dict): - if self.received_handshake is False: - return self.handle_handshake(request_dict) - else: - return self.handle_normal_request(request_dict) - - def handle_handshake(self, request_dict): - if 'version' not in request_dict: - raise ValueError("Client should send version") - self.peer_version = int(request_dict['version']) - if self.peer_version != 0: - raise ValueError("I don't know that version!") - self.received_handshake = True - return defer.succeed({'version': 0}) - - def determine_blob_needed(self, blob): - if blob.is_validated(): - return {'send_blob': False} - else: - self.incoming_blob = blob - self.blob_finished_d, self.blob_write, self.cancel_write = blob.open_for_writing(self.peer) - return {'send_blob': True} - - def close_blob(self): - self.blob_finished_d = None - self.blob_write = None - self.cancel_write = None - self.incoming_blob = None - self.receiving_blob = False - - def handle_normal_request(self, request_dict): - if self.blob_write is None: - # we haven't opened a blob yet, meaning we must be waiting for the - # next message containing a blob hash and a length. this message - # should be it. if it's one we want, open the blob for writing, and - # return a nice response dict (in a Deferred) saying go ahead - if not 'blob_hash' in request_dict or not 'blob_size' in request_dict: - raise ValueError("Expected a blob hash and a blob size") - if not is_valid_blobhash(request_dict['blob_hash']): - raise ValueError("Got a bad blob hash: {}".format(request_dict['blob_hash'])) - d = self.blob_manager.get_blob( - request_dict['blob_hash'], - True, - int(request_dict['blob_size']) - ) - d.addCallback(self.determine_blob_needed) - else: - # we have a blob open already, so this message should have nothing - # important in it. to the deferred that fires when the blob is done, - # add a callback which returns a nice response dict saying to keep - # sending, and then return that deferred - self.receiving_blob = True - d = self.blob_finished_d - d.addCallback(lambda _: self.close_blob()) - d.addCallback(lambda _: {'received_blob': True}) - return d - - def send_response(self, response_dict): - self.transport.write(json.dumps(response_dict)) - - def handle_error(self, err): - pass - - -class ReflectorServerFactory(ServerFactory): - protocol = ReflectorServer - - def __init__(self, peer_manager, blob_manager): - self.peer_manager = peer_manager - self.blob_manager = blob_manager \ No newline at end of file From b65570fb1605b1fb4303824693816ebde541dd32 Mon Sep 17 00:00:00 2001 From: Jack Date: Tue, 9 Aug 2016 19:53:13 -0400 Subject: [PATCH 17/81] drop connection on response longer than 100 bytes that can't' be decoded --- lbrynet/reflector/server/server.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lbrynet/reflector/server/server.py b/lbrynet/reflector/server/server.py index a8f36ae22..c1098dc40 100644 --- a/lbrynet/reflector/server/server.py +++ b/lbrynet/reflector/server/server.py @@ -45,19 +45,23 @@ class ReflectorServer(Protocol): extra_data = None response = None curr_pos = 0 + size_of_message = len(response_msg) while 1: next_close_paren = response_msg.find('}', curr_pos) if next_close_paren != -1: curr_pos = next_close_paren + 1 try: response = json.loads(response_msg[:curr_pos]) + failed_to_decode = False except ValueError: - pass + failed_to_decode = True else: extra_data = response_msg[curr_pos:] break else: break + if size_of_message > 100 and failed_to_decode: + raise Exception("error decoding response") return response, extra_data def handle_request(self, request_dict): @@ -121,7 +125,8 @@ class ReflectorServer(Protocol): self.transport.write(json.dumps(response_dict)) def handle_error(self, err): - pass + log.error(err.getTraceback()) + self.transport.loseConnection() class ReflectorServerFactory(ServerFactory): From 3fb48318049ef945cd725ebd4f4827bd5a9ec350 Mon Sep 17 00:00:00 2001 From: Jack Date: Wed, 10 Aug 2016 02:30:41 -0400 Subject: [PATCH 18/81] raise exception when curr_pos > 100 and json fails to decode --- lbrynet/reflector/server/server.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lbrynet/reflector/server/server.py b/lbrynet/reflector/server/server.py index c1098dc40..d2569b6aa 100644 --- a/lbrynet/reflector/server/server.py +++ b/lbrynet/reflector/server/server.py @@ -45,23 +45,22 @@ class ReflectorServer(Protocol): extra_data = None response = None curr_pos = 0 - size_of_message = len(response_msg) while 1: next_close_paren = response_msg.find('}', curr_pos) if next_close_paren != -1: curr_pos = next_close_paren + 1 try: response = json.loads(response_msg[:curr_pos]) - failed_to_decode = False except ValueError: - failed_to_decode = True + if curr_pos > 100: + raise Exception("error decoding response") + else: + pass else: extra_data = response_msg[curr_pos:] break else: break - if size_of_message > 100 and failed_to_decode: - raise Exception("error decoding response") return response, extra_data def handle_request(self, request_dict): From 67909724e7fba4a3bd5f81d891972fc1f39b0e9a Mon Sep 17 00:00:00 2001 From: Job Evers-Meltzer Date: Wed, 10 Aug 2016 07:44:41 -0500 Subject: [PATCH 19/81] fix up import paths --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 13 +++++++------ lbrynet/lbrynet_daemon/LBRYPublisher.py | 10 ++++++---- lbrynet/reflector/__init__.py | 2 ++ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index 6f0a78a90..d4e52b91e 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -57,8 +57,7 @@ from lbrynet.core.PTCWallet import PTCWallet from lbrynet.core.LBRYWallet import LBRYcrdWallet, LBRYumWallet from lbrynet.lbryfilemanager.LBRYFileManager import LBRYFileManager from lbrynet.lbryfile.LBRYFileMetadataManager import DBLBRYFileMetadataManager, TempLBRYFileMetadataManager -from lbrynet.reflector.server import ReflectorServerFactory -# from lbryum import LOG_PATH as lbryum_log +from lbrynet import reflector # TODO: this code snippet is everywhere. Make it go away @@ -686,13 +685,15 @@ class LBRYDaemon(jsonrpc.JSONRPC): def _start_reflector(self): if self.run_reflector_server: if self.reflector_port is not None: - reflector_factory = ReflectorServerFactory(self.session.peer_manager, self.session.blob_manager) + reflector_factory = reflector.ServerFactory( + self.session.peer_manager, + self.session.blob_manager + ) try: self.reflector_server_port = reactor.listenTCP(self.reflector_port, reflector_factory) except error.CannotListenError as e: - import traceback - log.error("Couldn't bind reflector to port %d. %s", self.reflector_port, traceback.format_exc()) - raise ValueError("%s lbrynet may already be running on your computer.", str(e)) + log.exception("Couldn't bind reflector to port %d", self.reflector_port) + raise ValueError("{} lbrynet may already be running on your computer.".format(e)) return defer.succeed(True) def _stop_reflector(self): diff --git a/lbrynet/lbrynet_daemon/LBRYPublisher.py b/lbrynet/lbrynet_daemon/LBRYPublisher.py index 970db91f6..2caf1d12b 100644 --- a/lbrynet/lbrynet_daemon/LBRYPublisher.py +++ b/lbrynet/lbrynet_daemon/LBRYPublisher.py @@ -12,7 +12,7 @@ from lbrynet.lbryfile.StreamDescriptor import publish_sd_blob from lbrynet.core.PaymentRateManager import PaymentRateManager from lbrynet.core.LBRYMetadata import Metadata, CURRENT_METADATA_VERSION from lbrynet.lbryfilemanager.LBRYFileDownloader import ManagedLBRYFileDownloader -from lbrynet.reflector.client import LBRYFileReflectorClientFactory +from lbrynet import reflector from lbrynet.conf import LOG_FILE_NAME, REFLECTOR_SERVERS from twisted.internet import threads, defer, reactor @@ -70,9 +70,11 @@ class Publisher(object): return d def start_reflector(self): - factory = LBRYFileReflectorClientFactory(self.session.blob_manager, - self.lbry_file_manager.stream_info_manager, - self.stream_hash) + factory = reflector.ClientFactory( + self.session.blob_manager, + self.lbry_file_manager.stream_info_manager, + self.stream_hash + ) reactor.connectTCP(self.reflector_server, self.reflector_port, factory) return factory.finished_deferred diff --git a/lbrynet/reflector/__init__.py b/lbrynet/reflector/__init__.py index e69de29bb..10e8292e7 100644 --- a/lbrynet/reflector/__init__.py +++ b/lbrynet/reflector/__init__.py @@ -0,0 +1,2 @@ +from lbrynet.reflector.server.server import ReflectorServerFactory as ServerFactory +from lbrynet.reflector.client.client import LBRYFileReflectorClientFactory as ClientFactory From d172d43ddf241348fc0417036ae17a331e1acee7 Mon Sep 17 00:00:00 2001 From: Job Evers-Meltzer Date: Wed, 10 Aug 2016 08:29:44 -0500 Subject: [PATCH 20/81] add reflector functional test to travis --- lbrynet/lbryfile/__init__.py | 2 + tests/{lbrynet => functional}/__init__.py | 0 tests/functional/test_reflector.py | 189 ++++++++++++++++++ tests/mocks.py | 164 +++++++++++++++ tests/{lbrynet/core => unit}/__init__.py | 0 .../core/server => unit/core}/__init__.py | 0 .../core/server}/__init__.py | 0 .../core/server/test_BlobRequestHandler.py | 0 .../core/test_LBRYExchangeRateManager.py | 0 .../core/test_LBRYMetadata.py | 0 tests/{lbrynet => unit}/core/test_utils.py | 0 tests/unit/lbrynet_daemon/__init__.py | 0 .../lbrynet_daemon/test_LBRYDaemon.py | 0 13 files changed, 355 insertions(+) rename tests/{lbrynet => functional}/__init__.py (100%) create mode 100644 tests/functional/test_reflector.py create mode 100644 tests/mocks.py rename tests/{lbrynet/core => unit}/__init__.py (100%) rename tests/{lbrynet/core/server => unit/core}/__init__.py (100%) rename tests/{lbrynet/lbrynet_daemon => unit/core/server}/__init__.py (100%) rename tests/{lbrynet => unit}/core/server/test_BlobRequestHandler.py (100%) rename tests/{lbrynet => unit}/core/test_LBRYExchangeRateManager.py (100%) rename tests/{lbrynet => unit}/core/test_LBRYMetadata.py (100%) rename tests/{lbrynet => unit}/core/test_utils.py (100%) create mode 100644 tests/unit/lbrynet_daemon/__init__.py rename tests/{lbrynet => unit}/lbrynet_daemon/test_LBRYDaemon.py (100%) diff --git a/lbrynet/lbryfile/__init__.py b/lbrynet/lbryfile/__init__.py index e69de29bb..8cd10066a 100644 --- a/lbrynet/lbryfile/__init__.py +++ b/lbrynet/lbryfile/__init__.py @@ -0,0 +1,2 @@ +from lbrynet.lbryfile.StreamDescriptor import get_sd_info +from lbrynet.lbryfile.StreamDescriptor import publish_sd_blob diff --git a/tests/lbrynet/__init__.py b/tests/functional/__init__.py similarity index 100% rename from tests/lbrynet/__init__.py rename to tests/functional/__init__.py diff --git a/tests/functional/test_reflector.py b/tests/functional/test_reflector.py new file mode 100644 index 000000000..d77557108 --- /dev/null +++ b/tests/functional/test_reflector.py @@ -0,0 +1,189 @@ +import os +import shutil + +from twisted.internet import defer, threads, error +from twisted.trial import unittest + +from lbrynet import reflector +from lbrynet import conf +from lbrynet import lbryfile +from lbrynet.core import BlobManager +from lbrynet.core import PeerManager +from lbrynet.core import RateLimiter +from lbrynet.core import Session +from lbrynet.core import StreamDescriptor +from lbrynet.lbryfile import LBRYFileMetadataManager +from lbrynet.lbryfile.client import LBRYFileOptions +from lbrynet.lbryfilemanager import LBRYFileCreator +from lbrynet.lbryfilemanager import LBRYFileManager + +from tests import mocks + + +class TestReflector(unittest.TestCase): + def setUp(self): + self.session = None + self.stream_info_manager = None + self.lbry_file_manager = None + self.server_blob_manager = None + self.reflector_port = None + self.addCleanup(self.take_down_env) + + def take_down_env(self): + d = defer.succeed(True) + if self.lbry_file_manager is not None: + d.addCallback(lambda _: self.lbry_file_manager.stop()) + if self.session is not None: + d.addCallback(lambda _: self.session.shut_down()) + if self.stream_info_manager is not None: + d.addCallback(lambda _: self.stream_info_manager.stop()) + if self.server_blob_manager is not None: + d.addCallback(lambda _: self.server_blob_manager.stop()) + if self.reflector_port is not None: + d.addCallback(lambda _: self.reflector_port.stopListening()) + + def delete_test_env(): + shutil.rmtree('client') + + d.addCallback(lambda _: threads.deferToThread(delete_test_env)) + return d + + def test_reflector(self): + wallet = mocks.Wallet() + peer_manager = PeerManager.PeerManager() + peer_finder = mocks.PeerFinder(5553, peer_manager, 2) + hash_announcer = mocks.Announcer() + rate_limiter = RateLimiter.DummyRateLimiter() + sd_identifier = StreamDescriptor.StreamDescriptorIdentifier() + + self.expected_blobs = [ + ( + 'dc4708f76a5e7af0f1cae0ee96b824e2ed9250c9346c093b' + '441f0a20d3607c17948b6fcfb4bc62020fe5286693d08586', + 2097152 + ), + ( + 'f4067522c1b49432a2a679512e3917144317caa1abba0c04' + '1e0cd2cf9f635d4cf127ce1824fa04189b63916174951f70', + 2097152 + ), + ( + '305486c434260484fcb2968ce0e963b72f81ba56c11b08b1' + 'af0789b55b44d78422600f9a38e3cf4f2e9569897e5646a9', + 1015056 + ), + ] + + db_dir = "client" + os.mkdir(db_dir) + + self.session = Session.LBRYSession( + conf.MIN_BLOB_DATA_PAYMENT_RATE, + db_dir=db_dir, + lbryid="abcd", + peer_finder=peer_finder, + hash_announcer=hash_announcer, + blob_dir=None, + peer_port=5553, + use_upnp=False, + rate_limiter=rate_limiter, + wallet=wallet + ) + + self.stream_info_manager = LBRYFileMetadataManager.TempLBRYFileMetadataManager() + + self.lbry_file_manager = LBRYFileManager.LBRYFileManager( + self.session, self.stream_info_manager, sd_identifier) + + self.server_blob_manager = BlobManager.TempBlobManager(hash_announcer) + + d = self.session.setup() + d.addCallback(lambda _: self.stream_info_manager.setup()) + d.addCallback(lambda _: LBRYFileOptions.add_lbry_file_to_sd_identifier(sd_identifier)) + d.addCallback(lambda _: self.lbry_file_manager.setup()) + d.addCallback(lambda _: self.server_blob_manager.setup()) + + def verify_equal(sd_info): + self.assertEqual(sd_info, mocks.create_stream_sd_file) + + def save_sd_blob_hash(sd_hash): + self.expected_blobs.append((sd_hash, 923)) + + def verify_stream_descriptor_file(stream_hash): + d = lbryfile.get_sd_info(self.lbry_file_manager.stream_info_manager, stream_hash, True) + d.addCallback(verify_equal) + d.addCallback( + lambda _: lbryfile.publish_sd_blob( + self.lbry_file_manager.stream_info_manager, + self.session.blob_manager, stream_hash + ) + ) + d.addCallback(save_sd_blob_hash) + d.addCallback(lambda _: stream_hash) + return d + + def iv_generator(): + iv = 0 + while 1: + iv += 1 + yield "%016d" % iv + + def create_stream(): + test_file = mocks.GenFile(5209343, b''.join([chr(i + 3) for i in xrange(0, 64, 6)])) + d = LBRYFileCreator.create_lbry_file( + self.session, + self.lbry_file_manager, + "test_file", + test_file, + key="0123456701234567", + iv_generator=iv_generator() + ) + return d + + def start_server(): + server_factory = reflector.ServerFactory(peer_manager, self.server_blob_manager) + from twisted.internet import reactor + port = 8943 + while self.reflector_port is None: + try: + self.reflector_port = reactor.listenTCP(port, server_factory) + except error.CannotListenError: + port += 1 + return defer.succeed(port) + + def send_to_server(port, stream_hash): + factory = reflector.ClientFactory( + self.session.blob_manager, + self.stream_info_manager, + stream_hash + ) + + from twisted.internet import reactor + reactor.connectTCP('localhost', port, factory) + return factory.finished_deferred + + def verify_blob_completed(blob, blob_size): + self.assertTrue(blob.is_validated()) + self.assertEqual(blob_size, blob.length) + + def verify_have_blob(blob_hash, blob_size): + d = self.server_blob_manager.get_blob(blob_hash, True) + d.addCallback(lambda blob: verify_blob_completed(blob, blob_size)) + return d + + def verify_data_on_reflector(): + check_blob_ds = [] + for blob_hash, blob_size in self.expected_blobs: + check_blob_ds.append(verify_have_blob(blob_hash, blob_size)) + return defer.DeferredList(check_blob_ds) + + def upload_to_reflector(stream_hash): + d = start_server() + d.addCallback(lambda port: send_to_server(port, stream_hash)) + d.addCallback(lambda _: verify_data_on_reflector()) + return d + + d.addCallback(lambda _: create_stream()) + d.addCallback(verify_stream_descriptor_file) + d.addCallback(upload_to_reflector) + return d diff --git a/tests/mocks.py b/tests/mocks.py new file mode 100644 index 000000000..6ea183195 --- /dev/null +++ b/tests/mocks.py @@ -0,0 +1,164 @@ +import io + +from Crypto.PublicKey import RSA +from twisted.internet import defer, threads, task, error + +from lbrynet.core import PTCWallet + + +class Node(object): + def __init__(self, *args, **kwargs): + pass + + def joinNetwork(self, *args): + pass + + def stop(self): + pass + + +class Wallet(object): + def __init__(self): + self.private_key = RSA.generate(1024) + self.encoded_public_key = self.private_key.publickey().exportKey() + + def start(self): + return defer.succeed(True) + + def stop(self): + return defer.succeed(True) + + def get_info_exchanger(self): + return PTCWallet.PointTraderKeyExchanger(self) + + def get_wallet_info_query_handler_factory(self): + return PTCWallet.PointTraderKeyQueryHandlerFactory(self) + + def reserve_points(self, *args): + return True + + def cancel_point_reservation(self, *args): + pass + + def send_points(self, *args): + return defer.succeed(True) + + def add_expected_payment(self, *args): + pass + + def get_balance(self): + return defer.succeed(1000) + + def set_public_key_for_peer(self, peer, public_key): + pass + + +class PeerFinder(object): + def __init__(self, start_port, peer_manager, num_peers): + self.start_port = start_port + self.peer_manager = peer_manager + self.num_peers = num_peers + self.count = 0 + + def find_peers_for_blob(self, *args): + peer_port = self.start_port + self.count + self.count += 1 + if self.count >= self.num_peers: + self.count = 0 + return defer.succeed([self.peer_manager.get_peer("127.0.0.1", peer_port)]) + + def run_manage_loop(self): + pass + + def stop(self): + pass + + +class Announcer(object): + def __init__(self, *args): + pass + + def add_supplier(self, supplier): + pass + + def immediate_announce(self, *args): + pass + + def run_manage_loop(self): + pass + + def stop(self): + pass + + +class GenFile(io.RawIOBase): + def __init__(self, size, pattern): + io.RawIOBase.__init__(self) + self.size = size + self.pattern = pattern + self.read_so_far = 0 + self.buff = b'' + self.last_offset = 0 + + def readable(self): + return True + + def writable(self): + return False + + def read(self, n=-1): + if n > -1: + bytes_to_read = min(n, self.size - self.read_so_far) + else: + bytes_to_read = self.size - self.read_so_far + output, self.buff = self.buff[:bytes_to_read], self.buff[bytes_to_read:] + bytes_to_read -= len(output) + while bytes_to_read > 0: + self.buff = self._generate_chunk() + new_output, self.buff = self.buff[:bytes_to_read], self.buff[bytes_to_read:] + bytes_to_read -= len(new_output) + output += new_output + self.read_so_far += len(output) + return output + + def readall(self): + return self.read() + + def _generate_chunk(self, n=2**10): + output = self.pattern[self.last_offset:self.last_offset + n] + n_left = n - len(output) + whole_patterns = n_left / len(self.pattern) + output += self.pattern * whole_patterns + self.last_offset = n - len(output) + output += self.pattern[:self.last_offset] + return output + + +create_stream_sd_file = { + 'stream_name': '746573745f66696c65', + 'blobs': [ + { + 'length': 2097152, + 'blob_num': 0, + 'blob_hash': 'dc4708f76a5e7af0f1cae0ee96b824e2ed9250c9346c093b441f0a20d3607c17948b6fcfb4bc62020fe5286693d08586', + 'iv': '30303030303030303030303030303031' + }, + { + 'length': 2097152, + 'blob_num': 1, + 'blob_hash': 'f4067522c1b49432a2a679512e3917144317caa1abba0c041e0cd2cf9f635d4cf127ce1824fa04189b63916174951f70', + 'iv': '30303030303030303030303030303032' + }, + { + 'length': 1015056, + 'blob_num': 2, + 'blob_hash': '305486c434260484fcb2968ce0e963b72f81ba56c11b08b1af0789b55b44d78422600f9a38e3cf4f2e9569897e5646a9', + 'iv': '30303030303030303030303030303033' + }, + {'length': 0, 'blob_num': 3, 'iv': '30303030303030303030303030303034'} + ], + 'stream_type': 'lbryfile', + 'key': '30313233343536373031323334353637', + 'suggested_file_name': '746573745f66696c65', + 'stream_hash': '6d27fbe10c86d81aacfb897c7a426d0a2214f5a299455a6d315c0f998c4b3545c2dc60906122d94653c23b1898229e3f' +} diff --git a/tests/lbrynet/core/__init__.py b/tests/unit/__init__.py similarity index 100% rename from tests/lbrynet/core/__init__.py rename to tests/unit/__init__.py diff --git a/tests/lbrynet/core/server/__init__.py b/tests/unit/core/__init__.py similarity index 100% rename from tests/lbrynet/core/server/__init__.py rename to tests/unit/core/__init__.py diff --git a/tests/lbrynet/lbrynet_daemon/__init__.py b/tests/unit/core/server/__init__.py similarity index 100% rename from tests/lbrynet/lbrynet_daemon/__init__.py rename to tests/unit/core/server/__init__.py diff --git a/tests/lbrynet/core/server/test_BlobRequestHandler.py b/tests/unit/core/server/test_BlobRequestHandler.py similarity index 100% rename from tests/lbrynet/core/server/test_BlobRequestHandler.py rename to tests/unit/core/server/test_BlobRequestHandler.py diff --git a/tests/lbrynet/core/test_LBRYExchangeRateManager.py b/tests/unit/core/test_LBRYExchangeRateManager.py similarity index 100% rename from tests/lbrynet/core/test_LBRYExchangeRateManager.py rename to tests/unit/core/test_LBRYExchangeRateManager.py diff --git a/tests/lbrynet/core/test_LBRYMetadata.py b/tests/unit/core/test_LBRYMetadata.py similarity index 100% rename from tests/lbrynet/core/test_LBRYMetadata.py rename to tests/unit/core/test_LBRYMetadata.py diff --git a/tests/lbrynet/core/test_utils.py b/tests/unit/core/test_utils.py similarity index 100% rename from tests/lbrynet/core/test_utils.py rename to tests/unit/core/test_utils.py diff --git a/tests/unit/lbrynet_daemon/__init__.py b/tests/unit/lbrynet_daemon/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/lbrynet/lbrynet_daemon/test_LBRYDaemon.py b/tests/unit/lbrynet_daemon/test_LBRYDaemon.py similarity index 100% rename from tests/lbrynet/lbrynet_daemon/test_LBRYDaemon.py rename to tests/unit/lbrynet_daemon/test_LBRYDaemon.py From d6f902653f90fb5405988ebb427a02a9f36a381f Mon Sep 17 00:00:00 2001 From: Job Evers-Meltzer Date: Wed, 10 Aug 2016 08:35:15 -0500 Subject: [PATCH 21/81] put import in alphabetical order --- tests/functional/test_reflector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_reflector.py b/tests/functional/test_reflector.py index d77557108..ee0aeb9e0 100644 --- a/tests/functional/test_reflector.py +++ b/tests/functional/test_reflector.py @@ -4,9 +4,9 @@ import shutil from twisted.internet import defer, threads, error from twisted.trial import unittest -from lbrynet import reflector from lbrynet import conf from lbrynet import lbryfile +from lbrynet import reflector from lbrynet.core import BlobManager from lbrynet.core import PeerManager from lbrynet.core import RateLimiter From 132aa569fe203077f9aec010f6372857ecfed383 Mon Sep 17 00:00:00 2001 From: Jack Date: Wed, 10 Aug 2016 16:51:46 -0400 Subject: [PATCH 22/81] move run_reflector_server into settings file and add debug lines --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 7 ++++--- lbrynet/lbrynet_daemon/LBRYPublisher.py | 1 + lbrynet/reflector/client/client.py | 3 +++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index d4e52b91e..2afcac7b2 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -175,9 +175,6 @@ class LBRYDaemon(jsonrpc.JSONRPC): self.first_run_after_update = False self.uploaded_temp_files = [] - # change this to enable reflector server - self.run_reflector_server = False - if os.name == "nt": from lbrynet.winhelpers.knownpaths import get_path, FOLDERID, UserHandle default_download_directory = get_path(FOLDERID.Downloads, UserHandle.current) @@ -216,6 +213,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): 'use_upnp': True, 'start_lbrycrdd': True, 'requested_first_run_credits': False, + 'run_reflector_server': False, 'cache_time': DEFAULT_CACHE_TIME, 'startup_scripts': [], 'last_version': {'lbrynet': lbrynet_version, 'lbryum': lbryum_version} @@ -275,6 +273,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): self.search_timeout = self.session_settings['search_timeout'] self.download_timeout = self.session_settings['download_timeout'] self.max_search_results = self.session_settings['max_search_results'] + self.run_reflector_server = self.session_settings['run_reflector_server'] #### # # Ignore the saved wallet type. Some users will have their wallet type @@ -684,6 +683,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): def _start_reflector(self): if self.run_reflector_server: + log.info("Starting reflector server") if self.reflector_port is not None: reflector_factory = reflector.ServerFactory( self.session.peer_manager, @@ -698,6 +698,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): def _stop_reflector(self): if self.run_reflector_server: + log.info("Stopping reflector server") try: if self.reflector_server_port is not None: self.reflector_server_port, p = None, self.reflector_server_port diff --git a/lbrynet/lbrynet_daemon/LBRYPublisher.py b/lbrynet/lbrynet_daemon/LBRYPublisher.py index 2caf1d12b..de9c558f3 100644 --- a/lbrynet/lbrynet_daemon/LBRYPublisher.py +++ b/lbrynet/lbrynet_daemon/LBRYPublisher.py @@ -70,6 +70,7 @@ class Publisher(object): return d def start_reflector(self): + log.info("Start reflector client") factory = reflector.ClientFactory( self.session.blob_manager, self.lbry_file_manager.stream_info_manager, diff --git a/lbrynet/reflector/client/client.py b/lbrynet/reflector/client/client.py index ce2102f95..5ce017114 100644 --- a/lbrynet/reflector/client/client.py +++ b/lbrynet/reflector/client/client.py @@ -118,10 +118,12 @@ class LBRYFileReflectorClient(Protocol): reactor.callLater(0, self.producer.resumeProducing) def get_blobs_to_send(self, stream_info_manager, stream_hash): + log.info("Get blobs to send to reflector") d = stream_info_manager.get_blobs_for_stream(stream_hash) def set_blobs(blob_hashes): for blob_hash, position, iv, length in blob_hashes: + log.info("Preparing to send %s", blob_hash) if blob_hash is not None: self.blob_hashes_to_send.append(blob_hash) @@ -202,6 +204,7 @@ class LBRYFileReflectorClient(Protocol): raise ValueError("Couldn't open that blob for some reason. blob_hash: {}".format(blob.blob_hash)) def send_blob_info(self): + log.info("Send blob info for %s", self.next_blob_to_send.blob_hash) assert self.next_blob_to_send is not None, "need to have a next blob to send at this point" self.write(json.dumps({ 'blob_hash': self.next_blob_to_send.blob_hash, From 1a42e4357316f4ec80da00dbeda11b1b3aab06e5 Mon Sep 17 00:00:00 2001 From: Job Evers-Meltzer Date: Wed, 10 Aug 2016 19:04:03 -0500 Subject: [PATCH 23/81] log all the things --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 1 + lbrynet/lbrynet_daemon/LBRYDaemonControl.py | 2 +- lbrynet/lbrynet_daemon/LBRYPublisher.py | 25 +++++++++++++---- lbrynet/reflector/client/client.py | 31 +++++++++++++++++++-- tests/functional/test_reflector.py | 15 +++++----- 5 files changed, 57 insertions(+), 17 deletions(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index 2afcac7b2..026793d43 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -691,6 +691,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): ) try: self.reflector_server_port = reactor.listenTCP(self.reflector_port, reflector_factory) + log.info('Started reflector on port %s', self.reflector_port) except error.CannotListenError as e: log.exception("Couldn't bind reflector to port %d", self.reflector_port) raise ValueError("{} lbrynet may already be running on your computer.".format(e)) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemonControl.py b/lbrynet/lbrynet_daemon/LBRYDaemonControl.py index 157087f0b..76cbe5152 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemonControl.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemonControl.py @@ -77,7 +77,7 @@ def start(): log_support.configure_file_handler(lbrynet_log) log_support.configure_loggly_handler() if args.logtoconsole: - log_support.configure_console() + log_support.configure_console(level='DEBUG') try: JSONRPCProxy.from_url(API_CONNECTION_STRING).is_running() diff --git a/lbrynet/lbrynet_daemon/LBRYPublisher.py b/lbrynet/lbrynet_daemon/LBRYPublisher.py index de9c558f3..7eb801cc3 100644 --- a/lbrynet/lbrynet_daemon/LBRYPublisher.py +++ b/lbrynet/lbrynet_daemon/LBRYPublisher.py @@ -47,9 +47,9 @@ class Publisher(object): self.metadata = {} def start(self, name, file_path, bid, metadata): - + log.info('Starting publish for %s', name) def _show_result(): - log.info("Published %s --> lbry://%s txid: %s", self.file_name, self.publish_name, self.txid) + log.info("Success! Published %s --> lbry://%s txid: %s", self.file_name, self.publish_name, self.txid) return defer.succeed(self.txid) self.publish_name = name @@ -99,10 +99,13 @@ class Publisher(object): return d def _create_sd_blob(self): - d = publish_sd_blob(self.lbry_file_manager.stream_info_manager, self.session.blob_manager, + log.debug('Creating stream descriptor blob') + d = publish_sd_blob(self.lbry_file_manager.stream_info_manager, + self.session.blob_manager, self.lbry_file.stream_hash) def set_sd_hash(sd_hash): + log.debug('stream descriptor hash: %s', sd_hash) if 'sources' not in self.metadata: self.metadata['sources'] = {} self.metadata['sources']['lbry_sd_hash'] = sd_hash @@ -111,23 +114,29 @@ class Publisher(object): return d def set_status(self): + log.debug('Setting status') d = self.lbry_file_manager.change_lbry_file_status(self.lbry_file, ManagedLBRYFileDownloader.STATUS_FINISHED) d.addCallback(lambda _: self.lbry_file.restore()) return d def _claim_name(self): - self.metadata['content-type'] = mimetypes.guess_type(os.path.join(self.lbry_file.download_directory, - self.lbry_file.file_name))[0] - self.metadata['ver'] = CURRENT_METADATA_VERSION + log.debug('Claiming name') + self._update_metadata() m = Metadata(self.metadata) def set_tx_hash(txid): + log.debug('Name claimed using txid: %s', txid) self.txid = txid d = self.wallet.claim_name(self.publish_name, self.bid_amount, m) d.addCallback(set_tx_hash) return d + def _update_metadata(self): + filename = os.path.join(self.lbry_file.download_directory, self.lbry_file.file_name) + self.metadata['content-type'] = get_content_type(filename) + self.metadata['ver'] = CURRENT_METADATA_VERSION + def _show_publish_error(self, err): log.info(err.getTraceback()) message = "An error occurred publishing %s to %s. Error: %s." @@ -140,3 +149,7 @@ class Publisher(object): log.error(message, str(self.file_name), str(self.publish_name), err.getTraceback()) return defer.fail(Exception("Publish failed")) + + +def get_content_type(filename): + return mimetypes.guess_type(filename)[0] diff --git a/lbrynet/reflector/client/client.py b/lbrynet/reflector/client/client.py index 5ce017114..d6631b8de 100644 --- a/lbrynet/reflector/client/client.py +++ b/lbrynet/reflector/client/client.py @@ -82,6 +82,7 @@ class LBRYFileReflectorClient(Protocol): d.addErrback(lambda err: log.warning("An error occurred immediately: %s", err.getTraceback())) def dataReceived(self, data): + log.debug('Recieved %s', data) self.response_buff += data try: msg = self.parse_response(self.response_buff) @@ -95,8 +96,10 @@ class LBRYFileReflectorClient(Protocol): def connectionLost(self, reason): if reason.check(error.ConnectionDone): + log.debug('Finished sending data via reflector') self.factory.finished_deferred.callback(True) else: + log.debug('reflector finished: %s', reason) self.factory.finished_deferred.callback(reason) # IConsumer stuff @@ -118,7 +121,7 @@ class LBRYFileReflectorClient(Protocol): reactor.callLater(0, self.producer.resumeProducing) def get_blobs_to_send(self, stream_info_manager, stream_hash): - log.info("Get blobs to send to reflector") + log.debug('Getting blobs from stream hash: %s', stream_hash) d = stream_info_manager.get_blobs_for_stream(stream_hash) def set_blobs(blob_hashes): @@ -139,6 +142,7 @@ class LBRYFileReflectorClient(Protocol): return d def send_handshake(self): + log.debug('Sending handshake') self.write(json.dumps({'version': 0})) def parse_response(self, buff): @@ -198,6 +202,7 @@ class LBRYFileReflectorClient(Protocol): if blob.is_validated(): read_handle = blob.open_for_reading() if read_handle is not None: + log.debug('Getting ready to send %s', blob.blob_hash) self.next_blob_to_send = blob self.read_handle = read_handle return None @@ -206,6 +211,7 @@ class LBRYFileReflectorClient(Protocol): def send_blob_info(self): log.info("Send blob info for %s", self.next_blob_to_send.blob_hash) assert self.next_blob_to_send is not None, "need to have a next blob to send at this point" + log.debug('sending blob info') self.write(json.dumps({ 'blob_hash': self.next_blob_to_send.blob_hash, 'blob_size': self.next_blob_to_send.length @@ -214,10 +220,12 @@ class LBRYFileReflectorClient(Protocol): def send_next_request(self): if self.file_sender is not None: # send the blob + log.debug('Sending the blob') return self.start_transfer() elif self.blob_hashes_to_send: # open the next blob to send blob_hash = self.blob_hashes_to_send[0] + log.debug('No current blob, sending the next one: %s', blob_hash) self.blob_hashes_to_send = self.blob_hashes_to_send[1:] d = self.blob_manager.get_blob(blob_hash, True) d.addCallback(self.open_blob_for_reading) @@ -226,7 +234,8 @@ class LBRYFileReflectorClient(Protocol): return d else: # close connection - self.transport.loseConnection() + log.debug('No more blob hashes, closing connection') + self.transport.closeConnection() class LBRYFileReflectorClientFactory(ClientFactory): @@ -243,4 +252,20 @@ class LBRYFileReflectorClientFactory(ClientFactory): p = self.protocol() p.factory = self self.p = p - return p \ No newline at end of file + return p + + def startFactory(self): + log.debug('Starting reflector factory') + ClientFactory.startFactory(self) + + def startedConnecting(self, connector): + log.debug('Started connecting') + + def clientConnectionLost(self, connector, reason): + """If we get disconnected, reconnect to server.""" + connector.connect() + + def clientConnectionFailed(self, connector, reason): + print("connection failed:", reason) + from twisted.internet import reactor + reactor.stop() diff --git a/tests/functional/test_reflector.py b/tests/functional/test_reflector.py index ee0aeb9e0..691653eae 100644 --- a/tests/functional/test_reflector.py +++ b/tests/functional/test_reflector.py @@ -104,7 +104,7 @@ class TestReflector(unittest.TestCase): d.addCallback(lambda _: self.server_blob_manager.setup()) def verify_equal(sd_info): - self.assertEqual(sd_info, mocks.create_stream_sd_file) + self.assertEqual(mocks.create_stream_sd_file, sd_info) def save_sd_blob_hash(sd_hash): self.expected_blobs.append((sd_hash, 923)) @@ -122,12 +122,6 @@ class TestReflector(unittest.TestCase): d.addCallback(lambda _: stream_hash) return d - def iv_generator(): - iv = 0 - while 1: - iv += 1 - yield "%016d" % iv - def create_stream(): test_file = mocks.GenFile(5209343, b''.join([chr(i + 3) for i in xrange(0, 64, 6)])) d = LBRYFileCreator.create_lbry_file( @@ -187,3 +181,10 @@ class TestReflector(unittest.TestCase): d.addCallback(verify_stream_descriptor_file) d.addCallback(upload_to_reflector) return d + + +def iv_generator(): + iv = 0 + while True: + iv += 1 + yield "%016d" % iv From 0995d864e8fa89fad2c073d9f47e0d6244b8694a Mon Sep 17 00:00:00 2001 From: Job Evers-Meltzer Date: Wed, 10 Aug 2016 19:09:20 -0500 Subject: [PATCH 24/81] connectTCP wants an ip address, not a url --- lbrynet/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lbrynet/conf.py b/lbrynet/conf.py index 1b956e092..79d82df03 100644 --- a/lbrynet/conf.py +++ b/lbrynet/conf.py @@ -28,7 +28,7 @@ SEARCH_SERVERS = ["http://lighthouse1.lbry.io:50005", "http://lighthouse2.lbry.io:50005", "http://lighthouse3.lbry.io:50005"] -REFLECTOR_SERVERS = [("http://reflector.lbry.io", 5566)] +REFLECTOR_SERVERS = [("reflector.lbry.io", 5566)] LOG_FILE_NAME = "lbrynet.log" LOG_POST_URL = "https://lbry.io/log-upload" From 35481a92f54f5d3bc6378ecf1940601943887e9b Mon Sep 17 00:00:00 2001 From: Job Evers-Meltzer Date: Wed, 10 Aug 2016 19:36:52 -0500 Subject: [PATCH 25/81] actually cleanup patched time.time --- lbrynet/reflector/client/client.py | 2 +- tests/unit/core/test_LBRYExchangeRateManager.py | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lbrynet/reflector/client/client.py b/lbrynet/reflector/client/client.py index d6631b8de..72d353085 100644 --- a/lbrynet/reflector/client/client.py +++ b/lbrynet/reflector/client/client.py @@ -235,7 +235,7 @@ class LBRYFileReflectorClient(Protocol): else: # close connection log.debug('No more blob hashes, closing connection') - self.transport.closeConnection() + self.transport.loseConnection() class LBRYFileReflectorClientFactory(ClientFactory): diff --git a/tests/unit/core/test_LBRYExchangeRateManager.py b/tests/unit/core/test_LBRYExchangeRateManager.py index 2a6457536..e07b49194 100644 --- a/tests/unit/core/test_LBRYExchangeRateManager.py +++ b/tests/unit/core/test_LBRYExchangeRateManager.py @@ -19,12 +19,10 @@ class LBRYFeeFormatTest(unittest.TestCase): class LBRYFeeTest(unittest.TestCase): def setUp(self): - self.patcher = mock.patch('time.time') - self.time = self.patcher.start() + patcher = mock.patch('time.time') + self.time = patcher.start() self.time.return_value = 0 - - def tearDown(self): - self.time.stop() + self.addCleanup(patcher.stop) def test_fee_converts_to_lbc(self): fee_dict = { @@ -35,4 +33,4 @@ class LBRYFeeTest(unittest.TestCase): } rates = {'BTCLBC': {'spot': 3.0, 'ts': 2}, 'USDBTC': {'spot': 2.0, 'ts': 3}} manager = LBRYExchangeRateManager.DummyExchangeRateManager(rates) - self.assertEqual(60.0, manager.to_lbc(fee_dict).amount) \ No newline at end of file + self.assertEqual(60.0, manager.to_lbc(fee_dict).amount) From 0a07c8f1312f2f628c78dba289c6afdebfef45d2 Mon Sep 17 00:00:00 2001 From: Job Evers-Meltzer Date: Wed, 10 Aug 2016 19:39:28 -0500 Subject: [PATCH 26/81] log on closed/lost connection --- lbrynet/reflector/client/client.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lbrynet/reflector/client/client.py b/lbrynet/reflector/client/client.py index 72d353085..0fe2c8ce1 100644 --- a/lbrynet/reflector/client/client.py +++ b/lbrynet/reflector/client/client.py @@ -263,9 +263,7 @@ class LBRYFileReflectorClientFactory(ClientFactory): def clientConnectionLost(self, connector, reason): """If we get disconnected, reconnect to server.""" - connector.connect() + log.debug("connection lost: %s", reason) def clientConnectionFailed(self, connector, reason): - print("connection failed:", reason) - from twisted.internet import reactor - reactor.stop() + log.debug("connection failed: %s", reason) From f83daa5a22a92fd0fd46bd347e5b95d7f14e3876 Mon Sep 17 00:00:00 2001 From: Job Evers-Meltzer Date: Thu, 11 Aug 2016 04:44:09 +0000 Subject: [PATCH 27/81] more logging on the server side --- lbrynet/__init__.py | 6 ----- lbrynet/core/log_support.py | 4 +++- lbrynet/lbrynet_daemon/LBRYDaemon.py | 4 ++-- lbrynet/lbrynet_daemon/LBRYDaemonControl.py | 4 ++-- lbrynet/reflector/server/server.py | 25 +++++++++++++++------ 5 files changed, 25 insertions(+), 18 deletions(-) diff --git a/lbrynet/__init__.py b/lbrynet/__init__.py index 6b754d3a2..9a7032852 100644 --- a/lbrynet/__init__.py +++ b/lbrynet/__init__.py @@ -1,8 +1,2 @@ -import logging - -log = logging.getLogger(__name__) -logging.getLogger(__name__).addHandler(logging.NullHandler()) -log.setLevel(logging.INFO) - __version__ = "0.3.17" version = tuple(__version__.split('.')) \ No newline at end of file diff --git a/lbrynet/core/log_support.py b/lbrynet/core/log_support.py index 50fb35e83..9644888df 100644 --- a/lbrynet/core/log_support.py +++ b/lbrynet/core/log_support.py @@ -58,8 +58,10 @@ def _log_decorator(fn): handler = fn(*args, **kwargs) if handler.name: remove_handlers(log, handler.name) + handler.setLevel(level) log.addHandler(handler) - log.setLevel(level) + if log.level > level: + log.setLevel(level) return helper diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index 026793d43..f9c0a7cbe 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -213,7 +213,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): 'use_upnp': True, 'start_lbrycrdd': True, 'requested_first_run_credits': False, - 'run_reflector_server': False, + 'run_reflector_server': True, 'cache_time': DEFAULT_CACHE_TIME, 'startup_scripts': [], 'last_version': {'lbrynet': lbrynet_version, 'lbryum': lbryum_version} @@ -683,8 +683,8 @@ class LBRYDaemon(jsonrpc.JSONRPC): def _start_reflector(self): if self.run_reflector_server: - log.info("Starting reflector server") if self.reflector_port is not None: + log.info("Starting reflector server listening to %s", self.reflector_port) reflector_factory = reflector.ServerFactory( self.session.peer_manager, self.session.blob_manager diff --git a/lbrynet/lbrynet_daemon/LBRYDaemonControl.py b/lbrynet/lbrynet_daemon/LBRYDaemonControl.py index 76cbe5152..4d68424c0 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemonControl.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemonControl.py @@ -74,8 +74,8 @@ def start(): args = parser.parse_args() log_support.disable_noisy_loggers() - log_support.configure_file_handler(lbrynet_log) - log_support.configure_loggly_handler() + log_support.configure_file_handler(lbrynet_log, level=10) + log_support.configure_loggly_handler(level=10) if args.logtoconsole: log_support.configure_console(level='DEBUG') diff --git a/lbrynet/reflector/server/server.py b/lbrynet/reflector/server/server.py index d2569b6aa..b8a1c6d68 100644 --- a/lbrynet/reflector/server/server.py +++ b/lbrynet/reflector/server/server.py @@ -11,9 +11,9 @@ log = logging.getLogger(__name__) class ReflectorServer(Protocol): - def connectionMade(self): peer_info = self.transport.getPeer() + log.debug('Connection made to %s', peer_info) self.peer = self.factory.peer_manager.get_peer(peer_info.host, peer_info.port) self.blob_manager = self.factory.blob_manager self.received_handshake = False @@ -29,17 +29,21 @@ class ReflectorServer(Protocol): pass def dataReceived(self, data): - if self.receiving_blob is False: + log.debug('Recieved data: %s', data) + if self.receiving_blob: + log.debug('Writing data to blob') + self.blob_write(data) + else: + log.debug('Not yet recieving blob, data needs further processing') self.request_buff += data msg, extra_data = self._get_valid_response(self.request_buff) - if msg is not None: + if msg: self.request_buff = '' d = self.handle_request(msg) d.addCallbacks(self.send_response, self.handle_error) - if self.receiving_blob is True and len(extra_data) != 0: + if self.receiving_blob and extra_data: + log.debug('Writing extra data to blog') self.blob_write(extra_data) - else: - self.blob_write(data) def _get_valid_response(self, response_msg): extra_data = None @@ -70,6 +74,7 @@ class ReflectorServer(Protocol): return self.handle_normal_request(request_dict) def handle_handshake(self, request_dict): + log.debug('Handling handshake') if 'version' not in request_dict: raise ValueError("Client should send version") self.peer_version = int(request_dict['version']) @@ -103,6 +108,7 @@ class ReflectorServer(Protocol): raise ValueError("Expected a blob hash and a blob size") if not is_valid_blobhash(request_dict['blob_hash']): raise ValueError("Got a bad blob hash: {}".format(request_dict['blob_hash'])) + log.debug('Recieved info for blob: %s', request_dict['blob_hash']) d = self.blob_manager.get_blob( request_dict['blob_hash'], True, @@ -114,6 +120,7 @@ class ReflectorServer(Protocol): # important in it. to the deferred that fires when the blob is done, # add a callback which returns a nice response dict saying to keep # sending, and then return that deferred + log.debug('blob is already open') self.receiving_blob = True d = self.blob_finished_d d.addCallback(lambda _: self.close_blob()) @@ -133,4 +140,8 @@ class ReflectorServerFactory(ServerFactory): def __init__(self, peer_manager, blob_manager): self.peer_manager = peer_manager - self.blob_manager = blob_manager \ No newline at end of file + self.blob_manager = blob_manager + + def buildProtocol(self, addr): + log.debug('Creating a protocol for %s', addr) + ServerFactory.buildProtocol(self, addr) From 52859825154384be6df36993bd0728cf9a9be86a Mon Sep 17 00:00:00 2001 From: Jack Date: Thu, 11 Aug 2016 01:06:51 -0400 Subject: [PATCH 28/81] resolve reflector ip --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 28 ++++++++++++++++++++++++- lbrynet/lbrynet_daemon/LBRYPublisher.py | 8 +++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index f9c0a7cbe..7a0be1eb1 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -48,7 +48,7 @@ from lbrynet.lbrynet_console.LBRYSettings import LBRYSettings from lbrynet.conf import MIN_BLOB_DATA_PAYMENT_RATE, DEFAULT_MAX_SEARCH_RESULTS, \ KNOWN_DHT_NODES, DEFAULT_MAX_KEY_FEE, DEFAULT_WALLET, \ DEFAULT_SEARCH_TIMEOUT, DEFAULT_CACHE_TIME, DEFAULT_UI_BRANCH, \ - LOG_POST_URL, LOG_FILE_NAME + LOG_POST_URL, LOG_FILE_NAME, REFLECTOR_SERVERS from lbrynet.conf import DEFAULT_SD_DOWNLOAD_TIMEOUT from lbrynet.conf import DEFAULT_TIMEOUT from lbrynet.core.StreamDescriptor import StreamDescriptorIdentifier, download_sd_blob, BlobStreamDescriptorReader @@ -1889,6 +1889,27 @@ class LBRYDaemon(jsonrpc.JSONRPC): m['fee'][currency]['address'] = address return m + def _reflect_if_possible(sd_hash, txid): + log.info("Trying to start reflector") + d = self._get_lbry_file('sd_hash', sd_hash, return_json=False) + d.addCallback(lambda r: False if not r else _start_reflector(r.stream_hash)) + d.addCallback(lambda _: txid) + return d + + def _start_reflector(stream_hash): + reflector_server = random.choice(REFLECTOR_SERVERS) + reflector_address, reflector_port = reflector_server[0], reflector_server[1] + log.info("Start reflector client") + factory = reflector.ClientFactory( + self.session.blob_manager, + self.lbry_file_manager.stream_info_manager, + stream_hash + ) + d = reactor.resolve(reflector_address) + d.addCallback(lambda ip: reactor.connectTCP(ip, reflector_port, factory)) + d.addCallback(lambda _: factory.finished_deferred) + return d + name = p['name'] log.info("Publish: ") @@ -1905,8 +1926,10 @@ class LBRYDaemon(jsonrpc.JSONRPC): try: metadata = Metadata(p['metadata']) make_lbry_file = False + sd_hash = metadata['sources']['lbry_sd_hash'] except AssertionError: make_lbry_file = True + sd_hash = None metadata = p['metadata'] file_path = p['file_path'] @@ -1930,6 +1953,9 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda meta: pub.start(name, file_path, bid, meta)) else: d.addCallback(lambda meta: self.session.wallet.claim_name(name, bid, meta)) + if sd_hash: + d.addCallback(lambda txid: _reflect_if_possible(sd_hash, txid)) + d.addCallback(lambda txid: self._add_to_pending_claims(name, txid)) d.addCallback(lambda r: self._render_response(r, OK_CODE)) diff --git a/lbrynet/lbrynet_daemon/LBRYPublisher.py b/lbrynet/lbrynet_daemon/LBRYPublisher.py index 7eb801cc3..5efbe66a9 100644 --- a/lbrynet/lbrynet_daemon/LBRYPublisher.py +++ b/lbrynet/lbrynet_daemon/LBRYPublisher.py @@ -70,14 +70,18 @@ class Publisher(object): return d def start_reflector(self): + reflector_server = random.choice(REFLECTOR_SERVERS) + reflector_address, reflector_port = reflector_server[0], reflector_server[1] log.info("Start reflector client") factory = reflector.ClientFactory( self.session.blob_manager, self.lbry_file_manager.stream_info_manager, self.stream_hash ) - reactor.connectTCP(self.reflector_server, self.reflector_port, factory) - return factory.finished_deferred + d = reactor.resolve(reflector_address) + d.addCallback(lambda ip: reactor.connectTCP(ip, reflector_port, factory)) + d.addCallback(lambda _: factory.finished_deferred) + return d def _check_file_path(self, file_path): def check_file_threaded(): From defbd73b4f24532ccceb98a7082ac05c648e1241 Mon Sep 17 00:00:00 2001 From: Jack Date: Thu, 11 Aug 2016 01:07:49 -0400 Subject: [PATCH 29/81] squelch very verbose dht error log --- lbrynet/dht/node.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lbrynet/dht/node.py b/lbrynet/dht/node.py index 8d61d74e2..053f46f27 100644 --- a/lbrynet/dht/node.py +++ b/lbrynet/dht/node.py @@ -240,9 +240,9 @@ class Node(object): known_nodes = {} def log_error(err, n): - log.error("error storing blob_hash %s at %s", binascii.hexlify(blob_hash), str(n)) - log.error(err.getErrorMessage()) - log.error(err.getTraceback()) + log.debug("error storing blob_hash %s at %s", binascii.hexlify(blob_hash), str(n)) + log.debug(err.getErrorMessage()) + log.debug(err.getTraceback()) def log_success(res): log.debug("Response to store request: %s", str(res)) From 80f27f2b296aee1af4f5f02d97a7aa7f1145f283 Mon Sep 17 00:00:00 2001 From: Job Evers-Meltzer Date: Thu, 11 Aug 2016 05:11:18 +0000 Subject: [PATCH 30/81] Revert "more logging on the server side" This reverts commit f83daa5a22a92fd0fd46bd347e5b95d7f14e3876. --- lbrynet/__init__.py | 6 +++++ lbrynet/core/log_support.py | 4 +--- lbrynet/lbrynet_daemon/LBRYDaemon.py | 4 ++-- lbrynet/lbrynet_daemon/LBRYDaemonControl.py | 4 ++-- lbrynet/reflector/server/server.py | 25 ++++++--------------- 5 files changed, 18 insertions(+), 25 deletions(-) diff --git a/lbrynet/__init__.py b/lbrynet/__init__.py index 9a7032852..6b754d3a2 100644 --- a/lbrynet/__init__.py +++ b/lbrynet/__init__.py @@ -1,2 +1,8 @@ +import logging + +log = logging.getLogger(__name__) +logging.getLogger(__name__).addHandler(logging.NullHandler()) +log.setLevel(logging.INFO) + __version__ = "0.3.17" version = tuple(__version__.split('.')) \ No newline at end of file diff --git a/lbrynet/core/log_support.py b/lbrynet/core/log_support.py index 9644888df..50fb35e83 100644 --- a/lbrynet/core/log_support.py +++ b/lbrynet/core/log_support.py @@ -58,10 +58,8 @@ def _log_decorator(fn): handler = fn(*args, **kwargs) if handler.name: remove_handlers(log, handler.name) - handler.setLevel(level) log.addHandler(handler) - if log.level > level: - log.setLevel(level) + log.setLevel(level) return helper diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index 7a0be1eb1..c39e16914 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -213,7 +213,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): 'use_upnp': True, 'start_lbrycrdd': True, 'requested_first_run_credits': False, - 'run_reflector_server': True, + 'run_reflector_server': False, 'cache_time': DEFAULT_CACHE_TIME, 'startup_scripts': [], 'last_version': {'lbrynet': lbrynet_version, 'lbryum': lbryum_version} @@ -683,8 +683,8 @@ class LBRYDaemon(jsonrpc.JSONRPC): def _start_reflector(self): if self.run_reflector_server: + log.info("Starting reflector server") if self.reflector_port is not None: - log.info("Starting reflector server listening to %s", self.reflector_port) reflector_factory = reflector.ServerFactory( self.session.peer_manager, self.session.blob_manager diff --git a/lbrynet/lbrynet_daemon/LBRYDaemonControl.py b/lbrynet/lbrynet_daemon/LBRYDaemonControl.py index 4d68424c0..76cbe5152 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemonControl.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemonControl.py @@ -74,8 +74,8 @@ def start(): args = parser.parse_args() log_support.disable_noisy_loggers() - log_support.configure_file_handler(lbrynet_log, level=10) - log_support.configure_loggly_handler(level=10) + log_support.configure_file_handler(lbrynet_log) + log_support.configure_loggly_handler() if args.logtoconsole: log_support.configure_console(level='DEBUG') diff --git a/lbrynet/reflector/server/server.py b/lbrynet/reflector/server/server.py index b8a1c6d68..d2569b6aa 100644 --- a/lbrynet/reflector/server/server.py +++ b/lbrynet/reflector/server/server.py @@ -11,9 +11,9 @@ log = logging.getLogger(__name__) class ReflectorServer(Protocol): + def connectionMade(self): peer_info = self.transport.getPeer() - log.debug('Connection made to %s', peer_info) self.peer = self.factory.peer_manager.get_peer(peer_info.host, peer_info.port) self.blob_manager = self.factory.blob_manager self.received_handshake = False @@ -29,21 +29,17 @@ class ReflectorServer(Protocol): pass def dataReceived(self, data): - log.debug('Recieved data: %s', data) - if self.receiving_blob: - log.debug('Writing data to blob') - self.blob_write(data) - else: - log.debug('Not yet recieving blob, data needs further processing') + if self.receiving_blob is False: self.request_buff += data msg, extra_data = self._get_valid_response(self.request_buff) - if msg: + if msg is not None: self.request_buff = '' d = self.handle_request(msg) d.addCallbacks(self.send_response, self.handle_error) - if self.receiving_blob and extra_data: - log.debug('Writing extra data to blog') + if self.receiving_blob is True and len(extra_data) != 0: self.blob_write(extra_data) + else: + self.blob_write(data) def _get_valid_response(self, response_msg): extra_data = None @@ -74,7 +70,6 @@ class ReflectorServer(Protocol): return self.handle_normal_request(request_dict) def handle_handshake(self, request_dict): - log.debug('Handling handshake') if 'version' not in request_dict: raise ValueError("Client should send version") self.peer_version = int(request_dict['version']) @@ -108,7 +103,6 @@ class ReflectorServer(Protocol): raise ValueError("Expected a blob hash and a blob size") if not is_valid_blobhash(request_dict['blob_hash']): raise ValueError("Got a bad blob hash: {}".format(request_dict['blob_hash'])) - log.debug('Recieved info for blob: %s', request_dict['blob_hash']) d = self.blob_manager.get_blob( request_dict['blob_hash'], True, @@ -120,7 +114,6 @@ class ReflectorServer(Protocol): # important in it. to the deferred that fires when the blob is done, # add a callback which returns a nice response dict saying to keep # sending, and then return that deferred - log.debug('blob is already open') self.receiving_blob = True d = self.blob_finished_d d.addCallback(lambda _: self.close_blob()) @@ -140,8 +133,4 @@ class ReflectorServerFactory(ServerFactory): def __init__(self, peer_manager, blob_manager): self.peer_manager = peer_manager - self.blob_manager = blob_manager - - def buildProtocol(self, addr): - log.debug('Creating a protocol for %s', addr) - ServerFactory.buildProtocol(self, addr) + self.blob_manager = blob_manager \ No newline at end of file From 623fda3087ad31ea9d8c72f82c1d2c909688d038 Mon Sep 17 00:00:00 2001 From: Job Evers-Meltzer Date: Thu, 11 Aug 2016 05:14:21 +0000 Subject: [PATCH 31/81] improve logging --- lbrynet/core/log_support.py | 10 ++++++++-- lbrynet/lbrynet_daemon/LBRYDaemonControl.py | 3 ++- lbrynet/reflector/server/server.py | 3 ++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lbrynet/core/log_support.py b/lbrynet/core/log_support.py index 50fb35e83..f393a3210 100644 --- a/lbrynet/core/log_support.py +++ b/lbrynet/core/log_support.py @@ -58,15 +58,21 @@ def _log_decorator(fn): handler = fn(*args, **kwargs) if handler.name: remove_handlers(log, handler.name) + handler.setLevel(level) log.addHandler(handler) - log.setLevel(level) + if log.level > level: + log.setLevel(level) return helper -def disable_noisy_loggers(): +def disable_third_party_loggers(): logging.getLogger('requests').setLevel(logging.WARNING) +def disable_noisy_loggers(): + logging.getLogger('lbrynet.dht').setLevel(logging.INFO) + + @_log_decorator def configure_console(**kwargs): """Convenience function to configure a logger that outputs to stdout""" diff --git a/lbrynet/lbrynet_daemon/LBRYDaemonControl.py b/lbrynet/lbrynet_daemon/LBRYDaemonControl.py index 76cbe5152..ce4b0f54a 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemonControl.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemonControl.py @@ -73,11 +73,12 @@ def start(): parser.set_defaults(branch=False, launchui=True, logtoconsole=False, quiet=False) args = parser.parse_args() - log_support.disable_noisy_loggers() log_support.configure_file_handler(lbrynet_log) log_support.configure_loggly_handler() if args.logtoconsole: log_support.configure_console(level='DEBUG') + log_support.disable_third_party_loggers() + log_support.disable_noisy_loggers() try: JSONRPCProxy.from_url(API_CONNECTION_STRING).is_running() diff --git a/lbrynet/reflector/server/server.py b/lbrynet/reflector/server/server.py index d2569b6aa..fc015cbf6 100644 --- a/lbrynet/reflector/server/server.py +++ b/lbrynet/reflector/server/server.py @@ -14,6 +14,7 @@ class ReflectorServer(Protocol): def connectionMade(self): peer_info = self.transport.getPeer() + log.debug('Connection made to %s', peer_info) self.peer = self.factory.peer_manager.get_peer(peer_info.host, peer_info.port) self.blob_manager = self.factory.blob_manager self.received_handshake = False @@ -45,7 +46,7 @@ class ReflectorServer(Protocol): extra_data = None response = None curr_pos = 0 - while 1: + while True: next_close_paren = response_msg.find('}', curr_pos) if next_close_paren != -1: curr_pos = next_close_paren + 1 From c7430f4ae9de7db916caac35dded71c31be0edec Mon Sep 17 00:00:00 2001 From: Job Evers-Meltzer Date: Thu, 11 Aug 2016 05:25:45 +0000 Subject: [PATCH 32/81] better more logging on the server side --- lbrynet/reflector/server/server.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/lbrynet/reflector/server/server.py b/lbrynet/reflector/server/server.py index fc015cbf6..c8fb36fa8 100644 --- a/lbrynet/reflector/server/server.py +++ b/lbrynet/reflector/server/server.py @@ -30,17 +30,20 @@ class ReflectorServer(Protocol): pass def dataReceived(self, data): - if self.receiving_blob is False: + if self.receiving_blob: + log.debug('Writing data to blob') + self.blob_write(data) + else: + log.debug('Not yet recieving blob, data needs further processing') self.request_buff += data msg, extra_data = self._get_valid_response(self.request_buff) if msg is not None: self.request_buff = '' d = self.handle_request(msg) d.addCallbacks(self.send_response, self.handle_error) - if self.receiving_blob is True and len(extra_data) != 0: + if self.receiving_blob and extra_data: + log.debug('Writing extra data to blog') self.blob_write(extra_data) - else: - self.blob_write(data) def _get_valid_response(self, response_msg): extra_data = None @@ -71,6 +74,7 @@ class ReflectorServer(Protocol): return self.handle_normal_request(request_dict) def handle_handshake(self, request_dict): + log.debug('Handling handshake') if 'version' not in request_dict: raise ValueError("Client should send version") self.peer_version = int(request_dict['version']) @@ -104,6 +108,7 @@ class ReflectorServer(Protocol): raise ValueError("Expected a blob hash and a blob size") if not is_valid_blobhash(request_dict['blob_hash']): raise ValueError("Got a bad blob hash: {}".format(request_dict['blob_hash'])) + log.debug('Recieved info for blob: %s', request_dict['blob_hash']) d = self.blob_manager.get_blob( request_dict['blob_hash'], True, @@ -115,6 +120,7 @@ class ReflectorServer(Protocol): # important in it. to the deferred that fires when the blob is done, # add a callback which returns a nice response dict saying to keep # sending, and then return that deferred + log.debug('blob is already open') self.receiving_blob = True d = self.blob_finished_d d.addCallback(lambda _: self.close_blob()) @@ -134,4 +140,8 @@ class ReflectorServerFactory(ServerFactory): def __init__(self, peer_manager, blob_manager): self.peer_manager = peer_manager - self.blob_manager = blob_manager \ No newline at end of file + self.blob_manager = blob_manager + + def buildProtocol(self, addr): + log.debug('Creating a protocol for %s', addr) + return ServerFactory.buildProtocol(self, addr) From f4ef92e653b88dcac6b3c6710d9eab4c786104f2 Mon Sep 17 00:00:00 2001 From: Job Evers-Meltzer Date: Thu, 11 Aug 2016 05:37:45 +0000 Subject: [PATCH 33/81] fixup logging some more --- lbrynet/__init__.py | 6 ------ lbrynet/core/log_support.py | 4 ++++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lbrynet/__init__.py b/lbrynet/__init__.py index 6b754d3a2..9a7032852 100644 --- a/lbrynet/__init__.py +++ b/lbrynet/__init__.py @@ -1,8 +1,2 @@ -import logging - -log = logging.getLogger(__name__) -logging.getLogger(__name__).addHandler(logging.NullHandler()) -log.setLevel(logging.INFO) - __version__ = "0.3.17" version = tuple(__version__.split('.')) \ No newline at end of file diff --git a/lbrynet/core/log_support.py b/lbrynet/core/log_support.py index f393a3210..e203c9c2a 100644 --- a/lbrynet/core/log_support.py +++ b/lbrynet/core/log_support.py @@ -55,6 +55,10 @@ def _log_decorator(fn): def helper(*args, **kwargs): log = kwargs.pop('log', logging.getLogger()) level = kwargs.pop('level', logging.INFO) + if not isinstance(level, int): + # despite the name, getLevelName returns + # the numeric level when passed a text level + level = logging.getLevelName(level) handler = fn(*args, **kwargs) if handler.name: remove_handlers(log, handler.name) From b8d23d0965a89db47eced6095a37da082004672e Mon Sep 17 00:00:00 2001 From: Jack Date: Thu, 11 Aug 2016 12:36:13 -0400 Subject: [PATCH 34/81] add announce_all_blobs_to_dht --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index c39e16914..a3c6046b5 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -2385,6 +2385,20 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda r: self._render_response(r, OK_CODE)) return d + def jsonrpc_announce_all_blobs_to_dht(self): + """ + Announce all blobs to the dht + + Args: + None + Returns: + + """ + + d = self.session.blob_manager.immediate_announce_all_blobs() + d.addCallback(lambda _: self._render_response("Announced", OK_CODE)) + return d + def get_lbrynet_version_from_github(): """Return the latest released version from github.""" From 2d5f8aed24dd3f429b346805fad059df9f84c22b Mon Sep 17 00:00:00 2001 From: Jack Date: Thu, 11 Aug 2016 12:38:10 -0400 Subject: [PATCH 35/81] whitespace --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index a3c6046b5..7df728521 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -2398,7 +2398,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d = self.session.blob_manager.immediate_announce_all_blobs() d.addCallback(lambda _: self._render_response("Announced", OK_CODE)) return d - + def get_lbrynet_version_from_github(): """Return the latest released version from github.""" From 6180e1a48b205cd45e4ef30ddede180d0f7fa70c Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Mon, 15 Aug 2016 19:19:26 -0400 Subject: [PATCH 36/81] Update to build and distribute >v0.3.12 --- setup_win32.py | 62 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 12 deletions(-) diff --git a/setup_win32.py b/setup_win32.py index 0e9b4982e..3100ab648 100644 --- a/setup_win32.py +++ b/setup_win32.py @@ -3,7 +3,9 @@ To create local builds and distributable .msi, run the following command: python setup_win32.py build bdist_msi """ +import opcode import os +import pkg_resources import sys from cx_Freeze import setup, Executable @@ -11,8 +13,13 @@ import requests.certs from lbrynet import __version__ +wordlist_path = pkg_resources.resource_filename('lbryum', 'wordlist') base_dir = os.path.abspath(os.path.dirname(__file__)) +# Allow virtualenv to find distutils of base python installation +distutils_path = os.path.join(os.path.dirname(opcode.__file__), 'distutils') + + def find_data_file(filename): if getattr(sys, 'frozen', False): # The application is frozen @@ -64,24 +71,56 @@ bdist_msi_options = { build_exe_options = { 'include_msvcr': True, 'includes': [], - 'packages': ['Crypto', 'twisted', 'miniupnpc', 'yapsy', 'seccure', - 'bitcoinrpc', 'txjsonrpc', 'requests', 'unqlite', 'lbryum', - 'jsonrpc', 'simplejson', 'appdirs', 'six', 'base58', 'googlefinance', - 'ecdsa', 'pbkdf2', 'qrcode', 'jsonrpclib', - 'os', 'cython', 'win32api', 'pkg_resources', 'zope.interface', - 'argparse', 'colorama', 'certifi' - # 'gmpy', 'wsgiref', 'slowaes', 'dnspython', 'protobuf', 'google', 'google.protobuf' + 'packages': ['cython', + 'twisted', + 'yapsy', + 'appdirs', + 'argparse', + 'base58', + 'colorama', + 'cx_Freeze', + 'dns', + 'ecdsa', + 'gmpy', + 'googlefinance', + 'jsonrpc', + 'jsonrpclib', + 'lbryum', + 'miniupnpc', + 'pbkdf2', + 'google.protobuf', + 'Crypto', + 'bitcoinrpc', + 'win32api', + 'qrcode', + 'requests', + 'seccure', + 'simplejson', + 'six', + 'aes', + 'txjsonrpc', + 'unqlite', + 'wsgiref', + 'zope.interface', + 'os', + 'pkg_resources' ], - 'excludes': ['collections.sys', 'collections._weakref', 'tkinter', 'tk', 'tcl' - 'zope.interface._zope_interface_coptimizations', 'matplotlib', 'numpy', 'pillow', 'pandas'], - 'include_files': [(requests.certs.where(), 'cacert.pem')], + 'excludes': ['distutils', 'collections.sys', 'collections._weakref', 'collections.abc', + 'Tkinter', 'tk', 'tcl', 'PyQt4' + 'zope.interface._zope_interface_coptimizations'], + 'include_files': [(distutils_path, 'distutils'), (requests.certs.where(), 'cacert.pem'), + (os.path.join(wordlist_path, 'chinese_simplified.txt'), os.path.join('wordlist', 'chinese_simplified.txt')), + (os.path.join(wordlist_path, 'english.txt'), os.path.join('wordlist', 'english.txt')), + (os.path.join(wordlist_path, 'japanese.txt'), os.path.join('wordlist', 'japanese.txt')), + (os.path.join(wordlist_path, 'portuguese.txt'), os.path.join('wordlist', 'portuguese.txt')), + (os.path.join(wordlist_path, 'spanish.txt'), os.path.join('wordlist', 'spanish.txt')) + ], 'namespace_packages': ['zope']} exe = Executable( script=os.path.join('lbrynet', 'lbrynet_daemon', 'LBRYDaemonControl.py'), # base='Win32GUI', icon=os.path.join('packaging', 'windows', 'icons', 'lbry256.ico'), - # icon=os.path.join('lbry-dark-icon.ico'), compress=True, shortcutName='lbrynet', shortcutDir='DesktopFolder', @@ -96,7 +135,6 @@ setup( url='lbry.io', author='', keywords='LBRY', - # entry_points={'console_scripts': console_scripts}, data_files=[ ('lbrynet/lbrynet_console/plugins', [ From e38f8076bc34038e10e5eb899672eacf2ee89190 Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Mon, 15 Aug 2016 19:40:32 -0400 Subject: [PATCH 37/81] Create site-package protobuf __init__ if needed --- setup_win32.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/setup_win32.py b/setup_win32.py index 3100ab648..0e615fca0 100644 --- a/setup_win32.py +++ b/setup_win32.py @@ -14,6 +14,14 @@ import requests.certs from lbrynet import __version__ wordlist_path = pkg_resources.resource_filename('lbryum', 'wordlist') + +# protobuf needs a blank __init__.py in the site-packages/google folder for cx_freeze to find +protobuf_path = os.path.dirname(os.path.dirname(pkg_resources.resource_filename('google.protobuf', '__init__.py'))) +protobuf_init = os.path.join(protobuf_path, '__init__.py') +if not os.path.isfile(protobuf_init): + with open(protobuf_init, 'w') as f: + f.write('') + base_dir = os.path.abspath(os.path.dirname(__file__)) # Allow virtualenv to find distutils of base python installation From 2af98d7d428c84f5cf6fb71161d9c92f445b6a70 Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Mon, 15 Aug 2016 20:48:27 -0400 Subject: [PATCH 38/81] Add loggly and request futures to includes --- setup_win32.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup_win32.py b/setup_win32.py index 0e615fca0..6c359327a 100644 --- a/setup_win32.py +++ b/setup_win32.py @@ -94,6 +94,7 @@ build_exe_options = { 'jsonrpc', 'jsonrpclib', 'lbryum', + 'loggly', 'miniupnpc', 'pbkdf2', 'google.protobuf', @@ -102,6 +103,7 @@ build_exe_options = { 'win32api', 'qrcode', 'requests', + 'requests_futures', 'seccure', 'simplejson', 'six', From e70842a38c3cac640a6b0919f01c2946d73d7032 Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Mon, 15 Aug 2016 23:09:46 -0400 Subject: [PATCH 39/81] Better fix for finding protobuf namespace --- setup_win32.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/setup_win32.py b/setup_win32.py index 6c359327a..3c150183d 100644 --- a/setup_win32.py +++ b/setup_win32.py @@ -15,13 +15,6 @@ from lbrynet import __version__ wordlist_path = pkg_resources.resource_filename('lbryum', 'wordlist') -# protobuf needs a blank __init__.py in the site-packages/google folder for cx_freeze to find -protobuf_path = os.path.dirname(os.path.dirname(pkg_resources.resource_filename('google.protobuf', '__init__.py'))) -protobuf_init = os.path.join(protobuf_path, '__init__.py') -if not os.path.isfile(protobuf_init): - with open(protobuf_init, 'w') as f: - f.write('') - base_dir = os.path.abspath(os.path.dirname(__file__)) # Allow virtualenv to find distutils of base python installation @@ -116,7 +109,7 @@ build_exe_options = { 'pkg_resources' ], 'excludes': ['distutils', 'collections.sys', 'collections._weakref', 'collections.abc', - 'Tkinter', 'tk', 'tcl', 'PyQt4' + 'Tkinter', 'tk', 'tcl', 'PyQt4', 'nose', 'mock' 'zope.interface._zope_interface_coptimizations'], 'include_files': [(distutils_path, 'distutils'), (requests.certs.where(), 'cacert.pem'), (os.path.join(wordlist_path, 'chinese_simplified.txt'), os.path.join('wordlist', 'chinese_simplified.txt')), @@ -125,7 +118,7 @@ build_exe_options = { (os.path.join(wordlist_path, 'portuguese.txt'), os.path.join('wordlist', 'portuguese.txt')), (os.path.join(wordlist_path, 'spanish.txt'), os.path.join('wordlist', 'spanish.txt')) ], - 'namespace_packages': ['zope']} + 'namespace_packages': ['zope', 'google']} exe = Executable( script=os.path.join('lbrynet', 'lbrynet_daemon', 'LBRYDaemonControl.py'), From 9e9cf63c907d41a60a5b2fc5283ad322b3bd8bf0 Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Tue, 16 Aug 2016 17:54:36 -0400 Subject: [PATCH 40/81] Add appveyor.yml build script --- appveyor.yml | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 appveyor.yml diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..60809ed19 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,61 @@ +version: 1.0.{build} + +init: +- ps: $env:Path += ";C:\MinGW\bin\" +- ps: gcc --version +- ps: mingw32-make --version +- ps: mkdir C:\temp +- ps: Invoke-WebRequest "https://pypi.python.org/packages/55/90/e987e28ed29b571f315afea7d317b6bf4a551e37386b344190cffec60e72/miniupnpc-1.9.tar.gz" -OutFile "C:\temp\miniupnpc-1.9.tar.gz" +- ps: cd C:\temp +- ps: 7z e miniupnpc-1.9.tar.gz +- ps: 7z x miniupnpc-1.9.tar +- ps: cd C:\temp\miniupnpc-1.9 +- ps: | + mingw32-make.exe -f Makefile.mingw + C:\Python27\python.exe C:\temp\miniupnpc-1.9\setupmingw32.py build --compiler=mingw32 + C:\Python27\python.exe C:\temp\miniupnpc-1.9\setupmingw32.py install +- ps: Invoke-WebRequest "https://bitbucket.org/david_amrhein/lbry/downloads/pywin32-220.1-cp27-cp27m-win32.whl" -OutFile "C:\temp\pywin32-220.1-cp27-cp27m-win32.whl" +- ps: Invoke-WebRequest "https://bitbucket.org/david_amrhein/lbry/downloads/gmpy-1.17-cp27-none-win32.whl" -OutFile "C:\temp\gmpy-1.17-cp27-none-win32.whl" +- ps: C:\Python27\Scripts\pip.exe install "C:\temp\gmpy-1.17-cp27-none-win32.whl" +- ps: C:\Python27\Scripts\pip.exe install "C:\temp\pywin32-220.1-cp27-cp27m-win32.whl" +- ps: C:\Python27\python.exe C:\Python27\Scripts\pywin32_postinstall.py -install +- ps: C:\Python27\Scripts\pip.exe install six==1.9.0 +- ps: C:\Python27\Scripts\pip.exe install requests==2.9.1 +- ps: C:\Python27\Scripts\pip.exe install zope.interface==4.1.3 +- ps: C:\Python27\Scripts\pip.exe install cx-freeze==4.3.3 +- ps: C:\Python27\Scripts\pip.exe install cython==0.24.1 +- ps: C:\Python27\Scripts\pip.exe install Twisted==16.0.0 +- ps: C:\Python27\Scripts\pip.exe install Yapsy==1.11.223 +- ps: C:\Python27\Scripts\pip.exe install appdirs==1.4.0 +- ps: C:\Python27\Scripts\pip.exe install argparse==1.2.1 +- ps: C:\Python27\Scripts\pip.exe install colorama==0.3.7 +- ps: C:\Python27\Scripts\pip.exe install dnspython==1.12.0 +- ps: C:\Python27\Scripts\pip.exe install ecdsa==0.13 +- ps: C:\Python27\Scripts\pip.exe install jsonrpc==1.2 +- ps: C:\Python27\Scripts\pip.exe install jsonrpclib==0.1.7 +- ps: C:\Python27\Scripts\pip.exe install loggly-python-handler==1.0.0 +- ps: C:\Python27\Scripts\pip.exe install pbkdf2==1.3 +- ps: C:\Python27\Scripts\pip.exe install protobuf==3.0.0b3 +- ps: C:\Python27\Scripts\pip.exe install pycrypto==2.6.1 +- ps: C:\Python27\Scripts\pip.exe install python-bitcoinrpc==0.1 +- ps: C:\Python27\Scripts\pip.exe install qrcode==5.2.2 +- ps: C:\Python27\Scripts\pip.exe install requests_futures==0.9.7 +- ps: C:\Python27\Scripts\pip.exe install seccure==0.3.1.3 +- ps: C:\Python27\Scripts\pip.exe install simplejson==3.8.2 +- ps: C:\Python27\Scripts\pip.exe install slowaes==0.1a1 +- ps: C:\Python27\Scripts\pip.exe install txJSON-RPC==0.3.1 +- ps: C:\Python27\Scripts\pip.exe install unqlite==0.5.3 +- ps: C:\Python27\Scripts\pip.exe install wsgiref==0.1.2 +- ps: C:\Python27\Scripts\pip.exe install base58==0.2.2 +- ps: C:\Python27\Scripts\pip.exe install googlefinance==0.7 +- ps: C:\Python27\Scripts\pip.exe install git+https://david_amrhein@bitbucket.org/david_amrhein/lbryum.git +- ps: cd C:\projects\lbry + +build_script: +- cmd: C:\Python27\python.exe setup_win32.py build bdist_msi + +artifacts: +- path: dist/*.msi + name: msi +- path: build/exe.win32-2.7/ + name: lbry-portable \ No newline at end of file From 690575193f5893fff4499e00bdbaaa86b6e1f301 Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Tue, 16 Aug 2016 18:01:18 -0400 Subject: [PATCH 41/81] Add gmpy windows wheel --- .../windows/libs/gmpy-1.17-cp27-none-win32.whl | Bin 0 -> 158318 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 packaging/windows/libs/gmpy-1.17-cp27-none-win32.whl diff --git a/packaging/windows/libs/gmpy-1.17-cp27-none-win32.whl b/packaging/windows/libs/gmpy-1.17-cp27-none-win32.whl new file mode 100644 index 0000000000000000000000000000000000000000..5d15f0efa8b8bd5b29940094a9d35d4709a0dbcc GIT binary patch literal 158318 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

G|?lH+Za5R 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%0AnKgR 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 z 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 I-OL025A=fr8UO$Q literal 0 HcmV?d00001 From b8ebb1faf103ca75004ebba20b14bac567ed1925 Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Tue, 16 Aug 2016 19:58:47 -0400 Subject: [PATCH 42/81] Convert to lbryum github repo --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 60809ed19..1318c9478 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -48,7 +48,7 @@ init: - ps: C:\Python27\Scripts\pip.exe install wsgiref==0.1.2 - ps: C:\Python27\Scripts\pip.exe install base58==0.2.2 - ps: C:\Python27\Scripts\pip.exe install googlefinance==0.7 -- ps: C:\Python27\Scripts\pip.exe install git+https://david_amrhein@bitbucket.org/david_amrhein/lbryum.git +- ps: C:\Python27\Scripts\pip.exe install git+https://github.com/DaveA50/lbryum.git - ps: cd C:\projects\lbry build_script: From 0c9e82ee6b5157f908d38456199c2fa36746909e Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Tue, 16 Aug 2016 20:04:11 -0400 Subject: [PATCH 43/81] Update appveyor to DaveA50 git repo links --- appveyor.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 1318c9478..729e394f8 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -14,11 +14,9 @@ init: mingw32-make.exe -f Makefile.mingw C:\Python27\python.exe C:\temp\miniupnpc-1.9\setupmingw32.py build --compiler=mingw32 C:\Python27\python.exe C:\temp\miniupnpc-1.9\setupmingw32.py install -- ps: Invoke-WebRequest "https://bitbucket.org/david_amrhein/lbry/downloads/pywin32-220.1-cp27-cp27m-win32.whl" -OutFile "C:\temp\pywin32-220.1-cp27-cp27m-win32.whl" -- ps: Invoke-WebRequest "https://bitbucket.org/david_amrhein/lbry/downloads/gmpy-1.17-cp27-none-win32.whl" -OutFile "C:\temp\gmpy-1.17-cp27-none-win32.whl" +- ps: Invoke-WebRequest "https://github.com/DaveA50/lbry/raw/master/packaging/windows/libs/gmpy-1.17-cp27-none-win32.whl" -OutFile "C:\temp\gmpy-1.17-cp27-none-win32.whl" - ps: C:\Python27\Scripts\pip.exe install "C:\temp\gmpy-1.17-cp27-none-win32.whl" -- ps: C:\Python27\Scripts\pip.exe install "C:\temp\pywin32-220.1-cp27-cp27m-win32.whl" -- ps: C:\Python27\python.exe C:\Python27\Scripts\pywin32_postinstall.py -install +- ps: C:\Python27\Scripts\pip.exe install pypiwin32==219 - ps: C:\Python27\Scripts\pip.exe install six==1.9.0 - ps: C:\Python27\Scripts\pip.exe install requests==2.9.1 - ps: C:\Python27\Scripts\pip.exe install zope.interface==4.1.3 From c524de9198df8a2c94e0cfaee831d18b2c0762c4 Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Wed, 17 Aug 2016 12:12:28 -0400 Subject: [PATCH 44/81] Update protobuf out of beta --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 729e394f8..f6c1a6ddf 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -33,7 +33,7 @@ init: - ps: C:\Python27\Scripts\pip.exe install jsonrpclib==0.1.7 - ps: C:\Python27\Scripts\pip.exe install loggly-python-handler==1.0.0 - ps: C:\Python27\Scripts\pip.exe install pbkdf2==1.3 -- ps: C:\Python27\Scripts\pip.exe install protobuf==3.0.0b3 +- ps: C:\Python27\Scripts\pip.exe install protobuf==3.0.0 - ps: C:\Python27\Scripts\pip.exe install pycrypto==2.6.1 - ps: C:\Python27\Scripts\pip.exe install python-bitcoinrpc==0.1 - ps: C:\Python27\Scripts\pip.exe install qrcode==5.2.2 From 726d85ebd0f54275ddac1827417a08d63d965525 Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Wed, 17 Aug 2016 12:13:47 -0400 Subject: [PATCH 45/81] Remove blindrepeater.yapsy-plugin from build --- setup_win32.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/setup_win32.py b/setup_win32.py index 3c150183d..c03574ae9 100644 --- a/setup_win32.py +++ b/setup_win32.py @@ -13,6 +13,7 @@ import requests.certs from lbrynet import __version__ +win_icon = os.path.join('packaging', 'windows', 'icons', 'lbry256.ico') wordlist_path = pkg_resources.resource_filename('lbryum', 'wordlist') base_dir = os.path.abspath(os.path.dirname(__file__)) @@ -52,7 +53,7 @@ shortcut_table = [ None, # Arguments None, # Description None, # Hotkey - os.path.join('lbry-dark-icon.ico'), # Icon + win_icon, # Icon None, # IconIndex None, # ShowCmd 'TARGETDIR', # WkDir @@ -123,7 +124,7 @@ build_exe_options = { exe = Executable( script=os.path.join('lbrynet', 'lbrynet_daemon', 'LBRYDaemonControl.py'), # base='Win32GUI', - icon=os.path.join('packaging', 'windows', 'icons', 'lbry256.ico'), + icon=win_icon, compress=True, shortcutName='lbrynet', shortcutDir='DesktopFolder', @@ -138,14 +139,7 @@ setup( url='lbry.io', author='', keywords='LBRY', - data_files=[ - ('lbrynet/lbrynet_console/plugins', - [ - os.path.join(base_dir, 'lbrynet', 'lbrynet_console', 'plugins', - 'blindrepeater.yapsy-plugin') - ] - ), - ], + data_files=[], options={'build_exe': build_exe_options, 'bdist_msi': bdist_msi_options}, executables=[exe], From 90b28b3dbd0e6c2825ea7d02e51e914a93e09eba Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Wed, 17 Aug 2016 14:15:55 -0400 Subject: [PATCH 46/81] Pass WindowsError when moving uploaded file --- lbrynet/lbrynet_daemon/LBRYDaemonServer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemonServer.py b/lbrynet/lbrynet_daemon/LBRYDaemonServer.py index dc345bff5..125aba714 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemonServer.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemonServer.py @@ -375,7 +375,10 @@ class LBRYFileUpload(resource.Resource): # Move to a new temporary dir and restore the original file name newdirpath = tempfile.mkdtemp() newpath = os.path.join(newdirpath, origfilename) - shutil.move(uploaded_file.name, newpath) + try: + shutil.move(uploaded_file.name, newpath) + except WindowsError: + pass self._api.uploaded_temp_files.append(newpath) return json.dumps(newpath) From 30b04ed0c295294ee8b6801a12e1c29fe8a019bc Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Wed, 17 Aug 2016 14:25:02 -0400 Subject: [PATCH 47/81] Change naming conventions --- setup_win32.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/setup_win32.py b/setup_win32.py index c03574ae9..462098931 100644 --- a/setup_win32.py +++ b/setup_win32.py @@ -49,7 +49,7 @@ shortcut_table = [ 'DesktopFolder', # Directory 'lbrynet', # Name 'TARGETDIR', # Component - '[TARGETDIR]\lbrynet.exe', # Target + '[TARGETDIR]\LBRY.exe', # Target None, # Arguments None, # Description None, # Hotkey @@ -66,7 +66,7 @@ msi_data = {'Shortcut': shortcut_table} bdist_msi_options = { # 'upgrade_code': '{66620F3A-DC3A-11E2-B341-002219E9B01F}', 'add_to_path': False, - 'initial_target_dir': r'[LocalAppDataFolder]\lbrynet', + 'initial_target_dir': r'[LocalAppDataFolder]\LBRY', 'data': msi_data, } @@ -126,18 +126,18 @@ exe = Executable( # base='Win32GUI', icon=win_icon, compress=True, - shortcutName='lbrynet', + shortcutName='LBRY', shortcutDir='DesktopFolder', - targetName='lbrynet.exe' + targetName='LBRY.exe' # targetDir="LocalAppDataFolder" ) setup( - name='lbrynet', + name='LBRY', version=__version__, description='A decentralized media library and marketplace', url='lbry.io', - author='', + author='LBRY, Inc.', keywords='LBRY', data_files=[], options={'build_exe': build_exe_options, From ece218785ca8892b7d415db92649ba0006ee67c0 Mon Sep 17 00:00:00 2001 From: Jack Date: Wed, 17 Aug 2016 20:20:03 -0400 Subject: [PATCH 48/81] reduce logging to sub fire hazard levels --- lbrynet/core/log_support.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lbrynet/core/log_support.py b/lbrynet/core/log_support.py index cc6bba682..54a5a03a1 100644 --- a/lbrynet/core/log_support.py +++ b/lbrynet/core/log_support.py @@ -77,6 +77,9 @@ def disable_third_party_loggers(): def disable_noisy_loggers(): logging.getLogger('lbrynet.dht').setLevel(logging.INFO) + logging.getLogger('lbrynet.core.client.ConnectionManager').setLevel(logging.INFO) + logging.getLogger('lbrynet.core.client.BlobRequester').setLevel(logging.INFO) + logging.getLogger('lbrynet.core.client.ClientProtocol').setLevel(logging.INFO) @_log_decorator From edbda1696e3f97d96cf7feb9a396925f8dabe91e Mon Sep 17 00:00:00 2001 From: Jack Date: Wed, 17 Aug 2016 20:24:48 -0400 Subject: [PATCH 49/81] logging fire hazard --- lbrynet/core/log_support.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lbrynet/core/log_support.py b/lbrynet/core/log_support.py index cc6bba682..54a5a03a1 100644 --- a/lbrynet/core/log_support.py +++ b/lbrynet/core/log_support.py @@ -77,6 +77,9 @@ def disable_third_party_loggers(): def disable_noisy_loggers(): logging.getLogger('lbrynet.dht').setLevel(logging.INFO) + logging.getLogger('lbrynet.core.client.ConnectionManager').setLevel(logging.INFO) + logging.getLogger('lbrynet.core.client.BlobRequester').setLevel(logging.INFO) + logging.getLogger('lbrynet.core.client.ClientProtocol').setLevel(logging.INFO) @_log_decorator From a32b6ebf82a9510b84d0dd1353f578e547c6c1f2 Mon Sep 17 00:00:00 2001 From: Jack Date: Wed, 17 Aug 2016 21:33:41 -0400 Subject: [PATCH 50/81] add reflect jsonrpc command --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 39 ++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index fc4c09c17..225881d50 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -1338,6 +1338,29 @@ class LBRYDaemon(jsonrpc.JSONRPC): d = defer.DeferredList([self._get_lbry_file('sd_hash', l.sd_hash) for l in self.lbry_file_manager.lbry_files]) return d + def _reflect(self, lbry_file): + if not lbry_file: + return defer.fail(Exception("no lbry file given to reflect")) + + sd_hash = lbry_file.sd_hash + stream_hash = lbry_file.stream_hash + + if sd_hash is None or stream_hash is None: + return defer.fail(Exception("unpopulated lbry file fields")) + + reflector_server = random.choice(REFLECTOR_SERVERS) + reflector_address, reflector_port = reflector_server[0], reflector_server[1] + log.info("Start reflector client") + factory = reflector.ClientFactory( + self.session.blob_manager, + self.lbry_file_manager.stream_info_manager, + stream_hash + ) + d = reactor.resolve(reflector_address) + d.addCallback(lambda ip: reactor.connectTCP(ip, reflector_port, factory)) + d.addCallback(lambda _: factory.finished_deferred) + return d + def _log_to_slack(self, msg): URL = "https://hooks.slack.com/services/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" msg = platform.platform() + ": " + base58.b58encode(self.lbryid)[:20] + ", " + msg @@ -2422,6 +2445,22 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda _: self._render_response("Announced", OK_CODE)) return d + def jsonrpc_reflect(self, p): + """ + Reflect a stream + + Args: + sd_hash + Returns: + True or traceback + """ + + sd_hash = p['sd_hash'] + d = self._get_lbry_file('sd_hash', sd_hash, return_json=False) + d.addCallback(self._reflect) + d.addCallbacks(lambda _: self._render_response(True, OK_CODE), lambda err: self._render_response(err.getTraceback(), OK_CODE)) + return d + def get_lbrynet_version_from_github(): """Return the latest released version from github.""" From ed2d2c6f864e478cf6a999876d75bbd7ee38edce Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Thu, 18 Aug 2016 00:13:31 -0400 Subject: [PATCH 51/81] Ignore msi_data shortcuts, which didn't work anyway --- setup_win32.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/setup_win32.py b/setup_win32.py index 462098931..1e6b48387 100644 --- a/setup_win32.py +++ b/setup_win32.py @@ -44,30 +44,30 @@ console_scripts = ['lbrynet-stdin-uploader = lbrynet.lbrynet_console.LBRYStdinUp 'stop-lbrynet-daemon = lbrynet.lbrynet_daemon.LBRYDaemonControl:stop', 'lbrynet-cli = lbrynet.lbrynet_daemon.LBRYDaemonCLI:main'] -shortcut_table = [ - ('DesktopShortcut', # Shortcut - 'DesktopFolder', # Directory - 'lbrynet', # Name - 'TARGETDIR', # Component - '[TARGETDIR]\LBRY.exe', # Target - None, # Arguments - None, # Description - None, # Hotkey - win_icon, # Icon - None, # IconIndex - None, # ShowCmd - 'TARGETDIR', # WkDir - ), - ] - -# Now create the table dictionary -msi_data = {'Shortcut': shortcut_table} +# shortcut_table = [ +# ('DesktopShortcut', # Shortcut +# 'DesktopFolder', # Directory +# 'LBRY 1', # Name +# 'TARGETDIR', # Component +# '[TARGETDIR]\LBRY.exe', # Target +# None, # Arguments +# None, # Description +# None, # Hotkey +# win_icon, # Icon +# None, # IconIndex +# None, # ShowCmd +# 'TARGETDIR', # WkDir +# ), +# ] +# +# # Now create the table dictionary +# msi_data = {'Shortcut': shortcut_table} bdist_msi_options = { # 'upgrade_code': '{66620F3A-DC3A-11E2-B341-002219E9B01F}', 'add_to_path': False, 'initial_target_dir': r'[LocalAppDataFolder]\LBRY', - 'data': msi_data, + # 'data': msi_data, } build_exe_options = { From 73dbe1de37ad3945786f10fe2894789e47b32fc6 Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Thu, 18 Aug 2016 01:25:48 -0400 Subject: [PATCH 52/81] Fix relative pathing for cacert for win builds --- lbrynet/lbrynet_daemon/LBRYDaemonControl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemonControl.py b/lbrynet/lbrynet_daemon/LBRYDaemonControl.py index 985ba4ff9..10b5b5d0b 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemonControl.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemonControl.py @@ -32,7 +32,7 @@ log = logging.getLogger(__name__) REMOTE_SERVER = "www.google.com" if getattr(sys, 'frozen', False) and os.name == "nt": - os.environ["REQUESTS_CA_BUNDLE"] = os.path.join(os.getcwd(), "cacert.pem") + os.environ["REQUESTS_CA_BUNDLE"] = os.path.join(os.path.dirname(sys.executable), "cacert.pem") def test_internet_connection(): From 702698d8aa4eec064a24a55bf86c374ac2d438c4 Mon Sep 17 00:00:00 2001 From: Jack Date: Thu, 18 Aug 2016 05:25:23 -0400 Subject: [PATCH 53/81] stop heartbeat looping call on shutdown --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index 225881d50..80e603d97 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -833,6 +833,8 @@ class LBRYDaemon(jsonrpc.JSONRPC): self.lbry_ui_manager.update_checker.stop() if self.pending_claim_checker.running: self.pending_claim_checker.stop() + if self.send_heartbeat.running: + self.send_heartbeat.stop() self._clean_up_temp_files() From adc2eab6dac939b60938a9dc4e69421e22e80e11 Mon Sep 17 00:00:00 2001 From: Jack Date: Thu, 18 Aug 2016 05:36:01 -0400 Subject: [PATCH 54/81] log request dict --- lbrynet/core/client/ClientProtocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lbrynet/core/client/ClientProtocol.py b/lbrynet/core/client/ClientProtocol.py index aa4381e3d..31aa86b74 100644 --- a/lbrynet/core/client/ClientProtocol.py +++ b/lbrynet/core/client/ClientProtocol.py @@ -73,7 +73,7 @@ class ClientProtocol(Protocol): return defer.fail(failure.Failure(ValueError("There is already a request for that response active"))) self._next_request.update(request.request_dict) d = defer.Deferred() - log.debug("Adding a request. Request: %s", str(request)) + log.debug("Adding a request. Request: %s", str(request.request_dict)) self._response_deferreds[request.response_identifier] = d return d From f5508fcdf1b664bb5bdba4e9b2c14e70d5d27613 Mon Sep 17 00:00:00 2001 From: Jack Date: Thu, 18 Aug 2016 05:36:17 -0400 Subject: [PATCH 55/81] typo --- lbrynet/reflector/server/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lbrynet/reflector/server/server.py b/lbrynet/reflector/server/server.py index c8fb36fa8..6d309cafa 100644 --- a/lbrynet/reflector/server/server.py +++ b/lbrynet/reflector/server/server.py @@ -42,7 +42,7 @@ class ReflectorServer(Protocol): d = self.handle_request(msg) d.addCallbacks(self.send_response, self.handle_error) if self.receiving_blob and extra_data: - log.debug('Writing extra data to blog') + log.debug('Writing extra data to blob') self.blob_write(extra_data) def _get_valid_response(self, response_msg): From c046cd02d5a29f128946c72b3c3d92707ff16aa7 Mon Sep 17 00:00:00 2001 From: Jack Date: Thu, 18 Aug 2016 05:58:13 -0400 Subject: [PATCH 56/81] call blob_completed after receiving blob --- lbrynet/core/server/BlobAvailabilityHandler.py | 1 + lbrynet/reflector/server/server.py | 1 + 2 files changed, 2 insertions(+) diff --git a/lbrynet/core/server/BlobAvailabilityHandler.py b/lbrynet/core/server/BlobAvailabilityHandler.py index 5203f72d2..a5d550bdf 100644 --- a/lbrynet/core/server/BlobAvailabilityHandler.py +++ b/lbrynet/core/server/BlobAvailabilityHandler.py @@ -44,6 +44,7 @@ class BlobAvailabilityHandler(object): d = self._get_available_blobs(queries[self.query_identifiers[0]]) def set_field(available_blobs): + log.debug("available blobs: %s", str(available_blobs)) return {'available_blobs': available_blobs} d.addCallback(set_field) diff --git a/lbrynet/reflector/server/server.py b/lbrynet/reflector/server/server.py index 6d309cafa..7ea759f97 100644 --- a/lbrynet/reflector/server/server.py +++ b/lbrynet/reflector/server/server.py @@ -89,6 +89,7 @@ class ReflectorServer(Protocol): else: self.incoming_blob = blob self.blob_finished_d, self.blob_write, self.cancel_write = blob.open_for_writing(self.peer) + self.blob_finished_d.addCallback(lambda _: self.blob_manager.blob_completed(blob)) return {'send_blob': True} def close_blob(self): From 8e7b8af180dc3153df3e5fa4c21e1fb5c1e8dd4b Mon Sep 17 00:00:00 2001 From: Jack Date: Thu, 18 Aug 2016 06:23:27 -0400 Subject: [PATCH 57/81] call blob_completed when is_validated is true --- lbrynet/reflector/server/server.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lbrynet/reflector/server/server.py b/lbrynet/reflector/server/server.py index 7ea759f97..1728a8ed1 100644 --- a/lbrynet/reflector/server/server.py +++ b/lbrynet/reflector/server/server.py @@ -85,11 +85,12 @@ class ReflectorServer(Protocol): def determine_blob_needed(self, blob): if blob.is_validated(): - return {'send_blob': False} + d = self.blob_manager.blob_completed(blob) + d.addCallback(lambda _: {'send_blob': False}) + return d else: self.incoming_blob = blob self.blob_finished_d, self.blob_write, self.cancel_write = blob.open_for_writing(self.peer) - self.blob_finished_d.addCallback(lambda _: self.blob_manager.blob_completed(blob)) return {'send_blob': True} def close_blob(self): From ba5cd741e14e7f8939deba28d574fb30e8f1a66e Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Thu, 18 Aug 2016 10:45:03 -0400 Subject: [PATCH 58/81] Testing copying instead of moving upload file to avoid access error --- lbrynet/lbrynet_daemon/LBRYDaemonServer.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemonServer.py b/lbrynet/lbrynet_daemon/LBRYDaemonServer.py index be9775a95..d210a830d 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemonServer.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemonServer.py @@ -377,10 +377,16 @@ class LBRYFileUpload(resource.Resource): # Move to a new temporary dir and restore the original file name newdirpath = tempfile.mkdtemp() newpath = os.path.join(newdirpath, origfilename) - try: + if os.name == "nt": + print('Debugging: shutil.copy({0}, {1}'.format(uploaded_file.name, newpath)) + shutil.copy(uploaded_file.name, newpath) + # TODO Still need to remove the file + try: + os.remove(uploaded_file.name) + except WindowsError as e: + print(e) + else: shutil.move(uploaded_file.name, newpath) - except WindowsError: - pass self._api.uploaded_temp_files.append(newpath) return json.dumps(newpath) From 929db152143c95c203b5226b70d7c12e173e9db6 Mon Sep 17 00:00:00 2001 From: Jack Date: Thu, 18 Aug 2016 22:15:49 -0400 Subject: [PATCH 59/81] fix get_history in lbrycrdwallet --- lbrynet/core/LBRYWallet.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lbrynet/core/LBRYWallet.py b/lbrynet/core/LBRYWallet.py index 0bc584aca..7d275278b 100644 --- a/lbrynet/core/LBRYWallet.py +++ b/lbrynet/core/LBRYWallet.py @@ -489,6 +489,10 @@ class LBRYWallet(object): d.addCallback(self._get_decoded_tx) return d + def get_history(self): + d = self._get_history() + return d + def get_name_and_validity_for_sd_hash(self, sd_hash): d = self._get_claim_metadata_for_sd_hash(sd_hash) d.addCallback(lambda name_txid: self._get_status_of_claim(name_txid[1], name_txid[0], sd_hash) if name_txid is not None else None) @@ -688,6 +692,9 @@ class LBRYWallet(object): def _get_balance_for_address(self, address): return defer.fail(NotImplementedError()) + def _get_history(self): + return defer.fail(NotImplementedError()) + def _start(self): pass @@ -823,6 +830,9 @@ class LBRYcrdWallet(LBRYWallet): def _get_value_for_name(self, name): return threads.deferToThread(self._get_value_for_name_rpc, name) + def _get_history(self): + return threads.deferToThread(self._list_transactions_rpc) + def _get_rpc_conn(self): return AuthServiceProxy(self.rpc_conn_string) @@ -1007,6 +1017,11 @@ class LBRYcrdWallet(LBRYWallet): rpc_conn = self._get_rpc_conn() return rpc_conn.getbestblockhash() + @_catch_connection_error + def _list_transactions_rpc(self): + rpc_conn = self._get_rpc_conn() + return rpc_conn.listtransactions() + @_catch_connection_error def _stop_rpc(self): # check if our lbrycrdd is actually running, or if we connected to one that was already @@ -1294,7 +1309,7 @@ class LBRYumWallet(LBRYWallet): func = getattr(self.cmd_runner, cmd.name) return threads.deferToThread(func) - def get_history(self): + def _get_history(self): cmd = known_commands['history'] func = getattr(self.cmd_runner, cmd.name) return threads.deferToThread(func) From 7a5489401516aa9f0dc1aa7fe46b5818817f91e5 Mon Sep 17 00:00:00 2001 From: Jack Date: Fri, 19 Aug 2016 02:41:23 -0400 Subject: [PATCH 60/81] debugging stuff --- lbrynet/core/client/ConnectionManager.py | 2 +- lbrynet/core/log_support.py | 1 + lbrynet/lbrynet_daemon/LBRYDaemon.py | 9 +++++++++ lbrynet/reflector/server/server.py | 4 +--- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lbrynet/core/client/ConnectionManager.py b/lbrynet/core/client/ConnectionManager.py index 683935036..f82f19a20 100644 --- a/lbrynet/core/client/ConnectionManager.py +++ b/lbrynet/core/client/ConnectionManager.py @@ -172,7 +172,7 @@ class ConnectionManager(object): def pick_best_peer(peers): # TODO: Eventually rank them based on past performance/reputation. For now # TODO: just pick the first to which we don't have an open connection - log.debug("Got a list of peers to choose from: %s", str(peers)) + log.debug("Got a list of peers to choose from: %s", str(["%s:%i" % (p.host, p.port) for p in peers])) if peers is None: return None for peer in peers: diff --git a/lbrynet/core/log_support.py b/lbrynet/core/log_support.py index 54a5a03a1..c2f652e74 100644 --- a/lbrynet/core/log_support.py +++ b/lbrynet/core/log_support.py @@ -80,6 +80,7 @@ def disable_noisy_loggers(): logging.getLogger('lbrynet.core.client.ConnectionManager').setLevel(logging.INFO) logging.getLogger('lbrynet.core.client.BlobRequester').setLevel(logging.INFO) logging.getLogger('lbrynet.core.client.ClientProtocol').setLevel(logging.INFO) + logging.getLogger('lbrynet.analytics.api').setLevel(logging.INFO) @_log_decorator diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index f3a820399..ac888b757 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -2463,6 +2463,15 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallbacks(lambda _: self._render_response(True, OK_CODE), lambda err: self._render_response(err.getTraceback(), OK_CODE)) return d + def jsonrpc_get_blobs(self): + """ + return all blobs + """ + + d = defer.succeed(self.session.blob_manager.blobs) + d.addCallback(lambda r: self._render_response(r, OK_CODE)) + return d + def get_lbrynet_version_from_github(): """Return the latest released version from github.""" diff --git a/lbrynet/reflector/server/server.py b/lbrynet/reflector/server/server.py index 1728a8ed1..6d309cafa 100644 --- a/lbrynet/reflector/server/server.py +++ b/lbrynet/reflector/server/server.py @@ -85,9 +85,7 @@ class ReflectorServer(Protocol): def determine_blob_needed(self, blob): if blob.is_validated(): - d = self.blob_manager.blob_completed(blob) - d.addCallback(lambda _: {'send_blob': False}) - return d + return {'send_blob': False} else: self.incoming_blob = blob self.blob_finished_d, self.blob_write, self.cancel_write = blob.open_for_writing(self.peer) From 31b9d22649f4ed50232eaebe284347034e3ec1a0 Mon Sep 17 00:00:00 2001 From: Sonata Green Date: Fri, 19 Aug 2016 21:12:02 -0500 Subject: [PATCH 61/81] spelling fix --- lbrynet/lbrynet_daemon/LBRYDaemonCLI.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemonCLI.py b/lbrynet/lbrynet_daemon/LBRYDaemonCLI.py index 116fd32cb..1d9733b19 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemonCLI.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemonCLI.py @@ -4,7 +4,7 @@ import json from lbrynet.conf import API_CONNECTION_STRING from jsonrpc.proxy import JSONRPCProxy -help_msg = "Useage: lbrynet-cli method json-args\n" \ +help_msg = "Usage: lbrynet-cli method json-args\n" \ + "Examples: " \ + "lbrynet-cli resolve_name '{\"name\": \"what\"}'\n" \ + "lbrynet-cli get_balance\n" \ From 5022ed2acc28df43699d647472345fa1c3240866 Mon Sep 17 00:00:00 2001 From: Jack Date: Sat, 20 Aug 2016 23:47:12 -0400 Subject: [PATCH 62/81] blob_completed before moving to next one otherwise blob files download to the blobfiles directory, but they are never recorded in blobs.db --- lbrynet/reflector/server/server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lbrynet/reflector/server/server.py b/lbrynet/reflector/server/server.py index 6d309cafa..dea539814 100644 --- a/lbrynet/reflector/server/server.py +++ b/lbrynet/reflector/server/server.py @@ -89,6 +89,7 @@ class ReflectorServer(Protocol): else: self.incoming_blob = blob self.blob_finished_d, self.blob_write, self.cancel_write = blob.open_for_writing(self.peer) + self.blob_finished_d.addCallback(lambda _ :self.blob_manager.blob_completed(blob)) return {'send_blob': True} def close_blob(self): From 3d92413372ab1de6ea751573caf3f87e5324c704 Mon Sep 17 00:00:00 2001 From: Jack Date: Sat, 20 Aug 2016 23:47:41 -0400 Subject: [PATCH 63/81] debug logging --- lbrynet/reflector/server/server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lbrynet/reflector/server/server.py b/lbrynet/reflector/server/server.py index dea539814..38127dce2 100644 --- a/lbrynet/reflector/server/server.py +++ b/lbrynet/reflector/server/server.py @@ -27,11 +27,11 @@ class ReflectorServer(Protocol): self.request_buff = "" def connectionLost(self, reason=failure.Failure(error.ConnectionDone())): - pass + log.info("Reflector upload from %s finished" % self.peer.host) def dataReceived(self, data): if self.receiving_blob: - log.debug('Writing data to blob') + # log.debug('Writing data to blob') self.blob_write(data) else: log.debug('Not yet recieving blob, data needs further processing') From 15cfa4564662a7953be6bc5c81c91b0aeaa34859 Mon Sep 17 00:00:00 2001 From: Jack Date: Sun, 21 Aug 2016 00:58:25 -0400 Subject: [PATCH 64/81] remove duplicate function --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 24 +++++------------------- lbrynet/lbrynet_daemon/LBRYPublisher.py | 2 +- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index ac888b757..8b4cc54fb 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -1344,11 +1344,12 @@ class LBRYDaemon(jsonrpc.JSONRPC): if not lbry_file: return defer.fail(Exception("no lbry file given to reflect")) - sd_hash = lbry_file.sd_hash stream_hash = lbry_file.stream_hash - if sd_hash is None or stream_hash is None: - return defer.fail(Exception("unpopulated lbry file fields")) + if stream_hash is None: + return defer.fail(Exception("no stream hash")) + + log.info("Reflecting stream: %s" % stream_hash) reflector_server = random.choice(REFLECTOR_SERVERS) reflector_address, reflector_port = reflector_server[0], reflector_server[1] @@ -1938,26 +1939,11 @@ class LBRYDaemon(jsonrpc.JSONRPC): return m def _reflect_if_possible(sd_hash, txid): - log.info("Trying to start reflector") d = self._get_lbry_file('sd_hash', sd_hash, return_json=False) - d.addCallback(lambda r: False if not r else _start_reflector(r.stream_hash)) + d.addCallback(self._reflect) d.addCallback(lambda _: txid) return d - def _start_reflector(stream_hash): - reflector_server = random.choice(REFLECTOR_SERVERS) - reflector_address, reflector_port = reflector_server[0], reflector_server[1] - log.info("Start reflector client") - factory = reflector.ClientFactory( - self.session.blob_manager, - self.lbry_file_manager.stream_info_manager, - stream_hash - ) - d = reactor.resolve(reflector_address) - d.addCallback(lambda ip: reactor.connectTCP(ip, reflector_port, factory)) - d.addCallback(lambda _: factory.finished_deferred) - return d - name = p['name'] log.info("Publish: ") diff --git a/lbrynet/lbrynet_daemon/LBRYPublisher.py b/lbrynet/lbrynet_daemon/LBRYPublisher.py index 5efbe66a9..04b9bdaab 100644 --- a/lbrynet/lbrynet_daemon/LBRYPublisher.py +++ b/lbrynet/lbrynet_daemon/LBRYPublisher.py @@ -72,7 +72,7 @@ class Publisher(object): def start_reflector(self): reflector_server = random.choice(REFLECTOR_SERVERS) reflector_address, reflector_port = reflector_server[0], reflector_server[1] - log.info("Start reflector client") + log.info("Reflecting new publication") factory = reflector.ClientFactory( self.session.blob_manager, self.lbry_file_manager.stream_info_manager, From 9c82689f65228a3f6cc0ffc4770f6704dfd60f56 Mon Sep 17 00:00:00 2001 From: Jack Date: Sun, 21 Aug 2016 04:04:11 -0400 Subject: [PATCH 65/81] Bump version: 0.3.19 -> 0.3.20 --- .bumpversion.cfg | 2 +- lbrynet/__init__.py | 2 +- packaging/ubuntu/lbry.desktop | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 78907de6a..686ee8c48 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.3.19 +current_version = 0.3.20 commit = True tag = True message = Bump version: {current_version} -> {new_version} diff --git a/lbrynet/__init__.py b/lbrynet/__init__.py index d83b7602f..3a92417c3 100644 --- a/lbrynet/__init__.py +++ b/lbrynet/__init__.py @@ -1,2 +1,2 @@ -__version__ = "0.3.19" +__version__ = "0.3.20" version = tuple(__version__.split('.')) \ No newline at end of file diff --git a/packaging/ubuntu/lbry.desktop b/packaging/ubuntu/lbry.desktop index 061299934..1af5a588c 100644 --- a/packaging/ubuntu/lbry.desktop +++ b/packaging/ubuntu/lbry.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -Version=0.3.19 +Version=0.3.20 Name=LBRY Comment=The world's first user-owned content marketplace Icon=lbry From d8cb62a92c85a2298e648b82d82d3197426c38eb Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Sun, 21 Aug 2016 18:44:16 -0400 Subject: [PATCH 66/81] Run win32 as systray app --- lbrynet/conf.py | 6 +- lbrynet/lbrynet_daemon/LBRYDaemonServer.py | 3 +- .../windows/lbry-win32-app/LBRYWin32App.py | 280 ++++++++++++++++++ .../{ => lbry-win32-app}/icons/lbry128.ico | Bin .../windows/lbry-win32-app/icons/lbry16.ico | Bin 0 -> 1150 bytes .../{ => lbry-win32-app}/icons/lbry256.ico | Bin .../{ => lbry-win32-app}/icons/lbry32.ico | Bin .../{ => lbry-win32-app}/icons/lbry48.ico | Bin .../{ => lbry-win32-app}/icons/lbry96.ico | Bin setup_win32.py | 11 +- 10 files changed, 293 insertions(+), 7 deletions(-) create mode 100644 packaging/windows/lbry-win32-app/LBRYWin32App.py rename packaging/windows/{ => lbry-win32-app}/icons/lbry128.ico (100%) create mode 100644 packaging/windows/lbry-win32-app/icons/lbry16.ico rename packaging/windows/{ => lbry-win32-app}/icons/lbry256.ico (100%) rename packaging/windows/{ => lbry-win32-app}/icons/lbry32.ico (100%) rename packaging/windows/{ => lbry-win32-app}/icons/lbry48.ico (100%) rename packaging/windows/{ => lbry-win32-app}/icons/lbry96.ico (100%) diff --git a/lbrynet/conf.py b/lbrynet/conf.py index 118a52f1f..c5c35b142 100644 --- a/lbrynet/conf.py +++ b/lbrynet/conf.py @@ -1,6 +1,7 @@ """ Some network wide and also application specific parameters """ +import os MAX_HANDSHAKE_SIZE = 2**16 @@ -38,7 +39,10 @@ CRYPTSD_FILE_EXTENSION = ".cryptsd" API_INTERFACE = "localhost" API_ADDRESS = "lbryapi" API_PORT = 5279 -ICON_PATH = "app.icns" +if os.name == "nt": + ICON_PATH = "icons" +else: + ICON_PATH = "app.icns" APP_NAME = "LBRY" API_CONNECTION_STRING = "http://%s:%i/%s" % (API_INTERFACE, API_PORT, API_ADDRESS) UI_ADDRESS = "http://%s:%i" % (API_INTERFACE, API_PORT) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemonServer.py b/lbrynet/lbrynet_daemon/LBRYDaemonServer.py index d210a830d..775761b41 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemonServer.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemonServer.py @@ -378,13 +378,12 @@ class LBRYFileUpload(resource.Resource): newdirpath = tempfile.mkdtemp() newpath = os.path.join(newdirpath, origfilename) if os.name == "nt": - print('Debugging: shutil.copy({0}, {1}'.format(uploaded_file.name, newpath)) shutil.copy(uploaded_file.name, newpath) # TODO Still need to remove the file try: os.remove(uploaded_file.name) except WindowsError as e: - print(e) + pass else: shutil.move(uploaded_file.name, newpath) self._api.uploaded_temp_files.append(newpath) diff --git a/packaging/windows/lbry-win32-app/LBRYWin32App.py b/packaging/windows/lbry-win32-app/LBRYWin32App.py new file mode 100644 index 000000000..fb3041c0b --- /dev/null +++ b/packaging/windows/lbry-win32-app/LBRYWin32App.py @@ -0,0 +1,280 @@ +import logging +import os +import socket +import sys +import threading +import webbrowser + +# from appdirs import user_data_dir +from twisted.internet import reactor +from twisted.web import server +# from twisted.internet.cfreactor import install +# install(runner=AppHelper.runEventLoop) +import win32api +import win32con +import win32gui_struct + +try: + import winxpgui as win32gui +except ImportError: + import win32gui + +from lbrynet.lbrynet_daemon.LBRYDaemonServer import LBRYDaemonServer, LBRYDaemonRequest +from lbrynet.conf import API_PORT, API_INTERFACE, ICON_PATH, APP_NAME +from lbrynet.conf import UI_ADDRESS + + +if getattr(sys, 'frozen', False) and os.name == "nt": + os.environ["REQUESTS_CA_BUNDLE"] = os.path.join(os.path.dirname(sys.executable), "cacert.pem") + +log = logging.getLogger(__name__) + +REMOTE_SERVER = "www.google.com" + + +def test_internet_connection(): + try: + host = socket.gethostbyname(REMOTE_SERVER) + s = socket.create_connection((host, 80), 2) + return True + except: + return False + + +def non_string_iterable(obj): + try: + iter(obj) + except TypeError: + return False + else: + return not isinstance(obj, basestring) + + +class SysTrayIcon(object): + """TODO""" + QUIT = 'QUIT' + SPECIAL_ACTIONS = [QUIT] + + FIRST_ID = 1023 + + def __init__(self, + icon, + hover_text, + menu_options, + on_quit=None, + default_menu_index=None, + window_class_name=None, ): + + self.icon = icon + self.hover_text = hover_text + self.on_quit = on_quit + + menu_options = menu_options + (('Quit', None, self.QUIT),) + self._next_action_id = self.FIRST_ID + self.menu_actions_by_id = set() + self.menu_options = self._add_ids_to_menu_options(list(menu_options)) + self.menu_actions_by_id = dict(self.menu_actions_by_id) + del self._next_action_id + + self.default_menu_index = (default_menu_index or 0) + self.window_class_name = window_class_name or "SysTrayIconPy" + + message_map = {win32gui.RegisterWindowMessage("TaskbarCreated"): self.restart, + win32con.WM_DESTROY: self.destroy, + win32con.WM_COMMAND: self.command, + win32con.WM_USER + 20: self.notify,} + # Register the Window class. + window_class = win32gui.WNDCLASS() + hinst = window_class.hInstance = win32gui.GetModuleHandle(None) + window_class.lpszClassName = self.window_class_name + window_class.style = win32con.CS_VREDRAW | win32con.CS_HREDRAW; + window_class.hCursor = win32gui.LoadCursor(0, win32con.IDC_ARROW) + window_class.hbrBackground = win32con.COLOR_WINDOW + window_class.lpfnWndProc = message_map # could also specify a wndproc. + classAtom = win32gui.RegisterClass(window_class) + # Create the Window. + style = win32con.WS_OVERLAPPED | win32con.WS_SYSMENU + self.hwnd = win32gui.CreateWindow(classAtom, + self.window_class_name, + style, + 0, + 0, + win32con.CW_USEDEFAULT, + win32con.CW_USEDEFAULT, + 0, + 0, + hinst, + None) + win32gui.UpdateWindow(self.hwnd) + self.notify_id = None + self.refresh_icon() + + win32gui.PumpMessages() + + def _add_ids_to_menu_options(self, menu_options): + result = [] + for menu_option in menu_options: + option_text, option_icon, option_action = menu_option + if callable(option_action) or option_action in self.SPECIAL_ACTIONS: + self.menu_actions_by_id.add((self._next_action_id, option_action)) + result.append(menu_option + (self._next_action_id,)) + elif non_string_iterable(option_action): + result.append((option_text, + option_icon, + self._add_ids_to_menu_options(option_action), + self._next_action_id)) + else: + print 'Unknown item', option_text, option_icon, option_action + self._next_action_id += 1 + return result + + def refresh_icon(self): + # Try and find a custom icon + hinst = win32gui.GetModuleHandle(None) + if os.path.isfile(self.icon): + icon_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE + hicon = win32gui.LoadImage(hinst, + self.icon, + win32con.IMAGE_ICON, + 0, + 0, + icon_flags) + else: + print "Can't find icon file - using default." + hicon = win32gui.LoadIcon(0, win32con.IDI_APPLICATION) + + if self.notify_id: + message = win32gui.NIM_MODIFY + else: + message = win32gui.NIM_ADD + self.notify_id = (self.hwnd, + 0, + win32gui.NIF_ICON | win32gui.NIF_MESSAGE | win32gui.NIF_TIP, + win32con.WM_USER + 20, + hicon, + self.hover_text) + win32gui.Shell_NotifyIcon(message, self.notify_id) + + def restart(self, hwnd, msg, wparam, lparam): + self.refresh_icon() + + def destroy(self, hwnd, msg, wparam, lparam): + if self.on_quit: self.on_quit(self) + nid = (self.hwnd, 0) + win32gui.Shell_NotifyIcon(win32gui.NIM_DELETE, nid) + win32gui.PostQuitMessage(0) # Terminate the app. + + def notify(self, hwnd, msg, wparam, lparam): + if lparam == win32con.WM_LBUTTONDBLCLK: + self.execute_menu_option(self.default_menu_index + self.FIRST_ID) + elif lparam == win32con.WM_RBUTTONUP: + self.show_menu() + elif lparam == win32con.WM_LBUTTONUP: + pass + return True + + def show_menu(self): + menu = win32gui.CreatePopupMenu() + self.create_menu(menu, self.menu_options) + # win32gui.SetMenuDefaultItem(menu, 1000, 0) + + pos = win32gui.GetCursorPos() + # See http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/menus_0hdi.asp + win32gui.SetForegroundWindow(self.hwnd) + win32gui.TrackPopupMenu(menu, + win32con.TPM_LEFTALIGN, + pos[0], + pos[1], + 0, + self.hwnd, + None) + win32gui.PostMessage(self.hwnd, win32con.WM_NULL, 0, 0) + + def create_menu(self, menu, menu_options): + for option_text, option_icon, option_action, option_id in menu_options[::-1]: + if option_icon: + option_icon = self.prep_menu_icon(option_icon) + + if option_id in self.menu_actions_by_id: + item, extras = win32gui_struct.PackMENUITEMINFO(text=option_text, + hbmpItem=option_icon, + wID=option_id) + win32gui.InsertMenuItem(menu, 0, 1, item) + else: + submenu = win32gui.CreatePopupMenu() + self.create_menu(submenu, option_action) + item, extras = win32gui_struct.PackMENUITEMINFO(text=option_text, + hbmpItem=option_icon, + hSubMenu=submenu) + win32gui.InsertMenuItem(menu, 0, 1, item) + + def prep_menu_icon(self, icon): + # First load the icon. + ico_x = win32api.GetSystemMetrics(win32con.SM_CXSMICON) + ico_y = win32api.GetSystemMetrics(win32con.SM_CYSMICON) + hicon = win32gui.LoadImage(0, icon, win32con.IMAGE_ICON, ico_x, ico_y, win32con.LR_LOADFROMFILE) + + hdcBitmap = win32gui.CreateCompatibleDC(0) + hdcScreen = win32gui.GetDC(0) + hbm = win32gui.CreateCompatibleBitmap(hdcScreen, ico_x, ico_y) + hbmOld = win32gui.SelectObject(hdcBitmap, hbm) + # Fill the background. + brush = win32gui.GetSysColorBrush(win32con.COLOR_MENU) + win32gui.FillRect(hdcBitmap, (0, 0, 16, 16), brush) + # unclear if brush needs to be feed. Best clue I can find is: + # "GetSysColorBrush returns a cached brush instead of allocating a new + # one." - implies no DeleteObject + # draw the icon + win32gui.DrawIconEx(hdcBitmap, 0, 0, hicon, ico_x, ico_y, 0, 0, win32con.DI_NORMAL) + win32gui.SelectObject(hdcBitmap, hbmOld) + win32gui.DeleteDC(hdcBitmap) + + return hbm + + def command(self, hwnd, msg, wparam, lparam): + id = win32gui.LOWORD(wparam) + self.execute_menu_option(id) + + def execute_menu_option(self, id): + menu_action = self.menu_actions_by_id[id] + if menu_action == self.QUIT: + win32gui.DestroyWindow(self.hwnd) + else: + menu_action(self) + + +def main(): + def LBRYApp(): + SysTrayIcon(icon, hover_text, menu_options, on_quit=stop) + + def openui_(sender): + webbrowser.open(UI_ADDRESS) + + def replyToApplicationShouldTerminate_(): + reactor.stop() + + def stop(sysTrayIcon): + replyToApplicationShouldTerminate_() + + icon = os.path.join(ICON_PATH, 'lbry16.ico') + hover_text = APP_NAME + menu_options = (('Open UI', icon, openui_),) + + if not test_internet_connection(): + log.warn('No Internet Connection') + sys.exit(1) + + systray_thread = threading.Thread(target=LBRYApp) + systray_thread.start() + + lbry = LBRYDaemonServer() + d = lbry.start() + d.addCallback(lambda _: webbrowser.open(UI_ADDRESS)) + lbrynet_server = server.Site(lbry.root) + lbrynet_server.requestFactory = LBRYDaemonRequest + reactor.listenTCP(API_PORT, lbrynet_server, interface=API_INTERFACE) + # reactor.addSystemEventTrigger("after", "shutdown", AppHelper.stopEventLoop) + reactor.run() + +if __name__ == '__main__': + main() diff --git a/packaging/windows/icons/lbry128.ico b/packaging/windows/lbry-win32-app/icons/lbry128.ico similarity index 100% rename from packaging/windows/icons/lbry128.ico rename to packaging/windows/lbry-win32-app/icons/lbry128.ico diff --git a/packaging/windows/lbry-win32-app/icons/lbry16.ico b/packaging/windows/lbry-win32-app/icons/lbry16.ico new file mode 100644 index 0000000000000000000000000000000000000000..40d84962861902f98d5d64b2627ae605932aebcb GIT binary patch literal 1150 zcmcIi-A|HX6n{uJ*4)Z<;f1Y_tL8<2K^OHObmM(jwOs3_wYgarIo47tr&B%!LPelR z6jCU%Fe%YxQ%P8WihKlv(H8@imzVcFo#z#X7r45p^X@#)`TCu6p7S0;MEEr~6Fkq7 zjz&US2qA6AXhO@r9WO%AS8Cd4LH|z&01#vmauaeR)&GWQT!l}@Vq3RduWTOw`icx~ z=dNFC7ku^d9KfB9$0MCDY-YbwE-jl)8c-^vV4pGtGnrHe#vQB1R`XQ!Q_<*Vr)|m< zP{?~Z$INp`CgXrMNW{0nZZp$7CGUdUQ7rCVL`(z4QTm#QN^FzHfI`;GIUQC=r4oQW zgZ{}NC}cgLRz3osZy7=%Kj^drU>H+oH#dU=g+jiIVVJgYgNiqsv;yuk#_2QzFghZK zP;ebsmKA!?>jvQ|#>8V0c<-5o{=RO?{Km!?nMU0Qe*Y@WOj`t8I2?d-xs2`As8nKL z(K`q7p&szeyWq|2G~DgF0Sh18-*85p*{A`WH*g$R1J#j!e9wY)Vhq%(#}Ejt!S3$P z%XB(dF$N z#J+?vfU^dy1z4+$yRnwf=PskSx-;Tj?q~aJm;IxB=mF<`>wvx89pHIRn4#sF9&jgE zr(@z1{@a^~Yo*vKQ5ok=bG1eyVTs8&oF2y68b%eg9)tC~_|~iFZ(-TepLX#G!xS$p zEzVo84_7vuxq`S8_=ZDq)jsG=d#SJ(@1s@yV9dXg=?KdA2q|77Bzl~X?uNrNzY`Ji OON_B+2;mV!vA+Qcgj3Q0 literal 0 HcmV?d00001 diff --git a/packaging/windows/icons/lbry256.ico b/packaging/windows/lbry-win32-app/icons/lbry256.ico similarity index 100% rename from packaging/windows/icons/lbry256.ico rename to packaging/windows/lbry-win32-app/icons/lbry256.ico diff --git a/packaging/windows/icons/lbry32.ico b/packaging/windows/lbry-win32-app/icons/lbry32.ico similarity index 100% rename from packaging/windows/icons/lbry32.ico rename to packaging/windows/lbry-win32-app/icons/lbry32.ico diff --git a/packaging/windows/icons/lbry48.ico b/packaging/windows/lbry-win32-app/icons/lbry48.ico similarity index 100% rename from packaging/windows/icons/lbry48.ico rename to packaging/windows/lbry-win32-app/icons/lbry48.ico diff --git a/packaging/windows/icons/lbry96.ico b/packaging/windows/lbry-win32-app/icons/lbry96.ico similarity index 100% rename from packaging/windows/icons/lbry96.ico rename to packaging/windows/lbry-win32-app/icons/lbry96.ico diff --git a/setup_win32.py b/setup_win32.py index 1e6b48387..602fb6b45 100644 --- a/setup_win32.py +++ b/setup_win32.py @@ -13,7 +13,7 @@ import requests.certs from lbrynet import __version__ -win_icon = os.path.join('packaging', 'windows', 'icons', 'lbry256.ico') +win_icon = os.path.join('packaging', 'windows', 'lbry-win32-app', 'icons', 'lbry256.ico') wordlist_path = pkg_resources.resource_filename('lbryum', 'wordlist') base_dir = os.path.abspath(os.path.dirname(__file__)) @@ -113,7 +113,10 @@ build_exe_options = { 'Tkinter', 'tk', 'tcl', 'PyQt4', 'nose', 'mock' 'zope.interface._zope_interface_coptimizations'], 'include_files': [(distutils_path, 'distutils'), (requests.certs.where(), 'cacert.pem'), - (os.path.join(wordlist_path, 'chinese_simplified.txt'), os.path.join('wordlist', 'chinese_simplified.txt')), + (os.path.join('packaging', 'windows', 'lbry-win32-app', 'icons', 'lbry16.ico'), + os.path.join('icons', 'lbry16.ico')), + (os.path.join(wordlist_path, 'chinese_simplified.txt'), + os.path.join('wordlist', 'chinese_simplified.txt')), (os.path.join(wordlist_path, 'english.txt'), os.path.join('wordlist', 'english.txt')), (os.path.join(wordlist_path, 'japanese.txt'), os.path.join('wordlist', 'japanese.txt')), (os.path.join(wordlist_path, 'portuguese.txt'), os.path.join('wordlist', 'portuguese.txt')), @@ -122,8 +125,8 @@ build_exe_options = { 'namespace_packages': ['zope', 'google']} exe = Executable( - script=os.path.join('lbrynet', 'lbrynet_daemon', 'LBRYDaemonControl.py'), - # base='Win32GUI', + script=os.path.join('packaging', 'windows', 'lbry-win32-app', 'LBRYWin32App.py'), + base='Win32GUI', icon=win_icon, compress=True, shortcutName='LBRY', From 5bfb067d0f40e0ca30710ec39b0dd98e9137da13 Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Sun, 21 Aug 2016 18:49:41 -0400 Subject: [PATCH 67/81] Rename to just Open --- packaging/windows/lbry-win32-app/LBRYWin32App.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/windows/lbry-win32-app/LBRYWin32App.py b/packaging/windows/lbry-win32-app/LBRYWin32App.py index fb3041c0b..0b043fcbf 100644 --- a/packaging/windows/lbry-win32-app/LBRYWin32App.py +++ b/packaging/windows/lbry-win32-app/LBRYWin32App.py @@ -258,7 +258,7 @@ def main(): icon = os.path.join(ICON_PATH, 'lbry16.ico') hover_text = APP_NAME - menu_options = (('Open UI', icon, openui_),) + menu_options = (('Open', icon, openui_),) if not test_internet_connection(): log.warn('No Internet Connection') From 84d340382b51f8a4f0658772cab93209302b3377 Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Sun, 21 Aug 2016 18:58:50 -0400 Subject: [PATCH 68/81] Fix for icon path when running shortcut --- packaging/windows/lbry-win32-app/LBRYWin32App.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packaging/windows/lbry-win32-app/LBRYWin32App.py b/packaging/windows/lbry-win32-app/LBRYWin32App.py index 0b043fcbf..1ebd0a441 100644 --- a/packaging/windows/lbry-win32-app/LBRYWin32App.py +++ b/packaging/windows/lbry-win32-app/LBRYWin32App.py @@ -256,7 +256,10 @@ def main(): def stop(sysTrayIcon): replyToApplicationShouldTerminate_() - icon = os.path.join(ICON_PATH, 'lbry16.ico') + if getattr(sys, 'frozen', False) and os.name == "nt": + icon = os.path.join(os.path.dirname(sys.executable), ICON_PATH, 'lbry16.ico') + else: + icon = os.path.join(ICON_PATH, 'lbry16.ico') hover_text = APP_NAME menu_options = (('Open', icon, openui_),) From e49f0f99a1820d95a8a64d2c40ff223955325d22 Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 22 Aug 2016 16:57:22 -0400 Subject: [PATCH 69/81] LBRYcrdWallet update claim fix -fix log line that could raise an exception -json encode value sent to lbrycrd-cli updateclaim --- lbrynet/core/LBRYWallet.py | 2 +- lbrynet/core/client/ConnectionManager.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lbrynet/core/LBRYWallet.py b/lbrynet/core/LBRYWallet.py index 7d275278b..459b28568 100644 --- a/lbrynet/core/LBRYWallet.py +++ b/lbrynet/core/LBRYWallet.py @@ -989,7 +989,7 @@ class LBRYcrdWallet(LBRYWallet): @_catch_connection_error def _update_name_rpc(self, txid, value, amount): rpc_conn = self._get_rpc_conn() - return rpc_conn.updateclaim(txid, value, amount) + return rpc_conn.updateclaim(txid, json.dumps(value), amount) @_catch_connection_error def _send_name_claim_rpc(self, name, value, amount): diff --git a/lbrynet/core/client/ConnectionManager.py b/lbrynet/core/client/ConnectionManager.py index f82f19a20..6cff01156 100644 --- a/lbrynet/core/client/ConnectionManager.py +++ b/lbrynet/core/client/ConnectionManager.py @@ -172,7 +172,8 @@ class ConnectionManager(object): def pick_best_peer(peers): # TODO: Eventually rank them based on past performance/reputation. For now # TODO: just pick the first to which we don't have an open connection - log.debug("Got a list of peers to choose from: %s", str(["%s:%i" % (p.host, p.port) for p in peers])) + + log.debug("Got a list of peers to choose from: %s", str(peers)) if peers is None: return None for peer in peers: From 5727c708cf78175cf0de4fccfd2dab62a46b974d Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 22 Aug 2016 18:43:52 -0400 Subject: [PATCH 70/81] fix get_transaction previously get_tx_json was only in LBRYumWallet --- lbrynet/core/LBRYWallet.py | 46 +++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/lbrynet/core/LBRYWallet.py b/lbrynet/core/LBRYWallet.py index 459b28568..99f4939fe 100644 --- a/lbrynet/core/LBRYWallet.py +++ b/lbrynet/core/LBRYWallet.py @@ -493,6 +493,29 @@ class LBRYWallet(object): d = self._get_history() return d + def get_tx_json(self, txid): + def _decode(raw_tx): + tx = Transaction(raw_tx).deserialize() + decoded_tx = {} + for txkey in tx.keys(): + if isinstance(tx[txkey], list): + decoded_tx[txkey] = [] + for i in tx[txkey]: + tmp = {} + for k in i.keys(): + if isinstance(i[k], Decimal): + tmp[k] = float(i[k] / 1e8) + else: + tmp[k] = i[k] + decoded_tx[txkey].append(tmp) + else: + decoded_tx[txkey] = tx[txkey] + return decoded_tx + + d = self._get_raw_tx(txid) + d.addCallback(_decode) + return d + def get_name_and_validity_for_sd_hash(self, sd_hash): d = self._get_claim_metadata_for_sd_hash(sd_hash) d.addCallback(lambda name_txid: self._get_status_of_claim(name_txid[1], name_txid[0], sd_hash) if name_txid is not None else None) @@ -1314,29 +1337,6 @@ class LBRYumWallet(LBRYWallet): func = getattr(self.cmd_runner, cmd.name) return threads.deferToThread(func) - def get_tx_json(self, txid): - def _decode(raw_tx): - tx = Transaction(raw_tx).deserialize() - decoded_tx = {} - for txkey in tx.keys(): - if isinstance(tx[txkey], list): - decoded_tx[txkey] = [] - for i in tx[txkey]: - tmp = {} - for k in i.keys(): - if isinstance(i[k], Decimal): - tmp[k] = float(i[k] / 1e8) - else: - tmp[k] = i[k] - decoded_tx[txkey].append(tmp) - else: - decoded_tx[txkey] = tx[txkey] - return decoded_tx - - d = self._get_raw_tx(txid) - d.addCallback(_decode) - return d - def get_pub_keys(self, wallet): cmd = known_commands['getpubkeys'] func = getattr(self.cmd_runner, cmd.name) From 95f4b29be54143454360464fe7b336c02926c9b1 Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 22 Aug 2016 18:59:17 -0400 Subject: [PATCH 71/81] noisy log --- lbrynet/core/log_support.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lbrynet/core/log_support.py b/lbrynet/core/log_support.py index c2f652e74..8ca5b2b9b 100644 --- a/lbrynet/core/log_support.py +++ b/lbrynet/core/log_support.py @@ -81,6 +81,7 @@ def disable_noisy_loggers(): logging.getLogger('lbrynet.core.client.BlobRequester').setLevel(logging.INFO) logging.getLogger('lbrynet.core.client.ClientProtocol').setLevel(logging.INFO) logging.getLogger('lbrynet.analytics.api').setLevel(logging.INFO) + logging.getLogger('BitcoinRPC').setLevel(logging.INFO) @_log_decorator From e23f67def6e6a2ee7d0f472d5d1321cd3e5201e7 Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Mon, 22 Aug 2016 19:24:57 -0400 Subject: [PATCH 72/81] File handle for publishing on windows needs to handle binary data as 'rb' --- lbrynet/lbrynet_daemon/LBRYPublisher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lbrynet/lbrynet_daemon/LBRYPublisher.py b/lbrynet/lbrynet_daemon/LBRYPublisher.py index 04b9bdaab..4d975c56b 100644 --- a/lbrynet/lbrynet_daemon/LBRYPublisher.py +++ b/lbrynet/lbrynet_daemon/LBRYPublisher.py @@ -59,7 +59,7 @@ class Publisher(object): d = self._check_file_path(self.file_path) d.addCallback(lambda _: create_lbry_file(self.session, self.lbry_file_manager, - self.file_name, open(self.file_path))) + self.file_name, open(self.file_path, 'rb'))) d.addCallback(self.add_to_lbry_files) d.addCallback(lambda _: self._create_sd_blob()) d.addCallback(lambda _: self._claim_name()) From c1233bd26b72ffd9ba2587fd807b405a9116370c Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Mon, 22 Aug 2016 19:55:08 -0400 Subject: [PATCH 73/81] Make 'rb' file mode windows specific --- lbrynet/lbrynet_daemon/LBRYPublisher.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lbrynet/lbrynet_daemon/LBRYPublisher.py b/lbrynet/lbrynet_daemon/LBRYPublisher.py index 4d975c56b..ec7b908dc 100644 --- a/lbrynet/lbrynet_daemon/LBRYPublisher.py +++ b/lbrynet/lbrynet_daemon/LBRYPublisher.py @@ -57,9 +57,14 @@ class Publisher(object): self.bid_amount = bid self.metadata = metadata + if os.name == "nt": + file_mode = 'rb' + else: + file_mode = 'r' + d = self._check_file_path(self.file_path) d.addCallback(lambda _: create_lbry_file(self.session, self.lbry_file_manager, - self.file_name, open(self.file_path, 'rb'))) + self.file_name, open(self.file_path, file_mode))) d.addCallback(self.add_to_lbry_files) d.addCallback(lambda _: self._create_sd_blob()) d.addCallback(lambda _: self._claim_name()) From 128b32c62c7c6ff303bc2c29ce2a8ab6355df30a Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 22 Aug 2016 19:56:42 -0400 Subject: [PATCH 74/81] even less noisy logs --- lbrynet/core/LBRYWallet.py | 2 +- lbrynet/core/log_support.py | 10 +++++++--- lbrynet/lbrynet_daemon/LBRYExchangeRateManager.py | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lbrynet/core/LBRYWallet.py b/lbrynet/core/LBRYWallet.py index 99f4939fe..571f981d6 100644 --- a/lbrynet/core/LBRYWallet.py +++ b/lbrynet/core/LBRYWallet.py @@ -289,7 +289,7 @@ class LBRYWallet(object): d = self._do_send_many(payments_to_send) d.addCallback(lambda txid: log.debug("Sent transaction %s", txid)) return d - log.info("There were no payments to send") + log.debug("There were no payments to send") return defer.succeed(True) def get_stream_info_for_name(self, name): diff --git a/lbrynet/core/log_support.py b/lbrynet/core/log_support.py index 8ca5b2b9b..04a6fb42d 100644 --- a/lbrynet/core/log_support.py +++ b/lbrynet/core/log_support.py @@ -76,12 +76,16 @@ def disable_third_party_loggers(): def disable_noisy_loggers(): - logging.getLogger('lbrynet.dht').setLevel(logging.INFO) + logging.getLogger('BitcoinRPC').setLevel(logging.INFO) + logging.getLogger('lbrynet.analytics.api').setLevel(logging.INFO) logging.getLogger('lbrynet.core.client.ConnectionManager').setLevel(logging.INFO) logging.getLogger('lbrynet.core.client.BlobRequester').setLevel(logging.INFO) logging.getLogger('lbrynet.core.client.ClientProtocol').setLevel(logging.INFO) - logging.getLogger('lbrynet.analytics.api').setLevel(logging.INFO) - logging.getLogger('BitcoinRPC').setLevel(logging.INFO) + logging.getLogger('lbrynet.core.server.ServerRequestHandler').setLevel(logging.INFO) + logging.getLogger('lbrynet.core.server.ServerProtocol').setLevel(logging.INFO) + logging.getLogger('lbrynet.core.server.BlobAvailabilityHandler').setLevel(logging.INFO) + logging.getLogger('lbrynet.dht').setLevel(logging.INFO) + logging.getLogger('lbrynet.lbrynet_daemon.LBRYExchangeRateManager').setLevel(logging.INFO) @_log_decorator diff --git a/lbrynet/lbrynet_daemon/LBRYExchangeRateManager.py b/lbrynet/lbrynet_daemon/LBRYExchangeRateManager.py index 0af938954..22d0cb4a2 100644 --- a/lbrynet/lbrynet_daemon/LBRYExchangeRateManager.py +++ b/lbrynet/lbrynet_daemon/LBRYExchangeRateManager.py @@ -47,7 +47,7 @@ class MarketFeed(object): return defer.succeed(from_amount / (1.0 - self.fee)) def _save_price(self, price): - log.info("Saving price update %f for %s" % (price, self.market)) + log.debug("Saving price update %f for %s" % (price, self.market)) self.rate = ExchangeRate(self.market, price, int(time.time())) def _update_price(self): @@ -191,7 +191,7 @@ class DummyExchangeRateManager(object): feed.rate = ExchangeRate(feed.market, rates[feed.market]['spot'], rates[feed.market]['ts']) def convert_currency(self, from_currency, to_currency, amount): - log.info("Converting %f %s to %s" % (amount, from_currency, to_currency)) + log.debug("Converting %f %s to %s" % (amount, from_currency, to_currency)) for market in self.market_feeds: if market.rate.currency_pair == (from_currency, to_currency): return amount * market.rate.spot From 0b149ce5f174069bd4515a0ddcfe8be0b6289b13 Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Mon, 22 Aug 2016 21:21:56 -0400 Subject: [PATCH 75/81] Remove old osx imports --- packaging/windows/lbry-win32-app/LBRYWin32App.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packaging/windows/lbry-win32-app/LBRYWin32App.py b/packaging/windows/lbry-win32-app/LBRYWin32App.py index 1ebd0a441..aaf8818ed 100644 --- a/packaging/windows/lbry-win32-app/LBRYWin32App.py +++ b/packaging/windows/lbry-win32-app/LBRYWin32App.py @@ -5,11 +5,8 @@ import sys import threading import webbrowser -# from appdirs import user_data_dir from twisted.internet import reactor from twisted.web import server -# from twisted.internet.cfreactor import install -# install(runner=AppHelper.runEventLoop) import win32api import win32con import win32gui_struct @@ -276,7 +273,6 @@ def main(): lbrynet_server = server.Site(lbry.root) lbrynet_server.requestFactory = LBRYDaemonRequest reactor.listenTCP(API_PORT, lbrynet_server, interface=API_INTERFACE) - # reactor.addSystemEventTrigger("after", "shutdown", AppHelper.stopEventLoop) reactor.run() if __name__ == '__main__': From 6097e14617d749d9ad15fba13b83d53a0512a416 Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Tue, 23 Aug 2016 01:05:07 -0400 Subject: [PATCH 76/81] Make systray thread a daemon to close on api.stop() --- packaging/windows/lbry-win32-app/LBRYWin32App.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packaging/windows/lbry-win32-app/LBRYWin32App.py b/packaging/windows/lbry-win32-app/LBRYWin32App.py index aaf8818ed..a15088fa0 100644 --- a/packaging/windows/lbry-win32-app/LBRYWin32App.py +++ b/packaging/windows/lbry-win32-app/LBRYWin32App.py @@ -5,7 +5,7 @@ import sys import threading import webbrowser -from twisted.internet import reactor +from twisted.internet import reactor, error from twisted.web import server import win32api import win32con @@ -235,20 +235,26 @@ class SysTrayIcon(object): def execute_menu_option(self, id): menu_action = self.menu_actions_by_id[id] if menu_action == self.QUIT: - win32gui.DestroyWindow(self.hwnd) + self.exit_app() else: menu_action(self) + def exit_app(self): + win32gui.DestroyWindow(self.hwnd) + def main(): def LBRYApp(): - SysTrayIcon(icon, hover_text, menu_options, on_quit=stop) + return SysTrayIcon(icon, hover_text, menu_options, on_quit=stop) def openui_(sender): webbrowser.open(UI_ADDRESS) def replyToApplicationShouldTerminate_(): - reactor.stop() + try: + reactor.stop() + except error.ReactorNotRunning: + log.debug('Reactor already stopped') def stop(sysTrayIcon): replyToApplicationShouldTerminate_() @@ -257,6 +263,7 @@ def main(): icon = os.path.join(os.path.dirname(sys.executable), ICON_PATH, 'lbry16.ico') else: icon = os.path.join(ICON_PATH, 'lbry16.ico') + hover_text = APP_NAME menu_options = (('Open', icon, openui_),) @@ -265,6 +272,7 @@ def main(): sys.exit(1) systray_thread = threading.Thread(target=LBRYApp) + systray_thread.daemon = True systray_thread.start() lbry = LBRYDaemonServer() From 0504e231a1dd8150a96f537171443c8ea146ccda Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Tue, 23 Aug 2016 01:16:04 -0400 Subject: [PATCH 77/81] Use main lbryum repo --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index f6c1a6ddf..e4530d383 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -46,7 +46,7 @@ init: - ps: C:\Python27\Scripts\pip.exe install wsgiref==0.1.2 - ps: C:\Python27\Scripts\pip.exe install base58==0.2.2 - ps: C:\Python27\Scripts\pip.exe install googlefinance==0.7 -- ps: C:\Python27\Scripts\pip.exe install git+https://github.com/DaveA50/lbryum.git +- ps: C:\Python27\Scripts\pip.exe install git+https://github.com/lbryio/lbryum.git - ps: cd C:\projects\lbry build_script: From 3bb4dece5acc945db18e4d6cc953bedb7f1229de Mon Sep 17 00:00:00 2001 From: David Amrhein Date: Tue, 23 Aug 2016 01:26:35 -0400 Subject: [PATCH 78/81] Use main lbry repo for gmpy wheel url --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index e4530d383..cf23b5d56 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -14,7 +14,7 @@ init: mingw32-make.exe -f Makefile.mingw C:\Python27\python.exe C:\temp\miniupnpc-1.9\setupmingw32.py build --compiler=mingw32 C:\Python27\python.exe C:\temp\miniupnpc-1.9\setupmingw32.py install -- ps: Invoke-WebRequest "https://github.com/DaveA50/lbry/raw/master/packaging/windows/libs/gmpy-1.17-cp27-none-win32.whl" -OutFile "C:\temp\gmpy-1.17-cp27-none-win32.whl" +- ps: Invoke-WebRequest "https://github.com/lbryio/lbry/raw/master/packaging/windows/libs/gmpy-1.17-cp27-none-win32.whl" -OutFile "C:\temp\gmpy-1.17-cp27-none-win32.whl" - ps: C:\Python27\Scripts\pip.exe install "C:\temp\gmpy-1.17-cp27-none-win32.whl" - ps: C:\Python27\Scripts\pip.exe install pypiwin32==219 - ps: C:\Python27\Scripts\pip.exe install six==1.9.0 From eda273170216bd39c093a3b563d00aaa702f5fe1 Mon Sep 17 00:00:00 2001 From: Jack Date: Tue, 23 Aug 2016 01:59:50 -0400 Subject: [PATCH 79/81] Bump version: 0.3.20 -> 0.3.21 --- .bumpversion.cfg | 2 +- lbrynet/__init__.py | 2 +- packaging/ubuntu/lbry.desktop | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 686ee8c48..d441f91bc 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.3.20 +current_version = 0.3.21 commit = True tag = True message = Bump version: {current_version} -> {new_version} diff --git a/lbrynet/__init__.py b/lbrynet/__init__.py index 3a92417c3..366801ee3 100644 --- a/lbrynet/__init__.py +++ b/lbrynet/__init__.py @@ -1,2 +1,2 @@ -__version__ = "0.3.20" +__version__ = "0.3.21" version = tuple(__version__.split('.')) \ No newline at end of file diff --git a/packaging/ubuntu/lbry.desktop b/packaging/ubuntu/lbry.desktop index 1af5a588c..52cea56e9 100644 --- a/packaging/ubuntu/lbry.desktop +++ b/packaging/ubuntu/lbry.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -Version=0.3.20 +Version=0.3.21 Name=LBRY Comment=The world's first user-owned content marketplace Icon=lbry From a9fccfd4ce96620e1bc3d8759c8d099ce3855ca9 Mon Sep 17 00:00:00 2001 From: Jack Date: Tue, 23 Aug 2016 02:17:32 -0400 Subject: [PATCH 80/81] fix pylint error --- lbrynet/lbrynet_daemon/LBRYDaemonServer.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemonServer.py b/lbrynet/lbrynet_daemon/LBRYDaemonServer.py index 775761b41..76af569c1 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemonServer.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemonServer.py @@ -380,9 +380,17 @@ class LBRYFileUpload(resource.Resource): if os.name == "nt": shutil.copy(uploaded_file.name, newpath) # TODO Still need to remove the file + + # TODO deal with pylint error in cleaner fashion than this + try: + excp = WindowsError + except Exception as e: + log.error("This shouldn't happen") + excp = Exception + try: os.remove(uploaded_file.name) - except WindowsError as e: + except excp as e: pass else: shutil.move(uploaded_file.name, newpath) From 46153a8991174a0eb928869e34736d8251ca6d79 Mon Sep 17 00:00:00 2001 From: Jack Date: Tue, 23 Aug 2016 02:25:32 -0400 Subject: [PATCH 81/81] second shot at pylint error --- lbrynet/lbrynet_daemon/LBRYDaemonServer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemonServer.py b/lbrynet/lbrynet_daemon/LBRYDaemonServer.py index 76af569c1..7fb6b0519 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemonServer.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemonServer.py @@ -383,14 +383,14 @@ class LBRYFileUpload(resource.Resource): # TODO deal with pylint error in cleaner fashion than this try: - excp = WindowsError - except Exception as e: + from exceptions import WindowsError as win_except + except ImportError as e: log.error("This shouldn't happen") - excp = Exception + win_except = Exception try: os.remove(uploaded_file.name) - except excp as e: + except win_except as e: pass else: shutil.move(uploaded_file.name, newpath)