修复主动关闭连接误判并保存人工处置实测进度

This commit is contained in:
xuans 2026-10-03 23:22:05 +08:00
parent 457f61721c
commit 7fa5e9b3d2
8 changed files with 162 additions and 11 deletions

View File

@ -11,6 +11,7 @@ import (
"os"
"path"
"strings"
"sync"
"sync/atomic"
"time"
@ -213,6 +214,8 @@ type sshClientAdapter struct {
ssh *ssh.Client
sftp *sftp.Client
disconnected atomic.Bool
closeOnce sync.Once
closeErr error
}
// sshOperation 取消时关闭传输并等待操作退出,避免回滚与仍在写入的操作并行。
@ -450,18 +453,24 @@ func (c *sshClientAdapter) runCommand(command []string) (CommandResult, error) {
}
func (c *sshClientAdapter) Close() error {
var first error
// 先关闭传输,避免无响应的 SFTP 服务阻塞关闭过程。
if c.ssh != nil {
c.disconnected.Store(true)
first = c.ssh.Close()
}
if c.sftp != nil {
if err := c.sftp.Close(); err != nil && first == nil {
first = err
c.closeOnce.Do(func() {
wasDisconnected := c.disconnected.Swap(true)
// 先关闭传输,避免无响应的 SFTP 服务阻塞关闭过程。
if c.ssh != nil {
c.closeErr = c.ssh.Close()
}
}
return first
if c.sftp != nil {
if err := c.sftp.Close(); err != nil && c.closeErr == nil {
// 正常主动关闭传输后,SFTP 收尾可能返回 EOF 或连接已关闭。
// 仅消除本次主动关闭的预期错误,保留既有断连及其他关闭失败。
if c.ssh != nil && !wasDisconnected && (errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed)) {
return
}
c.closeErr = err
}
}
})
return c.closeErr
}
func validateCommand(command []string) error {

View File

@ -0,0 +1,65 @@
package deployment
import (
"sync"
"testing"
"github.com/pkg/sftp"
)
func TestSSHCloseHealthyTransportDoesNotReportSFTPShutdownEOF(t *testing.T) {
factory, server := safetyFactory(t, requestSFTPHandler(sftp.InMemHandler(), false, false))
client, err := factory.Connect(t.Context(), server)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = client.Close() })
if err := client.MkdirAll(t.Context(), "/close-check"); err != nil {
t.Fatal(err)
}
if err := client.Close(); err != nil {
t.Fatalf("正常关闭连接不得把本机主动关闭产生的错误当作部署失败:%v", err)
}
if err := client.Close(); err != nil {
t.Fatalf("重复关闭应返回相同的成功结果:%v", err)
}
}
func TestSSHCloseConcurrentCallsShareResult(t *testing.T) {
factory, server := safetyFactory(t, requestSFTPHandler(sftp.InMemHandler(), false, false))
client, err := factory.Connect(t.Context(), server)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = client.Close() })
var calls sync.WaitGroup
for range 8 {
calls.Go(func() {
if err := client.Close(); err != nil {
t.Errorf("并发关闭不得制造伪失败:%v", err)
}
})
}
calls.Wait()
}
func TestSSHClosePreservesPriorTransportFailure(t *testing.T) {
factory, server := safetyFactory(t, requestSFTPHandler(sftp.InMemHandler(), false, false))
client, err := factory.Connect(t.Context(), server)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = client.Close() })
adapter := client.(*sshClientAdapter)
adapter.disconnected.Store(true)
if err := adapter.ssh.Close(); err != nil {
t.Fatal(err)
}
first := client.Close()
if first == nil {
t.Fatal("既有传输失败不能被正常关闭分支吞掉")
}
if second := client.Close(); second != first {
t.Fatalf("重复关闭必须保留首次失败:%v / %v", first, second)
}
}

View File

