mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-11-13 00:14:22 +08:00
140 lines
5.1 KiB
Go
140 lines
5.1 KiB
Go
package socket
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type agentServiceManager struct{ kind, control, runner string }
|
|
|
|
// Alpine must never select a systemctl compatibility shim. Elsewhere prefer
|
|
// an active systemd instance, matching the installer, then OpenRC.
|
|
func detectAgentServiceManager(alpine, systemd bool, lookup func(string) (string, error)) (agentServiceManager, error) {
|
|
if !alpine && systemd {
|
|
control, err := lookup("systemctl")
|
|
if err != nil {
|
|
return agentServiceManager{}, fmt.Errorf("systemctl 不可用: %w", err)
|
|
}
|
|
runner, err := lookup("systemd-run")
|
|
if err != nil {
|
|
return agentServiceManager{}, fmt.Errorf("systemd-run 不可用: %w", err)
|
|
}
|
|
return agentServiceManager{kind: "systemd", control: control, runner: runner}, nil
|
|
}
|
|
control, err := lookup("rc-service")
|
|
if err != nil {
|
|
return agentServiceManager{}, fmt.Errorf("未检测到受支持的服务管理器 (systemd/OpenRC): %w", err)
|
|
}
|
|
return agentServiceManager{kind: "openrc", control: control}, nil
|
|
}
|
|
|
|
func (w *WebSocketReporter) agentRestartLauncher(cleanLegacy bool) (func(string, string) error, error) {
|
|
// Existing per-reporter seam is also used by checksum-only tests.
|
|
if w.startUpgradeRestart != nil {
|
|
return w.startUpgradeRestart, nil
|
|
}
|
|
detect := w.detectAgentManager
|
|
if detect == nil {
|
|
detect = func() (agentServiceManager, error) {
|
|
_, alpineErr := os.Stat("/etc/alpine-release")
|
|
_, systemdErr := os.Stat("/run/systemd/system")
|
|
return detectAgentServiceManager(alpineErr == nil, systemdErr == nil, exec.LookPath)
|
|
}
|
|
}
|
|
manager, err := detect()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if manager.kind == "openrc" {
|
|
if err := checkAgentDetach(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return func(staged, binary string) error {
|
|
script := buildManagedAgentRestartScript(manager, staged, binary, cleanLegacy)
|
|
if manager.kind == "systemd" {
|
|
// Wait only for submission, never for helper completion. A separate unit is
|
|
// essential: systemctl stop kills every process in the agent's cgroup.
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
out, err := exec.CommandContext(ctx, manager.runner, "--quiet", "/bin/sh", "-c", script).CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("提交重启任务失败: %w: %s", err, strings.TrimSpace(string(out)))
|
|
}
|
|
return nil
|
|
}
|
|
return startDetachedAgentHelper(script)
|
|
}, nil
|
|
}
|
|
|
|
func shellQuote(value string) string { return "'" + strings.ReplaceAll(value, "'", "'\"'\"'") + "'" }
|
|
|
|
func buildManagedAgentRestartScript(manager agentServiceManager, staged, binary string, cleanLegacy bool) string {
|
|
control := shellQuote(manager.control)
|
|
stop, start := control+" stop flux_agent", control+" start flux_agent"
|
|
if manager.kind == "openrc" {
|
|
stop, start = control+" flux_agent stop", control+" flux_agent start"
|
|
}
|
|
legacy := ""
|
|
if manager.kind == "systemd" && cleanLegacy {
|
|
legacy = `legacy_service=''; for service_file in /etc/systemd/system/gost.service /lib/systemd/system/gost.service /usr/lib/systemd/system/gost.service; do if [ -f "$service_file" ] && grep -Fq "WorkingDirectory=/etc/gost" "$service_file" && (grep -Fq "ExecStart=/etc/gost/gost" "$service_file" || (grep -Fq "ExecStart=/usr/local/bin/gost" "$service_file" && [ -f /etc/gost/config.json ] && [ -f /etc/gost/gost.json ])); then legacy_service="$service_file"; break; fi; done; if [ -n "$legacy_service" ]; then (` + control + ` stop gost 2>/dev/null || true); (` + control + ` disable gost 2>/dev/null || true); rm -f /usr/local/bin/gost /etc/gost/gost "$legacy_service"; (` + control + ` daemon-reload 2>/dev/null || true); fi; `
|
|
}
|
|
// The caller owns staging until launch. Afterwards the helper always removes
|
|
// it, including stop/replace failures. Never truncate the running executable.
|
|
return "trap " + shellQuote("rm -f -- "+shellQuote(staged)) + " EXIT; sleep 1; " + stop + " || exit $?; " + legacy +
|
|
"if [ ! -d " + shellQuote(binary) + " ] && mv -f -- " + shellQuote(staged) + " " + shellQuote(binary) + "; then " + start + "; else status=$?; " + start + "; exit \"$status\"; fi"
|
|
}
|
|
|
|
// Copy to a unique file on the destination filesystem, then let the caller
|
|
// atomically rename. Failure never truncates a previous usable backup.
|
|
func stageAgentCopy(source, destination string) (path string, err error) {
|
|
in, err := os.Open(source)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer in.Close()
|
|
info, err := in.Stat()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !info.Mode().IsRegular() {
|
|
return "", fmt.Errorf("不是普通文件: %s", source)
|
|
}
|
|
out, err := os.CreateTemp(filepath.Dir(destination), ".flux-agent-*")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
path = out.Name()
|
|
defer func() {
|
|
out.Close()
|
|
if err != nil {
|
|
os.Remove(path)
|
|
}
|
|
}()
|
|
if _, err = io.Copy(out, in); err != nil {
|
|
return path, err
|
|
}
|
|
if err = out.Chmod(0755); err != nil {
|
|
return path, err
|
|
}
|
|
if err = out.Close(); err != nil {
|
|
return path, err
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
func backupAgentBinary(binary string) error {
|
|
staged, err := stageAgentCopy(binary, binary+".old")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer os.Remove(staged)
|
|
return os.Rename(staged, binary+".old")
|
|
}
|