Add auto-update check and DDC/CI timeout fix
Some checks failed
Build / build (push) Successful in 8h0m12s
Build and Release / build (push) Failing after 9h0m41s

- 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 <noreply@anthropic.com>
This commit is contained in:
David H. Friedel Jr. 2026-01-07 11:48:19 -05:00
parent b7597fd4d0
commit 526974da24
5 changed files with 205 additions and 31 deletions

View File

@ -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>.*</Version>', "<Version>$version</Version>" | Set-Content $csprojPath
(Get-Content $csprojPath) -replace '<AssemblyVersion>.*</AssemblyVersion>', "<AssemblyVersion>$version.0</AssemblyVersion>" | Set-Content $csprojPath
(Get-Content $csprojPath) -replace '<FileVersion>.*</FileVersion>', "<FileVersion>$version.0</FileVersion>" | 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: |

View File

@ -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)

View File

@ -12,17 +12,10 @@
<Copyright>Copyright © DangWang $([System.DateTime]::Now.ToString(yyyy))</Copyright>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<Major>1</Major>
<Minor>0</Minor>
<ProjectStartedDate>$([System.DateTime]::op_Subtraction($([System.DateTime]::get_Now().get_Date()),$([System.DateTime]::new(2023,7,2))).get_TotalDays())</ProjectStartedDate>
<DaysSinceProjectStarted>$([System.DateTime]::Now.ToString(Hmm))</DaysSinceProjectStarted>
<DateTimeSuffix>$([System.DateTime]::Now.ToString(yyyyMMdd))</DateTimeSuffix>
<VersionSuffix>$(Major).$(Minor).$(ProjectStartedDate).$(DaysSinceProjectStarted)</VersionSuffix>
<AssemblyVersion Condition=" '$(DateTimeSuffix)' == '' ">0.0.0.1</AssemblyVersion>
<AssemblyVersion Condition=" '$(DateTimeSuffix)' != '' ">$(VersionSuffix)</AssemblyVersion>
<Version Condition=" '$(DateTimeSuffix)' == '' ">0.0.0.1</Version>
<Version Condition=" '$(DateTimeSuffix)' != '' ">$(DateTimeSuffix)</Version>
<PropertyGroup>
<Version>1.0.13</Version>
<AssemblyVersion>1.0.13.0</AssemblyVersion>
<FileVersion>1.0.13.0</FileVersion>
</PropertyGroup>
<ItemGroup>

View File

@ -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<UpdateInfo?> CheckForUpdateAsync()
{
try
{
DebugLogger.Log($"Checking for updates... Current version: {CurrentVersion}");
var response = await _httpClient.GetStringAsync(ReleasesApiUrl);
var release = JsonSerializer.Deserialize<GiteaRelease>(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; }
}

View File

@ -19,7 +19,7 @@ internal class ConsoleHelper
}
};
public static async Task<string> ExecuteCommand(string command)
public static async Task<string> 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<string> CmdCommandAsync(params string[] cmds) =>
@ -40,6 +49,11 @@ internal class ConsoleHelper
Command(cmdFileName, cmds);
public static async Task<string> CommandAsync(string fileName, params string[] cmds)
{
return await CommandAsync(fileName, 5000, cmds);
}
public static async Task<string> 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<Error Message>:\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<Error Message>:\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)