From f48c1562f3efa8a95bcee6acaae14aea72bfc697 Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Fri, 20 May 2016 19:59:49 -0400 Subject: [PATCH 01/28] better log names --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 31 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index 0a10dff9d..58e8fbdde 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -397,9 +397,9 @@ class LBRYDaemon(jsonrpc.JSONRPC): # self.lbrynet_connection_checker.start(3600) if self.first_run: - d = self._upload_log(name_prefix="fr") + d = self._upload_log(log_type="first_run") else: - d = self._upload_log(exclude_previous=True, name_prefix="start") + d = self._upload_log(exclude_previous=True, log_type="start") if float(self.session.wallet.wallet_balance) == 0.0: d.addCallback(lambda _: self._check_first_run()) @@ -579,12 +579,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): dl.addCallback(_set_query_handlers) return dl - def _upload_log(self, name_prefix=None, exclude_previous=False, force=False): - if name_prefix: - name_prefix = name_prefix + "-" + platform.system() - else: - name_prefix = platform.system() - + def _upload_log(self, log_type=None, exclude_previous=False, force=False): if self.upload_log or force: LOG_URL = "https://lbry.io/log-upload" if exclude_previous: @@ -596,9 +591,13 @@ class LBRYDaemon(jsonrpc.JSONRPC): f = open(self.log_file, "r") log_contents = f.read() f.close() - t = datetime.now() - log_name = name_prefix + "-" + base58.b58encode(self.lbryid)[:20] + "-" + str(t.month) + "-" + str(t.day) + "-" + str(t.year) + "-" + str(t.hour) + "-" + str(t.minute) - params = {'name': log_name, 'log': log_contents} + params = { + 'date': datetime.utcnow().strftime('%Y%m%d-%H%M%S'), + 'hash': base58.b58encode(self.lbryid)[:20], + 'sys': platform.system(), + 'type': log_type, + 'log': log_contents + } requests.post(LOG_URL, params) return defer.succeed(None) @@ -609,7 +608,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): log.info("Closing lbrynet session") log.info("Status at time of shutdown: " + self.startup_status[0]) - d = self._upload_log(name_prefix="close", exclude_previous=False if self.first_run else True) + 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.lbry_file_manager.stop()) d.addErrback(lambda err: log.info("Bad server shutdown: " + err.getTraceback())) @@ -2084,9 +2083,9 @@ class LBRYDaemon(jsonrpc.JSONRPC): if p: if 'name_prefix' in p.keys(): - prefix = p['name_prefix'] + '_api' + log_type = p['name_prefix'] + '_api' else: - prefix = None + log_type = None if 'exclude_previous' in p.keys(): exclude_previous = p['exclude_previous'] @@ -2101,10 +2100,10 @@ class LBRYDaemon(jsonrpc.JSONRPC): else: force = False else: - prefix = "api" + log_type = "api" exclude_previous = True - d = self._upload_log(name_prefix=prefix, exclude_previous=exclude_previous, force=force) + d = self._upload_log(log_type=log_type, exclude_previous=exclude_previous, force=force) if 'message' in p.keys(): d.addCallback(lambda _: self._log_to_slack(p['message'])) d.addCallback(lambda _: self._render_response(True, OK_CODE)) From c5dc06962d12599879257d937771908debbe0d3e Mon Sep 17 00:00:00 2001 From: Jack Date: Tue, 24 May 2016 17:44:26 -0400 Subject: [PATCH 02/28] travis code signing --- .travis.yml | 32 +++++++++++++++++++++++++++++++ packaging/osx/certs/cert.cer.enc | 30 +++++++++++++++++++++++++++++ packaging/osx/certs/cert.p12.enc | 33 ++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 .travis.yml create mode 100644 packaging/osx/certs/cert.cer.enc create mode 100644 packaging/osx/certs/cert.p12.enc diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..1f5f36cd7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,32 @@ +matrix: + include: + - os: linux + sudo: required + dist: trust + language: generic +before_install: +- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; brew install python; fi +- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then sudo pip install --upgrade pip virtualenv; + fi +- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then virtualenv $HOME/venv; source $HOME/venv/bin/activate; + fi +install: true +before_script: +- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then openssl aes-256-cbc -k "$ENCRYPTION_SECRET" + -in packaging/osx/certs/dist.cer.enc -d -a -out packaging/osx/certs/dist.cer; fi +- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then openssl aes-256-cbc -k "$ENCRYPTION_SECRET" + -in packaging/osx/certs/dist.p12.enc -d -a -out packaging/osx/certs/dist.p12; fi +- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then ./packaging/osx/add-key.sh; fi +script: +- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then bash packaging/ubuntu/ubuntu_package_setup.sh; + fi +- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew upgrade gmp; fi +- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pip install git+https://github.com/metachris/py2app; + fi +- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pip install json-rpc; fi +- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then cd packaging/osx/lbry-osx-app; ./setup_app.sh; + cd $TRAVIS_BUILD_DIR; fi +env: + global: + - secure: d3glJxXC0goiFETAP0JxMDEQoSNlh1fRDR76D3tjYY4Brxh2UgvUvpNJOAkFApynciA3n8kva4uBsv52jwnKG0VmCy/meaWowouhyi8ChdPpg7HZL84oC7rz3bZ2OA2iuYFPpAQrd6p7OMhmiCkeqhRKtW0YmOKn1F45kaIMnmq+bD0QK3IdP3QBdGz0rf6TQlNSQLSJtDAP0+HO+NaawJ1TQJXHUHA8mKnbOTRal4bH007uxfhvthXysm1QRQOfthGud2q/DN+f6RfCqF2Fv9l7NwR5BwVKluQYgqJjdhk9IOU7D+zW4Ne2fSt6V1PRAASAfyhtDiOA7B3ZUw2igimQ6rWyWao1csilq0RW9EmycOT8S7x5YgXltk0kVdNizdQeHCDII0mOxkIFBF0bs2ZgTgKasZrU5jGnEhV42eACl9CGeoT5/ots7an+zgCBoQ8c4HGkMQyKV4uNnvKD+gq1FPDuwlHEUHhbjy5uRI4kAVMzjsCJX7XBSumFELuwiOpE/XguiJAV/LvjYbN6OFemJ6HUOhep6kgq7Zcoxh+UnaMixiq6NQTSoLpffatSxCM9EG8uBoXZJBH45cS9aD0eP9zjV2COCC0Iom5BQFhkArgMVodYtcrNevPUA3AuvJOjdJpSwKB4fqaU/CDqThw/ieZQlzaZqWIM5RnHf6Y= + - secure: n08IQBOLaipKzAwS5aQM+JfFtvLSCrWqFFztTMeElmck6IJOVnPlDvPYRCrRmrXtgFmJ1G6hEU5Vri086MkTaISq3V04ndquhz7nv93Pp1do42vWPmvmFHdMrhaOGPVtYZ4m4XfgNcA+NC79V9X2VQ+MoQgCRhcGlgGomxJBHc924bGxppkIrDMljvSipkR5v9g/UFGRPYJ6BIF0imWrdsHUfqhbZMdAxAmdlBAjx/ZUOmUyPWYttHGYEW4HpLwvkU2K+sJrt1emogvR7GT94CadtqjmRsiKh0Y2lHHhHkqV1J941T08p9hyKU6S4fHQqPHfpkHRiPeJQY8JiipCbuERn0YeGqsp5q0jRE6PFUxCWcvlWqc7XNihOMQAb5JQD8vF2Lvs3YYycFBgZkIOaZQFbemqkx2pnQMjVF7GCZp4u+p5yo8iBeImYFYdkqIBHqPqsKxM9aBfe05XhAmi/6jUP0L2xikVFvJZDNxKP2uyqXVbSMR8KF8v1eaYnsSL3vzBolwkn96zaj2E/lItHaNeNIXvutAsGuy2ybTXeabm5rTaUr+5cmdqx4JqmyGM3DinGFik/fzpLUJCd4GaC1n1taLy6aUUW2oH46QRqLLAAjZ4AWao92Eb4cnFiVxxBqVG3efWaoEt+uW7/hzqG27iieoVqCXs3Hd5g4bCYyo= diff --git a/packaging/osx/certs/cert.cer.enc b/packaging/osx/certs/cert.cer.enc new file mode 100644 index 000000000..4dc96d9ba --- /dev/null +++ b/packaging/osx/certs/cert.cer.enc @@ -0,0 +1,30 @@ +U2FsdGVkX1/oLoj7zhPd0imh2T8RhLpKzZLk4EHQV0GUJ1g8nwGvxWov+CUMnmjh +Y+LNdomGBYWoUilhe4JaWDUYepwIXn6+TvuWBdVEMGpJhXGbmIf+ncMXo6AP8Fh/ +g9x79SE4RJxFj3utc02B2ivVehoQno5sEvNSZMVml5n9skJoJUBbAsbp1p+7Hm5j +p2Z7UI7/qiih6TmszX5KQvOl/DPezVNksn1c1mUShuaBTjxbprGlr/LvtboR3mAd +8DN4yTGLLJAQ2+FNftM4rAedrr6db2AhQ8WxRmiwTfdubnEoC6zYySFq2kmdjj3S +gPnK0atx+ZihMp+S+GqMnvfAHEtb0vqxoq6nFcSIvcQVxKPyzu5E1kMriY4Oq3xr +K6ebc1NKJHjh7niaUmR3NImBx2h1LAAf/hcKRH2+ATEVczGtI1AsSGgGhUM34eGH +7G+m7+bIkgb8AtlaIGS/VVHsIZCNSgzwZJoNd3hD6ZV65Hb2yeT6Hhos88/05iFT +ewYasa73TqFm5SJHRwt4d1W9WVIJKJPDJ910p+V+NZVUsKOx34+vMNrjCrqW9p9x +gQnza2V/F6blIHTbSzIGc+MFbeHYBO80d+v5jVxheL8z6ollDVts1SyJ5rKJBY6c +quvSgmc/ltE0dqRxLOQJ9mAFbayuMIUP6CbRkPXp8GfE55UtUJkDilalzcpCPrUC +YJpuAI61INOQZZPEVKWW8L68/tLY+oEwWpexQX7xs4FUCblIFf20T3XE2lVuBHf9 +Bp9k7cD2m4mNrbzWOJuqrVt1pr176l9+VSP/ESdDFbmPch2FHl8HK8kgfJvkV+iB +kudmAmzI9DTUpWd5lJp6Fr/rLCMjslFDs37zMg4/E5ikKFSDNeYMtgPZhCwM83kh +OAktow4QAzh3RdbVZMFxaKk9nbiGPuBEsgvraPjb2gY8U34RC9R2FINIuTnJttLK +q7CKFTdbJIf+TIIgzfNu/c978adsK/qS68iltyyx8WFflcybnlqVgja192Ptqw1M +PXBQkH4mUrAeWDfmCPPh/mhO67Bau5u9Wzv/qZ2RXcX0dgXOoMa2sO6ZpR2SzxCJ +/XZwXnElMl+pvojLURDOV16fMPpjMCbzCN+hQabiTASqFNCsz4C9hmOquNh2t+V9 +8xvU/bnOM+/SMhahjYnvdhmRMcY+5Wv32ZnKATq88dq4T7/OZI7q3IsROZ7MnucT +x4vADvcFOfOdtPK35IFfMTfl+Ri3q7REIHMts2WEwXddf8CUiVeIaf8NgrWYW0hP +f9DQbMGKFcqqCHlKrQkv2dBKX/qEbIzN7T7535Ly68zyFuBS252gsLO7nrf+CLEZ +AROOfmt2jv0BvQ4MI5dslzsXFAU11tS36gOZ303R+NJVVqySkza964h2rH5M1F7i +A5p7w/l0OVV7r6aXkmsrIcsUZuY7QnZJORQ1MxNtK20weKfrqs90nMTklUVPc4V8 +LnAW6AYem0ZaeDHn2kx947sglMYxf0h/mFECGhif9hfDTErw7TkSJ26t9ByuEyEf +vGpp3P4iTXHUx7HSh7L4KDva6CP6slGjFMAFUEETn7N5uX3VEYeztMBdHLz0XHZc +PcgVZ8kytXVTEg95upvWmliEbQqWRsy6sr9PanaN1QY6re6RLlYj4pOWVm8qgCXU +IJVTWkROMlYZTWCibCsTsY8fk8aNObZamHjzZGvnU8nEGTx7xQJS8i0r3NM1j2Ka +ehBA+WfXbTplI/Fq8Z2Nrb/O39hQpGbXp4ERsEmVbK0twwsqVNehI0CdobxmGsd5 +E9Afb9iL97DTXsna1Il6FXnHvj3iAQsxxaNLIaj0sN1GaQd9N1mbxThlFNOM3wry +jI8TKCWEfLRQzykkcR3sMg== diff --git a/packaging/osx/certs/cert.p12.enc b/packaging/osx/certs/cert.p12.enc new file mode 100644 index 000000000..40828aff6 --- /dev/null +++ b/packaging/osx/certs/cert.p12.enc @@ -0,0 +1,33 @@ +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 +vOtCHu46xXuBJbDmz2mslw== From 7fca1f865d4de45de0221f6451ed8bc58623b5a5 Mon Sep 17 00:00:00 2001 From: Jack Date: Tue, 24 May 2016 17:50:01 -0400 Subject: [PATCH 03/28] update from master --- .gitmodules | 3 + INSTALL.md | 36 ++++-- README.md | 25 ++-- lbrynet/lbrynet_gui/lbry.png | Bin 10651 -> 18944 bytes packaging/osx/add-key.sh | 25 ++++ packaging/osx/lbry-osx-app | 1 + packaging/ubuntu/lbry | 4 +- packaging/ubuntu/postinst_append | 28 ++++ packaging/ubuntu/ubuntu_package_setup.sh | 157 ++++++++++++++++++----- setup.py | 2 +- 10 files changed, 229 insertions(+), 52 deletions(-) create mode 100644 .gitmodules create mode 100755 packaging/osx/add-key.sh create mode 160000 packaging/osx/lbry-osx-app create mode 100755 packaging/ubuntu/postinst_append diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..a8c2e8de6 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "packaging/osx/lbry-osx-app"] + path = packaging/osx/lbry-osx-app + url = https://github.com/jobevers/lbry-osx-app.git diff --git a/INSTALL.md b/INSTALL.md index 6dc552bcf..23491e60c 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,19 +1,35 @@ -#### Installing lbrynet on Linux +#### Installing the LBRY app -------------------------- -The following packages are necessary (the following are their names on Ubuntu): -libgmp3-dev build-essential python2.7 python2.7-dev python-pip +Installing LBRY is simple. You can get a dmg installer for OS X (Mavericks and up) or a .deb for linux [here](https://lbry.io/get). -To install them on Ubuntu: -sudo apt-get install libgmp3-dev build-essential python2.7 python2.7-dev python-pip +##### OS X +Just drag and drop LBRY.app into your applications folder (replacing any older versions). When it's running you'll have a LBRY icon in your status bar. -python setup.py build bdist_egg -sudo python setup.py install -``` +##### Linux +Double click the .deb file and follow the prompts. The app can be started by searching "LBRY", and it can be turned off by clicking the red 'x' in the browser interface. -this will install all of the libraries and a few applications +On both systems you can also open the UI while the app is running by going to lbry://lbry in Firefox or Safari, or localhost:5279 in Chrome. -For running the file sharing application, see [RUNNING](RUNNING.md) + + +#### Installing LBRY command line +-------------------------- + +##### OS X +You can install LBRY command line by running `curl -sL https://rawgit.com/lbryio/lbry-setup/master/lbry_setup_osx.sh | sudo bash` in a terminal. This script will install lbrynet and its dependancies, as well as the app. You can start LBRY by either starting the app or by running `lbrynet-daemon` from a terminal. + +##### Linux +On Ubuntu or Mint you can install the prerequisites and lbrynet by running + + ``` + sudo apt-get install libgmp3-dev build-essential python2.7 python2.7-dev python-pip + git clone https://github.com/lbryio/lbry.git + cd lbry + sudo python setup.py install + ``` + +To start LBRY, run `lbrynet-daemon` in a terminal. #### On windows: diff --git a/README.md b/README.md index dc8b5c81f..cd1693811 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,15 @@ +[![Build Status](https://travis-ci.org/lbryio/lbry.svg?branch=master)](https://travis-ci.org/lbryio/lbry) + # LBRYnet LBRYnet is a fully decentralized network for distributing data. It consists of peers uploading and downloading data from other peers, possibly in exchange for payments, and a distributed hash table, used by peers to discover other peers. +## Installation + +Download the [latest release](https://github.com/lbryio/lbry/releases/latest) or see [INSTALL.md](INSTALL.md) for manual installation. + ## Overview On LBRYnet, data is broken into chunks, and each chunk is specified by its sha384 hash sum. This @@ -18,9 +24,9 @@ help peers find each other. For example, an application for which clients don't necessary chunks may use some identifier, chosen by the application, to find clients which do know all of the necessary chunks. -## Running +## For Developers -LBRYnet comes with an file sharing application, called 'lbrynet-console', which breaks +LBRY comes with an file sharing application, called 'lbrynet-console', which breaks files into chunks, encrypts them with a symmetric key, computes their sha384 hash sum, generates a special file called a 'stream descriptor' containing the hash sums and some other file metadata, and makes the chunks available for download by other peers. A peer wishing to download the file @@ -28,22 +34,19 @@ must first obtain the 'stream descriptor' and then may open it with his 'lbrynet download all of the chunks by locating peers with the chunks via the DHT, and then combine the chunks into the original file, according to the metadata included in the 'stream descriptor'. -To install and use this client, see [INSTALL](INSTALL.md) and [RUNNING](RUNNING.md) +For detailed instructions, see [INSTALL.md](INSTALL.md) and [RUNNING.md](RUNNING.md). -## Installation +Documentation: doc.lbry.io (may be out of date) -See [INSTALL](INSTALL.md) - -## Developers - -Documentation: doc.lbry.io Source code: https://github.com/lbryio/lbry -To contribute to the development of LBRYnet or lbrynet-console, contact jimmy@lbry.io +To contribute, [join us on Slack](https://lbry-slackin.herokuapp.com/) or contact josh@lbry.io. Pull requests are also welcome. ## Support -Send all support requests to jimmy@lbry.io +Please open an issue and describe your situation in detail. We will respond as soon as we can. + +For private issues, contact josh@lbry.io. ## License diff --git a/lbrynet/lbrynet_gui/lbry.png b/lbrynet/lbrynet_gui/lbry.png index 16c7c4683affa3e73c5bcf4fbd0b6bbb0778f275..6599574065ccfcc176cdd5e8726ca783f85f7097 100644 GIT binary patch literal 18944 zcmd3O1yfev7cM0t2);DZozf`{(%s$C-Q6K2@` z3^<%~*4cZlr#6vF3X-VE_{cCYFsRZ}Vk$5&u;5Er7$ijSNzb{+9DG7Fk(CsKd4c}R zZY_)l-+ALGrR@R(gK`i3fNitOyaC^Q?J6xV{(AKlE<7Ct-#lzB3=AoZw3vvR=fYvD zhnAZ5@8_3!b#ZLz&h$@dE~2k6B|6ewEbaK_8-2`=Gdy>=J+_zmTy_+87Pr%DB9hTD zld*dVS-!KY@ju*11{wBMVJi}0U$%@*DJCHoUVpTx^eAt=(K&%5;&t5}|M@8#?f;iA zV5I1>22OTq4I%dwo@c0rNXn1%?^qI@4dy~2Vebr}ce*vaAxj6JpYW_Qh@kClo1mhR zpmKkJ9t3_eew&|=n3sT>m+)^hUWD%KPM6LMlP8eNY^c$Z)Xy+99I52k=C^nV4E$DT`KU5j2}`o-R=4Iap@CD%EvZtk!Pw<<(={|C z8%?$iQyR1+8eztZzKf9k$*1y#==2-SZ1iGXntEFOe~!;^*bHjf;_-cAmuO~=?f%Il zsgO>ARfN-g2)PaoTn3D*=kZlL3@>c!w84S33WgAqw#4HZCS_s1F$aE}N{*CtqD&Bn z)!cubPxoy6AKpgymi3-Gz*%+Jh(@UP`rzId2PwetW)2^`FrS_f>qXx&XjIwja&Vt4 zcIMOK5wnPjD?>zAPj)1`o7~O z*JW^rf%LfP@k#x$(Mi?Dx*UyncZz~Pa^9G5;1_F`1WB$V`PG2&a?yLPjtPYs()sV-jjz1+xUgWBz#4(Z5DybLsLdh$V@Jh?bsw zWDd2Y%jZBQ(|boDNyH1=5<%P!>-ZG(=OOYxL#2+yCFSxe zgC_47GXkz^^C68;;j7et`Lv`|BWWB3P9gVt4bF)+()EtKM9Fg0u`z@Bv{*PeDuw3q z5@Mkfe3YR-y*vnq1kPqKB`8_h$gFk^bbEM`;( zwz|A`c<6Yk4Q|sbw;Yo&WF3L)SjQdA57W>K?U>zCao?rF*f?b!3GLR>U!RdKOJ z?Iz=?Of5m+zJ1Fx{{f*RNZIuB)#1?1zz%76}YAXWqR+Ls|jlw zCn?+wvB(kr%$wu6piKSd@j_jsAXr$P7C44eI!6U&@xO0Yz25tyuDWESL(2M2?cDif z|CZA@_cJl+HE&O);uOgH_vj0Zl6#bMOU zAD;C%(IKOShghrAPyJM9@OD3v5zAUuLz@moQVEnY%7BX;wamGxZoQC>ruPTW+l(nboJ7Dn@FrNn<~I%+OwJkyt_a`q0HdgI?HN@#H5R_Ga@ zrMlV1#`M~zoNhK&*mbDtvW&l8g&Ns_4xW_=pxi9SQnhsa=Ex@^gTkn z(5N8wJ`t3oj;GF3E%UVHN-NeW#`tu9vvE|cTB}0Wgpw?)M%q^U0M83sba8JH$d##g z^6wRjcC|hHtFSQQx&`_l7x?I0fw3#zA=ZfuLKov2GLa+pJALKgCm@juVZ-`HG>R{NxcI zZPfZ%Q=@%(>f;`676fCuc$mNPpx*v0mc-v=WsL_*d}2^*2~z{9g%PI@m8h$Es?aOd z#8)VgUI#xNN7U6fKq&NH+V*N+Q{42#Z;l11x?lf9<;&UaMr_u*n#_u8qa^Ltc8 zb2+?z$;z=0HDhOkF$BIJEr?&o5_cavT6)`w%M4Rv#!H~>)SYb>n@B!_$64L)qsX-v|JyjefdS&mSX4xKBAvR;*eqYMTtw8tUiMbc zEBX~iJ-NpLRV*wVZ^FBT(5bDi!>!()weDmtcMi@Hb=3^^Gl=iImRQ}U;WKg!FmKS+SzCv1k zk~=ygp9n&r!+Q(^#Ep5iaZ?EFzp%~rzh{d|_5Xo4l=W{dX^K+K8k&?`v5Fgqw4F=GeFHhz}o&}nw ze#+ZetCg34X$OBO8lk=wgVZx*c`Ke}v9`RWespz1$lp_N#5ebw8!=BU1Np$YQ7MnI z;qz;Wcldt-4Fu|VSD2C-ymrukv$+2; z`f_h;m`kF~OF`}JD}M69%VeF? zo#2*&d6ny&&6Wmb8&{m42eCot5Z@eKN-f61=KgqgbzR^U-{*?5-;>-KQmccn*HbQg z8V4b-+VuDl77u?$PSBp<%yUZ8Fyp1Bt!=Ks<)T?;+TW&iOi4qLua@!8zUE(5a%=KO zC-UyD`>BR>)AK~mJPUJYZmO^?0$!y+ObtRfp zCUZMZ4A`&WGNLh~T;az5p2`v*p>{B$;E#+WOXDEqctkcj{edfqJ^7_WlKfY&&$=(tM6hjCcfY3heECL zFaGEr-cIp5tsD^M?6vy7jPk7yCc5gx+m$Q6lEXi{=MLRtyEscnB*4VzYlGL zr1+Ylf(emlX37?@MgP|>rpvnC=DVcQ2L%%_WL9@%SB{swTm~r_BUf>boi{ZzanU1i zp$i}}@GU8c5*0}?S~Ou+C9Y;~$IaRp|QU!+J1NtYe20tedp@1CldRLk)KpjMq-`-_AfC zlVS5{R{QPu&lVW`%P}6j8PlcuX@>1LXiwg*vy2xXR~4MuT#_nHFGlq@cXn*(6;u|{ z{FGJ%hrQkCQTL}OoOgXfAy33#ZQbB*HFLSwm}b@a8-Gk~tv`fCr_}g3k%pBwyb|2 z&C4nn5Le_B6zOzqIUBR!A)OEn9$?apw>VMAmk>ms;;xQ~$AzofkVCcxK#gY{Jpvlx zi!d6&gPZPbpYO%iXk#{F*CZwFFFmbObb{{2YJN((oFOQ0xRd0RS77nWamM<0Z_j^; z@zFEZtt68^Ew9%cxiyF&3-NoO`@wZSE3)It78H%aalsYq{FcPs>O74We1NA3q%QH$ zadwGZCg@Ib*^bLh%-0z+kcrHpRYOL~{&b-6xL(dZ7{eQj#?4k;t<~Z#FyZrAYHXO2 zTZklAA{D_s9KI$A_sQqlXz~wwt>xt0RMhBqF&3N#(om`KU<9#`ERWv8>PE0Hm1E18 zoz^u@)MxxcO!|G#w-kT{#_nIXj^N;P7WF4g4T-D|x$GERK_bD}@5kY9o#i62wm?Rz@R6I#_asdo_0u+T2CIPWCQa zO{&Q+EoS1MHn?T&j~}%@;HYz#(;_P*MSpQSSQL8qckI%T&~#_B+T8}r2aRZb@ah0; z>@_nsD^UcEG*oaYzb22*reZt*{9SP*W#wQskCLY8cgDO<51a`l2Ow1B!h3SjIf*o%lX^|o}Ni%cI297ad)OgD;6 zvay|>pP%1aHY6jp$We=a3VuJ?PGYB9n{C|Mq=V|mrbHS#cS{MLD|yWaz8ygaK3++? zFyQvq>951TQBa)C?X6;s9j7KeDLSj!>Q~*}-FO0)*_mAr80aFyKY6bE`)gkQC{MDp z1CpP%^liX0&u53UOVkH_x`I(2pKID=F08{Luv9dHF`q1`8VlC<4Z>S>5+b_W>Jz=CP z(2yqcnBY~)<4KO}&t%Ey+7U6MIbWPZ$F8!EiB_E#JDt@YAo4b%2uw-A^M~T)`!jos z6^!u`G1-Yt5vwj_J3~1Hi6roTuE` z-_KPpU`PIQgX)urF&SV=wo9$yW(VBP%$(xv!Q=ReB^ERi>dge)+Yw)agc3$MpWx|#UGT%Ms0`p$~GjWdbgYXSa8y|4uG?}_4 z`e0D=t(lH4wFnD)Oh0!b0xVpYhil&(p#U$vyfa^!Vvj_(Mm?amoTlBFv;I;f8R;Q+-Sxy{tEtf@!5+!sw2_vUotmBi({S{7{;omc z&)63j;?rv?vxWdQpOMF4I1Ssy;YDnQfzg38t#J{VfnUF1AGe=8ig!;c#}tUh-g>=y zRklnxuwHin8sH^uFyogQ^Nq!2{jn?<8Bd=e-6#gEdOVsUo&{B`upZf@yGrKv?cKd8 zV(wswa4zE2U9Yag1Vcl#Ab}fyyB}Oy40ak2RB_*w0u09$^ z*RezxVwOiP;V|+I&BWpOl#G!$%)(ofW4%$swy3PE%KZWYmEg))ZX%uf^$l)=8_GD* z@Nb34bW{vn_9psj93IC@6b#(BpXhp}GYMc5knTQ3`5V^F*7FhSwYg{9m1@-3+hE~D zZ#i*a{z2-pa(0fV!o ze<4Hz!N?l}$PW@QiGf?z4lFd>vAKncHI^efCL8N#XV)5bk7$KcjJ4x+C=?W3k-Jm6 zSt7iHoclAyF+=(MH7D$eeW%f1+nImCB`&$c0FA{AtTEfK%L+j=CA>$!8^(N^e=xo$ zChi-DeJa*z{4VI@nxH#p%k}#x#`vH@KQ6Q& zuMU#GO3VbO0}_x32bav^i`R{KS@J9cafbO>w zm`6Ay#MI)(hJ^1KRS(+DoKX#!G`|)OOHfz9c5|8An{6fd9F9iD(NJ13nwzUfLBWs) zpysd7EB+YF`dw-XCodn;;8SJrZQ=x*5>&Y0xnN4_Dk_&+27=XX=6p^e#d~{rUnSJ3 zeyhlwqaa%<)ibfvd@Xc{wb!{eo%*@aXf#x@1YO1{Jz~gqTp#Go4*019F}}nmRI~je zIMuLuRFQ%1?R~~L%B41!cI{w%(40ddUkP|a2XZCG3%p#OEEOdi`w|JbZ=D8IfR;o~ z#fX|K(_f^XY(yV7?n2}td(>ym`~yhdSz{@(U3Vy(TRWdw@YLShI`tY{^u^p7ZQB5m zNV{@sR+&jTop8gsF}c}4hyT=Pw?g1IXbU#@cL8GiF(k~fkh%(Ua zc%ZsEnEvZpMlF+qs)YeI;d=DPi$Bb?r*ld%i0~pu|IV`7A2U`2 ziLFNBn%ytL+h5?tmDRZCTpRU(`nNw@++8Gs-oj~C4c*E%d1mI$ySuDy>3p#S_4XS_ zN976~1%CvA9GK@f^rPjY83vT(7u=jn!TlK?Ey&IejKZcbGT|(olAfVY+?&rw0b^XT zMxt^T+-)P;4YOhU=;Br>&;tl+lBC@PqJTO z8bHVMayLvF?*%ML;U6yls(QL5*yuI-cP~9zbW4EFe|n~Yi_V~8QZsL<)-DL|eP#N1 z`V$bzo#%EN+sA8ii^m#NQO=`?pZ|ldh;I8VA8cVje<(1ovxc@A`#4~>TqpaL#VigE z)abbEKtmaX{?H0P;8-TFd-M_83XanT4_AxXGY^uX@54ii>(e*Ui$ zA&C2FzPy71P-)n1n#-MpM_9q0*qqrdqfnm-m#aA>+uaWDBTI0n-vdOcYJG~h4M8-p zPGwM8G=kxs?60X%m{3Tq&CGYYT!~4B252Mv-#c74u%;iHb)W=97oZ|ADN4@oRb-Hn z%csi>5?MoDvo{zT>Tp;+mf3&8iWAl~icfHkfUJ9;HAF=UW5ThdwI*}g(fc)fOj*Q& z-FT%uQMQaRa5t1hSF595AM3)CAw~BF<*0X4nF8r}{Ml>z5jS3#!6+ty4F!)e^bZ|1 zoIFwo>D%ckabP2U)&mBH0}j*r+#WlCsX(1le;?pP=kvtt=IOIv%z17jN<7LxJ z2~oS;X%$BorXdS;_kqdeI-dTy;k9<)g`(ANN3fTo-Jr)blj5sTh?zU_rw6^;%dU$D zf4W#zL!n)U>5&fyeMUW7J7quGkC`*^<>6TpaNIqN72Zr{?>d+U>j2F`x3ge*RkdZ zwH;4h)?#aICW1XI5>3NFnOt8Wm*aM^efw}%jH5k$X3}-+ z2nkrH%zENT3q=vnKO}a|{GB|)VLcvb*!o1>nZYX<@8j60*MRj(T-mV^3oE}TEBZMr z%MdCDOtRMiph7CZ9{qLrt;*ySpj#~;y)Xot-RUF|R$cigO9^5m9)0~H`TT>EyPW{# z>8cFSlm35MfJ>*(R{wH7LN`UkDpk0wa)b91qU@}Lk^H8H9@AC+VekH5AS{0#&JJqf zbgQ-&`1*a;wMmaBLjk}$rb3j$p~#xs7baX_(Uct$g6l6KW^6q(v$IEEmVIEWW{Z9n zB%s57i`eZK#uNfU3V{vp?gTi-YNpKUP8+V_O^0^L_@F7Cmqe!{WX9Y_Mb;bsR2AGJG2ELdti8w>Av~Hc~mVIs_0{l6RsZ-9)7kMD)V4jm;hgh_GDpb2!)xM8Egy)K|tvEY6X;8eop&)BQD|AGWScz zj!?iQ74Vp9zY{^=3(oBGD-@#C%txb@>$02wA^j4D>S^-iS2)yr4F6jDw?Dks-7C|r z&Hqy3L#Ebxj2sCn*DSs+7_mUIMFTcEPfo@QI9r>;_?P3r{t{kyZd7}!=(UxQkse;- zK_e89xXC63GfjRLjff*j<*%~4<8_xf6aj7(jF_5RY#bcq@^X04A%kH}-QqzTFY#8B zvCoP%&1$OUV-$I#`vHz14vN2^;OfO&TPc~xr^nsD#hB_0noJf5q{{LztAebjzqS5T zsXlQeJfl$@&-e4Wzur}-tkAbos{dxs%8UfmfH|LC76xv+?YR^L-_}<3(hC5)T3TD* zV=P1i-tBrqxOsL9)!oxnsn6;i+9=p*!YvXz0fMny`UQ*bN6bJ=535{&)M3JRejcBZ^^FNG9TU!R6#vhmIFP!5jo=BQB95%>OPUmD~9+)(H(Xz3E`9y&z0>{dEK zCBdG$EmiyBPKV5uma(cpl+0p__p?(UY5G@!0C;cy*IC0eLPd-O5(eKQ(B^4F}!I-5xI*gGo@KOTUwhuB1goR z3Vc%X{B>2bo|Qo?SU^FcqJz>uE@;SN=NaC1xhlT)>s#NJ zRsjmQ=vU!W168Vp9|QK-Si|38P8MT+MtnD!glI zr~R3~+$cl%-!cTdTM|oo-0wLWy)x_4$AHXjHdO#1Pnf+(Mv+GTAbVptuqJrit_h=O zE1Fk`v}|ajM2&ipfyh)=TUuNvieyDU#M}-n9>DMlv~wlC;&c3W_3w9;^92qvN_olE zd{ktIDVwp!93W`C+zu-a~w^jFN#_xsg4Tyz47!3Yh@_};PqOBIFN6vP> ze5v`K&y@wNf3NK@Gm^Gv{5QBvj<>gwh0cD=@sSF43klSQA~0)#sDpEK9b=ud{oy$B z_!k3d4lGtZNI=f}a~_ihST1pSX($9QtmD}AyXym*hiVR#v9x{)_VPOyvsSOH&|PcGM(OJy=qUq_MI&^3$A0VK%=x6qO;;#~thC%PJ^ZaO z-v^OA16bl@?0jj7XZ3ZA=QdA{yBBfVxam(zD>lVCmQm>$iR1BH(6syOc6WV<8qIF^qla zXip$ER0aUYm(_y_MWeivG_=3(iWEVxo6|Dbl~RX&TGrge?wTr<(h-8%YlB(xCSb_6QW@ z71n^ZW7^+mn`8lm2%gt(VO~#@T-S5mP+kPFFTIb6=p2f#DuzO(u7h-d~ z>hq$ke6N8k+Q9&NPCjkYYO{DWP@ajPw*UY=AdC^98QO2X3tC!R^;W@HVK(UcEx?MW zzJH8M&<6BU_YwTB@A)I@3l-=vZ;}m_gaK{#y`Cccmd+biS$U$$=`XIF%<1M_9Fq6C z^#w&9rng8xm-prD>@{HN-IG@Y5kgMi0l_h@`^SotXi`E2=MeoWt-G@x1-Hue39;ZS z@xbXX7QmLjWPxlQm4M0SsQ$3Q@AS*z55wj)B~oQt{>|Qr^sam3neWWy3Ps*a?YKAU zfwWEHOq{NEn?EK-aTDR4bf$QW^$eaeuY8v&>6YgvP>fPY^o zXcUW&t27Bk0C}1GX@yB0gEzYR`~+Uha+^=ApOe$DZetX~>=HTUm`TXovE%7|&zpSh z3zSt%vYyUxH>AHk*4W}}mv=)If)U#l+~|2*jiW1UaVHA~{Fr>_JQW$}VEXmzITlqd$fct$ zr|8Pibv(>Bl;{32vGi^T*HG}fFBGCysT1Ze;0;Z}j29KDxs!*iC7+wk40($Ov$yLW(9}gz3EdL+9EBJ&{^&Prw=<>!wGnDb4>si| z%-On^sG(7oX);To;!6Q>c&JApQ$dc>d6pQcQ51YM{ka8MAgnVO1RIL@+J%!Yl09;V zL^oYcN$ZcpazGMDynTqdc}awd`_QX9>C_Y(8TwNaM)K&;==(DwOA`W>l8}j`rhPe!_IQc07nA zVd5-AgPA!fzb-eK!AJPfD{8lum5|^2ZQ4@B8}x=K5VxEu{uHjk_

