Skip to content

Commit caea930

Browse files
committed
Fix Build
1 parent c429dfc commit caea930

5 files changed

Lines changed: 337 additions & 1 deletion

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ logs/
8484
Audit/
8585

8686
# MySQL portable install + config (created by MySqlSetupService at runtime)
87-
database/
87+
# /database anchors to repo root only — avoids matching src/*/Database/ source folders
88+
/database/
8889
my.ini
8990

9091
# Nginx + PHP portable install (created by NginxPhpSetupService at runtime)
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
namespace Trion.Core.Database;
2+
3+
/// <summary>Live snapshot of a MySQL package download in progress.</summary>
4+
public record DownloadProgress(
5+
long DownloadedBytes,
6+
long TotalBytes, // -1 when Content-Length header is absent
7+
double SpeedBytesPerSec,
8+
TimeSpan? Eta, // null when total is unknown or speed is zero
9+
int Percent); // 0-100; 0 when total is unknown
10+
11+
public interface IMySqlSetupService
12+
{
13+
// ── Installation state ────────────────────────────────────────────────────
14+
15+
/// <summary>True when the MySQL binaries are present on this machine.</summary>
16+
bool IsInstalled { get; }
17+
18+
// ── Setup operation state ─────────────────────────────────────────────────
19+
20+
/// <summary>True while RunStartupCheckAsync is executing (download / extract / init).</summary>
21+
bool IsRunning { get; }
22+
23+
/// <summary>The most recent progress message (empty string before first run).</summary>
24+
string LastMessage { get; }
25+
26+
/// <summary>
27+
/// The most recent download snapshot while a download is active; null otherwise.
28+
/// Always null on Linux (package manager handles downloads transparently).
29+
/// </summary>
30+
DownloadProgress? CurrentDownload { get; }
31+
32+
// ── Process state ─────────────────────────────────────────────────────────
33+
34+
/// <summary>True when the mysqld process / systemd service is alive.</summary>
35+
bool IsProcessRunning { get; }
36+
37+
/// <summary>The OS process ID of the running mysqld, or null if not running.</summary>
38+
int? ProcessId { get; }
39+
40+
/// <summary>Wall-clock time when the service was last started, or null.</summary>
41+
DateTime? StartTime { get; }
42+
43+
/// <summary>How long MySQL has been running. Zero when not running.</summary>
44+
TimeSpan Uptime { get; }
45+
46+
// ── Events ────────────────────────────────────────────────────────────────
47+
48+
/// <summary>
49+
/// Raised on every progress step and once more when the setup operation finishes.
50+
/// </summary>
51+
event Action<string>? ProgressChanged;
52+
53+
/// <summary>
54+
/// Raised during active downloads with live speed / ETA / size data.
55+
/// Raised with <c>null</c> when the download completes or is aborted.
56+
/// Never raised on Linux (package manager handles downloads transparently).
57+
/// </summary>
58+
event Action<DownloadProgress?>? DownloadProgressChanged;
59+
60+
/// <summary>Raised when mysqld starts (<c>true</c>) or stops / crashes (<c>false</c>).</summary>
61+
event Action<bool>? StatusChanged;
62+
63+
// ── Setup ─────────────────────────────────────────────────────────────────
64+
65+
/// <summary>
66+
/// Called once at startup.
67+
/// Installs MySQL if absent, starts it if not running, and runs first-time
68+
/// database setup (phoenix user + all expansion schemas) if not yet done.
69+
/// </summary>
70+
Task RunStartupCheckAsync(CancellationToken ct = default);
71+
72+
/// <summary>
73+
/// Writes (or refreshes) MySQL configuration tuned to the current hardware.
74+
/// No-op on Linux (system MySQL uses /etc/mysql/conf.d/).
75+
/// </summary>
76+
void GenerateMyIni();
77+
78+
// ── Lifecycle ─────────────────────────────────────────────────────────────
79+
80+
/// <summary>Starts MySQL. No-op if already running.</summary>
81+
Task StartAsync(bool? hideConsole = null, CancellationToken ct = default);
82+
83+
/// <summary>Gracefully stops MySQL. No-op if not running.</summary>
84+
Task StopAsync(CancellationToken ct = default);
85+
86+
/// <summary>Regenerates config and restarts MySQL.</summary>
87+
Task RepairAsync(CancellationToken ct = default);
88+
89+
/// <summary>
90+
/// Stops MySQL and removes the installation.
91+
/// On Linux, removes the package but preserves /var/lib/mysql data.
92+
/// On Windows, deletes the entire portable database/ folder.
93+
/// </summary>
94+
Task UninstallAsync(CancellationToken ct = default);
95+
96+
/// <summary>Runs UninstallAsync then a full RunStartupCheckAsync.</summary>
97+
Task ReinstallAsync(CancellationToken ct = default);
98+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
namespace Trion.Core.Database;
2+
3+
/// <summary>
4+
/// SQL executed once immediately after MySQL is initialized for the first time.
5+
/// Creates the root password, the 'phoenix' application user, and all
6+
/// expansion-specific database schemas.
7+
/// </summary>
8+
public static class MySqlInitSql
9+
{
10+
/// <summary>
11+
/// Password assigned to <c>root@localhost</c> during first-run setup.
12+
/// Used by DatabaseService.PingAsync to verify MySQL connectivity
13+
/// with known-good credentials regardless of the application user configuration.
14+
/// </summary>
15+
public const string RootPassword = "FlyingPhoenix";
16+
17+
/// <summary>
18+
/// Full first-run initialization script for MySQL 8.
19+
/// Run via <c>mysql.exe --user=root --host=127.0.0.1 --port=3306 --protocol=TCP</c>
20+
/// with this text piped to stdin (Windows).
21+
/// On Linux use <see cref="FirstRunSetupLinux"/> instead (MariaDB-compatible syntax).
22+
/// </summary>
23+
public const string FirstRunSetup = """
24+
-- ── Root account ──────────────────────────────────────────────────────
25+
ALTER USER 'root'@'localhost' IDENTIFIED WITH 'caching_sha2_password' BY 'FlyingPhoenix';
26+
27+
-- ── Application user ───────────────────────────────────────────────────
28+
DROP USER IF EXISTS 'phoenix'@'localhost';
29+
CREATE USER 'phoenix'@'localhost'
30+
IDENTIFIED WITH 'caching_sha2_password' BY 'phoenix'
31+
WITH MAX_QUERIES_PER_HOUR 0
32+
MAX_CONNECTIONS_PER_HOUR 0
33+
MAX_UPDATES_PER_HOUR 0;
34+
GRANT ALL PRIVILEGES ON *.* TO 'phoenix'@'localhost' WITH GRANT OPTION;
35+
36+
-- ── Classic databases ──────────────────────────────────────────────────
37+
CREATE DATABASE IF NOT EXISTS `classic_world` DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_unicode_ci;
38+
CREATE DATABASE IF NOT EXISTS `classic_characters` DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_unicode_ci;
39+
CREATE DATABASE IF NOT EXISTS `classic_auth` DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_unicode_ci;
40+
CREATE DATABASE IF NOT EXISTS `classic_logs` DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_general_ci;
41+
42+
GRANT ALL PRIVILEGES ON `classic_world` .* TO 'phoenix'@'localhost' WITH GRANT OPTION;
43+
GRANT ALL PRIVILEGES ON `classic_characters` .* TO 'phoenix'@'localhost' WITH GRANT OPTION;
44+
GRANT ALL PRIVILEGES ON `classic_auth` .* TO 'phoenix'@'localhost' WITH GRANT OPTION;
45+
GRANT ALL PRIVILEGES ON `classic_logs` .* TO 'phoenix'@'localhost' WITH GRANT OPTION;
46+
47+
-- ── TBC databases ──────────────────────────────────────────────────────
48+
CREATE DATABASE IF NOT EXISTS `tbc_world` DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_unicode_ci;
49+
CREATE DATABASE IF NOT EXISTS `tbc_characters` DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_unicode_ci;
50+
CREATE DATABASE IF NOT EXISTS `tbc_auth` DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_unicode_ci;
51+
CREATE DATABASE IF NOT EXISTS `tbc_logs` DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_general_ci;
52+
53+
GRANT ALL PRIVILEGES ON `tbc_world` .* TO 'phoenix'@'localhost' WITH GRANT OPTION;
54+
GRANT ALL PRIVILEGES ON `tbc_characters` .* TO 'phoenix'@'localhost' WITH GRANT OPTION;
55+
GRANT ALL PRIVILEGES ON `tbc_auth` .* TO 'phoenix'@'localhost' WITH GRANT OPTION;
56+
GRANT ALL PRIVILEGES ON `tbc_logs` .* TO 'phoenix'@'localhost' WITH GRANT OPTION;
57+
58+
-- ── WotLK databases ────────────────────────────────────────────────────
59+
CREATE DATABASE IF NOT EXISTS `wotlk_world` DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_unicode_ci;
60+
CREATE DATABASE IF NOT EXISTS `wotlk_characters` DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_unicode_ci;
61+
CREATE DATABASE IF NOT EXISTS `wotlk_auth` DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_unicode_ci;
62+
CREATE DATABASE IF NOT EXISTS `wotlk_playerbots` DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_general_ci;
63+
64+
GRANT ALL PRIVILEGES ON `wotlk_world` .* TO 'phoenix'@'localhost' WITH GRANT OPTION;
65+
GRANT ALL PRIVILEGES ON `wotlk_characters` .* TO 'phoenix'@'localhost' WITH GRANT OPTION;
66+
GRANT ALL PRIVILEGES ON `wotlk_auth` .* TO 'phoenix'@'localhost' WITH GRANT OPTION;
67+
GRANT ALL PRIVILEGES ON `wotlk_playerbots` .* TO 'phoenix'@'localhost' WITH GRANT OPTION;
68+
69+
-- ── Cataclysm databases ────────────────────────────────────────────────
70+
CREATE DATABASE IF NOT EXISTS `cata_world` DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_unicode_ci;
71+
CREATE DATABASE IF NOT EXISTS `cata_characters` DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_unicode_ci;
72+
CREATE DATABASE IF NOT EXISTS `cata_auth` DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_unicode_ci;
73+
74+
GRANT ALL PRIVILEGES ON `cata_world` .* TO 'phoenix'@'localhost' WITH GRANT OPTION;
75+
GRANT ALL PRIVILEGES ON `cata_characters` .* TO 'phoenix'@'localhost' WITH GRANT OPTION;
76+
GRANT ALL PRIVILEGES ON `cata_auth` .* TO 'phoenix'@'localhost' WITH GRANT OPTION;
77+
78+
-- ── MoP databases ──────────────────────────────────────────────────────
79+
CREATE DATABASE IF NOT EXISTS `mop_world` DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_unicode_ci;
80+
CREATE DATABASE IF NOT EXISTS `mop_characters` DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_unicode_ci;
81+
CREATE DATABASE IF NOT EXISTS `mop_auth` DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_unicode_ci;
82+
83+
GRANT ALL PRIVILEGES ON `mop_world` .* TO 'phoenix'@'localhost' WITH GRANT OPTION;
84+
GRANT ALL PRIVILEGES ON `mop_characters` .* TO 'phoenix'@'localhost' WITH GRANT OPTION;
85+
GRANT ALL PRIVILEGES ON `mop_auth` .* TO 'phoenix'@'localhost' WITH GRANT OPTION;
86+
87+
-- ── Custom core databases ─────────────────────────────────────────────
88+
CREATE DATABASE IF NOT EXISTS `custom_world` DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_unicode_ci;
89+
CREATE DATABASE IF NOT EXISTS `custom_characters` DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_unicode_ci;
90+
CREATE DATABASE IF NOT EXISTS `custom_auth` DEFAULT CHARACTER SET UTF8MB4 COLLATE utf8mb4_unicode_ci;
91+
92+
GRANT ALL PRIVILEGES ON `custom_world` .* TO 'phoenix'@'localhost' WITH GRANT OPTION;
93+
GRANT ALL PRIVILEGES ON `custom_characters` .* TO 'phoenix'@'localhost' WITH GRANT OPTION;
94+
GRANT ALL PRIVILEGES ON `custom_auth` .* TO 'phoenix'@'localhost' WITH GRANT OPTION;
95+
96+
-- ── Flush ──────────────────────────────────────────────────────────────
97+
FLUSH PRIVILEGES;
98+
""";
99+
100+
/// <summary>
101+
/// First-run initialization script compatible with both MySQL 8 and MariaDB.
102+
/// Uses <c>IDENTIFIED BY</c> without specifying an auth plugin, which works on
103+
/// both MySQL 8 (defaults to caching_sha2_password) and MariaDB.
104+
/// Used by <c>LinuxMySqlService</c>.
105+
/// </summary>
106+
public static readonly string FirstRunSetupLinux =
107+
FirstRunSetup.Replace(
108+
"IDENTIFIED WITH 'caching_sha2_password' BY",
109+
"IDENTIFIED BY");
110+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using Dapper;
2+
using MySqlConnector;
3+
4+
namespace Trion.Core.Database;
5+
6+
/// <summary>
7+
/// Forward-only schema migrations for a MySQL settings database.
8+
/// Mirrors <see cref="Settings.SettingsMigrations"/> but targets MySQL syntax.
9+
/// </summary>
10+
public static class MySqlSettingsMigrations
11+
{
12+
private static readonly (int Version, string Sql)[] Migrations =
13+
[
14+
(1, """
15+
CREATE TABLE IF NOT EXISTS settings (
16+
`key` VARCHAR(255) NOT NULL PRIMARY KEY,
17+
`value` LONGTEXT NOT NULL,
18+
updated_at VARCHAR(64) NOT NULL
19+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
20+
"""),
21+
22+
(2, """
23+
CREATE TABLE IF NOT EXISTS refresh_tokens (
24+
id VARCHAR(64) NOT NULL PRIMARY KEY,
25+
username VARCHAR(255) NOT NULL,
26+
user_hash VARCHAR(64) NOT NULL,
27+
token_hash VARCHAR(64) NOT NULL,
28+
issued_at VARCHAR(64) NOT NULL,
29+
expires_at VARCHAR(64) NOT NULL,
30+
revoked TINYINT(1) NOT NULL DEFAULT 0,
31+
revoked_at VARCHAR(64) NULL,
32+
issuing_ip VARCHAR(64) NOT NULL,
33+
INDEX idx_token_hash (token_hash)
34+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
35+
"""),
36+
];
37+
38+
public static async Task RunAsync(MySqlConnection db)
39+
{
40+
await db.ExecuteAsync("""
41+
CREATE TABLE IF NOT EXISTS schema_version (
42+
version INT NOT NULL
43+
);
44+
INSERT INTO schema_version (version)
45+
SELECT 0 FROM DUAL
46+
WHERE NOT EXISTS (SELECT 1 FROM schema_version);
47+
""");
48+
49+
foreach (var (version, sql) in Migrations)
50+
{
51+
var current = await db.QuerySingleAsync<int>("SELECT version FROM schema_version");
52+
if (current >= version) continue;
53+
54+
await using var tx = await db.BeginTransactionAsync();
55+
try
56+
{
57+
await db.ExecuteAsync(sql, transaction: tx);
58+
await db.ExecuteAsync(
59+
"UPDATE schema_version SET version = @Version",
60+
new { Version = version },
61+
tx);
62+
await tx.CommitAsync();
63+
}
64+
catch
65+
{
66+
await tx.RollbackAsync();
67+
throw;
68+
}
69+
}
70+
}
71+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System.Text.Json;
2+
using Dapper;
3+
using MySqlConnector;
4+
using Trion.Core.Abstractions.Settings;
5+
6+
namespace Trion.Core.Database;
7+
8+
/// <summary>
9+
/// MySQL-backed implementation of <see cref="ISettingsRepository"/>.
10+
/// Schema matches the SQLite variant — drop-in replacement.
11+
/// </summary>
12+
public sealed class MySqlSettingsRepository : ISettingsRepository
13+
{
14+
private readonly string _connectionString;
15+
16+
public MySqlSettingsRepository(string connectionString)
17+
{
18+
_connectionString = connectionString;
19+
}
20+
21+
public async Task<T?> GetAsync<T>(string key, CancellationToken ct = default)
22+
{
23+
await using var db = new MySqlConnection(_connectionString);
24+
var json = await db.QuerySingleOrDefaultAsync<string?>(
25+
"SELECT `value` FROM settings WHERE `key` = @Key",
26+
new { Key = key });
27+
28+
if (json is null) return default;
29+
return JsonSerializer.Deserialize<T>(json);
30+
}
31+
32+
public async Task SetAsync<T>(string key, T value, CancellationToken ct = default)
33+
{
34+
var json = JsonSerializer.Serialize(value);
35+
await using var db = new MySqlConnection(_connectionString);
36+
await db.ExecuteAsync("""
37+
INSERT INTO settings (`key`, `value`, updated_at)
38+
VALUES (@Key, @Value, @UpdatedAt)
39+
ON DUPLICATE KEY UPDATE `value` = VALUES(`value`), updated_at = VALUES(updated_at)
40+
""",
41+
new
42+
{
43+
Key = key,
44+
Value = json,
45+
UpdatedAt = DateTimeOffset.UtcNow.ToString("O")
46+
});
47+
}
48+
49+
public async Task DeleteAsync(string key, CancellationToken ct = default)
50+
{
51+
await using var db = new MySqlConnection(_connectionString);
52+
await db.ExecuteAsync(
53+
"DELETE FROM settings WHERE `key` = @Key",
54+
new { Key = key });
55+
}
56+
}

0 commit comments

Comments
 (0)