Skip to content

Commit b5c88d5

Browse files
committed
debugstream: implement the debug stream
The package debugstream provides two utility types, the Stream and the Client. The Stream type listens in a TCP endpoint and can be used to broadcast events to all connected clients. The event type has a uint64 code and a byte slice data fields. The Stream implementation is used only when btcd is compiled with the debug tag, otherwise a nop implementation is used. The Client connects with the Stream via TCP and starts receiving the events, which are passed into a handler callback, allowing assertions inside tests. All events broadcasted in the stream are stored in memory and sent to all clients after connection, this way if the client connects after the broadcast of events, it still receives all of them.
1 parent ea5959a commit b5c88d5

12 files changed

Lines changed: 678 additions & 0 deletions

File tree

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ unit:
101101
cd btcutil && $(GOTEST_DEV) ./... -test.timeout=20m
102102
cd chaincfg && $(GOTEST_DEV) ./... -test.timeout=20m
103103
cd chainhash && $(GOTEST_DEV) ./... -test.timeout=20m
104+
cd debugstream && $(GOTEST_DEV) ./... -test.timeout=20m
104105
cd txscript && $(GOTEST_DEV) ./... -test.timeout=20m
105106
cd psbt && $(GOTEST_DEV) ./... -test.timeout=20m
106107
cd wire && $(GOTEST_DEV) ./... -test.timeout=20m
@@ -117,6 +118,7 @@ unit-cover:
117118
cd btcutil && $(GOTEST) $(COVER_FLAGS) ./... && sed -i.bak 's/v2\///g' coverage.txt
118119
cd chaincfg && $(GOTEST) $(COVER_FLAGS) ./... && sed -i.bak 's/v2\///g' coverage.txt
119120
cd chainhash && $(GOTEST) $(COVER_FLAGS) ./... && sed -i.bak 's/v2\///g' coverage.txt
121+
cd debugstream && $(GOTEST) $(COVER_FLAGS) ./... && sed -i.bak 's/v2\///g' coverage.txt
120122
cd txscript && $(GOTEST) $(COVER_FLAGS) ./... && sed -i.bak 's/v2\///g' coverage.txt
121123
cd psbt && $(GOTEST) $(COVER_FLAGS) ./... && sed -i.bak 's/v2\///g' coverage.txt
122124
cd wire && $(GOTEST) $(COVER_FLAGS) ./... && sed -i.bak 's/v2\///g' coverage.txt
@@ -130,6 +132,7 @@ unit-race:
130132
cd btcutil && env CGO_ENABLED=1 GORACE="history_size=7 halt_on_errors=1" $(GOTEST) -race -test.timeout=20m ./...
131133
cd chaincfg && env CGO_ENABLED=1 GORACE="history_size=7 halt_on_errors=1" $(GOTEST) -race -test.timeout=20m ./...
132134
cd chainhash && env CGO_ENABLED=1 GORACE="history_size=7 halt_on_errors=1" $(GOTEST) -race -test.timeout=20m ./...
135+
cd debugstream && env CGO_ENABLED=1 GORACE="history_size=7 halt_on_errors=1" $(GOTEST) -race -test.timeout=20m ./...
133136
cd txscript && env CGO_ENABLED=1 GORACE="history_size=7 halt_on_errors=1" $(GOTEST) -race -test.timeout=20m ./...
134137
cd psbt && env CGO_ENABLED=1 GORACE="history_size=7 halt_on_errors=1" $(GOTEST) -race -test.timeout=20m ./...
135138
cd wire && env CGO_ENABLED=1 GORACE="history_size=7 halt_on_errors=1" $(GOTEST) -race -test.timeout=20m ./...

