fix(qsl): cancel an in-flight confirmation download on close / new download

The LoTW/QRZ confirmation download ran on the app-lifetime context, so it
kept going after the QSL Manager was closed and a new download didn't stop the
previous one. A slow QRZ sync therefore bled its "flag cleared" log into a
freshly started LoTW download — the operator saw QRZ activity during a LoTW
run. Now each download gets a cancellable context: starting one cancels the
previous, closing the window cancels it (CancelConfirmations), and the parse
loops bail promptly on cancel. (A LoTW http 503 is separate — ARRL's server.)
This commit is contained in:
2026-08-04 19:25:14 +02:00
parent bb3542c920
commit dfe3afbf2d
5 changed files with 55 additions and 7 deletions
+40 -4
View File
@@ -622,6 +622,12 @@ type App struct {
dvkRecSlot int // slot currently being recorded (DVKStartRecord → DVKStopRecord)
dvkPttKeyed bool // we keyed PTT for a voice message; unkey when it ends
pttMu sync.Mutex
// confDLMu/confDLCancel cancel the in-flight confirmation download (LoTW/QRZ)
// so closing the QSL Manager — or starting another download — stops the previous
// one instead of leaving it running against the app-lifetime context (which made
// a still-running QRZ sync bleed its log into a freshly started LoTW download).
confDLMu sync.Mutex
confDLCancel context.CancelFunc
udpLogMu sync.Mutex // serialises UDP auto-log so concurrent packets can't both pass the dedup check
adifMonMu sync.Mutex // guards the ADIF-monitor config (file list + per-file read offsets)
relayAutoMu sync.Mutex // serialises relay auto-control evaluation
@@ -10180,13 +10186,35 @@ func (a *App) DownloadConfirmations(service string, addNotFound bool, since stri
}
svc := extsvc.Service(service)
cfg := a.loadExternalServices()
go a.runDownloadConfirmations(svc, cfg, addNotFound, since)
// Cancel any download still running (a slow QRZ sync, say) before starting this
// one, so the two don't run at once and interleave their logs — and derive a
// cancellable context so closing the QSL Manager can stop it too.
a.confDLMu.Lock()
if a.confDLCancel != nil {
a.confDLCancel()
}
ctx, cancel := context.WithCancel(a.ctx)
a.confDLCancel = cancel
a.confDLMu.Unlock()
go a.runDownloadConfirmations(ctx, svc, cfg, addNotFound, since)
return nil
}
func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalServices, addNotFound bool, since string) {
// CancelConfirmations stops the in-flight LoTW/QRZ download — the QSL Manager
// calls it when the window closes, so work doesn't keep running in the background.
func (a *App) CancelConfirmations() {
a.confDLMu.Lock()
if a.confDLCancel != nil {
a.confDLCancel()
a.confDLCancel = nil
}
a.confDLMu.Unlock()
}
func (a *App) runDownloadConfirmations(ctx context.Context, svc extsvc.Service, cfg extsvc.ExternalServices, addNotFound bool, since string) {
emit := func(line string) {
if a.ctx != nil {
// Don't keep logging into a closed/cancelled window.
if a.ctx != nil && ctx.Err() == nil {
wruntime.EventsEmit(a.ctx, "qslmgr:log", line)
}
}
@@ -10195,7 +10223,6 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe
wruntime.EventsEmit(a.ctx, "qslmgr:done", map[string]any{"uploaded": matched, "total": total})
}
}
ctx := a.ctx
matched, total, added := 0, 0, 0
// resolveSince turns the UI's request into a concrete date (or ""):
@@ -10247,6 +10274,9 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe
var items []ConfirmationItem
var unmatched []string
perr := adif.Parse(strings.NewReader(adifText), func(rec adif.Record) error {
if ctx.Err() != nil {
return ctx.Err() // window closed / superseded
}
q, ok := adif.RecordToQSO(rec)
if !ok {
return nil
@@ -10429,6 +10459,9 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe
// QRZ FETCH returns headerless ADIF (no <EOH>); prepend one so the
// parser treats the stream as records.
perr := adif.Parse(strings.NewReader("<EOH>\n"+adifText), func(rec adif.Record) error {
if ctx.Err() != nil {
return ctx.Err() // window closed / superseded — stop clearing flags
}
parsed++
for k := range rec {
allKeys[k] = true
@@ -10598,6 +10631,9 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe
var items []ConfirmationItem
var unmatched []string
perr := adif.Parse(strings.NewReader(adifText), func(rec adif.Record) error {
if ctx.Err() != nil {
return ctx.Err() // window closed / superseded
}
q, ok := adif.RecordToQSO(rec)
if !ok {
return nil