From 526974da2493323abd901cc70ca2d5e96d216bf4 Mon Sep 17 00:00:00 2001 From: Dave Friedel Date: Wed, 7 Jan 2026 11:48:19 -0500 Subject: [PATCH] Add auto-update check and DDC/CI timeout fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Auto-update checker on startup via Gitea API - Balloon notification when update available, click to download - 5-second timeout on DDC/CI commands to prevent hangs - Simplified version scheme to match release tags - Workflow auto-updates version in csproj from tag 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .gitea/workflows/release.yaml | 23 ++-- DellMonitorControl/App.xaml.cs | 31 ++++- DellMonitorControl/DellMonitorControl.csproj | 15 +-- DellMonitorControl/UpdateChecker.cs | 119 +++++++++++++++++++ Library/Helpers/ConsoleHelper.cs | 48 ++++++-- 5 files changed, 205 insertions(+), 31 deletions(-) create mode 100644 DellMonitorControl/UpdateChecker.cs diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index b686947..2b6ecbe 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -17,19 +17,28 @@ jobs: with: dotnet-version: '9.0.x' + - name: Update version in project files + shell: powershell + run: | + $version = "${{ gitea.ref_name }}".TrimStart("v") + Write-Host "Setting version to: $version" + + # Update csproj + $csprojPath = "DellMonitorControl/DellMonitorControl.csproj" + (Get-Content $csprojPath) -replace '.*', "$version" | Set-Content $csprojPath + (Get-Content $csprojPath) -replace '.*', "$version.0" | Set-Content $csprojPath + (Get-Content $csprojPath) -replace '.*', "$version.0" | Set-Content $csprojPath + + # Update ISS + $issPath = "DellMonitorControl/MonitorControl.iss" + (Get-Content $issPath) -replace '#define MyAppVersion ".*"', "#define MyAppVersion `"$version`"" | Set-Content $issPath + - name: Restore dependencies run: dotnet restore - name: Build Release run: dotnet build --configuration Release --no-restore - - name: Update version in ISS - shell: powershell - run: | - $version = "${{ gitea.ref_name }}".TrimStart("v") - $issPath = "DellMonitorControl/MonitorControl.iss" - (Get-Content $issPath) -replace '#define MyAppVersion ".*"', "#define MyAppVersion `"$version`"" | Set-Content $issPath - - name: List build output shell: cmd run: | diff --git a/DellMonitorControl/App.xaml.cs b/DellMonitorControl/App.xaml.cs index 7fcae20..fb7f718 100644 --- a/DellMonitorControl/App.xaml.cs +++ b/DellMonitorControl/App.xaml.cs @@ -7,13 +7,42 @@ namespace DellMonitorControl { private TaskbarIcon? _trayIcon; private MainWindow? _mainWindow; + private UpdateInfo? _pendingUpdate; - private void Application_Startup(object sender, StartupEventArgs e) + private async void Application_Startup(object sender, StartupEventArgs e) { _trayIcon = (TaskbarIcon)FindResource("TrayIcon"); _trayIcon.TrayLeftMouseUp += TrayIcon_Click; + _trayIcon.TrayBalloonTipClicked += TrayIcon_BalloonTipClicked; _mainWindow = new MainWindow(); + + // Check for updates in background + await CheckForUpdatesAsync(); + } + + private async System.Threading.Tasks.Task CheckForUpdatesAsync() + { + try + { + _pendingUpdate = await UpdateChecker.CheckForUpdateAsync(); + if (_pendingUpdate != null && _trayIcon != null) + { + _trayIcon.ShowBalloonTip( + "Update Available", + $"Monitor Control v{_pendingUpdate.LatestVersion} is available.\nClick to download.", + BalloonIcon.Info); + } + } + catch { } + } + + private void TrayIcon_BalloonTipClicked(object sender, RoutedEventArgs e) + { + if (_pendingUpdate != null && !string.IsNullOrEmpty(_pendingUpdate.DownloadUrl)) + { + UpdateChecker.OpenDownloadPage(_pendingUpdate.DownloadUrl); + } } private async void TrayIcon_Click(object sender, RoutedEventArgs e) diff --git a/DellMonitorControl/DellMonitorControl.csproj b/DellMonitorControl/DellMonitorControl.csproj index 97da5cb..811b3bd 100644 --- a/DellMonitorControl/DellMonitorControl.csproj +++ b/DellMonitorControl/DellMonitorControl.csproj @@ -12,17 +12,10 @@ Copyright © DangWang $([System.DateTime]::Now.ToString(yyyy)) - - 1 - 0 - $([System.DateTime]::op_Subtraction($([System.DateTime]::get_Now().get_Date()),$([System.DateTime]::new(2023,7,2))).get_TotalDays()) - $([System.DateTime]::Now.ToString(Hmm)) - $([System.DateTime]::Now.ToString(yyyyMMdd)) - $(Major).$(Minor).$(ProjectStartedDate).$(DaysSinceProjectStarted) - 0.0.0.1 - $(VersionSuffix) - 0.0.0.1 - $(DateTimeSuffix) + + 1.0.13 + 1.0.13.0 + 1.0.13.0 diff --git a/DellMonitorControl/UpdateChecker.cs b/DellMonitorControl/UpdateChecker.cs new file mode 100644 index 0000000..c71a773 --- /dev/null +++ b/DellMonitorControl/UpdateChecker.cs @@ -0,0 +1,119 @@ +using System; +using System.Diagnostics; +using System.Net.Http; +using System.Reflection; +using System.Text.Json; +using System.Threading.Tasks; + +namespace DellMonitorControl; + +public class UpdateChecker +{ + private const string ReleasesApiUrl = "https://git.marketally.com/api/v1/repos/misc/ControlMyMonitorManagement/releases/latest"; + private static readonly HttpClient _httpClient = new() { Timeout = TimeSpan.FromSeconds(10) }; + + public static Version CurrentVersion => Assembly.GetExecutingAssembly().GetName().Version ?? new Version(1, 0, 0); + + public static async Task CheckForUpdateAsync() + { + try + { + DebugLogger.Log($"Checking for updates... Current version: {CurrentVersion}"); + + var response = await _httpClient.GetStringAsync(ReleasesApiUrl); + var release = JsonSerializer.Deserialize(response); + + if (release == null || string.IsNullOrEmpty(release.tag_name)) + { + DebugLogger.Log("No release info found"); + return null; + } + + var latestVersionStr = release.tag_name.TrimStart('v', 'V'); + if (!Version.TryParse(latestVersionStr, out var latestVersion)) + { + DebugLogger.Log($"Could not parse version: {release.tag_name}"); + return null; + } + + DebugLogger.Log($"Latest version: {latestVersion}"); + + if (latestVersion > CurrentVersion) + { + // Find the installer asset + string? downloadUrl = null; + if (release.assets != null) + { + foreach (var asset in release.assets) + { + if (asset.name?.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) == true) + { + downloadUrl = asset.browser_download_url; + break; + } + } + } + + // Fallback to release page if no direct download + downloadUrl ??= release.html_url; + + DebugLogger.Log($"Update available: {latestVersion}, URL: {downloadUrl}"); + + return new UpdateInfo + { + CurrentVersion = CurrentVersion, + LatestVersion = latestVersion, + DownloadUrl = downloadUrl ?? "", + ReleaseNotes = release.body ?? "" + }; + } + + DebugLogger.Log("No update available"); + return null; + } + catch (Exception ex) + { + DebugLogger.LogError("Update check failed", ex); + return null; + } + } + + public static void OpenDownloadPage(string url) + { + try + { + Process.Start(new ProcessStartInfo + { + FileName = url, + UseShellExecute = true + }); + } + catch (Exception ex) + { + DebugLogger.LogError("Failed to open download page", ex); + } + } +} + +public class UpdateInfo +{ + public Version CurrentVersion { get; set; } = new(1, 0, 0); + public Version LatestVersion { get; set; } = new(1, 0, 0); + public string DownloadUrl { get; set; } = ""; + public string ReleaseNotes { get; set; } = ""; +} + +public class GiteaRelease +{ + public string? tag_name { get; set; } + public string? name { get; set; } + public string? body { get; set; } + public string? html_url { get; set; } + public GiteaAsset[]? assets { get; set; } +} + +public class GiteaAsset +{ + public string? name { get; set; } + public string? browser_download_url { get; set; } +} diff --git a/Library/Helpers/ConsoleHelper.cs b/Library/Helpers/ConsoleHelper.cs index f6c50b7..c8fefc1 100644 --- a/Library/Helpers/ConsoleHelper.cs +++ b/Library/Helpers/ConsoleHelper.cs @@ -19,7 +19,7 @@ internal class ConsoleHelper } }; - public static async Task ExecuteCommand(string command) + public static async Task ExecuteCommand(string command, int timeoutMs = 5000) { Process p = new Process(); p.StartInfo.UseShellExecute = false; @@ -27,10 +27,19 @@ internal class ConsoleHelper p.StartInfo.RedirectStandardOutput = true; p.StartInfo.FileName = command; p.Start(); - var output = await p.StandardOutput.ReadToEndAsync(); - await p.WaitForExitAsync(); - return output; + using var cts = new CancellationTokenSource(timeoutMs); + try + { + var output = await p.StandardOutput.ReadToEndAsync(cts.Token); + await p.WaitForExitAsync(cts.Token); + return output; + } + catch (OperationCanceledException) + { + try { p.Kill(); } catch { } + return string.Empty; + } } public static async Task CmdCommandAsync(params string[] cmds) => @@ -40,6 +49,11 @@ internal class ConsoleHelper Command(cmdFileName, cmds); public static async Task CommandAsync(string fileName, params string[] cmds) + { + return await CommandAsync(fileName, 5000, cmds); + } + + public static async Task CommandAsync(string fileName, int timeoutMs, params string[] cmds) { var p = CreatProcess(fileName); p.Start(); @@ -48,14 +62,24 @@ internal class ConsoleHelper p.StandardInput.WriteLine(cmd); } p.StandardInput.WriteLine("exit"); - var result = await p.StandardOutput.ReadToEndAsync(); - var error = await p.StandardError.ReadToEndAsync(); - if (!string.IsNullOrWhiteSpace(error)) - result = result + "\r\n:\r\n" + error; - await p.WaitForExitAsync(); - p.Close(); - Debug.WriteLine(result); - return result; + + using var cts = new CancellationTokenSource(timeoutMs); + try + { + var result = await p.StandardOutput.ReadToEndAsync(cts.Token); + var error = await p.StandardError.ReadToEndAsync(cts.Token); + if (!string.IsNullOrWhiteSpace(error)) + result = result + "\r\n:\r\n" + error; + await p.WaitForExitAsync(cts.Token); + p.Close(); + Debug.WriteLine(result); + return result; + } + catch (OperationCanceledException) + { + try { p.Kill(); } catch { } + return string.Empty; + } } public static string Command(string fileName, params string[] cmds)