修复主动关闭连接误判并保存人工处置实测进度
This commit is contained in:
parent
457f61721c
commit
7fa5e9b3d2
@ -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 {
|
||||
|
||||
65
backend/internal/deployment/ssh_close_test.go
Normal file
65
backend/internal/deployment/ssh_close_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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 多对多、恢复负向与外部门禁
|
||||
|
||||
### 正确域名的第二组双机推送
|
||||
|
||||
@ -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。
|
||||
|
||||
@ -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 当前执行结论
|
||||
|
||||
- [✓] 多对多正常路径取得直接证据:两个有效域名、两台服务器、四目标成功,第二域名通过正式导入接口接入,未直接改数据库。
|
||||
|
||||
@ -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,整体生产验收未通过。上批的失败事务继续保留,不用后续成功覆盖失败记录。
|
||||
|
||||
@ -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。
|
||||
|
||||
@ -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` 为空,返回处置已拒绝,未执行远端变更。
|
||||
|
||||
Loading…
Reference in New Issue
Block a user