1
0

ServerDbManager.cs 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303
  1. using System.Collections.Immutable;
  2. using System.IO;
  3. using System.Net;
  4. using System.Text.Json;
  5. using System.Threading;
  6. using System.Threading.Tasks;
  7. using Content.Server.Administration.Logs;
  8. using Content.Shared.Administration.Logs;
  9. using Content.Shared.CCVar;
  10. using Content.Shared.Database;
  11. using Content.Shared.Preferences;
  12. using Content.Shared.Roles;
  13. using Microsoft.Data.Sqlite;
  14. using Microsoft.EntityFrameworkCore;
  15. using Microsoft.Extensions.Logging;
  16. using Npgsql;
  17. using Prometheus;
  18. using Robust.Shared.Configuration;
  19. using Robust.Shared.ContentPack;
  20. using Robust.Shared.Network;
  21. using Robust.Shared.Prototypes;
  22. using LogLevel = Robust.Shared.Log.LogLevel;
  23. using MSLogLevel = Microsoft.Extensions.Logging.LogLevel;
  24. namespace Content.Server.Database
  25. {
  26. public interface IServerDbManager
  27. {
  28. void Init();
  29. void Shutdown();
  30. #region Preferences
  31. Task<PlayerPreferences> InitPrefsAsync(
  32. NetUserId userId,
  33. ICharacterProfile defaultProfile,
  34. CancellationToken cancel);
  35. Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index);
  36. Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile? profile, int slot);
  37. Task SaveAdminOOCColorAsync(NetUserId userId, Color color);
  38. // Single method for two operations for transaction.
  39. Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot);
  40. Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel);
  41. #endregion
  42. #region User Ids
  43. // Username assignment (for guest accounts, so they persist GUID)
  44. Task AssignUserIdAsync(string name, NetUserId userId);
  45. Task<NetUserId?> GetAssignedUserIdAsync(string name);
  46. #endregion
  47. #region Bans
  48. /// <summary>
  49. /// Looks up a ban by id.
  50. /// This will return a pardoned ban as well.
  51. /// </summary>
  52. /// <param name="id">The ban id to look for.</param>
  53. /// <returns>The ban with the given id or null if none exist.</returns>
  54. Task<ServerBanDef?> GetServerBanAsync(int id);
  55. /// <summary>
  56. /// Looks up an user's most recent received un-pardoned ban.
  57. /// This will NOT return a pardoned ban.
  58. /// One of <see cref="address"/> or <see cref="userId"/> need to not be null.
  59. /// </summary>
  60. /// <param name="address">The ip address of the user.</param>
  61. /// <param name="userId">The id of the user.</param>
  62. /// <param name="hwId">The legacy HWID of the user.</param>
  63. /// <param name="modernHWIds">The modern HWIDs of the user.</param>
  64. /// <returns>The user's latest received un-pardoned ban, or null if none exist.</returns>
  65. Task<ServerBanDef?> GetServerBanAsync(
  66. IPAddress? address,
  67. NetUserId? userId,
  68. ImmutableArray<byte>? hwId,
  69. ImmutableArray<ImmutableArray<byte>>? modernHWIds);
  70. /// <summary>
  71. /// Looks up an user's ban history.
  72. /// One of <see cref="address"/> or <see cref="userId"/> need to not be null.
  73. /// </summary>
  74. /// <param name="address">The ip address of the user.</param>
  75. /// <param name="userId">The id of the user.</param>
  76. /// <param name="hwId">The legacy HWId of the user.</param>
  77. /// <param name="modernHWIds">The modern HWIDs of the user.</param>
  78. /// <param name="includeUnbanned">If true, bans that have been expired or pardoned are also included.</param>
  79. /// <returns>The user's ban history.</returns>
  80. Task<List<ServerBanDef>> GetServerBansAsync(
  81. IPAddress? address,
  82. NetUserId? userId,
  83. ImmutableArray<byte>? hwId,
  84. ImmutableArray<ImmutableArray<byte>>? modernHWIds,
  85. bool includeUnbanned=true);
  86. Task AddServerBanAsync(ServerBanDef serverBan);
  87. Task AddServerUnbanAsync(ServerUnbanDef serverBan);
  88. public Task EditServerBan(
  89. int id,
  90. string reason,
  91. NoteSeverity severity,
  92. DateTimeOffset? expiration,
  93. Guid editedBy,
  94. DateTimeOffset editedAt);
  95. /// <summary>
  96. /// Update ban exemption information for a player.
  97. /// </summary>
  98. /// <remarks>
  99. /// Database rows are automatically created and removed when appropriate.
  100. /// </remarks>
  101. /// <param name="userId">The user to update</param>
  102. /// <param name="flags">The new ban exemption flags.</param>
  103. Task UpdateBanExemption(NetUserId userId, ServerBanExemptFlags flags);
  104. /// <summary>
  105. /// Get current ban exemption flags for a user
  106. /// </summary>
  107. /// <returns><see cref="ServerBanExemptFlags.None"/> if the user is not exempt from any bans.</returns>
  108. Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId, CancellationToken cancel = default);
  109. #endregion
  110. #region Role Bans
  111. /// <summary>
  112. /// Looks up a role ban by id.
  113. /// This will return a pardoned role ban as well.
  114. /// </summary>
  115. /// <param name="id">The role ban id to look for.</param>
  116. /// <returns>The role ban with the given id or null if none exist.</returns>
  117. Task<ServerRoleBanDef?> GetServerRoleBanAsync(int id);
  118. /// <summary>
  119. /// Looks up an user's role ban history.
  120. /// This will return pardoned role bans based on the <see cref="includeUnbanned"/> bool.
  121. /// Requires one of <see cref="address"/>, <see cref="userId"/>, or <see cref="hwId"/> to not be null.
  122. /// </summary>
  123. /// <param name="address">The IP address of the user.</param>
  124. /// <param name="userId">The NetUserId of the user.</param>
  125. /// <param name="hwId">The Hardware Id of the user.</param>
  126. /// <param name="modernHWIds">The modern HWIDs of the user.</param>
  127. /// <param name="includeUnbanned">Whether expired and pardoned bans are included.</param>
  128. /// <returns>The user's role ban history.</returns>
  129. Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(
  130. IPAddress? address,
  131. NetUserId? userId,
  132. ImmutableArray<byte>? hwId,
  133. ImmutableArray<ImmutableArray<byte>>? modernHWIds,
  134. bool includeUnbanned = true);
  135. Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverBan);
  136. Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverBan);
  137. public Task EditServerRoleBan(
  138. int id,
  139. string reason,
  140. NoteSeverity severity,
  141. DateTimeOffset? expiration,
  142. Guid editedBy,
  143. DateTimeOffset editedAt);
  144. #endregion
  145. #region Playtime
  146. /// <summary>
  147. /// Look up a player's role timers.
  148. /// </summary>
  149. /// <param name="player">The player to get the role timer information from.</param>
  150. /// <param name="cancel"></param>
  151. /// <returns>All role timers belonging to the player.</returns>
  152. Task<List<PlayTime>> GetPlayTimes(Guid player, CancellationToken cancel = default);
  153. /// <summary>
  154. /// Update play time information in bulk.
  155. /// </summary>
  156. /// <param name="updates">The list of all updates to apply to the database.</param>
  157. Task UpdatePlayTimes(IReadOnlyCollection<PlayTimeUpdate> updates);
  158. #endregion
  159. #region Player Records
  160. Task UpdatePlayerRecordAsync(
  161. NetUserId userId,
  162. string userName,
  163. IPAddress address,
  164. ImmutableTypedHwid? hwId);
  165. Task<PlayerRecord?> GetPlayerRecordByUserName(string userName, CancellationToken cancel = default);
  166. Task<PlayerRecord?> GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel = default);
  167. #endregion
  168. #region Connection Logs
  169. /// <returns>ID of newly inserted connection log row.</returns>
  170. Task<int> AddConnectionLogAsync(
  171. NetUserId userId,
  172. string userName,
  173. IPAddress address,
  174. ImmutableTypedHwid? hwId,
  175. float trust,
  176. ConnectionDenyReason? denied,
  177. int serverId);
  178. Task AddServerBanHitsAsync(int connection, IEnumerable<ServerBanDef> bans);
  179. #endregion
  180. #region Admin Ranks
  181. Task<Admin?> GetAdminDataForAsync(NetUserId userId, CancellationToken cancel = default);
  182. Task<AdminRank?> GetAdminRankAsync(int id, CancellationToken cancel = default);
  183. Task<((Admin, string? lastUserName)[] admins, AdminRank[])> GetAllAdminAndRanksAsync(
  184. CancellationToken cancel = default);
  185. Task RemoveAdminAsync(NetUserId userId, CancellationToken cancel = default);
  186. Task AddAdminAsync(Admin admin, CancellationToken cancel = default);
  187. Task UpdateAdminAsync(Admin admin, CancellationToken cancel = default);
  188. /// <summary>
  189. /// Update whether an admin has voluntarily deadminned.
  190. /// </summary>
  191. /// <remarks>
  192. /// This does nothing if the player is not an admin.
  193. /// </remarks>
  194. /// <param name="userId">The user ID of the admin.</param>
  195. /// <param name="deadminned">Whether the admin is deadminned or not.</param>
  196. Task UpdateAdminDeadminnedAsync(NetUserId userId, bool deadminned, CancellationToken cancel = default);
  197. Task RemoveAdminRankAsync(int rankId, CancellationToken cancel = default);
  198. Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel = default);
  199. Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel = default);
  200. #endregion
  201. #region Rounds
  202. Task<int> AddNewRound(Server server, params Guid[] playerIds);
  203. Task<Round> GetRound(int id);
  204. Task AddRoundPlayers(int id, params Guid[] playerIds);
  205. #endregion
  206. #region Admin Logs
  207. Task<Server> AddOrGetServer(string serverName);
  208. Task AddAdminLogs(List<AdminLog> logs);
  209. IAsyncEnumerable<string> GetAdminLogMessages(LogFilter? filter = null);
  210. IAsyncEnumerable<SharedAdminLog> GetAdminLogs(LogFilter? filter = null);
  211. IAsyncEnumerable<JsonDocument> GetAdminLogsJson(LogFilter? filter = null);
  212. Task<int> CountAdminLogs(int round);
  213. #endregion
  214. #region Whitelist
  215. Task<bool> GetWhitelistStatusAsync(NetUserId player);
  216. Task AddToWhitelistAsync(NetUserId player);
  217. Task RemoveFromWhitelistAsync(NetUserId player);
  218. #endregion
  219. #region Blacklist
  220. Task<bool> GetBlacklistStatusAsync(NetUserId player);
  221. Task AddToBlacklistAsync(NetUserId player);
  222. Task RemoveFromBlacklistAsync(NetUserId player);
  223. #endregion
  224. #region Uploaded Resources Logs
  225. Task AddUploadedResourceLogAsync(NetUserId user, DateTimeOffset date, string path, byte[] data);
  226. Task PurgeUploadedResourceLogAsync(int days);
  227. #endregion
  228. #region Rules
  229. Task<DateTimeOffset?> GetLastReadRules(NetUserId player);
  230. Task SetLastReadRules(NetUserId player, DateTimeOffset? time);
  231. #endregion
  232. #region Admin Notes
  233. Task<int> AddAdminNote(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, NoteSeverity severity, bool secret, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime);
  234. Task<int> AddAdminWatchlist(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime);
  235. Task<int> AddAdminMessage(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime);
  236. Task<AdminNoteRecord?> GetAdminNote(int id);
  237. Task<AdminWatchlistRecord?> GetAdminWatchlist(int id);
  238. Task<AdminMessageRecord?> GetAdminMessage(int id);
  239. Task<ServerBanNoteRecord?> GetServerBanAsNoteAsync(int id);
  240. Task<ServerRoleBanNoteRecord?> GetServerRoleBanAsNoteAsync(int id);
  241. Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player);
  242. Task<List<IAdminRemarksRecord>> GetVisibleAdminNotes(Guid player);
  243. Task<List<AdminWatchlistRecord>> GetActiveWatchlists(Guid player);
  244. Task<List<AdminMessageRecord>> GetMessages(Guid player);
  245. Task EditAdminNote(int id, string message, NoteSeverity severity, bool secret, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime);
  246. Task EditAdminWatchlist(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime);
  247. Task EditAdminMessage(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime);
  248. Task DeleteAdminNote(int id, Guid deletedBy, DateTimeOffset deletedAt);
  249. Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTimeOffset deletedAt);
  250. Task DeleteAdminMessage(int id, Guid deletedBy, DateTimeOffset deletedAt);
  251. Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt);
  252. Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt);
  253. /// <summary>
  254. /// Mark an admin message as being seen by the target player.
  255. /// </summary>
  256. /// <param name="id">The database ID of the admin message.</param>
  257. /// <param name="dismissedToo">
  258. /// If true, the message is "permanently dismissed" and will not be shown to the player again when they join.
  259. /// </param>
  260. Task MarkMessageAsSeen(int id, bool dismissedToo);
  261. #endregion
  262. #region Job Whitelists
  263. Task AddJobWhitelist(Guid player, ProtoId<JobPrototype> job);
  264. Task<List<string>> GetJobWhitelists(Guid player, CancellationToken cancel = default);
  265. Task<bool> IsJobWhitelisted(Guid player, ProtoId<JobPrototype> job);
  266. Task<bool> RemoveJobWhitelist(Guid player, ProtoId<JobPrototype> job);
  267. #endregion
  268. #region IPintel
  269. Task<bool> UpsertIPIntelCache(DateTime time, IPAddress ip, float score);
  270. Task<IPIntelCache?> GetIPIntelCache(IPAddress ip);
  271. Task<bool> CleanIPIntelCache(TimeSpan range);
  272. #endregion
  273. #region DB Notifications
  274. void SubscribeToNotifications(Action<DatabaseNotification> handler);
  275. /// <summary>
  276. /// Inject a notification as if it was created by the database. This is intended for testing.
  277. /// </summary>
  278. /// <param name="notification">The notification to trigger</param>
  279. void InjectTestNotification(DatabaseNotification notification);
  280. /// <summary>
  281. /// Send a notification to all other servers connected to the same database.
  282. /// </summary>
  283. /// <remarks>
  284. /// The local server will receive the sent notification itself again.
  285. /// </remarks>
  286. /// <param name="notification">The notification to send.</param>
  287. Task SendNotification(DatabaseNotification notification);
  288. #endregion
  289. }
  290. /// <summary>
  291. /// Represents a notification sent between servers via the database layer.
  292. /// </summary>
  293. /// <remarks>
  294. /// <para>
  295. /// Database notifications are a simple system to broadcast messages to an entire server group
  296. /// backed by the same database. For example, this is used to notify all servers of new ban records.
  297. /// </para>
  298. /// <para>
  299. /// They are currently implemented by the PostgreSQL <c>NOTIFY</c> and <c>LISTEN</c> commands.
  300. /// </para>
  301. /// </remarks>
  302. public struct DatabaseNotification
  303. {
  304. /// <summary>
  305. /// The channel for the notification. This can be used to differentiate notifications for different purposes.
  306. /// </summary>
  307. public required string Channel { get; set; }
  308. /// <summary>
  309. /// The actual contents of the notification. Optional.
  310. /// </summary>
  311. public string? Payload { get; set; }
  312. }
  313. public sealed class ServerDbManager : IServerDbManager
  314. {
  315. public static readonly Counter DbReadOpsMetric = Metrics.CreateCounter(
  316. "db_read_ops",
  317. "Amount of read operations processed by the database manager.");
  318. public static readonly Counter DbWriteOpsMetric = Metrics.CreateCounter(
  319. "db_write_ops",
  320. "Amount of write operations processed by the database manager.");
  321. public static readonly Gauge DbActiveOps = Metrics.CreateGauge(
  322. "db_executing_ops",
  323. "Amount of active database operations. Note that some operations may be waiting for a database connection.");
  324. [Dependency] private readonly IConfigurationManager _cfg = default!;
  325. [Dependency] private readonly IResourceManager _res = default!;
  326. [Dependency] private readonly ILogManager _logMgr = default!;
  327. private ServerDbBase _db = default!;
  328. private LoggingProvider _msLogProvider = default!;
  329. private ILoggerFactory _msLoggerFactory = default!;
  330. private bool _synchronous;
  331. // When running in integration tests, we'll use a single in-memory SQLite database connection.
  332. // This is that connection, close it when we shut down.
  333. private SqliteConnection? _sqliteInMemoryConnection;
  334. private readonly List<Action<DatabaseNotification>> _notificationHandlers = [];
  335. public void Init()
  336. {
  337. _msLogProvider = new LoggingProvider(_logMgr);
  338. _msLoggerFactory = LoggerFactory.Create(builder =>
  339. {
  340. builder.AddProvider(_msLogProvider);
  341. });
  342. _synchronous = _cfg.GetCVar(CCVars.DatabaseSynchronous);
  343. var engine = _cfg.GetCVar(CCVars.DatabaseEngine).ToLower();
  344. var opsLog = _logMgr.GetSawmill("db.op");
  345. var notifyLog = _logMgr.GetSawmill("db.notify");
  346. switch (engine)
  347. {
  348. case "sqlite":
  349. SetupSqlite(out var contextFunc, out var inMemory);
  350. _db = new ServerDbSqlite(contextFunc, inMemory, _cfg, _synchronous, opsLog);
  351. break;
  352. case "postgres":
  353. var (pgOptions, conString) = CreatePostgresOptions();
  354. _db = new ServerDbPostgres(pgOptions, conString, _cfg, opsLog, notifyLog);
  355. break;
  356. default:
  357. throw new InvalidDataException($"Unknown database engine {engine}.");
  358. }
  359. _db.OnNotificationReceived += HandleDatabaseNotification;
  360. }
  361. public void Shutdown()
  362. {
  363. _db.OnNotificationReceived -= HandleDatabaseNotification;
  364. _sqliteInMemoryConnection?.Dispose();
  365. _db.Shutdown();
  366. }
  367. public Task<PlayerPreferences> InitPrefsAsync(
  368. NetUserId userId,
  369. ICharacterProfile defaultProfile,
  370. CancellationToken cancel)
  371. {
  372. DbWriteOpsMetric.Inc();
  373. return RunDbCommand(() => _db.InitPrefsAsync(userId, defaultProfile));
  374. }
  375. public Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index)
  376. {
  377. DbWriteOpsMetric.Inc();
  378. return RunDbCommand(() => _db.SaveSelectedCharacterIndexAsync(userId, index));
  379. }
  380. public Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile? profile, int slot)
  381. {
  382. DbWriteOpsMetric.Inc();
  383. return RunDbCommand(() => _db.SaveCharacterSlotAsync(userId, profile, slot));
  384. }
  385. public Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot)
  386. {
  387. DbWriteOpsMetric.Inc();
  388. return RunDbCommand(() => _db.DeleteSlotAndSetSelectedIndex(userId, deleteSlot, newSlot));
  389. }
  390. public Task SaveAdminOOCColorAsync(NetUserId userId, Color color)
  391. {
  392. DbWriteOpsMetric.Inc();
  393. return RunDbCommand(() => _db.SaveAdminOOCColorAsync(userId, color));
  394. }
  395. public Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel)
  396. {
  397. DbReadOpsMetric.Inc();
  398. return RunDbCommand(() => _db.GetPlayerPreferencesAsync(userId, cancel));
  399. }
  400. public Task AssignUserIdAsync(string name, NetUserId userId)
  401. {
  402. DbWriteOpsMetric.Inc();
  403. return RunDbCommand(() => _db.AssignUserIdAsync(name, userId));
  404. }
  405. public Task<NetUserId?> GetAssignedUserIdAsync(string name)
  406. {
  407. DbReadOpsMetric.Inc();
  408. return RunDbCommand(() => _db.GetAssignedUserIdAsync(name));
  409. }
  410. public Task<ServerBanDef?> GetServerBanAsync(int id)
  411. {
  412. DbReadOpsMetric.Inc();
  413. return RunDbCommand(() => _db.GetServerBanAsync(id));
  414. }
  415. public Task<ServerBanDef?> GetServerBanAsync(
  416. IPAddress? address,
  417. NetUserId? userId,
  418. ImmutableArray<byte>? hwId,
  419. ImmutableArray<ImmutableArray<byte>>? modernHWIds)
  420. {
  421. DbReadOpsMetric.Inc();
  422. return RunDbCommand(() => _db.GetServerBanAsync(address, userId, hwId, modernHWIds));
  423. }
  424. public Task<List<ServerBanDef>> GetServerBansAsync(
  425. IPAddress? address,
  426. NetUserId? userId,
  427. ImmutableArray<byte>? hwId,
  428. ImmutableArray<ImmutableArray<byte>>? modernHWIds,
  429. bool includeUnbanned=true)
  430. {
  431. DbReadOpsMetric.Inc();
  432. return RunDbCommand(() => _db.GetServerBansAsync(address, userId, hwId, modernHWIds, includeUnbanned));
  433. }
  434. public Task AddServerBanAsync(ServerBanDef serverBan)
  435. {
  436. DbWriteOpsMetric.Inc();
  437. return RunDbCommand(() => _db.AddServerBanAsync(serverBan));
  438. }
  439. public Task AddServerUnbanAsync(ServerUnbanDef serverUnban)
  440. {
  441. DbWriteOpsMetric.Inc();
  442. return RunDbCommand(() => _db.AddServerUnbanAsync(serverUnban));
  443. }
  444. public Task EditServerBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt)
  445. {
  446. DbWriteOpsMetric.Inc();
  447. return RunDbCommand(() => _db.EditServerBan(id, reason, severity, expiration, editedBy, editedAt));
  448. }
  449. public Task UpdateBanExemption(NetUserId userId, ServerBanExemptFlags flags)
  450. {
  451. DbWriteOpsMetric.Inc();
  452. return RunDbCommand(() => _db.UpdateBanExemption(userId, flags));
  453. }
  454. public Task<ServerBanExemptFlags> GetBanExemption(NetUserId userId, CancellationToken cancel = default)
  455. {
  456. DbReadOpsMetric.Inc();
  457. return RunDbCommand(() => _db.GetBanExemption(userId, cancel));
  458. }
  459. #region Role Ban
  460. public Task<ServerRoleBanDef?> GetServerRoleBanAsync(int id)
  461. {
  462. DbReadOpsMetric.Inc();
  463. return RunDbCommand(() => _db.GetServerRoleBanAsync(id));
  464. }
  465. public Task<List<ServerRoleBanDef>> GetServerRoleBansAsync(
  466. IPAddress? address,
  467. NetUserId? userId,
  468. ImmutableArray<byte>? hwId,
  469. ImmutableArray<ImmutableArray<byte>>? modernHWIds,
  470. bool includeUnbanned = true)
  471. {
  472. DbReadOpsMetric.Inc();
  473. return RunDbCommand(() => _db.GetServerRoleBansAsync(address, userId, hwId, modernHWIds, includeUnbanned));
  474. }
  475. public Task<ServerRoleBanDef> AddServerRoleBanAsync(ServerRoleBanDef serverRoleBan)
  476. {
  477. DbWriteOpsMetric.Inc();
  478. return RunDbCommand(() => _db.AddServerRoleBanAsync(serverRoleBan));
  479. }
  480. public Task AddServerRoleUnbanAsync(ServerRoleUnbanDef serverRoleUnban)
  481. {
  482. DbWriteOpsMetric.Inc();
  483. return RunDbCommand(() => _db.AddServerRoleUnbanAsync(serverRoleUnban));
  484. }
  485. public Task EditServerRoleBan(int id, string reason, NoteSeverity severity, DateTimeOffset? expiration, Guid editedBy, DateTimeOffset editedAt)
  486. {
  487. DbWriteOpsMetric.Inc();
  488. return RunDbCommand(() => _db.EditServerRoleBan(id, reason, severity, expiration, editedBy, editedAt));
  489. }
  490. #endregion
  491. #region Playtime
  492. public Task<List<PlayTime>> GetPlayTimes(Guid player, CancellationToken cancel)
  493. {
  494. DbReadOpsMetric.Inc();
  495. return RunDbCommand(() => _db.GetPlayTimes(player, cancel));
  496. }
  497. public Task UpdatePlayTimes(IReadOnlyCollection<PlayTimeUpdate> updates)
  498. {
  499. DbWriteOpsMetric.Inc();
  500. return RunDbCommand(() => _db.UpdatePlayTimes(updates));
  501. }
  502. #endregion
  503. public Task UpdatePlayerRecordAsync(
  504. NetUserId userId,
  505. string userName,
  506. IPAddress address,
  507. ImmutableTypedHwid? hwId)
  508. {
  509. DbWriteOpsMetric.Inc();
  510. return RunDbCommand(() => _db.UpdatePlayerRecord(userId, userName, address, hwId));
  511. }
  512. public Task<PlayerRecord?> GetPlayerRecordByUserName(string userName, CancellationToken cancel = default)
  513. {
  514. DbReadOpsMetric.Inc();
  515. return RunDbCommand(() => _db.GetPlayerRecordByUserName(userName, cancel));
  516. }
  517. public Task<PlayerRecord?> GetPlayerRecordByUserId(NetUserId userId, CancellationToken cancel = default)
  518. {
  519. DbReadOpsMetric.Inc();
  520. return RunDbCommand(() => _db.GetPlayerRecordByUserId(userId, cancel));
  521. }
  522. public Task<int> AddConnectionLogAsync(
  523. NetUserId userId,
  524. string userName,
  525. IPAddress address,
  526. ImmutableTypedHwid? hwId,
  527. float trust,
  528. ConnectionDenyReason? denied,
  529. int serverId)
  530. {
  531. DbWriteOpsMetric.Inc();
  532. return RunDbCommand(() => _db.AddConnectionLogAsync(userId, userName, address, hwId, trust, denied, serverId));
  533. }
  534. public Task AddServerBanHitsAsync(int connection, IEnumerable<ServerBanDef> bans)
  535. {
  536. DbWriteOpsMetric.Inc();
  537. return RunDbCommand(() => _db.AddServerBanHitsAsync(connection, bans));
  538. }
  539. public Task<Admin?> GetAdminDataForAsync(NetUserId userId, CancellationToken cancel = default)
  540. {
  541. DbReadOpsMetric.Inc();
  542. return RunDbCommand(() => _db.GetAdminDataForAsync(userId, cancel));
  543. }
  544. public Task<AdminRank?> GetAdminRankAsync(int id, CancellationToken cancel = default)
  545. {
  546. DbReadOpsMetric.Inc();
  547. return RunDbCommand(() => _db.GetAdminRankDataForAsync(id, cancel));
  548. }
  549. public Task<((Admin, string? lastUserName)[] admins, AdminRank[])> GetAllAdminAndRanksAsync(
  550. CancellationToken cancel = default)
  551. {
  552. DbReadOpsMetric.Inc();
  553. return RunDbCommand(() => _db.GetAllAdminAndRanksAsync(cancel));
  554. }
  555. public Task RemoveAdminAsync(NetUserId userId, CancellationToken cancel = default)
  556. {
  557. DbWriteOpsMetric.Inc();
  558. return RunDbCommand(() => _db.RemoveAdminAsync(userId, cancel));
  559. }
  560. public Task AddAdminAsync(Admin admin, CancellationToken cancel = default)
  561. {
  562. DbWriteOpsMetric.Inc();
  563. return RunDbCommand(() => _db.AddAdminAsync(admin, cancel));
  564. }
  565. public Task UpdateAdminAsync(Admin admin, CancellationToken cancel = default)
  566. {
  567. DbWriteOpsMetric.Inc();
  568. return RunDbCommand(() => _db.UpdateAdminAsync(admin, cancel));
  569. }
  570. public Task UpdateAdminDeadminnedAsync(NetUserId userId, bool deadminned, CancellationToken cancel = default)
  571. {
  572. DbWriteOpsMetric.Inc();
  573. return RunDbCommand(() => _db.UpdateAdminDeadminnedAsync(userId, deadminned, cancel));
  574. }
  575. public Task RemoveAdminRankAsync(int rankId, CancellationToken cancel = default)
  576. {
  577. DbWriteOpsMetric.Inc();
  578. return RunDbCommand(() => _db.RemoveAdminRankAsync(rankId, cancel));
  579. }
  580. public Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel = default)
  581. {
  582. DbWriteOpsMetric.Inc();
  583. return RunDbCommand(() => _db.AddAdminRankAsync(rank, cancel));
  584. }
  585. public Task<int> AddNewRound(Server server, params Guid[] playerIds)
  586. {
  587. DbWriteOpsMetric.Inc();
  588. return RunDbCommand(() => _db.AddNewRound(server, playerIds));
  589. }
  590. public Task<Round> GetRound(int id)
  591. {
  592. DbReadOpsMetric.Inc();
  593. return RunDbCommand(() => _db.GetRound(id));
  594. }
  595. public Task AddRoundPlayers(int id, params Guid[] playerIds)
  596. {
  597. DbWriteOpsMetric.Inc();
  598. return RunDbCommand(() => _db.AddRoundPlayers(id, playerIds));
  599. }
  600. public Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel = default)
  601. {
  602. DbWriteOpsMetric.Inc();
  603. return RunDbCommand(() => _db.UpdateAdminRankAsync(rank, cancel));
  604. }
  605. public async Task<Server> AddOrGetServer(string serverName)
  606. {
  607. var (server, existed) = await RunDbCommand(() => _db.AddOrGetServer(serverName));
  608. if (existed)
  609. DbReadOpsMetric.Inc();
  610. else
  611. DbWriteOpsMetric.Inc();
  612. return server;
  613. }
  614. public Task AddAdminLogs(List<AdminLog> logs)
  615. {
  616. DbWriteOpsMetric.Inc();
  617. return RunDbCommand(() => _db.AddAdminLogs(logs));
  618. }
  619. public IAsyncEnumerable<string> GetAdminLogMessages(LogFilter? filter = null)
  620. {
  621. DbReadOpsMetric.Inc();
  622. return RunDbCommand(() => _db.GetAdminLogMessages(filter));
  623. }
  624. public IAsyncEnumerable<SharedAdminLog> GetAdminLogs(LogFilter? filter = null)
  625. {
  626. DbReadOpsMetric.Inc();
  627. return RunDbCommand(() => _db.GetAdminLogs(filter));
  628. }
  629. public IAsyncEnumerable<JsonDocument> GetAdminLogsJson(LogFilter? filter = null)
  630. {
  631. DbReadOpsMetric.Inc();
  632. return RunDbCommand(() => _db.GetAdminLogsJson(filter));
  633. }
  634. public Task<int> CountAdminLogs(int round)
  635. {
  636. DbReadOpsMetric.Inc();
  637. return RunDbCommand(() => _db.CountAdminLogs(round));
  638. }
  639. public Task<bool> GetWhitelistStatusAsync(NetUserId player)
  640. {
  641. DbReadOpsMetric.Inc();
  642. return RunDbCommand(() => _db.GetWhitelistStatusAsync(player));
  643. }
  644. public Task AddToWhitelistAsync(NetUserId player)
  645. {
  646. DbWriteOpsMetric.Inc();
  647. return RunDbCommand(() => _db.AddToWhitelistAsync(player));
  648. }
  649. public Task RemoveFromWhitelistAsync(NetUserId player)
  650. {
  651. DbWriteOpsMetric.Inc();
  652. return RunDbCommand(() => _db.RemoveFromWhitelistAsync(player));
  653. }
  654. public Task<bool> GetBlacklistStatusAsync(NetUserId player)
  655. {
  656. DbReadOpsMetric.Inc();
  657. return RunDbCommand(() => _db.GetBlacklistStatusAsync(player));
  658. }
  659. public Task AddToBlacklistAsync(NetUserId player)
  660. {
  661. DbWriteOpsMetric.Inc();
  662. return RunDbCommand(() => _db.AddToBlacklistAsync(player));
  663. }
  664. public Task RemoveFromBlacklistAsync(NetUserId player)
  665. {
  666. DbWriteOpsMetric.Inc();
  667. return RunDbCommand(() => _db.RemoveFromBlacklistAsync(player));
  668. }
  669. public Task AddUploadedResourceLogAsync(NetUserId user, DateTimeOffset date, string path, byte[] data)
  670. {
  671. DbWriteOpsMetric.Inc();
  672. return RunDbCommand(() => _db.AddUploadedResourceLogAsync(user, date, path, data));
  673. }
  674. public Task PurgeUploadedResourceLogAsync(int days)
  675. {
  676. DbWriteOpsMetric.Inc();
  677. return RunDbCommand(() => _db.PurgeUploadedResourceLogAsync(days));
  678. }
  679. public Task<DateTimeOffset?> GetLastReadRules(NetUserId player)
  680. {
  681. DbReadOpsMetric.Inc();
  682. return RunDbCommand(() => _db.GetLastReadRules(player));
  683. }
  684. public Task SetLastReadRules(NetUserId player, DateTimeOffset? time)
  685. {
  686. DbWriteOpsMetric.Inc();
  687. return RunDbCommand(() => _db.SetLastReadRules(player, time));
  688. }
  689. public Task<int> AddAdminNote(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, NoteSeverity severity, bool secret, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime)
  690. {
  691. DbWriteOpsMetric.Inc();
  692. var note = new AdminNote
  693. {
  694. RoundId = roundId,
  695. CreatedById = createdBy,
  696. LastEditedById = createdBy,
  697. PlayerUserId = player,
  698. PlaytimeAtNote = playtimeAtNote,
  699. Message = message,
  700. Severity = severity,
  701. Secret = secret,
  702. CreatedAt = createdAt.UtcDateTime,
  703. LastEditedAt = createdAt.UtcDateTime,
  704. ExpirationTime = expiryTime?.UtcDateTime
  705. };
  706. return RunDbCommand(() => _db.AddAdminNote(note));
  707. }
  708. public Task<int> AddAdminWatchlist(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime)
  709. {
  710. DbWriteOpsMetric.Inc();
  711. var note = new AdminWatchlist
  712. {
  713. RoundId = roundId,
  714. CreatedById = createdBy,
  715. LastEditedById = createdBy,
  716. PlayerUserId = player,
  717. PlaytimeAtNote = playtimeAtNote,
  718. Message = message,
  719. CreatedAt = createdAt.UtcDateTime,
  720. LastEditedAt = createdAt.UtcDateTime,
  721. ExpirationTime = expiryTime?.UtcDateTime
  722. };
  723. return RunDbCommand(() => _db.AddAdminWatchlist(note));
  724. }
  725. public Task<int> AddAdminMessage(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime)
  726. {
  727. DbWriteOpsMetric.Inc();
  728. var note = new AdminMessage
  729. {
  730. RoundId = roundId,
  731. CreatedById = createdBy,
  732. LastEditedById = createdBy,
  733. PlayerUserId = player,
  734. PlaytimeAtNote = playtimeAtNote,
  735. Message = message,
  736. CreatedAt = createdAt.UtcDateTime,
  737. LastEditedAt = createdAt.UtcDateTime,
  738. ExpirationTime = expiryTime?.UtcDateTime
  739. };
  740. return RunDbCommand(() => _db.AddAdminMessage(note));
  741. }
  742. public Task<AdminNoteRecord?> GetAdminNote(int id)
  743. {
  744. DbReadOpsMetric.Inc();
  745. return RunDbCommand(() => _db.GetAdminNote(id));
  746. }
  747. public Task<AdminWatchlistRecord?> GetAdminWatchlist(int id)
  748. {
  749. DbReadOpsMetric.Inc();
  750. return RunDbCommand(() => _db.GetAdminWatchlist(id));
  751. }
  752. public Task<AdminMessageRecord?> GetAdminMessage(int id)
  753. {
  754. DbReadOpsMetric.Inc();
  755. return RunDbCommand(() => _db.GetAdminMessage(id));
  756. }
  757. public Task<ServerBanNoteRecord?> GetServerBanAsNoteAsync(int id)
  758. {
  759. DbReadOpsMetric.Inc();
  760. return RunDbCommand(() => _db.GetServerBanAsNoteAsync(id));
  761. }
  762. public Task<ServerRoleBanNoteRecord?> GetServerRoleBanAsNoteAsync(int id)
  763. {
  764. DbReadOpsMetric.Inc();
  765. return RunDbCommand(() => _db.GetServerRoleBanAsNoteAsync(id));
  766. }
  767. public Task<List<IAdminRemarksRecord>> GetAllAdminRemarks(Guid player)
  768. {
  769. DbReadOpsMetric.Inc();
  770. return RunDbCommand(() => _db.GetAllAdminRemarks(player));
  771. }
  772. public Task<List<IAdminRemarksRecord>> GetVisibleAdminNotes(Guid player)
  773. {
  774. DbReadOpsMetric.Inc();
  775. return RunDbCommand(() => _db.GetVisibleAdminRemarks(player));
  776. }
  777. public Task<List<AdminWatchlistRecord>> GetActiveWatchlists(Guid player)
  778. {
  779. DbReadOpsMetric.Inc();
  780. return RunDbCommand(() => _db.GetActiveWatchlists(player));
  781. }
  782. public Task<List<AdminMessageRecord>> GetMessages(Guid player)
  783. {
  784. DbReadOpsMetric.Inc();
  785. return RunDbCommand(() => _db.GetMessages(player));
  786. }
  787. public Task EditAdminNote(int id, string message, NoteSeverity severity, bool secret, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime)
  788. {
  789. DbWriteOpsMetric.Inc();
  790. return RunDbCommand(() => _db.EditAdminNote(id, message, severity, secret, editedBy, editedAt, expiryTime));
  791. }
  792. public Task EditAdminWatchlist(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime)
  793. {
  794. DbWriteOpsMetric.Inc();
  795. return RunDbCommand(() => _db.EditAdminWatchlist(id, message, editedBy, editedAt, expiryTime));
  796. }
  797. public Task EditAdminMessage(int id, string message, Guid editedBy, DateTimeOffset editedAt, DateTimeOffset? expiryTime)
  798. {
  799. DbWriteOpsMetric.Inc();
  800. return RunDbCommand(() => _db.EditAdminMessage(id, message, editedBy, editedAt, expiryTime));
  801. }
  802. public Task DeleteAdminNote(int id, Guid deletedBy, DateTimeOffset deletedAt)
  803. {
  804. DbWriteOpsMetric.Inc();
  805. return RunDbCommand(() => _db.DeleteAdminNote(id, deletedBy, deletedAt));
  806. }
  807. public Task DeleteAdminWatchlist(int id, Guid deletedBy, DateTimeOffset deletedAt)
  808. {
  809. DbWriteOpsMetric.Inc();
  810. return RunDbCommand(() => _db.DeleteAdminWatchlist(id, deletedBy, deletedAt));
  811. }
  812. public Task DeleteAdminMessage(int id, Guid deletedBy, DateTimeOffset deletedAt)
  813. {
  814. DbWriteOpsMetric.Inc();
  815. return RunDbCommand(() => _db.DeleteAdminMessage(id, deletedBy, deletedAt));
  816. }
  817. public Task HideServerBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt)
  818. {
  819. DbWriteOpsMetric.Inc();
  820. return RunDbCommand(() => _db.HideServerBanFromNotes(id, deletedBy, deletedAt));
  821. }
  822. public Task HideServerRoleBanFromNotes(int id, Guid deletedBy, DateTimeOffset deletedAt)
  823. {
  824. DbWriteOpsMetric.Inc();
  825. return RunDbCommand(() => _db.HideServerRoleBanFromNotes(id, deletedBy, deletedAt));
  826. }
  827. public Task MarkMessageAsSeen(int id, bool dismissedToo)
  828. {
  829. DbWriteOpsMetric.Inc();
  830. return RunDbCommand(() => _db.MarkMessageAsSeen(id, dismissedToo));
  831. }
  832. public Task AddJobWhitelist(Guid player, ProtoId<JobPrototype> job)
  833. {
  834. DbWriteOpsMetric.Inc();
  835. return RunDbCommand(() => _db.AddJobWhitelist(player, job));
  836. }
  837. public Task<List<string>> GetJobWhitelists(Guid player, CancellationToken cancel = default)
  838. {
  839. DbReadOpsMetric.Inc();
  840. return RunDbCommand(() => _db.GetJobWhitelists(player, cancel));
  841. }
  842. public Task<bool> IsJobWhitelisted(Guid player, ProtoId<JobPrototype> job)
  843. {
  844. DbReadOpsMetric.Inc();
  845. return RunDbCommand(() => _db.IsJobWhitelisted(player, job));
  846. }
  847. public Task<bool> RemoveJobWhitelist(Guid player, ProtoId<JobPrototype> job)
  848. {
  849. DbWriteOpsMetric.Inc();
  850. return RunDbCommand(() => _db.RemoveJobWhitelist(player, job));
  851. }
  852. public Task<bool> UpsertIPIntelCache(DateTime time, IPAddress ip, float score)
  853. {
  854. DbWriteOpsMetric.Inc();
  855. return RunDbCommand(() => _db.UpsertIPIntelCache(time, ip, score));
  856. }
  857. public Task<IPIntelCache?> GetIPIntelCache(IPAddress ip)
  858. {
  859. return RunDbCommand(() => _db.GetIPIntelCache(ip));
  860. }
  861. public Task<bool> CleanIPIntelCache(TimeSpan range)
  862. {
  863. DbWriteOpsMetric.Inc();
  864. return RunDbCommand(() => _db.CleanIPIntelCache(range));
  865. }
  866. public void SubscribeToNotifications(Action<DatabaseNotification> handler)
  867. {
  868. lock (_notificationHandlers)
  869. {
  870. _notificationHandlers.Add(handler);
  871. }
  872. }
  873. public void InjectTestNotification(DatabaseNotification notification)
  874. {
  875. HandleDatabaseNotification(notification);
  876. }
  877. public Task SendNotification(DatabaseNotification notification)
  878. {
  879. DbWriteOpsMetric.Inc();
  880. return RunDbCommand(() => _db.SendNotification(notification));
  881. }
  882. private async void HandleDatabaseNotification(DatabaseNotification notification)
  883. {
  884. lock (_notificationHandlers)
  885. {
  886. foreach (var handler in _notificationHandlers)
  887. {
  888. handler(notification);
  889. }
  890. }
  891. }
  892. // Wrapper functions to run DB commands from the thread pool.
  893. // This will avoid SynchronizationContext capturing and avoid running CPU work on the main thread.
  894. // For SQLite, this will also enable read parallelization (within limits).
  895. //
  896. // If we're configured to be synchronous (for integration tests) we shouldn't thread pool it,
  897. // as that would make things very random and undeterministic.
  898. // That only works on SQLite though, since SQLite is internally synchronous anyways.
  899. private async Task<T> RunDbCommand<T>(Func<Task<T>> command)
  900. {
  901. using var _ = DbActiveOps.TrackInProgress();
  902. if (_synchronous)
  903. return await RunDbCommandCoreSync(command);
  904. return await Task.Run(command);
  905. }
  906. private async Task RunDbCommand(Func<Task> command)
  907. {
  908. using var _ = DbActiveOps.TrackInProgress();
  909. if (_synchronous)
  910. {
  911. await RunDbCommandCoreSync(command);
  912. return;
  913. }
  914. await Task.Run(command);
  915. }
  916. private static T RunDbCommandCoreSync<T>(Func<T> command) where T : IAsyncResult
  917. {
  918. var task = command();
  919. if (!task.IsCompleted)
  920. {
  921. // We can't just do BlockWaitOnTask here, because that could cause deadlocks.
  922. // This flag is only intended for integration tests. If we trip this, it's a bug.
  923. throw new InvalidOperationException(
  924. "Database task is running asynchronously. " +
  925. "This should be impossible when the database is set to synchronous.");
  926. }
  927. return task;
  928. }
  929. private IAsyncEnumerable<T> RunDbCommand<T>(Func<IAsyncEnumerable<T>> command)
  930. {
  931. var enumerable = command();
  932. if (_synchronous)
  933. return new SyncAsyncEnumerable<T>(enumerable);
  934. return enumerable;
  935. }
  936. private (DbContextOptions<PostgresServerDbContext> options, string connectionString) CreatePostgresOptions()
  937. {
  938. var host = _cfg.GetCVar(CCVars.DatabasePgHost);
  939. var port = _cfg.GetCVar(CCVars.DatabasePgPort);
  940. var db = _cfg.GetCVar(CCVars.DatabasePgDatabase);
  941. var user = _cfg.GetCVar(CCVars.DatabasePgUsername);
  942. var pass = _cfg.GetCVar(CCVars.DatabasePgPassword);
  943. var builder = new DbContextOptionsBuilder<PostgresServerDbContext>();
  944. var connectionString = new NpgsqlConnectionStringBuilder
  945. {
  946. Host = host,
  947. Port = port,
  948. Database = db,
  949. Username = user,
  950. Password = pass
  951. }.ConnectionString;
  952. Logger.DebugS("db.manager", $"Using Postgres \"{host}:{port}/{db}\"");
  953. builder.UseNpgsql(connectionString);
  954. SetupLogging(builder);
  955. return (builder.Options, connectionString);
  956. }
  957. private void SetupSqlite(out Func<DbContextOptions<SqliteServerDbContext>> contextFunc, out bool inMemory)
  958. {
  959. #if USE_SYSTEM_SQLITE
  960. SQLitePCL.raw.SetProvider(new SQLitePCL.SQLite3Provider_sqlite3());
  961. #endif
  962. // Can't re-use the SqliteConnection across multiple threads, so we have to make it every time.
  963. Func<SqliteConnection> getConnection;
  964. var configPreferencesDbPath = _cfg.GetCVar(CCVars.DatabaseSqliteDbPath);
  965. inMemory = _res.UserData.RootDir == null;
  966. if (!inMemory)
  967. {
  968. var finalPreferencesDbPath = Path.Combine(_res.UserData.RootDir!, configPreferencesDbPath);
  969. Logger.DebugS("db.manager", $"Using SQLite DB \"{finalPreferencesDbPath}\"");
  970. getConnection = () => new SqliteConnection($"Data Source={finalPreferencesDbPath}");
  971. }
  972. else
  973. {
  974. Logger.DebugS("db.manager", "Using in-memory SQLite DB");
  975. _sqliteInMemoryConnection = new SqliteConnection("Data Source=:memory:");
  976. // When using an in-memory DB we have to open it manually
  977. // so EFCore doesn't open, close and wipe it every operation.
  978. _sqliteInMemoryConnection.Open();
  979. getConnection = () => _sqliteInMemoryConnection;
  980. }
  981. contextFunc = () =>
  982. {
  983. var builder = new DbContextOptionsBuilder<SqliteServerDbContext>();
  984. builder.UseSqlite(getConnection());
  985. SetupLogging(builder);
  986. return builder.Options;
  987. };
  988. }
  989. private void SetupLogging(DbContextOptionsBuilder builder)
  990. {
  991. builder.UseLoggerFactory(_msLoggerFactory);
  992. }
  993. private sealed class LoggingProvider : ILoggerProvider
  994. {
  995. private readonly ILogManager _logManager;
  996. public LoggingProvider(ILogManager logManager)
  997. {
  998. _logManager = logManager;
  999. }
  1000. public void Dispose()
  1001. {
  1002. }
  1003. public ILogger CreateLogger(string categoryName)
  1004. {
  1005. return new MSLogger(_logManager.GetSawmill("db.ef"));
  1006. }
  1007. }
  1008. private sealed class MSLogger : ILogger
  1009. {
  1010. private readonly ISawmill _sawmill;
  1011. public MSLogger(ISawmill sawmill)
  1012. {
  1013. _sawmill = sawmill;
  1014. }
  1015. public void Log<TState>(MSLogLevel logLevel, EventId eventId, TState state, Exception? exception,
  1016. Func<TState, Exception?, string> formatter)
  1017. {
  1018. var lvl = logLevel switch
  1019. {
  1020. MSLogLevel.Trace => LogLevel.Debug,
  1021. MSLogLevel.Debug => LogLevel.Debug,
  1022. // EFCore feels the need to log individual DB commands as "Information" so I'm slapping debug on it.
  1023. MSLogLevel.Information => LogLevel.Debug,
  1024. MSLogLevel.Warning => LogLevel.Warning,
  1025. MSLogLevel.Error => LogLevel.Error,
  1026. MSLogLevel.Critical => LogLevel.Fatal,
  1027. MSLogLevel.None => LogLevel.Debug,
  1028. _ => LogLevel.Debug
  1029. };
  1030. _sawmill.Log(lvl, formatter(state, exception));
  1031. }
  1032. public bool IsEnabled(MSLogLevel logLevel)
  1033. {
  1034. return true;
  1035. }
  1036. public IDisposable? BeginScope<TState>(TState state) where TState : notnull
  1037. {
  1038. // TODO: this
  1039. return null;
  1040. }
  1041. }
  1042. }
  1043. public sealed record PlayTimeUpdate(NetUserId User, string Tracker, TimeSpan Time);
  1044. internal sealed class SyncAsyncEnumerable<T> : IAsyncEnumerable<T>
  1045. {
  1046. private readonly IAsyncEnumerable<T> _enumerable;
  1047. public SyncAsyncEnumerable(IAsyncEnumerable<T> enumerable)
  1048. {
  1049. _enumerable = enumerable;
  1050. }
  1051. public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default)
  1052. {
  1053. return new Enumerator(_enumerable.GetAsyncEnumerator(cancellationToken));
  1054. }
  1055. private sealed class Enumerator : IAsyncEnumerator<T>
  1056. {
  1057. private readonly IAsyncEnumerator<T> _enumerator;
  1058. public Enumerator(IAsyncEnumerator<T> enumerator)
  1059. {
  1060. _enumerator = enumerator;
  1061. }
  1062. public ValueTask DisposeAsync()
  1063. {
  1064. var task = _enumerator.DisposeAsync();
  1065. if (!task.IsCompleted)
  1066. throw new InvalidOperationException("DisposeAsync did not complete synchronously.");
  1067. return task;
  1068. }
  1069. public ValueTask<bool> MoveNextAsync()
  1070. {
  1071. var task = _enumerator.MoveNextAsync();
  1072. if (!task.IsCompleted)
  1073. throw new InvalidOperationException("MoveNextAsync did not complete synchronously.");
  1074. return task;
  1075. }
  1076. public T Current => _enumerator.Current;
  1077. }
  1078. }
  1079. }