Files
LightlessClient/LightlessSync/FileCache/CacheMonitor.cs
defnotken 7672f147f5
Some checks failed
Tag and Release Lightless / tag-and-release (push) Failing after 32s
1.12.0 - LightFinder + Bugfixes + Other fixes from 1.11.X (#39)
![image.png](/attachments/c984e826-254e-4ce3-af92-4e795ea717ab)

Lightless 1.12.0 is HERE! In this major update, we are introducing something we've been working on and testing for the last couple of weeks. In this update we are introducing a new (**OPTIONAL**) feature called **LightFinder**! We took inspiration from FFXIV's very own Party Finder and decided to implement something that allows users to not only look for fellow Lightless users, but also put up their Syncshell for others looking to join a sync community!

When you enable LightFinder, you will be visible to other LightFinder users for **3 hours** or when you want to disabled it. When the 3 hours are up, you can either leave it disabled or enable it again for another 3 hours. The tag shown above will show above your nameplate, and you will be able to receive pair requests in your UI from other users with LightFinder enabled without having to input their uid!

![image.png](/attachments/91a7ce60-7771-49d7-bae8-6d7a67e46fa3)

Are you at a Venue? In Limsa? Partying in the streets of Uldah? If you're looking for fellow Lightless users you can now enable LightFinder and you will be shown to others who also have LightFinder enabled!

Looking for a Syncshell to join? Enable LightFinder to see what SyncShells are available to join!

Want to advertise your Syncshell? Choose the syncshell you want to put up in LightFinder and enable LightFinder.

**IMPORTANT: We want to stress the fact that, if you just want to sync with just your friends and no one else, this does not take that away. No one will know you use Lightless unless you choose to tell them, or use this **OPTIONAL** feature. Your privacy is still maintained if you don't want to use the feature.**

# Major Changes

## LightFinder - **OPTIONAL FEATURE** - **DOES NOT AFFECT YOU IF YOU DON'T WANT TO USE IT**

![image.png](/attachments/9a1e7455-f9b3-4e3a-9ec5-5bc74ed43c26)

* New **OPTIONAL** syncing feature where one can enable something like a Party Finder so that other Lightless users in the area can see you are looking for people to sync with. Enable it by clicking the compass button and then the `Enable LightFinder` button.

![image.png](/attachments/7be5c1fc-439f-43fc-bb1e-8ae32ab5e52e)

* [L] Send Pair Request has been added to player context menus. You should still be able to send a request without Lightfinder on BUT you will need to know the other player is using Lightless and have them send a pair request back.

![image.png](/attachments/75e0d600-baf9-43d5-af37-e86b6a1e209b)

* When in LightFinder mode, for X mins you will be visible to all Lightless Users WHO ALSO HAVE LIGHTFINDER ON and will receive notifications of people wanting to pair with you. If you are the person using LightFinder, you understand the risks of pairing with someone you don't know. If you are the person sending a request to someone with LightFinder on, you also understand the risks of pairing with someone you don't know. **AGAIN, THIS IS OPTIONAL.**

* When in LightFinder mode, you can also put up a Syncshell you own on the Syncshell Finder so that others can easily find it and join. This has to be done prior to enabling LightFinder.

![image.png](/attachments/1929d8e0-778d-4ced-9bf0-f350887980db)

* Syncshell Finder allows you to join Syncshells that are indexed by LightFinder

![image.png](/attachments/fef7b786-b1a0-4967-a4a9-886cdddc1a61)

# Minor Changes

* Vanity addition to our supporters: On top of vanity ids you can find a fun addition under Your User Settings -> Edit Lightless Profile -> Vanity Settings to change the colour of your name in the lightless ui. This will be shown to all users.
![image.png](/attachments/9b950c07-6b27-4bd1-a0d4-5346396b23e2)

* Pairing nameplate colour override can also now override FC tag (new option_

* Bunch of UI fixes, updates, and changes

* kdb is now a whitelisted filetype that can upload

Co-authored-by: CakeAndBanana <admin@cakeandbanana.nl>
Co-authored-by: azyges <aaaaaa@aaa.aaa>
Co-authored-by: choco <thijmenhogenkamp@gmail.com>
Co-authored-by: choco <choco@noreply.git.lightless-sync.org>
Co-authored-by: defnotken <itsdefnotken@gmail.com>
Reviewed-on: #39
2025-10-05 07:48:32 +02:00

701 lines
26 KiB
C#

using LightlessSync.Interop.Ipc;
using LightlessSync.LightlessConfiguration;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Collections.Immutable;
namespace LightlessSync.FileCache;
public sealed class CacheMonitor : DisposableMediatorSubscriberBase
{
private readonly LightlessConfigService _configService;
private readonly DalamudUtilService _dalamudUtil;
private readonly FileCompactor _fileCompactor;
private readonly FileCacheManager _fileDbManager;
private readonly IpcManager _ipcManager;
private readonly PerformanceCollectorService _performanceCollector;
private long _currentFileProgress = 0;
private CancellationTokenSource _scanCancellationTokenSource = new();
private readonly CancellationTokenSource _periodicCalculationTokenSource = new();
public static readonly IImmutableList<string> AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk", ".kdb"];
public CacheMonitor(ILogger<CacheMonitor> logger, IpcManager ipcManager, LightlessConfigService configService,
FileCacheManager fileDbManager, LightlessMediator mediator, PerformanceCollectorService performanceCollector, DalamudUtilService dalamudUtil,
FileCompactor fileCompactor) : base(logger, mediator)
{
_ipcManager = ipcManager;
_configService = configService;
_fileDbManager = fileDbManager;
_performanceCollector = performanceCollector;
_dalamudUtil = dalamudUtil;
_fileCompactor = fileCompactor;
Mediator.Subscribe<PenumbraInitializedMessage>(this, (_) =>
{
StartPenumbraWatcher(_ipcManager.Penumbra.ModDirectory);
StartLightlessWatcher(configService.Current.CacheFolder);
InvokeScan();
});
Mediator.Subscribe<HaltScanMessage>(this, (msg) => HaltScan(msg.Source));
Mediator.Subscribe<ResumeScanMessage>(this, (msg) => ResumeScan(msg.Source));
Mediator.Subscribe<DalamudLoginMessage>(this, (_) =>
{
StartLightlessWatcher(configService.Current.CacheFolder);
StartPenumbraWatcher(_ipcManager.Penumbra.ModDirectory);
InvokeScan();
});
Mediator.Subscribe<PenumbraDirectoryChangedMessage>(this, (msg) =>
{
StartPenumbraWatcher(msg.ModDirectory);
InvokeScan();
});
if (_ipcManager.Penumbra.APIAvailable && !string.IsNullOrEmpty(_ipcManager.Penumbra.ModDirectory))
{
StartPenumbraWatcher(_ipcManager.Penumbra.ModDirectory);
}
if (configService.Current.HasValidSetup())
{
StartLightlessWatcher(configService.Current.CacheFolder);
InvokeScan();
}
var token = _periodicCalculationTokenSource.Token;
_ = Task.Run(async () =>
{
Logger.LogInformation("Starting Periodic Storage Directory Calculation Task");
var token = _periodicCalculationTokenSource.Token;
while (!token.IsCancellationRequested)
{
try
{
while (_dalamudUtil.IsOnFrameworkThread && !token.IsCancellationRequested)
{
await Task.Delay(1).ConfigureAwait(false);
}
RecalculateFileCacheSize(token);
}
catch
{
// ignore
}
await Task.Delay(TimeSpan.FromMinutes(1), token).ConfigureAwait(false);
}
}, token);
}
public long CurrentFileProgress => _currentFileProgress;
public long FileCacheSize { get; set; }
public long FileCacheDriveFree { get; set; }
public ConcurrentDictionary<string, int> HaltScanLocks { get; set; } = new(StringComparer.Ordinal);
public bool IsScanRunning => CurrentFileProgress > 0 || TotalFiles > 0;
public long TotalFiles { get; private set; }
public long TotalFilesStorage { get; private set; }
public void HaltScan(string source)
{
if (!HaltScanLocks.ContainsKey(source)) HaltScanLocks[source] = 0;
HaltScanLocks[source]++;
}
record WatcherChange(WatcherChangeTypes ChangeType, string? OldPath = null);
private readonly Dictionary<string, WatcherChange> _watcherChanges = new Dictionary<string, WatcherChange>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, WatcherChange> _lightlessChanges = new Dictionary<string, WatcherChange>(StringComparer.OrdinalIgnoreCase);
public void StopMonitoring()
{
Logger.LogInformation("Stopping monitoring of Penumbra and Lightless storage folders");
LightlessWatcher?.Dispose();
PenumbraWatcher?.Dispose();
LightlessWatcher = null;
PenumbraWatcher = null;
}
public bool StorageisNTFS { get; private set; } = false;
public void StartLightlessWatcher(string? lightlessPath)
{
LightlessWatcher?.Dispose();
if (string.IsNullOrEmpty(lightlessPath) || !Directory.Exists(lightlessPath))
{
LightlessWatcher = null;
Logger.LogWarning("Lightless file path is not set, cannot start the FSW for Lightless.");
return;
}
DriveInfo di = new(new DirectoryInfo(_configService.Current.CacheFolder).Root.FullName);
StorageisNTFS = string.Equals("NTFS", di.DriveFormat, StringComparison.OrdinalIgnoreCase);
Logger.LogInformation("Lightless Storage is on NTFS drive: {isNtfs}", StorageisNTFS);
Logger.LogDebug("Initializing Lightless FSW on {path}", lightlessPath);
LightlessWatcher = new()
{
Path = lightlessPath,
InternalBufferSize = 8388608,
NotifyFilter = NotifyFilters.CreationTime
| NotifyFilters.LastWrite
| NotifyFilters.FileName
| NotifyFilters.DirectoryName
| NotifyFilters.Size,
Filter = "*.*",
IncludeSubdirectories = false,
};
LightlessWatcher.Deleted += LightlessWatcher_FileChanged;
LightlessWatcher.Created += LightlessWatcher_FileChanged;
LightlessWatcher.EnableRaisingEvents = true;
}
private void LightlessWatcher_FileChanged(object sender, FileSystemEventArgs e)
{
Logger.LogTrace("Lightless FSW: FileChanged: {change} => {path}", e.ChangeType, e.FullPath);
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
lock (_watcherChanges)
{
_lightlessChanges[e.FullPath] = new(e.ChangeType);
}
_ = LightlessWatcherExecution();
}
public void StartPenumbraWatcher(string? penumbraPath)
{
PenumbraWatcher?.Dispose();
if (string.IsNullOrEmpty(penumbraPath))
{
PenumbraWatcher = null;
Logger.LogWarning("Penumbra is not connected or the path is not set, cannot start FSW for Penumbra.");
return;
}
Logger.LogDebug("Initializing Penumbra FSW on {path}", penumbraPath);
PenumbraWatcher = new()
{
Path = penumbraPath,
InternalBufferSize = 8388608,
NotifyFilter = NotifyFilters.CreationTime
| NotifyFilters.LastWrite
| NotifyFilters.FileName
| NotifyFilters.DirectoryName
| NotifyFilters.Size,
Filter = "*.*",
IncludeSubdirectories = true
};
PenumbraWatcher.Deleted += Fs_Changed;
PenumbraWatcher.Created += Fs_Changed;
PenumbraWatcher.Changed += Fs_Changed;
PenumbraWatcher.Renamed += Fs_Renamed;
PenumbraWatcher.EnableRaisingEvents = true;
}
private void Fs_Changed(object sender, FileSystemEventArgs e)
{
if (Directory.Exists(e.FullPath)) return;
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
if (e.ChangeType is not (WatcherChangeTypes.Changed or WatcherChangeTypes.Deleted or WatcherChangeTypes.Created))
return;
lock (_watcherChanges)
{
_watcherChanges[e.FullPath] = new(e.ChangeType);
}
Logger.LogTrace("FSW {event}: {path}", e.ChangeType, e.FullPath);
_ = PenumbraWatcherExecution();
}
private void Fs_Renamed(object sender, RenamedEventArgs e)
{
if (Directory.Exists(e.FullPath))
{
var directoryFiles = Directory.GetFiles(e.FullPath, "*.*", SearchOption.AllDirectories);
lock (_watcherChanges)
{
foreach (var file in directoryFiles)
{
if (!AllowedFileExtensions.Any(ext => file.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) continue;
var oldPath = file.Replace(e.FullPath, e.OldFullPath, StringComparison.OrdinalIgnoreCase);
_watcherChanges.Remove(oldPath);
_watcherChanges[file] = new(WatcherChangeTypes.Renamed, oldPath);
Logger.LogTrace("FSW Renamed: {path} -> {new}", oldPath, file);
}
}
}
else
{
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
lock (_watcherChanges)
{
_watcherChanges.Remove(e.OldFullPath);
_watcherChanges[e.FullPath] = new(WatcherChangeTypes.Renamed, e.OldFullPath);
}
Logger.LogTrace("FSW Renamed: {path} -> {new}", e.OldFullPath, e.FullPath);
}
_ = PenumbraWatcherExecution();
}
private CancellationTokenSource _penumbraFswCts = new();
private CancellationTokenSource _lightlessFswCts = new();
public FileSystemWatcher? PenumbraWatcher { get; private set; }
public FileSystemWatcher? LightlessWatcher { get; private set; }
private async Task LightlessWatcherExecution()
{
_lightlessFswCts = _lightlessFswCts.CancelRecreate();
var token = _lightlessFswCts.Token;
var delay = TimeSpan.FromSeconds(5);
Dictionary<string, WatcherChange> changes;
lock (_lightlessChanges)
changes = _lightlessChanges.ToDictionary(t => t.Key, t => t.Value, StringComparer.Ordinal);
try
{
do
{
await Task.Delay(delay, token).ConfigureAwait(false);
} while (HaltScanLocks.Any(f => f.Value > 0));
}
catch (TaskCanceledException)
{
return;
}
lock (_lightlessChanges)
{
foreach (var key in changes.Keys)
{
_lightlessChanges.Remove(key);
}
}
HandleChanges(changes);
}
private void HandleChanges(Dictionary<string, WatcherChange> changes)
{
lock (_fileDbManager)
{
var deletedEntries = changes.Where(c => c.Value.ChangeType == WatcherChangeTypes.Deleted).Select(c => c.Key);
var renamedEntries = changes.Where(c => c.Value.ChangeType == WatcherChangeTypes.Renamed);
var remainingEntries = changes.Where(c => c.Value.ChangeType != WatcherChangeTypes.Deleted).Select(c => c.Key);
foreach (var entry in deletedEntries)
{
Logger.LogDebug("FSW Change: Deletion - {val}", entry);
}
foreach (var entry in renamedEntries)
{
Logger.LogDebug("FSW Change: Renamed - {oldVal} => {val}", entry.Value.OldPath, entry.Key);
}
foreach (var entry in remainingEntries)
{
Logger.LogDebug("FSW Change: Creation or Change - {val}", entry);
}
var allChanges = deletedEntries
.Concat(renamedEntries.Select(c => c.Value.OldPath!))
.Concat(renamedEntries.Select(c => c.Key))
.Concat(remainingEntries)
.ToArray();
_ = _fileDbManager.GetFileCachesByPaths(allChanges);
_fileDbManager.WriteOutFullCsv();
}
}
private async Task PenumbraWatcherExecution()
{
_penumbraFswCts = _penumbraFswCts.CancelRecreate();
var token = _penumbraFswCts.Token;
Dictionary<string, WatcherChange> changes;
lock (_watcherChanges)
changes = _watcherChanges.ToDictionary(t => t.Key, t => t.Value, StringComparer.Ordinal);
var delay = TimeSpan.FromSeconds(10);
try
{
do
{
await Task.Delay(delay, token).ConfigureAwait(false);
} while (HaltScanLocks.Any(f => f.Value > 0));
}
catch (TaskCanceledException)
{
return;
}
lock (_watcherChanges)
{
foreach (var key in changes.Keys)
{
_watcherChanges.Remove(key);
}
}
HandleChanges(changes);
}
public void InvokeScan()
{
TotalFiles = 0;
_currentFileProgress = 0;
_scanCancellationTokenSource = _scanCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource();
var token = _scanCancellationTokenSource.Token;
_ = Task.Run(async () =>
{
Logger.LogDebug("Starting Full File Scan");
TotalFiles = 0;
_currentFileProgress = 0;
while (_dalamudUtil.IsOnFrameworkThread)
{
Logger.LogWarning("Scanner is on framework, waiting for leaving thread before continuing");
await Task.Delay(250, token).ConfigureAwait(false);
}
Thread scanThread = new(() =>
{
try
{
_performanceCollector.LogPerformance(this, $"FullFileScan", () => FullFileScan(token));
}
catch (Exception ex)
{
Logger.LogError(ex, "Error during Full File Scan");
}
})
{
Priority = ThreadPriority.Lowest,
IsBackground = true
};
scanThread.Start();
while (scanThread.IsAlive)
{
await Task.Delay(250, token).ConfigureAwait(false);
}
TotalFiles = 0;
_currentFileProgress = 0;
}, token);
}
public void RecalculateFileCacheSize(CancellationToken token)
{
if (string.IsNullOrEmpty(_configService.Current.CacheFolder) || !Directory.Exists(_configService.Current.CacheFolder))
{
FileCacheSize = 0;
return;
}
FileCacheSize = -1;
DriveInfo di = new(new DirectoryInfo(_configService.Current.CacheFolder).Root.FullName);
try
{
FileCacheDriveFree = di.AvailableFreeSpace;
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Could not determine drive size for Storage Folder {folder}", _configService.Current.CacheFolder);
}
var files = Directory.EnumerateFiles(_configService.Current.CacheFolder).Select(f => new FileInfo(f))
.OrderBy(f => f.LastAccessTime).ToList();
FileCacheSize = files
.Sum(f =>
{
token.ThrowIfCancellationRequested();
try
{
return _fileCompactor.GetFileSizeOnDisk(f, StorageisNTFS);
}
catch
{
return 0;
}
});
var maxCacheInBytes = (long)(_configService.Current.MaxLocalCacheInGiB * 1024d * 1024d * 1024d);
if (FileCacheSize < maxCacheInBytes) return;
var maxCacheBuffer = maxCacheInBytes * 0.05d;
while (FileCacheSize > maxCacheInBytes - (long)maxCacheBuffer)
{
var oldestFile = files[0];
FileCacheSize -= _fileCompactor.GetFileSizeOnDisk(oldestFile);
File.Delete(oldestFile.FullName);
files.Remove(oldestFile);
}
}
public void ResetLocks()
{
HaltScanLocks.Clear();
}
public void ResumeScan(string source)
{
if (!HaltScanLocks.ContainsKey(source)) HaltScanLocks[source] = 0;
HaltScanLocks[source]--;
if (HaltScanLocks[source] < 0) HaltScanLocks[source] = 0;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_scanCancellationTokenSource?.Cancel();
PenumbraWatcher?.Dispose();
LightlessWatcher?.Dispose();
_penumbraFswCts?.CancelDispose();
_lightlessFswCts?.CancelDispose();
_periodicCalculationTokenSource?.CancelDispose();
}
private void FullFileScan(CancellationToken ct)
{
TotalFiles = 1;
var penumbraDir = _ipcManager.Penumbra.ModDirectory;
bool penDirExists = true;
bool cacheDirExists = true;
if (string.IsNullOrEmpty(penumbraDir) || !Directory.Exists(penumbraDir))
{
penDirExists = false;
Logger.LogWarning("Penumbra directory is not set or does not exist.");
}
if (string.IsNullOrEmpty(_configService.Current.CacheFolder) || !Directory.Exists(_configService.Current.CacheFolder))
{
cacheDirExists = false;
Logger.LogWarning("Lightless Cache directory is not set or does not exist.");
}
if (!penDirExists || !cacheDirExists)
{
return;
}
var previousThreadPriority = Thread.CurrentThread.Priority;
Thread.CurrentThread.Priority = ThreadPriority.Lowest;
Logger.LogDebug("Getting files from {penumbra} and {storage}", penumbraDir, _configService.Current.CacheFolder);
Dictionary<string, string[]> penumbraFiles = new(StringComparer.Ordinal);
foreach (var folder in Directory.EnumerateDirectories(penumbraDir!))
{
try
{
penumbraFiles[folder] =
[
.. Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories)
.AsParallel()
.Where(f => AllowedFileExtensions.Any(e => f.EndsWith(e, StringComparison.OrdinalIgnoreCase))
&& !f.Contains(@"\bg\", StringComparison.OrdinalIgnoreCase)
&& !f.Contains(@"\bgcommon\", StringComparison.OrdinalIgnoreCase)
&& !f.Contains(@"\ui\", StringComparison.OrdinalIgnoreCase)),
];
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Could not enumerate path {path}", folder);
}
Thread.Sleep(50);
if (ct.IsCancellationRequested) return;
}
var allCacheFiles = Directory.GetFiles(_configService.Current.CacheFolder, "*.*", SearchOption.TopDirectoryOnly)
.AsParallel()
.Where(f =>
{
var val = f.Split('\\')[^1];
return val.Length == 40 || (val.Split('.').FirstOrDefault()?.Length ?? 0) == 40;
});
if (ct.IsCancellationRequested) return;
var allScannedFiles = (penumbraFiles.SelectMany(k => k.Value))
.Concat(allCacheFiles)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToDictionary(t => t.ToLowerInvariant(), t => false, StringComparer.OrdinalIgnoreCase);
TotalFiles = allScannedFiles.Count;
Thread.CurrentThread.Priority = previousThreadPriority;
Thread.Sleep(TimeSpan.FromSeconds(2));
if (ct.IsCancellationRequested) return;
// scan files from database
var threadCount = Math.Clamp((int)(Environment.ProcessorCount / 2.0f), 2, 8);
List<FileCacheEntity> entitiesToRemove = [];
List<FileCacheEntity> entitiesToUpdate = [];
object sync = new();
Thread[] workerThreads = new Thread[threadCount];
ConcurrentQueue<FileCacheEntity> fileCaches = new(_fileDbManager.GetAllFileCaches());
TotalFilesStorage = fileCaches.Count;
for (int i = 0; i < threadCount; i++)
{
Logger.LogTrace("Creating Thread {i}", i);
workerThreads[i] = new((tcounter) =>
{
var threadNr = (int)tcounter!;
Logger.LogTrace("Spawning Worker Thread {i}", threadNr);
while (!ct.IsCancellationRequested && fileCaches.TryDequeue(out var workload))
{
try
{
if (ct.IsCancellationRequested) return;
if (!_ipcManager.Penumbra.APIAvailable)
{
Logger.LogWarning("Penumbra not available");
return;
}
var validatedCacheResult = _fileDbManager.ValidateFileCacheEntity(workload);
if (validatedCacheResult.State != FileState.RequireDeletion)
{
lock (sync) { allScannedFiles[validatedCacheResult.FileCache.ResolvedFilepath] = true; }
}
if (validatedCacheResult.State == FileState.RequireUpdate)
{
Logger.LogTrace("To update: {path}", validatedCacheResult.FileCache.ResolvedFilepath);
lock (sync) { entitiesToUpdate.Add(validatedCacheResult.FileCache); }
}
else if (validatedCacheResult.State == FileState.RequireDeletion)
{
Logger.LogTrace("To delete: {path}", validatedCacheResult.FileCache.ResolvedFilepath);
lock (sync) { entitiesToRemove.Add(validatedCacheResult.FileCache); }
}
}
catch (Exception ex)
{
if (workload != null)
{
Logger.LogWarning(ex, "Failed validating {path}", workload.ResolvedFilepath);
}
else
{
Logger.LogWarning(ex, "Failed validating unknown workload");
}
}
Interlocked.Increment(ref _currentFileProgress);
}
Logger.LogTrace("Ending Worker Thread {i}", threadNr);
})
{
Priority = ThreadPriority.Lowest,
IsBackground = true
};
workerThreads[i].Start(i);
}
while (!ct.IsCancellationRequested && workerThreads.Any(u => u.IsAlive))
{
Thread.Sleep(1000);
}
if (ct.IsCancellationRequested) return;
Logger.LogTrace("Threads exited");
if (!_ipcManager.Penumbra.APIAvailable)
{
Logger.LogWarning("Penumbra not available");
return;
}
if (entitiesToUpdate.Count != 0 || entitiesToRemove.Count != 0)
{
foreach (var entity in entitiesToUpdate)
{
_fileDbManager.UpdateHashedFile(entity);
}
foreach (var entity in entitiesToRemove)
{
_fileDbManager.RemoveHashedFile(entity.Hash, entity.PrefixedFilePath);
}
_fileDbManager.WriteOutFullCsv();
}
Logger.LogTrace("Scanner validated existing db files");
if (!_ipcManager.Penumbra.APIAvailable)
{
Logger.LogWarning("Penumbra not available");
return;
}
if (ct.IsCancellationRequested) return;
// scan new files
if (allScannedFiles.Any(c => !c.Value))
{
Parallel.ForEach(allScannedFiles.Where(c => !c.Value).Select(c => c.Key),
new ParallelOptions()
{
MaxDegreeOfParallelism = threadCount,
CancellationToken = ct
}, (cachePath) =>
{
if (_fileDbManager == null || _ipcManager?.Penumbra == null || cachePath == null)
{
Logger.LogTrace("Potential null in db: {isDbNull} penumbra: {isPenumbraNull} cachepath: {isPathNull}", _fileDbManager == null, _ipcManager?.Penumbra == null, cachePath == null);
return;
}
if (ct.IsCancellationRequested) return;
if (!_ipcManager.Penumbra.APIAvailable)
{
Logger.LogWarning("Penumbra not available");
return;
}
try
{
var entry = _fileDbManager.CreateFileEntry(cachePath);
if (entry == null) _ = _fileDbManager.CreateCacheEntry(cachePath);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed adding {file}", cachePath);
}
Interlocked.Increment(ref _currentFileProgress);
});
Logger.LogTrace("Scanner added {notScanned} new files to db", allScannedFiles.Count(c => !c.Value));
}
Logger.LogDebug("Scan complete");
TotalFiles = 0;
_currentFileProgress = 0;
entitiesToRemove.Clear();
allScannedFiles.Clear();
if (!_configService.Current.InitialScanComplete)
{
_configService.Current.InitialScanComplete = true;
_configService.Save();
StartLightlessWatcher(_configService.Current.CacheFolder);
StartPenumbraWatcher(penumbraDir);
}
}
}