package app import ( "net/http" "strings" "time" ) func executeTransportLifecycleActionLocked(id, action string) (int, TransportClientLifecycleResponse) { now := time.Now().UTC() transportMu.Lock() st := loadTransportClientsState() idx := findTransportClientIndex(st.Items, id) if idx < 0 { transportMu.Unlock() return http.StatusNotFound, TransportClientLifecycleResponse{ OK: false, Message: "not found", Code: "TRANSPORT_CLIENT_NOT_FOUND", } } it := st.Items[idx] prev := it.Status ifaces, err := syncTransportInterfacesWithClientsLocked(st.Items) if err != nil { transportMu.Unlock() return http.StatusOK, TransportClientLifecycleResponse{ OK: false, Message: "interfaces sync failed: " + err.Error(), Code: "TRANSPORT_INTERFACES_SAVE_FAILED", ClientID: id, Kind: it.Kind, Action: action, StatusBefore: prev, StatusAfter: it.Status, Health: it.Health, Runtime: transportRuntimeSnapshot(it, now), } } if bound, changed := applyTransportIfaceBinding(it, ifaces, now); changed { it = bound } peerStopPlans := []transportLifecyclePeerStopPlan(nil) if action == "start" || action == "restart" { peerStopPlans = planTransportSingBoxPeerStops(st.Items, idx, ifaces, now) } transportMu.Unlock() peerStopResults := []transportLifecyclePeerStopExecution(nil) peerStopSummary := transportBackendActionResult{OK: true, ExitCode: 0} if action == "start" || action == "restart" { peerStopResults = executeTransportLifecyclePeerStops(peerStopPlans) peerStopSummary = summarizeTransportLifecyclePeerStops(peerStopResults) } backend := selectTransportBackend(it) actionResult := transportBackendActionResult{ OK: true, ExitCode: 0, } if action == "start" || action == "restart" { if !peerStopSummary.OK { transportMu.Lock() st = loadTransportClientsState() idx = findTransportClientIndex(st.Items, id) if idx < 0 { transportMu.Unlock() return http.StatusNotFound, TransportClientLifecycleResponse{ OK: false, Message: "not found", Code: "TRANSPORT_CLIENT_NOT_FOUND", } } it = st.Items[idx] ifaces, err = syncTransportInterfacesWithClientsLocked(st.Items) if err != nil { transportMu.Unlock() return http.StatusOK, TransportClientLifecycleResponse{ OK: false, Message: "interfaces sync failed: " + err.Error(), Code: "TRANSPORT_INTERFACES_SAVE_FAILED", ClientID: id, Kind: it.Kind, Action: action, StatusBefore: prev, StatusAfter: it.Status, Health: it.Health, Runtime: transportRuntimeSnapshot(it, now), } } if bound, changed := applyTransportIfaceBinding(it, ifaces, now); changed { it = bound } peerEvents := applyTransportLifecyclePeerStopExecutionsLocked(&st, ifaces, now, peerStopResults) if len(peerStopResults) > 0 { if err := saveTransportClientsState(st); err != nil { transportMu.Unlock() return http.StatusOK, TransportClientLifecycleResponse{ OK: false, Message: "save failed: " + err.Error(), Code: "TRANSPORT_CLIENT_SAVE_FAILED", ClientID: id, Kind: it.Kind, Action: action, ExitCode: peerStopSummary.ExitCode, Stdout: peerStopSummary.Stdout, Stderr: peerStopSummary.Stderr, } } } for _, peerEvent := range peerEvents { events.push("transport_client_state_changed", map[string]any{ "id": peerEvent.ClientID, "from": peerEvent.From, "to": peerEvent.To, }) } transportMu.Unlock() if len(peerEvents) > 0 { clientIDs := make([]string, 0, len(peerEvents)) for _, peerEvent := range peerEvents { clientIDs = append(clientIDs, peerEvent.ClientID) } publishTransportRuntimeObservabilitySnapshotChanged( "transport_client_state_changed", clientIDs, nil, ) } msg := strings.TrimSpace(peerStopSummary.Message) if msg == "" { msg = "failed to stop conflicting singbox peers" } return http.StatusOK, TransportClientLifecycleResponse{ OK: false, Message: msg, Code: peerStopSummary.Code, ExitCode: peerStopSummary.ExitCode, Stdout: peerStopSummary.Stdout, Stderr: peerStopSummary.Stderr, ClientID: id, Kind: it.Kind, Action: action, StatusBefore: prev, StatusAfter: it.Status, Health: it.Health, Runtime: transportRuntimeSnapshot(it, now), } } } if peerStopSummary.OK { actionResult = backend.Action(it, action) actionResult.Stdout = joinNonEmptyLines(peerStopSummary.Stdout, actionResult.Stdout) actionResult.Stderr = joinNonEmptyLines(peerStopSummary.Stderr, actionResult.Stderr) } transportMu.Lock() st = loadTransportClientsState() idx = findTransportClientIndex(st.Items, id) if idx < 0 { transportMu.Unlock() return http.StatusNotFound, TransportClientLifecycleResponse{ OK: false, Message: "not found", Code: "TRANSPORT_CLIENT_NOT_FOUND", } } it = st.Items[idx] ifaces, err = syncTransportInterfacesWithClientsLocked(st.Items) if err != nil { transportMu.Unlock() return http.StatusOK, TransportClientLifecycleResponse{ OK: false, Message: "interfaces sync failed: " + err.Error(), Code: "TRANSPORT_INTERFACES_SAVE_FAILED", ClientID: id, Kind: it.Kind, Action: action, StatusBefore: prev, StatusAfter: it.Status, Health: it.Health, Runtime: transportRuntimeSnapshot(it, now), } } if bound, changed := applyTransportIfaceBinding(it, ifaces, now); changed { it = bound } peerEvents := applyTransportLifecyclePeerStopExecutionsLocked(&st, ifaces, now, peerStopResults) if actionResult.OK { applyTransportLifecycleAction(&it, action, now) it.Runtime.Backend = backend.ID() } else { applyTransportLifecycleFailure(&it, action, now, backend.ID(), actionResult) } st.Items[idx] = it if err := saveTransportClientsState(st); err != nil { transportMu.Unlock() return http.StatusOK, TransportClientLifecycleResponse{ OK: false, Message: "save failed: " + err.Error(), Code: "TRANSPORT_CLIENT_SAVE_FAILED", ClientID: id, Kind: it.Kind, Action: action, ExitCode: actionResult.ExitCode, Stdout: actionResult.Stdout, Stderr: actionResult.Stderr, } } for _, peerEvent := range peerEvents { events.push("transport_client_state_changed", map[string]any{ "id": peerEvent.ClientID, "from": peerEvent.From, "to": peerEvent.To, }) } events.push("transport_client_state_changed", map[string]any{ "id": id, "from": prev, "to": it.Status, }) queueRefresh := actionResult.OK && (action == "start" || action == "restart") transportMu.Unlock() publishClientIDs := make([]string, 0, len(peerEvents)+1) publishClientIDs = append(publishClientIDs, id) for _, peerEvent := range peerEvents { publishClientIDs = append(publishClientIDs, peerEvent.ClientID) } publishTransportRuntimeObservabilitySnapshotChanged( "transport_client_state_changed", publishClientIDs, nil, ) msg := strings.TrimSpace(actionResult.Message) if msg == "" { msg = action + " done" } resp := TransportClientLifecycleResponse{ OK: actionResult.OK, Message: msg, Code: actionResult.Code, ExitCode: actionResult.ExitCode, Stdout: actionResult.Stdout, Stderr: actionResult.Stderr, ClientID: id, Kind: it.Kind, Action: action, StatusBefore: prev, StatusAfter: it.Status, Health: it.Health, Runtime: transportRuntimeSnapshot(it, now), } if queueRefresh { _, _ = egressIdentitySWR.queueRefresh([]string{"transport:" + id}, true) } if !actionResult.OK { return http.StatusOK, resp } return http.StatusOK, resp }