feat: complete daemon operations and recovery tooling
This commit is contained in:
@@ -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。
|
||||
|
||||
Reference in New Issue
Block a user