Release v0.6.11: show branded update progress and completion
This commit is contained in:
+3
-3
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user