Cleanup FirmwareManager
This commit is contained in:
parent
2f18c74840
commit
dc3bd050da
|
@ -1,4 +1,6 @@
|
||||||
using System;
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
@ -9,67 +11,73 @@ using BizHawk.Emulation.Common;
|
||||||
|
|
||||||
namespace BizHawk.Client.Common
|
namespace BizHawk.Client.Common
|
||||||
{
|
{
|
||||||
public class FirmwareManager
|
public sealed class FirmwareManager
|
||||||
{
|
{
|
||||||
private static readonly FirmwareID NDS_FIRMWARE = new("NDS", "firmware");
|
private static readonly FirmwareID NDS_FIRMWARE = new("NDS", "firmware");
|
||||||
|
|
||||||
public List<FirmwareEventArgs> RecentlyServed { get; } = new List<FirmwareEventArgs>();
|
private readonly IReadOnlyCollection<long> _firmwareSizes;
|
||||||
|
|
||||||
|
private readonly List<FirmwareEventArgs> _recentlyServed = new();
|
||||||
|
|
||||||
private readonly Dictionary<FirmwareRecord, ResolutionInfo> _resolutionDictionary = new();
|
private readonly Dictionary<FirmwareRecord, ResolutionInfo> _resolutionDictionary = new();
|
||||||
|
|
||||||
// purpose of forbidScan: sometimes this is called from a loop in Scan(). we don't want to repeatedly DoScanAndResolve in that case, its already been done.
|
public ICollection<FirmwareEventArgs> RecentlyServed => _recentlyServed;
|
||||||
public ResolutionInfo Resolve(PathEntryCollection pathEntries, IDictionary<string, string> userSpecifications, FirmwareRecord record, bool forbidScan = false)
|
|
||||||
|
public FirmwareManager()
|
||||||
{
|
{
|
||||||
_resolutionDictionary.TryGetValue(record, out var resolved);
|
_firmwareSizes = new HashSet<long>(FirmwareDatabase.FirmwareFiles.Select(ff => ff.Size)); // build a list of expected file sizes, used as a simple filter to speed up scanning
|
||||||
// couldn't find it! do a scan and resolve to try harder
|
}
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// Sometimes this is called from a loop in <c>FirmwaresConfig.DoScan</c>.
|
||||||
|
/// In that case, we don't want to call <see cref="DoScanAndResolve"/> repeatedly, so we use <paramref name="forbidScan"/> to skip it.
|
||||||
|
/// </remarks>
|
||||||
|
public ResolutionInfo? Resolve(PathEntryCollection pathEntries, IDictionary<string, string> userSpecifications, FirmwareRecord record, bool forbidScan = false)
|
||||||
|
{
|
||||||
|
if (_resolutionDictionary.TryGetValue(record, out var resolved)) return resolved;
|
||||||
|
// else couldn't find it
|
||||||
|
|
||||||
|
if (forbidScan) return null;
|
||||||
|
// try harder by doing a scan and resolve
|
||||||
// NOTE: this could result in bad performance in some cases if the scanning happens repeatedly...
|
// NOTE: this could result in bad performance in some cases if the scanning happens repeatedly...
|
||||||
if (resolved == null && !forbidScan)
|
DoScanAndResolve(pathEntries, userSpecifications);
|
||||||
{
|
return _resolutionDictionary.TryGetValue(record, out var resolved1) ? resolved1 : null;
|
||||||
DoScanAndResolve(pathEntries, userSpecifications);
|
|
||||||
_resolutionDictionary.TryGetValue(record, out resolved);
|
|
||||||
}
|
|
||||||
return resolved;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Requests the specified firmware. tries really hard to scan and resolve as necessary
|
// Requests the specified firmware. tries really hard to scan and resolve as necessary
|
||||||
public string Request(PathEntryCollection pathEntries, IDictionary<string, string> userSpecifications, FirmwareID id)
|
public string? Request(PathEntryCollection pathEntries, IDictionary<string, string> userSpecifications, FirmwareID id)
|
||||||
{
|
{
|
||||||
var resolved = Resolve(
|
var resolved = Resolve(
|
||||||
pathEntries,
|
pathEntries,
|
||||||
userSpecifications,
|
userSpecifications,
|
||||||
FirmwareDatabase.FirmwareRecords.First(fr => fr.ID == id));
|
FirmwareDatabase.FirmwareRecords.First(fr => fr.ID == id));
|
||||||
if (resolved == null)
|
if (resolved == null) return null;
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
RecentlyServed.Add(new(id, resolved.Hash, resolved.Size));
|
RecentlyServed.Add(new(id, resolved.Hash, resolved.Size));
|
||||||
return resolved.FilePath;
|
return resolved.FilePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class RealFirmwareReader : IDisposable
|
private sealed class RealFirmwareReader : IDisposable
|
||||||
{
|
{
|
||||||
private SHA1 _sha1 = SHA1.Create();
|
private readonly Dictionary<string, RealFirmwareFile> _dict = new();
|
||||||
|
|
||||||
|
private SHA1? _sha1 = SHA1.Create();
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, RealFirmwareFile> Dict => _dict;
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_sha1.Dispose();
|
_sha1?.Dispose();
|
||||||
_sha1 = null;
|
_sha1 = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RealFirmwareFile Read(FileInfo fi)
|
public RealFirmwareFile Read(FileInfo fi)
|
||||||
{
|
{
|
||||||
using (var fs = fi.OpenRead())
|
if (_sha1 == null) throw new ObjectDisposedException(nameof(RealFirmwareReader));
|
||||||
{
|
using var fs = fi.OpenRead();
|
||||||
_sha1.ComputeHash(fs);
|
_sha1!.ComputeHash(fs);
|
||||||
}
|
|
||||||
|
|
||||||
var hash = _sha1.Hash.BytesToHexString();
|
var hash = _sha1.Hash.BytesToHexString();
|
||||||
var rff = new RealFirmwareFile(fi, hash);
|
return _dict![hash] = new RealFirmwareFile(fi, hash);
|
||||||
Dict[hash] = rff;
|
|
||||||
return rff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dictionary<string, RealFirmwareFile> Dict { get; } = new Dictionary<string, RealFirmwareFile>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -80,8 +88,7 @@ namespace BizHawk.Client.Common
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var fi = new FileInfo(f);
|
var fi = new FileInfo(f);
|
||||||
if (!fi.Exists)
|
if (!fi.Exists) return false;
|
||||||
return false;
|
|
||||||
|
|
||||||
// weed out filesizes first to reduce the unnecessary overhead of a hashing operation
|
// weed out filesizes first to reduce the unnecessary overhead of a hashing operation
|
||||||
if (FirmwareDatabase.FirmwareFiles.All(a => a.Size != fi.Length)) return false;
|
if (FirmwareDatabase.FirmwareFiles.All(a => a.Size != fi.Length)) return false;
|
||||||
|
@ -90,131 +97,102 @@ namespace BizHawk.Client.Common
|
||||||
using var reader = new RealFirmwareReader();
|
using var reader = new RealFirmwareReader();
|
||||||
reader.Read(fi);
|
reader.Read(fi);
|
||||||
var hash = reader.Dict.Values.First().Hash;
|
var hash = reader.Dict.Values.First().Hash;
|
||||||
if (FirmwareDatabase.FirmwareFiles.Any(a => a.Hash == hash)) return true;
|
return FirmwareDatabase.FirmwareFiles.Any(a => a.Hash == hash);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
catch { }
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DoScanAndResolve(PathEntryCollection pathEntries, IDictionary<string, string> userSpecifications)
|
public void DoScanAndResolve(PathEntryCollection pathEntries, IDictionary<string, string> userSpecifications)
|
||||||
{
|
{
|
||||||
// build a list of file sizes. Only those will be checked during scanning
|
|
||||||
var sizes = new HashSet<long>();
|
|
||||||
foreach (var ff in FirmwareDatabase.FirmwareFiles)
|
|
||||||
{
|
|
||||||
sizes.Add(ff.Size);
|
|
||||||
}
|
|
||||||
|
|
||||||
using var reader = new RealFirmwareReader();
|
using var reader = new RealFirmwareReader();
|
||||||
|
|
||||||
// build a list of files under the global firmwares path, and build a hash for each of them while we're at it
|
// build a list of files under the global firmwares path, and build a hash for each of them (as ResolutionInfo) while we're at it
|
||||||
var todo = new Queue<DirectoryInfo>();
|
var todo = new Queue<DirectoryInfo>(new[] { new DirectoryInfo(pathEntries.AbsolutePathFor(pathEntries.FirmwaresPathFragment, null)) });
|
||||||
todo.Enqueue(new DirectoryInfo(pathEntries.AbsolutePathFor(pathEntries.FirmwaresPathFragment, null)));
|
|
||||||
|
|
||||||
while (todo.Count != 0)
|
while (todo.Count != 0)
|
||||||
{
|
{
|
||||||
var di = todo.Dequeue();
|
var di = todo.Dequeue();
|
||||||
|
if (!di.Exists) continue;
|
||||||
|
|
||||||
if (!di.Exists)
|
foreach (var subDir in di.GetDirectories()) todo.Enqueue(subDir); // recurse
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we're going to allow recursing into subdirectories, now. its been verified to work OK
|
foreach (var fi in di.GetFiles().Where(fi => _firmwareSizes.Contains(fi.Length))) reader.Read(fi);
|
||||||
foreach (var subDir in di.GetDirectories())
|
|
||||||
{
|
|
||||||
todo.Enqueue(subDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var fi in di.GetFiles())
|
|
||||||
{
|
|
||||||
if (sizes.Contains(fi.Length))
|
|
||||||
{
|
|
||||||
reader.Read(fi);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// now, for each firmware record, try to resolve it
|
// now, for each firmware record, try to resolve it
|
||||||
foreach (var fr in FirmwareDatabase.FirmwareRecords)
|
foreach (var fr in FirmwareDatabase.FirmwareRecords)
|
||||||
{
|
{
|
||||||
// clear previous resolution results
|
_resolutionDictionary.Remove(fr); // clear previous resolution results
|
||||||
_resolutionDictionary.Remove(fr);
|
FirmwareOption fo;
|
||||||
|
try
|
||||||
// get all options for this firmware (in order)
|
|
||||||
var id = fr.ID;
|
|
||||||
var options = FirmwareDatabase.FirmwareOptions.Where(fo => fo.ID == id && fo.IsAcceptableOrIdeal);
|
|
||||||
|
|
||||||
// try each option
|
|
||||||
foreach (var fo in options)
|
|
||||||
{
|
{
|
||||||
var hash = fo.Hash;
|
// check each acceptable option for this firmware, looking for the first that's in the reader's file list
|
||||||
|
fo = FirmwareDatabase.FirmwareOptions.First(fo1 => fo1.ID == fr.ID && fo1.IsAcceptableOrIdeal
|
||||||
// did we find this firmware?
|
&& reader.Dict.ContainsKey(fo1.Hash));
|
||||||
if (reader.Dict.ContainsKey(hash))
|
|
||||||
{
|
|
||||||
// rad! then we can use it
|
|
||||||
var ri = new ResolutionInfo
|
|
||||||
{
|
|
||||||
FilePath = reader.Dict[hash].FileInfo.FullName,
|
|
||||||
KnownFirmwareFile = FirmwareDatabase.FirmwareFilesByHash[hash],
|
|
||||||
Hash = hash,
|
|
||||||
Size = fo.Size
|
|
||||||
};
|
|
||||||
_resolutionDictionary[fr] = ri;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
continue; // didn't find any of them
|
||||||
|
}
|
||||||
|
// else found one, add it to the dict
|
||||||
|
_resolutionDictionary[fr] = new ResolutionInfo
|
||||||
|
{
|
||||||
|
FilePath = reader.Dict[fo.Hash].FileInfo.FullName,
|
||||||
|
KnownFirmwareFile = FirmwareDatabase.FirmwareFilesByHash[fo.Hash],
|
||||||
|
Hash = fo.Hash,
|
||||||
|
Size = fo.Size
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// apply user overrides
|
// apply user overrides
|
||||||
foreach (var fr in FirmwareDatabase.FirmwareRecords)
|
foreach (var fr in FirmwareDatabase.FirmwareRecords)
|
||||||
{
|
{
|
||||||
// do we have a user specification for this firmware record?
|
// do we have a user specification for this firmware record?
|
||||||
if (userSpecifications.TryGetValue(fr.ID.ConfigKey, out var userSpec))
|
if (!userSpecifications.TryGetValue(fr.ID.ConfigKey, out var userSpec)) continue;
|
||||||
|
|
||||||
|
if (!_resolutionDictionary.TryGetValue(fr, out var ri))
|
||||||
{
|
{
|
||||||
// flag it as user specified
|
ri = new ResolutionInfo();
|
||||||
if (!_resolutionDictionary.TryGetValue(fr, out ResolutionInfo ri))
|
_resolutionDictionary[fr] = ri;
|
||||||
|
}
|
||||||
|
// local ri is a reference to a ResolutionInfo which is now definitely in the dict
|
||||||
|
|
||||||
|
// flag it as user specified
|
||||||
|
ri.UserSpecified = true;
|
||||||
|
ri.KnownFirmwareFile = null;
|
||||||
|
ri.FilePath = userSpec;
|
||||||
|
ri.Hash = null;
|
||||||
|
|
||||||
|
// check whether it exists
|
||||||
|
var fi = new FileInfo(userSpec);
|
||||||
|
if (!fi.Exists)
|
||||||
|
{
|
||||||
|
ri.Missing = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// compute its hash
|
||||||
|
// NDS's firmware file contains user settings; these are over-written by sync settings, so we shouldn't allow them to impact the hash
|
||||||
|
var rff = reader.Read(fr.ID == NDS_FIRMWARE
|
||||||
|
? new FileInfo(Emulation.Cores.Consoles.Nintendo.NDS.MelonDS.CreateModifiedFirmware(userSpec))
|
||||||
|
: fi);
|
||||||
|
ri.Size = fi.Length;
|
||||||
|
ri.Hash = rff.Hash;
|
||||||
|
|
||||||
|
// check whether it was a known file anyway, and go ahead and bind to the known file, as a perk (the firmwares config doesn't really use this information right now)
|
||||||
|
if (FirmwareDatabase.FirmwareFilesByHash.TryGetValue(rff.Hash, out var ff))
|
||||||
|
{
|
||||||
|
ri.KnownFirmwareFile = ff;
|
||||||
|
|
||||||
|
// if the known firmware file is for a different firmware, flag it so we can show a warning
|
||||||
|
if (FirmwareDatabase.FirmwareOptions.Any(fo => fo.Hash == rff.Hash && fo.ID != fr.ID))
|
||||||
{
|
{
|
||||||
ri = new ResolutionInfo();
|
ri.KnownMismatching = true;
|
||||||
_resolutionDictionary[fr] = ri;
|
|
||||||
}
|
|
||||||
|
|
||||||
ri.UserSpecified = true;
|
|
||||||
ri.KnownFirmwareFile = null;
|
|
||||||
ri.FilePath = userSpec;
|
|
||||||
ri.Hash = null;
|
|
||||||
|
|
||||||
// check whether it exists
|
|
||||||
var fi = new FileInfo(userSpec);
|
|
||||||
if (!fi.Exists)
|
|
||||||
{
|
|
||||||
ri.Missing = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// compute its hash
|
|
||||||
// NDS's firmware file contains user settings; these are over-written by sync settings, so we shouldn't allow them to impact the hash
|
|
||||||
var rff = reader.Read(fr.ID == NDS_FIRMWARE
|
|
||||||
? new FileInfo(Emulation.Cores.Consoles.Nintendo.NDS.MelonDS.CreateModifiedFirmware(userSpec))
|
|
||||||
: fi);
|
|
||||||
ri.Size = fi.Length;
|
|
||||||
ri.Hash = rff.Hash;
|
|
||||||
|
|
||||||
// check whether it was a known file anyway, and go ahead and bind to the known file, as a perk (the firmwares config doesn't really use this information right now)
|
|
||||||
if (FirmwareDatabase.FirmwareFilesByHash.TryGetValue(rff.Hash, out var ff))
|
|
||||||
{
|
|
||||||
ri.KnownFirmwareFile = ff;
|
|
||||||
|
|
||||||
// if the known firmware file is for a different firmware, flag it so we can show a warning
|
|
||||||
if (FirmwareDatabase.FirmwareOptions.Any(fo => fo.Hash == rff.Hash && fo.ID != fr.ID))
|
|
||||||
{
|
|
||||||
ri.KnownMismatching = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // foreach(firmware record)
|
}
|
||||||
} // DoScanAndResolve()
|
}
|
||||||
} // class FirmwareManager
|
}
|
||||||
} // namespace
|
}
|
||||||
|
|
|
@ -235,7 +235,7 @@ namespace BizHawk.Client.Common
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (firmwareManager.RecentlyServed.Any())
|
if (firmwareManager.RecentlyServed.Count != 0)
|
||||||
{
|
{
|
||||||
foreach (var firmware in firmwareManager.RecentlyServed)
|
foreach (var firmware in firmwareManager.RecentlyServed)
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue