diff --git a/dbus.go b/dbus.go index 4585ffe..086bdf9 100644 --- a/dbus.go +++ b/dbus.go @@ -6,7 +6,7 @@ import ( "strings" "sync" - "github.com/godbus/dbus" + "github.com/godbus/dbus/v5" ) const ( @@ -329,7 +329,7 @@ func logBadProp(id, prop string, err error) { } func newDevice(id string) (*Device, error) { - conn, err := dbus.SessionBus() + conn, err := DBusSessionBusForPlatform() obj := conn.Object(dest, dbus.ObjectPath(fmt.Sprintf("%s/devices/%s", path, id))) if err != nil { return nil, err @@ -352,7 +352,7 @@ func newDevice(id string) (*Device, error) { } func newDeviceList() (*deviceList, error) { - conn, err := dbus.SessionBus() + conn, err := DBusSessionBusForPlatform() if err != nil { return nil, err } diff --git a/dbus_connection_unix.go b/dbus_connection_unix.go new file mode 100644 index 0000000..46cc003 --- /dev/null +++ b/dbus_connection_unix.go @@ -0,0 +1,10 @@ +//go:build !windows +// +build !windows + +package main + +import "github.com/godbus/dbus/v5" + +func DBusSessionBusForPlatform() (conn *dbus.Conn, err error) { + return dbus.SessionBus() +} diff --git a/dbus_connection_windows.go b/dbus_connection_windows.go new file mode 100644 index 0000000..5c4c39a --- /dev/null +++ b/dbus_connection_windows.go @@ -0,0 +1,149 @@ +//go:build windows +// +build windows + +package main + +import ( + "bytes" + "crypto/sha1" + "fmt" + "io" + "os" + "reflect" + "strings" + "syscall" + "time" + "errors" + "unsafe" + + "github.com/godbus/dbus/v5" + "golang.org/x/sys/windows" +) + +var ( + modkernel32 = windows.NewLazySystemDLL("kernel32.dll") + procOpenFileMappingW = modkernel32.NewProc("OpenFileMappingW") //syscall only provides CreateFileMapping + + cachedDbusSectionName = "" +) + +func normaliseAndHashDbusPath(kdeConnectPath string) string { + if !strings.HasSuffix(kdeConnectPath, "\\") { + kdeConnectPath = kdeConnectPath + "\\" + } + + if strings.HasSuffix(kdeConnectPath, "\\bin\\") { + kdeConnectPath = strings.TrimSuffix(kdeConnectPath, "bin\\") + } + + kdeConnectPath = strings.ToLower(kdeConnectPath) + + sha1 := sha1.New() + io.WriteString(sha1, kdeConnectPath) + kdeConnectPath = fmt.Sprintf("%x", sha1.Sum(nil)) // produces a lower-case hash, which is required + + return kdeConnectPath +} + +func readStringFromSection(sectionName string) (ret string, err error) { + lpSectionName, err := syscall.UTF16PtrFromString(sectionName) + if err != nil { + return "", err + } + + var hSharedMem uintptr + for i := 0; i < 3; i++ { + hSharedMem, _, err = syscall.Syscall(procOpenFileMappingW.Addr(), 3, windows.FILE_MAP_READ, 0, uintptr(unsafe.Pointer(lpSectionName))) + if hSharedMem == 0 { + time.Sleep(100 * time.Millisecond) + } else { + break + } + } + if hSharedMem == 0 { + return "", err + } + defer windows.CloseHandle(windows.Handle(hSharedMem)) + + lpsharedAddr, err := syscall.MapViewOfFile(syscall.Handle(hSharedMem), windows.FILE_MAP_READ, 0, 0, 0) + if err != nil { + return "", err + } + defer syscall.UnmapViewOfFile(lpsharedAddr) + + // obtain section size - rounded up to the page size + mbi := windows.MemoryBasicInformation{} + err = windows.VirtualQueryEx(windows.CurrentProcess(), uintptr(lpsharedAddr), &mbi, unsafe.Sizeof(mbi)) + if err != nil { + return "", err + } + + // get a byte[] representation of the mapping, using the same technique as syscall_unix.go + // alternatively, unsafe.Slice((*byte)(unsafe.Pointer(sec)), mbi.RegionSize) works, with a "possible misuse of unsafe.Pointer" warning + var bSharedAddr []byte + hdr := (*reflect.SliceHeader)(unsafe.Pointer(&bSharedAddr)) + hdr.Data = lpsharedAddr + hdr.Cap = int(mbi.RegionSize) + hdr.Len = int(mbi.RegionSize) + + // copy section's contents into this process + dbusAddress := make([]byte, len(bSharedAddr)) + copy(dbusAddress, bSharedAddr) + dbusAddress[len(dbusAddress) - 1] = 0 // force null-termination somewhere + + // assuming a valid string, get the first null-terminator and cap the new slice's length to it + strlen := bytes.IndexByte(dbusAddress, 0) + return string(dbusAddress[:strlen]), nil +} + +func DBusSessionBusForPlatform() (conn *dbus.Conn, err error) { + // If $DBUS_SESSION_BUS_ADDRESS is set, just use that + if this_env_sess_addr := os.Getenv("DBUS_SESSION_BUS_ADDRESS"); this_env_sess_addr != "" { + return dbus.SessionBus() + } + + var sectionName string = cachedDbusSectionName + if sectionName == "" { + // Prioritise Store-installed copies because it's more determinable + kdeConnectPath, err := KdeConnectPathFromWindowsStore() + if err != nil { + //fmt.Fprintln(os.Stderr, err) + kdeConnectPath, err = KdeConnectPathFromRunningIndicator() + } + + if err != nil { + return nil, err + } + + kdeConnectPath = normaliseAndHashDbusPath(kdeConnectPath) + + sectionName = "Local\\DBusDaemonAddressInfo-" + kdeConnectPath + } + + if sectionName != "" { + dbusAddress, err := readStringFromSection(sectionName) + if err != nil { + //cachedDbusSectionName = "" + return nil, err + } + + conn, err := dbus.Connect(dbusAddress) + if err == nil { + // Keep a copy of the computed section name to avoid the path lookups, + // but not the section contents itself, as KDE Connect could be started again + // from the same path but using a different port. + // The odds of its install directory being changed on the other hand are low + cachedDbusSectionName = sectionName + } else { + cachedDbusSectionName = "" + } + + return conn, err + } + + if err == nil { + err = errors.New("could not determine KDE Connect's D-Bus daemon location") + } + + return nil, err +} diff --git a/kdeconnect_path_running_process_windows.go b/kdeconnect_path_running_process_windows.go new file mode 100644 index 0000000..b6c398e --- /dev/null +++ b/kdeconnect_path_running_process_windows.go @@ -0,0 +1,79 @@ +//go:build windows +// +build windows + +package main + +import ( + "errors" + "unsafe" + "path/filepath" + + "golang.org/x/sys/windows" +) + +const ( + WTS_CURRENT_SERVER_HANDLE = 0 + WTS_CURRENT_SESSION = 0xFFFFFFFF + WTSTypeProcessInfoLevel0 = 0 +) + +type WTS_PROCESS_INFO struct { + SessionId uint32 + ProcessId uint32 + pProcessName *uint16 + pUserSid uintptr // incomplete: avoid defining a struct for something unneeded +} + +var ( + libwtsapi32 = windows.NewLazySystemDLL("wtsapi32.dll") + procWTSEnumerateProcessesExW = libwtsapi32.NewProc("WTSEnumerateProcessesExW") + procWTSFreeMemoryExW = libwtsapi32.NewProc("WTSFreeMemoryExW") +) + +func wtsFreeMemoryExW(WTSTypeClass uint32, pMemory unsafe.Pointer, NumberOfEntries uint32) { + procWTSFreeMemoryExW.Call(uintptr(WTSTypeClass), uintptr(pMemory), uintptr(NumberOfEntries)) +} + +func wtsEnumerateProcessesExW(hServer windows.Handle, pLevel *uint32, SessionId uint32, ppProcessInfo unsafe.Pointer, pCount *uint32) (ret bool, lastErr error) { + r1, _, lastErr := procWTSEnumerateProcessesExW.Call(uintptr(hServer), uintptr(unsafe.Pointer(pLevel)), uintptr(SessionId), uintptr(unsafe.Pointer(ppProcessInfo)), uintptr(unsafe.Pointer(pCount))) + return r1 != 0, lastErr +} + +func KdeConnectPathFromRunningIndicator() (ret string, err error) { + var ( + Level uint32 = WTSTypeProcessInfoLevel0 + pProcessInfo *WTS_PROCESS_INFO + count uint32 + + hostSessionId uint32 = WTS_CURRENT_SESSION + ) + // Only look at processes started in the same Windows session as the host is running in + windows.ProcessIdToSessionId(windows.GetCurrentProcessId(), &hostSessionId) + + r1, lastErr := wtsEnumerateProcessesExW(WTS_CURRENT_SERVER_HANDLE, &Level, hostSessionId, unsafe.Pointer(&pProcessInfo), &count) + if !r1 { + return "", lastErr + } + defer wtsFreeMemoryExW(Level, unsafe.Pointer(pProcessInfo), count) + + size := unsafe.Sizeof(WTS_PROCESS_INFO{}) + for i := uint32(0); i < count; i++ { + p := *(*WTS_PROCESS_INFO)(unsafe.Pointer(uintptr(unsafe.Pointer(pProcessInfo)) + (uintptr(size) * uintptr(i)))) + procName := windows.UTF16PtrToString(p.pProcessName) + if procName == "kdeconnect-indicator.exe" || procName == "kdeconnectd.exe" || procName == "kdeconnect-app.exe" { + hProcess, _ := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, p.ProcessId) + if hProcess != 0 { + var exeNameBuf [261]uint16 // MAX_PATH + 1 + exeNameLen := uint32(len(exeNameBuf) - 1) + err = windows.QueryFullProcessImageName(hProcess, 0, &exeNameBuf[0], &exeNameLen) + windows.CloseHandle(hProcess) + if err == nil { + exeName := windows.UTF16ToString(exeNameBuf[:exeNameLen]) + return filepath.Dir(exeName), nil + } + } + } + } + + return "", errors.New("could not find KDE Connect processes") +} diff --git a/kdeconnect_path_store_windows.go b/kdeconnect_path_store_windows.go new file mode 100644 index 0000000..c796962 --- /dev/null +++ b/kdeconnect_path_store_windows.go @@ -0,0 +1,152 @@ +//go:build windows +// +build windows + +package main + +import ( + "unsafe" + "syscall" + "os/user" + + "golang.org/x/sys/windows" + "github.com/go-ole/go-ole" +) + +type IPackageManager struct { + ole.IInspectable +} + +type IPackageManagerVtbl struct { + ole.IInspectableVtbl + _[14] uintptr // skip defining unused methods + FindPackagesByUserSecurityIdPackageFamilyName uintptr +} + +type IIterable struct { + ole.IInspectable +} + +type IIterableVtbl struct { + ole.IInspectableVtbl + First uintptr +} + +type IIterator struct { + ole.IInspectable +} + +type IIteratorVtbl struct { + ole.IInspectableVtbl + get_Current uintptr +} + +type IPackage struct { + ole.IInspectable +} + +type IPackageVtbl struct { + ole.IInspectableVtbl + _[1] uintptr + get_InstalledLocation uintptr +} + +type IStorageFolder struct { + ole.IInspectable +} + +type IStorageItem struct { + ole.IInspectable +} + +type IStorageItemVtbl struct { + ole.IInspectableVtbl + _[6] uintptr + get_Path uintptr +} + +/* go-ole's wrapping of RoInitialize treats S_FALSE as an error */ +var ( + modcombase = windows.NewLazySystemDLL("combase.dll") + procRoInitialize = modcombase.NewProc("RoInitialize") + procRoUninitialize = modcombase.NewProc("RoUninitialize") +) + +func KdeConnectPathFromWindowsStore() (ret string, err error) { + hr, _, _ := procRoInitialize.Call(uintptr(1)) // RO_INIT_MULTITHREADED + if int32(hr) < 0 { + return "", ole.NewError(hr) + } + defer procRoUninitialize.Call() + + // Get SID of running user - the package management APIs assume a search scoped to the system otherwise, which requires elevation + me, err := user.Current() + if err != nil { + return "", err + } + + inspectable, err := ole.RoActivateInstance("Windows.Management.Deployment.PackageManager") + if err != nil { + return "", err + } + defer inspectable.Release() + unk, err := inspectable.QueryInterface(ole.NewGUID("9a7d4b65-5e8f-4fc7-a2e5-7f6925cb8b53")) + if err != nil { + return "", err + } + defer unk.Release() + pm := (*IPackageManager)(unsafe.Pointer(unk)) + pmVtbl := (*IPackageManagerVtbl)(unsafe.Pointer(pm.RawVTable)) + + var iterable *IIterable + hsUid, _ := ole.NewHString(me.Uid) + hsPackageFamilyName, _ := ole.NewHString("KDEe.V.KDEConnect_7vt06qxq7ptv8") + defer ole.DeleteHString(hsUid) + defer ole.DeleteHString(hsPackageFamilyName) + hr, _, _ = syscall.Syscall6(pmVtbl.FindPackagesByUserSecurityIdPackageFamilyName, 4, uintptr(unsafe.Pointer(pm)), uintptr(hsUid), uintptr(hsPackageFamilyName), uintptr(unsafe.Pointer(&iterable)), 0, 0) + if hr != 0 { + return "", ole.NewError(hr) + } + defer iterable.Release() + iterableVtbl := (*IIterableVtbl)(unsafe.Pointer(iterable.RawVTable)) + + // TODO: Only the first result is looked at. Is it actually possible to have multiple KDE Connect installs from the Store? + var iterator *IIterator + hr, _, _ = syscall.Syscall(iterableVtbl.First, 2, uintptr(unsafe.Pointer(iterable)), uintptr(unsafe.Pointer(&iterator)), 0) + if hr != 0 { + return "", ole.NewError(hr) + } + defer iterator.Release() + iteratorVtbl := (*IIteratorVtbl)(unsafe.Pointer(iterator.RawVTable)) + + var ip *IPackage + hr, _, _ = syscall.Syscall(iteratorVtbl.get_Current, 2, uintptr(unsafe.Pointer(iterator)), uintptr(unsafe.Pointer(&ip)), 0) + if hr != 0 { + return "", ole.NewError(hr) + } + defer ip.Release() + ipVtbl := (*IPackageVtbl)(unsafe.Pointer(ip.RawVTable)) + + var isf *IStorageFolder + hr, _, _ = syscall.Syscall(ipVtbl.get_InstalledLocation, 2, uintptr(unsafe.Pointer(ip)), uintptr(unsafe.Pointer(&isf)), 0) + if hr != 0 { + return "", ole.NewError(hr) + } + defer isf.Release() + unk, err = isf.QueryInterface(ole.NewGUID("4207a996-ca2f-42f7-bde8-8b10457a7f30")) + if err != nil { + return "", err + } + defer unk.Release() + isi := (*IStorageItem)(unsafe.Pointer(unk)) + isiVtbl := (*IStorageItemVtbl)(unsafe.Pointer(isi.RawVTable)) + + var kdeConnectPath ole.HString + hr, _, _ = syscall.Syscall(isiVtbl.get_Path, 2, uintptr(unsafe.Pointer(isi)), uintptr(unsafe.Pointer(&kdeConnectPath)), 0) + if hr != 0 { + return "", ole.NewError(hr) + } + ret = kdeConnectPath.String() + ole.DeleteHString(kdeConnectPath) + + return ret, nil +}