示例#1
0
 def createProxy(self, forceNew=False):
     if self.proxy == None or forceNew:
         LOGDEBUG('Creating proxy')
         usr,pas,hst,prt = [self.bitconf[k] for k in ['rpcuser','rpcpassword',\
                                                      'host', 'rpcport']]
         pstr = 'http://%s:%s@%s:%d' % (usr, pas, hst, prt)
         LOGINFO('Creating proxy in SDM: host=%s, port=%s', hst, prt)
         self.proxy = ServiceProxy(pstr)
示例#2
0
 def createProxy(self, forceNew=False):
    if self.proxy==None or forceNew:
       LOGDEBUG('Creating proxy')
       usr,pas,hst,prt = [self.bitconf[k] for k in ['rpcuser','rpcpassword',\
                                                    'host', 'rpcport']]
       pstr = 'http://%s:%s@%s:%d' % (usr,pas,hst,prt)
       LOGINFO('Creating proxy in SDM: host=%s, port=%s', hst,prt)
       self.proxy = ServiceProxy(pstr)
示例#3
0
class SatoshiDaemonManager(object):
    """
   Use an existing implementation of groestlcoind
   """
    class BitcoindError(Exception):
        pass

    class BitcoindNotAvailableError(Exception):
        pass

    class BitcoinDotConfError(Exception):
        pass

    class SatoshiHomeDirDNE(Exception):
        pass

    class ConfigFileUserDNE(Exception):
        pass

    class ConfigFilePwdDNE(Exception):
        pass

    #############################################################################
    def __init__(self):
        self.executable = None
        self.satoshiHome = None
        self.bitconf = {}
        self.proxy = None
        self.groestlcoind = None
        self.isMidQuery = False
        self.last20queries = []
        self.disabled = False
        self.failedFindExe = False
        self.failedFindHome = False
        self.foundExe = []
        self.circBufferState = []
        self.circBufferTime = []
        self.btcOut = None
        self.btcErr = None
        self.lastTopBlockInfo = { \
                                   'numblks':    -1,
                                   'tophash':    '',
                                   'toptime':    -1,
                                   'error':      'Uninitialized',
                                   'blkspersec': -1     }

        # Added torrent DL before we *actually* start SDM (if it makes sense)
        self.useTorrentFinalAnswer = False
        self.useTorrentFile = ''
        self.torrentDisabled = False
        self.tdm = None
        self.satoshiHome = None
        self.satoshiRoot = None

    #############################################################################
    def setSatoshiDir(self, newDir):
        self.satoshiHome = newDir
        self.satoshiRoot = newDir

        if 'testnet' in newDir:
            self.satoshiRoot, tail = os.path.split(newDir)

    #############################################################################
    def setDisableTorrentDL(self, b):
        self.torrentDisabled = b

    #############################################################################
    def tryToSetupTorrentDL(self, torrentPath):
        if self.torrentDisabled:
            LOGWARN('Tried to setup torrent download mgr but we are disabled')
            return False

        if not torrentPath or not os.path.exists(torrentPath):
            self.useTorrentFinalAnswer = False
            return False

        bootfile = os.path.join(self.satoshiHome, 'bootstrap.dat')
        bootfilePart = bootfile + '.partial'
        bootfileOld = bootfile + '.old'

        # cleartorrent.flag means we should remove any pre-existing files
        delTorrentFlag = os.path.join(ARMORY_HOME_DIR, 'cleartorrent.flag')
        if os.path.exists(delTorrentFlag):
            LOGWARN('Flag found to delete any pre-existing torrent files')
            if os.path.exists(bootfile): os.remove(bootfile)
            if os.path.exists(bootfilePart): os.remove(bootfilePart)
            if os.path.exists(bootfileOld): os.remove(bootfileOld)
            if os.path.exists(delTorrentFlag): os.remove(delTorrentFlag)

        TheTDM.setupTorrent(torrentPath, bootfile)
        if not TheTDM.getTDMState() == 'ReadyToStart':
            LOGERROR('Unknown error trying to start torrent manager')
            self.useTorrentFinalAnswer = False
            return False

        # We will tell the TDM to write status updates to the log file, and only
        # every 90 seconds.  After it finishes (or fails), simply launch groestlcoind
        # as we would've done without the torrent
        #####
        def torrentLogToFile(dpflag=Event(),
                             fractionDone=None,
                             timeEst=None,
                             downRate=None,
                             upRate=None,
                             activity=None,
                             statistics=None,
                             **kws):
            statStr = ''
            if fractionDone:
                statStr += '   Done: %0.1f%%  ' % (fractionDone * 100)
            if downRate:
                statStr += ' / DLRate: %0.1f/sec' % (downRate / 1024.)
            if timeEst:
                statStr += ' / TLeft: %s' % secondsToHumanTime(timeEst)
            if statistics:
                statStr += ' / Seeds: %d' % (statistics.numSeeds)
                statStr += ' / Peers: %d' % (statistics.numPeers)

            if len(statStr) == 0:
                statStr = 'No torrent info available'

            LOGINFO('Torrent: %s' % statStr)

        #####
        def torrentFinished():
            bootsz = '<Unknown>'
            if os.path.exists(bootfile):
                bootsz = bytesToHumanSize(os.path.getsize(bootfile))

            LOGINFO('Torrent finished; size of %s is %s', torrentPath, bootsz)
            LOGINFO('Remove the core btc databases before doing bootstrap')
            deleteBitcoindDBs()
            self.launchBitcoindAndGuardian()

        #####
        def warnUserHashFail():
            from PyQt4.QtGui import QMessageBox
            QMessageBox.warning(
                self, tr('Hash Failure'),
                tr("""The torrent download 
            is currently encountering too many packet hash failures to allow it to 
            progress properly. As a result, the torrent engine has been halted. You 
            should report this incident to the Groestlcoin Armory team and turn off this feature 
            until further notice."""), QMessageBox.Ok)

        #####
        def torrentFailed(errMsg=''):
            # Not sure there's actually anything we need to do here...
            if errMsg == 'hashFail':
                warnUserHashFail()

            bootsz = '<Unknown>'
            if os.path.exists(bootfile):
                bootsz = bytesToHumanSize(os.path.getsize(bootfile))

            LOGERROR('Torrent failed; size of %s is %s', torrentPath, bootsz)
            self.launchBitcoindAndGuardian()

        TheTDM.setSecondsBetweenUpdates(90)
        TheTDM.setCallback('displayFunc', torrentLogToFile)
        TheTDM.setCallback('finishedFunc', torrentFinished)
        TheTDM.setCallback('failedFunc', torrentFailed)

        LOGINFO('Bootstrap file is %s' % bytesToHumanSize(TheTDM.torrentSize))

        self.useTorrentFinalAnswer = True
        self.useTorrentFile = torrentPath
        return True

    #############################################################################
    def shouldTryBootstrapTorrent(self):
        if DISABLE_TORRENTDL or TheTDM.getTDMState() == 'Disabled':
            return False

        # The only torrent we have is for the primary Groestlcoin network
        if not MAGIC_BYTES == '\xf9\xbe\xb4\xd4':
            return False

        if TheTDM.torrentSize:
            bootfile = os.path.join(self.satoshiHome, 'bootstrap.dat')
            if os.path.exists(bootfile):
                if os.path.getsize(bootfile) >= TheTDM.torrentSize / 2:
                    LOGWARN('Looks like a full bootstrap is already here')
                    LOGWARN('Skipping torrent download')
                    return False

        # If they don't even have a BTC_HOME_DIR, corebtc never been installed
        blockDir = os.path.join(self.satoshiHome, 'blocks')
        if not os.path.exists(
                self.satoshiHome) or not os.path.exists(blockDir):
            return True

        # Get the cumulative size of the blk*.dat files
        blockDirSize = sum([os.path.getsize(os.path.join(blockDir, a)) \
                    for a in os.listdir(blockDir) if a.startswith('blk')])
        sizeStr = bytesToHumanSize(blockDirSize)
        LOGINFO('Total size of files in %s is %s' % (blockDir, sizeStr))

        # If they have only a small portion of the blockchain, do it
        szThresh = 100 * MEGABYTE if USE_TESTNET else 6 * GIGABYTE
        if blockDirSize < szThresh:
            return True

        # So far we know they have a BTC_HOME_DIR, with more than 6GB in blocks/
        # The only thing that can induce torrent now is if we have a partially-
        # finished bootstrap file bigger than the blocks dir.
        bootFiles = ['', '']
        bootFiles[0] = os.path.join(self.satoshiHome, 'bootstrap.dat')
        bootFiles[1] = os.path.join(self.satoshiHome, 'bootstrap.dat.partial')
        for fn in bootFiles:
            if os.path.exists(fn):
                if os.path.getsize(fn) > blockDirSize:
                    return True

        # Okay, we give up -- just download [the rest] via P2P
        return False

    #############################################################################
    #def setSatoshiDir(self, newDir):
    #self.satoshiHome = newDir

    #############################################################################
    def setupSDM(self, pathToBitcoindExe=None, satoshiHome=None, \
                       extraExeSearch=[], createHomeIfDNE=True):
        LOGDEBUG('Exec setupSDM')
        self.failedFindExe = False
        self.failedFindHome = False
        # If we are supplied a path, then ignore the extra exe search paths
        if pathToBitcoindExe == None:
            pathToBitcoindExe = self.findBitcoind(extraExeSearch)
            if len(pathToBitcoindExe) == 0:
                LOGDEBUG('Failed to find groestlcoind')
                self.failedFindExe = True
            else:
                LOGINFO('Found groestlcoind in the following places:')
                for p in pathToBitcoindExe:
                    LOGINFO('   %s', p)
                pathToBitcoindExe = pathToBitcoindExe[0]
                LOGINFO('Using: %s', pathToBitcoindExe)

                if not os.path.exists(pathToBitcoindExe):
                    LOGINFO(
                        'Somehow failed to find exe even after finding it...?')
                    self.failedFindExe = True

        self.executable = pathToBitcoindExe

        # Four possible conditions for already-set satoshi home dir, and input arg
        if satoshiHome is not None:
            self.satoshiHome = satoshiHome
        else:
            if self.satoshiHome is None:
                self.satoshiHome = BTC_HOME_DIR

        # If no new dir is specified, leave satoshi home if it's already set
        # Give it a default BTC_HOME_DIR if not.
        if not os.path.exists(self.satoshiHome):
            if createHomeIfDNE:
                LOGINFO('Making satoshi home dir')
                os.makedirs(self.satoshiHome)
            else:
                LOGINFO('No home dir, makedir not requested')
                self.failedFindHome = True

        if self.failedFindExe:
            raise self.BitcoindError, 'groestlcoind not found'
        if self.failedFindHome: raise self.BitcoindError, 'homedir not found'

        self.disabled = False
        self.proxy = None
        self.groestlcoind = None  # this will be a Popen object
        self.isMidQuery = False
        self.last20queries = []

        self.readBitcoinConf(makeIfDNE=True)

    #############################################################################
    def setDisabled(self, newBool=True):
        s = self.getSDMState()

        if newBool == True:
            if s in ('GroestlcoindInitializing', 'GroestlcoindSynchronizing',
                     'GroestlcoindReady'):
                self.stopBitcoind()

        self.disabled = newBool

    #############################################################################
    def getAllFoundExe(self):
        return list(self.foundExe)

    #############################################################################
    def findBitcoind(self, extraSearchPaths=[]):
        self.foundExe = []

        searchPaths = list(extraSearchPaths)  # create a copy

        if OS_WINDOWS:
            # Making sure the search path argument comes with /daemon and /Bitcoin on Windows

            searchPaths.extend(
                [os.path.join(sp, 'Groestlcoin') for sp in searchPaths])
            searchPaths.extend(
                [os.path.join(sp, 'daemon') for sp in searchPaths])

            possBaseDir = []

            from platform import machine
            if '64' in machine():
                possBaseDir.append(os.getenv("ProgramW6432"))
                possBaseDir.append(os.getenv('PROGRAMFILES(X86)'))
            else:
                possBaseDir.append(os.getenv('PROGRAMFILES'))

            # check desktop for links

            home = os.path.expanduser('~')
            desktop = os.path.join(home, 'Desktop')

            if os.path.exists(desktop):
                dtopfiles = os.listdir(desktop)
                for path in [os.path.join(desktop, fn) for fn in dtopfiles]:
                    if 'groestlcoin' in path.lower() and path.lower().endswith(
                            '.lnk'):
                        import win32com.client
                        shell = win32com.client.Dispatch('WScript.Shell')
                        targ = shell.CreateShortCut(path).Targetpath
                        targDir = os.path.dirname(targ)
                        LOGINFO('Found Groestlcoin-Qt link on desktop: %s',
                                targDir)
                        possBaseDir.append(targDir)

            # Also look in default place in ProgramFiles dirs

            # Now look at a few subdirs of the
            searchPaths.extend(possBaseDir)
            searchPaths.extend([
                os.path.join(p, 'Groestlcoin', 'daemon') for p in possBaseDir
            ])
            searchPaths.extend(
                [os.path.join(p, 'daemon') for p in possBaseDir])
            searchPaths.extend(
                [os.path.join(p, 'Groestlcoin') for p in possBaseDir])

            for p in searchPaths:
                testPath = os.path.join(p, 'groestlcoind.exe')
                if os.path.exists(testPath):
                    self.foundExe.append(testPath)

        else:
            # In case this was a downloaded copy, make sure we traverse to bin/64 dir
            searchPaths.extend(
                [os.path.join(p, 'bin') for p in extraSearchPaths])
            if SystemSpecs.IsX64:
                searchPaths.extend(
                    [os.path.join(p, 'bin/64') for p in extraSearchPaths])
            else:
                searchPaths.extend(
                    [os.path.join(p, 'bin/32') for p in extraSearchPaths])

            searchPaths.extend(['/usr/lib/groestlcoin/'])
            searchPaths.extend(os.getenv("PATH").split(':'))

            for p in searchPaths:
                testPath = os.path.join(p, 'groestlcoind')
                if os.path.exists(testPath):
                    self.foundExe.append(testPath)

            try:
                locs = subprocess_check_output(['whereis',
                                                'groestlcoind']).split()
                if len(locs) > 1:
                    locs = filter(
                        lambda x: os.path.basename(x) == 'groestlcoind', locs)
                    LOGINFO('"whereis" returned: %s', str(locs))
                    self.foundExe.extend(locs)
            except:
                LOGEXCEPT('Error executing "whereis" command')

        # For logging purposes, check that the first answer matches one of the
        # extra search paths.  There should be some kind of notification that
        # their supplied search path was invalid and we are using something else.
        if len(self.foundExe) > 0 and len(extraSearchPaths) > 0:
            foundIt = False
            for p in extraSearchPaths:
                if self.foundExe[0].startswith(p):
                    foundIt = True

            if not foundIt:
                LOGERROR(
                    'Groestlcoind could not be found in the specified installation:'
                )
                for p in extraSearchPaths:
                    LOGERROR('   %s', p)
                LOGERROR('Groestlcoind is being started from:')
                LOGERROR('   %s', self.foundExe[0])

        return self.foundExe

    #############################################################################
    def getGuardianPath(self):
        if OS_WINDOWS:
            armoryInstall = os.path.dirname(
                inspect.getsourcefile(SatoshiDaemonManager))
            # This should return a zip file because of py2exe
            if armoryInstall.endswith('.zip'):
                armoryInstall = os.path.dirname(armoryInstall)
            gpath = os.path.join(armoryInstall, 'guardian.exe')
        else:
            theDir = os.path.dirname(
                inspect.getsourcefile(SatoshiDaemonManager))
            gpath = os.path.join(theDir, 'guardian.py')

        if not os.path.exists(gpath):
            LOGERROR('Could not find guardian script: %s', gpath)
            raise FileExistsError
        return gpath

    #############################################################################
    def readBitcoinConf(self, makeIfDNE=False):
        LOGINFO('Reading groestlcoin.conf file')
        bitconf = os.path.join(self.satoshiRoot, 'groestlcoin.conf')
        if not os.path.exists(bitconf):
            if not makeIfDNE:
                raise self.BitcoinDotConfError, 'Could not find groestlcoin.conf'
            else:
                LOGINFO('No groestlcoin.conf available.  Creating it...')
                touchFile(bitconf)

        # Guarantee that bitcoin.conf file has very strict permissions
        if OS_WINDOWS:
            if OS_VARIANT[0].lower() == 'xp':
                LOGERROR('Cannot set permissions correctly in XP!')
                LOGERROR('Please confirm permissions on the following file ')
                LOGERROR('are set to exclusive access only for your user ')
                LOGERROR(
                    '(it usually is, but Groestlcoin Armory cannot guarantee it '
                )
                LOGERROR('on XP systems):')
                LOGERROR('    %s', bitconf)
            else:
                LOGINFO('Setting permissions on groestlcoin.conf')
                import ctypes
                username_u16 = ctypes.create_unicode_buffer(u'\0', 512)
                str_length = ctypes.c_int(512)
                ctypes.windll.Advapi32.GetUserNameW(ctypes.byref(username_u16),
                                                    ctypes.byref(str_length))

                CLI_OPTIONS.disableConfPermis = True  #!!!GRS
                if not CLI_OPTIONS.disableConfPermis:
                    import win32process
                    LOGINFO('Setting permissions on groestlcoin.conf')
                    cmd_icacls = [
                        u'icacls', bitconf, u'/inheritance:r', u'/grant:r',
                        u'%s:F' % username_u16.value
                    ]
                    kargs = {}
                    kargs['shell'] = True
                    kargs['creationflags'] = win32process.CREATE_NO_WINDOW
                    icacls_out = subprocess_check_output(cmd_icacls, **kargs)
                    LOGINFO('icacls returned: %s', icacls_out)
                else:
                    LOGWARN(
                        'Skipped setting permissions on groestlcoin.conf file')

        else:
            if not CLI_OPTIONS.disableConfPermis:
                LOGINFO('Setting permissions on groestlcoin.conf')
                os.chmod(bitconf, stat.S_IRUSR | stat.S_IWUSR)
            else:
                LOGWARN('Skipped setting permissions on groestlcoin.conf file')

        with open(bitconf, 'r') as f:
            # Find the last character of the each line:  either a newline or '#'
            endchr = lambda line: line.find('#') if line.find(
                '#') > 1 else len(line)

            # Reduce each line to a list of key,value pairs separated with '='
            allconf = [l[:endchr(l)].strip().split('=') for l in f.readlines()]

            # Need to convert to (x[0],x[1:]) in case the password has '=' in it
            allconfPairs = [[x[0], '='.join(x[1:])] for x in allconf
                            if len(x) > 1]

            # Convert the list of pairs to a dictionary
            self.bitconf = dict(allconfPairs)

        # Look for rpcport, use default if not there
        self.bitconf['rpcport'] = int(
            self.bitconf.get('rpcport', BITCOIN_RPC_PORT))

        # We must have a username and password.  If not, append to file
        if not self.bitconf.has_key('rpcuser'):
            LOGDEBUG('No rpcuser: creating one')
            with open(bitconf, 'a') as f:
                f.write('\n')
                f.write('rpcuser=generated_by_armory\n')
                self.bitconf['rpcuser'] = '******'

        if not self.bitconf.has_key('rpcpassword'):
            LOGDEBUG('No rpcpassword: creating one')
            with open(bitconf, 'a') as f:
                randBase58 = SecureBinaryData().GenerateRandom(32).toBinStr()
                randBase58 = binary_to_base58(randBase58)
                f.write('\n')
                f.write('rpcpassword=%s' % randBase58)
                self.bitconf['rpcpassword'] = randBase58

        if not isASCII(self.bitconf['rpcuser']):
            LOGERROR('Non-ASCII character in bitcoin.conf (rpcuser)!')
        if not isASCII(self.bitconf['rpcpassword']):
            LOGERROR('Non-ASCII character in bitcoin.conf (rpcpassword)!')

        self.bitconf['host'] = '127.0.0.1'

    #############################################################################
    def cleanupFailedTorrent(self):
        # Right now I think don't do anything
        pass

    #############################################################################
    def startBitcoind(self, callback):
        self.btcOut, self.btcErr = None, None
        if self.disabled:
            LOGERROR('SDM was disabled, must be re-enabled before starting')
            return

        LOGINFO('Called startGroestlcoind')

        if self.isRunningBitcoind() or TheTDM.getTDMState() == 'Downloading':
            raise self.BitcoindError, 'Looks like we have already started theSDM'

        if not os.path.exists(self.executable):
            raise self.BitcoindError, 'Could not find groestlcoind'

        chk1 = os.path.exists(self.useTorrentFile)
        chk2 = self.shouldTryBootstrapTorrent()
        chk3 = TheTDM.getTDMState() == 'ReadyToStart'

        if chk1 and chk2 and chk3:
            TheTDM.startDownload()
        else:
            self.launchBitcoindAndGuardian()

        #New backend code: we wont be polling the SDM state in the main thread
        #anymore, instead create a thread at groestlcoind start to poll the SDM state
        #and notify the main thread once groestlcoind is ready, then terminates
        self.pollBitcoindState(callback, async=True)

    #############################################################################
    @AllowAsync
    def pollBitcoindState(self, callback):
        while self.getSDMStateLogic() != 'GroestlcoindReady':
            time.sleep(1.0)
        callback()

    #############################################################################
    def launchBitcoindAndGuardian(self):

        pargs = [self.executable]

        if USE_TESTNET:
            pargs.append('-testnet')

        pargs.append('-datadir=%s' % self.satoshiRoot)

        try:
            # Don't want some strange error in this size-check to abort loading
            blocksdir = os.path.join(self.satoshiHome, 'blocks')
            sz = long(0)
            if os.path.exists(blocksdir):
                for fn in os.listdir(blocksdir):
                    fnpath = os.path.join(blocksdir, fn)
                    sz += long(os.path.getsize(fnpath))

            if sz < 5 * GIGABYTE:
                if SystemSpecs.Memory > 9.0:
                    pargs.append('-dbcache=2000')
                elif SystemSpecs.Memory > 5.0:
                    pargs.append('-dbcache=1000')
                elif SystemSpecs.Memory > 3.0:
                    pargs.append('-dbcache=500')
        except:
            LOGEXCEPT('Failed size check of blocks directory')

        kargs = {}
        if OS_WINDOWS:
            import win32process
            kargs['shell'] = True
            kargs['creationflags'] = win32process.CREATE_NO_WINDOW

        # Startup groestlcoind and get its process ID (along with our own)
        self.groestlcoind = launchProcess(pargs, **kargs)

        self.btcdpid = self.groestlcoind.pid
        self.selfpid = os.getpid()

        LOGINFO('PID of groestlcoind: %d', self.btcdpid)
        LOGINFO('PID of groestlcoin armory:   %d', self.selfpid)

        # Startup guardian process -- it will watch Armory's PID
        gpath = self.getGuardianPath()
        pargs = [gpath, str(self.selfpid), str(self.btcdpid)]
        if not OS_WINDOWS:
            pargs.insert(0, 'python')
        launchProcess(pargs, **kargs)

    #############################################################################
    def stopBitcoind(self):
        LOGINFO('Called stopGroestlcoind')
        try:
            if not self.isRunningBitcoind():
                LOGINFO(
                    '...but groestlcoind is not running, to be able to stop')
                return

            #signal groestlcoind to stop
            self.proxy.stop()

            #poll the pid until it's gone, for as long as 2 minutes
            total = 0
            while self.groestlcoind.poll() == None:
                time.sleep(0.1)
                total += 1

                if total > 1200:
                    LOGERROR(
                        "groestlcoind failed to shutdown in less than 2 minutes."
                        " Terminating.")
                    return

            self.groestlcoind = None
        except Exception as e:
            LOGERROR(e)
            return

    #############################################################################
    def isRunningBitcoind(self):
        """
      armoryengine satoshiIsAvailable() only tells us whether there's a
      running groestlcoind that is actively responding on its port.  But it
      won't be responding immediately after we've started it (still doing
      startup operations).  If groestlcoind was started and still running,
      then poll() will return None.  Any othe poll() return value means
      that the process terminated
      """
        if self.groestlcoind == None:
            return False
        else:
            if not self.groestlcoind.poll() == None:
                LOGDEBUG('groestlcoind is no more')
                if self.btcOut == None:
                    self.btcOut, self.btcErr = self.groestlcoind.communicate()
                    LOGWARN('groestlcoind exited, groestlcoind STDOUT:')
                    for line in self.btcOut.split('\n'):
                        LOGWARN(line)
                    LOGWARN('groestlcoind exited, groestlcoind STDERR:')
                    for line in self.btcErr.split('\n'):
                        LOGWARN(line)
            return self.groestlcoind.poll() == None

    #############################################################################
    def wasRunningBitcoind(self):
        return (not self.groestlcoind == None)

    #############################################################################
    def bitcoindIsResponsive(self):
        return satoshiIsAvailable(self.bitconf['host'],
                                  self.bitconf['rpcport'])

    #############################################################################
    def getSDMState(self):
        """
      As for why I'm doing this:  it turns out that between "initializing"
      and "synchronizing", groestlcoind temporarily stops responding entirely,
      which causes "not-available" to be the state.  I need to smooth that
      out because it wreaks havoc on the GUI which will switch to showing
      a nasty error.
      """

        state = self.getSDMStateLogic()
        self.circBufferState.append(state)
        self.circBufferTime.append(RightNow())
        if len(self.circBufferTime)>2 and \
           (self.circBufferTime[-1] - self.circBufferTime[1]) > 5:
            # Only remove the first element if we have at least 5s history
            self.circBufferState = self.circBufferState[1:]
            self.circBufferTime = self.circBufferTime[1:]

        # Here's where we modify the output to smooth out the gap between
        # "initializing" and "synchronizing" (which is a couple seconds
        # of "not available").   "NotAvail" keeps getting added to the
        # buffer, but if it was "initializing" in the last 5 seconds,
        # we will keep "initializing"
        if state == 'GroestlcoindNotAvailable':
            if 'GroestlcoindInitializing' in self.circBufferState:
                LOGWARN(
                    'Overriding not-available state. This should happen 0-5 times'
                )
                return 'GroestlcoindInitializing'

        return state

    #############################################################################
    def getSDMStateLogic(self):

        if self.disabled:
            return 'GroestlcoindMgmtDisabled'

        if self.failedFindExe:
            return 'GroestlcoindExeMissing'

        if self.failedFindHome:
            return 'GroestlcoindHomeMissing'

        if TheTDM.isRunning():
            return 'TorrentSynchronizing'

        latestInfo = self.getTopBlockInfo()

        if self.groestlcoind == None and latestInfo['error'] == 'Uninitialized':
            return 'GroestlcoindNeverStarted'

        if not self.isRunningBitcoind():
            # Not running at all:  either never started, or process terminated
            if not self.btcErr == None and len(self.btcErr) > 0:
                errstr = self.btcErr.replace(',', ' ').replace('.',
                                                               ' ').replace(
                                                                   '!', ' ')
                errPcs = set([a.lower() for a in errstr.split()])
                runPcs = set(
                    ['cannot', 'obtain', 'lock', 'already', 'running'])
                dbePcs = set([
                    'database', 'recover', 'backup', 'except', 'wallet', 'dat'
                ])
                if len(errPcs.intersection(runPcs)) >= (len(runPcs) - 1):
                    return 'GroestlcoindAlreadyRunning'
                elif len(errPcs.intersection(dbePcs)) >= (len(dbePcs) - 1):
                    return 'GroestlcoindDatabaseEnvError'
                else:
                    return 'GroestlcoindUnknownCrash'
            else:
                return 'GroestlcoindNotAvailable'
        elif not self.bitcoindIsResponsive():
            # Running but not responsive... must still be initializing
            return 'GroestlcoindInitializing'
        else:
            # If it's responsive, get the top block and check
            # TODO: These conditionals are based on experimental results.  May
            #       not be accurate what the specific errors mean...
            if latestInfo['error'] == 'ValueError':
                return 'GroestlcoindWrongPassword'
            elif latestInfo['error'] == 'JsonRpcException':
                return 'GroestlcoindInitializing'
            elif latestInfo['error'] == 'SocketError':
                return 'GroestlcoindNotAvailable'

            if 'GroestlcoindReady' in self.circBufferState:
                # If ready, always ready
                return 'GroestlcoindReady'

            # If we get here, groestlcoind is gave us a response.
            secSinceLastBlk = RightNow() - latestInfo['toptime']
            blkspersec = latestInfo['blkspersec']
            #print 'Blocks per 10 sec:', ('UNKNOWN' if blkspersec==-1 else blkspersec*10)
            if secSinceLastBlk > 4 * HOUR or blkspersec == -1:
                return 'GroestlcoindSynchronizing'
            else:
                if blkspersec * 20 > 2 and not 'GroestlcoindReady' in self.circBufferState:
                    return 'GroestlcoindSynchronizing'
                else:
                    return 'GroestlcoindReady'

    #############################################################################
    def createProxy(self, forceNew=False):
        if self.proxy == None or forceNew:
            LOGDEBUG('Creating proxy')
            usr,pas,hst,prt = [self.bitconf[k] for k in ['rpcuser','rpcpassword',\
                                                         'host', 'rpcport']]
            pstr = 'http://%s:%s@%s:%d' % (usr, pas, hst, prt)
            LOGINFO('Creating proxy in SDM: host=%s, port=%s', hst, prt)
            self.proxy = ServiceProxy(pstr)

    #############################################################################
    def __backgroundRequestTopBlock(self):
        self.createProxy()
        self.isMidQuery = True
        try:
            numblks = self.proxy.getinfo()['blocks']
            blkhash = self.proxy.getblockhash(numblks)
            toptime = self.proxy.getblock(blkhash)['time']
            #LOGDEBUG('RPC Call: numBlks=%d, toptime=%d', numblks, toptime)
            # Only overwrite once all outputs are retrieved
            self.lastTopBlockInfo['numblks'] = numblks
            self.lastTopBlockInfo['tophash'] = blkhash
            self.lastTopBlockInfo['toptime'] = toptime
            self.lastTopBlockInfo['error'] = None  # Holds error info

            if len(self.last20queries)==0 or \
                  (RightNow()-self.last20queries[-1][0]) > 0.99:
                # This conditional guarantees last 20 queries spans at least 20s
                self.last20queries.append([RightNow(), numblks])
                self.last20queries = self.last20queries[-20:]
                t0, b0 = self.last20queries[0]
                t1, b1 = self.last20queries[-1]

                # Need at least 10s of data to give meaning answer
                if (t1 - t0) < 10:
                    self.lastTopBlockInfo['blkspersec'] = -1
                else:
                    self.lastTopBlockInfo['blkspersec'] = float(
                        b1 - b0) / float(t1 - t0)

        except ValueError:
            # I believe this happens when you used the wrong password
            LOGEXCEPT('ValueError in bkgd req top blk')
            self.lastTopBlockInfo['error'] = 'ValueError'
        except authproxy.JSONRPCException:
            # This seems to happen when groestlcoind is overwhelmed... not quite ready
            LOGDEBUG('generic jsonrpc exception')
            self.lastTopBlockInfo['error'] = 'JsonRpcException'
        except socket.error:
            # Connection isn't available... is groestlcoind not running anymore?
            LOGDEBUG('generic socket error')
            self.lastTopBlockInfo['error'] = 'SocketError'
        except:
            LOGEXCEPT('generic error')
            self.lastTopBlockInfo['error'] = 'UnknownError'
            raise
        finally:
            self.isMidQuery = False

    #############################################################################
    def updateTopBlockInfo(self):
        """
      We want to get the top block information, but if groestlcoind is rigorously
      downloading and verifying the blockchain, it can sometimes take 10s to
      to respond to JSON-RPC calls!  We must do it in the background...

      If it's already querying, no need to kick off another background request,
      just return the last value, which may be "stale" but we don't really
      care for this particular use-case
      """
        if not self.isRunningBitcoind():
            return

        if self.isMidQuery:
            return

        self.createProxy()
        self.queryThread = PyBackgroundThread(self.__backgroundRequestTopBlock)
        self.queryThread.start()

    #############################################################################
    def getTopBlockInfo(self):
        if self.isRunningBitcoind():
            self.updateTopBlockInfo()
            try:
                self.queryThread.join(
                    0.001)  # In most cases, result should come in 1 ms
                # We return a copy so that the data is not changing as we use it
            except:
                pass

        return self.lastTopBlockInfo.copy()

    #############################################################################
    def callJSON(self, func, *args):
        state = self.getSDMState()
        if not state in ('GroestlcoindReady', 'GroestlcoindSynchronizing'):
            LOGWARN('Called callJSON(%s, %s)', func, str(args))
            LOGWARN('Current SDM state: %s', state)
            raise self.BitcoindError, 'callJSON while %s' % state

        return self.proxy.__getattr__(func)(*args)

    #############################################################################
    def returnSDMInfo(self):
        sdminfo = {}
        for key, val in self.bitconf.iteritems():
            sdminfo['bitconf_%s' % key] = val

        for key, val in self.lastTopBlockInfo.iteritems():
            sdminfo['topblk_%s' % key] = val

        sdminfo['executable'] = self.executable
        sdminfo['isrunning'] = self.isRunningBitcoind()
        sdminfo['homedir'] = self.satoshiHome
        sdminfo['proxyinit'] = (not self.proxy == None)
        sdminfo['ismidquery'] = self.isMidQuery
        sdminfo['querycount'] = len(self.last20queries)

        return sdminfo

    #############################################################################
    def printSDMInfo(self):
        print '\nCurrent SDM State:'
        print '\t', 'SDM State Str'.ljust(20), ':', self.getSDMState()
        for key, value in self.returnSDMInfo().iteritems():
            print '\t', str(key).ljust(20), ':', str(value)
