package main // query_firmware_active_tasks.go — M9-4.5:firmware-aware close handler helper // // Wails 控制台收到使用者關閉視窗請求時、必須先查 server 是否有韌體升降版進 // 行中(會寫 flash 的破壞性操作、中斷會 brick)。本 helper 提供小型 HTTP 客戶 // 端、打 `GET /api/firmware/active-tasks`(M9-3 端點)、解析 hasActive + // tasks 給 OnBeforeClose 做判斷。 // // 設計原則(仿 shutdown_notify.go): // - 1 秒 timeout:server 沒起來 / 卡死 / network error 全視為「無 active task」 // 不擋關閉、避免使用者卡在「關不掉的視窗」 // - port <= 0 → server 還沒起來、直接視為「無 active task」乾淨關 // - 任何錯誤、不擋關閉路徑(fail-open):寧可錯放也不卡使用者 // - 變數化 client / timeout 以便測試注入 // // 為什麼 fail-open:誤判「沒 active task」最壞情況是進到既有 OnShutdown // 的 ctrl.Stop() → server SIGTERM handler、那邊 firmware.AwaitActiveTasksOrTimeout // 還會再擋一層;誤判「有 active task」要使用者強制關閉 = 體驗很差。雙層 // 防護下 Wails 這層 fail-open 是合理取捨。 import ( "context" "encoding/json" "fmt" "net/http" "time" ) // 變數化以便測試注入。 var ( queryFirmwareTasksTimeout = 1 * time.Second queryFirmwareTasksClient = &http.Client{Timeout: queryFirmwareTasksTimeout} ) // FirmwareActiveTaskSummary 是 OnBeforeClose 用來顯示 modal 所需的最小欄位。 // 對齊 server 端 firmware.ActiveTaskInfo 的 JSON 欄位(snake → camel 由 server // 端 jsonTag 統一)。 type FirmwareActiveTaskSummary struct { TaskID string `json:"taskId"` DeviceID string `json:"deviceId"` DeviceName string `json:"deviceName,omitempty"` Chip string `json:"chip"` Direction string `json:"direction"` Stage string `json:"stage"` ElapsedMs int64 `json:"elapsedMs"` EtaSeconds int `json:"etaSeconds,omitempty"` } // FirmwareActiveTasksResponse 對應 server 端 GET /api/firmware/active-tasks // 回應的 `data` 欄位(server 包裝 {success, data:{hasActive, tasks}}、本 struct // 只描述 data 內層)。 type FirmwareActiveTasksResponse struct { HasActive bool `json:"hasActive"` Tasks []FirmwareActiveTaskSummary `json:"tasks"` } // queryFirmwareActiveTasks 打 GET /api/firmware/active-tasks、回傳 hasActive // + 任務清單。 // // 錯誤路徑(fail-open):任何錯誤回傳 hasActive=false + 空 tasks + 該 error、 // caller 可選擇 log(不強制)、但**不**應該據此擋關閉。 // // 參數: // // ctx — caller context(會被 timeout 包裹) // port — server 正在聽的 port;<= 0 代表 server 沒起來、直接回 hasActive=false // // 回傳 error 主要供 caller 寫 log debug 用;正常 fail-open 邏輯不檢查 error。 func queryFirmwareActiveTasks(ctx context.Context, port int) (FirmwareActiveTasksResponse, error) { empty := FirmwareActiveTasksResponse{HasActive: false, Tasks: nil} if port <= 0 { return empty, fmt.Errorf("server port not available") } if ctx == nil { ctx = context.Background() } reqCtx, cancel := context.WithTimeout(ctx, queryFirmwareTasksTimeout) defer cancel() url := fmt.Sprintf("http://127.0.0.1:%d/api/firmware/active-tasks", port) req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, url, nil) if err != nil { return empty, fmt.Errorf("build request: %w", err) } resp, err := queryFirmwareTasksClient.Do(req) if err != nil { return empty, fmt.Errorf("http get: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return empty, fmt.Errorf("unexpected status %d", resp.StatusCode) } // server 包了一層 {success, data:{...}}、本 helper 只取 data var wrapper struct { Success bool `json:"success"` Data FirmwareActiveTasksResponse `json:"data"` } if err := json.NewDecoder(resp.Body).Decode(&wrapper); err != nil { return empty, fmt.Errorf("decode response: %w", err) } if !wrapper.Success { return empty, fmt.Errorf("server returned success=false") } if wrapper.Data.Tasks == nil { wrapper.Data.Tasks = []FirmwareActiveTaskSummary{} } return wrapper.Data, nil }