Skip to content

Commit c5c4acd

Browse files
committed
fix(deploy): 加固客户端部署与更新流程
1 parent 86820c8 commit c5c4acd

23 files changed

Lines changed: 1297 additions & 476 deletions

cmd/daemon.go

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import (
77
"os/exec"
88
"os/signal"
99
"path/filepath"
10-
"strconv"
11-
"strings"
1210
"sync"
1311
"syscall"
1412
"time"
@@ -25,6 +23,12 @@ func CreateDaemonCmd() *cobra.Command {
2523
Short: "启动守护进程(后台运行)",
2624
Long: "在后台启动证书部署守护进程,进程崩溃或更新后将自动重启",
2725
RunE: func(cmd *cobra.Command, args []string) error {
26+
releaseLock, err := acquireDaemonStartLock()
27+
if err != nil {
28+
return err
29+
}
30+
defer releaseLock()
31+
2832
// 检查是否已经在运行,如果是则先停止
2933
if IsRunning() {
3034
fmt.Println("守护进程已在运行,正在重启...")
@@ -113,7 +117,7 @@ func runSupervisor() {
113117

114118
// 将 supervisor 自身的 PID 写入 PID 文件
115119
pidFile := GetPIDFile()
116-
if err := os.WriteFile(pidFile, []byte(strconv.Itoa(os.Getpid())), 0600); err != nil {
120+
if err := writePIDRecord(pidFile, pidFileRecord{PID: os.Getpid(), Executable: execPath, StartedAt: time.Now()}); err != nil {
117121
fmt.Printf("写入PID文件失败: %v\n", err)
118122
return
119123
}
@@ -239,30 +243,37 @@ func getHomeDir() string {
239243
func StopDaemon() error {
240244
// 保留 stop marker 作为备用停止机制
241245
stopMarker := filepath.Join(getHomeDir(), ".anssl-stop")
242-
os.WriteFile(stopMarker, []byte("stop"), 0600)
246+
if err := os.WriteFile(stopMarker, []byte("stop"), 0600); err != nil {
247+
return fmt.Errorf("写入停止标记失败: %w", err)
248+
}
243249

244250
pidFile := GetPIDFile()
245-
data, err := os.ReadFile(pidFile)
251+
record, err := readPIDRecord(pidFile)
246252
if err != nil {
253+
_ = os.Remove(stopMarker)
247254
return fmt.Errorf("读取PID文件失败: %w", err)
248255
}
249-
250-
pidStr := strings.TrimSpace(string(data))
251-
pid, err := strconv.Atoi(pidStr)
252-
if err != nil {
253-
return fmt.Errorf("无效的PID: %w", err)
256+
if !supervisorProcessMatches(record) {
257+
if removeErr := os.Remove(pidFile); removeErr != nil && !os.IsNotExist(removeErr) {
258+
return fmt.Errorf("PID 文件不是当前 supervisor 且清理失败: %w", removeErr)
259+
}
260+
_ = os.Remove(stopMarker)
261+
return fmt.Errorf("PID 文件不是当前 anssl supervisor,未发送停止信号")
254262
}
255263

256-
process, err := os.FindProcess(pid)
264+
process, err := os.FindProcess(record.PID)
257265
if err != nil {
266+
_ = os.Remove(stopMarker)
258267
return fmt.Errorf("查找进程失败: %w", err)
259268
}
260269

261270
// 发送 SIGTERM,supervisor 的信号处理会级联杀掉 worker
262271
if err := process.Signal(syscall.SIGTERM); err != nil {
263272
// 进程可能已经退出
264-
os.Remove(pidFile)
265-
os.Remove(stopMarker)
273+
if removeErr := os.Remove(pidFile); removeErr != nil && !os.IsNotExist(removeErr) {
274+
return fmt.Errorf("清理 PID 文件失败: %w", removeErr)
275+
}
276+
_ = os.Remove(stopMarker)
266277
return nil
267278
}
268279

@@ -271,18 +282,21 @@ func StopDaemon() error {
271282
time.Sleep(1 * time.Second)
272283
if err := process.Signal(syscall.Signal(0)); err != nil {
273284
// 进程已退出
274-
os.Remove(pidFile)
275-
os.Remove(stopMarker)
285+
_ = os.Remove(pidFile)
286+
_ = os.Remove(stopMarker)
276287
return nil
277288
}
278289
}
279290

280291
// 超时,强制杀死
281-
process.Signal(syscall.SIGKILL)
292+
if err := process.Signal(syscall.SIGKILL); err != nil {
293+
_ = os.Remove(stopMarker)
294+
return fmt.Errorf("强制停止守护进程失败: %w", err)
295+
}
282296
time.Sleep(500 * time.Millisecond)
283297

284-
os.Remove(pidFile)
285-
os.Remove(stopMarker)
298+
_ = os.Remove(pidFile)
299+
_ = os.Remove(stopMarker)
286300

287301
return nil
288302
}

cmd/pid.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package cmd
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
"runtime"
10+
"strconv"
11+
"strings"
12+
"time"
13+
)
14+
15+
// pidFileRecord identifies the supervisor process that owns the daemon PID file.
16+
type pidFileRecord struct {
17+
PID int `json:"pid"` // PID 是 supervisor 进程号。
18+
Executable string `json:"executable"` // Executable 是启动 supervisor 的可执行文件路径。
19+
StartedAt time.Time `json:"startedAt"` // StartedAt 是 supervisor 启动时间。
20+
}
21+
22+
// writePIDRecord atomically writes a structured daemon PID record.
23+
func writePIDRecord(path string, record pidFileRecord) error {
24+
data, err := json.Marshal(record)
25+
if err != nil {
26+
return err
27+
}
28+
temp, err := os.CreateTemp(filepath.Dir(path), ".anssl-pid-*")
29+
if err != nil {
30+
return err
31+
}
32+
tempPath := temp.Name()
33+
defer func() {
34+
_ = temp.Close()
35+
_ = os.Remove(tempPath)
36+
}()
37+
if err := temp.Chmod(0600); err != nil {
38+
return err
39+
}
40+
if _, err := temp.Write(append(data, '\n')); err != nil {
41+
return err
42+
}
43+
if err := temp.Sync(); err != nil {
44+
return err
45+
}
46+
if err := temp.Close(); err != nil {
47+
return err
48+
}
49+
return os.Rename(tempPath, path)
50+
}
51+
52+
// readPIDRecord reads structured PID files and accepts legacy numeric files during upgrade.
53+
func readPIDRecord(path string) (pidFileRecord, error) {
54+
data, err := os.ReadFile(path)
55+
if err != nil {
56+
return pidFileRecord{}, err
57+
}
58+
var record pidFileRecord
59+
if json.Unmarshal(data, &record) == nil && record.PID > 0 {
60+
return record, nil
61+
}
62+
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
63+
if err != nil || pid <= 0 {
64+
return pidFileRecord{}, fmt.Errorf("无效的 PID 文件")
65+
}
66+
return pidFileRecord{PID: pid}, nil
67+
}
68+
69+
// supervisorProcessMatches verifies that a PID belongs to an anssl supervisor process.
70+
func supervisorProcessMatches(record pidFileRecord) bool {
71+
if record.PID <= 0 {
72+
return false
73+
}
74+
commandLine, err := processCommandLine(record.PID)
75+
if err != nil || !strings.Contains(commandLine, "_supervisor") {
76+
return false
77+
}
78+
if record.Executable == "" {
79+
return true
80+
}
81+
return strings.Contains(commandLine, filepath.Base(record.Executable))
82+
}
83+
84+
// processCommandLine returns a process command line using the native inspection tool.
85+
func processCommandLine(pid int) (string, error) {
86+
if runtime.GOOS == "windows" {
87+
filter := fmt.Sprintf("(Get-CimInstance Win32_Process -Filter \"ProcessId = %d\").CommandLine", pid)
88+
output, err := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", filter).CombinedOutput()
89+
return strings.TrimSpace(string(output)), err
90+
}
91+
psPath := "/bin/ps"
92+
if _, err := os.Stat(psPath); err != nil {
93+
psPath = "/usr/bin/ps"
94+
}
95+
output, err := exec.Command(psPath, "-p", strconv.Itoa(pid), "-o", "command=").CombinedOutput()
96+
return strings.TrimSpace(string(output)), err
97+
}
98+
99+
// acquireDaemonStartLock prevents concurrent daemon and restart commands from spawning supervisors.
100+
func acquireDaemonStartLock() (func(), error) {
101+
path := GetPIDFile() + ".lock"
102+
for attempt := 0; attempt < 2; attempt++ {
103+
file, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
104+
if err == nil {
105+
if _, err := fmt.Fprintf(file, "%d\n", os.Getpid()); err != nil {
106+
_ = file.Close()
107+
_ = os.Remove(path)
108+
return nil, fmt.Errorf("写入守护进程启动锁失败: %w", err)
109+
}
110+
if err := file.Close(); err != nil {
111+
_ = os.Remove(path)
112+
return nil, fmt.Errorf("关闭守护进程启动锁失败: %w", err)
113+
}
114+
return func() { _ = os.Remove(path) }, nil
115+
}
116+
if !os.IsExist(err) {
117+
return nil, fmt.Errorf("创建守护进程启动锁失败: %w", err)
118+
}
119+
data, readErr := os.ReadFile(path)
120+
ownerPID, parseErr := strconv.Atoi(strings.TrimSpace(string(data)))
121+
if readErr == nil && parseErr == nil {
122+
if commandLine, inspectErr := processCommandLine(ownerPID); inspectErr == nil && commandLine != "" {
123+
return nil, fmt.Errorf("已有另一个启动或重启操作正在进行")
124+
}
125+
}
126+
if removeErr := os.Remove(path); removeErr != nil && !os.IsNotExist(removeErr) {
127+
return nil, fmt.Errorf("清理失效启动锁失败: %w", removeErr)
128+
}
129+
}
130+
return nil, fmt.Errorf("创建守护进程启动锁失败")
131+
}

cmd/restart.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ func CreateRestartCmd() *cobra.Command {
1616
Short: "重启守护进程",
1717
Long: "重启证书部署守护进程",
1818
RunE: func(cmd *cobra.Command, args []string) error {
19+
releaseLock, err := acquireDaemonStartLock()
20+
if err != nil {
21+
return err
22+
}
23+
defer releaseLock()
24+
1925
if IsRunning() {
2026
if err := StopDaemon(); err != nil {
2127
return fmt.Errorf("停止守护进程失败: %w", err)

cmd/root.go

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import (
44
"os"
55
"path/filepath"
66
"strconv"
7-
"strings"
8-
"syscall"
97

108
"github.com/spf13/cobra"
119
)
@@ -79,32 +77,19 @@ func GetLogFile() string {
7977
// IsRunning 检查守护进程是否在运行
8078
func IsRunning() bool {
8179
pidFile := GetPIDFile()
82-
data, err := os.ReadFile(pidFile)
80+
record, err := readPIDRecord(pidFile)
8381
if err != nil {
8482
return false
8583
}
86-
87-
pidStr := strings.TrimSpace(string(data))
88-
pid, err := strconv.Atoi(pidStr)
89-
if err != nil {
90-
return false
91-
}
92-
93-
process, err := os.FindProcess(pid)
94-
if err != nil {
95-
return false
96-
}
97-
98-
err = process.Signal(syscall.Signal(0))
99-
return err == nil
84+
return supervisorProcessMatches(record)
10085
}
10186

10287
// GetPID 获取守护进程PID
10388
func GetPID() string {
10489
pidFile := GetPIDFile()
105-
data, err := os.ReadFile(pidFile)
90+
record, err := readPIDRecord(pidFile)
10691
if err != nil {
10792
return "unknown"
10893
}
109-
return strings.TrimSpace(string(data))
94+
return strconv.Itoa(record.PID)
11095
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/spf13/viper v1.21.0
1313
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.129
1414
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.3.105
15+
golang.org/x/net v0.56.0
1516
google.golang.org/protobuf v1.36.11
1617
)
1718

@@ -42,7 +43,6 @@ require (
4243
github.com/subosito/gotenv v1.6.0 // indirect
4344
github.com/tjfoc/gmsm v1.4.1 // indirect
4445
go.yaml.in/yaml/v3 v3.0.4 // indirect
45-
golang.org/x/net v0.56.0 // indirect
4646
golang.org/x/sys v0.46.0 // indirect
4747
golang.org/x/text v0.39.0 // indirect
4848
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect

0 commit comments

Comments
 (0)