Files
arcrun-collector/cmd/arcrun-app/main.go
T
Leo ee2fc2a803 Windows 三修:不再無限多開/首頁不再謊報「引擎沒在跑」/視窗改螢幕 50%
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 真機行為(需重打包後由封測者確認)⇒ 狀態 ◐,不是 
2026-08-06 12:20:25 +08:00

144 lines
5.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 不出現 iconWails 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-06Windows 封測,圖三標註):
//
// 「視窗尺寸應該是 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() {}