arblant
firefox-vpn-client / core / internal / service / sleep_test.go
sleep_test.go142 lines · 3.6 KB
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 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
package service

import (
	"net"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/armanlife/firefox-vpn-client/core/internal/config"
	"github.com/armanlife/firefox-vpn-client/core/internal/proxy"
)

// sleepyEngine is an engine whose wake only installs a new (empty)
// controller, counting wakes.
func sleepyEngine(t *testing.T, idleSleep time.Duration) (*Daemon, *proxyEngine, *atomic.Int32) {
	t.Helper()
	d := newTestDaemon(t)
	cfg := d.config()
	cfg.Proxy.IdleSleep = config.Duration(idleSleep)
	if _, err := d.updateSettings(cfg, nil); err != nil {
		t.Fatal(err)
	}
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatal(err)
	}
	e := &proxyEngine{controller: &proxy.Controller{}, listener: ln}
	e.routed = proxy.NewRoutedOpener(&engineUpstream{d: d, engine: e})
	t.Cleanup(func() { _ = ln.Close(); e.routed.Close() })
	e.idle.touch()
	d.mu.Lock()
	d.engine = e
	d.mu.Unlock()
	var wakes atomic.Int32
	d.wakeHook = func(e *proxyEngine) error {
		time.Sleep(20 * time.Millisecond) // a pass and a session take a while
		wakes.Add(1)
		e.idle.mu.Lock()
		e.controller, e.idle.sleeping = &proxy.Controller{}, time.Time{}
		e.idle.mu.Unlock()
		return nil
	}
	return d, e, &wakes
}

func idleFor(e *proxyEngine, d time.Duration) {
	e.idle.mu.Lock()
	e.idle.lastUsed = time.Now().Add(-d)
	e.idle.mu.Unlock()
}

// The proxy sleeps only after the idle time without use, and wakes once
// for many connections arriving together.
func TestIdleSleepAndWake(t *testing.T) {
	d, e, wakes := sleepyEngine(t, 15*time.Minute)

	idleFor(e, 10*time.Minute)
	if d.sleepIfIdle(e) {
		t.Fatal("not idle long enough")
	}
	idleFor(e, 16*time.Minute)
	if !d.sleepIfIdle(e) || e.currentController() != nil || e.asleepSince().IsZero() {
		t.Fatal("idle for 16 minutes: asleep")
	}
	if view, _ := d.StatusView(); view.Pass != nil || view.Proxy == nil || view.Proxy.SleepingSince.IsZero() {
		t.Fatal("a sleeping proxy reports no pass and since when it sleeps")
	}

	// Connections arriving together wake it once.
	var wg sync.WaitGroup
	releases := make(chan func(), 5)
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			c, release, err := d.useController(e, wakeForClient)
			if err != nil || c == nil {
				t.Error(err)
				return
			}
			releases <- release
		}()
	}
	wg.Wait()
	close(releases)
	if wakes.Load() != 1 || !e.asleepSince().IsZero() {
		t.Fatalf("woken %d times", wakes.Load())
	}

	// An open connection keeps it awake however long it lasts.
	idleFor(e, time.Hour)
	if d.sleepIfIdle(e) {
		t.Fatal("asleep while connections are open")
	}
	for release := range releases {
		release()
		release() // releasing twice counts once
	}
	if d.sleepIfIdle(e) {
		t.Fatal("asleep right after the last connection closed")
	}
	idleFor(e, 16*time.Minute)
	if !d.sleepIfIdle(e) {
		t.Fatal("idle again: asleep")
	}
}

func TestIdleSleepOff(t *testing.T) {
	d, e, _ := sleepyEngine(t, 0)
	idleFor(e, 24*time.Hour)
	if d.sleepIfIdle(e) || e.currentController() == nil {
		t.Fatal("idle_sleep 0 never sleeps")
	}
}

// A tunnel keeps the controller in use until it is closed.
func TestReleasingConnKeepsHalfClose(t *testing.T) {
	released := 0
	tcp, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatal(err)
	}
	defer tcp.Close()
	go func() {
		if c, err := tcp.Accept(); err == nil {
			c.Close()
		}
	}()
	raw, err := net.Dial("tcp", tcp.Addr().String())
	if err != nil {
		t.Fatal(err)
	}
	conn := releasingConn(raw, func() { released++ })
	if _, ok := conn.(interface{ CloseWrite() error }); !ok {
		t.Fatal("half-close must be kept")
	}
	_ = conn.Close()
	if released != 1 {
		t.Fatalf("released %d", released)
	}
}