Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dap/banked_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func TestBankedDPAccessPreservesSELECTFields(t *testing.T) {
if value != dlcr {
t.Fatalf("DLCR = %#08x, want %#08x", value, dlcr)
}
wantRequests := []swdsim.Request{dpWrite(0x08), dpRead(0x0c), dpRead(0x04)}
wantRequests := []swdsim.Request{dpRead(0x0c), dpWrite(0x08), dpRead(0x0c), dpRead(0x04)}
if got := target.requests[beforeBankedRead : beforeBankedRead+len(wantRequests)]; !equalRequests(got, wantRequests) {
t.Fatalf("banked DP requests = %#v, want %#v", got, wantRequests)
}
Expand Down
58 changes: 50 additions & 8 deletions dap/cleanup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import (
)

type cleanupBudgetWire struct {
inner swd.Wire
fail bool
calls int
entryBudget time.Duration
inner swd.Wire
fail bool
calls int
entryDeadline time.Time
}

func (w *cleanupBudgetWire) SWDIO(ctx context.Context, direction, output []byte, bits int) ([]byte, error) {
Expand All @@ -28,7 +28,7 @@ func (w *cleanupBudgetWire) SWDIO(ctx context.Context, direction, output []byte,
if bits == 136 {
deadline, ok := ctx.Deadline()
if ok {
w.entryBudget = time.Until(deadline)
w.entryDeadline = deadline
}
}
return w.inner.SWDIO(ctx, direction, output, bits)
Expand All @@ -53,7 +53,7 @@ func TestConnectRejectsNilContextBeforeTraffic(t *testing.T) {
}
}

func TestRecoveryUsesConfiguredIndependentBudget(t *testing.T) {
func TestReleaseReentryUsesCallerBudget(t *testing.T) {
wire := &cleanupBudgetWire{inner: swdsim.New(sim.New(0x2ba01477))}
dp := dap.NewDebugPort(dap.SWDP(swd.New(wire)), dap.WithCleanupTimeout(3*time.Second))
if _, err := dp.Connect(t.Context()); err != nil {
Expand All @@ -65,10 +65,52 @@ func TestRecoveryUsesConfiguredIndependentBudget(t *testing.T) {
}
ctx, cancel := context.WithCancel(t.Context())
cancel()
before := wire.calls
if err := dp.Release(ctx); !errors.Is(err, context.Canceled) || wire.calls != before {
t.Fatalf("canceled release = %v, wire calls = %d; want %d", err, wire.calls, before)
}
ctx, cancel = context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
deadline, _ := ctx.Deadline()
if err := dp.Release(ctx); err != nil {
t.Fatal(err)
}
if wire.entryBudget < 2*time.Second || wire.entryBudget > 3*time.Second {
t.Fatalf("recovery entry budget = %v", wire.entryBudget)
if wire.entryDeadline != deadline {
t.Fatalf("release entry deadline = %v, want %v", wire.entryDeadline, deadline)
}
}

type recoveryBudgetTarget struct {
*waitTarget
abortBudget time.Duration
}

func (target *recoveryBudgetTarget) Write(ctx context.Context, req swdsim.Request, value uint32) error {
if req == dpWrite(0) && value == 1 {
deadline, _ := ctx.Deadline()
target.abortBudget = time.Until(deadline)
}
return target.waitTarget.Write(ctx, req, value)
}

func TestRecoveryUsesConfiguredIndependentBudget(t *testing.T) {
target := &recoveryBudgetTarget{waitTarget: newWaitTarget()}
addAP(t, target.waitTarget, 0, 0x24770011)
dp := dap.NewDebugPort(dap.SWDP(swd.New(swdsim.New(target))),
dap.WithMaxWaits(1), dap.WithCleanupTimeout(3*time.Second))
if _, err := dp.Connect(t.Context()); err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
defer cancel()
target.arm(apRead(12), 1)
if _, err := dp.ReadAPIDR(ctx, dap.NewAPSel(0)); !errors.Is(err, dap.ErrWait) {
t.Fatalf("AP WAIT = %v", err)
}
if target.abortBudget < 2*time.Second || target.abortBudget > 3*time.Second {
t.Fatalf("independent recovery budget = %v", target.abortBudget)
}
if err := dp.Release(t.Context()); err != nil {
t.Fatal(err)
}
}
44 changes: 44 additions & 0 deletions dap/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package dap_test

import (
"context"
"errors"
"maps"
"slices"
"testing"
"time"

"github.com/jon/ostiole/dap"
"github.com/jon/ostiole/dap/sim"
Expand Down Expand Up @@ -97,3 +99,45 @@ func nilContextOperations(dp *dap.DebugPort, mem *dap.MemAP, sel dap.APSel, link
},
}
}

func TestReleasePreflightContextPreservesConnectedPort(t *testing.T) {
for _, link := range []string{"SWD", "JTAG"} {
for _, failure := range []string{"canceled", "expired"} {
t.Run(link+"/"+failure, func(t *testing.T) {
checkReleasePreflightContext(t, link, failure)
})
}
}
}

func checkReleasePreflightContext(t *testing.T, link, failure string) {
t.Helper()
wire := &cleanupBudgetWire{inner: swdsim.New(sim.New(0x2ba01477))}
port := dap.SWDP(swd.New(wire))
calls := func() int { return wire.calls }
if link == "JTAG" {
_, jwire, chain := jtagModelChain(t, 4, 0)
port = dap.JTAGDP(chain, 0)
calls = func() int { return jwire.calls }
}
dp := dap.NewDebugPort(port)
if _, err := dp.Connect(t.Context()); err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(t.Context())
if failure == "expired" {
cancel()
ctx, cancel = context.WithDeadline(t.Context(), time.Now().Add(-time.Second))
}
cancel()
before := calls()
if err := dp.Release(ctx); !errors.Is(err, ctx.Err()) || calls() != before {
t.Fatalf("Release = %v, wire calls = %d; want %d", err, calls(), before)
}
if _, err := dp.ReadDP(t.Context(), dap.CTRLSTAT); err != nil {
t.Fatalf("preflight context invalidated connected port: %v", err)
}
if err := dp.Release(t.Context()); err != nil {
t.Fatal(err)
}
}
13 changes: 8 additions & 5 deletions dap/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,14 @@
//
// A failed Connect attempts bounded cleanup before returning. If that cleanup
// also fails, Release remains available but other debug-port and access-port
// operations fail until cleanup succeeds. A failed Release has the same
// cleanup-only behavior and may be retried. WithCleanupTimeout configures each
// independent recovery attempt: one second by default for SWD, thirty for JTAG.
// JTAG recovery revalidates the exact chain and reacquires the TAP before
// restoration. The debug port never closes or automatically reopens the probe.
// operations fail until cleanup succeeds. A failure after Release starts
// restoration has the same cleanup-only behavior and may be retried. Calling
// Release with an expired or canceled context sends no traffic and leaves a
// connected port usable.
// WithCleanupTimeout configures each independent recovery attempt: one second
// by default for SWD, thirty for JTAG. JTAG recovery revalidates the exact chain
// and reacquires the TAP before restoration. The debug port never closes or
// automatically reopens the probe.
//
// A DebugPort does not replay a request which returns FAULT. It reads
// bank-zero CTRL/STAT when the register selection is known, clears the sticky
Expand Down
16 changes: 13 additions & 3 deletions dap/jtag_cleanup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ func TestJTAGReservedACKInvalidatesProtocol(t *testing.T) {
}
}

func TestJTAGRecoveryDeadlineCanBeRetriedIndependently(t *testing.T) {
func TestJTAGReleaseReentryHonorsCallerContext(t *testing.T) {
_, wire, chain := jtagModelChain(t, 4, 0)
dp := dap.NewDebugPort(dap.JTAGDP(chain, 0), dap.WithCleanupTimeout(time.Second))
if _, err := dp.Connect(t.Context()); err != nil {
Expand All @@ -302,11 +302,21 @@ func TestJTAGRecoveryDeadlineCanBeRetriedIndependently(t *testing.T) {
wire.expire = true
ctx, cancel := context.WithCancel(t.Context())
cancel()
before := wire.calls
if err := dp.Release(ctx); !errors.Is(err, context.Canceled) || wire.calls != before {
t.Fatalf("canceled release = %v, wire calls = %d; want %d", err, wire.calls, before)
}
ctx, cancel = context.WithTimeout(t.Context(), 2*time.Second)
defer cancel()
deadline, _ := ctx.Deadline()
if err := dp.Release(ctx); !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("bounded revalidation: %v", err)
}
if err := dp.Release(ctx); err != nil {
t.Fatalf("fresh recovery with canceled caller: %v", err)
if wire.deadline != deadline {
t.Fatalf("release scan deadline = %v, want %v", wire.deadline, deadline)
}
if err := dp.Release(t.Context()); err != nil {
t.Fatalf("fresh release attempt: %v", err)
}
}

Expand Down
3 changes: 3 additions & 0 deletions dap/jtag_model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"testing"
"time"

"github.com/jon/ostiole/jtag"
)
Expand Down Expand Up @@ -34,6 +35,7 @@ type jtagDPWire struct {
fail bool
failAfter bool
expire bool
deadline time.Time
}

type jtagRequest struct {
Expand Down Expand Up @@ -148,6 +150,7 @@ func (m *jtagDPModel) execute(r jtagRequest) uint32 {

func (w *jtagDPWire) MaxTransferBits() int { return w.limit }
func (w *jtagDPWire) JTAGIO(ctx context.Context, tms, tdi []byte, bits int) ([]byte, error) {
w.deadline, _ = ctx.Deadline()
if err := ctx.Err(); err != nil {
return nil, err
}
Expand Down
21 changes: 11 additions & 10 deletions dap/lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,10 @@ func (dp *DebugPort) failConnect(cause error) error {

// Release restores volatile debug-port state owned by this connection.
//
// Release may be retried. Once an attempt starts, other DP and AP operations
// remain blocked until Release succeeds.
// Release may be retried. Each attempt, including protocol re-entry, uses ctx.
// Once an attempt starts, other DP and AP operations remain blocked until
// Release succeeds. Supply a fresh bounded context for cleanup retries.
// Recovery inside a failed transport operation retains its independent budget.
func (dp *DebugPort) Release(ctx context.Context) error {
if !dp.bound() {
return nil
Expand All @@ -173,23 +175,22 @@ func (dp *DebugPort) Release(ctx context.Context) error {
if ctx == nil {
return errors.New("dap: nil context")
}
if err := ctx.Err(); err != nil {
return err
}
dp.state.beginRepair()
releaseCtx := ctx
if !dp.state.responseKnown() {
var cancel context.CancelFunc
releaseCtx, cancel = dp.cleanupContext()
defer cancel()
if err := dp.reenter(releaseCtx); err != nil {
if err := dp.reenter(ctx); err != nil {
return fmt.Errorf("dap: restore protocol state for release: %w", err)
}
}
if err := dp.selectAPAddress(releaseCtx, 0); err != nil {
if err := dp.selectAPAddress(ctx, 0); err != nil {
return fmt.Errorf("dap: clear selection while releasing debug port: %w", err)
}
if err := dp.releasePower(releaseCtx); err != nil {
if err := dp.releasePower(ctx); err != nil {
return err
}
if err := dp.releaseLink(releaseCtx); err != nil {
if err := dp.releaseLink(ctx); err != nil {
return err
}
dp.completeRelease()
Expand Down
Loading
Loading