Files
lneto/x/netdev/runner.go
Pat Whittingslow 813b7b5e57 Lay out device/stack interfaces - Netdev/Netlink (#92)
* good code today

* add netdev.Runner

* round off APIs more

* better interface method documentation

* improve DHCP netstack API

* add pico w netdev example

* remove temp file

* keep thinking about this. this is hard :/

* fix ci

* small fixer

* working dhcp

* fix rebase API mismatches

* begin adding espradio example

* keep working on espradio, icmp not working

* fix icmp by fixing dhcp
2026-06-17 14:18:08 -03:00

142 lines
4.0 KiB
Go

package netdev
import (
"context"
"errors"
"sync/atomic"
"github.com/soypat/lneto"
)
// Runner orchestrates an Interface and a Stack asynchronously.
type Runner[C any] struct {
running atomic.Uint32
// buflen stores the length of data inside buf. It is used as a buffer acquisition synchronizing primitive.
buflen atomic.Uint32
// pktlost is incremented each time an incoming packet is lost due to insufficient buffer size.
pktlost atomic.Uint64
tx, rx atomic.Uint64
// buf stores actual data.
buf []byte
// bufsaux is used as ana argument to stack processing so that no allocations are performed
bufsaux [1][]byte
sizesaux [1]int
handlerTriggered bool
deviceIsPollOnly bool
}
func (r *Runner[C]) Run(ctx context.Context, iface Interface[C], stack Stack, backoff lneto.BackoffStrategy) error {
if stack == nil || backoff == nil {
return errors.New("nil arguments to Run")
}
if !r.acquire() {
return errors.New("runner currently running.")
}
defer func() {
iface.dev.SetEthRecvHandler(nil)
r.release()
}()
r.rx.Store(0)
r.tx.Store(0)
r.buflen.Store(0)
r.pktlost.Store(0)
r.handlerTriggered = false
r.deviceIsPollOnly = false
bufsize := iface.bufsize()
if cap(r.buf) < bufsize {
r.buf = make([]byte, bufsize)
}
r.buf = r.buf[:bufsize]
iface.dev.SetEthRecvHandler(r.recvEthHandler)
// backoffs stores number of consecutive times no data was sent/received.
var backoffs uint
for ctx.Err() == nil {
n1, _ := r.processRx(stack, 0)
eoff, efrm, err := iface.dev.EthPoll(r.buf)
n2, _ := r.processRx(stack, 0)
if efrm > 0 && n2 == 0 {
r.deviceIsPollOnly = true
r.buflen.Store(uint32(eoff + efrm))
r.processRx(stack, eoff)
} else if efrm > 0 && n2 > 0 {
return errors.New("device both returns nonzero poll read and calls, choose one")
} else if err != nil {
println("err EthPoll:", err.Error())
}
// Now do Tx, but first acquire buffer.
if !r.buflen.CompareAndSwap(0, 1) {
continue // Oh no, async data received, go back to Rx processing.
}
r.bufsaux = [1][]byte{r.buf}
err = stack.EgressPackets(r.bufsaux[:], r.sizesaux[:], iface.frameOff)
n := r.sizesaux[0]
if err != nil {
println("err EgressPackets:", err.Error())
} else if n > 0 {
if n+iface.frameOff > len(r.buf) {
return errors.New("EgressPackets returned invalid written data given frameOffset and argument buffer size")
}
err = iface.dev.SendOffsetEthFrame(r.bufsaux[0][:n+iface.frameOff])
r.tx.Add(uint64(n + iface.frameOff))
if err != nil {
println("err SendOffsetEthFrame:", err.Error())
}
}
r.buflen.Store(0) // Release buffer.
if n1 > 0 || n2 > 0 || efrm > 0 || n > 0 {
backoffs = 0
} else {
backoff.Do(backoffs)
backoffs++
}
}
return ctx.Err()
}
// PrintDebug
//
// Deprecated: Might be given other shape in future, but this is not how we do debugging. use freely meanwhile.
func (r *Runner[C]) PrintDebug() {
print("RUNNER: tx|rx:", r.tx.Load(), "|", r.rx.Load(),
" devpollonly:", r.deviceIsPollOnly, " pktlost:", r.pktlost.Load(),
" handles:", r.handlerTriggered, " bufsize:", len(r.buf),
"\n")
}
func (r *Runner[C]) acquire() bool {
return r.running.CompareAndSwap(0, 1)
}
func (r *Runner[C]) release() {
if r.running.Load()&1 == 0 {
panic("release of unacquired resource")
}
r.running.Store(0)
}
// recvEthHandler is called asynchronously. Should be as fast as possible. Do not block inside.
func (r *Runner[C]) recvEthHandler(incomingEthernet []byte) {
if !r.buflen.CompareAndSwap(0, uint32(len(incomingEthernet))) {
// Failed to acquire buffer, packet dropped.
r.pktlost.Add(1)
return
}
copy(r.buf, incomingEthernet)
}
// processRx is called after a packet is received asynchronously and compied to buffer via recvEthHandler
func (r *Runner[C]) processRx(stack Stack, ethFrameOff int) (int, error) {
r.handlerTriggered = true
n := r.buflen.Load()
if n == 0 {
return 0, nil
}
r.rx.Add(uint64(n))
defer r.buflen.Store(0)
r.bufsaux = [1][]byte{r.buf[:n]}
return int(n), stack.IngressPackets(r.bufsaux[:], ethFrameOff)
}