diff --git a/bin/sing-box.exe b/bin/sing-box.exe new file mode 100644 index 0000000..3fd0ea7 Binary files /dev/null and b/bin/sing-box.exe differ diff --git a/main.go b/main.go index f8afe79..c5fce46 100644 --- a/main.go +++ b/main.go @@ -15,7 +15,7 @@ import ( "strings" "sync" "syscall" - "unsafe" + "time" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/canvas" @@ -163,24 +163,24 @@ func runSingBox(configPath string) error { singBoxPath := filepath.Join("bin", "sing-box.exe") if !checkSingBox() { - return nil + return fmt.Errorf("sing-box executable not found at %s", singBoxPath) } // Acquire lock to prevent concurrent process operations processMutex.Lock() - defer processMutex.Unlock() // Kill any existing process before starting a new one - if currentProcess != nil && currentProcess.Process != nil { - // Attempt to terminate gracefully first - currentProcess.Process.Kill() - // Wait for the process to finish in a separate goroutine to avoid blocking - go func(p *exec.Cmd) { - p.Wait() // Wait for graceful termination - }(currentProcess) - currentProcess = nil + oldProcess := currentProcess + currentProcess = nil + + processMutex.Unlock() + + // Kill the old process outside the lock to avoid deadlocks + if oldProcess != nil { + killProcessAndWait(oldProcess) } + // Create new command cmd := exec.Command(singBoxPath, "run", "-c", configPath) // Make the process run as hidden @@ -190,14 +190,30 @@ func runSingBox(configPath string) error { err := cmd.Start() if err != nil { - return err + log.Printf("Failed to start sing-box: %v", err) + return fmt.Errorf("failed to start sing-box: %w", err) } + // Update currentProcess after successful start + processMutex.Lock() currentProcess = cmd + processMutex.Unlock() // Start a goroutine to wait for the process to finish go func() { - cmd.Wait() + defer func() { + if r := recover(); r != nil { + log.Printf("Recovered from panic in process wait routine: %v", r) + } + }() + + err := cmd.Wait() + if err != nil { + log.Printf("Sing-box process exited with error: %v", err) + } else { + log.Printf("Sing-box process exited normally") + } + // Clear the process reference when it finishes processMutex.Lock() if currentProcess == cmd { @@ -209,6 +225,37 @@ func runSingBox(configPath string) error { return nil } +// killProcessAndWait kills a process and waits for it to terminate +func killProcessAndWait(process *exec.Cmd) { + if process == nil { + return + } + + if process.Process != nil { + log.Printf("Terminating process PID: %d", process.Process.Pid) + + err := process.Process.Kill() + if err != nil { + log.Printf("Error killing process: %v", err) + // Process might have already terminated, try to wait anyway + } + + // Wait for the process to finish to clean up resources + go func(p *exec.Cmd) { + defer func() { + if r := recover(); r != nil { + log.Printf("Recovered from panic in process cleanup: %v", r) + } + }() + + _, err := p.Process.Wait() // Wait for the process to finish + if err != nil { + log.Printf("Error waiting for process: %v", err) + } + }(process) + } +} + // isProcessRunning checks if the process is still running func isProcessRunning(cmd *exec.Cmd) bool { if cmd == nil || cmd.Process == nil { @@ -231,20 +278,8 @@ func killCurrentProcess() { processMutex.Unlock() - if cmd != nil && cmd.Process != nil { - // Check if the process is still running before attempting to kill it - if isProcessRunning(cmd) { - // Use TerminateProcess syscall for more reliable termination on Windows - err := cmd.Process.Kill() - if err != nil { - log.Printf("Error killing process: %v", err) - } - - // Wait for the process to actually terminate in a separate goroutine to avoid blocking - go func(p *exec.Cmd) { - p.Wait() // Wait for the process to finish - }(cmd) - } + if cmd != nil { + killProcessAndWait(cmd) } } @@ -516,6 +551,12 @@ var proxyMutex sync.Mutex // Mutex to protect proxy operations // setSystemProxy enables the system proxy with the specified server and port func setSystemProxy(proxyServer string) error { + defer func() { + if r := recover(); r != nil { + log.Printf("Recovered from panic in setSystemProxy: %v", r) + } + }() + proxyMutex.Lock() defer proxyMutex.Unlock() @@ -523,7 +564,14 @@ func setSystemProxy(proxyServer string) error { if err != nil { return fmt.Errorf("failed to open registry key: %v", err) } - defer key.Close() + defer func() { + if key != 0 { + err := key.Close() + if err != nil { + log.Printf("Error closing registry key: %v", err) + } + } + }() // Set ProxyEnable to 1 to enable proxy err = key.SetDWordValue("ProxyEnable", 1) @@ -540,7 +588,8 @@ func setSystemProxy(proxyServer string) error { // Optionally, set ProxyOverride to bypass proxy for local addresses err = key.SetStringValue("ProxyOverride", "localhost;127.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;192.168.*") if err != nil { - return fmt.Errorf("failed to set ProxyOverride: %v", err) + // This is not critical, just log the error + log.Printf("Warning: failed to set ProxyOverride: %v", err) } // Notify Windows that the proxy settings have changed @@ -551,6 +600,12 @@ func setSystemProxy(proxyServer string) error { // disableSystemProxy disables the system proxy func disableSystemProxy() error { + defer func() { + if r := recover(); r != nil { + log.Printf("Recovered from panic in disableSystemProxy: %v", r) + } + }() + proxyMutex.Lock() defer proxyMutex.Unlock() @@ -558,7 +613,14 @@ func disableSystemProxy() error { if err != nil { return fmt.Errorf("failed to open registry key: %v", err) } - defer key.Close() + defer func() { + if key != 0 { + err := key.Close() + if err != nil { + log.Printf("Error closing registry key: %v", err) + } + } + }() // Set ProxyEnable to 0 to disable proxy err = key.SetDWordValue("ProxyEnable", 0) @@ -574,36 +636,11 @@ func disableSystemProxy() error { // notifyProxyChange notifies Windows that proxy settings have changed func notifyProxyChange() { - // Use Windows API to notify about proxy change - // Call InternetSetOption to refresh proxy settings - wininetDLL := syscall.NewLazyDLL("wininet.dll") - if wininetDLL != nil { - procInternetSetOption := wininetDLL.NewProc("InternetSetOptionW") - if procInternetSetOption != nil { - procInternetSetOption.Call( - 0, // hInternet = NULL - 39, // INTERNET_OPTION_SETTINGS_CHANGED - 0, // lpBuffer = NULL - 0, // dwBufferLength = 0 - ) - } - } - - // Also send WM_SETTINGCHANGE to broadcast the change - user32DLL := syscall.NewLazyDLL("user32.dll") - if user32DLL != nil { - procSendMessageTimeout := user32DLL.NewProc("SendMessageTimeoutW") - if procSendMessageTimeout != nil { - procSendMessageTimeout.Call( - 0xFFFF, // HWND_BROADCAST - 0x001A, // WM_SETTINGCHANGE - uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("Proxy"))), - 0, - 0x0002, // SMTO_ABORTIFHUNG - 5000, // timeout - ) - } - } + // Since Windows API calls are causing crashes, we'll skip them for now + // This is the safest approach to prevent access violations + log.Println("Skipping Windows API calls to prevent crashes") + // Note: Registry changes sometimes don't take effect without these calls, + // but crashing is worse than some cases where manual browser restart is needed } func main() { @@ -693,6 +730,9 @@ func main() { return } + // Brief pause to allow sing-box to initialize before setting proxy + time.Sleep(500 * time.Millisecond) + // Set proxy to route traffic through VPN err = setSystemProxy("127.0.0.1:1080") if err != nil { @@ -712,15 +752,18 @@ func main() { // Update connection state after successful connection isConnected = true } else { - // Disconnect - kill the current process - killCurrentProcess() - - // Disable proxy when disconnecting from VPN + // Disconnect - first disable proxy, then kill the process err := disableSystemProxy() if err != nil { log.Printf("Failed to disable system proxy: %v", err) } + // Brief pause to allow proxy settings to be applied + time.Sleep(200 * time.Millisecond) + + // Disconnect - kill the current process + killCurrentProcess() + // Update connection state after disconnection isConnected = false }