示例#4
0
class SatoshiDaemonManager(object):
   """
   Use an existing implementation of bitcoind
   """

   class BitcoindError(Exception): pass
   class BitcoindNotAvailableError(Exception): pass
   class BitcoinDotConfError(Exception): pass
   class SatoshiHomeDirDNE(Exception): pass
   class ConfigFileUserDNE(Exception): pass
   class ConfigFilePwdDNE(Exception): pass


   #############################################################################
   def __init__(self):
      self.executable = None
      self.satoshiHome = None
      self.bitconf = {}
      self.proxy = None
      self.bitcoind = None
      self.isMidQuery = False
      self.last20queries = []
      self.disabled = False
      self.failedFindExe  = False
      self.failedFindHome = False
      self.foundExe = []
      self.circBufferState = []
      self.circBufferTime = []
      self.btcOut = None
      self.btcErr = None
      self.lastTopBlockInfo = { \
                                 'numblks':    -1,
                                 'tophash':    '',
                                 'toptime':    -1,
                                 'error':      'Uninitialized',
                                 'blkspersec': -1     }

      # Added torrent DL before we *actually* start SDM (if it makes sense)
      self.useTorrentFinalAnswer = False
      self.useTorrentFile = ''
      self.torrentDisabled = False
      self.tdm = None
      self.satoshiHome = None
      self.satoshiRoot = None
      

   #############################################################################
   def setSatoshiDir(self, newDir):
      self.satoshiHome = newDir   
      self.satoshiRoot = newDir
      
      if 'testnet' in newDir:
         self.satoshiRoot, tail = os.path.split(newDir) 
      
   #############################################################################
   def setDisableTorrentDL(self, b):
      self.torrentDisabled = b

   #############################################################################
   def tryToSetupTorrentDL(self, torrentPath):
      if self.torrentDisabled:
         LOGWARN('Tried to setup torrent download mgr but we are disabled')
         return False
      
      if not torrentPath or not os.path.exists(torrentPath):
         self.useTorrentFinalAnswer = False
         return False

      bootfile = os.path.join(self.satoshiHome, 'bootstrap.dat')
      bootfilePart = bootfile + '.partial'
      bootfileOld  = bootfile + '.old'

      # cleartorrent.flag means we should remove any pre-existing files
      delTorrentFlag = os.path.join(ARMORY_HOME_DIR, 'cleartorrent.flag')
      if os.path.exists(delTorrentFlag):
         LOGWARN('Flag found to delete any pre-existing torrent files')
         if os.path.exists(bootfile):       os.remove(bootfile)
         if os.path.exists(bootfilePart):   os.remove(bootfilePart)
         if os.path.exists(bootfileOld):    os.remove(bootfileOld)
         if os.path.exists(delTorrentFlag): os.remove(delTorrentFlag)


      TheTDM.setupTorrent(torrentPath, bootfile)
      if not TheTDM.getTDMState()=='ReadyToStart':
         LOGERROR('Unknown error trying to start torrent manager')
         self.useTorrentFinalAnswer = False
         return False


      # We will tell the TDM to write status updates to the log file, and only
      # every 90 seconds.  After it finishes (or fails), simply launch bitcoind
      # as we would've done without the torrent
      #####
      def torrentLogToFile(dpflag=Event(), fractionDone=None, timeEst=None,
                           downRate=None, upRate=None, activity=None,
                           statistics=None, **kws):
         statStr = ''
         if fractionDone:
            statStr += '   Done: %0.1f%%  ' % (fractionDone*100)
         if downRate:
            statStr += ' / DLRate: %0.1f/sec' % (downRate/1024.)
         if timeEst:
            statStr += ' / TLeft: %s' % secondsToHumanTime(timeEst)
         if statistics:
            statStr += ' / Seeds: %d' % (statistics.numSeeds)
            statStr += ' / Peers: %d' % (statistics.numPeers)

         if len(statStr)==0:
            statStr = 'No torrent info available'

         LOGINFO('Torrent: %s' % statStr)

      #####
      def torrentFinished():
         bootsz = '<Unknown>'
         if os.path.exists(bootfile):
            bootsz = bytesToHumanSize(os.path.getsize(bootfile))

         LOGINFO('Torrent finished; size of %s is %s', torrentPath, bootsz)
         LOGINFO('Remove the core btc databases before doing bootstrap')
         deleteBitcoindDBs()
         self.launchBitcoindAndGuardian()

      #####
      def warnUserHashFail():
         from PyQt4.QtGui import QMessageBox
         QMessageBox.warning(self, tr('Hash Failure'), tr("""The torrent download 
            is currently encountering too many packet hash failures to allow it to 
            progress properly. As a result, the torrent engine has been halted. You 
            should report this incident to the Armory team and turn off this feature 
            until further notice."""), QMessageBox.Ok)      
      
      #####
      def torrentFailed(errMsg=''):
         # Not sure there's actually anything we need to do here...
         if errMsg == 'hashFail':
            warnUserHashFail()
            
         bootsz = '<Unknown>'
         if os.path.exists(bootfile):
            bootsz = bytesToHumanSize(os.path.getsize(bootfile))

         LOGERROR('Torrent failed; size of %s is %s', torrentPath, bootsz)
         self.launchBitcoindAndGuardian()
         

 
 
      TheTDM.setSecondsBetweenUpdates(90)
      TheTDM.setCallback('displayFunc',  torrentLogToFile)
      TheTDM.setCallback('finishedFunc', torrentFinished)
      TheTDM.setCallback('failedFunc',   torrentFailed)

      LOGINFO('Bootstrap file is %s' % bytesToHumanSize(TheTDM.torrentSize))
         
      self.useTorrentFinalAnswer = True
      self.useTorrentFile = torrentPath
      return True
      

   #############################################################################
   def shouldTryBootstrapTorrent(self):
      if DISABLE_TORRENTDL or TheTDM.getTDMState()=='Disabled':
         return False

      # The only torrent we have is for the primary Bitcoin network
      if not MAGIC_BYTES=='\xf9\xbe\xb4\xd9':
         return False
      
         

      if TheTDM.torrentSize:
         bootfile = os.path.join(self.satoshiHome, 'bootstrap.dat')
         if os.path.exists(bootfile):
            if os.path.getsize(bootfile) >= TheTDM.torrentSize/2:
               LOGWARN('Looks like a full bootstrap is already here')
               LOGWARN('Skipping torrent download')
               return False
               

      # If they don't even have a BTC_HOME_DIR, corebtc never been installed
      blockDir = os.path.join(self.satoshiHome, 'blocks')
      if not os.path.exists(self.satoshiHome) or not os.path.exists(blockDir):
         return True
      
      # Get the cumulative size of the blk*.dat files
      blockDirSize = sum([os.path.getsize(os.path.join(blockDir, a)) \
                  for a in os.listdir(blockDir) if a.startswith('blk')])
      sizeStr = bytesToHumanSize(blockDirSize)
      LOGINFO('Total size of files in %s is %s' % (blockDir, sizeStr))

      # If they have only a small portion of the blockchain, do it
      szThresh = 100*MEGABYTE if USE_TESTNET else 6*GIGABYTE
      if blockDirSize < szThresh:
         return True

      # So far we know they have a BTC_HOME_DIR, with more than 6GB in blocks/
      # The only thing that can induce torrent now is if we have a partially-
      # finished bootstrap file bigger than the blocks dir.
      bootFiles = ['','']
      bootFiles[0] = os.path.join(self.satoshiHome, 'bootstrap.dat')
      bootFiles[1] = os.path.join(self.satoshiHome, 'bootstrap.dat.partial')
      for fn in bootFiles:
         if os.path.exists(fn):
            if os.path.getsize(fn) > blockDirSize:
               return True
            
      # Okay, we give up -- just download [the rest] via P2P
      return False


   #############################################################################
   #def setSatoshiDir(self, newDir):
      #self.satoshiHome = newDir

   #############################################################################
   def setupSDM(self, pathToBitcoindExe=None, satoshiHome=None, \
                      extraExeSearch=[], createHomeIfDNE=True):
      LOGDEBUG('Exec setupSDM')
      self.failedFindExe = False
      self.failedFindHome = False
      # If we are supplied a path, then ignore the extra exe search paths
      if pathToBitcoindExe==None:
         pathToBitcoindExe = self.findBitcoind(extraExeSearch)
         if len(pathToBitcoindExe)==0:
            LOGDEBUG('Failed to find bitcoind')
            self.failedFindExe = True
         else:
            LOGINFO('Found bitcoind in the following places:')
            for p in pathToBitcoindExe:
               LOGINFO('   %s', p)
            pathToBitcoindExe = pathToBitcoindExe[0]
            LOGINFO('Using: %s', pathToBitcoindExe)

            if not os.path.exists(pathToBitcoindExe):
               LOGINFO('Somehow failed to find exe even after finding it...?')
               self.failedFindExe = True

      self.executable = pathToBitcoindExe

      # Four possible conditions for already-set satoshi home dir, and input arg
      if satoshiHome is not None:
         self.satoshiHome = satoshiHome
      else:
         if self.satoshiHome is None:
            self.satoshiHome = BTC_HOME_DIR

      # If no new dir is specified, leave satoshi home if it's already set
      # Give it a default BTC_HOME_DIR if not.
      if not os.path.exists(self.satoshiHome):
         if createHomeIfDNE:
            LOGINFO('Making satoshi home dir')
            os.makedirs(self.satoshiHome)
         else:
            LOGINFO('No home dir, makedir not requested')
            self.failedFindHome = True

      if self.failedFindExe:  raise self.BitcoindError, 'bitcoind not found'
      if self.failedFindHome: raise self.BitcoindError, 'homedir not found'

      self.disabled = False
      self.proxy = None
      self.bitcoind = None  # this will be a Popen object
      self.isMidQuery = False
      self.last20queries = []

      self.readBitcoinConf(makeIfDNE=True)





   #############################################################################
   def setDisabled(self, newBool=True):
      s = self.getSDMState()

      if newBool==True:
         if s in ('BitcoindInitializing', 'BitcoindSynchronizing', 'BitcoindReady'):
            self.stopBitcoind()

      self.disabled = newBool


   #############################################################################
   def getAllFoundExe(self):
      return list(self.foundExe)


   #############################################################################
   def findBitcoind(self, extraSearchPaths=[]):
      self.foundExe = []

      searchPaths = list(extraSearchPaths)  # create a copy

      if OS_WINDOWS:
         # Making sure the search path argument comes with /daemon and /Bitcoin on Windows

         searchPaths.extend([os.path.join(sp, 'Bitcoin') for sp in searchPaths])
         searchPaths.extend([os.path.join(sp, 'daemon') for sp in searchPaths])

         possBaseDir = []         
         
         from platform import machine
         if '64' in machine():
            possBaseDir.append(os.getenv("ProgramW6432"))            
            possBaseDir.append(os.getenv('PROGRAMFILES(X86)'))
         else:
            possBaseDir.append(os.getenv('PROGRAMFILES'))
        
         # check desktop for links

         home      = os.path.expanduser('~')
         desktop   = os.path.join(home, 'Desktop')

         if os.path.exists(desktop):
            dtopfiles = os.listdir(desktop)
            for path in [os.path.join(desktop, fn) for fn in dtopfiles]:
               if 'bitcoin' in path.lower() and path.lower().endswith('.lnk'):
                  import win32com.client
                  shell = win32com.client.Dispatch('WScript.Shell')
                  targ = shell.CreateShortCut(path).Targetpath
                  targDir = os.path.dirname(targ)
                  LOGINFO('Found Bitcoin-Core link on desktop: %s', targDir)
                  possBaseDir.append( targDir )

         # Also look in default place in ProgramFiles dirs




         # Now look at a few subdirs of the
         searchPaths.extend(possBaseDir)
         searchPaths.extend([os.path.join(p, 'Bitcoin', 'daemon') for p in possBaseDir])
         searchPaths.extend([os.path.join(p, 'daemon') for p in possBaseDir])
         searchPaths.extend([os.path.join(p, 'Bitcoin') for p in possBaseDir])

         for p in searchPaths:
            testPath = os.path.join(p, 'bitcoind.exe')
            if os.path.exists(testPath):
               self.foundExe.append(testPath)

      else:
         # In case this was a downloaded copy, make sure we traverse to bin/64 dir
         if SystemSpecs.IsX64:
            searchPaths.extend([os.path.join(p, 'bin/64') for p in extraSearchPaths])
         else:
            searchPaths.extend([os.path.join(p, 'bin/32') for p in extraSearchPaths])

         searchPaths.extend(['/usr/lib/bitcoin/'])
         searchPaths.extend(os.getenv("PATH").split(':'))

         for p in searchPaths:
            testPath = os.path.join(p, 'bitcoind')
            if os.path.exists(testPath):
               self.foundExe.append(testPath)

         try:
            locs = subprocess_check_output(['whereis','bitcoind']).split()
            if len(locs)>1:
               locs = filter(lambda x: os.path.basename(x)=='bitcoind', locs)
               LOGINFO('"whereis" returned: %s', str(locs))
               self.foundExe.extend(locs)
         except:
            LOGEXCEPT('Error executing "whereis" command')


      # For logging purposes, check that the first answer matches one of the
      # extra search paths.  There should be some kind of notification that
      # their supplied search path was invalid and we are using something else.
      if len(self.foundExe)>0 and len(extraSearchPaths)>0:
         foundIt = False
         for p in extraSearchPaths:
            if self.foundExe[0].startswith(p):
               foundIt=True

         if not foundIt:
            LOGERROR('Bitcoind could not be found in the specified installation:')
            for p in extraSearchPaths:
               LOGERROR('   %s', p)
            LOGERROR('Bitcoind is being started from:')
            LOGERROR('   %s', self.foundExe[0])

      return self.foundExe

   #############################################################################
   def getGuardianPath(self):
      if OS_WINDOWS:
         armoryInstall = os.path.dirname(inspect.getsourcefile(SatoshiDaemonManager))
         # This should return a zip file because of py2exe
         if armoryInstall.endswith('.zip'):
            armoryInstall = os.path.dirname(armoryInstall)
         gpath = os.path.join(armoryInstall, 'guardian.exe')
      else:
         theDir = os.path.dirname(inspect.getsourcefile(SatoshiDaemonManager))
         gpath = os.path.join(theDir, 'guardian.py')

      if not os.path.exists(gpath):
         LOGERROR('Could not find guardian script: %s', gpath)
         raise FileExistsError
      return gpath

   #############################################################################
   def readBitcoinConf(self, makeIfDNE=False):
      LOGINFO('Reading bitcoin.conf file')
      bitconf = os.path.join(self.satoshiRoot, 'bitcoin.conf')
      if not os.path.exists(bitconf):
         if not makeIfDNE:
            raise self.BitcoinDotConfError, 'Could not find bitcoin.conf'
         else:
            LOGINFO('No bitcoin.conf available.  Creating it...')
            touchFile(bitconf)

      # Guarantee that bitcoin.conf file has very strict permissions
      if OS_WINDOWS:
         if OS_VARIANT[0].lower()=='xp':
            LOGERROR('Cannot set permissions correctly in XP!')
            LOGERROR('Please confirm permissions on the following file ')
            LOGERROR('are set to exclusive access only for your user ')
            LOGERROR('(it usually is, but Armory cannot guarantee it ')
            LOGERROR('on XP systems):')
            LOGERROR('    %s', bitconf)
         else:
            LOGINFO('Setting permissions on bitcoin.conf')
            import ctypes
            username_u16 = ctypes.create_unicode_buffer(u'\0', 512)
            str_length = ctypes.c_int(512)
            ctypes.windll.Advapi32.GetUserNameW(ctypes.byref(username_u16), 
                                                ctypes.byref(str_length))
            
            if not CLI_OPTIONS.disableConfPermis:
               import win32process
               LOGINFO('Setting permissions on bitcoin.conf')
               cmd_icacls = [u'icacls',bitconf,u'/inheritance:r',u'/grant:r', u'%s:F' % username_u16.value]
               kargs = {}
               kargs['shell'] = True
               kargs['creationflags'] = win32process.CREATE_NO_WINDOW
               icacls_out = subprocess_check_output(cmd_icacls, **kargs)
               LOGINFO('icacls returned: %s', icacls_out)
            else:
               LOGWARN('Skipped setting permissions on bitcoin.conf file')
            
      else:
         if not CLI_OPTIONS.disableConfPermis:
            LOGINFO('Setting permissions on bitcoin.conf')
            os.chmod(bitconf, stat.S_IRUSR | stat.S_IWUSR)
         else:
            LOGWARN('Skipped setting permissions on bitcoin.conf file')


      with open(bitconf,'r') as f:
         # Find the last character of the each line:  either a newline or '#'
         endchr = lambda line: line.find('#') if line.find('#')>1 else len(line)

         # Reduce each line to a list of key,value pairs separated with '='
         allconf = [l[:endchr(l)].strip().split('=') for l in f.readlines()]

         # Need to convert to (x[0],x[1:]) in case the password has '=' in it
         allconfPairs = [[x[0], '='.join(x[1:])] for x in allconf if len(x)>1]

         # Convert the list of pairs to a dictionary
         self.bitconf = dict(allconfPairs)


      # Look for rpcport, use default if not there
      self.bitconf['rpcport'] = int(self.bitconf.get('rpcport', BITCOIN_RPC_PORT))

      # We must have a username and password.  If not, append to file
      if not self.bitconf.has_key('rpcuser'):
         LOGDEBUG('No rpcuser: creating one')
         with open(bitconf,'a') as f:
            f.write('\n')
            f.write('rpcuser=generated_by_armory\n')
            self.bitconf['rpcuser'] = '******'

      if not self.bitconf.has_key('rpcpassword'):
         LOGDEBUG('No rpcpassword: creating one')
         with open(bitconf,'a') as f:
            randBase58 = SecureBinaryData().GenerateRandom(32).toBinStr()
            randBase58 = binary_to_base58(randBase58)
            f.write('\n')
            f.write('rpcpassword=%s' % randBase58)
            self.bitconf['rpcpassword'] = randBase58


      if not isASCII(self.bitconf['rpcuser']):
         LOGERROR('Non-ASCII character in bitcoin.conf (rpcuser)!')
      if not isASCII(self.bitconf['rpcpassword']):
         LOGERROR('Non-ASCII character in bitcoin.conf (rpcpassword)!')

      self.bitconf['host'] = '127.0.0.1'


   #############################################################################
   def cleanupFailedTorrent(self):
      # Right now I think don't do anything
      pass    

   #############################################################################
   def startBitcoind(self, callback):
      self.btcOut, self.btcErr = None,None
      if self.disabled:
         LOGERROR('SDM was disabled, must be re-enabled before starting')
         return

      LOGINFO('Called startBitcoind')

      if self.isRunningBitcoind() or TheTDM.getTDMState()=='Downloading':
         raise self.BitcoindError, 'Looks like we have already started theSDM'

      if not os.path.exists(self.executable):
         raise self.BitcoindError, 'Could not find bitcoind'

      
      chk1 = os.path.exists(self.useTorrentFile)
      chk2 = self.shouldTryBootstrapTorrent()
      chk3 = TheTDM.getTDMState()=='ReadyToStart'

      if chk1 and chk2 and chk3:
         TheTDM.startDownload()
      else:
         self.launchBitcoindAndGuardian()
            
      #New backend code: we wont be polling the SDM state in the main thread
      #anymore, instead create a thread at bitcoind start to poll the SDM state
      #and notify the main thread once bitcoind is ready, then terminates
      self.pollBitcoindState(callback, async=True)

      
   #############################################################################
   @AllowAsync
   def pollBitcoindState(self, callback):
      while self.getSDMStateLogic() != 'BitcoindReady':
         time.sleep(1.0)
      callback()
      
   #############################################################################
   def launchBitcoindAndGuardian(self):

      pargs = [self.executable]

      if USE_TESTNET:
         pargs.append('-testnet')

      pargs.append('-datadir=%s' % self.satoshiRoot)
      
      try:
         # Don't want some strange error in this size-check to abort loading
         blocksdir = os.path.join(self.satoshiHome, 'blocks')
         sz = long(0)
         if os.path.exists(blocksdir):
            for fn in os.listdir(blocksdir):
               fnpath = os.path.join(blocksdir, fn)
               sz += long(os.path.getsize(fnpath))

         if sz < 5*GIGABYTE:
            if SystemSpecs.Memory>9.0:
               pargs.append('-dbcache=2000')
            elif SystemSpecs.Memory>5.0:
               pargs.append('-dbcache=1000')
            elif SystemSpecs.Memory>3.0:
               pargs.append('-dbcache=500')
      except:
         LOGEXCEPT('Failed size check of blocks directory')

      kargs = {}
      if OS_WINDOWS:
         import win32process
         kargs['shell'] = True
         kargs['creationflags'] = win32process.CREATE_NO_WINDOW
         
      # Startup bitcoind and get its process ID (along with our own)
      self.bitcoind = launchProcess(pargs, **kargs)

      self.btcdpid  = self.bitcoind.pid
      self.selfpid  = os.getpid()

      LOGINFO('PID of bitcoind: %d',  self.btcdpid)
      LOGINFO('PID of armory:   %d',  self.selfpid)

      # Startup guardian process -- it will watch Armory's PID
      gpath = self.getGuardianPath()
      pargs = [gpath, str(self.selfpid), str(self.btcdpid)]
      if not OS_WINDOWS:
         pargs.insert(0, 'python')
      launchProcess(pargs, **kargs)



   #############################################################################
   def stopBitcoind(self):
      LOGINFO('Called stopBitcoind')
      try:
         if not self.isRunningBitcoind():
               LOGINFO('...but bitcoind is not running, to be able to stop')
               return

         #signal bitcoind to stop
         self.proxy.stop()

         #poll the pid until it's gone, for as long as 2 minutes
         total = 0
         while self.bitcoind.poll()==None:
            time.sleep(0.1)
            total += 1

            if total > 1200:
               LOGERROR("bitcoind failed to shutdown in less than 2 minutes."
                      " Terminating.")
               return

         self.bitcoind = None
      except Exception as e:
         LOGERROR(e)
         return


   #############################################################################
   def isRunningBitcoind(self):
      """
      armoryengine satoshiIsAvailable() only tells us whether there's a
      running bitcoind that is actively responding on its port.  But it
      won't be responding immediately after we've started it (still doing
      startup operations).  If bitcoind was started and still running,
      then poll() will return None.  Any othe poll() return value means
      that the process terminated
      """
      if self.bitcoind==None:
         return False
      else:
         if not self.bitcoind.poll()==None:
            LOGDEBUG('Bitcoind is no more')
            if self.btcOut==None:
               self.btcOut, self.btcErr = self.bitcoind.communicate()
               LOGWARN('bitcoind exited, bitcoind STDOUT:')
               for line in self.btcOut.split('\n'):
                  LOGWARN(line)
               LOGWARN('bitcoind exited, bitcoind STDERR:')
               for line in self.btcErr.split('\n'):
                  LOGWARN(line)
         return self.bitcoind.poll()==None

   #############################################################################
   def wasRunningBitcoind(self):
      return (not self.bitcoind==None)

   #############################################################################
   def bitcoindIsResponsive(self):
      return satoshiIsAvailable(self.bitconf['host'], self.bitconf['rpcport'])

   #############################################################################
   def getSDMState(self):
      """
      As for why I'm doing this:  it turns out that between "initializing"
      and "synchronizing", bitcoind temporarily stops responding entirely,
      which causes "not-available" to be the state.  I need to smooth that
      out because it wreaks havoc on the GUI which will switch to showing
      a nasty error.
      """

      state = self.getSDMStateLogic()
      self.circBufferState.append(state)
      self.circBufferTime.append(RightNow())
      if len(self.circBufferTime)>2 and \
         (self.circBufferTime[-1] - self.circBufferTime[1]) > 5:
         # Only remove the first element if we have at least 5s history
         self.circBufferState = self.circBufferState[1:]
         self.circBufferTime  = self.circBufferTime[1:]

      # Here's where we modify the output to smooth out the gap between
      # "initializing" and "synchronizing" (which is a couple seconds
      # of "not available").   "NotAvail" keeps getting added to the
      # buffer, but if it was "initializing" in the last 5 seconds,
      # we will keep "initializing"
      if state=='BitcoindNotAvailable':
         if 'BitcoindInitializing' in self.circBufferState:
            LOGWARN('Overriding not-available state. This should happen 0-5 times')
            return 'BitcoindInitializing'

      return state

   #############################################################################
   def getSDMStateLogic(self):

      if self.disabled:
         return 'BitcoindMgmtDisabled'

      if self.failedFindExe:
         return 'BitcoindExeMissing'

      if self.failedFindHome:
         return 'BitcoindHomeMissing'

      if TheTDM.isRunning():
         return 'TorrentSynchronizing'

      latestInfo = self.getTopBlockInfo()

      if self.bitcoind==None and latestInfo['error']=='Uninitialized':
         return 'BitcoindNeverStarted'

      if not self.isRunningBitcoind():
         # Not running at all:  either never started, or process terminated
         if not self.btcErr==None and len(self.btcErr)>0:
            errstr = self.btcErr.replace(',',' ').replace('.',' ').replace('!',' ')
            errPcs = set([a.lower() for a in errstr.split()])
            runPcs = set(['cannot','obtain','lock','already','running'])
            dbePcs = set(['database', 'recover','backup','except','wallet','dat'])
            if len(errPcs.intersection(runPcs))>=(len(runPcs)-1):
               return 'BitcoindAlreadyRunning'
            elif len(errPcs.intersection(dbePcs))>=(len(dbePcs)-1):
               return 'BitcoindDatabaseEnvError'
            else:
               return 'BitcoindUnknownCrash'
         else:
            return 'BitcoindNotAvailable'
      elif not self.bitcoindIsResponsive():
         # Running but not responsive... must still be initializing
         return 'BitcoindInitializing'
      else:
         # If it's responsive, get the top block and check
         # TODO: These conditionals are based on experimental results.  May
         #       not be accurate what the specific errors mean...
         if latestInfo['error']=='ValueError':
            return 'BitcoindWrongPassword'
         elif latestInfo['error']=='JsonRpcException':
            return 'BitcoindInitializing'
         elif latestInfo['error']=='SocketError':
            return 'BitcoindNotAvailable'

         if 'BitcoindReady' in self.circBufferState:
            # If ready, always ready
            return 'BitcoindReady'

         # If we get here, bitcoind is gave us a response.
         secSinceLastBlk = RightNow() - latestInfo['toptime']
         blkspersec = latestInfo['blkspersec']
         #print 'Blocks per 10 sec:', ('UNKNOWN' if blkspersec==-1 else blkspersec*10)
         if secSinceLastBlk > 4*HOUR or blkspersec==-1:
            return 'BitcoindSynchronizing'
         else:
            if blkspersec*20 > 2 and not 'BitcoindReady' in self.circBufferState:
               return 'BitcoindSynchronizing'
            else:
               return 'BitcoindReady'




   #############################################################################
   def createProxy(self, forceNew=False):
      if self.proxy==None or forceNew:
         LOGDEBUG('Creating proxy')
         usr,pas,hst,prt = [self.bitconf[k] for k in ['rpcuser','rpcpassword',\
                                                      'host', 'rpcport']]
         pstr = 'http://%s:%s@%s:%d' % (usr,pas,hst,prt)
         LOGINFO('Creating proxy in SDM: host=%s, port=%s', hst,prt)
         self.proxy = ServiceProxy(pstr)


   #############################################################################
   def __backgroundRequestTopBlock(self):
      self.createProxy()
      self.isMidQuery = True
      try:
         numblks = self.proxy.getinfo()['blocks']
         blkhash = self.proxy.getblockhash(numblks)
         toptime = self.proxy.getblock(blkhash)['time']
         #LOGDEBUG('RPC Call: numBlks=%d, toptime=%d', numblks, toptime)
         # Only overwrite once all outputs are retrieved
         self.lastTopBlockInfo['numblks'] = numblks
         self.lastTopBlockInfo['tophash'] = blkhash
         self.lastTopBlockInfo['toptime'] = toptime
         self.lastTopBlockInfo['error']   = None    # Holds error info

         if len(self.last20queries)==0 or \
               (RightNow()-self.last20queries[-1][0]) > 0.99:
            # This conditional guarantees last 20 queries spans at least 20s
            self.last20queries.append([RightNow(), numblks])
            self.last20queries = self.last20queries[-20:]
            t0,b0 = self.last20queries[0]
            t1,b1 = self.last20queries[-1]

            # Need at least 10s of data to give meaning answer
            if (t1-t0)<10:
               self.lastTopBlockInfo['blkspersec'] = -1
            else:
               self.lastTopBlockInfo['blkspersec'] = float(b1-b0)/float(t1-t0)

      except ValueError:
         # I believe this happens when you used the wrong password
         LOGEXCEPT('ValueError in bkgd req top blk')
         self.lastTopBlockInfo['error'] = 'ValueError'
      except authproxy.JSONRPCException:
         # This seems to happen when bitcoind is overwhelmed... not quite ready
         LOGDEBUG('generic jsonrpc exception')
         self.lastTopBlockInfo['error'] = 'JsonRpcException'
      except socket.error:
         # Connection isn't available... is bitcoind not running anymore?
         LOGDEBUG('generic socket error')
         self.lastTopBlockInfo['error'] = 'SocketError'
      except:
         LOGEXCEPT('generic error')
         self.lastTopBlockInfo['error'] = 'UnknownError'
         raise
      finally:
         self.isMidQuery = False


   #############################################################################
   def updateTopBlockInfo(self):
      """
      We want to get the top block information, but if bitcoind is rigorously
      downloading and verifying the blockchain, it can sometimes take 10s to
      to respond to JSON-RPC calls!  We must do it in the background...

      If it's already querying, no need to kick off another background request,
      just return the last value, which may be "stale" but we don't really
      care for this particular use-case
      """
      if not self.isRunningBitcoind():
         return

      if self.isMidQuery:
         return

      self.createProxy()
      self.queryThread = PyBackgroundThread(self.__backgroundRequestTopBlock)
      self.queryThread.start()


   #############################################################################
   def getTopBlockInfo(self):
      if self.isRunningBitcoind():
         self.updateTopBlockInfo()
         try:
            self.queryThread.join(0.001)  # In most cases, result should come in 1 ms
            # We return a copy so that the data is not changing as we use it
         except:
            pass

      return self.lastTopBlockInfo.copy()


   #############################################################################
   def callJSON(self, func, *args):
      state = self.getSDMState()
      if not state in ('BitcoindReady', 'BitcoindSynchronizing'):
         LOGWARN('Called callJSON(%s, %s)', func, str(args))
         LOGWARN('Current SDM state: %s', state)
         raise self.BitcoindError, 'callJSON while %s'%state

      return self.proxy.__getattr__(func)(*args)


   #############################################################################
   def returnSDMInfo(self):
      sdminfo = {}
      for key,val in self.bitconf.iteritems():
         sdminfo['bitconf_%s'%key] = val

      for key,val in self.lastTopBlockInfo.iteritems():
         sdminfo['topblk_%s'%key] = val

      sdminfo['executable'] = self.executable
      sdminfo['isrunning']  = self.isRunningBitcoind()
      sdminfo['homedir']    = self.satoshiHome
      sdminfo['proxyinit']  = (not self.proxy==None)
      sdminfo['ismidquery'] = self.isMidQuery
      sdminfo['querycount'] = len(self.last20queries)

      return sdminfo

   #############################################################################
   def printSDMInfo(self):
      print '\nCurrent SDM State:'
      print '\t', 'SDM State Str'.ljust(20), ':', self.getSDMState()
      for key,value in self.returnSDMInfo().iteritems():
         print '\t', str(key).ljust(20), ':', str(value)
