Release v0.6.63: automatic LAN transport fallback and connection help

This commit is contained in:
agrynco committed 2026-11-10 19:30:50 +02:00
1 parent 3761b790ae
commit 5a2106899d
16 files changed
+376 -42

No files matched your search

+27 -4
View File
@@ -72,6 +72,7 @@ struct LanService::Impl
std::map<std::string,uint64_t> inputReady;
std::unique_ptr<lan::InputMesh> router;
std::string inputStatus="Input sharing disabled.";
bool inputNeedsHelp{};
HWND inputWindow{};std::atomic<bool> inputNotification{false};
explicit Impl(const std::filesystem::path &root,HWND window) : path(root/L"lan-pairing.bin"),inputWindow(window)
{
@@ -210,7 +211,7 @@ struct LanService::Impl
auto previous=trusted;auto previousPositions=positions;
trusted[other]={{"name",peer.name},{"key",pending->derived}};
try {lan::fillPositions(positions,trusted,inputPeer,inputEdge);save();} catch(...) {trusted=std::move(previous);positions=std::move(previousPositions);throw;}
layoutChanged=true;peer.verified=now; status="Paired. Select a screen position and enable mouse and keyboard on both PCs.";
layoutChanged=true;peer.verified=now; status="Pair saved automatically. Arrange the monitors, then enable input sharing on both PCs.";
}
}
}
@@ -232,6 +233,10 @@ struct LanService::Impl
if(bind(socket,(sockaddr*)&bound,sizeof(bound))) throw std::runtime_error("UDP port 45883 is in use. Only one LAN-enabled KSwitcher may run per computer.");
uint64_t nextHello=0,nextOffer=0;
lan::InputHub hub(id);
hub.datagrams([&](const auto &target,const auto &packet){
std::lock_guard lock(mutex);
if(shareInput&&trusted.contains(target)&&peers.contains(target))send(socket,peers.at(target).address,packet);
});
while(!stopping)
{
auto now=GetTickCount64();bool clipboardActive=false;
@@ -279,7 +284,7 @@ struct LanService::Impl
}
if(!in.empty()&&inputWindow&&!inputNotification.exchange(true))
if(!PostMessageW(inputWindow,LanInputReadyMessage,0,0))inputNotification=false;
{std::lock_guard lock(mutex);inputStatus=hub.status();}
{std::lock_guard lock(mutex);inputStatus=hub.status();inputNeedsHelp=hub.needsHelp();}
fd_set read; FD_ZERO(&read); FD_SET(socket,&read); timeval delay{0,!endpoints.empty()||clipboardActive?5000:100000};
int ready=select(0,&read,nullptr,nullptr,&delay);
if(ready==SOCKET_ERROR) throw std::runtime_error("LAN discovery socket failed.");
@@ -289,7 +294,14 @@ struct LanService::Impl
int received=recvfrom(socket,buffer,sizeof(buffer),0,(sockaddr*)&sender,&size);
if(received<=0) break;
if(received>2048||sender.sin_port!=htons(lan::Port)||!localAddress(sender.sin_addr.s_addr)) continue;
try { auto j=Json::parse(buffer,buffer+received); std::lock_guard lock(mutex); receive(socket,sender,j,GetTickCount64()); }
try {
auto j=Json::parse(buffer,buffer+received);
if(j.value("type","")=="input-udp"){
bool allowed=false;auto peer=j.value("id","");
{std::lock_guard lock(mutex);allowed=shareInput&&trusted.contains(peer)&&peers.contains(peer)&&peers.at(peer).address.sin_addr.s_addr==sender.sin_addr.s_addr;}
if(allowed)hub.receiveDatagram(j,GetTickCount64());
}else {std::lock_guard lock(mutex);receive(socket,sender,j,GetTickCount64());}
}
catch(const Json::exception &) {} // Malformed network data cannot interrupt discovery.
}
}
@@ -334,6 +346,14 @@ struct LanService::Impl
configureRouter();inputStatus=active?"Connecting input channel...":"Input sharing disabled.";
}
if(action=="code") return encoded({{"code",lan::randomHex(16)}});
if(action=="connectionHelp"){
auto exe=utf8(executable());std::string quoted;
for(char c:exe){quoted+=c;if(c=='\'')quoted+='\'';}
auto script="$ksPath = '"+quoted+"'\r\n"
"New-NetFirewallRule -DisplayName 'KSwitcher LAN TCP' -Direction Inbound -Action Allow -Program $ksPath -Protocol TCP -LocalPort 45884 -RemoteAddress LocalSubnet -Profile Any\r\n"
"New-NetFirewallRule -DisplayName 'KSwitcher LAN UDP' -Direction Inbound -Action Allow -Program $ksPath -Protocol UDP -LocalPort 45883 -RemoteAddress LocalSubnet -Profile Any";
return encoded({{"script",script}});
}
if(action=="cancel") { pending.reset(); status="Pairing cancelled."; }
if(action=="pair")
{
@@ -354,9 +374,12 @@ struct LanService::Impl
}
Json result={{"enabled",enabled},{"id",id},{"name",name},{"status",status},{"pending",pending&&!pending->confirmed},{"peers",Json::array()},
{"positions",positions},{"shareInput",shareInput},{"inputPeer",inputPeer},{"controlPeer",router?router->peer():std::string{}},{"inputEdge",inputEdge},{"inputConnected",inputConnected.load()},
{"inputStatus",enabled&&shareInput?inputStatus:"Input sharing disabled."},{"controlMode",router?int(router->mode()):0}};
{"inputStatus",enabled&&shareInput?inputStatus:"Input sharing disabled."},{"inputNeedsHelp",enabled&&shareInput&&inputNeedsHelp},{"controlMode",router?int(router->mode()):0}};
{std::lock_guard inputLock(inputMutex);result["inputReady"]=inputReady;}
auto now=GetTickCount64();
result["pendingPeer"]=pending&&!pending->confirmed ? pending->peer : "";
result["pairSecondsRemaining"]=pending&&!pending->confirmed&&!pending->expired(now)
? (lan::PairLifetime-(now-pending->started)+999)/1000 : 0;
for(const auto &[peer,p]:peers)
{
char ip[INET_ADDRSTRLEN]{}; InetNtopA(AF_INET,(void*)&p.address.sin_addr,ip,sizeof(ip));
+60 -18
View File
@@ -1,6 +1,7 @@
#include <winsock2.h>
#include <ws2tcpip.h>
#include "lan_channel.h"
#include "lan_datagram.h"
#include <bcrypt.h>
#include <limits>
@@ -78,17 +79,18 @@ std::string inputSessionKey(const std::string &secret,const std::string &from,co
}
struct InputChannel::Impl
{
std::string local,peer,secret,nonce,state="Waiting for the paired computer...";
std::string local,peer,secret,nonce,state="Waiting for the paired computer...",lastError;
int edge;uint16_t port;SOCKET listener=INVALID_SOCKET,socket=INVALID_SOCKET;
bool connecting{},authenticated{},helloRead{},managed{};
uint64_t changed{},lastRead{},lastPing{},retry{};
uint64_t changed{},lastRead{},lastPing{},retry{},waitingSince{};
bool reverse{};
Bytes incoming,outgoing;
std::unique_ptr<InputCipher> tx,rx;
Impl(std::string a,std::string b,std::string key,int side,uint16_t p,bool multiple):local(std::move(a)),peer(std::move(b)),secret(std::move(key)),edge(side),port(p),managed(multiple){}
~Impl(){closeSocket(socket);closeSocket(listener);}
void reset(uint64_t now,const std::string &reason)
{
closeSocket(socket);connecting=authenticated=helloRead=false;incoming.clear();outgoing.clear();tx.reset();rx.reset();++changed;retry=now+1500;state=reason;
closeSocket(socket);connecting=authenticated=helloRead=false;incoming.clear();outgoing.clear();tx.reset();rx.reset();++changed;retry=now+1500;waitingSince=0;state=reason;lastError=reason;
}
void begin(uint64_t now)
{
@@ -101,6 +103,7 @@ struct InputChannel::Impl
{
try
{
if(!waitingSince)waitingSince=now;
if(socket==INVALID_SOCKET&&now<retry)return;
if(!managed && listener==INVALID_SOCKET && local>peer)
{
@@ -111,14 +114,15 @@ struct InputChannel::Impl
}
if(socket==INVALID_SOCKET && address && now>=retry)
{
if(local<peer)
if(local<peer||(managed&&now-waitingSince>=1500))
{
reverse=local>peer;
socket=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);if(socket==INVALID_SOCKET||!nonblocking(socket))throw std::runtime_error("Cannot open input connection.");
sockaddr_in a{};a.sin_family=AF_INET;a.sin_port=htons(port);a.sin_addr.s_addr=address;
int result=connect(socket,(sockaddr*)&a,sizeof(a));
if(result && WSAGetLastError()!=WSAEWOULDBLOCK)throw std::runtime_error("Paired computer is not accepting input connections.");
connecting=result!=0;begin(now);
if(connecting)state="Connecting to paired PC on TCP 45884...";
if(connecting)state=reverse?"Trying reverse TCP connection...":"Trying direct TCP connection...";
}
else if(!managed)
{
@@ -185,7 +189,7 @@ struct InputChannel::Impl
while(helloRead && incoming.size()>=36)
{
auto f=rx->decrypt(Bytes(incoming.begin(),incoming.begin()+36));incoming.erase(incoming.begin(),incoming.begin()+36);lastRead=now;
if(!authenticated){if(f.type!=InputType::Ready)throw std::runtime_error("Input received before authentication.");authenticated=true;state="Input connected. Move through the configured screen edge.";}
if(!authenticated){if(f.type!=InputType::Ready)throw std::runtime_error("Input received before authentication.");authenticated=true;lastError.clear();state="Input connected. Move through the configured screen edge.";}
else if(f.type==InputType::Ready)throw std::runtime_error("Repeated input handshake.");
else if(f.type!=InputType::Ping)received.push_back(f);
}
@@ -198,30 +202,44 @@ InputChannel::~InputChannel()=default;
void InputChannel::poll(uint32_t ip,const std::vector<InputFrame>&out,std::vector<InputFrame>&in,uint64_t now){impl->poll(ip,out,in,now);}
bool InputChannel::adopt(uintptr_t accepted,uint64_t now)
{
if(!impl->managed||impl->socket!=INVALID_SOCKET||!nonblocking(SOCKET(accepted)))return false;
if(!impl->managed||impl->authenticated||!nonblocking(SOCKET(accepted)))return false;
// Reverse connection wins a simultaneous connect race. Never replace an
// authenticated channel or accept two sockets for the same input stream.
if(impl->socket!=INVALID_SOCKET){
if(impl->local>impl->peer||!impl->connecting)return false;
impl->reset(now,"Accepting reverse TCP connection.");
}
impl->reverse=impl->local<impl->peer;
impl->connecting=false;impl->begin(now);impl->socket=SOCKET(accepted);return true;
}
bool InputChannel::ready()const{return impl->authenticated;}
uint64_t InputChannel::generation()const{return impl->changed;}
std::string InputChannel::status()const{return impl->state;}
std::string InputChannel::status()const{
return impl->lastError.empty() ? impl->state : impl->lastError+" Retrying automatically.";
}
void InputChannel::disconnect(){impl->reset(GetTickCount64(),"Input disconnected; local control restored.");}
std::string InputChannel::transport()const{return impl->reverse?"reverse TCP":"direct TCP";}
struct InputHub::Impl
{
struct Link { InputEndpoint endpoint;std::unique_ptr<InputChannel> channel;uint64_t generation{},epoch{};std::string lastState; };
struct Link { InputEndpoint endpoint;std::unique_ptr<InputChannel> channel;std::unique_ptr<InputDatagram> udp;uint64_t generation{},epoch{},since{},failedSince{};bool udpReady{};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{};
std::string error;
InputHub::SendDatagram sendDatagram;
uint64_t currentTime{};
Impl(std::string id,uint16_t p):local(std::move(id)),port(p){}
~Impl(){closeSocket(listener);for(auto &p:pending)closeSocket(p.socket);}
void poll(const std::vector<InputEndpoint> &endpoints,const std::vector<RoutedInput> &out,std::vector<RoutedInput> &in,uint64_t now)
{
currentTime=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;});
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));
Link link;link.endpoint=e;link.channel=std::make_unique<InputChannel>(local,e.id,e.key,0,e.port,true);
link.udp=std::make_unique<InputDatagram>(local,e.id,e.key);link.since=link.failedSince=now;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
@@ -231,7 +249,7 @@ struct InputHub::Impl
link.channel->disconnect();link.endpoint.address=e.address;
}
}
bool listenNeeded=std::any_of(endpoints.begin(),endpoints.end(),[&](auto &e){return e.id<local;});
bool listenNeeded=!links.empty();
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);
@@ -253,7 +271,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.channel->adopt(uintptr_t(it->socket),now))it->socket=INVALID_SOCKET;
if(hello.value("app","")=="KSwitcher-input"&&hello.value("to","")==local&&hello.value("version",0)==2&&link!=links.end()&&!link->second.udp->active()&&link->second.channel->adopt(uintptr_t(it->socket),now))it->socket=INVALID_SOCKET;
}catch(const std::exception &){}
}
}
@@ -261,8 +279,16 @@ 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(sendDatagram&&!link.channel->ready()&&!link.udp->active()&&local<id&&now-link.since>=5000){link.udp->start(now);link.channel->disconnect();frames.clear();}
if(link.udp->active()){
link.udp->poll(frames,received,now,sendDatagram);
if(!link.udp->active()){link.since=now;link.channel->disconnect();received.clear();}
}else link.channel->poll(link.endpoint.address,frames,received,now);
if(link.udpReady!=link.udp->ready()){link.udpReady=link.udp->ready();link.epoch=++epoch;}
bool ready=link.channel->ready()||link.udp->ready();
if(ready)link.failedSince=link.since=now;
auto state=link.udp->ready()?"Input connected via encrypted UDP.":link.udp->active()?"Trying encrypted UDP over discovery...":link.channel->ready()?"Input connected via "+link.channel->transport()+".":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});
}
@@ -271,12 +297,28 @@ struct InputHub::Impl
InputHub::InputHub(std::string id,uint16_t port):impl(std::make_unique<Impl>(std::move(id),port)){}
InputHub::~InputHub()=default;
void InputHub::poll(const std::vector<InputEndpoint> &peers,const std::vector<RoutedInput> &out,std::vector<RoutedInput> &in,uint64_t now){impl->poll(peers,out,in,now);}
std::map<std::string,uint64_t> InputHub::ready()const{std::map<std::string,uint64_t> result;for(auto &[id,l]:impl->links)if(l.channel->ready())result[id]=l.epoch;return result;}
std::map<std::string,uint64_t> InputHub::ready()const{std::map<std::string,uint64_t> result;for(auto &[id,l]:impl->links)if(l.channel->ready()||l.udp->ready())result[id]=l.epoch;return result;}
std::string InputHub::status()const{
auto count=ready().size();if(count)return "Input connected to "+std::to_string(count)+" computer(s). Move through a neighboring screen edge.";
auto count=ready().size();if(count==impl->links.size()&&count){
std::string routes;for(auto &[id,l]:impl->links){if(!routes.empty())routes+=", ";routes+=id.substr(0,8)+": "+(l.udp->ready()?"encrypted UDP":l.channel->transport());}
return "Input connected ("+routes+"). Move through a neighboring screen edge.";
}
if(needsHelp())return "Automatic connection attempts have not succeeded for every PC. Enable input on both PCs. Open Connection help for firewall commands.";
if(!impl->error.empty())return impl->error;
for(auto &[id,l]:impl->links)if(!l.channel->status().empty())return l.channel->status();
for(auto &[id,l]:impl->links)if(!l.channel->ready()&&!l.udp->ready())return l.lastState;
return "Waiting for paired computers...";
}
void InputHub::disconnect(){for(auto &[id,l]:impl->links)l.channel->disconnect();}
void InputHub::disconnect(){for(auto &[id,l]:impl->links){l.channel->disconnect();l.udp->reset();l.since=impl->currentTime;}}
void InputHub::datagrams(SendDatagram send){impl->sendDatagram=std::move(send);}
void InputHub::receiveDatagram(const Json &j,uint64_t now){
if(!impl->sendDatagram)return;
auto it=impl->links.find(j.value("id",""));if(it==impl->links.end()||it->second.channel->ready())return;
auto &link=it->second;bool active=link.udp->active();
try{link.udp->receive(j,now,impl->sendDatagram);}catch(const std::exception &){return;}
if(!active&&link.udp->active())link.channel->disconnect();
}
bool InputHub::needsHelp()const{
for(auto &[id,l]:impl->links)if(!l.channel->ready()&&!l.udp->ready()&&impl->currentTime-l.failedSince>=15000)return true;
return false;
}
}
+6
View File
@@ -2,6 +2,7 @@
#include "lan_pairing.h"
#include <memory>
#include <map>
#include <functional>
namespace ks::lan
{
enum class InputType : uint32_t { Ready, Ping, Enter, Accept, Return, Move, Key, Button, Wheel, Cross };
@@ -57,6 +58,7 @@ class InputChannel
uint64_t generation() const;
std::string status() const;
void disconnect();
std::string transport()const;
};
struct InputEndpoint {std::string id,key;uint32_t address{};uint16_t port=45884;};
struct RoutedInput {std::string peer;InputFrame frame;uint64_t epoch{};};
@@ -70,5 +72,9 @@ class InputHub
std::map<std::string,uint64_t> ready()const;
std::string status()const;
void disconnect();
using SendDatagram=std::function<void(const std::string &,const Json &)>;
void datagrams(SendDatagram);
void receiveDatagram(const Json &,uint64_t now);
bool needsHelp()const;
};
}
+97
View File
@@ -0,0 +1,97 @@
#pragma once
#include "lan_channel.h"
#include <deque>
namespace ks::lan
{
// Ordered, bounded input on the discovery socket. Retransmission uses fresh
// authenticated packets; event sequence numbers prevent duplicate injection.
// Fresh challenge/response nonces bind every reconnect to a new session.
class InputDatagram
{
std::string local,peer,secret,nonce,remote,txKey,rxKey;
uint64_t started{},lastRead{},lastSend{},sentNumber{},seenNumber{},nextEvent{1},receivedEvent{},acked{},progress{};
bool live{},ackDue{};
std::deque<std::pair<uint64_t,InputFrame>> queue;
std::vector<InputFrame> incoming;
static std::string hex(const Bytes &b){std::string s;for(auto v:b){s+="0123456789abcdef"[v>>4];s+="0123456789abcdef"[v&15];}return s;}
static Bytes unhex(const std::string &s){
if(s.empty()||s.size()>2080||s.size()%2||s.find_first_not_of("0123456789abcdef")!=s.npos)throw std::runtime_error("Invalid UDP input packet.");
Bytes b;for(size_t i=0;i<s.size();i+=2)b.push_back(static_cast<unsigned char>(std::stoul(s.substr(i,2),nullptr,16)));return b;
}
std::string proof(const std::string &from,const std::string &to,const std::string &a,const std::string &b)const{
return mac(secret,"KSwitcher UDP input v1|"+from+"|"+to+"|"+a+"|"+b);
}
void keys(){
// Handshake proofs are public. Session keys use a separate domain and
// must never be the MAC transmitted in a welcome packet.
txKey=mac(secret,"KSwitcher UDP input key v1|"+local+"|"+peer+"|"+nonce+"|"+remote);
rxKey=mac(secret,"KSwitcher UDP input key v1|"+peer+"|"+local+"|"+remote+"|"+nonce);
}
public:
using Send=InputHub::SendDatagram;
explicit InputDatagram(std::string a,std::string b,std::string key):local(std::move(a)),peer(std::move(b)),secret(std::move(key)){}
bool active()const{return !nonce.empty();}
bool ready()const{return live;}
void reset(){nonce.clear();remote.clear();txKey.clear();rxKey.clear();queue.clear();incoming.clear();live=ackDue=false;sentNumber=seenNumber=receivedEvent=acked=lastSend=0;nextEvent=1;}
void start(uint64_t now){reset();nonce=randomHex(16);started=lastRead=progress=now;}
void receive(const Json &j,uint64_t now,const Send &send)
{
if(j.value("app","")!="KSwitcher-LAN"||j.value("version",0)!=1||j.value("type","")!="input-udp"||j.value("id","")!=peer||j.value("to","")!=local)return;
auto op=j.value("op","");
if(op=="hello"){
auto other=j.value("nonce","");if(local<peer||!identifier(other)||!equal(j.value("proof",""),proof(peer,local,other,"")))return;
if(active()&&remote!=other)return;
if(!active()){start(now);remote=other;keys();}
auto response=envelope("input-udp",local);response["to"]=peer;response["op"]="welcome";response["nonce"]=nonce;response["echo"]=remote;response["proof"]=proof(local,peer,nonce,remote);send(peer,response);return;
}
if(op=="welcome"){
auto other=j.value("nonce","");
if(local>peer||!active()||j.value("echo","")!=nonce||!identifier(other)||!equal(j.value("proof",""),proof(peer,local,other,nonce)))return;
if(!remote.empty()&&remote!=other)return;
remote=other;keys();ackDue=true;return;
}
if(op!="data"||rxKey.empty()||j.value("session","")!=nonce||j.value("fromSession","")!=remote)return;
auto number=j.value("number",uint64_t{});if(!number||number<=seenNumber)return;
InputCipher cipher(mac(rxKey,"packet|"+std::to_string(number)));
auto body=Json::from_cbor(cipher.decryptPayload(unhex(j.at("data").get<std::string>())));
auto ack=body.at("ack").get<uint64_t>(),first=body.at("first").get<uint64_t>();auto frames=body.at("frames");
if(ack>=nextEvent||!frames.is_array()||frames.size()>12||!first)return;
std::vector<InputFrame> decoded;
for(auto &f:frames){
if(!f.is_array()||f.size()!=5)return;
auto type=f[0].get<uint32_t>();if(type<uint32_t(InputType::Enter)||type>uint32_t(InputType::Cross))return;
decoded.push_back({InputType(type),f[1].get<uint32_t>(),f[2].get<int32_t>(),f[3].get<int32_t>(),f[4].get<int32_t>()});
}
seenNumber=number;lastRead=now;live=true;
if(ack>acked){acked=ack;progress=now;while(!queue.empty()&&queue.front().first<=ack)queue.pop_front();}
if(!decoded.empty()){
if(first==receivedEvent+1){
if(incoming.size()+decoded.size()>512){reset();return;}
incoming.insert(incoming.end(),decoded.begin(),decoded.end());receivedEvent+=decoded.size();
}
ackDue=true;
}
}
void poll(const std::vector<InputFrame> &out,std::vector<InputFrame> &in,uint64_t now,const Send &send)
{
if(!active())return;
if((live&&(now-lastRead>2000||(!queue.empty()&&now-progress>2000)))||(!live&&now-started>3000)){reset();return;}
if(live){
if(queue.empty()&&!out.empty())progress=now;
if(queue.size()+out.size()>512){reset();return;}
for(auto &f:out)queue.emplace_back(nextEvent++,f);
in.insert(in.end(),incoming.begin(),incoming.end());incoming.clear();
}
if(remote.empty()){
if(!lastSend||now-lastSend>=250){auto j=envelope("input-udp",local);j["to"]=peer;j["op"]="hello";j["nonce"]=nonce;j["proof"]=proof(local,peer,nonce,"");send(peer,j);lastSend=now;}return;
}
if(!ackDue&&lastSend&&now-lastSend<(queue.empty()?300:50))return;
Json frames=Json::array();for(auto &[sequence,f]:queue){frames.push_back(Json::array({uint32_t(f.type),f.token,f.a,f.b,f.c}));if(frames.size()==12)break;}
Json body={{"ack",receivedEvent},{"first",queue.empty()?nextEvent:queue.front().first},{"frames",frames}};
auto number=++sentNumber;InputCipher cipher(mac(txKey,"packet|"+std::to_string(number)));
auto j=envelope("input-udp",local);j["to"]=peer;j["op"]="data";j["number"]=number;j["session"]=remote;j["fromSession"]=nonce;j["data"]=hex(cipher.encryptPayload(Json::to_cbor(body)));
send(peer,j);lastSend=now;ackDue=false;
}
};
}
+4 -3
View File
@@ -1045,7 +1045,7 @@ struct SettingsView
int selection = -1;
bool filling{};
std::vector<std::vector<HWND>> pages;
bool ready{}, refreshing{};
bool ready{}, refreshing{}, visitedLan{};
};
void updateSettingsState(HWND w, SettingsView *v);
void selectSettingsPage(HWND w, SettingsView *v)
@@ -1646,7 +1646,8 @@ void updateSettingsState(HWND w, SettingsView *v)
v->refreshing = true;
bool changed = settingsChanged(w, v);
EnableWindow(GetDlgItem(w, IDOK), changed);
const wchar_t *text = changed ? L"Unsaved changes — click Save to apply." : L"No unsaved changes.";
const wchar_t *text = changed ? L"Unsaved changes — click Save to apply." :
v->visitedLan ? L"No unsaved settings. Network changes save automatically in the Computers window." : L"No unsaved changes.";
if (value(w, 261) != text) value(w, 261, text);
v->refreshing = false;
}
@@ -1743,7 +1744,7 @@ LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
}
if (id == 257) { showOcrOptions(w, v); return 0; }
if (id == 258) { showSyncOptions(w, *v->app); return 0; }
if (id == 262) { showLan(w, *v->app); return 0; }
if (id == 262) { v->visitedLan=true; showLan(w, *v->app); return 0; }
if (id == 263) { collectLayout(w, v); showAppLanguages(w, v); return 0; }
if (id == 259) { collectLayout(w, v); showChatLanguages(w, v); return 0; }
if (id == 230)
+44 -7
View File
@@ -20,6 +20,26 @@ std::string selectedLan(HWND w,LanView &v)
auto index=SendDlgItemMessageW(w,702,LB_GETCURSEL,0,0);
return index>=0 && size_t(index)<v.ids.size() ? v.ids[size_t(index)] : "";
}
LRESULT CALLBACK lanHelpProc(HWND w,UINT msg,WPARAM wp,LPARAM lp)
{
if(msg==WM_DPICHANGED){resizeForDpi(w,wp,lp);return 0;}
if(msg==WM_COMMAND&&LOWORD(wp)==724){SendDlgItemMessageW(w,723,EM_SETSEL,0,-1);SendDlgItemMessageW(w,723,WM_COPY,0,0);value(w,724,L"Copied");return 0;}
if(msg==WM_CLOSE||(msg==WM_COMMAND&&LOWORD(wp)==IDCANCEL)){DestroyWindow(w);return 0;}
if(msg==WM_DESTROY){auto parent=GetWindow(w,GW_OWNER);EnableWindow(parent,TRUE);SetForegroundWindow(parent);return 0;}
return DefWindowProcW(w,msg,wp,lp);
}
void showLanHelp(HWND parent,App &app)
{
auto script=wide(lanRequest(app,{{"action","connectionHelp"}}).at("script").get<std::string>());
auto w=makeWindow(app,L"KSwitcher.LAN.Help",L"KSwitcher — connection help",lanHelpProc,740,430,0,false);
SetWindowLongPtrW(w,GWLP_HWNDPARENT,(LONG_PTR)parent);
control(w,L"STATIC",L"First enable input sharing on both PCs. KSwitcher tries direct TCP, reverse TCP, then encrypted UDP automatically. Both PCs need an updated version for all three methods.",0,725,18,16,700,60);
control(w,L"STATIC",L"Still disconnected? A firewall may be blocking the app. On EACH PC, open this help and run its own command in PowerShell as administrator. This allows only local-network connections to that PC's current KSwitcher executable. No restart or new pairing is needed.",0,726,18,88,700,80);
auto field=edit(w,723,18,180,700,180,ES_MULTILINE|ES_READONLY|ES_AUTOVSCROLL|WS_VSCROLL);
SetWindowTextW(field,script.c_str());
button(w,L"&Copy command",724,18,380,180);button(w,L"&Close",IDCANCEL,618,380,100);
EnableWindow(parent,FALSE);showRequestedWindow(w);SetForegroundWindow(w);
}
void refreshLan(HWND w,LanView &v)
{
auto selected=selectedLan(w,v);
@@ -60,12 +80,26 @@ void refreshLan(HWND w,LanView &v)
for(size_t i=0;i<v.ids.size();++i)if(v.ids[i]==selected)
SendDlgItemMessageW(w,702,LB_SETCURSEL,i,0);
selected=selectedLan(w,v);
// Keep the code available while the user enters it on the other PC, and
// after cancellation/timeout for retry. Clear it only after trust is saved.
if(v.snapshot.value("pending",false))
for(const auto &peer:next["peers"])
if(peer.at("id")==v.snapshot.value("pendingPeer",std::string{})&&peer.value("paired",false))value(w,703,L"");
v.snapshot=std::move(next);
bool enabled=v.snapshot.at("enabled"), pending=v.snapshot.at("pending"), online=false, paired=false;
for(const auto &peer:v.snapshot["peers"]) if(peer.at("id")==selected) {online=peer.at("online");paired=peer.at("paired");}
value(w,701,enabled ? L"&Disable discovery" : L"&Enable discovery");
value(w,709,L"This KSwitcher: "+wide(v.snapshot.at("name").get<std::string>())+L" ["+wide(v.snapshot.at("id").get<std::string>().substr(0,8))+L"]");
value(w,708,v.error.empty() ? wide(v.snapshot.at("status").get<std::string>()) : v.error);
auto pairingStatus=wide(v.snapshot.at("status").get<std::string>());
if(pending)
{
std::wstring peerName;
for(const auto &peer:v.snapshot["peers"])
if(peer.at("id")==v.snapshot.value("pendingPeer",std::string{}))peerName=wide(peer.at("name").get<std::string>());
pairingStatus=L"Waiting for "+peerName+L" ("+std::to_wstring(v.snapshot.value("pairSecondsRemaining",0))+L" s).\nEnter the same code and click Pair there too. Pair is not saved yet.";
}
value(w,708,v.error.empty() ? pairingStatus : v.error);
value(w,705,pending ? L"Waiting..." : L"&Pair");
value(w,710,v.ids.empty() ? (enabled ? L"No KSwitcher found. Enable discovery on the other PC too. UDP 45883 must be allowed on your private network." : L"Discovery is disabled on this computer.") : L"Select the matching KSwitcher name and ID shown on the other computer.");
EnableWindow(GetDlgItem(w,705),enabled&&online&&!pending&&!paired);
EnableWindow(GetDlgItem(w,706),pending);
@@ -89,6 +123,7 @@ void refreshLan(HWND w,LanView &v)
if(mode==2)inputStatus=L"Mouse and keyboard control the OTHER PC. Ctrl+Alt+Esc returns here.";
else if(mode==3)inputStatus=L"This PC is controlled remotely. Local physical input takes control back.";
value(w,718,inputStatus);
value(w,722,v.snapshot.value("inputNeedsHelp",false)?L"Connection failed — &help":L"Connection &help");
}
#include "ui_lan_layout.inl"
LRESULT CALLBACK lanProc(HWND w,UINT msg,WPARAM wp,LPARAM lp)
@@ -108,6 +143,7 @@ LRESULT CALLBACK lanProc(HWND w,UINT msg,WPARAM wp,LPARAM lp)
if(HIWORD(wp)!=BN_CLICKED) return DefWindowProcW(w,msg,wp,lp);
if(!IsWindowEnabled(GetDlgItem(w,id)))return 0;
v->error.clear();
if(id==722){showLanHelp(w,*v->app);return 0;}
if(id==701) lanRequest(*v->app,{{"action","enable"},{"enabled",!v->snapshot.value("enabled",false)}});
if(id==704)
{
@@ -115,7 +151,7 @@ LRESULT CALLBACK lanProc(HWND w,UINT msg,WPARAM wp,LPARAM lp)
std::string grouped; for(size_t i=0;i<code.size();++i) {if(i&&i%4==0) grouped+='-';grouped+=code[i];}
value(w,703,wide(grouped)); SetFocus(GetDlgItem(w,703)); SendDlgItemMessageW(w,703,EM_SETSEL,0,-1);
}
if(id==705) {lanRequest(*v->app,{{"action","pair"},{"peer",peer},{"code",utf8(value(w,703))}});value(w,703,L"");}
if(id==705) lanRequest(*v->app,{{"action","pair"},{"peer",peer},{"code",utf8(value(w,703))}});
if(id==706) lanRequest(*v->app,{{"action","cancel"}});
if(id==707) lanRequest(*v->app,{{"action","forget"},{"peer",peer}});
if(id==715)
@@ -131,10 +167,10 @@ LRESULT CALLBACK lanProc(HWND w,UINT msg,WPARAM wp,LPARAM lp)
}
void showLan(HWND parent,App &app)
{
HWND w=makeWindow(app,L"KSwitcher.LAN",L"KSwitcher computers — mouse and keyboard",lanProc,740,836,0,false);
HWND w=makeWindow(app,L"KSwitcher.LAN",L"KSwitcher computers — mouse and keyboard",lanProc,740,908,0,false);
SetWindowLongPtrW(w,GWLP_HWNDPARENT,(LONG_PTR)parent);
auto v=new LanView{parent,&app};SetWindowLongPtrW(w,GWLP_USERDATA,(LONG_PTR)v);
label(w,L"Find running KSwitcher instances on your local network and pair them.",18,16,700);
label(w,L"Changes here save automatically. No need to click Save in Settings.",18,16,700);
control(w,L"STATIC",L"",0,709,18,45,700,24);
button(w,L"&Enable discovery",701,18,78,210);
label(w,L"Only enabled KSwitcher instances announce themselves.",244,82,470);
@@ -154,9 +190,10 @@ void showLan(HWND parent,App &app)
control(w,L"STATIC",L"",0,719,18,674,700,24);
button(w,L"Enable &input sharing",715,18,706,330);
button(w,L"&Stop input sharing",716,370,706,240);
control(w,L"STATIC",L"",0,718,18,744,700,28);
control(w,L"STATIC",L"Arrange the same physical layout on every PC. Pair each input source with every destination and enable input on all PCs. Clipboard goes to all paired PCs.",0,720,18,776,580,40);
button(w,L"&Close",IDCANCEL,618,792,100);
control(w,L"STATIC",L"",0,718,18,744,700,64);
control(w,L"STATIC",L"Arrange the same physical layout on every PC. Pair each input source with every destination and enable input on all PCs. Clipboard goes to all paired PCs.",0,720,18,816,580,40);
button(w,L"Connection &help",722,18,864,300);
button(w,L"&Close",IDCANCEL,618,864,100);
EnableWindow(parent,FALSE);showRequestedWindow(w);SetForegroundWindow(w);
SetTimer(w,1,1000,nullptr);
try {refreshLan(w,*v);} catch(const std::exception &e){v->error=wide(e.what());value(w,708,v->error);}
+1 -1
View File
@@ -2,7 +2,7 @@
#include "platform.h"
namespace ks
{
inline constexpr wchar_t AppVersion[] = L"0.6.62";
inline constexpr wchar_t AppVersion[] = L"0.6.63";
inline constexpr UINT UpdateMessage = WM_APP + 7;
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;