draconisplusplus/src/os/windows.cpp
2025-04-29 01:31:35 -04:00

370 lines
12 KiB
C++

#ifdef _WIN32
// clang-format off
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <wincrypt.h>
#include <dwmapi.h>
#include <tlhelp32.h>
#include <ranges>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Media.Control.h>
#include <winrt/Windows.Storage.h>
#include <winrt/Windows.System.Profile.h>
#include <winrt/Windows.Management.Deployment.h>
#include <winrt/base.h>
#include <winrt/impl/Windows.Media.Control.2.h>
#include "src/core/util/error.hpp"
#include "src/core/util/helpers.hpp"
#include "src/core/util/logging.hpp"
#include "src/core/util/types.hpp"
#include "os.hpp"
// clang-format on
using RtlGetVersionPtr = NTSTATUS(WINAPI*)(PRTL_OSVERSIONINFOW);
namespace {
using util::error::DraconisError, util::error::DraconisErrorCode;
using namespace util::types;
struct ProcessData {
DWORD parentPid = 0;
String baseExeNameLower;
};
// clang-format off
constexpr Array<Pair<StringView, StringView>, 3> windowsShellMap = {{
{ "cmd", "Command Prompt" },
{ "powershell", "PowerShell" },
{ "pwsh", "PowerShell Core" },
}};
constexpr Array<Pair<StringView, StringView>, 3> msysShellMap = {{
{ "bash", "Bash" },
{ "zsh", "Zsh" },
{ "fish", "Fish" },
}};
// clang-format on
fn GetRegistryValue(const HKEY& hKey, const String& subKey, const String& valueName) -> String {
HKEY key = nullptr;
if (RegOpenKeyExA(hKey, subKey.c_str(), 0, KEY_READ, &key) != ERROR_SUCCESS)
return "";
DWORD dataSize = 0;
DWORD type = 0;
if (RegQueryValueExA(key, valueName.c_str(), nullptr, &type, nullptr, &dataSize) != ERROR_SUCCESS) {
RegCloseKey(key);
return "";
}
String value((type == REG_SZ || type == REG_EXPAND_SZ) ? dataSize - 1 : dataSize, '\0');
if (RegQueryValueExA(key, valueName.c_str(), nullptr, nullptr, reinterpret_cast<LPBYTE>(value.data()), &dataSize) !=
ERROR_SUCCESS) {
RegCloseKey(key);
return "";
}
RegCloseKey(key);
return value;
}
template <usize sz>
fn FindShellInProcessTree(const DWORD startPid, const Array<Pair<StringView, StringView>, sz>& shellMap)
-> Option<String> {
if (startPid == 0)
return None;
std::unordered_map<DWORD, ProcessData> processMap;
const HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnap == INVALID_HANDLE_VALUE) {
error_log("FindShellInProcessTree: Failed snapshot, error {}", GetLastError());
return None;
}
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(hSnap, &pe32)) {
do {
String fullName = pe32.szExeFile;
String baseName;
const size_t lastSlash = fullName.find_last_of("\\/");
baseName = (lastSlash == String::npos) ? fullName : fullName.substr(lastSlash + 1);
std::transform(baseName.begin(), baseName.end(), baseName.begin(), [](const u8 character) {
return std::tolower(character);
});
if (baseName.length() > 4 && baseName.ends_with(".exe"))
baseName.resize(baseName.length() - 4);
processMap[pe32.th32ProcessID] = ProcessData { pe32.th32ParentProcessID, std::move(baseName) };
} while (Process32Next(hSnap, &pe32));
} else {
error_log("FindShellInProcessTree: Process32First failed, error {}", GetLastError());
}
CloseHandle(hSnap);
DWORD currentPid = startPid;
i32 depth = 0;
constexpr int maxDepth = 32;
while (currentPid != 0 && depth < maxDepth) {
auto procIt = processMap.find(currentPid);
if (procIt == processMap.end())
break;
const String& processName = procIt->second.baseExeNameLower;
auto mapIter =
std::ranges::find_if(shellMap, [&](const auto& pair) { return StringView { processName } == pair.first; });
if (mapIter != std::ranges::end(shellMap))
return String { mapIter->second };
currentPid = procIt->second.parentPid;
depth++;
}
if (depth >= maxDepth)
error_log("FindShellInProcessTree: Reached max depth limit ({}) walking parent PIDs from {}", maxDepth, startPid);
return None;
}
fn GetBuildNumber() -> Option<u64> {
try {
using namespace winrt::Windows::System::Profile;
const auto versionInfo = AnalyticsInfo::VersionInfo();
const winrt::hstring familyVersion = versionInfo.DeviceFamilyVersion();
if (!familyVersion.empty()) {
const u64 versionUl = std::stoull(winrt::to_string(familyVersion));
return (versionUl >> 16) & 0xFFFF;
}
} catch (const winrt::hresult_error& e) {
debug_log("WinRT error getting build number: {}", winrt::to_string(e.message()));
} catch (const Exception& e) { debug_log("Standard exception getting build number: {}", e.what()); }
return None;
}
} // namespace
namespace os {
fn GetMemInfo() -> Result<u64, DraconisError> {
MEMORYSTATUSEX memInfo;
memInfo.dwLength = sizeof(MEMORYSTATUSEX);
if (GlobalMemoryStatusEx(&memInfo))
return memInfo.ullTotalPhys;
DWORD lastError = GetLastError();
return Err(DraconisError(
DraconisErrorCode::PlatformSpecific, std::format("GlobalMemoryStatusEx failed with error code {}", lastError)
));
}
fn GetNowPlaying() -> Result<MediaInfo, DraconisError> {
using namespace winrt::Windows::Media::Control;
using namespace winrt::Windows::Foundation;
using Session = GlobalSystemMediaTransportControlsSession;
using SessionManager = GlobalSystemMediaTransportControlsSessionManager;
using MediaProperties = GlobalSystemMediaTransportControlsSessionMediaProperties;
try {
const IAsyncOperation<SessionManager> sessionManagerOp = SessionManager::RequestAsync();
const SessionManager sessionManager = sessionManagerOp.get();
if (const Session currentSession = sessionManager.GetCurrentSession()) {
const MediaProperties mediaProperties = currentSession.TryGetMediaPropertiesAsync().get();
return MediaInfo(
winrt::to_string(mediaProperties.Title()), winrt::to_string(mediaProperties.Artist()), None, None
);
}
return Err(DraconisError(DraconisErrorCode::NotFound, "No media session found"));
} catch (const winrt::hresult_error& e) { return Err(DraconisError(e)); }
}
fn GetOSVersion() -> Result<String, DraconisError> {
try {
const String regSubKey = R"(SOFTWARE\Microsoft\Windows NT\CurrentVersion)";
String productName = GetRegistryValue(HKEY_LOCAL_MACHINE, regSubKey, "ProductName");
const String displayVersion = GetRegistryValue(HKEY_LOCAL_MACHINE, regSubKey, "DisplayVersion");
if (productName.empty())
return Err(DraconisError(DraconisErrorCode::NotFound, "ProductName not found in registry"));
if (const Option<u64> buildNumberOpt = GetBuildNumber()) {
if (const u64 buildNumber = *buildNumberOpt; buildNumber >= 22000) {
if (const size_t pos = productName.find("Windows 10"); pos != String::npos) {
const bool startBoundary = (pos == 0 || !isalnum(static_cast<unsigned char>(productName[pos - 1])));
const bool endBoundary =
(pos + 10 == productName.length() || !isalnum(static_cast<unsigned char>(productName[pos + 10])));
if (startBoundary && endBoundary) {
productName.replace(pos, 10, "Windows 11");
}
}
}
} else {
debug_log("Warning: Could not get build number via WinRT; Win11 detection might be inaccurate.");
}
return displayVersion.empty() ? productName : productName + " " + displayVersion;
} catch (const std::exception& e) { return Err(DraconisError(e)); }
}
fn GetHost() -> Result<String, DraconisError> {
return GetRegistryValue(HKEY_LOCAL_MACHINE, R"(SYSTEM\HardwareConfig\Current)", "SystemFamily");
}
fn GetKernelVersion() -> Result<String, DraconisError> {
if (const HMODULE ntdllHandle = GetModuleHandleW(L"ntdll.dll")) {
if (const auto rtlGetVersion = std::bit_cast<RtlGetVersionPtr>(GetProcAddress(ntdllHandle, "RtlGetVersion"))) {
RTL_OSVERSIONINFOW osInfo = {};
osInfo.dwOSVersionInfoSize = sizeof(osInfo);
if (rtlGetVersion(&osInfo) == 0)
return std::format(
"{}.{}.{}.{}", osInfo.dwMajorVersion, osInfo.dwMinorVersion, osInfo.dwBuildNumber, osInfo.dwPlatformId
);
}
}
return Err(DraconisError(DraconisErrorCode::NotFound, "Could not determine kernel version using RtlGetVersion"));
}
fn GetWindowManager() -> Option<String> {
BOOL compositionEnabled = FALSE;
if (SUCCEEDED(DwmIsCompositionEnabled(&compositionEnabled))) {
return compositionEnabled ? "DWM" : "Windows Manager (Basic)";
}
error_log("GetWindowManager: DwmIsCompositionEnabled failed");
return None;
}
fn GetDesktopEnvironment() -> Option<String> {
const String buildStr =
GetRegistryValue(HKEY_LOCAL_MACHINE, R"(SOFTWARE\Microsoft\Windows NT\CurrentVersion)", "CurrentBuildNumber");
if (buildStr.empty()) {
debug_log("Failed to get CurrentBuildNumber from registry");
return None;
}
try {
const i32 build = stoi(buildStr);
// Windows 11+ (Fluent)
if (build >= 22000)
return "Fluent (Windows 11)";
// Windows 10 Fluent Era
if (build >= 15063)
return "Fluent (Windows 10)";
// Windows 8.1/10 Metro Era
if (build >= 9200) { // Windows 8+
// Distinguish between Windows 8 and 10
const String productName =
GetRegistryValue(HKEY_LOCAL_MACHINE, R"(SOFTWARE\Microsoft\Windows NT\CurrentVersion)", "ProductName");
if (productName.find("Windows 10") != String::npos)
return "Metro (Windows 10)";
if (build >= 9600)
return "Metro (Windows 8.1)";
return "Metro (Windows 8)";
}
// Windows 7 Aero
if (build >= 7600)
return "Aero (Windows 7)";
// Pre-Win7
return "Classic";
} catch (...) {
debug_log("Failed to parse CurrentBuildNumber");
return None;
}
}
fn GetShell() -> Option<String> {
using util::helpers::GetEnv;
if (const Result<String, DraconisError> msystemResult = GetEnv("MSYSTEM");
msystemResult && !msystemResult->empty()) {
String shellPath;
if (const Result<String, DraconisError> shellResult = GetEnv("SHELL"); shellResult && !shellResult->empty()) {
shellPath = *shellResult;
} else if (const Result<String, DraconisError> loginShellResult = GetEnv("LOGINSHELL");
loginShellResult && !loginShellResult->empty()) {
shellPath = *loginShellResult;
}
if (!shellPath.empty()) {
const usize lastSlash = shellPath.find_last_of("\\/");
String shellExe = (lastSlash != String::npos) ? shellPath.substr(lastSlash + 1) : shellPath;
std::ranges::transform(shellExe, shellExe.begin(), [](const u8 character) { return std::tolower(character); });
if (shellExe.ends_with(".exe"))
shellExe.resize(shellExe.length() - 4);
const auto iter =
std::ranges::find_if(msysShellMap, [&](const auto& pair) { return StringView { shellExe } == pair.first; });
if (iter != std::ranges::end(msysShellMap))
return String { iter->second };
}
const DWORD currentPid = GetCurrentProcessId();
if (const Option<String> msysShell = FindShellInProcessTree(currentPid, msysShellMap))
return msysShell;
return "MSYS2 Environment";
}
const DWORD currentPid = GetCurrentProcessId();
if (const Option<String> windowsShell = FindShellInProcessTree(currentPid, windowsShellMap))
return windowsShell;
return None;
}
fn GetDiskUsage() -> Result<DiskSpace, DraconisError> {
ULARGE_INTEGER freeBytes, totalBytes;
if (GetDiskFreeSpaceExW(L"C:\\", nullptr, &totalBytes, &freeBytes))
return DiskSpace { .used_bytes = totalBytes.QuadPart - freeBytes.QuadPart, .total_bytes = totalBytes.QuadPart };
return Err(DraconisError(util::error::DraconisErrorCode::NotFound, "Failed to get disk usage"));
}
fn GetPackageCount() -> Result<u64, DraconisError> {
try {
return std::ranges::distance(winrt::Windows::Management::Deployment::PackageManager().FindPackagesForUser(L""));
} catch (const winrt::hresult_error& e) { return Err(DraconisError(e)); }
}
} // namespace os
#endif