ee2fc2a803
leo 08-06 封測回報①④+圖三標註。三個都是**只在 Windows 現形**的病。
## ④ 托盤一次開好幾個 icon
leo:「點一次開一個新的,測試者原本給我看顯示到 4 個」。
真兇:main.go 的 options.App 沒設 SingleInstanceLock(Wails v2.13 有這選項)。
每個實例還各自拉一份 collector 子行程。
macOS 由 LaunchServices 天然單實例 ⇒ Mac 上驗不出來。
修:加 SingleInstanceLock,第二次啟動叫醒既有視窗後自己退出。
## ① 根本不運行(首頁恆顯示「同步引擎沒有在跑」)
真兇:collectorAlive() 的憑據是 `pgrep -f <bin>`,**Windows 沒有 pgrep**
⇒ 恆回 false ⇒ app.go:356 一定走「同步引擎沒有在跑」,與實際狀態無關。
修:憑據換成 supervisor 自己的狀態機(親手拉起的子行程、runOnce 以 cmd.Wait 收尾)
——比掃行程表更有憑據且跨平台,順帶修掉 pgrep 會匹配到別的實例的偽陽性。
⚠️ 不是發明第三種判斷法:db17f28(08-05)已為「同步中」立下同一條路
「改讀 sup.Status().State ⇒ 接回既有機制,不發明第三種判斷法」,
當時只改了 collectorSyncing 那半邊,這裡把漏掉的 collectorAlive 補上。
順帶移除 waitCollectorGone/pkill:翻 supervisor 原始碼確認 Stop() 回來時
子行程已被 cmd.Wait 收屍完畢,補刀多餘;且那兩支指令 Windows 根本沒有。
## 視窗高度擠出畫面
leo 圖三:「視窗尺寸應該是 default 50% 螢幕長寬,高度太高快擠到外面了」。
修:OnStartup 依 ScreenGetAll 算 50% 並置中;保底值 1000x700 → 960x600
(拿不到螢幕資訊時不瞎猜,沿用保底值)。
## 驗(實測輸出)
· go vet 全綠(darwin + GOOS=windows 各一次)
· mac build OK;GOOS=windows CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc 交叉編譯 OK
· 位元組比對交叉編譯出的 .exe:
新 CIS trayicon.ico 在 exe 裡 = True
舊 Wails「W」圖在 exe 裡 = False
· 未驗:Windows 真機行為(需重打包後由封測者確認)⇒ 狀態 ◐,不是 ✅
144 lines
5.9 KiB
Go
144 lines
5.9 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"embed"
|
||
|
||
"github.com/wailsapp/wails/v2"
|
||
"github.com/wailsapp/wails/v2/pkg/options"
|
||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
|
||
)
|
||
|
||
//go:embed all:frontend/dist
|
||
var assets embed.FS
|
||
|
||
//go:embed build/trayicon.png
|
||
var trayIcon []byte
|
||
|
||
type wailsCtx = context.Context
|
||
|
||
// 版本號由 build 時 -ldflags 注入(同 arcrun-tray 的規矩)。
|
||
var version = "dev"
|
||
|
||
func main() {
|
||
app := NewApp()
|
||
|
||
// 🔴 托盤必須在**主執行緒**建立(NSStatusItem 內部會 new NSWindow,
|
||
// AppKit 規定 NSWindow 只能在主執行緒 new)。
|
||
// 踩過的兩個錯(都真機實測炸掉):
|
||
// ① systray.Run() 自己開 loop ⇒ 與 Wails 搶主 loop ⇒ SIGTRAP
|
||
// ② 在 OnStartup 或 goroutine 裡呼叫 ⇒ 「NSWindow should only be
|
||
// instantiated on the main thread!」⇒ SIGABRT
|
||
// 正解:main() 一開始(還在主執行緒)就 RunWithExternalLoop 註冊並 start,
|
||
// 它不阻塞,接著再把主執行緒交給 wails.Run()。
|
||
// 🔴 托盤必須在**主執行緒**建立(NSStatusItem 內部會 new NSWindow)。
|
||
// 改用原生 NSStatusItem(見 tray_darwin.m)——實測證明 energye/systray
|
||
// 在 RunWithExternalLoop 之下**根本不會建立托盤**(onReady 從未觸發)。
|
||
setupTray(app)
|
||
installSignalHandler() // 收到 TERM/INT 要能正常退出(不必強制結束)
|
||
|
||
err := wails.Run(&options.App{
|
||
Title: "Arcrun",
|
||
// 這裡的 Width/Height 只是**開得起來的保底值**;真正的尺寸在 OnStartup
|
||
// 依螢幕算成 50%(見下方 resizeToHalfScreen)。保底值不能大於常見筆電
|
||
// 的可視高度,否則 ScreenGetAll 失敗時會沿用它而擠出畫面外
|
||
// (leo 08-06 實撞:1000x700 在他的 Windows 上「高度太高快擠到外面了」)。
|
||
Width: 960, Height: 600,
|
||
MinWidth: 820, MinHeight: 560,
|
||
AssetServer: &assetserver.Options{Assets: assets},
|
||
// 🔴 2026-08-06:沒有這段,Arcrun.exe **點幾次就開幾個實例**,
|
||
// 每個實例各掛一個系統匣 icon(leo 的封測者開到 4 個,分不清該點哪個),
|
||
// 而且每個實例還各自拉一份 collector 子行程。
|
||
// macOS 由 LaunchServices 天然單實例 ⇒ 這個病**只在 Windows 現形**。
|
||
// 第二次啟動=叫醒既有視窗後自己退出(Wails 內部 os.Exit(0))。
|
||
SingleInstanceLock: &options.SingleInstanceLock{
|
||
UniqueId: "dev.arcrun.rag.daemon",
|
||
OnSecondInstanceLaunch: func(options.SecondInstanceData) {
|
||
app.ShowWindow()
|
||
},
|
||
},
|
||
OnStartup: func(ctx wailsCtx) {
|
||
app.startup(ctx)
|
||
resizeToHalfScreen(ctx) // 視窗=螢幕的一半,置中(leo 08-06)
|
||
startSupervisor() // 🔴 daemon 本體:不啟動就不會同步
|
||
// 🔴 托盤必須在**主執行緒**建立(NSStatusItem 會 new NSWindow)。
|
||
// OnStartup 不是主執行緒 ⇒ 直接呼叫會炸
|
||
// 「NSWindow should only be instantiated on the main thread!」(實測)。
|
||
// 用 Wails 的 dispatch 把它丟回主執行緒。
|
||
|
||
},
|
||
// 🔴 daemon 是常駐程式:關窗只是收起來,**不能真的結束**
|
||
// (結束=停止看守資料夾,使用者不會預期按 × 就不再同步)。
|
||
OnBeforeClose: func(ctx wailsCtx) bool {
|
||
wruntime.WindowHide(ctx)
|
||
return true // true=攔下關閉
|
||
},
|
||
HideWindowOnClose: true,
|
||
// Dock 不出現 icon:Wails v2.13 的 mac.ActivationPolicy 還沒開放(原始碼裡是註解掉的),
|
||
// 改走 Info.plist 的 LSUIElement(與 fyne 版同一個做法,見 build/darwin/Info.plist)。
|
||
Bind: []interface{}{app},
|
||
})
|
||
if err != nil {
|
||
println("Error:", err.Error())
|
||
}
|
||
}
|
||
|
||
// resizeToHalfScreen 把視窗調成螢幕長寬的一半並置中。
|
||
//
|
||
// 🔴 leo 2026-08-06(Windows 封測,圖三標註):
|
||
//
|
||
// 「視窗尺寸應該是 default 50% 螢幕長寬,**高度太高快擠到外面了**」
|
||
//
|
||
// 寫死 1000x700 在 1366x768 這種常見筆電上,扣掉工作列就幾乎頂到底。
|
||
// 依螢幕算才不會在別人機器上爆版——我們沒有全世界的螢幕可以試。
|
||
//
|
||
// 拿不到螢幕資訊時**什麼都不做**(沿用 options 的保底值),不要瞎猜一個數字:
|
||
// 猜錯的後果就是現在這個病。
|
||
func resizeToHalfScreen(ctx wailsCtx) {
|
||
screens, err := wruntime.ScreenGetAll(ctx)
|
||
if err != nil || len(screens) == 0 {
|
||
return
|
||
}
|
||
// 優先用「視窗現在所在的那面」;沒有就退回主螢幕,再退回第一面。
|
||
s := screens[0]
|
||
for _, c := range screens {
|
||
if c.IsCurrent {
|
||
s = c
|
||
break
|
||
}
|
||
if c.IsPrimary {
|
||
s = c
|
||
}
|
||
}
|
||
// Size 是邏輯像素(Wails 設定尺寸用的單位);舊欄位 Width/Height 已 deprecated,
|
||
// 但某些平台只填舊欄位 ⇒ 兩個都試,取得到值的那個。
|
||
w, h := s.Size.Width, s.Size.Height
|
||
if w == 0 || h == 0 {
|
||
w, h = s.Width, s.Height
|
||
}
|
||
if w == 0 || h == 0 {
|
||
return
|
||
}
|
||
wruntime.WindowSetSize(ctx, w/2, h/2)
|
||
wruntime.WindowCenter(ctx)
|
||
}
|
||
|
||
// setupTray 建立選單列 icon。
|
||
//
|
||
// 🔴 leo 2026-08-05:「依照 google drive,托盤裡**只剩下按右鍵會結束**,
|
||
//
|
||
// 其他設定都在這個頁面,**點擊托盤的 icon 就立刻展開界面**」
|
||
//
|
||
// ⇒ 左鍵=直接開視窗(不彈選單);右鍵=只有一項「結束 Arcrun」。
|
||
//
|
||
// 所有設定都在視窗裡,托盤不再是第二套 UI(fyne 版把設定塞在下拉選單,
|
||
// leo:「daemon 設定項越來越多,不可能統統塞在下拉選單」)。
|
||
//
|
||
// 🔴 **不可以用 systray.Run() 開自己的迴圈**(我第一版這樣寫,真機一跑就 SIGTRAP):
|
||
//
|
||
// macOS 的選單列必須在**主執行緒**跑,Wails 已經佔著那條 loop,
|
||
// 兩個 toolkit 搶主 loop ⇒ `signal arrived during cgo execution` 直接崩。
|
||
// 正解是 `RunWithExternalLoop`——它只註冊、把 start/end 交給既有的 loop 呼叫。
|
||
var trayStart, trayEnd = func() {}, func() {}
|