@ -1,5 +1,30 @@
# Linux 生产流程测试记录
## 2026年10月3日23:14 真实中断恢复与关闭误判
### 重载阶段中断与自动恢复
- 第一轮任务 `b795be80-e086-46b4-a2ba-e7b51c8662f0` 因旧 systemd 不支持 `--kill-whom`,注入失败并正常完成;不能计为崩溃测试通过。确认本机帮助后改用 `--kill-who=main`。
- 22:57:25 对正式 Scert 主进程发送 SIGKILL,命中任务 `c68e3956-1b6d-42c7-ba9a-753ef2d1a44b` 第二目标事务的 reload_attempted 阶段。
- systemd 的 MainPID 从 130794 变为 134595,NRestarts=1;23:58 前已核实服务恢复 active、回环严格 TLS 就绪检查通过。此处实际核查时间为 22:58:16。
- 事务 `20261003-145716.121859533-09a2a331489a` 与 `20261003-145720.641188477-c769c8d3e153` 均 rolled_back;任务为 failed/service_restart,没有覆盖为成功。
- 双机 cert/fullchain 摘要保持 `7e8b56227b345ed9b66659e428f9bd4421604f11253ab88d65b3ed61737f35cf`,未发现该域名遗留事务文件。第二台正常 DNS HTTPS 200、peer=45.136.13.241、TLS 校验结果零。
### 真实待恢复事务及人工提交异常
- 任务 `5e89aa6d-3e25-4e3b-bf6c-71bf612aaf32` 在第二台 reload_attempted 阶段再次中断;临时 OUTPUT 拒绝规则仅匹配第一台 Scert 用户 UID 997 到第二台 TCP 22,预设九十秒撤销定时器。
- 第一目标自动回滚;第二事务 `20261003-150034.784155038-30bcd090470c` 因 SSH 被拒进入 recovery_required。确认服务已恢复后立即撤销规则和定时器;未影响 root SSH 或第二台 HTTPS,未创建辅助脚本。
- 正式本机管理菜单三选择完成提交,完整确认后生成处置 `bc4099fd53c10ce0ae643ae2a89c2597fa48ed7ea6d6661fe4126f7f310b7007`,执行时间 23:02:05—23:02:34。
- 目标文件及备份摘要匹配,锁归属匹配;动作后原事务 phase=commit_verified、status=recovery_required,远端备份及锁均已不存在,处置却为 uncertain,持久化占用保留。这是失败,不算人工提交通过。
- 原无可信旧基线的两条事务未改写。新不确定处置不能重放或直接删占用;第二台站点保持严格 HTTPS 200,但后续新部署被占用阻止。
### 修复及验证边界
- 新增真实 SSH/SFTP 协议用例在第一台 Linux 复现健康 Close 返回 use of closed network connection,CLOSE_REPRO_EXIT=1。
- 修复正常主动关闭后的预期错误分类及重复、并发关闭幂等,保留已存在的断连和其他关闭错误。`ssh-close-race.log` 中部署整包竞态通过,CLOSE_RACE_EXIT=0,约 3.261 秒。
- 后端全量及静态检查仍在同一 SSH 会话执行,正式候选包尚未更新。该修复不追溯改写 uncertain 历史记录。
- 本轮为保留数据、同一有效证书重新部署,不是清库首次 Web、不同版本证书回滚、ACME 新签发或完整生产验收。
## 2026年10月3日22:44 多对多、恢复负向与外部门禁
### 正确域名的第二组双机推送

View File

@ -1,5 +1,14 @@
# Scert 开发任务清单
## 2026年10月3日23:14 真实故障整改进度
- [✓] 正式包真实进程中断后双机自动回滚,任务保留重启失败历史,第二台网站继续通过严格 HTTPS。
- [!] 实机人工提交发现连接关闭误判:远端收尾完成,但处置为 uncertain,第二台仍有持久化部署占用。
- [✓] 原生 Linux 真实 SSH/SFTP 新增回归先复现失败;不是仅根据源码推断。
- [x] 主动关闭错误分类、重复及并发关闭幂等已修复;部署整包竞态通过,既有关闭故障负向未放宽。
- [~] 后端全量、静态检查、干净候选包及实机更新进行中。
- [ ] 未确定处置需保留原记录并通过独立授权重新核查收尾,不能直接删占用或重放动作;人工成功场景及其余上线门禁继续待验。
## 2026年10月3日22:44 最新生产验证进度
- [✓] 两个有效域名各绑定同两台服务器,正式部署及导入任务取得四个目标成功、四条 committed 的直接证据;第二台两域名正常 DNS 严格 HTTPS 200。

View File

@ -1,5 +1,14 @@
# Scert 整改任务执行清单
## 2026年10月3日23:14 当前整改任务
- [✓] 真实部署重载阶段 SIGKILL 后,systemd 自动重启及双机回滚通过;第二台 HTTPS 正常。
- [✓] 故障注入仅限制 Scert 用户到第二台 SSH,未改变 root 管理连接及网站端口;临时规则和撤销定时器已撤销。
- [!] 人工提交到达 commit_verified 并清理远端锁后,被连接关闭错误误判为 uncertain;原记录及持久化占用保留,不手动修正终态。
- [x] 健康 SSH/SFTP 关闭错误已通过真实协议测试复现并修复,整包竞态退出码零。
- [~] 后端全量、静态检查及正式包更新;完成后补实机验证。
- [ ] 独立授权的未确定处置重新核查收尾、人工回滚成功、执行中断连查询与其他生产门禁未完成。
## 2026年10月3日22:44 当前执行结论
- [✓] 多对多正常路径取得直接证据:两个有效域名、两台服务器、四目标成功,第二域名通过正式导入接口接入,未直接改数据库。

View File

