19 Commits

Author SHA1 Message Date
dfec8c07b5 Show 'Unsupported' in config dialog for monitors without input options
All checks were successful
Build / build (push) Successful in 8h0m10s
Build and Release / build (push) Successful in 9h0m14s
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 12:14:28 -05:00
bd7a58a5fe Fix retry loop - don't retry on timeout
All checks were successful
Build / build (push) Successful in 9h0m11s
Build and Release / build (push) Successful in 8h0m13s
- Fixed buggy recursive retry logic
- Reduced max retries from 5 to 2
- Don't retry if timeout occurred (empty result)
- Monitor is unresponsive, retrying just wastes time

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 12:01:47 -05:00
af8b09cfa2 Fix DDC/CI timeout using Task.WhenAny
All checks were successful
Build / build (push) Successful in 9h0m11s
Build and Release / build (push) Successful in 8h0m12s
Previous timeout using CancellationToken didn't work because
ReadToEndAsync blocks waiting for process output. Now using
Task.WhenAny with Task.Delay to properly timeout and kill
hung processes after 5 seconds.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 11:57:13 -05:00
77e9b0505a Fix csproj encoding issue in workflow
All checks were successful
Build / build (push) Successful in 8h0m9s
Build and Release / build (push) Successful in 9h0m16s
Use System.IO.File methods with UTF-8 BOM to preserve special characters

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 11:52:10 -05:00
526974da24 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>
2026-01-07 11:48:39 -05:00
b7597fd4d0 Test with PowerShell enabled
All checks were successful
Build / build (push) Successful in 8h0m23s
2026-01-07 16:34:55 +00:00
260379a330 Test with Git in PATH
Some checks failed
Build / build (push) Failing after 8h0m26s
2026-01-07 16:31:14 +00:00
e189731d24 Test with nvm PATH
Some checks failed
Build / build (push) Failing after 8h0m26s
2026-01-07 16:27:17 +00:00
c6a2b5b22f Test nvm node
Some checks failed
Build / build (push) Failing after 9h0m33s
2026-01-07 16:25:24 +00:00
c394320a3e Trigger build
Some checks failed
Build / build (push) Failing after 9h0m40s
2026-01-07 16:23:06 +00:00
0d810bbf49 Add debug logging with Show Log button
Some checks failed
Build / build (push) Failing after 8h0m9s
Build and Release / build (push) Successful in 8h0m16s
- DebugLogger class saves logs to %TEMP%\CMM\debug.log
- Show Log button appears after 3s if loading takes too long
- Log viewer with Copy to Clipboard and Open Log File options
- Detailed logging throughout LoadMonitors for debugging

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 09:46:09 -05:00
4039e70d0c Copy to C:\build to avoid path length limits
All checks were successful
Build and Release / build (push) Successful in 9h0m10s
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 10:56:07 -05:00
28c6cad153 Add SourceDir to ISS and set TEMP for ISCC
All checks were successful
Build and Release / build (push) Successful in 8h0m19s
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 10:52:11 -05:00
dda6bf1002 Try cmd shell with working-directory for ISCC
All checks were successful
Build and Release / build (push) Successful in 9h0m11s
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 10:29:56 -05:00
821f119547 Add verbose output to ISCC for debugging
All checks were successful
Build and Release / build (push) Successful in 8h0m9s
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 10:05:04 -05:00
a33430faa0 Use Push-Location and Start-Process for ISCC
All checks were successful
Build and Release / build (push) Successful in 9h0m11s
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 10:01:45 -05:00
401d717adf Add debug output and create installer dir before ISCC
All checks were successful
Build and Release / build (push) Successful in 8h0m10s
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 09:57:48 -05:00
cfac39a838 Fix Inno Setup path issue - use PowerShell Set-Location
All checks were successful
Build and Release / build (push) Successful in 9h0m9s
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 08:20:38 -05:00
a408b0fd27 Add debug step to check build output
All checks were successful
Build and Release / build (push) Successful in 8h0m10s
2026-01-04 08:16:26 -05:00
11 changed files with 475 additions and 43 deletions

View File

