Skip to content

Fix Applying indicator and jobMonitor notifications #3197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 19, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public class ALoadoutSynchronizer : ILoadoutSynchronizer
private readonly IOSInformation _os;
private readonly ISorter _sorter;
private readonly IGarbageCollectorRunner _garbageCollectorRunner;
private readonly ISynchronizerService _synchronizerService;
private readonly IServiceProvider _serviceProvider;

/// <summary>
Expand All @@ -71,7 +72,9 @@ protected ALoadoutSynchronizer(
IGarbageCollectorRunner garbageCollectorRunner)
{
_serviceProvider = serviceProvider;
_synchronizerService = serviceProvider.GetRequiredService<ISynchronizerService>();
_jobMonitor = serviceProvider.GetRequiredService<IJobMonitor>();

_fileHashService = fileHashService;

Logger = logger;
Expand Down Expand Up @@ -353,7 +356,7 @@ private static bool FileIsEnabled(LoadoutItem.ReadOnly arg)
/// <inheritdoc />
public async Task<Dictionary<GamePath, SyncNode>> BuildSyncTree(Loadout.ReadOnly loadout)
{
var metadata = await ReindexState(loadout.InstallationInstance, false, Connection);
var metadata = await ReindexState(loadout.InstallationInstance, ignoreModifiedDates: false, Connection);
var previouslyApplied = loadout.Installation.GetLastAppliedDiskState();
return BuildSyncTree(DiskStateToPathPartPair(metadata.DiskStateEntries), DiskStateToPathPartPair(previouslyApplied), loadout);
}
Expand Down Expand Up @@ -530,7 +533,7 @@ where versionFiles.Contains(path)
// Delete all the matching override files
foreach (var file in toDelete)
{
tx.Delete(file, false);
tx.Delete(file, recursive: false);

// The backed up file is being 'promoted' to a game file, which needs
// to be rooted explicitly in case the user uses a feature like 'undo'
Expand Down Expand Up @@ -689,7 +692,7 @@ private void ActionAddReifiedDelete(Dictionary<GamePath, SyncNode> groupings, Lo
continue;

// If we found a match, we need to remove the entity itself
tx.Delete(match, false);
tx.Delete(match, recursive: false);
continue;
}
}
Expand Down Expand Up @@ -1440,15 +1443,17 @@ private async ValueTask<Hash> MaybeHashFile(IDb hashDb, GamePath gamePath, Absol
// If there is no currently synced loadout, then we can ingest the game folder
if (!GameInstallMetadata.LastSyncedLoadout.TryGetValue(remappedLoadout.Installation, out var lastSyncedLoadoutId))
{
remappedLoadout = await Synchronize(remappedLoadout);
await _synchronizerService.Synchronize(remappedLoadout.LoadoutId);
remappedLoadout = remappedLoadout.Rebase();
}
else
{
// check if the last synced loadout is valid (can apparently happen if the user unmanaged the game and manages it again)
var lastSyncedLoadout = Loadout.Load(remappedLoadout.Db, lastSyncedLoadoutId);
if (!lastSyncedLoadout.IsValid())
{
remappedLoadout = await Synchronize(lastSyncedLoadout);
await _synchronizerService.Synchronize(remappedLoadout.LoadoutId);
remappedLoadout = remappedLoadout.Rebase();
}
}
return remappedLoadout;
Expand Down Expand Up @@ -1491,7 +1496,7 @@ public Optional<LoadoutId> GetCurrentlyActiveLoadout(GameInstallation installati
public async Task ActivateLoadout(LoadoutId loadoutId)
{
var loadout = Loadout.Load(Connection.Db, loadoutId);
var reindexed = await ReindexState(loadout.InstallationInstance, false, Connection);
var reindexed = await ReindexState(loadout.InstallationInstance, ignoreModifiedDates: false, Connection);

var tree = BuildSyncTree(DiskStateToPathPartPair(reindexed.DiskStateEntries), DiskStateToPathPartPair(reindexed.DiskStateEntries), loadout);
ProcessSyncTree(tree);
Expand Down Expand Up @@ -1551,7 +1556,7 @@ await _jobMonitor.Begin(new UnmanageGameJob(installation), async ctx =>
foreach (var file in GameBackedUpFile.All(Connection.Db))
{
if (file.GameInstallId.Value == installation.GameMetadataId)
tx.Delete(file, false);
tx.Delete(file, recursive: false);
}

await tx.Commit();
Expand Down Expand Up @@ -1622,7 +1627,7 @@ await _jobMonitor.Begin(new UnmanageGameJob(installation), async ctx =>
datom.ValueSpan.CopyTo(buffer.Span);

// Create the new datom and reference the copied value
var prefix = new KeyPrefix(newId, datom.A, TxId.Tmp, false, datom.Prefix.ValueTag);
var prefix = new KeyPrefix(newId, datom.A, TxId.Tmp, isRetract: false, datom.Prefix.ValueTag);
var newDatom = new Datom(prefix, buffer[..datom.ValueSpan.Length]);

// Remap any entity ids in the value
Expand Down Expand Up @@ -1681,10 +1686,10 @@ public async Task DeleteLoadout(LoadoutId loadoutId, GarbageCollectorRunMode gcR
}

using var tx = Connection.BeginTransaction();
tx.Delete(loadoutId, false);
tx.Delete(loadoutId, recursive: false);
foreach (var item in loadout.Items)
{
tx.Delete(item.Id, false);
tx.Delete(item.Id, recursive: false);
}
await tx.Commit();

Expand All @@ -1695,7 +1700,7 @@ public async Task DeleteLoadout(LoadoutId loadoutId, GarbageCollectorRunMode gcR
public async Task ResetToOriginalGameState(GameInstallation installation, LocatorId[] locatorIds)
{
var gameState = _fileHashService.GetGameFiles((installation.Store, locatorIds));
var metaData = await ReindexState(installation, false, Connection);
var metaData = await ReindexState(installation, ignoreModifiedDates: false, Connection);

List<PathPartPair> diskState = [];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ public interface ILoadoutSynchronizer
/// Rescan the files in the folders this game requires. This is used to bring the local cache up to date with the
/// whatever is on disk.
/// </summary>
/// <param name="gameInstallation">The game installation to rescan.</param>
/// <param name="ignoreModifiedDate">
/// If false, files that have unchanged modified date since the last scan will be skipped.
/// If true, all files will be rehashed.
/// </param>
Task<GameInstallMetadata.ReadOnly> RescanFiles(GameInstallation gameInstallation, bool ignoreModifiedDate = false);

#endregion
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using NexusMods.Abstractions.Jobs;
using R3;

namespace NexusMods.Abstractions.Loadouts.Synchronizers;

/// <summary>
/// Synchronize the loadout with the game folder,
/// any changes in the game folder will be added to the loadout,
/// and any new changes in the loadout will be applied to the game folder.
/// </summary>
/// <param name="LoadoutId"></param>
public record SynchronizeLoadoutJob(LoadoutId LoadoutId) : IJobDefinition<Unit>;

Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ public ApplyControlView()

this.OneWayBind(ViewModel, vm => vm.IsProcessing, v => v.ProcessingChangesStackPanel.IsVisible)
.DisposeWith(disposables);

this.OneWayBind(ViewModel, vm => vm.IsApplying, v => v.ProgressBarControl.IsVisible)
.DisposeWith(disposables);

this.WhenAnyObservable(view => view.ViewModel!.ApplyCommand.CanExecute)
.OnUI()
Expand All @@ -37,11 +40,6 @@ public ApplyControlView()
)
.DisposeWith(disposables);

this.WhenAnyObservable(view => view.ViewModel!.ApplyCommand.IsExecuting)
.OnUI()
.Subscribe(isApplying => { ProgressBarControl.IsVisible = isApplying; })
.DisposeWith(disposables);

this.OneWayBind(ViewModel, vm => vm.ApplyButtonText, v => v.ApplyButtonTextBlock.Text)
.DisposeWith(disposables);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Reactive.Linq;
using DynamicData;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NexusMods.Abstractions.Jobs;
using NexusMods.Abstractions.Loadouts;
using NexusMods.Abstractions.UI;
Expand Down Expand Up @@ -31,6 +32,7 @@ public class ApplyControlViewModel : AViewModel<IApplyControlViewModel>, IApplyC
private readonly IServiceProvider _serviceProvider;
private readonly GameInstallMetadataId _gameMetadataId;
[Reactive] private bool CanApply { get; set; } = true;
[Reactive] public bool IsApplying { get; private set; }

public ReactiveCommand<Unit, Unit> ApplyCommand { get; }
public ReactiveCommand<NavigationInformation, Unit> ShowApplyDiffCommand { get; }
Expand Down Expand Up @@ -97,7 +99,8 @@ public ApplyControlViewModel(LoadoutId loadoutId, IServiceProvider serviceProvid
// - This is done in 'Synchronize' method.
// - They're running a tool from within the App.
// - Check running jobs.
loadoutStatuses.CombineLatest(isProcessingObservable, gameStatuses, gameRunningTracker.GetWithCurrentStateAsStarting(), (loadout, isProcessing, game, running) => (loadout, isProcessing, game, running))
loadoutStatuses.CombineLatest(isProcessingObservable, gameStatuses, gameRunningTracker.GetWithCurrentStateAsStarting(),
(loadout, isProcessing, game, running) => (loadout, isProcessing, game, running))
.OnUI()
.Subscribe(status =>
{
Expand All @@ -113,8 +116,16 @@ public ApplyControlViewModel(LoadoutId loadoutId, IServiceProvider serviceProvid
&& !running
&& gameStatus != GameSynchronizerState.Busy
&& ldStatus == LoadoutSynchronizerState.Current;

})
.DisposeWith(disposables);

_jobMonitor.HasActiveJob<SynchronizeLoadoutJob>(job => job.LoadoutId == loadoutId)
.Prepend(_jobMonitor.Jobs.Any(job => job.Definition is SynchronizeLoadoutJob sJob && sJob.LoadoutId == loadoutId))
.OnUI()
.Subscribe(isApplying => IsApplying = isApplying)
.DisposeWith(disposables);

}
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@ public interface IApplyControlViewModel : IViewModelInterface

bool IsProcessing { get; }

bool IsApplying { get; }

string ApplyButtonText { get; }
}
37 changes: 22 additions & 15 deletions src/NexusMods.DataModel/Synchronizer/SynchronizerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,24 +81,31 @@ public IJobTask<ProcessLoadoutChangesJob, bool> GetShouldSynchronize(LoadoutId l
/// <inheritdoc />
public async Task Synchronize(LoadoutId loadoutId)
{
await _semaphore.WaitAsync();
try
{
var loadout = Loadout.Load(_conn.Db, loadoutId);
ThrowIfMainBinaryInUse(loadout);
await _jobMonitor.Begin(new SynchronizeLoadoutJob(loadoutId),
async ctx =>
{
await _semaphore.WaitAsync();
try
{
var loadout = Loadout.Load(_conn.Db, loadoutId);
ThrowIfMainBinaryInUse(loadout);

var loadoutState = GetOrAddLoadoutState(loadoutId);
using var _ = loadoutState.WithLock();
var loadoutState = GetOrAddLoadoutState(loadoutId);
using var _ = loadoutState.WithLock();

var gameState = GetOrAddGameState(loadout.InstallationInstance.GameMetadataId);
using var _2 = gameState.WithLock();
var gameState = GetOrAddGameState(loadout.InstallationInstance.GameMetadataId);
using var _2 = gameState.WithLock();

await loadout.InstallationInstance.GetGame().Synchronizer.Synchronize(loadout);
}
finally
{
_semaphore.Release();
}
await loadout.InstallationInstance.GetGame().Synchronizer.Synchronize(loadout);
}
finally
{
_semaphore.Release();
}

return Unit.Default;
}
);
}

private SynchronizerState GetOrAddLoadoutState(LoadoutId loadoutId)
Expand Down
4 changes: 2 additions & 2 deletions src/NexusMods.Jobs/JobContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace NexusMods.Jobs;
public sealed class JobContext<TJobDefinition, TJobResult> : IJobWithResult<TJobResult>, IJobContext<TJobDefinition>
where TJobDefinition : IJobDefinition<TJobResult> where TJobResult : notnull
{
private readonly Subject<JobStatus> _status;
private readonly BehaviorSubject<JobStatus> _status;
private readonly Subject<Optional<Percent>> _progress;
private readonly Subject<Optional<double>> _rateOfProgress;
private readonly TJobDefinition _definition;
Expand All @@ -25,7 +25,7 @@ internal JobContext(TJobDefinition definition, IJobMonitor monitor, IJobGroup jo
_action = action;
_definition = definition;
Monitor = monitor;
_status = new Subject<JobStatus>();
_status = new BehaviorSubject<JobStatus>(JobStatus.Created);
_progress = new Subject<Optional<Percent>>();
_rateOfProgress = new Subject<Optional<double>>();

Expand Down
Loading