debugstream/client.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package debugstream
2+
3+
import (
4+
"fmt"
5+
"net"
6+
"sync"
7+
"time"
8+
)
9+
10+
const (
11+
csStopped = iota
12+
csStarted
13+
)
14+
15+
type Client struct {
16+
addr string
17+
18+
conn *net.TCPConn
19+
eventCh chan Event
20+
handler func(e Event)
21+
wg sync.WaitGroup
22+
23+
stateMu sync.Mutex
24+
state int
25+
shutdownRequested chan struct{}
26+
}
27+
28+
type EventHandler struct {
29+
Code uint64
30+
Handler func(e Event)
31+
}
32+
33+
func NewClient(streamAddr string, handler func(ev Event)) *Client {
34+
return &Client{
35+
addr: streamAddr,
36+
eventCh: make(chan Event),
37+
handler: handler,
38+
shutdownRequested: make(chan struct{}),
39+
}
40+
}
41+
42+
func (c *Client) loop() {
43+
for {
44+
var ev Event
45+
err := ev.read(c.conn)
46+
if err != nil {
47+
select {
48+
case <-c.shutdownRequested:
49+
return
50+
default:
51+
cliLog.Errorf("client loop %s", err)
52+
return
53+
}
54+
}
55+
c.handler(ev)
56+
}
57+
}
58+
59+
func (c *Client) Start() error {
60+
c.stateMu.Lock()
61+
defer c.stateMu.Unlock()
62+
if c.state != csStopped {
63+
return fmt.Errorf("client not stopped")
64+
}
65+
66+
const maxConnAttempts = 7
67+
68+
var (
69+
conn net.Conn
70+
err error
71+
)
72+
for i := range maxConnAttempts {
73+
if i > 0 {
74+
time.Sleep((time.Millisecond*100)<<i - 1)
75+
}
76+
conn, err = net.Dial("tcp", c.addr)
77+
if err != nil {
78+
continue
79+
}
80+
break
81+
}
82+
if err != nil {
83+
return err
84+
}
85+
86+
conn.SetReadDeadline(time.Now().Add(time.Second * 5))
87+
var ev Event
88+
err = ev.read(conn)
89+
if err != nil {
90+
conn.Close()
91+
return err
92+
}
93+
if ev.Code != 0 || len(ev.Data) != 0 {
94+
conn.Close()
95+
return fmt.Errorf("handshake failed")
96+
}
97+
conn.SetReadDeadline(time.Time{})
98+
99+
c.conn = conn.(*net.TCPConn)
100+
101+
c.wg.Go(c.loop)
102+
103+
c.state = csStarted
104+
return nil
105+
}
106+
107+
func (c *Client) Stop() {
108+
c.stateMu.Lock()
109+
defer c.stateMu.Unlock()
110+
if c.state != csStarted {
111+
return
112+
}
113+
114+
close(c.shutdownRequested)
115+
c.conn.Close()
116+
c.wg.Wait()
117+
118+
c.conn = nil
119+
c.shutdownRequested = make(chan struct{})
120+
c.state = csStopped
121+
}

debugstream/codes.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package debugstream
2+
3+
// The following are guard event codes, that can be used to assert debug events
4+
// in tests.
5+
const (
6+
DEStart = iota + 1
7+
DEShutdown
8+
)

debugstream/common.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package debugstream
2+
3+
import (
4+
"encoding/binary"
5+
"fmt"
6+
"io"
7+
)
8+
9+
// Event is the message that is sent from the Stream to the Client.
10+
type Event struct {
11+
Code uint64
12+
Data []byte
13+
}
14+
15+
func (e *Event) write(w io.Writer) error {
16+
err := binary.Write(w, binary.BigEndian, e.Code)
17+
if err != nil {
18+
return err
19+
}
20+
dataLen := uint64(len(e.Data))
21+
err = binary.Write(w, binary.BigEndian, dataLen)
22+
if err != nil {
23+
return err
24+
}
25+
n, err := w.Write(e.Data)
26+
if err != nil {
27+
return err
28+
}
29+
if n != int(dataLen) {
30+
return fmt.Errorf("nWrite != dataLen")
31+
}
32+
return nil
33+
}
34+
35+
func (e *Event) read(r io.Reader) error {
36+
err := binary.Read(r, binary.BigEndian, &e.Code)
37+
if err != nil {
38+
return err
39+
}
40+
var dataLen uint64
41+
err = binary.Read(r, binary.BigEndian, &dataLen)
42+
if err != nil {
43+
return err
44+
}
45+
e.Data = make([]byte, dataLen)
46+
n, err := r.Read(e.Data)
47+
if err != nil {
48+
return err
49+
}
50+
if n != int(dataLen) {
51+
return fmt.Errorf("nRead != dataLen")
52+
}
53+
return nil
54+
}

debugstream/debug.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//go:build debug
2+
3+
package debugstream
4+
5+
// We use StreamServer as Stream when compiling with debug tag.
6+
type Stream = StreamServer
7+
8+
func New() *Stream {
9+
return NewStreamServer()
10+
}

debugstream/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/btcsuite/btcd/debugstream
2+
3+
go 1.25

debugstream/log.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package debugstream
2+
3+
import "github.com/btcsuite/btclog"
4+
5+
var strLog btclog.Logger
6+
var cliLog btclog.Logger
7+
8+
func init() {
9+
DisableLog()
10+
}
11+
12+
// DisableLog disables all library log output. Logging output is disabled
13+
// by default until either UseLogger or SetLogWriter are called.
14+
func DisableLog() {
15+
strLog, cliLog = btclog.Disabled, btclog.Disabled
16+
}
17+
18+
// UseLogger uses a specified Logger to output package logging info.
19+
// This should be used in preference to SetLogWriter if the caller is also
20+
// using btclog.
21+
func UseLoggers(strLogger, cliLogger btclog.Logger) {
22+
strLog, cliLog = strLogger, cliLogger
23+
}

debugstream/nop.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//go:build !debug
2+
3+
package debugstream
4+
5+
// Stream is StreamNop, which does nothing when btcd is not compiled with the
6+
// debug tag.
7+
type Stream = StreamNop
8+
9+
func New() *Stream {
10+
return NewStreamNop()
11+
}

0 commit comments

Comments
 (0)