package main import ( "context" "fmt" "log" "net" "net/http" "os" "path/filepath" "sync/atomic" "syscall" "time" "unsafe" webview "github.com/jchv/go-webview2" ) var AppVersion = "dev" var currentHwnd atomic.Uintptr func setupFileLogging() { logPath := filepath.Join(userDataDir(), "debug.log") f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { return } log.SetOutput(f) log.SetFlags(log.LstdFlags | log.Lshortfile) } func main() { setupFileLogging() log.Printf("HPL Toolbox starting (pid=%d)", os.Getpid()) if focusExistingInstance() { log.Printf("Another instance is running; focused it and exiting.") return } listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { log.Fatal(err) } port := listener.Addr().(*net.TCPAddr).Port log.Printf("Server listening on 127.0.0.1:%d", port) mux := buildMux() srv := &http.Server{Handler: mux} go func() { if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed { log.Printf("Server error: %v", err) } }() if err := writeLockFile(port); err != nil { log.Printf("Failed to write lock file: %v", err) } defer removeLockFile() webviewTmp, err := os.MkdirTemp("", "hpltoolbox-wv2-*") if err != nil { log.Fatalf("Failed to create webview temp dir: %v", err) } defer os.RemoveAll(webviewTmp) w := webview.NewWithOptions(webview.WebViewOptions{ Debug: false, AutoFocus: true, DataPath: webviewTmp, WindowOptions: webview.WindowOptions{ Title: "HPL Toolbox " + AppVersion, Width: 1100, Height: 720, IconId: 1, Center: true, }, }) if w == nil { log.Fatalln("Failed to create webview.") } defer w.Destroy() // Stash HWND for the /api/focus handler. currentHwnd.Store(uintptr(w.Window())) w.SetSize(1100, 720, webview.HintNone) w.Navigate(fmt.Sprintf("http://127.0.0.1:%d/", port)) w.Run() // Window closed — shut the server down cleanly. log.Printf("Window closed; shutting down server.") ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() _ = srv.Shutdown(ctx) } func buildMux() *http.ServeMux { mux := http.NewServeMux() // Pages mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" && r.URL.Path != "/home" { http.NotFound(w, r) return } HomePage(w, r) }) mux.HandleFunc("GET /plec", PlecPage) mux.HandleFunc("GET /applovin", ApplovinPage) mux.HandleFunc("GET /base64", Base64Page) mux.HandleFunc("GET /mraid", MraidPage) mux.HandleFunc("GET /mintegral", MintegralPage) mux.HandleFunc("GET /mobile", MobilePage) mux.HandleFunc("GET /playworks", PlayworksPage) mux.HandleFunc("GET /changelog", ChangelogPage) mux.HandleFunc("GET /assets/qrcode.min.js", MobileQrScript) // Shared API mux.HandleFunc("POST /api/clipboard", ClipboardEndpoint) mux.HandleFunc("POST /api/open", OpenEndpoint) mux.HandleFunc("POST /api/focus", FocusEndpoint) mux.HandleFunc("POST /api/betaTools", BetaToolsEndpoint) mux.HandleFunc("POST /api/drop/resolve", DropResolveEndpoint) // PLEC mux.HandleFunc("POST /api/plec/pick", PlecPick) mux.HandleFunc("POST /api/plec/pickFolder", PlecPickFolder) mux.HandleFunc("POST /api/plec/upload", PlecUpload) // AppLovin mux.HandleFunc("POST /api/applovin/pick", ApplovinPick) mux.HandleFunc("POST /api/applovin/pickFolder", ApplovinPickFolder) mux.HandleFunc("POST /api/applovin/upload", ApplovinUpload) mux.HandleFunc("POST /api/applovin/saveQr", ApplovinSaveQR) mux.HandleFunc("POST /api/applovin/saveAllQrs", ApplovinSaveAllQRs) // Base64 mux.HandleFunc("POST /api/base64/pickFolder", Base64PickFolder) mux.HandleFunc("POST /api/base64/pickFiles", Base64PickFiles) mux.HandleFunc("POST /api/base64/scan", Base64Scan) // MRAID Checker mux.HandleFunc("POST /api/mraid/pickFolder", MraidPickFolder) mux.HandleFunc("POST /api/mraid/pickFiles", MraidPickFiles) mux.HandleFunc("POST /api/mraid/scan", MraidScan) // Mintegral Checker mux.HandleFunc("POST /api/mintegral/pick", MintegralPick) mux.HandleFunc("POST /api/mintegral/load", MintegralLoad) mux.HandleFunc("GET /mintegral/runtime", MintegralRuntimePage) mux.HandleFunc("GET /mintegral/ad", MintegralAd) mux.HandleFunc("GET /mintegral/monitor.js", MintegralMonitor) mux.HandleFunc("GET /mintegral/preview-util.js", MintegralPreviewUtil) // Send To Mobile mux.HandleFunc("POST /api/mobile/pick", MobilePick) mux.HandleFunc("POST /api/mobile/pickFolder", MobilePickFolder) mux.HandleFunc("POST /api/mobile/start", MobileStart) mux.HandleFunc("POST /api/mobile/stop", MobileStop) // Playworks Converter mux.HandleFunc("POST /api/playworks/pickSource", PlayworksPickSource) mux.HandleFunc("POST /api/playworks/pickOutput", PlayworksPickOutput) mux.HandleFunc("POST /api/playworks/convert", PlayworksConvert) return mux } // FocusEndpoint brings the webview window to the foreground. Used by a second // instance that wants to focus the running one before exiting. func FocusEndpoint(w http.ResponseWriter, r *http.Request) { hwnd := currentHwnd.Load() if hwnd != 0 { restoreAndFocus(hwnd) } w.WriteHeader(http.StatusOK) } var ( user32 = syscall.NewLazyDLL("user32.dll") procShowWindow = user32.NewProc("ShowWindow") procSetForeground = user32.NewProc("SetForegroundWindow") procIsIconic = user32.NewProc("IsIconic") ) const ( swRestore = 9 swShow = 5 ) func restoreAndFocus(hwnd uintptr) { // If minimized, restore. Otherwise just ensure it's shown. iconic, _, _ := procIsIconic.Call(hwnd) if iconic != 0 { _, _, _ = procShowWindow.Call(hwnd, swRestore) } else { _, _, _ = procShowWindow.Call(hwnd, swShow) } _, _, _ = procSetForeground.Call(hwnd) } // Silence "imported and not used" complaints if any. var _ = unsafe.Sizeof(int(0))