170 lines
12 KiB
C++
170 lines
12 KiB
C++
// Run from an isolated fixture folder containing KSwitcher.exe and Screenshot.exe,
|
|
// with the normal resident stopped. Never points at the installed user's data.
|
|
#include "storage.h"
|
|
#include "ocr.h"
|
|
#include "json.hpp"
|
|
#include <commctrl.h>
|
|
#include <psapi.h>
|
|
#include <iostream>
|
|
static HWND editor{};
|
|
static bool pastedImage{};
|
|
static LRESULT CALLBACK targetProc(HWND w,UINT message,WPARAM wp,LPARAM lp) {
|
|
if(message==WM_SETFOCUS && editor) {SetFocus(editor);return 0;}
|
|
return DefWindowProcW(w,message,wp,lp);
|
|
}
|
|
static void require(bool ok,const char *what) {if(!ok)throw std::runtime_error(what);std::cout<<"PASS "<<what<<'\n';}
|
|
static void pump() {MSG m;while(PeekMessageW(&m,nullptr,0,0,PM_REMOVE)){TranslateMessage(&m);DispatchMessageW(&m);}}
|
|
template<class F> static void until(F condition,const char *what,DWORD timeout=10000) {
|
|
auto end=GetTickCount64()+timeout;
|
|
while(!condition()){if(GetTickCount64()>=end)throw std::runtime_error(what);pump();Sleep(10);}
|
|
}
|
|
static LRESULT send(HWND w,UINT message,WPARAM wp=0,LPARAM lp=0) {
|
|
DWORD_PTR result{};
|
|
if(!SendMessageTimeoutW(w,message,wp,lp,SMTO_ABORTIFHUNG,5000,&result))throw std::runtime_error("window request timed out");
|
|
return LRESULT(result);
|
|
}
|
|
static void command(HWND resident,int cmd) {
|
|
COPYDATASTRUCT data{1,sizeof(cmd),&cmd};send(resident,WM_COPYDATA,0,LPARAM(&data));
|
|
}
|
|
static size_t memory(HANDLE process) {
|
|
ks::Bytes bytes(1024*1024);auto ws=reinterpret_cast<PSAPI_WORKING_SET_INFORMATION*>(bytes.data());
|
|
if(!QueryWorkingSet(process,ws,DWORD(bytes.size())))throw std::runtime_error("memory query failed");
|
|
size_t pages=0;for(size_t i=0;i<ws->NumberOfEntries;++i)if(!ws->WorkingSetInfo[i].Shared)++pages;
|
|
return pages*4096;
|
|
}
|
|
static std::wstring editorText() {wchar_t text[256]{};GetWindowTextW(editor,text,256);return text;}
|
|
static LRESULT CALLBACK editProc(HWND w,UINT message,WPARAM wp,LPARAM lp,UINT_PTR,DWORD_PTR) {
|
|
if(message==WM_PASTE && IsClipboardFormatAvailable(CF_DIB)) {pastedImage=true;return 0;}
|
|
return DefSubclassProc(w,message,wp,lp);
|
|
}
|
|
static ks::Clip excluded(ks::Clip clip) {
|
|
clip.formats.push_back({0,L"ExcludeClipboardContentFromMonitorProcessing",ks::Bytes{0}});return clip;
|
|
}
|
|
int main() {
|
|
CoInitializeEx(nullptr,COINIT_APARTMENTTHREADED);
|
|
PROCESS_INFORMATION process{};HWND resident{},target{};ks::Clip saved;bool captured=false;int result=0;
|
|
try {
|
|
auto folder=std::filesystem::path(ks::executable()).parent_path();
|
|
require(folder.filename().wstring().starts_with(L"history-host-fixture-"),"isolated fixture directory");
|
|
require(!FindWindowW(L"KSwitcher.Main",nullptr),"normal resident is stopped");
|
|
WNDCLASSW targetClass{};targetClass.lpfnWndProc=targetProc;targetClass.hInstance=GetModuleHandleW(nullptr);
|
|
targetClass.lpszClassName=L"KSwitcher.HistoryTestDestination";RegisterClassW(&targetClass);
|
|
target=CreateWindowExW(0,targetClass.lpszClassName,L"KSwitcher history test destination",WS_OVERLAPPEDWINDOW|WS_VISIBLE,
|
|
50,50,500,250,nullptr,nullptr,nullptr,nullptr);
|
|
editor=CreateWindowExW(0,L"EDIT",L"",WS_CHILD|WS_VISIBLE|ES_MULTILINE,10,10,450,180,target,nullptr,nullptr,nullptr);
|
|
SetWindowSubclass(editor,editProc,1,0);
|
|
saved=ks::readClipboard(target,128*1024*1024);captured=true;
|
|
ks::writeClipboard(target,excluded(ks::textClip(L"fixture clipboard")));
|
|
auto settings=ks::loadSettings(folder/L"missing.json",ks::readLayouts());
|
|
settings.autoCorrect=false;settings.writeLog=true;settings.history=true;settings.maxItems=500;settings.maxMiB=100;
|
|
std::filesystem::create_directories(folder/L"data");
|
|
ks::saveSettings(folder/L"data/settings.json",settings);
|
|
ks::History history(folder/L"data/history");
|
|
history.clear(); // This executable only runs in its dedicated fixture directory.
|
|
ks::Pixels pixels{1566,1068,ks::Bytes(1566*1068*4,255)};
|
|
auto image=ks::Clip{{ks::imageDib(pixels)}};
|
|
history.add(image,L"test",settings);
|
|
auto imageId=history.entries().front().id;
|
|
// Seed once rather than rewriting the encrypted index 500 times. This
|
|
// also avoids measuring fixture creation in the resident under test.
|
|
auto indexPath=folder/L"data/history/index.dat";
|
|
auto rows=nlohmann::json::parse(ks::unprotect(ks::readFile(indexPath,1024*1024)));
|
|
for(int i=0;i<499;++i) {
|
|
auto id=ks::lower(ks::newSnippetId().substr(1,36)),text=L"History fixture "+std::to_wstring(i);
|
|
auto bytes=ks::protect(ks::encodeClip(ks::textClip(text)));
|
|
ks::atomicWrite(folder/L"data/history"/(id+L".dat"),bytes);
|
|
rows.insert(rows.begin(),nlohmann::json{{"id",ks::utf8(id)},{"preview",ks::utf8(text)},
|
|
{"app","test"},{"bytes",bytes.size()},{"at",i+1},{"pinned",false},{"image",false},{"files",false}});
|
|
}
|
|
auto encoded=rows.dump();ks::atomicWrite(indexPath,ks::protect(ks::Bytes(encoded.begin(),encoded.end())));
|
|
history.reload();
|
|
auto textId=history.entries().front().id;
|
|
auto exe=folder/L"KSwitcher.exe";auto line=L"\""+exe.wstring()+L"\"";
|
|
STARTUPINFOW startup{sizeof(startup)};startup.dwFlags=STARTF_USESHOWWINDOW;startup.wShowWindow=SW_HIDE;
|
|
require(CreateProcessW(exe.c_str(),line.data(),nullptr,nullptr,FALSE,CREATE_NO_WINDOW,nullptr,folder.c_str(),&startup,&process),"start isolated resident");
|
|
CloseHandle(process.hThread);process.hThread=nullptr;
|
|
until([&]{resident=FindWindowW(L"KSwitcher.Main",nullptr);return resident!=nullptr;},"resident startup");
|
|
auto base=memory(process.hProcess);
|
|
std::string forged="{\"action\":\"clear\",\"id\":\"\",\"plain\":false}";
|
|
COPYDATASTRUCT forgedRequest{0x4b530200,DWORD(forged.size()),forged.data()};
|
|
require(send(resident,WM_COPYDATA,WPARAM(target),LPARAM(&forgedRequest))==0,
|
|
"history rejects requests from an untrusted process");
|
|
auto open=[&] {
|
|
require(ks::activateWindow(target),"destination foreground");SetFocus(editor);command(resident,5);
|
|
HWND window{};
|
|
until([&]{window=FindWindowW(L"KSwitcher.History",nullptr);return window&&IsWindowVisible(window);},"history startup");
|
|
until([&]{return send(GetDlgItem(window,102),LB_GETCOUNT)>0;},"history rows");
|
|
DWORD pid{};GetWindowThreadProcessId(window,&pid);
|
|
require(pid!=process.dwProcessId,"history belongs to a separate process");
|
|
return window;
|
|
};
|
|
auto close=[&](HWND window) {
|
|
DWORD pid{};GetWindowThreadProcessId(window,&pid);HANDLE child=OpenProcess(SYNCHRONIZE,FALSE,pid);
|
|
PostMessageW(window,WM_KEYDOWN,VK_ESCAPE,0); // Escape also works with focus on the window itself.
|
|
until([&]{return WaitForSingleObject(child,0)==WAIT_OBJECT_0;},"history helper exits after Escape");
|
|
CloseHandle(child);
|
|
require(!FindWindowW(L"KSwitcher.History",nullptr),"Escape destroys history window");
|
|
};
|
|
for(int i=0;i<3;++i){auto w=open();until([&]{return send(GetDlgItem(w,102),LB_GETCOUNT)==500;},"all history rows loaded");require(true,"all history entries visible");close(w);}
|
|
auto idle=memory(process.hProcess);
|
|
std::cout<<"MEMORY resident_before="<<base<<" after_three_closes="<<idle<<'\n';
|
|
require(idle<base+2*1024*1024,"repeated history does not retain UI heap in resident");
|
|
auto w=open();send(w,WM_COMMAND,112);
|
|
until([&]{history.reload();return history.entries().front().pinned;},"resident pin mutation");
|
|
require(history.entries().front().id==textId,"pin preserves history identity");
|
|
send(GetDlgItem(w,101),WM_SETTEXT,0,LPARAM(L"History fixture 498"));
|
|
until([&]{return send(GetDlgItem(w,102),LB_GETCOUNT)==1;},"history search");
|
|
close(w);w=open();
|
|
wchar_t query[100]{};send(GetDlgItem(w,101),WM_GETTEXT,100,LPARAM(query));
|
|
require(std::wstring(query)==L"History fixture 498","search survives helper restart");
|
|
send(w,WM_COMMAND,114);
|
|
HWND entry{};until([&]{entry=FindWindowW(L"KSwitcher.Entry",nullptr);return entry!=nullptr;},"full item opens");
|
|
DWORD childPid{};GetWindowThreadProcessId(w,&childPid);HANDLE child=OpenProcess(SYNCHRONIZE,FALSE,childPid);
|
|
send(GetDlgItem(w,101),WM_KEYDOWN,VK_ESCAPE);
|
|
require(WaitForSingleObject(child,0)==WAIT_TIMEOUT && IsWindow(entry),"full item remains usable after closing history");
|
|
send(entry,WM_CLOSE);
|
|
until([&]{return WaitForSingleObject(child,0)==WAIT_OBJECT_0;},"helper exits after last full item closes");CloseHandle(child);
|
|
w=open();send(GetDlgItem(w,101),WM_SETTEXT,0,LPARAM(L""));
|
|
until([&]{return send(GetDlgItem(w,102),LB_GETCOUNT)==500;},"clear search");close(w);
|
|
ks::writeClipboard(target,excluded(image));
|
|
w=open();send(w,WM_COMMAND,111);
|
|
until([&]{return editorText()==L"History fixture 498";},"paste plain text into native editor");
|
|
until([&]{return IsClipboardFormatAvailable(CF_DIB)&&!FindWindowW(L"KSwitcher.HistoryHost",nullptr);},"image clipboard restored after paste");
|
|
require(ks::decodeImage(ks::readClipboard(target,64*1024*1024)).width==pixels.width,"image clipboard survives history paste");
|
|
auto afterPaste=memory(process.hProcess);
|
|
std::cout<<"MEMORY after_paste_with_image_backup="<<afterPaste<<'\n';
|
|
require(afterPaste<base+2*1024*1024,"image clipboard backup stays outside resident");
|
|
ks::writeClipboard(target,excluded(ks::textClip(L"keep clipboard")));
|
|
w=open();send(GetDlgItem(w,101),WM_SETTEXT,0,LPARAM(L"Image"));
|
|
until([&]{return send(GetDlgItem(w,102),LB_GETCOUNT)==1;},"image search");
|
|
pastedImage=false;send(w,WM_COMMAND,110);
|
|
until([&]{return pastedImage;},"image paste reaches destination");
|
|
until([&]{return ks::readClipboard(target).text()==L"keep clipboard";},"clipboard restored after image paste");
|
|
std::cout<<"MEMORY after_image_paste="<<memory(process.hProcess)<<'\n';
|
|
require(memory(process.hProcess)<base+2*1024*1024,"image paste does not retain full bitmap in resident");
|
|
until([&]{return !FindWindowW(L"KSwitcher.HistoryHost",nullptr);},"image paste helper exits");
|
|
w=open();send(w,WM_COMMAND,113);
|
|
until([&]{history.reload();return history.entries().size()==499;},"resident removes selected image");
|
|
GetWindowThreadProcessId(w,&childPid);child=OpenProcess(SYNCHRONIZE,FALSE,childPid);
|
|
// Startup update checks temporarily reject exit; retry after that
|
|
// operation finishes instead of treating its guard as a host leak.
|
|
ULONGLONG nextExit{};
|
|
until([&]{
|
|
if(WaitForSingleObject(process.hProcess,0)==WAIT_OBJECT_0) return true;
|
|
if(GetTickCount64()>=nextExit) {
|
|
command(resident,99);nextExit=GetTickCount64()+500;
|
|
}
|
|
return false;
|
|
},"resident accepts exit after startup update check",30000);
|
|
until([&]{return WaitForSingleObject(child,0)==WAIT_OBJECT_0;},"resident shutdown closes history helper");
|
|
CloseHandle(child);resident=nullptr;
|
|
std::cout<<"PASS isolated history host suite\n";
|
|
} catch(const std::exception &e){std::cerr<<"FAIL "<<e.what()<<" editor="<<ks::utf8(editorText())<<" foreground="<<(GetForegroundWindow()==target)<<'\n';result=1;}
|
|
if(resident)try{command(resident,99);}catch(...){}
|
|
if(process.hProcess){if(WaitForSingleObject(process.hProcess,10000)!=WAIT_OBJECT_0){TerminateProcess(process.hProcess,1);WaitForSingleObject(process.hProcess,INFINITE);}CloseHandle(process.hProcess);}
|
|
if(captured)try{ks::writeClipboard(target,saved);}catch(...){result=1;}
|
|
if(target) DestroyWindow(target);
|
|
CoUninitialize();
|
|
return result;
|
|
}
|