Release v0.6.59: clarify sync results and refresh downloaded settings
This commit is contained in:
1 parent
d0387904a0
commit
4d8d8d47b9
16 files changed
+107
-28
No files matched your search
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.24)
|
cmake_minimum_required(VERSION 3.24)
|
||||||
project(KSwitcher VERSION 0.6.58 LANGUAGES CXX RC)
|
project(KSwitcher VERSION 0.6.59 LANGUAGES CXX RC)
|
||||||
if(NOT WIN32)
|
if(NOT WIN32)
|
||||||
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
|
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -590,8 +590,8 @@ Nonconflicting changes synchronize immediately, and unresolved conflicts survive
|
|||||||
- **Upload local** creates the remote profile or replaces it with this PC's saved shared settings.
|
- **Upload local** creates the remote profile or replaces it with this PC's saved shared settings.
|
||||||
- **Download remote** replaces this PC's shared settings with the server profile.
|
- **Download remote** replaces this PC's shared settings with the server profile.
|
||||||
- If the server has no profile yet, choose **Upload local** to create it.
|
- If the server has no profile yet, choose **Upload local** to create it.
|
||||||
- Unsaved edits in Settings are not synchronized. Close and reopen Settings after a download;
|
- Unsaved edits in Settings are not synchronized. After a successful **Download remote**, the open Settings window refreshes automatically and keeps the selected tab. The download confirmation warns that unsaved edits will be discarded.
|
||||||
the existing stale-editor check prevents an old Settings window overwriting downloaded changes.
|
- The status distinguishes downloading, downloaded settings, no local changes and failure. HTTP 400 errors identify authentication, download or upload; field errors remain visible until the next action.
|
||||||
|
|
||||||
Once connected, the resident checks at startup, every **10 minutes**, and about two seconds after saving settings, on a separate
|
Once connected, the resident checks at startup, every **10 minutes**, and about two seconds after saving settings, on a separate
|
||||||
network thread. Errors back off for 15 minutes; the Sync button allows a manual retry. Local use
|
network thread. Errors back off for 15 minutes; the Sync button allows a manual retry. Local use
|
||||||
|
|||||||
Binary file not shown.
+6
-7
@@ -1,16 +1,15 @@
|
|||||||
# Latest Windows release
|
# Latest Windows release
|
||||||
|
|
||||||
## v0.6.58 — 2026-11-06
|
## v0.6.59 — 2026-11-09
|
||||||
|
|
||||||
## Changes
|
## Changes
|
||||||
- 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 clear download/upload progress and results, including the server version, applied changes or unchanged local settings. Preserve action errors instead of replacing them with an older status.
|
||||||
- Show saved pairing separately from actual input connectivity: connected, disabled here, offline or not connected. Background identity verification does not flash the list.
|
- Identify OpenBao HTTP 400 failures as authentication, download or write failures instead of labeling every failure as a rejected write. Failed downloads do not apply remote settings.
|
||||||
- Log TCP listener failures with the Windows error and peer connection state changes for troubleshooting.
|
- Refresh the already-open Settings window after a successful Download remote, preserving its selected tab. This also works if the Sync dialog was closed while the request was running. Confirmation explains that unsaved Settings edits will be discarded.
|
||||||
|
- Retain all v0.6.58 LAN, clipboard, screenshot, snippet and chat-profile behavior. No sync schema or compatibility change.
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
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.
|
Passed: 132 sync, 571 input/UI, 341 native, 829 chat-profile/UI, 205 LAN input, 289 LAN clipboard, 43 LAN pairing 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. Regression tests use an isolated mock OpenBao transport, not a live user account.
|
||||||
|
|
||||||
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
|
## 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.
|
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 @@
|
|||||||
16d8138c63442931bf947707b28820d9b89943169ac9f25db3cb50d23c0ff319 KSwitcher-win-x64.zip
|
f0d7d15534db21c17b113a25cc07d59a68fa151ecf995ac360ac476751171a9a KSwitcher-win-x64.zip
|
||||||
+3
-3
@@ -9,8 +9,8 @@ IDC_SCREENSHOT_PENCIL CURSOR "icons/lucide/pencil.cur"
|
|||||||
1 RT_MANIFEST "app.manifest"
|
1 RT_MANIFEST "app.manifest"
|
||||||
201 RCDATA "update.ps1"
|
201 RCDATA "update.ps1"
|
||||||
1 VERSIONINFO
|
1 VERSIONINFO
|
||||||
FILEVERSION 0,6,58,0
|
FILEVERSION 0,6,59,0
|
||||||
PRODUCTVERSION 0,6,58,0
|
PRODUCTVERSION 0,6,59,0
|
||||||
FILEOS VOS_NT_WINDOWS32
|
FILEOS VOS_NT_WINDOWS32
|
||||||
FILETYPE VFT_APP
|
FILETYPE VFT_APP
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -20,7 +20,7 @@ BEGIN
|
|||||||
BEGIN
|
BEGIN
|
||||||
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
|
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
|
||||||
VALUE "ProductName", "KSwitcher"
|
VALUE "ProductName", "KSwitcher"
|
||||||
VALUE "FileVersion", "0.6.58"
|
VALUE "FileVersion", "0.6.59"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
+16
-2
@@ -13,7 +13,7 @@ std::wstring App::syncCommand(int action, const std::vector<SyncChoice> &choices
|
|||||||
validateSyncConfig(c);
|
validateSyncConfig(c);
|
||||||
if (action == 4 && (c != conflictConnection || choices.empty()))
|
if (action == 4 && (c != conflictConnection || choices.empty()))
|
||||||
throw std::runtime_error("Connection changed or no choices selected. Sync again.");
|
throw std::runtime_error("Connection changed or no choices selected. Sync again.");
|
||||||
if (busy) return L"Wait for the current text operation to finish.";
|
if (busy) return syncStatus = L"Sync did not start. Wait for the current text operation to finish, then try again.";
|
||||||
syncRetryAfter = 0; // Explicit retries may bypass the automatic error backoff.
|
syncRetryAfter = 0; // Explicit retries may bypass the automatic error backoff.
|
||||||
if (syncWorker.joinable()) syncWorker.join();
|
if (syncWorker.joinable()) syncWorker.join();
|
||||||
syncConnection = c;
|
syncConnection = c;
|
||||||
@@ -22,7 +22,8 @@ std::wstring App::syncCommand(int action, const std::vector<SyncChoice> &choices
|
|||||||
auto availableLayouts = layouts;
|
auto availableLayouts = layouts;
|
||||||
syncRunning = true;
|
syncRunning = true;
|
||||||
syncDone = false;
|
syncDone = false;
|
||||||
syncStatus = L"Connecting to OpenBao...";
|
syncStatus = action == 2 ? L"Downloading remote settings from OpenBao..." :
|
||||||
|
action == 3 ? L"Uploading local settings to OpenBao..." : L"Connecting to OpenBao...";
|
||||||
nextSync = GetTickCount64() + SyncIntervalMs;
|
nextSync = GetTickCount64() + SyncIntervalMs;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -42,6 +43,9 @@ std::wstring App::syncCommand(int action, const std::vector<SyncChoice> &choices
|
|||||||
}
|
}
|
||||||
catch (const nlohmann::json::exception &) { result.status = L"Invalid sync data or server response."; }
|
catch (const nlohmann::json::exception &) { result.status = L"Invalid sync data or server response."; }
|
||||||
catch (const std::exception &e) { result.status = wide(e.what()); }
|
catch (const std::exception &e) { result.status = wide(e.what()); }
|
||||||
|
if (!result.success)
|
||||||
|
result.status = std::wstring(action == 2 ? L"Download failed. Local settings were not changed. " :
|
||||||
|
action == 3 ? L"Upload failed. " : L"Synchronization failed. ") + result.status;
|
||||||
syncResult = std::move(result);
|
syncResult = std::move(result);
|
||||||
syncDone.store(true);
|
syncDone.store(true);
|
||||||
});
|
});
|
||||||
@@ -76,8 +80,18 @@ void App::pollSync()
|
|||||||
{
|
{
|
||||||
atomicWrite(data / L"settings-before-sync.bin", protect(snapshot));
|
atomicWrite(data / L"settings-before-sync.bin", protect(snapshot));
|
||||||
applySettings(next);
|
applySettings(next);
|
||||||
|
syncStatus += L" Changes applied on this PC; local backup saved.";
|
||||||
}
|
}
|
||||||
|
else syncStatus += result.conflicts.empty() ? L" This PC's settings are already up to date; no local changes needed." :
|
||||||
|
L" No local settings were changed; conflicts still need your choice.";
|
||||||
saveSyncBase(data, syncConnection, result.base.empty() ? result.settings : result.base);
|
saveSyncBase(data, syncConnection, result.base.empty() ? result.settings : result.base);
|
||||||
|
if (result.downloaded)
|
||||||
|
{
|
||||||
|
if (settingsWindow) SendMessageW(settingsWindow, ConfigSettingsDownloaded, 0, 0);
|
||||||
|
if (auto config = FindWindowW(L"KSwitcher.Config", nullptr);
|
||||||
|
config && configurationPeer(config, L"KSwitcher.Config.exe"))
|
||||||
|
PostMessageW(config, ConfigSettingsDownloaded, 0, 0);
|
||||||
|
}
|
||||||
pendingSyncConflicts = std::move(result.conflicts);
|
pendingSyncConflicts = std::move(result.conflicts);
|
||||||
conflictConnection = syncConnection;
|
conflictConnection = syncConnection;
|
||||||
SYSTEMTIME time{}; GetLocalTime(&time);
|
SYSTEMTIME time{}; GetLocalTime(&time);
|
||||||
|
|||||||
@@ -52,6 +52,19 @@ LRESULT CALLBACK controller(HWND window, UINT message, WPARAM wp, LPARAM lp)
|
|||||||
}
|
}
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
if (message == ConfigSettingsDownloaded)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
app->settings = decodeSettings(request(*app, ConfigRead), app->layouts);
|
||||||
|
if (app->settingsWindow) SendMessageW(app->settingsWindow, ConfigSettingsDownloaded, 0, 0);
|
||||||
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
|
{
|
||||||
|
MessageBoxW(app->settingsWindow, wide(e.what()).c_str(), L"Unable to refresh downloaded settings", MB_OK | MB_ICONERROR);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
if (message == WM_APP + 20)
|
if (message == WM_APP + 20)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "app.h"
|
#include "app.h"
|
||||||
namespace ks
|
namespace ks
|
||||||
{
|
{
|
||||||
|
inline constexpr UINT ConfigSettingsDownloaded = WM_APP + 24;
|
||||||
inline constexpr ULONG_PTR ConfigRead = 0x4b530100, ConfigSave = ConfigRead + 1,
|
inline constexpr ULONG_PTR ConfigRead = 0x4b530100, ConfigSave = ConfigRead + 1,
|
||||||
ConfigLayouts = ConfigRead + 2, ConfigClearHistory = ConfigRead + 3,
|
ConfigLayouts = ConfigRead + 2, ConfigClearHistory = ConfigRead + 3,
|
||||||
ConfigCapture = ConfigRead + 4, ConfigLogs = ConfigRead + 5,
|
ConfigCapture = ConfigRead + 4, ConfigLogs = ConfigRead + 5,
|
||||||
|
|||||||
+14
-6
@@ -39,7 +39,7 @@ class OpenBao final : public SyncProvider
|
|||||||
SyncConfig config;
|
SyncConfig config;
|
||||||
SyncTransport transport;
|
SyncTransport transport;
|
||||||
std::wstring token;
|
std::wstring token;
|
||||||
Json response(unsigned status, const Bytes &result, bool missing)
|
Json response(unsigned status, const Bytes &result, bool missing, const std::wstring &path, const wchar_t *method)
|
||||||
{
|
{
|
||||||
if (result.size() > Limit) throw std::runtime_error("OpenBao response is too large.");
|
if (result.size() > Limit) throw std::runtime_error("OpenBao response is too large.");
|
||||||
if (status == 404 && missing)
|
if (status == 404 && missing)
|
||||||
@@ -50,7 +50,14 @@ class OpenBao final : public SyncProvider
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
if (status == 401 || status == 403) throw std::runtime_error("OpenBao denied access. Check login and profile policy.");
|
if (status == 401 || status == 403) throw std::runtime_error("OpenBao denied access. Check login and profile policy.");
|
||||||
if (status == 400) throw std::runtime_error("OpenBao rejected the write or its version changed. Sync again; check KV v2 configuration.");
|
if (status == 400)
|
||||||
|
{
|
||||||
|
if (path.starts_with(L"/v1/auth/"))
|
||||||
|
throw std::runtime_error("OpenBao rejected the authentication request (HTTP 400). Check username, password and login mount.");
|
||||||
|
if (std::wstring_view(method) == L"GET")
|
||||||
|
throw std::runtime_error("OpenBao rejected the download request (HTTP 400). Check KV v2 mount and profile path.");
|
||||||
|
throw std::runtime_error("OpenBao rejected the write (HTTP 400) or its version changed. Sync again; check KV v2 configuration.");
|
||||||
|
}
|
||||||
if (status < 200 || status >= 300) throw std::runtime_error("OpenBao HTTP error " + std::to_string(status) + ".");
|
if (status < 200 || status >= 300) throw std::runtime_error("OpenBao HTTP error " + std::to_string(status) + ".");
|
||||||
return result.empty() ? Json::object() : parse(result);
|
return result.empty() ? Json::object() : parse(result);
|
||||||
}
|
}
|
||||||
@@ -59,7 +66,7 @@ class OpenBao final : public SyncProvider
|
|||||||
if (transport)
|
if (transport)
|
||||||
{
|
{
|
||||||
auto reply = transport(method, path, token, body ? bytes(*body) : Bytes{});
|
auto reply = transport(method, path, token, body ? bytes(*body) : Bytes{});
|
||||||
return response(reply.status, reply.body, missing);
|
return response(reply.status, reply.body, missing, path, method);
|
||||||
}
|
}
|
||||||
URL_COMPONENTS url{sizeof(url)};
|
URL_COMPONENTS url{sizeof(url)};
|
||||||
url.dwHostNameLength = url.dwUrlPathLength = url.dwExtraInfoLength = (DWORD)-1;
|
url.dwHostNameLength = url.dwUrlPathLength = url.dwExtraInfoLength = (DWORD)-1;
|
||||||
@@ -98,7 +105,7 @@ class OpenBao final : public SyncProvider
|
|||||||
if (result.size() + received > Limit) throw std::runtime_error("OpenBao response is too large.");
|
if (result.size() + received > Limit) throw std::runtime_error("OpenBao response is too large.");
|
||||||
result.insert(result.end(), chunk, chunk + received);
|
result.insert(result.end(), chunk, chunk + received);
|
||||||
}
|
}
|
||||||
return response(status, result, missing);
|
return response(status, result, missing, path, method);
|
||||||
}
|
}
|
||||||
public:
|
public:
|
||||||
explicit OpenBao(const SyncConfig &c, SyncTransport t) : config(c), transport(std::move(t))
|
explicit OpenBao(const SyncConfig &c, SyncTransport t) : config(c), transport(std::move(t))
|
||||||
@@ -306,9 +313,10 @@ SyncResult synchronize(SyncProvider &provider, const Bytes &base, const Bytes &l
|
|||||||
auto revision = remote.revision;
|
auto revision = remote.revision;
|
||||||
if (parse(merged.remote) != parse(remote.settings) && action != SyncAction::Download) revision = provider.write(merged.remote, revision);
|
if (parse(merged.remote) != parse(remote.settings) && action != SyncAction::Download) revision = provider.write(merged.remote, revision);
|
||||||
return {true, merged.local, revision,
|
return {true, merged.local, revision,
|
||||||
merged.conflicts.empty() ? L"Synchronized. Server version " + std::to_wstring(revision) + L"." :
|
merged.conflicts.empty() ? std::wstring(action == SyncAction::Download ? L"Downloaded remote settings. Server version " :
|
||||||
|
action == SyncAction::Upload ? L"Uploaded local settings. Server version " : L"Synchronized. Server version ") + std::to_wstring(revision) + L"." :
|
||||||
L"Other changes synchronized. Resolve " + std::to_wstring(merged.conflicts.size()) + L" conflicting setting(s).",
|
L"Other changes synchronized. Resolve " + std::to_wstring(merged.conflicts.size()) + L" conflicting setting(s).",
|
||||||
merged.base, merged.conflicts};
|
merged.base, merged.conflicts, action == SyncAction::Download};
|
||||||
}
|
}
|
||||||
Bytes encodeSyncConflicts(const std::vector<SyncConflict> &conflicts)
|
Bytes encodeSyncConflicts(const std::vector<SyncConflict> &conflicts)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ struct SyncResult
|
|||||||
std::wstring status;
|
std::wstring status;
|
||||||
Bytes base;
|
Bytes base;
|
||||||
std::vector<SyncConflict> conflicts;
|
std::vector<SyncConflict> conflicts;
|
||||||
|
bool downloaded{};
|
||||||
};
|
};
|
||||||
SyncResult synchronize(SyncProvider &, const Bytes &base, const Bytes &local, SyncAction,
|
SyncResult synchronize(SyncProvider &, const Bytes &base, const Bytes &local, SyncAction,
|
||||||
const std::function<void(const Bytes &)> &validate = {}, const std::vector<SyncChoice> &choices = {},
|
const std::function<void(const Bytes &)> &validate = {}, const std::vector<SyncChoice> &choices = {},
|
||||||
|
|||||||
@@ -1688,6 +1688,10 @@ LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
{
|
{
|
||||||
switch (msg)
|
switch (msg)
|
||||||
{
|
{
|
||||||
|
case ConfigSettingsDownloaded:
|
||||||
|
v->draft = v->app->settings;
|
||||||
|
fillSettings(w, v);
|
||||||
|
return 0;
|
||||||
case WM_ENABLE:
|
case WM_ENABLE:
|
||||||
if (wp) updateSettingsState(w, v);
|
if (wp) updateSettingsState(w, v);
|
||||||
break;
|
break;
|
||||||
|
|||||||
+9
-4
@@ -1,5 +1,5 @@
|
|||||||
#include "ui_sync_conflicts.inl"
|
#include "ui_sync_conflicts.inl"
|
||||||
struct SyncView { HWND parent; App *app; };
|
struct SyncView { HWND parent; App *app; bool actionError{}; };
|
||||||
void updateSyncControls(HWND w, App &app)
|
void updateSyncControls(HWND w, App &app)
|
||||||
{
|
{
|
||||||
bool enabled = checked(w, 601);
|
bool enabled = checked(w, 601);
|
||||||
@@ -17,7 +17,8 @@ void updateSyncControls(HWND w, App &app)
|
|||||||
status = L"Configure the connection, then click Save / Sign in / Sync to enable synchronization.";
|
status = L"Configure the connection, then click Save / Sign in / Sync to enable synchronization.";
|
||||||
else
|
else
|
||||||
status = app.syncCommand();
|
status = app.syncCommand();
|
||||||
if (value(w, 608) != status) value(w, 608, status);
|
auto *view = reinterpret_cast<SyncView *>(GetWindowLongPtrW(w, GWLP_USERDATA));
|
||||||
|
if ((!view || !view->actionError) && value(w, 608) != status) value(w, 608, status);
|
||||||
}
|
}
|
||||||
SyncConfig syncFields(HWND w)
|
SyncConfig syncFields(HWND w)
|
||||||
{
|
{
|
||||||
@@ -47,6 +48,7 @@ LRESULT CALLBACK syncProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
int id = LOWORD(wp);
|
int id = LOWORD(wp);
|
||||||
if (id == 601 && HIWORD(wp) == BN_CLICKED)
|
if (id == 601 && HIWORD(wp) == BN_CLICKED)
|
||||||
{
|
{
|
||||||
|
v->actionError = false;
|
||||||
updateSyncControls(w, *v->app);
|
updateSyncControls(w, *v->app);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -56,6 +58,7 @@ LRESULT CALLBACK syncProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
if (id == IDCANCEL) { DestroyWindow(w); return 0; }
|
if (id == IDCANCEL) { DestroyWindow(w); return 0; }
|
||||||
if (id == 609)
|
if (id == 609)
|
||||||
{
|
{
|
||||||
|
v->actionError = false;
|
||||||
auto c = loadSyncConfig(v->app->data);
|
auto c = loadSyncConfig(v->app->data);
|
||||||
c.enabled = false;
|
c.enabled = false;
|
||||||
saveSyncConfig(v->app->data, c);
|
saveSyncConfig(v->app->data, c);
|
||||||
@@ -69,6 +72,7 @@ LRESULT CALLBACK syncProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
}
|
}
|
||||||
if (id == IDOK || id == 610 || id == 611)
|
if (id == IDOK || id == 610 || id == 611)
|
||||||
{
|
{
|
||||||
|
v->actionError = false;
|
||||||
if (!checked(w, 601))
|
if (!checked(w, 601))
|
||||||
{
|
{
|
||||||
if (id != IDOK) return 0;
|
if (id != IDOK) return 0;
|
||||||
@@ -80,7 +84,7 @@ LRESULT CALLBACK syncProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (id != IDOK && MessageBoxW(w, id == 610 ?
|
if (id != IDOK && MessageBoxW(w, id == 610 ?
|
||||||
L"Replace this PC's shared settings with the remote profile? A local backup will be saved. Unsaved edits in Settings are not included." :
|
L"Replace this PC's shared settings with the remote profile? A local backup will be saved. The open Settings window will refresh automatically; its unsaved edits will be discarded." :
|
||||||
L"Replace the remote profile with this PC's saved settings? OpenBao retains older versions according to its retention policy. Unsaved edits are not included.",
|
L"Replace the remote profile with this PC's saved settings? OpenBao retains older versions according to its retention policy. Unsaved edits are not included.",
|
||||||
L"Resolve synchronization", MB_YESNO | MB_ICONWARNING) != IDYES) return 0;
|
L"Resolve synchronization", MB_YESNO | MB_ICONWARNING) != IDYES) return 0;
|
||||||
auto c = syncFields(w);
|
auto c = syncFields(w);
|
||||||
@@ -100,6 +104,7 @@ LRESULT CALLBACK syncProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
}
|
}
|
||||||
catch (const std::exception &e)
|
catch (const std::exception &e)
|
||||||
{
|
{
|
||||||
|
v->actionError = true; // Keep this action's error visible across status polling.
|
||||||
// Never display parser exceptions containing connection file contents.
|
// Never display parser exceptions containing connection file contents.
|
||||||
value(w, 608, L"Unable to complete sync action. Check connection fields and saved configuration.");
|
value(w, 608, L"Unable to complete sync action. Check connection fields and saved configuration.");
|
||||||
if (dynamic_cast<const std::runtime_error *>(&e))
|
if (dynamic_cast<const std::runtime_error *>(&e))
|
||||||
@@ -130,7 +135,7 @@ void showSyncOptions(HWND parent, App &app)
|
|||||||
label(w, L"KV v2 &mount", 18, 210, 160); edit(w, 605, 190, 206, 450); value(w, 605, c.mount);
|
label(w, L"KV v2 &mount", 18, 210, 160); edit(w, 605, 190, 206, 450); value(w, 605, c.mount);
|
||||||
label(w, L"Profile pa&th", 18, 250, 160); edit(w, 606, 190, 246, 450); value(w, 606, c.path);
|
label(w, L"Profile pa&th", 18, 250, 160); edit(w, 606, 190, 246, 450); value(w, 606, c.path);
|
||||||
label(w, L"&Login mount", 18, 290, 160); edit(w, 607, 190, 286, 450); value(w, 607, c.authMount);
|
label(w, L"&Login mount", 18, 290, 160); edit(w, 607, 190, 286, 450); value(w, 607, c.authMount);
|
||||||
label(w, L"Sync uses saved settings. Close and reopen Settings after downloading changes.", 18, 330, 622);
|
label(w, L"Sync uses saved settings. Download remote refreshes the open Settings window.", 18, 330, 622);
|
||||||
edit(w, 608, 18, 362, 622, 76, ES_MULTILINE | ES_READONLY | WS_VSCROLL | ES_AUTOVSCROLL);
|
edit(w, 608, 18, 362, 622, 76, ES_MULTILINE | ES_READONLY | WS_VSCROLL | ES_AUTOVSCROLL);
|
||||||
value(w, 608, app.syncCommand());
|
value(w, 608, app.syncCommand());
|
||||||
checkbox(w, L"&Keep this PC's layouts and layout shortcuts (sync other settings)", 613, 18, 446, 622);
|
checkbox(w, L"&Keep this PC's layouts and layout shortcuts (sync other settings)", 613, 18, 446, 622);
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
namespace ks
|
namespace ks
|
||||||
{
|
{
|
||||||
inline constexpr wchar_t AppVersion[] = L"0.6.58";
|
inline constexpr wchar_t AppVersion[] = L"0.6.59";
|
||||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||||
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||||
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "app.h"
|
#include "app.h"
|
||||||
|
#include "configuration.h"
|
||||||
#include "ui_scroll.h"
|
#include "ui_scroll.h"
|
||||||
#include "ui_geometry.h"
|
#include "ui_geometry.h"
|
||||||
#include "screenshot.h"
|
#include "screenshot.h"
|
||||||
@@ -752,6 +753,21 @@ int main(int argc, char **argv) {
|
|||||||
require(fitted.left==-600&&fitted.top==680&&fitted.right==0,"negative-coordinate monitor supported");
|
require(fitted.left==-600&&fitted.top==680&&fitted.right==0,"negative-coordinate monitor supported");
|
||||||
ks::showSettings(app,false);
|
ks::showSettings(app,false);
|
||||||
{
|
{
|
||||||
|
auto window = app.settingsWindow;
|
||||||
|
auto previous = app.settings;
|
||||||
|
TabCtrl_SetCurSel(GetDlgItem(window,260),3);
|
||||||
|
app.settings.awake = !previous.awake;
|
||||||
|
app.settings.maxItems = previous.maxItems + 1;
|
||||||
|
SendMessageW(window,ks::ConfigSettingsDownloaded,0,0);
|
||||||
|
wchar_t count[32]{};
|
||||||
|
GetDlgItemTextW(window,240,count,32);
|
||||||
|
require(app.settingsWindow == window &&
|
||||||
|
SendDlgItemMessageW(window,205,BM_GETCHECK,0,0) == (app.settings.awake ? BST_CHECKED : BST_UNCHECKED) &&
|
||||||
|
std::stoi(count) == app.settings.maxItems && TabCtrl_GetCurSel(GetDlgItem(window,260)) == 3,
|
||||||
|
"download refreshes existing settings controls while preserving window and active tab");
|
||||||
|
app.settings = previous;
|
||||||
|
SendMessageW(window,ks::ConfigSettingsDownloaded,0,0);
|
||||||
|
TabCtrl_SetCurSel(GetDlgItem(window,260),0);
|
||||||
auto timer = SetTimer(nullptr, 0, 10, answerSettingsClose);
|
auto timer = SetTimer(nullptr, 0, 10, answerSettingsClose);
|
||||||
require(timer != 0, "settings close prompt responder");
|
require(timer != 0, "settings close prompt responder");
|
||||||
SendMessageW(app.settingsWindow, WM_CLOSE, 0, 0);
|
SendMessageW(app.settingsWindow, WM_CLOSE, 0, 0);
|
||||||
|
|||||||
+19
-1
@@ -298,6 +298,9 @@ int main()
|
|||||||
require(provider.writes == 1 && result.revision == 1, "no-op avoids versions");
|
require(provider.writes == 1 && result.revision == 1, "no-op avoids versions");
|
||||||
result = synchronize(provider, base, bytes(left), SyncAction::Download);
|
result = synchronize(provider, base, bytes(left), SyncAction::Download);
|
||||||
require(parse(result.settings) == parse(base) && provider.writes == 1, "download never writes");
|
require(parse(result.settings) == parse(base) && provider.writes == 1, "download never writes");
|
||||||
|
require(result.downloaded, "explicit download requests a settings window refresh");
|
||||||
|
require(result.status.find(L"Downloaded remote settings. Server version 1.") != std::wstring::npos,
|
||||||
|
"explicit download reports its result and server version");
|
||||||
provider.race = true;
|
provider.race = true;
|
||||||
rejects([&] { synchronize(provider, base, bytes(left), SyncAction::Upload); }, "racing writer protected");
|
rejects([&] { synchronize(provider, base, bytes(left), SyncAction::Upload); }, "racing writer protected");
|
||||||
require(provider.writes == 1 && provider.remote.settings == base, "CAS failure preserves remote");
|
require(provider.writes == 1 && provider.remote.settings == base, "CAS failure preserves remote");
|
||||||
@@ -395,10 +398,25 @@ int main()
|
|||||||
rejects([&] { bao->read(); }, "permission denied is not absent");
|
rejects([&] { bao->read(); }, "permission denied is not absent");
|
||||||
readStatus = 302;
|
readStatus = 302;
|
||||||
rejects([&] { bao->read(); }, "redirect response rejected");
|
rejects([&] { bao->read(); }, "redirect response rejected");
|
||||||
|
readStatus = 400;
|
||||||
|
try { bao->read(); require(false, "download HTTP 400 must fail"); }
|
||||||
|
catch (const std::runtime_error &e) {
|
||||||
|
require(std::string(e.what()).find("download request (HTTP 400)") != std::string::npos,
|
||||||
|
"download failure is not mislabeled as a write failure");
|
||||||
|
}
|
||||||
writeStatus = 400;
|
writeStatus = 400;
|
||||||
rejects([&] { bao->write(base, 7); }, "server CAS refusal propagated");
|
rejects([&] { bao->write(base, 7); }, "server CAS refusal propagated");
|
||||||
}
|
}
|
||||||
require(logins == 1 && revokes == 1 && reads == 5 && writes == 2, "temporary login lifecycle");
|
require(logins == 1 && revokes == 1 && reads == 6 && writes == 2, "temporary login lifecycle");
|
||||||
|
try {
|
||||||
|
makeSyncProvider(config, [](const std::wstring &, const std::wstring &, const std::wstring &, const Bytes &) {
|
||||||
|
return SyncHttpResponse{400, {}};
|
||||||
|
});
|
||||||
|
require(false, "login HTTP 400 must fail");
|
||||||
|
} catch (const std::runtime_error &e) {
|
||||||
|
require(std::string(e.what()).find("authentication request (HTTP 400)") != std::string::npos,
|
||||||
|
"login failure identifies authentication instead of a write");
|
||||||
|
}
|
||||||
forgetSyncPassword(config); forgetSyncPassword(config);
|
forgetSyncPassword(config); forgetSyncPassword(config);
|
||||||
++checks;
|
++checks;
|
||||||
std::filesystem::remove_all(root);
|
std::filesystem::remove_all(root);
|
||||||
|
|||||||
Reference in new issue
Block a user