Files

182 lines
4.3 KiB
Go
Raw Permalink Normal View History

package web
import (
"fmt"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
var (
shell32 = syscall.NewLazyDLL("shell32.dll")
shellExecuteExW = shell32.NewProc("ShellExecuteExW")
)
const (
SEE_MASK_NOCLOSEPROCESS = 0x00000040
SW_SHOW = 5
)
// SHELLEXECUTEINFO structure for ShellExecuteEx
type shellExecuteInfo struct {
cbSize uint32
fMask uint32
hwnd uintptr
lpVerb *uint16
lpFile *uint16
lpParameters *uint16
lpDirectory *uint16
nShow int32
hInstApp uintptr
lpIDList uintptr
lpClass *uint16
hkeyClass uintptr
dwHotKey uint32
hIconOrMonitor uintptr
hProcess windows.Handle
}
// BrowserHandle represents a handle to the opened browser process
type BrowserHandle struct {
ProcessID uint32
Handle windows.Handle
}
// OpenBrowser opens the default browser to the specified URL using Windows ShellExecute API
// Returns a handle to the browser process that can be used to close it later
func OpenBrowser(url string) (*BrowserHandle, error) {
// Convert strings to UTF16 pointers
operation, err := syscall.UTF16PtrFromString("open")
if err != nil {
return nil, fmt.Errorf("failed to convert operation string: %w", err)
}
urlPtr, err := syscall.UTF16PtrFromString(url)
if err != nil {
return nil, fmt.Errorf("failed to convert URL string: %w", err)
}
// Initialize SHELLEXECUTEINFO structure
sei := shellExecuteInfo{
cbSize: uint32(unsafe.Sizeof(shellExecuteInfo{})),
fMask: SEE_MASK_NOCLOSEPROCESS, // Request process handle
hwnd: 0,
lpVerb: operation,
lpFile: urlPtr,
lpParameters: nil,
lpDirectory: nil,
nShow: SW_SHOW,
hInstApp: 0,
}
// Call ShellExecuteExW
ret, _, err := shellExecuteExW.Call(uintptr(unsafe.Pointer(&sei)))
if ret == 0 {
return nil, fmt.Errorf("ShellExecuteExW failed: %w", err)
}
if sei.hInstApp <= 32 {
return nil, fmt.Errorf("ShellExecuteExW failed with code: %d", sei.hInstApp)
}
// Get process ID from handle
var processID uint32
if sei.hProcess != 0 {
processID, err = windows.GetProcessId(sei.hProcess)
if err != nil {
// If we can't get PID, still return the handle
processID = 0
}
}
fmt.Printf("[web] Browser opened successfully (PID: %d)\n", processID)
return &BrowserHandle{
ProcessID: processID,
Handle: sei.hProcess,
}, nil
}
// Close terminates the browser process and all child processes
func (bh *BrowserHandle) Close() error {
if bh == nil {
return nil
}
// First try to kill the direct process if we have a handle
if bh.Handle != 0 {
windows.CloseHandle(bh.Handle)
}
// Kill all browser processes that might have our URL open
// This is more reliable than trying to track the exact process tree
killed := killBrowserProcesses()
fmt.Printf("[web] Terminated %d browser process(es)\n", killed)
return nil
}
// killBrowserProcesses finds and kills common browser processes
func killBrowserProcesses() int {
browserExes := []string{
"chrome.exe",
"msedge.exe",
"firefox.exe",
"brave.exe",
"opera.exe",
"iexplore.exe",
}
killed := 0
for _, exeName := range browserExes {
count := killProcessByName(exeName)
killed += count
}
return killed
}
// killProcessByName kills all processes with the given executable name
func killProcessByName(exeName string) int {
snapshot, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0)
if err != nil {
return 0
}
defer windows.CloseHandle(snapshot)
var procEntry windows.ProcessEntry32
procEntry.Size = uint32(unsafe.Sizeof(procEntry))
err = windows.Process32First(snapshot, &procEntry)
if err != nil {
return 0
}
killed := 0
for {
// Convert the process name from [260]uint16 to string
processName := syscall.UTF16ToString(procEntry.ExeFile[:])
if processName == exeName {
// Open process with terminate rights
handle, err := windows.OpenProcess(windows.PROCESS_TERMINATE, false, procEntry.ProcessID)
if err == nil {
err = windows.TerminateProcess(handle, 0)
if err == nil {
killed++
fmt.Printf("[web] Killed %s (PID: %d)\n", exeName, procEntry.ProcessID)
}
windows.CloseHandle(handle)
}
}
err = windows.Process32Next(snapshot, &procEntry)
if err != nil {
break
}
}
return killed
}