示例#5
0
class SatoshiDaemonManager(object):
    """
   Use an existing implementation of bitcoind
   """
    class BitcoindError(Exception):
        pass

    class BitcoindNotAvailableError(Exception):
        pass

    class BadPath(Exception):
        pass

    class BitcoinDotConfError(Exception):
        pass

    class SatoshiHomeDirDNE(Exception):
        pass

    class ConfigFileUserDNE(Exception):
        pass

    class ConfigFilePwdDNE(Exception):
        pass

    #############################################################################
    def __init__(self):
        self.executable = None
        self.satoshiHome = None
        self.bitconf = {}
        self.proxy = None
        self.bitcoind = None
        self.isMidQuery = False
        self.last20queries = []
        self.disabled = False
        self.failedFindExe = False
        self.failedFindHome = False
        self.foundExe = []
        self.circBufferState = []
        self.circBufferTime = []
        self.btcOut = None
        self.btcErr = None
        self.lastTopBlockInfo = { \
                                   'numblks':    -1,
                                   'tophash':    '',
                                   'toptime':    -1,
                                   'error':      'Uninitialized',
                                   'blkspersec': -1     }

        self.tdm = None
        self.satoshiHome = None
        self.satoshiRoot = None

    #############################################################################
    def setSatoshiDir(self, newDir):
        self.satoshiHome = newDir
        self.satoshiRoot = newDir

        if 'testnet' in newDir or 'regtest' in newDir:
            self.satoshiRoot, tail = os.path.split(newDir)

        path = os.path.dirname(os.path.abspath(__file__))
        if OS_MACOSX:
            # OSX separates binaries/start scripts from the Python code. Back up!
            path = os.path.join(path, '../../bin/')
        self.dbExecutable = os.path.join(path, 'ArmoryDB')

        if OS_WINDOWS:
            self.dbExecutable += ".exe"
            if not os.path.exists(self.dbExecutable):
                self.dbExecutable = "./ArmoryDB.exe"

        if OS_LINUX:
            #if there is no local armorydb in the execution folder,
            #look for an installed one
            if not os.path.exists(self.dbExecutable):
                self.dbExecutable = "/usr/bin/ArmoryDB"

    #############################################################################
    def setupSDM(self, pathToBitcoindExe=None, satoshiHome=None, \
                       extraExeSearch=[], createHomeIfDNE=True):
        LOGDEBUG('Exec setupSDM')
        # If the client is remote, don't do anything.
        if not self.localClient:
            LOGWARN("No SDM since the client is remote")
            return

        self.failedFindExe = False
        self.failedFindHome = False
        # If we are supplied a path, then ignore the extra exe search paths
        if pathToBitcoindExe == None:
            pathToBitcoindExe = self.findBitcoind(extraExeSearch)
            if len(pathToBitcoindExe) == 0:
                LOGDEBUG('Failed to find bitcoind')
                self.failedFindExe = True
            else:
                LOGINFO('Found bitcoind in the following places:')
                for p in pathToBitcoindExe:
                    LOGINFO('   %s', p)
                pathToBitcoindExe = pathToBitcoindExe[0]
                LOGINFO('Using: %s', pathToBitcoindExe)

                if not os.path.exists(pathToBitcoindExe):
                    LOGINFO(
                        'Somehow failed to find exe even after finding it...?')
                    self.failedFindExe = True

        self.executable = pathToBitcoindExe

        # Four possible conditions for already-set satoshi home dir, and input arg
        if satoshiHome is not None:
            self.satoshiHome = satoshiHome
        else:
            if self.satoshiHome is None:
                self.satoshiHome = BTC_HOME_DIR

        # If no new dir is specified, leave satoshi home if it's already set
        # Give it a default BTC_HOME_DIR if not.
        if not os.path.exists(self.satoshiHome):
            if createHomeIfDNE:
                LOGINFO('Making satoshi home dir')
                os.makedirs(self.satoshiHome)
            else:
                LOGINFO('No home dir, makedir not requested')
                self.failedFindHome = True

        if self.failedFindExe: raise self.BitcoindError, 'bitcoind not found'
        if self.failedFindHome: raise self.BitcoindError, 'homedir not found'

        self.disabled = False
        self.proxy = None
        self.bitcoind = None  # this will be a Popen object
        self.isMidQuery = False
        self.last20queries = []

        self.readBitcoinConf()

    #############################################################################
    def setupManualSDM(self):
        LOGDEBUG('Exec setupManualSDM')
        # If the client is remote, don't do anything.
        if not self.localClient:
            LOGWARN("No SDM since the client is remote")
            return

        # Setup bitcoind stuff
        self.bitcoind = False
        self.readBitcoinConf()
        self.readCookieFile()

        # Check bitcoind is actually up. If it is not, remove self.bitcoind
        try:
            self.createProxy()
            self.proxy.getinfo()
        except:
            LOGDEBUG("bitcoind rpc is not actually availalbe")
            self.bitcoind = None
            self.proxy = None

    #############################################################################
    def checkClientIsLocal(self):
        if ARMORYDB_IP != ARMORYDB_DEFAULT_IP or ARMORYDB_PORT != ARMORYDB_DEFAULT_PORT:
            self.localClient = False
        else:
            self.localClient = True

    #############################################################################
    def setDisabled(self, newBool=True):
        s = self.getSDMState()

        if newBool == True:
            if s in ('BitcoindInitializing', 'BitcoindSynchronizing',
                     'BitcoindReady'):
                self.stopBitcoind()

        self.disabled = newBool

    #############################################################################
    def getAllFoundExe(self):
        return list(self.foundExe)

    #############################################################################
    def findBitcoind(self, extraSearchPaths=[]):
        self.foundExe = []

        searchPaths = list(extraSearchPaths)  # create a copy

        if OS_WINDOWS:
            # Making sure the search path argument comes with /daemon and /Bitcoin on Windows

            searchPaths.extend(
                [os.path.join(sp, 'Bitcoin') for sp in searchPaths])
            searchPaths.extend(
                [os.path.join(sp, 'daemon') for sp in searchPaths])

            possBaseDir = []

            from platform import machine
            if '64' in machine():
                possBaseDir.append(os.getenv("ProgramW6432"))
                possBaseDir.append(os.getenv('PROGRAMFILES(X86)'))
            else:
                possBaseDir.append(os.getenv('PROGRAMFILES'))

            # check desktop for links

            home = os.path.expanduser('~')
            desktop = os.path.join(home, 'Desktop')

            if os.path.exists(desktop):
                dtopfiles = os.listdir(desktop)
                for path in [os.path.join(desktop, fn) for fn in dtopfiles]:
                    if 'bitcoin' in path.lower() and path.lower().endswith(
                            '.lnk'):
                        import win32com.client
                        shell = win32com.client.Dispatch('WScript.Shell')
                        targ = shell.CreateShortCut(path).Targetpath
                        targDir = os.path.dirname(targ)
                        LOGINFO('Found Bitcoin-Core link on desktop: %s',
                                targDir)
                        possBaseDir.append(targDir)

            # Also look in default place in ProgramFiles dirs

            # Now look at a few subdirs of the
            searchPaths.extend(possBaseDir)
            searchPaths.extend(
                [os.path.join(p, 'Bitcoin', 'daemon') for p in possBaseDir])
            searchPaths.extend(
                [os.path.join(p, 'daemon') for p in possBaseDir])
            searchPaths.extend(
                [os.path.join(p, 'Bitcoin') for p in possBaseDir])

            for p in searchPaths:
                testPath = os.path.join(p, 'bitcoind.exe')
                if os.path.exists(testPath):
                    self.foundExe.append(testPath)

        else:
            # In case this was a downloaded copy, make sure we traverse to bin/64 dir
            if SystemSpecs.IsX64:
                searchPaths.extend(
                    [os.path.join(p, 'bin/64') for p in extraSearchPaths])
            else:
                searchPaths.extend(
                    [os.path.join(p, 'bin/32') for p in extraSearchPaths])

            searchPaths.extend(['/usr/lib/bitcoin/'])
            searchPaths.extend(os.getenv("PATH").split(':'))

            for p in searchPaths:
                testPath = os.path.join(p, 'bitcoind')
                if os.path.exists(testPath):
                    self.foundExe.append(testPath)

            try:
                locs = subprocess_check_output(['whereis', 'bitcoind']).split()
                if len(locs) > 1:
                    locs = filter(lambda x: os.path.basename(x) == 'bitcoind',
                                  locs)
                    LOGINFO('"whereis" returned: %s', str(locs))
                    self.foundExe.extend(locs)
            except:
                LOGEXCEPT('Error executing "whereis" command')

        # For logging purposes, check that the first answer matches one of the
        # extra search paths.  There should be some kind of notification that
        # their supplied search path was invalid and we are using something else.
        if len(self.foundExe) > 0 and len(extraSearchPaths) > 0:
            foundIt = False
            for p in extraSearchPaths:
                if self.foundExe[0].startswith(p):
                    foundIt = True

            if not foundIt:
                LOGERROR(
                    'Bitcoind could not be found in the specified installation:'
                )
                for p in extraSearchPaths:
                    LOGERROR('   %s', p)
                LOGERROR('Bitcoind is being started from:')
                LOGERROR('   %s', self.foundExe[0])

        return self.foundExe

    #############################################################################
    def getGuardianPath(self):
        if OS_WINDOWS:
            armoryInstall = os.path.dirname(
                inspect.getsourcefile(SatoshiDaemonManager))
            # This should return a zip file because of py2exe
            if armoryInstall.endswith('.zip'):
                armoryInstall = os.path.dirname(armoryInstall)
            gpath = os.path.join(armoryInstall, 'guardian.exe')
        else:
            theDir = os.path.dirname(
                inspect.getsourcefile(SatoshiDaemonManager))
            gpath = os.path.join(theDir, 'guardian.py')

        if not os.path.exists(gpath):
            LOGERROR('Could not find guardian script: %s', gpath)
            raise FileExistsError
        return gpath

    #############################################################################
    def readBitcoinConf(self):
        LOGINFO('Reading bitcoin.conf file')
        bitconf = os.path.join(self.satoshiRoot, 'bitcoin.conf')
        if os.path.exists(bitconf):
            # Guarantee that bitcoin.conf file has very strict permissions
            if OS_WINDOWS:
                if OS_VARIANT[0].lower() == 'xp':
                    LOGERROR('Cannot set permissions correctly in XP!')
                    LOGERROR(
                        'Please confirm permissions on the following file ')
                    LOGERROR('are set to exclusive access only for your user ')
                    LOGERROR('(it usually is, but Armory cannot guarantee it ')
                    LOGERROR('on XP systems):')
                    LOGERROR('    %s', bitconf)
                else:
                    LOGINFO('Setting permissions on bitcoin.conf')
                    import ctypes
                    username_u16 = ctypes.create_unicode_buffer(u'\0', 512)
                    str_length = ctypes.c_int(512)
                    ctypes.windll.Advapi32.GetUserNameW(
                        ctypes.byref(username_u16), ctypes.byref(str_length))

                    if not CLI_OPTIONS.disableConfPermis:
                        import win32process
                        LOGINFO('Setting permissions on bitcoin.conf')
                        cmd_icacls = [
                            u'icacls', bitconf, u'/inheritance:r', u'/grant:r',
                            u'%s:F' % username_u16.value
                        ]
                        kargs = {}
                        kargs['shell'] = True
                        kargs['creationflags'] = win32process.CREATE_NO_WINDOW
                        icacls_out = subprocess_check_output(
                            cmd_icacls, **kargs)
                        LOGINFO('icacls returned: %s', icacls_out)
                    else:
                        LOGWARN(
                            'Skipped setting permissions on bitcoin.conf file')

            else:
                if not CLI_OPTIONS.disableConfPermis:
                    LOGINFO('Setting permissions on bitcoin.conf')
                    os.chmod(bitconf, stat.S_IRUSR | stat.S_IWUSR)
                else:
                    LOGWARN('Skipped setting permissions on bitcoin.conf file')

            with open(bitconf, 'r') as f:
                # Find the last character of the each line:  either a newline or '#'
                endchr = lambda line: line.find('#') if line.find(
                    '#') > 1 else len(line)

                # Reduce each line to a list of key,value pairs separated with '='
                allconf = [
                    l[:endchr(l)].strip().split('=') for l in f.readlines()
                ]

                # Need to convert to (x[0],x[1:]) in case the password has '=' in it
                allconfPairs = [[x[0], '='.join(x[1:])] for x in allconf
                                if len(x) > 1]

                # Convert the list of pairs to a dictionary
                self.bitconf = dict(allconfPairs)

            # If there is no password, use cookie auth
            if not self.bitconf.has_key('rpcpassword'):
                LOGDEBUG('No rpcpassword: Using cookie Auth')
                self.readCookieFile()

        # defaults
        self.bitconf['host'] = '127.0.0.1'
        self.bitconf['rpcport'] = BITCOIN_RPC_PORT

    def readCookieFile(self):
        cookiefile = os.path.join(self.satoshiHome, '.cookie')
        if os.path.exists(cookiefile):
            # This only works if bitcoind has started
            with open(cookiefile, 'r') as f:
                userpass = f.readline().split(":", 1)
                self.bitconf['rpcuser'] = userpass[0]
                self.bitconf['rpcpassword'] = urlquote(userpass[1])

    #############################################################################
    def startBitcoind(self, callback):
        self.btcOut, self.btcErr = None, None
        if self.disabled:
            LOGERROR('SDM was disabled, must be re-enabled before starting')
            return

        LOGINFO('Called startBitcoind')

        if self.isRunningBitcoind():
            raise self.BitcoindError, 'Looks like we have already started theSDM'

        if not os.path.exists(self.executable):
            raise self.BitcoindError, 'Could not find bitcoind'

        self.launchBitcoindAndGuardian()

        # wait for user and pass from cookie file after bitcoind has started. Should be very quick
        self.readCookieFile()

        #New backend code: we wont be polling the SDM state in the main thread
        #anymore, instead create a thread at bitcoind start to poll the SDM state
        #and notify the main thread once bitcoind is ready, then terminates
        self.pollBitcoindState(callback, async=True)

    #############################################################################
    @AllowAsync
    def pollBitcoindState(self, callback):
        while self.getSDMStateLogic() != 'BitcoindReady':
            time.sleep(1.0)
        callback()

    #############################################################################
    def spawnDB(self, dbDir):
        pargs = [self.dbExecutable]

        pargs.append('--db-type="' + ARMORY_DB_TYPE + '"')

        if USE_TESTNET:
            pargs.append('--testnet')
        if USE_REGTEST:
            pargs.append('--regtest')

        blocksdir = os.path.join(self.satoshiHome, 'blocks')
        if not os.path.exists(blocksdir):
            raise self.BadPath, "Invalid blockdata path"

        randBase58 = SecureBinaryData().GenerateRandom(32).toBinStr()
        spawnId = binary_to_base58(randBase58)

        pargs.append('--spawnId="' + spawnId + '"')
        pargs.append('--satoshi-datadir="' + blocksdir + '"')
        pargs.append('--dbdir="' + dbDir + '"')

        if CLI_OPTIONS.rebuild:
            pargs.append('--rebuild')
        elif CLI_OPTIONS.rescan:
            pargs.append('--rescan')
        elif CLI_OPTIONS.rescanBalance:
            pargs.append('--rescanSSH')

        if ARMORY_RAM_USAGE != -1:
            pargs.append('--ram-usage=' + ARMORY_RAM_USAGE)
        if ARMORY_THREAD_COUNT != -1:
            pargs.append('--thread-count=' + ARMORY_THREAD_COUNT)

        kargs = {}
        if OS_WINDOWS:
            #import win32process
            kargs['shell'] = True
            #kargs['creationflags'] = win32process.CREATE_NO_WINDOW

        launchProcess(pargs, **kargs)

        return spawnId

    #############################################################################
    def launchBitcoindAndGuardian(self):

        pargs = [self.executable]

        if USE_TESTNET:
            pargs.append('-testnet')
        elif USE_REGTEST:
            pargs.append('-regtest')

        pargs.append('-datadir=%s' % self.satoshiRoot)

        try:
            # Don't want some strange error in this size-check to abort loading
            blocksdir = os.path.join(self.satoshiHome, 'blocks')
            sz = long(0)
            if os.path.exists(blocksdir):
                for fn in os.listdir(blocksdir):
                    fnpath = os.path.join(blocksdir, fn)
                    sz += long(os.path.getsize(fnpath))

            if sz < 5 * GIGABYTE:
                if SystemSpecs.Memory > 9.0:
                    pargs.append('-dbcache=2000')
                elif SystemSpecs.Memory > 5.0:
                    pargs.append('-dbcache=1000')
                elif SystemSpecs.Memory > 3.0:
                    pargs.append('-dbcache=500')
        except:
            LOGEXCEPT('Failed size check of blocks directory')

        kargs = {}
        if OS_WINDOWS:
            import win32process
            kargs['shell'] = True
            kargs['creationflags'] = win32process.CREATE_NO_WINDOW

        # Startup bitcoind and get its process ID (along with our own)
        self.bitcoind = launchProcess(pargs, **kargs)

        self.btcdpid = self.bitcoind.pid
        self.selfpid = os.getpid()

        LOGINFO('PID of bitcoind: %d', self.btcdpid)
        LOGINFO('PID of armory:   %d', self.selfpid)

        # Startup guardian process -- it will watch Armory's PID
        gpath = self.getGuardianPath()
        pargs = [gpath, str(self.selfpid), str(self.btcdpid)]
        if not OS_WINDOWS:
            pargs.insert(0, 'python')
        launchProcess(pargs, **kargs)

    #############################################################################
    def stopBitcoind(self):
        LOGINFO('Called stopBitcoind')
        if self.bitcoind == False:
            self.bitcoind = None
            return
        try:
            if not self.isRunningBitcoind():
                LOGINFO('...but bitcoind is not running, to be able to stop')
                return

            #signal bitcoind to stop
            self.proxy.stop()

            #poll the pid until it's gone, for as long as 2 minutes
            total = 0
            while self.bitcoind.poll() == None:
                time.sleep(0.1)
                total += 1

                if total > 1200:
                    LOGERROR(
                        "bitcoind failed to shutdown in less than 2 minutes."
                        " Terminating.")
                    return

            self.bitcoind = None
        except Exception as e:
            LOGERROR(e)
            return

    #############################################################################
    def isRunningBitcoind(self):
        """
      armoryengine satoshiIsAvailable() only tells us whether there's a
      running bitcoind that is actively responding on its port.  But it
      won't be responding immediately after we've started it (still doing
      startup operations).  If bitcoind was started and still running,
      then poll() will return None.  Any othe poll() return value means
      that the process terminated
      """
        if self.bitcoind == None:
            return False
        # Assume Bitcoind is running if manually started
        if self.bitcoind == False:
            return True
        else:
            if not self.bitcoind.poll() == None:
                LOGDEBUG('Bitcoind is no more')
                if self.btcOut == None:
                    self.btcOut, self.btcErr = self.bitcoind.communicate()
                    LOGWARN('bitcoind exited, bitcoind STDOUT:')
                    for line in self.btcOut.split('\n'):
                        LOGWARN(line)
                    LOGWARN('bitcoind exited, bitcoind STDERR:')
                    for line in self.btcErr.split('\n'):
                        LOGWARN(line)
            return self.bitcoind.poll() == None

    #############################################################################
    def wasRunningBitcoind(self):
        return (not self.bitcoind == None)

    #############################################################################
    def bitcoindIsResponsive(self):
        return satoshiIsAvailable(self.bitconf['host'],
                                  self.bitconf['rpcport'])

    #############################################################################
    def getSDMState(self):
        """
      As for why I'm doing this:  it turns out that between "initializing"
      and "synchronizing", bitcoind temporarily stops responding entirely,
      which causes "not-available" to be the state.  I need to smooth that
      out because it wreaks havoc on the GUI which will switch to showing
      a nasty error.
      """

        state = self.getSDMStateLogic()
        self.circBufferState.append(state)
        self.circBufferTime.append(RightNow())
        if len(self.circBufferTime)>2 and \
           (self.circBufferTime[-1] - self.circBufferTime[1]) > 5:
            # Only remove the first element if we have at least 5s history
            self.circBufferState = self.circBufferState[1:]
            self.circBufferTime = self.circBufferTime[1:]

        # Here's where we modify the output to smooth out the gap between
        # "initializing" and "synchronizing" (which is a couple seconds
        # of "not available").   "NotAvail" keeps getting added to the
        # buffer, but if it was "initializing" in the last 5 seconds,
        # we will keep "initializing"
        if state == 'BitcoindNotAvailable':
            if 'BitcoindInitializing' in self.circBufferState:
                LOGWARN(
                    'Overriding not-available state. This should happen 0-5 times'
                )
                return 'BitcoindInitializing'

        return state

    #############################################################################
    def getSDMStateLogic(self):

        if self.disabled:
            return 'BitcoindMgmtDisabled'

        if self.failedFindExe:
            return 'BitcoindExeMissing'

        if self.failedFindHome:
            return 'BitcoindHomeMissing'

        latestInfo = self.getTopBlockInfo()

        if self.bitcoind == None and latestInfo['error'] == 'Uninitialized':
            return 'BitcoindNeverStarted'

        if not self.isRunningBitcoind():
            # Not running at all:  either never started, or process terminated
            if not self.btcErr == None and len(self.btcErr) > 0:
                errstr = self.btcErr.replace(',', ' ').replace('.',
                                                               ' ').replace(
                                                                   '!', ' ')
                errPcs = set([a.lower() for a in errstr.split()])
                runPcs = set(
                    ['cannot', 'obtain', 'lock', 'already', 'running'])
                dbePcs = set([
                    'database', 'recover', 'backup', 'except', 'wallet', 'dat'
                ])
                if len(errPcs.intersection(runPcs)) >= (len(runPcs) - 1):
                    return 'BitcoindAlreadyRunning'
                elif len(errPcs.intersection(dbePcs)) >= (len(dbePcs) - 1):
                    return 'BitcoindDatabaseEnvError'
                else:
                    return 'BitcoindUnknownCrash'
            else:
                return 'BitcoindNotAvailable'
        elif not self.bitcoindIsResponsive():
            # Running but not responsive... must still be initializing
            return 'BitcoindInitializing'
        else:
            # If it's responsive, get the top block and check
            # TODO: These conditionals are based on experimental results.  May
            #       not be accurate what the specific errors mean...
            if latestInfo['error'] == 'ValueError':
                return 'BitcoindWrongPassword'
            elif latestInfo['error'] == 'JsonRpcException':
                return 'BitcoindInitializing'
            elif latestInfo['error'] == 'SocketError':
                return 'BitcoindNotAvailable'

            if 'BitcoindReady' in self.circBufferState:
                # If ready, always ready
                return 'BitcoindReady'

            # If we get here, bitcoind is gave us a response.
            secSinceLastBlk = RightNow() - latestInfo['toptime']
            blkspersec = latestInfo['blkspersec']
            #print 'Blocks per 10 sec:', ('UNKNOWN' if blkspersec==-1 else blkspersec*10)
            if secSinceLastBlk > 4 * HOUR or blkspersec == -1:
                return 'BitcoindSynchronizing'
            else:
                if blkspersec * 20 > 2 and not 'BitcoindReady' in self.circBufferState:
                    return 'BitcoindSynchronizing'
                else:
                    return 'BitcoindReady'

    #############################################################################
    def createProxy(self, forceNew=False):
        if self.proxy == None or forceNew:
            LOGDEBUG('Creating proxy')
            usr,pas,hst,prt = [self.bitconf[k] for k in ['rpcuser','rpcpassword',\
                                                         'host', 'rpcport']]
            pstr = 'http://%s:%s@%s:%d' % (usr, pas, hst, prt)
            LOGINFO('Creating proxy in SDM: host=%s, port=%s', hst, prt)
            self.proxy = ServiceProxy(pstr)

    #############################################################################
    def __backgroundRequestTopBlock(self):
        self.createProxy()
        self.isMidQuery = True
        try:
            numblks = self.proxy.getinfo()['blocks']
            blkhash = self.proxy.getblockhash(numblks)
            toptime = self.proxy.getblock(blkhash)['time']
            #LOGDEBUG('RPC Call: numBlks=%d, toptime=%d', numblks, toptime)
            # Only overwrite once all outputs are retrieved
            self.lastTopBlockInfo['numblks'] = numblks
            self.lastTopBlockInfo['tophash'] = blkhash
            self.lastTopBlockInfo['toptime'] = toptime
            self.lastTopBlockInfo['error'] = None  # Holds error info

            if len(self.last20queries)==0 or \
                  (RightNow()-self.last20queries[-1][0]) > 0.99:
                # This conditional guarantees last 20 queries spans at least 20s
                self.last20queries.append([RightNow(), numblks])
                self.last20queries = self.last20queries[-20:]
                t0, b0 = self.last20queries[0]
                t1, b1 = self.last20queries[-1]

                # Need at least 10s of data to give meaning answer
                if (t1 - t0) < 10:
                    self.lastTopBlockInfo['blkspersec'] = -1
                else:
                    self.lastTopBlockInfo['blkspersec'] = float(
                        b1 - b0) / float(t1 - t0)

        except ValueError:
            # I believe this happens when you used the wrong password
            LOGEXCEPT('ValueError in bkgd req top blk')
            self.lastTopBlockInfo['error'] = 'ValueError'
        except authproxy.JSONRPCException:
            # This seems to happen when bitcoind is overwhelmed... not quite ready
            LOGDEBUG('generic jsonrpc exception')
            self.lastTopBlockInfo['error'] = 'JsonRpcException'
        except socket.error:
            # Connection isn't available... is bitcoind not running anymore?
            LOGDEBUG('generic socket error')
            self.lastTopBlockInfo['error'] = 'SocketError'
        except:
            LOGEXCEPT('generic error')
            self.lastTopBlockInfo['error'] = 'UnknownError'
            raise
        finally:
            self.isMidQuery = False

    #############################################################################
    def updateTopBlockInfo(self):
        """
      We want to get the top block information, but if bitcoind is rigorously
      downloading and verifying the blockchain, it can sometimes take 10s to
      to respond to JSON-RPC calls!  We must do it in the background...

      If it's already querying, no need to kick off another background request,
      just return the last value, which may be "stale" but we don't really
      care for this particular use-case
      """
        if not self.isRunningBitcoind():
            return

        if self.isMidQuery:
            return

        self.createProxy()
        self.queryThread = PyBackgroundThread(self.__backgroundRequestTopBlock)
        self.queryThread.start()

    #############################################################################
    def getTopBlockInfo(self):
        if self.isRunningBitcoind():
            self.updateTopBlockInfo()
            try:
                self.queryThread.join(
                    0.001)  # In most cases, result should come in 1 ms
                # We return a copy so that the data is not changing as we use it
            except:
                pass

        return self.lastTopBlockInfo.copy()

    #############################################################################
    def callJSONIgnoreOwnership(self, func, *args):
        if self.proxy is None:
            raise self.BitcoindError, 'no node RPC connection'

        return self.proxy.__getattr__(func)(*args)

    #############################################################################
    def callJSON(self, func, *args):
        state = self.getSDMState()
        if not state in ('BitcoindReady', 'BitcoindSynchronizing'):
            LOGWARN('Called callJSON(%s, %s)', func, str(args))
            LOGWARN('Current SDM state: %s', state)
            raise self.BitcoindError, 'callJSON while %s' % state

        return self.proxy.__getattr__(func)(*args)

    #############################################################################
    def returnSDMInfo(self):
        sdminfo = {}
        for key, val in self.bitconf.iteritems():
            sdminfo['bitconf_%s' % key] = val

        for key, val in self.lastTopBlockInfo.iteritems():
            sdminfo['topblk_%s' % key] = val

        sdminfo['executable'] = self.executable
        sdminfo['isrunning'] = self.isRunningBitcoind()
        sdminfo['homedir'] = self.satoshiHome
        sdminfo['proxyinit'] = (not self.proxy == None)
        sdminfo['ismidquery'] = self.isMidQuery
        sdminfo['querycount'] = len(self.last20queries)

        return sdminfo

    #############################################################################
    def printSDMInfo(self):
        print '\nCurrent SDM State:'
        print '\t', 'SDM State Str'.ljust(20), ':', self.getSDMState()
        for key, value in self.returnSDMInfo().iteritems():
            print '\t', str(key).ljust(20), ':', str(value)
