2021-01-21 19:53:16 +08:00
|
|
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
|
|
// See the LICENCE file in the repository root for full licence text.
|
|
|
|
|
2021-01-14 15:13:10 +08:00
|
|
|
using System;
|
2022-01-18 13:30:41 +08:00
|
|
|
using System.IO;
|
2021-10-18 14:35:51 +08:00
|
|
|
using System.Linq;
|
2021-11-05 16:01:00 +08:00
|
|
|
using System.Reflection;
|
2021-01-07 13:07:36 +08:00
|
|
|
using System.Threading;
|
2021-01-22 16:28:47 +08:00
|
|
|
using osu.Framework.Allocation;
|
2021-07-04 16:59:39 +08:00
|
|
|
using osu.Framework.Development;
|
2021-10-29 10:14:22 +08:00
|
|
|
using osu.Framework.Input.Bindings;
|
2021-01-12 13:36:35 +08:00
|
|
|
using osu.Framework.Logging;
|
2021-01-07 13:07:36 +08:00
|
|
|
using osu.Framework.Platform;
|
|
|
|
using osu.Framework.Statistics;
|
2021-11-22 17:07:28 +08:00
|
|
|
using osu.Game.Configuration;
|
2021-11-19 18:07:21 +08:00
|
|
|
using osu.Game.Beatmaps;
|
2021-10-29 10:14:22 +08:00
|
|
|
using osu.Game.Input.Bindings;
|
2021-10-18 14:35:51 +08:00
|
|
|
using osu.Game.Models;
|
2021-11-29 16:59:41 +08:00
|
|
|
using osu.Game.Skinning;
|
2021-11-25 13:12:49 +08:00
|
|
|
using osu.Game.Stores;
|
2021-11-23 12:00:33 +08:00
|
|
|
using osu.Game.Rulesets;
|
2022-01-13 12:40:09 +08:00
|
|
|
using osu.Game.Scoring;
|
2021-01-07 13:07:36 +08:00
|
|
|
using Realms;
|
|
|
|
|
2021-09-30 22:42:40 +08:00
|
|
|
#nullable enable
|
|
|
|
|
2021-01-07 13:07:36 +08:00
|
|
|
namespace osu.Game.Database
|
|
|
|
{
|
2021-09-30 22:42:40 +08:00
|
|
|
/// <summary>
|
|
|
|
/// A factory which provides both the main (update thread bound) realm context and creates contexts for async usage.
|
|
|
|
/// </summary>
|
2021-10-15 12:58:14 +08:00
|
|
|
public class RealmContextFactory : IDisposable, IRealmFactory
|
2021-01-07 13:07:36 +08:00
|
|
|
{
|
|
|
|
private readonly Storage storage;
|
2021-01-13 17:24:19 +08:00
|
|
|
|
2021-01-12 13:36:35 +08:00
|
|
|
/// <summary>
|
2021-09-30 22:42:40 +08:00
|
|
|
/// The filename of this realm.
|
2021-01-12 13:36:35 +08:00
|
|
|
/// </summary>
|
2021-09-30 22:42:40 +08:00
|
|
|
public readonly string Filename;
|
|
|
|
|
2021-11-23 15:27:28 +08:00
|
|
|
private readonly IDatabaseContextFactory? efContextFactory;
|
2021-11-22 17:51:37 +08:00
|
|
|
|
2021-10-18 14:35:51 +08:00
|
|
|
/// <summary>
|
|
|
|
/// Version history:
|
2021-11-22 14:23:16 +08:00
|
|
|
/// 6 ~2021-10-18 First tracked version.
|
|
|
|
/// 7 2021-10-18 Changed OnlineID fields to non-nullable to add indexing support.
|
|
|
|
/// 8 2021-10-29 Rebind scroll adjust keys to not have control modifier.
|
|
|
|
/// 9 2021-11-04 Converted BeatmapMetadata.Author from string to RealmUser.
|
2021-11-22 17:07:28 +08:00
|
|
|
/// 10 2021-11-22 Use ShortName instead of RulesetID for ruleset settings.
|
2021-11-22 17:34:04 +08:00
|
|
|
/// 11 2021-11-22 Use ShortName instead of RulesetID for ruleset key bindings.
|
2021-11-24 17:45:34 +08:00
|
|
|
/// 12 2021-11-24 Add Status to RealmBeatmapSet.
|
2022-01-13 12:28:46 +08:00
|
|
|
/// 13 2022-01-13 Final migration of beatmaps and scores to realm (multiple new storage fields).
|
2021-10-18 14:35:51 +08:00
|
|
|
/// </summary>
|
2022-01-13 12:28:46 +08:00
|
|
|
private const int schema_version = 13;
|
2021-01-07 13:07:36 +08:00
|
|
|
|
2021-06-24 13:37:26 +08:00
|
|
|
/// <summary>
|
2021-09-30 22:42:40 +08:00
|
|
|
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking context creation during blocking periods.
|
2021-06-24 13:37:26 +08:00
|
|
|
/// </summary>
|
2021-09-30 22:42:40 +08:00
|
|
|
private readonly SemaphoreSlim contextCreationLock = new SemaphoreSlim(1);
|
2021-06-24 13:37:26 +08:00
|
|
|
|
2021-11-29 16:38:54 +08:00
|
|
|
private readonly ThreadLocal<bool> currentThreadCanCreateContexts = new ThreadLocal<bool>();
|
2021-11-29 15:18:57 +08:00
|
|
|
|
2021-11-23 16:47:43 +08:00
|
|
|
private static readonly GlobalStatistic<int> refreshes = GlobalStatistics.Get<int>(@"Realm", @"Dirty Refreshes");
|
|
|
|
private static readonly GlobalStatistic<int> contexts_created = GlobalStatistics.Get<int>(@"Realm", @"Contexts (Created)");
|
2021-01-22 16:28:47 +08:00
|
|
|
|
2021-10-01 02:45:00 +08:00
|
|
|
private readonly object contextLock = new object();
|
2021-09-30 22:42:40 +08:00
|
|
|
private Realm? context;
|
2021-01-13 16:34:44 +08:00
|
|
|
|
|
|
|
public Realm Context
|
2021-01-07 13:07:36 +08:00
|
|
|
{
|
2021-01-13 16:34:44 +08:00
|
|
|
get
|
|
|
|
{
|
2021-07-04 16:59:39 +08:00
|
|
|
if (!ThreadSafety.IsUpdateThread)
|
2021-11-23 16:47:43 +08:00
|
|
|
throw new InvalidOperationException(@$"Use {nameof(CreateContext)} when performing realm operations from a non-update thread");
|
2021-07-04 16:59:39 +08:00
|
|
|
|
2021-10-01 02:45:00 +08:00
|
|
|
lock (contextLock)
|
2021-01-13 16:34:44 +08:00
|
|
|
{
|
2021-10-01 02:45:00 +08:00
|
|
|
if (context == null)
|
|
|
|
{
|
2021-10-01 02:46:53 +08:00
|
|
|
context = CreateContext();
|
2021-11-23 16:47:43 +08:00
|
|
|
Logger.Log(@$"Opened realm ""{context.Config.DatabasePath}"" at version {context.Config.SchemaVersion}");
|
2021-10-01 02:45:00 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// creating a context will ensure our schema is up-to-date and migrated.
|
|
|
|
return context;
|
2021-07-04 16:59:39 +08:00
|
|
|
}
|
2021-01-13 16:34:44 +08:00
|
|
|
}
|
2021-01-07 14:41:29 +08:00
|
|
|
}
|
|
|
|
|
2021-11-23 16:47:43 +08:00
|
|
|
/// <summary>
|
|
|
|
/// Construct a new instance of a realm context factory.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="storage">The game storage which will be used to create the realm backing file.</param>
|
|
|
|
/// <param name="filename">The filename to use for the realm backing file. A ".realm" extension will be added automatically if not specified.</param>
|
|
|
|
/// <param name="efContextFactory">An EF factory used only for migration purposes.</param>
|
2021-11-23 15:27:28 +08:00
|
|
|
public RealmContextFactory(Storage storage, string filename, IDatabaseContextFactory? efContextFactory = null)
|
2021-01-07 14:41:29 +08:00
|
|
|
{
|
2021-01-13 16:34:44 +08:00
|
|
|
this.storage = storage;
|
2021-11-23 15:27:28 +08:00
|
|
|
this.efContextFactory = efContextFactory;
|
2021-01-07 13:07:36 +08:00
|
|
|
|
2021-09-30 22:42:40 +08:00
|
|
|
Filename = filename;
|
2021-01-07 13:07:36 +08:00
|
|
|
|
2021-11-23 16:47:43 +08:00
|
|
|
const string realm_extension = @".realm";
|
2021-01-14 14:51:19 +08:00
|
|
|
|
2021-09-30 22:42:40 +08:00
|
|
|
if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
|
|
|
|
Filename += realm_extension;
|
2021-09-30 22:45:09 +08:00
|
|
|
|
2022-01-18 15:05:12 +08:00
|
|
|
try
|
|
|
|
{
|
|
|
|
// This method triggers the first `CreateContext` call, which will implicitly run realm migrations and bring the schema up-to-date.
|
|
|
|
cleanupPendingDeletions();
|
|
|
|
}
|
|
|
|
catch (Exception e)
|
|
|
|
{
|
|
|
|
Logger.Error(e, "Realm startup failed with unrecoverable error; starting with a fresh database. A backup of your database has been made.");
|
|
|
|
|
|
|
|
CreateBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_corrupt{realm_extension}");
|
|
|
|
storage.Delete(Filename);
|
|
|
|
|
|
|
|
cleanupPendingDeletions();
|
|
|
|
}
|
2021-09-30 22:45:09 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
private void cleanupPendingDeletions()
|
|
|
|
{
|
|
|
|
using (var realm = CreateContext())
|
|
|
|
using (var transaction = realm.BeginWrite())
|
|
|
|
{
|
2022-01-13 12:40:09 +08:00
|
|
|
var pendingDeleteScores = realm.All<ScoreInfo>().Where(s => s.DeletePending);
|
|
|
|
|
|
|
|
foreach (var score in pendingDeleteScores)
|
|
|
|
realm.Remove(score);
|
|
|
|
|
2021-11-19 18:07:21 +08:00
|
|
|
var pendingDeleteSets = realm.All<BeatmapSetInfo>().Where(s => s.DeletePending);
|
2021-09-30 22:45:09 +08:00
|
|
|
|
2022-01-12 14:09:56 +08:00
|
|
|
foreach (var beatmapSet in pendingDeleteSets)
|
2021-09-30 22:45:09 +08:00
|
|
|
{
|
2022-01-12 14:09:56 +08:00
|
|
|
foreach (var beatmap in beatmapSet.Beatmaps)
|
|
|
|
{
|
|
|
|
// Cascade delete related scores, else they will have a null beatmap against the model's spec.
|
|
|
|
foreach (var score in beatmap.Scores)
|
|
|
|
realm.Remove(score);
|
2021-09-30 22:45:09 +08:00
|
|
|
|
2022-01-12 15:17:11 +08:00
|
|
|
realm.Remove(beatmap.Metadata);
|
|
|
|
|
2022-01-12 14:09:56 +08:00
|
|
|
realm.Remove(beatmap);
|
|
|
|
}
|
|
|
|
|
|
|
|
realm.Remove(beatmapSet);
|
2021-09-30 22:45:09 +08:00
|
|
|
}
|
|
|
|
|
2021-11-29 16:59:41 +08:00
|
|
|
var pendingDeleteSkins = realm.All<SkinInfo>().Where(s => s.DeletePending);
|
|
|
|
|
|
|
|
foreach (var s in pendingDeleteSkins)
|
|
|
|
realm.Remove(s);
|
|
|
|
|
2021-09-30 22:45:09 +08:00
|
|
|
transaction.Commit();
|
|
|
|
}
|
2021-11-25 13:28:27 +08:00
|
|
|
|
|
|
|
// clean up files after dropping any pending deletions.
|
|
|
|
// in the future we may want to only do this when the game is idle, rather than on every startup.
|
|
|
|
new RealmFileStore(this, storage).Cleanup();
|
2021-06-24 13:37:26 +08:00
|
|
|
}
|
2021-01-13 16:34:44 +08:00
|
|
|
|
2021-09-30 22:42:40 +08:00
|
|
|
/// <summary>
|
|
|
|
/// Compact this realm.
|
|
|
|
/// </summary>
|
|
|
|
/// <returns></returns>
|
|
|
|
public bool Compact() => Realm.Compact(getConfiguration());
|
|
|
|
|
2021-10-15 12:58:14 +08:00
|
|
|
/// <summary>
|
|
|
|
/// Perform a blocking refresh on the main realm context.
|
|
|
|
/// </summary>
|
|
|
|
public void Refresh()
|
2021-01-07 13:07:36 +08:00
|
|
|
{
|
2021-10-01 02:45:00 +08:00
|
|
|
lock (contextLock)
|
|
|
|
{
|
|
|
|
if (context?.Refresh() == true)
|
|
|
|
refreshes.Value++;
|
|
|
|
}
|
2021-01-07 13:07:36 +08:00
|
|
|
}
|
|
|
|
|
2021-10-01 02:46:53 +08:00
|
|
|
public Realm CreateContext()
|
2021-01-12 13:36:35 +08:00
|
|
|
{
|
2021-10-15 12:58:14 +08:00
|
|
|
if (isDisposed)
|
2021-10-01 02:46:53 +08:00
|
|
|
throw new ObjectDisposedException(nameof(RealmContextFactory));
|
|
|
|
|
2021-11-29 17:26:37 +08:00
|
|
|
bool tookSemaphoreLock = false;
|
|
|
|
|
2021-06-24 13:37:26 +08:00
|
|
|
try
|
|
|
|
{
|
2021-11-29 16:38:42 +08:00
|
|
|
if (!currentThreadCanCreateContexts.Value)
|
2021-11-29 17:26:37 +08:00
|
|
|
{
|
2021-11-29 15:18:57 +08:00
|
|
|
contextCreationLock.Wait();
|
2021-11-29 17:52:29 +08:00
|
|
|
currentThreadCanCreateContexts.Value = true;
|
2021-11-29 17:26:37 +08:00
|
|
|
tookSemaphoreLock = true;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// the semaphore is used to handle blocking of all context creation during certain periods.
|
|
|
|
// once the semaphore has been taken by this code section, it is safe to create further contexts on the same thread.
|
|
|
|
// this can happen if a realm subscription is active and triggers a callback which has user code that calls `CreateContext`.
|
|
|
|
}
|
2021-01-22 16:28:47 +08:00
|
|
|
|
2021-06-24 13:37:26 +08:00
|
|
|
contexts_created.Value++;
|
2021-01-12 13:36:35 +08:00
|
|
|
|
2021-09-30 22:42:40 +08:00
|
|
|
return Realm.GetInstance(getConfiguration());
|
2021-06-24 13:37:26 +08:00
|
|
|
}
|
|
|
|
finally
|
2021-01-12 13:36:35 +08:00
|
|
|
{
|
2021-11-29 17:26:37 +08:00
|
|
|
if (tookSemaphoreLock)
|
|
|
|
{
|
|
|
|
contextCreationLock.Release();
|
|
|
|
currentThreadCanCreateContexts.Value = false;
|
|
|
|
}
|
2021-06-24 13:37:26 +08:00
|
|
|
}
|
2021-01-12 13:36:35 +08:00
|
|
|
}
|
|
|
|
|
2021-09-30 22:42:40 +08:00
|
|
|
private RealmConfiguration getConfiguration()
|
2021-06-28 15:14:14 +08:00
|
|
|
{
|
2022-01-18 10:39:22 +08:00
|
|
|
// This is currently the only usage of temporary files at the osu! side.
|
|
|
|
// If we use the temporary folder in more situations in the future, this should be moved to a higher level (helper method or OsuGameBase).
|
|
|
|
string tempPathLocation = Path.Combine(Path.GetTempPath(), @"lazer");
|
|
|
|
if (!Directory.Exists(tempPathLocation))
|
|
|
|
Directory.CreateDirectory(tempPathLocation);
|
|
|
|
|
2021-09-30 22:42:40 +08:00
|
|
|
return new RealmConfiguration(storage.GetFullPath(Filename, true))
|
|
|
|
{
|
|
|
|
SchemaVersion = schema_version,
|
|
|
|
MigrationCallback = onMigration,
|
2022-01-18 10:39:22 +08:00
|
|
|
FallbackPipePath = tempPathLocation,
|
2021-09-30 22:42:40 +08:00
|
|
|
};
|
2021-06-28 15:14:14 +08:00
|
|
|
}
|
|
|
|
|
2021-01-13 17:24:19 +08:00
|
|
|
private void onMigration(Migration migration, ulong lastSchemaVersion)
|
2021-01-07 13:07:36 +08:00
|
|
|
{
|
2021-11-22 17:37:28 +08:00
|
|
|
for (ulong i = lastSchemaVersion + 1; i <= schema_version; i++)
|
2021-11-04 17:39:23 +08:00
|
|
|
applyMigrationsForVersion(migration, i);
|
|
|
|
}
|
|
|
|
|
2021-11-22 17:37:28 +08:00
|
|
|
private void applyMigrationsForVersion(Migration migration, ulong targetVersion)
|
2021-11-04 17:39:23 +08:00
|
|
|
{
|
2021-11-22 17:37:28 +08:00
|
|
|
switch (targetVersion)
|
2021-11-04 17:32:50 +08:00
|
|
|
{
|
2021-11-04 17:39:23 +08:00
|
|
|
case 7:
|
2021-11-19 18:07:21 +08:00
|
|
|
convertOnlineIDs<BeatmapInfo>();
|
|
|
|
convertOnlineIDs<BeatmapSetInfo>();
|
|
|
|
convertOnlineIDs<RulesetInfo>();
|
2021-11-04 17:32:50 +08:00
|
|
|
|
2021-11-04 17:39:23 +08:00
|
|
|
void convertOnlineIDs<T>() where T : RealmObject
|
|
|
|
{
|
2021-11-05 16:01:00 +08:00
|
|
|
string className = getMappedOrOriginalName(typeof(T));
|
2021-11-04 17:32:50 +08:00
|
|
|
|
2021-11-04 17:39:23 +08:00
|
|
|
// version was not bumped when the beatmap/ruleset models were added
|
|
|
|
// therefore we must manually check for their presence to avoid throwing on the `DynamicApi` calls.
|
|
|
|
if (!migration.OldRealm.Schema.TryFindObjectSchema(className, out _))
|
|
|
|
return;
|
2021-11-04 17:32:50 +08:00
|
|
|
|
2021-11-04 17:39:23 +08:00
|
|
|
var oldItems = migration.OldRealm.DynamicApi.All(className);
|
|
|
|
var newItems = migration.NewRealm.DynamicApi.All(className);
|
2021-11-04 17:32:50 +08:00
|
|
|
|
2021-11-04 17:39:23 +08:00
|
|
|
int itemCount = newItems.Count();
|
2021-11-04 17:32:50 +08:00
|
|
|
|
2021-11-04 17:39:23 +08:00
|
|
|
for (int i = 0; i < itemCount; i++)
|
|
|
|
{
|
|
|
|
dynamic? oldItem = oldItems.ElementAt(i);
|
|
|
|
dynamic? newItem = newItems.ElementAt(i);
|
2021-10-29 10:14:22 +08:00
|
|
|
|
2021-11-04 17:39:23 +08:00
|
|
|
long? nullableOnlineID = oldItem?.OnlineID;
|
|
|
|
newItem.OnlineID = (int)(nullableOnlineID ?? -1);
|
|
|
|
}
|
|
|
|
}
|
2021-10-29 10:14:22 +08:00
|
|
|
|
2021-11-04 17:39:23 +08:00
|
|
|
break;
|
2021-10-29 10:14:22 +08:00
|
|
|
|
2021-11-04 17:39:23 +08:00
|
|
|
case 8:
|
|
|
|
// Ctrl -/+ now adjusts UI scale so let's clear any bindings which overlap these combinations.
|
|
|
|
// New defaults will be populated by the key store afterwards.
|
|
|
|
var keyBindings = migration.NewRealm.All<RealmKeyBinding>();
|
2021-10-18 14:35:51 +08:00
|
|
|
|
2021-11-04 17:39:23 +08:00
|
|
|
var increaseSpeedBinding = keyBindings.FirstOrDefault(k => k.ActionInt == (int)GlobalAction.IncreaseScrollSpeed);
|
|
|
|
if (increaseSpeedBinding != null && increaseSpeedBinding.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Control, InputKey.Plus }))
|
|
|
|
migration.NewRealm.Remove(increaseSpeedBinding);
|
2021-10-18 14:35:51 +08:00
|
|
|
|
2021-11-04 17:39:23 +08:00
|
|
|
var decreaseSpeedBinding = keyBindings.FirstOrDefault(k => k.ActionInt == (int)GlobalAction.DecreaseScrollSpeed);
|
|
|
|
if (decreaseSpeedBinding != null && decreaseSpeedBinding.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Control, InputKey.Minus }))
|
|
|
|
migration.NewRealm.Remove(decreaseSpeedBinding);
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 9:
|
|
|
|
// Pretty pointless to do this as beatmaps aren't really loaded via realm yet, but oh well.
|
2021-11-19 18:07:21 +08:00
|
|
|
string metadataClassName = getMappedOrOriginalName(typeof(BeatmapMetadata));
|
2021-11-09 13:51:06 +08:00
|
|
|
|
|
|
|
// May be coming from a version before `RealmBeatmapMetadata` existed.
|
2021-11-09 16:42:03 +08:00
|
|
|
if (!migration.OldRealm.Schema.TryFindObjectSchema(metadataClassName, out _))
|
2021-11-09 13:51:06 +08:00
|
|
|
return;
|
|
|
|
|
2021-11-09 16:42:03 +08:00
|
|
|
var oldMetadata = migration.OldRealm.DynamicApi.All(metadataClassName);
|
2021-11-19 18:07:21 +08:00
|
|
|
var newMetadata = migration.NewRealm.All<BeatmapMetadata>();
|
2021-10-18 14:35:51 +08:00
|
|
|
|
2021-11-05 17:24:07 +08:00
|
|
|
int metadataCount = newMetadata.Count();
|
2021-10-18 14:35:51 +08:00
|
|
|
|
2021-11-05 17:24:07 +08:00
|
|
|
for (int i = 0; i < metadataCount; i++)
|
2021-10-18 14:35:51 +08:00
|
|
|
{
|
2021-11-05 16:02:23 +08:00
|
|
|
dynamic? oldItem = oldMetadata.ElementAt(i);
|
|
|
|
var newItem = newMetadata.ElementAt(i);
|
2021-10-18 14:35:51 +08:00
|
|
|
|
2021-11-04 17:39:23 +08:00
|
|
|
string username = oldItem.Author;
|
|
|
|
newItem.Author = new RealmUser
|
|
|
|
{
|
|
|
|
Username = username
|
|
|
|
};
|
2021-10-18 14:35:51 +08:00
|
|
|
}
|
2021-11-04 17:39:23 +08:00
|
|
|
|
2021-11-22 17:07:28 +08:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 10:
|
|
|
|
string rulesetSettingClassName = getMappedOrOriginalName(typeof(RealmRulesetSetting));
|
|
|
|
|
2021-11-28 22:00:40 +08:00
|
|
|
if (!migration.OldRealm.Schema.TryFindObjectSchema(rulesetSettingClassName, out _))
|
|
|
|
return;
|
|
|
|
|
2021-11-22 17:07:28 +08:00
|
|
|
var oldSettings = migration.OldRealm.DynamicApi.All(rulesetSettingClassName);
|
|
|
|
var newSettings = migration.NewRealm.All<RealmRulesetSetting>().ToList();
|
|
|
|
|
|
|
|
for (int i = 0; i < newSettings.Count; i++)
|
|
|
|
{
|
|
|
|
dynamic? oldItem = oldSettings.ElementAt(i);
|
|
|
|
var newItem = newSettings.ElementAt(i);
|
|
|
|
|
|
|
|
long rulesetId = oldItem.RulesetID;
|
2021-11-23 15:27:28 +08:00
|
|
|
string? rulesetName = getRulesetShortNameFromLegacyID(rulesetId);
|
2021-11-22 17:34:04 +08:00
|
|
|
|
|
|
|
if (string.IsNullOrEmpty(rulesetName))
|
|
|
|
migration.NewRealm.Remove(newItem);
|
|
|
|
else
|
|
|
|
newItem.RulesetName = rulesetName;
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 11:
|
|
|
|
string keyBindingClassName = getMappedOrOriginalName(typeof(RealmKeyBinding));
|
|
|
|
|
2021-11-28 22:00:40 +08:00
|
|
|
if (!migration.OldRealm.Schema.TryFindObjectSchema(keyBindingClassName, out _))
|
|
|
|
return;
|
|
|
|
|
2021-11-22 17:34:04 +08:00
|
|
|
var oldKeyBindings = migration.OldRealm.DynamicApi.All(keyBindingClassName);
|
|
|
|
var newKeyBindings = migration.NewRealm.All<RealmKeyBinding>().ToList();
|
|
|
|
|
|
|
|
for (int i = 0; i < newKeyBindings.Count; i++)
|
|
|
|
{
|
|
|
|
dynamic? oldItem = oldKeyBindings.ElementAt(i);
|
|
|
|
var newItem = newKeyBindings.ElementAt(i);
|
|
|
|
|
|
|
|
if (oldItem.RulesetID == null)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
long rulesetId = oldItem.RulesetID;
|
2021-11-23 16:48:25 +08:00
|
|
|
string? rulesetName = getRulesetShortNameFromLegacyID(rulesetId);
|
2021-11-22 17:07:28 +08:00
|
|
|
|
|
|
|
if (string.IsNullOrEmpty(rulesetName))
|
|
|
|
migration.NewRealm.Remove(newItem);
|
|
|
|
else
|
|
|
|
newItem.RulesetName = rulesetName;
|
|
|
|
}
|
|
|
|
|
2021-11-04 17:39:23 +08:00
|
|
|
break;
|
2021-10-18 14:35:51 +08:00
|
|
|
}
|
2021-01-13 17:24:19 +08:00
|
|
|
}
|
2021-01-07 13:07:36 +08:00
|
|
|
|
2021-11-23 17:13:05 +08:00
|
|
|
private string? getRulesetShortNameFromLegacyID(long rulesetId) =>
|
2021-11-23 18:15:52 +08:00
|
|
|
efContextFactory?.Get().RulesetInfo.FirstOrDefault(r => r.ID == rulesetId)?.ShortName;
|
2021-11-23 15:27:28 +08:00
|
|
|
|
2022-01-18 13:30:41 +08:00
|
|
|
public void CreateBackup(string filename)
|
|
|
|
{
|
|
|
|
using (BlockAllOperations())
|
|
|
|
{
|
2022-01-18 13:41:02 +08:00
|
|
|
Logger.Log($"Creating full realm database backup at {filename}", LoggingTarget.Database);
|
2022-01-18 13:30:41 +08:00
|
|
|
using (var source = storage.GetStream(Filename))
|
|
|
|
using (var destination = storage.GetStream(filename, FileAccess.Write, FileMode.CreateNew))
|
|
|
|
source.CopyTo(destination);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-30 22:42:40 +08:00
|
|
|
/// <summary>
|
|
|
|
/// Flush any active contexts and block any further writes.
|
|
|
|
/// </summary>
|
|
|
|
/// <remarks>
|
|
|
|
/// This should be used in places we need to ensure no ongoing reads/writes are occurring with realm.
|
|
|
|
/// ie. to move the realm backing file to a new location.
|
|
|
|
/// </remarks>
|
|
|
|
/// <returns>An <see cref="IDisposable"/> which should be disposed to end the blocking section.</returns>
|
|
|
|
public IDisposable BlockAllOperations()
|
2021-01-22 16:28:47 +08:00
|
|
|
{
|
2021-10-15 12:58:14 +08:00
|
|
|
if (isDisposed)
|
2021-09-30 22:42:40 +08:00
|
|
|
throw new ObjectDisposedException(nameof(RealmContextFactory));
|
2021-06-29 19:21:31 +08:00
|
|
|
|
2021-10-01 00:32:28 +08:00
|
|
|
try
|
|
|
|
{
|
|
|
|
contextCreationLock.Wait();
|
|
|
|
|
2021-10-01 02:53:33 +08:00
|
|
|
lock (contextLock)
|
|
|
|
{
|
2022-01-18 13:30:32 +08:00
|
|
|
if (!ThreadSafety.IsUpdateThread && context != null)
|
|
|
|
throw new InvalidOperationException(@$"{nameof(BlockAllOperations)} must be called from the update thread.");
|
|
|
|
|
|
|
|
Logger.Log(@"Blocking realm operations.", LoggingTarget.Database);
|
|
|
|
|
2021-10-01 02:53:33 +08:00
|
|
|
context?.Dispose();
|
|
|
|
context = null;
|
|
|
|
}
|
|
|
|
|
2021-10-01 00:32:28 +08:00
|
|
|
const int sleep_length = 200;
|
|
|
|
int timeout = 5000;
|
2021-01-22 16:28:47 +08:00
|
|
|
|
2022-01-18 15:05:12 +08:00
|
|
|
try
|
2021-10-01 00:32:28 +08:00
|
|
|
{
|
2022-01-18 15:05:12 +08:00
|
|
|
// see https://github.com/realm/realm-dotnet/discussions/2657
|
|
|
|
while (!Compact())
|
|
|
|
{
|
|
|
|
Thread.Sleep(sleep_length);
|
|
|
|
timeout -= sleep_length;
|
2021-06-29 19:21:31 +08:00
|
|
|
|
2022-01-18 15:05:12 +08:00
|
|
|
if (timeout < 0)
|
|
|
|
throw new TimeoutException(@"Took too long to acquire lock");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (Exception e)
|
|
|
|
{
|
|
|
|
// Compact may fail if the realm is in a bad state.
|
|
|
|
// We still want to continue with the blocking operation, though.
|
|
|
|
Logger.Log($"Realm compact failed with error {e}", LoggingTarget.Database);
|
2021-10-01 00:32:28 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
catch
|
|
|
|
{
|
|
|
|
contextCreationLock.Release();
|
|
|
|
throw;
|
|
|
|
}
|
|
|
|
|
|
|
|
return new InvokeOnDisposal<RealmContextFactory>(this, factory =>
|
2021-09-30 22:42:40 +08:00
|
|
|
{
|
|
|
|
factory.contextCreationLock.Release();
|
|
|
|
Logger.Log(@"Restoring realm operations.", LoggingTarget.Database);
|
2021-10-01 00:32:28 +08:00
|
|
|
});
|
2021-01-15 13:26:06 +08:00
|
|
|
}
|
|
|
|
|
2021-11-05 16:01:00 +08:00
|
|
|
// https://github.com/realm/realm-dotnet/blob/32f4ebcc88b3e80a3b254412665340cd9f3bd6b5/Realm/Realm/Extensions/ReflectionExtensions.cs#L46
|
|
|
|
private static string getMappedOrOriginalName(MemberInfo member) => member.GetCustomAttribute<MapToAttribute>()?.Mapping ?? member.Name;
|
|
|
|
|
2021-10-15 12:58:14 +08:00
|
|
|
private bool isDisposed;
|
|
|
|
|
|
|
|
public void Dispose()
|
2021-06-28 15:14:14 +08:00
|
|
|
{
|
2021-10-01 02:46:53 +08:00
|
|
|
lock (contextLock)
|
|
|
|
{
|
|
|
|
context?.Dispose();
|
|
|
|
}
|
2021-09-30 22:42:40 +08:00
|
|
|
|
2021-10-15 12:58:14 +08:00
|
|
|
if (!isDisposed)
|
2021-06-28 15:14:14 +08:00
|
|
|
{
|
2021-10-01 00:32:28 +08:00
|
|
|
// intentionally block context creation indefinitely. this ensures that nothing can start consuming a new context after disposal.
|
|
|
|
contextCreationLock.Wait();
|
2021-09-30 22:42:40 +08:00
|
|
|
contextCreationLock.Dispose();
|
2021-06-28 15:14:14 +08:00
|
|
|
|
2021-10-15 12:58:14 +08:00
|
|
|
isDisposed = true;
|
|
|
|
}
|
2021-06-28 15:14:14 +08:00
|
|
|
}
|
2021-01-07 13:07:36 +08:00
|
|
|
}
|
|
|
|
}
|