#include "sanal_masaustu.hpp" #include #include // IServiceProvider — dokumante, standart (undocumented DEGIL) #include #include "windows_surum.hpp" namespace fiko { namespace { IVirtualDesktopManager* g_vdm = nullptr; bool g_denendi = false; } // namespace // ============================================================================ // FAZ 2 — IVirtualDesktopManagerInternal / IApplicationViewCollection // ============================================================================ // Bu arayuzler UNDOCUMENTED'dir (Windows SDK header'larinda YOK). Asagidaki // GUID'ler ve vtable siralamalari, MScholtes/VirtualDesktop projesinin // (MIT lisansli, github.com/MScholtes/VirtualDesktop) Windows surumune-ozel // C# kaynak dosyalarindan (VirtualDesktopServer2016.cs / VirtualDesktop.cs / // VirtualDesktop11.cs / VirtualDesktop11-24H2.cs) derlenen OLGUSAL COM ABI // bilgisidir — GUID/metod-sirasi telif konusu olan "eser" degil, protokol // gercegidir; kod burada SIFIRDAN, projenin kendi adlandirma/yorum kuralina // gore yazildi (C# kaynagindan kopya-yapistir degil). // // DOGRULANMIS OLGU (proje dokumaninin "KRITIK BULGU" bolumu ile TUTARLI): // Microsoft bu arayuzlerin GUID'ini VE vtable sirasini Windows surumleri // ARASINDA DEGISTIRIYOR — bazen ayni IID degeri bile FARKLI bir vtable'a // karsilik gelebiliyor (cunku IID sozlesmesi yalniz OS'nin kendi ic // bilesenleri icin gecerli, disaridan hicbir uyumluluk sozu YOK). Bu yuzden // dogru struct/IID SECIMI calisma zamaninda gercek build/UBR numarasina gore // yapilmali (bkz. windows_surum.hpp) — Faz 1'deki tek-yontem varsayimi burada // YOK, her sekil ayri struct. // // KAPSAM (bilerek DAR): yalnizca istemci Windows 10 (build 17763+, yani 1809 // ve sonrasi) ve Windows 11 (tum surumler) hedeflenir — Windows Server // SKU'lari (farkli, "hWndOrMon" parametreli COK BASKA bir vtable kullanir) // bilerek KAPSAM DISI, cagirinca DesteklenmiyorSurum doner. build < 17763 // (Windows 10 1607-1803) icin IApplicationViewCollection IID'si FARKLI // oldugundan (bu surumler test edilemedigi icin) o dal da DesteklenmiyorSurum // doner — sahte destek iddiasi YOK. namespace { // ---- Sabit (surumler arasi DEGISMEYEN) GUID'ler ---- // CLSID_ImmersiveShell — QueryService alabilmek icin CoCreateInstance edilen // kabuk nesnesi. static const CLSID kClsidImmersiveShell = { 0xC2F03A33, 0x21F5, 0x47FA, {0xB4, 0xBB, 0x15, 0x63, 0x62, 0xA2, 0xF2, 0x39}}; // CLSID_VirtualDesktopManagerInternal — shell->QueryService'e "hangi servisi // istiyorum" olarak verilen kimlik (IID DEGIL). static const GUID kClsidVdmInternal = { 0xC5E0CDCA, 0x7B6E, 0x41B2, {0x9F, 0xC4, 0xD9, 0x39, 0x75, 0xCC, 0x46, 0x7B}}; // IApplicationViewCollection IID — hem servis hem arayuz kimligi olarak ayni // deger kullanilir (kaynak projede de boyle: QueryService(iid, iid)). // Windows 10 1809+ / Windows 11 (24H2 dahil) icin GECERLI. static const IID kIidAppViewCollectionModern = { 0x1841C6D7, 0x4F9D, 0x42C0, {0xAF, 0x41, 0x87, 0x47, 0x53, 0x8F, 0x10, 0xE5}}; // ---- Surume gore DEGISEN IVirtualDesktopManagerInternal IID'leri ---- // Sekil A: Windows 10 (1809-22H2 arasi tum build'ler). static const IID kIidVdmInternal_SekilA = { 0xF31574D6, 0xB682, 0x4CDC, {0xBD, 0x56, 0x18, 0x27, 0x86, 0x0A, 0xBE, 0xC6}}; // Sekil B/C: Windows 11 — AYNI IID degeri iki FARKLI vtable'a karsilik gelir // (OS build'ine gore hangi gercek nesne dondugu degisir, bkz. dosya basi not). static const IID kIidVdmInternal_SekilBC = { 0x53F5CA0B, 0x158F, 0x4124, {0x90, 0x0C, 0x05, 0x71, 0x58, 0x06, 0x0B, 0x27}}; // ---- IVirtualDesktop: IsViewVisible+GetId slotlari TUM surumlerde ayni ---- struct FikoIVirtualDesktop : public IUnknown { virtual HRESULT STDMETHODCALLTYPE IsViewVisible(IUnknown* gorunum, int* gorunurCikis) = 0; virtual HRESULT STDMETHODCALLTYPE GetId(GUID* idCikis) = 0; }; // ---- IApplicationViewCollection: GetViewForHwnd'e kadar TUM surumlerde ayni // sirada (Server2016 dahil) ---- struct FikoIApplicationViewCollection : public IUnknown { virtual HRESULT STDMETHODCALLTYPE GetViews(void** dizi) = 0; virtual HRESULT STDMETHODCALLTYPE GetViewsByZOrder(void** dizi) = 0; virtual HRESULT STDMETHODCALLTYPE GetViewsByAppUserModelId(LPCWSTR id, void** dizi) = 0; virtual HRESULT STDMETHODCALLTYPE GetViewForHwnd(HWND hwnd, IUnknown** gorunumCikis) = 0; }; // ---- IVirtualDesktopManagerInternal — 3 FARKLI vtable sekli ---- // Sekil A (Windows 10, kIidVdmInternal_SekilA): GetCount..FindDesktop, ARA // metot YOK. struct FikoIVdmInternal_SekilA : public IUnknown { virtual HRESULT STDMETHODCALLTYPE GetCount(int* sayiCikis) = 0; virtual HRESULT STDMETHODCALLTYPE MoveViewToDesktop(IUnknown* gorunum, FikoIVirtualDesktop* masaustu) = 0; virtual HRESULT STDMETHODCALLTYPE CanViewMoveDesktops(IUnknown* gorunum, int* cikis) = 0; virtual HRESULT STDMETHODCALLTYPE GetCurrentDesktop(FikoIVirtualDesktop** cikis) = 0; virtual HRESULT STDMETHODCALLTYPE GetDesktops(void** diziCikis) = 0; virtual HRESULT STDMETHODCALLTYPE GetAdjacentDesktop(FikoIVirtualDesktop* kaynak, int yon, FikoIVirtualDesktop** cikis) = 0; virtual HRESULT STDMETHODCALLTYPE SwitchDesktop(FikoIVirtualDesktop* masaustu) = 0; virtual HRESULT STDMETHODCALLTYPE CreateDesktop(FikoIVirtualDesktop** cikis) = 0; virtual HRESULT STDMETHODCALLTYPE RemoveDesktop(FikoIVirtualDesktop* masaustu, FikoIVirtualDesktop* yedek) = 0; virtual HRESULT STDMETHODCALLTYPE FindDesktop(const GUID* id, FikoIVirtualDesktop** cikis) = 0; }; // Sekil B (Windows 11, build 22000..22621 & UBR<3085): CreateDesktop'tan SONRA // fazladan MoveDesktop slotu var. struct FikoIVdmInternal_SekilB : public IUnknown { virtual HRESULT STDMETHODCALLTYPE GetCount(int* sayiCikis) = 0; virtual HRESULT STDMETHODCALLTYPE MoveViewToDesktop(IUnknown* gorunum, FikoIVirtualDesktop* masaustu) = 0; virtual HRESULT STDMETHODCALLTYPE CanViewMoveDesktops(IUnknown* gorunum, int* cikis) = 0; virtual HRESULT STDMETHODCALLTYPE GetCurrentDesktop(FikoIVirtualDesktop** cikis) = 0; virtual HRESULT STDMETHODCALLTYPE GetDesktops(void** diziCikis) = 0; virtual HRESULT STDMETHODCALLTYPE GetAdjacentDesktop(FikoIVirtualDesktop* kaynak, int yon, FikoIVirtualDesktop** cikis) = 0; virtual HRESULT STDMETHODCALLTYPE SwitchDesktop(FikoIVirtualDesktop* masaustu) = 0; virtual HRESULT STDMETHODCALLTYPE CreateDesktop(FikoIVirtualDesktop** cikis) = 0; virtual HRESULT STDMETHODCALLTYPE MoveDesktop(FikoIVirtualDesktop* masaustu, int indeks) = 0; virtual HRESULT STDMETHODCALLTYPE RemoveDesktop(FikoIVirtualDesktop* masaustu, FikoIVirtualDesktop* yedek) = 0; virtual HRESULT STDMETHODCALLTYPE FindDesktop(const GUID* id, FikoIVirtualDesktop** cikis) = 0; }; // Sekil C (Windows 11 23H2 build 22621 UBR>=3085 VE 24H2+): SwitchDesktop'tan // SONRA fazladan SwitchDesktopAndMoveForegroundView slotu VAR (Sekil B'ye ek // olarak). struct FikoIVdmInternal_SekilC : public IUnknown { virtual HRESULT STDMETHODCALLTYPE GetCount(int* sayiCikis) = 0; virtual HRESULT STDMETHODCALLTYPE MoveViewToDesktop(IUnknown* gorunum, FikoIVirtualDesktop* masaustu) = 0; virtual HRESULT STDMETHODCALLTYPE CanViewMoveDesktops(IUnknown* gorunum, int* cikis) = 0; virtual HRESULT STDMETHODCALLTYPE GetCurrentDesktop(FikoIVirtualDesktop** cikis) = 0; virtual HRESULT STDMETHODCALLTYPE GetDesktops(void** diziCikis) = 0; virtual HRESULT STDMETHODCALLTYPE GetAdjacentDesktop(FikoIVirtualDesktop* kaynak, int yon, FikoIVirtualDesktop** cikis) = 0; virtual HRESULT STDMETHODCALLTYPE SwitchDesktop(FikoIVirtualDesktop* masaustu) = 0; virtual HRESULT STDMETHODCALLTYPE SwitchDesktopAndMoveForegroundView( FikoIVirtualDesktop* masaustu) = 0; virtual HRESULT STDMETHODCALLTYPE CreateDesktop(FikoIVirtualDesktop** cikis) = 0; virtual HRESULT STDMETHODCALLTYPE MoveDesktop(FikoIVirtualDesktop* masaustu, int indeks) = 0; virtual HRESULT STDMETHODCALLTYPE RemoveDesktop(FikoIVirtualDesktop* masaustu, FikoIVirtualDesktop* yedek) = 0; virtual HRESULT STDMETHODCALLTYPE FindDesktop(const GUID* id, FikoIVirtualDesktop** cikis) = 0; }; enum class VdmSekli { Yok, A, B, C }; // Bu makinenin gercek build/UBR'sine gore hangi sekli kullanacagimizi secer // (windows_surum.hpp — RtlGetVersion tabanli, manifest yalanindan etkilenmez). VdmSekli vdmSekliBelirle() { const WindowsSurumBilgi& w = windowsSurumTespitEt(); if (!w.tespitBasarili) return VdmSekli::Yok; if (!w.windows11Mi) { // Windows 10: yalniz 1809+ (build 17763+) desteklenir — bkz. dosya basi // "KAPSAM" notu (1607-1803 icin IApplicationViewCollection IID'si farkli // ve DOGRULANMADI). if (w.build < 17763) return VdmSekli::Yok; return VdmSekli::A; } // Windows 11. if (w.build > 22621) return VdmSekli::C; // 23H2 sonrasi/24H2+ (build 22635+/26100+) if (w.build == 22621 && w.ubr >= 3085) return VdmSekli::C; // 23H2 (22621.3085+) return VdmSekli::B; // 22H2 erken (22000-22621, UBR<3085) / 21H2 } // shell->QueryService(CLSID_VirtualDesktopManagerInternal, dogruIID) yapip // void* IUnknown* dondurur — cagiran, secilen sekle gore DOGRU struct'a // reinterpret_cast eder. Basarisizlikta nullptr + HRESULT'u hataCikis'e yazar. IUnknown* vdmInternalAl(IServiceProvider* shell, VdmSekli sekil, HRESULT& hrCikis) { const IID* iid = (sekil == VdmSekli::A) ? &kIidVdmInternal_SekilA : &kIidVdmInternal_SekilBC; IUnknown* sonuc = nullptr; hrCikis = shell->QueryService(kClsidVdmInternal, *iid, reinterpret_cast(&sonuc)); return SUCCEEDED(hrCikis) ? sonuc : nullptr; } // Ortak akis: ImmersiveShell CoCreateInstance + IServiceProvider al. Basarisiz // olursa nullptr, cagiran Release etmekle yukumlu. IServiceProvider* immersiveShellAl(HRESULT& hrCikis) { IServiceProvider* shell = nullptr; hrCikis = CoCreateInstance(kClsidImmersiveShell, nullptr, CLSCTX_LOCAL_SERVER, IID_IServiceProvider, reinterpret_cast(&shell)); return SUCCEEDED(hrCikis) ? shell : nullptr; } std::string guidToMetin(const GUID& g) { wchar_t genis[64]{}; StringFromGUID2(g, genis, 64); char dar[96]{}; WideCharToMultiByte(CP_UTF8, 0, genis, -1, dar, sizeof(dar), nullptr, nullptr); return dar; } bool metniGuidCevir(const std::string& metin, GUID& cikis) { std::wstring genis(metin.begin(), metin.end()); // GUID metni yalniz ASCII icerir return SUCCEEDED(CLSIDFromString(genis.c_str(), &cikis)); } } // namespace const char* masaustuDurumMetni(MasaustuDurum d) { switch (d) { case MasaustuDurum::Ok: return "Basarili"; case MasaustuDurum::DesteklenmiyorSurum: return "Bu Windows surumunde/derlemede desteklenmiyor"; case MasaustuDurum::HenuzUygulanmadi: return "Henuz uygulanmadi (Faz 2)"; case MasaustuDurum::ComHatasi: return "COM cagrisi basarisiz"; case MasaustuDurum::PencereGecersiz: return "Pencere artik gecerli degil"; } return "Bilinmiyor"; } ComOturum::ComOturum() { HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); if (hr == S_OK) { basarili_ = true; bizAcik_ = true; } else if (hr == S_FALSE || hr == RPC_E_CHANGED_MODE) { // S_FALSE: bu thread'de COM zaten baslatilmis (baska bir cagiran). // RPC_E_CHANGED_MODE: farkli threading modeliyle zaten baslatilmis — // yine de calisabiliriz (kendi CoUninitialize'imizi cagirmayiz). basarili_ = true; bizAcik_ = false; } else { basarili_ = false; } } ComOturum::~ComOturum() { if (g_vdm) { g_vdm->Release(); g_vdm = nullptr; g_denendi = false; // bir sonraki ComOturum yeniden kursun } if (bizAcik_) CoUninitialize(); } bool masaustuYoneticisiHazirMi() { if (g_denendi) return g_vdm != nullptr; g_denendi = true; HRESULT hr = CoCreateInstance(CLSID_VirtualDesktopManager, nullptr, CLSCTX_INPROC_SERVER, IID_IVirtualDesktopManager, reinterpret_cast(&g_vdm)); if (FAILED(hr)) { g_vdm = nullptr; return false; } return true; } MasaustuDurum pencereMasaustuGetir(void* hwnd, std::string& guidMetniCikis) { if (!masaustuYoneticisiHazirMi()) return MasaustuDurum::DesteklenmiyorSurum; HWND h = static_cast(hwnd); if (!h || !IsWindow(h)) return MasaustuDurum::PencereGecersiz; GUID g{}; HRESULT hr = g_vdm->GetWindowDesktopId(h, &g); if (FAILED(hr)) return MasaustuDurum::ComHatasi; wchar_t genis[64]{}; StringFromGUID2(g, genis, 64); char dar[96]{}; WideCharToMultiByte(CP_UTF8, 0, genis, -1, dar, sizeof(dar), nullptr, nullptr); guidMetniCikis = dar; return MasaustuDurum::Ok; } MasaustuDurum pencereSuankiMasaustundeMi(void* hwnd, bool& sonucCikis) { if (!masaustuYoneticisiHazirMi()) return MasaustuDurum::DesteklenmiyorSurum; HWND h = static_cast(hwnd); if (!h || !IsWindow(h)) return MasaustuDurum::PencereGecersiz; BOOL sonuc = FALSE; HRESULT hr = g_vdm->IsWindowOnCurrentVirtualDesktop(h, &sonuc); if (FAILED(hr)) return MasaustuDurum::ComHatasi; sonucCikis = (sonuc != FALSE); return MasaustuDurum::Ok; } // ---- Faz 2 — GERCEK implementasyon (bkz. dosya basi FAZ 2 aciklamasi) ---- // UCU: bu 3 fonksiyon her cagrida KENDI ImmersiveShell+QueryService COM // nesnesini acip kapatir (Faz 1'deki onbellekli IVirtualDesktopManager'in // aksine) — cunku undocumented arayuz nesnesi surec omru boyunca guvenle // onbelleklenecek kadar test edilmedi; her cagri BAGIMSIZ ve DURUST basarisiz // olabilir. MasaustuDurum masaustuOlustur(std::string& yeniGuidCikis) { const VdmSekli sekil = vdmSekliBelirle(); if (sekil == VdmSekli::Yok) return MasaustuDurum::DesteklenmiyorSurum; HRESULT hr = S_OK; IServiceProvider* shell = immersiveShellAl(hr); if (!shell) return MasaustuDurum::ComHatasi; IUnknown* ham = vdmInternalAl(shell, sekil, hr); if (!ham) { shell->Release(); return MasaustuDurum::ComHatasi; } FikoIVirtualDesktop* yeni = nullptr; if (sekil == VdmSekli::A) hr = static_cast(ham)->CreateDesktop(¥i); else if (sekil == VdmSekli::B) hr = static_cast(ham)->CreateDesktop(¥i); else hr = static_cast(ham)->CreateDesktop(¥i); ham->Release(); shell->Release(); if (FAILED(hr) || !yeni) return MasaustuDurum::ComHatasi; GUID g{}; hr = yeni->GetId(&g); yeni->Release(); if (FAILED(hr)) return MasaustuDurum::ComHatasi; yeniGuidCikis = guidToMetin(g); return MasaustuDurum::Ok; } MasaustuDurum masaustunaGec(const std::string& guidMetni) { const VdmSekli sekil = vdmSekliBelirle(); if (sekil == VdmSekli::Yok) return MasaustuDurum::DesteklenmiyorSurum; GUID hedefId{}; if (!metniGuidCevir(guidMetni, hedefId)) return MasaustuDurum::ComHatasi; HRESULT hr = S_OK; IServiceProvider* shell = immersiveShellAl(hr); if (!shell) return MasaustuDurum::ComHatasi; IUnknown* ham = vdmInternalAl(shell, sekil, hr); if (!ham) { shell->Release(); return MasaustuDurum::ComHatasi; } FikoIVirtualDesktop* hedef = nullptr; MasaustuDurum sonuc; if (sekil == VdmSekli::A) { auto* m = static_cast(ham); hr = m->FindDesktop(&hedefId, &hedef); if (SUCCEEDED(hr) && hedef) hr = m->SwitchDesktop(hedef); } else if (sekil == VdmSekli::B) { auto* m = static_cast(ham); hr = m->FindDesktop(&hedefId, &hedef); if (SUCCEEDED(hr) && hedef) hr = m->SwitchDesktop(hedef); } else { auto* m = static_cast(ham); hr = m->FindDesktop(&hedefId, &hedef); if (SUCCEEDED(hr) && hedef) hr = m->SwitchDesktop(hedef); } sonuc = SUCCEEDED(hr) ? MasaustuDurum::Ok : MasaustuDurum::ComHatasi; if (hedef) hedef->Release(); ham->Release(); shell->Release(); return sonuc; } MasaustuDurum pencereyiMasaustunaTasi(void* hwnd, const std::string& guidMetni) { const VdmSekli sekil = vdmSekliBelirle(); if (sekil == VdmSekli::Yok) return MasaustuDurum::DesteklenmiyorSurum; HWND h = static_cast(hwnd); if (!h || !IsWindow(h)) return MasaustuDurum::PencereGecersiz; GUID hedefId{}; if (!metniGuidCevir(guidMetni, hedefId)) return MasaustuDurum::ComHatasi; HRESULT hr = S_OK; IServiceProvider* shell = immersiveShellAl(hr); if (!shell) return MasaustuDurum::ComHatasi; // 1) IApplicationViewCollection uzerinden bu HWND'nin gorunumunu al. IUnknown* gorunumHam = nullptr; hr = shell->QueryService(kIidAppViewCollectionModern, kIidAppViewCollectionModern, reinterpret_cast(&gorunumHam)); if (FAILED(hr) || !gorunumHam) { shell->Release(); return MasaustuDurum::ComHatasi; } auto* koleksiyon = static_cast(gorunumHam); IUnknown* gorunum = nullptr; hr = koleksiyon->GetViewForHwnd(h, &gorunum); koleksiyon->Release(); if (FAILED(hr) || !gorunum) { shell->Release(); return MasaustuDurum::ComHatasi; } // 2) Manager internal uzerinden hedef masaustunu bul + tasi. IUnknown* ham = vdmInternalAl(shell, sekil, hr); if (!ham) { gorunum->Release(); shell->Release(); return MasaustuDurum::ComHatasi; } FikoIVirtualDesktop* hedef = nullptr; MasaustuDurum sonuc; if (sekil == VdmSekli::A) { auto* m = static_cast(ham); hr = m->FindDesktop(&hedefId, &hedef); if (SUCCEEDED(hr) && hedef) hr = m->MoveViewToDesktop(gorunum, hedef); } else if (sekil == VdmSekli::B) { auto* m = static_cast(ham); hr = m->FindDesktop(&hedefId, &hedef); if (SUCCEEDED(hr) && hedef) hr = m->MoveViewToDesktop(gorunum, hedef); } else { auto* m = static_cast(ham); hr = m->FindDesktop(&hedefId, &hedef); if (SUCCEEDED(hr) && hedef) hr = m->MoveViewToDesktop(gorunum, hedef); } sonuc = SUCCEEDED(hr) ? MasaustuDurum::Ok : MasaustuDurum::ComHatasi; if (hedef) hedef->Release(); ham->Release(); gorunum->Release(); shell->Release(); return sonuc; } MasaustuDurum masaustuSiradanGuidBul(int sira, std::string& guidCikis) { if (sira < 0) return MasaustuDurum::ComHatasi; const VdmSekli sekil = vdmSekliBelirle(); if (sekil == VdmSekli::Yok) return MasaustuDurum::DesteklenmiyorSurum; HRESULT hr = S_OK; IServiceProvider* shell = immersiveShellAl(hr); if (!shell) return MasaustuDurum::ComHatasi; IUnknown* ham = vdmInternalAl(shell, sekil, hr); if (!ham) { shell->Release(); return MasaustuDurum::ComHatasi; } // GetDesktops() — IObjectArray dondurur; bu arayuz (SDK'nin kendi // / tanimi — Jump List kategorileri icin de // kullanilir) DOKUMANTE'dir, undocumented olan yalniz ICINDEKI eleman // tipi (IVirtualDesktop). Bu yuzden burada Fiko-ozel struct GEREKMEZ. void* diziHamVoid = nullptr; if (sekil == VdmSekli::A) hr = static_cast(ham)->GetDesktops(&diziHamVoid); else if (sekil == VdmSekli::B) hr = static_cast(ham)->GetDesktops(&diziHamVoid); else hr = static_cast(ham)->GetDesktops(&diziHamVoid); ham->Release(); if (FAILED(hr) || !diziHamVoid) { shell->Release(); return MasaustuDurum::ComHatasi; } auto* dizi = static_cast(diziHamVoid); UINT sayi = 0; hr = dizi->GetCount(&sayi); if (FAILED(hr) || static_cast(sira) >= sayi) { dizi->Release(); shell->Release(); return MasaustuDurum::ComHatasi; // sira artik gecerli araligin disinda } IUnknown* elemanHam = nullptr; hr = dizi->GetAt(static_cast(sira), IID_IUnknown, reinterpret_cast(&elemanHam)); dizi->Release(); shell->Release(); if (FAILED(hr) || !elemanHam) return MasaustuDurum::ComHatasi; auto* masaustu = static_cast(elemanHam); GUID g{}; hr = masaustu->GetId(&g); masaustu->Release(); if (FAILED(hr)) return MasaustuDurum::ComHatasi; guidCikis = guidToMetin(g); return MasaustuDurum::Ok; } } // namespace fiko