248 lines
6.6 KiB
Go
248 lines
6.6 KiB
Go
//go:build windows
|
|
|
|
package apps
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"golang.org/x/sys/windows/registry"
|
|
)
|
|
|
|
// ScanApps returns a list of installed applications via Registry
|
|
func ScanApps() ([]AppInfo, error) {
|
|
var apps []AppInfo
|
|
|
|
// Keys to search
|
|
// HKLM Software\Microsoft\Windows\CurrentVersion\Uninstall
|
|
// HKLM Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall
|
|
// HKCU Software\Microsoft\Windows\CurrentVersion\Uninstall
|
|
|
|
keys := []struct {
|
|
hive registry.Key
|
|
path string
|
|
}{
|
|
{registry.LOCAL_MACHINE, `Software\Microsoft\Windows\CurrentVersion\Uninstall`},
|
|
{registry.LOCAL_MACHINE, `Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall`},
|
|
{registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Uninstall`},
|
|
}
|
|
|
|
seen := make(map[string]bool)
|
|
|
|
for _, k := range keys {
|
|
baseKey, err := registry.OpenKey(k.hive, k.path, registry.READ)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
subkeys, err := baseKey.ReadSubKeyNames(-1)
|
|
baseKey.Close()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
for _, subkeyName := range subkeys {
|
|
appKey, err := registry.OpenKey(k.hive, k.path+`\`+subkeyName, registry.READ)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
displayName, _, err := appKey.GetStringValue("DisplayName")
|
|
if err != nil || displayName == "" {
|
|
appKey.Close()
|
|
continue
|
|
}
|
|
|
|
// Define installLocation explicitly
|
|
installLocation, _, _ := appKey.GetStringValue("InstallLocation")
|
|
uninstallString, _, _ := appKey.GetStringValue("UninstallString")
|
|
quietUninstallString, _, _ := appKey.GetStringValue("QuietUninstallString")
|
|
|
|
if uninstallString == "" && quietUninstallString != "" {
|
|
uninstallString = quietUninstallString
|
|
}
|
|
|
|
// Debug Log
|
|
if strings.Contains(displayName, "Foxit") {
|
|
fmt.Printf("found Foxit: %s | UninstallString: %s\n", displayName, uninstallString)
|
|
}
|
|
|
|
// Deduplication: If we've seen this Name + Location combination, skip it.
|
|
// This handles the common case of 32-bit apps appearing in both HKLM and WOW6432Node.
|
|
dedupKey := displayName + "|" + strings.ToLower(installLocation)
|
|
if seen[dedupKey] {
|
|
appKey.Close()
|
|
continue
|
|
}
|
|
seen[dedupKey] = true
|
|
|
|
// Try to get size from registry (EstimatedSize is in KB)
|
|
sizeVal, _, errSize := appKey.GetIntegerValue("EstimatedSize")
|
|
var sizeBytes int64
|
|
if errSize == nil {
|
|
sizeBytes = int64(sizeVal) * 1024
|
|
}
|
|
|
|
// Construct Full Registry Key Path as BundleID for later use
|
|
hiveName := "HKLM"
|
|
if k.hive == registry.CURRENT_USER {
|
|
hiveName = "HKCU"
|
|
}
|
|
fullRegPath := hiveName + `\` + k.path + `\` + subkeyName
|
|
|
|
apps = append(apps, AppInfo{
|
|
Name: displayName,
|
|
Path: installLocation,
|
|
BundleID: fullRegPath,
|
|
UninstallString: uninstallString,
|
|
Size: sizeBytes,
|
|
})
|
|
appKey.Close()
|
|
}
|
|
}
|
|
|
|
return apps, nil
|
|
}
|
|
|
|
// GetAppDetails finds all associated files (simplified for Windows)
|
|
func GetAppDetails(appPath, bundleID string) (*AppDetails, error) {
|
|
// appPath might come from ScanApps which set it to InstallLocation.
|
|
// bundleID is used as the Registry Key Path.
|
|
|
|
// Re-construct basic info
|
|
info := AppInfo{
|
|
Name: filepath.Base(appPath),
|
|
Path: appPath,
|
|
BundleID: bundleID,
|
|
// UninstallString is hard to recover if not passed, but usually we call GetAppDetails after ScanApps which has it.
|
|
// For now leave empty, or we'd need to re-query registry if bundleID is a registry path.
|
|
Size: 0,
|
|
}
|
|
|
|
if appPath == "" && bundleID != "" {
|
|
// Fallback name if path is empty
|
|
parts := strings.Split(bundleID, `\`)
|
|
if len(parts) > 0 {
|
|
info.Name = parts[len(parts)-1]
|
|
}
|
|
}
|
|
|
|
details := &AppDetails{
|
|
AppInfo: info,
|
|
TotalSize: 0,
|
|
}
|
|
|
|
// 1. Scan File System
|
|
if appPath != "" {
|
|
var size int64
|
|
filepath.WalkDir(appPath, func(_ string, d os.DirEntry, err error) error {
|
|
if err == nil && !d.IsDir() {
|
|
i, _ := d.Info()
|
|
size += i.Size()
|
|
}
|
|
return nil
|
|
})
|
|
|
|
details.AppInfo.Size = size
|
|
details.TotalSize = size
|
|
|
|
// Add the main folder as associated data
|
|
details.Associated = append(details.Associated, AssociatedFile{
|
|
Path: appPath,
|
|
Type: "data",
|
|
Size: size,
|
|
})
|
|
}
|
|
|
|
// 2. Add Registry Key (Uninstall Entry)
|
|
if bundleID != "" && (strings.HasPrefix(bundleID, "HKLM") || strings.HasPrefix(bundleID, "HKCU")) {
|
|
// We treat the registry key as a "file" with special type and 0 size
|
|
details.Associated = append(details.Associated, AssociatedFile{
|
|
Path: "REG:" + bundleID,
|
|
Type: "registry", // New type
|
|
Size: 0, // Registry entries are negligible in size
|
|
})
|
|
}
|
|
|
|
return details, nil
|
|
}
|
|
|
|
// DeleteFiles removes the requested paths
|
|
func DeleteFiles(paths []string) error {
|
|
for _, p := range paths {
|
|
if p == "" {
|
|
continue
|
|
}
|
|
|
|
// Registry Deletion
|
|
if strings.HasPrefix(p, "REG:") {
|
|
regPath := strings.TrimPrefix(p, "REG:")
|
|
deleteRegistryKey(regPath)
|
|
continue
|
|
}
|
|
|
|
// Safety checks
|
|
if p == "C:\\" || p == "c:\\" ||
|
|
p == "C:\\Windows" || strings.HasPrefix(strings.ToLower(p), "c:\\windows") {
|
|
continue
|
|
}
|
|
|
|
err := os.RemoveAll(p)
|
|
if err != nil {
|
|
// Log error but continue? Or return?
|
|
// return err
|
|
// On Windows file locking is common, best effort
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func deleteRegistryKey(fullPath string) error {
|
|
var hive registry.Key
|
|
var subPath string
|
|
|
|
if strings.HasPrefix(fullPath, "HKLM\\") {
|
|
hive = registry.LOCAL_MACHINE
|
|
subPath = strings.TrimPrefix(fullPath, "HKLM\\")
|
|
} else if strings.HasPrefix(fullPath, "HKCU\\") {
|
|
hive = registry.CURRENT_USER
|
|
subPath = strings.TrimPrefix(fullPath, "HKCU\\")
|
|
} else {
|
|
return nil
|
|
}
|
|
|
|
// Provide parent key and subkey name to DeleteKey
|
|
// path: Software\...\Uninstall\AppGUID
|
|
lastSlash := strings.LastIndex(subPath, `\`)
|
|
if lastSlash == -1 {
|
|
return nil
|
|
}
|
|
parentPath := subPath[:lastSlash]
|
|
keyName := subPath[lastSlash+1:]
|
|
|
|
k, err := registry.OpenKey(hive, parentPath, registry.WRITE)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer k.Close()
|
|
|
|
return registry.DeleteKey(k, keyName)
|
|
}
|
|
|
|
// RunUninstaller executes the uninstall command
|
|
func RunUninstaller(cmdString string) error {
|
|
fmt.Printf("RunUninstaller Called with: %s\n", cmdString)
|
|
cmd := exec.Command("cmd", "/C", cmdString)
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: false} // Show window so user can click next
|
|
err := cmd.Start()
|
|
if err != nil {
|
|
fmt.Printf("RunUninstaller Error: %v\n", err)
|
|
return err
|
|
}
|
|
fmt.Printf("RunUninstaller Started Successfully\n")
|
|
return nil
|
|
}
|