Skip to content

Commit 013a372

Browse files
committed
persistent cache IsSSD (TTL 30 hari, validasi drive+serial) + tombol Deteksi Ulang Hardware di System Info
- IsSSD() baca cache JSON dulu; IOCTL/WMI hanya saat cache hilang/kedaluwarsa/drive berubah - ForceRefreshHardwareCache() + RefreshHardwareDetectionCommand + tombol di FastFlagsPage - +2 entry Strings (resx/id.resx/Designer)
1 parent e2e3a4c commit 013a372

6 files changed

Lines changed: 425 additions & 222 deletions

File tree

Bloxstrap/Integrations/AutoOptimizeService.cs

Lines changed: 221 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,126 @@ private static ulong GetTotalPhysicalMemory()
117117
// Ini LEBIH RELIABLE daripada vendor-name heuristic karena langsung nanya ke driver
118118
// storage — HDD selalu punya seek penalty, SSD tidak pernah.
119119
// Tidak perlu admin rights karena pake volume handle (bukan physical drive handle).
120+
//
121+
// ── Persistent cache (v7.3.0) ────────────────────────────────────────────
122+
// Sebelumnya hasil hanya di-cache in-memory (static field) yang reset tiap
123+
// Play karena bootstrapper selalu spawn proses baru → query IOCTL jalan
124+
// ULANG di SETIAP launch padahal hardware laptop tidak berubah.
125+
// Sekarang hasil + timestamp disimpan ke %LocalAppData%\BoneFish\HardwareCache.json.
126+
// IsSSD() memakai cache bila < 30 hari DAN drive sistem tidak berubah
127+
// (root path + volume serial). Tombol "Deteksi Ulang Hardware" di panel
128+
// System Info memanggil ForceRefreshHardwareCache() untuk refresh paksa
129+
// (misal user upgrade HDD → SSD tanpa mau nunggu cache kedaluwarsa).
130+
//
131+
// AUDIT CheckAndApply() untuk kandidat cache serupa: DetectSystemTier()
132+
// (Environment.ProcessorCount + GlobalMemoryStatusEx) dan GetTotalPhysicalMemory()
133+
// adalah syscall kernel super ringan (±µs) — membaca cache dari disk justru
134+
// lebih lambat, jadi TIDAK ikut di-persist. Query IOCTL volume handle
135+
// satu-satunya yang worth di-cache lintas sesi.
136+
private const int HardwareCacheMaxAgeDays = 30;
137+
private static readonly string HardwareCachePath = Path.Combine(Paths.LocalAppData, App.ProjectName, "HardwareCache.json");
138+
139+
public sealed class HardwareCacheEntry
140+
{
141+
public bool IsSSD { get; set; }
142+
public string SystemDriveRoot { get; set; } = "";
143+
public uint VolumeSerial { get; set; }
144+
public DateTime DetectedAtUtc { get; set; }
145+
}
146+
147+
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
148+
private static extern bool GetVolumeInformation(
149+
string lpRootPathName,
150+
StringBuilder? lpVolumeNameBuffer,
151+
int nVolumeNameSize,
152+
out uint lpVolumeSerialNumber,
153+
out uint lpMaximumComponentLength,
154+
out uint lpFileSystemFlags,
155+
StringBuilder? lpFileSystemNameBuffer,
156+
int nFileSystemNameSize);
157+
120158
private static bool? _isSSDCached = null;
121159
private static readonly object _storageLock = new();
122160

