Skip to content

Refactor cleanup logic and improve argument validation in AsusFanControl - #6

Merged
beenycool merged 2 commits into
masterfrom
fix/cleanup-validation-refactor-2926545181562135474
Jan 28, 2026
Merged

Refactor cleanup logic and improve argument validation in AsusFanControl#6
beenycool merged 2 commits into
masterfrom
fix/cleanup-validation-refactor-2926545181562135474

Conversation

@beenycool

@beenycool beenycool commented Jan 28, 2026

Copy link
Copy Markdown
Owner
  • Implemented IDisposable and ResetToDefault in AsusControl to ensure proper resource cleanup.
  • Refactored Program.cs to use int.TryParse and validate fan ID range (0-255).
  • Added ProcessExit handler and try-finally block in Program.cs to ensure fan reset on exit.
  • Updated Form1.cs to wrap ResetToDefault in try-catch and ensure Dispose is called in finally block during process exit.

PR created automatically by Jules for task 2926545181562135474 started by @beenycool

Summary by CodeRabbit

  • New Features

    • Added command-line interface options to query and control fan speeds and retrieve temperature data.
  • Refactor

    • Improved resource cleanup and shutdown behavior with enhanced error handling during application exit.

✏️ Tip: You can customize this high-level summary in your review settings.

Co-authored-by: beenycool <129210955+beenycool@users.noreply.github.com>
@google-labs-jules

Copy link
Copy Markdown

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello @beenycool, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the stability and reliability of the Asus fan control application by implementing proper resource cleanup mechanisms and improving argument validation. It ensures that fan settings are consistently reset to default upon application termination, preventing unintended fan behavior, and makes the command-line interface more robust against invalid user input.

Highlights

  • Resource Management: The AsusControl class now implements IDisposable for explicit resource management and includes a ResetToDefault method to restore fan settings to their default state.
  • Robust Argument Handling: The console application (Program.cs) has been enhanced with int.TryParse for safer argument parsing, includes validation for fan ID inputs (0-255), and ensures fan speeds are reset on application exit using a ProcessExit handler and a try-finally block.
  • Graceful Shutdown in GUI: The GUI application (Form1.cs) now safely calls ResetToDefault and Dispose on exit, incorporating error handling within a try-catch-finally block for a more robust shutdown process.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai

coderabbitai Bot commented Jan 28, 2026

Copy link
Copy Markdown

Walkthrough

This pull request improves resource management by implementing the IDisposable pattern in AsusControl, introducing a ResetToDefault() method to safely reset all fans, and updating all three entry points to use a consistent cleanup sequence with error handling and disposal guarantees.

Changes

Cohort / File(s) Change Summary
Core Resource Management
AsusFanControl/AsusControl.cs
Implements IDisposable interface with explicit Dispose() method that calls ShutdownWinIo() and GC.SuppressFinalize(). Adds ResetToDefault() method to iterate over all fans and reset speeds to 0 with 20ms delays. Replaces destructor with proper disposal pattern.
CLI Command Handling & Cleanup
AsusFanControl/Program.cs
Introduces ProcessExit handler with skipResetOnExit flag for conditional cleanup. Wraps argument processing in try/finally block ensuring guaranteed disposal. Adds comprehensive command support: --get-fan-speeds, --set-fan-speeds, --get-fan-speed=<ids>, --set-fan-speed=<settings>, --get-fan-count, --get-cpu-temp. Implements validation for fan IDs (0-255) with per-ID error reporting.
GUI Shutdown Sequence
AsusFanControlGUI/Form1.cs
Replaces direct fan-speed-zero calls with ResetToDefault() invocation. Wraps shutdown in try/catch/finally block with null-check before disposal, improving error resilience during process exit.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 With IDisposable grace, we clean our fans,
Reset to zero, release our plans,
Resources freed, no leaks remain,
Try/catch/finally guides the way,
A tidy shutdown ends the day!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: refactoring cleanup logic (IDisposable, ResetToDefault, try-finally patterns) and improving argument validation (fan ID range checking, TryParse). It is concise and clearly reflects the primary objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request does a great job of refactoring the cleanup logic by implementing IDisposable and adding handlers for process exit. The argument validation is also improved. My review includes a few suggestions to make the resource management even more robust by correctly implementing the full IDisposable pattern and ensuring cleanup happens on all exit paths. I've also pointed out a couple of places where argument parsing can be made safer to prevent crashes from invalid input.

