Reduce idle CPU (cheap PollUI, binary cache, logbuf cap, DOM fingerprint), speed ping/AWG HostPort, CopyBuffer pool; ship arm64 build and update notes. Co-authored-by: Cursor <cursoragent@cursor.com>
187 lines
4.5 KiB
Go
187 lines
4.5 KiB
Go
//go:build windows
|
|
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"github.com/jchv/go-webview2"
|
|
"golang.org/x/sys/windows"
|
|
|
|
"vpnclient/internal/apphost"
|
|
"vpnclient/internal/appui"
|
|
"vpnclient/internal/config"
|
|
"vpnclient/internal/core"
|
|
"vpnclient/internal/logbuf"
|
|
"vpnclient/internal/update"
|
|
)
|
|
|
|
func main() {
|
|
if update.MaybeFinishUpdate(os.Args[1:]) {
|
|
return
|
|
}
|
|
update.CleanupStaleDownloads()
|
|
|
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
|
log.Printf("Navis %s · GOMAXPROCS=%d NumCPU=%d", update.DisplayVersion(), runtime.GOMAXPROCS(0), runtime.NumCPU())
|
|
|
|
cfgPath, err := config.LocateConfig()
|
|
if err != nil {
|
|
fatalDialog("Не удалось найти путь конфига: %v", err)
|
|
}
|
|
if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
|
|
if err := config.WriteExample(cfgPath); err != nil {
|
|
fatalDialog("Не удалось создать конфиг: %v", err)
|
|
}
|
|
}
|
|
|
|
cfg, err := config.Load(cfgPath)
|
|
if err != nil {
|
|
fatalDialog("Ошибка конфига %s: %v", cfgPath, err)
|
|
}
|
|
|
|
logBuf := logbuf.New(0)
|
|
mgr, err := core.NewManager(cfgPath, cfg, logBuf)
|
|
if err != nil {
|
|
fatalDialog("%v", err)
|
|
}
|
|
|
|
a := apphost.New(mgr, cfgPath, logBuf)
|
|
a.OpenURL = shellOpen
|
|
a.OnAfterUpdate = func() {
|
|
// Hard-exit so Windows unlocks Navis.exe; do not wait on webview.Terminate.
|
|
go func() {
|
|
time.Sleep(300 * time.Millisecond)
|
|
os.Exit(0)
|
|
}()
|
|
}
|
|
|
|
dataPath := filepath.Join(filepath.Dir(cfgPath), ".navis-webview")
|
|
_ = os.MkdirAll(dataPath, 0o755)
|
|
|
|
w := webview2.NewWithOptions(webview2.WebViewOptions{
|
|
Debug: false,
|
|
AutoFocus: true,
|
|
DataPath: dataPath,
|
|
WindowOptions: webview2.WindowOptions{
|
|
Title: "Navis",
|
|
Width: 500,
|
|
Height: 900,
|
|
Center: true,
|
|
IconId: 1,
|
|
},
|
|
})
|
|
if w == nil {
|
|
fatalDialog("Не удалось создать окно WebView2.\nУстановите Microsoft Edge WebView2 Runtime.")
|
|
}
|
|
defer func() {
|
|
_ = mgr.Disconnect()
|
|
w.Destroy()
|
|
}()
|
|
|
|
applyWindowIcon(w.Window())
|
|
w.SetSize(500, 900, webview2.HintNone)
|
|
|
|
mustBind(w, "getState", a.GetState)
|
|
mustBind(w, "getEditState", a.GetEditState)
|
|
mustBind(w, "connect", a.Connect)
|
|
mustBind(w, "disconnect", a.Disconnect)
|
|
mustBind(w, "connectProfile", a.ConnectProfile)
|
|
mustBind(w, "saveProfile", a.SaveProfile)
|
|
mustBind(w, "createProfile", a.CreateProfile)
|
|
mustBind(w, "selectProfile", a.SelectProfile)
|
|
mustBind(w, "deleteProfile", a.DeleteProfile)
|
|
mustBind(w, "installCore", a.InstallCore)
|
|
mustBind(w, "openURL", a.OpenShopURL)
|
|
mustBind(w, "pingServers", a.PingServers)
|
|
mustBind(w, "pingBest", a.PingBest)
|
|
mustBind(w, "checkUpdate", a.CheckUpdate)
|
|
mustBind(w, "applyUpdate", a.ApplyUpdate)
|
|
mustBind(w, "saveHy2", a.SaveHy2)
|
|
mustBind(w, "importSubscription", a.ImportSubscription)
|
|
|
|
go a.AutoCheckUpdate()
|
|
|
|
w.SetHtml(appui.IndexHTML)
|
|
w.Run()
|
|
}
|
|
|
|
func mustBind(w webview2.WebView, name string, fn interface{}) {
|
|
if err := w.Bind(name, fn); err != nil {
|
|
fatalDialog("bind %s: %v", name, err)
|
|
}
|
|
}
|
|
|
|
func shellOpen(raw string) error {
|
|
verb, err := windows.UTF16PtrFromString("open")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
file, err := windows.UTF16PtrFromString(raw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return windows.ShellExecute(0, verb, file, nil, nil, windows.SW_SHOWNORMAL)
|
|
}
|
|
|
|
func applyWindowIcon(hwnd unsafe.Pointer) {
|
|
ico := findIconPath()
|
|
if ico == "" || hwnd == nil {
|
|
return
|
|
}
|
|
pathPtr, err := windows.UTF16PtrFromString(ico)
|
|
if err != nil {
|
|
return
|
|
}
|
|
user32 := windows.NewLazySystemDLL("user32.dll")
|
|
loadImage := user32.NewProc("LoadImageW")
|
|
sendMessage := user32.NewProc("SendMessageW")
|
|
const (
|
|
imageIcon = 1
|
|
lrLoadFromFile = 0x0010
|
|
lrDefaultSize = 0x0040
|
|
wmSetIcon = 0x0080
|
|
iconSmall = 0
|
|
iconBig = 1
|
|
)
|
|
h, _, _ := loadImage.Call(0, uintptr(unsafe.Pointer(pathPtr)), imageIcon, 0, 0, lrLoadFromFile|lrDefaultSize)
|
|
if h == 0 {
|
|
return
|
|
}
|
|
sendMessage.Call(uintptr(hwnd), wmSetIcon, iconSmall, h)
|
|
sendMessage.Call(uintptr(hwnd), wmSetIcon, iconBig, h)
|
|
}
|
|
|
|
func findIconPath() string {
|
|
candidates := []string{}
|
|
if exe, err := os.Executable(); err == nil {
|
|
dir := filepath.Dir(exe)
|
|
candidates = append(candidates,
|
|
filepath.Join(dir, "assets", "navis.ico"),
|
|
filepath.Join(dir, "navis.ico"),
|
|
)
|
|
}
|
|
if cwd, err := os.Getwd(); err == nil {
|
|
candidates = append(candidates, filepath.Join(cwd, "assets", "navis.ico"))
|
|
}
|
|
for _, c := range candidates {
|
|
if st, err := os.Stat(c); err == nil && !st.IsDir() {
|
|
return c
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func fatalDialog(format string, args ...interface{}) {
|
|
msg := fmt.Sprintf(format, args...)
|
|
log.Println(msg)
|
|
messageBox(msg)
|
|
os.Exit(1)
|
|
}
|