package app import ( "strings" "time" ) type transportNetnsPeerMatch struct { Index int Binding transportIfaceBinding } type transportLifecyclePeerStopPlan struct { ClientID string Client TransportClient } type transportLifecyclePeerStopExecution struct { ClientID string BackendID string Result transportBackendActionResult } type transportLifecycleStateChange struct { ClientID string From TransportClientStatus To TransportClientStatus } func matchTransportSingBoxPeersInSameNetns( items []TransportClient, targetIdx int, ifaces transportInterfacesState, ) (transportIfaceBinding, []transportNetnsPeerMatch) { if targetIdx < 0 || targetIdx >= len(items) { return transportIfaceBinding{}, nil } target := items[targetIdx] targetBinding := resolveTransportIfaceBinding(target, ifaces) if target.Kind != TransportClientSingBox || !transportNetnsEnabled(target) { return targetBinding, nil } targetNS := strings.TrimSpace(targetBinding.NetnsName) if targetNS == "" { return targetBinding, nil } peers := make([]transportNetnsPeerMatch, 0, 2) for i := range items { if i == targetIdx { continue } peer := items[i] if peer.Kind != TransportClientSingBox { continue } if !transportNetnsEnabled(peer) { continue } if normalizeTransportStatus(peer.Status) == TransportClientDown { continue } peerBinding := resolveTransportIfaceBinding(peer, ifaces) if strings.TrimSpace(peerBinding.NetnsName) != targetNS { continue } peers = append(peers, transportNetnsPeerMatch{ Index: i, Binding: peerBinding, }) } return targetBinding, peers } func matchTransportSingBoxPeersInSameNetnsForLock( items []TransportClient, targetIdx int, ifaces transportInterfacesState, ) (transportIfaceBinding, []transportNetnsPeerMatch) { if targetIdx < 0 || targetIdx >= len(items) { return transportIfaceBinding{}, nil } target := items[targetIdx] targetBinding := resolveTransportIfaceBinding(target, ifaces) if target.Kind != TransportClientSingBox || !transportNetnsEnabled(target) { return targetBinding, nil } targetNS := strings.TrimSpace(targetBinding.NetnsName) if targetNS == "" { return targetBinding, nil } peers := make([]transportNetnsPeerMatch, 0, 2) for i := range items { if i == targetIdx { continue } peer := items[i] if peer.Kind != TransportClientSingBox { continue } if !transportNetnsEnabled(peer) { continue } peerBinding := resolveTransportIfaceBinding(peer, ifaces) if strings.TrimSpace(peerBinding.NetnsName) != targetNS { continue } peers = append(peers, transportNetnsPeerMatch{ Index: i, Binding: peerBinding, }) } return targetBinding, peers } func planTransportSingBoxPeerStops( items []TransportClient, targetIdx int, ifaces transportInterfacesState, now time.Time, ) []transportLifecyclePeerStopPlan { _, peers := matchTransportSingBoxPeersInSameNetns(items, targetIdx, ifaces) if len(peers) == 0 { return nil } plans := make([]transportLifecyclePeerStopPlan, 0, len(peers)) for _, peerMatch := range peers { if peerMatch.Index < 0 || peerMatch.Index >= len(items) { continue } peer := items[peerMatch.Index] if bound, changed := applyTransportIfaceBinding(peer, ifaces, now); changed { peer = bound } plans = append(plans, transportLifecyclePeerStopPlan{ ClientID: peer.ID, Client: peer, }) } return plans } func executeTransportLifecyclePeerStops( plans []transportLifecyclePeerStopPlan, ) []transportLifecyclePeerStopExecution { if len(plans) == 0 { return nil } results := make([]transportLifecyclePeerStopExecution, 0, len(plans)) for _, plan := range plans { backend := selectTransportBackend(plan.Client) stopRes := backend.Action(plan.Client, "stop") results = append(results, transportLifecyclePeerStopExecution{ ClientID: plan.ClientID, BackendID: backend.ID(), Result: stopRes, }) if !stopRes.OK { break } } return results } func summarizeTransportLifecyclePeerStops( results []transportLifecyclePeerStopExecution, ) transportBackendActionResult { if len(results) == 0 { return transportBackendActionResult{OK: true, ExitCode: 0} } stopped := make([]string, 0, len(results)) aggOut := make([]string, 0, len(results)) aggErr := make([]string, 0, len(results)) for _, res := range results { if s := strings.TrimSpace(res.Result.Stdout); s != "" { aggOut = append(aggOut, res.ClientID+": "+s) } if s := strings.TrimSpace(res.Result.Stderr); s != "" { aggErr = append(aggErr, res.ClientID+": "+s) } if !res.Result.OK { msg := strings.TrimSpace(res.Result.Message) if msg == "" { msg = "stop conflicting peer failed: " + res.ClientID } return transportBackendActionResult{ OK: false, Code: res.Result.Code, Message: msg, ExitCode: res.Result.ExitCode, Stdout: strings.Join(aggOut, "\n"), Stderr: strings.Join(aggErr, "\n"), } } stopped = append(stopped, res.ClientID) } if len(stopped) == 0 { return transportBackendActionResult{OK: true, ExitCode: 0} } return transportBackendActionResult{ OK: true, ExitCode: 0, Message: "stopped conflicting peers: " + strings.Join(stopped, ", "), Stdout: strings.Join(aggOut, "\n"), Stderr: strings.Join(aggErr, "\n"), } } func applyTransportLifecyclePeerStopExecutionsLocked( st *transportClientsState, ifaces transportInterfacesState, now time.Time, results []transportLifecyclePeerStopExecution, ) []transportLifecycleStateChange { if st == nil || len(results) == 0 { return nil } changes := make([]transportLifecycleStateChange, 0, len(results)) for _, res := range results { idx := findTransportClientIndex(st.Items, res.ClientID) if idx < 0 { continue } peer := st.Items[idx] if bound, changed := applyTransportIfaceBinding(peer, ifaces, now); changed { peer = bound } prev := peer.Status if res.Result.OK { applyTransportLifecycleAction(&peer, "stop", now) peer.Runtime.Backend = res.BackendID } else { applyTransportLifecycleFailure(&peer, "stop", now, res.BackendID, res.Result) } st.Items[idx] = peer if prev != peer.Status { changes = append(changes, transportLifecycleStateChange{ ClientID: peer.ID, From: prev, To: peer.Status, }) } } return changes } func joinNonEmptyLines(parts ...string) string { lines := make([]string, 0, len(parts)) for _, part := range parts { v := strings.TrimSpace(part) if v == "" { continue } lines = append(lines, v) } return strings.Join(lines, "\n") } func stopTransportSingBoxPeersInSameNetnsLocked( st *transportClientsState, targetIdx int, now time.Time, ) transportBackendActionResult { if st == nil || targetIdx < 0 || targetIdx >= len(st.Items) { return transportBackendActionResult{ OK: false, Code: "TRANSPORT_CLIENT_NOT_FOUND", Message: "target client not found", ExitCode: -1, } } ifaces, err := syncTransportInterfacesWithClientsLocked(st.Items) if err != nil { return transportBackendActionResult{ OK: false, Code: "TRANSPORT_INTERFACES_SAVE_FAILED", Message: "interfaces sync failed: " + err.Error(), ExitCode: -1, } } plans := planTransportSingBoxPeerStops(st.Items, targetIdx, ifaces, now) results := executeTransportLifecyclePeerStops(plans) summary := summarizeTransportLifecyclePeerStops(results) applyTransportLifecyclePeerStopExecutionsLocked(st, ifaces, now, results) return summary }