U2Ho%8h$Z1A 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 O6N9I#pUXO@geCykjvm1P literal 10651 zcmYLv1z1$k);1!YLnGZtr*wx5Dc#)&2m?xYO1FTNNGk|P3rLrgN_T^FO2@y)d;jnH zJUTuz=j?O#+AH4oT`NjmO#vH&3W#cS9c?g#0Vf$<8j<#;Jop|TP5Oi;3Bmxu zN5v(fijrf5{AN^Fg;gc?^?aT(KiG?A{5-YVnrpoBim zwIw_eL5dfkJ3Cm6Q)56v59>aD;Zb6@oPGoq^F3aTk>sllloX0C=P7le#z#jFqwz-Y zJUOkNKCZl-b1f-9e5>$FE*8 z0);q+f4}4PMb&Io3D7`rF=H}f2m60Q;_28j)Uu`E+xo&V(ZS=T9(Y-=m zzL;wul0a?kjWZV<#X8r0_wQ$?6Mb?>SlD=L>HbAM*<>Z|uk0CpYiD#yXXj+FvE>?} zb33Y@-bC)ycWK8j1KzxTWAV%#Wp8NsXRRUueCx|8Vh56bZzt}hh1`$nDz`orRKztg zjaB`$q%y3xmxgbk&VL6jCQde$pUP#a)ym8AXUDE!pqqfFDmL~|witR$ImfU1H;(Yd z)N6$Tnp5K@?jQAYdG()VhBt(Y50zwDzUY;f8^Cux?07u#C0zY!_jt9{GR;?+lJk8< z1_nroqqms?d*Kr^9Q19eoI|5saI8UM3Qsw=8^_U%mC2i%$;goQzEpGPYb;p*~W5E8=>Pl1GP%4(Jv+| z_EvGQuE-=Pl!gJvTiES{VG=p~VXg!I~0Z-Hr_7-Dh?flkmP9b_qK8TGt_++elelWDAX{j}kTH`BHA*Dp; zS*>=a2a0o5|13sLi3$_g8I&TAASdx{)jrJYl)2fH`ubu-BDNPr<_q!q9MGQ@9c|aJeRwa9euRL#( zzf^8eVOFBIu`x}jEOnbJez~S2YL7;d0Xfjdw|$+v-#3VzQrVLA?D zn$&~h;@mI$%}q_4kAzql2AVTQnt1>2PJ~YkWdhfZ@_DnHtO9OiOlY~i(chpB!qof1 zOP*dFfwE~d6na7DThtgom*PRkUN!Q3gIP zch#Nr=A2>@1totOMq5+;?A0`6NmSJB$JBGh0m$9KCZ?E0V!m5V4_`Th(^H+C;Sc>{#Z(R9xqpPsrmMi;h9lxHTYjETHr? zGkTckYJU@qK=NT{ls~ijOFWLTle3Vb=8)ZpaZ4C93TungoxD-7(98_P5&Zf>kBw>p zZc=n#&CK4gSSk^uL9uxuZ-i2I>#cPf89Ld?8Tah-vA$_iHA~h7MMRA%AslOvP=2}= zngpM?$3Sej46W1VP^YCH z$#vp)Dbss@xq;Q{2PjiXuAxJt>kMVKAY7njmjL?GpOt!jEAGbKcMr}s3S3oj)j>@romg>{S5^+k@d zCkB*4$F6UY|5D!B;{DKcm8#>VkFSOOLqP56lhc(hR5_(Jy!7nYEiO~H$pn#i2ZV`Y z>sz*_Q~~Fh?P>E%nQ_X>%9uEl?_BF@igLan)HCk4*B9nwqa2iJZL2+sl9dpMl$T0w ziGlI11?aDAM1knFw`0oyui&H#?Ot289J&7aYqB@nLCeC$;mg+|QDlbrO8513mU2uC z@Iv~PFe`SCHxb)m$F(VrnG4%brs-nr41ZPfz9%Eg;BlZMHZYoedMZvWPeq$jI zWM!hx9iJ=FbrKqJ^;(Pl;&r6@)53*8dT@&Jz~LGW7D0sQurILCz;083+k;GRW2pYk zQGx#UEJqN;+VkeLJ_>Pvlq3wad9AeHIf(du56L+xp*jJN8sA({)l^UXnh3uMxvI(% zO!p|khi!Js!;@j)6-LQjEGa*zgpO)-pNMGj9}TQ&@vi-qrk&2L&)c46ilqn!&hzW5 zxBG08(vKoX;kT*Kq&Ej*C3jC9x7C70Vjd%$TT^tmbiSeI7IagwrLepdMS{euITl7j~ z33r(a$H86g;x;S`Eb4+V1S#{^SoKUyJlKspOEt`X{$j%3-_(v0LKG6JHzbKJ8nNcr zeBmz=6&hpR8%HM4ei4t8!RO4n&-$#ev=E60T@k*Mv3BwAzz<48#d1^MS;|65dE+VV zMCuK+n5@d0*_e@I&^A`D2GghCCx(TI&!ZeX8yoBLbLSfp%HyIO)G8=LIE9Xi`;-i&nW^4`F6r2T$S6?^k4;82Ba=YO+8r8 zk<#y9gWH-EKVm#I669D=X7%@WVr_kVYClF9uKcc}kb@Qq4ZxNcK>^3BhUJ`~g&FX! z`8yoNSZ572I`2*%{XJi1z(}U~Bnu4vHuZytRswdeYKt5{k%FSGEGrU)gxLo&J<_U0R? z1Hb!!_Cd5r8}jmZMdN*uW9xIgmHEDXYAz-D34zk+nB8cVxC8U*Md8QWzm6LuXrHwd zR4nmj8ft*0d-~DX{9AtZHHub<$rO2A+>6)uui~Y*kaL;pa=0dtAX+fS4+=M3&Ts`e zDBXc-=n(ef)KQKQLCq&31J>WNqK6Ub5W^q6(;+)eLEOQg3HwaUQ8hG1X%&fGL?mBIWK7&`$pm25 zf7vFL-Rze+!@wUVGd;}L<*9~Kn~YZXv8s`DH&hk|GTuE#*hCqYE?ii9V_CWc`ML~;l0jzl1wyLh#`M9U81HQ*%x5{JJQ0nO zcO^6!Q=%J>&hqxw-yrGdPt?}ZN-;5F->8-lLONzCQ=vyyz#+S2$%O6d*1kupvvk)- zf`c2BWSspa+(EI0rsnW6oF{$pRBJ zqWAp?qd<6s#S4@<>t3zi_zrwMJARkhRA4^=z(Opv^pSq!P+QD?8MuVOgR@xm{Znfy zH*{;e*??QJrlKFhsNW-`KkI^!={^ch9m-JWf@dnyRw6F5R?A6S-^Y*3kIuUj1N@0Z z(p@mDS1u&EEA;Zq%Fun;Fum9_Ua&=}vo(4tC>beZ0uCq?BVWf`;(`%YB!=>L`>`4I z=*#q3dNdmwXLD}+^TMwjmm{H`h5SUnqCogvrq>YIs=SB7m-roTZK-81q%`;=qi*J- zgBCl?v&Wi>&~M9_=2S%$?$?Iz-@{LGWib@7938FOCH-q=9Sb3b1)m);aq~&ZvZ!72 zH}^=K%Yi;je;?AZm%uwdWH`esfhQK`_X~l8__W1b9nM4(0*Qdz%VQ&NjZDnK_%Ard z`%cyEo!M|BTjz*J)&+MAoi2h}W|do}STVG1gm1x`m=ub-!c5JjWUEq2B+tk=O-d0R z%KSB>Uex_W4j=^S0u=0*ZvxCx(6oZtYq^=dAuoH*DF@JI>RV@#bMMIk>N@&++}Xa^ z>cux2oze7zE}uCINdud6Yuz#}kml9q29Kkbo&qXoy+BpN*Y*7twirOTAUeyV4KCFZ zPmx5&&iR}@_p0*scUB@MudkJ0l&r5Dz1ScnjLH_{BariV@9clYu3EpHwe$2r2|!Qi zt|8$9cKj;u&VK6J*SOM^dh-5AmnN`2y4mv(lJ^x=^t{Mo1b}I4=Q932F`28Y zXLK0VqAADsm2KdOZFVm%uf0BM-JB~0`d_jodIoYy{Jp>PW!PVcG`Ybw-&|L8t*ZYh zR)%CpA?%F-l8?InZJ0q;9JxUE#Yj9r|6hL?GcRJS?~^R9`U3tBT?|BE*?y8C0Yjva zo3yw48KEoI5vCw7uTMf6Yh@Q;HTxxVfS>)ZYiN9Zy^;wRRi4!Md$J?I8{x!D6PL1g z9A>rRWEF=K5mEd2?aLZ@;sii+7HXfx!=ikPOQufbqEN`txi>)TqX@5H;tzVd>= z$uBI&@`)q28cvAO*+tUK#03lPAK~puCPxieJ?|QLF89VQiWPQ$LR5Z^E4O%eGc;RX z@EP;s%A<-;a4n4xII{XlE!K-X@f+K#v^wSV(nXG;^vla*dQkIauYXx(2)$&;?x71u zp~Cyo2e5H94+B;i>P+!=HuwRMR_+^69kT?z^!4BV{%ZHY7W7g{+Vm{zS=U(6LMisr- zS8y9h8N{*2i?wyC*u52p;-mF>EVbLaLTpu zE6kx;LNuhR42x(Rssji{cy{dE$UGMFNUqI$91T<37ddpE_jtilG!Vbf0eVauEm70o zUJK7%_~s^Y56a1q$$FUNk3r!p`fEnvgSmR01Hl$=u0)IzOe#7})}HUO3P&W7blWp^ zZ1DA-2&8&^_`O(c+79aOeBk9-;)dDu z`Z;hB)G>m!)j=sTvOb0?j+&FrpDo@07MhFZ4(LV$@V_(VwEaEB4 z?TIvUL3+lu4>H%$xai3@X{benP~?P|XgFwHzuQ*fGt(;u>D9HqZbkkMdvy?+>`DLB zmK?+npX1m+=H_krC+NOTqRqilRE(W{wKjqF?wZbREP{1F)&W^l8m7on&%|Ka_nrUt z>YT&wj`Rsxry_d@s7tj*-rWX!~dJE zL$LPI!JV(1l64}(VdtP;rOC>01q$6LF>L6)XVr+x5OQp0$zJ= zEcgM5DdA0U$3J>wl&;HRNMk}~#Nl09E+xS%LnpsCVl5VUoLnOT6vB9U-FH>l!l51_ zsQ)z5!Wl_v8Hu7~5%gk@$t-BeR;OTPY+8!uu#D*DcVwtg@)b)*H|90dHxt$P>d6c}! z4x%>B{xH?ZbGW%jLKL^JC@SRNR>u3q!?JO9CRyrJD7y{r$29_tvvqVke}0E?G?0k^ z#1AQ_VFuI+ZGmd=Y_1k3!g*zPV_>i@6Ofik)bM2(tXk$~0+9COb%ya@6O6&*VU)gO z5XKlgAC)gE1rgYAy%w-w-fogcufJqK6IniQ^()TmHgI~3nnZNCc~e@7wi_zrdUlp+ zxV;3-11jX_BrzyWUiFQX2p|=3DFwVxxf!*9yIq%oQSN3CH84oupDD#4<^wG!GhjpZ%5?U^uSGbvxhocuNOmVtqx#;buzVb6a%-hD&(9Plgv&_l;K0$&4YWy_?>^t6>U@lohNzS zL}iTkY~KRQ%!TVU&rW;um0MCv)BoYB+rax_hXLN~Ca>eHHc=3cVHIYBa#Y!vg&~~J zpSu#ra4SX?7$)RMgo3L4WudXdfEv*347-;h;qQp;f0dUhMXmjeVs}Erk=IZ}uMr@B z<`i^DNy%+DtGzg!9cO^(iLiqzwL39J#Xuh|VY94ZVyRCq_=d1o|ABf!!#Nv*&*# zL#B(V#ON^aAe0J2%gjj8=h#nfC&o@ZA1<~kB@ExYyb&>}v`DQ>j?- zu@;n-Ma^h2bxB)?!||GD95^D78C0}5ULX9V`o3t|GE_b)e_{(t>a@}OE~^#9rhxkg zr+eh)??-Wyn|Xd4BhFop!P-B8$gXCYWK2|b`6K+Z9v&52+U|YLYz$QPF&~p^SV6zx zfnQo3$JDd?;6^?5_HBPjvqpGvLqb|PL#}r;2U?o`!FKSuGxn!5U#5CY^`lEz5rGO@ zVNWl2TBI@JOipevG%9xNf*xzGHlhF;29y+*3Ml8+ef*IS`f0)SmOWZHcP2MgJl!rU z7puIwp>Bk#1s+t?FU+BNO#!P<({n0t{^B$^(YbmWKd+si&y-JG96;jUTA0=6zc+f7 zW|0+Bo`KEbhTCh5p0~JK!P-^KYczC4yz3-l>w8^59>D%H!o{8ikrhoNTCz5B@cQ*OVsDV#G9^=hNRi?T{aSL3IR z2p;plPSJ)t66;lW*u+4H_=B2G`Ww6g!FI?ZyD zY$NZrw8?s?afcGcKG?k}xss=&s2-kGai96|wbj$f;oxBJ)Y?CG2VwD_6$lc~zgx&a%X;E=xX zs=p`F5$rgC0^Q<6k)QOvpb^szG4ecgJMY*QbVTuWRMsDTUsg-kbnyW}gBSjoUL0}^ z@ED)h`<({V4hp>0Ec_Hesv4RdcbrFiaPwHT`93q))Mix|i12EDz-snjXn>%4dP4p{ z$#r5yLnCQ^X<0{y3Un=l60q*Wp036XZ>s(^cX22iNB0Hn_jxKil`k)j+xr`f^$)?W z^+W{;FwbIqV**Hj?fOS4nszZQ0szWWZYm2umO|MY;nmnfwB8ZbjQRy!aML@j&GGDLrJ z<|}5l|B^X(?xRJ~L zt_tvfJG^=HNz6jW8|YPZtA&XNv%?O~SyU#??FLSv)m;&*ZfZb@LtV9{YIY&7t*x!< z|AGJN)Y;oJj+&U*67+%NbFShXnnQ2SJ1|^cytbdyrEMU2HdFE}sZS8Ovk1y*+!_s< zWGZ=o^oj#ozg1=Op{iNp9p)bCA~9DAq9T& zpmTb=yoxl2zD9I!{87ZWw)iB`h{iaY0WcbiYW!@Gz~Gu5&B}uk!VnygM!4+cM@-)5z->rr zVxm8qTXe>PM5HlSEP@s;18$)8tap(=Ih)~g!ii~Ll#R?>Li}=e90J=KspCwT{m>g~ z!;z9rE(l-Z_WRi(-LK${p(pz079)dnvL|$NIVWZH*j?;qelt*ZMU{nJ z1;7MUt#~wUCPwNuiz^zEkL{!KxQo> zQvAK>4?29ywe`bO0?8$JGWx@-W+uz?Mh#W7T#4RfO7DmcQgtYXkUP_$p#IZ}>2}`w zhmC5a85c&EI-d$N5%gMB#}IH4NLK=>8lHR}#s1SL&b!+sS|_f@JIbUQ@dx&OFF-Dq z@2y_T4)1dW;_H6|gH#a`mZ>`DS#nbI@CN3Hv)#ewro&OOXx01`l|0<-oj>h8lPK_Z zIgUJf7+cbRDH7-f2oppBH{4XoK}2C+el}o!v#^;<3c_Erv!44c6q- z5?b~GO+qudu-9z$&o&?MI-0Fs)uuoN_-FvkeZ@wVniKJk|JgI1rTJI`0e;XFsDN*c zP>fR!2<{HK*#GH0c4;uIZp@{f(7gyMh)_rSH9rH?<)P+dccMRnAn$g>zyc+Y68p&Ix6d0ow=Kf*hIw%(NB3*{{Ga}KG$@7afm$QOr)pk2@#wKT#j zdBt)1I=#6RKT38O2?Q4c4Z8piOw@CwhdR(z-~f=_&K|X)=2e+!22EC3x$M+!*?mCf z%*!Ild$FmXm*C(-_oLB77am17VzlZjd?GFPaH;O?- z;?Z2P2XBR@ZsxtXFIYM@|L&%2&ytOUt!uLiZSNlfd+c)&QrW;!pmJ5-Vas1IVWR-)IEEhCf1*;8g=0q{`D82x|m?8xOWP>mYG{ymm|JYcYS_32Z+{OY5 zPfZJ9wDo`FU0&5&>-i==zgF>IHAZ?OZxW@nWf_1P4@ZV)lo3B(AC9As)YHXM^MqVX z3GG9ffO5?zomyNrwUZs!d2!|1*hq&A}G#r(ywLk$0*qM z=jwYfxNvO;vJ|NpAVv65bU)btr$N-YEvRLSN4^P`e(Gi1U#<#9dW8P98B&D6TuCx$ zaDgO4*S1V+xAqE%4b0VOV^3@*u8G56`8?lc)-F(OK9!~jcBD)!I@!$T4UYFX`0NezK zYI(rEwJ*^XlAdjg@9S&w3AXID? z-)Py!49%!P9US6&R^~xoxzy5e{Q9Nn>Zg_Qmxs<#9@2+%u<`)2IXclz7 zIGLvt(l_d`>^)<>Tl{i|dZcVkI2P2&6i2q=%dBAAV8DQbj~0cLhanl>(@T4?$uZJf zu~hzKuY$uu+;LMc@W6J_1jq$a017wvFhSZuv3TeGO;%Ye1^$AKkO2@tbRb;xDwmd> z=7+O(Eb@zx0pPk_d-!jQ-odcrRgQ>dQ38q28>^J0xPPzc_t=}Ye>6;&393vR2U_Jb;Ee0{tF}`f&NJ-L4CKGXDXe?oop)2 z91KzV4@971?)WukCq&&0>O=L6@$KMD5`1NCMTWMeZ*hQc#b=B440zrA-FpNl(+5BQ z9h9~X1DvN&v2{p4r@a8vKAoU@2OpD(3Z)4yje)*eYK{cl83>-09wBSq#Y~sK7KlJy zWxxqR`WR0)FxZxWR)&Sjo3I+1U0LS2Z;u2PKjS%2D*+eC&^1>?#d#e0(vlGc-K>C+AdRXASL5Ag0WtKJ&GPsDa@phNWUKlmq&kNjWJo zjITyW1%gx!RFhG`WEy=Ew~;p7XTV6CH+-%T9saVATSD-OB$Eh*Qh_l+^;jsB0({EJ lz#oNlK}QRNn5{lM7KLBRY Installed\n\nRun LBRY now?" --title="LBRY Installed" --ok-label=" Yes" \ + --cancel-label=" No" --icon-name="system-software-install" + +case $? in + 0) RUN=1 # yes + ;; + 1) RUN=0 # no + ;; + *) RUN=0 # timeout or escape or whatever + ;; +esac + +if [ $RUN = 1 ]; then + xdg-open "lbry://lbry" +else + zenity --info --text="LBRY Installed\n\nNo problem. You can run LBRY later by going to + +lbry://lbry + +in your browser." --title="LBRY Installed" --icon-name="system-software-install" +fi + +) & diff --git a/packaging/ubuntu/ubuntu_package_setup.sh b/packaging/ubuntu/ubuntu_package_setup.sh index 4be10c8a7..f35a78b72 100755 --- a/packaging/ubuntu/ubuntu_package_setup.sh +++ b/packaging/ubuntu/ubuntu_package_setup.sh @@ -1,37 +1,124 @@ #!/bin/bash -# Tested on fresh Ubuntu 14.04 install. - -# wget https://raw.githubusercontent.com/lbryio/lbry/master/packaging/ubuntu/ubuntu_package_setup.sh -# bash ubuntu_package_setup.sh master - set -euo pipefail -BRANCH=${1:-master} +function HELP { + echo "Build a debian package for lbry" + echo "-----" + echo "When run without any arguments, this script expects the current directory" + echo "to be the main lbry repo and it builds what is in that directory" + echo + echo "Optional arguments:" + echo + echo "-c: clone a fresh copy of the repo" + echo "-b : use the specified branch of the lbry repo" + echo "-w : set the webui branch" + echo "-d : specifiy the build directory" + echo "-h: show help" + echo "-t: turn trace on" + exit 1 +} -BUILD_DIR="lbry-build-$(date +%Y%m%d-%H%M%S)" -mkdir "$BUILD_DIR" +CLONE=false +BUILD_DIR="" +BRANCH="" +WEB_UI_BRANCH="master" + +while getopts :hctb:w:d: FLAG; do + case $FLAG in + c) + CLONE=true + ;; + b) + BRANCH=${OPTARG} + ;; + w) + WEB_UI_BRANCH=${OPTARG} + ;; + d) + BUILD_DIR=${OPTARG} + ;; + t) + set -o xtrace + ;; + h) + HELP + ;; + \?) #unrecognized option - show help + echo "Option -$OPTARG not allowed." + HELP + ;; + :) + echo "Option -$OPTARG requires an argument." + HELP + ;; + esac +done + +shift $((OPTIND-1)) + + +SUDO='' +if (( $EUID != 0 )); then + SUDO='sudo' +fi + +if [ "$CLONE" = false ]; then + if [ `basename $PWD` != "lbry" ]; then + echo "Not currently in the lbry directory. Cowardly refusing to go forward" + exit 1 + fi + SOURCE_DIR=$PWD +fi + +if [ -z "${BUILD_DIR}" ]; then + if [ "$CLONE" = true ]; then + # build in the current directory + BUILD_DIR="lbry-build-$(date +%Y%m%d-%H%M%S)" + else + BUILD_DIR="../lbry-build-$(date +%Y%m%d-%H%M%S)" + fi +fi + +mkdir -p "$BUILD_DIR" cd "$BUILD_DIR" +if [ -z ${TRAVIS+x} ]; then + # if not on travis, its nice to see progress + QUIET="" +else + QUIET="-qq" +fi + # get the required OS packages -sudo add-apt-repository -y ppa:spotify-jyrki/dh-virtualenv -sudo apt-get update -sudo apt-get install -y build-essential git python-dev libffi-dev libssl-dev libgmp3-dev dh-virtualenv debhelper +$SUDO apt-get ${QUIET} update +$SUDO apt-get ${QUIET} install -y --no-install-recommends software-properties-common +$SUDO add-apt-repository -y ppa:spotify-jyrki/dh-virtualenv +$SUDO apt-get ${QUIET} update +$SUDO apt-get ${QUIET} install -y --no-install-recommends \ + build-essential git python-dev libffi-dev libssl-dev \ + libgmp3-dev dh-virtualenv debhelper wget python-pip fakeroot # need a modern version of pip (more modern than ubuntu default) -wget https://bootstrap.pypa.io/get-pip.py -sudo python get-pip.py -rm get-pip.py -sudo pip install make-deb - -# check out LBRY -git clone https://github.com/lbryio/lbry.git --branch "$BRANCH" +$SUDO pip install --upgrade pip +$SUDO pip install make-deb # build packages +# +# dpkg-buildpackage outputs its results into '..' so +# we need to move/clone lbry into the build directory +if [ "$CLONE" == true]; then + cp -a $SOURCE_DIR lbry +else + git clone https://github.com/lbryio/lbry.git +fi ( - cd lbry - make-deb - dpkg-buildpackage -us -uc + cd lbry + if [ -n "${BRANCH}" ]; then + git checkout "${BRANCH}" + fi + make-deb + dpkg-buildpackage -us -uc ) @@ -41,8 +128,20 @@ git clone https://github.com/lbryio/lbry.git --branch "$BRANCH" PACKAGE="$(ls | grep '.deb')" ar vx "$PACKAGE" mkdir control data -tar -xvzf control.tar.gz --directory control -tar -xvJf data.tar.xz --directory data +tar -xzf control.tar.gz --directory control + +# The output of the travis build is a +# tar.gz and the output locally is tar.xz. +# Instead of having tar detect the compression used, we +# could update the config to output the same in either spot. +# Unfortunately, doing so requires editting some auto-generated +# files: http://linux.spiney.org/forcing_gzip_compression_when_building_debian_packages +tar -xf data.tar.?z --directory data + +PACKAGING_DIR='lbry/packaging/ubuntu' + +# set web ui branch +sed -i "s/^WEB_UI_BRANCH='[^']\+'/WEB_UI_BRANCH='$WEB_UI_BRANCH'/" "$PACKAGING_DIR/lbry" # add files function addfile() { @@ -52,16 +151,16 @@ function addfile() { cp "$FILE" "data/$TARGET" echo "$(md5sum "data/$TARGET" | cut -d' ' -f1) $TARGET" >> control/md5sums } -PACKAGING_DIR='lbry/packaging/ubuntu' addfile "$PACKAGING_DIR/lbry" usr/share/python/lbrynet/bin/lbry addfile "$PACKAGING_DIR/lbry.desktop" usr/share/applications/lbry.desktop -#addfile lbry/packaging/ubuntu/lbry-init.conf etc/init/lbry.conf + +cat "$PACKAGING_DIR/postinst_append" >> control/postinst # repackage .deb -sudo chown -R root:root control data -tar -cvzf control.tar.gz -C control . -tar -cvJf data.tar.xz -C data . -sudo chown root:root debian-binary control.tar.gz data.tar.xz +$SUDO chown -R root:root control data +tar -czf control.tar.gz -C control . +tar -cJf data.tar.xz -C data . +$SUDO chown root:root debian-binary control.tar.gz data.tar.xz ar r "$PACKAGE" debian-binary control.tar.gz data.tar.xz # TODO: we can append to data.tar instead of extracting it all and recompressing diff --git a/setup.py b/setup.py index e4a9432f0..baf069fa7 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ gui_data_files = ['close2.gif', 'lbry-dark-242x80.gif', 'lbry-dark-icon.xbm', 'l gui_data_paths = [os.path.join(base_dir, 'lbrynet', 'lbrynet_gui', f) for f in gui_data_files] setup(name='lbrynet', - description='A fully decentralized network for distributing data', + description='A fully-decentralized content marketplace', version=__version__, maintainer='Jimmy Kiselak', maintainer_email='jimmy@lbry.io', From eb0dd827b1f46a66f0038fe7ed8124f2d23412bf Mon Sep 17 00:00:00 2001 From: Jack Date: Tue, 24 May 2016 17:54:44 -0400 Subject: [PATCH 04/28] delete old unused app and move uri handler to lbry-osx-app --- .../lbrynet_daemon/Apps/LBRYOSXStatusBar.py | 108 ------------------ lbrynet/lbrynet_daemon/Apps/LBRYURIHandler.py | 60 ---------- .../{Apps => daemon_scripts}/__init__.py | 0 setup_uri_handler.py | 25 ---- 4 files changed, 193 deletions(-) delete mode 100644 lbrynet/lbrynet_daemon/Apps/LBRYOSXStatusBar.py delete mode 100644 lbrynet/lbrynet_daemon/Apps/LBRYURIHandler.py rename lbrynet/lbrynet_daemon/{Apps => daemon_scripts}/__init__.py (100%) delete mode 100644 setup_uri_handler.py diff --git a/lbrynet/lbrynet_daemon/Apps/LBRYOSXStatusBar.py b/lbrynet/lbrynet_daemon/Apps/LBRYOSXStatusBar.py deleted file mode 100644 index 5ca433f72..000000000 --- a/lbrynet/lbrynet_daemon/Apps/LBRYOSXStatusBar.py +++ /dev/null @@ -1,108 +0,0 @@ -import rumps -import xmlrpclib -import os -import webbrowser -import subprocess -import argparse - - -class DaemonStatusBarApp(rumps.App): - def __init__(self): - icon_path = 'app.icns' - if os.path.isfile(icon_path): - rumps.App.__init__(self, name="LBRY", icon=icon_path, quit_button=None, - menu=["Open", "Preferences", "View balance", "Quit"]) - else: - rumps.App.__init__(self, name="LBRY", title="LBRY", quit_button=None, - menu=["Open", "Preferences", "View balance", "Quit"]) - - @rumps.timer(1) - def alert_daemon_start(self): - daemon = xmlrpclib.ServerProxy("http://localhost:7080/") - try: - start_msg = daemon.is_running() - if isinstance(start_msg, str): - rumps.notification(title='LBRY', subtitle='', message=str(start_msg), sound=True) - update_info = daemon.check_for_new_version() - update_msg = "" - for p in update_info: - if not p[0]: - update_msg += p[1] + "\n" - if update_msg: - update_msg += "\n Try running the installer again to fix this" - rumps.notification(title='LBRY', subtitle='', message=update_msg, sound=True) - except: - pass - - @rumps.clicked('Open') - def get_ui(self): - daemon = xmlrpclib.ServerProxy("http://localhost:7080/") - try: - daemon.is_running() - webbrowser.get('safari').open("lbry://lbry") - except: - try: - rumps.notification(title='LBRY', subtitle='', message="Couldn't connect to lbrynet daemon", sound=True) - except: - rumps.alert(title='LBRY', message="Couldn't connect to lbrynet daemon") - - @rumps.clicked("Preferences") - def prefs(self): - daemon = xmlrpclib.ServerProxy("http://localhost:7080/") - try: - daemon.is_running() - webbrowser.get('safari').open("lbry://settings") - except: - rumps.notification(title='LBRY', subtitle='', message="Couldn't connect to lbrynet daemon", sound=True) - - @rumps.clicked("View balance") - def disp_balance(self): - daemon = xmlrpclib.ServerProxy("http://localhost:7080/") - try: - balance = daemon.get_balance() - r = round(float(balance), 2) - try: - rumps.notification(title='LBRY', subtitle='', message=str("Your balance is %.2f LBC" % r), sound=False) - except: - rumps.alert(title='LBRY', message=str("Your balance is %.2f LBC" % r)) - - except: - try: - rumps.notification(title='LBRY', subtitle='', message="Couldn't connect to lbrynet daemon", sound=True) - except: - rumps.alert(title='LBRY', message="Couldn't connect to lbrynet daemon") - - @rumps.clicked('Quit') - def clean_quit(self): - daemon = xmlrpclib.ServerProxy("http://localhost:7080/") - try: - daemon.stop() - except: - pass - rumps.quit_application() - - -def main(): - parser = argparse.ArgumentParser(description="Launch lbrynet status bar application") - parser.add_argument("--startdaemon", - help="true or false, default true", - type=str, - default="true") - args = parser.parse_args() - - if str(args.startdaemon).lower() == "true": - daemon = xmlrpclib.ServerProxy('http://localhost:7080') - try: - daemon.is_running() - except: - subprocess.Popen("screen -dmS lbrynet bash -c " - "'PYTHONPATH=$PYTHONPATH:`cat /Users/${USER}/Library/Application\ Support/lbrynet/.python_path`; " - "PATH=$PATH:`cat /Users/${USER}/Library/Application\ Support/lbrynet/.lbry_bin_path`; " - "lbrynet-daemon --update=False'", shell=True) - - status_app = DaemonStatusBarApp() - status_app.run() - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/lbrynet/lbrynet_daemon/Apps/LBRYURIHandler.py b/lbrynet/lbrynet_daemon/Apps/LBRYURIHandler.py deleted file mode 100644 index f6990cfea..000000000 --- a/lbrynet/lbrynet_daemon/Apps/LBRYURIHandler.py +++ /dev/null @@ -1,60 +0,0 @@ -import os -import json -import webbrowser -import subprocess -import sys - -from time import sleep -from jsonrpc.proxy import JSONRPCProxy - -API_CONNECTION_STRING = "http://localhost:5279/lbryapi" -UI_ADDRESS = "http://localhost:5279" - - -class LBRYURIHandler(object): - def __init__(self): - self.started_daemon = False - self.daemon = JSONRPCProxy.from_url(API_CONNECTION_STRING) - - def handle_osx(self, lbry_name): - try: - status = self.daemon.is_running() - except: - os.system("open /Applications/LBRY.app") - sleep(3) - - if lbry_name == "lbry" or lbry_name == "": - webbrowser.open(UI_ADDRESS) - else: - webbrowser.open(UI_ADDRESS + "/?watch=" + lbry_name) - - def handle_linux(self, lbry_name): - try: - status = self.daemon.is_running() - except: - cmd = r'DIR = "$( cd "$(dirname "${BASH_SOURCE[0]}" )" && pwd )"' \ - r'if [-z "$(pgrep lbrynet-daemon)"]; then' \ - r'echo "running lbrynet-daemon..."' \ - r'$DIR / lbrynet - daemon &' \ - r'sleep 3 # let the daemon load before connecting' \ - r'fi' - subprocess.Popen(cmd, shell=True) - - if lbry_name == "lbry" or lbry_name == "": - webbrowser.open(UI_ADDRESS) - else: - webbrowser.open(UI_ADDRESS + "/?watch=" + lbry_name) - - -def main(args): - if len(args) != 1: - args = ['lbry://lbry'] - - name = args[0][7:] - if sys.platform == "darwin": - LBRYURIHandler().handle_osx(lbry_name=name) - else: - LBRYURIHandler().handle_linux(lbry_name=name) - -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/lbrynet/lbrynet_daemon/Apps/__init__.py b/lbrynet/lbrynet_daemon/daemon_scripts/__init__.py similarity index 100% rename from lbrynet/lbrynet_daemon/Apps/__init__.py rename to lbrynet/lbrynet_daemon/daemon_scripts/__init__.py diff --git a/setup_uri_handler.py b/setup_uri_handler.py deleted file mode 100644 index e9ba6749c..000000000 --- a/setup_uri_handler.py +++ /dev/null @@ -1,25 +0,0 @@ -from setuptools import setup -import os - -APP = [os.path.join('lbrynet', 'lbrynet_daemon', 'Apps', 'LBRYURIHandler.py')] -DATA_FILES = [] -OPTIONS = {'argv_emulation': True, - 'packages': ['jsonrpc'], - 'plist': { - 'LSUIElement': True, - 'CFBundleIdentifier': 'io.lbry.LBRYURIHandler', - 'CFBundleURLTypes': [ - { - 'CFBundleURLTypes': 'LBRYURIHandler', - 'CFBundleURLSchemes': ['lbry'] - } - ] - } - } - -setup( - app=APP, - data_files=DATA_FILES, - options={'py2app': OPTIONS}, - setup_requires=['py2app'], -) \ No newline at end of file From 6b39e549f7ca44dc5f6b5b455ded0ea4695fc985 Mon Sep 17 00:00:00 2001 From: Jack Date: Wed, 25 May 2016 21:02:30 -0400 Subject: [PATCH 05/28] add developer id to travis.yml --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 1f5f36cd7..5215f0df5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,3 +30,4 @@ env: global: - secure: d3glJxXC0goiFETAP0JxMDEQoSNlh1fRDR76D3tjYY4Brxh2UgvUvpNJOAkFApynciA3n8kva4uBsv52jwnKG0VmCy/meaWowouhyi8ChdPpg7HZL84oC7rz3bZ2OA2iuYFPpAQrd6p7OMhmiCkeqhRKtW0YmOKn1F45kaIMnmq+bD0QK3IdP3QBdGz0rf6TQlNSQLSJtDAP0+HO+NaawJ1TQJXHUHA8mKnbOTRal4bH007uxfhvthXysm1QRQOfthGud2q/DN+f6RfCqF2Fv9l7NwR5BwVKluQYgqJjdhk9IOU7D+zW4Ne2fSt6V1PRAASAfyhtDiOA7B3ZUw2igimQ6rWyWao1csilq0RW9EmycOT8S7x5YgXltk0kVdNizdQeHCDII0mOxkIFBF0bs2ZgTgKasZrU5jGnEhV42eACl9CGeoT5/ots7an+zgCBoQ8c4HGkMQyKV4uNnvKD+gq1FPDuwlHEUHhbjy5uRI4kAVMzjsCJX7XBSumFELuwiOpE/XguiJAV/LvjYbN6OFemJ6HUOhep6kgq7Zcoxh+UnaMixiq6NQTSoLpffatSxCM9EG8uBoXZJBH45cS9aD0eP9zjV2COCC0Iom5BQFhkArgMVodYtcrNevPUA3AuvJOjdJpSwKB4fqaU/CDqThw/ieZQlzaZqWIM5RnHf6Y= - secure: n08IQBOLaipKzAwS5aQM+JfFtvLSCrWqFFztTMeElmck6IJOVnPlDvPYRCrRmrXtgFmJ1G6hEU5Vri086MkTaISq3V04ndquhz7nv93Pp1do42vWPmvmFHdMrhaOGPVtYZ4m4XfgNcA+NC79V9X2VQ+MoQgCRhcGlgGomxJBHc924bGxppkIrDMljvSipkR5v9g/UFGRPYJ6BIF0imWrdsHUfqhbZMdAxAmdlBAjx/ZUOmUyPWYttHGYEW4HpLwvkU2K+sJrt1emogvR7GT94CadtqjmRsiKh0Y2lHHhHkqV1J941T08p9hyKU6S4fHQqPHfpkHRiPeJQY8JiipCbuERn0YeGqsp5q0jRE6PFUxCWcvlWqc7XNihOMQAb5JQD8vF2Lvs3YYycFBgZkIOaZQFbemqkx2pnQMjVF7GCZp4u+p5yo8iBeImYFYdkqIBHqPqsKxM9aBfe05XhAmi/6jUP0L2xikVFvJZDNxKP2uyqXVbSMR8KF8v1eaYnsSL3vzBolwkn96zaj2E/lItHaNeNIXvutAsGuy2ybTXeabm5rTaUr+5cmdqx4JqmyGM3DinGFik/fzpLUJCd4GaC1n1taLy6aUUW2oH46QRqLLAAjZ4AWao92Eb4cnFiVxxBqVG3efWaoEt+uW7/hzqG27iieoVqCXs3Hd5g4bCYyo= + - secure: Unv3cSrgeT6fdHbWWXqBAH2WRpudiX1aPG90HFT2zwfdvXBTS8zUSTmsQJu0UJjsYUjErqjjwMNrzh0HFn1MaIc31aJQurC2CuKXZvzViBXh1abr5WBYzXEvkXuwgzhpcyNcg9SU2Qmhmxtdb1r1WV+HNnTofQahrWVCmUJh8mCunrXnFRMaTwW0qBfUpW9UvMaZ4N9RWdkkEAMV0at/kuP2MnYgW0EWA/42BV7N92HtWxAyvmiHyk2EjQ3cQ9avb4/6C2vFIhiFaYzU3ZR5VtT/XQxiuRRtVYj9aG4RpXhcXMg1LTTqCJ9JJTvcpQX75gpKz3pLr7rA3hVoV5nueHLpyGwoZU6iy+8auCA/6WnP+xGAgL/JRq9ozisr8NVD5BX+BrQED3FAxH9uARM1LVR24PphErctqzdDTvjkvdj9rHROdlD5DfSRuroNjwsRrrRTENruekWyYp0W3pORQQTO/A0wE+/YWfKR941j9w+Q1GdYWcvDnkPzTnL2o7D76N9REsy2jpIIT9dqsOcgp1fvCYaTN1k5GXlmgRydsRWBnelr0vD1uQxEyGhBzCrPwrCJz2U5VZDMmoCBPEMwZOLaRNkLbGX0Qn3gQqhKLeM/SYeJSuuxTO0WVyQvheNJIaE+crhZrk8sB5SrdN9hGGD4NYYUXrdcgEt91K78/BE= From 8922fd6dde76dc4c413f566ebe5142fa52aa1fd3 Mon Sep 17 00:00:00 2001 From: Jack Date: Wed, 25 May 2016 22:28:45 -0400 Subject: [PATCH 06/28] add startup scripts -populate blockchainname.db on first run from older version --- lbrynet/__init__.py | 2 +- lbrynet/core/LBRYcrdWallet.py | 5 ++ lbrynet/lbrynet_daemon/LBRYDaemon.py | 78 +++++++++++++++++-- lbrynet/lbrynet_daemon/LBRYDaemonServer.py | 51 +----------- .../daemon_scripts/Autofetcher.py | 68 ++++++++++++++++ .../daemon_scripts/migrateto025.py | 33 ++++++++ 6 files changed, 179 insertions(+), 58 deletions(-) create mode 100644 lbrynet/lbrynet_daemon/daemon_scripts/Autofetcher.py create mode 100644 lbrynet/lbrynet_daemon/daemon_scripts/migrateto025.py diff --git a/lbrynet/__init__.py b/lbrynet/__init__.py index 26dafe45d..3d80f11a2 100644 --- a/lbrynet/__init__.py +++ b/lbrynet/__init__.py @@ -4,5 +4,5 @@ import logging logging.getLogger(__name__).addHandler(logging.NullHandler()) -version = (0, 2, 4) +version = (0, 2, 5) __version__ = ".".join([str(x) for x in version]) diff --git a/lbrynet/core/LBRYcrdWallet.py b/lbrynet/core/LBRYcrdWallet.py index 8ec200611..17fd0e229 100644 --- a/lbrynet/core/LBRYcrdWallet.py +++ b/lbrynet/core/LBRYcrdWallet.py @@ -295,6 +295,11 @@ class LBRYWallet(object): d.addCallback(self._get_stream_info_from_value, name) return d + def get_txid_for_name(self, name): + d = self._get_value_for_name(name) + d.addCallback(lambda r: None if 'txid' not in r else r['txid']) + return d + def get_stream_info_from_txid(self, name, txid): d = self.get_claims_from_tx(txid) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index 0a10dff9d..320ab6a51 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -151,6 +151,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): self.waiting_on = {} self.streams = {} self.known_dht_nodes = KNOWN_DHT_NODES + self.first_run_after_update = False self.platform_info = { "processor": platform.processor(), "python_version: ": platform.python_version(), @@ -197,7 +198,9 @@ class LBRYDaemon(jsonrpc.JSONRPC): 'use_upnp': True, 'start_lbrycrdd': True, 'requested_first_run_credits': False, - 'cache_time': DEFAULT_CACHE_TIME + 'cache_time': DEFAULT_CACHE_TIME, + 'startup_scripts': [], + 'last_version': {'lbrynet': lbrynet_version, 'lbryum': lbryum_version} } if os.path.isfile(self.daemon_conf): @@ -234,6 +237,20 @@ class LBRYDaemon(jsonrpc.JSONRPC): self.session_settings = settings_dict + if 'last_version' in missing_settings.keys(): + self.session_settings['last_version'] = None + + if self.session_settings['last_version'] != self.default_settings['last_version']: + self.session_settings['last_version'] = self.default_settings['last_version'] + f = open(self.daemon_conf, "w") + f.write(json.dumps(self.session_settings)) + f.close() + + self.first_run_after_update = True + log.info("First run after update") + if lbrynet_version == '0.2.5': + self.session_settings['startup_scripts'].append({'script_name': 'migrateto025', 'run_once': True}) + self.run_on_startup = self.session_settings['run_on_startup'] self.data_rate = self.session_settings['data_rate'] self.max_key_fee = self.session_settings['max_key_fee'] @@ -252,6 +269,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): self.start_lbrycrdd = self.session_settings['start_lbrycrdd'] self.requested_first_run_credits = self.session_settings['requested_first_run_credits'] self.cache_time = self.session_settings['cache_time'] + self.startup_scripts = self.session_settings['startup_scripts'] if os.path.isfile(os.path.join(self.db_dir, "stream_info_cache.json")): f = open(os.path.join(self.db_dir, "stream_info_cache.json"), "r") @@ -394,6 +412,10 @@ class LBRYDaemon(jsonrpc.JSONRPC): self.announced_startup = True self.startup_status = STARTUP_STAGES[5] log.info("[" + str(datetime.now()) + "] Started lbrynet-daemon") + if len(self.startup_scripts): + log.info("Scheduling scripts") + reactor.callLater(3, self._run_scripts) + # self.lbrynet_connection_checker.start(3600) if self.first_run: @@ -608,6 +630,9 @@ class LBRYDaemon(jsonrpc.JSONRPC): def _shutdown(self): log.info("Closing lbrynet session") log.info("Status at time of shutdown: " + self.startup_status[0]) + self.internet_connection_checker.stop() + self.version_checker.stop() + self.connection_problem_checker.stop() d = self._upload_log(name_prefix="close", exclude_previous=False if self.first_run else True) d.addCallback(lambda _: self._stop_server()) @@ -1017,19 +1042,31 @@ class LBRYDaemon(jsonrpc.JSONRPC): f.close() return defer.succeed(True) - def _resolve_name(self, name): + def _resolve_name(self, name, force_refresh=False): def _cache_stream_info(stream_info): + def _add_txid(txid): + self.name_cache[name]['txid'] = txid + return defer.succeed(None) + self.name_cache[name] = {'claim_metadata': stream_info, 'timestamp': self._get_long_count_timestamp()} - d = self._update_claim_cache() + d = self.session.wallet.get_txid_for_name(name) + d.addCallback(_add_txid) + d.addCallback(lambda _: self._update_claim_cache()) d.addCallback(lambda _: self.name_cache[name]['claim_metadata']) + return d - if name in self.name_cache.keys(): - if (self._get_long_count_timestamp() - self.name_cache[name]['timestamp']) < self.cache_time: - log.info("[" + str(datetime.now()) + "] Returning cached stream info for lbry://" + name) - d = defer.succeed(self.name_cache[name]['claim_metadata']) + if not force_refresh: + if name in self.name_cache.keys(): + if (self._get_long_count_timestamp() - self.name_cache[name]['timestamp']) < self.cache_time: + log.info("[" + str(datetime.now()) + "] Returning cached stream info for lbry://" + name) + d = defer.succeed(self.name_cache[name]['claim_metadata']) + else: + log.info("[" + str(datetime.now()) + "] Refreshing stream info for lbry://" + name) + d = self.session.wallet.get_stream_info_for_name(name) + d.addCallbacks(_cache_stream_info, lambda _: defer.fail(UnknownNameError)) else: - log.info("[" + str(datetime.now()) + "] Refreshing stream info for lbry://" + name) + log.info("[" + str(datetime.now()) + "] Resolving stream info for lbry://" + name) d = self.session.wallet.get_stream_info_for_name(name) d.addCallbacks(_cache_stream_info, lambda _: defer.fail(UnknownNameError)) else: @@ -1221,6 +1258,31 @@ class LBRYDaemon(jsonrpc.JSONRPC): requests.post(URL, json.dumps({"text": msg})) return defer.succeed(None) + def _run_scripts(self): + if len([k for k in self.startup_scripts if 'run_once' in k.keys()]): + log.info("Removing one time startup scripts") + f = open(self.daemon_conf, "r") + initialsettings = json.loads(f.read()) + f.close() + t = [s for s in self.startup_scripts if 'run_once' not in s.keys()] + initialsettings['startup_scripts'] = t + f = open(self.daemon_conf, "w") + f.write(json.dumps(initialsettings)) + f.close() + + for script in self.startup_scripts: + if script['script_name'] == 'migrateto025': + log.info("Running migrator to 0.2.5") + from lbrynet.lbrynet_daemon.daemon_scripts.migrateto025 import run as run_migrate + run_migrate(self) + + if script['script_name'] == 'Autofetcher': + log.info("Starting autofetcher script") + from lbrynet.lbrynet_daemon.daemon_scripts.Autofetcher import run as run_autofetcher + run_autofetcher(self) + + return defer.succeed(None) + def _render_response(self, result, code): return defer.succeed({'result': result, 'code': code}) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemonServer.py b/lbrynet/lbrynet_daemon/LBRYDaemonServer.py index 85494d21c..5843bca06 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemonServer.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemonServer.py @@ -189,50 +189,6 @@ class HostedLBRYFile(resource.Resource): call.cancel() -class MyLBRYFiles(resource.Resource): - isLeaf = False - - def __init__(self): - resource.Resource.__init__(self) - self.files_table = None - - def delayed_render(self, request, result): - request.write(result.encode('utf-8')) - request.finish() - - def render_GET(self, request): - self.files_table = None - api = jsonrpc.Proxy(API_CONNECTION_STRING) - d = api.callRemote("get_lbry_files", {}) - d.addCallback(self._get_table) - d.addCallback(lambda results: self.delayed_render(request, results)) - - return server.NOT_DONE_YET - - def _get_table(self, files): - if not self.files_table: - self.files_table = r'My LBRY files' - self.files_table += r'' - self.files_table += r'' - self.files_table += r'' - self.files_table += r'' - self.files_table += r'' - self.files_table += r'' - return self._get_table(files) - if not len(files): - self.files_table += r'
Stream nameCompletedToggleRemove
' - return self.files_table - else: - f = files.pop() - self.files_table += r'' - self.files_table += r'%s' % (f['stream_name']) - self.files_table += r'%s' % (f['completed']) - self.files_table += r'Start' if f['stopped'] else r'Stop' - self.files_table += r'Delete' - self.files_table += r'' - return self._get_table(files) - - class LBRYDaemonServer(object): def __init__(self): self.data_dir = user_data_dir("LBRY") @@ -336,12 +292,9 @@ class LBRYDaemonServer(object): def _setup_server(self, ui_ver, wallet): self._api = LBRYDaemon(ui_ver, wallet_type=wallet) self.root = LBRYindex(self.ui_dir) - self.root.putChild("css", static.File(os.path.join(self.ui_dir, "css"))) - self.root.putChild("font", static.File(os.path.join(self.ui_dir, "font"))) - self.root.putChild("img", static.File(os.path.join(self.ui_dir, "img"))) - self.root.putChild("js", static.File(os.path.join(self.ui_dir, "js"))) + for d in [i[0] for i in os.walk(self.ui_dir) if os.path.dirname(i[0]) == self.ui_dir]: + self.root.putChild(os.path.basename(d), static.File(d)) self.root.putChild("view", HostedLBRYFile(self._api)) - self.root.putChild("files", MyLBRYFiles()) self.root.putChild(API_ADDRESS, self._api) return defer.succeed(True) diff --git a/lbrynet/lbrynet_daemon/daemon_scripts/Autofetcher.py b/lbrynet/lbrynet_daemon/daemon_scripts/Autofetcher.py new file mode 100644 index 000000000..cd7ed02cb --- /dev/null +++ b/lbrynet/lbrynet_daemon/daemon_scripts/Autofetcher.py @@ -0,0 +1,68 @@ +import json +import logging.handlers +import sys +import os + +from appdirs import user_data_dir +from twisted.internet.task import LoopingCall +from twisted.internet import reactor + + +if sys.platform != "darwin": + log_dir = os.path.join(os.path.expanduser("~"), ".lbrynet") +else: + log_dir = user_data_dir("LBRY") + +if not os.path.isdir(log_dir): + os.mkdir(log_dir) + +LOG_FILENAME = os.path.join(log_dir, 'lbrynet-daemon.log') + +if os.path.isfile(LOG_FILENAME): + f = open(LOG_FILENAME, 'r') + PREVIOUS_LOG = len(f.read()) + f.close() +else: + PREVIOUS_LOG = 0 + +log = logging.getLogger(__name__) +handler = logging.handlers.RotatingFileHandler(LOG_FILENAME, maxBytes=2097152, backupCount=5) +log.addHandler(handler) +log.setLevel(logging.INFO) + + +class Autofetcher(object): + """ + Download name claims as they occur + """ + + def __init__(self, api): + self._api = api + self._checker = LoopingCall(self._check_for_new_claims) + self.best_block = None + + def start(self): + reactor.addSystemEventTrigger('before', 'shutdown', self.stop) + self._checker.start(5) + + def stop(self): + log.info("Stopping autofetcher") + self._checker.stop() + + def _check_for_new_claims(self): + block = self._api.get_best_blockhash() + if block != self.best_block: + log.info("Checking new block for name claims, block hash: %s" % block) + self.best_block = block + transactions = self._api.get_block({'blockhash': block})['tx'] + for t in transactions: + c = self._api.get_claims_for_tx({'txid': t}) + if len(c): + for i in c: + log.info("Downloading stream for claim txid: %s" % t) + self._api.get({'name': t, 'stream_info': json.loads(i['value'])}) + + +def run(api): + fetcher = Autofetcher(api) + fetcher.start() \ No newline at end of file diff --git a/lbrynet/lbrynet_daemon/daemon_scripts/migrateto025.py b/lbrynet/lbrynet_daemon/daemon_scripts/migrateto025.py new file mode 100644 index 000000000..9283b48aa --- /dev/null +++ b/lbrynet/lbrynet_daemon/daemon_scripts/migrateto025.py @@ -0,0 +1,33 @@ +from twisted.internet import defer + + +class migrator(object): + """ + Re-resolve lbry names to write missing data to blockchain.db and to cache the nametrie + """ + + def __init__(self, api): + self._api = api + + def start(self): + def _resolve_claims(claimtrie): + claims = [i for i in claimtrie if 'txid' in i.keys()] + r = defer.DeferredList([self._api._resolve_name(claim['name'], force_refresh=True) for claim in claims], consumeErrors=True) + return r + + def _restart_lbry_files(): + def _restart_lbry_file(lbry_file): + return lbry_file.restore() + + r = defer.DeferredList([_restart_lbry_file(lbry_file) for lbry_file in self._api.lbry_file_manager.lbry_files if not lbry_file.txid], consumeErrors=True) + r.callback(None) + return r + + d = self._api.session.wallet.get_nametrie() + d.addCallback(_resolve_claims) + d.addCallback(lambda _: _restart_lbry_files()) + + +def run(api): + refresher = migrator(api) + refresher.start() From b557a14134385a4f625bf7e2d649871166fb71c5 Mon Sep 17 00:00:00 2001 From: Job Evers-Meltzer Date: Mon, 23 May 2016 12:19:52 -0500 Subject: [PATCH 07/28] fix whitespace --- .travis.yml | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index d21cb628d..5257823d5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,3 @@ - matrix: include: - os: linux @@ -17,16 +16,15 @@ before_install: install: true before_script: -- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then openssl aes-256-cbc -k "$ENCRYPTION_SECRET" -in packaging/osx/certs/dist.cer.enc -d -a -out packaging/osx/certs/dist.cer; fi -- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then openssl aes-256-cbc -k "$ENCRYPTION_SECRET" -in packaging/osx/certs/dist.p12.enc -d -a -out packaging/osx/certs/dist.p12; fi -- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then ./packaging/osx/add-key.sh; fi + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then openssl aes-256-cbc -k "$ENCRYPTION_SECRET" -in packaging/osx/certs/dist.cer.enc -d -a -out packaging/osx/certs/dist.cer; fi + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then openssl aes-256-cbc -k "$ENCRYPTION_SECRET" -in packaging/osx/certs/dist.p12.enc -d -a -out packaging/osx/certs/dist.p12; fi + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then ./packaging/osx/add-key.sh; fi script: -- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then bash packaging/ubuntu/ubuntu_package_setup.sh; fi -- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew upgrade gmp; fi -# the default py2app (v0.9) has a bug that is fixed in the head of /metachris/py2app -- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pip install git+https://github.com/metachris/py2app; fi -# py2app fails to find jsonrpc unless json-rpc is installed. why? I don't know. -- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pip install json-rpc; fi -- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then cd packaging/osx/lbry-osx-app; ./setup_app.sh; cd $TRAVIS_BUILD_DIR; fi - + - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then bash packaging/ubuntu/ubuntu_package_setup.sh; fi + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew upgrade gmp; fi + # the default py2app (v0.9) has a bug that is fixed in the head of /metachris/py2app + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pip install git+https://github.com/metachris/py2app; fi + # py2app fails to find jsonrpc unless json-rpc is installed. why? I don't know. + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pip install json-rpc; fi + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then cd packaging/osx/lbry-osx-app; ./setup_app.sh; cd $TRAVIS_BUILD_DIR; fi From 53af9db28585feb76d3c416d15f7951494b9ece5 Mon Sep 17 00:00:00 2001 From: Job Evers Date: Fri, 27 May 2016 15:40:30 -0500 Subject: [PATCH 08/28] add tests and linting --- .gitignore | 2 + .pylintrc | 379 ++++++++++++++++++ .travis.yml | 3 +- .../install_dependencies_and_run_tests.sh | 46 +++ 4 files changed, 429 insertions(+), 1 deletion(-) create mode 100644 .pylintrc create mode 100755 packaging/travis/install_dependencies_and_run_tests.sh diff --git a/.gitignore b/.gitignore index 819306a87..1bd0eea94 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ lbrynet.egg-info/PKG-INFO *.pem *.decTest + +.coverage diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 000000000..5e5aab35c --- /dev/null +++ b/.pylintrc @@ -0,0 +1,379 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=1 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Allow optimization of some AST trees. This will activate a peephole AST +# optimizer, which will apply various small optimizations. For instance, it can +# be used to obtain the result of joining multiple strings with the addition +# operator. Joining a lot of strings can lead to a maximum recursion error in +# Pylint and this flag can prevent that. It has one side effect, the resulting +# AST will be different than the one from reality. +optimize-ast=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=import-star-module-level,old-octal-literal,oct-method,print-statement,unpacking-in-except,parameter-unpacking,backtick,old-raise-syntax,old-ne-operator,long-suffix,dict-view-method,dict-iter-method,metaclass-assignment,next-method-called,raising-string,indexing-exception,raw_input-builtin,long-builtin,file-builtin,execfile-builtin,coerce-builtin,cmp-builtin,buffer-builtin,basestring-builtin,apply-builtin,filter-builtin-not-iterating,using-cmp-argument,useless-suppression,range-builtin-not-iterating,suppressed-message,no-absolute-import,old-division,cmp-method,reload-builtin,zip-builtin-not-iterating,intern-builtin,unichr-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,input-builtin,round-builtin,hex-method,nonzero-method,map-builtin-not-iterating + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". +files-output=no + +# Tells whether to display a full report or only the messages +reports=yes + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_$|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[BASIC] + +# List of builtins function names that should not be used, separated by a comma +bad-functions=map,filter,input + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# Regular expression matching correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for function names +function-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for variable names +variable-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for attribute names +attr-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for argument names +argument-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Naming hint for class names +class-name-hint=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for method names +method-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + + +[ELIF] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=100 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma,dict-separator + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules=twisted.internet.reactor,leveldb + +# List of classes names for which member attributes should not be checked +# (useful for classes with attributes dynamically set). This supports can work +# with qualified names. +ignored-classes=twisted.internet,RequestMessage + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/.travis.yml b/.travis.yml index 5257823d5..232e23219 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,8 @@ before_install: - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then sudo pip install --upgrade pip virtualenv; fi - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then virtualenv $HOME/venv; source $HOME/venv/bin/activate; fi -install: true +install: + - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then ./packaging/travis/install_dependencies_and_run_tests.sh; fi before_script: - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then openssl aes-256-cbc -k "$ENCRYPTION_SECRET" -in packaging/osx/certs/dist.cer.enc -d -a -out packaging/osx/certs/dist.cer; fi diff --git a/packaging/travis/install_dependencies_and_run_tests.sh b/packaging/travis/install_dependencies_and_run_tests.sh new file mode 100755 index 000000000..9bef0afc4 --- /dev/null +++ b/packaging/travis/install_dependencies_and_run_tests.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# +# This script is used by travis to install lbry from source +# + +set -euo pipefail +set -o xtrace + +SUDO='' +if (( $EUID != 0 )); then + SUDO='sudo' +fi + +if [ -z ${TRAVIS+x} ]; then + # if not on travis, its nice to see progress + QUIET="" +else + QUIET="-qq" +fi + +# get the required OS packages +$SUDO apt-get ${QUIET} update +$SUDO apt-get ${QUIET} install -y --no-install-recommends \ + build-essential python-dev libffi-dev libssl-dev git \ + libgmp3-dev wget ca-certificates python-virtualenv + +# create a virtualenv so we don't muck with anything on the system +virtualenv venv +# need to unset these or else we can't activate +set +eu +source venv/bin/activate +set -eu + +# need a modern version of pip (more modern than ubuntu default) +wget https://bootstrap.pypa.io/get-pip.py +python get-pip.py +rm get-pip.py + +pip install -r requirements.txt + +pip install nose coverage coveralls pylint +nosetests --with-coverage --cover-package=lbrynet -v -I lbrynet_test_bot.py -I functional_tests.py tests/ +# TODO: submit coverage report to coveralls + +# TODO: as code quality improves, make pylint be more strict +pylint -E --disable=inherit-non-class --disable=no-member lbrynet From e7a580fd3a3be55025abe386c638690b363c58f2 Mon Sep 17 00:00:00 2001 From: Jack Date: Sun, 29 May 2016 23:18:30 -0400 Subject: [PATCH 09/28] add lbry_ui_manager --- lbrynet/conf.py | 4 +- lbrynet/lbrynet_daemon/LBRYDaemon.py | 127 +++++++----- lbrynet/lbrynet_daemon/LBRYDaemonServer.py | 160 +++------------ lbrynet/lbrynet_daemon/LBRYUIManager.py | 219 +++++++++++++++++++++ 4 files changed, 330 insertions(+), 180 deletions(-) create mode 100644 lbrynet/lbrynet_daemon/LBRYUIManager.py diff --git a/lbrynet/conf.py b/lbrynet/conf.py index dfae3d274..526dc711d 100644 --- a/lbrynet/conf.py +++ b/lbrynet/conf.py @@ -36,8 +36,10 @@ UI_ADDRESS = "http://" + API_INTERFACE + ":" + str(API_PORT) PROTOCOL_PREFIX = "lbry" DEFAULT_WALLET = "lbryum" +WALLET_TYPES = ["lbryum", "lbrycrd"] DEFAULT_TIMEOUT = 30 DEFAULT_MAX_SEARCH_RESULTS = 25 DEFAULT_MAX_KEY_FEE = 100.0 DEFAULT_SEARCH_TIMEOUT = 3.0 -DEFAULT_CACHE_TIME = 3600 \ No newline at end of file +DEFAULT_CACHE_TIME = 3600 +DEFAULT_UI_BRANCH = "master" diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index 320ab6a51..0d3535012 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -31,13 +31,14 @@ from lbrynet.core.Error import UnknownNameError, InsufficientFundsError from lbrynet.lbryfile.StreamDescriptor import LBRYFileStreamType from lbrynet.lbryfile.client.LBRYFileDownloader import LBRYFileSaverFactory, LBRYFileOpenerFactory from lbrynet.lbryfile.client.LBRYFileOptions import add_lbry_file_to_sd_identifier +from lbrynet.lbrynet_daemon.LBRYUIManager import LBRYUIManager from lbrynet.lbrynet_daemon.LBRYDownloader import GetStream from lbrynet.lbrynet_daemon.LBRYPublisher import Publisher from lbrynet.core.utils import generate_id 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 -from lbrynet.conf import API_CONNECTION_STRING, API_PORT, API_ADDRESS, DEFAULT_TIMEOUT, UI_ADDRESS + DEFAULT_WALLET, DEFAULT_SEARCH_TIMEOUT, DEFAULT_CACHE_TIME, DEFAULT_UI_BRANCH +from lbrynet.conf import DEFAULT_TIMEOUT, WALLET_TYPES from lbrynet.core.StreamDescriptor import StreamDescriptorIdentifier, download_sd_blob from lbrynet.core.Session import LBRYSession from lbrynet.core.PTCWallet import PTCWallet @@ -129,7 +130,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): isLeaf = True - def __init__(self, ui_version_info, wallet_type=DEFAULT_WALLET): + def __init__(self, root, wallet_type=DEFAULT_WALLET): jsonrpc.JSONRPC.__init__(self) reactor.addSystemEventTrigger('before', 'shutdown', self._shutdown) @@ -139,9 +140,10 @@ class LBRYDaemon(jsonrpc.JSONRPC): self.connected_to_internet = True self.connection_problem = None self.query_handlers = {} - self.ui_version = ui_version_info.replace('\n', '') self.git_lbrynet_version = None self.git_lbryum_version = None + self.ui_version = None + self.ip = None self.wallet_type = wallet_type self.first_run = None self.log_file = LOG_FILENAME @@ -152,20 +154,6 @@ class LBRYDaemon(jsonrpc.JSONRPC): self.streams = {} self.known_dht_nodes = KNOWN_DHT_NODES self.first_run_after_update = False - self.platform_info = { - "processor": platform.processor(), - "python_version: ": platform.python_version(), - "platform": platform.platform(), - "os_release": platform.release(), - "os_system": platform.system(), - "lbrynet_version: ": lbrynet_version, - "lbryum_version: ": lbryum_version, - "ui_version": self.ui_version, - } - try: - self.platform_info['ip'] = json.load(urlopen('http://jsonip.com'))['ip'] - except: - self.platform_info['ip'] = "Could not determine" if os.name == "nt": from lbrynet.winhelpers.knownpaths import get_path, FOLDERID, UserHandle @@ -261,7 +249,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.wallet_type = self.session_settings['wallet_type'] if self.session_settings['wallet_type'] == wallet_type else wallet_type + self.wallet_type = self.session_settings['wallet_type'] if self.session_settings['wallet_type'] in WALLET_TYPES else wallet_type self.delete_blobs_on_remove = self.session_settings['delete_blobs_on_remove'] self.peer_port = self.session_settings['peer_port'] self.dht_node_port = self.session_settings['dht_node_port'] @@ -319,6 +307,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): self.sd_identifier = StreamDescriptorIdentifier() self.stream_info_manager = TempLBRYFileMetadataManager() self.settings = LBRYSettings(self.db_dir) + self.lbry_ui_manager = LBRYUIManager(root) self.blob_request_payment_rate_manager = None self.lbry_file_metadata_manager = None self.lbry_file_manager = None @@ -392,7 +381,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): log.error(failure) return jsonrpclib.Fault(self.FAILURE, "error") - def setup(self): + def setup(self, branch=DEFAULT_UI_BRANCH, user_specified=False): def _log_starting_vals(): d = self._get_lbry_files() d.addCallback(lambda r: json.dumps([d[1] for d in r])) @@ -437,6 +426,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): self.connection_problem_checker.start(1) d = defer.Deferred() + d.addCallback(lambda _: self.lbry_ui_manager.setup(branch=branch, user_specified=user_specified)) d.addCallback(lambda _: self._initial_setup()) d.addCallback(lambda _: threads.deferToThread(self._setup_data_directory)) d.addCallback(lambda _: self._check_db_migration()) @@ -455,9 +445,29 @@ class LBRYDaemon(jsonrpc.JSONRPC): return defer.succeed(None) + def _get_platform(self): + r = { + "processor": platform.processor(), + "python_version: ": platform.python_version(), + "platform": platform.platform(), + "os_release": platform.release(), + "os_system": platform.system(), + "lbrynet_version: ": lbrynet_version, + "lbryum_version: ": lbryum_version, + "ui_version": self.lbry_ui_manager.loaded_git_version, + } + if not self.ip: + try: + r['ip'] = json.load(urlopen('http://jsonip.com'))['ip'] + self.ip = r['ip'] + except: + r['ip'] = "Could not determine" + + return r + def _initial_setup(self): def _log_platform(): - log.info("Platform: " + json.dumps(self.platform_info)) + log.info("Platform: " + json.dumps(self._get_platform())) return defer.succeed(None) d = _log_platform() @@ -545,10 +555,13 @@ class LBRYDaemon(jsonrpc.JSONRPC): return defer.succeed(True) def _stop_server(self): - if self.lbry_server_port is not None: - self.lbry_server_port, p = None, self.lbry_server_port - return defer.maybeDeferred(p.stopListening) - else: + try: + if self.lbry_server_port is not None: + self.lbry_server_port, p = None, self.lbry_server_port + return defer.maybeDeferred(p.stopListening) + else: + return defer.succeed(True) + except AttributeError: return defer.succeed(True) def _setup_server(self): @@ -630,17 +643,21 @@ class LBRYDaemon(jsonrpc.JSONRPC): def _shutdown(self): log.info("Closing lbrynet session") log.info("Status at time of shutdown: " + self.startup_status[0]) - self.internet_connection_checker.stop() - self.version_checker.stop() - self.connection_problem_checker.stop() + if self.internet_connection_checker.running: + self.internet_connection_checker.stop() + if self.version_checker.running: + self.version_checker.stop() + if self.connection_problem_checker.running: + self.connection_problem_checker.stop() d = self._upload_log(name_prefix="close", exclude_previous=False if self.first_run else True) d.addCallback(lambda _: self._stop_server()) + d.addErrback(lambda err: True) d.addCallback(lambda _: self.lbry_file_manager.stop()) - d.addErrback(lambda err: log.info("Bad server shutdown: " + err.getTraceback())) + d.addErrback(lambda err: True) if self.session is not None: d.addCallback(lambda _: self.session.shut_down()) - d.addErrback(lambda err: log.info("Bad session shutdown: " + err.getTraceback())) + d.addErrback(lambda err: True) return d def _update_settings(self, settings): @@ -819,7 +836,8 @@ class LBRYDaemon(jsonrpc.JSONRPC): log.info("Using PTC wallet") d = defer.succeed(PTCWallet(self.db_dir)) else: - d = defer.fail() + log.info("Requested unknown wallet '%s', using default lbryum" % self.wallet_type) + d = defer.succeed(LBRYumWallet(self.db_dir)) d.addCallback(lambda wallet: {"wallet": wallet}) return d @@ -1392,10 +1410,11 @@ class LBRYDaemon(jsonrpc.JSONRPC): "remote_lbryum": most recent lbryum version available from github """ + platform_info = self._get_platform() msg = { - 'platform': self.platform_info['platform'], - 'os_release': self.platform_info['os_release'], - 'os_system': self.platform_info['os_system'], + 'platform': platform_info['platform'], + 'os_release': platform_info['os_release'], + 'os_system': platform_info['os_system'], 'lbrynet_version': lbrynet_version, 'lbryum_version': lbryum_version, 'ui_version': self.ui_version, @@ -2113,25 +2132,18 @@ class LBRYDaemon(jsonrpc.JSONRPC): # # return d - def jsonrpc_check_for_new_version(self): + def jsonrpc_log(self, message): """ - Checks local version against versions in __init__.py and version.py in the lbrynet and lbryum repos + Log message Args: - None + message: message to be logged Returns: - true/false, true meaning that there is a new version available + True """ - def _check_version(): - if (lbrynet_version >= self.git_lbrynet_version) and (lbryum_version >= self.git_lbryum_version): - log.info("[" + str(datetime.now()) + "] Up to date") - return self._render_response(False, OK_CODE) - else: - log.info("[" + str(datetime.now()) + "] Updates available") - return self._render_response(True, OK_CODE) - - return _check_version() + log.info(message) + return self._render_response(True, OK_CODE) def jsonrpc_upload_log(self, p=None): """ @@ -2140,7 +2152,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): Args, optional: 'name_prefix': prefix to indicate what is requesting the log upload 'exclude_previous': true/false, whether or not to exclude previous sessions from upload, defaults on true - Returns + Returns: True """ @@ -2171,3 +2183,22 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda _: self._log_to_slack(p['message'])) d.addCallback(lambda _: self._render_response(True, OK_CODE)) return d + + def jsonrpc_configure_ui(self, p): + """ + Configure the UI being hosted + + Args, optional: + 'branch': a branch name on lbryio/lbry-web-ui + 'path': path to a ui folder + """ + + if 'path' in p.keys(): + d = self.lbry_ui_manager.setup(user_specified=p['path']) + elif 'branch' in p.keys(): + d = self.lbry_ui_manager.setup(branch=p['branch']) + else: + d = self.lbry_ui_manager.setup() + d.addCallback(lambda r: self._render_response(r, OK_CODE)) + + return d \ No newline at end of file diff --git a/lbrynet/lbrynet_daemon/LBRYDaemonServer.py b/lbrynet/lbrynet_daemon/LBRYDaemonServer.py index 5843bca06..948c3ea2e 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemonServer.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemonServer.py @@ -18,7 +18,7 @@ from txjsonrpc.web import jsonrpc from zope.interface import implements from lbrynet.lbrynet_daemon.LBRYDaemon import LBRYDaemon -from lbrynet.conf import API_CONNECTION_STRING, API_ADDRESS, DEFAULT_WALLET, UI_ADDRESS +from lbrynet.conf import API_CONNECTION_STRING, API_ADDRESS, DEFAULT_WALLET, UI_ADDRESS, DEFAULT_UI_BRANCH if sys.platform != "darwin": @@ -149,22 +149,23 @@ class HostedLBRYFile(resource.Resource): self._producer = None resource.Resource.__init__(self) - def makeProducer(self, request, stream): - def _save_producer(producer): - self._producer = producer - return defer.succeed(None) - - range_header = request.getAllHeaders()['range'].replace('bytes=', '').split('-') - start, stop = int(range_header[0]), range_header[1] - log.info("[" + str(datetime.now()) + "] GET range %s-%s" % (start, stop)) - path = os.path.join(self._api.download_directory, stream.file_name) - - d = stream.get_total_bytes() - d.addCallback(lambda size: _save_producer(LBRYFileStreamer(request, path, start, stop, size))) - d.addCallback(lambda _: request.registerProducer(self._producer, streaming=True)) - # request.notifyFinish().addCallback(lambda _: self._producer.stopProducing()) - request.notifyFinish().addErrback(self._responseFailed, d) - return d + # todo: fix LBRYFileStreamer and use it instead of static.File + # def makeProducer(self, request, stream): + # def _save_producer(producer): + # self._producer = producer + # return defer.succeed(None) + # + # range_header = request.getAllHeaders()['range'].replace('bytes=', '').split('-') + # start, stop = int(range_header[0]), range_header[1] + # log.info("[" + str(datetime.now()) + "] GET range %s-%s" % (start, stop)) + # path = os.path.join(self._api.download_directory, stream.file_name) + # + # d = stream.get_total_bytes() + # d.addCallback(lambda size: _save_producer(LBRYFileStreamer(request, path, start, stop, size))) + # d.addCallback(lambda _: request.registerProducer(self._producer, streaming=True)) + # # request.notifyFinish().addCallback(lambda _: self._producer.stopProducing()) + # request.notifyFinish().addErrback(self._responseFailed, d) + # return d def render_GET(self, request): if 'name' in request.args.keys(): @@ -182,125 +183,22 @@ class HostedLBRYFile(resource.Resource): request.finish() return server.NOT_DONE_YET - def _responseFailed(self, err, call): - call.addErrback(lambda err: err.trap(error.ConnectionDone)) - call.addErrback(lambda err: err.trap(defer.CancelledError)) - call.addErrback(lambda err: log.info("Error: " + str(err))) - call.cancel() + # def _responseFailed(self, err, call): + # call.addErrback(lambda err: err.trap(error.ConnectionDone)) + # call.addErrback(lambda err: err.trap(defer.CancelledError)) + # call.addErrback(lambda err: log.info("Error: " + str(err))) + # call.cancel() class LBRYDaemonServer(object): - def __init__(self): - self.data_dir = user_data_dir("LBRY") - if not os.path.isdir(self.data_dir): - os.mkdir(self.data_dir) - self.version_dir = os.path.join(self.data_dir, "ui_version_history") - if not os.path.isdir(self.version_dir): - os.mkdir(self.version_dir) - self.config = os.path.join(self.version_dir, "active.json") - self.ui_dir = os.path.join(self.data_dir, "lbry-web-ui") - self.git_version = None - self._api = None - self.root = None - - if not os.path.isfile(os.path.join(self.config)): - self.loaded_git_version = None - else: - try: - f = open(self.config, "r") - loaded_ui = json.loads(f.read()) - f.close() - self.loaded_git_version = loaded_ui['commit'] - self.loaded_branch = loaded_ui['branch'] - version_log.info("[" + str(datetime.now()) + "] Last used " + self.loaded_branch + " commit " + str(self.loaded_git_version).replace("\n", "")) - except: - self.loaded_git_version = None - self.loaded_branch = None - - def setup(self, branch="master", user_specified=None): - self.branch = branch - if user_specified: - if os.path.isdir(user_specified): - log.info("Using user specified UI directory: " + str(user_specified)) - self.branch = "user-specified" - self.loaded_git_version = "user-specified" - self.ui_dir = user_specified - return defer.succeed("user-specified") - else: - log.info("User specified UI directory doesn't exist, using " + branch) - else: - log.info("Using UI branch: " + branch) - self._git_url = "https://api.github.com/repos/lbryio/lbry-web-ui/git/refs/heads/%s" % branch - self._dist_url = "https://raw.githubusercontent.com/lbryio/lbry-web-ui/%s/dist.zip" % branch - - d = self._up_to_date() - d.addCallback(lambda r: self._download_ui() if not r else self.branch) - return d - - def _up_to_date(self): - def _get_git_info(): - response = urlopen(self._git_url) - data = json.loads(response.read()) - return defer.succeed(data['object']['sha']) - - def _set_git(version): - self.git_version = version - version_log.info("[" + str(datetime.now()) + "] UI branch " + self.branch + " has a most recent commit of: " + str(self.git_version).replace("\n", "")) - - if self.git_version == self.loaded_git_version and os.path.isdir(self.ui_dir): - version_log.info("[" + str(datetime.now()) + "] local copy of " + self.branch + " is up to date") - return defer.succeed(True) - else: - if self.git_version == self.loaded_git_version: - version_log.info("[" + str(datetime.now()) + "] Can't find ui files, downloading them again") - else: - version_log.info("[" + str(datetime.now()) + "] local copy of " + self.branch + " branch is out of date, updating") - f = open(self.config, "w") - f.write(json.dumps({'commit': self.git_version, - 'time': str(datetime.now()), - 'branch': self.branch})) - f.close() - return defer.succeed(False) - - d = _get_git_info() - d.addCallback(_set_git) - return d - - def _download_ui(self): - def _delete_ui_dir(): - if os.path.isdir(self.ui_dir): - if self.loaded_git_version: - version_log.info("[" + str(datetime.now()) + "] Removed ui files for commit " + str(self.loaded_git_version).replace("\n", "")) - log.info("Removing out of date ui files") - shutil.rmtree(self.ui_dir) - return defer.succeed(None) - - def _dl_ui(): - url = urlopen(self._dist_url) - z = ZipFile(StringIO(url.read())) - names = [i for i in z.namelist() if '.DS_exStore' not in i and '__MACOSX' not in i] - z.extractall(self.ui_dir, members=names) - version_log.info("[" + str(datetime.now()) + "] Updated branch " + self.branch + ": " + str(self.loaded_git_version).replace("\n", "") + " --> " + self.git_version.replace("\n", "")) - log.info("Downloaded files for UI commit " + str(self.git_version).replace("\n", "")) - self.loaded_git_version = self.git_version - return self.branch - - d = _delete_ui_dir() - d.addCallback(lambda _: _dl_ui()) - return d - - def _setup_server(self, ui_ver, wallet): - self._api = LBRYDaemon(ui_ver, wallet_type=wallet) - self.root = LBRYindex(self.ui_dir) - for d in [i[0] for i in os.walk(self.ui_dir) if os.path.dirname(i[0]) == self.ui_dir]: - self.root.putChild(os.path.basename(d), static.File(d)) + def _setup_server(self, wallet): + self.root = LBRYindex(os.path.join(os.path.join(data_dir, "lbry-ui"), "active")) + self._api = LBRYDaemon(self.root, wallet_type=wallet) self.root.putChild("view", HostedLBRYFile(self._api)) self.root.putChild(API_ADDRESS, self._api) return defer.succeed(True) - def start(self, branch="master", user_specified=False, wallet=DEFAULT_WALLET): - d = self.setup(branch=branch, user_specified=user_specified) - d.addCallback(lambda v: self._setup_server(v, wallet)) - d.addCallback(lambda _: self._api.setup()) - + def start(self, branch=DEFAULT_UI_BRANCH, user_specified=False, wallet=DEFAULT_WALLET): + d = self._setup_server(self._setup_server(wallet)) + d.addCallback(lambda _: self._api.setup(branch, user_specified)) return d diff --git a/lbrynet/lbrynet_daemon/LBRYUIManager.py b/lbrynet/lbrynet_daemon/LBRYUIManager.py new file mode 100644 index 000000000..6c48d018e --- /dev/null +++ b/lbrynet/lbrynet_daemon/LBRYUIManager.py @@ -0,0 +1,219 @@ +import os +import logging +import shutil +import sys +import json + +from urllib2 import urlopen +from StringIO import StringIO +from twisted.web import static +from twisted.internet import defer +from lbrynet.conf import DEFAULT_UI_BRANCH +from lbrynet import __version__ as lbrynet_version +from lbryum.version import ELECTRUM_VERSION as lbryum_version +from zipfile import ZipFile +from appdirs import user_data_dir + +if sys.platform != "darwin": + data_dir = os.path.join(os.path.expanduser("~"), ".lbrynet") +else: + data_dir = user_data_dir("LBRY") + +if not os.path.isdir(data_dir): + os.mkdir(data_dir) +version_dir = os.path.join(data_dir, "ui_version_history") +if not os.path.isdir(version_dir): + os.mkdir(version_dir) + +log = logging.getLogger(__name__) +log.addHandler(logging.FileHandler(os.path.join(data_dir, 'lbrynet-daemon.log'))) +log.setLevel(logging.INFO) + + +class LBRYUIManager(object): + def __init__(self, root): + self.data_dir = user_data_dir("LBRY") + self.ui_root = os.path.join(self.data_dir, "lbry-ui") + self.active_dir = os.path.join(self.ui_root, "active") + self.update_dir = os.path.join(self.ui_root, "update") + + if not os.path.isdir(self.data_dir): + os.mkdir(self.data_dir) + if not os.path.isdir(self.ui_root): + os.mkdir(self.ui_root) + if not os.path.isdir(self.ui_root): + os.mkdir(self.ui_root) + if not os.path.isdir(self.ui_root): + os.mkdir(self.ui_root) + + self.config = os.path.join(self.ui_root, "active.json") + self.update_requires = os.path.join(self.update_dir, "requirements.txt") + self.requirements = {} + self.ui_dir = self.active_dir + self.git_version = None + self.root = root + + if not os.path.isfile(os.path.join(self.config)): + self.loaded_git_version = None + else: + try: + f = open(self.config, "r") + loaded_ui = json.loads(f.read()) + f.close() + self.loaded_git_version = loaded_ui['commit'] + self.loaded_branch = loaded_ui['branch'] + self.loaded_requirements = loaded_ui['requirements'] + except: + self.loaded_git_version = None + self.loaded_branch = None + self.loaded_requirements = None + + def setup(self, branch=DEFAULT_UI_BRANCH, user_specified=None): + self.branch = branch + if user_specified: + if os.path.isdir(user_specified): + log.info("Checking user specified UI directory: " + str(user_specified)) + self.branch = "user-specified" + self.loaded_git_version = "user-specified" + self.ui_dir = user_specified + d = self.migrate_ui(source=user_specified) + d.addCallback(lambda _: self._load_ui()) + return d + else: + log.info("User specified UI directory doesn't exist, using " + branch) + else: + log.info("Checking for updates for UI branch: " + branch) + self._git_url = "https://api.github.com/repos/lbryio/lbry-web-ui/git/refs/heads/%s" % branch + self._dist_url = "https://raw.githubusercontent.com/lbryio/lbry-web-ui/%s/dist.zip" % branch + + d = self._up_to_date() + d.addCallback(lambda r: self._download_ui() if not r else self.branch) + return d + + def _up_to_date(self): + def _get_git_info(): + response = urlopen(self._git_url) + data = json.loads(response.read()) + return defer.succeed(data['object']['sha']) + + def _set_git(version): + self.git_version = version.replace('\n', '') + if self.git_version == self.loaded_git_version: + log.info("UI is up to date") + return defer.succeed(True) + else: + log.info("UI updates available, checking if installation meets requirements") + return defer.succeed(False) + + d = _get_git_info() + d.addCallback(_set_git) + return d + + def migrate_ui(self, source=None): + if not source: + requires_file = self.update_requires + source_dir = self.update_dir + delete_source = True + else: + requires_file = os.path.join(source, "requirements.txt") + source_dir = source + delete_source = False + + def _check_requirements(): + if not os.path.isfile(requires_file): + log.info("No requirements.txt file, rejecting request to migrate this UI") + return defer.succeed(False) + + f = open(requires_file, "r") + for requirement in [line for line in f.read().split('\n') if line]: + t = requirement.split('=') + if len(t) == 3: + self.requirements[t[0]] = {'version': t[1], 'operator': '=='} + elif t[0][-1] == ">": + self.requirements[t[0][:-1]] = {'version': t[1], 'operator': '>='} + elif t[0][-1] == "<": + self.requirements[t[0][:-1]] = {'version': t[1], 'operator': '<='} + f.close() + passed_requirements = True + for r in self.requirements: + if r == 'lbrynet': + c = lbrynet_version + elif r == 'lbryum': + c = lbryum_version + else: + c = None + if c: + if self.requirements[r]['operator'] == '==': + if not self.requirements[r]['version'] == c: + passed_requirements = False + log.info("Local version %s of %s does not meet UI requirement for version %s" % ( + c, r, self.requirements[r]['version'])) + else: + log.info("Local version of %s meets ui requirement" % r) + if self.requirements[r]['operator'] == '>=': + if not self.requirements[r]['version'] <= c: + passed_requirements = False + log.info("Local version %s of %s does not meet UI requirement for version %s" % ( + c, r, self.requirements[r]['version'])) + else: + log.info("Local version of %s meets ui requirement" % r) + if self.requirements[r]['operator'] == '<=': + if not self.requirements[r]['version'] >= c: + passed_requirements = False + log.info("Local version %s of %s does not meet UI requirement for version %s" % ( + c, r, self.requirements[r]['version'])) + else: + log.info("Local version of %s meets ui requirement" % r) + return defer.succeed(passed_requirements) + + def _disp_failure(): + log.info("Failed to satisfy requirements for branch '%s', update was not loaded" % self.branch) + return defer.succeed(False) + + def _do_migrate(): + if os.path.isdir(self.active_dir): + shutil.rmtree(self.active_dir) + shutil.copytree(source_dir, self.active_dir) + if delete_source: + shutil.rmtree(source_dir) + + log.info("Loaded UI update") + + f = open(self.config, "w") + loaded_ui = {'commit': self.git_version, 'branch': self.branch, 'requirements': self.requirements} + f.write(json.dumps(loaded_ui)) + f.close() + + self.loaded_git_version = loaded_ui['commit'] + self.loaded_branch = loaded_ui['branch'] + self.loaded_requirements = loaded_ui['requirements'] + return defer.succeed(True) + + d = _check_requirements() + d.addCallback(lambda r: _do_migrate() if r else _disp_failure()) + return d + + def _download_ui(self): + def _delete_update_dir(): + if os.path.isdir(self.update_dir): + shutil.rmtree(self.update_dir) + return defer.succeed(None) + + def _dl_ui(): + url = urlopen(self._dist_url) + z = ZipFile(StringIO(url.read())) + names = [i for i in z.namelist() if '.DS_exStore' not in i and '__MACOSX' not in i] + z.extractall(self.update_dir, members=names) + log.info("Downloaded files for UI commit " + str(self.git_version).replace("\n", "")) + return self.branch + + d = _delete_update_dir() + d.addCallback(lambda _: _dl_ui()) + d.addCallback(lambda _: self.migrate_ui()) + d.addCallback(lambda _: self._load_ui()) + return d + + def _load_ui(self): + for d in [i[0] for i in os.walk(self.active_dir) if os.path.dirname(i[0]) == self.active_dir]: + self.root.putChild(os.path.basename(d), static.File(d)) + return defer.succeed(True) \ No newline at end of file From 75052fc7739b276a06be04a5fba9fdcf9dc44c47 Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 30 May 2016 00:10:43 -0400 Subject: [PATCH 10/28] remove check_for_new_version vestige --- 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 0d3535012..928f98f2a 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -110,7 +110,7 @@ CONNECTION_PROBLEM_CODES = [ ALLOWED_DURING_STARTUP = ['is_running', 'is_first_run', 'get_time_behind_blockchain', 'stop', 'daemon_status', 'get_start_notice', - 'version', 'check_for_new_version'] + 'version'] BAD_REQUEST = 400 NOT_FOUND = 404 From 41f8b5aee25ac0e8073b6c15f2f89795ded2dd77 Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 30 May 2016 02:05:16 -0400 Subject: [PATCH 11/28] add reveal() function and delete_target_file param for delete_lbry_file --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 36 ++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index 928f98f2a..bd8c75f62 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -1,5 +1,6 @@ import locale import os +import subprocess import sys import simplejson as json import binascii @@ -1094,7 +1095,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): return d - def _delete_lbry_file(self, lbry_file): + def _delete_lbry_file(self, lbry_file, delete_file=True): d = self.lbry_file_manager.delete_lbry_file(lbry_file) def finish_deletion(lbry_file): @@ -1107,7 +1108,8 @@ class LBRYDaemon(jsonrpc.JSONRPC): d = self.lbry_file_manager.get_count_for_stream_hash(s_h) # TODO: could possibly be a timing issue here d.addCallback(lambda c: self.stream_info_manager.delete_stream(s_h) if c == 0 else True) - d.addCallback(lambda _: os.remove(os.path.join(self.download_directory, lbry_file.file_name)) if + if delete_file: + d.addCallback(lambda _: os.remove(os.path.join(self.download_directory, lbry_file.file_name)) if os.path.isfile(os.path.join(self.download_directory, lbry_file.file_name)) else defer.succeed(None)) return d @@ -1792,14 +1794,19 @@ class LBRYDaemon(jsonrpc.JSONRPC): confirmation message """ + if 'delete_target_file' in p.keys(): + delete_file = p['delete_target_file'] + else: + delete_file = True + def _delete_file(f): file_name = f.file_name - d = self._delete_lbry_file(f) + d = self._delete_lbry_file(f, delete_file=delete_file) d.addCallback(lambda _: "Deleted LBRY file" + file_name) return d - if p.keys()[0] in ['name', 'sd_hash', 'file_name']: - search_type = p.keys()[0] + if 'name' in p.keys() or 'sd_hash' in p.keys() or 'file_name' in p.keys(): + search_type = [k for k in p.keys() if k != 'delete_target_file'][0] d = self._get_lbry_file(search_type, p[search_type], return_json=False) d.addCallback(lambda l: _delete_file(l) if l else False) @@ -2201,4 +2208,23 @@ class LBRYDaemon(jsonrpc.JSONRPC): d = self.lbry_ui_manager.setup() d.addCallback(lambda r: self._render_response(r, OK_CODE)) + return d + + def jsonrpc_reveal(self, p): + """ + Open a folder in finder/file explorer + + Args: + 'path': path to be selected in finder + Returns: + True, opens finder + """ + + path = p['path'] + if sys.platform == "darwin": + d = threads.deferToThread(subprocess.Popen, ("open -R %s" % path), shell=True) + else: + d = threads.deferToThread(subprocess.Popen, ("xdg-open %s" % path), shell=True) + + d.addCallback(lambda _: self._render_response(True, OK_CODE)) return d \ No newline at end of file From 772866389f248e43f88a89c77e545119ffae0ea7 Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 30 May 2016 02:36:13 -0400 Subject: [PATCH 12/28] ui manager fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit load a ui when it’s supposed to be loaded --- lbrynet/lbrynet_daemon/LBRYUIManager.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lbrynet/lbrynet_daemon/LBRYUIManager.py b/lbrynet/lbrynet_daemon/LBRYUIManager.py index 6c48d018e..baa4b09f9 100644 --- a/lbrynet/lbrynet_daemon/LBRYUIManager.py +++ b/lbrynet/lbrynet_daemon/LBRYUIManager.py @@ -55,6 +55,8 @@ class LBRYUIManager(object): if not os.path.isfile(os.path.join(self.config)): self.loaded_git_version = None + self.loaded_branch = None + self.loaded_requirements = None else: try: f = open(self.config, "r") @@ -75,19 +77,22 @@ class LBRYUIManager(object): log.info("Checking user specified UI directory: " + str(user_specified)) self.branch = "user-specified" self.loaded_git_version = "user-specified" - self.ui_dir = user_specified d = self.migrate_ui(source=user_specified) d.addCallback(lambda _: self._load_ui()) return d else: log.info("User specified UI directory doesn't exist, using " + branch) + elif self.loaded_branch == "user-specified": + log.info("Loading user provided UI") + d = self._load_ui() + return d else: log.info("Checking for updates for UI branch: " + branch) self._git_url = "https://api.github.com/repos/lbryio/lbry-web-ui/git/refs/heads/%s" % branch self._dist_url = "https://raw.githubusercontent.com/lbryio/lbry-web-ui/%s/dist.zip" % branch d = self._up_to_date() - d.addCallback(lambda r: self._download_ui() if not r else self.branch) + d.addCallback(lambda r: self._download_ui() if not r else self._load_ui()) return d def _up_to_date(self): From c56b3e75eae1e5e73d65930de40efbe675c1535e Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 30 May 2016 02:42:33 -0400 Subject: [PATCH 13/28] add download_directory to lbry_file --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index bd8c75f62..5518bef1c 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -1217,7 +1217,8 @@ class LBRYDaemon(jsonrpc.JSONRPC): if status[0] == DOWNLOAD_RUNNING_CODE: d = f.status() d.addCallback(_get_file_status) - d.addCallback(lambda message: {'completed': f.completed, 'file_name': f.file_name, 'key': key, + d.addCallback(lambda message: {'completed': f.completed, 'file_name': f.file_name, + 'download_directory': f.download_directory,'key': key, 'points_paid': f.points_paid, 'stopped': f.stopped, 'stream_hash': f.stream_hash, 'stream_name': f.stream_name, @@ -1229,6 +1230,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): 'message': message}) else: d = defer.succeed({'completed': f.completed, 'file_name': f.file_name, 'key': key, + 'download_directory': f.download_directory, 'points_paid': f.points_paid, 'stopped': f.stopped, 'stream_hash': f.stream_hash, 'stream_name': f.stream_name, 'suggested_file_name': f.suggested_file_name, 'upload_allowed': f.upload_allowed, 'sd_hash': f.sd_hash, 'total_bytes': size, From 16ef259ae129c502d8c12671affac67de0542994 Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 30 May 2016 02:53:36 -0400 Subject: [PATCH 14/28] add full_path to lbry_file --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index 5518bef1c..cd472d677 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -1218,7 +1218,9 @@ class LBRYDaemon(jsonrpc.JSONRPC): d = f.status() d.addCallback(_get_file_status) d.addCallback(lambda message: {'completed': f.completed, 'file_name': f.file_name, - 'download_directory': f.download_directory,'key': key, + 'download_directory': f.download_directory, + 'full_path': os.path.join(f.download_directory, f.file_name), + 'key': key, 'points_paid': f.points_paid, 'stopped': f.stopped, 'stream_hash': f.stream_hash, 'stream_name': f.stream_name, @@ -1231,6 +1233,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): else: d = defer.succeed({'completed': f.completed, 'file_name': f.file_name, 'key': key, 'download_directory': f.download_directory, + 'full_path': os.path.join(f.download_directory, f.file_name), 'points_paid': f.points_paid, 'stopped': f.stopped, 'stream_hash': f.stream_hash, 'stream_name': f.stream_name, 'suggested_file_name': f.suggested_file_name, 'upload_allowed': f.upload_allowed, 'sd_hash': f.sd_hash, 'total_bytes': size, From 2c3b625b6b7010649c2007e04eb34c6d01ea1daa Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 30 May 2016 02:56:08 -0400 Subject: [PATCH 15/28] change full_path to path --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index cd472d677..7e87d4ff2 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -1219,7 +1219,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(_get_file_status) d.addCallback(lambda message: {'completed': f.completed, 'file_name': f.file_name, 'download_directory': f.download_directory, - 'full_path': os.path.join(f.download_directory, f.file_name), + 'path': os.path.join(f.download_directory, f.file_name), 'key': key, 'points_paid': f.points_paid, 'stopped': f.stopped, 'stream_hash': f.stream_hash, @@ -1233,7 +1233,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): else: d = defer.succeed({'completed': f.completed, 'file_name': f.file_name, 'key': key, 'download_directory': f.download_directory, - 'full_path': os.path.join(f.download_directory, f.file_name), + 'path': os.path.join(f.download_directory, f.file_name), 'points_paid': f.points_paid, 'stopped': f.stopped, 'stream_hash': f.stream_hash, 'stream_name': f.stream_name, 'suggested_file_name': f.suggested_file_name, 'upload_allowed': f.upload_allowed, 'sd_hash': f.sd_hash, 'total_bytes': size, From da68bcf952c46f66c33d1c58a8935a5376424600 Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 30 May 2016 03:14:59 -0400 Subject: [PATCH 16/28] third time's the charm --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index 7e87d4ff2..32e811fc9 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -1219,7 +1219,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(_get_file_status) d.addCallback(lambda message: {'completed': f.completed, 'file_name': f.file_name, 'download_directory': f.download_directory, - 'path': os.path.join(f.download_directory, f.file_name), + 'download_path': os.path.join(f.download_directory, f.file_name), 'key': key, 'points_paid': f.points_paid, 'stopped': f.stopped, 'stream_hash': f.stream_hash, @@ -1233,7 +1233,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): else: d = defer.succeed({'completed': f.completed, 'file_name': f.file_name, 'key': key, 'download_directory': f.download_directory, - 'path': os.path.join(f.download_directory, f.file_name), + 'download_path': os.path.join(f.download_directory, f.file_name), 'points_paid': f.points_paid, 'stopped': f.stopped, 'stream_hash': f.stream_hash, 'stream_name': f.stream_name, 'suggested_file_name': f.suggested_file_name, 'upload_allowed': f.upload_allowed, 'sd_hash': f.sd_hash, 'total_bytes': size, From f9c644b96494a9d5c3ed01f32e8db80320ec844f Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 30 May 2016 04:00:47 -0400 Subject: [PATCH 17/28] fix switching between --branch and --ui --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 6 ++++-- lbrynet/lbrynet_daemon/LBRYDaemonControl.py | 13 ++++++++----- lbrynet/lbrynet_daemon/LBRYDaemonServer.py | 4 ++-- lbrynet/lbrynet_daemon/LBRYUIManager.py | 4 ++-- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index 32e811fc9..4d1494f80 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -382,7 +382,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): log.error(failure) return jsonrpclib.Fault(self.FAILURE, "error") - def setup(self, branch=DEFAULT_UI_BRANCH, user_specified=False): + def setup(self, branch=DEFAULT_UI_BRANCH, user_specified=False, branch_specified=False): def _log_starting_vals(): d = self._get_lbry_files() d.addCallback(lambda r: json.dumps([d[1] for d in r])) @@ -427,7 +427,9 @@ class LBRYDaemon(jsonrpc.JSONRPC): self.connection_problem_checker.start(1) d = defer.Deferred() - d.addCallback(lambda _: self.lbry_ui_manager.setup(branch=branch, user_specified=user_specified)) + d.addCallback(lambda _: self.lbry_ui_manager.setup(branch=branch, + user_specified=user_specified, + branch_specified=branch_specified)) d.addCallback(lambda _: self._initial_setup()) d.addCallback(lambda _: threads.deferToThread(self._setup_data_directory)) d.addCallback(lambda _: self._check_db_migration()) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemonControl.py b/lbrynet/lbrynet_daemon/LBRYDaemonControl.py index 1d7531331..72c8d2c1e 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemonControl.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemonControl.py @@ -13,7 +13,8 @@ from twisted.internet import reactor, defer from jsonrpc.proxy import JSONRPCProxy from lbrynet.lbrynet_daemon.LBRYDaemonServer import LBRYDaemonServer -from lbrynet.conf import API_CONNECTION_STRING, API_INTERFACE, API_ADDRESS, API_PORT, DEFAULT_WALLET, UI_ADDRESS +from lbrynet.conf import API_CONNECTION_STRING, API_INTERFACE, API_ADDRESS, API_PORT, \ + DEFAULT_WALLET, UI_ADDRESS, DEFAULT_UI_BRANCH if sys.platform != "darwin": @@ -68,12 +69,11 @@ def start(): help="path to custom UI folder", default=None) parser.add_argument("--branch", - help="Branch of lbry-web-ui repo to use, defaults on master", - default="master") + help="Branch of lbry-web-ui repo to use, defaults on master") parser.add_argument('--no-launch', dest='launchui', action="store_false") parser.add_argument('--log-to-console', dest='logtoconsole', action="store_true") parser.add_argument('--quiet', dest='quiet', action="store_true") - parser.set_defaults(launchui=True, logtoconsole=False, quiet=False) + parser.set_defaults(branch=False, launchui=True, logtoconsole=False, quiet=False) args = parser.parse_args() if args.logtoconsole: @@ -104,7 +104,10 @@ def start(): if test_internet_connection(): lbry = LBRYDaemonServer() - d = lbry.start(branch=args.branch, user_specified=args.ui, wallet=args.wallet) + d = lbry.start(branch=args.branch if args.branch else DEFAULT_UI_BRANCH, + user_specified=args.ui, + wallet=args.wallet, + branch_specified=True if args.branch else False) if args.launchui: d.addCallback(lambda _: webbrowser.open(UI_ADDRESS)) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemonServer.py b/lbrynet/lbrynet_daemon/LBRYDaemonServer.py index 948c3ea2e..393a87b72 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemonServer.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemonServer.py @@ -198,7 +198,7 @@ class LBRYDaemonServer(object): self.root.putChild(API_ADDRESS, self._api) return defer.succeed(True) - def start(self, branch=DEFAULT_UI_BRANCH, user_specified=False, wallet=DEFAULT_WALLET): + def start(self, branch=DEFAULT_UI_BRANCH, user_specified=False, branch_specified=False, wallet=DEFAULT_WALLET): d = self._setup_server(self._setup_server(wallet)) - d.addCallback(lambda _: self._api.setup(branch, user_specified)) + d.addCallback(lambda _: self._api.setup(branch, user_specified, branch_specified)) return d diff --git a/lbrynet/lbrynet_daemon/LBRYUIManager.py b/lbrynet/lbrynet_daemon/LBRYUIManager.py index baa4b09f9..b4aa33cd5 100644 --- a/lbrynet/lbrynet_daemon/LBRYUIManager.py +++ b/lbrynet/lbrynet_daemon/LBRYUIManager.py @@ -70,7 +70,7 @@ class LBRYUIManager(object): self.loaded_branch = None self.loaded_requirements = None - def setup(self, branch=DEFAULT_UI_BRANCH, user_specified=None): + def setup(self, branch=DEFAULT_UI_BRANCH, user_specified=None, branch_specified=False): self.branch = branch if user_specified: if os.path.isdir(user_specified): @@ -82,7 +82,7 @@ class LBRYUIManager(object): return d else: log.info("User specified UI directory doesn't exist, using " + branch) - elif self.loaded_branch == "user-specified": + elif self.loaded_branch == "user-specified" and not branch_specified: log.info("Loading user provided UI") d = self._load_ui() return d From 4cd1cdf495e9e503fed34e8643c1e88203587cab Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 30 May 2016 04:37:34 -0400 Subject: [PATCH 18/28] escape spaces in paths given to reveal() --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index 4d1494f80..8c4389c89 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -2229,9 +2229,9 @@ class LBRYDaemon(jsonrpc.JSONRPC): path = p['path'] if sys.platform == "darwin": - d = threads.deferToThread(subprocess.Popen, ("open -R %s" % path), shell=True) + d = threads.deferToThread(subprocess.Popen, ("open -R %s" % path.replace(' ', '\ ')), shell=True) else: - d = threads.deferToThread(subprocess.Popen, ("xdg-open %s" % path), shell=True) + d = threads.deferToThread(subprocess.Popen, ("xdg-open %s" % path.replace(' ', '\ ')), shell=True) d.addCallback(lambda _: self._render_response(True, OK_CODE)) return d \ No newline at end of file From ba605e985a44a99365c7e5f37c23b58455bffd73 Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 30 May 2016 04:40:58 -0400 Subject: [PATCH 19/28] no shell=True --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index 8c4389c89..cf38ee02e 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -2229,9 +2229,9 @@ class LBRYDaemon(jsonrpc.JSONRPC): path = p['path'] if sys.platform == "darwin": - d = threads.deferToThread(subprocess.Popen, ("open -R %s" % path.replace(' ', '\ ')), shell=True) + d = threads.deferToThread(subprocess.Popen, ['open', '-R', path]) else: - d = threads.deferToThread(subprocess.Popen, ("xdg-open %s" % path.replace(' ', '\ ')), shell=True) + d = threads.deferToThread(subprocess.Popen, ['xdg-open', '-R', path]) d.addCallback(lambda _: self._render_response(True, OK_CODE)) return d \ No newline at end of file From c10e7b2b4196c5f06e7b972790032330d5b33728 Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 30 May 2016 15:39:27 -0400 Subject: [PATCH 20/28] fix import --- lbrynet/lbrylive/LBRYStdoutDownloader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lbrynet/lbrylive/LBRYStdoutDownloader.py b/lbrynet/lbrylive/LBRYStdoutDownloader.py index 0c1fe8c60..8c413e129 100644 --- a/lbrynet/lbrylive/LBRYStdoutDownloader.py +++ b/lbrynet/lbrylive/LBRYStdoutDownloader.py @@ -1,7 +1,7 @@ import logging import sys -from lbrynet.lbrynet_console.plugins.LBRYLive.LBRYLiveStreamDownloader import LBRYLiveStreamDownloader +from lbrynet.lbrylive.client.LiveStreamDownloader import LBRYLiveStreamDownloader from lbrynet.core.BlobManager import TempBlobManager from lbrynet.core.Session import LBRYSession from lbrynet.core.client.StandaloneBlobDownloader import StandaloneBlobDownloader From 2026024c8a8d879f6dcca77c08bdc1eaeaa44e1b Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 30 May 2016 15:49:25 -0400 Subject: [PATCH 21/28] updates from development --- lbrynet/__init__.py | 2 +- lbrynet/conf.py | 4 +- lbrynet/core/LBRYcrdWallet.py | 5 + .../lbrynet_daemon/Apps/LBRYOSXStatusBar.py | 108 -------- lbrynet/lbrynet_daemon/Apps/LBRYURIHandler.py | 60 ----- lbrynet/lbrynet_daemon/LBRYDaemon.py | 246 +++++++++++++----- lbrynet/lbrynet_daemon/LBRYDaemonControl.py | 13 +- lbrynet/lbrynet_daemon/LBRYDaemonServer.py | 207 +++------------ lbrynet/lbrynet_daemon/LBRYUIManager.py | 224 ++++++++++++++++ .../daemon_scripts/Autofetcher.py | 68 +++++ .../{Apps => daemon_scripts}/__init__.py | 0 .../daemon_scripts/migrateto025.py | 33 +++ .../daemon_scripts/network_tester.py | 160 ++++++++++++ packaging/osx/certs/cert.cer.enc | 30 +++ packaging/osx/certs/cert.p12.enc | 33 +++ setup_uri_handler.py | 25 -- 16 files changed, 780 insertions(+), 438 deletions(-) delete mode 100644 lbrynet/lbrynet_daemon/Apps/LBRYOSXStatusBar.py delete mode 100644 lbrynet/lbrynet_daemon/Apps/LBRYURIHandler.py create mode 100644 lbrynet/lbrynet_daemon/LBRYUIManager.py create mode 100644 lbrynet/lbrynet_daemon/daemon_scripts/Autofetcher.py rename lbrynet/lbrynet_daemon/{Apps => daemon_scripts}/__init__.py (100%) create mode 100644 lbrynet/lbrynet_daemon/daemon_scripts/migrateto025.py create mode 100644 lbrynet/lbrynet_daemon/daemon_scripts/network_tester.py create mode 100644 packaging/osx/certs/cert.cer.enc create mode 100644 packaging/osx/certs/cert.p12.enc delete mode 100644 setup_uri_handler.py diff --git a/lbrynet/__init__.py b/lbrynet/__init__.py index 26dafe45d..3d80f11a2 100644 --- a/lbrynet/__init__.py +++ b/lbrynet/__init__.py @@ -4,5 +4,5 @@ import logging logging.getLogger(__name__).addHandler(logging.NullHandler()) -version = (0, 2, 4) +version = (0, 2, 5) __version__ = ".".join([str(x) for x in version]) diff --git a/lbrynet/conf.py b/lbrynet/conf.py index dfae3d274..526dc711d 100644 --- a/lbrynet/conf.py +++ b/lbrynet/conf.py @@ -36,8 +36,10 @@ UI_ADDRESS = "http://" + API_INTERFACE + ":" + str(API_PORT) PROTOCOL_PREFIX = "lbry" DEFAULT_WALLET = "lbryum" +WALLET_TYPES = ["lbryum", "lbrycrd"] DEFAULT_TIMEOUT = 30 DEFAULT_MAX_SEARCH_RESULTS = 25 DEFAULT_MAX_KEY_FEE = 100.0 DEFAULT_SEARCH_TIMEOUT = 3.0 -DEFAULT_CACHE_TIME = 3600 \ No newline at end of file +DEFAULT_CACHE_TIME = 3600 +DEFAULT_UI_BRANCH = "master" diff --git a/lbrynet/core/LBRYcrdWallet.py b/lbrynet/core/LBRYcrdWallet.py index 8ec200611..17fd0e229 100644 --- a/lbrynet/core/LBRYcrdWallet.py +++ b/lbrynet/core/LBRYcrdWallet.py @@ -295,6 +295,11 @@ class LBRYWallet(object): d.addCallback(self._get_stream_info_from_value, name) return d + def get_txid_for_name(self, name): + d = self._get_value_for_name(name) + d.addCallback(lambda r: None if 'txid' not in r else r['txid']) + return d + def get_stream_info_from_txid(self, name, txid): d = self.get_claims_from_tx(txid) diff --git a/lbrynet/lbrynet_daemon/Apps/LBRYOSXStatusBar.py b/lbrynet/lbrynet_daemon/Apps/LBRYOSXStatusBar.py deleted file mode 100644 index 5ca433f72..000000000 --- a/lbrynet/lbrynet_daemon/Apps/LBRYOSXStatusBar.py +++ /dev/null @@ -1,108 +0,0 @@ -import rumps -import xmlrpclib -import os -import webbrowser -import subprocess -import argparse - - -class DaemonStatusBarApp(rumps.App): - def __init__(self): - icon_path = 'app.icns' - if os.path.isfile(icon_path): - rumps.App.__init__(self, name="LBRY", icon=icon_path, quit_button=None, - menu=["Open", "Preferences", "View balance", "Quit"]) - else: - rumps.App.__init__(self, name="LBRY", title="LBRY", quit_button=None, - menu=["Open", "Preferences", "View balance", "Quit"]) - - @rumps.timer(1) - def alert_daemon_start(self): - daemon = xmlrpclib.ServerProxy("http://localhost:7080/") - try: - start_msg = daemon.is_running() - if isinstance(start_msg, str): - rumps.notification(title='LBRY', subtitle='', message=str(start_msg), sound=True) - update_info = daemon.check_for_new_version() - update_msg = "" - for p in update_info: - if not p[0]: - update_msg += p[1] + "\n" - if update_msg: - update_msg += "\n Try running the installer again to fix this" - rumps.notification(title='LBRY', subtitle='', message=update_msg, sound=True) - except: - pass - - @rumps.clicked('Open') - def get_ui(self): - daemon = xmlrpclib.ServerProxy("http://localhost:7080/") - try: - daemon.is_running() - webbrowser.get('safari').open("lbry://lbry") - except: - try: - rumps.notification(title='LBRY', subtitle='', message="Couldn't connect to lbrynet daemon", sound=True) - except: - rumps.alert(title='LBRY', message="Couldn't connect to lbrynet daemon") - - @rumps.clicked("Preferences") - def prefs(self): - daemon = xmlrpclib.ServerProxy("http://localhost:7080/") - try: - daemon.is_running() - webbrowser.get('safari').open("lbry://settings") - except: - rumps.notification(title='LBRY', subtitle='', message="Couldn't connect to lbrynet daemon", sound=True) - - @rumps.clicked("View balance") - def disp_balance(self): - daemon = xmlrpclib.ServerProxy("http://localhost:7080/") - try: - balance = daemon.get_balance() - r = round(float(balance), 2) - try: - rumps.notification(title='LBRY', subtitle='', message=str("Your balance is %.2f LBC" % r), sound=False) - except: - rumps.alert(title='LBRY', message=str("Your balance is %.2f LBC" % r)) - - except: - try: - rumps.notification(title='LBRY', subtitle='', message="Couldn't connect to lbrynet daemon", sound=True) - except: - rumps.alert(title='LBRY', message="Couldn't connect to lbrynet daemon") - - @rumps.clicked('Quit') - def clean_quit(self): - daemon = xmlrpclib.ServerProxy("http://localhost:7080/") - try: - daemon.stop() - except: - pass - rumps.quit_application() - - -def main(): - parser = argparse.ArgumentParser(description="Launch lbrynet status bar application") - parser.add_argument("--startdaemon", - help="true or false, default true", - type=str, - default="true") - args = parser.parse_args() - - if str(args.startdaemon).lower() == "true": - daemon = xmlrpclib.ServerProxy('http://localhost:7080') - try: - daemon.is_running() - except: - subprocess.Popen("screen -dmS lbrynet bash -c " - "'PYTHONPATH=$PYTHONPATH:`cat /Users/${USER}/Library/Application\ Support/lbrynet/.python_path`; " - "PATH=$PATH:`cat /Users/${USER}/Library/Application\ Support/lbrynet/.lbry_bin_path`; " - "lbrynet-daemon --update=False'", shell=True) - - status_app = DaemonStatusBarApp() - status_app.run() - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/lbrynet/lbrynet_daemon/Apps/LBRYURIHandler.py b/lbrynet/lbrynet_daemon/Apps/LBRYURIHandler.py deleted file mode 100644 index f6990cfea..000000000 --- a/lbrynet/lbrynet_daemon/Apps/LBRYURIHandler.py +++ /dev/null @@ -1,60 +0,0 @@ -import os -import json -import webbrowser -import subprocess -import sys - -from time import sleep -from jsonrpc.proxy import JSONRPCProxy - -API_CONNECTION_STRING = "http://localhost:5279/lbryapi" -UI_ADDRESS = "http://localhost:5279" - - -class LBRYURIHandler(object): - def __init__(self): - self.started_daemon = False - self.daemon = JSONRPCProxy.from_url(API_CONNECTION_STRING) - - def handle_osx(self, lbry_name): - try: - status = self.daemon.is_running() - except: - os.system("open /Applications/LBRY.app") - sleep(3) - - if lbry_name == "lbry" or lbry_name == "": - webbrowser.open(UI_ADDRESS) - else: - webbrowser.open(UI_ADDRESS + "/?watch=" + lbry_name) - - def handle_linux(self, lbry_name): - try: - status = self.daemon.is_running() - except: - cmd = r'DIR = "$( cd "$(dirname "${BASH_SOURCE[0]}" )" && pwd )"' \ - r'if [-z "$(pgrep lbrynet-daemon)"]; then' \ - r'echo "running lbrynet-daemon..."' \ - r'$DIR / lbrynet - daemon &' \ - r'sleep 3 # let the daemon load before connecting' \ - r'fi' - subprocess.Popen(cmd, shell=True) - - if lbry_name == "lbry" or lbry_name == "": - webbrowser.open(UI_ADDRESS) - else: - webbrowser.open(UI_ADDRESS + "/?watch=" + lbry_name) - - -def main(args): - if len(args) != 1: - args = ['lbry://lbry'] - - name = args[0][7:] - if sys.platform == "darwin": - LBRYURIHandler().handle_osx(lbry_name=name) - else: - LBRYURIHandler().handle_linux(lbry_name=name) - -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index 0a10dff9d..cf38ee02e 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -1,5 +1,6 @@ import locale import os +import subprocess import sys import simplejson as json import binascii @@ -31,13 +32,14 @@ from lbrynet.core.Error import UnknownNameError, InsufficientFundsError from lbrynet.lbryfile.StreamDescriptor import LBRYFileStreamType from lbrynet.lbryfile.client.LBRYFileDownloader import LBRYFileSaverFactory, LBRYFileOpenerFactory from lbrynet.lbryfile.client.LBRYFileOptions import add_lbry_file_to_sd_identifier +from lbrynet.lbrynet_daemon.LBRYUIManager import LBRYUIManager from lbrynet.lbrynet_daemon.LBRYDownloader import GetStream from lbrynet.lbrynet_daemon.LBRYPublisher import Publisher from lbrynet.core.utils import generate_id 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 -from lbrynet.conf import API_CONNECTION_STRING, API_PORT, API_ADDRESS, DEFAULT_TIMEOUT, UI_ADDRESS + DEFAULT_WALLET, DEFAULT_SEARCH_TIMEOUT, DEFAULT_CACHE_TIME, DEFAULT_UI_BRANCH +from lbrynet.conf import DEFAULT_TIMEOUT, WALLET_TYPES from lbrynet.core.StreamDescriptor import StreamDescriptorIdentifier, download_sd_blob from lbrynet.core.Session import LBRYSession from lbrynet.core.PTCWallet import PTCWallet @@ -109,7 +111,7 @@ CONNECTION_PROBLEM_CODES = [ ALLOWED_DURING_STARTUP = ['is_running', 'is_first_run', 'get_time_behind_blockchain', 'stop', 'daemon_status', 'get_start_notice', - 'version', 'check_for_new_version'] + 'version'] BAD_REQUEST = 400 NOT_FOUND = 404 @@ -129,7 +131,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): isLeaf = True - def __init__(self, ui_version_info, wallet_type=DEFAULT_WALLET): + def __init__(self, root, wallet_type=DEFAULT_WALLET): jsonrpc.JSONRPC.__init__(self) reactor.addSystemEventTrigger('before', 'shutdown', self._shutdown) @@ -139,9 +141,10 @@ class LBRYDaemon(jsonrpc.JSONRPC): self.connected_to_internet = True self.connection_problem = None self.query_handlers = {} - self.ui_version = ui_version_info.replace('\n', '') self.git_lbrynet_version = None self.git_lbryum_version = None + self.ui_version = None + self.ip = None self.wallet_type = wallet_type self.first_run = None self.log_file = LOG_FILENAME @@ -151,20 +154,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): self.waiting_on = {} self.streams = {} self.known_dht_nodes = KNOWN_DHT_NODES - self.platform_info = { - "processor": platform.processor(), - "python_version: ": platform.python_version(), - "platform": platform.platform(), - "os_release": platform.release(), - "os_system": platform.system(), - "lbrynet_version: ": lbrynet_version, - "lbryum_version: ": lbryum_version, - "ui_version": self.ui_version, - } - try: - self.platform_info['ip'] = json.load(urlopen('http://jsonip.com'))['ip'] - except: - self.platform_info['ip'] = "Could not determine" + self.first_run_after_update = False if os.name == "nt": from lbrynet.winhelpers.knownpaths import get_path, FOLDERID, UserHandle @@ -197,7 +187,9 @@ class LBRYDaemon(jsonrpc.JSONRPC): 'use_upnp': True, 'start_lbrycrdd': True, 'requested_first_run_credits': False, - 'cache_time': DEFAULT_CACHE_TIME + 'cache_time': DEFAULT_CACHE_TIME, + 'startup_scripts': [], + 'last_version': {'lbrynet': lbrynet_version, 'lbryum': lbryum_version} } if os.path.isfile(self.daemon_conf): @@ -234,6 +226,20 @@ class LBRYDaemon(jsonrpc.JSONRPC): self.session_settings = settings_dict + if 'last_version' in missing_settings.keys(): + self.session_settings['last_version'] = None + + if self.session_settings['last_version'] != self.default_settings['last_version']: + self.session_settings['last_version'] = self.default_settings['last_version'] + f = open(self.daemon_conf, "w") + f.write(json.dumps(self.session_settings)) + f.close() + + self.first_run_after_update = True + log.info("First run after update") + if lbrynet_version == '0.2.5': + self.session_settings['startup_scripts'].append({'script_name': 'migrateto025', 'run_once': True}) + self.run_on_startup = self.session_settings['run_on_startup'] self.data_rate = self.session_settings['data_rate'] self.max_key_fee = self.session_settings['max_key_fee'] @@ -244,7 +250,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.wallet_type = self.session_settings['wallet_type'] if self.session_settings['wallet_type'] == wallet_type else wallet_type + self.wallet_type = self.session_settings['wallet_type'] if self.session_settings['wallet_type'] in WALLET_TYPES else wallet_type self.delete_blobs_on_remove = self.session_settings['delete_blobs_on_remove'] self.peer_port = self.session_settings['peer_port'] self.dht_node_port = self.session_settings['dht_node_port'] @@ -252,6 +258,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): self.start_lbrycrdd = self.session_settings['start_lbrycrdd'] self.requested_first_run_credits = self.session_settings['requested_first_run_credits'] self.cache_time = self.session_settings['cache_time'] + self.startup_scripts = self.session_settings['startup_scripts'] if os.path.isfile(os.path.join(self.db_dir, "stream_info_cache.json")): f = open(os.path.join(self.db_dir, "stream_info_cache.json"), "r") @@ -301,6 +308,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): self.sd_identifier = StreamDescriptorIdentifier() self.stream_info_manager = TempLBRYFileMetadataManager() self.settings = LBRYSettings(self.db_dir) + self.lbry_ui_manager = LBRYUIManager(root) self.blob_request_payment_rate_manager = None self.lbry_file_metadata_manager = None self.lbry_file_manager = None @@ -374,7 +382,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): log.error(failure) return jsonrpclib.Fault(self.FAILURE, "error") - def setup(self): + def setup(self, branch=DEFAULT_UI_BRANCH, user_specified=False, branch_specified=False): def _log_starting_vals(): d = self._get_lbry_files() d.addCallback(lambda r: json.dumps([d[1] for d in r])) @@ -394,6 +402,10 @@ class LBRYDaemon(jsonrpc.JSONRPC): self.announced_startup = True self.startup_status = STARTUP_STAGES[5] log.info("[" + str(datetime.now()) + "] Started lbrynet-daemon") + if len(self.startup_scripts): + log.info("Scheduling scripts") + reactor.callLater(3, self._run_scripts) + # self.lbrynet_connection_checker.start(3600) if self.first_run: @@ -415,6 +427,9 @@ class LBRYDaemon(jsonrpc.JSONRPC): self.connection_problem_checker.start(1) d = defer.Deferred() + d.addCallback(lambda _: self.lbry_ui_manager.setup(branch=branch, + user_specified=user_specified, + branch_specified=branch_specified)) d.addCallback(lambda _: self._initial_setup()) d.addCallback(lambda _: threads.deferToThread(self._setup_data_directory)) d.addCallback(lambda _: self._check_db_migration()) @@ -433,9 +448,29 @@ class LBRYDaemon(jsonrpc.JSONRPC): return defer.succeed(None) + def _get_platform(self): + r = { + "processor": platform.processor(), + "python_version: ": platform.python_version(), + "platform": platform.platform(), + "os_release": platform.release(), + "os_system": platform.system(), + "lbrynet_version: ": lbrynet_version, + "lbryum_version: ": lbryum_version, + "ui_version": self.lbry_ui_manager.loaded_git_version, + } + if not self.ip: + try: + r['ip'] = json.load(urlopen('http://jsonip.com'))['ip'] + self.ip = r['ip'] + except: + r['ip'] = "Could not determine" + + return r + def _initial_setup(self): def _log_platform(): - log.info("Platform: " + json.dumps(self.platform_info)) + log.info("Platform: " + json.dumps(self._get_platform())) return defer.succeed(None) d = _log_platform() @@ -523,10 +558,13 @@ class LBRYDaemon(jsonrpc.JSONRPC): return defer.succeed(True) def _stop_server(self): - if self.lbry_server_port is not None: - self.lbry_server_port, p = None, self.lbry_server_port - return defer.maybeDeferred(p.stopListening) - else: + try: + if self.lbry_server_port is not None: + self.lbry_server_port, p = None, self.lbry_server_port + return defer.maybeDeferred(p.stopListening) + else: + return defer.succeed(True) + except AttributeError: return defer.succeed(True) def _setup_server(self): @@ -608,14 +646,21 @@ class LBRYDaemon(jsonrpc.JSONRPC): def _shutdown(self): log.info("Closing lbrynet session") log.info("Status at time of shutdown: " + self.startup_status[0]) + if self.internet_connection_checker.running: + self.internet_connection_checker.stop() + if self.version_checker.running: + self.version_checker.stop() + if self.connection_problem_checker.running: + self.connection_problem_checker.stop() d = self._upload_log(name_prefix="close", exclude_previous=False if self.first_run else True) d.addCallback(lambda _: self._stop_server()) + d.addErrback(lambda err: True) d.addCallback(lambda _: self.lbry_file_manager.stop()) - d.addErrback(lambda err: log.info("Bad server shutdown: " + err.getTraceback())) + d.addErrback(lambda err: True) if self.session is not None: d.addCallback(lambda _: self.session.shut_down()) - d.addErrback(lambda err: log.info("Bad session shutdown: " + err.getTraceback())) + d.addErrback(lambda err: True) return d def _update_settings(self, settings): @@ -794,7 +839,8 @@ class LBRYDaemon(jsonrpc.JSONRPC): log.info("Using PTC wallet") d = defer.succeed(PTCWallet(self.db_dir)) else: - d = defer.fail() + log.info("Requested unknown wallet '%s', using default lbryum" % self.wallet_type) + d = defer.succeed(LBRYumWallet(self.db_dir)) d.addCallback(lambda wallet: {"wallet": wallet}) return d @@ -1017,19 +1063,31 @@ class LBRYDaemon(jsonrpc.JSONRPC): f.close() return defer.succeed(True) - def _resolve_name(self, name): + def _resolve_name(self, name, force_refresh=False): def _cache_stream_info(stream_info): + def _add_txid(txid): + self.name_cache[name]['txid'] = txid + return defer.succeed(None) + self.name_cache[name] = {'claim_metadata': stream_info, 'timestamp': self._get_long_count_timestamp()} - d = self._update_claim_cache() + d = self.session.wallet.get_txid_for_name(name) + d.addCallback(_add_txid) + d.addCallback(lambda _: self._update_claim_cache()) d.addCallback(lambda _: self.name_cache[name]['claim_metadata']) + return d - if name in self.name_cache.keys(): - if (self._get_long_count_timestamp() - self.name_cache[name]['timestamp']) < self.cache_time: - log.info("[" + str(datetime.now()) + "] Returning cached stream info for lbry://" + name) - d = defer.succeed(self.name_cache[name]['claim_metadata']) + if not force_refresh: + if name in self.name_cache.keys(): + if (self._get_long_count_timestamp() - self.name_cache[name]['timestamp']) < self.cache_time: + log.info("[" + str(datetime.now()) + "] Returning cached stream info for lbry://" + name) + d = defer.succeed(self.name_cache[name]['claim_metadata']) + else: + log.info("[" + str(datetime.now()) + "] Refreshing stream info for lbry://" + name) + d = self.session.wallet.get_stream_info_for_name(name) + d.addCallbacks(_cache_stream_info, lambda _: defer.fail(UnknownNameError)) else: - log.info("[" + str(datetime.now()) + "] Refreshing stream info for lbry://" + name) + log.info("[" + str(datetime.now()) + "] Resolving stream info for lbry://" + name) d = self.session.wallet.get_stream_info_for_name(name) d.addCallbacks(_cache_stream_info, lambda _: defer.fail(UnknownNameError)) else: @@ -1039,7 +1097,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): return d - def _delete_lbry_file(self, lbry_file): + def _delete_lbry_file(self, lbry_file, delete_file=True): d = self.lbry_file_manager.delete_lbry_file(lbry_file) def finish_deletion(lbry_file): @@ -1052,7 +1110,8 @@ class LBRYDaemon(jsonrpc.JSONRPC): d = self.lbry_file_manager.get_count_for_stream_hash(s_h) # TODO: could possibly be a timing issue here d.addCallback(lambda c: self.stream_info_manager.delete_stream(s_h) if c == 0 else True) - d.addCallback(lambda _: os.remove(os.path.join(self.download_directory, lbry_file.file_name)) if + if delete_file: + d.addCallback(lambda _: os.remove(os.path.join(self.download_directory, lbry_file.file_name)) if os.path.isfile(os.path.join(self.download_directory, lbry_file.file_name)) else defer.succeed(None)) return d @@ -1160,7 +1219,10 @@ class LBRYDaemon(jsonrpc.JSONRPC): if status[0] == DOWNLOAD_RUNNING_CODE: d = f.status() d.addCallback(_get_file_status) - d.addCallback(lambda message: {'completed': f.completed, 'file_name': f.file_name, 'key': key, + d.addCallback(lambda message: {'completed': f.completed, 'file_name': f.file_name, + 'download_directory': f.download_directory, + 'download_path': os.path.join(f.download_directory, f.file_name), + 'key': key, 'points_paid': f.points_paid, 'stopped': f.stopped, 'stream_hash': f.stream_hash, 'stream_name': f.stream_name, @@ -1172,6 +1234,8 @@ class LBRYDaemon(jsonrpc.JSONRPC): 'message': message}) else: d = defer.succeed({'completed': f.completed, 'file_name': f.file_name, 'key': key, + 'download_directory': f.download_directory, + 'download_path': os.path.join(f.download_directory, f.file_name), 'points_paid': f.points_paid, 'stopped': f.stopped, 'stream_hash': f.stream_hash, 'stream_name': f.stream_name, 'suggested_file_name': f.suggested_file_name, 'upload_allowed': f.upload_allowed, 'sd_hash': f.sd_hash, 'total_bytes': size, @@ -1221,6 +1285,31 @@ class LBRYDaemon(jsonrpc.JSONRPC): requests.post(URL, json.dumps({"text": msg})) return defer.succeed(None) + def _run_scripts(self): + if len([k for k in self.startup_scripts if 'run_once' in k.keys()]): + log.info("Removing one time startup scripts") + f = open(self.daemon_conf, "r") + initialsettings = json.loads(f.read()) + f.close() + t = [s for s in self.startup_scripts if 'run_once' not in s.keys()] + initialsettings['startup_scripts'] = t + f = open(self.daemon_conf, "w") + f.write(json.dumps(initialsettings)) + f.close() + + for script in self.startup_scripts: + if script['script_name'] == 'migrateto025': + log.info("Running migrator to 0.2.5") + from lbrynet.lbrynet_daemon.daemon_scripts.migrateto025 import run as run_migrate + run_migrate(self) + + if script['script_name'] == 'Autofetcher': + log.info("Starting autofetcher script") + from lbrynet.lbrynet_daemon.daemon_scripts.Autofetcher import run as run_autofetcher + run_autofetcher(self) + + return defer.succeed(None) + def _render_response(self, result, code): return defer.succeed({'result': result, 'code': code}) @@ -1330,10 +1419,11 @@ class LBRYDaemon(jsonrpc.JSONRPC): "remote_lbryum": most recent lbryum version available from github """ + platform_info = self._get_platform() msg = { - 'platform': self.platform_info['platform'], - 'os_release': self.platform_info['os_release'], - 'os_system': self.platform_info['os_system'], + 'platform': platform_info['platform'], + 'os_release': platform_info['os_release'], + 'os_system': platform_info['os_system'], 'lbrynet_version': lbrynet_version, 'lbryum_version': lbryum_version, 'ui_version': self.ui_version, @@ -1711,14 +1801,19 @@ class LBRYDaemon(jsonrpc.JSONRPC): confirmation message """ + if 'delete_target_file' in p.keys(): + delete_file = p['delete_target_file'] + else: + delete_file = True + def _delete_file(f): file_name = f.file_name - d = self._delete_lbry_file(f) + d = self._delete_lbry_file(f, delete_file=delete_file) d.addCallback(lambda _: "Deleted LBRY file" + file_name) return d - if p.keys()[0] in ['name', 'sd_hash', 'file_name']: - search_type = p.keys()[0] + if 'name' in p.keys() or 'sd_hash' in p.keys() or 'file_name' in p.keys(): + search_type = [k for k in p.keys() if k != 'delete_target_file'][0] d = self._get_lbry_file(search_type, p[search_type], return_json=False) d.addCallback(lambda l: _delete_file(l) if l else False) @@ -2051,25 +2146,18 @@ class LBRYDaemon(jsonrpc.JSONRPC): # # return d - def jsonrpc_check_for_new_version(self): + def jsonrpc_log(self, message): """ - Checks local version against versions in __init__.py and version.py in the lbrynet and lbryum repos + Log message Args: - None + message: message to be logged Returns: - true/false, true meaning that there is a new version available + True """ - def _check_version(): - if (lbrynet_version >= self.git_lbrynet_version) and (lbryum_version >= self.git_lbryum_version): - log.info("[" + str(datetime.now()) + "] Up to date") - return self._render_response(False, OK_CODE) - else: - log.info("[" + str(datetime.now()) + "] Updates available") - return self._render_response(True, OK_CODE) - - return _check_version() + log.info(message) + return self._render_response(True, OK_CODE) def jsonrpc_upload_log(self, p=None): """ @@ -2078,7 +2166,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): Args, optional: 'name_prefix': prefix to indicate what is requesting the log upload 'exclude_previous': true/false, whether or not to exclude previous sessions from upload, defaults on true - Returns + Returns: True """ @@ -2109,3 +2197,41 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda _: self._log_to_slack(p['message'])) d.addCallback(lambda _: self._render_response(True, OK_CODE)) return d + + def jsonrpc_configure_ui(self, p): + """ + Configure the UI being hosted + + Args, optional: + 'branch': a branch name on lbryio/lbry-web-ui + 'path': path to a ui folder + """ + + if 'path' in p.keys(): + d = self.lbry_ui_manager.setup(user_specified=p['path']) + elif 'branch' in p.keys(): + d = self.lbry_ui_manager.setup(branch=p['branch']) + else: + d = self.lbry_ui_manager.setup() + d.addCallback(lambda r: self._render_response(r, OK_CODE)) + + return d + + def jsonrpc_reveal(self, p): + """ + Open a folder in finder/file explorer + + Args: + 'path': path to be selected in finder + Returns: + True, opens finder + """ + + path = p['path'] + if sys.platform == "darwin": + d = threads.deferToThread(subprocess.Popen, ['open', '-R', path]) + else: + d = threads.deferToThread(subprocess.Popen, ['xdg-open', '-R', path]) + + d.addCallback(lambda _: self._render_response(True, OK_CODE)) + return d \ No newline at end of file diff --git a/lbrynet/lbrynet_daemon/LBRYDaemonControl.py b/lbrynet/lbrynet_daemon/LBRYDaemonControl.py index 1d7531331..72c8d2c1e 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemonControl.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemonControl.py @@ -13,7 +13,8 @@ from twisted.internet import reactor, defer from jsonrpc.proxy import JSONRPCProxy from lbrynet.lbrynet_daemon.LBRYDaemonServer import LBRYDaemonServer -from lbrynet.conf import API_CONNECTION_STRING, API_INTERFACE, API_ADDRESS, API_PORT, DEFAULT_WALLET, UI_ADDRESS +from lbrynet.conf import API_CONNECTION_STRING, API_INTERFACE, API_ADDRESS, API_PORT, \ + DEFAULT_WALLET, UI_ADDRESS, DEFAULT_UI_BRANCH if sys.platform != "darwin": @@ -68,12 +69,11 @@ def start(): help="path to custom UI folder", default=None) parser.add_argument("--branch", - help="Branch of lbry-web-ui repo to use, defaults on master", - default="master") + help="Branch of lbry-web-ui repo to use, defaults on master") parser.add_argument('--no-launch', dest='launchui', action="store_false") parser.add_argument('--log-to-console', dest='logtoconsole', action="store_true") parser.add_argument('--quiet', dest='quiet', action="store_true") - parser.set_defaults(launchui=True, logtoconsole=False, quiet=False) + parser.set_defaults(branch=False, launchui=True, logtoconsole=False, quiet=False) args = parser.parse_args() if args.logtoconsole: @@ -104,7 +104,10 @@ def start(): if test_internet_connection(): lbry = LBRYDaemonServer() - d = lbry.start(branch=args.branch, user_specified=args.ui, wallet=args.wallet) + d = lbry.start(branch=args.branch if args.branch else DEFAULT_UI_BRANCH, + user_specified=args.ui, + wallet=args.wallet, + branch_specified=True if args.branch else False) if args.launchui: d.addCallback(lambda _: webbrowser.open(UI_ADDRESS)) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemonServer.py b/lbrynet/lbrynet_daemon/LBRYDaemonServer.py index 85494d21c..393a87b72 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemonServer.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemonServer.py @@ -18,7 +18,7 @@ from txjsonrpc.web import jsonrpc from zope.interface import implements from lbrynet.lbrynet_daemon.LBRYDaemon import LBRYDaemon -from lbrynet.conf import API_CONNECTION_STRING, API_ADDRESS, DEFAULT_WALLET, UI_ADDRESS +from lbrynet.conf import API_CONNECTION_STRING, API_ADDRESS, DEFAULT_WALLET, UI_ADDRESS, DEFAULT_UI_BRANCH if sys.platform != "darwin": @@ -149,22 +149,23 @@ class HostedLBRYFile(resource.Resource): self._producer = None resource.Resource.__init__(self) - def makeProducer(self, request, stream): - def _save_producer(producer): - self._producer = producer - return defer.succeed(None) - - range_header = request.getAllHeaders()['range'].replace('bytes=', '').split('-') - start, stop = int(range_header[0]), range_header[1] - log.info("[" + str(datetime.now()) + "] GET range %s-%s" % (start, stop)) - path = os.path.join(self._api.download_directory, stream.file_name) - - d = stream.get_total_bytes() - d.addCallback(lambda size: _save_producer(LBRYFileStreamer(request, path, start, stop, size))) - d.addCallback(lambda _: request.registerProducer(self._producer, streaming=True)) - # request.notifyFinish().addCallback(lambda _: self._producer.stopProducing()) - request.notifyFinish().addErrback(self._responseFailed, d) - return d + # todo: fix LBRYFileStreamer and use it instead of static.File + # def makeProducer(self, request, stream): + # def _save_producer(producer): + # self._producer = producer + # return defer.succeed(None) + # + # range_header = request.getAllHeaders()['range'].replace('bytes=', '').split('-') + # start, stop = int(range_header[0]), range_header[1] + # log.info("[" + str(datetime.now()) + "] GET range %s-%s" % (start, stop)) + # path = os.path.join(self._api.download_directory, stream.file_name) + # + # d = stream.get_total_bytes() + # d.addCallback(lambda size: _save_producer(LBRYFileStreamer(request, path, start, stop, size))) + # d.addCallback(lambda _: request.registerProducer(self._producer, streaming=True)) + # # request.notifyFinish().addCallback(lambda _: self._producer.stopProducing()) + # request.notifyFinish().addErrback(self._responseFailed, d) + # return d def render_GET(self, request): if 'name' in request.args.keys(): @@ -182,172 +183,22 @@ class HostedLBRYFile(resource.Resource): request.finish() return server.NOT_DONE_YET - def _responseFailed(self, err, call): - call.addErrback(lambda err: err.trap(error.ConnectionDone)) - call.addErrback(lambda err: err.trap(defer.CancelledError)) - call.addErrback(lambda err: log.info("Error: " + str(err))) - call.cancel() - - -class MyLBRYFiles(resource.Resource): - isLeaf = False - - def __init__(self): - resource.Resource.__init__(self) - self.files_table = None - - def delayed_render(self, request, result): - request.write(result.encode('utf-8')) - request.finish() - - def render_GET(self, request): - self.files_table = None - api = jsonrpc.Proxy(API_CONNECTION_STRING) - d = api.callRemote("get_lbry_files", {}) - d.addCallback(self._get_table) - d.addCallback(lambda results: self.delayed_render(request, results)) - - return server.NOT_DONE_YET - - def _get_table(self, files): - if not self.files_table: - self.files_table = r'My LBRY files' - self.files_table += r'' - self.files_table += r'' - self.files_table += r'' - self.files_table += r'' - self.files_table += r'' - self.files_table += r'' - return self._get_table(files) - if not len(files): - self.files_table += r'
Stream nameCompletedToggleRemove
' - return self.files_table - else: - f = files.pop() - self.files_table += r'' - self.files_table += r'%s' % (f['stream_name']) - self.files_table += r'%s' % (f['completed']) - self.files_table += r'Start' if f['stopped'] else r'Stop' - self.files_table += r'Delete' - self.files_table += r'' - return self._get_table(files) + # def _responseFailed(self, err, call): + # call.addErrback(lambda err: err.trap(error.ConnectionDone)) + # call.addErrback(lambda err: err.trap(defer.CancelledError)) + # call.addErrback(lambda err: log.info("Error: " + str(err))) + # call.cancel() class LBRYDaemonServer(object): - def __init__(self): - self.data_dir = user_data_dir("LBRY") - if not os.path.isdir(self.data_dir): - os.mkdir(self.data_dir) - self.version_dir = os.path.join(self.data_dir, "ui_version_history") - if not os.path.isdir(self.version_dir): - os.mkdir(self.version_dir) - self.config = os.path.join(self.version_dir, "active.json") - self.ui_dir = os.path.join(self.data_dir, "lbry-web-ui") - self.git_version = None - self._api = None - self.root = None - - if not os.path.isfile(os.path.join(self.config)): - self.loaded_git_version = None - else: - try: - f = open(self.config, "r") - loaded_ui = json.loads(f.read()) - f.close() - self.loaded_git_version = loaded_ui['commit'] - self.loaded_branch = loaded_ui['branch'] - version_log.info("[" + str(datetime.now()) + "] Last used " + self.loaded_branch + " commit " + str(self.loaded_git_version).replace("\n", "")) - except: - self.loaded_git_version = None - self.loaded_branch = None - - def setup(self, branch="master", user_specified=None): - self.branch = branch - if user_specified: - if os.path.isdir(user_specified): - log.info("Using user specified UI directory: " + str(user_specified)) - self.branch = "user-specified" - self.loaded_git_version = "user-specified" - self.ui_dir = user_specified - return defer.succeed("user-specified") - else: - log.info("User specified UI directory doesn't exist, using " + branch) - else: - log.info("Using UI branch: " + branch) - self._git_url = "https://api.github.com/repos/lbryio/lbry-web-ui/git/refs/heads/%s" % branch - self._dist_url = "https://raw.githubusercontent.com/lbryio/lbry-web-ui/%s/dist.zip" % branch - - d = self._up_to_date() - d.addCallback(lambda r: self._download_ui() if not r else self.branch) - return d - - def _up_to_date(self): - def _get_git_info(): - response = urlopen(self._git_url) - data = json.loads(response.read()) - return defer.succeed(data['object']['sha']) - - def _set_git(version): - self.git_version = version - version_log.info("[" + str(datetime.now()) + "] UI branch " + self.branch + " has a most recent commit of: " + str(self.git_version).replace("\n", "")) - - if self.git_version == self.loaded_git_version and os.path.isdir(self.ui_dir): - version_log.info("[" + str(datetime.now()) + "] local copy of " + self.branch + " is up to date") - return defer.succeed(True) - else: - if self.git_version == self.loaded_git_version: - version_log.info("[" + str(datetime.now()) + "] Can't find ui files, downloading them again") - else: - version_log.info("[" + str(datetime.now()) + "] local copy of " + self.branch + " branch is out of date, updating") - f = open(self.config, "w") - f.write(json.dumps({'commit': self.git_version, - 'time': str(datetime.now()), - 'branch': self.branch})) - f.close() - return defer.succeed(False) - - d = _get_git_info() - d.addCallback(_set_git) - return d - - def _download_ui(self): - def _delete_ui_dir(): - if os.path.isdir(self.ui_dir): - if self.loaded_git_version: - version_log.info("[" + str(datetime.now()) + "] Removed ui files for commit " + str(self.loaded_git_version).replace("\n", "")) - log.info("Removing out of date ui files") - shutil.rmtree(self.ui_dir) - return defer.succeed(None) - - def _dl_ui(): - url = urlopen(self._dist_url) - z = ZipFile(StringIO(url.read())) - names = [i for i in z.namelist() if '.DS_exStore' not in i and '__MACOSX' not in i] - z.extractall(self.ui_dir, members=names) - version_log.info("[" + str(datetime.now()) + "] Updated branch " + self.branch + ": " + str(self.loaded_git_version).replace("\n", "") + " --> " + self.git_version.replace("\n", "")) - log.info("Downloaded files for UI commit " + str(self.git_version).replace("\n", "")) - self.loaded_git_version = self.git_version - return self.branch - - d = _delete_ui_dir() - d.addCallback(lambda _: _dl_ui()) - return d - - def _setup_server(self, ui_ver, wallet): - self._api = LBRYDaemon(ui_ver, wallet_type=wallet) - self.root = LBRYindex(self.ui_dir) - self.root.putChild("css", static.File(os.path.join(self.ui_dir, "css"))) - self.root.putChild("font", static.File(os.path.join(self.ui_dir, "font"))) - self.root.putChild("img", static.File(os.path.join(self.ui_dir, "img"))) - self.root.putChild("js", static.File(os.path.join(self.ui_dir, "js"))) + def _setup_server(self, wallet): + self.root = LBRYindex(os.path.join(os.path.join(data_dir, "lbry-ui"), "active")) + self._api = LBRYDaemon(self.root, wallet_type=wallet) self.root.putChild("view", HostedLBRYFile(self._api)) - self.root.putChild("files", MyLBRYFiles()) self.root.putChild(API_ADDRESS, self._api) return defer.succeed(True) - def start(self, branch="master", user_specified=False, wallet=DEFAULT_WALLET): - d = self.setup(branch=branch, user_specified=user_specified) - d.addCallback(lambda v: self._setup_server(v, wallet)) - d.addCallback(lambda _: self._api.setup()) - + def start(self, branch=DEFAULT_UI_BRANCH, user_specified=False, branch_specified=False, wallet=DEFAULT_WALLET): + d = self._setup_server(self._setup_server(wallet)) + d.addCallback(lambda _: self._api.setup(branch, user_specified, branch_specified)) return d diff --git a/lbrynet/lbrynet_daemon/LBRYUIManager.py b/lbrynet/lbrynet_daemon/LBRYUIManager.py new file mode 100644 index 000000000..b4aa33cd5 --- /dev/null +++ b/lbrynet/lbrynet_daemon/LBRYUIManager.py @@ -0,0 +1,224 @@ +import os +import logging +import shutil +import sys +import json + +from urllib2 import urlopen +from StringIO import StringIO +from twisted.web import static +from twisted.internet import defer +from lbrynet.conf import DEFAULT_UI_BRANCH +from lbrynet import __version__ as lbrynet_version +from lbryum.version import ELECTRUM_VERSION as lbryum_version +from zipfile import ZipFile +from appdirs import user_data_dir + +if sys.platform != "darwin": + data_dir = os.path.join(os.path.expanduser("~"), ".lbrynet") +else: + data_dir = user_data_dir("LBRY") + +if not os.path.isdir(data_dir): + os.mkdir(data_dir) +version_dir = os.path.join(data_dir, "ui_version_history") +if not os.path.isdir(version_dir): + os.mkdir(version_dir) + +log = logging.getLogger(__name__) +log.addHandler(logging.FileHandler(os.path.join(data_dir, 'lbrynet-daemon.log'))) +log.setLevel(logging.INFO) + + +class LBRYUIManager(object): + def __init__(self, root): + self.data_dir = user_data_dir("LBRY") + self.ui_root = os.path.join(self.data_dir, "lbry-ui") + self.active_dir = os.path.join(self.ui_root, "active") + self.update_dir = os.path.join(self.ui_root, "update") + + if not os.path.isdir(self.data_dir): + os.mkdir(self.data_dir) + if not os.path.isdir(self.ui_root): + os.mkdir(self.ui_root) + if not os.path.isdir(self.ui_root): + os.mkdir(self.ui_root) + if not os.path.isdir(self.ui_root): + os.mkdir(self.ui_root) + + self.config = os.path.join(self.ui_root, "active.json") + self.update_requires = os.path.join(self.update_dir, "requirements.txt") + self.requirements = {} + self.ui_dir = self.active_dir + self.git_version = None + self.root = root + + if not os.path.isfile(os.path.join(self.config)): + self.loaded_git_version = None + self.loaded_branch = None + self.loaded_requirements = None + else: + try: + f = open(self.config, "r") + loaded_ui = json.loads(f.read()) + f.close() + self.loaded_git_version = loaded_ui['commit'] + self.loaded_branch = loaded_ui['branch'] + self.loaded_requirements = loaded_ui['requirements'] + except: + self.loaded_git_version = None + self.loaded_branch = None + self.loaded_requirements = None + + def setup(self, branch=DEFAULT_UI_BRANCH, user_specified=None, branch_specified=False): + self.branch = branch + if user_specified: + if os.path.isdir(user_specified): + log.info("Checking user specified UI directory: " + str(user_specified)) + self.branch = "user-specified" + self.loaded_git_version = "user-specified" + d = self.migrate_ui(source=user_specified) + d.addCallback(lambda _: self._load_ui()) + return d + else: + log.info("User specified UI directory doesn't exist, using " + branch) + elif self.loaded_branch == "user-specified" and not branch_specified: + log.info("Loading user provided UI") + d = self._load_ui() + return d + else: + log.info("Checking for updates for UI branch: " + branch) + self._git_url = "https://api.github.com/repos/lbryio/lbry-web-ui/git/refs/heads/%s" % branch + self._dist_url = "https://raw.githubusercontent.com/lbryio/lbry-web-ui/%s/dist.zip" % branch + + d = self._up_to_date() + d.addCallback(lambda r: self._download_ui() if not r else self._load_ui()) + return d + + def _up_to_date(self): + def _get_git_info(): + response = urlopen(self._git_url) + data = json.loads(response.read()) + return defer.succeed(data['object']['sha']) + + def _set_git(version): + self.git_version = version.replace('\n', '') + if self.git_version == self.loaded_git_version: + log.info("UI is up to date") + return defer.succeed(True) + else: + log.info("UI updates available, checking if installation meets requirements") + return defer.succeed(False) + + d = _get_git_info() + d.addCallback(_set_git) + return d + + def migrate_ui(self, source=None): + if not source: + requires_file = self.update_requires + source_dir = self.update_dir + delete_source = True + else: + requires_file = os.path.join(source, "requirements.txt") + source_dir = source + delete_source = False + + def _check_requirements(): + if not os.path.isfile(requires_file): + log.info("No requirements.txt file, rejecting request to migrate this UI") + return defer.succeed(False) + + f = open(requires_file, "r") + for requirement in [line for line in f.read().split('\n') if line]: + t = requirement.split('=') + if len(t) == 3: + self.requirements[t[0]] = {'version': t[1], 'operator': '=='} + elif t[0][-1] == ">": + self.requirements[t[0][:-1]] = {'version': t[1], 'operator': '>='} + elif t[0][-1] == "<": + self.requirements[t[0][:-1]] = {'version': t[1], 'operator': '<='} + f.close() + passed_requirements = True + for r in self.requirements: + if r == 'lbrynet': + c = lbrynet_version + elif r == 'lbryum': + c = lbryum_version + else: + c = None + if c: + if self.requirements[r]['operator'] == '==': + if not self.requirements[r]['version'] == c: + passed_requirements = False + log.info("Local version %s of %s does not meet UI requirement for version %s" % ( + c, r, self.requirements[r]['version'])) + else: + log.info("Local version of %s meets ui requirement" % r) + if self.requirements[r]['operator'] == '>=': + if not self.requirements[r]['version'] <= c: + passed_requirements = False + log.info("Local version %s of %s does not meet UI requirement for version %s" % ( + c, r, self.requirements[r]['version'])) + else: + log.info("Local version of %s meets ui requirement" % r) + if self.requirements[r]['operator'] == '<=': + if not self.requirements[r]['version'] >= c: + passed_requirements = False + log.info("Local version %s of %s does not meet UI requirement for version %s" % ( + c, r, self.requirements[r]['version'])) + else: + log.info("Local version of %s meets ui requirement" % r) + return defer.succeed(passed_requirements) + + def _disp_failure(): + log.info("Failed to satisfy requirements for branch '%s', update was not loaded" % self.branch) + return defer.succeed(False) + + def _do_migrate(): + if os.path.isdir(self.active_dir): + shutil.rmtree(self.active_dir) + shutil.copytree(source_dir, self.active_dir) + if delete_source: + shutil.rmtree(source_dir) + + log.info("Loaded UI update") + + f = open(self.config, "w") + loaded_ui = {'commit': self.git_version, 'branch': self.branch, 'requirements': self.requirements} + f.write(json.dumps(loaded_ui)) + f.close() + + self.loaded_git_version = loaded_ui['commit'] + self.loaded_branch = loaded_ui['branch'] + self.loaded_requirements = loaded_ui['requirements'] + return defer.succeed(True) + + d = _check_requirements() + d.addCallback(lambda r: _do_migrate() if r else _disp_failure()) + return d + + def _download_ui(self): + def _delete_update_dir(): + if os.path.isdir(self.update_dir): + shutil.rmtree(self.update_dir) + return defer.succeed(None) + + def _dl_ui(): + url = urlopen(self._dist_url) + z = ZipFile(StringIO(url.read())) + names = [i for i in z.namelist() if '.DS_exStore' not in i and '__MACOSX' not in i] + z.extractall(self.update_dir, members=names) + log.info("Downloaded files for UI commit " + str(self.git_version).replace("\n", "")) + return self.branch + + d = _delete_update_dir() + d.addCallback(lambda _: _dl_ui()) + d.addCallback(lambda _: self.migrate_ui()) + d.addCallback(lambda _: self._load_ui()) + return d + + def _load_ui(self): + for d in [i[0] for i in os.walk(self.active_dir) if os.path.dirname(i[0]) == self.active_dir]: + self.root.putChild(os.path.basename(d), static.File(d)) + return defer.succeed(True) \ No newline at end of file diff --git a/lbrynet/lbrynet_daemon/daemon_scripts/Autofetcher.py b/lbrynet/lbrynet_daemon/daemon_scripts/Autofetcher.py new file mode 100644 index 000000000..cd7ed02cb --- /dev/null +++ b/lbrynet/lbrynet_daemon/daemon_scripts/Autofetcher.py @@ -0,0 +1,68 @@ +import json +import logging.handlers +import sys +import os + +from appdirs import user_data_dir +from twisted.internet.task import LoopingCall +from twisted.internet import reactor + + +if sys.platform != "darwin": + log_dir = os.path.join(os.path.expanduser("~"), ".lbrynet") +else: + log_dir = user_data_dir("LBRY") + +if not os.path.isdir(log_dir): + os.mkdir(log_dir) + +LOG_FILENAME = os.path.join(log_dir, 'lbrynet-daemon.log') + +if os.path.isfile(LOG_FILENAME): + f = open(LOG_FILENAME, 'r') + PREVIOUS_LOG = len(f.read()) + f.close() +else: + PREVIOUS_LOG = 0 + +log = logging.getLogger(__name__) +handler = logging.handlers.RotatingFileHandler(LOG_FILENAME, maxBytes=2097152, backupCount=5) +log.addHandler(handler) +log.setLevel(logging.INFO) + + +class Autofetcher(object): + """ + Download name claims as they occur + """ + + def __init__(self, api): + self._api = api + self._checker = LoopingCall(self._check_for_new_claims) + self.best_block = None + + def start(self): + reactor.addSystemEventTrigger('before', 'shutdown', self.stop) + self._checker.start(5) + + def stop(self): + log.info("Stopping autofetcher") + self._checker.stop() + + def _check_for_new_claims(self): + block = self._api.get_best_blockhash() + if block != self.best_block: + log.info("Checking new block for name claims, block hash: %s" % block) + self.best_block = block + transactions = self._api.get_block({'blockhash': block})['tx'] + for t in transactions: + c = self._api.get_claims_for_tx({'txid': t}) + if len(c): + for i in c: + log.info("Downloading stream for claim txid: %s" % t) + self._api.get({'name': t, 'stream_info': json.loads(i['value'])}) + + +def run(api): + fetcher = Autofetcher(api) + fetcher.start() \ No newline at end of file diff --git a/lbrynet/lbrynet_daemon/Apps/__init__.py b/lbrynet/lbrynet_daemon/daemon_scripts/__init__.py similarity index 100% rename from lbrynet/lbrynet_daemon/Apps/__init__.py rename to lbrynet/lbrynet_daemon/daemon_scripts/__init__.py diff --git a/lbrynet/lbrynet_daemon/daemon_scripts/migrateto025.py b/lbrynet/lbrynet_daemon/daemon_scripts/migrateto025.py new file mode 100644 index 000000000..9283b48aa --- /dev/null +++ b/lbrynet/lbrynet_daemon/daemon_scripts/migrateto025.py @@ -0,0 +1,33 @@ +from twisted.internet import defer + + +class migrator(object): + """ + Re-resolve lbry names to write missing data to blockchain.db and to cache the nametrie + """ + + def __init__(self, api): + self._api = api + + def start(self): + def _resolve_claims(claimtrie): + claims = [i for i in claimtrie if 'txid' in i.keys()] + r = defer.DeferredList([self._api._resolve_name(claim['name'], force_refresh=True) for claim in claims], consumeErrors=True) + return r + + def _restart_lbry_files(): + def _restart_lbry_file(lbry_file): + return lbry_file.restore() + + r = defer.DeferredList([_restart_lbry_file(lbry_file) for lbry_file in self._api.lbry_file_manager.lbry_files if not lbry_file.txid], consumeErrors=True) + r.callback(None) + return r + + d = self._api.session.wallet.get_nametrie() + d.addCallback(_resolve_claims) + d.addCallback(lambda _: _restart_lbry_files()) + + +def run(api): + refresher = migrator(api) + refresher.start() diff --git a/lbrynet/lbrynet_daemon/daemon_scripts/network_tester.py b/lbrynet/lbrynet_daemon/daemon_scripts/network_tester.py new file mode 100644 index 000000000..d62bdc3b1 --- /dev/null +++ b/lbrynet/lbrynet_daemon/daemon_scripts/network_tester.py @@ -0,0 +1,160 @@ +import json +import logging +import os +import sys + +from appdirs import user_data_dir +from datetime import datetime +from twisted.internet import defer +from twisted.internet.task import LoopingCall + +from lbrynet.core.Error import InvalidStreamInfoError, InsufficientFundsError +from lbrynet.core.PaymentRateManager import PaymentRateManager +from lbrynet.core.StreamDescriptor import download_sd_blob +from lbrynet.lbryfilemanager.LBRYFileDownloader import ManagedLBRYFileDownloaderFactory +from lbrynet.conf import DEFAULT_TIMEOUT + +INITIALIZING_CODE = 'initializing' +DOWNLOAD_METADATA_CODE = 'downloading_metadata' +DOWNLOAD_TIMEOUT_CODE = 'timeout' +DOWNLOAD_RUNNING_CODE = 'running' +DOWNLOAD_STOPPED_CODE = 'stopped' +STREAM_STAGES = [ + (INITIALIZING_CODE, 'Initializing...'), + (DOWNLOAD_METADATA_CODE, 'Downloading metadata'), + (DOWNLOAD_RUNNING_CODE, 'Started stream'), + (DOWNLOAD_STOPPED_CODE, 'Paused stream'), + (DOWNLOAD_TIMEOUT_CODE, 'Stream timed out') + ] + +if sys.platform != "darwin": + log_dir = os.path.join(os.path.expanduser("~"), ".lbrynet") +else: + log_dir = user_data_dir("LBRY") + +if not os.path.isdir(log_dir): + os.mkdir(log_dir) + +LOG_FILENAME = os.path.join(log_dir, 'lbrynet-daemon.log') +log = logging.getLogger(__name__) +handler = logging.handlers.RotatingFileHandler(LOG_FILENAME, maxBytes=2097152, backupCount=5) +log.addHandler(handler) +log.setLevel(logging.INFO) + + +class GetStream(object): + def __init__(self, sd_identifier, session, wallet, lbry_file_manager, max_key_fee, pay_key=True, data_rate=0.5, + timeout=DEFAULT_TIMEOUT, download_directory=None, file_name=None): + self.wallet = wallet + self.resolved_name = None + self.description = None + self.key_fee = None + self.key_fee_address = None + self.data_rate = data_rate + self.pay_key = pay_key + self.name = None + self.file_name = file_name + self.session = session + self.payment_rate_manager = PaymentRateManager(self.session.base_payment_rate_manager) + self.lbry_file_manager = lbry_file_manager + self.sd_identifier = sd_identifier + self.stream_hash = None + self.max_key_fee = max_key_fee + self.stream_info = None + self.stream_info_manager = None + self.d = defer.Deferred(None) + self.timeout = timeout + self.timeout_counter = 0 + self.download_directory = download_directory + self.download_path = None + self.downloader = None + self.finished = defer.Deferred() + self.checker = LoopingCall(self.check_status) + self.code = STREAM_STAGES[0] + + def check_status(self): + self.timeout_counter += 1 + + if self.download_path: + self.checker.stop() + self.finished.callback((self.stream_hash, self.download_path)) + + elif self.timeout_counter >= self.timeout: + log.info("Timeout downloading lbry://" + self.resolved_name + ", " + str(self.stream_info)) + self.checker.stop() + self.d.cancel() + self.code = STREAM_STAGES[4] + self.finished.callback(False) + + def start(self, stream_info, name): + self.resolved_name = name + self.stream_info = stream_info + if 'stream_hash' in self.stream_info.keys(): + self.stream_hash = self.stream_info['stream_hash'] + elif 'sources' in self.stream_info.keys(): + self.stream_hash = self.stream_info['sources']['lbry_sd_hash'] + else: + raise InvalidStreamInfoError(self.stream_info) + if 'description' in self.stream_info.keys(): + self.description = self.stream_info['description'] + if 'key_fee' in self.stream_info.keys(): + self.key_fee = float(self.stream_info['key_fee']) + if 'key_fee_address' in self.stream_info.keys(): + self.key_fee_address = self.stream_info['key_fee_address'] + else: + self.key_fee_address = None + else: + self.key_fee = None + self.key_fee_address = None + if self.key_fee > self.max_key_fee: + if self.pay_key: + log.info("Key fee (" + str(self.key_fee) + ") above limit of " + str( + self.max_key_fee) + ", didn't download lbry://" + str(self.resolved_name)) + return defer.fail(None) + else: + pass + + def _cause_timeout(): + self.timeout_counter = self.timeout * 2 + + def _set_status(x, status): + self.code = next(s for s in STREAM_STAGES if s[0] == status) + return x + + self.checker.start(1) + + self.d.addCallback(lambda _: _set_status(None, DOWNLOAD_METADATA_CODE)) + self.d.addCallback(lambda _: download_sd_blob(self.session, self.stream_hash, self.payment_rate_manager)) + self.d.addCallback(self.sd_identifier.get_metadata_for_sd_blob) + self.d.addCallback(lambda r: _set_status(r, DOWNLOAD_RUNNING_CODE)) + self.d.addCallback(lambda metadata: ( + next(factory for factory in metadata.factories if isinstance(factory, ManagedLBRYFileDownloaderFactory)), + metadata)) + self.d.addCallback(lambda (factory, metadata): factory.make_downloader(metadata, + [self.data_rate, True], + self.payment_rate_manager, + download_directory=self.download_directory, + file_name=self.file_name)) + self.d.addCallbacks(self._start_download, lambda _: _cause_timeout()) + self.d.callback(None) + + return self.finished + + def _start_download(self, downloader): + def _pay_key_fee(): + if self.key_fee is not None and self.key_fee_address is not None: + reserved_points = self.wallet.reserve_points(self.key_fee_address, self.key_fee) + if reserved_points is None: + return defer.fail(InsufficientFundsError()) + log.info("Key fee: " + str(self.key_fee) + " | " + str(self.key_fee_address)) + return self.wallet.send_points_to_address(reserved_points, self.key_fee) + return defer.succeed(None) + + if self.pay_key: + d = _pay_key_fee() + else: + d = defer.Deferred() + self.downloader = downloader + self.download_path = os.path.join(downloader.download_directory, downloader.file_name) + d.addCallback(lambda _: log.info("[" + str(datetime.now()) + "] Downloading " + str(self.stream_hash) + " --> " + str(self.download_path))) + d.addCallback(lambda _: self.downloader.start()) diff --git a/packaging/osx/certs/cert.cer.enc b/packaging/osx/certs/cert.cer.enc new file mode 100644 index 000000000..4dc96d9ba --- /dev/null +++ b/packaging/osx/certs/cert.cer.enc @@ -0,0 +1,30 @@ +U2FsdGVkX1/oLoj7zhPd0imh2T8RhLpKzZLk4EHQV0GUJ1g8nwGvxWov+CUMnmjh +Y+LNdomGBYWoUilhe4JaWDUYepwIXn6+TvuWBdVEMGpJhXGbmIf+ncMXo6AP8Fh/ +g9x79SE4RJxFj3utc02B2ivVehoQno5sEvNSZMVml5n9skJoJUBbAsbp1p+7Hm5j +p2Z7UI7/qiih6TmszX5KQvOl/DPezVNksn1c1mUShuaBTjxbprGlr/LvtboR3mAd +8DN4yTGLLJAQ2+FNftM4rAedrr6db2AhQ8WxRmiwTfdubnEoC6zYySFq2kmdjj3S +gPnK0atx+ZihMp+S+GqMnvfAHEtb0vqxoq6nFcSIvcQVxKPyzu5E1kMriY4Oq3xr +K6ebc1NKJHjh7niaUmR3NImBx2h1LAAf/hcKRH2+ATEVczGtI1AsSGgGhUM34eGH +7G+m7+bIkgb8AtlaIGS/VVHsIZCNSgzwZJoNd3hD6ZV65Hb2yeT6Hhos88/05iFT +ewYasa73TqFm5SJHRwt4d1W9WVIJKJPDJ910p+V+NZVUsKOx34+vMNrjCrqW9p9x +gQnza2V/F6blIHTbSzIGc+MFbeHYBO80d+v5jVxheL8z6ollDVts1SyJ5rKJBY6c +quvSgmc/ltE0dqRxLOQJ9mAFbayuMIUP6CbRkPXp8GfE55UtUJkDilalzcpCPrUC +YJpuAI61INOQZZPEVKWW8L68/tLY+oEwWpexQX7xs4FUCblIFf20T3XE2lVuBHf9 +Bp9k7cD2m4mNrbzWOJuqrVt1pr176l9+VSP/ESdDFbmPch2FHl8HK8kgfJvkV+iB +kudmAmzI9DTUpWd5lJp6Fr/rLCMjslFDs37zMg4/E5ikKFSDNeYMtgPZhCwM83kh +OAktow4QAzh3RdbVZMFxaKk9nbiGPuBEsgvraPjb2gY8U34RC9R2FINIuTnJttLK +q7CKFTdbJIf+TIIgzfNu/c978adsK/qS68iltyyx8WFflcybnlqVgja192Ptqw1M +PXBQkH4mUrAeWDfmCPPh/mhO67Bau5u9Wzv/qZ2RXcX0dgXOoMa2sO6ZpR2SzxCJ +/XZwXnElMl+pvojLURDOV16fMPpjMCbzCN+hQabiTASqFNCsz4C9hmOquNh2t+V9 +8xvU/bnOM+/SMhahjYnvdhmRMcY+5Wv32ZnKATq88dq4T7/OZI7q3IsROZ7MnucT +x4vADvcFOfOdtPK35IFfMTfl+Ri3q7REIHMts2WEwXddf8CUiVeIaf8NgrWYW0hP +f9DQbMGKFcqqCHlKrQkv2dBKX/qEbIzN7T7535Ly68zyFuBS252gsLO7nrf+CLEZ +AROOfmt2jv0BvQ4MI5dslzsXFAU11tS36gOZ303R+NJVVqySkza964h2rH5M1F7i +A5p7w/l0OVV7r6aXkmsrIcsUZuY7QnZJORQ1MxNtK20weKfrqs90nMTklUVPc4V8 +LnAW6AYem0ZaeDHn2kx947sglMYxf0h/mFECGhif9hfDTErw7TkSJ26t9ByuEyEf +vGpp3P4iTXHUx7HSh7L4KDva6CP6slGjFMAFUEETn7N5uX3VEYeztMBdHLz0XHZc +PcgVZ8kytXVTEg95upvWmliEbQqWRsy6sr9PanaN1QY6re6RLlYj4pOWVm8qgCXU +IJVTWkROMlYZTWCibCsTsY8fk8aNObZamHjzZGvnU8nEGTx7xQJS8i0r3NM1j2Ka +ehBA+WfXbTplI/Fq8Z2Nrb/O39hQpGbXp4ERsEmVbK0twwsqVNehI0CdobxmGsd5 +E9Afb9iL97DTXsna1Il6FXnHvj3iAQsxxaNLIaj0sN1GaQd9N1mbxThlFNOM3wry +jI8TKCWEfLRQzykkcR3sMg== diff --git a/packaging/osx/certs/cert.p12.enc b/packaging/osx/certs/cert.p12.enc new file mode 100644 index 000000000..40828aff6 --- /dev/null +++ b/packaging/osx/certs/cert.p12.enc @@ -0,0 +1,33 @@ +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 +vOtCHu46xXuBJbDmz2mslw== diff --git a/setup_uri_handler.py b/setup_uri_handler.py deleted file mode 100644 index e9ba6749c..000000000 --- a/setup_uri_handler.py +++ /dev/null @@ -1,25 +0,0 @@ -from setuptools import setup -import os - -APP = [os.path.join('lbrynet', 'lbrynet_daemon', 'Apps', 'LBRYURIHandler.py')] -DATA_FILES = [] -OPTIONS = {'argv_emulation': True, - 'packages': ['jsonrpc'], - 'plist': { - 'LSUIElement': True, - 'CFBundleIdentifier': 'io.lbry.LBRYURIHandler', - 'CFBundleURLTypes': [ - { - 'CFBundleURLTypes': 'LBRYURIHandler', - 'CFBundleURLSchemes': ['lbry'] - } - ] - } - } - -setup( - app=APP, - data_files=DATA_FILES, - options={'py2app': OPTIONS}, - setup_requires=['py2app'], -) \ No newline at end of file From 0151dd887551d643fc92eeae8851e3e5178cce74 Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 30 May 2016 16:14:32 -0400 Subject: [PATCH 22/28] remove test_bot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test bot was moved to keynes repo, and shouldn’t be with the tests script --- .../install_dependencies_and_run_tests.sh | 2 +- tests/lbrynet_test_bot.py | 60 ------------------- 2 files changed, 1 insertion(+), 61 deletions(-) delete mode 100644 tests/lbrynet_test_bot.py diff --git a/packaging/travis/install_dependencies_and_run_tests.sh b/packaging/travis/install_dependencies_and_run_tests.sh index 9bef0afc4..f55ab609b 100755 --- a/packaging/travis/install_dependencies_and_run_tests.sh +++ b/packaging/travis/install_dependencies_and_run_tests.sh @@ -39,7 +39,7 @@ rm get-pip.py pip install -r requirements.txt pip install nose coverage coveralls pylint -nosetests --with-coverage --cover-package=lbrynet -v -I lbrynet_test_bot.py -I functional_tests.py tests/ +nosetests --with-coverage --cover-package=lbrynet -v -I functional_tests.py tests/ # TODO: submit coverage report to coveralls # TODO: as code quality improves, make pylint be more strict diff --git a/tests/lbrynet_test_bot.py b/tests/lbrynet_test_bot.py deleted file mode 100644 index 46e5d9f0a..000000000 --- a/tests/lbrynet_test_bot.py +++ /dev/null @@ -1,60 +0,0 @@ -import xmlrpclib -import json -from datetime import datetime -from time import sleep -from slackclient import SlackClient - -def get_conf(): - f = open('testbot.conf', 'r') - token = f.readline().replace('\n', '') - f.close() - return token - -def test_lbrynet(lbry, slack, channel): - logfile = open('lbrynet_test_log.txt', 'a') - - try: - path = lbry.get('testlbrynet')['path'] - except: - msg = '[' + str(datetime.now()) + '] ! Failed to obtain LBRYnet test file' - slack.rtm_connect() - slack.rtm_send_message(channel, msg) - print msg - logfile.write(msg + '\n') - - file_name = path.split('/')[len(path.split('/'))-1] - - for n in range(10): - files = [f for f in lbry.get_lbry_files() if (json.loads(f)['file_name'] == file_name) and json.loads(f)['completed']] - if files: - break - sleep(30) - - if files: - msg = '[' + str(datetime.now()) + '] LBRYnet download test successful' - slack.rtm_connect() - # slack.rtm_send_message(channel, msg) - print msg - logfile.write(msg + '\n') - - else: - msg = '[' + str(datetime.now()) + '] ! Failed to obtain LBRYnet test file' - slack.rtm_connect() - slack.rtm_send_message(channel, msg) - print msg - logfile.write(msg + '\n') - - lbry.delete_lbry_file('test.jpg') - logfile.close() - -token = get_conf() - -sc = SlackClient(token) -sc.rtm_connect() -channel = [c['id'] for c in json.loads(sc.api_call('channels.list'))['channels'] if c['name'] == 'tech'][0] -print 'Connected to slack' -daemon = xmlrpclib.ServerProxy("http://localhost:7080") - -while True: - test_lbrynet(daemon, sc, channel) - sleep(600) From ef62fd7e346a6621a750a818242339797bbadabc Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 30 May 2016 16:37:47 -0400 Subject: [PATCH 23/28] fix pip install jsonrpc --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 482489063..c7a722242 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,5 +27,5 @@ script: # the default py2app (v0.9) has a bug that is fixed in the head of /metachris/py2app - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pip install git+https://github.com/metachris/py2app; fi # py2app fails to find jsonrpc unless json-rpc is installed. why? I don't know. - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pip install json-rpc; fi + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pip install jsonrpc; fi - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then cd packaging/osx/lbry-osx-app; ./setup_app.sh; cd $TRAVIS_BUILD_DIR; fi \ No newline at end of file From 284d307c0f6b8fdc214b693f190bd4a481edeb25 Mon Sep 17 00:00:00 2001 From: Job Evers-Meltzer Date: Tue, 31 May 2016 22:49:00 -0500 Subject: [PATCH 24/28] fix logging-too-few-args errors --- lbrynet/core/client/DownloadManager.py | 4 ++-- .../plugins/BlindRepeater/ValuableBlobQueryHandler.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lbrynet/core/client/DownloadManager.py b/lbrynet/core/client/DownloadManager.py index fced77969..d601833dd 100644 --- a/lbrynet/core/client/DownloadManager.py +++ b/lbrynet/core/client/DownloadManager.py @@ -52,7 +52,7 @@ class DownloadManager(object): def check_stop(result, manager): if isinstance(result, failure.Failure): - log.error("Failed to stop the %s: %s", manager. result.getErrorMessage()) + log.error("Failed to stop the %s: %s", manager, result.getErrorMessage()) return False return True @@ -115,4 +115,4 @@ class DownloadManager(object): if not self.blobs: return self.calculate_total_bytes() else: - return sum([b.length for b in self.needed_blobs() if b.length is not None]) \ No newline at end of file + return sum([b.length for b in self.needed_blobs() if b.length is not None]) diff --git a/lbrynet/lbrynet_console/plugins/BlindRepeater/ValuableBlobQueryHandler.py b/lbrynet/lbrynet_console/plugins/BlindRepeater/ValuableBlobQueryHandler.py index 08d5a57a0..d8dd0009e 100644 --- a/lbrynet/lbrynet_console/plugins/BlindRepeater/ValuableBlobQueryHandler.py +++ b/lbrynet/lbrynet_console/plugins/BlindRepeater/ValuableBlobQueryHandler.py @@ -101,7 +101,7 @@ class ValuableBlobHashQueryHandler(ValuableQueryHandler): for blob_hash, count in valuable_hashes: hashes_and_scores.append((blob_hash, 1.0 * count / 10.0)) if len(hashes_and_scores) != 0: - log.info("Responding to a valuable blob hashes request with %s blob hashes: %s", + log.info("Responding to a valuable blob hashes request with %s blob hashes", str(len(hashes_and_scores))) expected_payment = 1.0 * len(hashes_and_scores) * self.valuable_blob_hash_payment_rate / 1000.0 self.wallet.add_expected_payment(self.peer, expected_payment) @@ -193,10 +193,10 @@ class ValuableBlobLengthQueryHandler(ValuableQueryHandler): if success is True: lengths.append(response_pair) if len(lengths) > 0: - log.info("Responding with %s blob lengths: %s", str(len(lengths))) + log.info("Responding with %s blob lengths", str(len(lengths))) expected_payment = 1.0 * len(lengths) * self.blob_length_payment_rate / 1000.0 self.wallet.add_expected_payment(self.peer, expected_payment) self.peer.update_stats('uploaded_valuable_blob_infos', len(lengths)) return {'blob_length': {'blob_lengths': lengths}} - dl.addCallback(make_response) \ No newline at end of file + dl.addCallback(make_response) From a30a9ad763955f0fb1df8cc5ded076e573eb5aa0 Mon Sep 17 00:00:00 2001 From: Job Evers-Meltzer Date: Wed, 1 Jun 2016 22:31:41 -0500 Subject: [PATCH 25/28] have pylint skip currently unmaintained files --- lbrynet/lbrylive/LBRYStdinUploader.py | 5 ++++- lbrynet/lbrylive/LBRYStdoutDownloader.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lbrynet/lbrylive/LBRYStdinUploader.py b/lbrynet/lbrylive/LBRYStdinUploader.py index 03570a975..7637e29e1 100644 --- a/lbrynet/lbrylive/LBRYStdinUploader.py +++ b/lbrynet/lbrylive/LBRYStdinUploader.py @@ -1,3 +1,6 @@ +# pylint: skip-file +# This file is not maintained, but might be used in the future +# import logging import sys from lbrynet.lbrylive.LiveStreamCreator import StdOutLiveStreamCreator @@ -114,4 +117,4 @@ def launch_stdin_uploader(): d.addCallback(lambda _: start_stdin_uploader()) d.addCallback(lambda _: shut_down()) reactor.addSystemEventTrigger('before', 'shutdown', uploader.shut_down) - reactor.run() \ No newline at end of file + reactor.run() diff --git a/lbrynet/lbrylive/LBRYStdoutDownloader.py b/lbrynet/lbrylive/LBRYStdoutDownloader.py index 8c413e129..e23cb36f1 100644 --- a/lbrynet/lbrylive/LBRYStdoutDownloader.py +++ b/lbrynet/lbrylive/LBRYStdoutDownloader.py @@ -1,3 +1,6 @@ +# pylint: skip-file +# This file is not maintained, but might be used in the future +# import logging import sys @@ -93,4 +96,4 @@ def launch_stdout_downloader(): d.addErrback(print_error) d.addCallback(lambda _: shut_down()) reactor.addSystemEventTrigger('before', 'shutdown', downloader.shut_down) - reactor.run() \ No newline at end of file + reactor.run() From 98efa9f46661e75f98afd326f7571a0bb9875807 Mon Sep 17 00:00:00 2001 From: Job Evers-Meltzer Date: Wed, 1 Jun 2016 22:35:30 -0500 Subject: [PATCH 26/28] pylint: ignore incorrect not-callable message --- lbrynet/lbrynet_console/ControlHandlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lbrynet/lbrynet_console/ControlHandlers.py b/lbrynet/lbrynet_console/ControlHandlers.py index cbe5dd270..d19d55c88 100644 --- a/lbrynet/lbrynet_console/ControlHandlers.py +++ b/lbrynet/lbrynet_console/ControlHandlers.py @@ -120,7 +120,7 @@ class CommandHandlerFactory(object): return self.control_handler_class.prompt_description def get_handler(self, console): - return self.control_handler_class(console, *self.args) + return self.control_handler_class(console, *self.args) # pylint: disable=not-callable class CommandHandler(object): From 85a571521bbb038718d56351ef720dcdb397d9c5 Mon Sep 17 00:00:00 2001 From: Job Evers-Meltzer Date: Wed, 1 Jun 2016 22:38:24 -0500 Subject: [PATCH 27/28] disambuguate popup functions --- lbrynet/lbrynet_gui/GuiApp.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lbrynet/lbrynet_gui/GuiApp.py b/lbrynet/lbrynet_gui/GuiApp.py index 94c977759..176cce9d7 100644 --- a/lbrynet/lbrynet_gui/GuiApp.py +++ b/lbrynet/lbrynet_gui/GuiApp.py @@ -176,10 +176,10 @@ class DownloaderApp(object): style="Stop.TButton", cursor=button_cursor) self.wallet_menu_button.grid(row=0, column=1, padx=(5, 0)) - def popup(event): + def popup_wallet(event): self.wallet_menu.tk_popup(event.x_root, event.y_root) - self.wallet_menu_button.bind("", popup) + self.wallet_menu_button.bind("", popup_wallet) self.uri_frame = ttk.Frame(self.frame, style="B.TFrame") self.uri_frame.grid() @@ -204,7 +204,7 @@ class DownloaderApp(object): def paste_command(): self.uri_entry.event_generate('') - def popup(event): + def popup_uri(event): selection_menu = tk.Menu( self.master, tearoff=0 ) @@ -214,7 +214,7 @@ class DownloaderApp(object): selection_menu.add_command(label=" Paste ", command=paste_command) selection_menu.tk_popup(event.x_root, event.y_root) - self.uri_entry.bind("", popup) + self.uri_entry.bind("", popup_uri) self.uri_button = ttk.Button( self.uri_frame, text="Go", command=self._open_stream, @@ -349,4 +349,4 @@ class AddressWindow(object): window, text="OK", command=window.destroy, style="LBRY.TButton" ) done_button.grid(row=1, column=1, pady=(0, 5), padx=5, sticky=tk.W) - window.focus_set() \ No newline at end of file + window.focus_set() From 80b912ea14f921b0998ed763c99333d1a71f51d2 Mon Sep 17 00:00:00 2001 From: Job Evers-Meltzer Date: Wed, 1 Jun 2016 22:40:01 -0500 Subject: [PATCH 28/28] pylint: disable incorrect not-callable message --- lbrynet/lbrynet_gui/StreamFrame.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lbrynet/lbrynet_gui/StreamFrame.py b/lbrynet/lbrynet_gui/StreamFrame.py index defcbacc9..de07c4d55 100644 --- a/lbrynet/lbrynet_gui/StreamFrame.py +++ b/lbrynet/lbrynet_gui/StreamFrame.py @@ -118,7 +118,7 @@ class StreamFrame(object): def cancel(self): if self.cancel_func is not None: - self.cancel_func() + self.cancel_func() # pylint: disable=not-callable self.stream_frame.destroy() self.app.stream_removed() @@ -460,4 +460,4 @@ class StreamFrame(object): if self.cost_label is not None and self.cost_label.winfo_exists(): self.cost_label.config(text=locale.format_string("%.2f LBC", (round(total_points_paid, 2),), - grouping=True)) \ No newline at end of file + grouping=True))