@@ -17,23 +17,70 @@ 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 (preserve UTF-8 BOM encoding)
$csprojPath = "DellMonitorControl/DellMonitorControl.csproj"
$content = [System.IO.File]::ReadAllText($csprojPath)
$content = $content -replace '<Version>.*</Version>', "<Version>$version</Version>"
$content = $content -replace '<AssemblyVersion>.*</AssemblyVersion>', "<AssemblyVersion>$version.0</AssemblyVersion>"
$content = $content -replace '<FileVersion>.*</FileVersion>', "<FileVersion>$version.0</FileVersion>"
[System.IO.File]::WriteAllText($csprojPath, $content, [System.Text.UTF8Encoding]::new($true))
# Update ISS
$issPath = "DellMonitorControl/MonitorControl.iss"
$issContent = [System.IO.File]::ReadAllText($issPath)
$issContent = $issContent -replace '#define MyAppVersion ".*"', "#define MyAppVersion `"$version`""
[System.IO.File]::WriteAllText($issPath, $issContent, [System.Text.UTF8Encoding]::new($true))
- name: Restore dependencies
run: dotnet restore
- name: Build Release
run: dotnet build --configuration Release --no-restore
- name: Update version in ISS
shell: powershell
- name: List build output
shell: cmd
run: |
$version = "${{ gitea.ref_name }}".TrimStart("v")
$issPath = "DellMonitorControl/MonitorControl.iss"
(Get-Content $issPath) -replace '#define MyAppVersion ".*"', "#define MyAppVersion `"$version`"" | Set-Content $issPath
echo "Current directory:"
cd
echo "DellMonitorControl contents:"
dir DellMonitorControl
echo "DellMonitorControl\bin contents:"
dir DellMonitorControl\bin
echo "DellMonitorControl\bin\Release contents:"
dir DellMonitorControl\bin\Release
echo "DellMonitorControl\bin\Release\net9.0-windows contents:"
dir DellMonitorControl\bin\Release\net9.0-windows
- name: Build Installer
shell: cmd
run: |
cd DellMonitorControl && "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" MonitorControl.iss
echo Copying to short path to avoid path length issues...
mkdir C:\build 2>nul
xcopy /E /I /Y DellMonitorControl C:\build\app
echo.
echo Working from C:\build\app
cd /d C:\build\app
echo Current directory:
cd
echo.
set TEMP=C:\build\temp
set TMP=C:\build\temp
mkdir C:\build\temp 2>nul
mkdir installer 2>nul
echo.
echo Running Inno Setup...
"C:\Program Files (x86)\Inno Setup 6\ISCC.exe" MonitorControl.iss
echo.
echo ISCC exit code: %ERRORLEVEL%
echo.
echo Installer directory contents:
dir installer
- name: Create Release
shell: powershell
@@ -66,7 +113,7 @@ jobs:
}
# Upload installer
$filePath = "DellMonitorControl/installer/MonitorControl-Setup-$version.exe"
$filePath = "C:\build\app\installer\MonitorControl-Setup-$version.exe"
$fileName = "MonitorControl-Setup-$version.exe"
Write-Host "Uploading $fileName..."

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

