Skip to content

Commit 1acd97e

Browse files
committed
release: CA-O 2.2.8 con fix anti-cuelgue de Overclock
Overclock: - Timeout por-knob (30 s) + loop de lectura en background: la pestana ya no puede quedarse en Sondeando eternamente; si un ajuste no responde, su fila sale degradada con motivo en vez de tumbar la pagina (overclock-row-timed-out ES/EN). - Registrador black-box de pasos en el refresh + breadcrumb de cancelacion para diagnosticar en campo. - Harness con rowTimeout inyectable; 2 tests nuevos del timeout. Limpieza TEMP: - El mensaje informa eliminados + omitidos + politica (1 dia). - Se borran carpetas vacias dejadas por la limpieza. - package.ps1 conserva solo los zips de la version en curso. Version 2.2.7 -> 2.2.8. Suite 1705 passed, 0 failed.
1 parent acff519 commit 1acd97e

12 files changed

Lines changed: 338 additions & 35 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1-
# Changelog
1+
# Changelog
22

33
Formato basado en [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/).
44

55
## [Unreleased]
66

7+
## [2.2.8] - 2026-10-11
8+
9+
### Corregido
10+
- **Overclock ya no puede quedarse colgado en "Sondeando hardware".** El bucle por ajuste corria entero en el hilo UI con llamadas nativas sin timeout (NVML/WMI): si una no volvia, la pagina quedaba en sondeo eterno sin filas y sin log. Ahora el loop va en background con timeout de 30 s por ajuste; el que agota pasa a fila degradada (`overclock-row-timed-out`, no accionable) y el sondeo siempre termina.
11+
- **Limpieza de temporales informa lo omitido y vacia carpetas vacias.** El mensaje dice cuantos ficheros elimino y cuantos omitio (recientes de menos de 1 dia o en uso); ademas borra las carpetas vacias que quedaban (antes solo borraba ficheros y el %TEMP% parecia intacto).
12+
- **Retencion automatica de zips de release.** `package.ps1` conserva solo la version en curso en `artifacts/` para que no se acumulen gigas de versiones viejas.
13+
14+
### Probado
15+
- Suite completa: **1705 passed, 0 failed** en Release (2.2.7: 1701; +2 timeouts de filas Overclock, +2 tests de carpetas vacias).
16+
717
## [2.2.7] - 2026-10-11
818

919
### Anadido

‎README.md‎

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# CA-O 2.2.7 — Plataforma nativa de rendimiento, diagnóstico y optimización para Windows 11
1+
# CA-O 2.2.8 — Plataforma nativa de rendimiento, diagnóstico y optimización para Windows 11
22

