Skip to content

Commit 0f184d3

Browse files
authored
Merge pull request #15526 from karalabe/accounts-new
accounts: fix two races in the account manager
2 parents f5091e5 + 6810674 commit 0f184d3

File tree

3 files changed

+129
-96
lines changed

3 files changed

+129
-96
lines changed

accounts/keystore/account_cache.go

Lines changed: 22 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"bufio"
2121
"encoding/json"
2222
"fmt"
23-
"io/ioutil"
2423
"os"
2524
"path/filepath"
2625
"sort"
@@ -75,13 +74,6 @@ type accountCache struct {
7574
fileC fileCache
7675
}
7776

78-
// fileCache is a cache of files seen during scan of keystore
79-
type fileCache struct {
80-
all *set.SetNonTS // list of all files
81-
mtime time.Time // latest mtime seen
82-
mu sync.RWMutex
83-
}
84-
8577
func newAccountCache(keydir string) (*accountCache, chan struct{}) {
8678
ac := &accountCache{
8779
keydir: keydir,
@@ -236,66 +228,22 @@ func (ac *accountCache) close() {
236228
ac.mu.Unlock()
237229
}
238230

239-
// scanFiles performs a new scan on the given directory, compares against the already
240-
// cached filenames, and returns file sets: new, missing , modified
241-
func (fc *fileCache) scanFiles(keyDir string) (set.Interface, set.Interface, set.Interface, error) {
242-
t0 := time.Now()
243-
files, err := ioutil.ReadDir(keyDir)
244-
t1 := time.Now()
245-
if err != nil {
246-
return nil, nil, nil, err
247-
}
248-
fc.mu.RLock()
249-
prevMtime := fc.mtime
250-
fc.mu.RUnlock()
251-
252-
filesNow := set.NewNonTS()
253-
moddedFiles := set.NewNonTS()
254-
var newMtime time.Time
255-
for _, fi := range files {
256-
modTime := fi.ModTime()
257-
path := filepath.Join(keyDir, fi.Name())
258-
if skipKeyFile(fi) {
259-
log.Trace("Ignoring file on account scan", "path", path)
260-
continue
261-
}
262-
filesNow.Add(path)
263-
if modTime.After(prevMtime) {
264-
moddedFiles.Add(path)
265-
}
266-
if modTime.After(newMtime) {
267-
newMtime = modTime
268-
}
269-
}
270-
t2 := time.Now()
271-
272-
fc.mu.Lock()
273-
// Missing = previous - current
274-
missing := set.Difference(fc.all, filesNow)
275-
// New = current - previous
276-
newFiles := set.Difference(filesNow, fc.all)
277-
// Modified = modified - new
278-
modified := set.Difference(moddedFiles, newFiles)
279-
fc.all = filesNow
280-
fc.mtime = newMtime
281-
fc.mu.Unlock()
282-
t3 := time.Now()
283-
log.Debug("FS scan times", "list", t1.Sub(t0), "set", t2.Sub(t1), "diff", t3.Sub(t2))
284-
return newFiles, missing, modified, nil
285-
}
286-
287231
// scanAccounts checks if any changes have occurred on the filesystem, and
288232
// updates the account cache accordingly
289233
func (ac *accountCache) scanAccounts() error {
290-
newFiles, missingFiles, modified, err := ac.fileC.scanFiles(ac.keydir)
291-
t1 := time.Now()
234+
// Scan the entire folder metadata for file changes
235+
creates, deletes, updates, err := ac.fileC.scan(ac.keydir)
292236
if err != nil {
293237
log.Debug("Failed to reload keystore contents", "err", err)
294238
return err
295239
}
240+
if creates.Size() == 0 && deletes.Size() == 0 && updates.Size() == 0 {
241+
return nil
242+
}
243+
// Create a helper method to scan the contents of the key files
296244
var (
297-
buf = new(bufio.Reader)
298-
keyJSON struct {
245+
buf = new(bufio.Reader)
246+
key struct {
299247
Address string `json:"address"`
300248
}
301249
)
@@ -308,9 +256,9 @@ func (ac *accountCache) scanAccounts() error {
308256
defer fd.Close()
309257
buf.Reset(fd)
310258
// Parse the address.
311-
keyJSON.Address = ""
312-
err = json.NewDecoder(buf).Decode(&keyJSON)
313-
addr := common.HexToAddress(keyJSON.Address)
259+
key.Address = ""
260+
err = json.NewDecoder(buf).Decode(&key)
261+
addr := common.HexToAddress(key.Address)
314262
switch {
315263
case err != nil:
316264
log.Debug("Failed to decode keystore key", "path", path, "err", err)
@@ -321,47 +269,30 @@ func (ac *accountCache) scanAccounts() error {
321269
}
322270
return nil
323271
}
272+
// Process all the file diffs
273+
start := time.Now()
324274

325-
for _, p := range newFiles.List() {
326-
path, _ := p.(string)
327-
a := readAccount(path)
328-
if a != nil {
275+
for _, p := range creates.List() {
276+
if a := readAccount(p.(string)); a != nil {
329277
ac.add(*a)
330278
}
331279
}
332-
for _, p := range missingFiles.List() {
333-
path, _ := p.(string)
334-
ac.deleteByFile(path)
280+
for _, p := range deletes.List() {
281+
ac.deleteByFile(p.(string))
335282
}
336-
337-
for _, p := range modified.List() {
338-
path, _ := p.(string)
339-
a := readAccount(path)
283+
for _, p := range updates.List() {
284+
path := p.(string)
340285
ac.deleteByFile(path)
341-
if a != nil {
286+
if a := readAccount(path); a != nil {
342287
ac.add(*a)
343288
}
344289
}
345-
346-
t2 := time.Now()
290+
end := time.Now()
347291

348292
select {
349293
case ac.notify <- struct{}{}:
350294
default:
351295
}
352-
log.Trace("Handled keystore changes", "time", t2.Sub(t1))
353-
296+
log.Trace("Handled keystore changes", "time", end.Sub(start))
354297
return nil
355298
}
356-
357-
func skipKeyFile(fi os.FileInfo) bool {
358-
// Skip editor backups and UNIX-style hidden files.
359-
if strings.HasSuffix(fi.Name(), "~") || strings.HasPrefix(fi.Name(), ".") {
360-
return true
361-
}
362-
// Skip misc special files, directories (yes, symlinks too).
363-
if fi.IsDir() || fi.Mode()&os.ModeType != 0 {
364-
return true
365-
}
366-
return false
367-
}

accounts/keystore/file_cache.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright 2017 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package keystore
18+
19+
import (
20+
"io/ioutil"
21+
"os"
22+
"path/filepath"
23+
"strings"
24+
"sync"
25+
"time"
26+
27+
"github.com/ethereum/go-ethereum/log"
28+
set "gopkg.in/fatih/set.v0"
29+
)
30+
31+
// fileCache is a cache of files seen during scan of keystore.
32+
type fileCache struct {
33+
all *set.SetNonTS // Set of all files from the keystore folder
34+
lastMod time.Time // Last time instance when a file was modified
35+
mu sync.RWMutex
36+
}
37+
38+
// scan performs a new scan on the given directory, compares against the already
39+
// cached filenames, and returns file sets: creates, deletes, updates.
40+
func (fc *fileCache) scan(keyDir string) (set.Interface, set.Interface, set.Interface, error) {
41+
t0 := time.Now()
42+
43+
// List all the failes from the keystore folder
44+
files, err := ioutil.ReadDir(keyDir)
45+
if err != nil {
46+
return nil, nil, nil, err
47+
}
48+
t1 := time.Now()
49+
50+
fc.mu.Lock()
51+
defer fc.mu.Unlock()
52+
53+
// Iterate all the files and gather their metadata
54+
all := set.NewNonTS()
55+
mods := set.NewNonTS()
56+
57+
var newLastMod time.Time
58+
for _, fi := range files {
59+
// Skip any non-key files from the folder
60+
path := filepath.Join(keyDir, fi.Name())
61+
if skipKeyFile(fi) {
62+
log.Trace("Ignoring file on account scan", "path", path)
63+
continue
64+
}
65+
// Gather the set of all and fresly modified files
66+
all.Add(path)
67+
68+
modified := fi.ModTime()
69+
if modified.After(fc.lastMod) {
70+
mods.Add(path)
71+
}
72+
if modified.After(newLastMod) {
73+
newLastMod = modified
74+
}
75+
}
76+
t2 := time.Now()
77+
78+
// Update the tracked files and return the three sets
79+
deletes := set.Difference(fc.all, all) // Deletes = previous - current
80+
creates := set.Difference(all, fc.all) // Creates = current - previous
81+
updates := set.Difference(mods, creates) // Updates = modified - creates
82+
83+
fc.all, fc.lastMod = all, newLastMod
84+
t3 := time.Now()
85+
86+
// Report on the scanning stats and return
87+
log.Debug("FS scan times", "list", t1.Sub(t0), "set", t2.Sub(t1), "diff", t3.Sub(t2))
88+
return creates, deletes, updates, nil
89+
}
90+
91+
// skipKeyFile ignores editor backups, hidden files and folders/symlinks.
92+
func skipKeyFile(fi os.FileInfo) bool {
93+
// Skip editor backups and UNIX-style hidden files.
94+
if strings.HasSuffix(fi.Name(), "~") || strings.HasPrefix(fi.Name(), ".") {
95+
return true
96+
}
97+
// Skip misc special files, directories (yes, symlinks too).
98+
if fi.IsDir() || fi.Mode()&os.ModeType != 0 {
99+
return true
100+
}
101+
return false
102+
}

accounts/manager.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,18 @@ type Manager struct {
4141
// NewManager creates a generic account manager to sign transaction via various
4242
// supported backends.
4343
func NewManager(backends ...Backend) *Manager {
44+
// Retrieve the initial list of wallets from the backends and sort by URL
45+
var wallets []Wallet
46+
for _, backend := range backends {
47+
wallets = merge(wallets, backend.Wallets()...)
48+
}
4449
// Subscribe to wallet notifications from all backends
4550
updates := make(chan WalletEvent, 4*len(backends))
4651

4752
subs := make([]event.Subscription, len(backends))
4853
for i, backend := range backends {
4954
subs[i] = backend.Subscribe(updates)
5055
}
51-
// Retrieve the initial list of wallets from the backends and sort by URL
52-
var wallets []Wallet
53-
for _, backend := range backends {
54-
wallets = merge(wallets, backend.Wallets()...)
55-
}
5656
// Assemble the account manager and return
5757
am := &Manager{
5858
backends: make(map[reflect.Type][]Backend),

0 commit comments

Comments
 (0)