133 lines
4.5 KiB
Go
133 lines
4.5 KiB
Go
package adminconsole
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
"unicode"
|
||
|
||
"git.pcux.cn/xuans/Scert/backend/internal/deployment"
|
||
"git.pcux.cn/xuans/Scert/backend/internal/localreset"
|
||
)
|
||
|
||
type recoveryClient interface {
|
||
ListRecoveries(string) (deployment.PendingRecoveryPage, error)
|
||
InspectRecovery(string) (deployment.RecoveryInspection, error)
|
||
}
|
||
|
||
func recoveryFlow(ctx context.Context, client recoveryClient, screen terminal) error {
|
||
if err := screen.Show("只读核查:不写入文件、不执行远端命令、不删锁、不改变事务状态。结果是当前采样,不证明历史部署成功。每页显示 5 条。"); err != nil {
|
||
return err
|
||
}
|
||
cursor := ""
|
||
for {
|
||
if err := ctx.Err(); err != nil {
|
||
return err
|
||
}
|
||
page, err := client.ListRecoveries(cursor)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if len(page.Items) == 0 {
|
||
return screen.Show("当前没有更多待核查的部署事务。")
|
||
}
|
||
for index, item := range page.Items {
|
||
line := fmt.Sprintf("%d. 事务:%s;服务器:%s;阶段:%s;状态:%s;历史证据:%s", index+1,
|
||
terminalValue(item.ID), terminalValue(item.ServerID), recoveryLabel(item.Phase), recoveryLabel(item.Status), evidenceLabel(item.HistoricalEvidence))
|
||
if err := screen.Show(line); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
choice, err := screen.Read(ctx, "输入本页序号进行只读核查;输入 n 查看下一页;输入 q 退出", false)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err := ctx.Err(); err != nil {
|
||
return err
|
||
}
|
||
switch strings.TrimSpace(choice) {
|
||
case "q":
|
||
return nil
|
||
case "n":
|
||
if page.NextCursor == "" {
|
||
return screen.Show("已到最后一页;本次未执行任何写入操作。")
|
||
}
|
||
cursor = page.NextCursor
|
||
continue
|
||
}
|
||
index, err := strconv.Atoi(strings.TrimSpace(choice))
|
||
if err != nil || index < 1 || index > len(page.Items) {
|
||
return localreset.ErrProtocol
|
||
}
|
||
result, err := client.InspectRecovery(page.Items[index-1].ID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return showRecovery(screen, result)
|
||
}
|
||
}
|
||
|
||
func showRecovery(screen terminal, result deployment.RecoveryInspection) error {
|
||
lines := []string{
|
||
fmt.Sprintf("事务:%s;服务器:%s;采样时间:%s", terminalValue(result.TransactionID), terminalValue(result.ServerID), result.ObservedAt.UTC().Format("2006-01-02 15:04:05 UTC")),
|
||
"历史证据:" + evidenceLabel(result.HistoricalEvidence),
|
||
}
|
||
for _, file := range result.Files {
|
||
exists := "不存在"
|
||
if file.Exists {
|
||
exists = "存在"
|
||
}
|
||
lines = append(lines, fmt.Sprintf("%s:%s;%s;比较:%s;摘要:%s", recoveryLabel(file.Kind), terminalValue(file.Path), exists, recoveryLabel(file.Comparison), terminalValue(file.Digest)))
|
||
}
|
||
for _, lock := range result.Locks {
|
||
exists := "不存在"
|
||
if lock.Exists {
|
||
exists = "存在"
|
||
}
|
||
lines = append(lines, fmt.Sprintf("锁:%s;%s;归属:%s", terminalValue(lock.Path), exists, recoveryLabel(lock.Ownership)))
|
||
}
|
||
lines = append(lines, "只读采样已完成,未提交或回滚事务;缺少历史证据不能视为历史部署成功。")
|
||
for _, line := range lines {
|
||
if err := screen.Show(line); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func evidenceLabel(present bool) string {
|
||
if present {
|
||
return "记录已保存(不代表本次部署通过验收)"
|
||
}
|
||
return "缺失,禁止据此认定历史成功"
|
||
}
|
||
|
||
func terminalValue(value string) string {
|
||
var output strings.Builder
|
||
for _, c := range value {
|
||
if unicode.IsControl(c) || unicode.Is(unicode.Cf, c) || c == '\u2028' || c == '\u2029' {
|
||
output.WriteString(fmt.Sprintf("\\u%04x", c))
|
||
} else {
|
||
output.WriteRune(c)
|
||
}
|
||
}
|
||
return output.String()
|
||
}
|
||
|
||
func recoveryLabel(value string) string {
|
||
labels := map[string]string{
|
||
"unrecorded": "无历史摘要", "unproven": "归属无法证明",
|
||
"pending": "待处理", "recovery_required": "需要恢复", "locked": "已锁定", "acquiring": "取得锁中",
|
||
"uploaded": "已上传", "replacing": "替换中", "installed": "已安装", "reload_attempted": "已尝试重载",
|
||
"committing": "提交中", "commit_verified": "提交已核验", "rollback_verified": "回滚已核验",
|
||
"committed": "已提交", "rolled_back": "已回滚", "target": "目标文件", "temporary": "临时文件", "backup": "备份文件",
|
||
"match": "匹配", "mismatch": "不匹配", "missing": "缺失", "unknown": "未知", "unverifiable": "无法验证",
|
||
"owned": "属于本事务", "foreign": "属于其他事务", "ownerless": "缺少归属", "absent": "不存在",
|
||
}
|
||
if label, ok := labels[value]; ok {
|
||
return label
|
||
}
|
||
return "未识别(" + terminalValue(value) + ")"
|
||
}
|