mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-09-04 13:36:35 +02:00
Port !997 from v1.0 onto the context-aware master recovery lifecycle. Preserve master logging and resolver APIs while adding macOS default-route DHCP detection, temporary DNS-target cleanup, and atomic recovery ownership. Includes parser, lifecycle, failure, and concurrency regressions plus the corrected macOS QA helper. Relates to #533 and #597.
81 lines
2.8 KiB
Go
81 lines
2.8 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/Control-D-Inc/ctrld"
|
|
)
|
|
|
|
// beginRecovery atomically transfers ownership of shared recovery state. A
|
|
// network change cancels and replaces the current owner without exposing a nil
|
|
// recoveryCancel gap; other triggers are coalesced while an owner exists.
|
|
func (p *prog) beginRecovery(reason RecoveryReason) (ctx context.Context, gen uint64, intercept bool, ok bool) {
|
|
p.recoveryCancelMu.Lock()
|
|
defer p.recoveryCancelMu.Unlock()
|
|
|
|
if reason != RecoveryReasonNetworkChange && p.recoveryCancel != nil {
|
|
return nil, 0, false, false
|
|
}
|
|
if p.recoveryCancel != nil {
|
|
p.recoveryCancel()
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
gen = p.recoveryGen.Add(1)
|
|
intercept = dnsIntercept && p.dnsInterceptState != nil
|
|
p.recoveryCancel = cancel
|
|
p.recoveryRunning.Store(true)
|
|
p.recoveryBypass.Store(intercept)
|
|
return ctx, gen, intercept, true
|
|
}
|
|
|
|
func (p *prog) recoveryOwnsState(gen uint64) bool {
|
|
p.recoveryCancelMu.Lock()
|
|
defer p.recoveryCancelMu.Unlock()
|
|
return p.recoveryGen.Load() == gen && p.recoveryCancel != nil
|
|
}
|
|
|
|
func (p *prog) systemNameserversForInterceptRetry() []string {
|
|
loggerCtx := ctrld.LoggerCtx(context.Background(), p.logger.Load())
|
|
_, system := initializeOsResolverWithSystemNameserversFn(loggerCtx, true)
|
|
if system == nil {
|
|
return []string{}
|
|
}
|
|
return system
|
|
}
|
|
|
|
// completeRecovery releases shared state only if gen still owns it. The bypass
|
|
// reset is unconditional because live intercept state can disappear while a
|
|
// recovery is running, but a stale true flag still affects proxy routing.
|
|
func (p *prog) completeRecoveryState(gen uint64) bool {
|
|
p.recoveryCancelMu.Lock()
|
|
defer p.recoveryCancelMu.Unlock()
|
|
if p.recoveryGen.Load() != gen || p.recoveryCancel == nil {
|
|
return false
|
|
}
|
|
p.recoveryBypass.Store(false)
|
|
p.recoveryRunning.Store(false)
|
|
p.recoveryCancel = nil
|
|
return true
|
|
}
|
|
|
|
// recoveryCanceledCleanup resets shared recovery state after a canceled or
|
|
// failed recovery, but only when the recovery identified by gen was NOT
|
|
// superseded by a newer one (issue #597).
|
|
//
|
|
// A network-change cancellation is normally followed immediately by a new
|
|
// handleRecovery that owns recoveryBypass/recoveryRunning/recoveryCancel;
|
|
// clearing them here would disable the successor's bypass mid-flight and
|
|
// make it uncancellable. But when the canceled recovery is the LAST one
|
|
// (e.g. the tail of a network flap burst), nothing else will ever clear the
|
|
// flags: the daemon would stay in recovery bypass forever — every query
|
|
// detouring to the OS resolver — and the DNS-settings watchdog would stay
|
|
// permanently disabled.
|
|
func (p *prog) recoveryCanceledCleanup(gen uint64) {
|
|
if !p.completeRecoveryState(gen) {
|
|
// Superseded: the newer recovery owns the shared state.
|
|
return
|
|
}
|
|
p.Info().Msg("Recovery canceled with no successor; cleared recovery state and DHCP bypass")
|
|
}
|