package app import ( "fmt" "os" "strings" "time" ) const ( transportSingBoxLegacyMigrateConfigKey = "singbox_legacy_unit_migrate" transportSingBoxLegacyMigrateDryRunConfigKey = "singbox_legacy_unit_migrate_dry_run" ) func transportSystemdMaybeMigrateLegacySingBoxUnits(client TransportClient, units []string) ([]string, []string) { out := make([]string, 0, 4) errOut := make([]string, 0, 4) if !transportSystemdLegacySingBoxMigrationEnabled(client) { return out, errOut } target := transportSystemdResolveSingBoxTemplateTargetUnit(client, units) if target == "" { return out, errOut } dryRun := transportSystemdLegacySingBoxMigrationDryRun(client) legacyUnits := transportSystemdLegacySingBoxUnitCandidates(client, target) if len(legacyUnits) == 0 { return out, errOut } migrated := make([]string, 0, len(legacyUnits)) for _, legacyUnit := range legacyUnits { if strings.EqualFold(strings.TrimSpace(legacyUnit), target) { continue } path := transportSystemdUnitPath(legacyUnit) owned, err := transportSystemdUnitOwnedByClient(path, client.ID) if err != nil { if os.IsNotExist(err) { continue } errOut = append(errOut, fmt.Sprintf("legacy-migrate ownership check failed (%s): %v", legacyUnit, err)) appendTraceLineRateLimited( "transport", fmt.Sprintf("legacy unit migrate warning: client=%s unit=%s ownership-check err=%v", client.ID, legacyUnit, err), 30*time.Second, ) continue } if !owned { appendTraceLineRateLimited( "transport", fmt.Sprintf("legacy unit migrate skip: client=%s unit=%s reason=ownership-mismatch", client.ID, legacyUnit), 30*time.Second, ) continue } if dryRun { msg := fmt.Sprintf("legacy-migrate dry-run: %s -> %s", legacyUnit, target) out = append(out, msg) appendTraceLineRateLimited( "transport", fmt.Sprintf("legacy unit migrate dry-run: client=%s from=%s to=%s", client.ID, legacyUnit, target), 20*time.Second, ) continue } transportSystemdRunLegacyMigrateCommand("stop", legacyUnit, &out, &errOut) transportSystemdRunLegacyMigrateCommand("disable", legacyUnit, &out, &errOut) if err := os.Remove(path); err != nil && !os.IsNotExist(err) { errOut = append(errOut, fmt.Sprintf("legacy-migrate remove failed (%s): %v", legacyUnit, err)) appendTraceLineRateLimited( "transport", fmt.Sprintf("legacy unit migrate warning: client=%s unit=%s remove err=%v", client.ID, legacyUnit, err), 30*time.Second, ) continue } out = append(out, fmt.Sprintf("legacy-migrate: %s -> %s", legacyUnit, target)) migrated = append(migrated, legacyUnit) appendTraceLine( "transport", fmt.Sprintf("legacy unit migrated: client=%s from=%s to=%s", client.ID, legacyUnit, target), ) } if dryRun || len(migrated) == 0 { return out, errOut } stdout, stderr, code, err := transportRunCommand(transportBackendActionTimeout, "systemctl", "daemon-reload") if s := strings.TrimSpace(stdout); s != "" { out = append(out, "legacy-migrate daemon-reload: "+s) } if s := strings.TrimSpace(stderr); s != "" { errOut = append(errOut, "legacy-migrate daemon-reload: "+s) } if err != nil || code != 0 { errOut = append(errOut, fmt.Sprintf("legacy-migrate daemon-reload failed (exit=%d)", code)) appendTraceLineRateLimited( "transport", fmt.Sprintf("legacy unit migrate warning: client=%s daemon-reload code=%d err=%v", client.ID, code, err), 20*time.Second, ) } for _, legacyUnit := range migrated { stdout, stderr, code, err = transportRunCommand(transportBackendActionTimeout, "systemctl", "reset-failed", legacyUnit) if s := strings.TrimSpace(stdout); s != "" { out = append(out, fmt.Sprintf("legacy-migrate reset-failed %s: %s", legacyUnit, s)) } if s := strings.TrimSpace(stderr); s != "" { errOut = append(errOut, fmt.Sprintf("legacy-migrate reset-failed %s: %s", legacyUnit, s)) } if err != nil || code != 0 { errOut = append(errOut, fmt.Sprintf("legacy-migrate reset-failed %s failed (exit=%d)", legacyUnit, code)) appendTraceLineRateLimited( "transport", fmt.Sprintf("legacy unit migrate warning: client=%s reset-failed unit=%s code=%d err=%v", client.ID, legacyUnit, code, err), 20*time.Second, ) } } return out, errOut } func transportSystemdRunLegacyMigrateCommand(action, unit string, out, errOut *[]string) { stdout, stderr, code, err := transportRunCommand(transportBackendActionTimeout, "systemctl", action, unit) if s := strings.TrimSpace(stdout); s != "" { *out = append(*out, fmt.Sprintf("legacy-migrate %s %s: %s", action, unit, s)) } if s := strings.TrimSpace(stderr); s != "" { *errOut = append(*errOut, fmt.Sprintf("legacy-migrate %s %s: %s", action, unit, s)) } if err == nil && code == 0 { return } if transportSystemdUnitMissingError(stdout, stderr, code, err) { return } *errOut = append(*errOut, fmt.Sprintf("legacy-migrate %s %s failed (exit=%d)", action, unit, code)) appendTraceLineRateLimited( "transport", fmt.Sprintf("legacy unit migrate warning: action=%s unit=%s code=%d err=%v", action, unit, code, err), 20*time.Second, ) } func transportSystemdLegacySingBoxMigrationEnabled(client TransportClient) bool { if client.Kind != TransportClientSingBox { return false } if transportConfigHasKey(client.Config, transportSingBoxLegacyMigrateConfigKey) { return transportConfigBool(client.Config, transportSingBoxLegacyMigrateConfigKey) } return true } func transportSystemdLegacySingBoxMigrationDryRun(client TransportClient) bool { return transportConfigBool(client.Config, transportSingBoxLegacyMigrateDryRunConfigKey) } func transportSystemdResolveSingBoxTemplateTargetUnit(client TransportClient, units []string) string { for _, unit := range units { u := strings.TrimSpace(unit) if transportSystemdSingBoxUsesTemplateInstance(client, u) { return u } } return "" } func transportSystemdLegacySingBoxUnitCandidates(client TransportClient, targetUnit string) []string { candidates := make([]string, 0, 4) addCandidate := func(unit string) { u := strings.TrimSpace(unit) if !validSystemdUnitName(u) { return } for _, existing := range candidates { if strings.EqualFold(existing, u) { return } } candidates = append(candidates, u) } instanceRaw := transportSystemdInstanceIDFromUnit(targetUnit) if instanceRaw != "" { addCandidate("singbox-" + strings.ToLower(instanceRaw) + ".service") if normalized := sanitizeID(instanceRaw); normalized != "" { addCandidate("singbox-" + normalized + ".service") } } if id := sanitizeID(client.ID); id != "" { addCandidate("singbox-" + id + ".service") } return candidates } func transportSystemdInstanceIDFromUnit(unit string) string { u := strings.TrimSpace(unit) if u == "" || !strings.HasSuffix(strings.ToLower(u), ".service") { return "" } at := strings.IndexByte(u, '@') if at <= 0 || at+1 >= len(u) { return "" } instance := strings.TrimSpace(strings.TrimSuffix(u[at+1:], ".service")) if instance == "" { return "" } return instance }