//go:build windows && !bindings // NB the !bindings tag: Wails generates the TypeScript bindings by BUILDING AND // RUNNING this binary. With the guard active, a normal OpsLog already running on // the dev machine holds the mutex, the generator's process exits instantly, and // no bindings are produced. Excluding the guard from that build keeps generation // working while shipping builds still get it. package main import ( "errors" "time" "unsafe" "golang.org/x/sys/windows" ) // singleInstanceName is a per-session named mutex. The Windows kernel releases // it automatically when the owning process dies (even on a crash), so a // lingering/zombie OpsLog can't permanently block future launches — killing it // frees the name at once. Session-local (no "Global\\") = one instance per // logged-in desktop, which is what we want. const singleInstanceName = "OpsLog-SingleInstance-Mutex" // acquireSingleInstance creates the named mutex. Returns ok=false when another // OpsLog already holds it (this instance should exit); on the way out it brings // the existing window to the front so a double-click just refocuses OpsLog // instead of spawning a duplicate that fights over the CAT / antenna. // // The mutex handle is deliberately never closed — it must live for the whole // process lifetime; the OS reclaims it on exit. func acquireSingleInstance() (ok bool) { namePtr, err := windows.UTF16PtrFromString(singleInstanceName) if err != nil { return true // never block launch on an unexpected error } kernel32 := windows.NewLazySystemDLL("kernel32.dll") createMutex := kernel32.NewProc("CreateMutexW") // CreateMutexW(lpSecurityAttributes=NULL, bInitialOwner=FALSE, lpName) h, _, callErr := createMutex.Call(0, 0, uintptr(unsafe.Pointer(namePtr))) if h == 0 { return true // couldn't create the mutex → don't block the app } if errors.Is(callErr, windows.ERROR_ALREADY_EXISTS) { focusExistingWindow() return false } return true } // focusExistingWindow finds the running OpsLog window by its title and restores // + foregrounds it. Best-effort; failures are silently ignored. func focusExistingWindow() { user32 := windows.NewLazySystemDLL("user32.dll") findWindow := user32.NewProc("FindWindowW") setForeground := user32.NewProc("SetForegroundWindow") showWindow := user32.NewProc("ShowWindow") title, err := windows.UTF16PtrFromString("OpsLog") if err != nil { return } hwnd, _, _ := findWindow.Call(0, uintptr(unsafe.Pointer(title))) if hwnd == 0 { return } const swRestore = 9 // SW_RESTORE — un-minimise if needed showWindow.Call(hwnd, swRestore) setForeground.Call(hwnd) } // waitForProcessExit blocks until the process with this pid is gone, or the // timeout runs out. Reports whether it actually went. // // This is what the auto-update relaunch needs, and it is the piece that was // lost when the PowerShell helper went. The old instance is allowed thirty // seconds to shut down (see armExitWatchdog) — it closes a remote logbook, a // CAT session, sometimes a backup — while the new one was only patient with the // mutex for twenty. On a station where shutdown took longer than that, the new // process gave up and exited, and the operator was left with the old one still // running and no new window: exactly the report. // // Waiting on a handle rather than sleeping a fixed time is also the honest // version: it ends the instant the old process ends, however long or short that // is, and it is a plain kernel wait — nothing that looks like a script starting // another program. func waitForProcessExit(pid int, timeout time.Duration) bool { if pid <= 0 { return true } h, err := windows.OpenProcess(windows.SYNCHRONIZE, false, uint32(pid)) if err != nil { // Already gone, or not ours to wait on. Either way there is nothing to // wait for — and refusing to launch over an unexpected permission error // would be worse than starting. return true } defer windows.CloseHandle(h) ms := uint32(timeout / time.Millisecond) ev, err := windows.WaitForSingleObject(h, ms) return err == nil && ev == uint32(windows.WAIT_OBJECT_0) }