package app import ( "context" "errors" "fmt" "regexp" "strings" "time" "proxyctl/internal/profile" "proxyctl/internal/target" ) // MutationLock serializes all state-changing operations across processes. type MutationLock interface { Lock(context.Context) (unlock func(), err error) } // LogEntry is the privacy-safe operation record written to bounded local logs. type LogEntry struct { Time time.Time Operation string Target string Result string Detail string } type OperationLogger interface{ Write(LogEntry) error } type Maintenance struct { state profile.State targets []target.Target lock MutationLock logger OperationLogger } func NewMaintenance(state profile.State, targets []target.Target, lock MutationLock, logger OperationLogger) *Maintenance { return &Maintenance{state: state, targets: append([]target.Target(nil), targets...), lock: lock, logger: logger} } type Diagnostic struct { TargetID string Check target.Check } type DoctorReport struct{ Diagnostics []Diagnostic } func (r DoctorReport) Healthy() bool { for _, diagnostic := range r.Diagnostics { if !diagnostic.Check.OK { return false } } return true } func (r DoctorReport) String() string { var output strings.Builder for _, diagnostic := range r.Diagnostics { status := "OK" if !diagnostic.Check.OK { status = "FAIL" } name := diagnostic.Check.Name if diagnostic.TargetID != "" { name = diagnostic.TargetID + ": " + name } fmt.Fprintf(&output, "[%s] %s — %s\n", status, name, profile.RedactText(diagnostic.Check.Evidence)) } return output.String() } // Doctor only invokes observational Target methods. func (m *Maintenance) Doctor(ctx context.Context, applicationChecks []target.Check) DoctorReport { report := DoctorReport{} for _, check := range applicationChecks { check.Evidence = profile.RedactText(check.Evidence) report.Diagnostics = append(report.Diagnostics, Diagnostic{Check: check}) } active := activeFromState(m.state) for _, adapter := range m.targets { availability := adapter.Availability(ctx) report.Diagnostics = append(report.Diagnostics, Diagnostic{TargetID: adapter.ID(), Check: target.Check{Name: "Target availability", OK: availability.Available, Evidence: availability.Evidence}}) inspection := adapter.Inspect(ctx, active) report.Diagnostics = append(report.Diagnostics, Diagnostic{TargetID: adapter.ID(), Check: target.Check{Name: "detected state", OK: inspection.State != target.Drifted, Evidence: string(inspection.State) + ": " + inspection.Evidence}}) if diagnosable, ok := adapter.(target.Diagnosable); ok { for _, check := range diagnosable.DiagnosticChecks(ctx, active) { check.Evidence = profile.RedactText(check.Evidence) report.Diagnostics = append(report.Diagnostics, Diagnostic{TargetID: adapter.ID(), Check: check}) } } } return report } type ResetTargetResult struct { TargetID string Inspection target.Inspection Skipped bool Err error } type ResetResult struct { Cancelled bool Targets []ResetTargetResult Err error } func (m *Maintenance) Reset(ctx context.Context, confirmed bool) ResetResult { if !confirmed { return ResetResult{Cancelled: true} } if m.lock == nil { return ResetResult{Err: errors.New("mutation lock is not configured")} } unlock, err := m.lock.Lock(ctx) if err != nil { return ResetResult{Err: fmt.Errorf("lock proxyctl mutation: %w", err)} } defer unlock() active := activeFromState(m.state) result := ResetResult{} for _, adapter := range m.targets { inspection := adapter.Inspect(ctx, active) if inspection.State != target.Enabled && inspection.State != target.Drifted { result.Targets = append(result.Targets, ResetTargetResult{TargetID: adapter.ID(), Inspection: inspection, Skipped: true}) continue } operation := adapter.SetDesired(ctx, false, active) detail, outcome := operation.Inspection.Evidence, "success" if operation.Err != nil { outcome = "failure" detail = operation.Err.Error() result.Err = errors.Join(result.Err, fmt.Errorf("reset %s Target: %w", adapter.ID(), redactError(operation.Err))) } detail = RedactLogDetail(detail) if m.logger != nil { _ = m.logger.Write(LogEntry{Time: time.Now(), Operation: "reset", Target: adapter.ID(), Result: outcome, Detail: detail}) } result.Targets = append(result.Targets, ResetTargetResult{TargetID: adapter.ID(), Inspection: operation.Inspection, Err: redactError(operation.Err)}) } return result } func activeFromState(state profile.State) *profile.Profile { for i := range state.Profiles { if state.Profiles[i].Name == state.ActiveProfile { value := state.Profiles[i] return &value } } return nil } var authenticatedURL = regexp.MustCompile(`(?i)(?:https?|socks5h?)://[^\s'\"<>]*@[^\s'\"<>]+`) // RedactLogDetail omits complete authenticated URLs, not just their passwords. func RedactLogDetail(value string) string { value = authenticatedURL.ReplaceAllString(value, "[REDACTED AUTHENTICATED URL]") return profile.RedactText(value) } func redactError(err error) error { if err == nil { return nil } return errors.New(profile.RedactText(err.Error())) }