Release v0.2.0 with incident diagnostics and in-app log viewer
This commit is contained in:
+131
-15
@@ -125,7 +125,10 @@ class Transaction
|
||||
write(textClip(L"\x2063KSwitcher-" + std::to_wstring(GetTickCount64())));
|
||||
DWORD before = expected;
|
||||
if (!valid() || !chord(L'C', MOD_CONTROL))
|
||||
{
|
||||
diagnosticEvent(L"Copy failed: context changed or simulated Ctrl+C blocked");
|
||||
return {};
|
||||
}
|
||||
ULONGLONG deadline = GetTickCount64() + 2000;
|
||||
DWORD changed = before;
|
||||
ULONGLONG stable = 0;
|
||||
@@ -146,7 +149,11 @@ class Transaction
|
||||
Sleep(10);
|
||||
}
|
||||
if (changed == before || !valid())
|
||||
{
|
||||
diagnosticEvent(changed == before ? L"Copy failed: clipboard timeout (2000 ms)" :
|
||||
L"Copy failed: context changed");
|
||||
return {};
|
||||
}
|
||||
if ((GetClipboardOwner() ? !ownsClipboard(target) : changedAgain) ||
|
||||
changed != GetClipboardSequenceNumber())
|
||||
throw std::runtime_error("Operation cancelled: copy did not belong to the target application");
|
||||
@@ -159,13 +166,19 @@ class Transaction
|
||||
{
|
||||
if (f.name == L"VisualStudioEditorOperationsLineCutCopyClipboardTag" ||
|
||||
f.name == L"MSDEVLineSelect")
|
||||
{
|
||||
diagnosticEvent(L"Copy rejected: editor copied line without selection");
|
||||
return {};
|
||||
}
|
||||
if (f.name == L"vscode-editor-data")
|
||||
{
|
||||
std::string json(f.data.begin(), f.data.end());
|
||||
if (json.find("\"isFromEmptySelection\":true") != json.npos ||
|
||||
json.find("\"isFromEmptySelection\": true") != json.npos)
|
||||
{
|
||||
diagnosticEvent(L"Copy rejected: editor reported empty selection");
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
return text;
|
||||
@@ -188,7 +201,7 @@ class Transaction
|
||||
auto text = copy();
|
||||
if (valid() && matches(text))
|
||||
{
|
||||
log(L"Selection: verified word boundary");
|
||||
diagnosticEvent(L"Selection: verified word boundary");
|
||||
return text;
|
||||
}
|
||||
if (!valid() || !chord(VK_RIGHT))
|
||||
@@ -230,6 +243,7 @@ App::App(HINSTANCE h, bool test, const std::filesystem::path &dataRoot) : instan
|
||||
settings = loadSettings(data / L"missing-defaults.json", layouts);
|
||||
registrationWarning = L"Settings were not loaded: " + wide(e.what());
|
||||
}
|
||||
setLoggingEnabled(settings.writeLog);
|
||||
try
|
||||
{
|
||||
history = std::make_unique<History>(data / L"history");
|
||||
@@ -349,6 +363,7 @@ void App::applySettings(const Settings &s)
|
||||
{
|
||||
std::lock_guard lock(stateMutex);
|
||||
settings = s;
|
||||
setLoggingEnabled(s.writeLog);
|
||||
run.reset();
|
||||
last.reset();
|
||||
++inputVersion;
|
||||
@@ -587,9 +602,36 @@ void App::dispatch(int command)
|
||||
}
|
||||
void App::transform(int command, int snippet)
|
||||
{
|
||||
if (busy.exchange(true))
|
||||
return;
|
||||
const auto attempt = ++transformAttempt;
|
||||
const auto requestedAt = GetTickCount64();
|
||||
HWND target = GetForegroundWindow();
|
||||
const bool manual = command != AutoCorrect && snippet < 0;
|
||||
if (manual)
|
||||
{
|
||||
const auto currentVersion = inputVersion.load();
|
||||
repeatAttempts = target == previousAttemptWindow && command == previousAttemptCommand &&
|
||||
currentVersion == previousAttemptVersion &&
|
||||
requestedAt - previousAttemptAt < 10000
|
||||
? repeatAttempts + 1 : 1;
|
||||
previousAttemptWindow = target;
|
||||
previousAttemptCommand = command;
|
||||
previousAttemptVersion = currentVersion;
|
||||
previousAttemptAt = requestedAt;
|
||||
diagnosticEvent(L"Transform request id=" + std::to_wstring(attempt) + L" command=" +
|
||||
std::to_wstring(command) + L" process=" + processName(target) +
|
||||
L" window=" + std::to_wstring(reinterpret_cast<uintptr_t>(target)) +
|
||||
L" version=" + std::to_wstring(currentVersion) +
|
||||
L" repeat=" + std::to_wstring(repeatAttempts));
|
||||
if (repeatAttempts >= 2)
|
||||
diagnosticEvent(L"Transform suspicion id=" + std::to_wstring(attempt) +
|
||||
L" reason=repeated-command-without-input (may be intentional cycling)");
|
||||
}
|
||||
if (busy.exchange(true))
|
||||
{
|
||||
if (manual)
|
||||
diagnosticEvent(L"Transform outcome id=" + std::to_wstring(attempt) + L" reason=busy", true);
|
||||
return;
|
||||
}
|
||||
Run recent;
|
||||
std::optional<LastTransform> previous;
|
||||
Settings config;
|
||||
@@ -604,7 +646,8 @@ void App::transform(int command, int snippet)
|
||||
version = inputVersion;
|
||||
}
|
||||
enqueue(
|
||||
[this, target, recent, previous, config, available, version, command, snippet]
|
||||
[this, target, recent, previous, config, available, version, command, snippet, attempt, manual, requestedAt,
|
||||
suspicious = manual && repeatAttempts >= 2]
|
||||
{
|
||||
struct BusyReset
|
||||
{
|
||||
@@ -615,6 +658,48 @@ void App::transform(int command, int snippet)
|
||||
}
|
||||
} reset{busy};
|
||||
auto start = GetTickCount64();
|
||||
std::wstring reason = L"cancelled-before-selection";
|
||||
std::wstring text, result, expected;
|
||||
int hint = -1, to = command >= Direct ? command - Direct : -1;
|
||||
if (manual)
|
||||
{
|
||||
std::wstring layoutIds;
|
||||
for (const auto &layout : available)
|
||||
{
|
||||
layoutIds += layout.id + L",";
|
||||
if (layout.handle == recent.layout)
|
||||
for (wchar_t key : recent.keys)
|
||||
expected += key == L' ' ? L' ' : layout.at(physicalLayout().code(key));
|
||||
}
|
||||
diagnosticEvent(L"Transform context id=" + std::to_wstring(attempt) +
|
||||
L" layouts=" + layoutIds + L" physicalKeys=" + diagnosticText(recent.keys) +
|
||||
L" trackedExpected=" + diagnosticText(expected));
|
||||
}
|
||||
struct Audit
|
||||
{
|
||||
App &app;
|
||||
HWND target;
|
||||
uint64_t version, attempt;
|
||||
ULONGLONG start;
|
||||
bool enabled;
|
||||
bool suspicious;
|
||||
std::wstring &reason;
|
||||
std::wstring &text, &result, &expected;
|
||||
int &hint, &to;
|
||||
~Audit()
|
||||
{
|
||||
if (enabled)
|
||||
diagnosticEvent(L"Transform outcome id=" + std::to_wstring(attempt) + L" reason=" + reason +
|
||||
L" foregroundSame=" + std::to_wstring(GetForegroundWindow() == target) +
|
||||
L" inputSame=" + std::to_wstring(app.inputVersion.load() == version) +
|
||||
L" elapsedMs=" + std::to_wstring(GetTickCount64() - start) +
|
||||
L" source=" + std::to_wstring(hint) + L" target=" + std::to_wstring(to) +
|
||||
L" expected=" + diagnosticText(expected) + L" copied=" + diagnosticText(text) +
|
||||
L" result=" + diagnosticText(result),
|
||||
suspicious || (reason != L"paste-sent-unverified" && reason != L"layout-only" &&
|
||||
reason != L"unchanged-expected"));
|
||||
}
|
||||
} audit{*this, target, version, attempt, requestedAt, manual, suspicious, reason, text, result, expected, hint, to};
|
||||
try
|
||||
{
|
||||
auto valid = [&]
|
||||
@@ -627,16 +712,16 @@ void App::transform(int command, int snippet)
|
||||
releaseModifiers();
|
||||
if (!valid())
|
||||
return;
|
||||
int hint = -1;
|
||||
for (int i = 0; i < (int)available.size(); ++i)
|
||||
if (available[i].handle == windowLayout(target))
|
||||
hint = i;
|
||||
int to = command >= Direct ? command - Direct : -1;
|
||||
reason = L"invalid-target-layout";
|
||||
if (to >= (int)available.size())
|
||||
return;
|
||||
if (to >= 0 && !available[to].convertible)
|
||||
{
|
||||
switchWindowLayout(target, available[to].handle, valid);
|
||||
reason = switchWindowLayout(target, available[to].handle, valid)
|
||||
? L"layout-only" : L"layout-switch-failed";
|
||||
return;
|
||||
}
|
||||
if (command == AutoCorrect)
|
||||
@@ -654,12 +739,16 @@ void App::transform(int command, int snippet)
|
||||
!autoCorrectionAllowed(target, config) || !valid())
|
||||
return;
|
||||
}
|
||||
reason = L"clipboard-snapshot";
|
||||
Transaction tx(*this, target, version);
|
||||
log(L"Transform target=" + processName(target) + L" command=" + std::to_wstring(command) +
|
||||
reason = L"selection-unavailable";
|
||||
diagnosticEvent(L"Transform target=" + processName(target) + L" command=" + std::to_wstring(command) +
|
||||
L" recent=" + std::to_wstring(config.recent) + L" keys=" +
|
||||
std::to_wstring(recent.keys.size()) + L" sameWindow=" +
|
||||
std::to_wstring(recent.window == target));
|
||||
std::wstring text, result;
|
||||
std::to_wstring(recent.window == target) + L" overflow=" +
|
||||
std::to_wstring(recent.overflow) + L" ageMs=" +
|
||||
std::to_wstring(recent.at ? start - recent.at : 0) +
|
||||
L" id=" + std::to_wstring(attempt));
|
||||
bool automatic = false;
|
||||
if (command == AutoCorrect)
|
||||
{
|
||||
@@ -692,6 +781,9 @@ void App::transform(int command, int snippet)
|
||||
}
|
||||
else if (command == Undo)
|
||||
{
|
||||
if (previous)
|
||||
expected = previous->result;
|
||||
reason = L"undo-unavailable";
|
||||
if (!previous || previous->window != target || previous->version != version ||
|
||||
GetTickCount64() - previous->at > 120000)
|
||||
return;
|
||||
@@ -700,6 +792,7 @@ void App::transform(int command, int snippet)
|
||||
{ return newlines(candidate) == newlines(previous->result); });
|
||||
if (newlines(text) != newlines(previous->result))
|
||||
{
|
||||
reason = L"undo-verification-mismatch";
|
||||
if (tx.valid())
|
||||
chord(VK_RIGHT);
|
||||
return;
|
||||
@@ -738,10 +831,11 @@ void App::transform(int command, int snippet)
|
||||
tx.copyRange(recent.keys.size(), recent.keys, [&](const std::wstring &candidate)
|
||||
{ return recent.match(candidate, available, physicalLayout()) >= 0; });
|
||||
hint = recent.match(text, available, physicalLayout());
|
||||
log(L"Recent verification copied=" + std::to_wstring(text.size()) + L" keys=" +
|
||||
diagnosticEvent(L"Recent verification copied=" + std::to_wstring(text.size()) + L" keys=" +
|
||||
std::to_wstring(recent.keys.size()) + L" match=" + std::to_wstring(hint));
|
||||
if (hint < 0)
|
||||
{
|
||||
reason = text.empty() ? L"recent-copy-empty" : L"recent-verification-mismatch";
|
||||
if (tx.valid())
|
||||
chord(VK_RIGHT);
|
||||
return;
|
||||
@@ -751,11 +845,13 @@ void App::transform(int command, int snippet)
|
||||
else if (config.recent && previous && previous->window == target &&
|
||||
previous->version == version)
|
||||
{
|
||||
expected = previous->result;
|
||||
text = tx.copyRange(caretSteps(previous->result), previous->result,
|
||||
[&](const std::wstring &candidate)
|
||||
{ return newlines(candidate) == newlines(previous->result); });
|
||||
if (newlines(text) != newlines(previous->result))
|
||||
{
|
||||
reason = L"previous-result-verification-mismatch";
|
||||
if (tx.valid())
|
||||
chord(VK_RIGHT);
|
||||
return;
|
||||
@@ -765,10 +861,12 @@ void App::transform(int command, int snippet)
|
||||
}
|
||||
else
|
||||
text = tx.copy();
|
||||
reason = L"context-changed-after-copy";
|
||||
if (!tx.valid())
|
||||
return;
|
||||
if (text.empty())
|
||||
{
|
||||
reason = L"copy-empty";
|
||||
if (to >= 0 && config.switchLayout)
|
||||
switchWindowLayout(target, available[to].handle, valid);
|
||||
return;
|
||||
@@ -790,6 +888,7 @@ void App::transform(int command, int snippet)
|
||||
cycle.push_back(i);
|
||||
if (cycle.size() < 2)
|
||||
{
|
||||
reason = L"cycle-has-fewer-than-two-layouts";
|
||||
if (automatic && tx.valid())
|
||||
chord(VK_RIGHT);
|
||||
return;
|
||||
@@ -804,12 +903,20 @@ void App::transform(int command, int snippet)
|
||||
}
|
||||
}
|
||||
bool sent = false;
|
||||
log(L"Conversion source=" + std::to_wstring(hint) + L" target=" + std::to_wstring(to) +
|
||||
diagnosticEvent(L"Conversion source=" + std::to_wstring(hint) + L" target=" + std::to_wstring(to) +
|
||||
L" changed=" + std::to_wstring(result != text));
|
||||
if (result != text)
|
||||
sent = tx.paste(textClip(result));
|
||||
else if (automatic && tx.valid())
|
||||
chord(VK_RIGHT);
|
||||
reason = result == text ? L"conversion-unchanged" :
|
||||
sent ? L"paste-sent-unverified" : L"paste-cancelled";
|
||||
if (result == text &&
|
||||
(std::none_of(text.begin(), text.end(), letter) ||
|
||||
(command >= Direct && detect(text, available, hint) == to)))
|
||||
reason = L"unchanged-expected";
|
||||
if (sent && !tx.valid())
|
||||
reason = L"context-changed-after-paste";
|
||||
if (sent && command == Undo && previous && previous->autoCorrected)
|
||||
autoCorrector.reject(previous->original, config);
|
||||
if (tx.valid() && (sent || automatic))
|
||||
@@ -835,12 +942,15 @@ void App::transform(int command, int snippet)
|
||||
if (to >= 0 && config.switchLayout && tx.valid() && (sent || result == text))
|
||||
{
|
||||
bool switched = switchWindowLayout(target, available[to].handle, valid);
|
||||
log(L"Layout switch confirmed=" + std::to_wstring(switched));
|
||||
diagnosticEvent(L"Layout switch confirmed=" + std::to_wstring(switched));
|
||||
if (!switched)
|
||||
reason += L";layout-switch-failed";
|
||||
}
|
||||
log(L"Text operation completed in " + std::to_wstring(GetTickCount64() - start) + L" ms");
|
||||
diagnosticEvent(L"Text operation completed in " + std::to_wstring(GetTickCount64() - start) + L" ms");
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
reason = L"exception:" + wide(e.what());
|
||||
report(wide(e.what()));
|
||||
}
|
||||
});
|
||||
@@ -984,6 +1094,7 @@ void App::trayMenu()
|
||||
AppendMenuW(pause, MF_STRING, 23, L"Until restart");
|
||||
AppendMenuW(menu, MF_POPUP, (UINT_PTR)pause, L"Pause clipboard history");
|
||||
AppendMenuW(menu, MF_STRING, 15, L"Open data folder");
|
||||
AppendMenuW(menu, MF_STRING, 18, L"View log…");
|
||||
AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
|
||||
AppendMenuW(menu, MF_STRING, 16, L"About KSwitcher");
|
||||
AppendMenuW(menu, MF_STRING, 17, L"Exit");
|
||||
@@ -1022,7 +1133,7 @@ void App::trayMenu()
|
||||
break;
|
||||
case 16:
|
||||
MessageBoxW(window,
|
||||
L"KSwitcher 0.1.0\nNative C++ / Win32 keyboard and clipboard utility\n\nBased on "
|
||||
L"KSwitcher 0.2.0\nNative C++ / Win32 keyboard and clipboard utility\n\nBased on "
|
||||
L"Typlo by bitcodepro. MIT license.\nHistory is encrypted for your Windows "
|
||||
L"account.\nNo .NET, WebView or graphics framework.",
|
||||
L"About KSwitcher", MB_OK);
|
||||
@@ -1030,6 +1141,9 @@ void App::trayMenu()
|
||||
case 17:
|
||||
PostMessageW(window, WM_CLOSE, 0, 0);
|
||||
break;
|
||||
case 18:
|
||||
showLogs(*this);
|
||||
break;
|
||||
case 20:
|
||||
pauseUntil = 0;
|
||||
break;
|
||||
@@ -1225,6 +1339,8 @@ int App::execute()
|
||||
continue;
|
||||
if (snippetsWindow && IsDialogMessageW(snippetsWindow, &msg))
|
||||
continue;
|
||||
if (logWindow && IsDialogMessageW(logWindow, &msg))
|
||||
continue;
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user