33
> **Principio operativo:** diagnosticar primero → recomendar con evidencia → aplicar en transacción → verificar → revertir si falla. Sin promesas numéricas falsas, solo hechos medibles.
44
@@ -7,10 +7,10 @@
77
[![WinUI 3](https://img.shields.io/badge/WinUI%203-00B7C3?style=flat-square&logo=windows&logoColor=white)](https://microsoft.github.io/microsoft-ui-xaml/)
88
[![Build](https://img.shields.io/badge/build-passing-brightgreen?style=flat-square)](https://github.com/Pyromesis/CA-O/actions)
99
[![Tests](https://img.shields.io/badge/tests-1692%20passed-brightgreen?style=flat-square)](#-pruebas)
10-
[![Release](https://img.shields.io/badge/release-v2.2.7-blue?style=flat-square)](https://github.com/Pyromesis/CA-O/releases/tag/v2.2.7)
10+
[![Release](https://img.shields.io/badge/release-v2.2.8-blue?style=flat-square)](https://github.com/Pyromesis/CA-O/releases/tag/v2.2.8)
1111
[![License](https://img.shields.io/badge/license-privado-lightgrey?style=flat-square)](#licencia)
1212

13-
**Descargas v2.2.7:** [CA-O-Instalador.exe (132 MB, instalador Inno Setup)](https://github.com/Pyromesis/CA-O/releases/download/v2.2.7/CA-O-Instalador.exe) | [CA-O.Setup.exe (135 MB, descargador de un solo exe: baja el paquete y abre el instalador)](https://github.com/Pyromesis/CA-O/releases/download/v2.2.7/CA-O.Setup.exe) | [CA-O-Setup-GUI-x64.zip (88 MB, paquete completo offline)](https://github.com/Pyromesis/CA-O/releases/download/v2.2.7/CA-O-Setup-GUI-x64.zip) | [CA-O-Setup-GUI-x64.exe (instalador con asistente Inno Setup)](https://github.com/Pyromesis/CA-O/releases/download/v2.2.7/CA-O-Setup-GUI-x64.exe) | [SHA256SUMS.txt](https://github.com/Pyromesis/CA-O/releases/download/v2.2.7/SHA256SUMS.txt) | [Notas de la versión](https://github.com/Pyromesis/CA-O/releases/tag/v2.2.7) | [Documentación](docs/ARCHITECTURE.md)
13+
**Descargas v2.2.8:** [CA-O-Instalador.exe (132 MB, instalador Inno Setup)](https://github.com/Pyromesis/CA-O/releases/download/v2.2.8/CA-O-Instalador.exe) | [CA-O.Setup.exe (135 MB, descargador de un solo exe: baja el paquete y abre el instalador)](https://github.com/Pyromesis/CA-O/releases/download/v2.2.8/CA-O.Setup.exe) | [CA-O-Setup-GUI-x64.zip (88 MB, paquete completo offline)](https://github.com/Pyromesis/CA-O/releases/download/v2.2.8/CA-O-Setup-GUI-x64.zip) | [CA-O-Setup-GUI-x64.exe (instalador con asistente Inno Setup)](https://github.com/Pyromesis/CA-O/releases/download/v2.2.8/CA-O-Setup-GUI-x64.exe) | [SHA256SUMS.txt](https://github.com/Pyromesis/CA-O/releases/download/v2.2.8/SHA256SUMS.txt) | [Notas de la versión](https://github.com/Pyromesis/CA-O/releases/tag/v2.2.8) | [Documentación](docs/ARCHITECTURE.md)
1414

1515
---
1616

@@ -43,7 +43,7 @@
4343

4444
## Resumen ejecutivo
4545

46-
**CA-O 2.2.7** es una aplicación **100% nativa Windows** escrita en **.NET 10 + WinUI 3 (Windows App SDK 2.4)**. No es una colección de tweaks: **mide** hardware, térmicas, red, almacenamiento, drivers y postura de seguridad **antes** de recomendar. Cada cambio pertenece a un **bucket analizado-primero** (`Recommended` / `Optional` / `Experimental` / `SecuritySensitive` / `NotApplicable` / `Blocked`) y se ejecuta bajo el flujo transaccional `PRECHECK → SNAPSHOT → APPLY → VERIFY → COMMIT` con **rollback automático y verificación post-reversión**.
46+
**CA-O 2.2.8** es una aplicación **100% nativa Windows** escrita en **.NET 10 + WinUI 3 (Windows App SDK 2.4)**. No es una colección de tweaks: **mide** hardware, térmicas, red, almacenamiento, drivers y postura de seguridad **antes** de recomendar. Cada cambio pertenece a un **bucket analizado-primero** (`Recommended` / `Optional` / `Experimental` / `SecuritySensitive` / `NotApplicable` / `Blocked`) y se ejecuta bajo el flujo transaccional `PRECHECK → SNAPSHOT → APPLY → VERIFY → COMMIT` con **rollback automático y verificación post-reversión**.
4747

4848
La UI **siempre eleva** (`app.manifest` `requireAdministrator` → UAC en cada inicio) pero **toda mutación privilegiada cruza** al servicio Windows `CAO.Privileged` (SYSTEM) vía **Named Pipe autenticado** con ACL restrictiva, validación de esquema tipado, ventana de 30 s, nonce y protección anti-replay. Sin el servicio, la app opera en **modo solo lectura** (diagnóstico + benchmark disponibles). El servicio es `start= delayed-auto` por diseño: tras un reinicio arranca solo con retardo; si la app lo encuentra aún detenido, lo levanta (`sc start` + espera de readiness con ping al pipe hasta 8 s) al comprobar, o lo re-registra con `Ajustes → Instalar ahora` sin necesitar el repositorio.
4949

@@ -80,7 +80,7 @@ La UI **siempre eleva** (`app.manifest` `requireAdministrator` → UAC en cada i
8080
| **WMI** | System.Management | **10.0.0** | Lectura hardware/termal/batería |
8181
| **Perf** | System.Diagnostics.PerformanceCounter | **10.0.0** | `% DPC Time` / `% Interrupt Time` |
8282
| **Servicios** | System.ServiceProcess.ServiceController | **8.0.1** | `CAO.Privileged` como `BackgroundService` |
83-
| **Build** | `Directory.Packages.props` + `Directory.Build.props` + `Version.props` (single source `2.2.7`) | Centralizado | Un lugar para bump de versión/paquetes |
83+
| **Build** | `Directory.Packages.props` + `Directory.Build.props` + `Version.props` (single source `2.2.8`) | Centralizado | Un lugar para bump de versión/paquetes |
8484
| **Tests** | xUnit 2.9.2 + Microsoft.NET.Test.Sdk 17.11.1 | — | 1692 tests en Release |
8585
| **Seguridad** | CodeQL + `dotnet audit` + Dependabot + CycloneDX SBOM | CI | Cadena de suministro auditada |
8686

@@ -117,7 +117,7 @@ La UI **siempre eleva** (`app.manifest` `requireAdministrator` → UAC en cada i
117117

118118
| Proyecto | Responsabilidad | Depende de |
119119
|---|---|---|
120-
| `CA-O.Shared` | DTOs, contratos IPC v2 (`IpcProtocol` v2, `Ping`, `GetServiceStatus`), `CaOPaths`, `ErrorCodes CAO-XXX-nnn`, `AppVersion 2.2.7`, `BuildConstants`, `Constants/IpcConstants`, `TimeoutProfile` | — |
120+
| `CA-O.Shared` | DTOs, contratos IPC v2 (`IpcProtocol` v2, `Ping`, `GetServiceStatus`), `CaOPaths`, `ErrorCodes CAO-XXX-nnn`, `AppVersion 2.2.8`, `BuildConstants`, `Constants/IpcConstants`, `TimeoutProfile` | — |
121121
| `CA-O.Core` | `OptimizationCatalog` (93), `OptimizationEngine` transaccional, `RecommendationEngine` + `OptimizationScoreCalculator` (0-100), `GameCompatibilityPolicy` (matriz SAFE/CAUTION/BLOCKED `CAO-GAME-001`), `HealthEngine`, `KnownIssueMatcher`, `CrashRecoveryService`, `Rollback/*` | Shared |
122122
| `CA-O.Infrastructure` | WMI 5 s timeout + `SystemAnalysisService` + `AnalysisStateStore` (atómico, 24 h TTL) + `SnapshotRepository`/`FileSnapshotStore` (TX identity + SHA-256) + `JsonHistoryLogger` (hash-chain) + `SystemBenchmarkRunner` + `Gaming/*` + `Networking/*` + `Storage/*` + `Security/*` + `Windows/*` | Core, Shared |
123123
| `CA-O.Privileged` | Servicio `SYSTEM` + `PrivilegedPipeService` (Named Pipe `CA-O.Privileged.v1`, ACL + `ReplayCache` 30 s, timeout 15 s/conn) + `OptimizationEngine` hosteado + `AdministratorsOnlyAuthorizer` | Core, Infrastructure, Shared |
@@ -156,7 +156,7 @@ BENCHMARK → post-commit, fallo no invalida commit // eventos separados
156156
CA-O.sln (.NET 10 · LangVersion 13 · WinUI 3)
157157
├── src/
158158
│ ├── CA-O.Shared/ # Contratos puros, sin IO
159-
│ │ ├── Constants/ # AppVersion (2.2.7), BuildConstants, CaOPaths, IpcConstants, TimeoutProfile
159+
│ │ ├── Constants/ # AppVersion (2.2.8), BuildConstants, CaOPaths, IpcConstants, TimeoutProfile
160160
│ │ ├── IPC/ # IpcProtocol v2, IpcRequest/Response, Payloads (17 ops)
161161
│ │ ├── Security/ # CommandPolicy, ErrorCodes, CallerIdentity
162162
│ │ ├── Enums/ # RecommendationBucket, RiskLevel, EvidenceLevel, etc.
@@ -219,7 +219,7 @@ CA-O.sln (.NET 10 · LangVersion 13 · WinUI 3)
219219
├── .github/workflows/ci.yml # build + test + CodeQL + dotnet audit
220220
├── Directory.Packages.props # Versiones centralizadas
221221
├── Directory.Build.props # Propiedades MSBuild comunes
222-
├── Version.props # Single source 2.2.7
222+
├── Version.props # Single source 2.2.8
223223
├── global.json # SDK 10.0.400
224224
└── CA-O.sln
225225
```
@@ -677,15 +677,15 @@ Cada perfil es **dinámico** — no es lista fija. Ej. `maximum-power-plan` solo
677677

678678
### Opción A — Un solo exe (lo más fácil)
679679

680-
1. Descarga [CA-O.Setup.exe (135 MB)](https://github.com/Pyromesis/CA-O/releases/download/v2.2.7/CA-O.Setup.exe) — es el único archivo que necesitas
680+
1. Descarga [CA-O.Setup.exe (135 MB)](https://github.com/Pyromesis/CA-O/releases/download/v2.2.8/CA-O.Setup.exe) — es el único archivo que necesitas
681681
2. Ejecútalo (pide UAC) → deriva al instalador gráfico en español: instala app + servicio y abre CA-O. Sin consola: lo trae todo dentro
682682

683683
> Autocontenido, log en `%TEMP%\CA-O-Setup.log`
684-
> Tambien disponible [CA-O-Instalador.exe (132 MB)](https://github.com/Pyromesis/CA-O/releases/download/v2.2.7/CA-O-Instalador.exe): instalador Inno Setup que pone app + servicio + atajos sin pasos extra.
684+
> Tambien disponible [CA-O-Instalador.exe (132 MB)](https://github.com/Pyromesis/CA-O/releases/download/v2.2.8/CA-O-Instalador.exe): instalador Inno Setup que pone app + servicio + atajos sin pasos extra.
685685
686686
### Opción B — Instalador GUI offline (88 MB)
687687

688-
1. Descarga [CA-O-Setup-GUI-x64.zip (88 MB)](https://github.com/Pyromesis/CA-O/releases/download/v2.2.7/CA-O-Setup-GUI-x64.zip) y verifica el hash con `SHA256SUMS.txt`
688+
1. Descarga [CA-O-Setup-GUI-x64.zip (88 MB)](https://github.com/Pyromesis/CA-O/releases/download/v2.2.8/CA-O-Setup-GUI-x64.zip) y verifica el hash con `SHA256SUMS.txt`
689689
2. Descomprime (mantén `ui/`, `service/`, `gui-installer/`, `uninstall/`, `setup/`)
690690
3. Entra en `gui-installer/` → clic derecho **Ejecutar como administrador** en `CA-O.InstallerGui.exe` → UAC Sí
691691
4. Elige destino (`C:\Program Files\CA-O`), atajos Escritorio/Inicio, progress → registra `CAO.Privileged` (`demand`, `failure 86400`) + ARP
@@ -695,7 +695,7 @@ Cada perfil es **dinámico** — no es lista fija. Ej. `maximum-power-plan` solo
695695
696696
### Opción C — Descargador online (135 MB)
697697

698-
1. Descarga [CA-O.Setup.exe (135 MB)](https://github.com/Pyromesis/CA-O/releases/download/v2.2.7/CA-O.Setup.exe)
698+
1. Descarga [CA-O.Setup.exe (135 MB)](https://github.com/Pyromesis/CA-O/releases/download/v2.2.8/CA-O.Setup.exe)
699699
2. Ejecútalo como admin: descarga el paquete `CA-O-Setup-GUI-x64.zip` desde GitHub Releases (requiere internet), deriva al instalador gráfico y continúa solo
700700

701701
> Log: `%TEMP%\CA-O-Setup.log`
@@ -750,7 +750,7 @@ powershell -ExecutionPolicy Bypass -File scripts/build-release.ps1
750750
751751
# Empaquetar (scripts/package.ps1, FullPackageName `CA-O-{versión}-win-x64.zip`)
752752
powershell -ExecutionPolicy Bypass -File scripts/package.ps1
753-
# → artifacts/CA-O-2.2.7-win-x64.zip + .sha256
753+
# → artifacts/CA-O-2.2.8-win-x64.zip + .sha256
754754
755755
# Endurecer ACLs de datos
756756
powershell -ExecutionPolicy Bypass -File scripts/harden-data-acls.ps1
@@ -790,7 +790,7 @@ dotnet test --filter "FullyQualifiedName~GameCompatibility"
790790

791791
## Release y verificación
792792

793-
- **Versión actual:** **2.2.7** — [Releases](https://github.com/Pyromesis/CA-O/releases/tag/v2.2.7) (solo se conserva el último release)
793+
- **Versión actual:** **2.2.8** — [Releases](https://github.com/Pyromesis/CA-O/releases/tag/v2.2.8) (solo se conserva el último release)
794794
- **Artefactos:**
795795
- `CA-O.Setup.exe` (135 MB, descargador single-file con handoff al GUI)
796796
- `CA-O-Setup-GUI-x64.zip` (88 MB, paquete completo offline con `ui/`, `service/`, `gui-installer/`, `setup/`, `uninstall/`)
@@ -801,7 +801,7 @@ dotnet test --filter "FullyQualifiedName~GameCompatibility"
801801
- **Empaquetado:** `scripts/package.ps1` genera ZIP versionado `CA-O-{versión}-win-x64.zip` + hash SHA-256. `scripts/build-release.ps1` publica `ui` y `service` como self-contained y `gui-installer` como self-contained **sin single-file** (requisito WinUI 3)
802802

803803
```powershell
804-
Get-FileHash artifacts/CA-O-2.2.7-win-x64.zip -Algorithm SHA256
804+
Get-FileHash artifacts/CA-O-2.2.8-win-x64.zip -Algorithm SHA256
805805
Get-Content artifacts/release/SHA256SUMS.txt
806806
cat artifacts/sbom/bom.json | ConvertFrom-Json | select -ExpandProperty components | measure
807807
```
@@ -880,4 +880,4 @@ Proyecto privado: no se concede licencia de uso, copia, modificación ni distrib
880880

881881
---
882882

883-
<p align="center"><i>CA-O 2.2.7 — Diagnóstico primero, evidencia después, transacción siempre. Sin promesas, solo hechos medibles.</i></p>
883+
<p align="center"><i>CA-O 2.2.8 — Diagnóstico primero, evidencia después, transacción siempre. Sin promesas, solo hechos medibles.</i></p>

‎Version.props‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
<Project>
22
<!-- Single source of truth for versioning (FASE 28). All other files derive from here. -->
33
<PropertyGroup>
4-
<Version>2.2.7</Version>
5-
<AssemblyVersion>2.2.7.0</AssemblyVersion>
6-
<FileVersion>2.2.7.0</FileVersion>
7-
<PackageVersion>2.2.7</PackageVersion>
8-
<ProductVersion>2.2.7</ProductVersion>
9-
<InformationalVersion>2.2.7+$(GHTag)</InformationalVersion>
4+
<Version>2.2.8</Version>
5+
<AssemblyVersion>2.2.8.0</AssemblyVersion>
6+
<FileVersion>2.2.8.0</FileVersion>
7+
<PackageVersion>2.2.8</PackageVersion>
8+
<ProductVersion>2.2.8</ProductVersion>
9+
<InformationalVersion>2.2.8+$(GHTag)</InformationalVersion>
1010
</PropertyGroup>
1111
</Project>
1212

‎installer/CA-O.iss‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
; (lee la versión de BuildConstants.cs y firma con CAO_SIGN_THUMBPRINT si existe)
44

55
#ifndef AppVersion
6-
#define AppVersion "2.2.7"
6+
#define AppVersion "2.2.8"
77
#endif
88
#ifndef RepoRoot
99
#define RepoRoot ".."

‎scripts/package.ps1‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,19 @@ $stableHash = (Get-FileHash $stableZip -Algorithm SHA256).Hash
5454
"$stableHash $stableName" | Set-Content "$stableZip.sha256"
5555
Write-Host "== Estable: $stableName + .sha256 ==" -ForegroundColor Green
5656

57+
# Retencion: cada release sellaba un zip con timestamp + copia estable sin
58+
# borrar nunca nada (17 GB acumulados hasta la 2.2.7). Se conservan solo los
59+
# ficheros de la version en curso; los antiguos ya estan publicados en
60+
# GitHub Releases y no aportan nada en disco.
61+
$keepVersionPattern = "*$ProductVersion*"
62+
Get-ChildItem "artifacts" -Filter "CA-O-*-win-x64*.zip" -File |
63+
Where-Object { $_.Name -notlike $keepVersionPattern } |
64+
Remove-Item -Force
65+
Get-ChildItem "artifacts" -Filter "CA-O-*-win-x64*.zip.sha256" -File |
66+
Where-Object { $_.Name -notlike $keepVersionPattern } |
67+
Remove-Item -Force
68+
Write-Host "== Retencion: en artifacts solo queda $ProductVersion ==" -ForegroundColor Green
69+
5770
# SBOM estándar (FASE 31): CycloneDX vía dotnet-CycloneDX si está instalada;
5871
# sin la herramienta, se advierte y el release NO debe publicarse sin SBOM.
5972
$hasTool = (dotnet tool list --global | Select-String -Quiet "cyclonedx")

‎src/CA-O.Core/Optimizations/Storage/TempFileCleanupOptimization.cs‎

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ public Task<OperationResult> ApplyAsync(OptimizationContext context, Cancellatio
223223
{
224224
var before = PendingFiles();
225225
var deleted = 0;
226+
var skipped = 0;
226227
long bytes = 0L;
227228
foreach (var path in before)
228229
{
@@ -234,13 +235,15 @@ public Task<OperationResult> ApplyAsync(OptimizationContext context, Cancellatio
234235
deleted++;
235236
bytes += length;
236237
}
237-
catch { /* en uso o sin acceso: se omite y se cuenta lo logrado */ }
238+
catch { skipped++; /* en uso o sin acceso: se omite y se cuenta lo logrado */ }
238239
}
239240

241+
var dirsRemoved = RemoveEmptyDirs();
242+
240243
_lastDeleted = deleted;
241-
return Task.FromResult(deleted > 0
242-
? OperationResult.Ok($"Limpieza completada: {deleted} fichero(s), {FormatSize(bytes)} liberados.")
243-
: OperationResult.Ok("No quedaban ficheros antiguos para limpiar."));
244+
return Task.FromResult(deleted > 0 || dirsRemoved > 0
245+
? OperationResult.Ok($"Limpieza completada: {deleted} fichero(s), {FormatSize(bytes)} liberados, {skipped} omitido(s) (en uso o sin acceso), {dirsRemoved} carpeta(s) vacía(s) eliminadas. Solo se borran ficheros antiguos; los recientes se conservan.")
246+
: OperationResult.Ok("No quedaban ficheros antiguos para limpiar. Solo se borran ficheros antiguos; los recientes y los que están en uso se conservan."));
244247
}
245248

246249
public Task<OperationResult> RevertAsync(OptimizationContext context, OptimizationSnapshot snapshot, CancellationToken ct = default) =>
@@ -272,6 +275,52 @@ public Task<VerificationResult> VerifyAsync(OptimizationContext context, Cancell
272275
"No se pudo eliminar ningún fichero (en uso o sin acceso)."));
273276
}
274277

278+
/// <summary>
279+
/// Borra carpetas vacías bajo los objetivos, de la más profunda a la raíz.
280+
/// Sin filtro de antigüedad: una carpeta vacía en un directorio temporal es
281+
/// un resto por contrato (las apps la recrean al pedirla). Se omiten
282+
/// junctions y puntos de reparse; lo que falle se deja, no se lanza.
283+
/// </summary>
284+
private int RemoveEmptyDirs()
285+
{
286+
var removed = 0;
287+
var seenDirs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
288+
foreach (var (directory, _, _) in Targets)
289+
{
290+
string dir;
291+
try { dir = ExpandDir(directory); }
292+
catch { continue; }
293+
if (!seenDirs.Add(dir)) continue;
294+
List<string> subdirs;
295+
try
296+
{
297+
subdirs = Directory.EnumerateDirectories(dir, "*", new EnumerationOptions
298+
{
299+
RecurseSubdirectories = true,
300+
IgnoreInaccessible = true,
301+
AttributesToSkip = FileAttributes.ReparsePoint,
302+
}).OrderByDescending(d => d.Length).ToList();
303+
}
304+
catch { continue; }
305+
foreach (var sub in subdirs)
306+
{
307+
try
308+
{
309+
var info = new DirectoryInfo(sub);
310+
if (!info.Exists) continue;
311+
if ((info.Attributes & FileAttributes.ReparsePoint) != 0) continue;
312+
if (!info.EnumerateFileSystemInfos().Any())
313+
{
314+
info.Delete();
315+
removed++;
316+
}
317+
}
318+
catch { /* en uso o sin acceso: se deja y se sigue */ }
319+
}
320+
}
321+
return removed;
322+
}
323+
275324
private int? _lastDeleted;
276325

277326
public Task<PreconditionResult> CheckPreconditionsAsync(SystemContext context, CancellationToken ct = default) =>

0 commit comments

Comments
 (0)