9 Commits

Author SHA1 Message Date
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
8 changed files with 407 additions and 33 deletions

View File

@@ -17,19 +17,32 @@ 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
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

@@ -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.14</Version>
<AssemblyVersion>1.0.14.0</AssemblyVersion>
<FileVersion>1.0.14.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

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