2020-05-13 21:52:34 +00:00
using System ;
2020-11-19 22:56:30 +00:00
using System.Collections.Concurrent ;
2020-05-13 21:52:34 +00:00
using System.Collections.Generic ;
2020-08-12 04:25:12 +00:00
using System.Diagnostics ;
2020-05-13 21:52:34 +00:00
using System.Linq ;
using System.Threading.Tasks ;
using Microsoft.Extensions.Logging ;
using Wabbajack.BuildServer ;
using Wabbajack.Common ;
using Wabbajack.Lib ;
using Wabbajack.Lib.Downloaders ;
using Wabbajack.Lib.ModListRegistry ;
2020-05-13 22:48:33 +00:00
using Wabbajack.Lib.NexusApi ;
2020-05-13 21:52:34 +00:00
using Wabbajack.Server.DataLayer ;
using Wabbajack.Server.DTOs ;
2021-04-28 20:30:02 +00:00
using ArchiveStatus = Wabbajack . Server . DTOs . ArchiveStatus ;
2020-05-13 21:52:34 +00:00
namespace Wabbajack.Server.Services
{
public class ListValidator : AbstractService < ListValidator , int >
{
private SqlService _sql ;
2020-05-16 15:08:40 +00:00
private DiscordWebHook _discord ;
private NexusKeyMaintainance _nexus ;
2020-05-20 12:18:47 +00:00
private ArchiveMaintainer _archives ;
2020-05-13 21:52:34 +00:00
2020-11-19 22:56:30 +00:00
public IEnumerable < ( ModListSummary Summary , DetailedStatus Detailed ) > Summaries = > ValidationInfo . Values . Select ( e = > ( e . Summary , e . Detailed ) ) ;
2021-01-09 21:46:46 +00:00
public ConcurrentDictionary < string , ( ModListSummary Summary , DetailedStatus Detailed , TimeSpan ValidationTime ) > ValidationInfo = new ( ) ;
2020-05-13 21:52:34 +00:00
2020-06-06 21:44:30 +00:00
public ListValidator ( ILogger < ListValidator > logger , AppSettings settings , SqlService sql , DiscordWebHook discord , NexusKeyMaintainance nexus , ArchiveMaintainer archives , QuickSync quickSync )
: base ( logger , settings , quickSync , TimeSpan . FromMinutes ( 5 ) )
2020-05-13 21:52:34 +00:00
{
_sql = sql ;
2020-05-16 15:08:40 +00:00
_discord = discord ;
_nexus = nexus ;
2020-05-20 12:18:47 +00:00
_archives = archives ;
2020-05-13 21:52:34 +00:00
}
public override async Task < int > Execute ( )
{
var data = await _sql . GetValidationData ( ) ;
2021-07-04 21:37:55 +00:00
_logger . LogInformation ( "Found {count} nexus files" , data . NexusFiles . Count ) ;
2020-05-16 15:08:40 +00:00
2020-05-13 21:52:34 +00:00
using var queue = new WorkQueue ( ) ;
2020-05-18 12:40:55 +00:00
var oldSummaries = Summaries ;
2020-05-13 21:52:34 +00:00
2020-08-12 04:25:12 +00:00
var stopwatch = new Stopwatch ( ) ;
stopwatch . Start ( ) ;
2020-11-19 22:56:30 +00:00
var results = await data . ModLists . Where ( m = > ! m . ForceDown ) . PMap ( queue , async metadata = >
2020-05-13 21:52:34 +00:00
{
2020-11-19 22:56:30 +00:00
var timer = new Stopwatch ( ) ;
timer . Start ( ) ;
2020-05-18 12:40:55 +00:00
var oldSummary =
2020-07-15 22:29:43 +00:00
oldSummaries . FirstOrDefault ( s = > s . Summary . MachineURL = = metadata . Links . MachineURL ) ;
2021-04-11 22:04:12 +00:00
var mainFile = await DownloadDispatcher . Infer ( new Uri ( metadata . Links . Download ) ) ;
var mainArchive = new Archive ( mainFile ! )
{
Size = metadata . DownloadMetadata ! . Size ,
Hash = metadata . DownloadMetadata ! . Hash
} ;
bool mainFailed = false ;
try
{
2021-04-13 21:59:40 +00:00
if ( mainArchive . State is WabbajackCDNDownloader . State )
2021-04-11 22:04:12 +00:00
{
2021-04-13 21:59:40 +00:00
if ( ! await mainArchive . State . Verify ( mainArchive ) )
{
mainFailed = true ;
}
2021-04-11 22:04:12 +00:00
}
}
catch ( Exception ex )
{
mainFailed = true ;
}
2020-07-15 22:29:43 +00:00
var listArchives = await _sql . ModListArchives ( metadata . Links . MachineURL ) ;
var archives = await listArchives . PMap ( queue , async archive = >
2020-05-13 21:52:34 +00:00
{
2021-04-11 22:04:12 +00:00
if ( mainFailed )
return ( archive , ArchiveStatus . InValid ) ;
2020-08-12 04:25:12 +00:00
try
2020-08-08 20:20:15 +00:00
{
2021-01-09 21:46:46 +00:00
ReportStarting ( archive . State . PrimaryKeyString ) ;
if ( timer . Elapsed > Delay )
{
return ( archive , ArchiveStatus . InValid ) ;
}
2020-08-12 04:25:12 +00:00
var ( _ , result ) = await ValidateArchive ( data , archive ) ;
if ( result = = ArchiveStatus . InValid )
{
2021-03-11 02:28:28 +00:00
if ( data . Mirrors . TryGetValue ( archive . Hash , out var done ) )
return ( archive , done ? ArchiveStatus . Mirrored : ArchiveStatus . Updating ) ;
if ( ( await data . AllowedMirrors . Value ) . TryGetValue ( archive . Hash , out var reason ) )
{
await _sql . StartMirror ( ( archive . Hash , reason ) ) ;
return ( archive , ArchiveStatus . Updating ) ;
}
2021-07-04 21:37:55 +00:00
if ( archive . State is NexusDownloader . State )
return ( archive , result ) ;
2020-08-12 04:25:12 +00:00
return await TryToHeal ( data , archive , metadata ) ;
}
2020-08-08 20:20:15 +00:00
2020-12-31 06:44:42 +00:00
2020-08-12 04:25:12 +00:00
return ( archive , result ) ;
}
catch ( Exception ex )
{
_logger . LogError ( ex , $"During Validation of {archive.Hash} {archive.State.PrimaryKeyString}" ) ;
2021-01-09 14:14:01 +00:00
Utils . Log ( $"Exception in validation of {archive.Hash} {archive.State.PrimaryKeyString} " + ex ) ;
2020-08-12 04:25:12 +00:00
return ( archive , ArchiveStatus . InValid ) ;
}
2020-12-31 06:44:42 +00:00
finally
{
ReportEnding ( archive . State . PrimaryKeyString ) ;
}
2020-05-13 21:52:34 +00:00
} ) ;
2021-03-11 02:28:28 +00:00
var failedCount = archives . Count ( f = > f . Item2 = = ArchiveStatus . InValid | | f . Item2 = = ArchiveStatus . Updating ) ;
2020-05-13 21:52:34 +00:00
var passCount = archives . Count ( f = > f . Item2 = = ArchiveStatus . Valid | | f . Item2 = = ArchiveStatus . Updated ) ;
var updatingCount = archives . Count ( f = > f . Item2 = = ArchiveStatus . Updating ) ;
2020-08-08 20:20:15 +00:00
var mirroredCount = archives . Count ( f = > f . Item2 = = ArchiveStatus . Mirrored ) ;
2020-05-13 21:52:34 +00:00
var summary = new ModListSummary
{
Checked = DateTime . UtcNow ,
Failed = failedCount ,
Passed = passCount ,
Updating = updatingCount ,
2020-08-08 20:20:15 +00:00
Mirrored = mirroredCount ,
2020-05-13 21:52:34 +00:00
MachineURL = metadata . Links . MachineURL ,
Name = metadata . Title ,
2021-04-11 22:04:12 +00:00
ModListIsMissing = mainFailed
2020-05-13 21:52:34 +00:00
} ;
var detailed = new DetailedStatus
{
Name = metadata . Title ,
Checked = DateTime . UtcNow ,
DownloadMetaData = metadata . DownloadMetadata ,
HasFailures = failedCount > 0 ,
MachineName = metadata . Links . MachineURL ,
Archives = archives . Select ( a = > new DetailedStatusItem
{
2020-06-14 13:13:29 +00:00
Archive = a . Item1 ,
2020-08-08 20:20:15 +00:00
IsFailing = a . Item2 = = ArchiveStatus . InValid ,
2020-06-14 13:13:29 +00:00
ArchiveStatus = a . Item2
2020-05-13 21:52:34 +00:00
} ) . ToList ( )
} ;
2020-11-19 22:56:30 +00:00
if ( timer . Elapsed > Delay )
{
await _discord . Send ( Channel . Ham ,
new DiscordMessage
{
Embeds = new [ ]
{
new DiscordEmbed
{
Title =
$"Failing {summary.Name} (`{summary.MachineURL}`) because the max validation time expired" ,
Url = new Uri (
$"https://build.wabbajack.org/lists/status/{summary.MachineURL}.html" )
}
}
} ) ;
}
2020-05-18 12:40:55 +00:00
if ( oldSummary ! = default & & oldSummary . Summary . Failed ! = summary . Failed )
{
_logger . Log ( LogLevel . Information , $"Number of failures {oldSummary.Summary.Failed} -> {summary.Failed}" ) ;
if ( summary . HasFailures )
{
await _discord . Send ( Channel . Ham ,
new DiscordMessage
{
Embeds = new [ ]
{
new DiscordEmbed
{
2020-06-20 22:51:47 +00:00
Title =
2020-05-18 12:40:55 +00:00
$"Number of failures in {summary.Name} (`{summary.MachineURL}`) was {oldSummary.Summary.Failed} is now {summary.Failed}" ,
Url = new Uri (
$"https://build.wabbajack.org/lists/status/{summary.MachineURL}.html" )
}
}
} ) ;
}
2020-05-22 20:56:58 +00:00
if ( ! summary . HasFailures & & oldSummary . Summary . HasFailures )
2020-05-18 12:40:55 +00:00
{
await _discord . Send ( Channel . Ham ,
new DiscordMessage
{
Embeds = new [ ]
{
new DiscordEmbed
{
2020-06-20 22:51:47 +00:00
Title = $"{summary.Name} (`{summary.MachineURL}`) is now passing." ,
Url = new Uri (
$"https://build.wabbajack.org/lists/status/{summary.MachineURL}.html" )
2020-05-18 12:40:55 +00:00
}
}
} ) ;
}
}
2020-11-19 22:56:30 +00:00
timer . Stop ( ) ;
ValidationInfo [ summary . MachineURL ] = ( summary , detailed , timer . Elapsed ) ;
2020-05-13 21:52:34 +00:00
return ( summary , detailed ) ;
} ) ;
2020-08-12 04:25:12 +00:00
stopwatch . Stop ( ) ;
_logger . LogInformation ( $"Finished Validation in {stopwatch.Elapsed}" ) ;
2020-05-13 21:52:34 +00:00
return Summaries . Count ( s = > s . Summary . HasFailures ) ;
}
2020-05-20 12:18:47 +00:00
private AsyncLock _healLock = new AsyncLock ( ) ;
2020-06-06 21:44:30 +00:00
private async Task < ( Archive , ArchiveStatus ) > TryToHeal ( ValidationData data , Archive archive , ModlistMetadata modList )
2020-05-20 12:18:47 +00:00
{
2021-02-23 04:38:28 +00:00
using var _ = await _healLock . WaitAsync ( ) ;
2020-05-20 12:18:47 +00:00
var srcDownload = await _sql . GetArchiveDownload ( archive . State . PrimaryKeyString , archive . Hash , archive . Size ) ;
if ( srcDownload = = null | | srcDownload . IsFailed = = true )
{
2020-05-22 20:56:58 +00:00
_logger . Log ( LogLevel . Information , $"Cannot heal {archive.State.PrimaryKeyString} Size: {archive.Size} Hash: {(long)archive.Hash} because it hasn't been previously successfully downloaded" ) ;
2020-05-20 12:18:47 +00:00
return ( archive , ArchiveStatus . InValid ) ;
}
2021-02-23 04:38:28 +00:00
2020-08-12 04:25:12 +00:00
var patches = await _sql . PatchesForSource ( archive . Hash ) ;
2020-05-20 12:18:47 +00:00
foreach ( var patch in patches )
{
if ( patch . Finished is null )
return ( archive , ArchiveStatus . Updating ) ;
if ( patch . IsFailed = = true )
2020-05-30 21:05:26 +00:00
return ( archive , ArchiveStatus . InValid ) ;
2020-05-20 12:18:47 +00:00
var ( _ , status ) = await ValidateArchive ( data , patch . Dest . Archive ) ;
if ( status = = ArchiveStatus . Valid )
return ( archive , ArchiveStatus . Updated ) ;
}
2020-08-12 04:25:12 +00:00
2021-02-23 04:38:28 +00:00
2020-05-20 12:18:47 +00:00
var upgradeTime = DateTime . UtcNow ;
2020-08-12 04:25:12 +00:00
_logger . LogInformation ( $"Validator Finding Upgrade for {archive.Hash} {archive.State.PrimaryKeyString}" ) ;
2020-08-12 22:23:02 +00:00
Func < Archive , Task < AbsolutePath > > resolver = async findIt = >
2020-08-12 04:25:12 +00:00
{
_logger . LogInformation ( $"Quick find for {findIt.State.PrimaryKeyString}" ) ;
var foundArchive = await _sql . GetArchiveDownload ( findIt . State . PrimaryKeyString ) ;
if ( foundArchive = = null )
{
_logger . LogInformation ( $"No Quick find for {findIt.State.PrimaryKeyString}" ) ;
return default ;
}
return _archives . TryGetPath ( foundArchive . Archive . Hash , out var path ) ? path : default ;
} ;
2021-03-11 02:28:28 +00:00
if ( archive . State is NexusDownloader . State )
{
DownloadDispatcher . GetInstance < NexusDownloader > ( ) . Client = await _nexus . GetClient ( ) ;
}
2020-08-12 22:23:02 +00:00
var upgrade = await DownloadDispatcher . FindUpgrade ( archive , resolver ) ;
2020-08-12 04:25:12 +00:00
2020-05-20 12:18:47 +00:00
if ( upgrade = = default )
{
2020-05-23 21:03:25 +00:00
_logger . Log ( LogLevel . Information , $"Cannot heal {archive.State.PrimaryKeyString} because an alternative wasn't found" ) ;
2020-05-20 12:18:47 +00:00
return ( archive , ArchiveStatus . InValid ) ;
}
2020-08-12 04:25:12 +00:00
_logger . LogInformation ( $"Upgrade {upgrade.Archive.State.PrimaryKeyString} found for {archive.State.PrimaryKeyString}" ) ;
{
}
2020-05-20 12:18:47 +00:00
2020-08-12 04:25:12 +00:00
var found = await _sql . GetArchiveDownload ( upgrade . Archive . State . PrimaryKeyString , upgrade . Archive . Hash ,
upgrade . Archive . Size ) ;
Guid id ;
if ( found = = null )
{
if ( upgrade . NewFile . Path . Exists )
await _archives . Ingest ( upgrade . NewFile . Path ) ;
id = await _sql . AddKnownDownload ( upgrade . Archive , upgradeTime ) ;
}
else
{
id = found . Id ;
}
2020-05-20 12:18:47 +00:00
var destDownload = await _sql . GetArchiveDownload ( id ) ;
2020-08-12 04:25:12 +00:00
if ( destDownload . Archive . Hash = = srcDownload . Archive . Hash & & destDownload . Archive . State . PrimaryKeyString = = srcDownload . Archive . State . PrimaryKeyString )
{
_logger . Log ( LogLevel . Information , $"Can't heal because src and dest match" ) ;
return ( archive , ArchiveStatus . InValid ) ;
}
2020-08-24 22:20:50 +00:00
if ( destDownload . Archive . Hash = = default )
{
_logger . Log ( LogLevel . Information , "Can't heal because we got back a default hash for the downloaded file" ) ;
return ( archive , ArchiveStatus . InValid ) ;
}
2020-08-12 04:25:12 +00:00
var existing = await _sql . FindPatch ( srcDownload . Id , destDownload . Id ) ;
if ( existing = = null )
{
2021-05-17 22:37:01 +00:00
if ( await _sql . AddPatch ( new Patch { Src = srcDownload , Dest = destDownload } ) )
{
2020-08-12 04:25:12 +00:00
2021-05-17 22:37:01 +00:00
_logger . Log ( LogLevel . Information ,
$"Enqueued Patch from {srcDownload.Archive.Hash} to {destDownload.Archive.Hash}" ) ;
await _discord . Send ( Channel . Ham ,
new DiscordMessage
{
Content =
$"Enqueued Patch from {srcDownload.Archive.Hash} to {destDownload.Archive.Hash} to auto-heal `{modList.Links.MachineURL}`"
} ) ;
}
2020-08-12 04:25:12 +00:00
}
2020-05-20 12:18:47 +00:00
2020-05-28 02:43:57 +00:00
await upgrade . NewFile . DisposeAsync ( ) ;
2020-05-20 12:18:47 +00:00
2020-08-12 04:25:12 +00:00
_logger . LogInformation ( $"Patch in progress {archive.Hash} {archive.State.PrimaryKeyString}" ) ;
2020-05-20 12:18:47 +00:00
return ( archive , ArchiveStatus . Updating ) ;
}
2020-05-13 22:48:33 +00:00
private async Task < ( Archive archive , ArchiveStatus ) > ValidateArchive ( ValidationData data , Archive archive )
2020-05-13 21:52:34 +00:00
{
switch ( archive . State )
{
case GoogleDriveDownloader . State _ :
// Disabled for now due to GDrive rate-limiting the build server
return ( archive , ArchiveStatus . Valid ) ;
2021-07-04 21:37:55 +00:00
case NexusDownloader . State nexusState when data . NexusFiles . TryGetValue (
( nexusState . Game . MetaData ( ) . NexusGameId , nexusState . ModID , nexusState . FileID ) , out var category ) :
return ( archive , category ! = null ? ArchiveStatus . Valid : ArchiveStatus . InValid ) ;
2020-05-13 22:48:33 +00:00
case NexusDownloader . State ns :
2020-05-16 15:08:40 +00:00
return ( archive , await FastNexusModStats ( ns ) ) ;
2020-05-13 21:52:34 +00:00
case ManualDownloader . State _ :
return ( archive , ArchiveStatus . Valid ) ;
2020-05-25 13:04:31 +00:00
case ModDBDownloader . State _ :
return ( archive , ArchiveStatus . Valid ) ;
2021-01-09 14:14:01 +00:00
case GameFileSourceDownloader . State _ :
return ( archive , ArchiveStatus . Valid ) ;
2020-07-19 23:09:59 +00:00
case MediaFireDownloader . State _ :
return ( archive , ArchiveStatus . Valid ) ;
2021-07-04 21:37:55 +00:00
case DeprecatedLoversLabDownloader . State _ :
return ( archive , ArchiveStatus . InValid ) ;
case DeprecatedVectorPlexusDownloader . State _ :
return ( archive , ArchiveStatus . InValid ) ;
2020-05-13 21:52:34 +00:00
default :
{
if ( data . ArchiveStatus . TryGetValue ( ( archive . State . PrimaryKeyString , archive . Hash ) ,
out bool isValid ) )
{
return isValid ? ( archive , ArchiveStatus . Valid ) : ( archive , ArchiveStatus . InValid ) ;
}
2020-05-20 21:48:26 +00:00
return ( archive , ArchiveStatus . Valid ) ;
2020-05-13 21:52:34 +00:00
}
}
}
2021-07-03 03:15:27 +00:00
2020-05-16 15:08:40 +00:00
public async Task < ArchiveStatus > FastNexusModStats ( NexusDownloader . State ns )
2020-05-13 22:48:33 +00:00
{
2020-05-16 15:08:40 +00:00
// Check if some other thread has added them
2021-07-04 21:37:55 +00:00
var file = await _sql . GetModFile ( ns . Game , ns . ModID , ns . FileID ) ;
2020-05-13 22:48:33 +00:00
2021-07-04 21:37:55 +00:00
if ( file = = null )
2020-05-13 22:48:33 +00:00
{
2021-07-04 21:37:55 +00:00
try
2020-05-13 22:48:33 +00:00
{
2021-07-04 21:37:55 +00:00
NexusApiClient nexusClient = await _nexus . GetClient ( ) ;
var queryTime = DateTime . UtcNow ;
2020-05-13 22:48:33 +00:00
2021-07-04 21:37:55 +00:00
_logger . Log ( LogLevel . Information , "Found missing Nexus file info {Game} {ModID} {FileID}" , ns . Game , ns . ModID , ns . FileID ) ;
2021-06-24 23:01:03 +00:00
try
{
2021-07-04 21:37:55 +00:00
file = await nexusClient . GetModFile ( ns . Game , ns . ModID , ns . FileID , false ) ;
}
catch
{
file = new NexusFileInfo ( ) { category_name = null } ;
}
2021-07-03 03:15:27 +00:00
2021-07-04 21:37:55 +00:00
try
{
await _sql . AddNexusModFile ( ns . Game , ns . ModID , ns . FileID , queryTime , file ) ;
2020-05-13 22:48:33 +00:00
}
2020-10-01 03:50:09 +00:00
catch ( Exception )
2020-05-13 22:48:33 +00:00
{
2021-07-04 21:37:55 +00:00
// Could be a PK constraint failure
2020-05-13 22:48:33 +00:00
}
}
2021-07-04 21:37:55 +00:00
catch ( Exception )
{
return ArchiveStatus . InValid ;
}
2020-05-13 22:48:33 +00:00
}
2021-07-04 21:37:55 +00:00
return file ? . category_name ! = null ? ArchiveStatus . Valid : ArchiveStatus . InValid ;
2020-05-13 22:48:33 +00:00
}
2020-05-13 21:52:34 +00:00
}
}