ipn/ipnlocal: do controlclient.Shutdown in a different goroutine

We do not need to wait for it to complete. And we might have to
call Shutdown from callback from the controlclient which might
already be holding a lock that Shutdown requires.

Updates #713

Signed-off-by: Maisem Ali <maisem@tailscale.com>
pull/6315/head
Maisem Ali 2 years ago committed by Maisem Ali
parent e0cadc5496
commit fe81ee62d7

@ -1141,9 +1141,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
// into sync with the minimal changes. But that's not how it // into sync with the minimal changes. But that's not how it
// is right now, which is a sign that the code is still too // is right now, which is a sign that the code is still too
// complicated. // complicated.
b.mu.Unlock() b.resetControlClientLockedAsync()
b.cc.Shutdown()
b.mu.Lock()
} }
httpTestClient := b.httpTestClient httpTestClient := b.httpTestClient
@ -1956,7 +1954,7 @@ func (b *LocalBackend) InServerMode() bool {
} }
// Login implements Backend. // Login implements Backend.
// As of 2022-02-17, this is only exists for tests. // As of 2022-11-15, this is only exists for Android.
func (b *LocalBackend) Login(token *tailcfg.Oauth2Token) { func (b *LocalBackend) Login(token *tailcfg.Oauth2Token) {
b.mu.Lock() b.mu.Lock()
b.assertClientLocked() b.assertClientLocked()
@ -3128,7 +3126,6 @@ func (b *LocalBackend) hasNodeKey() bool {
// its internal state. // its internal state.
func (b *LocalBackend) nextState() ipn.State { func (b *LocalBackend) nextState() ipn.State {
b.mu.Lock() b.mu.Lock()
b.assertClientLocked()
var ( var (
cc = b.cc cc = b.cc
netMap = b.netMap netMap = b.netMap
@ -3150,7 +3147,7 @@ func (b *LocalBackend) nextState() ipn.State {
case !wantRunning && !loggedOut && !blocked && b.hasNodeKey(): case !wantRunning && !loggedOut && !blocked && b.hasNodeKey():
return ipn.Stopped return ipn.Stopped
case netMap == nil: case netMap == nil:
if cc.AuthCantContinue() || loggedOut { if (cc != nil && cc.AuthCantContinue()) || loggedOut {
// Auth was interrupted or waiting for URL visit, // Auth was interrupted or waiting for URL visit,
// so it won't proceed without human help. // so it won't proceed without human help.
return ipn.NeedsLogin return ipn.NeedsLogin
@ -3238,6 +3235,18 @@ func (b *LocalBackend) requestEngineStatusAndWait() {
b.statusLock.Unlock() b.statusLock.Unlock()
} }
// resetControlClientLockedAsync sets b.cc to nil, and starts a
// goroutine to Shutdown the old client. It does not wait for the
// shutdown to complete.
func (b *LocalBackend) resetControlClientLockedAsync() {
if b.cc == nil {
return
}
go b.cc.Shutdown()
b.cc = nil
b.ccAuto = nil
}
// ResetForClientDisconnect resets the backend for GUI clients running // ResetForClientDisconnect resets the backend for GUI clients running
// in interactive (non-headless) mode. This is currently used only by // in interactive (non-headless) mode. This is currently used only by
// Windows. This causes all state to be cleared, lest an unrelated user // Windows. This causes all state to be cleared, lest an unrelated user
@ -3249,11 +3258,7 @@ func (b *LocalBackend) ResetForClientDisconnect() {
b.mu.Lock() b.mu.Lock()
defer b.mu.Unlock() defer b.mu.Unlock()
b.logf("LocalBackend.ResetForClientDisconnect") b.logf("LocalBackend.ResetForClientDisconnect")
b.resetControlClientLockedAsync()
if b.cc != nil {
go b.cc.Shutdown()
b.cc = nil
}
b.setNetMapLocked(nil) b.setNetMapLocked(nil)
b.pm.Reset() b.pm.Reset()
b.keyExpired = false b.keyExpired = false

@ -91,30 +91,43 @@ func (nt *notifyThrottler) drain(count int) []ipn.Notify {
// in the controlclient.Client, so by controlling it, we can check that // in the controlclient.Client, so by controlling it, we can check that
// the state machine works as expected. // the state machine works as expected.
type mockControl struct { type mockControl struct {
tb testing.TB tb testing.TB
opts controlclient.Options logf logger.Logf
logfActual logger.Logf opts controlclient.Options
statusFunc func(controlclient.Status) paused atomic.Bool
mu sync.Mutex mu sync.Mutex
machineKey key.MachinePrivate
persist *persist.Persist
calls []string calls []string
authBlocked bool authBlocked bool
persist *persist.Persist shutdown chan struct{}
machineKey key.MachinePrivate
} }
func newMockControl(tb testing.TB) *mockControl { func newClient(tb testing.TB, opts controlclient.Options) *mockControl {
return &mockControl{ return &mockControl{
tb: tb, tb: tb,
authBlocked: true, authBlocked: true,
logf: opts.Logf,
opts: opts,
shutdown: make(chan struct{}),
persist: opts.Persist.Clone(),
} }
} }
func (cc *mockControl) logf(format string, args ...any) { func (cc *mockControl) assertShutdown(wasPaused bool) {
if cc.logfActual == nil { cc.tb.Helper()
return select {
case <-cc.shutdown:
// ok
case <-time.After(500 * time.Millisecond):
cc.tb.Fatalf("timed out waiting for shutdown")
}
if wasPaused {
cc.assertCalls("unpause", "Shutdown")
} else {
cc.assertCalls("Shutdown")
} }
cc.logfActual(format, args...)
} }
func (cc *mockControl) populateKeys() (newKeys bool) { func (cc *mockControl) populateKeys() (newKeys bool) {
@ -143,7 +156,12 @@ func (cc *mockControl) populateKeys() (newKeys bool) {
// send publishes a controlclient.Status notification upstream. // send publishes a controlclient.Status notification upstream.
// (In our tests here, upstream is the ipnlocal.Local instance.) // (In our tests here, upstream is the ipnlocal.Local instance.)
func (cc *mockControl) send(err error, url string, loginFinished bool, nm *netmap.NetworkMap) { func (cc *mockControl) send(err error, url string, loginFinished bool, nm *netmap.NetworkMap) {
if cc.statusFunc != nil { if loginFinished {
cc.mu.Lock()
cc.authBlocked = false
cc.mu.Unlock()
}
if cc.opts.Status != nil {
pv := cc.persist.View() pv := cc.persist.View()
s := controlclient.Status{ s := controlclient.Status{
URL: url, URL: url,
@ -156,7 +174,7 @@ func (cc *mockControl) send(err error, url string, loginFinished bool, nm *netma
} else if url == "" && err == nil && nm == nil { } else if url == "" && err == nil && nm == nil {
s.LogoutFinished = &empty.Message{} s.LogoutFinished = &empty.Message{}
} }
cc.statusFunc(s) cc.opts.Status(s)
} }
} }
@ -178,34 +196,16 @@ func (cc *mockControl) assertCalls(want ...string) {
cc.calls = nil cc.calls = nil
} }
// setAuthBlocked changes the return value of AuthCantContinue.
// Auth is blocked if you haven't called Login, the control server hasn't
// provided an auth URL, or it has provided an auth URL and you haven't
// visited it yet.
func (cc *mockControl) setAuthBlocked(blocked bool) {
cc.mu.Lock()
defer cc.mu.Unlock()
cc.authBlocked = blocked
}
// Shutdown disconnects the client. // Shutdown disconnects the client.
//
// Note that in a normal controlclient, Shutdown would be the last thing you
// do before discarding the object. In this mock, we don't actually discard
// the object, but if you see a call to Shutdown, you should always see a
// call to New right after it, if the object continues to be used.
// (Note that "New" is the ccGen function here; it means ipn.Backend wanted
// to create an entirely new controlclient.)
func (cc *mockControl) Shutdown() { func (cc *mockControl) Shutdown() {
cc.logf("Shutdown") cc.logf("Shutdown")
cc.called("Shutdown") cc.called("Shutdown")
close(cc.shutdown)
} }
// Login starts a login process. // Login starts a login process. Note that in this mock, we don't automatically
// Note that in this mock, we don't automatically generate notifications // generate notifications about the progress of the login operation. You have to
// about the progress of the login operation. You have to call setAuthBlocked() // call send() as required by the test.
// and send() as required by the test.
func (cc *mockControl) Login(t *tailcfg.Oauth2Token, flags controlclient.LoginFlags) { func (cc *mockControl) Login(t *tailcfg.Oauth2Token, flags controlclient.LoginFlags) {
cc.logf("Login token=%v flags=%v", t, flags) cc.logf("Login token=%v flags=%v", t, flags)
cc.called("Login") cc.called("Login")
@ -213,7 +213,9 @@ func (cc *mockControl) Login(t *tailcfg.Oauth2Token, flags controlclient.LoginFl
interact := (flags & controlclient.LoginInteractive) != 0 interact := (flags & controlclient.LoginInteractive) != 0
cc.logf("Login: interact=%v newKeys=%v", interact, newKeys) cc.logf("Login: interact=%v newKeys=%v", interact, newKeys)
cc.setAuthBlocked(interact || newKeys) cc.mu.Lock()
defer cc.mu.Unlock()
cc.authBlocked = interact || newKeys
} }
func (cc *mockControl) StartLogout() { func (cc *mockControl) StartLogout() {
@ -228,6 +230,10 @@ func (cc *mockControl) Logout(ctx context.Context) error {
} }
func (cc *mockControl) SetPaused(paused bool) { func (cc *mockControl) SetPaused(paused bool) {
was := cc.paused.Swap(paused)
if was == paused {
return
}
cc.logf("SetPaused=%v", paused) cc.logf("SetPaused=%v", paused)
if paused { if paused {
cc.called("pause") cc.called("pause")
@ -303,16 +309,10 @@ func TestStateMachine(t *testing.T) {
t.Fatalf("NewLocalBackend: %v", err) t.Fatalf("NewLocalBackend: %v", err)
} }
cc := newMockControl(t) var cc, previousCC *mockControl
cc.statusFunc = b.setClientStatus
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) { b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
cc.mu.Lock() previousCC = cc
cc.opts = opts cc = newClient(t, opts)
cc.logfActual = opts.Logf
cc.authBlocked = true
cc.persist = cc.opts.Persist.Clone()
cc.mu.Unlock()
t.Logf("ccGen: new mockControl.") t.Logf("ccGen: new mockControl.")
cc.called("New") cc.called("New")
@ -336,7 +336,7 @@ func TestStateMachine(t *testing.T) {
// Check that it hasn't called us right away. // Check that it hasn't called us right away.
// The state machine should be idle until we call Start(). // The state machine should be idle until we call Start().
cc.assertCalls() c.Assert(cc, qt.IsNil)
// Start the state machine. // Start the state machine.
// Since !WantRunning by default, it'll create a controlclient, // Since !WantRunning by default, it'll create a controlclient,
@ -346,7 +346,7 @@ func TestStateMachine(t *testing.T) {
c.Assert(b.Start(ipn.Options{}), qt.IsNil) c.Assert(b.Start(ipn.Options{}), qt.IsNil)
{ {
// BUG: strictly, it should pause, not unpause, here, since !WantRunning. // BUG: strictly, it should pause, not unpause, here, since !WantRunning.
cc.assertCalls("New", "unpause") cc.assertCalls("New")
nn := notifies.drain(2) nn := notifies.drain(2)
cc.assertCalls() cc.assertCalls()
@ -370,8 +370,8 @@ func TestStateMachine(t *testing.T) {
notifies.expect(2) notifies.expect(2)
c.Assert(b.Start(ipn.Options{}), qt.IsNil) c.Assert(b.Start(ipn.Options{}), qt.IsNil)
{ {
// BUG: strictly, it should pause, not unpause, here, since !WantRunning. previousCC.assertShutdown(false)
cc.assertCalls("Shutdown", "unpause", "New", "unpause") cc.assertCalls("New")
nn := notifies.drain(2) nn := notifies.drain(2)
cc.assertCalls() cc.assertCalls()
@ -397,6 +397,7 @@ func TestStateMachine(t *testing.T) {
// (This behaviour is needed so that b.Login() won't // (This behaviour is needed so that b.Login() won't
// start connecting to an old account right away, if one // start connecting to an old account right away, if one
// exists when you launch another login.) // exists when you launch another login.)
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
} }
// Attempted non-interactive login with no key; indicate that // Attempted non-interactive login with no key; indicate that
@ -406,7 +407,7 @@ func TestStateMachine(t *testing.T) {
url1 := "http://localhost:1/1" url1 := "http://localhost:1/1"
cc.send(nil, url1, false, nil) cc.send(nil, url1, false, nil)
{ {
cc.assertCalls("unpause") cc.assertCalls()
// ...but backend eats that notification, because the user // ...but backend eats that notification, because the user
// didn't explicitly request interactive login yet, and // didn't explicitly request interactive login yet, and
@ -416,6 +417,7 @@ func TestStateMachine(t *testing.T) {
c.Assert(nn[0].Prefs, qt.IsNotNil) c.Assert(nn[0].Prefs, qt.IsNotNil)
c.Assert(nn[0].Prefs.LoggedOut(), qt.IsFalse) c.Assert(nn[0].Prefs.LoggedOut(), qt.IsFalse)
c.Assert(nn[0].Prefs.WantRunning(), qt.IsFalse) c.Assert(nn[0].Prefs.WantRunning(), qt.IsFalse)
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
} }
// Now we'll try an interactive login. // Now we'll try an interactive login.
@ -427,9 +429,10 @@ func TestStateMachine(t *testing.T) {
b.StartLoginInteractive() b.StartLoginInteractive()
{ {
nn := notifies.drain(1) nn := notifies.drain(1)
cc.assertCalls("unpause") cc.assertCalls()
c.Assert(nn[0].BrowseToURL, qt.IsNotNil) c.Assert(nn[0].BrowseToURL, qt.IsNotNil)
c.Assert(url1, qt.Equals, *nn[0].BrowseToURL) c.Assert(url1, qt.Equals, *nn[0].BrowseToURL)
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
} }
// Sometimes users press the Login button again, in the middle of // Sometimes users press the Login button again, in the middle of
@ -444,6 +447,7 @@ func TestStateMachine(t *testing.T) {
notifies.drain(0) notifies.drain(0)
// backend asks control for another login sequence // backend asks control for another login sequence
cc.assertCalls("Login") cc.assertCalls("Login")
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
} }
// Provide a new interactive login URL. // Provide a new interactive login URL.
@ -452,13 +456,14 @@ func TestStateMachine(t *testing.T) {
url2 := "http://localhost:1/2" url2 := "http://localhost:1/2"
cc.send(nil, url2, false, nil) cc.send(nil, url2, false, nil)
{ {
cc.assertCalls("unpause", "unpause") cc.assertCalls()
// This time, backend should emit it to the UI right away, // This time, backend should emit it to the UI right away,
// because the UI is anxiously awaiting a new URL to visit. // because the UI is anxiously awaiting a new URL to visit.
nn := notifies.drain(1) nn := notifies.drain(1)
c.Assert(nn[0].BrowseToURL, qt.IsNotNil) c.Assert(nn[0].BrowseToURL, qt.IsNotNil)
c.Assert(url2, qt.Equals, *nn[0].BrowseToURL) c.Assert(url2, qt.Equals, *nn[0].BrowseToURL)
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
} }
// Pretend that the interactive login actually happened. // Pretend that the interactive login actually happened.
@ -467,7 +472,6 @@ func TestStateMachine(t *testing.T) {
// The backend should propagate this upward for the UI. // The backend should propagate this upward for the UI.
t.Logf("\n\nLoginFinished") t.Logf("\n\nLoginFinished")
notifies.expect(3) notifies.expect(3)
cc.setAuthBlocked(false)
cc.persist.LoginName = "user1" cc.persist.LoginName = "user1"
cc.send(nil, "", true, &netmap.NetworkMap{}) cc.send(nil, "", true, &netmap.NetworkMap{})
{ {
@ -480,12 +484,13 @@ func TestStateMachine(t *testing.T) {
// wait until it gets into Starting. // wait until it gets into Starting.
// TODO: (Currently this test doesn't detect that bug, but // TODO: (Currently this test doesn't detect that bug, but
// it's visible in the logs) // it's visible in the logs)
cc.assertCalls("unpause", "unpause", "unpause") cc.assertCalls()
c.Assert(nn[0].LoginFinished, qt.IsNotNil) c.Assert(nn[0].LoginFinished, qt.IsNotNil)
c.Assert(nn[1].Prefs, qt.IsNotNil) c.Assert(nn[1].Prefs, qt.IsNotNil)
c.Assert(nn[2].State, qt.IsNotNil) c.Assert(nn[2].State, qt.IsNotNil)
c.Assert(nn[1].Prefs.Persist().LoginName, qt.Equals, "user1") c.Assert(nn[1].Prefs.Persist().LoginName, qt.Equals, "user1")
c.Assert(ipn.NeedsMachineAuth, qt.Equals, *nn[2].State) c.Assert(ipn.NeedsMachineAuth, qt.Equals, *nn[2].State)
c.Assert(ipn.NeedsMachineAuth, qt.Equals, b.State())
} }
// Pretend that the administrator has authorized our machine. // Pretend that the administrator has authorized our machine.
@ -502,7 +507,7 @@ func TestStateMachine(t *testing.T) {
}) })
{ {
nn := notifies.drain(1) nn := notifies.drain(1)
cc.assertCalls("unpause", "unpause", "unpause") cc.assertCalls()
c.Assert(nn[0].State, qt.IsNotNil) c.Assert(nn[0].State, qt.IsNotNil)
c.Assert(ipn.Starting, qt.Equals, *nn[0].State) c.Assert(ipn.Starting, qt.Equals, *nn[0].State)
} }
@ -543,7 +548,7 @@ func TestStateMachine(t *testing.T) {
{ {
nn := notifies.drain(2) nn := notifies.drain(2)
// BUG: Login isn't needed here. We never logged out. // BUG: Login isn't needed here. We never logged out.
cc.assertCalls("Login", "unpause", "unpause") cc.assertCalls("Login", "unpause")
// BUG: I would expect Prefs to change first, and state after. // BUG: I would expect Prefs to change first, and state after.
c.Assert(nn[0].State, qt.IsNotNil) c.Assert(nn[0].State, qt.IsNotNil)
c.Assert(nn[1].Prefs, qt.IsNotNil) c.Assert(nn[1].Prefs, qt.IsNotNil)
@ -580,7 +585,7 @@ func TestStateMachine(t *testing.T) {
b.Logout() b.Logout()
{ {
nn := notifies.drain(2) nn := notifies.drain(2)
cc.assertCalls("pause", "StartLogout", "pause") cc.assertCalls("pause", "StartLogout")
c.Assert(nn[0].State, qt.IsNotNil) c.Assert(nn[0].State, qt.IsNotNil)
c.Assert(nn[1].Prefs, qt.IsNotNil) c.Assert(nn[1].Prefs, qt.IsNotNil)
c.Assert(ipn.Stopped, qt.Equals, *nn[0].State) c.Assert(ipn.Stopped, qt.Equals, *nn[0].State)
@ -593,11 +598,11 @@ func TestStateMachine(t *testing.T) {
// Let's make the logout succeed. // Let's make the logout succeed.
t.Logf("\n\nLogout (async) - succeed") t.Logf("\n\nLogout (async) - succeed")
notifies.expect(3) notifies.expect(3)
cc.setAuthBlocked(true)
cc.send(nil, "", false, nil) cc.send(nil, "", false, nil)
{ {
previousCC.assertShutdown(true)
nn := notifies.drain(3) nn := notifies.drain(3)
cc.assertCalls("unpause", "unpause", "Shutdown", "unpause", "New", "unpause") cc.assertCalls("New")
c.Assert(nn[0].State, qt.IsNotNil) c.Assert(nn[0].State, qt.IsNotNil)
c.Assert(*nn[0].State, qt.Equals, ipn.NoState) c.Assert(*nn[0].State, qt.Equals, ipn.NoState)
c.Assert(nn[1].Prefs, qt.IsNotNil) // emptyPrefs c.Assert(nn[1].Prefs, qt.IsNotNil) // emptyPrefs
@ -617,7 +622,7 @@ func TestStateMachine(t *testing.T) {
c.Assert(nn[0].Prefs, qt.IsNotNil) // emptyPrefs c.Assert(nn[0].Prefs, qt.IsNotNil) // emptyPrefs
// BUG: the backend has already called StartLogout, and we're // BUG: the backend has already called StartLogout, and we're
// still logged out. So it shouldn't call it again. // still logged out. So it shouldn't call it again.
cc.assertCalls("StartLogout", "unpause") cc.assertCalls("StartLogout")
cc.assertCalls() cc.assertCalls()
c.Assert(b.Prefs().LoggedOut(), qt.IsTrue) c.Assert(b.Prefs().LoggedOut(), qt.IsTrue)
c.Assert(b.Prefs().WantRunning(), qt.IsFalse) c.Assert(b.Prefs().WantRunning(), qt.IsFalse)
@ -627,7 +632,6 @@ func TestStateMachine(t *testing.T) {
// Let's acknowledge the second logout too. // Let's acknowledge the second logout too.
t.Logf("\n\nLogout2 (async) - succeed") t.Logf("\n\nLogout2 (async) - succeed")
notifies.expect(0) notifies.expect(0)
cc.setAuthBlocked(true)
cc.send(nil, "", false, nil) cc.send(nil, "", false, nil)
{ {
notifies.drain(0) notifies.drain(0)
@ -645,7 +649,7 @@ func TestStateMachine(t *testing.T) {
// I guess, since that's supposed to be synchronous. // I guess, since that's supposed to be synchronous.
{ {
notifies.drain(0) notifies.drain(0)
cc.assertCalls("Logout", "unpause") cc.assertCalls("Logout")
c.Assert(b.Prefs().LoggedOut(), qt.IsTrue) c.Assert(b.Prefs().LoggedOut(), qt.IsTrue)
c.Assert(b.Prefs().WantRunning(), qt.IsFalse) c.Assert(b.Prefs().WantRunning(), qt.IsFalse)
c.Assert(ipn.NeedsLogin, qt.Equals, b.State()) c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
@ -654,7 +658,6 @@ func TestStateMachine(t *testing.T) {
// Generate the third logout event. // Generate the third logout event.
t.Logf("\n\nLogout3 (sync) - succeed") t.Logf("\n\nLogout3 (sync) - succeed")
notifies.expect(0) notifies.expect(0)
cc.setAuthBlocked(true)
cc.send(nil, "", false, nil) cc.send(nil, "", false, nil)
{ {
notifies.drain(0) notifies.drain(0)
@ -676,9 +679,10 @@ func TestStateMachine(t *testing.T) {
notifies.expect(2) notifies.expect(2)
c.Assert(b.Start(ipn.Options{}), qt.IsNil) c.Assert(b.Start(ipn.Options{}), qt.IsNil)
{ {
previousCC.assertShutdown(false)
// BUG: We already called Shutdown(), no need to do it again. // BUG: We already called Shutdown(), no need to do it again.
// BUG: don't unpause because we're not logged in. // BUG: don't unpause because we're not logged in.
cc.assertCalls("Shutdown", "unpause", "New", "unpause") cc.assertCalls("New")
nn := notifies.drain(2) nn := notifies.drain(2)
cc.assertCalls() cc.assertCalls()
@ -693,14 +697,13 @@ func TestStateMachine(t *testing.T) {
b.Login(nil) b.Login(nil)
t.Logf("\n\nLoginFinished3") t.Logf("\n\nLoginFinished3")
notifies.expect(3) notifies.expect(3)
cc.setAuthBlocked(false)
cc.persist.LoginName = "user2" cc.persist.LoginName = "user2"
cc.send(nil, "", true, &netmap.NetworkMap{ cc.send(nil, "", true, &netmap.NetworkMap{
MachineStatus: tailcfg.MachineAuthorized, MachineStatus: tailcfg.MachineAuthorized,
}) })
{ {
nn := notifies.drain(3) nn := notifies.drain(3)
cc.assertCalls("Login", "unpause", "unpause", "unpause") cc.assertCalls("Login")
c.Assert(nn[0].LoginFinished, qt.IsNotNil) c.Assert(nn[0].LoginFinished, qt.IsNotNil)
c.Assert(nn[1].Prefs, qt.IsNotNil) c.Assert(nn[1].Prefs, qt.IsNotNil)
c.Assert(nn[1].Prefs.Persist(), qt.IsNotNil) c.Assert(nn[1].Prefs.Persist(), qt.IsNotNil)
@ -736,12 +739,14 @@ func TestStateMachine(t *testing.T) {
{ {
// NOTE: cc.Shutdown() is correct here, since we didn't call // NOTE: cc.Shutdown() is correct here, since we didn't call
// b.Shutdown() explicitly ourselves. // b.Shutdown() explicitly ourselves.
previousCC.assertShutdown(false)
// Note: unpause happens because ipn needs to get at least one netmap // Note: unpause happens because ipn needs to get at least one netmap
// on startup, otherwise UIs can't show the node list, login // on startup, otherwise UIs can't show the node list, login
// name, etc when in state ipn.Stopped. // name, etc when in state ipn.Stopped.
// Arguably they shouldn't try. But they currently do. // Arguably they shouldn't try. But they currently do.
nn := notifies.drain(2) nn := notifies.drain(2)
cc.assertCalls("Shutdown", "unpause", "New", "Login", "unpause") cc.assertCalls("New", "Login")
c.Assert(nn[0].Prefs, qt.IsNotNil) c.Assert(nn[0].Prefs, qt.IsNotNil)
c.Assert(nn[1].State, qt.IsNotNil) c.Assert(nn[1].State, qt.IsNotNil)
c.Assert(nn[0].Prefs.WantRunning(), qt.IsFalse) c.Assert(nn[0].Prefs.WantRunning(), qt.IsFalse)
@ -765,7 +770,7 @@ func TestStateMachine(t *testing.T) {
}) })
{ {
notifies.drain(0) notifies.drain(0)
cc.assertCalls("pause", "pause") cc.assertCalls("pause")
} }
// Request connection. // Request connection.
@ -778,7 +783,7 @@ func TestStateMachine(t *testing.T) {
}) })
{ {
nn := notifies.drain(2) nn := notifies.drain(2)
cc.assertCalls("Login", "unpause", "unpause") cc.assertCalls("Login", "unpause")
// BUG: I would expect Prefs to change first, and state after. // BUG: I would expect Prefs to change first, and state after.
c.Assert(nn[0].State, qt.IsNotNil) c.Assert(nn[0].State, qt.IsNotNil)
c.Assert(nn[1].Prefs, qt.IsNotNil) c.Assert(nn[1].Prefs, qt.IsNotNil)
@ -817,7 +822,7 @@ func TestStateMachine(t *testing.T) {
// //
// Because the login hasn't yet completed, the old login // Because the login hasn't yet completed, the old login
// is still valid, so it's correct that we stay paused. // is still valid, so it's correct that we stay paused.
cc.assertCalls("Login", "pause", "pause") cc.assertCalls("Login")
c.Assert(nn[0].BrowseToURL, qt.IsNotNil) c.Assert(nn[0].BrowseToURL, qt.IsNotNil)
c.Assert(*nn[0].BrowseToURL, qt.Equals, url3) c.Assert(*nn[0].BrowseToURL, qt.Equals, url3)
} }
@ -839,7 +844,7 @@ func TestStateMachine(t *testing.T) {
// and !WantRunning. But since it's a fresh and successful // and !WantRunning. But since it's a fresh and successful
// new login, WantRunning is true, so there was never a // new login, WantRunning is true, so there was never a
// reason to pause(). // reason to pause().
cc.assertCalls("pause", "unpause", "unpause") cc.assertCalls("unpause")
c.Assert(nn[0].LoginFinished, qt.IsNotNil) c.Assert(nn[0].LoginFinished, qt.IsNotNil)
c.Assert(nn[1].Prefs, qt.IsNotNil) c.Assert(nn[1].Prefs, qt.IsNotNil)
c.Assert(nn[2].State, qt.IsNotNil) c.Assert(nn[2].State, qt.IsNotNil)
@ -853,35 +858,35 @@ func TestStateMachine(t *testing.T) {
// The last test case is the most common one: restarting when both // The last test case is the most common one: restarting when both
// logged in and WantRunning. // logged in and WantRunning.
t.Logf("\n\nStart5") t.Logf("\n\nStart5")
notifies.expect(1) notifies.expect(2)
c.Assert(b.Start(ipn.Options{}), qt.IsNil) c.Assert(b.Start(ipn.Options{}), qt.IsNil)
{ {
// NOTE: cc.Shutdown() is correct here, since we didn't call // NOTE: cc.Shutdown() is correct here, since we didn't call
// b.Shutdown() ourselves. // b.Shutdown() ourselves.
cc.assertCalls("Shutdown", "unpause", "New", "Login", "unpause") previousCC.assertShutdown(false)
cc.assertCalls("New", "Login")
nn := notifies.drain(1) nn := notifies.drain(2)
cc.assertCalls() cc.assertCalls()
c.Assert(nn[0].Prefs, qt.IsNotNil) c.Assert(nn[0].Prefs, qt.IsNotNil)
c.Assert(nn[0].Prefs.LoggedOut(), qt.IsFalse) c.Assert(nn[0].Prefs.LoggedOut(), qt.IsFalse)
c.Assert(nn[0].Prefs.WantRunning(), qt.IsTrue) c.Assert(nn[0].Prefs.WantRunning(), qt.IsTrue)
c.Assert(ipn.NoState, qt.Equals, b.State()) c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
} }
// Control server accepts our valid key from before. // Control server accepts our valid key from before.
t.Logf("\n\nLoginFinished5") t.Logf("\n\nLoginFinished5")
notifies.expect(1) notifies.expect(2)
cc.setAuthBlocked(false)
cc.send(nil, "", true, &netmap.NetworkMap{ cc.send(nil, "", true, &netmap.NetworkMap{
MachineStatus: tailcfg.MachineAuthorized, MachineStatus: tailcfg.MachineAuthorized,
}) })
{ {
nn := notifies.drain(1) nn := notifies.drain(2)
cc.assertCalls("unpause", "unpause", "unpause") cc.assertCalls()
// NOTE: No LoginFinished message since no interactive // NOTE: No LoginFinished message since no interactive
// login was needed. // login was needed.
c.Assert(nn[0].State, qt.IsNotNil) c.Assert(nn[1].State, qt.IsNotNil)
c.Assert(ipn.Starting, qt.Equals, *nn[0].State) c.Assert(ipn.Starting, qt.Equals, *nn[1].State)
// NOTE: No prefs change this time. WantRunning stays true. // NOTE: No prefs change this time. WantRunning stays true.
// We were in Starting in the first place, so that doesn't // We were in Starting in the first place, so that doesn't
// change either. // change either.
@ -895,7 +900,7 @@ func TestStateMachine(t *testing.T) {
}) })
{ {
nn := notifies.drain(1) nn := notifies.drain(1)
cc.assertCalls("unpause", "unpause") cc.assertCalls()
c.Assert(nn[0].State, qt.IsNotNil) c.Assert(nn[0].State, qt.IsNotNil)
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[0].State) c.Assert(ipn.NeedsLogin, qt.Equals, *nn[0].State)
c.Assert(ipn.NeedsLogin, qt.Equals, b.State()) c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
@ -910,7 +915,7 @@ func TestStateMachine(t *testing.T) {
}) })
{ {
nn := notifies.drain(1) nn := notifies.drain(1)
cc.assertCalls("unpause", "unpause", "unpause") cc.assertCalls()
c.Assert(nn[0].State, qt.IsNotNil) c.Assert(nn[0].State, qt.IsNotNil)
c.Assert(ipn.Starting, qt.Equals, *nn[0].State) c.Assert(ipn.Starting, qt.Equals, *nn[0].State)
c.Assert(ipn.Starting, qt.Equals, b.State()) c.Assert(ipn.Starting, qt.Equals, b.State())
@ -921,7 +926,7 @@ func TestStateMachine(t *testing.T) {
b.setWgengineStatus(&wgengine.Status{DERPs: 1, AsOf: time.Now()}, nil) b.setWgengineStatus(&wgengine.Status{DERPs: 1, AsOf: time.Now()}, nil)
{ {
nn := notifies.drain(1) nn := notifies.drain(1)
cc.assertCalls("unpause") cc.assertCalls()
c.Assert(nn[0].State, qt.IsNotNil) c.Assert(nn[0].State, qt.IsNotNil)
c.Assert(ipn.Running, qt.Equals, *nn[0].State) c.Assert(ipn.Running, qt.Equals, *nn[0].State)
c.Assert(ipn.Running, qt.Equals, b.State()) c.Assert(ipn.Running, qt.Equals, b.State())
@ -1017,11 +1022,9 @@ func TestWGEngineStatusRace(t *testing.T) {
b, err := NewLocalBackend(logf, "logid", new(mem.Store), "", nil, eng, 0) b, err := NewLocalBackend(logf, "logid", new(mem.Store), "", nil, eng, 0)
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
cc := newMockControl(t) var cc *mockControl
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) { b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
cc.mu.Lock() cc = newClient(t, opts)
defer cc.mu.Unlock()
cc.logfActual = opts.Logf
return cc, nil return cc, nil
}) })

Loading…
Cancel
Save