@@ -34,6 +34,22 @@ public partial class ConfigWindow : Window
spPorts.Children.Clear();
_portRows.Clear();
// Show unsupported message if no ports available
if (_availablePorts == null || _availablePorts.Count == 0)
{
spPorts.Children.Add(new TextBlock
{
Text = "Unsupported",
Foreground = Brushes.Gray,
FontSize = 16,
FontStyle = FontStyles.Italic,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 40, 0, 40)
});
return;
}
foreach (var port in _availablePorts)
{
var existingPortConfig = config.Ports.FirstOrDefault(p => p.VcpValue == port.Value);

View File

@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace DellMonitorControl;
public static class DebugLogger
{
private static readonly List<string> _logs = new();
private static readonly object _lock = new();
private static readonly string _logFilePath;
static DebugLogger()
{
var tempPath = Path.Combine(Path.GetTempPath(), "CMM");
Directory.CreateDirectory(tempPath);
_logFilePath = Path.Combine(tempPath, "debug.log");
}
public static void Log(string message)
{
var entry = $"[{DateTime.Now:HH:mm:ss.fff}] {message}";
lock (_lock)
{
_logs.Add(entry);
try
{
File.AppendAllText(_logFilePath, entry + Environment.NewLine);
}
catch { }
}
}
public static void LogError(string message, Exception? ex = null)
{
var entry = ex != null
? $"[{DateTime.Now:HH:mm:ss.fff}] ERROR: {message} - {ex.GetType().Name}: {ex.Message}"
: $"[{DateTime.Now:HH:mm:ss.fff}] ERROR: {message}";
lock (_lock)
{
_logs.Add(entry);
if (ex != null)
_logs.Add($" Stack: {ex.StackTrace}");
try
{
File.AppendAllText(_logFilePath, entry + Environment.NewLine);
if (ex != null)
File.AppendAllText(_logFilePath, $" Stack: {ex.StackTrace}" + Environment.NewLine);
}
catch { }
}
}
public static string GetLogs()
{
lock (_lock)
{
return string.Join(Environment.NewLine, _logs);
}
}
public static void Clear()
{
lock (_lock)
{
_logs.Clear();
try
{
File.WriteAllText(_logFilePath, "");
}
catch { }
}
}
public static string LogFilePath => _logFilePath;
}

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.16</Version>
<AssemblyVersion>1.0.16.0</AssemblyVersion>
<FileVersion>1.0.16.0</FileVersion>
</PropertyGroup>
<ItemGroup>

View File

@@ -215,6 +215,9 @@
</Ellipse>
<TextBlock Name="loadingText" Text="Loading..." Foreground="LightGray" FontSize="12"
HorizontalAlignment="Center"/>
<Button Name="showLogButton" Content="Show Log" Margin="0,10,0,0"
Style="{StaticResource DarkButton}" FontSize="10"
Click="ShowLogButton_Click" Visibility="Collapsed"/>
</StackPanel>
</StackPanel>
</ScrollViewer>

View File

@@ -9,6 +9,7 @@ using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Threading;
namespace DellMonitorControl;
@@ -16,15 +17,18 @@ public partial class MainWindow : Window
{
private List<(XMonitor Monitor, List<InputSourceOption> Options)> _loadedMonitors = new();
private Storyboard? _spinnerStoryboard;
private DispatcherTimer? _showLogButtonTimer;
public MainWindow()
{
InitializeComponent();
_spinnerStoryboard = (Storyboard)FindResource("SpinnerAnimation");
DebugLogger.Log("MainWindow initialized");
}
public void ShowNearTray()
{
DebugLogger.Log("ShowNearTray called");
var workArea = SystemParameters.WorkArea;
Left = workArea.Right - Width - 10;
Top = workArea.Bottom - 350;
@@ -34,10 +38,25 @@ public partial class MainWindow : Window
// Start spinner
_spinnerStoryboard?.Begin(this, true);
// Show log button after 3 seconds if still loading
showLogButton.Visibility = Visibility.Collapsed;
_showLogButtonTimer?.Stop();
_showLogButtonTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(3) };
_showLogButtonTimer.Tick += (s, e) =>
{
_showLogButtonTimer.Stop();
if (loadingPanel.Visibility == Visibility.Visible || sp.Children.Contains(loadingPanel))
{
showLogButton.Visibility = Visibility.Visible;
DebugLogger.Log("Show Log button displayed (loading took > 3s)");
}
};
_showLogButtonTimer.Start();
Dispatcher.BeginInvoke(new Action(() =>
{
Top = workArea.Bottom - ActualHeight - 10;
}), System.Windows.Threading.DispatcherPriority.Loaded);
}), DispatcherPriority.Loaded);
}
private void Window_Deactivated(object sender, EventArgs e)
@@ -51,18 +70,86 @@ public partial class MainWindow : Window
Application.Current.Shutdown();
}
private void ShowLogButton_Click(object sender, RoutedEventArgs e)
{
var logWindow = new Window
{
Title = "Debug Log",
Width = 600,
Height = 400,
WindowStartupLocation = WindowStartupLocation.CenterScreen,
Background = new SolidColorBrush(Color.FromRgb(45, 45, 45))
};
var grid = new Grid();
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
var textBox = new TextBox
{
Text = DebugLogger.GetLogs(),
IsReadOnly = true,
Background = new SolidColorBrush(Color.FromRgb(30, 30, 30)),
Foreground = Brushes.LightGray,
FontFamily = new FontFamily("Consolas"),
FontSize = 11,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
HorizontalScrollBarVisibility = ScrollBarVisibility.Auto,
TextWrapping = TextWrapping.NoWrap,
Margin = new Thickness(10)
};
Grid.SetRow(textBox, 0);
grid.Children.Add(textBox);
var buttonPanel = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(10) };
var copyBtn = new Button { Content = "Copy to Clipboard", Padding = new Thickness(10, 5, 10, 5), Margin = new Thickness(0, 0, 10, 0) };
copyBtn.Click += (s, args) =>
{
Clipboard.SetText(DebugLogger.GetLogs());
MessageBox.Show("Log copied to clipboard!", "Debug Log", MessageBoxButton.OK, MessageBoxImage.Information);
};
buttonPanel.Children.Add(copyBtn);
var openFileBtn = new Button { Content = "Open Log File", Padding = new Thickness(10, 5, 10, 5), Margin = new Thickness(0, 0, 10, 0) };
openFileBtn.Click += (s, args) =>
{
try
{
System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{DebugLogger.LogFilePath}\"");
}
catch { }
};
buttonPanel.Children.Add(openFileBtn);
var closeBtn = new Button { Content = "Close", Padding = new Thickness(10, 5, 10, 5) };
closeBtn.Click += (s, args) => logWindow.Close();
buttonPanel.Children.Add(closeBtn);
Grid.SetRow(buttonPanel, 1);
grid.Children.Add(buttonPanel);
logWindow.Content = grid;
logWindow.Show();
}
public async Task LoadMonitors()
{
DebugLogger.Log("LoadMonitors started");
var newChildren = new List<UIElement>();
_loadedMonitors.Clear();
try
{
DebugLogger.Log("Scanning for monitors...");
await CMMCommand.ScanMonitor();
DebugLogger.Log("Scan complete, reading monitor data...");
var monitors = (await CMMCommand.ReadMonitorsData()).ToList();
DebugLogger.Log($"Found {monitors.Count} monitor(s)");
if (!monitors.Any())
{
DebugLogger.Log("No DDC/CI monitors detected");
newChildren.Add(new TextBlock
{
Text = "No DDC/CI monitors detected",
@@ -76,16 +163,33 @@ public partial class MainWindow : Window
{
foreach (var m in monitors)
{
DebugLogger.Log($"Processing monitor: {m.MonitorName} (SN: {m.SerialNumber})");
DebugLogger.Log($" Getting brightness...");
var brightness = await CMMCommand.GetBrightness(m.SerialNumber) ?? 50;
DebugLogger.Log($" Brightness: {brightness}");
DebugLogger.Log($" Getting contrast...");
var contrast = await CMMCommand.GetContrast(m.SerialNumber) ?? 50;
DebugLogger.Log($" Contrast: {contrast}");
DebugLogger.Log($" Getting input source...");
var inputSource = await CMMCommand.GetInputSource(m.SerialNumber);
DebugLogger.Log($" Input source: {inputSource}");
DebugLogger.Log($" Getting input options...");
var inputOptions = await CMMCommand.GetInputSourceOptions(m.SerialNumber);
DebugLogger.Log($" Input options count: {inputOptions.Count}");
DebugLogger.Log($" Getting power status...");
var powerStatus = await CMMCommand.GetMonPowerStatus(m.SerialNumber) ?? "Unknown";
DebugLogger.Log($" Power status: {powerStatus}");
_loadedMonitors.Add((m, inputOptions));
// Apply config to filter hidden ports and use custom labels
var filteredOptions = MonitorConfigManager.ApplyConfigToOptions(m.SerialNumber, inputOptions);
DebugLogger.Log($" Filtered options count: {filteredOptions.Count}");
// Monitor name header with Config button
newChildren.Add(CreateMonitorHeader(m, inputOptions));
@@ -100,6 +204,8 @@ public partial class MainWindow : Window
// Separator
newChildren.Add(new Border { Height = 1, Background = new SolidColorBrush(Color.FromRgb(80, 80, 80)), Margin = new Thickness(0, 10, 0, 2) });
DebugLogger.Log($" Monitor {m.MonitorName} processing complete");
}
// Remove last separator
@@ -108,27 +214,33 @@ public partial class MainWindow : Window
}
// Load quick-switch toolbar
DebugLogger.Log("Loading quick-switch toolbar...");
LoadQuickSwitchToolbar();
DebugLogger.Log("Quick-switch toolbar loaded");
}
catch (Exception ex)
{
DebugLogger.LogError("LoadMonitors failed", ex);
newChildren.Clear();
newChildren.Add(new TextBlock { Text = $"Error: {ex.Message}", Foreground = Brushes.OrangeRed, FontSize = 11, TextWrapping = TextWrapping.Wrap });
}
// Stop spinner
// Stop spinner and timer
_spinnerStoryboard?.Stop(this);
_showLogButtonTimer?.Stop();
sp.VerticalAlignment = VerticalAlignment.Top;
sp.Children.Clear();
foreach (var child in newChildren)
sp.Children.Add(child);
DebugLogger.Log("LoadMonitors complete, UI updated");
Dispatcher.BeginInvoke(new Action(() =>
{
var workArea = SystemParameters.WorkArea;
Top = workArea.Bottom - ActualHeight - 10;
}), System.Windows.Threading.DispatcherPriority.Loaded);
}), DispatcherPriority.Loaded);
}
private void LoadQuickSwitchToolbar()

