Release v0.6.57: multi-PC input, shared clipboard and resilient Settings Save

This commit is contained in:
agrynco committed 2026-11-06 21:33:37 +02:00
1 parent 82292c8a7e
commit 5de010ee44
39 files changed
+1707 -312

No files matched your search

+32 -9
View File
@@ -293,8 +293,9 @@ int main()
require(!IsWindowEnabled(GetDlgItem(lan,707)),"cannot forget nonexistent pair");
require(!IsWindowEnabled(GetDlgItem(lan,715)),"input sharing requires a paired computer");
require(!IsWindowEnabled(GetDlgItem(lan,716)),"stop input disabled when sharing is off");
for(int id:{711,712,713,714}) require(!IsWindowEnabled(GetDlgItem(lan,id)),"screen positions unavailable until peer selected");
require(IsDlgButtonChecked(lan,712)==BST_CHECKED,"default screen position is Right");
require(GetDlgItem(lan,721)!=nullptr,"monitor arrangement grid is present");
require(!(GetWindowLongPtrW(lan,GWL_STYLE)&(WS_THICKFRAME|WS_MAXIMIZEBOX)),"LAN window has a fixed size");
require(GetDlgItem(lan,711)==nullptr,"obsolete side radios are removed");
require(SendDlgItemMessageW(lan,702,LB_GETCOUNT,0,0)==0,"disabled discovery lists no generic network devices");
require(!std::filesystem::exists(root/L"lan-pairing.bin"),"opening LAN settings does not enable or save discovery");
RedrawWindow(lan,nullptr,nullptr,RDW_INVALIDATE|RDW_UPDATENOW|RDW_ALLCHILDREN);
@@ -306,19 +307,23 @@ int main()
SendMessageW(parent,WM_COMMAND,IDCANCEL,0);
}
{
ks::lan::Json fixture={{"version",1},{"id",std::string(32,'a')},{"enabled",false},
ks::lan::Json fixture={{"version",1},{"id",std::string(32,'a')},{"enabled",false},{"inputPeer",std::string(32,'b')},{"inputEdge",1},
{"trusted",{{std::string(32,'b'),{{"name","Test paired KSwitcher"},{"key",std::string(64,'f')}}}}}};
auto text=fixture.dump();ks::atomicWrite(root/L"lan-pairing.bin",ks::protect(ks::Bytes(text.begin(),text.end())));
ks::App pairedApp(GetModuleHandleW(nullptr),true,root);ks::showSettings(pairedApp);
SendMessageW(pairedApp.settingsWindow,WM_COMMAND,262,0);
HWND lan=FindWindowW(L"KSwitcher.LAN",nullptr);
SendDlgItemMessageW(lan,702,LB_SETCURSEL,0,0);
SendMessageW(lan,WM_COMMAND,MAKEWPARAM(702,LBN_SELCHANGE),0);
for(int id:{711,712,713,714})require(IsWindowEnabled(GetDlgItem(lan,id)),"paired computer exposes all four layout positions");
SendDlgItemMessageW(lan,713,BM_CLICK,0,0);
require(IsDlgButtonChecked(lan,713)==BST_CHECKED&&IsDlgButtonChecked(lan,712)==BST_UNCHECKED,"Above selection changes radio group");
require(SendDlgItemMessageW(lan,702,LB_GETCURSEL,0,0)==0,"single saved pair selected automatically when opening LAN settings");
require(!IsWindowEnabled(GetDlgItem(lan,705))&&!IsWindowEnabled(GetDlgItem(lan,703))&&!IsWindowEnabled(GetDlgItem(lan,704)),"saved pair disables pairing and code controls");
SendDlgItemMessageW(lan,702,LB_SETCURSEL,-1,0);
SendMessageW(lan,WM_TIMER,1,0);
require(IsDlgButtonChecked(lan,713)==BST_CHECKED,"status refresh preserves unsaved position choice");
require(SendDlgItemMessageW(lan,702,LB_GETCURSEL,0,0)==0,"refresh restores single paired selection without a peer list change");
auto grid=GetDlgItem(lan,721);
SendMessageW(grid,WM_KEYDOWN,VK_UP,0);SendMessageW(grid,WM_KEYDOWN,VK_LEFT,0);
auto saved=ks::lan::Json::parse(ks::unprotect(ks::readFile(root/L"lan-pairing.bin",256*1024)));
require(saved["positions"][std::string(32,'b')]==ks::lan::Json::array({0,-1}),"keyboard moves monitor above this PC and saves immediately");
SendMessageW(lan,WM_TIMER,1,0);
require(saved["inputEdge"]==2,"monitor arrangement updates the existing input direction");
require(!IsWindowEnabled(GetDlgItem(lan,715)),"input cannot start while discovery is disabled");
// Let the native radio-button theme animation finish before visual capture.
auto repaintUntil=GetTickCount64()+200;
@@ -334,6 +339,24 @@ int main()
SendMessageW(lan,WM_COMMAND,IDCANCEL,0);
SendMessageW(pairedApp.settingsWindow,WM_COMMAND,IDCANCEL,0);
}
{
using J=ks::lan::Json;const std::string b(32,'b'),c(32,'c');
J fixture={{"version",1},{"id",std::string(32,'a')},{"enabled",false},{"inputPeer",b},{"inputEdge",0},
{"trusted",{{b,{{"name","Left desktop"},{"key",std::string(64,'f')}}},{c,{{"name","Right laptop"},{"key",std::string(64,'e')}}}}},
{"positions",{{b,J::array({-1,0})},{c,J::array({1,0})}}}};
auto text=fixture.dump();ks::atomicWrite(root/L"lan-pairing.bin",ks::protect(ks::Bytes(text.begin(),text.end())));
ks::App layoutApp(GetModuleHandleW(nullptr),true,root);ks::showSettings(layoutApp);
SendMessageW(layoutApp.settingsWindow,WM_COMMAND,262,0);auto lan=FindWindowW(L"KSwitcher.LAN",nullptr);auto grid=GetDlgItem(lan,721);
RECT r{};GetClientRect(grid,&r);
auto point=[&](int column,int row){return MAKELPARAM((2*column+1)*r.right/10,(2*row+1)*r.bottom/6);};
SendMessageW(grid,WM_LBUTTONDOWN,MK_LBUTTON,point(1,1));SendMessageW(grid,WM_MOUSEMOVE,MK_LBUTTON,point(1,0));SendMessageW(grid,WM_LBUTTONUP,0,point(1,0));
auto saved=J::parse(ks::unprotect(ks::readFile(root/L"lan-pairing.bin",256*1024)));
require(saved["positions"][b]==J::array({-1,-1})&&saved["positions"][c]==J::array({1,0}),"drag moves one monitor without moving another paired PC");
RedrawWindow(lan,nullptr,nullptr,RDW_INVALIDATE|RDW_UPDATENOW|RDW_ALLCHILDREN);
GetClientRect(lan,&r);auto dc=GetDC(lan);auto screenshot=ks::screenshotClip(dc,r.right,r.bottom,{});ReleaseDC(lan,dc);
ks::atomicWrite(std::filesystem::path(ks::executable()).parent_path()/L"lan-three-computers.png",ks::encodePng(ks::decodeImage(screenshot)));
SendMessageW(lan,WM_COMMAND,IDCANCEL,0);SendMessageW(layoutApp.settingsWindow,WM_COMMAND,IDCANCEL,0);
}
std::filesystem::remove_all(root);
std::cout << "Passed " << passed << " chat profile checks\n";
CoUninitialize();
+13
View File
@@ -676,6 +676,19 @@ int main(int argc, char **argv) {
};
auto commands=[&] { MSG m{}; int n=0; while(PeekMessageW(&m,app.window,WM_HOTKEY,WM_HOTKEY,PM_REMOVE)) ++n; return n; };
auto f12=event(VK_F12);
{
auto localF12=f12;localF12.flags=0;localF12.dwExtraInfo=0;
app.handleKeyboard(localF12,true,MOD_CONTROL);commands();
app.resetLanInput();
require(app.handleKeyboard(localF12,true,MOD_CONTROL)&&commands()==1,
"local shortcut works after LAN handoff even when release happened on another PC");
app.handleKeyboard(localF12,false,0);
auto letter=event('A',30);letter.flags=0;letter.dwExtraInfo=0;
app.resetLanInput();app.handleKeyboard(letter,true,0);
require(!app.run.keys.empty(),"local typing tracking resumes after LAN ownership reset");
app.run.reset();
}
{
const auto before = app.inputVersion.load();
const auto recorded = app.run.keys;
+48
View File
@@ -114,6 +114,54 @@ int main(int argc,char **argv)
check(request(restored,{{"action","status"}})["peers"].empty(),"forget removes offline pair");
check(!request(restored,{{"action","status"}}).at("shareInput").get<bool>(),"forgetting the input peer revokes persisted input permission");
}
if(argc>1&&std::string(argv[1])=="--network")
{
auto saved=Json::parse(unprotect(readFile(root/L"lan-pairing.bin",256*1024)));
saved["trusted"][b]={{"name","Restart peer"},{"key",left.derived}};
saved["enabled"]=true;saved["shareInput"]=true;saved["inputPeer"]=b;saved["inputEdge"]=3;
auto text=saved.dump();atomicWrite(root/L"lan-pairing.bin",protect(Bytes(text.begin(),text.end())));
for(int restart=0;restart<2;++restart)
{
LanService restored(root);Sleep(100);
auto state=request(restored,{{"action","status"}});
check(restored.inputEnabled()&&state.at("enabled")==true&&state.at("inputPeer")==b&&state.at("inputEdge")==3,
"restart restores discovery, pairing, input permission and screen edge without opening settings");
}
}
{
auto folder=root/L"layout";std::filesystem::create_directories(folder);
auto saved=Json::parse(unprotect(readFile(root/L"lan-pairing.bin",256*1024)));
saved["enabled"]=false;saved["shareInput"]=false;saved["inputPeer"]=b;saved["inputEdge"]=1;saved.erase("positions");
saved["trusted"]={{b,{{"name","Right PC"},{"key",left.derived}}},{c,{{"name","Third PC"},{"key",left.derived}}}};
auto text=saved.dump();atomicWrite(folder/L"lan-pairing.bin",protect(Bytes(text.begin(),text.end())));
{
LanService layout(folder);auto state=request(layout,{{"action","status"}});
check(state["positions"][b]==Json::array({1,0}),"legacy side migrates to monitor grid");
check(state["positions"].size()==2&&state["positions"][c]!=state["positions"][b],"three-PC layout has unique cells");
request(layout,{{"action","position"},{"peer",c},{"x",-1},{"y",0}});
request(layout,{{"action","position"},{"peer",b},{"x",0},{"y",-1}});
rejects([&]{request(layout,{{"action","position"},{"peer",c},{"x",0},{"y",-1}});},"occupied cell rejected");
rejects([&]{request(layout,{{"action","position"},{"peer",c},{"x",0},{"y",0}});},"local monitor cell protected");
rejects([&]{request(layout,{{"action","position"},{"peer",c},{"x",9},{"y",0}});},"out-of-grid position rejected");
check(request(layout,{{"action","status"}})["positions"][c]==Json::array({-1,0}),"failed drops preserve previous position");
}
{
LanService layout(folder);auto state=request(layout,{{"action","status"}});
check(state["positions"][b]==Json::array({0,-1})&&state["positions"][c]==Json::array({-1,0}),"all monitor positions survive restart");
check(state["inputEdge"]==2,"active peer direction follows monitor position");
if(argc>1&&std::string(argv[1])=="--network"){
request(layout,{{"action","enable"},{"enabled",true}});
request(layout,{{"action","input"},{"enabled",true}});
request(layout,{{"action","position"},{"peer",c},{"x",0},{"y",-2}});
auto multi=request(layout,{{"action","status"}});
check(multi["shareInput"]==true&&multi["positions"][c]==Json::array({0,-2}),"multi-PC sharing remains enabled for a chained nonadjacent destination");
request(layout,{{"action","forget"},{"peer",b}});
check(request(layout,{{"action","status"}})["shareInput"]==true,"forgetting legacy selected pair keeps remaining pairs enabled");
}
request(layout,{{"action","forget"},{"peer",c}});
check(!request(layout,{{"action","status"}})["positions"].contains(c),"forgetting pair removes its monitor");
}
}
std::cout<<"Passed "<<passed<<" LAN discovery/pairing checks\n";
}
catch(const std::exception &e){std::cerr<<"FAIL: "<<e.what()<<'\n';return 1;}
+76
View File
@@ -0,0 +1,76 @@
#include <winsock2.h>
#include "lan_clipboard.h"
#include <iostream>
using namespace ks;using namespace ks::lan;
int checks{};
void check(bool b,const char *s){if(!b)throw std::runtime_error(s);++checks;}
int main(){try{
const std::string aid(32,'a'),bid(32,'b'),cid(32,'c'),key(64,'d');
ClipboardExchange a(aid),b(bid),c(cid);
a.peerSession(bid,b.session());a.peerSession(cid,c.session());b.peerSession(aid,a.session());c.peerSession(aid,a.session());
Json trusted{{bid,{{"key",key}}},{cid,{{"key",key}}}},back{{aid,{{"key",key}}}};
Bytes payload(13000);for(size_t i=0;i<payload.size();++i)payload[i]=static_cast<unsigned char>(i);
a.queue({bid,cid},payload,"editor.exe",1);
std::deque<std::pair<std::string,Json>> packets,acks;Json firstPacket;int sent=0;
auto output=[&](const std::string &to,const Json &p){check(p.dump().size()<=2048,"datagram fits discovery bound");if(firstPacket.is_null())firstPacket=p;packets.push_back({to,p});};
auto ack=[&](const std::string &,const Json &p){acks.push_back({p.at("id"),p});};
for(uint64_t now=10;now<10000;now+=20){
a.poll(trusted,now,output);
while(!packets.empty()){
auto [to,p]=std::move(packets.front());packets.pop_front();if(++sent%7==0)continue;
auto &receiver=to==bid?b:c;
receiver.receive(aid,key,p,now,123,ack);
receiver.receive(aid,key,p,now,123,ack); // duplicate datagrams do not duplicate history
}
while(!acks.empty()){
auto [from,p]=std::move(acks.front());acks.pop_front();if(++sent%11==0)continue;
a.receive(from,key,p,now,123,output);
}
}
check(b.deliveries.size()==1&&c.deliveries.size()==1,"all paired instances receive exactly once despite loss/duplicates");
check(b.deliveries[0].bytes==payload&&c.deliveries[0].bytes==payload,"binary clipboard survives retransmission");
check(b.deliveries[0].sequence==123&&b.deliveries[0].app=="editor.exe","capture sequence and origin retained");
b.receive(aid,key,firstPacket,10000,123,ack);
check(acks.empty(),"authenticated old packet replay is ignored");
ClipboardExchange restarted(bid);restarted.peerSession(aid,a.session());
restarted.receive(aid,key,firstPacket,10001,123,ack);
check(restarted.deliveries.empty()&&acks.empty(),"old datagrams rejected after recipient restart");
auto damaged=firstPacket;damaged["number"]=1000000;auto data=damaged["data"].get<std::string>();data[0]=data[0]=='0'?'1':'0';damaged["data"]=data;
bool rejected=false;try{b.receive(aid,key,damaged,10002,123,ack);}catch(...){rejected=true;}
check(rejected,"modified ciphertext rejected");
a.queue({bid},Bytes{1,2,3},"second",10003);
a.peerSession(bid,restarted.session());
for(uint64_t now=10004;now<11000;now+=20){
a.poll(trusted,now,output);
while(!packets.empty()){auto p=std::move(packets.front().second);packets.pop_front();restarted.receive(aid,key,p,now,456,ack);}
while(!acks.empty()){auto p=std::move(acks.front().second);acks.pop_front();a.receive(bid,key,p,now,456,output);}
}
check(restarted.deliveries.size()==1&&restarted.deliveries[0].bytes==Bytes({1,2,3}),"clipboard works after peer restart");
bool limited=false;try{a.queue({bid},Bytes(32*1024*1024+1),"large",12000);}catch(...){limited=true;}
check(limited,"oversized clipboard bounded");
{
WSADATA startup{};check(WSAStartup(MAKEWORD(2,2),&startup)==0,"UDP benchmark starts");
SOCKET sa=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP),sb=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
struct Cleanup {SOCKET a,b;~Cleanup(){closesocket(a);closesocket(b);WSACleanup();}} cleanup{sa,sb};
sockaddr_in aa{},ab{};aa.sin_family=ab.sin_family=AF_INET;aa.sin_addr.s_addr=ab.sin_addr.s_addr=htonl(INADDR_LOOPBACK);
check(bind(sa,reinterpret_cast<sockaddr*>(&aa),sizeof(aa))==0&&bind(sb,reinterpret_cast<sockaddr*>(&ab),sizeof(ab))==0,"UDP benchmark binds");
int length=sizeof(aa);getsockname(sa,reinterpret_cast<sockaddr*>(&aa),&length);length=sizeof(ab);getsockname(sb,reinterpret_cast<sockaddr*>(&ab),&length);
for(auto socket:{sa,sb}){u_long nonblock=1;ioctlsocket(socket,FIONBIO,&nonblock);int buffer=1024*1024;setsockopt(socket,SOL_SOCKET,SO_RCVBUF,reinterpret_cast<char*>(&buffer),sizeof(buffer));}
ClipboardExchange sender(aid),receiver(bid);sender.peerSession(bid,receiver.session());receiver.peerSession(aid,sender.session());
Bytes large(2*1024*1024);uint32_t random=123;for(auto &v:large){random=random*1664525+1013904223;v=static_cast<unsigned char>(random>>24);}
auto started=GetTickCount64();sender.queue({bid},large,"benchmark",started);
auto send=[&](SOCKET socket,const sockaddr_in &address,const Json &p){auto data=p.dump();if(sendto(socket,data.data(),int(data.size()),0,reinterpret_cast<const sockaddr*>(&address),sizeof(address))!=int(data.size()))throw std::runtime_error("UDP benchmark send failed");};
while(receiver.deliveries.empty()&&GetTickCount64()-started<15000){
auto now=GetTickCount64();sender.poll(trusted,now,[&](const auto &,const Json &p){send(sa,ab,p);});
bool progressed=false;
for(int n=0;n<64;++n){char buffer[2048];auto size=recv(sb,buffer,sizeof(buffer),0);if(size<=0)break;progressed=true;
receiver.receive(aid,key,Json::parse(buffer,buffer+size),now,123,[&](const auto &,const Json &p){send(sb,aa,p);});}
for(int n=0;n<64;++n){char buffer[2048];auto size=recv(sa,buffer,sizeof(buffer),0);if(size<=0)break;progressed=true;
sender.receive(bid,key,Json::parse(buffer,buffer+size),now,123,[&](const auto &,const Json &p){send(sa,ab,p);});}
if(!progressed)Sleep(1);
}
check(receiver.deliveries.size()==1&&receiver.deliveries[0].bytes==large,"2 MiB crosses actual encrypted UDP sockets intact");
std::cout<<"2 MiB encrypted UDP loopback: "<<(GetTickCount64()-started)<<" ms\n";
}
std::cout<<checks<<" LAN clipboard checks passed\n";return 0;
}catch(const std::exception &e){std::cerr<<e.what()<<'\n';return 1;}}
+219 -8
View File
@@ -1,5 +1,6 @@
#include <winsock2.h>
#include "lan_input.h"
#include "lan_mesh.h"
#include <iostream>
#include <deque>
@@ -12,23 +13,31 @@ template<class F>void rejects(F f,const char *text){bool failed=false;try{f();}c
struct Desktop
{
RECT rect{-1920,-200,0,880};POINT point{-1000,300};bool available=true,physicalHeld=false,allow=true;
std::vector<INPUT> events;
std::vector<INPUT> events;int dpi=96;bool delayed=false;InputRouter *router{};
InputEnvironment environment()
{
return {[this]{return rect;},[this]{return point;},[this](const INPUT &i){
if(!allow)return false;
events.push_back(i);
if(i.type==INPUT_MOUSE&&(i.mi.dwFlags&MOUSEEVENTF_MOVE))
if(i.type==INPUT_MOUSE&&(i.mi.dwFlags&MOUSEEVENTF_MOVE)&&!delayed)
{
point={rect.left+LONG(int64_t(i.mi.dx)*(rect.right-rect.left)/65536),rect.top+LONG(int64_t(i.mi.dy)*(rect.bottom-rect.top)/65536)};
if(router){MSLLHOOKSTRUCT m{};m.pt=point;m.flags=LLMHF_INJECTED;m.dwExtraInfo=i.mi.dwExtraInfo;router->mouse(WM_MOUSEMOVE,m,1000);}
}
return true;
},[this]{return available;},[this]{return physicalHeld;}};
},[this]{return available;},[this]{return physicalHeld;},{},{},[this](POINT){return dpi;}};
}
unsigned keyEvents(bool down)const
{return unsigned(std::count_if(events.begin(),events.end(),[down](auto &i){return i.type==INPUT_KEYBOARD&&bool(i.ki.dwFlags&KEYEVENTF_KEYUP)!=down;}));}
};
KBDLLHOOKSTRUCT key(unsigned vk,unsigned scan=0){KBDLLHOOKSTRUCT k{};k.vkCode=vk;k.scanCode=scan;return k;}
bool motion(InputRouter &router,Desktop &desktop,POINT p,uint64_t now)
{desktop.point=p;MSLLHOOKSTRUCT m{};m.pt=p;return router.mouse(WM_MOUSEMOVE,m,now);}
{
auto before=desktop.point;desktop.point=p;MSLLHOOKSTRUCT m{};m.pt=p;
bool consumed=router.mouse(WM_MOUSEMOVE,m,now);
if(consumed&&router.mode()==ControlMode::Sending)desktop.point=before;
return consumed;
}
void exchange(InputRouter &source,InputRouter &target,std::deque<InputFrame> &out,uint64_t now)
{(void)source;while(!out.empty()){auto f=out.front();out.pop_front();target.receive(f,now);}}
struct Pair
@@ -37,8 +46,9 @@ struct Pair
InputRouter left,right;
explicit Pair(int side):left(a.environment(),[this](auto f){ab.push_back(f);return true;},side,true),
right(b.environment(),[this](auto f){ba.push_back(f);return true;},side^1,false)
{b.rect={0,0,1600,900};b.point={600,400};left.tick(true,1000);right.tick(true,1000);}
void deliver(uint64_t now=1100){exchange(left,right,ab,now);exchange(right,left,ba,now);}
{a.router=&left;b.router=&right;b.rect={0,0,1600,900};b.point={600,400};left.tick(true,1000);right.tick(true,1000);}
~Pair(){a.router=b.router=nullptr;}
void deliver(uint64_t now=1100){exchange(left,right,ab,now);right.tick(true,now);exchange(right,left,ba,now);left.tick(true,now);}
void enter(int edge)
{
motion(left,a,edgePoint(a.rect,edge,30000,0),1000);
@@ -48,6 +58,93 @@ struct Pair
};
void routerChecks()
{
{
std::vector<InputFrame> queue;
for(int i=0;i<20;++i)enqueueInput(queue,{InputType::Wheel,1,120,0});
check(queue.size()==1&&queue[0].a==2400,"pending wheel burst coalesces without losing same-direction distance");
enqueueInput(queue,{InputType::Wheel,1,-120,0});
check(queue.size()==1&&queue[0].a==-120,"wheel reversal replaces undelivered stale direction");
enqueueInput(queue,{InputType::Key,1,VK_CONTROL,0,1});enqueueInput(queue,{InputType::Wheel,1,120,0});
check(queue.size()==3&&queue[1].type==InputType::Key,"wheel coalescing preserves modifier barriers");
enqueueInput(queue,{InputType::Wheel,1,120,1});enqueueInput(queue,{InputType::Wheel,2,120,1});
check(queue.size()==5,"wheel coalescing preserves axis and ownership boundaries");
}
{
Pair p(1);p.enter(1);p.a.delayed=true;
p.left.cancel(1200);
auto before=p.a.point;
check(motion(p.left,p.a,{before.x+10,before.y},1201),"physical movement is gated while return-to-edge is queued");
p.a.point=edgePoint(p.a.rect,1,edgePosition(p.a.point,p.a.rect,1));p.left.tick(true,1500);
check(!motion(p.left,p.a,{p.a.point.x-10,p.a.point.y},1501),"return movement gate always clears within bounded timeout");
}
{
Pair p(1);p.enter(1);
for(auto vk:{VK_SHIFT,VK_LSHIFT,VK_RSHIFT,VK_CONTROL,VK_MENU})
p.right.keyboard(key(vk),false,1200);
check(p.right.mode()==ControlMode::Receiving&&p.ba.empty(),"layout-generated unpaired modifier releases cannot return remote ownership");
auto own=key(VK_SHIFT);own.dwExtraInfo=inputMarker();
p.right.keyboard(own,true,1201);p.right.keyboard(own,false,1202);
check(p.right.mode()==ControlMode::Receiving&&p.ba.empty(),"own correction marker cannot trigger takeover even with injection flag stripped");
p.right.keyboard(key('B'),true,1203);
check(p.right.mode()==ControlMode::Local&&!p.ba.empty(),"real local key press still takes over after conversion release noise");
}
{
Pair p(1);p.enter(1);MSLLHOOKSTRUCT mouse{};mouse.pt=p.b.point;
p.right.mouse(WM_MOUSEMOVE,mouse,1200);
check(p.right.mode()==ControlMode::Receiving,"stationary untagged mouse notification does not cancel remote control");
mouse.pt.x+=20;p.right.mouse(WM_MOUSEMOVE,mouse,1201);
check(p.right.mode()==ControlMode::Local,"actual local mouse movement still takes control back");
}
{
Pair p(1);p.enter(1);p.b.delayed=true;auto before=p.b.events.back().mi.dx;
for(int i=0;i<5;++i){auto point=p.a.point;point.x+=10;motion(p.left,p.a,point,1300+i);p.deliver(1300+i);}
auto pixels=LONG(int64_t(p.b.events.back().mi.dx)*(p.b.rect.right-p.b.rect.left)/65536);
check(pixels==58&&p.b.point.x==8&&p.b.events.back().mi.dx>before,
"queued remote moves accumulate from entry edge even before Windows updates cursor position");
}
{
Pair p(1);p.b.delayed=true;
motion(p.left,p.a,edgePoint(p.a.rect,1,30000,0),1000);p.deliver(1100);
check(p.left.mode()==ControlMode::Requesting,"source stays local until destination cursor actually reaches entry edge");
p.right.tick(true,1400);p.deliver(1400);
check(p.left.mode()==ControlMode::Local&&p.right.mode()==ControlMode::Local,"failed destination positioning returns both peers locally");
}
{
Pair p(1);p.a.delayed=true;p.enter(1);p.left.tick(true,1400);p.deliver(1400);
check(p.left.mode()==ControlMode::Local&&p.right.mode()==ControlMode::Local,"missing source warp acknowledgement cannot suppress movement indefinitely");
}
{
Pair p(1);p.a.delayed=true;p.enter(1);auto queued=p.ab.size();
motion(p.left,p.a,edgePoint(p.a.rect,1,30000,0),1200);
check(p.ab.size()==queued,"pre-parking physical motion cannot become a giant centre-relative delta");
MSLLHOOKSTRUCT warp{};warp.flags=LLMHF_INJECTED;warp.dwExtraInfo=cursorWarpMarker();
p.left.mouse(WM_MOUSEMOVE,warp,1201);p.a.point={(p.a.rect.left+p.a.rect.right)/2,(p.a.rect.top+p.a.rect.bottom)/2};
auto point=p.a.point;++point.x;motion(p.left,p.a,point,1202);
check(p.ab.size()==queued+1,"source movement resumes after actual warp acknowledgement");
}
for(auto modifier:{VK_LCONTROL,VK_LMENU,VK_LSHIFT,VK_LWIN})
{
Pair p(1);p.enter(1);p.b.events.clear();
auto mod=key(modifier,modifier==VK_LCONTROL?0x1d:modifier==VK_LMENU?0x38:modifier==VK_LSHIFT?0x2a:0x5b);
if(modifier==VK_LWIN)mod.flags=LLKHF_EXTENDED;
p.left.keyboard(mod,true,1200);p.left.keyboard(key('C',0x2e),true,1201);
p.left.keyboard(key('C',0x2e),false,1202);p.left.keyboard(mod,false,1203);p.deliver(1204);
check(p.b.events.size()==4&&p.b.events[0].ki.wVk==modifier&&p.b.events[1].ki.wVk=='C'&&
!(p.b.events[0].ki.dwFlags&KEYEVENTF_KEYUP)&&(p.b.events[3].ki.dwFlags&KEYEVENTF_KEYUP),
"modifier chord delivered in order with matching release");
check(!(p.b.events[0].ki.dwFlags&KEYEVENTF_SCANCODE)&&!(p.b.events[3].ki.dwFlags&KEYEVENTF_SCANCODE),
"modifier down and up use explicit Windows key identities");
auto correction=key(VK_CONTROL,0x1d);correction.flags=LLKHF_INJECTED;correction.dwExtraInfo=ks::inputMarker();
p.right.keyboard(correction,true,1210);p.right.keyboard(correction,false,1211);p.right.tick(true,1220);
check(p.right.mode()==ControlMode::Receiving&&p.left.mode()==ControlMode::Sending,
"generated correction keys do not take back remote ownership");
}
for(auto dpis:{std::pair{96,192},std::pair{240,96}})
{
Pair p(1);p.a.dpi=dpis.first;p.b.dpi=dpis.second;p.enter(1);auto before=p.b.point;
for(int i=0;i<100;++i){auto point=p.a.point;++point.x;motion(p.left,p.a,point,1300+i);p.deliver(1300+i);}
check(std::abs(p.b.point.x-before.x-100*dpis.second/dpis.first)<=1,"mouse movement scales between monitor DPIs without losing small deltas");
}
for(int side=0;side<4;++side)
{
Pair p(side);auto middle=edgePoint(p.a.rect,side,32768,0);
@@ -145,6 +242,61 @@ void routerChecks()
check(router.mode()==ControlMode::Local&&router.keyboard(key('Q'),false,1300),"queue failure returns control and consumes orphan release");
}
}
struct MeshFixture
{
struct Message{std::string from,to;InputFrame frame;};
std::deque<Message> messages;Desktop desktops[3];std::unique_ptr<InputMesh> meshes[3];uint64_t now=1000;
std::map<std::string,uint64_t> connections[3];
MeshFixture(bool vertical=false){
POINT cells[3]={{0,0},{1,0},vertical?POINT{1,-1}:POINT{2,0}};
for(int i=0;i<3;++i){
desktops[i].rect={0,0,800,600};desktops[i].point={400,300};std::map<std::string,POINT> positions;
for(int j=0;j<3;++j)if(i!=j){auto name=std::to_string(j);positions[name]={cells[j].x-cells[i].x,cells[j].y-cells[i].y};connections[i][name]=1;}
meshes[i]=std::make_unique<InputMesh>(std::to_string(i),desktops[i].environment(),positions,[this,i](auto &peer,auto f){messages.push_back({std::to_string(i),peer,f});return true;});
}pump();
}
~MeshFixture(){for(auto &m:meshes)m.reset();}
void pump(){for(int rounds=0;rounds<10;++rounds){
while(!messages.empty()){auto m=messages.front();messages.pop_front();meshes[std::stoi(m.to)]->receive(m.from,m.frame,now);}
for(int i=0;i<3;++i)meshes[i]->tick(connections[i],now);
}}
void move(int x,int y){++now;auto &d=desktops[0];auto old=d.point;MSLLHOOKSTRUCT m{};m.pt={old.x+x,old.y+y};d.point=m.pt;
bool consumed=meshes[0]->mouse(WM_MOUSEMOVE,m,now);if(consumed&&meshes[0]->mode()==ControlMode::Sending)d.point=old;pump();}
void enter(){desktops[0].point={798,300};move(1,0);check(meshes[0]->mode()==ControlMode::Sending&&meshes[1]->mode()==ControlMode::Receiving,"mesh enters first neighbor");}
};
void meshChecks()
{
{
MeshFixture f;f.enter();f.move(900,0);
check(f.meshes[0]->peer()=="2"&&f.meshes[2]->mode()==ControlMode::Receiving&&f.meshes[1]->mode()==ControlMode::Local,"A routes directly to C through B right edge");
f.desktops[1].events.clear();f.desktops[2].events.clear();
for(auto k:{VK_LSHIFT,int('A')})f.meshes[0]->keyboard(key(k),true,++f.now);
for(auto k:{int('A'),VK_LSHIFT})f.meshes[0]->keyboard(key(k),false,++f.now);
f.pump();
check(f.desktops[1].keyEvents(true)==0&&f.desktops[2].keyEvents(true)==2&&f.desktops[2].keyEvents(false)==2,"modifier chord goes only to current destination C");
f.move(-900,0);check(f.meshes[0]->peer()=="1"&&f.meshes[1]->mode()==ControlMode::Receiving,"C left edge returns through B");
f.move(-900,0);check(f.meshes[0]->mode()==ControlMode::Local&&f.desktops[0].point.x>780,"B left edge restores source at its right edge");
}
{
MeshFixture f(true);f.enter();f.move(0,-900);
check(f.meshes[0]->peer()=="2"&&f.desktops[2].point.y>580,"vertical turn enters C at bottom edge");
f.move(0,900);check(f.meshes[0]->peer()=="1","vertical return routes back to B");
}
{
MeshFixture f;f.connections[0].erase("2");f.pump();f.enter();f.move(900,0);
check(f.meshes[0]->peer()=="1"&&f.meshes[1]->mode()==ControlMode::Receiving,"offline neighbor blocks passage without losing current control");
f.meshes[0]->keyboard(key(VK_LSHIFT),true,++f.now);f.pump();f.connections[0].erase("1");f.connections[1].erase("0");f.pump();
check(f.meshes[0]->mode()==ControlMode::Local&&f.meshes[1]->mode()==ControlMode::Local&&f.desktops[1].keyEvents(false)==1,"disconnect restores local control and releases held modifier");
}
{
MeshFixture f;f.enter();f.meshes[1]->receive("2",{InputType::Enter,99,1,20000},++f.now);
check(f.meshes[1]->peer()=="0"&&f.meshes[1]->mode()==ControlMode::Receiving,"another controller cannot steal receiving desktop");f.pump();
f.move(900,0);f.meshes[0]->receive("1",{InputType::Return,1,20000},++f.now);
check(f.meshes[0]->mode()==ControlMode::Sending&&f.meshes[0]->peer()=="2","late previous peer return cannot cancel current destination");
f.connections[0]["2"]=2;f.pump();check(f.meshes[0]->mode()==ControlMode::Local,"new connection generation does not continue stale ownership");
}
}
uint16_t freePort()
{
SOCKET s=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);if(s==INVALID_SOCKET)throw std::runtime_error("test socket");
@@ -152,6 +304,32 @@ uint16_t freePort()
if(bind(s,(sockaddr*)&a,sizeof(a))){closesocket(s);throw std::runtime_error("test bind");}
int size=sizeof(a);getsockname(s,(sockaddr*)&a,&size);auto port=ntohs(a.sin_port);closesocket(s);return port;
}
void hubChecks()
{
WSADATA data{};if(WSAStartup(MAKEWORD(2,2),&data))throw std::runtime_error("WSAStartup");
struct Close{~Close(){WSACleanup();}} close;
std::string ids[3]={std::string(32,'a'),std::string(32,'b'),std::string(32,'c')},secret(64,'f');
uint16_t ports[3]={freePort(),freePort(),freePort()};
std::unique_ptr<InputHub> hubs[3];std::vector<InputEndpoint> endpoints[3];std::vector<RoutedInput> incoming[3];
for(int i=0;i<3;++i){hubs[i]=std::make_unique<InputHub>(ids[i],ports[i]);for(int j=0;j<3;++j)if(i!=j)endpoints[i].push_back({ids[j],secret,htonl(INADDR_LOOPBACK),ports[j]});}
auto pump=[&](uint64_t offset=0){for(int i=2;i>=0;--i)hubs[i]->poll(endpoints[i],{},incoming[i],GetTickCount64()+offset);Sleep(1);};
auto deadline=GetTickCount64()+5000;
while((hubs[0]->ready().size()!=2||hubs[1]->ready().size()!=2||hubs[2]->ready().size()!=2)&&GetTickCount64()<deadline)pump();
check(hubs[0]->ready().size()==2&&hubs[1]->ready().size()==2&&hubs[2]->ready().size()==2,"three TCP hubs authenticate all independent pair channels on one listener per PC");
InputFrame keyFrame{InputType::Key,7,'A',0,1},cross{InputType::Cross,8,2,30000};
hubs[0]->poll(endpoints[0],{{ids[1],keyFrame},{ids[2],cross}},incoming[0],GetTickCount64());
for(int n=0;n<100&&(incoming[1].empty()||incoming[2].empty());++n)pump();
check(incoming[1].size()==1&&incoming[1][0].peer==ids[0]&&incoming[1][0].frame==keyFrame&&incoming[2].size()==1&&incoming[2][0].frame==cross,"hub routes encrypted frames only to their named destinations");
for(auto &in:incoming)in.clear();
auto survivor=hubs[0]->ready().at(ids[2]),old=hubs[0]->ready().at(ids[1]);
hubs[1]->disconnect();for(int n=0;n<30;++n)pump();
check(hubs[0]->ready().contains(ids[2])&&hubs[0]->ready().at(ids[2])==survivor,"one computer disconnect leaves other authenticated channel intact");
for(int n=0;n<300&&(hubs[0]->ready().size()!=2||hubs[1]->ready().size()!=2);++n)pump(1800);
check(hubs[0]->ready().size()==2&&hubs[0]->ready().at(ids[1])!=old,"mesh reconnect restores channel with fresh ownership generation");
endpoints[0].erase(endpoints[0].begin());hubs[0]->poll(endpoints[0],{},incoming[0],GetTickCount64()+1800);
check(!hubs[0]->ready().contains(ids[1])&&hubs[0]->ready().contains(ids[2]),"forgetting one pair revokes only its channel");
}
void channelChecks()
{
std::string a(32,'a'),b(32,'b'),secret(64,'f');auto na=randomHex(16),nb=randomHex(16);
@@ -167,6 +345,31 @@ void channelChecks()
InputCipher otherSession(inputSessionKey(secret,a,b,randomHex(16),nb));rejects([&]{otherSession.decrypt(packet);},"old-session input rejected");
WSADATA data{};if(WSAStartup(MAKEWORD(2,2),&data))throw std::runtime_error("WSAStartup");
struct Close{~Close(){WSACleanup();}} close;
{
auto port=freePort();InputChannel client(a,b,secret,1,port),server(b,a,secret,0,port);
std::vector<InputFrame> received;auto start=GetTickCount64();
server.poll(htonl(INADDR_LOOPBACK),{},received,start);
client.poll(htonl(INADDR_LOOPBACK),{},received,start);
auto generation=client.generation();
client.poll(htonl(INADDR_LOOPBACK),{},received,start+3000);
check(client.generation()==generation,"initial handshake allows more than the active heartbeat timeout");
for(int n=0;n<200&&(!client.ready()||!server.ready());++n)
{
auto now=GetTickCount64()+3000;
server.poll(htonl(INADDR_LOOPBACK),{},received,now);client.poll(htonl(INADDR_LOOPBACK),{},received,now);Sleep(1);
}
check(client.ready()&&server.ready(),"delayed initial handshake still authenticates");
}
{
auto port=freePort();InputChannel client(a,b,secret,1,port),server(b,a,secret,0,port);
std::vector<InputFrame> received;auto start=GetTickCount64();
server.poll(htonl(INADDR_LOOPBACK),{},received,start);
client.poll(htonl(INADDR_LOOPBACK),{},received,start);
client.poll(htonl(INADDR_LOOPBACK),{},received,start+100);
client.poll(htonl(INADDR_LOOPBACK),{},received,start+11000);
check(!client.ready()&&client.status().find("authentication timed out")!=std::string::npos,
"initial authentication timeout distinguished from lost active connection");
}
{
auto port=freePort();InputChannel client(a,b,secret,1,port),server(b,a,secret,0,port);
auto deadline=GetTickCount64()+5000;std::vector<InputFrame> received;
@@ -182,7 +385,15 @@ void channelChecks()
InputFrame reverse{InputType::Wheel,37,-120,1};server.poll(htonl(INADDR_LOOPBACK),{reverse},received,GetTickCount64());
for(int n=0;n<100&&received.empty();++n){client.poll(htonl(INADDR_LOOPBACK),{},received,GetTickCount64());Sleep(1);}
check(received.size()==1&&received[0]==reverse,"real input channel is bidirectional");
auto generation=client.generation();client.poll(htonl(INADDR_LOOPBACK),{},received,GetTickCount64()+3000);
received.clear();server.disconnect();
client.poll(htonl(INADDR_LOOPBACK),{},received,GetTickCount64());
for(int n=0;n<300&&(!client.ready()||!server.ready());++n)
{
auto now=GetTickCount64()+1800;
server.poll(htonl(INADDR_LOOPBACK),{},received,now);client.poll(htonl(INADDR_LOOPBACK),{},received,now);Sleep(1);
}
check(client.ready()&&server.ready(),"paired channel reconnects automatically after peer disconnect without new pairing");
auto generation=client.generation();client.poll(htonl(INADDR_LOOPBACK),{},received,GetTickCount64()+5000);
check(!client.ready()&&client.generation()!=generation,"heartbeat timeout closes channel and changes generation");
}
{
@@ -201,6 +412,6 @@ void channelChecks()
}
int main()
{
try{channelChecks();routerChecks();std::cout<<"Passed "<<passed<<" LAN input checks\n";}
try{channelChecks();hubChecks();routerChecks();meshChecks();std::cout<<"Passed "<<passed<<" LAN input checks\n";}
catch(const std::exception &e){std::cerr<<"FAIL: "<<e.what()<<'\n';return 1;}
}
+21
View File
@@ -0,0 +1,21 @@
#include "settings_merge.h"
#include <iostream>
using namespace ks;
int main(){try{
Settings base;base.autoCorrect=false;base.toggle=L"Ctrl+Alt+F11";
auto draft=base,current=base;draft.toggle=L"Ctrl+Alt+F12";current.autoCorrect=true;
auto merged=decodeSettings(mergeConfigurationSettings(encodeSettings(base),encodeSettings(draft),encodeSettings(current)),{});
if(merged.toggle!=draft.toggle||!merged.autoCorrect)throw std::runtime_error("independent changes lost");
current.toggle=L"Ctrl+Alt+F10";bool rejected=false;
try{mergeConfigurationSettings(encodeSettings(base),encodeSettings(draft),encodeSettings(current));}
catch(const std::exception &e){rejected=std::string(e.what()).starts_with("Settings conflict: ");}
if(!rejected)throw std::runtime_error("same-field conflict silently overwritten");
merged=decodeSettings(mergeConfigurationSettings(encodeSettings(base),encodeSettings(draft),encodeSettings(current),true),{});
if(merged.toggle!=draft.toggle||!merged.autoCorrect)throw std::runtime_error("confirmed edits discarded unrelated current changes");
merged=decodeSettings(mergeConfigurationSettings(encodeSettings(base),encodeSettings(base),encodeSettings(current)),{});
if(merged.toggle!=current.toggle||!merged.autoCorrect)throw std::runtime_error("unchanged draft reverted current settings");
draft=base;current=base;draft.chatCycles[L"telegram\nfirst"]={L"win:04090409"};current.chatCycles[L"telegram\nsecond"]={L"win:04220422"};
merged=decodeSettings(mergeConfigurationSettings(encodeSettings(base),encodeSettings(draft),encodeSettings(current)),{});
if(merged.chatCycles.size()!=2)throw std::runtime_error("independent nested changes lost");
std::cout<<"Passed 5 settings-save merge checks\n";
}catch(const std::exception &e){std::cerr<<e.what()<<'\n';return 1;}}
+28
View File
@@ -1,6 +1,7 @@
#include "autocorrect.h"
#include "history_preview.h"
#include "ocr.h"
#include "lan_clipboard_image.h"
#include "richtext.h"
#include "storage.h"
#include "diagnostics.h"
@@ -895,6 +896,33 @@ int main(int argc, char **argv)
ks::validateTesseract(options);
require(true, "Windows OCR works without optional installation");
}
{
ks::History h(temp/L"computer-origins");auto clip=ks::textClip(L"shared text");
h.add(clip,L"editor",settings);
h.add(clip,L"editor",settings,"peer-a",L"Office PC");
h.add(clip,L"editor",settings,"peer-a",L"Office PC");
require(h.entries().size()==2,"duplicate suppression preserves different clipboard sources");
ks::History restored(temp/L"computer-origins");
require(restored.entries().front().computer=="peer-a"&&restored.entries().front().computerName==L"Office PC","computer metadata survives history reload");
require(restored.entries().back().computer.empty(),"legacy and local records remain local");
}
{
ks::Pixels pixels{1920,1080,ks::Bytes(1920*1080*4,255)};
for(size_t i=0;i<pixels.bgra.size();i+=4){pixels.bgra[i]=static_cast<unsigned char>((i/4)%1920/16);pixels.bgra[i+1]=static_cast<unsigned char>((i/4)/1920/16);}
ks::Clip original;original.formats.push_back(ks::imageDib(pixels));
original.formats.push_back({RegisterClipboardFormatW(L"PNG"),L"PNG",ks::encodePng(pixels)});
auto packed=ks::lan::compactClipboardImage(original);
require(packed.formats.size()==1&&packed.formats[0].name==L"PNG","LAN sends only one image format");
require(packed.size()*10<original.size(),"screen-like image LAN payload reduced by more than 90 percent");
require(ks::decodeImage(packed).bgra==pixels.bgra,"LAN PNG preserves exact pixels");
auto paste=ks::lan::clipboardImageForPaste(packed);
require(std::any_of(paste.formats.begin(),paste.formats.end(),[](const auto &f){return f.id==CF_DIB||f.id==CF_DIBV5;}),"LAN receiver restores a DIB for ordinary paste");
original.formats.pop_back();
require(ks::decodeImage(ks::lan::compactClipboardImage(original)).bgra==pixels.bgra,"DIB-only clipboard is losslessly compressed");
auto text=ks::textClip(L"plain text");
require(ks::encodeClip(ks::lan::compactClipboardImage(text))==ks::encodeClip(text),"LAN text clipboard remains unchanged");
std::cout<<"LAN screenshot bytes: "<<original.size()<<" -> "<<packed.size()<<"\n";
}
// Test-owned directory only; never touch the user's clipboard or stored history.
std::filesystem::remove_all(temp);
CoUninitialize();