From 8d6dae1ccc942a6435dba819d1857156f600379b Mon Sep 17 00:00:00 2001 From: Inso <insomniak.fr@gmail.com> Date: Thu, 22 Jan 2015 20:57:15 +0100 Subject: [PATCH] Fixed various errors when connection timeouts --- lib/ucoinpy/api/bma/__init__.py | 6 +- src/cutecoin/core/account.py | 3 +- src/cutecoin/core/community.py | 136 +++++++++++++----------- src/cutecoin/core/wallet.py | 82 +++++++------- src/cutecoin/gui/certification.py | 12 ++- src/cutecoin/gui/community_tab.py | 10 +- src/cutecoin/gui/currency_tab.py | 24 +++-- src/cutecoin/gui/mainwindow.py | 35 ++++-- src/cutecoin/gui/process_cfg_account.py | 18 +++- src/cutecoin/gui/transfer.py | 10 +- src/cutecoin/models/peering.py | 17 ++- src/cutecoin/tools/exceptions.py | 9 +- 12 files changed, 214 insertions(+), 148 deletions(-) diff --git a/lib/ucoinpy/api/bma/__init__.py b/lib/ucoinpy/api/bma/__init__.py index 02fac8a9..768bd447 100644 --- a/lib/ucoinpy/api/bma/__init__.py +++ b/lib/ucoinpy/api/bma/__init__.py @@ -109,7 +109,8 @@ class API(object): - `path`: the request path """ - response = requests.get(self.reverse_url(path), params=kwargs, headers=self.headers) + response = requests.get(self.reverse_url(path), params=kwargs, + headers=self.headers, timeout=15) if response.status_code != 200: raise ValueError('status code != 200 => %d (%s)' % (response.status_code, response.text)) @@ -127,7 +128,8 @@ class API(object): kwargs['self'] = kwargs.pop('self_') logging.debug("POST : {0}".format(kwargs)) - response = requests.post(self.reverse_url(path), data=kwargs, headers=self.headers) + response = requests.post(self.reverse_url(path), data=kwargs, headers=self.headers, + timeout=15) if response.status_code != 200: raise ValueError('status code != 200 => %d (%s)' % (response.status_code, response.text)) diff --git a/src/cutecoin/core/account.py b/src/cutecoin/core/account.py index cfc93f61..984b7adf 100644 --- a/src/cutecoin/core/account.py +++ b/src/cutecoin/core/account.py @@ -18,6 +18,7 @@ import time from .wallet import Wallet from .community import Community from .person import Person +from ..tools.exceptions import NoPeerAvailable class Account(object): @@ -64,7 +65,7 @@ class Account(object): communities = [] for data in json_data['communities']: - communities.append(Community.load(data)) + communities.append(Community.load(data)) account = cls(salt, pubkey, name, communities, wallets, contacts) return account diff --git a/src/cutecoin/core/community.py b/src/cutecoin/core/community.py index c43f5fab..3b05d30c 100644 --- a/src/cutecoin/core/community.py +++ b/src/cutecoin/core/community.py @@ -10,31 +10,71 @@ from ucoinpy.documents.peer import Peer, Endpoint, BMAEndpoint from ucoinpy.documents.block import Block from ..tools.exceptions import NoPeerAvailable import logging -import time import inspect import hashlib +from requests.exceptions import ConnectTimeout + + +class Cache(): + def __init__(self, community): + self.latest_block = 0 + self.community = community + self.data = {} + + def refresh(self): + self.latest_block = self.community.current_blockid()['number'] + self.data = {} + + def request(self, request, req_args={}, get_args={}): + cache_key = (hash(request), + hash(tuple(frozenset(sorted(req_args.keys())))), + hash(tuple(frozenset(sorted(req_args.items())))), + hash(tuple(frozenset(sorted(get_args.keys())))), + hash(tuple(frozenset(sorted(get_args.items()))))) + + if cache_key not in self.data.keys(): + result = self.community.request(request, req_args, get_args, + cached=False) + + # Do not cache block 0 + if self.latest_block == 0: + return result + else: + self.data[cache_key] = result + return self.data[cache_key] + else: + return self.data[cache_key] class Community(object): ''' classdocs ''' + def __init__(self, currency, peers): ''' A community is a group of nodes using the same currency. ''' self.currency = currency self.peers = [p for p in peers if p.currency == currency] - self.requests_cache = {} - self.last_block = None + self.cache = Cache(self) # After initializing the community from latest peers, # we refresh its peers tree logging.debug("Creating community") - found_peers = self.peering() - for p in found_peers: - if p.pubkey not in [peer.pubkey for peer in peers]: - self.peers.append(p) + try: + found_peers = self.peering() + for p in found_peers: + if p.pubkey not in [peer.pubkey for peer in peers]: + self.peers.append(p) + except NoPeerAvailable: + pass + logging.debug("{0} peers found".format(len(self.peers))) + + try: + self.cache.refresh() + except NoPeerAvailable: + pass @classmethod def create(cls, currency, peer): @@ -124,76 +164,40 @@ class Community(object): members.append(m['pubkey']) return members - def _check_current_block(self, endpoint): - if self.last_block is None: - blockid = self.current_blockid() - self.last_block = {"request_ts": time.time(), - "number": blockid['number']} - elif self.last_block["request_ts"] + 60 < time.time(): - self.last_block["request_ts"] = time.time() - blockid = self.current_blockid() - if blockid['number'] > self.last_block['number']: - self.last_block["number"] = blockid['number'] - self.requests_cache = {} - - def _cached_request(self, request, req_args={}, get_args={}): - for peer in self.peers: - e = next(e for e in peer.endpoints if type(e) is BMAEndpoint) - self._check_current_block(e) - try: - # Do not cache block 0 - if self.last_block["number"] != 0: - cache_key = (hash(request), - hash(tuple(frozenset(sorted(req_args.keys())))), - hash(tuple(frozenset(sorted(req_args.items())))), - hash(tuple(frozenset(sorted(get_args.keys())))), - hash(tuple(frozenset(sorted(get_args.items()))))) - - if cache_key not in self.requests_cache.keys(): - if e.server: - logging.debug("Connecting to {0}:{1}".format(e.server, - e.port)) - else: - logging.debug("Connecting to {0}:{1}".format(e.ipv4, - e.port)) - - req = request(e.conn_handler(), **req_args) - data = req.get(**get_args) - if inspect.isgenerator(data): - cached_data = [] - for d in data: - cached_data.append(d) - self.requests_cache[cache_key] = cached_data - else: - self.requests_cache[cache_key] = data - return self.requests_cache[cache_key] - else: - req = request(e.conn_handler(), **req_args) - data = req.get(**get_args) - return data - except ValueError as e: - if '502' in str(e): - continue - else: - raise - raise NoPeerAvailable(self.currency) - def request(self, request, req_args={}, get_args={}, cached=True): if cached: - return self._cached_request(request, req_args, get_args) + return self.cache.request(request, req_args, get_args) else: - for peer in self.peers: + for peer in self.peers.copy(): e = next(e for e in peer.endpoints if type(e) is BMAEndpoint) try: req = request(e.conn_handler(), **req_args) data = req.get(**get_args) - return data + + if inspect.isgenerator(data): + generated = [] + for d in data: + generated.append(d) + return generated + else: + return data except ValueError as e: if '502' in str(e): continue else: raise - raise NoPeerAvailable(self.currency) + except ConnectTimeout: + # Move the timeout peer to the end + self.peers.remove(peer) + self.peers.append(peer) + continue + except TimeoutError: + # Move the timeout peer to the end + self.peers.remove(peer) + self.peers.append(peer) + continue + + raise NoPeerAvailable(self.currency, len(self.peers)) def post(self, request, req_args={}, post_args={}): for peer in self.peers: @@ -207,6 +211,7 @@ class Community(object): except: pass return + raise NoPeerAvailable(self.currency, len(self.peers)) def broadcast(self, request, req_args={}, post_args={}): for peer in self.peers: @@ -220,6 +225,7 @@ class Community(object): raise except: pass + raise NoPeerAvailable(self.currency, len(self.peers)) def jsonify_peers_list(self): data = [] diff --git a/src/cutecoin/core/wallet.py b/src/cutecoin/core/wallet.py index 3c48bf67..64e788f1 100644 --- a/src/cutecoin/core/wallet.py +++ b/src/cutecoin/core/wallet.py @@ -9,7 +9,7 @@ from ucoinpy.api import bma from ucoinpy.documents.block import Block from ucoinpy.documents.transaction import InputSource, OutputSource, Transaction from ucoinpy.key import SigningKey -from ..tools.exceptions import NotEnoughMoneyError +from ..tools.exceptions import NotEnoughMoneyError, NoPeerAvailable import logging @@ -83,44 +83,48 @@ class Cache(): def refresh(self, community): current_block = 0 try: - block_data = community.request(bma.blockchain.Current) - current_block = block_data['number'] - except ValueError as e: - if '404' in str(e): - current_block = 0 - else: - raise - - with_tx = community.request(bma.blockchain.TX) - # We parse only blocks with transactions - parsed_blocks = reversed(range(self.latest_block + 1, - current_block + 1)) - logging.debug("Refresh from {0} to {1}".format(self.latest_block + 1, - current_block + 1)) - parsed_blocks = [n for n in parsed_blocks - if n in with_tx['result']['blocks']] - - for block_number in parsed_blocks: - block = community.request(bma.blockchain.Block, - req_args={'number': block_number}) - signed_raw = "{0}{1}\n".format(block['raw'], block['signature']) - block_doc = Block.from_signed_raw(signed_raw) - for tx in block_doc.transactions: - in_outputs = [o for o in tx.outputs - if o.pubkey == self.wallet.pubkey] - if len(in_outputs) > 0: - self.tx_received.append(tx) - - in_inputs = [i for i in tx.issuers if i == self.wallet.pubkey] - if len(in_inputs) > 0: - # remove from waiting transactions list the one which were - # validated in the blockchain - self.awaiting_tx = [awaiting for awaiting in self.awaiting_tx - if awaiting.compact() != tx.compact()] - self.tx_sent.append(tx) - - if current_block > self.latest_block: - self.available_sources = self.wallet.sources(community) + try: + block_data = community.request(bma.blockchain.Current) + current_block = block_data['number'] + except ValueError as e: + if '404' in str(e): + current_block = 0 + else: + raise + with_tx = community.request(bma.blockchain.TX) + + # We parse only blocks with transactions + parsed_blocks = reversed(range(self.latest_block + 1, + current_block + 1)) + logging.debug("Refresh from {0} to {1}".format(self.latest_block + 1, + current_block + 1)) + parsed_blocks = [n for n in parsed_blocks + if n in with_tx['result']['blocks']] + + for block_number in parsed_blocks: + block = community.request(bma.blockchain.Block, + req_args={'number': block_number}) + signed_raw = "{0}{1}\n".format(block['raw'], block['signature']) + block_doc = Block.from_signed_raw(signed_raw) + for tx in block_doc.transactions: + in_outputs = [o for o in tx.outputs + if o.pubkey == self.wallet.pubkey] + if len(in_outputs) > 0: + self.tx_received.append(tx) + + in_inputs = [i for i in tx.issuers if i == self.wallet.pubkey] + if len(in_inputs) > 0: + # remove from waiting transactions list the one which were + # validated in the blockchain + self.awaiting_tx = [awaiting for awaiting in self.awaiting_tx + if awaiting.compact() != tx.compact()] + self.tx_sent.append(tx) + + if current_block > self.latest_block: + self.available_sources = self.wallet.sources(community) + + except NoPeerAvailable: + return self.tx_sent = self.tx_sent[:50] self.tx_received = self.tx_received[:50] diff --git a/src/cutecoin/gui/certification.py b/src/cutecoin/gui/certification.py index 6d87765d..05c61bf0 100644 --- a/src/cutecoin/gui/certification.py +++ b/src/cutecoin/gui/certification.py @@ -3,11 +3,9 @@ Created on 24 dec. 2014 @author: inso ''' -from PyQt5.QtWidgets import QDialog, QErrorMessage, QInputDialog, QLineEdit, QMessageBox - -from cutecoin.core.person import Person - -from cutecoin.gen_resources.certification_uic import Ui_CertificationDialog +from PyQt5.QtWidgets import QDialog, QMessageBox +from ..tools.exceptions import NoPeerAvailable +from ..gen_resources.certification_uic import Ui_CertificationDialog class CertificationDialog(QDialog, Ui_CertificationDialog): @@ -52,6 +50,10 @@ class CertificationDialog(QDialog, Ui_CertificationDialog): QMessageBox.critical(self, "Certification", "Something wrong happened : {0}".format(e), QMessageBox.Ok) + except NoPeerAvailable as e: + QMessageBox.critical(self, "Certification", + "Couldn't connect to network : {0}".format(e), + QMessageBox.Ok) self.accepted.emit() self.close() diff --git a/src/cutecoin/gui/community_tab.py b/src/cutecoin/gui/community_tab.py index c6c79275..7e35e148 100644 --- a/src/cutecoin/gui/community_tab.py +++ b/src/cutecoin/gui/community_tab.py @@ -14,7 +14,7 @@ from .add_contact import AddContactDialog from .wot_tab import WotTabWidget from .transfer import TransferMoneyDialog from .certification import CertificationDialog -from ..tools.exceptions import PersonNotFoundError +from ..tools.exceptions import PersonNotFoundError, NoPeerAvailable class CommunityTabWidget(QWidget, Ui_CommunityTabWidget): @@ -111,6 +111,10 @@ class CommunityTabWidget(QWidget, Ui_CommunityTabWidget): QMessageBox.critical(self, "Key not sent to community", "Your key wasn't sent in the community. \ You can't request a membership.") + except NoPeerAvailable as e: + QMessageBox.critical(self, "Network error", + "Couldn't connect to network : {0}".format(e), + QMessageBox.Ok) def send_membership_leaving(self): password = self.password_asker.ask() @@ -124,3 +128,7 @@ class CommunityTabWidget(QWidget, Ui_CommunityTabWidget): except ValueError as e: QMessageBox.critical(self, "Leaving demand error", e.message) + except NoPeerAvailable as e: + QMessageBox.critical(self, "Network error", + "Couldn't connect to network : {0}".format(e), + QMessageBox.Ok) diff --git a/src/cutecoin/gui/currency_tab.py b/src/cutecoin/gui/currency_tab.py index c305f4e1..6a626f1a 100644 --- a/src/cutecoin/gui/currency_tab.py +++ b/src/cutecoin/gui/currency_tab.py @@ -17,6 +17,7 @@ from ..models.sent import SentListModel from ..models.received import ReceivedListModel from ..models.wallets import WalletsListModel from ..models.wallet import WalletListModel +from ..tools.exceptions import NoPeerAvailable class BlockchainWatcher(QObject): @@ -32,16 +33,19 @@ class BlockchainWatcher(QObject): def watch(self): while not self.exiting: time.sleep(10) - blockid = self.community.current_blockid() - block_number = blockid['number'] - if self.last_block != block_number: - for w in self.account.wallets: - w.refresh_cache(self.community) - - logging.debug("New block, {0} mined in {1}".format(block_number, - self.community.currency)) - self.new_block_mined.emit(block_number) - self.last_block = block_number + try: + blockid = self.community.current_blockid() + block_number = blockid['number'] + if self.last_block != block_number: + for w in self.account.wallets: + w.refresh_cache(self.community) + + logging.debug("New block, {0} mined in {1}".format(block_number, + self.community.currency)) + self.new_block_mined.emit(block_number) + self.last_block = block_number + except NoPeerAvailable: + return new_block_mined = pyqtSignal(int) diff --git a/src/cutecoin/gui/mainwindow.py b/src/cutecoin/gui/mainwindow.py index 9d2e6945..8f7545dc 100644 --- a/src/cutecoin/gui/mainwindow.py +++ b/src/cutecoin/gui/mainwindow.py @@ -4,7 +4,7 @@ Created on 1 févr. 2014 @author: inso ''' from cutecoin.gen_resources.mainwindow_uic import Ui_MainWindow -from PyQt5.QtWidgets import QMainWindow, QAction, QFileDialog, QProgressBar, QLabel +from PyQt5.QtWidgets import QMainWindow, QAction, QFileDialog, QProgressBar, QMessageBox, QLabel from PyQt5.QtCore import QSignalMapper, QModelIndex, QObject, QThread, pyqtSlot, pyqtSignal from PyQt5.QtGui import QIcon from .process_cfg_account import ProcessConfigureAccount @@ -14,6 +14,7 @@ from .add_contact import AddContactDialog from .import_account import ImportAccountDialog from .certification import CertificationDialog from .password_asker import PasswordAskerDialog +from ..tools.exceptions import NoPeerAvailable from ..__init__ import __version__ import logging @@ -109,12 +110,24 @@ class MainWindow(QMainWindow, Ui_MainWindow): def open_configure_account_dialog(self): dialog = ProcessConfigureAccount(self.app, self.app.current_account) dialog.accepted.connect(self.refresh_wallets) + dialog.accepted.connect(self.refresh_communities) dialog.exec_() def refresh_wallets(self): currency_tab = self.currencies_tabwidget.currentWidget() currency_tab.refresh_wallets() + def refresh_communities(self): + self.currencies_tabwidget.clear() + for community in self.app.current_account.communities: + tab_currency = CurrencyTabWidget(self.app, community, + self.password_asker, + self.status_label) + tab_currency.refresh() + self.currencies_tabwidget.addTab(tab_currency, + QIcon(":/icons/currency_icon"), + community.name()) + def set_as_default_account(self): self.app.default_account = self.app.current_account.name logging.debug(self.app.current_account) @@ -155,13 +168,19 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.currencies_tabwidget.clear() for community in self.app.current_account.communities: - tab_currency = CurrencyTabWidget(self.app, community, - self.password_asker, - self.status_label) - tab_currency.refresh() - self.currencies_tabwidget.addTab(tab_currency, - QIcon(":/icons/currency_icon"), - community.name()) + try: + tab_currency = CurrencyTabWidget(self.app, community, + self.password_asker, + self.status_label) + tab_currency.refresh() + self.currencies_tabwidget.addTab(tab_currency, + QIcon(":/icons/currency_icon"), + community.name()) + except NoPeerAvailable as e: + QMessageBox.critical(self, "Could not join {0}".format(community.currency), + str(e), + QMessageBox.Ok) + continue self.menu_contacts_list.clear() for contact in self.app.current_account.contacts: diff --git a/src/cutecoin/gui/process_cfg_account.py b/src/cutecoin/gui/process_cfg_account.py index 3fa823e7..a2dd2ff3 100644 --- a/src/cutecoin/gui/process_cfg_account.py +++ b/src/cutecoin/gui/process_cfg_account.py @@ -10,9 +10,9 @@ from ..gen_resources.account_cfg_uic import Ui_AccountConfigurationDialog from ..gui.process_cfg_community import ProcessConfigureCommunity from ..gui.password_asker import PasswordAskerDialog from ..models.communities import CommunitiesListModel -from ..tools.exceptions import KeyAlreadyUsed, Error +from ..tools.exceptions import KeyAlreadyUsed, Error, NoPeerAvailable -from PyQt5.QtWidgets import QDialog, QErrorMessage, QInputDialog, QMessageBox, QLineEdit +from PyQt5.QtWidgets import QDialog, QMessageBox class Step(): @@ -191,7 +191,13 @@ class ProcessConfigureAccount(QDialog, Ui_AccountConfigurationDialog): def open_process_edit_community(self, index): community = self.account.communities[index.row()] - dialog = ProcessConfigureCommunity(self.account, community, self.password_asker) + try: + dialog = ProcessConfigureCommunity(self.account, community, self.password_asker) + except NoPeerAvailable as e: + QMessageBox.critical(self, "Error", + str(e), QMessageBox.Ok) + return + dialog.accepted.connect(self.action_edit_community) dialog.exec_() @@ -205,7 +211,8 @@ class ProcessConfigureAccount(QDialog, Ui_AccountConfigurationDialog): self.stacked_pages.setCurrentIndex(next_index) self.step.display_page() except Error as e: - QErrorMessage(self).showMessage(e.message) + QMessageBox.critical(self, "Error", + str(e), QMessageBox.Ok) else: self.accept() @@ -223,7 +230,8 @@ class ProcessConfigureAccount(QDialog, Ui_AccountConfigurationDialog): try: self.app.add_account(self.account) except KeyAlreadyUsed as e: - QErrorMessage(self).showMessage(e.message) + QMessageBox.critical(self, "Error", + str(e), QMessageBox.Ok) password = self.edit_password.text() else: password = self.password_asker.ask() diff --git a/src/cutecoin/gui/transfer.py b/src/cutecoin/gui/transfer.py index c903e5ed..e95968f6 100644 --- a/src/cutecoin/gui/transfer.py +++ b/src/cutecoin/gui/transfer.py @@ -3,10 +3,9 @@ Created on 2 févr. 2014 @author: inso ''' -from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QMessageBox +from PyQt5.QtWidgets import QDialog, QMessageBox -from ..tools.exceptions import NotEnoughMoneyError -from ..core.person import Person +from ..tools.exceptions import NotEnoughMoneyError, NoPeerAvailable from ..gen_resources.transfer_uic import Ui_TransferMoneyDialog import logging @@ -75,7 +74,10 @@ class TransferMoneyDialog(QDialog, Ui_TransferMoneyDialog): QMessageBox.critical(self, "Money transfer", "You don't have enough money available in this block : \n{0}" .format(e.message)) - + except NoPeerAvailable as e: + QMessageBox.critical(self, "Money transfer", + "Couldn't connect to network : {0}".format(e), + QMessageBox.Ok) self.accepted.emit() self.close() diff --git a/src/cutecoin/models/peering.py b/src/cutecoin/models/peering.py index f757f771..dffe9639 100644 --- a/src/cutecoin/models/peering.py +++ b/src/cutecoin/models/peering.py @@ -8,6 +8,7 @@ from ucoinpy.api import bma from ucoinpy.documents.peer import BMAEndpoint, Peer from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt from .peer import PeerItem, RootItem +from requests.exceptions import ConnectTimeout import logging @@ -104,10 +105,16 @@ class PeeringTreeModel(QAbstractItemModel): try: e = next((e for e in peer.endpoints if type(e) is BMAEndpoint)) peers = bma.network.peering.Peers(e.conn_handler()).get() - for peer_data in peers: - peer = Peer.from_signed_raw("{0}{1}\n".format(peer_data['value']['raw'], - peer_data['value']['signature'])) - child_node_item = PeerItem(peer, peer_item) - peer_item.appendChild(child_node_item) + try: + for peer_data in peers: + peer = Peer.from_signed_raw("{0}{1}\n".format(peer_data['value']['raw'], + peer_data['value']['signature'])) + child_node_item = PeerItem(peer, peer_item) + peer_item.appendChild(child_node_item) + except ConnectTimeout: + continue + except TimeoutError: + continue + except StopIteration as e: continue diff --git a/src/cutecoin/tools/exceptions.py b/src/cutecoin/tools/exceptions.py index 2397d91d..27317ae6 100644 --- a/src/cutecoin/tools/exceptions.py +++ b/src/cutecoin/tools/exceptions.py @@ -13,6 +13,9 @@ class Error(Exception): ''' self.message = "Error : " + message + def __str__(self): + return self.message + class NotMemberOfCommunityError(Error): @@ -137,10 +140,10 @@ class NoPeerAvailable(Error): Exception raised when a community doesn't have any peer available. ''' - def __init__(self, currency): + def __init__(self, currency, peers): ''' Constructor ''' super() .__init__( - "No peer found in {0} community" - .format(currency)) + "No peer answered in {0} community ({1} peers available)" + .format(currency, peers)) -- GitLab