Compare commits

...

1 Commits

Author SHA1 Message Date
agrynco c326db7dc5 Release v0.6.11: show branded update progress and completion 2026-10-29 23:03:06 +02:00
13 changed files with 290 additions and 28 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.24)
project(KSwitcher VERSION 0.6.10 LANGUAGES CXX RC)
project(KSwitcher VERSION 0.6.11 LANGUAGES CXX RC)
if(NOT WIN32)
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
endif()
+9 -1
View File
@@ -184,7 +184,15 @@ Click the notification or **Review update** to check the latest release and conf
Failed notification submissions are logged and retried after five minutes instead of being marked
as delivered. Check results and successful submissions are recorded in the log when logging is enabled.
Network failures stay in diagnostics; the next scheduled check retries. You can also check manually at any time.
Confirm the offered version to download and install it. The updater uses the existing
Confirm the offered version to download and install it. A progress window shows
download percentage and received/total size, package verification,
installation and restart. After the new process confirms startup, that window shows the installed
version and a Done button; it does not send a separate completion notification. If installation
fails after KSwitcher closes, the same window shows recovery and the error. Background checks
remain silent unless a new version is available. This progress UI is supplied by the updater in
the currently installed build, so it becomes available for subsequent updates after installing it.
The updater uses the existing
`KSwitcher-win-x64.zip` and `SHA256SUMS.txt` assets, checks the checksum, package contents, x64
executable identity and version, then closes and restarts KSwitcher. Downloads and staging happen
before the application exits. If replacement or startup fails, it restores the previous files;
Binary file not shown.
+9 -7
View File
@@ -1,24 +1,26 @@
# Latest Windows release
## v0.6.10 — 2026-10-29
## v0.6.11 — 2026-10-29
## Changes
- Preserve typing during automatic layout correction: queue key presses and releases until replacement, clipboard restoration and layout switching finish. Preserve modifiers and Backspace; collapse temporary selection before replay when a correction is rejected.
- Add My dictionary fields for English, Ukrainian and Russian in Auto-correction options. Enter correct words separated by commas or new lines; save them with settings without editing dictionary files, sorting entries or assigning ranks.
- Accept unsorted dictionary files and duplicate entries, retaining the lowest rank for duplicates. Include Ukrainian `реліз` in the bundled vocabulary so `релыз` can be corrected when Russian and Ukrainian are enabled.
- Show an update progress window for manual checks and installation. Downloads display percentage and received/total size; verification, unpacking, installation and restart have distinct status messages.
- Keep the progress window running independently while KSwitcher exits and restarts. After the new application confirms startup, show the installed version and a Done button in the same window, without a separate completion notification.
- Show recovery and installation errors in the same window after the application exits. Background update checks remain quiet.
The progress UI is provided by the currently installed updater. Updating from v0.6.10 to this release still uses the older interface; subsequent updates use the new window.
## Validation
Native regression checks cover personal words, settings persistence, language validation, dictionary ordering and duplicate ranks. Input/UI checks cover personal-word entry and normalization. Native editor integration covers typing with Shift and Backspace during successful and rejected corrections. Release checks also cover screenshot UI, isolated notification registration and updater validation.
214 native regression checks, 466 input/UI checks, screenshot editor UI checks, isolated notification registration, 45 updater regression checks, and progress-window checks for download, installation, recovery and completion states. Network tests verify real download progress and release download/checksum validation. Startup acknowledgement tests prevent reporting success for ordinary startup or a missing updater handshake.
**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.
For manual installation, exit KSwitcher and extract the entire ZIP into its installation folder. Keep personal `data/` and customized dictionaries. Existing customized dictionary files may be preserved by the updater; use My dictionary to add missing words without replacing those files.
For manual installation, exit KSwitcher and extract the entire ZIP into its installation folder. Keep personal `data/` and customized dictionaries.
Download **KSwitcher-win-x64.zip** with **SHA256SUMS.txt**. The package contains all three executables, a release manifest, offline dictionaries, documentation and license notices. It contains no personal settings, clipboard history, Tesseract installation or language models.
Build and package:
```powershell
.\build.ps1 -Stage -Test -Integration
.\build.ps1 -Stage -Test
.\package.ps1 -PublishRelease
```
+1 -1
View File
@@ -1 +1 @@
36966eb0b2b0af47c245aec6337a16703e0fbdca4d3eda7a8f25fa14a20f347e KSwitcher-win-x64.zip
d12994e3038e3a10f76b6eb4dba4aca0fac4c4d5f3805ba16f132f890a941bcf KSwitcher-win-x64.zip
+2
View File
@@ -61,6 +61,8 @@ try {
& "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File tests/update.ps1 -Executable $executablePath
if ($LASTEXITCODE) { throw 'Updater tests failed' }
& "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File tests/update_progress.ps1
if ($LASTEXITCODE) { throw 'Updater progress UI tests failed' }
}
if ($Integration) {
& $compiler @common tests/integration.cpp build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/ocr.o build/richtext.o build/update.o build/screenshot.o build/screenshot_launcher.o build/screenshot_activation.o build/screenshot_notifications.o build/app.o build/configuration.o build/ui.o build/screenshot-res.o @libs -static -s -o build/KSwitcherIntegration.exe
+3 -3
View File
@@ -9,8 +9,8 @@ IDC_SCREENSHOT_PENCIL CURSOR "icons/lucide/pencil.cur"
1 RT_MANIFEST "app.manifest"
201 RCDATA "update.ps1"
1 VERSIONINFO
FILEVERSION 0,6,10,0
PRODUCTVERSION 0,6,10,0
FILEVERSION 0,6,11,0
PRODUCTVERSION 0,6,11,0
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_APP
BEGIN
@@ -20,7 +20,7 @@ BEGIN
BEGIN
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
VALUE "ProductName", "KSwitcher"
VALUE "FileVersion", "0.6.10"
VALUE "FileVersion", "0.6.11"
END
END
BLOCK "VarFileInfo"
+215 -5
View File
@@ -3,10 +3,192 @@
[string]$CurrentVersion, [string]$ExpectedVersion,
[string]$InstallDirectory, [string]$WorkDirectory,
[int]$ParentProcessId, [string]$ReadyEvent,
[switch]$ShowProgress,
[switch]$Library
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$script:progressWindow = $null
function Start-UpdateProgress {
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
if (!('KSwitcherUpdateProgress' -as [type])) {
Add-Type -ReferencedAssemblies System.Windows.Forms,System.Drawing -TypeDefinition @'
using System;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;
using System.Drawing.Drawing2D;
using System.Runtime.InteropServices;
public sealed class UpdateMeter : Control {
public int Percent = -1;
private int position;
private readonly System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer { Interval = 35 };
public UpdateMeter() {
DoubleBuffered = true;
timer.Tick += delegate { if (Percent < 0) { position = (position + 5) % Math.Max(1,Width); Invalidate(); } };
timer.Start();
}
protected override void OnPaint(PaintEventArgs e) {
using(var track = new SolidBrush(Color.FromArgb(232,234,241))) e.Graphics.FillRectangle(track,0,0,Width,Height);
using(var fill = new SolidBrush(Color.FromArgb(79,70,229))) {
if(Percent < 0) e.Graphics.FillRectangle(fill,position-Width/4,0,Width/4,Height);
else e.Graphics.FillRectangle(fill,0,0,Width*Percent/100,Height);
}
}
protected override void Dispose(bool disposing) { if(disposing) timer.Dispose(); base.Dispose(disposing); }
}
public sealed class UpdateSteps : Control {
public int Current = -1;
public bool Complete;
public UpdateSteps() { DoubleBuffered = true; }
protected override void OnPaint(PaintEventArgs e) {
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
string[] names = {"Download","Verify","Install","Restart"};
for(int i=0;i<4;i++) {
int x = i*(Width-80)/3;
Color color = Complete || i<=Current ? Color.FromArgb(79,70,229) : Color.FromArgb(135,141,158);
using(var pen = new Pen(Color.FromArgb(231,233,240)))
if(i<3) e.Graphics.DrawLine(pen,x+22,8,x+(Width-80)/3-10,8);
using(var brush = new SolidBrush(color)) {
if(Complete || i<Current) {
e.Graphics.FillEllipse(brush,x,0,16,16);
using(var pen = new Pen(Color.White,1.6f)) e.Graphics.DrawLines(pen,new Point[]{new Point(x+4,8),new Point(x+7,11),new Point(x+12,5)});
} else {
using(var pen = new Pen(color,i==Current?2:1)) e.Graphics.DrawEllipse(pen,x+1,1,14,14);
if(i==Current) e.Graphics.FillEllipse(brush,x+5,5,6,6);
}
e.Graphics.DrawString(names[i],Font,brush,x,26);
}
}
}
}
public sealed class KSwitcherUpdateProgress : IDisposable {
private Form form;
private Label stage, detail, percentage, footer;
private UpdateMeter progress;
private UpdateSteps steps;
private Button done, close;
private bool closing;
private Exception failure;
private readonly ManualResetEvent ready = new ManualResetEvent(false);
private readonly Thread thread;
[DllImport("user32.dll")] private static extern bool ReleaseCapture();
[DllImport("user32.dll")] private static extern IntPtr SendMessage(IntPtr h,int m,IntPtr w,IntPtr l);
[DllImport("dwmapi.dll")] private static extern int DwmSetWindowAttribute(IntPtr h,int a,ref int v,int size);
private void Drag(object sender,MouseEventArgs e) { if(e.Button==MouseButtons.Left) { ReleaseCapture(); SendMessage(form.Handle,0xA1,(IntPtr)2,IntPtr.Zero); } }
public KSwitcherUpdateProgress() {
thread = new Thread(delegate() {
try {
Application.EnableVisualStyles();
form = new Form { Text = "KSwitcher Update", ClientSize = new Size(540,350), BackColor = Color.White,
ForeColor = Color.FromArgb(32,36,48), StartPosition = FormStartPosition.CenterScreen,
FormBorderStyle = FormBorderStyle.None, Font = new Font("Segoe UI",10), AutoScaleMode = AutoScaleMode.Dpi };
var logo = new Panel { Location = new Point(28,24), Size = new Size(34,34), BackColor = Color.FromArgb(79,70,229) };
logo.Paint += delegate(object sender,PaintEventArgs e) {
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
using(var pen = new Pen(Color.White,4)) { e.Graphics.DrawLine(pen,10,8,10,26); e.Graphics.DrawLine(pen,11,17,25,7); }
using(var pen = new Pen(Color.FromArgb(94,234,212),4)) e.Graphics.DrawLine(pen,12,17,25,27);
};
var brand = new Label { Text = "KSwitcher", Location = new Point(74,22), Size = new Size(260,24), Font = new Font("Segoe UI Semibold",11) };
var caption = new Label { Text = "Application update", Location = new Point(74,45), Size = new Size(260,20), ForeColor = Color.FromArgb(111,117,134), Font = new Font("Segoe UI",9) };
var minimize = new Button { Text = "\u2013", AccessibleName = "Minimize", Location = new Point(452,20), Size = new Size(32,32), FlatStyle = FlatStyle.Flat, TabStop = false };
minimize.FlatAppearance.BorderSize = 0; minimize.Click += delegate { form.WindowState = FormWindowState.Minimized; };
close = new Button { Text = "\u00d7", AccessibleName = "Close", Location = new Point(490,20), Size = new Size(32,32), FlatStyle = FlatStyle.Flat, Enabled = false };
close.FlatAppearance.BorderSize = 0; close.Click += delegate { form.Close(); };
stage = new Label { Text = "Checking for updates", Location = new Point(28,98), Size = new Size(414,36), Font = new Font("Segoe UI Semibold",20) };
percentage = new Label { Location = new Point(440,104), Size = new Size(72,30), TextAlign = ContentAlignment.MiddleRight, ForeColor = Color.FromArgb(79,70,229), Font = new Font("Segoe UI Semibold",16) };
detail = new Label { Text = "Connecting to the release server", Location = new Point(28,148), Size = new Size(484,42), ForeColor = Color.FromArgb(111,117,134) };
progress = new UpdateMeter { Location = new Point(28,202), Size = new Size(484,6), AccessibleName = "Update progress" };
steps = new UpdateSteps { Location = new Point(28,230), Size = new Size(484,48), Font = new Font("Segoe UI",9) };
var bottom = new Panel { Location = new Point(1,296), Size = new Size(538,53), BackColor = Color.FromArgb(247,248,251) };
footer = new Label { Text = "Your settings and history stay with you.", Location = new Point(27,18), Size = new Size(350,22), ForeColor = Color.FromArgb(111,117,134), Font = new Font("Segoe UI",9) };
done = new Button { Text = "Done", Location = new Point(410,9), Size = new Size(100,34), BackColor = Color.FromArgb(79,70,229), ForeColor = Color.White, FlatStyle = FlatStyle.Flat, Visible = false };
done.FlatAppearance.BorderSize = 0; done.Click += delegate { form.Close(); };
bottom.Controls.AddRange(new Control[]{footer,done});
form.Controls.AddRange(new Control[]{logo,brand,caption,minimize,close,stage,percentage,detail,progress,steps,bottom});
form.MouseDown += Drag; logo.MouseDown += Drag; brand.MouseDown += Drag; caption.MouseDown += Drag;
form.Paint += delegate(object sender,PaintEventArgs e) { using(var pen = new Pen(Color.FromArgb(220,223,232))) e.Graphics.DrawRectangle(pen,0,0,form.ClientSize.Width-1,form.ClientSize.Height-1); };
form.FormClosing += delegate(object sender, FormClosingEventArgs e) { if (!closing) e.Cancel = true; };
form.Shown += delegate { int rounded=2; try { DwmSetWindowAttribute(form.Handle,33,ref rounded,4); } catch { } ready.Set(); };
Application.Run(form);
} catch(Exception e) { failure = e; ready.Set(); }
});
thread.IsBackground = true;
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
if (!ready.WaitOne(10000)) throw new Exception("Update progress window did not start.");
if (failure != null) throw new Exception("Cannot display update progress.",failure);
}
public void Report(string title, string text, int percent) {
if (form == null || form.IsDisposed) return;
form.BeginInvoke((Action)delegate {
stage.Text = title; detail.Text = text;
progress.Percent = percent < 0 ? -1 : Math.Max(0,Math.Min(100,percent));
percentage.Text = percent < 0 ? "" : progress.Percent+"%";
int separator = text.IndexOf("% - ");
if(separator>=0) detail.Text = text.Substring(separator+4)+" downloaded";
if(title.StartsWith("Downloading")) steps.Current=0;
else if(title.StartsWith("Verifying") || title.StartsWith("Preparing")) steps.Current=1;
else if(title.StartsWith("Installing") || title.StartsWith("Closing")) steps.Current=2;
else if(title.StartsWith("Restarting")) steps.Current=3;
progress.Invalidate(); steps.Invalidate();
});
}
public void SavePreview(string path) {
form.Invoke((Action)delegate {
using(var bitmap = new Bitmap(form.Width,form.Height)) {
form.DrawToBitmap(bitmap,new Rectangle(0,0,bitmap.Width,bitmap.Height));
bitmap.Save(path,System.Drawing.Imaging.ImageFormat.Png);
}
});
}
public string Status { get { return (string)form.Invoke((Func<string>)delegate { return stage.Text; }); } }
public int Percent { get { return (int)form.Invoke((Func<int>)delegate { return progress.Percent; }); } }
public void Finish(string title, string text, bool success) {
form.Invoke((Action)delegate {
closing = true;
stage.Text = title; detail.Text = text;
progress.Percent = success ? 100 : 0; percentage.Text = success ? "100%" : "";
steps.Complete = success;
if(!success) {
detail.Visible = false; steps.Visible = false;
var result = new TextBox { Text = text, Location = new Point(28,148), Size = new Size(484,126),
Multiline = true, ReadOnly = true, BorderStyle = BorderStyle.None,
BackColor = Color.White, ScrollBars = ScrollBars.Vertical };
form.Controls.Add(result); result.BringToFront(); progress.Visible = false;
}
footer.Text = success ? "Your settings and history are preserved." : "Review the details above.";
close.Enabled = true; done.Visible = true;
form.AcceptButton = done; form.CancelButton = done;
progress.Invalidate(); steps.Invalidate();
});
}
public void WaitForClose() { thread.Join(); }
public void Dispose() {
if (form != null && !form.IsDisposed)
form.BeginInvoke((Action)delegate { closing = true; form.Close(); });
thread.Join(3000);
}
}
'@
}
$script:progressWindow = New-Object KSwitcherUpdateProgress
}
function Show-UpdateProgress([string]$Stage,[string]$Detail,[int]$Percent = -1) {
if ($script:progressWindow) {
try { $script:progressWindow.Report($Stage,$Detail,$Percent) }
catch { Write-Warning 'Could not refresh update progress.' }
}
}
function Complete-UpdateProgress([string]$Title,[string]$Detail,[bool]$Success) {
if ($script:progressWindow) {
try { $script:progressWindow.Finish($Title,$Detail,$Success); return $true }
catch { Write-Warning 'Could not display the update result.' }
}
return $false
}
function Get-StableVersion([string]$Tag) {
if ($Tag -notmatch '^(?:v|release-v)?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$') { return $null }
@@ -20,7 +202,7 @@ function Assert-ReleaseUrl([string]$Url) {
throw 'Release asset URL is outside the KSwitcher repository.'
}
}
function Receive-File([string]$Url,[string]$Path,[long]$Limit) {
function Receive-File([string]$Url,[string]$Path,[long]$Limit,[string]$ProgressLabel = '') {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$request = [Net.HttpWebRequest]::Create($Url)
$request.AllowAutoRedirect = $false
@@ -36,11 +218,20 @@ function Receive-File([string]$Url,[string]$Path,[long]$Limit) {
$buffer = New-Object byte[] 65536
$total = 0L
$deadline = [DateTime]::UtcNow.AddSeconds(60)
$lastProgress = [DateTime]::MinValue
while (($count = $inputStream.Read($buffer,0,$buffer.Length)) -gt 0) {
$total += $count
if ($total -gt $Limit -or [DateTime]::UtcNow -gt $deadline) { throw 'Download exceeded size or time limit.' }
$outputStream.Write($buffer,0,$count)
if ($ProgressLabel -and ([DateTime]::UtcNow - $lastProgress).TotalMilliseconds -ge 100) {
$percent = if ($response.ContentLength -gt 0) { [int][Math]::Min(100,100.0*$total/$response.ContentLength) } else { -1 }
$detail = if ($percent -ge 0) { '{0}% - {1:N1} / {2:N1} MB' -f $percent,($total/1MB),($response.ContentLength/1MB) } else { '{0:N1} MB received' -f ($total/1MB) }
Show-UpdateProgress $ProgressLabel $detail $percent
$lastProgress = [DateTime]::UtcNow
}
}
if ($response.ContentLength -ge 0 -and $total -ne $response.ContentLength) { throw 'The download ended before the complete file was received.' }
if ($ProgressLabel) { Show-UpdateProgress $ProgressLabel ('Downloaded {0:N1} MB' -f ($total/1MB)) 100 }
} finally {
if ($outputStream) { $outputStream.Dispose() }
if ($inputStream) { $inputStream.Dispose() }
@@ -220,8 +411,9 @@ function Write-Result($Value) {
if ($Library) { return }
$transaction = $null; $parentProcess = $null; $ready = $null; $started = $null
$changed = [Collections.Generic.List[string]]::new()
$parentExited = $false; $success = $false; $preserveBackup = $false; $child = $null
$parentExited = $false; $success = $false; $preserveBackup = $false; $child = $null; $uiFinished = $false
try {
if ($ShowProgress -or $Mode -eq 'Install') { Start-UpdateProgress }
$current = Get-StableVersion $CurrentVersion
if (!$current) { throw 'Invalid installed version.' }
if ($Mode -eq 'Install') {
@@ -242,25 +434,35 @@ try {
$stage = Join-Path $transaction 'stage'; $backup = Join-Path $transaction 'backup'
[IO.Directory]::CreateDirectory($stage) | Out-Null
$archive = Join-Path $WorkDirectory 'release.zip'; $checksum = Join-Path $WorkDirectory 'SHA256SUMS.txt'
Receive-File $release.zip $archive 50MB
Show-UpdateProgress 'Downloading update' $release.version 0
Receive-File $release.zip $archive 50MB 'Downloading update'
Show-UpdateProgress 'Verifying download' 'Checking the archive SHA-256 checksum'
Receive-File $release.checksum $checksum 64KB
Assert-Checksum $archive $checksum
Show-UpdateProgress 'Preparing installation' 'Validating and unpacking the release files'
Expand-Release $archive $stage (Get-StableVersion $release.version)
$screenshotPath = [IO.Path]::GetFullPath((Join-Path $InstallDirectory 'KSwitcher.Screenshot.exe'))
if (Get-Process 'KSwitcher.Screenshot' -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq $screenshotPath }) { throw 'Close screenshot editors before installing an update.' }
Write-Result @{state='prepared';version=$release.version}
Show-UpdateProgress 'Closing KSwitcher' 'Waiting for the running application to exit safely'
$ready.Set() | Out-Null
if (!$parentProcess.WaitForExit(120000)) { throw 'KSwitcher did not exit; the installed files were not changed.' }
$parentExited = $true
Show-UpdateProgress 'Installing update' 'Replacing application files; keeping your personal data'
Install-Files $stage $InstallDirectory $backup $changed
$startedName = 'Local\KSwitcher.Update.Started.'+[guid]::NewGuid().ToString('N')
$started = [Threading.EventWaitHandle]::new($false,[Threading.EventResetMode]::ManualReset,$startedName)
Show-UpdateProgress 'Restarting KSwitcher' 'Waiting for the updated application to confirm startup'
$child = Start-Process -FilePath (Join-Path $InstallDirectory 'KSwitcher.exe') -ArgumentList "--update-ready-event=$startedName" -WorkingDirectory $InstallDirectory -WindowStyle Hidden -PassThru
if (!$started.WaitOne(30000) -or $child.HasExited) { throw 'The updated application did not confirm startup.' }
$success = $true
if ($script:progressWindow) {
$uiFinished = Complete-UpdateProgress 'Update completed' "KSwitcher $(Get-StableVersion $release.version) is now running." $true
}
} catch {
$message = $_.Exception.Message
if ($parentExited) {
Show-UpdateProgress 'Restoring previous version' 'The update failed; recovering the previous installation'
try {
if ($child -and !$child.HasExited) { $child.Kill(); $child.WaitForExit() }
Undo-Files $InstallDirectory $backup $changed
@@ -273,8 +475,12 @@ try {
}
Write-Result @{state='error';error=$message}
if ($ready -and $ready.WaitOne(0)) {
Add-Type -AssemblyName System.Windows.Forms
[Windows.Forms.MessageBox]::Show($message,'KSwitcher update failed') | Out-Null
if ($script:progressWindow) {
$uiFinished = Complete-UpdateProgress 'Update failed' $message $false
} else {
Add-Type -AssemblyName System.Windows.Forms
[Windows.Forms.MessageBox]::Show($message,'KSwitcher update failed') | Out-Null
}
}
exit 1
} finally {
@@ -291,4 +497,8 @@ try {
Remove-Item -LiteralPath $WorkDirectory -Recurse -Force -ErrorAction SilentlyContinue
}
}
if ($script:progressWindow) {
if ($uiFinished) { $script:progressWindow.WaitForClose() }
$script:progressWindow.Dispose(); $script:progressWindow = $null
}
}
+3 -3
View File
@@ -1387,7 +1387,7 @@ void App::checkUpdate(bool interactive, bool install, const std::wstring &versio
updateWorker = std::thread([this, interactive, install, version]
{
auto result = std::make_unique<UpdateResult>();
try { *result = runUpdate(install, version); }
try { *result = runUpdate(install, version, interactive); }
catch (const std::exception &e) { result->state = L"error"; result->error = wide(e.what()); }
result->interactive = interactive;
result->install = install;
@@ -1635,8 +1635,8 @@ int App::execute()
SetTimer(window, 2, 10000, nullptr);
else
SetTimer(window, 5, 5000, nullptr);
if (keyboardHook && mouseHook)
confirmUpdatedStartup(updateStartupEvent);
if (keyboardHook && mouseHook && confirmUpdatedStartup(updateStartupEvent))
log(std::wstring(L"Update completed; running version ") + AppVersion);
if (startCommand)
PostMessageW(window, WM_HOTKEY, startCommand, 0);
try { registerScreenshotNotifications(); }
+5 -4
View File
@@ -26,7 +26,7 @@ std::wstring quoteArgument(const std::wstring &value)
return out + L'"';
}
}
UpdateResult runUpdate(bool install, const std::wstring &expectedVersion)
UpdateResult runUpdate(bool install, const std::wstring &expectedVersion, bool showProgress)
{
UpdateResult result;
GUID guid{};
@@ -56,6 +56,7 @@ UpdateResult runUpdate(bool install, const std::wstring &expectedVersion)
L" -InstallDirectory " + quoteArgument(std::filesystem::path(executable()).parent_path().wstring()) +
L" -ParentProcessId " + std::to_wstring(GetCurrentProcessId()) + L" -ReadyEvent " + quoteArgument(readyName);
if (install) command += L" -ExpectedVersion " + quoteArgument(expectedVersion);
if (showProgress) command += L" -ShowProgress";
STARTUPINFOW startup{sizeof(startup)};
PROCESS_INFORMATION process{};
if (!CreateProcessW(powershell.c_str(), command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW,
@@ -94,10 +95,10 @@ UpdateResult runUpdate(bool install, const std::wstring &expectedVersion)
throw;
}
}
void confirmUpdatedStartup(const std::wstring &eventName)
bool confirmUpdatedStartup(const std::wstring &eventName)
{
if (!eventName.starts_with(L"Local\\KSwitcher.Update.Started.")) return;
if (!eventName.starts_with(L"Local\\KSwitcher.Update.Started.")) return false;
Handle event{OpenEventW(EVENT_MODIFY_STATE, FALSE, eventName.c_str())};
if (event.value) SetEvent(event.value);
return event.value && SetEvent(event.value);
}
}
+3 -3
View File
@@ -2,7 +2,7 @@
#include "platform.h"
namespace ks
{
inline constexpr wchar_t AppVersion[] = L"0.6.10";
inline constexpr wchar_t AppVersion[] = L"0.6.11";
inline constexpr UINT UpdateMessage = WM_APP + 7;
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
@@ -13,6 +13,6 @@ struct UpdateResult
std::wstring state, version, error;
bool interactive{}, install{};
};
UpdateResult runUpdate(bool install, const std::wstring &expectedVersion);
void confirmUpdatedStartup(const std::wstring &eventName);
UpdateResult runUpdate(bool install, const std::wstring &expectedVersion, bool showProgress = false);
bool confirmUpdatedStartup(const std::wstring &eventName);
}
+8
View File
@@ -55,6 +55,14 @@ int main(int argc, char **argv) {
// Message-only window: no hooks, synthetic input, clipboard changes or visible editor.
app.window=CreateWindowExW(0,L"STATIC",L"Input tests",0,0,0,0,0,HWND_MESSAGE,nullptr,nullptr,nullptr);
require(app.window!=nullptr,"message-only test window");
require(!ks::confirmUpdatedStartup(L""),"normal startup does not report update success");
require(!ks::confirmUpdatedStartup(L"Local.Other.Event"),"unrelated event cannot report update success");
auto startedName=L"Local\\KSwitcher.Update.Started.Test."+std::to_wstring(GetCurrentProcessId());
require(!ks::confirmUpdatedStartup(startedName),"missing updater handshake does not report success");
auto started=CreateEventW(nullptr,TRUE,FALSE,startedName.c_str());
require(started&&ks::confirmUpdatedStartup(startedName)&&WaitForSingleObject(started,0)==WAIT_OBJECT_0,
"successful startup signals the updater before reporting completion");
CloseHandle(started);
require(ks::UpdateCheckIntervalMs==21600000,"automatic update interval is six hours");
require(ks::InputRegressionAccess::updateTimer(app),"update timer rearms and avoids overlapping checks");
require(ks::InputRegressionAccess::updateResult(app,L"available",L"99.0.0")==L"99.0.0","background update records announced version");
+31
View File
@@ -0,0 +1,31 @@
param([string]$PreviewDirectory = (Join-Path $PSScriptRoot '../build/update-preview'), [switch]$Network)
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot '../resources/update.ps1') -Library
[IO.Directory]::CreateDirectory($PreviewDirectory) | Out-Null
Start-UpdateProgress
try {
if ($script:progressWindow.Status -ne 'Checking for updates') { throw 'Initial progress stage missing' }
Show-UpdateProgress 'Downloading update' '42% - 1.6 / 3.9 MB' 42
if ($script:progressWindow.Percent -ne 42) { throw 'Determinate download progress missing' }
$script:progressWindow.SavePreview((Join-Path $PreviewDirectory 'download.png'))
if ($Network) {
Receive-File 'https://gitea.hrynco.com/api/v1/repos/hrynco/hrynco-kswitcher/releases?limit=100' (Join-Path $PreviewDirectory 'releases.json') 2MB 'Downloading update'
if ($script:progressWindow.Percent -ne 100) { throw 'Real download did not finish at 100 percent' }
}
foreach ($stage in @('Verifying download','Preparing installation','Closing KSwitcher','Installing update','Restarting KSwitcher','Restoring previous version')) {
Show-UpdateProgress $stage 'Update stage under test'
if ($script:progressWindow.Status -ne $stage -or $script:progressWindow.Percent -ne -1) { throw "Invalid progress stage: $stage" }
}
$script:progressWindow.SavePreview((Join-Path $PreviewDirectory 'rollback.png'))
$script:progressWindow.Finish('Update completed','KSwitcher 0.6.11 is now running.',$true)
if ($script:progressWindow.Status -ne 'Update completed' -or $script:progressWindow.Percent -ne 100) { throw 'Completion state missing' }
Start-Sleep -Milliseconds 800 # Let the native progress-bar animation finish before capturing it.
$script:progressWindow.SavePreview((Join-Path $PreviewDirectory 'complete.png'))
$script:progressWindow.Finish('Update failed',"The updated application did not confirm startup.`r`nThe previous version has been restored and restarted.",$false)
if ($script:progressWindow.Status -ne 'Update failed' -or $script:progressWindow.Percent -ne 0) { throw 'Failure state missing' }
$script:progressWindow.SavePreview((Join-Path $PreviewDirectory 'failed.png'))
'Passed updater progress UI checks'
} finally {
$script:progressWindow.Dispose()
$script:progressWindow = $null
}