161+
private static string GetSystemDriveRoot() =>
162+
Path.GetPathRoot(Environment.SystemDirectory) ?? "C:\\";
163+
164+
private static uint GetSystemVolumeSerial()
165+
{
166+
try
167+
{
168+
var nameBuffer = new StringBuilder(256);
169+
if (GetVolumeInformation(GetSystemDriveRoot(), nameBuffer, nameBuffer.Capacity,
170+
out uint serial, out _, out _, null, 0))
171+
return serial;
172+
}
173+
catch (Exception ex)
174+
{
175+
App.Logger.WriteLine(LOG_IDENT, $"GetVolumeInformation failed: {ex.Message}");
176+
}
177+
return 0;
178+
}
179+
180+
private static bool? TryLoadStorageTypeFromCache()
181+
{
182+
try
183+
{
184+
if (!File.Exists(HardwareCachePath))
185+
return null;
186+
187+
var entry = JsonSerializer.Deserialize<HardwareCacheEntry>(File.ReadAllText(HardwareCachePath));
188+
if (entry is null)
189+
return null;
190+
191+
if ((DateTime.UtcNow - entry.DetectedAtUtc).TotalDays > HardwareCacheMaxAgeDays)
192+
{
193+
App.Logger.WriteLine(LOG_IDENT, $"Persistent storage cache expired (>{HardwareCacheMaxAgeDays} days) — re-detecting");
194+
return null;
195+
}
196+
197+
if (!string.Equals(entry.SystemDriveRoot, GetSystemDriveRoot(), StringComparison.OrdinalIgnoreCase))
198+
{
199+
App.Logger.WriteLine(LOG_IDENT, "System drive root changed since cache write — re-detecting");
200+
return null;
201+
}
202+
203+
uint currentSerial = GetSystemVolumeSerial();
204+
if (currentSerial != 0 && entry.VolumeSerial != 0 && currentSerial != entry.VolumeSerial)
205+
{
206+
App.Logger.WriteLine(LOG_IDENT, "System volume serial changed since cache write — re-detecting");
207+
return null;
208+
}
209+
210+
return entry.IsSSD;
211+
}
212+
catch (Exception ex)
213+
{
214+
App.Logger.WriteLine(LOG_IDENT, $"Could not read persistent storage cache: {ex.Message}");
215+
return null;
216+
}
217+
}
218+
219+
private static void SaveStorageTypeToCache(bool isSSD)
220+
{
221+
try
222+
{
223+
var entry = new HardwareCacheEntry
224+
{
225+
IsSSD = isSSD,
226+
SystemDriveRoot = GetSystemDriveRoot(),
227+
VolumeSerial = GetSystemVolumeSerial(),
228+
DetectedAtUtc = DateTime.UtcNow
229+
};
230+
231+
Directory.CreateDirectory(Path.GetDirectoryName(HardwareCachePath)!);
232+
File.WriteAllText(HardwareCachePath, JsonSerializer.Serialize(entry, new JsonSerializerOptions { WriteIndented = true }));
233+
}
234+
catch (Exception ex)
235+
{
236+
App.Logger.WriteLine(LOG_IDENT, $"Could not save persistent storage cache: {ex.Message}");
237+
}
238+
}
239+
123240
private static bool IsSSD()
124241
{
125242
if (_isSSDCached.HasValue)
@@ -130,98 +247,130 @@ private static bool IsSSD()
130247
if (_isSSDCached.HasValue)
131248
return _isSSDCached.Value;
132249

133-
try
250+
bool? cachedSSD = TryLoadStorageTypeFromCache();
251+
if (cachedSSD.HasValue)
134252
{
135-
// Buka handle ke system volume ("\\.\C:")
136-
string systemDrive = Path.GetPathRoot(Environment.SystemDirectory) ?? "C:\\";
137-
string volumePath = @"\\.\" + systemDrive.TrimEnd('\\');
138-
139-
IntPtr hVolume = CreateFile(
140-
volumePath,
141-
GENERIC_READ,
142-
FILE_SHARE_READ | FILE_SHARE_WRITE,
143-
IntPtr.Zero,
144-
OPEN_EXISTING,
145-
FILE_ATTRIBUTE_NORMAL,
146-
IntPtr.Zero);
147-
148-
if (hVolume == INVALID_HANDLE_VALUE)
149-
{
150-
App.Logger.WriteLine(LOG_IDENT, $"Could not open volume {volumePath} for seek penalty query. Assuming HDD.");
151-
_isSSDCached = false;
152-
return false;
153-
}
253+
_isSSDCached = cachedSSD.Value;
254+
App.Logger.WriteLine(LOG_IDENT, $"Storage type from persistent cache: {(cachedSSD.Value ? "SSD" : "HDD")} (IOCTL skipped)");
255+
return cachedSSD.Value;
256+
}
154257

155-
try
258+
var stopwatch = Stopwatch.StartNew();
259+
bool isSSD = DetectStorageType();
260+
stopwatch.Stop();
261+
262+
_isSSDCached = isSSD;
263+
SaveStorageTypeToCache(isSSD);
264+
App.Logger.WriteLine(LOG_IDENT, $"Storage detection via IOCTL finished in {stopwatch.ElapsedMilliseconds} ms — persistent cache updated");
265+
266+
return isSSD;
267+
}
268+
}
269+
270+
public static void ForceRefreshHardwareCache()
271+
{
272+
lock (_storageLock)
273+
{
274+
_isSSDCached = null;
275+
try { File.Delete(HardwareCachePath); } catch { }
276+
}
277+
278+
bool isSSD = IsSSD();
279+
App.Logger.WriteLine(LOG_IDENT, $"Hardware detection manually refreshed: {(isSSD ? "SSD" : "HDD")} (persistent cache re-written)");
280+
}
281+
282+
// Query seek-penalty IOCTL — logika deteksi ASLI (tidak diubah), hanya
283+
// dipisah dari caching supaya IsSSD() bisa memotong query ini saat
284+
// persistent cache masih valid.
285+
private static bool DetectStorageType()
286+
{
287+
try
288+
{
289+
// Buka handle ke system volume ("\\.\C:")
290+
string systemDrive = Path.GetPathRoot(Environment.SystemDirectory) ?? "C:\\";
291+
string volumePath = @"\\.\" + systemDrive.TrimEnd('\\');
292+
293+
IntPtr hVolume = CreateFile(
294+
volumePath,
295+
GENERIC_READ,
296+
FILE_SHARE_READ | FILE_SHARE_WRITE,
297+
IntPtr.Zero,
298+
OPEN_EXISTING,
299+
FILE_ATTRIBUTE_NORMAL,
300+
IntPtr.Zero);
301+
302+
if (hVolume == INVALID_HANDLE_VALUE)
303+
{
304+
App.Logger.WriteLine(LOG_IDENT, $"Could not open volume {volumePath} for seek penalty query. Assuming HDD.");
305+
return false;
306+
}
307+
308+
try
309+
{
310+
// Prepare query struct
311+
var query = new STORAGE_PROPERTY_QUERY
156312
{
157-
// Prepare query struct
158-
var query = new STORAGE_PROPERTY_QUERY
159-
{
160-
PropertyId = StorageDeviceSeekPenaltyProperty,
161-
QueryType = 0 // PropertyStandardQuery
162-
};
313+
PropertyId = StorageDeviceSeekPenaltyProperty,
314+
QueryType = 0 // PropertyStandardQuery
315+
};
163316

164-
int querySize = Marshal.SizeOf(typeof(STORAGE_PROPERTY_QUERY));
165-
int descSize = Marshal.SizeOf(typeof(STORAGE_SEEK_PENALTY_DESCRIPTOR));
317+
int querySize = Marshal.SizeOf(typeof(STORAGE_PROPERTY_QUERY));
318+
int descSize = Marshal.SizeOf(typeof(STORAGE_SEEK_PENALTY_DESCRIPTOR));
166319

167-
IntPtr queryPtr = Marshal.AllocHGlobal(querySize);
168-
IntPtr descPtr = Marshal.AllocHGlobal(descSize);
320+
IntPtr queryPtr = Marshal.AllocHGlobal(querySize);
321+
IntPtr descPtr = Marshal.AllocHGlobal(descSize);
169322

170-
try
171-
{
172-
Marshal.StructureToPtr(query, queryPtr, false);
173-
// Zero-initialize descriptor memory
323+
try
324+
{
325+
Marshal.StructureToPtr(query, queryPtr, false);
326+
// Zero-initialize descriptor memory
174327
var zeroBytes = new byte[descSize];
175328
Marshal.Copy(zeroBytes, 0, descPtr, descSize);
176329

177-
bool success = DeviceIoControl(
178-
hVolume,
179-
IOCTL_STORAGE_QUERY_PROPERTY,
180-
queryPtr,
181-
(uint)querySize,
182-
descPtr,
183-
(uint)descSize,
184-
out uint bytesReturned,
185-
IntPtr.Zero);
186-
187-
if (success && bytesReturned >= (uint)descSize)
188-
{
189-
var descriptor = Marshal.PtrToStructure<STORAGE_SEEK_PENALTY_DESCRIPTOR>(descPtr);
190-
bool isSSD = descriptor.IncursSeekPenalty == 0; // false = no seek penalty = SSD
191-
_isSSDCached = isSSD;
192-
193-
App.Logger.WriteLine(LOG_IDENT,
194-
$"Storage detected via DeviceIoControl: {(isSSD ? "SSD" : "HDD")} " +
195-
$"(IncursSeekPenalty={descriptor.IncursSeekPenalty})");
196-
return isSSD;
197-
}
198-
else
199-
{
200-
int lastError = Marshal.GetLastWin32Error();
201-
App.Logger.WriteLine(LOG_IDENT,
202-
$"DeviceIoControl seek penalty query failed (error={lastError}). Assuming HDD.");
203-
_isSSDCached = false;
204-
return false;
205-
}
330+
bool success = DeviceIoControl(
331+
hVolume,
332+
IOCTL_STORAGE_QUERY_PROPERTY,
333+
queryPtr,
334+
(uint)querySize,
335+
descPtr,
336+
(uint)descSize,
337+
out uint bytesReturned,
338+
IntPtr.Zero);
339+
340+
if (success && bytesReturned >= (uint)descSize)
341+
{
342+
var descriptor = Marshal.PtrToStructure<STORAGE_SEEK_PENALTY_DESCRIPTOR>(descPtr);
343+
bool isSSD = descriptor.IncursSeekPenalty == 0; // false = no seek penalty = SSD
344+
345+
App.Logger.WriteLine(LOG_IDENT,
346+
$"Storage detected via DeviceIoControl: {(isSSD ? "SSD" : "HDD")} " +
347+
$"(IncursSeekPenalty={descriptor.IncursSeekPenalty})");
348+
return isSSD;
206349
}
207-
finally
350+
else
208351
{
209-
Marshal.FreeHGlobal(queryPtr);
210-
Marshal.FreeHGlobal(descPtr);
352+
int lastError = Marshal.GetLastWin32Error();
353+
App.Logger.WriteLine(LOG_IDENT,
354+
$"DeviceIoControl seek penalty query failed (error={lastError}). Assuming HDD.");
355+
return false;
211356
}
212357
}
213358
finally
214359
{
215-
CloseHandle(hVolume);
360+
Marshal.FreeHGlobal(queryPtr);
361+
Marshal.FreeHGlobal(descPtr);
216362
}
217363
}
218-
catch (Exception ex)
364+
finally
219365
{
220-
App.Logger.WriteLine(LOG_IDENT, $"Storage detection via DeviceIoControl failed: {ex.Message}. Assuming HDD.");
221-
_isSSDCached = false;
222-
return false;
366+
CloseHandle(hVolume);
223367
}
224368
}
369+
catch (Exception ex)
370+
{
371+
App.Logger.WriteLine(LOG_IDENT, $"Storage detection via DeviceIoControl failed: {ex.Message}. Assuming HDD.");
372+
return false;
373+
}
225374
}
226375

227376
private static SystemTier DetectSystemTier(bool ignoreForceExtreme = false)

Bloxstrap/Resources/Strings.Designer.cs

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)