@ -1,5 +1,17 @@
# Scert 整改、合并与优化清单
## 2026年10月3日23:14 崩溃恢复实测与关闭误判修复
- [✓] 正式 API 任务 `c68e3956-1b6d-42c7-ba9a-753ef2d1a44b` 在第二目标重载阶段被真实 SIGKILL 中断;systemd 自动重启,两条事务均 rolled_back,任务保留 service_restart 失败记录。
- [✓] 双机证书摘要一致,事务临时文件、备份和锁由产品恢复链路清理;第二台正常 DNS 严格 HTTPS 200。该证据仅覆盖同一有效证书重新部署,不代表不同版本证书回滚。
- [!] 短暂限制 Scert 用户到第二台 SSH 后形成真实待恢复事务。正式人工提交完成文件及锁收尾,却因连接关闭误判持久化 uncertain,持久化占用仍阻止第二台的新部署;网站仍正常。临时网络限制及撤销定时器已撤销。
- [✓] Linux 真实 SSH/SFTP 协议回归复现健康连接关闭返回连接已关闭错误,修复前退出码一;不是内存远端替身。
- [x] 已修正主动关闭产生的预期 SFTP 错误分类,并使重复和并发关闭幂等;既有断连及其他关闭错误仍返回失败。部署整包竞态通过。
- [~] 正在执行 Linux 后端全量与静态检查;随后构建干净候选包并安装。现有 uncertain 记录和占用不直接改写,需独立的受保护重新核查收尾流程,不能自动重放。
- [ ] 人工提交及回滚成功、执行中断连查询、未确定处置安全收尾、三数据库首次 Web、系统邮件、DNS/ACME 全矩阵与公网验收尚未完成。
第一台公网备案拦截和 Smail 系统通知授权缺失仍是门禁;未开放额外端口、关闭 TLS 验证或创建辅助验收脚本。
## 2026年10月3日22:44 多对多推送通过与公网门禁复核
本节为最新结论;仅 Linux,整体生产验收未通过。上批的失败事务继续保留,不用后续成功覆盖失败记录。

View File

@ -1,5 +1,16 @@
# 部署事务人工处置设计
## 2026年10月3日23:14 实机暴露的关闭与重新核查边界
真实人工提交已完成文件核验、备份删除、commit_verified 持久化和远端解锁,但 SSH 先主动关闭、SFTP 后关闭返回预期连接已关闭错误,导致执行器报告 uncertain。新增 Linux 真实协议回归已复现,修复限制为正常主动关闭后的 EOF 或连接已关闭,不吞掉既有断连、其他关闭错误或动作错误。重复和并发 Close 共享首次结果。
历史处置 `bc4099fd53c10ce0ae643ae2a89c2597fa48ed7ea6d6661fe4126f7f310b7007` 保持 uncertain,其占用继续阻止同服务器部署。不能因为修复了代码、当前站点正常或历史阶段为 commit_verified 就直接改写该处置、原事务或删除占用。
- [ ] 后续需要独立、明确授权的重新核查收尾流程,保留原不确定结果,并重新核验身份、历史证据、全量文件、临时文件、备份、锁与严格 HTTPS 后记录新审计。
- [ ] 该流程不能把超时视作执行者已停止,不能自动重放原动作或静默选择相反动作;同样需要并发、重启和审计失败验证。
本节记录新发现的缺口,不代表该收尾能力已经实现;已有三动作的成功实机验收仍未通过。
## 2026年10月3日22:44 无可信基线的实机拒绝
正式本机终端对有事务记录但 `previous_fingerprint` 为空的真实失败事务执行恢复基线请求,持久处置结果为 rejected。确认动作没有放宽执行器前置要求,目标文件和锁没有因该请求被变更,原事务仍为 recovery_required。

View File

@ -1,5 +1,16 @@
# 部署事务人工核查任务清单
## 2026年10月3日23:08 实机关闭误判整改任务
- [✓] 真实 SIGKILL 中断部署后,两条事务自动回滚,第二台正常 DNS 严格 HTTPS 仍通过。
- [✓] 短暂限制 Scert 用户到第二台 SSH 后形成真实待恢复事务;限制已撤销,未改变历史证据或手动删锁。
- [!] 正式人工提交完成远端收尾,却持久化 uncertain;该记录及占用保留,不直接改终态或重放。
- [✓] 第一台 Linux 真实 SSH/SFTP 协议回归复现:健康连接主动关闭后返回连接已关闭错误,原用例失败。
- [~] 修复主动关闭的预期 SFTP 错误分类和并发幂等,保留既有断连及非预期关闭错误。
- [ ] 完成 Linux 竞态、执行器负向及后端回归,构建干净候选包并重新实机验证人工提交、回滚和断连查询。
本轮复用同一有效证书,不表示不同证书版本切换已验收;原两条无可信旧基线事务不在本次修复中解除。
## 2026年10月3日22:44 真实恢复拒绝证据
- [✓] 对事务 `20261003-142919.192475924-417b9bcc5b09`,真实本机终端选择恢复基线并完整确认;因历史 `previous_fingerprint` 为空,返回处置已拒绝,未执行远端变更。