1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package service
import (
"time"
"github.com/armanlife/firefox-vpn-client/core/internal/logx"
"github.com/armanlife/firefox-vpn-client/core/internal/netstate"
"github.com/armanlife/firefox-vpn-client/core/internal/proxy"
)
// A connect the user asked for in an app (the button) that never reaches
// Mozilla is given up after connectWindow: the proxy turns off again, and
// the phone's VPN with its notification, instead of staying "Connecting"
// with no end. Why it failed stays in the status (StatusView.ConnectFailure)
// for the app to say under its button. A proxy that connected once stays on
// through later failures (a network that comes and goes), and reconnects by
// itself.
// connectWindow is how long a connect may take before it is given up. Tests
// shorten it.
var connectWindow = time.Minute
// Connect turns the proxy on for the user of an app, and gives the connect
// up after connectWindow unless it reaches Mozilla.
func (d *Daemon) Connect() {
attempt := make(chan struct{})
started := time.Now()
d.mu.Lock()
d.autoConnect = nil
d.connectAttempt, d.connectFailure = attempt, ""
d.mu.Unlock()
d.setOn(true)
go d.giveUpUnlessConnected(attempt, started)
}
// markConnected records a connection that reached Mozilla.
func (d *Daemon) markConnected() {
d.mu.Lock()
d.lastConnected = time.Now()
d.mu.Unlock()
}
// endConnect ends a connect attempt: the user acted.
func (d *Daemon) endConnect() {
d.mu.Lock()
d.connectAttempt = nil
d.mu.Unlock()
}
func (d *Daemon) giveUpUnlessConnected(attempt chan struct{}, started time.Time) {
select {
case <-d.stop:
return
case <-time.After(connectWindow):
}
d.mu.Lock()
current := d.connectAttempt == attempt && !d.off
connected := !d.lastConnected.Before(started)
if !current || connected {
if d.connectAttempt == attempt {
d.connectAttempt = nil
}
d.mu.Unlock()
return
}
d.mu.Unlock()
reason := d.connectFailureCode(started)
d.mu.Lock()
gaveUp := d.connectAttempt == attempt && !d.off && d.lastConnected.Before(started)
if gaveUp {
d.connectAttempt, d.connectFailure, d.off = nil, reason, true
}
d.mu.Unlock()
if gaveUp {
logx.Warn("no connection to Mozilla within %s of connecting; the proxy is off again reason=%s", connectWindow, reason)
d.RequestRestart()
}
}
// connectFailureCode says why a connect since started did not reach
// Mozilla: the last failure's code, else what the internet check finds.
func (d *Daemon) connectFailureCode(started time.Time) string {
if engine := d.currentEngine(); engine != nil {
if at, code, err := engine.wakeFailure(); err != nil && !at.Before(started) {
return code
}
}
d.mu.Lock()
test := d.lastTest
d.mu.Unlock()
if test != nil && !test.OK && !test.At.Before(started) && test.ErrorCode != "" {
return test.ErrorCode
}
if netstate.ReportedDown() {
return proxy.FailureNoNetwork
}
return checkedFailure(proxy.FailureOther)
}
// RetryNow drops the waits that failures left before the next attempt, for
// a user who is looking (the app came to the front): the next connection
// tries at once. Failures that retrying cannot fix (a spent quota, an
// account that must sign in again) keep theirs.
func (d *Daemon) RetryNow() {
engine := d.currentEngine()
if engine == nil {
return
}
engine.retryNow()
if controller := engine.currentController(); controller != nil {
controller.RetryNow()
}
}