feat: complete daemon operations and recovery tooling

This commit is contained in:
2026-08-22 15:34:09 +08:00
parent a0a4a7556a
commit 1abe8df6ee
19 changed files with 1421 additions and 171 deletions
+51 -5
View File
@@ -38,6 +38,15 @@ type backendUpdater interface {
Restart(context.Context, backendupdate.ProgressReporter) (transaction.Transaction, error)
}
// backendDiagnoser 抽象了后端状态诊断与对账能力,供 status/doctor/reconcile 操作使用。
// 它由 backendstatus 包在运行时注入,便于在测试中用轻量替身替换。
type backendDiagnoser interface {
// Diagnose 执行只读诊断,返回结构化诊断结果。
Diagnose(context.Context) (daemonapi.Diagnosis, error)
// Reconcile 生成修复计划或执行自动修复,apply 为 true 时执行修复动作。
Reconcile(context.Context, bool) (daemonapi.Diagnosis, error)
}
// Server 持有本地 Unix Socket 并把收到的请求分派给事务编排器。
// 它负责连接的生命周期管理、请求解码与校验、执行进度回写以及最终结果的返回。
type Server struct {
@@ -45,24 +54,29 @@ type Server struct {
socketPath string
// updater 后端更新与重启动作的实际执行者。
updater backendUpdater
// diagnoser 后端状态诊断与对账动作的实际执行者。
diagnoser backendDiagnoser
// logger 用于记录服务运行与请求处理过程中的日志。
logger *slog.Logger
}
// New 构造一个 Server 实例并校验入参。
// socketPath 必须是规范化的绝对路径,updater 不能为空,logger 为空时回退为默认日志器。
// 当路径非法或 updater 为空时返回 error。
func New(socketPath string, updater backendUpdater, logger *slog.Logger) (*Server, error) {
// socketPath 必须是规范化的绝对路径,updater 与 diagnoser 不能为空,logger 为空时回退为默认日志器。
// 当路径非法或依赖为空时返回 error。
func New(socketPath string, updater backendUpdater, diagnoser backendDiagnoser, logger *slog.Logger) (*Server, error) {
if !filepath.IsAbs(socketPath) || filepath.Clean(socketPath) != socketPath {
return nil, errors.New("daemon Unix Socket path must be a clean absolute path")
}
if updater == nil {
return nil, errors.New("backend updater is required")
}
if diagnoser == nil {
return nil, errors.New("backend diagnoser is required")
}
if logger == nil {
logger = slog.Default()
}
return &Server{socketPath: socketPath, updater: updater, logger: logger}, nil
return &Server{socketPath: socketPath, updater: updater, diagnoser: diagnoser, logger: logger}, nil
}
// Serve 监听 Unix Socket 直到 ctx 被取消,返回前会等待所有连接处理完成并清理 Socket 文件。
@@ -176,8 +190,24 @@ func (s *Server) handle(ctx context.Context, connection net.Conn) {
return
}
record, updateErr = s.updater.Restart(ctx, report)
case daemonapi.OperationStatus, daemonapi.OperationDoctor:
if request.InputType != "" || request.File != "" || request.ImageReference != "" || request.Apply {
_ = s.writeResponse(connection, daemonapi.Response{Kind: daemonapi.ResponseResult, Error: request.Operation + " does not accept inputType, file, imageReference, or apply"})
return
}
diagnosis, diagnosisErr := s.diagnoser.Diagnose(ctx)
s.writeDiagnosis(ctx, connection, request.Operation, diagnosis, diagnosisErr)
return
case daemonapi.OperationReconcile:
if request.InputType != "" || request.File != "" || request.ImageReference != "" {
_ = s.writeResponse(connection, daemonapi.Response{Kind: daemonapi.ResponseResult, Error: "reconcile does not accept inputType, file, or imageReference"})
return
}
diagnosis, diagnosisErr := s.diagnoser.Reconcile(ctx, request.Apply)
s.writeDiagnosis(ctx, connection, request.Operation, diagnosis, diagnosisErr)
return
default:
_ = s.writeResponse(connection, daemonapi.Response{Kind: daemonapi.ResponseResult, Error: "operation must be update or restart"})
_ = s.writeResponse(connection, daemonapi.Response{Kind: daemonapi.ResponseResult, Error: "operation must be update, restart, status, doctor, or reconcile"})
return
}
response := daemonapi.Response{Kind: daemonapi.ResponseResult, TransactionID: record.ID, State: string(record.State)}
@@ -197,6 +227,22 @@ func (s *Server) writeResponse(connection net.Conn, response daemonapi.Response)
return json.NewEncoder(connection).Encode(response)
}
// writeDiagnosis 把 status/doctor/reconcile 的诊断结果写回客户端。
// 诊断失败时返回携带错误信息的响应并清空诊断结果,同时记录相应日志。
func (s *Server) writeDiagnosis(ctx context.Context, connection net.Conn, operation string, diagnosis daemonapi.Diagnosis, err error) {
response := daemonapi.Response{Kind: daemonapi.ResponseResult, Diagnosis: &diagnosis}
if err != nil {
response.Diagnosis = nil
response.Error = err.Error()
s.logger.ErrorContext(ctx, "backend diagnosis failed", "operation", operation, "error", err)
} else {
s.logger.InfoContext(ctx, "backend diagnosis completed", "operation", operation, "healthy", diagnosis.Healthy, "items", len(diagnosis.Items))
}
if writeErr := s.writeResponse(connection, response); writeErr != nil {
s.logger.ErrorContext(ctx, "write daemon diagnosis result", "operation", operation, "error", writeErr)
}
}
// decodeRequest 从 reader 中解码并校验一条客户端请求。
// 它限制请求体大小不超过 maximumRequestBytes,禁止未知字段,并要求请求只能包含一个 JSON 值,
// 且所有字符串字段不得带有前后空白字符;任一条不满足时返回 error。