github.com/arieschain/arieschain@v0.0.0-20191023063405-37c074544356/accounts/usbwallet/hub.go (about)

     1  package usbwallet
     2  
     3  import (
     4  	"errors"
     5  	"runtime"
     6  	"sync"
     7  	"time"
     8  
     9  	"github.com/quickchainproject/quickchain/accounts"
    10  	"github.com/quickchainproject/quickchain/event"
    11  	"github.com/quickchainproject/quickchain/log"
    12  	"github.com/karalabe/hid"
    13  )
    14  
    15  // LedgerScheme is the protocol scheme prefixing account and wallet URLs.
    16  const LedgerScheme = "ledger"
    17  
    18  // TrezorScheme is the protocol scheme prefixing account and wallet URLs.
    19  const TrezorScheme = "trezor"
    20  
    21  // refreshCycle is the maximum time between wallet refreshes (if USB hotplug
    22  // notifications don't work).
    23  const refreshCycle = time.Second
    24  
    25  // refreshThrottling is the minimum time between wallet refreshes to avoid USB
    26  // trashing.
    27  const refreshThrottling = 500 * time.Millisecond
    28  
    29  // Hub is a accounts.Backend that can find and handle generic USB hardware wallets.
    30  type Hub struct {
    31  	scheme     string                  // Protocol scheme prefixing account and wallet URLs.
    32  	vendorID   uint16                  // USB vendor identifier used for device discovery
    33  	productIDs []uint16                // USB product identifiers used for device discovery
    34  	usageID    uint16                  // USB usage page identifier used for macOS device discovery
    35  	endpointID int                     // USB endpoint identifier used for non-macOS device discovery
    36  	makeDriver func(log.Logger) driver // Factory method to construct a vendor specific driver
    37  
    38  	refreshed   time.Time               // Time instance when the list of wallets was last refreshed
    39  	wallets     []accounts.Wallet       // List of USB wallet devices currently tracking
    40  	updateFeed  event.Feed              // Event feed to notify wallet additions/removals
    41  	updateScope event.SubscriptionScope // Subscription scope tracking current live listeners
    42  	updating    bool                    // Whether the event notification loop is running
    43  
    44  	quit chan chan error
    45  
    46  	stateLock sync.RWMutex // Protects the internals of the hub from racey access
    47  
    48  	// TODO(karalabe): remove if hotplug lands on Windows
    49  	commsPend int        // Number of operations blocking enumeration
    50  	commsLock sync.Mutex // Lock protecting the pending counter and enumeration
    51  }
    52  
    53  // NewLedgerHub creates a new hardware wallet manager for Ledger devices.
    54  func NewLedgerHub() (*Hub, error) {
    55  	return newHub(LedgerScheme, 0x2c97, []uint16{0x0000 /* Ledger Blue */, 0x0001 /* Ledger Nano S */}, 0xffa0, 0, newLedgerDriver)
    56  }
    57  
    58  // NewTrezorHub creates a new hardware wallet manager for Trezor devices.
    59  func NewTrezorHub() (*Hub, error) {
    60  	return newHub(TrezorScheme, 0x534c, []uint16{0x0001 /* Trezor 1 */}, 0xff00, 0, newTrezorDriver)
    61  }
    62  
    63  // newHub creates a new hardware wallet manager for generic USB devices.
    64  func newHub(scheme string, vendorID uint16, productIDs []uint16, usageID uint16, endpointID int, makeDriver func(log.Logger) driver) (*Hub, error) {
    65  	if !hid.Supported() {
    66  		return nil, errors.New("unsupported platform")
    67  	}
    68  	hub := &Hub{
    69  		scheme:     scheme,
    70  		vendorID:   vendorID,
    71  		productIDs: productIDs,
    72  		usageID:    usageID,
    73  		endpointID: endpointID,
    74  		makeDriver: makeDriver,
    75  		quit:       make(chan chan error),
    76  	}
    77  	hub.refreshWallets()
    78  	return hub, nil
    79  }
    80  
    81  // Wallets implements accounts.Backend, returning all the currently tracked USB
    82  // devices that appear to be hardware wallets.
    83  func (hub *Hub) Wallets() []accounts.Wallet {
    84  	// Make sure the list of wallets is up to date
    85  	hub.refreshWallets()
    86  
    87  	hub.stateLock.RLock()
    88  	defer hub.stateLock.RUnlock()
    89  
    90  	cpy := make([]accounts.Wallet, len(hub.wallets))
    91  	copy(cpy, hub.wallets)
    92  	return cpy
    93  }
    94  
    95  // refreshWallets scans the USB devices attached to the machine and updates the
    96  // list of wallets based on the found devices.
    97  func (hub *Hub) refreshWallets() {
    98  	// Don't scan the USB like crazy it the user fetches wallets in a loop
    99  	hub.stateLock.RLock()
   100  	elapsed := time.Since(hub.refreshed)
   101  	hub.stateLock.RUnlock()
   102  
   103  	if elapsed < refreshThrottling {
   104  		return
   105  	}
   106  	// Retrieve the current list of USB wallet devices
   107  	var devices []hid.DeviceInfo
   108  
   109  	if runtime.GOOS == "linux" {
   110  		// hidapi on Linux opens the device during enumeration to retrieve some infos,
   111  		// breaking the Ledger protocol if that is waiting for user confirmation. This
   112  		// is a bug acknowledged at Ledger, but it won't be fixed on old devices so we
   113  		// need to prevent concurrent comms ourselves. The more elegant solution would
   114  		// be to ditch enumeration in favor of hotplug events, but that don't work yet
   115  		// on Windows so if we need to hack it anyway, this is more elegant for now.
   116  		hub.commsLock.Lock()
   117  		if hub.commsPend > 0 { // A confirmation is pending, don't refresh
   118  			hub.commsLock.Unlock()
   119  			return
   120  		}
   121  	}
   122  	for _, info := range hid.Enumerate(hub.vendorID, 0) {
   123  		for _, id := range hub.productIDs {
   124  			if info.ProductID == id && (info.UsagePage == hub.usageID || info.Interface == hub.endpointID) {
   125  				devices = append(devices, info)
   126  				break
   127  			}
   128  		}
   129  	}
   130  	if runtime.GOOS == "linux" {
   131  		// See rationale before the enumeration why this is needed and only on Linux.
   132  		hub.commsLock.Unlock()
   133  	}
   134  	// Transform the current list of wallets into the new one
   135  	hub.stateLock.Lock()
   136  
   137  	wallets := make([]accounts.Wallet, 0, len(devices))
   138  	events := []accounts.WalletEvent{}
   139  
   140  	for _, device := range devices {
   141  		url := accounts.URL{Scheme: hub.scheme, Path: device.Path}
   142  
   143  		// Drop wallets in front of the next device or those that failed for some reason
   144  		for len(hub.wallets) > 0 {
   145  			// Abort if we're past the current device and found an operational one
   146  			_, failure := hub.wallets[0].Status()
   147  			if hub.wallets[0].URL().Cmp(url) >= 0 || failure == nil {
   148  				break
   149  			}
   150  			// Drop the stale and failed devices
   151  			events = append(events, accounts.WalletEvent{Wallet: hub.wallets[0], Kind: accounts.WalletDropped})
   152  			hub.wallets = hub.wallets[1:]
   153  		}
   154  		// If there are no more wallets or the device is before the next, wrap new wallet
   155  		if len(hub.wallets) == 0 || hub.wallets[0].URL().Cmp(url) > 0 {
   156  			logger := log.New("url", url)
   157  			wallet := &wallet{hub: hub, driver: hub.makeDriver(logger), url: &url, info: device, log: logger}
   158  
   159  			events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletArrived})
   160  			wallets = append(wallets, wallet)
   161  			continue
   162  		}
   163  		// If the device is the same as the first wallet, keep it
   164  		if hub.wallets[0].URL().Cmp(url) == 0 {
   165  			wallets = append(wallets, hub.wallets[0])
   166  			hub.wallets = hub.wallets[1:]
   167  			continue
   168  		}
   169  	}
   170  	// Drop any leftover wallets and set the new batch
   171  	for _, wallet := range hub.wallets {
   172  		events = append(events, accounts.WalletEvent{Wallet: wallet, Kind: accounts.WalletDropped})
   173  	}
   174  	hub.refreshed = time.Now()
   175  	hub.wallets = wallets
   176  	hub.stateLock.Unlock()
   177  
   178  	// Fire all wallet events and return
   179  	for _, event := range events {
   180  		hub.updateFeed.Send(event)
   181  	}
   182  }
   183  
   184  // Subscribe implements accounts.Backend, creating an async subscription to
   185  // receive notifications on the addition or removal of USB wallets.
   186  func (hub *Hub) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription {
   187  	// We need the mutex to reliably start/stop the update loop
   188  	hub.stateLock.Lock()
   189  	defer hub.stateLock.Unlock()
   190  
   191  	// Subscribe the caller and track the subscriber count
   192  	sub := hub.updateScope.Track(hub.updateFeed.Subscribe(sink))
   193  
   194  	// Subscribers require an active notification loop, start it
   195  	if !hub.updating {
   196  		hub.updating = true
   197  		go hub.updater()
   198  	}
   199  	return sub
   200  }
   201  
   202  // updater is responsible for maintaining an up-to-date list of wallets managed
   203  // by the USB hub, and for firing wallet addition/removal events.
   204  func (hub *Hub) updater() {
   205  	for {
   206  		// TODO: Wait for a USB hotplug event (not supported yet) or a refresh timeout
   207  		// <-hub.changes
   208  		time.Sleep(refreshCycle)
   209  
   210  		// Run the wallet refresher
   211  		hub.refreshWallets()
   212  
   213  		// If all our subscribers left, stop the updater
   214  		hub.stateLock.Lock()
   215  		if hub.updateScope.Count() == 0 {
   216  			hub.updating = false
   217  			hub.stateLock.Unlock()
   218  			return
   219  		}
   220  		hub.stateLock.Unlock()
   221  	}
   222  }