//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/trayhost" "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) _ = a.Disconnect() 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) mustBind(w, "getLogs", a.GetLogs) mustBind(w, "probeTunnel", a.ProbeTunnel) mustBind(w, "savePrefs", a.SavePrefs) a.StartBackground() trayhost.Start("Navis", trayhost.Hooks{ Connect: func() { _ = a.Connect() }, Disconnect: func() { _ = a.Disconnect() }, Quit: func() { _ = a.Disconnect() os.Exit(0) }, IsUp: func() bool { return a.Mgr.Status().Connected }, }) 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) }