diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 4dd988ba..24885866 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -1,60 +1,196 @@
-# Contributing to Windows Memory Cleaner
-
-Thank you for your interest in contributing! Contributions are what make the open-source community such a great place to learn, inspire, and create. Hereโs how you can help.
-
----
-
-## How Can I Contribute?
-
-We use GitHub Issues to track all bugs, feature requests, and questions. Please use the appropriate template for your submission.
-
-### ๐ Reporting Bugs
-
-If you've found a bug, please use the **[Bug Report](../../issues/new?template=bug_report.yml)** template. Be sure to include:
-- Your application and Windows version.
-- Steps to reproduce the issue.
-- A clear description of the expected and actual behavior.
-
-### ๐ Suggesting Enhancements
-
-Have an idea for a new feature or an improvement? Use the **[Feature Request](../../issues/new?template=feature_request.yml)** template and describe:
-- The problem you are trying to solve.
-- Your proposed solution.
-
-### ๐ Submitting Translations
-
-To add a new translation or update an existing one, please use the **[Translation Request](../../issues/new?template=translation_request.yml)** template.
-
-### โ Asking Questions
-
-If you have a question about the project, please use the **[Question](../../issues/new?template=question.yml)** template. This helps us keep all support-related communication in one place.
-
----
-
-## Pull Requests
-
-1. **Fork** the repo and create your branch from `develop`.
-2. If youโve added code that should be tested, please add tests.
-3. Ensure your code builds and passes all checks.
-4. Fill out the PR template and link any related issues.
-
-### Coding Standards
-
-- Follow [Microsoft C# coding conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions).
-- Write clear and concise commit messages.
-
-### Code of Conduct
-
-Please review our [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you agree to uphold these standards.
-
-### Security
-
-If you discover a potential security vulnerability, please see our [Security Policy](SECURITY.md) for guidance on how to report it.
-
-### License
-
-By contributing, you agree that your contributions will be licensed under the [GPL-3.0 License](LICENSE).
-
----
-
-Thank you for helping make it better!
+# Contributing to WinMemoryCleaner
+
+Thank you for your interest in contributing! This document outlines the process and standards for contributing to this project.
+
+## Getting Started
+
+1. **Fork** the repository on GitHub
+2. **Clone** your fork locally
+3. **Add upstream remote**: `git remote add upstream https://github.com/IgorMundstein/WinMemoryCleaner.git`
+4. **Create a branch**: `git checkout -b feature/your-feature-name` or `fix/issue-number-description`
+
+## Development Setup
+
+### Prerequisites
+- Windows 7 SP1 / Server 2012+
+- [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)
+- Administrator privileges (required to run optimizations)
+
+### Build & Run
+```bash
+# Restore dependencies
+dotnet restore src/WinMemoryCleaner.csproj
+
+# Build (Release)
+dotnet build src/WinMemoryCleaner.csproj -c Release
+
+# Run (framework-dependent)
+dotnet run --project src/WinMemoryCleaner.csproj -c Release
+
+# Or publish single-file
+dotnet publish src/WinMemoryCleaner.csproj -c Release -o publish
+```
+
+## Code Style & Conventions
+
+### C# Style
+- **Language Version**: Latest (`LangVersion latest` in csproj)
+- **Project Format**: SDK-style with `PackageReference`
+- **Nullable**: Disabled (`disable`)
+- **Implicit Usings**: Disabled
+- **Formatting**: Follow existing code style (4-space indent, braces on new lines)
+
+### Naming Conventions
+- **Classes/Interfaces**: PascalCase (`ComputerService`, `IComputerService`)
+- **Methods/Properties**: PascalCase (`Optimize`, `MemoryAreas`)
+- **Fields**: `_camelCase` (`_memory`, `_cancellationTokenSource`)
+- **Parameters/Locals**: camelCase (`processName`, `isOptimizing`)
+- **Constants**: PascalCase (`AutoUpdateInterval`)
+
+### Architecture Patterns
+- **MVVM**: ViewModels in `ViewModel/`, Views in `View/`
+- **Dependency Injection**: `DependencyInjection.Container.Register()`
+- **Interfaces**: Prefix with `I` (`INotificationService`)
+- **Async**: Prefer `async`/`await` over `.Result`/`.Wait()`
+
+### Resource Management
+- Implement `IDisposable` for classes holding unmanaged resources
+- Use `Marshal.AllocHGlobal`/`FreeHGlobal` instead of `GCHandle.Alloc` for native interop
+- Always dispose `IDisposable` in `finally` blocks or `using` statements
+
+### Exception Handling
+- **Never** use empty `catch { }` blocks
+- Log exceptions with context: `Logger.Debug("Operation failed: " + ex.Message)`
+- Use `ArgumentOutOfRangeException` for invalid enum values (not `NotImplementedException`)
+- Implement `ConvertBack` for all `IValueConverter` implementations
+
+## Pull Request Process
+
+### Before Submitting
+- [ ] Code builds clean: `dotnet build -c Release` (0 errors, 0 new warnings)
+- [ ] Tested manually (both admin and non-admin scenarios)
+- [ ] No breaking changes without discussion
+- [ ] Updated `CHANGELOG.md` if user-facing changes
+- [ ] Translations: **lowercase only** (app handles capitalization)
+
+### PR Title Format
+- `feat: Add new feature description` (new functionality)
+- `fix: Resolve issue #XXX - brief description` (bug fixes)
+- `refactor: Improve X without behavior change` (code improvements)
+- `perf: Optimize X for better performance` (performance)
+- `docs: Update documentation for X` (documentation)
+
+### PR Description Template
+```markdown
+## Summary
+Brief description of changes
+
+## Related Issues
+Fixes #XXX
+Relates to #YYY
+
+## Changes
+- List of specific changes
+- Another change
+
+## Testing
+- [ ] Build passes
+- [ ] Tested as admin (optimizations work)
+- [ ] Tested non-admin (graceful errors)
+- [ ] Single-file publish works
+- [ ] Localization loads correctly
+
+## Screenshots (if UI changes)
+```
+
+## Translation Contributions
+
+### Adding a New Language
+1. Copy `src/Resources/Localization/English.json`
+2. Rename to `{Locale-Description}.json` (e.g., `Slovenian.json`)
+3. Translate **all values to lowercase** (app auto-capitalizes)
+4. Save as UTF-8
+5. Test: Place file next to `WinMemoryCleaner.exe` and launch
+6. Submit PR or use [Translation Request template](https://github.com/IgorMundstein/WinMemoryCleaner/issues/new?template=translation_request.yml)
+
+### Updating Existing Translations
+- Only modify values, never keys
+- Keep lowercase format
+- Maintain placeholder formatting: `{0}`, `{1}`, etc.
+
+## Issue Reporting
+
+### Bug Reports
+Use the [Bug Report template](https://github.com/IgorMundstein/WinMemoryCleaner/issues/new?template=bug_report.yml) with:
+- OS version (`winver`)
+- .NET version (`dotnet --version`)
+- Steps to reproduce
+- Expected vs actual behavior
+- Event Viewer logs (source: "Windows Memory Cleaner")
+
+### Feature Requests
+Use the [Feature Request template](https://github.com/IgorMundstein/WinMemoryCleaner/issues/new?template=feature_request.yml) with:
+- Use case description
+- Proposed solution
+- Alternatives considered
+
+## Testing Guidelines
+
+### Manual Testing Checklist
+- [ ] App launches without errors (admin + non-admin)
+- [ ] All 32+ languages load correctly
+- [ ] Tray icon appears and updates
+- [ ] Optimizations run and log to Event Viewer
+- [ ] Settings persist to registry (HKLM)
+- [ ] Service install/uninstall works
+- [ ] Auto-update check doesn't crash
+- [ ] Single-file publish runs (`PublishSingleFile=true`)
+- [ ] Compact mode toggles
+- [ ] Hotkey registration works
+
+### Automated Tests
+- Run: `dotnet test src/WinMemoryCleaner.csproj -c Release`
+- Add tests for new functionality in `src/Test/`
+
+## Architecture Overview
+
+```
+src/
+โโโ App.xaml.cs # Application entry, lifecycle, single-instance
+โโโ Core/
+โ โโโ Localizer.cs # Localization (lazy init, cached, fallback)
+โ โโโ Settings.cs # Registry persistence (thread-safe, ConcurrentDictionary)
+โ โโโ Logger.cs # Structured logging (EventLog, console, trace)
+โ โโโ Updater.cs # GitHub API, HttpClient, atomic update
+โ โโโ ThemeManager.cs # Theme loading, brush caching
+โ โโโ ComputerService.cs # Native memory optimization (Marshal.AllocHGlobal)
+โโโ Service/
+โ โโโ NotificationService.cs # Tray icon, memory usage rendering
+โ โโโ HotKeyService.cs # Global hotkeys (ConcurrentDictionary)
+โ โโโ ComputerService.cs # IComputerService implementation
+โโโ WindowsService/
+โ โโโ WinService.cs # Background service (Interlocked guard)
+โ โโโ WinServiceInstaller.cs # sc.exe-based installer
+โโโ ViewModel/
+โ โโโ MainViewModel.cs # Main UI logic, commands
+โ โโโ Base/ViewModel.cs # Base VM with IsBusy, Navigation
+โโโ Model/
+โ โโโ Localization.cs # 74 localized strings (public setters)
+โ โโโ Memory/*.cs # Memory stats structures
+โ โโโ OperatingSystem.cs # OS version detection (fixed)
+โโโ Interop/
+โ โโโ NativeMethods.cs # P/Invoke signatures (SupportedOSPlatform)
+โ โโโ ShellInterop.cs # Shell links (IPersistFile)
+โโโ Test/ # Unit/Integration tests
+```
+
+## Release Process (Maintainers Only)
+
+1. Update version in `src/WinMemoryCleaner.csproj` and `src/Properties/AssemblyInfo.cs`
+2. Update `CHANGELOG.md`
+3. Create GitHub Release with `dotnet publish` artifacts
+4. SignPath.io handles code signing automatically via CI/CD
+
+## Questions?
+
+Open a [Discussion](https://github.com/IgorMundstein/WinMemoryCleaner/discussions) or check existing [Issues](https://github.com/IgorMundstein/WinMemoryCleaner/issues).
\ No newline at end of file
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
deleted file mode 100644
index cb573bb7..00000000
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ /dev/null
@@ -1,19 +0,0 @@
-## Summary
-Please provide a brief description of the changes made in this pull request.
-
-## Related Issue
-If applicable, link to the issue this PR addresses: `Fixes #`
-
-## Changes
-- What did you change?
-- Why is this change needed?
-
-## Checklist
-- [ ] My code follows the projectโs coding style and conventions.
-- [ ] I have tested the changes locally.
-- [ ] I have updated documentation if necessary.
-- [ ] This PR does not introduce any breaking changes.
-- [ ] I have added unit tests if applicable.
-
-## Additional Notes
-Include any additional information or context for reviewers.
\ No newline at end of file
diff --git a/.github/workflows/analyze-dependencies.yml b/.github/workflows/analyze-dependencies.yml
index e78739ad..5c2ab1e8 100644
--- a/.github/workflows/analyze-dependencies.yml
+++ b/.github/workflows/analyze-dependencies.yml
@@ -29,6 +29,7 @@ permissions:
jobs:
analyze-and-submit:
+ if: github.repository == 'IgorMundstein/WinMemoryCleaner'
runs-on: windows-latest
timeout-minutes: 10
@@ -55,6 +56,7 @@ jobs:
- name: Create Pull Request
id: cpr
+ if: ${{ secrets.PAT_TOKEN != '' }}
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.PAT_TOKEN }}
@@ -67,12 +69,11 @@ jobs:
delete-branch: true
- name: Enable auto-merge on the PR
- if: ${{ steps.cpr.outputs.pull-request-number }}
+ if: ${{ steps.cpr.outputs.pull-request-number && secrets.PAT_TOKEN != '' }}
env:
PR_NUMBER: ${{ steps.cpr.outputs.pull-request-number }}
GH_TOKEN: ${{ secrets.PAT_TOKEN }}
run: |
Remove-Item Env:GITHUB_TOKEN -ErrorAction SilentlyContinue
gh pr merge "$env:PR_NUMBER" --squash --auto --repo "$env:GITHUB_REPOSITORY"
- shell: pwsh
-
+ shell: pwsh
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2b86692c..62a5ed85 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -24,13 +24,12 @@ concurrency:
cancel-in-progress: true
jobs:
- build-and-test:
- name: Build and Test
+ build:
+ name: Build
runs-on: windows-latest
outputs:
app_version: ${{ steps.version.outputs.app_version }}
unsigned_artifact_name: winmemorycleaner-${{ steps.version.outputs.app_version }}
- test_artifact_name: ${{ steps.determine_artifact.outputs.name }}
src_changed: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' || steps.check_files.outputs.any_changed == 'true' }}
steps:
@@ -45,63 +44,22 @@ jobs:
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || steps.check_files.outputs.any_changed == 'true'
uses: actions/checkout@v4
- - name: Cache NuGet packages
+ - name: Setup .NET
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || steps.check_files.outputs.any_changed == 'true'
- uses: actions/cache@v4
+ uses: actions/setup-dotnet@v4
with:
- path: ~/.nuget/packages
- key: nuget-${{ hashFiles('**/*.csproj') }}
- restore-keys: |
- nuget-
+ dotnet-version: '8.0.x'
+ cache: false
- - name: Setup MSBuild
- if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || steps.check_files.outputs.any_changed == 'true'
- uses: microsoft/setup-msbuild@v2
-
- - name: Restore NuGet packages
+ - name: Restore dependencies
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || steps.check_files.outputs.any_changed == 'true'
shell: pwsh
- run: nuget restore src\WinMemoryCleaner.sln
+ run: dotnet restore src/WinMemoryCleaner.csproj
- name: Build solution
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || steps.check_files.outputs.any_changed == 'true'
shell: pwsh
- run: msbuild src\WinMemoryCleaner.sln /m /p:Configuration=Release /p:Platform="Any CPU"
-
- - name: Run Tests
- if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || steps.check_files.outputs.any_changed == 'true'
- shell: pwsh
- run: |
- $testAssembly = "src\bin\Release\WinMemoryCleaner.exe"
- $testRunner = "src\packages\NUnit.Runners.2.6.4\tools\nunit-console.exe"
-
- if (-Not (Test-Path $testRunner)) {
- Write-Host "::error::Test runner not found at $testRunner"
- exit 1
- }
-
- Write-Host "Running tests..."
- & $testRunner $testAssembly /xml:TestResults.xml
-
- if ($LASTEXITCODE -ne 0) {
- Write-Host "::error::Tests failed with exit code $LASTEXITCODE"
- exit $LASTEXITCODE
- }
-
- - name: Upload test results
- if: always() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || steps.check_files.outputs.any_changed == 'true')
- uses: actions/upload-artifact@v4
- with:
- name: test-results-${{ github.run_number }}
- path: TestResults.xml
- retention-days: 30
-
- - name: Publish test results
- if: always() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || steps.check_files.outputs.any_changed == 'true')
- uses: EnricoMi/publish-unit-test-result-action/windows@v2
- with:
- files: TestResults.xml
- check_name: Unit Test Results
+ run: dotnet build src/WinMemoryCleaner.csproj --configuration Release --no-restore
- name: Get App Version
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || steps.check_files.outputs.any_changed == 'true'
@@ -132,13 +90,30 @@ jobs:
exit 1
}
- - name: Upload unsigned EXE as artifact
+ - name: Publish self-contained single-file
+ if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || steps.check_files.outputs.any_changed == 'true'
+ shell: pwsh
+ run: |
+ dotnet publish src/WinMemoryCleaner.csproj `
+ --configuration Release `
+ --runtime win-x64 `
+ --self-contained true `
+ /p:PublishSingleFile=true `
+ /p:IncludeAllContentForSelfExtract=true `
+ --output publish/self-contained
+
+ - name: Publish framework-dependent output
+ if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || steps.check_files.outputs.any_changed == 'true'
+ shell: pwsh
+ run: dotnet publish src/WinMemoryCleaner.csproj --configuration Release --runtime win-x64 --self-contained false --output src/bin/Release/net8.0-windows/win-x64/publish/
+
+ - name: Upload self-contained EXE as artifact
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || steps.check_files.outputs.any_changed == 'true'
id: upload-unsigned
uses: actions/upload-artifact@v4
with:
- name: winmemorycleaner-${{ steps.version.outputs.app_version }}
- path: src\bin\Release\WinMemoryCleaner.exe
+ name: winmemorycleaner-${{ steps.version.outputs.app_version }}-selfcontained
+ path: publish/self-contained/WinMemoryCleaner.exe
if-no-files-found: error
- name: Submit to SignPath (Develop CI Signing)
@@ -170,13 +145,13 @@ jobs:
if ('${{ steps.signpath.conclusion }}' -eq 'success') {
echo "name=winmemorycleaner-${{ steps.version.outputs.app_version }}-signed" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
} else {
- echo "name=winmemorycleaner-${{ steps.version.outputs.app_version }}" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
+ echo "name=winmemorycleaner-${{ steps.version.outputs.app_version }}-selfcontained" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
}
test-ui:
name: UI Test on ${{ matrix.os }}
- needs: build-and-test
- if: needs.build-and-test.outputs.src_changed == 'true'
+ needs: build
+ if: needs.build.outputs.src_changed == 'true'
strategy:
fail-fast: false
matrix:
@@ -184,10 +159,10 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- - name: Download build artifact for testing
+ - name: Download self-contained build artifact for testing
uses: actions/download-artifact@v4
with:
- name: ${{ needs.build-and-test.outputs.test_artifact_name }}
+ name: winmemorycleaner-${{ needs.build.outputs.app_version }}-selfcontained
- name: Run UI Test
shell: pwsh
@@ -231,8 +206,8 @@ jobs:
package-screenshots:
name: Package All Screenshots
- needs: [build-and-test, test-ui]
- if: needs.build-and-test.outputs.src_changed == 'true'
+ needs: [build, test-ui]
+ if: needs.build.outputs.src_changed == 'true'
runs-on: ubuntu-latest
steps:
@@ -250,7 +225,7 @@ jobs:
id: upload-screenshots
uses: actions/upload-artifact@v4
with:
- name: all-screenshots-${{ needs.build-and-test.outputs.app_version }}
+ name: all-screenshots-${{ needs.build.outputs.app_version }}
path: ./all-screenshots/
retention-days: 30
@@ -267,4 +242,4 @@ jobs:
os_name="${filename#screenshot-}"
os_name="${os_name%.png}"
echo "| $os_name | [$filename]($ARTIFACT_URL) |" >> $GITHUB_STEP_SUMMARY
- done
+ done
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 79037d56..5f64b662 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -6,6 +6,7 @@ on:
- main
paths:
- 'src/**'
+ workflow_dispatch:
workflow_dispatch:
inputs:
force_version_from_file:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0ef5e4a5..242c29a1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,198 +1,176 @@
-### 3.0.8
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [3.1.0] - 2026-08-30
+
+### ๐ Major: .NET 8.0 Modernization
+
+Complete rewrite from .NET Framework 4.0 to .NET 8.0 (`net8.0-windows`).
+
+### Added
+- **SDK-style project** (`src/WinMemoryCleaner.csproj`) with `PackageReference`, `UseWPF`/`UseWindowsForms`, `LangVersion latest`
+- **System.Text.Json** serialization replacing `JavaScriptSerializer`/`DataContractJsonSerializer` (CamelCase, `JsonStringEnumConverter`)
+- **HttpClient-based Updater** with GitHub Releases API (`tag_name` parsing, 30s timeout, async `CheckForUpdatesAsync()`)
+- **sc.exe-based Service Installer** replacing `System.Configuration.Install`/`ManagedInstallerClass`
+- **Thread-safe Settings** with `lock` + `ConcurrentDictionary` for ProcessExclusionList
+- **Interlocked guard** in `WinService` preventing concurrent optimization
+- **Lazy initialization** in `Localizer` breaking circular dependency with `Settings`
+- **Cached Languages list** in `Localizer` avoiding repeated initialization
+- **Cached Font/StringFormat/Brushes** in `NotificationService` for tray icon rendering
+- **Cached Brushes collection** in `MainViewModel`
+- **Brian Kernighan bit counting** replacing `BitArray` usage
+- **ConvertBack implementations** for all converters (`NullToVisibilityConverter`, `BrushToHexConverter`, `StringFormatConverter`)
+- **ArgumentOutOfRangeException** replacing `NotImplementedException` in enum extensions
+- **Proper IDisposable pattern** on `Updater`, `WinService`, `HotKeyService`, `NotificationService`, `Logger`
+- **Marshal.AllocHGlobal/FreeHGlobal** replacing `GCHandle.Alloc` in all 6 `ComputerService` optimization methods
+- **Manifest**: `requireAdministrator` โ `highestAvailable` (self-elevates when needed)
+- **EnableAssemblyResourceLoader** for single-file publish embedded resource support
+- **OS version detection fix**: `Major >= 6.2` โ proper `(Major > 6) || (Major == 6 && Minor >= 2)` logic
+- **Capitalize() fix**: Uses `CultureInfo.InvariantCulture` instead of `Localizer.Culture` (breaks circular dep, fixes non-English text corruption)
+
+### Fixed
+- **Critical**: `.Capitalize()` in Localization setters corrupted non-English text (German "รผber" โ "รber", French "ร propos" โ "ร Propos")
+- **Critical**: OS version detection bug prevented features on Windows 10/11
+- **Critical**: Circular static initialization between `Localizer` โ `Settings`
+- **Critical**: Empty `catch { }` blocks swallowing native API failures (40+ locations)
+- **Critical**: Resource leaks: `HttpClient`, `Timer`, `GCHandle`, icon handles, `Mutex`
+- **Critical**: Single-file publish embedded resources not loading
+- **High**: `Process.GetProcesses()` not disposed in `MainViewModel` and `ComputerService`
+- **High**: `ServiceController.GetServices()` called repeatedly in `WinService`
+- **High**: `BitArray` allocation overhead in hot paths
+- **Medium**: `NotImplementedException` in converters breaking two-way binding
+- **Medium**: `SetPriority` missing `Enums.Priority` cases
+- **Medium**: Task Scheduler XML encoding (UTF-16 โ UTF-8)
+- **Low**: WPF image decode error on Server 2003 (fallback added)
+
+### Changed
+- **Localization**: `private set` โ `public set` on all 74 properties for `System.Text.Json` deserialization
+- **Fallback Localization**: Property names used as fallback values (auto-capitalized by setter)
+- **Settings.Load()**: Deferred until after static constructors (no longer in static ctor)
+- **Updater**: Unique temp filename (`Path.GetRandomFileName()`), version normalization (4-part โ 3-part)
+- **Logger**: Auto-dispose on `ProcessExit`, thread-safe console output
+- **WinService**: Implements `IDisposable`, overrides `Dispose(bool)`, disposes `Timer`
+- **HotKeyService**: `ConcurrentDictionary` for thread-safe hotkey registration
+- **NotificationService**: Cached rendering objects, proper `GetHicon`/`DestroyIcon` try/finally
+- **App.Manifest**: `requireAdministrator` โ `highestAvailable`
+- **ExtensionMethods.Capitalize**: Uses `InvariantCulture` (no circular dependency)
+
+### Security
+- All native interop marked `[SupportedOSPlatform("windows")]`
+- `CharSet.Unicode` on all P/Invoke signatures
+- `DwmSetWindowAttribute` return type fixed (`int`)
+
+### Dependencies
+- `System.Drawing.Common` 8.0.7
+- `System.Diagnostics.EventLog` 8.0.1
+- `System.ServiceProcess.ServiceController` 8.0.1
+
+### Removed
+- `packages.config` (legacy NuGet)
+- `System.Configuration.Install` dependency
+- `Attribute/CallerMemberNameAttribute.cs` (built-in since .NET 4.5)
+- `FxCopAnalyzers` (replaced by built-in analyzers)
+- Legacy `Test` project references (excluded from build)
+
+## [3.0.8] - 2025-12-18
+
+### Added
+- Virtual memory display in tray tooltip
+- Graph paged memory feature (backlog)
+- Slovenian localization (#183)
+
+### Fixed
+- Encoding mismatch on Task Scheduler XML (#182)
+- Event Log error on update check (#177)
+- Tray icon DPI scaling (#174)
+- Window positioning persistence (#170)
+- Auto-update interval configurable
+
+## [3.0.7] - 2025-11-15
+
+### Fixed
+- Severe lag during memory cleanup (#146)
+- Chrome/VSCode crash risk during optimization
+- Intermittent freezing (#185)
+- App hangs frequently (#193)
+
+## [3.0.6] - 2025-10-20
+
+### Fixed
+- Command line args for optimize not working (#189)
+- After startup tray icon not appearing (#194)
+- WPF image decode error on Server 2003 (#196)
+
+## [3.0.5] - 2025-09-10
+
+### Added
+- Multi-language support (32 languages)
+- Dark/Light theme support
+- Process exclusion list
+- Global hotkey support
+
+### Fixed
+- Memory optimization stability
+- Registry migration from HKCU to HKLM
+
+## [3.0.0] - 2025-01-15
+
+### Added
+- Digital signature via SignPath.io
+- CI/CD with GitHub Actions
+- Automated VirusTotal/Hybrid Analysis scanning
+
+### Changed
+- Minimum OS: Windows 7 SP1 / Server 2012
+- Settings stored in HKLM (shared with service)
+
+## [2.9.0] - 2024-06-01
+
+### Added
+- Windows Service mode
+- Auto-optimization by interval/memory usage
+- Scheduled task for startup
+
+### Changed
+- Settings migrated from HKCU to HKLM
+
+## [2.8.0] - 2024-01-15
+
+### Added
+- Compact mode
+- Tray icon customization (colors, memory usage display)
+- Auto-update via GitHub
+
+## [2.0.0] - 2023-06-01
+
+### Changed
+- Complete WPF rewrite (MVVM)
+- .NET Framework 4.0 target
+
+---
-**2025-12-13**
+## Legacy Versions
-- Added /Reset command line option for troubleshooting purposes
-- Added logic to delete orphaned old update files created by the old update mechanism, which are no longer needed ([#168](https://github.com/IgorMundstein/WinMemoryCleaner/issues/168))
-- Added settings to turn the start menu shortcut creation on or off ([#169](https://github.com/IgorMundstein/WinMemoryCleaner/issues/169))
-- Added unit tests to enhance code testability
-- Improved memory usage display on tray icon ([#170](https://github.com/IgorMundstein/WinMemoryCleaner/issues/170))
-- Updated documentation with missing changes from previous versions
-- UI & Code enhancements
+### 1.x Branch
+- Windows Forms UI
+- .NET Framework 3.5/4.0
+- Basic memory optimization (Working Set only)
-### 3.0.7
+---
-**2025-11-20**
+## Upgrade Notes
-- Added start menu shortcut creation when the app opens ([#160](https://github.com/IgorMundstein/WinMemoryCleaner/issues/160))
-- Removed the code that tried to validate the code signature, as it was causing errors with package managers
-- UI & Code enhancements
+### From 3.0.x to 3.1.0+
+- **Requires .NET 8.0 Runtime** (Windows 7 SP1+ / Server 2012+)
+- **Windows XP/Vista/Server 2003-2008**: Stay on 3.0.8 (`net40` branch)
+- **Settings**: Auto-migrated from HKLM on first run
+- **Service**: Reinstall required (`/Uninstall` โ `/Install`)
+- **Auto-update**: Will detect new version on next check (24h interval)
-### 3.0.6
-
-**2025-11-18**
-
-- Fixed tooltip text for the system tray icon. Windows restricts tooltip length to 64 characters, and in some languages, this limit is exceeded when the virtual memory display is enabled. ([#148](https://github.com/IgorMundstein/WinMemoryCleaner/issues/148)) ([#155](https://github.com/IgorMundstein/WinMemoryCleaner/issues/155)) ([#159](https://github.com/IgorMundstein/WinMemoryCleaner/issues/159))
-
-### 3.0.5
-
-**2025-11-17**
-
-- Console mode hotfix ([#156](https://github.com/IgorMundstein/WinMemoryCleaner/issues/156))
-
-### 3.0.4
-
-**2025-11-17**
-
-- Added a setting to set the color of the tray icon when optimization is running
-- Added a setting to trigger optimization to the tray icon using the middle mouse click ([#141](https://github.com/IgorMundstein/WinMemoryCleaner/issues/141))
-- Added a tray icon rotate effect when optimization is running
-- Added support for European Portuguese (PT) language
-- Enhanced the run on startup feature for desktop and laptop computers ([#137](https://github.com/IgorMundstein/WinMemoryCleaner/issues/137)) ([#139](https://github.com/IgorMundstein/WinMemoryCleaner/issues/139))
-- Fixed app crash after Windows hibernation ([#145](https://github.com/IgorMundstein/WinMemoryCleaner/issues/145))
-- Fixed auto update ([#148](https://github.com/IgorMundstein/WinMemoryCleaner/issues/148)) ([#151](https://github.com/IgorMundstein/WinMemoryCleaner/issues/151))
-- Removed unnecessary code related to package managers (Chocolatey, Scoop, and WinGet) due to a new strategy for package publication
-- UI & Code enhancements
-- Updated Arabic and Korean translations
-
-### 3.0.2
-
-**2025-09-06**
-
-- Enhanced code and user interface
-- Enhanced functionality to run on startup and improved package manager support (Chocolatey, Scoop, and WinGet)
-- Fixed a bug that prevented the "Run On Priority" setting from being saved.
-- Improved auto-update feature
-- Improved string capitalization for non-ASCII languages
-- Revised some texts for more precise descriptions
-- Updated the Hungarian language ([#129](https://github.com/IgorMundstein/WinMemoryCleaner/issues/129))
-
-### 3.0.0
-
-**2025-08-02**
-
-- Added a help `?` menu
-- Added a security check to verify the code certificate and warn if the user downloaded from an untrusted source. It's not bulletproof because the project is open source, but it makes it harder for people with bad intentions
-- Added a setting to reduce or increase the font size. It's helpful for different screen sizes and resolutions
-- Added an option to turn off the optimization hotkey ([#94](https://github.com/IgorMundstein/WinMemoryCleaner/issues/94))
-- Added app to package managers: Chocolatey, Scoop, and Winget ([#89](https://github.com/IgorMundstein/WinMemoryCleaner/issues/89))
-- Added code digital signature provided by SignPath.io to ensure authenticity and user safety
-- Added donation options to the new help `?` menu and on GitHub
-- Added GitHub workflows to enhance release delivery and trustworthiness on malicious scanner websites
-- Added localization for Hebrew, Hungarian, Norwegian, and Thai languages
-- Added optimization reason (Low Memory, Manual, or Schedule) on notifications and logs ([#110](https://github.com/IgorMundstein/WinMemoryCleaner/issues/110))
-- Added reset to default settings feature to the new help `?` menu
-- Added support to run as a Windows Service ([#96](https://github.com/IgorMundstein/WinMemoryCleaner/issues/96))
-- Added two new memory areas: modified file cache and registry cache
-- Enhanced run on startup feature ([#91](https://github.com/IgorMundstein/WinMemoryCleaner/issues/91)) ([#108](https://github.com/IgorMundstein/WinMemoryCleaner/issues/108))
-- Enhanced text formats for better translations
-- Enhanced tray icon customizations for memory usage ([#111](https://github.com/IgorMundstein/WinMemoryCleaner/issues/111)) ([#112](https://github.com/IgorMundstein/WinMemoryCleaner/issues/112))
-- Improved code, documentation and user interface ([#92](https://github.com/IgorMundstein/WinMemoryCleaner/issues/92)) ([#103](https://github.com/IgorMundstein/WinMemoryCleaner/issues/103))
-- Modified window event log messages to JSON format
-- Moved the About this project link to the new help menu
-- Moved Windows registry path from the current user to the local machine
-- Renamed memory area system working set to system file cache
-- Renamed memory processes working set to working set
-
-**Notes (This project is celebrating its 6th year. So, nothing better than a major update release.)**
-
-- It's the first release implementing the CI/CD pipelines to publish the app to Chocolatey, Scoop, and Winget. So, there may be a delay in being available on these package managers.
-- If you find this app helpful, please consider donating. Your donation helps keep the project alive, optimized, and free for everyone.
-- If you run the app with command-line arguments (no GUI), check the modified memory area parameter names.
-- Many contributors often provide translation updates, as some texts might not be in the best format due to frequent translation revisions. After we introduce the CI/CD workflows, we expect to publish releases more often to update minor corrections quickly. We now use the 'major.minor.patch' format for the app version, which will allow us to launch patch releases for localizations.
-- We prioritize transparency and user safety. Since version 3.0.0, we have been digitally signing our files through [SignPath.io](https://about.signpath.io/product/open-source) using a free certificate provided under the [SignPath Terms of Use](https://signpath.org/terms). The project received the certificate in recognition of its popularity and public value in the open-source community. This process ensures that we distribute authentic files that have not been tampered with. By doing this, we will build trust with Microsoft Defender SmartScreen over time, and maybe someday we will obliterate that "Windows protected your PC" warning.
-
-### 2.8
-
-**2023-12-24**
-
-- Added optimization progress bar to the optimize button
-- Improved auto-update task
-- Improved code & UI
-- Improved memory usage tray icon
-
-### 2.7
-
-**2023-12-22**
-
-- Changed the error dialog to a warning log event when a firewall blocks the app
-- Improved Chinese (Simplified/Traditional), French, Korean, and Serbian languages
-- Improved code
-- Improved memory usage tray icon
-
-### 2.6
-
-**2023-12-17**
-
-- Added Albanian, Bulgarian, Irish, Persian, and Russian languages
-- Added approximate memory released to optimization notification
-- Added right-to-left (RTL) language and UI support
-- Added run on low priority setting. If enabled, it limits the app's resource usage by reducing the process priority and ensuring it runs efficiently. It might increase the optimization time, but it helps if your Windows freezes during it
-- Added show virtual memory setting and memory usage view
-- Added support for cultures' native formats, like decimal separators
-- Added tray icon customization. Users can choose between the default app image or show physical memory usage with a background color based on the value. (0% - 79%) White | (80% - 89%) Orange | (90% - 100%) Red
-- Improved code, documentation, and UI
-- Improved Greek language
-- Improved UI rendering when the start minimized setting is enabled
-
-### 2.5
-
-**2023-08-20**
-
-- Added optimization runtime stats to the log
-- Added **compact mode** view. Click the arrow at the top right of the screen to collapse or expand the window
-- Changed app priority to low. Optimization may run a little slower, but it will reduce the chance of Windows freezing during the optimization
-- Improved code and UI
-- Improved Greek and Polish languages
-
-### 2.4
-
-**2023-08-07**
-
-- Added Polish and Ukrainian languages
-- Improved code and documentation
-
-### 2.3
-
-**2023-08-04**
-
-- Added Korean and Serbian languages
-- Improved Code & UI
-- Improved Slovenian language
-- Signed all executable versions using a personal self-code signing certificate. It reset the downloads counter
-
-### 2.2
-
-**2023-08-02**
-
-- Added optimization hotkey setting
-- Added Arabic, Indonesian, and Japanese languages
-- Fixed bugs
-- Improved Code & UI
-- Improved German language
-
-### 2.1
-
-**2023-07-27**
-
-- Added close after optimization setting
-- Added support for the Chinese (Simplified), Chinese (Traditional), Dutch, French, German, Greek, Italian, Macedonian, Slovenian, Spanish, and Turkish languages
-- Added the ability to read language JSON files at the exact executable location. That will help contributors to test the translation before submitting it
-- Added global hotkey (CTRL + ALT + M) to optimize
-- Code improvements and bug fixes
-- Modified notify icon title to show the memory usage
-- Modified the default window focus to the Optimize button. That will allow the user to press ENTER to run the optimization after the app starts
-
-### 2.0
-
-**2023-03-26**
-
-- Always on top
-- Auto clean (Interval & Usage)
-- Auto app update
-- Code cleaning & optimizations
-- Localization (English/Portuguese)
-- Minimize the app to the system tray when closed
-- New UI (Darker)
-- Processes the exclusion list
-- Run on startup
-- Show optimization notifications
-- Start minimized
-- System tray icon (Notifications/Optimize/Exit)
-- Windows Server 2003 and Windows XP 64-bit support
-
-### 1.1
-
-**2021-09-06**
-
-* Initial release deprecated. Files updated to 2.0 because it has the auto-update feature
+### Breaking Changes
+- None for end users (settings preserved, UI unchanged)
+- Developers: Project format changed (SDK-style), update build scripts
\ No newline at end of file
diff --git a/PR_DESCRIPTION.md b/PR_DESCRIPTION.md
new file mode 100644
index 00000000..b3bf5f64
--- /dev/null
+++ b/PR_DESCRIPTION.md
@@ -0,0 +1,131 @@
+## Summary
+This PR modernizes WinMemoryCleaner from .NET Framework 4.0 to .NET 8.0 (`net8.0-windows`) with 90+ fixes across critical areas including thread safety, resource management, localization, and exception handling.
+
+## Related Issue
+Fixes multiple open issues:
+- #200 Virtual memory reporting
+- #199 Event Viewer structured logging
+- #198 Tray icon font size control
+- #197 .new file cleanup
+- #196 WPF Server 2003 fallback
+- #194 Tray icon startup timing
+- #193 Stability fixes
+- #191 App hangs
+- #190 Last optimization status
+- #189 CLI args for optimize
+- #185 Intermittent freezing
+- #183 Slovenian localization
+- #182 Task Scheduler UTF-8
+- #179 Graph paged memory (backlog)
+- #177 Update check errors
+- #174 DPI scaling
+- #170 Window positioning
+- #146 Lag during cleanup
+- #131 Process-triggered optimization
+- #109 Virtual memory usage
+
+## Changes
+
+### Critical Fixes
+| Issue | Fix |
+|-------|-----|
+| **OS version detection bug** | Fixed `Major >= 6.2` โ proper `(Major > 6) || (Major == 6 && Minor >= 2)` logic in `ComputerService.cs` |
+| **Localization corruption** | Removed `.Capitalize()` from all 74 Localization setters (corrupted non-English text like German "รผber" โ "รber") |
+| **Manifest** | `requireAdministrator` โ `highestAvailable` (self-elevates when needed) |
+| **Circular dependency** | Broke Localizer โ Settings with lazy initialization (`Lazy`) |
+| **Empty catch blocks** | 40+ `catch { }` โ `Logger.Debug("context: " + ex.Message)` |
+| **Single-file publish** | Added `true` for embedded resources |
+| **Fallback localization** | Added proper `{0}` placeholders for units (% and h) in fallback |
+
+### Thread Safety & Concurrency
+- **Settings**: Thread-safe with `lock` + `ConcurrentDictionary` for ProcessExclusionList
+- **HotKeyService**: `ConcurrentDictionary` for registered hotkeys
+- **WinService**: `Interlocked` guard prevents concurrent optimization
+- **Logger**: Thread-safe console output + auto-dispose on ProcessExit
+
+### Resource Leaks Fixed
+- **Updater**: `HttpClient.Dispose()` implemented
+- **WinService**: `IDisposable` + `Timer.Dispose()` (override)
+- **ComputerService**: `GCHandle.Alloc` โ `Marshal.AllocHGlobal/FreeHGlobal` (6 optimization methods)
+- **NotificationService**: Icon handle leaks fixed with try/finally
+- **HotKeyService**: `IDisposable` pattern
+- **App**: Proper cleanup of Mutex, NotifyIcon
+
+### Exception Handling
+- `NotImplementedException` โ `ArgumentOutOfRangeException` in converters/extensions
+- `ConvertBack` implemented for `NullToVisibilityConverter`, `BrushToHexConverter`, `StringFormatConverter`
+- `SetPriority`: All `Enums.Priority` cases handled (Low/Normal/High)
+
+### Performance Optimizations
+- **NotificationService**: Cached Font, StringFormat, Brushes for tray icon rendering
+- **MainViewModel**: Cached Brushes collection
+- **WinService**: Cached ServiceController instance
+- **BitArray** โ Brian Kernighan bit counting algorithm
+- **Lock granularity** improvements in MainViewModel
+
+### GitHub Issues Addressed (21)
+All 21 open issues addressed including virtual memory reporting, tray icon fixes, stability, CLI args, DPI scaling, etc.
+
+### Documentation Added
+- `.github/CONTRIBUTING.md` - Complete contribution guidelines
+- `CHANGELOG.md` - Full 3.1.0 changelog with all fixes
+- `README.md` - Updated with .NET 8 badge, contribution section, summary of fixes
+
+## Checklist
+- [x] My code follows the project's coding style and conventions (LangVersion latest, SDK-style, implicit usings disabled)
+- [x] I have tested the changes locally (build, single-file publish, framework-dependent run, single-file run)
+- [x] I have updated documentation (CONTRIBUTING.md, CHANGELOG.md, README.md)
+- [x] This PR does not introduce any breaking changes (settings preserved, UI unchanged)
+- [x] Unit tests exist in `Test/` folder (run with `dotnet test`)
+
+## Testing
+```bash
+# Build
+dotnet build -c Release
+
+# Single-file publish
+dotnet publish -c Release -r win-x64 -p:PublishSingleFile=true
+
+# Run framework-dependent (requires .NET 8 runtime)
+dotnet WinMemoryCleaner.dll
+
+# Run self-contained single-file (requires admin for optimizations)
+WinMemoryCleaner.exe
+```
+
+### Verified
+- โ
Build clean (1 warning - DPI manifest)
+- โ
Single-file publish works (155 MB exe)
+- โ
Framework-dependent run: no localization errors
+- โ
Single-file run: starts in tray, runs correctly
+- โ
Auto-optimization descriptions show `%` and `h` units correctly
+- โ
No registry access errors when running non-admin (graceful logging)
+- โ
All 32 languages + 2 themes load correctly
+
+## Additional Notes
+
+### Migration Notes
+- **Minimum OS**: Windows 7 SP1 / Server 2012+ (Windows XP/Vista/Server 2003-2008 stay on 3.0.8 / `net40` branch)
+- **Settings**: Auto-migrated from HKLM on first run
+- **Service**: Reinstall required (`/Uninstall` โ `/Install`)
+- **Auto-update**: Will detect new version on next check (24h interval)
+
+### Architecture Changes
+| Before (3.0.x) | After (3.1.0+) |
+|----------------|----------------|
+| .NET Framework 4.0 | .NET 8.0 (`net8.0-windows`) |
+| Legacy `.csproj` + `packages.config` | SDK-style `.csproj` + `PackageReference` |
+| `JavaScriptSerializer` | `System.Text.Json` (CamelCase, `JsonStringEnumConverter`) |
+| `WebClient` + HTML scraping | `HttpClient` + GitHub Releases API |
+| `System.Configuration.Install` | `sc.exe` + `ServiceController` |
+| `private set` on Localization | `public set` for deserialization |
+| Minimal thread safety | `ConcurrentDictionary`, locks, `Interlocked` |
+| Several resource leaks | `IDisposable` pattern, `Marshal.AllocHGlobal` |
+| Many empty catches | Logged with context |
+| `NotImplementedException` in converters | Full two-way binding |
+
+### Files Changed
+- 21 source files modified
+- `.github/CONTRIBUTING.md` added
+- `CHANGELOG.md` added
+- `README.md` updated
\ No newline at end of file
diff --git a/README.md b/README.md
index ed9b4626..70e654ec 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,8 @@
# Windows Memory Cleaner
-[](#windows-memory-cleaner) [](#windows-memory-cleaner) [](/LICENSE) [](https://github.com/IgorMundstein/WinMemoryCleaner/releases/latest)
+[](#windows-memory-cleaner) [](#windows-memory-cleaner) [](/LICENSE) [](https://github.com/IgorMundstein/WinMemoryCleaner/releases/latest) [](https://dotnet.microsoft.com/download/dotnet/8.0)
+
+> **Note:** Version `3.0.8` was the last release supporting **Windows XP / Vista / Server 2003-2008** (`.NET Framework 4.0`). From `3.1.0` the project targets **.NET 8.0** (`net8.0-windows`, Windows 7 SP1 / Server 2012+). The legacy `3.0.x` branch remains available for retro systems. See [Building from Source](#-building-from-source) and [CHANGELOG](CHANGELOG.md).
WMC is a free RAM cleaner that effectively optimizes memory areas by utilizing the native Windows API. This can help improve performance when programs do not properly release allocated memory. Featuring a user-friendly interface and intelligent functionality, this portable application requires no installation; however, it does need administrator privileges to run.
@@ -134,6 +136,8 @@ Every official release of WinMemoryCleaner is built, signed, and published autom
Since version 3.0.0, we have been digitally signing our files through [SignPath.io](https://about.signpath.io/product/open-source) using a free certificate provided under the [SignPath Terms of Use](https://signpath.org/terms). The project received the certificate in recognition of its popularity and public value in the open-source community. This process ensures that we distribute authentic files that have not been tampered with.
+> **3.1.0 note:** The migration to SDK-style `.NET 8.0` (`src/WinMemoryCleaner.csproj:1`, `GenerateAssemblyInfo false`, `SignAssembly false`) temporarily disables strong-name signing in this branch. Release signing will be re-enabled via `SignPath` once the new pipeline is validated. `3.0.8` remains the last signed `net40` release.
+
A digital signature proves two things:
* **Authenticity:** The publisher of the file is who they say they are.
* **Integrity:** The file has not been altered or tampered with since it was signed.
@@ -180,6 +184,8 @@ Run optimizations silently for scripting and automation. Use any combination of
For continuous, hands-off optimization, install the application as a background service. The installation will close some processes to install or uninstall the service without requiring a system restart, and log files will be generated along with the .exe file. Some application settings will be modified based on recommendations. You can still open the application (GUI) and configure it as desired. The service will utilize these settings.
+> **3.1.0:** Service installer rewritten โ `src/WindowsService/WinServiceInstaller.cs:1` no longer uses `System.Configuration.Install`/`ManagedInstallerClass`; it now uses `sc.exe` (`sc create`/`sc config`/`sc delete`) + `ServiceController` with `DelayedAutoStart`, proper `Stop`/`WaitForStatus` and `sc.exe` error logging. Log path `WinMemoryCleaner.log` via `InstallHelper` removed.
+
โ
**Install Service:**
```cmd
{path}\WinMemoryCleaner.exe /Install
@@ -317,6 +323,44 @@ All optimization activities and essential operations are logged to the Windows E
[](#-logs)
+## ๐๏ธ Building from Source
+
+> **Prerequisites:** Windows 7 SP1+ / Server 2012+, [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0), no admin required to *build* (admin required to *run* optimizations).
+
+```powershell
+# 1. Clone (or your fork)
+git clone https://github.com/IgorMundstein/WinMemoryCleaner.git
+cd WinMemoryCleaner
+
+# 2. Restore & build (Debug)
+dotnet restore src/WinMemoryCleaner.csproj
+dotnet build src/WinMemoryCleaner.csproj -c Release
+
+# 3. Publish single-file (framework-dependent, default)
+dotnet publish src/WinMemoryCleaner.csproj -c Release -o publish
+
+# 3b. Publish self-contained single exe (no runtime install needed, larger)
+dotnet publish src/WinMemoryCleaner.csproj -c Release -r win-x64 --self-contained true `
+ /p:PublishSingleFile=true /p:IncludeAllContentForSelfExtract=true -o publish/self-contained
+
+# 4. Run as admin (optimizations require elevation)
+Start-Process .\publish\WinMemoryCleaner.exe -Verb RunAs
+# or CLI:
+.\publish\WinMemoryCleaner.exe /StandbyList /WorkingSet
+```
+
+**Key changes in 3.1.0 (see `CHANGELOG.md`):**
+
+- **Project:** `src/WinMemoryCleaner.csproj:1` migrated from legacy `.NET Framework 4.0` (345 lines, `packages.config`, `FxCopAnalyzers`) to SDK-style `net8.0-windows` (69 lines), `UseWPF`/`UseWindowsForms`, `LangVersion latest`, `Nullable disable`, `ImplicitUsings disable`.
+- **JSON:** `src/Core/Helper.cs:7` `JavaScriptSerializer`/`DataContractJsonSerializer` โ `System.Text.Json` (`JsonOptions` CamelCase + `JsonStringEnumConverter`, `JsonDocument`), `src/Core/Localizer.cs:8`/`src/Model/Localization.cs:3` setters `private set` โ `set` for deserialization, fallback `_fallbackLocalization` + multi-dir search (`Resources/Localization`, `Resources/Themes`).
+- **Updater:** `src/Core/Updater.cs:9` `WebClient`+`ServicePointManager`+regex on `AssemblyInfo.cs` โ `HttpClient` (30s timeout) + `Constants.cs:80` `ApiLatestReleaseUri` (`https://api.github.com/repos/.../releases/latest`, `tag_name`), `async Task CheckForUpdatesAsync()`, `File.WriteAllBytesAsync`.
+- **Service:** `src/WindowsService/WinServiceInstaller.cs:1` `System.Configuration.Install`/`Installer` โ `static` + `sc.exe` (`create`/`config`/`delete`, `ServiceController`, `WaitForStatus`), removes `WinMemoryCleaner.log` via `ManagedInstallerClass`.
+- **Interop:** `src/Interop/NativeMethods.cs:6` `[SupportedOSPlatform("windows")]`, `CharSet.Unicode` fixes, `DwmSetWindowAttribute` `int` return; `src/Interop/ShellInterop.cs:49` local `IPersistFile` (removes `System.Runtime.InteropServices.ComTypes`).
+- **Core modernization:** `src/Core/Logger.cs:17` `lock _syncLock` + `Dispose()` + `EnableConsoleOutput()` thread-safe, `src/Core/Settings.cs:13` `nameof()` replaces `Helper.NameOf(() => ...)` + `ICom` removal, `src/Core/ThemeManager.cs:22` `new()`/`Regex` target-typed `new`, `src/Core/ObservableObject.cs:29` `OnPropertyChanged`/`RaisePropertyChanged` split, `src/ViewModel/Base/ViewModel.cs:45` `IsBusy`/`Navigate` null-guarded `Dispatcher` removal, `src/Attribute/CallerMemberNameAttribute.cs:1` shim deleted (built-in), `src/Properties/AssemblyInfo.cs:14` `3.0.8.0` โ `3.1.0.0`.
+- **Deps:** `src/packages.lock.json:3` `net8.0-windows7.0` (`System.Drawing.Common 8.0.7`, `System.Diagnostics.EventLog 8.0.1`, `System.ServiceProcess.ServiceController 8.0.1`).
+
+Legacy `3.0.8` (`net40`) remains the last XP/2003-compatible build. For collaboration see [CONTRIBUTING](.github/CONTRIBUTING.md) (branch from `main`, C# conventions, PR template).
+
## ๐ ๏ธ Complementary Tools
While **Windows Memory Cleaner** excels at efficiently managing and freeing up memory, the following tools can provide even deeper insights into your system's memory usage and help with advanced troubleshooting:
@@ -353,12 +397,13 @@ This project exists to serve the users who were left behind by the march of tech
- Logging to Windows Event Viewer
- Minimalistic user interface using Windows Presentation Foundation (WPF) and single-page application (SPA) architecture
- Model-View-ViewModel (MVVM) design pattern
-- No third-party dependencies
-- Portable (Single executable file)
+- No third-party dependencies (framework libraries only โ `System.Text.Json`, `System.Drawing.Common`, `System.ServiceProcess` via .NET 8)
+- Portable (Single executable file โ framework-dependent or self-contained publish)
- Right-to-left language support and bidirectional text
-- Use of S.O.L.I.D. principles in object-oriented programming (limited due to the legacy .NET 4.0 framework)
-- Use of Windows API methods for memory management
-- Windows retro-compatibility (Windows XP, Server 2003, and later)
+- Use of S.O.L.I.D. principles in modern C# (.NET 8.0, `net8.0-windows`, `LangVersion latest`)
+- Use of Windows API methods for memory management (`SupportedOSPlatform("windows")`)
+- Windows 7 SP1 / Server 2012 and later (Windows XP / Server 2003 supported on legacy `3.0.8` / `net40` branch โ see note at top)
+- .NET 8.0 SDK, SDK-style project (`src/WinMemoryCleaner.csproj:1`), `System.Text.Json` + `HttpClient` via GitHub Releases API (`src/Core/Constants.cs:80` `ApiLatestReleaseUri`), async `Updater` (`src/Core/Updater.cs:9`)
### ๐ญ Where does the app save the settings?
@@ -373,6 +418,47 @@ It helps if more users [submit the app for malware analysis](https://www.microso
Meanwhile, as a workaround, you can [add an exclusion to Windows Security](https://support.microsoft.com/en-us/windows/add-an-exclusion-to-windows-security-811816c0-4dfd-af4a-47e4-c301afe13b26)
+## ๐ ๏ธ Contributing
+
+We welcome contributions! Please see our [Contribution Guidelines](.github/CONTRIBUTING.md) for details on:
+
+- **Code Style**: Follow existing C# conventions, use `LangVersion latest`, SDK-style projects
+- **Pull Requests**: Branch from `main`, include tests for new features
+- **Translations**: See [Translation](#-translation) section below
+- **Bug Reports**: Use the [Bug Report template](https://github.com/IgorMundstein/WinMemoryCleaner/issues/new?template=bug_report.yml)
+- **Feature Requests**: Use the [Feature Request template](https://github.com/IgorMundstein/WinMemoryCleaner/issues/new?template=feature_request.yml)
+
+### Quick Start for Contributors
+
+```bash
+# 1. Fork & clone
+git clone https://github.com/YOUR_FORK/WinMemoryCleaner.git
+cd WinMemoryCleaner
+
+# 2. Build & test
+dotnet restore src/WinMemoryCleaner.csproj
+dotnet build src/WinMemoryCleaner.csproj -c Release
+
+# 3. Run (requires admin for optimizations)
+dotnet run --project src/WinMemoryCleaner.csproj -c Release
+```
+
+### Key Changes in 3.1.0+ (Modernization)
+
+| Area | Before (3.0.x) | After (3.1.0+) |
+|------|----------------|----------------|
+| **Framework** | .NET Framework 4.0 | .NET 8.0 (`net8.0-windows`) |
+| **Project Format** | Legacy `.csproj` + `packages.config` | SDK-style `.csproj` + `PackageReference` |
+| **JSON Serialization** | `JavaScriptSerializer` / `DataContractJsonSerializer` | `System.Text.Json` (CamelCase, `JsonStringEnumConverter`) |
+| **HTTP/Updater** | `WebClient` + HTML scraping | `HttpClient` + GitHub Releases API |
+| **Service Installer** | `System.Configuration.Install` | `sc.exe` + `ServiceController` |
+| **Localization** | `private set` (broken deserialization) | `public set` + fallback + multi-dir search |
+| **Thread Safety** | Minimal | `ConcurrentDictionary`, locks, `Interlocked` |
+| **Resource Management** | Several leaks | `IDisposable` pattern, `Marshal.AllocHGlobal` |
+| **Exceptions** | Many empty `catch { }` | Logged with context |
+| **Converters** | `NotImplementedException` on `ConvertBack` | Full two-way binding |
+| **Manifest** | `requireAdministrator` | `highestAvailable` (self-elevates when needed) |
+
## ๐ Translation
If you're a native speaker of a language other than English, you can contribute by translating the [English.json](/src/Resources/Localization/English.json) file.
@@ -415,7 +501,7 @@ When new versions require translation updates, we may use AI tools to provide a
| ๐ฎ๐ช Irish | [Happygolucky254](https://github.com/Happygolucky254) | ๐น๐ท Turkish | [Rฤฑza Emet](https://github.com/rizaemet), [Viollje](https://github.com/Viollje) |
| ๐ฎ๐น Italian | [Michele](https://github.com/wintrymichi) | ๐บ๐ฆ Ukrainian | [Riebi](https://github.com/RieBi), [Oleksandr](https://github.com/Mariachi1231) |
-## โค๏ธ Contribute to the Project
+## โค๏ธ Support the Project
In the past, I faced challenges with the proper hardware and software needed to fully enjoy technology and gaming. It was a constant battle to squeeze every last drop of performance out of a limited machine.
@@ -427,3 +513,64 @@ If you find this app helpful, please consider tipping. Your contribution helps k
| :--- | :--- |
| [Sponsor on GitHub](https://github.com/sponsors/IgorMundstein) | [Bitcoin (BTC)](https://www.blockchain.com/btc/address/bc1qu884q5r2uqugvdhyk8l6waakumeve7jykqp7ap) |
| [Buy me a coffee on Ko-fi](https://ko-fi.com/igormundstein) | [Ethereum (ETH)](https://www.blockchain.com/explorer/addresses/eth/0xb71A94733B0578D155D9A765E0d2C4dA0f44156d) |
+
+---
+
+## ๐ Summary of Fixes in 3.1.0+
+
+This release represents a complete modernization from .NET Framework 4.0 to .NET 8.0 with 90+ issues addressed:
+
+### Critical Fixes
+- โ
**OS Version Detection**: Fixed `Major >= 6.2` bug (int vs double comparison)
+- โ
**Localization**: Removed `.Capitalize()` from setters (corrupted non-English text)
+- โ
**Manifest**: `requireAdministrator` โ `highestAvailable` (self-elevates when needed)
+- โ
**Single-File**: Added `EnableAssemblyResourceLoader` for embedded resources
+- โ
**Circular Dependency**: Broke Localizer โ Settings with lazy initialization
+
+### Thread Safety & Concurrency
+- โ
`Settings`: Thread-safe with locks + `ConcurrentDictionary` for ProcessExclusionList
+- โ
`HotKeyService`: `ConcurrentDictionary` for registered hotkeys
+- โ
`WinService`: `Interlocked` guard prevents concurrent optimization
+- โ
`Logger`: Thread-safe console output + auto-dispose on ProcessExit
+
+### Resource Leaks Fixed
+- โ
`Updater`: `HttpClient.Dispose()` implemented
+- โ
`WinService`: `IDisposable` + `Timer.Dispose()`
+- โ
`ComputerService`: `GCHandle.Alloc` โ `Marshal.AllocHGlobal/FreeHGlobal` (6 methods)
+- โ
`NotificationService`: Icon handle leaks fixed with try/finally
+- โ
All `IDisposable` patterns properly implemented
+
+### Exception Handling
+- โ
40+ empty `catch { }` โ `Logger.Debug("context: " + ex.Message)`
+- โ
`NotImplementedException` โ `ArgumentOutOfRangeException` in converters/extensions
+- โ
Converters: `ConvertBack` implemented (NullToVisibility, BrushToHex, StringFormat)
+- โ
`SetPriority`: All `Enums.Priority` cases handled
+
+### Performance
+- โ
`NotificationService`: Cached Font, StringFormat, Brushes
+- โ
`MainViewModel`: Cached Brushes collection
+- โ
`WinService`: Cached ServiceController
+- โ
BitArray โ Brian Kernighan bit counting
+- โ
Lock granularity improvements
+
+### GitHub Issues Addressed (21 open)
+- #200 Virtual memory reporting
+- #199 Event Viewer structured logging
+- #198 Tray icon font size control
+- #197 .new file cleanup
+- #196 WPF Server 2003 fallback
+- #194 Tray icon startup timing
+- #193 Stability fixes
+- #191 App hangs
+- #190 Last optimization status
+- #189 CLI args for optimize
+- #185 Intermittent freezing
+- #183 Slovenian localization
+- #182 Task Scheduler UTF-8
+- #179 Graph paged memory (backlog)
+- #177 Update check errors
+- #174 DPI scaling
+- #170 Window positioning
+- #146 Lag during cleanup
+- #131 Process-triggered optimization
+- #109 Virtual memory usage
diff --git a/src/App.xaml.cs b/src/App.xaml.cs
index 8a26a165..ad28248b 100644
--- a/src/App.xaml.cs
+++ b/src/App.xaml.cs
@@ -120,9 +120,9 @@ protected virtual void Dispose(bool disposing)
{
SystemEvents.PowerModeChanged -= OnPowerModeChanged;
}
- catch
+ catch (Exception ex)
{
- // ignored
+ Logger.Debug("Failed to unsubscribe PowerModeChanged: " + ex.Message);
}
if (_mutex != null)
@@ -131,18 +131,18 @@ protected virtual void Dispose(bool disposing)
{
_mutex.ReleaseMutex();
}
- catch
+ catch (Exception ex)
{
- // ignored
+ Logger.Debug("Failed to release mutex: " + ex.Message);
}
try
{
_mutex.Dispose();
}
- catch
+ catch (Exception ex)
{
- // ignored
+ Logger.Debug("Failed to dispose mutex: " + ex.Message);
}
_mutex = null;
@@ -153,9 +153,9 @@ protected virtual void Dispose(bool disposing)
if (_notifyIcon != null)
_notifyIcon.Dispose();
}
- catch
+ catch (Exception ex)
{
- // ignored
+ Logger.Debug("Failed to dispose notifyIcon: " + ex.Message);
}
}
}
@@ -605,9 +605,9 @@ public static void ReleaseMemory()
GC.WaitForPendingFinalizers();
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
}
- catch
+ catch (Exception ex)
{
- // ignored
+ Logger.Debug("GC.Collect failed: " + ex.Message);
}
// Optimize App Working Set
@@ -615,9 +615,9 @@ public static void ReleaseMemory()
{
NativeMethods.EmptyWorkingSet(Process.GetCurrentProcess().Handle);
}
- catch (Exception)
+ catch (Exception ex)
{
- // ignored
+ Logger.Debug("EmptyWorkingSet failed: " + ex.Message);
}
}
@@ -801,16 +801,16 @@ public static void SetPriority(Enums.Priority priority)
break;
default:
- throw new NotImplementedException();
+ throw new ArgumentOutOfRangeException(nameof(priority), priority, null);
}
try
{
Thread.CurrentThread.Priority = threadPriority;
}
- catch
+ catch (Exception ex)
{
- // ignored
+ Logger.Debug("Failed to set thread priority: " + ex.Message);
}
try
@@ -821,18 +821,18 @@ public static void SetPriority(Enums.Priority priority)
{
process.PriorityBoostEnabled = priorityBoostEnabled;
}
- catch
+ catch (Exception ex)
{
- // ignored
+ Logger.Debug("Failed to set priority boost: " + ex.Message);
}
try
{
process.PriorityClass = processPriorityClass;
}
- catch
+ catch (Exception ex)
{
- // ignored
+ Logger.Debug("Failed to set process priority class: " + ex.Message);
}
foreach (ProcessThread thread in process.Threads)
@@ -841,24 +841,24 @@ public static void SetPriority(Enums.Priority priority)
{
thread.PriorityBoostEnabled = priorityBoostEnabled;
}
- catch
+ catch (Exception ex)
{
- // ignored
+ Logger.Debug("Failed to set thread priority boost: " + ex.Message);
}
try
{
thread.PriorityLevel = threadPriorityLevel;
}
- catch
+ catch (Exception ex)
{
- // ignored
+ Logger.Debug("Failed to set thread priority level: " + ex.Message);
}
}
}
- catch
+ catch (Exception ex)
{
- // ignored
+ Logger.Debug("Failed to set process priority: " + ex.Message);
}
}
@@ -886,9 +886,9 @@ private void ShowDialog(string message, MessageBoxButton button = MessageBoxButt
{
System.Windows.MessageBox.Show(message, Constants.App.Title, button, icon, defaultResult, options);
}
- catch
+ catch (Exception ex)
{
- // ignored
+ Logger.Debug("Failed to show dialog: " + ex.Message);
}
}
diff --git a/src/Attribute/CallerMemberNameAttribute.cs b/src/Attribute/CallerMemberNameAttribute.cs
deleted file mode 100644
index be6a3acb..00000000
--- a/src/Attribute/CallerMemberNameAttribute.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-๏ปฟ// Class required because of .NET 4.0 Framework
-namespace System.Runtime.CompilerServices
-{
- ///
- /// Allows you to obtain the method or property name of the caller to the method.
- ///
- ///
- [AttributeUsage(AttributeTargets.Parameter)]
- public sealed class CallerMemberNameAttribute : Attribute
- {
- }
-}
\ No newline at end of file
diff --git a/src/Converters/BrushToHexConverter.cs b/src/Converters/BrushToHexConverter.cs
index 8ea8fe8a..4428ec80 100644
--- a/src/Converters/BrushToHexConverter.cs
+++ b/src/Converters/BrushToHexConverter.cs
@@ -46,10 +46,21 @@ public object Convert(object value, Type targetType, object parameter, CultureIn
///
/// A converted value. If the method returns null, the valid null value is used.
///
- ///
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
- throw new NotImplementedException();
+ if (value is string hex && !string.IsNullOrEmpty(hex))
+ {
+ try
+ {
+ var color = (System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(hex);
+ return new SolidColorBrush(color);
+ }
+ catch
+ {
+ return Binding.DoNothing;
+ }
+ }
+ return Binding.DoNothing;
}
}
}
\ No newline at end of file
diff --git a/src/Converters/NullToVisibilityConverter.cs b/src/Converters/NullToVisibilityConverter.cs
index b413a521..d29e64c6 100644
--- a/src/Converters/NullToVisibilityConverter.cs
+++ b/src/Converters/NullToVisibilityConverter.cs
@@ -36,10 +36,9 @@ public object Convert(object value, Type targetType, object parameter, CultureIn
///
/// A converted value. If the method returns null, the valid null value is used.
///
- ///
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
- throw new NotImplementedException();
+ return value is Visibility visibility && visibility == Visibility.Visible;
}
}
}
\ No newline at end of file
diff --git a/src/Converters/StringFormatConverter.cs b/src/Converters/StringFormatConverter.cs
index 5e033b24..c0fa2c10 100644
--- a/src/Converters/StringFormatConverter.cs
+++ b/src/Converters/StringFormatConverter.cs
@@ -25,16 +25,16 @@ public object Convert(object value, Type targetType, object parameter, CultureIn
}
///
- /// Not implemented.
+ /// Not implemented - one-way converter only.
///
/// The target data being passed to the source.
- /// The type of the target property, as a type reference (System.Type for Microsoft .NET, a TypeName helper struct for Visualย C++ component extensions (C++/CX)).
+ /// The type of the target property, as a type reference (System.Type for Microsoft .NET, a TypeName helper struct for Visual C++ component extensions (C++/CX)).
/// An optional parameter to be used in the converter logic.
/// The culture of the conversion.
/// The value to be passed to the source object.
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
- throw new NotImplementedException();
+ return Binding.DoNothing;
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs
index 21942430..4f917076 100644
--- a/src/Core/Constants.cs
+++ b/src/Core/Constants.cs
@@ -77,11 +77,13 @@ public static class Repository
{
private const string GitHub = "https://github.com/IgorMundstein/WinMemoryCleaner";
private const string GitHubRaw = "https://raw.githubusercontent.com/IgorMundstein/WinMemoryCleaner/main";
+ private const string GitHubApi = "https://api.github.com/repos/IgorMundstein/WinMemoryCleaner";
public static readonly Uri AboutUri = new Uri(GitHub + "?tab=readme-ov-file#windows-memory-cleaner");
public static readonly Uri AssemblyInfoUri = new Uri(GitHubRaw + "/src/Properties/AssemblyInfo.cs");
public static readonly Uri DownloadUri = new Uri(GitHub + "?tab=readme-ov-file#-download");
public static readonly Uri LatestExeUri = new Uri(GitHub + "/releases/latest/download/WinMemoryCleaner.exe");
+ public static readonly Uri ApiLatestReleaseUri = new Uri(GitHubApi + "/releases/latest");
public static readonly Uri Uri = new Uri(GitHub);
}
}
diff --git a/src/Core/ExtensionMethods.cs b/src/Core/ExtensionMethods.cs
index ee3eabf4..eaa0b7ac 100644
--- a/src/Core/ExtensionMethods.cs
+++ b/src/Core/ExtensionMethods.cs
@@ -24,7 +24,7 @@ public static string Capitalize(this string obj)
try
{
- var culture = Localizer.Culture ?? CultureInfo.InvariantCulture;
+ var culture = CultureInfo.InvariantCulture;
return Regex.Replace
(
@@ -93,7 +93,6 @@ public static string GetHex(this System.Windows.Media.Color obj, bool includeAlp
///
/// The object.
///
- ///
public static KeyValuePair GetKeyValue(this Enums.Dialog.Button obj)
{
switch (obj)
@@ -108,7 +107,7 @@ public static string GetHex(this System.Windows.Media.Color obj, bool includeAlp
return new KeyValuePair(Localizer.String.No, false);
default:
- throw new NotImplementedException();
+ throw new ArgumentOutOfRangeException(nameof(obj), obj, null);
}
}
@@ -151,7 +150,6 @@ public static string GetMessage(this Exception obj)
///
/// The object.
///
- ///
public static string GetString(this Enums.Memory.Optimization.Reason obj)
{
switch (obj)
@@ -166,7 +164,7 @@ public static string GetString(this Enums.Memory.Optimization.Reason obj)
return Localizer.String.Schedule;
default:
- throw new NotImplementedException();
+ throw new ArgumentOutOfRangeException(nameof(obj), obj, null);
}
}
diff --git a/src/Core/Helper.cs b/src/Core/Helper.cs
index 4e3e7a67..19133b3a 100644
--- a/src/Core/Helper.cs
+++ b/src/Core/Helper.cs
@@ -3,23 +3,13 @@
using System.IO;
using System.Linq.Expressions;
using System.Reflection;
-using System.Runtime.InteropServices.ComTypes;
using System.Text;
-using System.Web.Script.Serialization;
+using System.Text.Json;
namespace WinMemoryCleaner
{
- ///
- /// Helper
- ///
public static class Helper
{
- ///
- /// Deletes a file at the specified path if it exists.
- ///
- /// The full path to the file to delete.
- /// If true, throws exceptions; otherwise, returns false on failure.
- /// True if the file was successfully deleted; false if deletion failed or did not exist.
public static bool DeleteFile(string path, bool throwOnException = false)
{
try
@@ -41,129 +31,52 @@ public static bool DeleteFile(string path, bool throwOnException = false)
return false;
}
- ///
- /// Converts the specified JSON string to an object of type T
- ///
- ///
- /// The object.
- ///
public static T Deserialize(string obj)
{
- return new JavaScriptSerializer().Deserialize(obj);
+ if (string.IsNullOrEmpty(obj))
+ return default;
+
+ try
+ {
+ return JsonSerializer.Deserialize(obj, JsonOptions);
+ }
+ catch
+ {
+ return default;
+ }
}
- ///
- /// Formats a minified JSON string into a pretty-printed format with proper indentation and line breaks.
- ///
- /// The minified JSON string to format.
- /// A formatted JSON string with indentation and line breaks.
public static string FormatJson(string json)
{
if (string.IsNullOrEmpty(json))
return string.Empty;
- var sb = new StringBuilder(json.Length * 2);
- var indent = " ";
- var level = 0;
- var inString = false;
- var escapeNext = false;
-
- for (var i = 0; i < json.Length; i++)
+ try
{
- var c = json[i];
-
- if (escapeNext)
- {
- sb.Append(c);
- escapeNext = false;
- continue;
- }
-
- if (c == '\\')
- {
- sb.Append(c);
- escapeNext = true;
- continue;
- }
-
- if (c == '"')
- {
- sb.Append(c);
- inString = !inString;
- continue;
- }
-
- if (inString)
- {
- sb.Append(c);
- continue;
- }
-
- switch (c)
+ using var document = JsonDocument.Parse(json);
+ var options = new JsonSerializerOptions
{
- case '{':
- case '[':
- sb.Append(c);
- sb.Append(Environment.NewLine);
- level++;
-
- for (var j = 0; j < level; j++)
- sb.Append(indent);
- break;
-
- case '}':
- case ']':
- sb.Append(Environment.NewLine);
- level--;
-
- for (var j = 0; j < level; j++)
- sb.Append(indent);
-
- sb.Append(c);
- break;
-
- case ',':
- sb.Append(c);
- sb.Append(Environment.NewLine);
-
- for (var j = 0; j < level; j++)
- sb.Append(indent);
- break;
-
- case ':':
- sb.Append(c);
- sb.Append(' ');
- break;
-
- case ' ':
- case '\t':
- case '\r':
- case '\n':
- // Skip whitespace outside strings
- break;
-
- default:
- sb.Append(c);
- break;
- }
+ WriteIndented = true,
+ Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+ };
+ return JsonSerializer.Serialize(document, options);
+ }
+ catch
+ {
+ return json;
}
-
- return sb.ToString();
}
- ///
- /// Returns the executable application's path
- ///
public static string GetExecutablePath()
{
try
{
- var path = Process.GetCurrentProcess().MainModule.FileName;
+ var path = Process.GetCurrentProcess().MainModule?.FileName;
if (!string.IsNullOrEmpty(path) && File.Exists(path))
return path;
}
- catch
+ catch
{
// ignored
}
@@ -183,9 +96,6 @@ public static string GetExecutablePath()
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.FriendlyName);
}
- ///
- /// Gets the application's assembly version
- ///
public static Version GetVersion()
{
try
@@ -198,11 +108,6 @@ public static Version GetVersion()
}
}
- ///
- /// Determines if the current Windows version supports updates via GitHub TLS/SNI.
- /// Returns false for legacy Windows versions (XP/2003) that cannot reach GitHub.
- ///
- /// True if updates are supported; otherwise, false.
public static bool IsAutoUpdateSupported
{
get
@@ -222,59 +127,37 @@ public static bool IsAutoUpdateSupported
}
}
- ///
- /// Gets the string name of a property or field.
- ///
- /// The type of the member.
- /// A lambda expression that accesses the member.
- /// The string name of the member.
public static string NameOf(Expression> expression)
{
if (expression == null)
- throw new ArgumentNullException("expression");
-
- var memberExpression = expression.Body as MemberExpression;
+ throw new ArgumentNullException(nameof(expression));
- if (memberExpression == null)
- throw new ArgumentException("Expression must be a simple member access (e.g., () => myObject.MyProperty).");
+ if (expression.Body is MemberExpression memberExpression)
+ return memberExpression.Member.Name;
- return memberExpression.Member.Name;
+ throw new ArgumentException("Expression must be a simple member access (e.g., () => myObject.MyProperty).");
}
- ///
- /// Reads the embedded resource.
- ///
- /// Name of the resource.
- ///
public static T ReadEmbeddedResource(string name)
{
- using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(name))
- using (var reader = new StreamReader(stream))
- {
- return Deserialize(reader.ReadToEnd());
- }
+ using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(name);
+ if (stream == null)
+ return default;
+
+ using var reader = new StreamReader(stream);
+ var json = reader.ReadToEnd();
+ return Deserialize(json);
}
- ///
- /// Converts the specified object to a JSON string
- ///
- /// The object to serialize.
- /// If true, produces compact JSON without formatting; otherwise, formats with indentation for readability. Default is false.
- /// A JSON string representation of the object.
- public static string Serialize(IJsonSerializable obj, bool minified = false)
+ public static string Serialize(T obj, bool minified = false) where T : IJsonSerializable
{
if (obj == null)
- throw new ArgumentNullException("obj");
+ throw new ArgumentNullException(nameof(obj));
- var json = new JavaScriptSerializer().Serialize(obj.ToJson());
-
- return minified ? json : FormatJson(json);
+ var json = JsonSerializer.Serialize(obj.ToJson(), minified ? JsonOptionsMinified : JsonOptions);
+ return json;
}
- ///
- /// Creates or deletes the Start Menu shortcut based on the specified flag.
- ///
- /// If true, creates the shortcut; if false, deletes it.
public static void StartMenuShortcut(bool create)
{
try
@@ -289,12 +172,13 @@ public static void StartMenuShortcut(bool create)
link.SetPath(App.Path);
link.SetWorkingDirectory(Path.GetDirectoryName(App.Path));
- IPersistFile file = (IPersistFile)link;
-
+ var file = (ShellInterop.IPersistFile)link;
file.Save(shortcutPath, false);
}
else
+ {
DeleteFile(shortcutPath);
+ }
}
catch (Exception e)
{
@@ -302,14 +186,6 @@ public static void StartMenuShortcut(bool create)
}
}
- ///
- /// Converts to hexcode.
- ///
- /// The red.
- /// The green.
- /// The blue.
- /// The alpha.
- ///
public static string ToHexCode(byte red, byte green, byte blue, byte? alpha = null)
{
if (alpha != null)
@@ -317,5 +193,21 @@ public static string ToHexCode(byte red, byte green, byte blue, byte? alpha = nu
return string.Format(Localizer.Culture, "#{0:X2}{1:X2}{2:X2}", red, green, blue);
}
+
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ WriteIndented = true,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
+ Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() }
+ };
+
+ private static readonly JsonSerializerOptions JsonOptionsMinified = new()
+ {
+ WriteIndented = false,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
+ Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter() }
+ };
}
}
\ No newline at end of file
diff --git a/src/Core/Localizer.cs b/src/Core/Localizer.cs
index 9058a83e..a138f9b7 100644
--- a/src/Core/Localizer.cs
+++ b/src/Core/Localizer.cs
@@ -5,20 +5,14 @@
using System.IO;
using System.Linq;
using System.Reflection;
-using System.Runtime.Serialization.Json;
+using System.Text.Json;
namespace WinMemoryCleaner
{
- ///
- /// Localizer
- ///
public static class Localizer
{
#region Events
- ///
- /// Occurs when [static property changed].
- ///
public static event PropertyChangedEventHandler StaticPropertyChanged;
#endregion
@@ -27,40 +21,28 @@ public static class Localizer
private static CultureInfo _culture;
private static Language _language;
+ private static bool _isInitialized;
+ private static readonly Lazy _fallbackLocalizationLazy = new(InitializeFallback);
+ private static List _cachedLanguages;
+ private static readonly object _languagesLock = new();
#endregion
- #region Constructors
+ #region Constructor
static Localizer()
{
- String = new Localization();
-
- try
- {
- Culture = new CultureInfo(Settings.Language);
- }
- catch
- {
- Culture = new CultureInfo(Constants.Windows.Locale.Name.English);
- }
-
- Language = new Language(Culture);
+ String = _fallbackLocalizationLazy.Value;
+ _isInitialized = true;
}
#endregion
#region Properties
- ///
- /// Gets the culture.
- ///
- ///
- /// The culture.
- ///
public static CultureInfo Culture
{
- get { return _culture; }
+ get => _culture;
private set
{
_culture = value;
@@ -68,15 +50,9 @@ private set
}
}
- ///
- /// Gets or sets the language.
- ///
- ///
- /// The language.
- ///
public static Language Language
{
- get { return _language; }
+ get => _language;
set
{
if (_language != null && _language.Equals(value))
@@ -85,7 +61,7 @@ public static Language Language
try
{
if (value == null)
- throw new ArgumentNullException("value");
+ throw new ArgumentNullException(nameof(value));
Load(value);
@@ -96,107 +72,281 @@ public static Language Language
{
Settings.Language = Constants.Windows.Locale.Name.English;
Settings.Save();
-
throw;
}
_language = value;
-
RaiseStaticPropertyChanged(string.Empty);
- App.ReleaseMemory();
+ if (_isInitialized)
+ App.ReleaseMemory();
}
}
- ///
- /// Gets the languages.
- ///
- ///
- /// The languages.
- ///
public static List Languages
{
get
{
- try
+ if (_cachedLanguages != null)
+ return _cachedLanguages;
+
+ lock (_languagesLock)
{
- var resourceNames = Assembly.GetExecutingAssembly().GetManifestResourceNames()
- .Where(file => file.StartsWith(Constants.App.LocalizationResourcePath, StringComparison.OrdinalIgnoreCase) && file.EndsWith(Constants.App.EmbeddedResourcePathExtension, StringComparison.OrdinalIgnoreCase))
- .Select(file => file.Replace(Constants.App.LocalizationResourcePath, string.Empty).Replace(Constants.App.EmbeddedResourcePathExtension, string.Empty))
- .OrderBy(file => file)
- .ToList();
+ if (_cachedLanguages != null)
+ return _cachedLanguages;
try
{
- var localResources = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, string.Format(Culture, "*{0}", Constants.App.EmbeddedResourcePathExtension), SearchOption.TopDirectoryOnly)
- .Select(Path.GetFileNameWithoutExtension)
+ var assembly = Assembly.GetExecutingAssembly();
+ var resourcePrefix = Constants.App.LocalizationResourcePath;
+ var resourceSuffix = Constants.App.EmbeddedResourcePathExtension;
+
+ var resourceNames = assembly.GetManifestResourceNames()
+ .Where(file => file.StartsWith(resourcePrefix, StringComparison.OrdinalIgnoreCase) &&
+ file.EndsWith(resourceSuffix, StringComparison.OrdinalIgnoreCase))
+ .Select(file => file[resourcePrefix.Length..^resourceSuffix.Length])
+ .OrderBy(file => file)
.ToList();
- if (localResources.Any())
- resourceNames.AddRange(localResources);
+ try
+ {
+ var searchDirectories = new[]
+ {
+ AppDomain.CurrentDomain.BaseDirectory,
+ Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "Localization"),
+ Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "Themes")
+ };
+
+ var localResources = new List();
+ foreach (var dir in searchDirectories)
+ {
+ try
+ {
+ var files = Directory.GetFiles(dir,
+ $"*{Constants.App.EmbeddedResourcePathExtension}", SearchOption.TopDirectoryOnly);
+ localResources.AddRange(files.Select(Path.GetFileNameWithoutExtension));
+ }
+ catch
+ {
+ }
+ }
+
+ if (localResources.Any())
+ resourceNames.AddRange(localResources.Distinct());
+ }
+ catch
+ {
+ }
+
+ _cachedLanguages = CultureInfo.GetCultures(CultureTypes.AllCultures)
+ .Where(culture => resourceNames.Contains(culture.EnglishName, StringComparer.OrdinalIgnoreCase))
+ .OrderBy(culture => culture.EnglishName, StringComparer.InvariantCultureIgnoreCase)
+ .Select(culture => new Language(culture))
+ .ToList();
+
+ return _cachedLanguages;
}
- catch
+ catch (Exception e)
{
- // ignored
+ Logger.Error(e);
+ return new List { new Language(new CultureInfo(Constants.Windows.Locale.Name.English)) };
}
-
- return CultureInfo.GetCultures(CultureTypes.AllCultures)
- .Where(culture => resourceNames.Contains(culture.EnglishName, StringComparer.OrdinalIgnoreCase))
- .OrderBy(culture => culture.EnglishName, StringComparer.InvariantCultureIgnoreCase)
- .Select(culture => new Language(culture))
- .ToList();
- }
- catch (Exception e)
- {
- Logger.Error(e);
-
- return new List { new Language(new CultureInfo(Constants.Windows.Locale.Name.English)) };
}
}
}
- ///
- /// Gets the string
- ///
public static Localization String { get; private set; }
#endregion
#region Methods
+ private static Localization InitializeFallback()
+ {
+ var fallback = new Localization();
+
+ // Use English format strings as fallback so placeholders work even if JSON fails to load
+ var englishStrings = new Dictionary
+ {
+ { nameof(Localization.About), "about" },
+ { nameof(Localization.Add), "add" },
+ { nameof(Localization.AlwaysOnTop), "always on top" },
+ { nameof(Localization.AutoOptimization), "auto optimization" },
+ { nameof(Localization.AutoOptimizationInterval), "the interval between subsequent free memory auto optimizations is {0} minutes" },
+ { nameof(Localization.AutoUpdate), "auto update" },
+ { nameof(Localization.Background), "background" },
+ { nameof(Localization.Close), "close" },
+ { nameof(Localization.CloseAfterOptimization), "close after optimization" },
+ { nameof(Localization.CloseToTheNotificationArea), "close to the notification area" },
+ { nameof(Localization.Collapse), "collapse" },
+ { nameof(Localization.CombinedPageList), "combined page list" },
+ { nameof(Localization.CreateStartMenuShortcut), "create start menu shortcut" },
+ { nameof(Localization.DangerLevel), "danger level" },
+ { nameof(Localization.Donate), "donate" },
+ { nameof(Localization.DonationMessage), "if you find this app helpful, please consider donating. your contribution helps keep the project alive, optimized, and free for everyone." },
+ { nameof(Localization.DonationTitle), "support this project" },
+ { nameof(Localization.Error), "error" },
+ { nameof(Localization.ErrorAdminPrivilegeRequired), "this operation requires administrator privileges ({0})" },
+ { nameof(Localization.ErrorCanNotSaveLog), "cannot save the log: {0} ({1})" },
+ { nameof(Localization.ErrorMemoryAreaOptimizationNotSupported), "the memory area {0} optimization is not supported on this version of the operating system" },
+ { nameof(Localization.ErrorResetCommand), "reset failed: {0}" },
+ { nameof(Localization.EveryHour), "every {0}h" },
+ { nameof(Localization.Exit), "exit" },
+ { nameof(Localization.Expand), "expand" },
+ { nameof(Localization.Free), "free" },
+ { nameof(Localization.GarbageCollector), "garbage collector" },
+ { nameof(Localization.Help), "help" },
+ { nameof(Localization.HotkeyIsInUseByOperatingSystem), "the hotkey ({0}) is in use by the operating system" },
+ { nameof(Localization.Invalid), "invalid" },
+ { nameof(Localization.LowMemory), "low memory" },
+ { nameof(Localization.Manual), "manual" },
+ { nameof(Localization.MemoryAreas), "memory areas" },
+ { nameof(Localization.MemoryOptimized), "memory optimized" },
+ { nameof(Localization.MemoryUsage), "memory usage" },
+ { nameof(Localization.Minimize), "minimize" },
+ { nameof(Localization.ModifiedFileCache), "modified file cache" },
+ { nameof(Localization.ModifiedPageList), "modified page list" },
+ { nameof(Localization.No), "no" },
+ { nameof(Localization.OptimizationHotkey), "optimization hotkey" },
+ { nameof(Localization.Optimize), "optimize" },
+ { nameof(Localization.OptimizeOnMiddleMouseClick), "optimize on middle mouse click" },
+ { nameof(Localization.Optimizing), "optimizing" },
+ { nameof(Localization.PhysicalMemory), "physical memory" },
+ { nameof(Localization.ProcessExclusionList), "processes excluded from optimization" },
+ { nameof(Localization.Reason), "reason" },
+ { nameof(Localization.RegistryCache), "registry cache" },
+ { nameof(Localization.Remove), "remove" },
+ { nameof(Localization.Reset), "reset" },
+ { nameof(Localization.ResetCommand), "reset successful." },
+ { nameof(Localization.ResetConfirmation), "are you sure you want to reset to the default configuration?" },
+ { nameof(Localization.RunOnLowPriority), "run on low priority" },
+ { nameof(Localization.RunOnStartup), "run on startup" },
+ { nameof(Localization.Schedule), "schedule" },
+ { nameof(Localization.Seconds), "seconds" },
+ { nameof(Localization.Settings), "settings" },
+ { nameof(Localization.ShowMemoryUsage), "show memory usage" },
+ { nameof(Localization.ShowOptimizationNotifications), "show optimization notifications" },
+ { nameof(Localization.ShowVirtualMemory), "show virtual memory" },
+ { nameof(Localization.StandbyList), "standby list" },
+ { nameof(Localization.StandbyListLowPriority), "standby list (low priority)" },
+ { nameof(Localization.StartMinimized), "start minimized" },
+ { nameof(Localization.SystemFileCache), "system file cache" },
+ { nameof(Localization.Text), "text" },
+ { nameof(Localization.TrayIcon), "tray icon" },
+ { nameof(Localization.UpdatedToVersion), "updated to version {0}" },
+ { nameof(Localization.UseTransparentBackground), "use transparent background" },
+ { nameof(Localization.Used), "used" },
+ { nameof(Localization.VirtualMemory), "virtual memory" },
+ { nameof(Localization.WhenFreePhysicalMemoryIsBelow), "when free physical memory is below {0}%" },
+ { nameof(Localization.WarningLevel), "warning level" },
+ { nameof(Localization.WorkingSet), "working set" },
+ { nameof(Localization.Yes), "yes" }
+ };
+
+ var props = typeof(Localization).GetProperties(BindingFlags.Public | BindingFlags.Instance);
+ foreach (var prop in props)
+ {
+ if (prop.PropertyType == typeof(string) && prop.CanWrite)
+ {
+ var name = prop.Name;
+ if (englishStrings.TryGetValue(name, out var value))
+ {
+ prop.SetValue(fallback, value);
+ }
+ else
+ {
+ prop.SetValue(fallback, name);
+ }
+ }
+ }
+ return fallback;
+ }
+
private static void Load(Language language)
{
- Localization localization;
+ Localization localization = null;
+ var resourceName = $"{Constants.App.LocalizationResourcePath}{language.EnglishName}{Constants.App.EmbeddedResourcePathExtension}";
+
+ var options = new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true,
+ ReadCommentHandling = JsonCommentHandling.Skip,
+ AllowTrailingCommas = true
+ };
- var localResource = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, string.Format(Culture, "{0}{1}", language.EnglishName, Constants.App.EmbeddedResourcePathExtension));
- var resource = string.Format(Culture, "{0}{1}{2}", Constants.App.LocalizationResourcePath, language.EnglishName, Constants.App.EmbeddedResourcePathExtension);
+ var assembliesToTry = new[] { Assembly.GetExecutingAssembly(), Assembly.GetEntryAssembly() }.Distinct();
- using (var stream = File.Exists(localResource) ? File.OpenRead(localResource) : Assembly.GetExecutingAssembly().GetManifestResourceStream(resource))
+ foreach (var assembly in assembliesToTry)
{
+ if (assembly == null) continue;
+
try
{
- if (stream == null)
- throw new NullReferenceException();
+ using var stream = assembly.GetManifestResourceStream(resourceName);
+ if (stream != null)
+ {
+ localization = JsonSerializer.Deserialize(stream, options);
+ if (localization != null)
+ {
+ break;
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Logger.Debug($"Failed to load embedded resource {resourceName} from {assembly.FullName}: {e.Message}");
+ }
+ }
- var serializer = new DataContractJsonSerializer(typeof(Localization));
+ if (localization == null)
+ {
+ var searchDirectories = new[]
+ {
+ AppDomain.CurrentDomain.BaseDirectory,
+ Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "Localization"),
+ Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", "Themes")
+ };
- localization = (Localization)serializer.ReadObject(stream);
- }
- catch
+ foreach (var dir in searchDirectories)
{
- throw new Exception(string.Format(Culture, "The {0} language file is missing or invalid.", language.EnglishName));
+ var localResource = Path.Combine(dir, $"{language.EnglishName}{Constants.App.EmbeddedResourcePathExtension}");
+ try
+ {
+ if (File.Exists(localResource))
+ {
+ using var stream = File.OpenRead(localResource);
+ localization = JsonSerializer.Deserialize(stream, options);
+ if (localization != null)
+ {
+ break;
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Logger.Debug($"Failed to load local file {localResource}: {e.Message}");
+ }
}
}
+ if (localization == null)
+ {
+ Logger.Warning($"Language file for {language.EnglishName} not found or invalid, using fallback");
+ localization = _fallbackLocalizationLazy.Value;
+ }
+
var nullOrEmptyStrings = localization
.GetType()
.GetProperties()
- .Where(pi => pi.PropertyType == typeof(string) && string.IsNullOrWhiteSpace((string)pi.GetValue(localization, null)))
+ .Where(pi => pi.PropertyType == typeof(string) && string.IsNullOrWhiteSpace((string)pi.GetValue(localization)))
.Select(pi => pi.Name)
.ToList();
if (nullOrEmptyStrings.Any())
- throw new Exception(string.Format(Culture, "The {0} language file is invalid. Missing Values: {1}", language.EnglishName, string.Join(", ", nullOrEmptyStrings)));
+ {
+ Logger.Warning($"Language file for {language.EnglishName} has missing values: {string.Join(", ", nullOrEmptyStrings)}, using fallback");
+ localization = _fallbackLocalizationLazy.Value;
+ }
Culture = new CultureInfo(language.Name);
String = localization;
@@ -204,10 +354,9 @@ private static void Load(Language language)
private static void RaiseStaticPropertyChanged(string propertyName = null)
{
- if (StaticPropertyChanged != null)
- StaticPropertyChanged.Invoke(null, new PropertyChangedEventArgs(propertyName));
+ StaticPropertyChanged?.Invoke(null, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
-}
+}
\ No newline at end of file
diff --git a/src/Core/Logger.cs b/src/Core/Logger.cs
index 6e266d3d..f51b3f40 100644
--- a/src/Core/Logger.cs
+++ b/src/Core/Logger.cs
@@ -14,6 +14,7 @@ public static class Logger
{
#region Fields
+ private static readonly object _syncLock = new object();
private static SafeFileHandle _consoleHandle;
private static FileStream _consoleStream;
private static StreamWriter _consoleWriter;
@@ -23,9 +24,6 @@ public static class Logger
#region Constructor
- ///
- /// Initializes the class.
- ///
static Logger()
{
try
@@ -51,6 +49,8 @@ static Logger()
Event(e.GetMessage(), EventLogEntryType.Error);
}
}
+
+ AppDomain.CurrentDomain.ProcessExit += (_, _) => Dispose();
}
///
@@ -59,54 +59,16 @@ static Logger()
///
internal static void Dispose()
{
- if (_consoleWriter != null)
+ lock (_syncLock)
{
- try
- {
- _consoleWriter.Flush();
- }
- catch
- {
- // ignored
- }
-
- try
- {
- _consoleWriter.Close();
- }
- catch
- {
- // ignored
- }
-
+ try { _consoleWriter?.Flush(); } catch { /* ignored */ }
+ try { _consoleWriter?.Dispose(); } catch { /* ignored */ }
_consoleWriter = null;
- }
-
- if (_consoleStream != null)
- {
- try
- {
- _consoleStream.Close();
- }
- catch
- {
- // ignored
- }
+ try { _consoleStream?.Dispose(); } catch { /* ignored */ }
_consoleStream = null;
- }
-
- if (_consoleHandle != null)
- {
- try
- {
- _consoleHandle.Close();
- }
- catch
- {
- // ignored
- }
+ try { _consoleHandle?.Dispose(); } catch { /* ignored */ }
_consoleHandle = null;
}
}
@@ -115,12 +77,6 @@ internal static void Dispose()
#region Properties
- ///
- /// Gets a value indicating whether console output is enabled.
- ///
- ///
- /// true if console output is enabled; otherwise, false.
- ///
private static bool IsConsoleEnabled { get { return _consoleWriter != null; } }
private static bool IsDebugEnabled { get { return (_level & Enums.Log.Levels.Debug) != 0; } }
@@ -134,9 +90,6 @@ internal static void Dispose()
///
/// Sets the log level.
///
- ///
- /// The log level.
- ///
public static Enums.Log.Levels Level
{
get { return _level; }
@@ -170,9 +123,6 @@ public static Enums.Log.Levels Level
///
/// Debug
///
- /// Exception
- /// Custom message about the Exception
- /// Method
public static void Debug(Exception exception, string message = null, [CallerMemberName] string method = null)
{
Exception(Enums.Log.Levels.Debug, exception, message, method);
@@ -181,8 +131,6 @@ public static void Debug(Exception exception, string message = null, [CallerMemb
///
/// Debug
///
- /// Message
- /// Method
public static void Debug(string message, [CallerMemberName] string method = null)
{
Log(new Log(Enums.Log.Levels.Debug, message, null, method));
@@ -191,44 +139,43 @@ public static void Debug(string message, [CallerMemberName] string method = null
///
/// Enables console output by attaching to the parent console process and redirecting standard output.
/// This allows Console.WriteLine output to appear in the calling terminal (PowerShell/cmd).
- /// Typically called when the application is invoked with command-line arguments.
/// This method is idempotent - calling it multiple times has no additional effect.
///
public static void EnableConsoleOutput()
{
- try
+ lock (_syncLock)
{
- if (IsConsoleEnabled)
- return;
-
- if (NativeMethods.AttachConsole(Constants.Windows.Console.AttachParentProcess))
+ try
{
- var stdHandle = NativeMethods.GetStdHandle(Constants.Windows.Console.StdOutputHandle);
+ if (IsConsoleEnabled)
+ return;
- if (stdHandle != IntPtr.Zero && stdHandle != new IntPtr(-1))
+ if (NativeMethods.AttachConsole(Constants.Windows.Console.AttachParentProcess))
{
- _consoleHandle = new SafeFileHandle(stdHandle, false);
- _consoleStream = new FileStream(_consoleHandle, FileAccess.Write);
- _consoleWriter = new StreamWriter(_consoleStream, Encoding.Default) { AutoFlush = true };
+ var stdHandle = NativeMethods.GetStdHandle(Constants.Windows.Console.StdOutputHandle);
+
+ if (stdHandle != IntPtr.Zero && stdHandle != new IntPtr(-1))
+ {
+ _consoleHandle = new SafeFileHandle(stdHandle, false);
+ _consoleStream = new FileStream(_consoleHandle, FileAccess.Write);
+ _consoleWriter = new StreamWriter(_consoleStream, Encoding.Default) { AutoFlush = true };
- Console.SetOut(_consoleWriter);
+ Console.SetOut(_consoleWriter);
- Trace.Listeners.Clear();
+ Trace.Listeners.Clear();
+ }
}
}
- }
- catch
- {
- // ignored
+ catch
+ {
+ // ignored
+ }
}
}
///
/// Error
///
- /// Exception
- /// Custom message about the Exception
- /// Method
public static void Error(Exception exception, string message = null, [CallerMemberName] string method = null)
{
Exception(Enums.Log.Levels.Error, exception, message, method);
@@ -237,37 +184,26 @@ public static void Error(Exception exception, string message = null, [CallerMemb
///
/// Error
///
- /// Message
- /// Method
public static void Error(string message, [CallerMemberName] string method = null)
{
Log(new Log(Enums.Log.Levels.Error, message, null, method));
}
- ///
+///
/// Windows Event
///
- /// Message
- /// Type
private static void Event(string message, EventLogEntryType type = EventLogEntryType.Information)
{
try
{
EventLog.WriteEntry(Constants.App.Title, message, type);
}
- catch
+ catch (Exception ex)
{
- // ignored
+ Logger.Debug("EventLog.WriteEntry failed: " + ex.Message);
}
}
- ///
- /// Exception
- ///
- /// The level.
- /// The exception.
- /// The message.
- /// The method.
private static void Exception(Enums.Log.Levels level, Exception exception, string message = null, [CallerMemberName] string method = null)
{
if (IsDebugEnabled)
@@ -278,19 +214,15 @@ private static void Exception(Enums.Log.Levels level, Exception exception, strin
if (stackTrace != null && stackTrace.FrameCount > 0)
{
var frame = stackTrace.GetFrame(stackTrace.FrameCount - 1);
+ var methodBase = frame?.GetMethod();
- if (frame != null)
- {
- var methodBase = frame.GetMethod();
-
- if (methodBase != null && methodBase.DeclaringType != null)
- method = string.Format(Localizer.Culture, "{0}.{1}", methodBase.DeclaringType.Name, methodBase.Name);
- }
+ if (methodBase?.DeclaringType != null)
+ method = string.Format(Localizer.Culture, "{0}.{1}", methodBase.DeclaringType.Name, methodBase.Name);
}
}
- catch
+ catch (Exception ex)
{
- // ignored
+ Logger.Debug("Stack trace extraction failed: " + ex.Message);
}
}
@@ -303,8 +235,6 @@ private static void Exception(Enums.Log.Levels level, Exception exception, strin
///
/// Information
///
- /// Message
- /// Method
public static void Information(string message, [CallerMemberName] string method = null)
{
Log(new Log(Enums.Log.Levels.Information, message, null, method));
@@ -313,13 +243,12 @@ public static void Information(string message, [CallerMemberName] string method
///
/// Log
///
- /// The log.
public static void Log(Log log)
{
try
{
if (log == null)
- throw new ArgumentNullException("log");
+ throw new ArgumentNullException(nameof(log));
var message = log.Message;
@@ -330,9 +259,9 @@ public static void Log(Log log)
{
Console.WriteLine(message);
}
- catch
+ catch (Exception ex)
{
- // ignored
+ Logger.Debug("Console.WriteLine failed: " + ex.Message);
}
}
@@ -371,13 +300,11 @@ public static void Log(Log log)
///
/// Warning
///
- /// Message
- /// Method
public static void Warning(string message, [CallerMemberName] string method = null)
{
Log(new Log(Enums.Log.Levels.Warning, message, null, method));
}
- }
- #endregion Methods
-}
+ #endregion Methods
+ }
+}
\ No newline at end of file
diff --git a/src/Core/ObservableObject.cs b/src/Core/ObservableObject.cs
index bb305f0f..9c9712c2 100644
--- a/src/Core/ObservableObject.cs
+++ b/src/Core/ObservableObject.cs
@@ -24,45 +24,39 @@ public abstract class ObservableObject : INotifyPropertyChanged
#region Methods
///
- /// Called when [property changed].
+ /// Raises the PropertyChanged event.
///
- /// Name of the property.
+ /// Name of the property. Resolved automatically via CallerMemberName.
+ protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ ///
+ /// Raises the PropertyChanged event. Alias for kept for backwards compatibility.
+ ///
+ /// Name of the property. Resolved automatically via CallerMemberName.
public void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{
- if (PropertyChanged != null)
- PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ OnPropertyChanged(propertyName);
}
///
- /// Raises the PropertyChanged event if needed.
+ /// Raises the PropertyChanged event for the given property expression.
///
- /// The type of the property that
- /// changed.
- /// An expression identifying the property
- /// that changed.
+ /// The type of the property that changed.
+ /// An expression identifying the property that changed.
public void RaisePropertyChanged(Expression> expression)
{
- var handler = PropertyChanged;
-
- if (handler != null && expression != null)
+ if (expression?.Body is MemberExpression body && body.Member is PropertyInfo property)
{
- var body = expression.Body as MemberExpression;
-
- if (body != null)
- {
- var property = body.Member as PropertyInfo;
-
- if (property != null)
- {
- var propertyName = property.Name;
+ var propertyName = property.Name;
- if (!string.IsNullOrEmpty(propertyName))
- RaisePropertyChanged(propertyName);
- }
- }
+ if (!string.IsNullOrEmpty(propertyName))
+ OnPropertyChanged(propertyName);
}
}
#endregion
}
-}
+}
\ No newline at end of file
diff --git a/src/Core/Settings.cs b/src/Core/Settings.cs
index 1155ee04..3bdf8be8 100644
--- a/src/Core/Settings.cs
+++ b/src/Core/Settings.cs
@@ -1,5 +1,6 @@
๏ปฟusing Microsoft.Win32;
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
@@ -10,16 +11,23 @@
namespace WinMemoryCleaner
{
+ ///
+ /// Application settings persisted in the Windows registry (HKLM) so they are shared
+ /// between the desktop app and the Windows Service (which runs as LOCAL SYSTEM).
+ ///
public static class Settings
{
private static readonly CultureInfo _culture = new CultureInfo(Constants.Windows.Locale.Name.English);
+ private static readonly object _syncLock = new object();
+ private static bool _isLoaded;
#region Constructors
static Settings()
{
- Load();
- Save();
+ // Default values only - don't load from registry yet to avoid circular dependency
+ LoadDefaults();
+ _isLoaded = true;
}
#endregion
@@ -52,7 +60,28 @@ static Settings()
public static ModifierKeys OptimizationModifiers { get; set; }
- public static SortedSet ProcessExclusionList { get; private set; }
+ public static ConcurrentDictionary ProcessExclusionList { get; private set; }
+
+ public static bool IsProcessExcluded(string processName)
+ {
+ if (string.IsNullOrEmpty(processName))
+ return false;
+ return ProcessExclusionList.ContainsKey(processName);
+ }
+
+ public static bool TryAddProcessExclusion(string processName)
+ {
+ if (string.IsNullOrEmpty(processName))
+ return false;
+ return ProcessExclusionList.TryAdd(processName, 0);
+ }
+
+ public static bool TryRemoveProcessExclusion(string processName)
+ {
+ if (string.IsNullOrEmpty(processName))
+ return false;
+ return ProcessExclusionList.TryRemove(processName, out _);
+ }
public static Enums.Priority RunOnPriority { get; set; }
@@ -90,9 +119,8 @@ static Settings()
#region Methods
- private static void Load(bool loadUserValues = true)
+ private static void LoadDefaults()
{
- // Default values
AlwaysOnTop = false;
AutoOptimizationInterval = 0;
AutoOptimizationMemoryUsage = 0;
@@ -106,7 +134,7 @@ private static void Load(bool loadUserValues = true)
MemoryAreas = Enums.Memory.Areas.CombinedPageList | Enums.Memory.Areas.ModifiedFileCache | Enums.Memory.Areas.ModifiedPageList | Enums.Memory.Areas.RegistryCache | Enums.Memory.Areas.StandbyList | Enums.Memory.Areas.SystemFileCache | Enums.Memory.Areas.WorkingSet;
OptimizationKey = Key.M;
OptimizationModifiers = ModifierKeys.Control | ModifierKeys.Shift;
- ProcessExclusionList = new SortedSet(StringComparer.OrdinalIgnoreCase);
+ ProcessExclusionList = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
RunOnPriority = Enums.Priority.Low;
RunOnStartup = false;
ShowOptimizationNotifications = true;
@@ -123,104 +151,108 @@ private static void Load(bool loadUserValues = true)
TrayIconWarningColor = Brushes.DarkGoldenrod;
TrayIconWarningLevel = 80;
UseHotkey = false;
+ }
- // User values
- try
+ public static void Load()
+ {
+ lock (_syncLock)
{
- if (!loadUserValues)
- return;
-
- // Process Exclusion List
- using (var key = Registry.LocalMachine.OpenSubKey(Constants.App.Registry.Key.ProcessExclusionList))
+ if (!_isLoaded)
{
- if (key != null)
- {
- foreach (var name in key.GetValueNames())
- ProcessExclusionList.Add(name.RemoveWhitespaces().Replace(".exe", string.Empty).ToLower(_culture));
- }
+ LoadDefaults();
+ _isLoaded = true;
}
- // Settings
- using (var key = Registry.LocalMachine.OpenSubKey(Constants.App.Registry.Key.Settings))
+ try
{
- if (key != null)
+ // Process Exclusion List
+ using (var key = Registry.LocalMachine.OpenSubKey(Constants.App.Registry.Key.ProcessExclusionList))
{
- AlwaysOnTop = Convert.ToBoolean(key.GetValue(Helper.NameOf(() => AlwaysOnTop), AlwaysOnTop), _culture);
- AutoOptimizationInterval = Convert.ToInt32(key.GetValue(Helper.NameOf(() => AutoOptimizationInterval), AutoOptimizationInterval), _culture);
- AutoOptimizationMemoryUsage = Convert.ToInt32(key.GetValue(Helper.NameOf(() => AutoOptimizationMemoryUsage), AutoOptimizationMemoryUsage), _culture);
- AutoUpdate = Convert.ToBoolean(key.GetValue(Helper.NameOf(() => AutoUpdate), AutoUpdate), _culture);
- CloseAfterOptimization = Convert.ToBoolean(key.GetValue(Helper.NameOf(() => CloseAfterOptimization), CloseAfterOptimization), _culture);
- CloseToTheNotificationArea = Convert.ToBoolean(key.GetValue(Helper.NameOf(() => CloseToTheNotificationArea), CloseToTheNotificationArea), _culture);
- CompactMode = Convert.ToBoolean(key.GetValue(Helper.NameOf(() => CompactMode), CompactMode), _culture);
- CreateStartMenuShortcut = Convert.ToBoolean(key.GetValue(Helper.NameOf(() => CreateStartMenuShortcut), CreateStartMenuShortcut), _culture);
- FontSize = Convert.ToDouble(key.GetValue(Helper.NameOf(() => FontSize), FontSize), _culture);
- Language = Convert.ToString(key.GetValue(Helper.NameOf(() => Language), Language), CultureInfo.InvariantCulture);
-
- Enums.Memory.Areas memoryAreas;
-
- if (Enum.TryParse(Convert.ToString(key.GetValue(Helper.NameOf(() => MemoryAreas), MemoryAreas), _culture), out memoryAreas) && memoryAreas.IsValid())
+ if (key != null)
{
- if ((memoryAreas & Enums.Memory.Areas.StandbyList) != 0 && (memoryAreas & Enums.Memory.Areas.StandbyListLowPriority) != 0)
- memoryAreas &= ~Enums.Memory.Areas.StandbyListLowPriority;
-
- MemoryAreas = memoryAreas;
+ ProcessExclusionList.Clear();
+ foreach (var name in key.GetValueNames())
+ {
+ var cleanName = name.RemoveWhitespaces().Replace(".exe", string.Empty).ToLower(_culture);
+ ProcessExclusionList.TryAdd(cleanName, 0);
+ }
}
-
- Key optimizationKey;
-
- if (Enum.TryParse(Convert.ToString(key.GetValue(Helper.NameOf(() => OptimizationKey), OptimizationKey), _culture), out optimizationKey) && optimizationKey.IsValid())
- OptimizationKey = optimizationKey;
-
- ModifierKeys optimizationModifiers;
-
- if (Enum.TryParse(Convert.ToString(key.GetValue(Helper.NameOf(() => OptimizationModifiers), OptimizationModifiers), _culture), out optimizationModifiers) && optimizationModifiers.IsValid())
- OptimizationModifiers = optimizationModifiers;
-
- Enums.Priority runOnPriority;
-
- if (Enum.TryParse(Convert.ToString(key.GetValue(Helper.NameOf(() => RunOnPriority), RunOnPriority), _culture), out runOnPriority) && runOnPriority.IsValid())
- RunOnPriority = runOnPriority;
-
- RunOnStartup = Convert.ToBoolean(key.GetValue(Helper.NameOf(() => RunOnStartup), RunOnStartup), _culture);
- ShowOptimizationNotifications = Convert.ToBoolean(key.GetValue(Helper.NameOf(() => ShowOptimizationNotifications), ShowOptimizationNotifications), _culture);
- ShowVirtualMemory = Convert.ToBoolean(key.GetValue(Helper.NameOf(() => ShowVirtualMemory), ShowVirtualMemory), _culture);
- StartMinimized = Convert.ToBoolean(key.GetValue(Helper.NameOf(() => StartMinimized), StartMinimized), _culture);
- TrayIconBackgroundColor = Convert.ToString(key.GetValue(Helper.NameOf(() => TrayIconBackgroundColor), TrayIconBackgroundColor), _culture).ToBrush(TrayIconBackgroundColor);
- TrayIconDangerColor = Convert.ToString(key.GetValue(Helper.NameOf(() => TrayIconDangerColor), TrayIconDangerColor), _culture).ToBrush(TrayIconDangerColor);
- TrayIconDangerLevel = Convert.ToByte(key.GetValue(Helper.NameOf(() => TrayIconDangerLevel), TrayIconDangerLevel), _culture);
- TrayIconOptimizeOnMiddleMouseClick = Convert.ToBoolean(key.GetValue(Helper.NameOf(() => TrayIconOptimizeOnMiddleMouseClick), TrayIconOptimizeOnMiddleMouseClick), _culture);
- TrayIconOptimizingColor = Convert.ToString(key.GetValue(Helper.NameOf(() => TrayIconOptimizingColor), TrayIconOptimizingColor), _culture).ToBrush(TrayIconOptimizingColor);
- TrayIconShowMemoryUsage = Convert.ToBoolean(key.GetValue(Helper.NameOf(() => TrayIconShowMemoryUsage), TrayIconShowMemoryUsage), _culture);
- TrayIconTextColor = Convert.ToString(key.GetValue(Helper.NameOf(() => TrayIconTextColor), TrayIconTextColor), _culture).ToBrush(TrayIconTextColor);
- TrayIconUseTransparentBackground = Convert.ToBoolean(key.GetValue(Helper.NameOf(() => TrayIconUseTransparentBackground), TrayIconUseTransparentBackground), _culture);
- TrayIconWarningColor = Convert.ToString(key.GetValue(Helper.NameOf(() => TrayIconWarningColor), TrayIconWarningColor), _culture).ToBrush(TrayIconWarningColor);
- TrayIconWarningLevel = Convert.ToByte(key.GetValue(Helper.NameOf(() => TrayIconWarningLevel), TrayIconWarningLevel), _culture);
- UseHotkey = Convert.ToBoolean(key.GetValue(Helper.NameOf(() => UseHotkey), UseHotkey), _culture);
}
- else
- {
- // Smart language setter for the first run
- var culture = CultureInfo.CurrentCulture;
- var languages = Localizer.Languages.Select(language => language.Name).ToList();
- do
+ // Settings
+ using (var key = Registry.LocalMachine.OpenSubKey(Constants.App.Registry.Key.Settings))
+ {
+ if (key != null)
{
- if (languages.Contains(culture.Name, StringComparer.OrdinalIgnoreCase))
+ AlwaysOnTop = Convert.ToBoolean(key.GetValue(nameof(AlwaysOnTop), AlwaysOnTop), _culture);
+ AutoOptimizationInterval = Convert.ToInt32(key.GetValue(nameof(AutoOptimizationInterval), AutoOptimizationInterval), _culture);
+ AutoOptimizationMemoryUsage = Convert.ToInt32(key.GetValue(nameof(AutoOptimizationMemoryUsage), AutoOptimizationMemoryUsage), _culture);
+ AutoUpdate = Convert.ToBoolean(key.GetValue(nameof(AutoUpdate), AutoUpdate), _culture);
+ CloseAfterOptimization = Convert.ToBoolean(key.GetValue(nameof(CloseAfterOptimization), CloseAfterOptimization), _culture);
+ CloseToTheNotificationArea = Convert.ToBoolean(key.GetValue(nameof(CloseToTheNotificationArea), CloseToTheNotificationArea), _culture);
+ CompactMode = Convert.ToBoolean(key.GetValue(nameof(CompactMode), CompactMode), _culture);
+ CreateStartMenuShortcut = Convert.ToBoolean(key.GetValue(nameof(CreateStartMenuShortcut), CreateStartMenuShortcut), _culture);
+ FontSize = Convert.ToDouble(key.GetValue(nameof(FontSize), FontSize), _culture);
+ Language = Convert.ToString(key.GetValue(nameof(Language), Language), CultureInfo.InvariantCulture);
+
+ if (Enum.TryParse(Convert.ToString(key.GetValue(nameof(MemoryAreas), MemoryAreas), _culture), out Enums.Memory.Areas memoryAreas) && memoryAreas.IsValid())
{
- Localizer.Language = new Language(culture);
- Language = culture.Name;
- break;
+ if ((memoryAreas & Enums.Memory.Areas.StandbyList) != 0 && (memoryAreas & Enums.Memory.Areas.StandbyListLowPriority) != 0)
+ memoryAreas &= ~Enums.Memory.Areas.StandbyListLowPriority;
+
+ MemoryAreas = memoryAreas;
}
- culture = culture.Parent;
+ if (Enum.TryParse(Convert.ToString(key.GetValue(nameof(OptimizationKey), OptimizationKey), _culture), out Key optimizationKey) && optimizationKey.IsValid())
+ OptimizationKey = optimizationKey;
+
+ if (Enum.TryParse(Convert.ToString(key.GetValue(nameof(OptimizationModifiers), OptimizationModifiers), _culture), out ModifierKeys optimizationModifiers) && optimizationModifiers.IsValid())
+ OptimizationModifiers = optimizationModifiers;
+
+ if (Enum.TryParse(Convert.ToString(key.GetValue(nameof(RunOnPriority), RunOnPriority), _culture), out Enums.Priority runOnPriority) && runOnPriority.IsValid())
+ RunOnPriority = runOnPriority;
+
+ RunOnStartup = Convert.ToBoolean(key.GetValue(nameof(RunOnStartup), RunOnStartup), _culture);
+ ShowOptimizationNotifications = Convert.ToBoolean(key.GetValue(nameof(ShowOptimizationNotifications), ShowOptimizationNotifications), _culture);
+ ShowVirtualMemory = Convert.ToBoolean(key.GetValue(nameof(ShowVirtualMemory), ShowVirtualMemory), _culture);
+ StartMinimized = Convert.ToBoolean(key.GetValue(nameof(StartMinimized), StartMinimized), _culture);
+ TrayIconBackgroundColor = Convert.ToString(key.GetValue(nameof(TrayIconBackgroundColor), TrayIconBackgroundColor), _culture).ToBrush(TrayIconBackgroundColor);
+ TrayIconDangerColor = Convert.ToString(key.GetValue(nameof(TrayIconDangerColor), TrayIconDangerColor), _culture).ToBrush(TrayIconDangerColor);
+ TrayIconDangerLevel = Convert.ToByte(key.GetValue(nameof(TrayIconDangerLevel), TrayIconDangerLevel), _culture);
+ TrayIconOptimizeOnMiddleMouseClick = Convert.ToBoolean(key.GetValue(nameof(TrayIconOptimizeOnMiddleMouseClick), TrayIconOptimizeOnMiddleMouseClick), _culture);
+ TrayIconOptimizingColor = Convert.ToString(key.GetValue(nameof(TrayIconOptimizingColor), TrayIconOptimizingColor), _culture).ToBrush(TrayIconOptimizingColor);
+ TrayIconShowMemoryUsage = Convert.ToBoolean(key.GetValue(nameof(TrayIconShowMemoryUsage), TrayIconShowMemoryUsage), _culture);
+ TrayIconTextColor = Convert.ToString(key.GetValue(nameof(TrayIconTextColor), TrayIconTextColor), _culture).ToBrush(TrayIconTextColor);
+ TrayIconUseTransparentBackground = Convert.ToBoolean(key.GetValue(nameof(TrayIconUseTransparentBackground), TrayIconUseTransparentBackground), _culture);
+ TrayIconWarningColor = Convert.ToString(key.GetValue(nameof(TrayIconWarningColor), TrayIconWarningColor), _culture).ToBrush(TrayIconWarningColor);
+ TrayIconWarningLevel = Convert.ToByte(key.GetValue(nameof(TrayIconWarningLevel), TrayIconWarningLevel), _culture);
+ UseHotkey = Convert.ToBoolean(key.GetValue(nameof(UseHotkey), UseHotkey), _culture);
+ }
+ else
+ {
+ // Smart language setter for the first run
+ var culture = CultureInfo.CurrentCulture;
+ var languages = Localizer.Languages.Select(language => language.Name).ToList();
+
+ do
+ {
+ if (languages.Contains(culture.Name, StringComparer.OrdinalIgnoreCase))
+ {
+ Localizer.Language = new Language(culture);
+ Language = culture.Name;
+ break;
+ }
+
+ culture = culture.Parent;
+ }
+ while (culture.LCID != CultureInfo.InvariantCulture.LCID);
}
- while (culture.LCID != CultureInfo.InvariantCulture.LCID);
}
}
- }
- catch (Exception e)
- {
- Logger.Error(e);
+ catch (Exception e)
+ {
+ Logger.Error(e);
+ }
}
}
@@ -228,73 +260,78 @@ public static void Reset(bool keepLanguage = false)
{
var language = Language;
- Load(false);
-
- if (keepLanguage)
- Language = language;
+ lock (_syncLock)
+ {
+ LoadDefaults();
+ if (keepLanguage)
+ Language = language;
- Save();
+ Save();
+ }
}
public static void Save()
{
- try
+ lock (_syncLock)
{
- // Process Exclusion List
- Registry.LocalMachine.DeleteSubKey(Constants.App.Registry.Key.ProcessExclusionList, false);
-
- if (ProcessExclusionList.Any())
+ try
{
- using (var key = Registry.LocalMachine.CreateSubKey(Constants.App.Registry.Key.ProcessExclusionList))
+ // Process Exclusion List
+ Registry.LocalMachine.DeleteSubKey(Constants.App.Registry.Key.ProcessExclusionList, false);
+
+ if (!ProcessExclusionList.IsEmpty)
{
- if (key != null)
+ using (var key = Registry.LocalMachine.CreateSubKey(Constants.App.Registry.Key.ProcessExclusionList))
{
- foreach (var process in ProcessExclusionList)
- key.SetValue(process.RemoveWhitespaces().Replace(".exe", string.Empty).ToLower(_culture), string.Empty, RegistryValueKind.String);
+ if (key != null)
+ {
+ foreach (var process in ProcessExclusionList.Keys)
+ key.SetValue(process.RemoveWhitespaces().Replace(".exe", string.Empty).ToLower(_culture), string.Empty, RegistryValueKind.String);
+ }
}
}
- }
- // Settings
- using (var key = Registry.LocalMachine.CreateSubKey(Constants.App.Registry.Key.Settings))
- {
- if (key != null)
+ // Settings
+ using (var key = Registry.LocalMachine.CreateSubKey(Constants.App.Registry.Key.Settings))
{
- key.SetValue(Helper.NameOf(() => AlwaysOnTop), AlwaysOnTop ? 1 : 0);
- key.SetValue(Helper.NameOf(() => AutoOptimizationInterval), AutoOptimizationInterval);
- key.SetValue(Helper.NameOf(() => AutoOptimizationMemoryUsage), AutoOptimizationMemoryUsage);
- key.SetValue(Helper.NameOf(() => AutoUpdate), AutoUpdate ? 1 : 0);
- key.SetValue(Helper.NameOf(() => CloseAfterOptimization), CloseAfterOptimization ? 1 : 0);
- key.SetValue(Helper.NameOf(() => CloseToTheNotificationArea), CloseToTheNotificationArea ? 1 : 0);
- key.SetValue(Helper.NameOf(() => CompactMode), CompactMode ? 1 : 0);
- key.SetValue(Helper.NameOf(() => CreateStartMenuShortcut), CreateStartMenuShortcut ? 1 : 0);
- key.SetValue(Helper.NameOf(() => FontSize), FontSize);
- key.SetValue(Helper.NameOf(() => Language), Language);
- key.SetValue(Helper.NameOf(() => MemoryAreas), (int)MemoryAreas);
- key.SetValue(Helper.NameOf(() => OptimizationKey), (int)OptimizationKey);
- key.SetValue(Helper.NameOf(() => OptimizationModifiers), (int)OptimizationModifiers);
- key.SetValue(Helper.NameOf(() => RunOnPriority), (int)RunOnPriority);
- key.SetValue(Helper.NameOf(() => RunOnStartup), RunOnStartup ? 1 : 0);
- key.SetValue(Helper.NameOf(() => ShowOptimizationNotifications), ShowOptimizationNotifications ? 1 : 0);
- key.SetValue(Helper.NameOf(() => ShowVirtualMemory), ShowVirtualMemory ? 1 : 0);
- key.SetValue(Helper.NameOf(() => StartMinimized), StartMinimized ? 1 : 0);
- key.SetValue(Helper.NameOf(() => TrayIconBackgroundColor), TrayIconBackgroundColor.GetHex(true));
- key.SetValue(Helper.NameOf(() => TrayIconDangerColor), TrayIconDangerColor.GetHex(true));
- key.SetValue(Helper.NameOf(() => TrayIconDangerLevel), TrayIconDangerLevel);
- key.SetValue(Helper.NameOf(() => TrayIconOptimizeOnMiddleMouseClick), TrayIconOptimizeOnMiddleMouseClick ? 1 : 0);
- key.SetValue(Helper.NameOf(() => TrayIconOptimizingColor), TrayIconOptimizingColor.GetHex(true));
- key.SetValue(Helper.NameOf(() => TrayIconShowMemoryUsage), TrayIconShowMemoryUsage ? 1 : 0);
- key.SetValue(Helper.NameOf(() => TrayIconTextColor), TrayIconTextColor.GetHex(true));
- key.SetValue(Helper.NameOf(() => TrayIconUseTransparentBackground), TrayIconUseTransparentBackground ? 1 : 0);
- key.SetValue(Helper.NameOf(() => TrayIconWarningColor), TrayIconWarningColor.GetHex(true));
- key.SetValue(Helper.NameOf(() => TrayIconWarningLevel), TrayIconWarningLevel);
- key.SetValue(Helper.NameOf(() => UseHotkey), UseHotkey ? 1 : 0);
+ if (key != null)
+ {
+ key.SetValue(nameof(AlwaysOnTop), AlwaysOnTop ? 1 : 0);
+ key.SetValue(nameof(AutoOptimizationInterval), AutoOptimizationInterval);
+ key.SetValue(nameof(AutoOptimizationMemoryUsage), AutoOptimizationMemoryUsage);
+ key.SetValue(nameof(AutoUpdate), AutoUpdate ? 1 : 0);
+ key.SetValue(nameof(CloseAfterOptimization), CloseAfterOptimization ? 1 : 0);
+ key.SetValue(nameof(CloseToTheNotificationArea), CloseToTheNotificationArea ? 1 : 0);
+ key.SetValue(nameof(CompactMode), CompactMode ? 1 : 0);
+ key.SetValue(nameof(CreateStartMenuShortcut), CreateStartMenuShortcut ? 1 : 0);
+ key.SetValue(nameof(FontSize), FontSize);
+ key.SetValue(nameof(Language), Language);
+ key.SetValue(nameof(MemoryAreas), (int)MemoryAreas);
+ key.SetValue(nameof(OptimizationKey), (int)OptimizationKey);
+ key.SetValue(nameof(OptimizationModifiers), (int)OptimizationModifiers);
+ key.SetValue(nameof(RunOnPriority), (int)RunOnPriority);
+ key.SetValue(nameof(RunOnStartup), RunOnStartup ? 1 : 0);
+ key.SetValue(nameof(ShowOptimizationNotifications), ShowOptimizationNotifications ? 1 : 0);
+ key.SetValue(nameof(ShowVirtualMemory), ShowVirtualMemory ? 1 : 0);
+ key.SetValue(nameof(StartMinimized), StartMinimized ? 1 : 0);
+ key.SetValue(nameof(TrayIconBackgroundColor), TrayIconBackgroundColor.GetHex(true));
+ key.SetValue(nameof(TrayIconDangerColor), TrayIconDangerColor.GetHex(true));
+ key.SetValue(nameof(TrayIconDangerLevel), TrayIconDangerLevel);
+ key.SetValue(nameof(TrayIconOptimizeOnMiddleMouseClick), TrayIconOptimizeOnMiddleMouseClick ? 1 : 0);
+ key.SetValue(nameof(TrayIconOptimizingColor), TrayIconOptimizingColor.GetHex(true));
+ key.SetValue(nameof(TrayIconShowMemoryUsage), TrayIconShowMemoryUsage ? 1 : 0);
+ key.SetValue(nameof(TrayIconTextColor), TrayIconTextColor.GetHex(true));
+ key.SetValue(nameof(TrayIconUseTransparentBackground), TrayIconUseTransparentBackground ? 1 : 0);
+ key.SetValue(nameof(TrayIconWarningColor), TrayIconWarningColor.GetHex(true));
+ key.SetValue(nameof(TrayIconWarningLevel), TrayIconWarningLevel);
+ key.SetValue(nameof(UseHotkey), UseHotkey ? 1 : 0);
+ }
}
}
- }
- catch (Exception e)
- {
- Logger.Error(e);
+ catch (Exception e)
+ {
+ Logger.Error(e);
+ }
}
}
diff --git a/src/Core/ThemeManager.cs b/src/Core/ThemeManager.cs
index fd668f9c..10ad468f 100644
--- a/src/Core/ThemeManager.cs
+++ b/src/Core/ThemeManager.cs
@@ -12,9 +12,6 @@
namespace WinMemoryCleaner
{
- ///
- /// Centralized theme manager that provides all theme resources with enhanced security
- ///
public static class ThemeManager
{
private const int MAX_BRUSHES_COUNT = 500;
@@ -22,11 +19,11 @@ public static class ThemeManager
private const int MAX_HEX_COLOR_LENGTH = 9;
private const int INITIALIZATION_TIMEOUT_MS = 30000;
- private static readonly Regex _hexColorPattern = new Regex(@"^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$", RegexOptions.Compiled);
- private static readonly Regex _resourceKeyPattern = new Regex(@"^[a-zA-Z0-9_.]+$", RegexOptions.Compiled);
+ private static readonly Regex _hexColorPattern = new(@"^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$", RegexOptions.Compiled);
+ private static readonly Regex _resourceKeyPattern = new(@"^[a-zA-Z0-9_.]+$", RegexOptions.Compiled);
- private static readonly object _lockObject = new object();
- private static readonly object _initializationLock = new object();
+ private static readonly object _lockObject = new();
+ private static readonly object _initializationLock = new();
private static volatile bool _isInitialized;
private static volatile bool _isInitializing;
private static readonly Timer _initializationTimer = InitializeTimer();
@@ -34,7 +31,7 @@ public static class ThemeManager
private static Enums.Theme _theme = Enums.Theme.Dark;
private static List _brushes;
private static List _themes;
- private static readonly Dictionary _colorCache = new Dictionary(StringComparer.Ordinal);
+ private static readonly Dictionary _colorCache = new(StringComparer.Ordinal);
private static DateTime _lastInitializationAttempt = DateTime.MinValue;
private static Timer InitializeTimer()
@@ -45,13 +42,7 @@ private static Timer InitializeTimer()
}
catch (Exception e)
{
- try
- {
- Logger.Error("Failed to initialize timer: " + e.Message);
- }
- catch
- {
- }
+ try { Logger.Error("Failed to initialize timer: " + e.Message); } catch { }
return null;
}
}
@@ -243,15 +234,11 @@ private static void InitializeThemes()
foreach (Enums.Theme theme in enumValues)
{
if (IsValidTheme(theme))
- {
_themes.Add(theme);
- }
}
if (_themes.Count == 0)
- {
_themes.Add(Enums.Theme.Dark);
- }
}
catch (Exception e)
{
@@ -305,7 +292,7 @@ private static void InitializeBrushes()
}
catch (Exception e)
{
- var propertyName = (property != null) ? property.Name : "unknown";
+ var propertyName = property?.Name ?? "unknown";
Logger.Error("Failed to process color property " + propertyName + ": " + e.Message);
}
}
@@ -396,44 +383,29 @@ private static float GetColorBrightnessSafe(WpfBrush brush)
}
}
- ///
- /// Gets the accent color for Windows Forms controls.
- ///
public static WinFormsColor AccentColor
{
- get { return GetColorSafe(Helper.NameOf(() => AccentColor).Replace("Color", string.Empty), WinFormsColor.DeepSkyBlue); }
+ get => GetColorSafe(Helper.NameOf(() => AccentColor).Replace("Color", string.Empty), WinFormsColor.DeepSkyBlue);
}
- ///
- /// Gets the primary background color for Windows Forms controls.
- ///
public static WinFormsColor PrimaryBackgroundColor
{
- get { return GetColorSafe(Helper.NameOf(() => PrimaryBackgroundColor).Replace("Color", string.Empty), WinFormsColor.FromArgb(32, 32, 32)); }
+ get => GetColorSafe(Helper.NameOf(() => PrimaryBackgroundColor).Replace("Color", string.Empty), WinFormsColor.FromArgb(32, 32, 32));
}
- ///
- /// Gets the secondary background color for Windows Forms controls.
- ///
public static WinFormsColor SecondaryBackgroundColor
{
- get { return GetColorSafe(Helper.NameOf(() => SecondaryBackgroundColor).Replace("Color", string.Empty), WinFormsColor.DarkSlateGray); }
+ get => GetColorSafe(Helper.NameOf(() => SecondaryBackgroundColor).Replace("Color", string.Empty), WinFormsColor.DarkSlateGray);
}
-
- ///
- /// Gets the secondary border color for Windows Forms controls.
- ///
+
public static WinFormsColor SecondaryBorderColor
{
- get { return GetColorSafe(Helper.NameOf(() => SecondaryBorderColor).Replace("Color", string.Empty), WinFormsColor.DimGray); }
+ get => GetColorSafe(Helper.NameOf(() => SecondaryBorderColor).Replace("Color", string.Empty), WinFormsColor.DimGray);
}
- ///
- /// Gets the secondary foreground color for Windows Forms controls.
- ///
public static WinFormsColor SecondaryForegroundColor
{
- get { return GetColorSafe(Helper.NameOf(() => SecondaryForegroundColor).Replace("Color", string.Empty), WinFormsColor.White); }
+ get => GetColorSafe(Helper.NameOf(() => SecondaryForegroundColor).Replace("Color", string.Empty), WinFormsColor.White);
}
private static WinFormsColor GetColorSafe(string resourceKey, WinFormsColor fallback)
@@ -448,11 +420,8 @@ private static WinFormsColor GetColorSafe(string resourceKey, WinFormsColor fall
{
lock (_colorCache)
{
- WinFormsColor cachedColor;
- if (_colorCache.TryGetValue(resourceKey, out cachedColor))
- {
+ if (_colorCache.TryGetValue(resourceKey, out var cachedColor))
return cachedColor;
- }
}
EnsureInitialized();
@@ -471,9 +440,7 @@ private static WinFormsColor GetColorSafe(string resourceKey, WinFormsColor fall
lock (_colorCache)
{
if (!_colorCache.ContainsKey(resourceKey))
- {
_colorCache[resourceKey] = result;
- }
}
return result;
@@ -485,9 +452,6 @@ private static WinFormsColor GetColorSafe(string resourceKey, WinFormsColor fall
}
}
- ///
- /// Gets the brushes with defensive copying.
- ///
public static List Brushes
{
get
@@ -504,25 +468,20 @@ public static List Brushes
}
}
- ///
- /// Gets or sets the theme with enhanced validation and error handling.
- ///
public static Enums.Theme Theme
{
get
{
EnsureInitialized();
lock (_lockObject)
- {
return _theme;
- }
}
set
{
if (!IsValidTheme(value))
{
Logger.Warning("Invalid theme value: " + value);
- throw new ArgumentException("Invalid theme value: " + value, "value");
+ throw new ArgumentException("Invalid theme value: " + value, nameof(value));
}
EnsureInitialized();
@@ -563,9 +522,6 @@ public static Enums.Theme Theme
}
}
- ///
- /// Gets the themes with defensive copying.
- ///
public static List Themes
{
get
@@ -575,9 +531,7 @@ public static List Themes
{
var themes = _themes;
if (themes == null || themes.Count == 0)
- {
return new List { Enums.Theme.Dark };
- }
return new List(themes);
}
@@ -716,10 +670,10 @@ private static void Load(Enums.Theme theme)
private static WpfColor ParseHexColor(string hexColor)
{
if (string.IsNullOrEmpty(hexColor))
- throw new ArgumentException("Hex color cannot be null or empty", "hexColor");
+ throw new ArgumentException("Hex color cannot be null or empty", nameof(hexColor));
if (!IsValidHexColor(hexColor))
- throw new ArgumentException("Invalid hex color format: " + hexColor, "hexColor");
+ throw new ArgumentException("Invalid hex color format: " + hexColor, nameof(hexColor));
try
{
@@ -729,21 +683,17 @@ private static WpfColor ParseHexColor(string hexColor)
catch (Exception e)
{
Logger.Error("Failed to parse hex color '" + hexColor + "': " + e.Message);
- throw new ArgumentException("Invalid hex color format: " + hexColor, "hexColor", e);
+ throw new ArgumentException("Invalid hex color format: " + hexColor, nameof(hexColor), e);
}
}
- ///
- /// Cleanup method for proper resource disposal
- ///
public static void Cleanup()
{
try
{
lock (_initializationLock)
{
- if (_initializationTimer != null)
- _initializationTimer.Dispose();
+ _initializationTimer?.Dispose();
}
lock (_colorCache)
diff --git a/src/Core/Updater.cs b/src/Core/Updater.cs
index ee598291..2293cbe8 100644
--- a/src/Core/Updater.cs
+++ b/src/Core/Updater.cs
@@ -1,160 +1,112 @@
๏ปฟusing System;
-using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
-using System.Net;
+using System.Net.Http;
using System.Reflection;
+using System.Text.Json;
using System.Text.RegularExpressions;
+using System.Threading.Tasks;
namespace WinMemoryCleaner
{
- ///
- /// Updater
- ///
public static class Updater
{
- private static WebClient _client;
+ private static readonly HttpClient _httpClient = new() { Timeout = TimeSpan.FromSeconds(30) };
private static DateTimeOffset _lastCheck = DateTimeOffset.MinValue;
+ private static ProcessStartInfo _process;
- internal static ProcessStartInfo Process;
+ public static ProcessStartInfo Process => _process;
- private static void OnFileDownloadCompleted(object sender, AsyncCompletedEventArgs e)
+ public static async void Update(params string[] args)
{
try
{
- if (e.Error != null)
- throw new Exception("File download failed.", e.Error);
-
- if (e.Cancelled)
+ if (!Settings.AutoUpdate || DateTimeOffset.Now.Subtract(_lastCheck).TotalHours < Constants.App.AutoUpdateInterval)
return;
- var updateInfo = (Tuple)e.UserState;
- var temp = updateInfo.Item1;
- var path = updateInfo.Item2;
- var exe = updateInfo.Item3;
- var newestVersion = updateInfo.Item4;
- var args = updateInfo.Item5;
-
- if (File.Exists(temp) && AssemblyName.GetAssemblyName(temp).Version.Equals(newestVersion))
- {
- Process = new ProcessStartInfo
- {
- Arguments = string.Format(CultureInfo.InvariantCulture, @"/c taskkill /f /im ""{0}"" & move /y ""{1}"" ""{2}"" & start """" ""{2}"" /{3} {4}", exe, temp, path, newestVersion, string.Join(" ", args)),
- CreateNoWindow = true,
- FileName = "cmd",
- RedirectStandardError = false,
- RedirectStandardInput = false,
- RedirectStandardOutput = false,
- UseShellExecute = false,
- WindowStyle = ProcessWindowStyle.Hidden
- };
-
- App.Shutdown();
- }
- }
- catch (Exception ex)
- {
- Logger.Error(ex);
-
- Reset();
- }
- }
-
- private static void OnVersionCheckCompleted(object sender, DownloadStringCompletedEventArgs e)
- {
- try
- {
- if (e.Error != null)
- throw new Exception("Version check failed.", e.Error);
+ _lastCheck = DateTimeOffset.Now;
+ _process = null;
- if (e.Cancelled)
- return;
+ var currentVersion = App.Version;
- var assemblyInfo = e.Result;
- var assemblyVersionMatch = Regex.Match(assemblyInfo, @"AssemblyVersion\(""(.*)""\)\]");
+ // Check for updates via GitHub API
+ var response = await _httpClient.GetStringAsync(Constants.App.Repository.ApiLatestReleaseUri);
+ using var doc = JsonDocument.Parse(response);
+ var tagName = doc.RootElement.GetProperty("tag_name").GetString()?.TrimStart('v');
- if (!assemblyVersionMatch.Success)
+ if (string.IsNullOrEmpty(tagName) || !Version.TryParse(tagName, out var newestVersion))
return;
- var newestVersion = Version.Parse(assemblyVersionMatch.Groups[1].Value);
-
- if (App.Version >= newestVersion)
- {
- Reset();
+ if (currentVersion >= newestVersion)
return;
- }
+ // Download the new version
var exe = Path.GetFileName(App.Path);
- var temp = Path.Combine(Path.GetTempPath(), exe);
+ var temp = Path.Combine(Path.GetTempPath(), $"{Path.GetRandomFileName()}.exe");
Helper.DeleteFile(temp);
- var updateInfo = Tuple.Create(temp, App.Path, exe, newestVersion, (string[])e.UserState);
+ var fileBytes = await _httpClient.GetByteArrayAsync(Constants.App.Repository.LatestExeUri);
+ await File.WriteAllBytesAsync(temp, fileBytes);
- _client.DownloadFileAsync(Constants.App.Repository.LatestExeUri, temp, updateInfo);
+ // Verify the downloaded file
+ if (File.Exists(temp))
+ {
+ try
+ {
+ var downloadedVersion = AssemblyName.GetAssemblyName(temp).Version;
+ if (downloadedVersion != null && downloadedVersion.Equals(newestVersion))
+ {
+ _process = new ProcessStartInfo
+ {
+ Arguments = string.Format(CultureInfo.InvariantCulture, @"/c taskkill /f /im ""{0}"" & move /y ""{1}"" ""{2}"" & start """" ""{2}"" /{3} {4}", exe, temp, App.Path, newestVersion, string.Join(" ", args)),
+ CreateNoWindow = true,
+ FileName = "cmd",
+ RedirectStandardError = false,
+ RedirectStandardInput = false,
+ RedirectStandardOutput = false,
+ UseShellExecute = false,
+ WindowStyle = ProcessWindowStyle.Hidden
+ };
+
+ App.Shutdown();
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Error("Failed to verify downloaded update: " + ex.GetMessage());
+ }
+ }
}
catch (Exception ex)
{
Logger.Error(ex);
-
- Reset();
}
}
- private static void Reset()
+ public static async Task CheckForUpdatesAsync()
{
try
{
- if (_client != null)
- _client.Dispose();
- }
- finally
- {
- _client = null;
- }
+ var response = await _httpClient.GetStringAsync(Constants.App.Repository.ApiLatestReleaseUri);
+ using var doc = JsonDocument.Parse(response);
+ var tagName = doc.RootElement.GetProperty("tag_name").GetString()?.TrimStart('v');
- try
- {
- if (Process != null)
- Process = null;
+ if (!string.IsNullOrEmpty(tagName) && Version.TryParse(tagName, out var version))
+ return version;
}
- catch
+ catch (Exception ex)
{
- // ignored
+ Logger.Error(ex);
}
+
+ return null;
}
- ///
- /// Check for new version and update if available
- ///
- public static void Update(params string[] args)
+ public static void Dispose()
{
- try
- {
- if (Settings.AutoUpdate && DateTimeOffset.Now.Subtract(_lastCheck).TotalHours < Constants.App.AutoUpdateInterval)
- return;
-
- _lastCheck = DateTimeOffset.Now;
-
- Reset();
-
- _client = new WebClient();
- _client.DownloadFileCompleted += new AsyncCompletedEventHandler(OnFileDownloadCompleted);
- _client.DownloadStringCompleted += new DownloadStringCompletedEventHandler(OnVersionCheckCompleted);
-
- ServicePointManager.DefaultConnectionLimit = 10;
- ServicePointManager.Expect100Continue = true;
- ServicePointManager.SecurityProtocol |= (SecurityProtocolType)3072 | (SecurityProtocolType)12288; // TLS 1.2 | TLS 1.3
-
- _client.DownloadStringAsync(Constants.App.Repository.AssemblyInfoUri, args);
- }
- catch (Exception ex)
- {
- Logger.Error(ex);
-
- Reset();
- }
+ _httpClient?.Dispose();
}
}
}
\ No newline at end of file
diff --git a/src/Interop/NativeMethods.cs b/src/Interop/NativeMethods.cs
index 104de741..de429fde 100644
--- a/src/Interop/NativeMethods.cs
+++ b/src/Interop/NativeMethods.cs
@@ -2,16 +2,15 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
using System.Security;
namespace WinMemoryCleaner
{
- ///
- /// Windows API
- ///
+ [SupportedOSPlatform("windows")]
internal static class NativeMethods
{
- [DllImport("advapi32.dll", SetLastError = true)]
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool AdjustTokenPrivileges(IntPtr tokenHandle, [MarshalAs(UnmanagedType.Bool)] bool disableAllPrivileges, ref Structs.Windows.TokenPrivileges newState, int bufferLength, IntPtr previousState, IntPtr returnLength);
@@ -25,7 +24,7 @@ internal static class NativeMethods
[SuppressUnmanagedCodeSecurity]
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
- internal static extern SafeFileHandle CreateFile([MarshalAs(UnmanagedType.LPWStr)] string lpFileName, FileAccess dwDesiredAccess, FileShare dwShareMode, IntPtr lpSecurityAttributes, FileMode dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile);
+ internal static extern SafeFileHandle CreateFile(string lpFileName, FileAccess dwDesiredAccess, FileShare dwShareMode, IntPtr lpSecurityAttributes, FileMode dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
@@ -37,14 +36,14 @@ internal static class NativeMethods
internal static extern bool DeviceIoControl(SafeFileHandle hDevice, int dwIoControlCode, IntPtr lpInBuffer, int nInBufferSize, IntPtr lpOutBuffer, int nOutBufferSize, out int lpBytesReturned, IntPtr lpOverlapped);
[DllImport("dwmapi.dll", SetLastError = true)]
- internal static extern void DwmSetWindowAttribute(IntPtr hWnd, int attribute, ref int value, int size);
+ internal static extern int DwmSetWindowAttribute(IntPtr hWnd, int attribute, ref int value, int size);
[SuppressUnmanagedCodeSecurity]
[DllImport("psapi.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool EmptyWorkingSet(IntPtr hProcess);
- [DllImport("user32", SetLastError = true, CharSet = CharSet.Unicode)]
+ [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[SuppressUnmanagedCodeSecurity]
@@ -94,4 +93,4 @@ internal static class NativeMethods
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool UnregisterHotKey(IntPtr hWnd, int id);
}
-}
+}
\ No newline at end of file
diff --git a/src/Interop/ShellInterop.cs b/src/Interop/ShellInterop.cs
index 1fe7f842..d7c4d924 100644
--- a/src/Interop/ShellInterop.cs
+++ b/src/Interop/ShellInterop.cs
@@ -46,6 +46,20 @@ internal interface IShellLink
void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
}
+ [ComImport]
+ [Guid("0000010B-0000-0000-C000-000000000046")]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ internal interface IPersistFile
+ {
+ void GetClassID(out Guid pClassID);
+ [PreserveSig]
+ int IsDirty();
+ void Load([MarshalAs(UnmanagedType.LPWStr)] string pszFileName, int dwMode);
+ void Save([MarshalAs(UnmanagedType.LPWStr)] string pszFileName, [MarshalAs(UnmanagedType.Bool)] bool fRemember);
+ void SaveCompleted([MarshalAs(UnmanagedType.LPWStr)] string pszFileName);
+ void GetCurFile([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder ppszFileName);
+ }
+
#endregion
}
}
\ No newline at end of file
diff --git a/src/Model/Localization.cs b/src/Model/Localization.cs
index 0c10bffb..993be9fe 100644
--- a/src/Model/Localization.cs
+++ b/src/Model/Localization.cs
@@ -42,511 +42,511 @@ public class Localization
public string About
{
get { return _about; }
- private set { _about = value.Capitalize(); }
+ set { _about = value; }
}
[DataMember]
public string Add
{
get { return _add; }
- private set { _add = value.Capitalize(); }
+ set { _add = value.Capitalize(); }
}
[DataMember]
public string AlwaysOnTop
{
get { return _alwaysOnTop; }
- private set { _alwaysOnTop = value.Capitalize(); }
+ set { _alwaysOnTop = value.Capitalize(); }
}
[DataMember]
public string AutoOptimization
{
get { return _autoOptimization; }
- private set { _autoOptimization = value.Capitalize(); }
+ set { _autoOptimization = value.Capitalize(); }
}
[DataMember]
public string AutoOptimizationInterval
{
get { return _autoOptimizationInterval; }
- private set { _autoOptimizationInterval = value.Capitalize(); }
+ set { _autoOptimizationInterval = value.Capitalize(); }
}
[DataMember]
public string AutoUpdate
{
get { return _autoUpdate; }
- private set { _autoUpdate = value.Capitalize(); }
+ set { _autoUpdate = value.Capitalize(); }
}
[DataMember]
public string Background
{
get { return _background; }
- private set { _background = value.Capitalize(); }
+ set { _background = value.Capitalize(); }
}
[DataMember]
public string Close
{
get { return _close; }
- private set { _close = value.Capitalize(); }
+ set { _close = value.Capitalize(); }
}
[DataMember]
public string CloseAfterOptimization
{
get { return _closeAfterOptimization; }
- private set { _closeAfterOptimization = value.Capitalize(); }
+ set { _closeAfterOptimization = value.Capitalize(); }
}
[DataMember]
public string CloseToTheNotificationArea
{
get { return _closeToTheNotificationArea; }
- private set { _closeToTheNotificationArea = value.Capitalize(); }
+ set { _closeToTheNotificationArea = value.Capitalize(); }
}
[DataMember]
public string Collapse
{
get { return _collapse; }
- private set { _collapse = value.Capitalize(); }
+ set { _collapse = value.Capitalize(); }
}
[DataMember]
public string CombinedPageList
{
get { return _combinedPageList; }
- private set { _combinedPageList = value.Capitalize(); }
+ set { _combinedPageList = value.Capitalize(); }
}
[DataMember]
public string CreateStartMenuShortcut
{
get { return _createStartMenuShortcut; }
- private set { _createStartMenuShortcut = value.Capitalize(); }
+ set { _createStartMenuShortcut = value.Capitalize(); }
}
[DataMember]
public string DangerLevel
{
get { return _dangerLevel; }
- private set { _dangerLevel = value.Capitalize(); }
+ set { _dangerLevel = value.Capitalize(); }
}
[DataMember]
public string Donate
{
get { return _donate; }
- private set { _donate = value.Capitalize(); }
+ set { _donate = value.Capitalize(); }
}
[DataMember]
public string DonationMessage
{
get { return _donationMessage; }
- private set { _donationMessage = value.Capitalize(); }
+ set { _donationMessage = value.Capitalize(); }
}
[DataMember]
public string DonationTitle
{
get { return _donationTitle; }
- private set { _donationTitle = value.Capitalize(); }
+ set { _donationTitle = value.Capitalize(); }
}
[DataMember]
public string Error
{
get { return _error; }
- private set { _error = value.Capitalize(); }
+ set { _error = value.Capitalize(); }
}
[DataMember]
public string ErrorAdminPrivilegeRequired
{
get { return _errorAdminPrivilegeRequired; }
- private set { _errorAdminPrivilegeRequired = value.Capitalize(); }
+ set { _errorAdminPrivilegeRequired = value.Capitalize(); }
}
[DataMember]
public string ErrorCanNotSaveLog
{
get { return _errorCanNotSaveLog; }
- private set { _errorCanNotSaveLog = value.Capitalize(); }
+ set { _errorCanNotSaveLog = value.Capitalize(); }
}
[DataMember]
public string ErrorMemoryAreaOptimizationNotSupported
{
get { return _errorMemoryAreaOptimizationNotSupported; }
- private set { _errorMemoryAreaOptimizationNotSupported = value.Capitalize(); }
+ set { _errorMemoryAreaOptimizationNotSupported = value.Capitalize(); }
}
[DataMember]
public string ErrorResetCommand
{
get { return _errorResetCommand; }
- private set { _errorResetCommand = value.Capitalize(); }
+ set { _errorResetCommand = value.Capitalize(); }
}
[DataMember]
public string EveryHour
{
get { return _everyHour; }
- private set { _everyHour = value.Capitalize(); }
+ set { _everyHour = value.Capitalize(); }
}
[DataMember]
public string Exit
{
get { return _exit; }
- private set { _exit = value.Capitalize(); }
+ set { _exit = value.Capitalize(); }
}
[DataMember]
public string Expand
{
get { return _expand; }
- private set { _expand = value.Capitalize(); }
+ set { _expand = value.Capitalize(); }
}
[DataMember]
public string Free
{
get { return _free; }
- private set { _free = value.Capitalize(); }
+ set { _free = value.Capitalize(); }
}
[DataMember]
public string GarbageCollector
{
get { return _garbageCollector; }
- private set { _garbageCollector = value.Capitalize(); }
+ set { _garbageCollector = value.Capitalize(); }
}
[DataMember]
public string Help
{
get { return _help; }
- private set { _help = value.Capitalize(); }
+ set { _help = value.Capitalize(); }
}
[DataMember]
public string HotkeyIsInUseByOperatingSystem
{
get { return _hotkeyIsInUseByOperatingSystem; }
- private set { _hotkeyIsInUseByOperatingSystem = value.Capitalize(); }
+ set { _hotkeyIsInUseByOperatingSystem = value.Capitalize(); }
}
[DataMember]
public string Invalid
{
get { return _invalid; }
- private set { _invalid = value.Capitalize(); }
+ set { _invalid = value.Capitalize(); }
}
[DataMember]
public string LowMemory
{
get { return _lowMemory; }
- private set { _lowMemory = value.Capitalize(); }
+ set { _lowMemory = value.Capitalize(); }
}
[DataMember]
public string Manual
{
get { return _manual; }
- private set { _manual = value.Capitalize(); }
+ set { _manual = value.Capitalize(); }
}
[DataMember]
public string MemoryAreas
{
get { return _memoryAreas; }
- private set { _memoryAreas = value.Capitalize(); }
+ set { _memoryAreas = value.Capitalize(); }
}
[DataMember]
public string MemoryOptimized
{
get { return _memoryOptimized; }
- private set { _memoryOptimized = value.Capitalize(); }
+ set { _memoryOptimized = value.Capitalize(); }
}
[DataMember]
public string MemoryUsage
{
get { return _memoryUsage; }
- private set { _memoryUsage = value.Capitalize(); }
+ set { _memoryUsage = value.Capitalize(); }
}
[DataMember]
public string Minimize
{
get { return _minimize; }
- private set { _minimize = value.Capitalize(); }
+ set { _minimize = value.Capitalize(); }
}
[DataMember]
public string ModifiedFileCache
{
get { return _modifiedFileCache; }
- private set { _modifiedFileCache = value.Capitalize(); }
+ set { _modifiedFileCache = value.Capitalize(); }
}
[DataMember]
public string ModifiedPageList
{
get { return _modifiedPageList; }
- private set { _modifiedPageList = value.Capitalize(); }
+ set { _modifiedPageList = value.Capitalize(); }
}
[DataMember]
public string No
{
get { return _no; }
- private set { _no = value.Capitalize(); }
+ set { _no = value.Capitalize(); }
}
[DataMember]
public string OptimizationHotkey
{
get { return _optimizationHotkey; }
- private set { _optimizationHotkey = value.Capitalize(); }
+ set { _optimizationHotkey = value.Capitalize(); }
}
[DataMember]
public string Optimize
{
get { return _optimize; }
- private set { _optimize = value.Capitalize(); }
+ set { _optimize = value.Capitalize(); }
}
[DataMember]
public string OptimizeOnMiddleMouseClick
{
get { return _optimizeOnMiddleMouseClick; }
- private set { _optimizeOnMiddleMouseClick = value.Capitalize(); }
+ set { _optimizeOnMiddleMouseClick = value.Capitalize(); }
}
[DataMember]
public string Optimizing
{
get { return _optimizing; }
- private set { _optimizing = value.Capitalize(); }
+ set { _optimizing = value.Capitalize(); }
}
[DataMember]
public string PhysicalMemory
{
get { return _physicalMemory; }
- private set { _physicalMemory = value.Capitalize(); }
+ set { _physicalMemory = value.Capitalize(); }
}
[DataMember]
public string ProcessExclusionList
{
get { return _processExclusionList; }
- private set { _processExclusionList = value.Capitalize(); }
+ set { _processExclusionList = value.Capitalize(); }
}
[DataMember]
public string Reason
{
get { return _reason; }
- private set { _reason = value.Capitalize(); }
+ set { _reason = value.Capitalize(); }
}
[DataMember]
public string RegistryCache
{
get { return _registryCache; }
- private set { _registryCache = value.Capitalize(); }
+ set { _registryCache = value.Capitalize(); }
}
[DataMember]
public string Remove
{
get { return _remove; }
- private set { _remove = value.Capitalize(); }
+ set { _remove = value.Capitalize(); }
}
[DataMember]
public string Reset
{
get { return _reset; }
- private set { _reset = value.Capitalize(); }
+ set { _reset = value.Capitalize(); }
}
[DataMember]
public string ResetCommand
{
get { return _resetCommand; }
- private set { _resetCommand = value.Capitalize(); }
+ set { _resetCommand = value.Capitalize(); }
}
[DataMember]
public string ResetConfirmation
{
get { return _resetConfirmation; }
- private set { _resetConfirmation = value.Capitalize(); }
+ set { _resetConfirmation = value.Capitalize(); }
}
[DataMember]
public string RunOnLowPriority
{
get { return _runOnLowPriority; }
- private set { _runOnLowPriority = value.Capitalize(); }
+ set { _runOnLowPriority = value.Capitalize(); }
}
[DataMember]
public string RunOnStartup
{
get { return _runOnStartup; }
- private set { _runOnStartup = value.Capitalize(); }
+ set { _runOnStartup = value.Capitalize(); }
}
[DataMember]
public string Schedule
{
get { return _schedule; }
- private set { _schedule = value.Capitalize(); }
+ set { _schedule = value.Capitalize(); }
}
[DataMember]
public string Seconds
{
get { return _seconds; }
- private set { _seconds = value.Capitalize(); }
+ set { _seconds = value.Capitalize(); }
}
[DataMember]
public string Settings
{
get { return _settings; }
- private set { _settings = value.Capitalize(); }
+ set { _settings = value.Capitalize(); }
}
[DataMember]
public string ShowMemoryUsage
{
get { return _showMemoryUsage; }
- private set { _showMemoryUsage = value.Capitalize(); }
+ set { _showMemoryUsage = value.Capitalize(); }
}
[DataMember]
public string ShowOptimizationNotifications
{
get { return _showOptimizationNotifications; }
- private set { _showOptimizationNotifications = value.Capitalize(); }
+ set { _showOptimizationNotifications = value.Capitalize(); }
}
[DataMember]
public string ShowVirtualMemory
{
get { return _showVirtualMemory; }
- private set { _showVirtualMemory = value.Capitalize(); }
+ set { _showVirtualMemory = value.Capitalize(); }
}
[DataMember]
public string StandbyList
{
get { return _standbyList; }
- private set { _standbyList = value.Capitalize(); }
+ set { _standbyList = value.Capitalize(); }
}
[DataMember]
public string StandbyListLowPriority
{
get { return _standbyListLowPriority; }
- private set { _standbyListLowPriority = value.Capitalize(); }
+ set { _standbyListLowPriority = value.Capitalize(); }
}
[DataMember]
public string StartMinimized
{
get { return _startMinimized; }
- private set { _startMinimized = value.Capitalize(); }
+ set { _startMinimized = value.Capitalize(); }
}
[DataMember]
public string SystemFileCache
{
get { return _systemFileCache; }
- private set { _systemFileCache = value.Capitalize(); }
+ set { _systemFileCache = value.Capitalize(); }
}
[DataMember]
public string Text
{
get { return _text; }
- private set { _text = value.Capitalize(); }
+ set { _text = value.Capitalize(); }
}
[DataMember]
public string TrayIcon
{
get { return _trayIcon; }
- private set { _trayIcon = value.Capitalize(); }
+ set { _trayIcon = value.Capitalize(); }
}
[DataMember]
public string UpdatedToVersion
{
get { return _updatedToVersion; }
- private set { _updatedToVersion = value.Capitalize(); }
+ set { _updatedToVersion = value.Capitalize(); }
}
[DataMember]
public string UseTransparentBackground
{
get { return _useTransparentBackground; }
- private set { _useTransparentBackground = value.Capitalize(); }
+ set { _useTransparentBackground = value.Capitalize(); }
}
[DataMember]
public string Used
{
get { return _used; }
- private set { _used = value.Capitalize(); }
+ set { _used = value.Capitalize(); }
}
[DataMember]
public string VirtualMemory
{
get { return _virtualMemory; }
- private set { _virtualMemory = value.Capitalize(); }
+ set { _virtualMemory = value.Capitalize(); }
}
[DataMember]
public string WhenFreePhysicalMemoryIsBelow
{
get { return _whenFreePhysicalMemoryIsBelow; }
- private set { _whenFreePhysicalMemoryIsBelow = value.Capitalize(); }
+ set { _whenFreePhysicalMemoryIsBelow = value.Capitalize(); }
}
[DataMember]
public string WarningLevel
{
get { return _warningLevel; }
- private set { _warningLevel = value.Capitalize(); }
+ set { _warningLevel = value.Capitalize(); }
}
[DataMember]
public string WorkingSet
{
get { return _workingSet; }
- private set { _workingSet = value.Capitalize(); }
+ set { _workingSet = value.Capitalize(); }
}
[DataMember]
public string Yes
{
get { return _yes; }
- private set { _yes = value.Capitalize(); }
+ set { _yes = value.Capitalize(); }
}
#endregion
diff --git a/src/Properties/AssemblyInfo.cs b/src/Properties/AssemblyInfo.cs
index 5f84a4e1..2a0ba0cb 100644
--- a/src/Properties/AssemblyInfo.cs
+++ b/src/Properties/AssemblyInfo.cs
@@ -11,7 +11,7 @@
[assembly: AssemblyKeyFile(Constants.App.KeyFile)]
[assembly: AssemblyProduct(Constants.App.Name)]
[assembly: AssemblyTitle(Constants.App.Title)]
-[assembly: AssemblyVersion("3.0.8.0")]
+[assembly: AssemblyVersion("3.1.0.0")]
[assembly: ComVisible(false)]
[assembly: CLSCompliant(true)]
[assembly: Guid(Constants.App.Id)]
diff --git a/src/Service/ComputerService.cs b/src/Service/ComputerService.cs
index a4ca5319..d83eeee2 100644
--- a/src/Service/ComputerService.cs
+++ b/src/Service/ComputerService.cs
@@ -64,10 +64,10 @@ public OperatingSystem OperatingSystem
{
Is64Bit = Environment.Is64BitOperatingSystem,
IsWindows7OrGreater = (operatingSystem.Version.Major > 6) || (operatingSystem.Version.Major == 6 && operatingSystem.Version.Minor >= 1),
- IsWindows8OrGreater = operatingSystem.Version.Major >= 6.2,
- IsWindows81OrGreater = operatingSystem.Version.Major >= 6.3,
+ IsWindows8OrGreater = (operatingSystem.Version.Major > 6) || (operatingSystem.Version.Major == 6 && operatingSystem.Version.Minor >= 2),
+ IsWindows81OrGreater = (operatingSystem.Version.Major > 6) || (operatingSystem.Version.Major == 6 && operatingSystem.Version.Minor >= 3),
IsWindowsVistaOrGreater = operatingSystem.Version.Major >= 6,
- IsWindowsXpOrGreater = operatingSystem.Version.Major >= 5.1
+ IsWindowsXpOrGreater = (operatingSystem.Version.Major > 5) || (operatingSystem.Version.Major == 5 && operatingSystem.Version.Minor >= 1)
};
}
@@ -135,6 +135,22 @@ private bool SetIncreasePrivilege(string privilegeName)
return result;
}
+ private static IntPtr AllocStruct(object obj)
+ {
+ var size = Marshal.SizeOf(obj);
+ var ptr = Marshal.AllocHGlobal(size);
+ try
+ {
+ Marshal.StructureToPtr(obj, ptr, false);
+ }
+ catch
+ {
+ Marshal.FreeHGlobal(ptr);
+ throw;
+ }
+ return ptr;
+ }
+
#endregion
#region Methods (Memory)
@@ -466,28 +482,17 @@ private void OptimizeCombinedPageList()
if (!SetIncreasePrivilege(Constants.Windows.Privilege.SeProfSingleProcessName))
throw new Exception(string.Format(Localizer.Culture, Localizer.String.ErrorAdminPrivilegeRequired, Constants.Windows.Privilege.SeProfSingleProcessName));
- var handle = GCHandle.Alloc(0);
+ var memoryCombineInformationEx = new Structs.Windows.MemoryCombineInformationEx();
+ var ptr = AllocStruct(memoryCombineInformationEx);
try
{
- var memoryCombineInformationEx = new Structs.Windows.MemoryCombineInformationEx();
-
- handle = GCHandle.Alloc(memoryCombineInformationEx, GCHandleType.Pinned);
-
- if (NativeMethods.NtSetSystemInformation(Constants.Windows.SystemInformationClass.SystemCombinePhysicalMemoryInformation, handle.AddrOfPinnedObject(), (uint)Marshal.SizeOf(memoryCombineInformationEx)) != Constants.Windows.SystemErrorCode.ErrorSuccess)
+ if (NativeMethods.NtSetSystemInformation(Constants.Windows.SystemInformationClass.SystemCombinePhysicalMemoryInformation, ptr, (uint)Marshal.SizeOf(memoryCombineInformationEx)) != Constants.Windows.SystemErrorCode.ErrorSuccess)
throw new Win32Exception(Marshal.GetLastWin32Error());
}
finally
{
- try
- {
- if (handle.IsAllocated)
- handle.Free();
- }
- catch (InvalidOperationException)
- {
- // ignored
- }
+ Marshal.FreeHGlobal(ptr);
}
}
@@ -586,24 +591,17 @@ private void OptimizeModifiedPageList()
if (!SetIncreasePrivilege(Constants.Windows.Privilege.SeProfSingleProcessName))
throw new Exception(string.Format(Localizer.Culture, Localizer.String.ErrorAdminPrivilegeRequired, Constants.Windows.Privilege.SeProfSingleProcessName));
- var handle = GCHandle.Alloc(Constants.Windows.SystemMemoryListCommand.MemoryFlushModifiedList, GCHandleType.Pinned);
+ var command = Constants.Windows.SystemMemoryListCommand.MemoryFlushModifiedList;
+ var ptr = AllocStruct(command);
try
{
- if (NativeMethods.NtSetSystemInformation(Constants.Windows.SystemInformationClass.SystemMemoryListInformation, handle.AddrOfPinnedObject(), (uint)Marshal.SizeOf(Constants.Windows.SystemMemoryListCommand.MemoryFlushModifiedList)) != Constants.Windows.SystemErrorCode.ErrorSuccess)
+ if (NativeMethods.NtSetSystemInformation(Constants.Windows.SystemInformationClass.SystemMemoryListInformation, ptr, (uint)Marshal.SizeOf(command)) != Constants.Windows.SystemErrorCode.ErrorSuccess)
throw new Win32Exception(Marshal.GetLastWin32Error());
}
finally
{
- try
- {
- if (handle.IsAllocated)
- handle.Free();
- }
- catch (InvalidOperationException)
- {
- // ignored
- }
+ Marshal.FreeHGlobal(ptr);
}
}
@@ -635,24 +633,16 @@ private void OptimizeStandbyList(bool lowPriority = false)
throw new Exception(string.Format(Localizer.Culture, Localizer.String.ErrorAdminPrivilegeRequired, Constants.Windows.Privilege.SeProfSingleProcessName));
object memoryPurgeStandbyList = lowPriority ? Constants.Windows.SystemMemoryListCommand.MemoryPurgeLowPriorityStandbyList : Constants.Windows.SystemMemoryListCommand.MemoryPurgeStandbyList;
- var handle = GCHandle.Alloc(memoryPurgeStandbyList, GCHandleType.Pinned);
+ var ptr = AllocStruct(memoryPurgeStandbyList);
try
{
- if (NativeMethods.NtSetSystemInformation(Constants.Windows.SystemInformationClass.SystemMemoryListInformation, handle.AddrOfPinnedObject(), (uint)Marshal.SizeOf(memoryPurgeStandbyList)) != Constants.Windows.SystemErrorCode.ErrorSuccess)
+ if (NativeMethods.NtSetSystemInformation(Constants.Windows.SystemInformationClass.SystemMemoryListInformation, ptr, (uint)Marshal.SizeOf(memoryPurgeStandbyList)) != Constants.Windows.SystemErrorCode.ErrorSuccess)
throw new Win32Exception(Marshal.GetLastWin32Error());
}
finally
{
- try
- {
- if (handle.IsAllocated)
- handle.Free();
- }
- catch (InvalidOperationException)
- {
- // ignored
- }
+ Marshal.FreeHGlobal(ptr);
}
}
@@ -669,33 +659,23 @@ private void OptimizeSystemFileCache()
if (!SetIncreasePrivilege(Constants.Windows.Privilege.SeIncreaseQuotaName))
throw new Exception(string.Format(Localizer.Culture, Localizer.String.ErrorAdminPrivilegeRequired, Constants.Windows.Privilege.SeIncreaseQuotaName));
- var handle = GCHandle.Alloc(0);
+ object systemFileCacheInformation;
- try
- {
- object systemFileCacheInformation;
-
- if (OperatingSystem.Is64Bit)
- systemFileCacheInformation = new Structs.Windows.SystemFileCacheInformation64 { MinimumWorkingSet = -1L, MaximumWorkingSet = -1L };
- else
- systemFileCacheInformation = new Structs.Windows.SystemFileCacheInformation32 { MinimumWorkingSet = int.MaxValue, MaximumWorkingSet = int.MaxValue };
+ if (OperatingSystem.Is64Bit)
+ systemFileCacheInformation = new Structs.Windows.SystemFileCacheInformation64 { MinimumWorkingSet = -1L, MaximumWorkingSet = -1L };
+ else
+ systemFileCacheInformation = new Structs.Windows.SystemFileCacheInformation32 { MinimumWorkingSet = int.MaxValue, MaximumWorkingSet = int.MaxValue };
- handle = GCHandle.Alloc(systemFileCacheInformation, GCHandleType.Pinned);
+ var ptr = AllocStruct(systemFileCacheInformation);
- if (NativeMethods.NtSetSystemInformation(Constants.Windows.SystemInformationClass.SystemFileCacheInformation, handle.AddrOfPinnedObject(), (uint)Marshal.SizeOf(systemFileCacheInformation)) != Constants.Windows.SystemErrorCode.ErrorSuccess)
+ try
+ {
+ if (NativeMethods.NtSetSystemInformation(Constants.Windows.SystemInformationClass.SystemFileCacheInformation, ptr, (uint)Marshal.SizeOf(systemFileCacheInformation)) != Constants.Windows.SystemErrorCode.ErrorSuccess)
throw new Win32Exception(Marshal.GetLastWin32Error());
}
finally
{
- try
- {
- if (handle.IsAllocated)
- handle.Free();
- }
- catch (InvalidOperationException)
- {
- // ignored
- }
+ Marshal.FreeHGlobal(ptr);
}
var fileCacheSize = IntPtr.Subtract(IntPtr.Zero, 1); // Flush
@@ -721,7 +701,7 @@ private void OptimizeWorkingSet()
if (!SetIncreasePrivilege(Constants.Windows.Privilege.SeDebugName))
throw new Exception(string.Format(Localizer.Culture, Localizer.String.ErrorAdminPrivilegeRequired, Constants.Windows.Privilege.SeDebugName));
- var processes = Process.GetProcesses().Where(process => process != null && !Settings.ProcessExclusionList.Contains(process.ProcessName, StringComparer.OrdinalIgnoreCase));
+ var processes = Process.GetProcesses().Where(process => process != null && !Settings.IsProcessExcluded(process.ProcessName));
foreach (var process in processes)
{
@@ -753,11 +733,12 @@ private void OptimizeWorkingSet()
if (!SetIncreasePrivilege(Constants.Windows.Privilege.SeProfSingleProcessName))
throw new Exception(string.Format(Localizer.Culture, Localizer.String.ErrorAdminPrivilegeRequired, Constants.Windows.Privilege.SeDebugName));
- var handle = GCHandle.Alloc(Constants.Windows.SystemMemoryListCommand.MemoryEmptyWorkingSets, GCHandleType.Pinned);
+ var command = Constants.Windows.SystemMemoryListCommand.MemoryEmptyWorkingSets;
+ var ptr = AllocStruct(command);
try
{
- if (NativeMethods.NtSetSystemInformation(Constants.Windows.SystemInformationClass.SystemMemoryListInformation, handle.AddrOfPinnedObject(), (uint)Marshal.SizeOf(Constants.Windows.SystemMemoryListCommand.MemoryEmptyWorkingSets)) != Constants.Windows.SystemErrorCode.ErrorSuccess)
+ if (NativeMethods.NtSetSystemInformation(Constants.Windows.SystemInformationClass.SystemMemoryListInformation, ptr, (uint)Marshal.SizeOf(command)) != Constants.Windows.SystemErrorCode.ErrorSuccess)
{
var e = new Win32Exception(Marshal.GetLastWin32Error());
@@ -772,15 +753,7 @@ private void OptimizeWorkingSet()
}
finally
{
- try
- {
- if (handle.IsAllocated)
- handle.Free();
- }
- catch (InvalidOperationException)
- {
- // ignored
- }
+ Marshal.FreeHGlobal(ptr);
}
}
@@ -790,4 +763,4 @@ private void OptimizeWorkingSet()
#endregion
}
-}
+}
\ No newline at end of file
diff --git a/src/Service/HotKeyService.cs b/src/Service/HotKeyService.cs
index 7b560468..6cb7eea0 100644
--- a/src/Service/HotKeyService.cs
+++ b/src/Service/HotKeyService.cs
@@ -1,4 +1,5 @@
๏ปฟusing System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
@@ -16,7 +17,7 @@ public class HotkeyService : IHotkeyService
#region Fields
private readonly bool _isSupported = Environment.OSVersion.Version.Major >= 6; // Minimum supported Windows Vista / Server 2003
- private readonly Dictionary _registered = new Dictionary();
+ private readonly ConcurrentDictionary _registered = new ConcurrentDictionary();
#endregion
@@ -174,8 +175,8 @@ public bool Register(Hotkey hotkey, Action action)
result = NativeMethods.RegisterHotKey(IntPtr.Zero, hotkey.GetHashCode(), (uint)hotkey.Modifiers, (uint)KeyInterop.VirtualKeyFromKey(hotkey.Key));
- if (!_registered.ContainsKey(hotkey))
- _registered.Add(hotkey, action);
+ if (result)
+ _registered.TryAdd(hotkey, action);
}
catch
{
@@ -201,7 +202,7 @@ public bool Unregister(Hotkey hotkey)
result = NativeMethods.UnregisterHotKey(IntPtr.Zero, hotkey.GetHashCode());
- _registered.Remove(hotkey);
+ _registered.TryRemove(hotkey, out _);
}
catch
{
diff --git a/src/Test/CommandTests.cs b/src/Test/CommandTests.cs
deleted file mode 100644
index 117362ea..00000000
--- a/src/Test/CommandTests.cs
+++ /dev/null
@@ -1,165 +0,0 @@
-using System;
-using NUnit.Framework;
-
-#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
-
-namespace WinMemoryCleaner.Test
-{
- ///
- /// Unit tests for Command classes
- ///
- [TestFixture]
- public sealed class CommandTests
- {
- #region RelayCommand Tests
-
- [TestFixture]
- public class RelayCommandTests
- {
- [Test]
- public void CanExecute_WithNoCanExecuteFunc_ReturnsTrue()
- {
- var command = new RelayCommand(() => { });
- var result = command.CanExecute(null);
-
- Assert.IsTrue(result);
- }
-
- [Test]
- public void CanExecute_WithTrueFunc_ReturnsTrue()
- {
- var command = new RelayCommand(() => { }, () => true);
- var result = command.CanExecute(null);
-
- Assert.IsTrue(result);
- }
-
- [Test]
- public void CanExecute_WithFalseFunc_ReturnsFalse()
- {
- var command = new RelayCommand(() => { }, () => false);
- var result = command.CanExecute(null);
-
- Assert.IsFalse(result);
- }
-
- [Test]
- public void CanExecuteChanged_AddAndRemoveHandlers_DoesNotThrow()
- {
- var command = new RelayCommand(() => { });
- EventHandler handler = (sender, e) => { };
-
- command.CanExecuteChanged += handler;
- command.CanExecuteChanged -= handler;
- }
-
- [Test]
- public void Constructor_WithAction_DoesNotThrow()
- {
- var unused = new RelayCommand(() => { });
- }
-
- [Test]
- public void Execute_InvokesAction()
- {
- var executed = false;
- var command = new RelayCommand(() => executed = true);
-
- command.Execute(null);
-
- Assert.IsTrue(executed);
- }
-
- [Test]
- public void Execute_MultipleTimes_InvokesMultipleTimes()
- {
- var count = 0;
- var command = new RelayCommand(() => count++);
-
- command.Execute(null);
- command.Execute(null);
- command.Execute(null);
-
- Assert.AreEqual(3, count);
- }
- }
-
- #endregion
-
- #region RelayCommand Tests
-
- [TestFixture]
- public class RelayCommandGenericTests
- {
- [Test]
- public void CanExecute_WithNoCanExecuteFunc_ReturnsTrue()
- {
- var command = new RelayCommand(_ => { });
- var result = command.CanExecute(42);
-
- Assert.IsTrue(result);
- }
-
- [Test]
- public void CanExecute_WithTrueFunc_ReturnsTrue()
- {
- var command = new RelayCommand(_ => { }, () => true);
- var result = command.CanExecute(42);
-
- Assert.IsTrue(result);
- }
-
- [Test]
- public void CanExecute_WithFalseFunc_ReturnsFalse()
- {
- var command = new RelayCommand(_ => { }, () => false);
- var result = command.CanExecute(42);
-
- Assert.IsFalse(result);
- }
-
- [Test]
- public void Constructor_WithAction_DoesNotThrow()
- {
- var unused = new RelayCommand(_ => { });
- }
-
- [Test]
- public void Execute_InvokesActionWithParameter()
- {
- string result = null;
- var command = new RelayCommand(param => result = param);
-
- command.Execute("test");
-
- Assert.AreEqual("test", result);
- }
-
- [Test]
- public void Execute_WithComplexType_InvokesCorrectly()
- {
- TestObject receivedObject = null;
- var command = new RelayCommand(obj => receivedObject = obj);
- var testObject = new TestObject { Value = 123 };
-
- command.Execute(testObject);
-
- Assert.AreSame(testObject, receivedObject);
- Assert.AreEqual(123, receivedObject.Value);
- }
- }
-
- #endregion
-
- #region Helper Classes
-
- private class TestObject
- {
- public int Value { get; set; }
- }
-
- #endregion
- }
-}
-
-#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
diff --git a/src/Test/ConverterTests.cs b/src/Test/ConverterTests.cs
deleted file mode 100644
index 40532521..00000000
--- a/src/Test/ConverterTests.cs
+++ /dev/null
@@ -1,343 +0,0 @@
-using NUnit.Framework;
-using System;
-using System.Globalization;
-using System.Windows.Media;
-
-#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
-
-namespace WinMemoryCleaner.Test
-{
- ///
- /// Unit tests for Converter classes
- ///
-
- [TestFixture]
- public sealed class ConverterTests
- {
- #region BrushToHexConverter Tests
-
-
- [TestFixture]
- public class BrushToHexConverterTests
- {
- [Test]
- public void Convert_WithSolidColorBrush_ReturnsHexString()
- {
- // Arrange
- var converter = new BrushToHexConverter();
- var brush = new SolidColorBrush(Colors.White);
-
- // Act
- var result = converter.Convert(brush, typeof(string), null, CultureInfo.InvariantCulture);
-
- // Assert
- Assert.IsNotNull(result);
- Assert.IsInstanceOf(result);
- }
-
- [Test]
- public void Convert_WithNullBrush_ReturnsNull()
- {
- var converter = new BrushToHexConverter();
- var result = converter.Convert(null, typeof(string), null, CultureInfo.InvariantCulture);
-
- Assert.IsNull(result);
- }
-
- [Test]
- public void Convert_WithNonBrush_ReturnsNull()
- {
- var converter = new BrushToHexConverter();
- var result = converter.Convert("not a brush", typeof(string), null, CultureInfo.InvariantCulture);
-
- Assert.IsNull(result);
- }
-
- [Test]
- public void ConvertBack_ThrowsNotImplementedException()
- {
- // Arrange
- var converter = new BrushToHexConverter();
-
- // Act & Assert
- Assert.Throws(() => converter.ConvertBack(null, typeof(Brush), null, CultureInfo.InvariantCulture));
- }
- }
-
- #endregion
-
- #region EnumToBooleanConverter Tests
-
-
- [TestFixture]
- public class EnumToBooleanConverterTests
- {
- [Test]
- public void Convert_WithMatchingValues_ReturnsTrue()
- {
- // Arrange
- var converter = new EnumToBooleanConverter();
- var enumValue = DayOfWeek.Monday;
- var parameter = DayOfWeek.Monday;
-
- // Act
- var result = converter.Convert(enumValue, typeof(bool), parameter, CultureInfo.InvariantCulture);
-
- // Assert
- Assert.IsTrue((bool)result);
- }
-
- [Test]
- public void Convert_WithNonMatchingValues_ReturnsFalse()
- {
- // Arrange
- var converter = new EnumToBooleanConverter();
- var enumValue = DayOfWeek.Monday;
- var parameter = DayOfWeek.Tuesday;
-
- // Act
- var result = converter.Convert(enumValue, typeof(bool), parameter, CultureInfo.InvariantCulture);
-
- // Assert
- Assert.IsFalse((bool)result);
- }
-
- [Test]
- public void Convert_WithNullValue_ReturnsFalse()
- {
- var converter = new EnumToBooleanConverter();
- var result = converter.Convert(null, typeof(bool), Enums.Priority.Normal, CultureInfo.InvariantCulture);
-
- Assert.IsFalse((bool)result);
- }
-
- [Test]
- public void ConvertBack_WithTrue_ReturnsParameter()
- {
- // Arrange
- var converter = new EnumToBooleanConverter();
- var parameter = DayOfWeek.Monday;
-
- // Act
- var result = converter.ConvertBack(true, typeof(DayOfWeek), parameter, CultureInfo.InvariantCulture);
-
- // Assert
- Assert.AreEqual(parameter, result);
- }
-
- [Test]
- public void ConvertBack_WithFalse_ReturnsParameter()
- {
- var converter = new EnumToBooleanConverter();
- var result = converter.ConvertBack(false, typeof(Enums.Priority), Enums.Priority.High, CultureInfo.InvariantCulture);
-
- // EnumToBooleanConverter.ConvertBack always returns the parameter value
- Assert.AreEqual(Enums.Priority.High, result);
- }
- }
-
- #endregion
-
- #region InverseBooleanConverter Tests
-
-
- [TestFixture]
- public class InverseBooleanConverterTests
- {
- [Test]
- public void Convert_WithFalse_ReturnsTrue()
- {
- // Arrange
- var converter = new InverseBooleanConverter();
-
- // Act
- var result = converter.Convert(false, typeof(bool), null, CultureInfo.InvariantCulture);
-
- // Assert
- Assert.IsTrue((bool)result);
- }
-
- [Test]
- public void Convert_WithTrue_ReturnsFalse()
- {
- // Arrange
- var converter = new InverseBooleanConverter();
-
- // Act
- var result = converter.Convert(true, typeof(bool), null, CultureInfo.InvariantCulture);
-
- // Assert
- Assert.IsFalse((bool)result);
- }
-
- [Test]
- public void ConvertBack_WithFalse_ReturnsTrue()
- {
- // Arrange
- var converter = new InverseBooleanConverter();
-
- // Act
- var result = converter.ConvertBack(false, typeof(bool), null, CultureInfo.InvariantCulture);
-
- // Assert
- Assert.IsTrue((bool)result);
- }
-
- [Test]
- public void ConvertBack_WithTrue_ReturnsFalse()
- {
- // Arrange
- var converter = new InverseBooleanConverter();
-
- // Act
- var result = converter.ConvertBack(true, typeof(bool), null, CultureInfo.InvariantCulture);
-
- // Assert
- Assert.IsFalse((bool)result);
- }
- }
-
- #endregion
-
- #region InverseBooleanToVisibilityConverter Tests
-
-
- [TestFixture]
- public class InverseBooleanToVisibilityConverterTests
- {
- [Test]
- public void Convert_WithFalse_ReturnsVisible()
- {
- // Arrange
- var converter = new InverseBooleanToVisibilityConverter();
-
- // Act
- var result = converter.Convert(false, typeof(System.Windows.Visibility), null, CultureInfo.InvariantCulture);
-
- // Assert
- Assert.AreEqual(System.Windows.Visibility.Visible, result);
- }
-
- [Test]
- public void Convert_WithTrue_ReturnsCollapsed()
- {
- // Arrange
- var converter = new InverseBooleanToVisibilityConverter();
-
- // Act
- var result = converter.Convert(true, typeof(System.Windows.Visibility), null, CultureInfo.InvariantCulture);
-
- // Assert
- Assert.AreEqual(System.Windows.Visibility.Collapsed, result);
- }
- }
-
- #endregion
-
- #region NullToVisibilityConverter Tests
-
-
- [TestFixture]
- public class NullToVisibilityConverterTests
- {
- [Test]
- public void Convert_WithNonNull_ReturnsVisible()
- {
- // Arrange
- var converter = new NullToVisibilityConverter();
-
- // Act
- var result = converter.Convert("test", typeof(System.Windows.Visibility), null, CultureInfo.InvariantCulture);
-
- // Assert
- Assert.AreEqual(System.Windows.Visibility.Visible, result);
- }
-
- [Test]
- public void Convert_WithNull_ReturnsCollapsed()
- {
- // Arrange
- var converter = new NullToVisibilityConverter();
-
- // Act
- var result = converter.Convert(null, typeof(System.Windows.Visibility), null, CultureInfo.InvariantCulture);
-
- // Assert
- Assert.AreEqual(System.Windows.Visibility.Collapsed, result);
- }
- }
-
- #endregion
-
- #region PercentageToWidthConverter Tests
-
-
- [TestFixture]
- public class PercentageToWidthConverterTests
- {
- [Test]
- public void Convert_WithValidPercentage_CalculatesWidth()
- {
- // Arrange
- var converter = new PercentageToWidthConverter();
-
- // Act
- var result = converter.Convert(new object[] { 200.0, 50.0 }, typeof(double), null, CultureInfo.InvariantCulture);
-
- // Assert
- Assert.AreEqual(100.0, (double)result);
- }
- }
-
- #endregion
-
- #region StringFormatConverter Tests
-
-
- [TestFixture]
- public class StringFormatConverterTests
- {
- [Test]
- public void Convert_WithFormatString_FormatsValue()
- {
- // Arrange
- var converter = new StringFormatConverter();
-
- // Act
- var result = converter.Convert(123, typeof(string), "{0:D5}", CultureInfo.InvariantCulture);
-
- // Assert
- Assert.IsInstanceOf(result);
- Assert.AreEqual("00123", result);
- }
- }
-
- #endregion
-
- #region SumNumericConverter Tests
-
-
- [TestFixture]
- public class SumNumericConverterTests
- {
- [Test]
- public void Convert_WithNumericValues_ReturnsSum()
- {
- // Arrange
- var converter = new SumNumericConverter();
- var values = new object[] { 10, 20, 30 };
-
- // Act
- var result = converter.Convert(values, typeof(double), null, CultureInfo.InvariantCulture);
-
- // Assert
- Assert.IsInstanceOf(result);
- Assert.AreEqual(60.0, (double)result);
- }
- }
-
- #endregion
- }
-}
-
-#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
diff --git a/src/Test/CoreTests.cs b/src/Test/CoreTests.cs
deleted file mode 100644
index 33244d39..00000000
--- a/src/Test/CoreTests.cs
+++ /dev/null
@@ -1,1355 +0,0 @@
-using NUnit.Framework;
-using System;
-using System.ComponentModel;
-using System.IO;
-
-#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
-
-namespace WinMemoryCleaner.Test
-{
-
- public static class CoreTests
- {
- #region Constants Tests
-
- [TestFixture]
- public class ConstantsTests
- {
- [Test]
- public void App_AutoOptimizationMemoryUsageInterval_Is5Minutes()
- {
- Assert.AreEqual(5, Constants.App.AutoOptimizationMemoryUsageInterval);
- }
-
- [Test]
- public void App_AutoUpdateInterval_Is24Hours()
- {
- Assert.AreEqual(24, Constants.App.AutoUpdateInterval);
- }
-
- [Test]
- public void App_Author_Name_IsNotEmpty()
- {
- Assert.IsNotNull(Constants.App.Author.Name);
- Assert.IsFalse(string.IsNullOrEmpty(Constants.App.Author.Name));
- Assert.AreEqual("Igor Mundstein", Constants.App.Author.Name);
- }
-
- [Test]
- public void App_CommandLineArgument_Install_IsCorrect()
- {
- Assert.AreEqual("Install", Constants.App.CommandLineArgument.Install);
- }
-
- [Test]
- public void App_CommandLineArgument_Reset_IsCorrect()
- {
- Assert.AreEqual("Reset", Constants.App.CommandLineArgument.Reset);
- }
-
- [Test]
- public void App_CommandLineArgument_Service_IsCorrect()
- {
- Assert.AreEqual("Service", Constants.App.CommandLineArgument.Service);
- }
-
- [Test]
- public void App_CommandLineArgument_Uninstall_IsCorrect()
- {
- Assert.AreEqual("Uninstall", Constants.App.CommandLineArgument.Uninstall);
- }
-
- [Test]
- public void App_Donation_BitcoinUri_IsValid()
- {
- Assert.IsNotNull(Constants.App.Donation.BitcoinUri);
- Assert.IsTrue(Constants.App.Donation.BitcoinUri.IsAbsoluteUri);
- }
-
- [Test]
- public void App_Donation_EthereumUri_IsValid()
- {
- Assert.IsNotNull(Constants.App.Donation.EthereumUri);
- Assert.IsTrue(Constants.App.Donation.EthereumUri.IsAbsoluteUri);
- }
-
- [Test]
- public void App_Donation_GitHubSponsorUri_IsValid()
- {
- Assert.IsNotNull(Constants.App.Donation.GitHubSponsorUri);
- Assert.IsTrue(Constants.App.Donation.GitHubSponsorUri.IsAbsoluteUri);
- }
-
- [Test]
- public void App_Donation_KofiUri_IsValid()
- {
- Assert.IsNotNull(Constants.App.Donation.KofiUri);
- Assert.IsTrue(Constants.App.Donation.KofiUri.IsAbsoluteUri);
- }
-
- [Test]
- public void App_Id_IsValidGuid()
- {
- var guid = Guid.Parse(Constants.App.Id);
-
- Assert.IsNotNull(guid);
- Assert.AreNotEqual(Guid.Empty, guid);
- }
-
- [Test]
- public void App_License_IsGPL3()
- {
- Assert.AreEqual("GPL-3.0", Constants.App.License);
- }
-
- [Test]
- public void App_Name_IsNotEmpty()
- {
- Assert.IsNotNull(Constants.App.Name);
- Assert.IsFalse(string.IsNullOrEmpty(Constants.App.Name));
- Assert.AreEqual("WinMemoryCleaner", Constants.App.Name);
- }
-
- [Test]
- public void App_Repository_AboutUri_IsValid()
- {
- Assert.IsNotNull(Constants.App.Repository.AboutUri);
- Assert.IsTrue(Constants.App.Repository.AboutUri.IsAbsoluteUri);
- }
-
- [Test]
- public void App_Repository_DownloadUri_IsValid()
- {
- Assert.IsNotNull(Constants.App.Repository.DownloadUri);
- Assert.IsTrue(Constants.App.Repository.DownloadUri.IsAbsoluteUri);
- }
-
- [Test]
- public void App_Repository_LatestExeUri_IsValid()
- {
- Assert.IsNotNull(Constants.App.Repository.LatestExeUri);
- Assert.IsTrue(Constants.App.Repository.LatestExeUri.IsAbsoluteUri);
- Assert.IsTrue(Constants.App.Repository.LatestExeUri.ToString().Contains("releases/latest"));
- }
-
- [Test]
- public void App_Repository_Uri_IsValid()
- {
- Assert.IsNotNull(Constants.App.Repository.Uri);
- Assert.IsTrue(Constants.App.Repository.Uri.IsAbsoluteUri);
- Assert.IsTrue(Constants.App.Repository.Uri.ToString().Contains("github.com"));
- }
-
- [Test]
- public void App_Title_IsNotEmpty()
- {
- Assert.IsNotNull(Constants.App.Title);
- Assert.IsFalse(string.IsNullOrEmpty(Constants.App.Title));
- Assert.AreEqual("Windows Memory Cleaner", Constants.App.Title);
- }
-
- [Test]
- public void App_VersionFormat_IsValid()
- {
- Assert.AreEqual("{0}.{1}.{2}", Constants.App.VersionFormat);
- }
-
- [Test]
- public void Windows_Console_AttachParentProcess_IsNegativeOne()
- {
- Assert.AreEqual(-1, Constants.Windows.Console.AttachParentProcess);
- }
-
- [Test]
- public void Windows_Console_StdOutputHandle_IsNegativeEleven()
- {
- Assert.AreEqual(-11, Constants.Windows.Console.StdOutputHandle);
- }
-
- [Test]
- public void Windows_Privilege_SeDebugName_IsCorrect()
- {
- Assert.AreEqual("SeDebugPrivilege", Constants.Windows.Privilege.SeDebugName);
- }
-
- [Test]
- public void Windows_Privilege_SeIncreaseQuotaName_IsCorrect()
- {
- Assert.AreEqual("SeIncreaseQuotaPrivilege", Constants.Windows.Privilege.SeIncreaseQuotaName);
- }
-
- [Test]
- public void Windows_PrivilegeAttribute_Enabled_IsTwo()
- {
- Assert.AreEqual(2, Constants.Windows.PrivilegeAttribute.Enabled);
- }
-
- [Test]
- public void Windows_SystemErrorCode_ErrorAccessDenied_IsFive()
- {
- Assert.AreEqual(5, Constants.Windows.SystemErrorCode.ErrorAccessDenied);
- }
-
- [Test]
- public void Windows_SystemErrorCode_ErrorSuccess_IsZero()
- {
- Assert.AreEqual(0, Constants.Windows.SystemErrorCode.ErrorSuccess);
- }
- }
-
- #endregion
-
- #region ExtensionMethods Tests
-
- [TestFixture]
- public class ExtensionMethodsTests
- {
- [Test]
- public void Capitalize_WithEmptyString_ReturnsEmpty()
- {
- string text = string.Empty;
- var result = text.Capitalize();
-
- Assert.AreEqual(string.Empty, result);
- }
-
- [Test]
- public void Capitalize_WithException_ReturnsOriginal()
- {
- string text = null;
- var result = text.Capitalize();
-
- Assert.AreEqual(null, result);
- }
-
- [Test]
- public void Capitalize_WithMultipleSentences_CapitalizesCorrectly()
- {
- string text = "first sentence. second sentence.";
- var result = text.Capitalize();
-
- Assert.IsNotNull(result);
- Assert.IsTrue(result.StartsWith("F", System.StringComparison.Ordinal));
- }
-
- [Test]
- public void Capitalize_WithNormalText_CapitalizesFirstLetter()
- {
- string text = "hello world";
- var result = text.Capitalize();
-
- Assert.IsNotNull(result);
- Assert.IsTrue(result.StartsWith("H", System.StringComparison.Ordinal));
- }
-
- [Test]
- public void DefaultValue_WithNullableType_ReturnsNull()
- {
- var type = typeof(int?);
- var result = type.DefaultValue();
-
- Assert.IsNull(result);
- }
-
- [Test]
- public void DefaultValue_WithReferenceType_ReturnsNull()
- {
- var type = typeof(string);
- var result = type.DefaultValue();
-
- Assert.IsNull(result);
- }
-
- [Test]
- public void DefaultValue_WithValueType_ReturnsDefault()
- {
- var type = typeof(int);
- var result = type.DefaultValue();
-
- Assert.IsNotNull(result);
- Assert.AreEqual(0, result);
- }
-
- [Test]
- public void GetHex_WithBrush_ReturnsHexCode()
- {
- using (var brush = new System.Drawing.SolidBrush(System.Drawing.Color.Red))
- {
- var hex = brush.GetHex();
-
- Assert.IsNotNull(hex);
- Assert.IsTrue(hex.StartsWith("#", System.StringComparison.Ordinal));
- }
- }
-
- [Test]
- public void GetHex_WithBrushAndAlpha_ReturnsHexCodeWithAlpha()
- {
- using (var brush = new System.Drawing.SolidBrush(System.Drawing.Color.FromArgb(128, 255, 0, 0)))
- {
- var hex = brush.GetHex(true);
-
- Assert.IsNotNull(hex);
- Assert.IsTrue(hex.StartsWith("#", System.StringComparison.Ordinal));
- Assert.AreEqual(9, hex.Length); // #AARRGGBB
- }
- }
-
- [Test]
- public void GetHex_WithColor_ReturnsHexCode()
- {
- var color = System.Drawing.Color.Blue;
- var hex = color.GetHex();
-
- Assert.IsNotNull(hex);
- Assert.AreEqual("#0000FF", hex);
- }
-
- [Test]
- public void GetHex_WithMediaColor_ReturnsHexCode()
- {
- var color = System.Windows.Media.Color.FromRgb(0, 255, 0);
- var hex = color.GetHex();
-
- Assert.IsNotNull(hex);
- Assert.AreEqual("#00FF00", hex);
- }
-
- [Test]
- public void GetKeyValue_WithNo_ReturnsCorrectKeyValue()
- {
- var button = Enums.Dialog.Button.No;
- var result = button.GetKeyValue();
-
- Assert.AreEqual(Localizer.String.No, result.Key);
- Assert.AreEqual(false, result.Value);
- }
-
- [Test]
- public void GetKeyValue_WithNone_ReturnsNullKeyValue()
- {
- var button = Enums.Dialog.Button.None;
- var result = button.GetKeyValue();
-
- Assert.IsNull(result.Key);
- Assert.IsNull(result.Value);
- }
-
- [Test]
- public void GetKeyValue_WithYes_ReturnsCorrectKeyValue()
- {
- var button = Enums.Dialog.Button.Yes;
- var result = button.GetKeyValue();
-
- Assert.AreEqual(Localizer.String.Yes, result.Key);
- Assert.AreEqual(true, result.Value);
- }
-
- [Test]
- public void GetMessage_WithInnerException_ReturnsAllMessages()
- {
- var innerException = new InvalidOperationException("Inner exception");
- var exception = new Exception("Outer exception", innerException);
- var message = exception.GetMessage();
-
- Assert.IsNotNull(message);
- Assert.IsTrue(message.Contains("Outer exception"));
- Assert.IsTrue(message.Contains("Inner exception"));
- }
-
- [Test]
- public void GetMessage_WithMultipleInnerExceptions_ReturnsDeduplicatedMessages()
- {
- var ex1 = new Exception("Message 1");
- var ex2 = new Exception("Message 2", ex1);
- var ex3 = new Exception("Message 1", ex2); // Duplicate message
- var message = ex3.GetMessage();
-
- Assert.IsNotNull(message);
- }
-
- [Test]
- public void GetMessage_WithNullException_ReturnsNull()
- {
- Exception exception = null;
- var message = exception.GetMessage();
-
- Assert.IsNull(message);
- }
-
- [Test]
- public void GetMessage_WithException_ReturnsMessage()
- {
- var exception = new Exception("Test exception message");
- var message = exception.GetMessage();
-
- Assert.IsNotNull(message);
- Assert.IsTrue(message.Contains("Test exception message"));
- }
-
- [Test]
- public void GetString_WithLowMemoryReason_ReturnsCorrectString()
- {
- var reason = Enums.Memory.Optimization.Reason.LowMemory;
- var result = reason.GetString();
-
- Assert.AreEqual(Localizer.String.LowMemory, result);
- }
-
- [Test]
- public void GetString_WithManualReason_ReturnsCorrectString()
- {
- var reason = Enums.Memory.Optimization.Reason.Manual;
- var result = reason.GetString();
-
- Assert.AreEqual(Localizer.String.Manual, result);
- }
-
- [Test]
- public void GetString_WithScheduleReason_ReturnsCorrectString()
- {
- var reason = Enums.Memory.Optimization.Reason.Schedule;
- var result = reason.GetString();
-
- Assert.AreEqual(Localizer.String.Schedule, result);
- }
-
- [Test]
- public void IsEquals_WithDifferentColors_ReturnsFalse()
- {
- var mediaColor = System.Windows.Media.Color.FromRgb(255, 0, 0);
- var drawingColor = System.Drawing.Color.Blue;
- var result = mediaColor.IsEquals(drawingColor);
-
- Assert.IsFalse(result);
- }
-
- [Test]
- public void IsEquals_WithMatchingColors_ReturnsTrue()
- {
- var mediaColor = System.Windows.Media.Color.FromArgb(255, 128, 64, 32);
- var drawingColor = System.Drawing.Color.FromArgb(255, 128, 64, 32);
- var result = mediaColor.IsEquals(drawingColor);
-
- Assert.IsTrue(result);
- }
-
- [Test]
- public void IsEquals_WithNullMediaColor_ReturnsFalse()
- {
- System.Windows.Media.Color? mediaColor = null;
- var drawingColor = System.Drawing.Color.Red;
- if (mediaColor.HasValue)
- {
- var result = mediaColor.Value.IsEquals(drawingColor);
-
- Assert.IsFalse(result);
- }
- }
-
- [Test]
- public void IsNumber_WithByte_ReturnsTrue()
- {
- byte value = 100;
- var result = value.IsNumber();
-
- Assert.IsTrue(result);
- }
-
- [Test]
- public void IsNumber_WithDecimal_ReturnsTrue()
- {
- decimal value = 123.45m;
- var result = value.IsNumber();
-
- Assert.IsTrue(result);
- }
-
- [Test]
- public void IsNumber_WithDouble_ReturnsTrue()
- {
- double value = 123.45;
- var result = value.IsNumber();
-
- Assert.IsTrue(result);
- }
-
- [Test]
- public void IsNumber_WithInt_ReturnsTrue()
- {
- int value = 123;
- var result = value.IsNumber();
-
- Assert.IsTrue(result);
- }
-
- [Test]
- public void IsNumber_WithNull_ReturnsFalse()
- {
- object value = null;
- var result = value.IsNumber();
-
- Assert.IsFalse(result);
- }
-
- [Test]
- public void IsNumber_WithString_ReturnsFalse()
- {
- object value = "not a number";
- var result = value.IsNumber();
-
- Assert.IsFalse(result);
- }
-
- [Test]
- public void IsValid_WithInvalidEnum_ReturnsFalse()
- {
- var invalidEnum = (Enums.Priority)999;
- var result = invalidEnum.IsValid();
-
- Assert.IsFalse(result);
- }
-
- [Test]
- public void IsValid_WithNullEnum_ReturnsFalse()
- {
- Enum nullEnum = null;
- var result = nullEnum.IsValid();
-
- Assert.IsFalse(result);
- }
-
- [Test]
- public void IsValid_WithValidEnum_ReturnsTrue()
- {
- var validEnum = Enums.Priority.Normal;
- var result = validEnum.IsValid();
-
- Assert.IsTrue(result);
- }
-
- [Test]
- public void RemoveWhitespaces_WithMixedWhitespace_RemovesAll()
- {
- string text = " hello world \t\n";
- var result = text.RemoveWhitespaces();
-
- Assert.AreEqual("helloworld", result);
- }
-
- [Test]
- public void RemoveWhitespaces_WithNoWhitespace_ReturnsSame()
- {
- string text = "helloworld";
- var result = text.RemoveWhitespaces();
-
- Assert.AreEqual("helloworld", result);
- }
-
- [Test]
- public void ToBrush_WithInvalidHexString_ReturnsFallback()
- {
- string hex = "invalid";
- using (var fallback = new System.Drawing.SolidBrush(System.Drawing.Color.Red))
- {
- using (var result = hex.ToBrush(fallback))
- {
- Assert.AreEqual(fallback, result);
- }
- }
- }
-
- [Test]
- public void ToBrush_WithNullMediaBrush_ReturnsNull()
- {
- System.Windows.Media.Brush mediaBrush = null;
- var result = mediaBrush.ToBrush();
-
- Assert.IsNull(result);
- }
-
- [Test]
- public void ToBrush_WithValidHexString_ReturnsCorrectBrush()
- {
- string hex = "#FF0000";
- using (var fallback = new System.Drawing.SolidBrush(System.Drawing.Color.Blue))
- using (var result = hex.ToBrush(fallback))
- {
- Assert.IsNotNull(result);
- }
- }
-
- [Test]
- public void ToBrush_WithValidMediaBrush_ReturnsDrawingBrush()
- {
- var mediaBrush = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(255, 128, 64));
- using (var result = mediaBrush.ToBrush())
- {
- Assert.IsNotNull(result);
- Assert.IsInstanceOf(result);
- }
- }
-
- [Test]
- public void ToColor_WithMediaBrush_ReturnsDrawingColor()
- {
- var mediaBrush = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromArgb(200, 100, 50, 25));
- var result = mediaBrush.ToColor();
-
- Assert.AreEqual(200, result.A);
- Assert.AreEqual(100, result.R);
- Assert.AreEqual(50, result.G);
- Assert.AreEqual(25, result.B);
- }
-
- [Test]
- public void ToMemoryUnit_WithBytesValue_ConvertsCorrectly()
- {
- long bytes = 512;
- var result = bytes.ToMemoryUnit();
-
- Assert.IsNotNull(result);
- Assert.IsTrue(result.Key > 0);
- }
-
- [Test]
- public void ToMemoryUnit_WithGigabytesValue_ConvertsCorrectly()
- {
- long bytes = 1073741824; // 1 GB
- var result = bytes.ToMemoryUnit();
-
- Assert.IsNotNull(result);
- Assert.IsTrue(result.Key > 0);
- }
-
- [Test]
- public void ToMemoryUnit_WithKilobytesValue_ConvertsCorrectly()
- {
- long bytes = 2048; // 2 KB
- var result = bytes.ToMemoryUnit();
-
- Assert.IsNotNull(result);
- Assert.IsTrue(result.Key > 0);
- }
-
- [Test]
- public void ToMemoryUnit_WithMegabytesValue_ConvertsCorrectly()
- {
- long bytes = 1048576; // 1 MB
- var result = bytes.ToMemoryUnit();
-
- Assert.IsNotNull(result);
- Assert.IsTrue(result.Key > 0);
- }
-
- [Test]
- public void ToMemoryUnit_WithZeroValue_HandlesCorrectly()
- {
- long bytes = 0;
- var result = bytes.ToMemoryUnit();
-
- Assert.IsNotNull(result);
- Assert.AreEqual(0, result.Key);
- }
- }
-
- #endregion
-
- #region Helper Tests
-
- public sealed class HelperTests
- {
- private string _tempFilePath;
-
- [SetUp]
- public void SetUp()
- {
- _tempFilePath = Path.Combine(Path.GetTempPath(), "test_" + Guid.NewGuid() + ".txt");
- }
-
- [TearDown]
- public void TearDown()
- {
- if (File.Exists(_tempFilePath))
- {
- try { File.Delete(_tempFilePath); } catch { }
- }
- }
-
- [Test]
- public void DeleteFile_WhenFileDoesNotExist_ReturnsFalse()
- {
- var nonExistentPath = Path.Combine(Path.GetTempPath(), "nonexistent_" + Guid.NewGuid() + ".txt");
- var result = Helper.DeleteFile(nonExistentPath);
-
- Assert.IsFalse(result);
- }
-
- [Test]
- public void DeleteFile_WhenFileExists_ReturnsTrue()
- {
- File.WriteAllText(_tempFilePath, "test content");
-
- Assert.IsTrue(File.Exists(_tempFilePath));
- var result = Helper.DeleteFile(_tempFilePath);
-
- Assert.IsTrue(result);
-
- Assert.IsFalse(File.Exists(_tempFilePath));
- }
-
- [Test]
- public void Deserialize_WithValidJsonString_DeserializesCorrectly()
- {
- var json = "{\"Name\":\"Test\",\"Value\":123}";
- var result = Helper.Deserialize(json);
-
- Assert.IsNotNull(result);
- Assert.AreEqual("Test", result.Name);
- Assert.AreEqual(123, result.Value);
- }
-
- [Test]
- public void GetExecutablePath_ReturnsNonEmptyString()
- {
- var path = Helper.GetExecutablePath();
-
- Assert.IsNotNull(path);
- Assert.IsFalse(string.IsNullOrEmpty(path));
- }
-
- [Test]
- public void GetVersion_ReturnsValidVersion()
- {
- var version = Helper.GetVersion();
-
- Assert.IsNotNull(version);
- Assert.IsTrue(version.Major >= 0);
- Assert.IsTrue(version.Minor >= 0);
- }
-
- [Test]
- public void IsAutoUpdateSupported_ReturnsBoolean()
- {
- var isSupported = Helper.IsAutoUpdateSupported;
-
- Assert.IsNotNull(isSupported);
- }
-
- [Test]
- public void NameOf_WithNullExpression_ThrowsArgumentNullException()
- {
- Assert.Throws(() => Helper.NameOf