View File

@@ -2,8 +2,10 @@
#define MyAppVersion "1.0.0"
#define MyAppPublisher "Dang"
#define MyAppExeName "DellMonitorControl.exe"
#define SourcePath "bin\Release\net9.0-windows"
[Setup]
SourceDir=.
AppId={{8F3E4A2B-1C5D-4E6F-9A8B-7C2D1E3F4A5B}
AppName={#MyAppName}
AppVersion={#MyAppVersion}

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,18 @@ internal class ConsoleHelper
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.FileName = command;
p.Start();
var output = await p.StandardOutput.ReadToEndAsync();
await p.WaitForExitAsync();
return output;
var readTask = p.StandardOutput.ReadToEndAsync();
var completedTask = await Task.WhenAny(readTask, Task.Delay(timeoutMs));
if (completedTask != readTask)
{
// Timeout - kill the process
try { p.Kill(true); } catch { }
return string.Empty;
}
return await readTask;
}
public static async Task<string> CmdCommandAsync(params string[] cmds) =>
@@ -40,6 +48,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 +61,29 @@ 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();
var readTask = Task.Run(async () =>
{
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;
return result;
});
var completedTask = await Task.WhenAny(readTask, Task.Delay(timeoutMs));
if (completedTask != readTask)
{
// Timeout - kill the process
try { p.Kill(true); } catch { }
return string.Empty;
}
var output = await readTask;
p.Close();
Debug.WriteLine(result);
return result;
Debug.WriteLine(output);
return output;
}
public static string Command(string fileName, params string[] cmds)

View File

@@ -30,26 +30,32 @@ public static class CMMCommand
return ConsoleHelper.CmdCommandAsync($"{CMMexe} /SetValue {monitorSN} D6 4");
}
private static async Task<string> GetMonitorValue(string monitorSN, string vcpCode = "D6", int? reTry = 0)
private static async Task<string> GetMonitorValue(string monitorSN, string vcpCode = "D6", int maxRetries = 2)
{
var value = string.Empty;
while (reTry <= 5)
for (int attempt = 0; attempt <= maxRetries; attempt++)
{
var cmdFileName = Path.Combine(CMMTmpFolder, $"{Guid.NewGuid()}.bat");
var cmd = $"{CMMexe} /GetValue {monitorSN} {vcpCode}\r\n" +
$"echo %errorlevel%";
File.WriteAllText(cmdFileName, cmd);
var values = await ConsoleHelper.ExecuteCommand(cmdFileName);
File.Delete(cmdFileName);
try { File.Delete(cmdFileName); } catch { }
value = values.Split("\r\n", StringSplitOptions.RemoveEmptyEntries).LastOrDefault();
// Empty result means timeout - don't retry, monitor is unresponsive
if (string.IsNullOrEmpty(values))
return string.Empty;
if (!string.IsNullOrEmpty(value) && value != "0") return value;
await Task.Delay(500);
await GetMonitorValue(monitorSN, vcpCode, reTry++);
};
var value = values.Split("\r\n", StringSplitOptions.RemoveEmptyEntries).LastOrDefault();
return value;
if (!string.IsNullOrEmpty(value) && value != "0")
return value;
// Only retry on non-timeout failures
if (attempt < maxRetries)
await Task.Delay(300);
}
return string.Empty;
}
#region Brightness (VCP Code 10)