示例#6
0
class SatoshiDaemonManager(object):
   """
   Use an existing implementation of bitcoind
   """

   class BitcoindError(Exception): pass
   class BitcoindNotAvailableError(Exception): pass
   class BadPath(Exception): pass
   class BitcoinDotConfError(Exception): pass
   class SatoshiHomeDirDNE(Exception): pass
   class ConfigFileUserDNE(Exception): pass
   class ConfigFilePwdDNE(Exception): pass


   #############################################################################
   def __init__(self):
      self.executable = None
      self.satoshiHome = None
      self.bitconf = {}
      self.proxy = None
      self.bitcoind = None
      self.isMidQuery = False
      self.last20queries = []
      self.disabled = False
      self.failedFindExe  = False
      self.failedFindHome = False
      self.foundExe = []
      self.circBufferState = []
      self.circBufferTime = []
      self.btcOut = None
      self.btcErr = None
      self.lastTopBlockInfo = { \
                                 'numblks':    -1,
                                 'tophash':    '',
                                 'toptime':    -1,
                                 'error':      'Uninitialized',
                                 'blkspersec': -1     }

      self.tdm = None
      self.satoshiHome = None
      self.satoshiRoot = None


   #############################################################################
   def setSatoshiDir(self, newDir):
      self.satoshiHome = newDir
      self.satoshiRoot = newDir

      if 'testnet' in newDir or 'regtest' in newDir:
         self.satoshiRoot, tail = os.path.split(newDir)

      path = os.path.dirname(os.path.abspath(__file__))
      if OS_MACOSX:
         # OSX separates binaries/start scripts from the Python code. Back up!
         path = os.path.join(path, '../../bin/')
      self.dbExecutable = os.path.join(path, 'ArmoryDB')  
         
      if OS_WINDOWS:
         self.dbExecutable += ".exe"
         if not os.path.exists(self.dbExecutable):
            self.dbExecutable = "./ArmoryDB.exe"
      
      if OS_LINUX:
         #if there is no local armorydb in the execution folder, 
         #look for an installed one
         if not os.path.exists(self.dbExecutable):
            self.dbExecutable = "/usr/bin/ArmoryDB"

   #############################################################################
   def setupSDM(self, pathToBitcoindExe=None, satoshiHome=None, \
                      extraExeSearch=[], createHomeIfDNE=True):
      LOGDEBUG('Exec setupSDM')
      # If the client is remote, don't do anything.
      if not self.localClient:
         LOGWARN("No SDM since the client is remote")
         return

      self.failedFindExe = False
      self.failedFindHome = False
      # If we are supplied a path, then ignore the extra exe search paths
      if pathToBitcoindExe==None:
         pathToBitcoindExe = self.findBitcoind(extraExeSearch)
         if len(pathToBitcoindExe)==0:
            LOGDEBUG('Failed to find bitcoind')
            self.failedFindExe = True
         else:
            LOGINFO('Found bitcoind in the following places:')
            for p in pathToBitcoindExe:
               LOGINFO('   %s', p)
            pathToBitcoindExe = pathToBitcoindExe[0]
            LOGINFO('Using: %s', pathToBitcoindExe)

            if not os.path.exists(pathToBitcoindExe):
               LOGINFO('Somehow failed to find exe even after finding it...?')
               self.failedFindExe = True

      self.executable = pathToBitcoindExe

      # Four possible conditions for already-set satoshi home dir, and input arg
      if satoshiHome is not None:
         self.satoshiHome = satoshiHome
      else:
         if self.satoshiHome is None:
            self.satoshiHome = BTC_HOME_DIR

      # If no new dir is specified, leave satoshi home if it's already set
      # Give it a default BTC_HOME_DIR if not.
      if not os.path.exists(self.satoshiHome):
         if createHomeIfDNE:
            LOGINFO('Making satoshi home dir')
            os.makedirs(self.satoshiHome)
         else:
            LOGINFO('No home dir, makedir not requested')
            self.failedFindHome = True

      if self.failedFindExe:  raise self.BitcoindError, 'bitcoind not found'
      if self.failedFindHome: raise self.BitcoindError, 'homedir not found'

      self.disabled = False
      self.proxy = None
      self.bitcoind = None  # this will be a Popen object
      self.isMidQuery = False
      self.last20queries = []

      self.readBitcoinConf()

   #############################################################################
   def setupManualSDM(self):
      LOGDEBUG('Exec setupManualSDM')
      # If the client is remote, don't do anything.
      if not self.localClient:
         LOGWARN("No SDM since the client is remote")
         return

      # Setup bitcoind stuff
      self.bitcoind = False
      self.readBitcoinConf()
      self.readCookieFile()

      # Check bitcoind is actually up. If it is not, remove self.bitcoind
      try:
         self.createProxy()         
         self.proxy.getinfo()
      except:
         LOGDEBUG("bitcoind rpc is not actually availalbe")
         self.bitcoind = None
         self.proxy = None

   #############################################################################
   def checkClientIsLocal(self):
      if ARMORYDB_IP != ARMORYDB_DEFAULT_IP or ARMORYDB_PORT != ARMORYDB_DEFAULT_PORT:
         self.localClient = False
      else:
         self.localClient = True

   #############################################################################
   def setDisabled(self, newBool=True):
      s = self.getSDMState()

      if newBool==True:
         if s in ('BitcoindInitializing', 'BitcoindSynchronizing', 'BitcoindReady'):
            self.stopBitcoind()

      self.disabled = newBool


   #############################################################################
   def getAllFoundExe(self):
      return list(self.foundExe)


   #############################################################################
   def findBitcoind(self, extraSearchPaths=[]):
      self.foundExe = []

      searchPaths = list(extraSearchPaths)  # create a copy

      if OS_WINDOWS:
         # Making sure the search path argument comes with /daemon and /Bitcoin on Windows

         searchPaths.extend([os.path.join(sp, 'Bitcoin') for sp in searchPaths])
         searchPaths.extend([os.path.join(sp, 'daemon') for sp in searchPaths])

         possBaseDir = []

         from platform import machine
         if '64' in machine():
            possBaseDir.append(os.getenv("ProgramW6432"))
            possBaseDir.append(os.getenv('PROGRAMFILES(X86)'))
         else:
            possBaseDir.append(os.getenv('PROGRAMFILES'))

         # check desktop for links

         home      = os.path.expanduser('~')
         desktop   = os.path.join(home, 'Desktop')

         if os.path.exists(desktop):
            dtopfiles = os.listdir(desktop)
            for path in [os.path.join(desktop, fn) for fn in dtopfiles]:
               if 'bitcoin' in path.lower() and path.lower().endswith('.lnk'):
                  import win32com.client
                  shell = win32com.client.Dispatch('WScript.Shell')
                  targ = shell.CreateShortCut(path).Targetpath
                  targDir = os.path.dirname(targ)
                  LOGINFO('Found Bitcoin-Core link on desktop: %s', targDir)
                  possBaseDir.append( targDir )

         # Also look in default place in ProgramFiles dirs




         # Now look at a few subdirs of the
         searchPaths.extend(possBaseDir)
         searchPaths.extend([os.path.join(p, 'Bitcoin', 'daemon') for p in possBaseDir])
         searchPaths.extend([os.path.join(p, 'daemon') for p in possBaseDir])
         searchPaths.extend([os.path.join(p, 'Bitcoin') for p in possBaseDir])

         for p in searchPaths:
            testPath = os.path.join(p, 'bitcoind.exe')
            if os.path.exists(testPath):
               self.foundExe.append(testPath)

      else:
         # In case this was a downloaded copy, make sure we traverse to bin/64 dir
         if SystemSpecs.IsX64:
            searchPaths.extend([os.path.join(p, 'bin/64') for p in extraSearchPaths])
         else:
            searchPaths.extend([os.path.join(p, 'bin/32') for p in extraSearchPaths])

         searchPaths.extend(['/usr/lib/bitcoin/'])
         searchPaths.extend(os.getenv("PATH").split(':'))

         for p in searchPaths:
            testPath = os.path.join(p, 'bitcoind')
            if os.path.exists(testPath):
               self.foundExe.append(testPath)

         try:
            locs = subprocess_check_output(['whereis','bitcoind']).split()
            if len(locs)>1:
               locs = filter(lambda x: os.path.basename(x)=='bitcoind', locs)
               LOGINFO('"whereis" returned: %s', str(locs))
               self.foundExe.extend(locs)
         except:
            LOGEXCEPT('Error executing "whereis" command')


      # For logging purposes, check that the first answer matches one of the
      # extra search paths.  There should be some kind of notification that
      # their supplied search path was invalid and we are using something else.
      if len(self.foundExe)>0 and len(extraSearchPaths)>0:
         foundIt = False
         for p in extraSearchPaths:
            if self.foundExe[0].startswith(p):
               foundIt=True

         if not foundIt:
            LOGERROR('Bitcoind could not be found in the specified installation:')
            for p in extraSearchPaths:
               LOGERROR('   %s', p)
            LOGERROR('Bitcoind is being started from:')
            LOGERROR('   %s', self.foundExe[0])

      return self.foundExe

   #############################################################################
   def getGuardianPath(self):
      if OS_WINDOWS:
         armoryInstall = os.path.dirname(inspect.getsourcefile(SatoshiDaemonManager))
         # This should return a zip file because of py2exe
         if armoryInstall.endswith('.zip'):
            armoryInstall = os.path.dirname(armoryInstall)
         gpath = os.path.join(armoryInstall, 'guardian.exe')
      else:
         theDir = os.path.dirname(inspect.getsourcefile(SatoshiDaemonManager))
         gpath = os.path.join(theDir, 'guardian.py')

      if not os.path.exists(gpath):
         LOGERROR('Could not find guardian script: %s', gpath)
         raise FileExistsError
      return gpath

   #############################################################################
   def readBitcoinConf(self):
      LOGINFO('Reading bitcoin.conf file')
      bitconf = os.path.join(self.satoshiRoot, 'bitcoin.conf')
      if os.path.exists(bitconf):
         # Guarantee that bitcoin.conf file has very strict permissions
         if OS_WINDOWS:
            if OS_VARIANT[0].lower()=='xp':
               LOGERROR('Cannot set permissions correctly in XP!')
               LOGERROR('Please confirm permissions on the following file ')
               LOGERROR('are set to exclusive access only for your user ')
               LOGERROR('(it usually is, but Armory cannot guarantee it ')
               LOGERROR('on XP systems):')
               LOGERROR('    %s', bitconf)
            else:
               LOGINFO('Setting permissions on bitcoin.conf')
               import ctypes
               username_u16 = ctypes.create_unicode_buffer(u'\0', 512)
               str_length = ctypes.c_int(512)
               ctypes.windll.Advapi32.GetUserNameW(ctypes.byref(username_u16),
                                                   ctypes.byref(str_length))

               if not CLI_OPTIONS.disableConfPermis:
                  import win32process
                  LOGINFO('Setting permissions on bitcoin.conf')
                  cmd_icacls = [u'icacls',bitconf,u'/inheritance:r',u'/grant:r', u'%s:F' % username_u16.value]
                  kargs = {}
                  kargs['shell'] = True
                  kargs['creationflags'] = win32process.CREATE_NO_WINDOW
                  icacls_out = subprocess_check_output(cmd_icacls, **kargs)
                  LOGINFO('icacls returned: %s', icacls_out)
               else:
                  LOGWARN('Skipped setting permissions on bitcoin.conf file')

         else:
            if not CLI_OPTIONS.disableConfPermis:
               LOGINFO('Setting permissions on bitcoin.conf')
               os.chmod(bitconf, stat.S_IRUSR | stat.S_IWUSR)
            else:
               LOGWARN('Skipped setting permissions on bitcoin.conf file')


         with open(bitconf,'r') as f:
            # Find the last character of the each line:  either a newline or '#'
            endchr = lambda line: line.find('#') if line.find('#')>1 else len(line)

            # Reduce each line to a list of key,value pairs separated with '='
            allconf = [l[:endchr(l)].strip().split('=') for l in f.readlines()]

            # Need to convert to (x[0],x[1:]) in case the password has '=' in it
            allconfPairs = [[x[0], '='.join(x[1:])] for x in allconf if len(x)>1]

            # Convert the list of pairs to a dictionary
            self.bitconf = dict(allconfPairs)

         # If there is no password, use cookie auth
         if not self.bitconf.has_key('rpcpassword'):
            LOGDEBUG('No rpcpassword: Using cookie Auth')
            self.readCookieFile()

      # defaults
      self.bitconf['host'] = '127.0.0.1'
      self.bitconf['rpcport'] = BITCOIN_RPC_PORT

   def readCookieFile(self):
      cookiefile = os.path.join(self.satoshiHome, '.cookie')
      if os.path.exists(cookiefile):
         # This only works if bitcoind has started
         with open(cookiefile, 'r') as f:
            userpass = f.readline().split(":", 1)
            self.bitconf['rpcuser'] = userpass[0]
            self.bitconf['rpcpassword'] = urlquote(userpass[1])

   #############################################################################
   def startBitcoind(self, callback):
      self.btcOut, self.btcErr = None,None
      if self.disabled:
         LOGERROR('SDM was disabled, must be re-enabled before starting')
         return

      LOGINFO('Called startBitcoind')

      if self.isRunningBitcoind():
         raise self.BitcoindError, 'Looks like we have already started theSDM'

      if not os.path.exists(self.executable):
         raise self.BitcoindError, 'Could not find bitcoind'

      self.launchBitcoindAndGuardian()

      # wait for user and pass from cookie file after bitcoind has started. Should be very quick
      self.readCookieFile()

      #New backend code: we wont be polling the SDM state in the main thread
      #anymore, instead create a thread at bitcoind start to poll the SDM state
      #and notify the main thread once bitcoind is ready, then terminates
      self.pollBitcoindState(callback, async=True)


   #############################################################################
   @AllowAsync
   def pollBitcoindState(self, callback):
      while self.getSDMStateLogic() != 'BitcoindReady':
         time.sleep(1.0)
      callback()

   #############################################################################
   def spawnDB(self, dataDir, dbDir):
      pargs = [self.dbExecutable]

      pargs.append('--db-type="' + ARMORY_DB_TYPE + '"')

      if USE_TESTNET:
         pargs.append('--testnet')
      if USE_REGTEST:
         pargs.append('--regtest');

      blocksdir = os.path.join(self.satoshiHome, 'blocks')
      if not os.path.exists(blocksdir):
         raise self.BadPath, "Invalid blockdata path"

      randBase58 = SecureBinaryData().GenerateRandom(32).toBinStr()
      spawnId = binary_to_base58(randBase58)

      pargs.append('--spawnId="' + spawnId + '"')
      pargs.append('--satoshi-datadir="' + blocksdir + '"')
      pargs.append('--datadir="' + dataDir + '"')
      pargs.append('--dbdir="' + dbDir + '"')

      if CLI_OPTIONS.rebuild:
         pargs.append('--rebuild')
      elif CLI_OPTIONS.rescan:
         pargs.append('--rescan')
      elif CLI_OPTIONS.rescanBalance:
         pargs.append('--rescanSSH')

      if ARMORY_RAM_USAGE != -1:
         pargs.append('--ram-usage=' + ARMORY_RAM_USAGE)
      if ARMORY_THREAD_COUNT != -1:
         pargs.append('--thread-count=' + ARMORY_THREAD_COUNT)

      kargs = {}
      if OS_WINDOWS:
         #import win32process
         kargs['shell'] = True
         #kargs['creationflags'] = win32process.CREATE_NO_WINDOW

      launchProcess(pargs, **kargs)

      return spawnId

   #############################################################################
   def launchBitcoindAndGuardian(self):

      pargs = [self.executable]

      if USE_TESTNET:
         pargs.append('-testnet')
      elif USE_REGTEST:
         pargs.append('-regtest')

      pargs.append('-datadir=%s' % self.satoshiRoot)

      try:
         # Don't want some strange error in this size-check to abort loading
         blocksdir = os.path.join(self.satoshiHome, 'blocks')
         sz = long(0)
         if os.path.exists(blocksdir):
            for fn in os.listdir(blocksdir):
               fnpath = os.path.join(blocksdir, fn)
               sz += long(os.path.getsize(fnpath))

         if sz < 5*GIGABYTE:
            if SystemSpecs.Memory>9.0:
               pargs.append('-dbcache=2000')
            elif SystemSpecs.Memory>5.0:
               pargs.append('-dbcache=1000')
            elif SystemSpecs.Memory>3.0:
               pargs.append('-dbcache=500')
      except:
         LOGEXCEPT('Failed size check of blocks directory')

      kargs = {}
      if OS_WINDOWS:
         import win32process
         kargs['shell'] = True
         kargs['creationflags'] = win32process.CREATE_NO_WINDOW

      # Startup bitcoind and get its process ID (along with our own)
      self.bitcoind = launchProcess(pargs, **kargs)

      self.btcdpid  = self.bitcoind.pid
      self.selfpid  = os.getpid()

      LOGINFO('PID of bitcoind: %d',  self.btcdpid)
      LOGINFO('PID of armory:   %d',  self.selfpid)

      # Startup guardian process -- it will watch Armory's PID
      gpath = self.getGuardianPath()
      pargs = [gpath, str(self.selfpid), str(self.btcdpid)]
      if not OS_WINDOWS:
         pargs.insert(0, 'python')
      launchProcess(pargs, **kargs)



   #############################################################################
   def stopBitcoind(self):
      LOGINFO('Called stopBitcoind')
      if self.bitcoind == False:
         self.bitcoind = None
         return
      try:
         if not self.isRunningBitcoind():
               LOGINFO('...but bitcoind is not running, to be able to stop')
               return

         #signal bitcoind to stop
         self.proxy.stop()

         #poll the pid until it's gone, for as long as 2 minutes
         total = 0
         while self.bitcoind.poll()==None:
            time.sleep(0.1)
            total += 1

            if total > 1200:
               LOGERROR("bitcoind failed to shutdown in less than 2 minutes."
                      " Terminating.")
               return

         self.bitcoind = None
      except Exception as e:
         LOGERROR(e)
         return


   #############################################################################
   def isRunningBitcoind(self):
      """
      armoryengine satoshiIsAvailable() only tells us whether there's a
      running bitcoind that is actively responding on its port.  But it
      won't be responding immediately after we've started it (still doing
      startup operations).  If bitcoind was started and still running,
      then poll() will return None.  Any othe poll() return value means
      that the process terminated
      """
      if self.bitcoind==None:
         return False
      # Assume Bitcoind is running if manually started
      if self.bitcoind==False:
         return True
      else:
         if not self.bitcoind.poll()==None:
            LOGDEBUG('Bitcoind is no more')
            if self.btcOut==None:
               self.btcOut, self.btcErr = self.bitcoind.communicate()
               LOGWARN('bitcoind exited, bitcoind STDOUT:')
               for line in self.btcOut.split('\n'):
                  LOGWARN(line)
               LOGWARN('bitcoind exited, bitcoind STDERR:')
               for line in self.btcErr.split('\n'):
                  LOGWARN(line)
         return self.bitcoind.poll()==None

   #############################################################################
   def wasRunningBitcoind(self):
      return (not self.bitcoind==None)

   #############################################################################
   def bitcoindIsResponsive(self):
      return satoshiIsAvailable(self.bitconf['host'], self.bitconf['rpcport'])

   #############################################################################
   def getSDMState(self):
      """
      As for why I'm doing this:  it turns out that between "initializing"
      and "synchronizing", bitcoind temporarily stops responding entirely,
      which causes "not-available" to be the state.  I need to smooth that
      out because it wreaks havoc on the GUI which will switch to showing
      a nasty error.
      """

      state = self.getSDMStateLogic()
      self.circBufferState.append(state)
      self.circBufferTime.append(RightNow())
      if len(self.circBufferTime)>2 and \
         (self.circBufferTime[-1] - self.circBufferTime[1]) > 5:
         # Only remove the first element if we have at least 5s history
         self.circBufferState = self.circBufferState[1:]
         self.circBufferTime  = self.circBufferTime[1:]

      # Here's where we modify the output to smooth out the gap between
      # "initializing" and "synchronizing" (which is a couple seconds
      # of "not available").   "NotAvail" keeps getting added to the
      # buffer, but if it was "initializing" in the last 5 seconds,
      # we will keep "initializing"
      if state=='BitcoindNotAvailable':
         if 'BitcoindInitializing' in self.circBufferState:
            LOGWARN('Overriding not-available state. This should happen 0-5 times')
            return 'BitcoindInitializing'

      return state

   #############################################################################
   def getSDMStateLogic(self):

      if self.disabled:
         return 'BitcoindMgmtDisabled'

      if self.failedFindExe:
         return 'BitcoindExeMissing'

      if self.failedFindHome:
         return 'BitcoindHomeMissing'

      latestInfo = self.getTopBlockInfo()

      if self.bitcoind==None and latestInfo['error']=='Uninitialized':
         return 'BitcoindNeverStarted'

      if not self.isRunningBitcoind():
         # Not running at all:  either never started, or process terminated
         if not self.btcErr==None and len(self.btcErr)>0:
            errstr = self.btcErr.replace(',',' ').replace('.',' ').replace('!',' ')
            errPcs = set([a.lower() for a in errstr.split()])
            runPcs = set(['cannot','obtain','lock','already','running'])
            dbePcs = set(['database', 'recover','backup','except','wallet','dat'])
            if len(errPcs.intersection(runPcs))>=(len(runPcs)-1):
               return 'BitcoindAlreadyRunning'
            elif len(errPcs.intersection(dbePcs))>=(len(dbePcs)-1):
               return 'BitcoindDatabaseEnvError'
            else:
               return 'BitcoindUnknownCrash'
         else:
            return 'BitcoindNotAvailable'
      elif not self.bitcoindIsResponsive():
         # Running but not responsive... must still be initializing
         return 'BitcoindInitializing'
      else:
         # If it's responsive, get the top block and check
         # TODO: These conditionals are based on experimental results.  May
         #       not be accurate what the specific errors mean...
         if latestInfo['error']=='ValueError':
            return 'BitcoindWrongPassword'
         elif latestInfo['error']=='JsonRpcException':
            return 'BitcoindInitializing'
         elif latestInfo['error']=='SocketError':
            return 'BitcoindNotAvailable'

         if 'BitcoindReady' in self.circBufferState:
            # If ready, always ready
            return 'BitcoindReady'

         # If we get here, bitcoind is gave us a response.
         secSinceLastBlk = RightNow() - latestInfo['toptime']
         blkspersec = latestInfo['blkspersec']
         #print 'Blocks per 10 sec:', ('UNKNOWN' if blkspersec==-1 else blkspersec*10)
         if secSinceLastBlk > 4*HOUR or blkspersec==-1:
            return 'BitcoindSynchronizing'
         else:
            if blkspersec*20 > 2 and not 'BitcoindReady' in self.circBufferState:
               return 'BitcoindSynchronizing'
            else:
               return 'BitcoindReady'




   #############################################################################
   def createProxy(self, forceNew=False):
      if self.proxy==None or forceNew:
         LOGDEBUG('Creating proxy')
         usr,pas,hst,prt = [self.bitconf[k] for k in ['rpcuser','rpcpassword',\
                                                      'host', 'rpcport']]
         pstr = 'http://%s:%s@%s:%d' % (usr,pas,hst,prt)
         LOGINFO('Creating proxy in SDM: host=%s, port=%s', hst,prt)
         self.proxy = ServiceProxy(pstr)


   #############################################################################
   def __backgroundRequestTopBlock(self):
      self.createProxy()
      self.isMidQuery = True
      try:
         numblks = self.proxy.getinfo()['blocks']
         blkhash = self.proxy.getblockhash(numblks)
         toptime = self.proxy.getblock(blkhash)['time']
         #LOGDEBUG('RPC Call: numBlks=%d, toptime=%d', numblks, toptime)
         # Only overwrite once all outputs are retrieved
         self.lastTopBlockInfo['numblks'] = numblks
         self.lastTopBlockInfo['tophash'] = blkhash
         self.lastTopBlockInfo['toptime'] = toptime
         self.lastTopBlockInfo['error']   = None    # Holds error info

         if len(self.last20queries)==0 or \
               (RightNow()-self.last20queries[-1][0]) > 0.99:
            # This conditional guarantees last 20 queries spans at least 20s
            self.last20queries.append([RightNow(), numblks])
            self.last20queries = self.last20queries[-20:]
            t0,b0 = self.last20queries[0]
            t1,b1 = self.last20queries[-1]

            # Need at least 10s of data to give meaning answer
            if (t1-t0)<10:
               self.lastTopBlockInfo['blkspersec'] = -1
            else:
               self.lastTopBlockInfo['blkspersec'] = float(b1-b0)/float(t1-t0)

      except ValueError:
         # I believe this happens when you used the wrong password
         LOGEXCEPT('ValueError in bkgd req top blk')
         self.lastTopBlockInfo['error'] = 'ValueError'
      except authproxy.JSONRPCException:
         # This seems to happen when bitcoind is overwhelmed... not quite ready
         LOGDEBUG('generic jsonrpc exception')
         self.lastTopBlockInfo['error'] = 'JsonRpcException'
      except socket.error:
         # Connection isn't available... is bitcoind not running anymore?
         LOGDEBUG('generic socket error')
         self.lastTopBlockInfo['error'] = 'SocketError'
      except:
         LOGEXCEPT('generic error')
         self.lastTopBlockInfo['error'] = 'UnknownError'
         raise
      finally:
         self.isMidQuery = False


   #############################################################################
   def updateTopBlockInfo(self):
      """
      We want to get the top block information, but if bitcoind is rigorously
      downloading and verifying the blockchain, it can sometimes take 10s to
      to respond to JSON-RPC calls!  We must do it in the background...

      If it's already querying, no need to kick off another background request,
      just return the last value, which may be "stale" but we don't really
      care for this particular use-case
      """
      if not self.isRunningBitcoind():
         return

      if self.isMidQuery:
         return

      self.createProxy()
      self.queryThread = PyBackgroundThread(self.__backgroundRequestTopBlock)
      self.queryThread.start()


   #############################################################################
   def getTopBlockInfo(self):
      if self.isRunningBitcoind():
         self.updateTopBlockInfo()
         try:
            self.queryThread.join(0.001)  # In most cases, result should come in 1 ms
            # We return a copy so that the data is not changing as we use it
         except:
            pass

      return self.lastTopBlockInfo.copy()

   #############################################################################
   def callJSONIgnoreOwnership(self, func, *args):
      if self.proxy is None:
         raise self.BitcoindError, 'no node RPC connection'
      
      return self.proxy.__getattr__(func)(*args)

   #############################################################################
   def callJSON(self, func, *args):
      state = self.getSDMState()
      if not state in ('BitcoindReady', 'BitcoindSynchronizing'):
         LOGWARN('Called callJSON(%s, %s)', func, str(args))
         LOGWARN('Current SDM state: %s', state)
         raise self.BitcoindError, 'callJSON while %s'%state

      return self.proxy.__getattr__(func)(*args)


   #############################################################################
   def returnSDMInfo(self):
      sdminfo = {}
      for key,val in self.bitconf.iteritems():
         sdminfo['bitconf_%s'%key] = val

      for key,val in self.lastTopBlockInfo.iteritems():
         sdminfo['topblk_%s'%key] = val

      sdminfo['executable'] = self.executable
      sdminfo['isrunning']  = self.isRunningBitcoind()
      sdminfo['homedir']    = self.satoshiHome
      sdminfo['proxyinit']  = (not self.proxy==None)
      sdminfo['ismidquery'] = self.isMidQuery
      sdminfo['querycount'] = len(self.last20queries)

      return sdminfo

   #############################################################################
   def printSDMInfo(self):
      print '\nCurrent SDM State:'
      print '\t', 'SDM State Str'.ljust(20), ':', self.getSDMState()
      for key,value in self.returnSDMInfo().iteritems():
         print '\t', str(key).ljust(20), ':', str(value)