Release v0.6.58: preserve LAN connections across missed discovery

This commit is contained in:
agrynco committed 2026-11-06 22:08:47 +02:00
1 parent 5de010ee44
commit d0387904a0
14 files changed
+121 -74

No files matched your search

+1 -1
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.24)
project(KSwitcher VERSION 0.6.57 LANGUAGES CXX RC)
project(KSwitcher VERSION 0.6.58 LANGUAGES CXX RC)
if(NOT WIN32)
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
endif()
+6 -2
View File
@@ -1,4 +1,4 @@
# LAN mouse, keyboard and clipboard — v0.6.57 (experimental)
# LAN mouse, keyboard and clipboard — v0.6.58 (experimental)
KSwitcher discovers other enabled KSwitcher instances, pairs them with the same
code on both computers, and shares mouse and keyboard through a screen edge.
@@ -10,7 +10,7 @@ is not implemented by clipboard synchronization.
## Set up multiple computers
1. Install the v0.6.57 or newer on every participating PC. Input protocol v2 requires all participants to update; earlier one-pair builds cannot connect. Preserve local data; do not copy pairing identities between PCs.
1. Install the v0.6.58 or newer on every participating PC. Input protocol v2 requires all participants to update; earlier one-pair builds cannot connect. Preserve local data; do not copy pairing identities between PCs.
2. Open Settings → Tools → KSwitcher computers and enable discovery on both PCs.
Only running LAN-enabled KSwitcher instances appear, not generic network devices.
3. Match the computer name and instance-ID prefix displayed on the other PC.
@@ -116,3 +116,7 @@ Three physical computers still need an end-to-end trial for their adapters,
firewalls, displays and application privilege levels. The automated TCP tests
use local sockets, and input-state tests use simulated desktop environments;
they do not claim a completed three-machine hardware test.
## v0.6.58 connection fixes
Saved trust keeps TCP listeners available even when discovery announcements are missed. Working authenticated input sessions no longer depend on discovery presence. Pair saved is distinct from input connected; the list refreshes every second. A changed instance ID requires a new pairing. Keep each PC's own data folder during upgrades. Connection state changes and listener Windows errors are written to the diagnostic log.
+7 -3
View File
@@ -10,14 +10,18 @@ The original [Typlo](https://github.com/bitcodepro/Typlo) project and its data r
[Donate via Monobank](https://send.monobank.ua/jar/88h2TddKfS) · Thank you for supporting KSwitcher.
## KSwitcher computers — v0.6.57 (experimental)
## KSwitcher computers — v0.6.58 (experimental)
TCP input listeners and authenticated connections survive missed discovery announcements.
The list distinguishes saved pairing from connected input and refreshes every second.
Keep each PC’s own data folder during updates; a different instance ID requires new pairing.
Arrange multiple paired PCs on a monitor grid. Mouse and keyboard follow shared
edges, including A → B → C and back. New text and images go to every online paired
PC's history and active clipboard. The history Computers menu filters by multiple
sources using checkboxes. See [LAN-PREVIEW.md](LAN-PREVIEW.md).
**Update every participating PC to v0.6.57:** input protocol v2 is incompatible
**Update every participating PC to v0.6.58:** input protocol v2 is incompatible
with older one-pair builds. Keep each PC's own data directory. Pair every input
source with all destinations, arrange the same physical layout on each PC and
enable input sharing. Empty cells and offline neighbors block passage.
@@ -25,7 +29,7 @@ enable input sharing. Empty cells and offline neighbors block passage.
**Українською.** Розставте кілька спарованих ПК по сітці: миша й клавіатура
переходять через спільний край, зокрема A → B → C і назад. Нові текст та зображення
передаються всім доступним спарованим ПК в історію й активний буфер. Фільтр
Computers в історії має чекбокси. Оновіть усі ПК до v0.6.57, залишивши кожному
Computers в історії має чекбокси. Оновіть усі ПК до v0.6.58, залишивши кожному
власну папку data. Спаруйте джерело вводу з усіма потрібними ПК й задайте
однакове фізичне розташування на кожному. Передача файлів не реалізована.
Binary file not shown.
+6 -9
View File
@@ -1,19 +1,16 @@
# Latest Windows release
## v0.6.57 — 2026-11-06
## v0.6.58 — 2026-11-06
## Changes
- Settings Save merges independent edits with newer resident settings. Same-field conflicts offer a choice; declining keeps the window and draft open.
- Multiple paired PCs on a draggable monitor grid; mouse and keyboard follow neighboring edges, including A → B → C and back. Connections reconnect independently.
- New text, formatted text and screenshots synchronize to all online paired PCs: history and active clipboard. Newer local copies are preserved during incoming transfers.
- Clipboard history has a persistent Computers checkbox filter and source labels. Lossless PNG transport avoids duplicate image payloads.
- LAN modifier chords, DPI-scaled motion, cursor parking/return, automatic conversion ownership and queued wheel reversal fixes. Local shortcut latches reset on ownership changes.
- Stable paired status, pairing/input button states, persistent selection and a fixed-size LAN window with scrolling where needed.
- Keep trusted TCP input listeners and connections alive across missing discovery announcements. Retain the last known address for reconnection; authenticate incoming trusted peers even before discovery sees their address.
- Show saved pairing separately from actual input connectivity: connected, disabled here, offline or not connected. Background identity verification does not flash the list.
- Log TCP listener failures with the Windows error and peer connection state changes for troubleshooting.
## Validation
Passed: 5 settings-save merge checks, 203 LAN input/three-peer TCP, 289 clipboard transfer, 43 pairing, 828 settings/chat UI, 570 input, 341 native and 126 sync checks; dictionary equivalence for 140,939 words; wheel, StayHere, Utilities, screenshot UI/save/registration, 54 updater checks and updater progress. Three physical PCs have not been verified together. LAN remains experimental; follow LAN-PREVIEW.md.
Passed: 205 LAN input checks (including missed discovery and authentication before discovery), 289 clipboard transfer, 43 pairing, 829 settings/chat UI, 570 input, 341 native, 126 sync and 5 settings-save merge checks; dictionary equivalence for 140,939 words; wheel, StayHere, Utilities, screenshot UI/save/registration, 54 updater checks and updater progress. LAN remains experimental; three physical PCs have not been verified together.
**Update ALL LAN participants to v0.6.57. Input protocol v2 cannot connect to older one-pair builds.** Pair every physical input source with each destination. Configure the same physical arrangement on each PC; layouts are not automatically synchronized. File transfer, offline clipboard backfill, cross-PC dragging and secure desktops remain unsupported.
Update both PCs to receive these fixes. Preserve each PC's own data directory and pairing identity. Input protocol v2 remains compatible with v0.6.57. A changed instance ID requires pairing again. The computer list refreshes automatically once per second while open.
## Upgrade and compatibility
Choose **Update** in the tray menu. Legacy v0.3.0–v0.4.1 clients first receive the v0.4.2 compatibility bridge, then the current full release. v0.2.3 and earlier require manual installation.
+1 -1
View File
@@ -1 +1 @@
f3ec4f7a62b4479929d94d6d6f7f68edb8b4a47b84edac7258afb64dbe07aef2 KSwitcher-win-x64.zip
16d8138c63442931bf947707b28820d9b89943169ac9f25db3cb50d23c0ff319 KSwitcher-win-x64.zip
+35 -35
View File
@@ -1,86 +1,86 @@
{
"version": "0.6.57",
"schema": 1,
"files": [
{
"size": 2106368,
"sha256": "a7be2f749082bb6417b4cf7e8c8ea221ef4f7ba63dabc3cbbe8694ab1a3d942c",
"path": "KSwitcher.exe",
"sha256": "3b7315e1545f70739fd1a7c099ddeab28362b323521d93879fa43fdd76133f8e"
"size": 2108416
},
{
"size": 2003456,
"sha256": "c8a88364c9967ce5fd150519d632b14460e225a0cfae51a8de50f39782d8010c",
"path": "KSwitcher.Config.exe",
"sha256": "a819ec42030990725326730396680388cbcb870dd00c0a97856cb1615f9db27b"
"size": 2005504
},
{
"size": 3151360,
"sha256": "68eed5abefed0208ef7bdd431094c7357c53bd3ca03dfd91aaeb7dbb4f291e06",
"path": "KSwitcher.Screenshot.exe",
"sha256": "8473697245c2c961496135eaaff6de9e9a19e917f2fd7299a16068210223d396"
"size": 3153408
},
{
"size": 4401,
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c",
"path": "LICENSE",
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c"
"size": 4401
},
{
"size": 1096,
"sha256": "61e08da79c44061654015ce6b0c4061c23ee1dd18237fbf09e9bfda3c0ea2831",
"path": "json-LICENSE.txt",
"sha256": "61e08da79c44061654015ce6b0c4061c23ee1dd18237fbf09e9bfda3c0ea2831"
"size": 1096
},
{
"size": 79896,
"sha256": "87f396a6bf988725b14bf5b6eb7c2b60ce33ff51828290ce40e136c66942cc09",
"path": "README.md",
"sha256": "8b4b1e905a2f0a39d532e61136ec7eca473492a39a538eb073ed829307adefae"
"size": 80168
},
{
"size": 7921,
"sha256": "72e131bdf46d32a8a17fb352292bc8983c0e5a7ed84812218b11cf6b69c9afc4",
"path": "LAN-PREVIEW.md",
"sha256": "2467b86bbad7d7dce1e0c65c8c0faf2320e4804e90a85784a058afa357127b6b"
"size": 8376
},
{
"size": 732485,
"sha256": "d0c022b27b9fbc2ff7b6b8bec723cc1adc4eef517202f9ff77eed5d8bea3acb9",
"path": "dictionaries/en.txt",
"sha256": "d0c022b27b9fbc2ff7b6b8bec723cc1adc4eef517202f9ff77eed5d8bea3acb9"
"size": 732485
},
{
"size": 20566,
"sha256": "0bdfb0201d1803634f268cdff0993b8349d2c70e956a06dbb667c8dd65254ec5",
"path": "dictionaries/LICENSE-CC-BY-SA-4.0.txt",
"sha256": "0bdfb0201d1803634f268cdff0993b8349d2c70e956a06dbb667c8dd65254ec5"
"size": 20566
},
{
"size": 2265,
"sha256": "3017bf44d3be9d9dfab21bd6778aa52b7947e61acf9ce2bc86b41d6e949ac311",
"path": "dictionaries/README.md",
"sha256": "3017bf44d3be9d9dfab21bd6778aa52b7947e61acf9ce2bc86b41d6e949ac311"
"size": 2265
},
{
"size": 1109134,
"sha256": "80e6b6a27815973535f7facb636c597eb845dfee45cfabfc4e9b3341b9332849",
"path": "dictionaries/ru.txt",
"sha256": "80e6b6a27815973535f7facb636c597eb845dfee45cfabfc4e9b3341b9332849"
"size": 1109134
},
{
"size": 1280,
"sha256": "98e5b3fe7304e37705c9e5ff48b0b399ccb16e806736d21f09921b99e5f2a408",
"path": "dictionaries/sources.json",
"sha256": "98e5b3fe7304e37705c9e5ff48b0b399ccb16e806736d21f09921b99e5f2a408"
"size": 1280
},
{
"size": 940799,
"sha256": "d30e990d6ec2983c04f3644a3c61c6c668e8c542323f5931916fdd55b916eebf",
"path": "dictionaries/uk.txt",
"sha256": "d30e990d6ec2983c04f3644a3c61c6c668e8c542323f5931916fdd55b916eebf"
"size": 940799
},
{
"size": 993,
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a",
"path": "tools/README.txt",
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a"
"size": 993
},
{
"size": 134656,
"sha256": "6ef5cf64708d62b7fc63c3c793cc99188a513433ff4774cae01dc014f6664376",
"path": "tools/StayHere.exe",
"sha256": "6ef5cf64708d62b7fc63c3c793cc99188a513433ff4774cae01dc014f6664376"
"size": 134656
},
{
"size": 2796,
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413",
"path": "tools/StayHere-README.md",
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413"
"size": 2796
}
]
],
"schema": 1,
"version": "0.6.58"
}
+3 -3
View File
@@ -9,8 +9,8 @@ IDC_SCREENSHOT_PENCIL CURSOR "icons/lucide/pencil.cur"
1 RT_MANIFEST "app.manifest"
201 RCDATA "update.ps1"
1 VERSIONINFO
FILEVERSION 0,6,57,0
PRODUCTVERSION 0,6,57,0
FILEVERSION 0,6,58,0
PRODUCTVERSION 0,6,58,0
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_APP
BEGIN
@@ -20,7 +20,7 @@ BEGIN
BEGIN
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
VALUE "ProductName", "KSwitcher"
VALUE "FileVersion", "0.6.57"
VALUE "FileVersion", "0.6.58"
END
END
BLOCK "VarFileInfo"
+2 -2
View File
@@ -242,8 +242,8 @@ struct LanService::Impl
if(peers.contains(target))send(socket,peers.at(target).address,packet);
});
clipboardActive=clipboard->active();
if(shareInput)for(auto &[peer,entry]:trusted.items())if(peers.contains(peer))
endpoints.push_back({peer,entry.at("key"),peers.at(peer).address.sin_addr.s_addr});
if(shareInput)for(auto &[peer,entry]:trusted.items())
endpoints.push_back({peer,entry.at("key"),peers.contains(peer)?peers.at(peer).address.sin_addr.s_addr:0});
if(now>=nextHello)
{
epoch=lan::randomHex(16); // Fresh challenge prevents replayed presence from staying online.
+16 -5
View File
@@ -207,7 +207,7 @@ std::string InputChannel::status()const{return impl->state;}
void InputChannel::disconnect(){impl->reset(GetTickCount64(),"Input disconnected; local control restored.");}
struct InputHub::Impl
{
struct Link { InputEndpoint endpoint;std::unique_ptr<InputChannel> channel;uint64_t generation{},epoch{}; };
struct Link { InputEndpoint endpoint;std::unique_ptr<InputChannel> channel;uint64_t generation{},epoch{};std::string lastState; };
struct Pending {SOCKET socket;uint32_t address;uint64_t since;};
std::string local;uint16_t port;SOCKET listener=INVALID_SOCKET;
std::map<std::string,Link> links;std::vector<Pending> pending;uint64_t epoch{},retry{};
@@ -217,19 +217,29 @@ struct InputHub::Impl
void poll(const std::vector<InputEndpoint> &endpoints,const std::vector<RoutedInput> &out,std::vector<RoutedInput> &in,uint64_t now)
{
for(auto it=links.begin();it!=links.end();){
auto match=std::find_if(endpoints.begin(),endpoints.end(),[&](auto &e){return e.id==it->first&&e.key==it->second.endpoint.key&&e.port==it->second.endpoint.port&&e.address==it->second.endpoint.address;});
auto match=std::find_if(endpoints.begin(),endpoints.end(),[&](auto &e){return e.id==it->first&&e.key==it->second.endpoint.key&&e.port==it->second.endpoint.port;});
if(match==endpoints.end())it=links.erase(it);else ++it;
}
for(auto &e:endpoints)if(e.id!=local&&!links.contains(e.id)){
Link link;link.endpoint=e;link.channel=std::make_unique<InputChannel>(local,e.id,e.key,0,e.port,true);link.epoch=++epoch;links.emplace(e.id,std::move(link));
}
// Discovery is a hint, not the lifetime of authenticated input sessions.
// Keep the last usable address across missed announcements. A new address
// is used only once a channel is down, never to tear down working input.
for(auto &e:endpoints){if(e.id==local)continue;auto &link=links.at(e.id);
if(e.address&&!link.channel->ready()&&e.address!=link.endpoint.address){
link.channel->disconnect();link.endpoint.address=e.address;
}
}
bool listenNeeded=std::any_of(endpoints.begin(),endpoints.end(),[&](auto &e){return e.id<local;});
if(!listenNeeded){closeSocket(listener);for(auto &p:pending)closeSocket(p.socket);pending.clear();}
if(listenNeeded&&listener==INVALID_SOCKET&&now>=retry){
listener=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);BOOL yes=TRUE;sockaddr_in a{};a.sin_family=AF_INET;a.sin_port=htons(port);
if(listener==INVALID_SOCKET||setsockopt(listener,SOL_SOCKET,SO_EXCLUSIVEADDRUSE,(char*)&yes,sizeof(yes))||!nonblocking(listener)||bind(listener,(sockaddr*)&a,sizeof(a))||listen(listener,SOMAXCONN)){
closeSocket(listener);retry=now+1500;error="TCP input port is unavailable.";
}else error.clear();
auto detail="TCP input port "+std::to_string(port)+" is unavailable (Windows error "+std::to_string(WSAGetLastError())+").";
if(error!=detail)log(L"LAN input: "+wide(detail));
closeSocket(listener);retry=now+1500;error=std::move(detail);
}else {error.clear();log(L"LAN input listener ready on TCP "+std::to_wstring(port));}
}
if(listener!=INVALID_SOCKET)for(int n=0;n<16&&pending.size()<64;++n){
sockaddr_in a{};int size=sizeof(a);SOCKET sock=accept(listener,(sockaddr*)&a,&size);if(sock==INVALID_SOCKET)break;
@@ -243,7 +253,7 @@ struct InputHub::Impl
else if(count>=int(length+4)){
done=true;
try{auto hello=Json::parse(bytes+4,bytes+4+length);auto id=hello.value("id","");auto link=links.find(id);
if(id<local&&hello.value("to","")==local&&hello.value("version",0)==2&&link!=links.end()&&link->second.endpoint.address==it->address&&link->second.channel->adopt(uintptr_t(it->socket),now))it->socket=INVALID_SOCKET;
if(id<local&&hello.value("to","")==local&&hello.value("version",0)==2&&link!=links.end()&&link->second.channel->adopt(uintptr_t(it->socket),now))it->socket=INVALID_SOCKET;
}catch(const std::exception &){}
}
}
@@ -252,6 +262,7 @@ struct InputHub::Impl
for(auto &[id,link]:links){
std::vector<InputFrame> frames,received;for(auto &f:out)if(f.peer==id&&(!f.epoch||f.epoch==link.epoch))enqueueInput(frames,f.frame);
link.channel->poll(link.endpoint.address,frames,received,now);
auto state=link.channel->status();if(state!=link.lastState){link.lastState=state;log(L"LAN input peer "+wide(id.substr(0,8))+L": "+wide(state));}
if(link.generation!=link.channel->generation()){link.generation=link.channel->generation();link.epoch=++epoch;}
for(auto &f:received)in.push_back({id,f,link.epoch});
}
+7 -1
View File
@@ -36,7 +36,13 @@ void refreshLan(HWND w,LanView &v)
for(const auto &peer:next["peers"])
{
auto id=peer.at("id").get<std::string>();ids.push_back(id);
auto state=peer.at("paired").get<bool>() ? L"Paired" : L"Not paired";
std::wstring state=L"Not paired";
if(peer.at("paired").get<bool>()){
bool connected=next.value("inputReady",nlohmann::json::object()).contains(id);
state=connected?L"Pair saved — input connected":
!next.value("shareInput",false)?L"Pair saved — input disabled here":
!peer.value("online",false)?L"Pair saved — computer offline":L"Pair saved — input not connected";
}
rows.push_back(wide(peer.at("name").get<std::string>())+L" ["+wide(id.substr(0,8))+L"] — "+state);
}
// Presence/challenge refreshes must not repaint or reset the pairing list.
+1 -1
View File
@@ -2,7 +2,7 @@
#include "platform.h"
namespace ks
{
inline constexpr wchar_t AppVersion[] = L"0.6.57";
inline constexpr wchar_t AppVersion[] = L"0.6.58";
inline constexpr UINT UpdateMessage = WM_APP + 7;
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
+22 -11
View File
@@ -7,6 +7,15 @@
namespace
{
HWND findTestWindow(LPCWSTR className, LPCWSTR title)
{
HWND window=nullptr;
while((window=FindWindowExW(nullptr,window,className,title))) {
DWORD process{}; GetWindowThreadProcessId(window,&process);
if(process==GetCurrentProcessId()) return window;
}
return nullptr;
}
int passed{};
void require(bool ok, const char *message)
{
@@ -141,31 +150,31 @@ int main()
SetDlgItemTextW(parent,240,originalLimit);
require(!saveEnabled(),"restoring valid original text clears dirty state");
SendMessageW(parent,WM_COMMAND,257,0);
HWND ocr=FindWindowW(L"KSwitcher.OcrOptions",nullptr);
HWND ocr=findTestWindow(L"KSwitcher.OcrOptions",nullptr);
require(ocr!=nullptr,"OCR preferences open");
wchar_t languages[128]{}; GetDlgItemTextW(ocr,503,languages,128);
SetDlgItemTextW(ocr,503,L"eng");
SendMessageW(ocr,WM_COMMAND,IDCANCEL,0);
require(!saveEnabled(),"cancelled child changes leave Settings clean");
SendMessageW(parent,WM_COMMAND,257,0);
ocr=FindWindowW(L"KSwitcher.OcrOptions",nullptr);
ocr=findTestWindow(L"KSwitcher.OcrOptions",nullptr);
SetDlgItemTextW(ocr,503,std::wstring(languages)==L"eng"?L"ukr":L"eng");
SendMessageW(ocr,WM_COMMAND,IDOK,0);
require(!IsWindow(ocr) && saveEnabled(),"accepted OCR edits enable parent Save");
SendMessageW(parent,WM_COMMAND,257,0);
ocr=FindWindowW(L"KSwitcher.OcrOptions",nullptr);
ocr=findTestWindow(L"KSwitcher.OcrOptions",nullptr);
SetDlgItemTextW(ocr,503,languages);
SendMessageW(ocr,WM_COMMAND,IDOK,0);
require(!saveEnabled(),"reverting OCR draft in child clears parent dirty state");
SendMessageW(parent,WM_COMMAND,255,0);
HWND automatic=FindWindowW(L"KSwitcher.AutoOptions",nullptr);
HWND automatic=findTestWindow(L"KSwitcher.AutoOptions",nullptr);
require(automatic!=nullptr,"automatic correction preferences open");
bool replace=IsDlgButtonChecked(automatic,414)==BST_CHECKED;
CheckDlgButton(automatic,414,replace?BST_UNCHECKED:BST_CHECKED);
SendMessageW(automatic,WM_COMMAND,IDOK,0);
require(!IsWindow(automatic) && saveEnabled(),"accepted auto-correction edits enable Save");
SendMessageW(parent,WM_COMMAND,255,0);
automatic=FindWindowW(L"KSwitcher.AutoOptions",nullptr);
automatic=findTestWindow(L"KSwitcher.AutoOptions",nullptr);
CheckDlgButton(automatic,414,replace?BST_CHECKED:BST_UNCHECKED);
SendMessageW(automatic,WM_COMMAND,IDOK,0);
require(!saveEnabled(),"reverting auto-correction draft disables Save");
@@ -201,7 +210,7 @@ int main()
SendMessageW(parent, WM_NOTIFY, 260, (LPARAM)&notification);
require(!saveEnabled(),"changing tabs leaves Settings clean");
SendMessageW(parent, WM_COMMAND, 259, 0);
HWND dialog = FindWindowW(L"KSwitcher.ChatLanguages", nullptr);
HWND dialog = findTestWindow(L"KSwitcher.ChatLanguages", nullptr);
require(dialog && !IsWindowEnabled(parent), "chat dialog opens with disabled parent");
int index = (int)SendDlgItemMessageW(dialog, 710, LB_FINDSTRINGEXACT, -1, (LPARAM)L"Telegram: ! Fixture chat");
require(index >= 0, "saved chat listed");
@@ -220,7 +229,7 @@ int main()
require(!IsWindow(dialog) && IsWindowEnabled(parent), "cancel closes dialog");
require(!saveEnabled(),"cancelled chat edits do not enable Save");
SendMessageW(parent, WM_COMMAND, 259, 0);
dialog = FindWindowW(L"KSwitcher.ChatLanguages", nullptr);
dialog = findTestWindow(L"KSwitcher.ChatLanguages", nullptr);
index = (int)SendDlgItemMessageW(dialog, 710, LB_FINDSTRINGEXACT, -1, (LPARAM)L"Telegram: ! Fixture chat");
SendDlgItemMessageW(dialog, 710, LB_SETCURSEL, index, 0);
SendMessageW(dialog, WM_COMMAND, MAKEWPARAM(710, LBN_SELCHANGE), 0);
@@ -236,7 +245,7 @@ int main()
parent = app.settingsWindow;
require(!saveEnabled(),"reopened saved Settings is clean");
SendMessageW(parent, WM_COMMAND, 259, 0);
dialog = FindWindowW(L"KSwitcher.ChatLanguages", nullptr);
dialog = findTestWindow(L"KSwitcher.ChatLanguages", nullptr);
index = (int)SendDlgItemMessageW(dialog, 710, LB_FINDSTRINGEXACT, -1, (LPARAM)L"Telegram: ! Fixture chat");
SendDlgItemMessageW(dialog, 710, LB_SETCURSEL, index, 0);
SendMessageW(dialog, WM_COMMAND, MAKEWPARAM(710, LBN_SELCHANGE), 0);
@@ -286,7 +295,7 @@ int main()
require(app.settings.snippets[0].apps==std::vector<std::wstring>{L"notepad",L"ms-teams"}, "cancel preserves existing scope");
ks::showSettings(app);parent=app.settingsWindow;
SendMessageW(parent,WM_COMMAND,262,0);
HWND lan=FindWindowW(L"KSwitcher.LAN",nullptr);
HWND lan=findTestWindow(L"KSwitcher.LAN",nullptr);
require(lan && !IsWindowEnabled(parent),"LAN pairing opens a native child window");
require(!IsWindowEnabled(GetDlgItem(lan,705)),"pairing disabled without discovered peer");
require(!IsWindowEnabled(GetDlgItem(lan,706)),"no cancellation without pending pairing");
@@ -312,8 +321,10 @@ int main()
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);
HWND lan=findTestWindow(L"KSwitcher.LAN",nullptr);
require(SendDlgItemMessageW(lan,702,LB_GETCURSEL,0,0)==0,"single saved pair selected automatically when opening LAN settings");
wchar_t pairState[512]{};SendDlgItemMessageW(lan,702,LB_GETTEXT,0,(LPARAM)pairState);
require(std::wstring(pairState).find(L"Pair saved — input disabled here")!=std::wstring::npos,"saved pairing does not imply an active input connection");
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);
@@ -346,7 +357,7 @@ int main()
{"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);
SendMessageW(layoutApp.settingsWindow,WM_COMMAND,262,0);auto lan=findTestWindow(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));
+14
View File
@@ -317,6 +317,11 @@ void hubChecks()
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");
auto generation=hubs[0]->ready();
for(auto &list:endpoints)for(auto &e:list)e.address=0;
for(int n=0;n<20;++n)pump();
check(hubs[0]->ready()==generation&&hubs[1]->ready().size()==2&&hubs[2]->ready().size()==2,"expired discovery does not destroy authenticated channels");
for(auto &list:endpoints)for(auto &e:list)e.address=htonl(INADDR_LOOPBACK);
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();
@@ -327,6 +332,15 @@ void hubChecks()
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");
{
auto serverPort=freePort();InputHub server(ids[2],serverPort),client(ids[0],freePort());
std::vector<InputEndpoint> unknown{{ids[0],secret,0}},known{{ids[2],secret,htonl(INADDR_LOOPBACK),serverPort}};
std::vector<RoutedInput> a,b;
for(int n=0;n<300&&(server.ready().empty()||client.ready().empty());++n){
server.poll(unknown,{},a,GetTickCount64());client.poll(known,{},b,GetTickCount64());Sleep(1);
}
check(server.ready().size()==1&&client.ready().size()==1,"saved pair listens and authenticates without receiving a discovery announcement");
}
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");
}