Release v0.6.36: offload notification registration and mitigate Viber paste timing

This commit is contained in:
2026-11-04 15:40:50 +02:00
parent d13e9849a7
commit 51c8aab834
16 changed files with 220 additions and 19 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.24) cmake_minimum_required(VERSION 3.24)
project(KSwitcher VERSION 0.6.35 LANGUAGES CXX RC) project(KSwitcher VERSION 0.6.36 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()
+4
View File
@@ -707,3 +707,7 @@ supporting old clients; their immutable update code only reads that page.
### Editors that omit copied trailing spaces ### Editors that omit copied trailing spaces
From v0.6.31, KSwitcher recovers trailing ordinary spaces omitted by Ctrl+C only when Windows accessibility proves the complete selection. The same focused control and range are rechecked before paste, and the result is read back even without Write log. Recent-text conversion, automatic correction, repeat conversion and Undo retain exact verification. Missing letters, tabs, line breaks and unavailable or ambiguous selections are not reconstructed. No setting or shortcut has changed. From v0.6.31, KSwitcher recovers trailing ordinary spaces omitted by Ctrl+C only when Windows accessibility proves the complete selection. The same focused control and range are rechecked before paste, and the result is read back even without Write log. Recent-text conversion, automatic correction, repeat conversion and Undo retain exact verification. Missing letters, tabs, line breaks and unavailable or ambiguous selections are not reconstructed. No setting or shortcut has changed.
### Notification registration and Viber — v0.6.36
Notification registration runs in KSwitcher.Screenshot.exe; keep all three executables together. Startup registration is asynchronous. Viber pastes allow 75 ms for clipboard notifications before Ctrl+V, retaining focus, ownership and selection checks. This mitigates an intermittent clipboard timing issue; it does not guarantee all Viber failures are resolved. UI Automation is unchanged.
Binary file not shown.
+8 -9
View File
@@ -1,18 +1,17 @@
# Latest Windows release # Latest Windows release
## v0.6.35 — 2026-11-04 ## v0.6.36 — 2026-11-04
## Changes ## Changes
- Reduced resident memory: the input predictor and correction worker share immutable dictionaries. Compact indexed blocks retain all 140,939 bundled words and their frequency ranks while reducing the EN/UK/RU lookup payload from approximately 3.06 MiB per copy to 0.90 MiB shared. Custom UTF-8 dictionary files, duplicate handling, frequency limits and personal dictionaries remain compatible. - Notification registration now runs in the existing KSwitcher.Screenshot.exe helper. Startup launches it asynchronously and checks its exit status without blocking keyboard work; update notification registration waits in its background worker. Keep all three executables together. UI Automation and notification activation routes remain unchanged.
- Closing clipboard history releases its duplicate row list and Windows list strings, retains selection/scroll identifiers for reopening, and rejects late search results and timers. Preview caches now have payload budgets of 1 MiB for thumbnails and 256 KiB for formatted text, plus the existing item-count limit. Visible entries are retained even if an unusually large viewport exceeds a budget. - Viber paste now allows 75 ms for clipboard notifications to settle before sending Ctrl+V, then rechecks input, focus and clipboard ownership. This mitigates a suspected clipboard timing race: an intermittent corrupted Cyrillic paste was reproduced on the prior build. It is not a guarantee that every Viber timing issue is resolved; no blind paste retries or relaxed selection checks were added.
- PNG decoding reads the existing input buffer without making another full PNG copy. DIB decoding allocates its temporary BMP buffer once at the required size. - Added regression coverage for an explicitly selected middle range followed by a repeated shortcut without selection, preserving surrounding text, trailing spaces and the original clipboard. Updated the integration fixture to initialize shared predictor dictionaries.
- Idle maintenance releases diagnostic context older than 30 seconds and expired Undo text after its existing two-minute eligibility window, without interrupting an active operation. Completed sync buffers are released on success, failure and stale responses; unresolved conflicts remain available. - Added reproducible standalone COM, UI Automation and Shell memory measurement tools. No shortcuts, settings or defaults changed.
- No shortcut, default, on-disk history format or screenshot-editing behavior changed. The v0.6.34 four-direction movement cursors remain included.
## Validation ## Validation
The full `build.ps1 -Stage -Test` suite covers complete bundled dictionary word/rank equivalence, malformed/custom dictionary boundaries, 105 sync checks, shared wheel batching, 304 native regressions, 559 input/UI checks, screenshot editor UI and PNG saving, isolated screenshot registration, 48 updater regressions and updater progress UI checks. Cache assertions now run once outside the exception helper rather than being repeated inside it. All three executable versions and all 12 package manifest file sizes/SHA-256 hashes were verified; no personal data is packaged. Full staged build and tests cover dictionary equivalence for all 140,939 words, 105 sync checks, wheel batching, 304 native checks, 559 input checks, screenshot UI/PNG/registration, 48 updater checks and updater progress UI. Conversion integration passed 43 assertions. Eight actual Viber conversions passed on the development build, including two complete long-text layout cycles and selected/repeated middle-word conversion; test messages were not sent.
Controlled local measurements of the final dictionary step, after equal restart/wait periods with three dictionaries enabled, reduced private resident memory from 7.08 to 4.65 MiB and private committed memory from 8.34 to 5.70 MiB. These compare consecutive development builds, not the published v0.6.34 with the full cumulative change. Randomized lookup tests were approximately 20% faster. Results depend on settings, workload, Windows and allocator state; these are not fixed memory limits. The 0.90 MiB dictionary figure excludes process/allocator overhead. A controlled fresh-launch before/after comparison (five private-working-set samples each, same installation and settings) measured 4.69 → 4.18 MiB, about 0.51 MiB saved. This is one local comparison, not a fixed memory limit or a long-term idle guarantee. The prior compact dictionaries and cache/idle memory reductions remain included.
## Upgrade ## Upgrade
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.
@@ -20,4 +19,4 @@ For manual installation, exit KSwitcher, close screenshot editors and extract th
Download **KSwitcher-win-x64.zip** with **SHA256SUMS.txt**. Keep all three executables together. The package contains no personal settings, credentials or clipboard history. Download **KSwitcher-win-x64.zip** with **SHA256SUMS.txt**. Keep all three executables together. The package contains no personal settings, credentials or clipboard history.
Publish stable `release-vX.Y.Z` tags matching the executable version. Keep `v0.4.2` as the last legacy tag. Publish stable `release-vX.Y.Z` tags matching the executable version. Keep `v0.4.2` as the last legacy tag.
+1 -1
View File
@@ -1 +1 @@
6e36a582eec59b3cc4f0f9d6b3e05cd699371c05cae30a95c6eae51a838f7bfe KSwitcher-win-x64.zip 794d74113bddf13ffee7317cadee9c472bca3b84b3361796db58ff8023f66571 KSwitcher-win-x64.zip
+3 -3
View File
@@ -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,35,0 FILEVERSION 0,6,36,0
PRODUCTVERSION 0,6,35,0 PRODUCTVERSION 0,6,36,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.35" VALUE "FileVersion", "0.6.36"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"
+18 -1
View File
@@ -358,6 +358,14 @@ class Transaction
if (!valid()) if (!valid())
return false; return false;
write(clip); write(clip);
// Viber's Qt editor observes clipboard changes asynchronously. Give it
// a chance to discard the data object from Ctrl+C before requesting paste.
// Recheck ownership and input below; never retry a possibly applied paste.
if (lower(processName(target)) == L"viber")
{
Sleep(75);
diagnosticEvent(L"Paste: Viber clipboard notification settling interval completed");
}
for (int i = 0; i < 2; ++i) for (int i = 0; i < 2; ++i)
{ {
check(); check();
@@ -433,6 +441,7 @@ App::App(HINSTANCE h, bool test, const std::filesystem::path &dataRoot) : instan
} }
App::~App() App::~App()
{ {
if (notificationRegistration) CloseHandle(notificationRegistration);
if (syncWorker.joinable()) syncWorker.join(); if (syncWorker.joinable()) syncWorker.join();
if (updateWorker.joinable()) if (updateWorker.joinable())
updateWorker.join(); updateWorker.join();
@@ -1893,6 +1902,14 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
case WM_TIMER: case WM_TIMER:
if (wp == 6) if (wp == 6)
{ {
if (app->notificationRegistration && WaitForSingleObject(app->notificationRegistration, 0) == WAIT_OBJECT_0)
{
DWORD code = ERROR_GEN_FAILURE;
GetExitCodeProcess(app->notificationRegistration, &code);
if (code) log(L"Screenshot notification registration helper failed: " + std::to_wstring(code));
CloseHandle(app->notificationRegistration);
app->notificationRegistration = nullptr;
}
app->releaseIdleMemory(GetTickCount64()); app->releaseIdleMemory(GetTickCount64());
app->pollSync(); app->pollSync();
return 0; return 0;
@@ -2012,7 +2029,7 @@ int App::execute()
log(std::wstring(L"Update completed; running version ") + AppVersion); log(std::wstring(L"Update completed; running version ") + AppVersion);
if (startCommand) if (startCommand)
PostMessageW(window, WM_HOTKEY, startCommand, 0); PostMessageW(window, WM_HOTKEY, startCommand, 0);
try { registerScreenshotNotifications(); } try { notificationRegistration = startScreenshotNotificationRegistration(); }
catch(const std::exception &e) { log(L"Screenshot notification registration: "+wide(e.what())); } catch(const std::exception &e) { log(L"Screenshot notification registration: "+wide(e.what())); }
auto screenshotActivation=startScreenshotActivationListener(); auto screenshotActivation=startScreenshotActivationListener();
MSG msg; MSG msg;
+1
View File
@@ -92,6 +92,7 @@ class App
std::condition_variable jobsReady; std::condition_variable jobsReady;
std::deque<std::function<void()>> jobs; std::deque<std::function<void()>> jobs;
std::thread worker; std::thread worker;
HANDLE notificationRegistration{};
std::thread historySearchWorker; std::thread historySearchWorker;
std::condition_variable historySearchReady; std::condition_variable historySearchReady;
std::function<void()> pendingHistorySearch; std::function<void()> pendingHistorySearch;
+24
View File
@@ -50,6 +50,30 @@ void registerScreenshotNotifications()
// Notify the shell that notification identity/activation registration changed. // Notify the shell that notification identity/activation registration changed.
SHChangeNotify(SHCNE_ASSOCCHANGED,SHCNF_IDLIST,nullptr,nullptr); SHChangeNotify(SHCNE_ASSOCCHANGED,SHCNF_IDLIST,nullptr,nullptr);
} }
HANDLE startScreenshotNotificationRegistration()
{
auto path=std::filesystem::path(executable()).parent_path()/L"KSwitcher.Screenshot.exe";
auto command=L"\""+path.wstring()+L"\" --register-notifications";
STARTUPINFOW startup{sizeof(startup)};
startup.dwFlags=STARTF_USESHOWWINDOW; startup.wShowWindow=SW_HIDE;
PROCESS_INFORMATION process{};
if(!CreateProcessW(path.c_str(),command.data(),nullptr,nullptr,FALSE,CREATE_NO_WINDOW,
nullptr,path.parent_path().c_str(),&startup,&process))
throw std::runtime_error(utf8(lastError(L"Cannot start notification registration helper")));
CloseHandle(process.hThread);
return process.hProcess;
}
void registerScreenshotNotificationsInHelper()
{
auto process=startScreenshotNotificationRegistration();
auto wait=WaitForSingleObject(process,10000);
DWORD code=ERROR_GEN_FAILURE;
bool completed=wait==WAIT_OBJECT_0 && GetExitCodeProcess(process,&code);
CloseHandle(process);
if(!completed || code)
throw std::runtime_error(completed ? "Notification registration helper failed: "+std::to_string(code)
: "Notification registration helper did not complete within 10 seconds");
}
bool validScreenshotToken(const std::wstring &id) bool validScreenshotToken(const std::wstring &id)
{ {
if(id.size()!=36) return false; if(id.size()!=36) return false;
+1 -1
View File
@@ -206,7 +206,7 @@ bool notifyUpdateAvailable(const std::wstring &version)
{ {
try { try {
// Reuse the screenshot notification identity, Windows settings and COM activator. // Reuse the screenshot notification identity, Windows settings and COM activator.
registerScreenshotNotifications(); registerScreenshotNotificationsInHelper();
ComPtr<Notifications::IToastNotificationManagerStatics> manager; ComPtr<Notifications::IToastNotificationManagerStatics> manager;
String managerClass(L"Windows.UI.Notifications.ToastNotificationManager"); String managerClass(L"Windows.UI.Notifications.ToastNotificationManager");
check(RoGetActivationFactory(managerClass.value,IID_PPV_ARGS(&manager))); check(RoGetActivationFactory(managerClass.value,IID_PPV_ARGS(&manager)));
+4
View File
@@ -4,6 +4,10 @@ namespace ks {
bool notifyScreenshot(const Clip &); bool notifyScreenshot(const Clip &);
std::string screenshotNotificationError(); std::string screenshotNotificationError();
void registerScreenshotNotifications(); void registerScreenshotNotifications();
// Caller owns the returned process handle; the helper exits after registration.
HANDLE startScreenshotNotificationRegistration();
// For background notification workers only; bounded wait, never on the UI thread.
void registerScreenshotNotificationsInHelper();
int runScreenshotToastActivator(); int runScreenshotToastActivator();
DWORD startScreenshotActivationListener(); DWORD startScreenshotActivationListener();
bool validScreenshotToken(const std::wstring &); bool validScreenshotToken(const std::wstring &);
+1 -1
View File
@@ -2,7 +2,7 @@
#include "platform.h" #include "platform.h"
namespace ks namespace ks
{ {
inline constexpr wchar_t AppVersion[] = L"0.6.35"; inline constexpr wchar_t AppVersion[] = L"0.6.36";
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;
+17 -1
View File
@@ -529,6 +529,9 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
require(text() == L"wrong ", "unrelated selection remains rejected with trimmed clipboard"); require(text() == L"wrong ", "unrelated selection remains rejected with trimmed clipboard");
app->settings.autoCorrect = true; app->settings.autoCorrect = true;
app->settings.autoLanguages = {L"en", L"ru"}; app->settings.autoLanguages = {L"en", L"ru"};
// The worker now shares the predictor's immutable dictionaries.
// This fixture changes settings directly instead of applySettings.
app->autoPredictor.configure(L"dictionaries", app->settings.autoLanguages);
app->autoCorrector = ks::AutoCorrector{}; app->autoCorrector = ks::AutoCorrector{};
ActivateKeyboardLayout(app->layouts[en].handle, 0); ActivateKeyboardLayout(app->layouts[en].handle, 0);
prepare(L"ghbdtn ", false); prepare(L"ghbdtn ", false);
@@ -556,9 +559,22 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp)
require(trimmedCopies >= 7, "fixture actually stripped spaces during recovery scenarios"); require(trimmedCopies >= 7, "fixture actually stripped spaces during recovery scenarios");
pasteBehavior = 0; pasteBehavior = 0;
trimCopiedSpaces = false; trimCopiedSpaces = false;
finish(); // User-selected middle range, followed by the same command with
// no selection: retain both surrounding text and trailing spaces.
prepare(L"OLDghbdtn NEXT", false);
SendMessageW(field, EM_SETSEL, 3, 11);
app->transform(ks::Direct + ru);
break; break;
} }
case 58:
require(text() == L"OLDпривет NEXT", "explicit middle selection preserves surrounding text");
app->transform(ks::Direct + en);
break;
case 59:
require(text() == L"OLDghbdtn NEXT", "hotkey without selection reconverts only the previous middle range");
require(ks::readClipboard(app->window).text() == sentinel, "selected and repeated conversions restore clipboard");
finish();
break;
case 30: case 30:
case 32: case 32:
case 34: case 34:
+11 -1
View File
@@ -5,9 +5,11 @@
static std::wstring programs; static std::wstring programs;
static std::wstring activationExecutable, activationCommand; static std::wstring activationExecutable, activationCommand;
static int activationCalls{}; static int activationCalls{};
static DWORD launchFlags{}; static bool inheritsHandles{};
static BOOL WINAPI testCreateProcess(LPCWSTR path, LPWSTR command, LPSECURITY_ATTRIBUTES, static BOOL WINAPI testCreateProcess(LPCWSTR path, LPWSTR command, LPSECURITY_ATTRIBUTES,
LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCWSTR, LPSTARTUPINFOW, LPPROCESS_INFORMATION) LPSECURITY_ATTRIBUTES, BOOL inherit, DWORD flags, LPVOID, LPCWSTR, LPSTARTUPINFOW, LPPROCESS_INFORMATION)
{ {
launchFlags=flags; inheritsHandles=inherit;
++activationCalls; activationExecutable=path; activationCommand=command; ++activationCalls; activationExecutable=path; activationCommand=command;
SetLastError(ERROR_CANCELLED); return FALSE; // Inspect activation without starting any program. SetLastError(ERROR_CANCELLED); return FALSE; // Inspect activation without starting any program.
} }
@@ -55,6 +57,14 @@ int main()
valid=id.vt==VT_CLSID && *id.puuid==ks::ActivatorId; PropVariantClear(&id); valid=id.vt==VT_CLSID && *id.puuid==ks::ActivatorId; PropVariantClear(&id);
if(!valid) throw std::runtime_error("shortcut activator missing"); if(!valid) throw std::runtime_error("shortcut activator missing");
ks::registerScreenshotNotifications(); // Registration is safe to repeat. ks::registerScreenshotNotifications(); // Registration is safe to repeat.
bool launchFailed=false;
try { ks::startScreenshotNotificationRegistration(); }
catch(const std::exception &) { launchFailed=true; }
if(!launchFailed || activationExecutable!=helper.wstring() ||
activationCommand!=L"\""+helper.wstring()+L"\" --register-notifications" ||
!(launchFlags&CREATE_NO_WINDOW) || inheritsHandles)
throw std::runtime_error("registration must launch the sibling helper and propagate failure");
activationCalls=0;
ks::Activator activator(helper.wstring(),false); ks::Activator activator(helper.wstring(),false);
activator.Activate(L"HrynCo.KSwitcher.Screenshot",L"check-update",nullptr,0); activator.Activate(L"HrynCo.KSwitcher.Screenshot",L"check-update",nullptr,0);
auto main=(helper.parent_path()/L"KSwitcher.exe").wstring(); auto main=(helper.parent_path()/L"KSwitcher.exe").wstring();
+61
View File
@@ -0,0 +1,61 @@
param([ValidateRange(1,20)][int]$Runs=3,[ValidateRange(100,10000)][int]$SettleMilliseconds=1500)
# Private resident pages are counted externally, so the measurement buffer is
# allocated in PowerShell, not in the measured process. Never trim working sets.
$ErrorActionPreference='Stop'
$root=Split-Path $PSScriptRoot -Parent
$output=Join-Path $root 'build/memory-components'
New-Item -ItemType Directory -Force $output | Out-Null
$exe=Join-Path $output 'probe.exe'
& "$root/.tools/w64devkit/bin/g++.exe" -std=c++20 -Os -static -s -DUNICODE -D_UNICODE -D_WIN32_WINNT=0x0A00 "$PSScriptRoot/memory-components.cpp" -o $exe -lole32 -loleaut32 -lshell32 -lpropsys -lshlwapi -luuid -luiautomationcore
if($LASTEXITCODE) { throw 'Probe compilation failed' }
Add-Type @'
using System;
using System.Runtime.InteropServices;
public static class ComponentWorkingSet {
[DllImport("psapi.dll", SetLastError=true)] static extern bool QueryWorkingSet(IntPtr process,IntPtr buffer,int size);
public static long PrivateBytes(IntPtr process) {
int size=4*1024*1024;
IntPtr buffer=Marshal.AllocHGlobal(size);
try {
if(!QueryWorkingSet(process,buffer,size)) throw new System.ComponentModel.Win32Exception();
long count=Marshal.ReadIntPtr(buffer).ToInt64(), pages=0;
if(count>(size/IntPtr.Size)-1) throw new InvalidOperationException("Working set buffer is too small");
for(int i=0;i<count;i++) if((Marshal.ReadIntPtr(buffer,(i+1)*IntPtr.Size).ToInt64() & 256)==0) pages++;
return pages*Environment.SystemPageSize;
} finally { Marshal.FreeHGlobal(buffer); }
}
}
'@
$rows=[Collections.Generic.List[object]]::new()
foreach($run in 1..$Runs) {
foreach($mode in @('baseline','sta','mta','shell','uia','focused','combined')) {
$start=[Diagnostics.ProcessStartInfo]::new($exe,$mode)
$start.UseShellExecute=$false; $start.CreateNoWindow=$true
$start.RedirectStandardOutput=$true; $start.RedirectStandardInput=$true; $start.RedirectStandardError=$true
$process=[Diagnostics.Process]::Start($start)
$errors=$process.StandardError.ReadToEndAsync()
try {
foreach($expected in @('baseline','com','active','released','uninitialized')) {
$pending=$process.StandardOutput.ReadLineAsync()
if(!$pending.Wait(15000)) { throw "Timeout: $mode/$expected" }
$phase=$pending.Result
if($phase -ne $expected) { throw "Unexpected phase '$phase', expected $expected" }
Start-Sleep -Milliseconds $SettleMilliseconds
$process.Refresh()
$rows.Add([pscustomobject]@{Run=$run;Mode=$mode;Phase=$phase;PrivateBytes=$process.PrivateMemorySize64;PrivateWorkingSet=[ComponentWorkingSet]::PrivateBytes($process.Handle);WorkingSet=$process.WorkingSet64;Threads=$process.Threads.Count;Modules=@($process.Modules | ForEach-Object ModuleName)})
$process.StandardInput.Write("`n"); $process.StandardInput.Flush()
}
if(!$process.WaitForExit(15000)) { throw 'Exit timeout' }
if($process.ExitCode) { throw "Probe failed: $($errors.Result)" }
$errors.Result | Set-Content (Join-Path $output "$run-$mode.txt")
} finally {
if(!$process.HasExited) { $process.Kill(); $process.WaitForExit() }
$process.Dispose()
$rows | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $output 'results.json')
}
Write-Host "Completed $run/$Runs $mode"
}
}
$rows | Group-Object Mode,Phase | ForEach-Object {
[pscustomobject]@{Case=$_.Name;PrivateMiB=[math]::Round(($_.Group.PrivateBytes | Measure-Object -Average).Average/1MB,3);PrivateWorkingSetMiB=[math]::Round(($_.Group.PrivateWorkingSet | Measure-Object -Average).Average/1MB,3)}
} | Format-Table
+65
View File
@@ -0,0 +1,65 @@
// Standalone attribution probe. No clipboard, registry or user input mutations.
#include <windows.h>
#include <shlobj.h>
#include <propkey.h>
#include <propvarutil.h>
#include <uiautomation.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>
static void check(HRESULT hr) {
if (FAILED(hr)) { fprintf(stderr,"HRESULT=%08lx\n",(unsigned long)hr); exit(2); }
}
static void phase(const char *name) {
printf("%s\n",name); fflush(stdout);
if (getchar()==EOF) exit(3);
}
int main(int argc,char **argv) {
const char *mode=argc>1?argv[1]:"baseline";
const bool baseline=!strcmp(mode,"baseline");
const bool shell=!strcmp(mode,"shell") || !strcmp(mode,"combined");
const bool uia=!strcmp(mode,"uia") || !strcmp(mode,"focused") || !strcmp(mode,"combined");
phase("baseline");
if(!baseline) check(CoInitializeEx(nullptr,!strcmp(mode,"sta") || shell ? COINIT_APARTMENTTHREADED : COINIT_MULTITHREADED));
phase("com");
IShellLinkW *link{}; IPropertyStore *store{}; IPersistFile *file{};
IUIAutomation2 *automation{}; IUIAutomationElement *element{}; IUIAutomationTextPattern *pattern{};
wchar_t temporary[MAX_PATH]{},shortcut[MAX_PATH]{},exe[MAX_PATH]{};
if(shell) {
check(CoCreateInstance(CLSID_ShellLink,nullptr,CLSCTX_INPROC_SERVER,IID_PPV_ARGS(&link)));
GetModuleFileNameW(nullptr,exe,MAX_PATH);
check(link->SetPath(exe)); check(link->SetIconLocation(exe,0));
check(link->QueryInterface(IID_PPV_ARGS(&store)));
PROPVARIANT value{}; check(InitPropVariantFromString(L"KSwitcher.MemoryProbe",&value));
check(store->SetValue(PKEY_AppUserModel_ID,value)); PropVariantClear(&value);
GUID id{0xe3f78047,0xf0ad,0x49b6,{0x90,0x74,0x41,0x19,0x31,0x70,0x61,0x67}};
value.vt=VT_CLSID; value.puuid=&id;
check(store->SetValue(PKEY_AppUserModel_ToastActivatorCLSID,value)); check(store->Commit());
PWSTR programs{}; check(SHGetKnownFolderPath(FOLDERID_Programs,0,nullptr,&programs)); CoTaskMemFree(programs);
GetTempPathW(MAX_PATH,temporary); if(!GetTempFileNameW(temporary,L"ksm",0,shortcut)) return 4;
check(link->QueryInterface(IID_PPV_ARGS(&file))); check(file->Save(shortcut,TRUE));
}
if(uia) {
check(CoCreateInstance(CLSID_CUIAutomation8,nullptr,CLSCTX_INPROC_SERVER,IID_PPV_ARGS(&automation)));
check(automation->put_ConnectionTimeout(200)); check(automation->put_TransactionTimeout(200));
if(strcmp(mode,"uia")) {
HRESULT focused=automation->GetFocusedElement(&element);
fprintf(stderr,"focus=%08lx\n",(unsigned long)focused);
if(SUCCEEDED(focused) && element) {
BOOL password=TRUE,enabled=FALSE; CONTROLTYPEID type{};
element->get_CurrentIsPassword(&password); element->get_CurrentIsEnabled(&enabled); element->get_CurrentControlType(&type);
fprintf(stderr,"type=%d enabled=%d protected=%d\n",type,enabled,password);
if(!password) fprintf(stderr,"pattern=%08lx\n",(unsigned long)element->GetCurrentPatternAs(UIA_TextPatternId,IID_PPV_ARGS(&pattern)));
}
}
}
phase("active");
if(pattern) pattern->Release(); if(element) element->Release(); if(automation) automation->Release();
if(file) file->Release(); if(store) store->Release(); if(link) link->Release();
if(shortcut[0]) DeleteFileW(shortcut);
phase("released");
if(!baseline) { CoFreeUnusedLibrariesEx(0,0); CoUninitialize(); }
phase("uninitialized");
return 0;
}