Comment on lines +17 to +21
public void Dispose()
{
AsusWinIO64.ShutdownWinIo();
GC.SuppressFinalize(this);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Implementing IDisposable is a good step. However, the current implementation has removed the finalizer (~AsusControl), which acted as a safety net for resource cleanup. If Dispose() isn't called, ShutdownWinIo() will not be called, leading to a resource leak. Also, GC.SuppressFinalize(this) is ineffective without a finalizer.

It's best practice to implement the full dispose pattern for classes that wrap unmanaged resources. This makes your class more robust by making Dispose() idempotent (safe to call multiple times) and ensuring cleanup even if Dispose() is not called explicitly.

Here's an example of how you could implement it:

public class AsusControl : IDisposable
{
    private bool _disposed = false;

    public AsusControl()
    {
        AsusWinIO64.InitializeWinIo();
    }

    ~AsusControl()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;

        // Unmanaged resources are cleaned up regardless of whether this was called
        // from Dispose() or the finalizer.
        AsusWinIO64.ShutdownWinIo();

        _disposed = true;
    }
    // ... rest of the class
}

Comment thread AsusFanControl/Program.cs
Comment on lines +30 to 33
if (!skipResetOnExit)
{
var fanSpeeds = asusControl.GetFanSpeeds();
Console.WriteLine($"Current fan speeds: {string.Join(" ", fanSpeeds)} RPM");
asusControl.ResetToDefault();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The ProcessExit handler is a good addition for cleanup on abnormal termination (e.g., Ctrl-C). However, it currently only calls ResetToDefault() and not Dispose(). This will lead to a resource leak because AsusWinIO64.ShutdownWinIo() won't be called. You should also call Dispose() within this handler.

To safely call Dispose() from both the finally block and the ProcessExit handler, the Dispose() method in AsusControl should be made idempotent (safe to call more than once), as suggested in my other comment.

                if (!skipResetOnExit)
                {
                    asusControl.ResetToDefault();
                    asusControl.Dispose();
                }

Comment thread AsusFanControl/Program.cs
Comment on lines +49 to +55
var newSpeed = int.Parse(newSpeedStr);
asusControl.SetFanSpeeds(newSpeed);

if (arg.StartsWith("--get-fan-speed="))
{
var fanIds = arg.Split('=')[1].Split(',');
foreach (var fanIdStr in fanIds)
if (newSpeed == 0)
Console.WriteLine("Test mode turned off");
else
Console.WriteLine($"New fan speeds: {newSpeed}%");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For robustness and consistency with other argument parsing improvements in this PR, you should use int.TryParse() here instead of int.Parse(). Additionally, it's a good idea to validate that the parsed speed is within the expected percentage range (0-100) to prevent unexpected behavior.

                        if (int.TryParse(newSpeedStr, out int newSpeed) && newSpeed >= 0 && newSpeed <= 100)
                        {
                            asusControl.SetFanSpeeds(newSpeed);

                            if (newSpeed == 0)
                                Console.WriteLine("Test mode turned off");
                            else
                                Console.WriteLine($"New fan speeds: {newSpeed}%");
                        }
                        else
                        {
                            Console.WriteLine($"Error: Invalid fan speed value. Must be a percentage between 0 and 100. Value: '{newSpeedStr}'");
                        }

Comment thread AsusFanControl/Program.cs
Comment on lines +94 to +100
var fanSpeed = int.Parse(settingParts[1]);
asusControl.SetFanSpeed(fanSpeed, (byte)fanId);

if (fanSpeed == 0)
Console.WriteLine($"Test mode turned off for fan {fanId}");
else
Console.WriteLine($"New fan speed for fan {fanId}: {fanSpeed}%");
if (fanSpeed == 0)
Console.WriteLine($"Test mode turned off for fan {fanId}");
else
Console.WriteLine($"New fan speed for fan {fanId}: {fanSpeed}%");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For consistency and robustness, you should use int.TryParse() here instead of int.Parse(). This will prevent the application from crashing if a non-integer value is provided for the fan speed. You should also validate the fan speed is within the expected 0-100 percent range.

                                    if (int.TryParse(settingParts[1], out int fanSpeed) && fanSpeed >= 0 && fanSpeed <= 100)
                                    {
                                        asusControl.SetFanSpeed(fanSpeed, (byte)fanId);

                                        if (fanSpeed == 0)
                                            Console.WriteLine($"Test mode turned off for fan {fanId}");
                                        else
                                            Console.WriteLine($"New fan speed for fan {fanId}: {fanSpeed}%");
                                    }
                                    else
                                    {
                                        Console.WriteLine($"Error: Invalid fan speed value for fan {fanId}. Must be a percentage between 0 and 100. Value: '{settingParts[1]}'");
                                    }

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@AsusFanControl/AsusControl.cs`:
- Around line 23-30: Change the loop counters from byte to int in
ResetToDefault, SetFanSpeeds(byte value), and GetFanSpeeds to avoid byte
overflow; iterate with "int fanIndex = 0; fanIndex < fanCount; fanIndex++" and
when calling SetFanSpeed or any API that expects a byte, cast the int index to
byte (e.g., (byte)fanIndex) so loop bounds use the int fanCount safely while
preserving the original API calls.

In `@AsusFanControl/Program.cs`:
- Around line 117-122: The finally block currently calls
asusControl.ResetToDefault() then asusControl.Dispose(), which will skip Dispose
if ResetToDefault throws; change it so Dispose always runs by wrapping
ResetToDefault() in its own try/catch or a nested try/finally and call
asusControl.Dispose() in the outer/final finally; preserve setting
skipResetOnExit = true and log or swallow any exception from ResetToDefault so
hardware I/O is always cleaned up by asusControl.Dispose().
- Around line 58-107: The argument parsing silently skips invalid fan IDs and
will throw on non-numeric speeds; update the logic around the "--get-fan-speed="
and "--set-fan-speed=" branches to validate and report errors: for each fanId
string in the "--get-fan-speed=" loop (where GetFanSpeed is called) emit a clear
error when int.TryParse fails and keep the existing 0–255 range check; in the
"--set-fan-speed=" branch (around parsing settingParts and where SetFanSpeed is
called) replace int.Parse with int.TryParse, validate that settingParts has
exactly two elements, report parsing errors for fanId or speed, enforce fanId in
0–255 and speed in 0–100 before calling asusControl.SetFanSpeed, and emit
descriptive Console.WriteLine messages for each validation failure (referencing
GetFanSpeed, HealthyTable_FanCounts, and SetFanSpeed to locate the code).
- Around line 46-56: Validate and guard parsing of the "--set-fan-speeds"
argument: ensure the arg contains '=' before splitting, use int.TryParse to
parse the RHS into newSpeed, and if parsing fails or the value is outside 0–100
write a clear error message and skip calling asusControl.SetFanSpeeds; also
harden SetFanSpeeds (or the percent→PWM conversion path) to clamp the percent
input to 0–100 (e.g., via Math.Clamp or explicit min/max) before converting to
byte PWM so values >100 or <0 cannot wrap into unexpected PWM bytes.

In `@AsusFanControlGUI/Form1.cs`:
- Around line 62-80: The current shutdown block only disposes resources when
Properties.Settings.Default.turnOffControlOnExit is true, so ensure that
AsusWinIO64.ShutdownWinIo() and asusControl.Dispose()/nulling always run; keep
only asusControl.ResetToDefault() inside the conditional. Update the block
around asusControl.ResetToDefault(), asusControl.Dispose() and
AsusWinIO64.ShutdownWinIo() so that ResetToDefault() is invoked only when
turnOffControlOnExit is true, but the null-checked Dispose() (call
asusControl.Dispose() and set asusControl = null) and a call to
AsusWinIO64.ShutdownWinIo() always execute (use try/finally as needed) to
guarantee driver cleanup after InitializeWinIo().
🧹 Nitpick comments (1)
AsusFanControl/AsusControl.cs (1)

10-21: Make Dispose() idempotent to avoid double shutdown.

Dispose() can be called from multiple exit paths; guarding against repeated ShutdownWinIo() calls keeps cleanup safe and predictable.

♻️ Proposed fix
 public class AsusControl : IDisposable
 {
+    private bool _disposed;
+
     public void Dispose()
     {
-        AsusWinIO64.ShutdownWinIo();
-        GC.SuppressFinalize(this);
+        if (_disposed) return;
+        _disposed = true;
+        AsusWinIO64.ShutdownWinIo();
+        GC.SuppressFinalize(this);
     }

Comment on lines +23 to +30
public void ResetToDefault()
{
var fanCount = AsusWinIO64.HealthyTable_FanCounts();
for (byte fanIndex = 0; fanIndex < fanCount; fanIndex++)
{
SetFanSpeed(0, fanIndex);
System.Threading.Thread.Sleep(20);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the AsusControl.cs file
find . -name "AsusControl.cs" -type f

Repository: beenycool/AsusFanControl

Length of output: 99


🏁 Script executed:

# Search for the HealthyTable_FanCounts method definition
rg "HealthyTable_FanCounts" -A 5 -B 2

Repository: beenycool/AsusFanControl

Length of output: 3445


🏁 Script executed:

# Search for the SetFanSpeed method signature
rg "SetFanSpeed" -B 2 -A 10

Repository: beenycool/AsusFanControl

Length of output: 6332


🏁 Script executed:

# Check for any other uses of byte loop counters in the codebase
rg "for\s*\(\s*byte\s+" -A 3

Repository: beenycool/AsusFanControl

Length of output: 953


Use an int loop counter to prevent byte wrap.

fanCount is int; a byte loop counter will wrap at 255. While unlikely for systems to exceed 255 fans, changing the loop counter to int eliminates the infinite-loop risk and keeps the loop bounds consistent. This pattern appears in at least three methods in this class: ResetToDefault(), SetFanSpeeds(byte value), and GetFanSpeeds().

🛠️ Suggested change
 public void ResetToDefault()
 {
     var fanCount = AsusWinIO64.HealthyTable_FanCounts();
-    for (byte fanIndex = 0; fanIndex < fanCount; fanIndex++)
+    for (int fanIndex = 0; fanIndex < fanCount; fanIndex++)
     {
-        SetFanSpeed(0, fanIndex);
+        SetFanSpeed(0, (byte)fanIndex);
         System.Threading.Thread.Sleep(20);
     }
 }
🤖 Prompt for AI Agents
In `@AsusFanControl/AsusControl.cs` around lines 23 - 30, Change the loop counters
from byte to int in ResetToDefault, SetFanSpeeds(byte value), and GetFanSpeeds
to avoid byte overflow; iterate with "int fanIndex = 0; fanIndex < fanCount;
fanIndex++" and when calling SetFanSpeed or any API that expects a byte, cast
the int index to byte (e.g., (byte)fanIndex) so loop bounds use the int fanCount
safely while preserving the original API calls.

Comment thread AsusFanControl/Program.cs
Comment on lines +46 to +56
if (arg.StartsWith("--set-fan-speeds"))
{
var newSpeedStr = arg.Split('=')[1];
var newSpeed = int.Parse(newSpeedStr);
asusControl.SetFanSpeeds(newSpeed);

if (arg.StartsWith("--get-fan-speed="))
{
var fanIds = arg.Split('=')[1].Split(',');
foreach (var fanIdStr in fanIds)
if (newSpeed == 0)
Console.WriteLine("Test mode turned off");
else
Console.WriteLine($"New fan speeds: {newSpeed}%");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "Program.cs" -type f | head -20

Repository: beenycool/AsusFanControl

Length of output: 126


🏁 Script executed:

wc -l ./AsusFanControl/Program.cs

Repository: beenycool/AsusFanControl

Length of output: 99


🏁 Script executed:

cat -n ./AsusFanControl/Program.cs

Repository: beenycool/AsusFanControl

Length of output: 6086


🏁 Script executed:

find . -name "*.cs" -type f | xargs grep -l "SetFanSpeeds" | head -10

Repository: beenycool/AsusFanControl

Length of output: 156


🏁 Script executed:

grep -n "SetFanSpeeds" ./AsusFanControl/AsusControl.cs

Repository: beenycool/AsusFanControl

Length of output: 206


🏁 Script executed:

sed -n '46,65p' ./AsusFanControl/AsusControl.cs

Repository: beenycool/AsusFanControl

Length of output: 722


🏁 Script executed:

# Verify the wrapping behavior with a quick test
cat > /tmp/test_cast.cs << 'EOF'
using System;
class Program {
    static void Main() {
        // Test wrapping behavior
        int[] testPercents = { -1, 0, 50, 100, 101, 200, 256, 300, 1000 };
        foreach (var percent in testPercents) {
            var value = (byte)(percent / 100.0f * 255);
            Console.WriteLine($"percent={percent} -> (int)formula={(int)(percent / 100.0f * 255)} -> byte={value}");
        }
    }
}
EOF
csc /tmp/test_cast.cs && mono /tmp/test_cast.exe 2>/dev/null || dotnet /tmp/test_cast.cs 2>/dev/null || echo "C# compiler not available, but cast behavior is standard"

Repository: beenycool/AsusFanControl

Length of output: 184


Harden --set-fan-speeds parsing and range checks.

The code has two vulnerabilities: arg.Split('=')[1] throws IndexOutOfRangeException if = is missing, and int.Parse() throws FormatException for non-numeric input. Additionally, SetFanSpeeds() converts percent to PWM via (byte)(percent / 100.0f * 255), which silently wraps values >100 (e.g., 101% becomes PWM 1, 300% becomes PWM 44) and negative values become high PWM values. Validate the argument format, use int.TryParse(), and enforce 0–100 range before applying.

🐛 Proposed fix
- if (arg.StartsWith("--set-fan-speeds"))
+ if (arg.StartsWith("--set-fan-speeds"))
   {
-      var newSpeedStr = arg.Split('=')[1];
-      var newSpeed = int.Parse(newSpeedStr);
+      var parts = arg.Split('=', 2);
+      if (parts.Length != 2 || !int.TryParse(parts[1], out var newSpeed) || newSpeed < 0 || newSpeed > 100)
+      {
+          Console.WriteLine("Error: --set-fan-speeds expects a value between 0 and 100");
+          continue;
+      }
       asusControl.SetFanSpeeds(newSpeed);
 
       if (newSpeed == 0)
           Console.WriteLine("Test mode turned off");
       else
           Console.WriteLine($"New fan speeds: {newSpeed}%");
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (arg.StartsWith("--set-fan-speeds"))
{
var newSpeedStr = arg.Split('=')[1];
var newSpeed = int.Parse(newSpeedStr);
asusControl.SetFanSpeeds(newSpeed);
if (arg.StartsWith("--get-fan-speed="))
{
var fanIds = arg.Split('=')[1].Split(',');
foreach (var fanIdStr in fanIds)
if (newSpeed == 0)
Console.WriteLine("Test mode turned off");
else
Console.WriteLine($"New fan speeds: {newSpeed}%");
}
if (arg.StartsWith("--set-fan-speeds"))
{
var parts = arg.Split('=', 2);
if (parts.Length != 2 || !int.TryParse(parts[1], out var newSpeed) || newSpeed < 0 || newSpeed > 100)
{
Console.WriteLine("Error: --set-fan-speeds expects a value between 0 and 100");
continue;
}
asusControl.SetFanSpeeds(newSpeed);
if (newSpeed == 0)
Console.WriteLine("Test mode turned off");
else
Console.WriteLine($"New fan speeds: {newSpeed}%");
}
🤖 Prompt for AI Agents
In `@AsusFanControl/Program.cs` around lines 46 - 56, Validate and guard parsing
of the "--set-fan-speeds" argument: ensure the arg contains '=' before
splitting, use int.TryParse to parse the RHS into newSpeed, and if parsing fails
or the value is outside 0–100 write a clear error message and skip calling
asusControl.SetFanSpeeds; also harden SetFanSpeeds (or the percent→PWM
conversion path) to clamp the percent input to 0–100 (e.g., via Math.Clamp or
explicit min/max) before converting to byte PWM so values >100 or <0 cannot wrap
into unexpected PWM bytes.

Comment thread AsusFanControl/Program.cs
Comment on lines +58 to +107
if (arg.StartsWith("--get-fan-speed="))
{
var fanId = int.Parse(fanIdStr);
var fanSpeed = asusControl.GetFanSpeed((byte)fanId);
Console.WriteLine($"Current fan speed for fan {fanId}: {fanSpeed} RPM");
var fanIds = arg.Split('=')[1].Split(',');
foreach (var fanIdStr in fanIds)
{
if (int.TryParse(fanIdStr, out int fanId))
{
if (fanId >= 0 && fanId <= 255)
{
var fanSpeed = asusControl.GetFanSpeed((byte)fanId);
Console.WriteLine($"Current fan speed for fan {fanId}: {fanSpeed} RPM");
}
else
{
Console.WriteLine($"Error: fan id must be between 0 and 255: {fanId}");
}
}
}
}
}

if (arg.StartsWith("--get-fan-count"))
{
var fanCount = asusControl.HealthyTable_FanCounts();
Console.WriteLine($"Fan count: {fanCount}");
}
if (arg.StartsWith("--get-fan-count"))
{
var fanCount = asusControl.HealthyTable_FanCounts();
Console.WriteLine($"Fan count: {fanCount}");
}

if (arg.StartsWith("--set-fan-speed="))
{
var fanSettings = arg.Split('=')[1].Split(',');
foreach (var fanSetting in fanSettings)
if (arg.StartsWith("--set-fan-speed="))
{
var fanId = int.Parse(fanSetting.Split(':')[0]);
var fanSpeed = int.Parse(fanSetting.Split(':')[1]);
asusControl.SetFanSpeed(fanSpeed, (byte)fanId);
var fanSettings = arg.Split('=')[1].Split(',');
foreach (var fanSetting in fanSettings)
{
var settingParts = fanSetting.Split(':');
if (settingParts.Length == 2 && int.TryParse(settingParts[0], out int fanId))
{
if (fanId >= 0 && fanId <= 255)
{
var fanSpeed = int.Parse(settingParts[1]);
asusControl.SetFanSpeed(fanSpeed, (byte)fanId);

if (fanSpeed == 0)
Console.WriteLine($"Test mode turned off for fan {fanId}");
else
Console.WriteLine($"New fan speed for fan {fanId}: {fanSpeed}%");
if (fanSpeed == 0)
Console.WriteLine($"Test mode turned off for fan {fanId}");
else
Console.WriteLine($"New fan speed for fan {fanId}: {fanSpeed}%");
}
else
{
Console.WriteLine($"Error: fan id must be between 0 and 255: {fanId}");
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

git ls-files "AsusFanControl/Program.cs"

Repository: beenycool/AsusFanControl

Length of output: 93


🏁 Script executed:

cat -n AsusFanControl/Program.cs | head -150

Repository: beenycool/AsusFanControl

Length of output: 6086


🏁 Script executed:

sed -n '50,115p' AsusFanControl/Program.cs

Repository: beenycool/AsusFanControl

Length of output: 3195


🏁 Script executed:

grep -n "255\|100\|fan.*speed\|fan.*count" AsusFanControl/Program.cs | head -20

Repository: beenycool/AsusFanControl

Length of output: 1592


🏁 Script executed:

sed -n '40,120p' AsusFanControl/Program.cs

Repository: beenycool/AsusFanControl

Length of output: 3809


Add explicit error handling and validation for per-fan IDs and speeds.

The code has three critical issues:

  1. Lines 63–74: Invalid fan IDs (non-numeric) are silently skipped without error feedback. When int.TryParse() fails, the loop just continues.

  2. Line 94: Fan speed parsing uses int.Parse() without error handling and will throw an unhandled FormatException if the input is non-numeric. Although wrapped in an outer try-finally, this crashes the program instead of providing a graceful error message.

  3. Lines 84–107: No range validation for fan speed values (should be 0–100 per the help text at line 20). Invalid speeds could be passed to SetFanSpeed() and also silently accepted if format is invalid.

Add explicit error handling with informative messages and validate both fan IDs (0–255) and speeds (0–100) before proceeding.

Proposed fix
 if (arg.StartsWith("--get-fan-speed="))
 {
     var fanIds = arg.Split('=')[1].Split(',');
     foreach (var fanIdStr in fanIds)
     {
-        if (int.TryParse(fanIdStr, out int fanId))
-        {
-            if (fanId >= 0 && fanId <= 255)
-            {
-                var fanSpeed = asusControl.GetFanSpeed((byte)fanId);
-                Console.WriteLine($"Current fan speed for fan {fanId}: {fanSpeed} RPM");
-            }
-            else
-            {
-                Console.WriteLine($"Error: fan id must be between 0 and 255: {fanId}");
-            }
-        }
+        if (!int.TryParse(fanIdStr, out int fanId))
+        {
+            Console.WriteLine($"Error: invalid fan id '{fanIdStr}'");
+            continue;
+        }
+        if (fanId < 0 || fanId > 255)
+        {
+            Console.WriteLine($"Error: fan id must be between 0 and 255: {fanId}");
+            continue;
+        }
+        var fanSpeed = asusControl.GetFanSpeed((byte)fanId);
+        Console.WriteLine($"Current fan speed for fan {fanId}: {fanSpeed} RPM");
     }
 }
 
 if (arg.StartsWith("--set-fan-speed="))
 {
     var fanSettings = arg.Split('=')[1].Split(',');
     foreach (var fanSetting in fanSettings)
     {
         var settingParts = fanSetting.Split(':');
-        if (settingParts.Length == 2 && int.TryParse(settingParts[0], out int fanId))
+        if (settingParts.Length == 2 &&
+            int.TryParse(settingParts[0], out int fanId) &&
+            int.TryParse(settingParts[1], out int fanSpeed))
         {
             if (fanId >= 0 && fanId <= 255)
             {
-                var fanSpeed = int.Parse(settingParts[1]);
-                asusControl.SetFanSpeed(fanSpeed, (byte)fanId);
+                if (fanSpeed < 0 || fanSpeed > 100)
+                {
+                    Console.WriteLine($"Error: fan speed must be between 0 and 100: {fanSpeed}");
+                    continue;
+                }
+                asusControl.SetFanSpeed(fanSpeed, (byte)fanId);
 
                 if (fanSpeed == 0)
                     Console.WriteLine($"Test mode turned off for fan {fanId}");
                 else
                     Console.WriteLine($"New fan speed for fan {fanId}: {fanSpeed}%");
             }
             else
             {
                 Console.WriteLine($"Error: fan id must be between 0 and 255: {fanId}");
             }
         }
+        else
+        {
+            Console.WriteLine($"Error: invalid fan setting '{fanSetting}' (expected fanId:percent)");
+        }
     }
 }
🤖 Prompt for AI Agents
In `@AsusFanControl/Program.cs` around lines 58 - 107, The argument parsing
silently skips invalid fan IDs and will throw on non-numeric speeds; update the
logic around the "--get-fan-speed=" and "--set-fan-speed=" branches to validate
and report errors: for each fanId string in the "--get-fan-speed=" loop (where
GetFanSpeed is called) emit a clear error when int.TryParse fails and keep the
existing 0–255 range check; in the "--set-fan-speed=" branch (around parsing
settingParts and where SetFanSpeed is called) replace int.Parse with
int.TryParse, validate that settingParts has exactly two elements, report
parsing errors for fanId or speed, enforce fanId in 0–255 and speed in 0–100
before calling asusControl.SetFanSpeed, and emit descriptive Console.WriteLine
messages for each validation failure (referencing GetFanSpeed,
HealthyTable_FanCounts, and SetFanSpeed to locate the code).

Comment thread AsusFanControl/Program.cs
Comment on lines +117 to +122
finally
{
skipResetOnExit = true;
asusControl.ResetToDefault();
asusControl.Dispose();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n AsusFanControl/Program.cs | sed -n '100,140p'

Repository: beenycool/AsusFanControl

Length of output: 1200


🏁 Script executed:

cat -n AsusFanControl/Program.cs | sed -n '1,130p'

Repository: beenycool/AsusFanControl

Length of output: 6086


🏁 Script executed:

find . -name "AsusControl.cs" -type f

Repository: beenycool/AsusFanControl

Length of output: 99


🏁 Script executed:

cat -n AsusFanControl/AsusControl.cs

Repository: beenycool/AsusFanControl

Length of output: 3302


Ensure Dispose() runs even if ResetToDefault() fails.

If ResetToDefault() throws, Dispose() is skipped, leaving hardware I/O resources unclean. A nested try/finally guarantees cleanup always executes.

🛠️ Suggested change
 finally
 {
     skipResetOnExit = true;
-    asusControl.ResetToDefault();
-    asusControl.Dispose();
+    try
+    {
+        asusControl.ResetToDefault();
+    }
+    finally
+    {
+        asusControl.Dispose();
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
finally
{
skipResetOnExit = true;
asusControl.ResetToDefault();
asusControl.Dispose();
}
finally
{
skipResetOnExit = true;
try
{
asusControl.ResetToDefault();
}
finally
{
asusControl.Dispose();
}
}
🤖 Prompt for AI Agents
In `@AsusFanControl/Program.cs` around lines 117 - 122, The finally block
currently calls asusControl.ResetToDefault() then asusControl.Dispose(), which
will skip Dispose if ResetToDefault throws; change it so Dispose always runs by
wrapping ResetToDefault() in its own try/catch or a nested try/finally and call
asusControl.Dispose() in the outer/final finally; preserve setting
skipResetOnExit = true and log or swallow any exception from ResetToDefault so
hardware I/O is always cleaned up by asusControl.Dispose().

Comment on lines 62 to +80
if (Properties.Settings.Default.turnOffControlOnExit)
asusControl.SetFanSpeeds(0);
{
try
{
asusControl.ResetToDefault();
}
catch
{
// Ignore exceptions during shutdown
}
finally
{
if (asusControl != null)
{
asusControl.Dispose();
asusControl = null;
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd AsusFanControlGUI && head -150 Form1.cs | tail -100

Repository: beenycool/AsusFanControl

Length of output: 3120


🏁 Script executed:

find . -name "Form1.cs" -type f

Repository: beenycool/AsusFanControl

Length of output: 96


🏁 Script executed:

sed -n '55,85p' AsusFanControlGUI/Form1.cs

Repository: beenycool/AsusFanControl

Length of output: 939


🏁 Script executed:

rg "OnProcessExit" -A 30 AsusFanControlGUI/Form1.cs

Repository: beenycool/AsusFanControl

Length of output: 2455


🏁 Script executed:

rg "asusControl" -B 2 -A 2 AsusFanControlGUI/Form1.cs | head -100

Repository: beenycool/AsusFanControl

Length of output: 1419


🏁 Script executed:

find . -name "AsusControl.cs" -type f

Repository: beenycool/AsusFanControl

Length of output: 99


🏁 Script executed:

rg "class AsusControl" -A 50 AsusFanControlGUI/AsusControl.cs | head -100

Repository: beenycool/AsusFanControl

Length of output: 140


🏁 Script executed:

rg "Dispose" -B 2 -A 10 AsusFanControlGUI/AsusControl.cs

Repository: beenycool/AsusFanControl

Length of output: 140


🏁 Script executed:

rg "override.*Dispose|Dispose\(" AsusFanControlGUI/Form1.cs

Repository: beenycool/AsusFanControl

Length of output: 114


🏁 Script executed:

cat AsusFanControl/AsusControl.cs

Repository: beenycool/AsusFanControl

Length of output: 2651


🏁 Script executed:

rg "Dispose|IDisposable|WinIo" AsusFanControl/AsusControl.cs -B 2 -A 8

Repository: beenycool/AsusFanControl

Length of output: 562


Ensure Dispose() runs even when "turn off on exit" is disabled.

Disposal is inside the turnOffControlOnExit conditional, so AsusWinIO64.ShutdownWinIo() is skipped when the user disables that setting. Since InitializeWinIo() is called in the constructor, cleanup must always run to properly release driver resources. Only the ResetToDefault() call should be conditional.

🐛 Proposed fix
 private void OnProcessExit(object sender, EventArgs e)
 {
-    if (Properties.Settings.Default.turnOffControlOnExit)
-    {
-        try
-        {
-            asusControl.ResetToDefault();
-        }
-        catch
-        {
-            // Ignore exceptions during shutdown
-        }
-        finally
-        {
-            if (asusControl != null)
-            {
-                asusControl.Dispose();
-                asusControl = null;
-            }
-        }
-    }
+    try
+    {
+        if (Properties.Settings.Default.turnOffControlOnExit)
+        {
+            asusControl?.ResetToDefault();
+        }
+    }
+    catch
+    {
+        // Ignore exceptions during shutdown
+    }
+    finally
+    {
+        asusControl?.Dispose();
+        asusControl = null;
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (Properties.Settings.Default.turnOffControlOnExit)
asusControl.SetFanSpeeds(0);
{
try
{
asusControl.ResetToDefault();
}
catch
{
// Ignore exceptions during shutdown
}
finally
{
if (asusControl != null)
{
asusControl.Dispose();
asusControl = null;
}
}
}
private void OnProcessExit(object sender, EventArgs e)
{
try
{
if (Properties.Settings.Default.turnOffControlOnExit)
{
asusControl?.ResetToDefault();
}
}
catch
{
// Ignore exceptions during shutdown
}
finally
{
asusControl?.Dispose();
asusControl = null;
}
}
🤖 Prompt for AI Agents
In `@AsusFanControlGUI/Form1.cs` around lines 62 - 80, The current shutdown block
only disposes resources when Properties.Settings.Default.turnOffControlOnExit is
true, so ensure that AsusWinIO64.ShutdownWinIo() and
asusControl.Dispose()/nulling always run; keep only asusControl.ResetToDefault()
inside the conditional. Update the block around asusControl.ResetToDefault(),
asusControl.Dispose() and AsusWinIO64.ShutdownWinIo() so that ResetToDefault()
is invoked only when turnOffControlOnExit is true, but the null-checked
Dispose() (call asusControl.Dispose() and set asusControl = null) and a call to
AsusWinIO64.ShutdownWinIo() always execute (use try/finally as needed) to
guarantee driver cleanup after InitializeWinIo().

Co-authored-by: beenycool <129210955+beenycool@users.noreply.github.com>
@beenycool
beenycool merged commit 066da08 into master Jan 28, 2026
2 checks passed
@beenycool
beenycool deleted the fix/cleanup-validation-refactor-2926545181562135474 branch March 20, 2026 21:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant