//go:build windows package instance import ( "errors" "fmt" "golang.org/x/sys/windows" ) // ErrAlreadyRunning means another instance holds the single-instance mutex. var ErrAlreadyRunning = errors.New("another instance of ipswap is already running") // mutexName is session-local rather than Global\: ipswap is a per-user tray // app, and two users logged into the same machine should each get their own. const mutexName = `Local\ipswap-single-instance` type winLock struct{ h windows.Handle } func (l winLock) release() { if l.h != 0 { windows.ReleaseMutex(l.h) windows.CloseHandle(l.h) } } // lockProcess claims the single-instance slot. // // CreateMutexW succeeds even when the mutex already exists, so the existing // object is detected via GetLastError rather than the return value. The handle // is kept for the process lifetime; Windows releases it automatically if we // crash, which is what stops a hard kill from locking the user out. func lockProcess() (releaser, error) { name, err := windows.UTF16PtrFromString(mutexName) if err != nil { return nil, err } h, err := windows.CreateMutex(nil, false, name) if err != nil { if errors.Is(err, windows.ERROR_ALREADY_EXISTS) { if h != 0 { windows.CloseHandle(h) } return nil, ErrAlreadyRunning } return nil, fmt.Errorf("creating the single-instance mutex: %w", err) } return winLock{h: h}, nil }