A 階段第二個 milestone、銜接 M9-1 bridge.py、暴露 service layer 給 M9-3 API/WebSocket。 New module `server/internal/firmware/`: - types.go: 123 行(FirmwareVersion / FirmwareProgress / ActiveTaskInfo / UpgradeDriver interface / 8 reason const) - progress.go: 147 行(仿 flash pattern 的 Tracker、Task.cancel 預留 SIGTERM force-cancel godoc) - service.go: 373 行(核心 service:UpgradeFirmware / HasActiveTask / GetActiveTaskInfo / RequestShutdown / WaitForActiveTasks / ListBundledVersions / GetCurrentVersion) - service_test.go: 676 行、13 個 test 含 MultiDeviceParallel Driver layer: - kl720_driver.go: 697 → 1054 行(+357、新 UpgradeFirmware method + tryRouteFirmwareEvent + sendCommandForUpgrade snapshot pattern) - kl720_driver_test.go: 360 行、11 個 test(含 InfoNotBlockedDuringUpgrade / CtxCancelReleasesBridge / StderrEventAfterCtxCancel 100 round stress) 關鍵設計: - flash 與 firmware 模組分離(不 import flash) - UpgradeDriver interface 隔離 driver 細節、DeviceLookup interface 隔離 device manager - 中介 channel pattern(service ↔ driver)方便 service 補欄位(DeviceID / Direction / BeforeVersion) - timeout 雙保險:chip timeout + 30s margin - 8 reason enum 對齊 bridge.py、stage 採 Design 命名 Concurrency race 修復(M9-2 Reviewer round 1 → round 2): - Major 1(mutex deadlock):新 fwUpgradeMu 獨立鎖 + sendCommandForUpgrade snapshot stdin/stdout pattern、避開 d.mu field-level race + 升級期間 Info/Disconnect 不被卡 + timeout 路徑無死鎖 - Major 2(close-channel race):tryRouteFirmwareEvent 持 fwMu 整段、配合 defer setFirmwareProgressCh(nil) 提供 happen-before、絕無 send on closed channel panic Reviewer 兩輪審查: - Round 1: 0 Critical / 2 Major / 5 Minor / 5 Suggestion - Round 2: 0 Critical / 0 Major / 2 Minor / 2 Suggestion(11/12 issue 修到位、Suggestion 4 留 follow-up) M9-1 follow-up 順手清: - m5(test 死碼 _firmware_upgrade_start_ts 殘留兩行)已清 - s5(test 註解 idempotent shape 說明)已加 測試: - go test ./... -race -count=1: 全綠(28s、無 regression) - Python: 36 tests + 22 subtests 全綠(0.31s) - go vet / build: 0 output 下一步:M9-3 API handler + WebSocket progress(CI 建議 `go test -race -count=3` 提升 race 偵測強度) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
148 lines
4.1 KiB
Go
148 lines
4.1 KiB
Go
package firmware
|
||
|
||
import (
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
// Task 代表一個進行中的 firmware 升降版 task(TDD §6 + §8.6.1)。
|
||
// service 用 ProgressTracker 維護 deviceID → *Task、用來:
|
||
// 1. 拒絕同一 device 同時兩個 firmware task(§3.3 FW_DEVICE_BUSY)
|
||
// 2. 給 graceful shutdown handler 查 HasActiveTask(§8.6.1)
|
||
// 3. 給 control panel modal 顯示 active task 資訊(§8.6.2)
|
||
type Task struct {
|
||
ID string
|
||
DeviceID string
|
||
DeviceName string
|
||
Chip string
|
||
Direction string
|
||
StartTs time.Time
|
||
ProgressCh chan FirmwareProgress
|
||
|
||
mu sync.Mutex
|
||
stage string // 目前推到的 stage、tracker 內部維護
|
||
done bool
|
||
// cancel 是 task 的 context.CancelFunc、由 service.UpgradeFirmware 寫入。
|
||
//
|
||
// 目前 service 只在 runUpgrade 的 defer 內呼叫一次(避免 ctx leak)、
|
||
// 沒有外部 reader 主動呼叫。預留給未來 SIGTERM force-cancel 流程
|
||
// (TDD §8.6.3)使用:graceful shutdown 等不到 task 結束時、可逐
|
||
// task 呼叫 cancel() 提前釋放 driver 資源。
|
||
cancel func()
|
||
}
|
||
|
||
// Stage 回傳該 task 最後一次廣播的 stage(thread-safe)。
|
||
func (t *Task) Stage() string {
|
||
t.mu.Lock()
|
||
defer t.mu.Unlock()
|
||
return t.stage
|
||
}
|
||
|
||
func (t *Task) setStage(s string) {
|
||
t.mu.Lock()
|
||
t.stage = s
|
||
t.mu.Unlock()
|
||
}
|
||
|
||
// Done 回傳 task 是否完成(thread-safe)。
|
||
func (t *Task) Done() bool {
|
||
t.mu.Lock()
|
||
defer t.mu.Unlock()
|
||
return t.done
|
||
}
|
||
|
||
func (t *Task) markDone() {
|
||
t.mu.Lock()
|
||
t.done = true
|
||
t.mu.Unlock()
|
||
}
|
||
|
||
// ProgressTracker 仿 flash.ProgressTracker 的 pattern、map[deviceID]*Task。
|
||
//
|
||
// 注意:key 是 deviceID 而非 taskID(每 device 同時只能有 1 個 firmware
|
||
// task、不像 flash 可以同 device 不同 model)。
|
||
type ProgressTracker struct {
|
||
tasks map[string]*Task
|
||
mu sync.Mutex
|
||
}
|
||
|
||
// NewProgressTracker 建立空 tracker。
|
||
func NewProgressTracker() *ProgressTracker {
|
||
return &ProgressTracker{tasks: make(map[string]*Task)}
|
||
}
|
||
|
||
// Create 嘗試建立新 task。若同 deviceID 已有未完成的 task、回傳 nil
|
||
// (caller 應回 FW_DEVICE_BUSY 給上層)。
|
||
func (pt *ProgressTracker) Create(deviceID, deviceName, chip, direction string) *Task {
|
||
pt.mu.Lock()
|
||
defer pt.mu.Unlock()
|
||
|
||
if existing, ok := pt.tasks[deviceID]; ok && !existing.Done() {
|
||
return nil
|
||
}
|
||
|
||
taskID := direction + "-" + deviceID + "-" + time.Now().UTC().Format("20060102T150405.000")
|
||
t := &Task{
|
||
ID: taskID,
|
||
DeviceID: deviceID,
|
||
DeviceName: deviceName,
|
||
Chip: chip,
|
||
Direction: direction,
|
||
StartTs: time.Now(),
|
||
ProgressCh: make(chan FirmwareProgress, 32),
|
||
stage: StagePreparing,
|
||
}
|
||
pt.tasks[deviceID] = t
|
||
return t
|
||
}
|
||
|
||
// Get 取得指定 device 的 task(thread-safe)、沒有時回 nil。
|
||
func (pt *ProgressTracker) Get(deviceID string) *Task {
|
||
pt.mu.Lock()
|
||
defer pt.mu.Unlock()
|
||
return pt.tasks[deviceID]
|
||
}
|
||
|
||
// Remove 移除指定 device 的 task entry(caller 應在 progressCh 已 close
|
||
// 且讀者已完成消費後呼叫、否則前端可能讀不到尾端事件)。
|
||
func (pt *ProgressTracker) Remove(deviceID string) {
|
||
pt.mu.Lock()
|
||
defer pt.mu.Unlock()
|
||
delete(pt.tasks, deviceID)
|
||
}
|
||
|
||
// ActiveTasks 列出所有未完成 task 的快照(thread-safe)、給 graceful
|
||
// shutdown handler 用(TDD §8.6.1)。
|
||
func (pt *ProgressTracker) ActiveTasks() []*ActiveTaskInfo {
|
||
pt.mu.Lock()
|
||
defer pt.mu.Unlock()
|
||
|
||
out := make([]*ActiveTaskInfo, 0, len(pt.tasks))
|
||
for _, t := range pt.tasks {
|
||
if t.Done() {
|
||
continue
|
||
}
|
||
elapsed := time.Since(t.StartTs).Milliseconds()
|
||
info := &ActiveTaskInfo{
|
||
TaskID: t.ID,
|
||
DeviceID: t.DeviceID,
|
||
DeviceName: t.DeviceName,
|
||
Chip: t.Chip,
|
||
Direction: t.Direction,
|
||
Stage: t.Stage(),
|
||
StartTs: t.StartTs,
|
||
ElapsedMs: elapsed,
|
||
}
|
||
// ETA:粗估「該 chip 該 stage 還剩多少秒」、用 UpgradeTimeoutFor
|
||
// 減去 elapsed 作為上界(不精確、Design §9.6 文案已用 "~{n}s")。
|
||
timeout := UpgradeTimeoutFor(t.Chip)
|
||
remaining := int((timeout.Milliseconds() - elapsed) / 1000)
|
||
if remaining < 0 {
|
||
remaining = 0
|
||
}
|
||
info.EtaSeconds = remaining
|
||
out = append(out, info)
|
||
}
|
||
return out
|
||
}
|