219 lines
6.9 KiB
Go
219 lines
6.9 KiB
Go
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
|
|
}
|