Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 95 additions & 2 deletions client/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -4947,13 +4947,104 @@ func (c *Core) upgradeV0CredsToV1(appPW []byte, creds db.PrimaryCredentials) (en
return newInnerCrypter, &creds, nil
}

// loginProgressReportInterval is how often a login phase reports the items
// it is still waiting on via LoginNote.
const loginProgressReportInterval = 5 * time.Second

// loginProgress emits LoginNotes as the items of a login phase complete, and
// periodically names the stragglers, so that the UI can show what a slow
// login is waiting on. Mark items with start/finish. stop when the phase is
// over.
type loginProgress struct {
c *Core
phase string
total int
done uint32
quit chan struct{}

mtx sync.Mutex
pending map[string]struct{}
}

// trackLoginProgress creates a loginProgress for a login phase with total
// expected items and starts the straggler reporter.
func (c *Core) trackLoginProgress(phase string, total int) *loginProgress {
p := &loginProgress{
c: c,
phase: phase,
total: total,
quit: make(chan struct{}),
pending: make(map[string]struct{}, total),
}
go func() {
ticker := time.NewTicker(loginProgressReportInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if waitingOn := p.waitingOn(); waitingOn != "" {
c.notify(newLoginNote(fmt.Sprintf("%s waiting on %s", p.phase, waitingOn)))
}
case <-p.quit:
return
case <-c.ctx.Done():
return
}
}
}()
return p
}

// start marks an item as pending.
func (p *loginProgress) start(name string) {
p.mtx.Lock()
p.pending[name] = struct{}{}
p.mtx.Unlock()
}

// finish marks an item as done and emits an updated progress note.
func (p *loginProgress) finish(name string) {
p.mtx.Lock()
delete(p.pending, name)
p.mtx.Unlock()
n := atomic.AddUint32(&p.done, 1)
p.c.notify(newLoginNote(fmt.Sprintf("%s (%d/%d)", p.phase, n, p.total)))
}

// stop ends the straggler reporting.
func (p *loginProgress) stop() {
close(p.quit)
}

// waitingOn returns a comma-separated list of the pending items.
func (p *loginProgress) waitingOn() string {
p.mtx.Lock()
defer p.mtx.Unlock()
if len(p.pending) == 0 {
return ""
}
names := make([]string, 0, len(p.pending))
for name := range p.pending {
names = append(names, name)
}
sort.Strings(names)
return strings.Join(names, ", ")
}

// connectWallets attempts to connect to and retrieve balance from all known
// wallets. This should be done only ONCE on Login.
func (c *Core) connectWallets(crypter encrypt.Crypter) {
var wg sync.WaitGroup
var connectCount uint32
wallets := c.xcWallets()
walletCount := len(wallets)
progress := c.trackLoginProgress("Connecting wallets...", walletCount)
defer progress.stop()
connectWallet := func(wallet *xcWallet) {
defer wg.Done()
symbol := strings.ToUpper(wallet.Symbol)
progress.start(symbol)
defer progress.finish(symbol)
// Return early if wallet is disabled.
if wallet.isDisabled() {
return
Expand Down Expand Up @@ -5019,8 +5110,6 @@ func (c *Core) connectWallets(crypter encrypt.Crypter) {
}
atomic.AddUint32(&connectCount, 1)
}
wallets := c.xcWallets()
walletCount := len(wallets)
var tokenWallets []*xcWallet

for _, wallet := range wallets {
Expand Down Expand Up @@ -5457,10 +5546,14 @@ func (c *Core) MaxSell(host string, base, quote uint32) (*MaxOrderEstimate, erro
func (c *Core) initializeDEXConnections(crypter encrypt.Crypter) {
var wg sync.WaitGroup
conns := c.dexConnections()
progress := c.trackLoginProgress("Connecting to DEX servers...", len(conns))
defer progress.stop()
for _, dc := range conns {
wg.Add(1)
go func(dc *dexConnection) {
defer wg.Done()
progress.start(dc.acct.host)
defer progress.finish(dc.acct.host)
c.initializeDEXConnection(dc, crypter)
}(dc)
}
Expand Down
6 changes: 6 additions & 0 deletions client/webserver/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1570,6 +1570,12 @@ func (s *WebServer) apiPreOrder(w http.ResponseWriter, r *http.Request) {
// apiActuallyLogin logs the user in. login form private data is expected to be
// cleared by the caller.
func (s *WebServer) actuallyLogin(w http.ResponseWriter, r *http.Request, login *loginForm) error {
// The first login after startup connects wallets and DEX servers, which
// can legitimately outlast the server's WriteTimeout. The response
// carries the auth cookie, so it must not be cut off by the deadline.
if err := http.NewResponseController(w).SetWriteDeadline(time.Time{}); err != nil {
log.Warnf("Unable to clear the login response write deadline: %v", err)
}
// Only allow login from the onion address when a companion app is
// actively paired. This prevents an unpaired companion app from
// re-authenticating with saved credentials.
Expand Down
17 changes: 17 additions & 0 deletions client/webserver/site/src/js/forms.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Doc, { Animation } from './doc'
import { postJSON } from './http'
import ws from './ws'
import State from './state'
import * as intl from './locales'
import { Wave } from './charts'
Expand Down Expand Up @@ -1787,7 +1788,23 @@ export class LoginForm {
return
}
const loaded = app().loading(this.form)
// The login request can run long while the backend connects wallets and
// DEX servers. Status updates arrive as login notes over the websocket,
// so if the websocket drops while the request is pending, flag the
// displayed status as possibly stale until the connection returns.
const staleHint = ' (connection to app lost, reconnecting...)'
const loaderMsg = () => Doc.idel(this.form, 'loaderMsg')
ws.registerRoute('close', () => {
const msg = loaderMsg()
if (msg && !msg.textContent?.includes(staleHint)) msg.textContent += staleHint
})
ws.registerRoute('open', () => {
const msg = loaderMsg()
if (msg?.textContent) msg.textContent = msg.textContent.replace(staleHint, '')
})
const res = await postJSON('/api/login', { pass: pw })
ws.deregisterRoute('close')
ws.deregisterRoute('open')
loaded()
page.pw.value = ''
if (!app().checkResponse(res)) {
Expand Down
Loading