Localtunnel hosting + admin panel + AppData db

- Default hosting mode is now Localtunnel — public *.loca.lt URL,
  no account, auto-fetched on first run. Replaces the prototyped
  Tailscale mode.
- Tailscale code purged from launcher + CLI; docs swapped to
  Localtunnel throughout (easy-setup, joining-a-server, server-cli,
  troubleshooting, faq, READMEs).
- Launcher sidebar gains an Open admin panel button — opens
  https://admin.<apex> in the browser once the server is live.
- SQLite db moves to %APPDATA%\DorkNet\dorknet.db (server reads
  Database__SqlitePath env var, launcher passes it). Existing dbs
  under <serverDir>\data\ are migrated on first run.
- CLI Program.cs: --mode default flips from tailscale to localtunnel;
  csproj links in LocaltunnelTunnel + LocaltunnelInstaller.
- New Backend files: LauncherNameServer, LocaltunnelInstaller,
  LocaltunnelTunnel, SingleOriginServiceMap.
- .gitignore: exclude installer/out/.
This commit is contained in:
Alexa
2026-05-28 01:39:38 +03:00
parent 7852b410ee
commit f5ad4b59ef
30 changed files with 942 additions and 184 deletions
+43 -5
View File
@@ -32,7 +32,11 @@ public sealed class ServerProcess : IAsyncDisposable
throw new InvalidOperationException("A server is already running; stop it first.");
var exe = FindServerExecutable(serverDir);
ApplySqliteCompatibilityPatches(serverDir);
// db lives under %APPDATA%\DorkNet so it survives reinstalls of
// the server cache dir (which sits under %LOCALAPPDATA%).
var dbPath = Path.Combine(AppPaths.RoamingRoot, "dorknet.db");
MigrateLegacyDbIfPresent(serverDir, dbPath);
ApplySqliteCompatibilityPatches(dbPath);
StdoutLogPath = Path.Combine(AppPaths.LogsRoot, $"server-{DateTime.UtcNow:yyyyMMdd-HHmmss}.log");
var psi = new ProcessStartInfo
@@ -45,23 +49,30 @@ public sealed class ServerProcess : IAsyncDisposable
RedirectStandardError = true,
};
psi.Environment["ASPNETCORE_ENVIRONMENT"] = "Development";
psi.Environment["ASPNETCORE_URLS"] = hostingMode == HostingMode.LocalNetwork
var singleOriginMode = hostingMode == HostingMode.SingleOriginTunnel;
var directMode = hostingMode is HostingMode.LocalNetwork or HostingMode.Internet;
psi.Environment["ASPNETCORE_URLS"] = directMode
? $"http://0.0.0.0:{LanPort}"
: $"http://127.0.0.1:{DefaultLocalPort}";
psi.Environment["Database__Provider"] = "sqlite";
psi.Environment["Database__SqlitePath"] = dbPath;
psi.Environment["Jwt__Secret"] = EnsureJwtSecret();
psi.Environment["Photon__AppId"] = state.PhotonAppId;
psi.Environment["Photon__VoiceAppId"] =
string.IsNullOrEmpty(state.PhotonVoiceAppId) ? state.PhotonAppId : state.PhotonVoiceAppId;
psi.Environment["Photon__CloudRegion"] = state.PhotonRegion;
psi.Environment["Domain__Apex"] = apex;
psi.Environment["Domain__Scheme"] = hostingMode == HostingMode.LocalNetwork ? "http" : "https";
psi.Environment["Domain__Scheme"] = directMode ? "http" : "https";
psi.Environment["Domain__Port"] = "";
if (singleOriginMode)
psi.Environment["Domain__SingleOriginBaseUrl"] = $"https://{apex}";
AppendLog($"[launcher] serverDir={serverDir}");
AppendLog($"[launcher] exe={exe}");
AppendLog($"[launcher] urls={psi.Environment["ASPNETCORE_URLS"]}");
AppendLog($"[launcher] domain apex={apex}, scheme={psi.Environment["Domain__Scheme"]}, port={psi.Environment["Domain__Port"]}");
if (singleOriginMode)
AppendLog($"[launcher] single origin={psi.Environment["Domain__SingleOriginBaseUrl"]}");
_process = new Process { StartInfo = psi, EnableRaisingEvents = true };
_process.OutputDataReceived += (_, e) => AppendLog(e.Data);
@@ -128,9 +139,8 @@ public sealed class ServerProcess : IAsyncDisposable
$"No server binary found in {serverDir}. The release artifact may be corrupt.");
}
private static void ApplySqliteCompatibilityPatches(string serverDir)
private static void ApplySqliteCompatibilityPatches(string dbPath)
{
var dbPath = Path.Combine(serverDir, "data", "dorknet.db");
if (!File.Exists(dbPath)) return;
using var conn = new SqliteConnection($"Data Source={dbPath}");
@@ -139,6 +149,34 @@ public sealed class ServerProcess : IAsyncDisposable
@"""HiddenFromBrowse"" INTEGER NOT NULL DEFAULT 0");
}
/// <summary>If an older launcher build left the SQLite db under
/// <c>&lt;serverDir&gt;\data\dorknet.db</c> (where the server defaulted
/// before <see cref="Database__SqlitePath"/> took over), copy it to
/// the new <see cref="AppPaths.RoamingRoot"/> location so existing
/// players, rooms, and dorms survive the move. Best-effort — never
/// throws, never overwrites a db that's already at the new path.</summary>
private static void MigrateLegacyDbIfPresent(string serverDir, string newDbPath)
{
try
{
if (File.Exists(newDbPath)) return;
var legacy = Path.Combine(serverDir, "data", "dorknet.db");
if (!File.Exists(legacy)) return;
var destDir = Path.GetDirectoryName(newDbPath);
if (!string.IsNullOrEmpty(destDir)) Directory.CreateDirectory(destDir);
File.Copy(legacy, newDbPath);
// SQLite WAL/SHM sidecars live next to the .db — copy them
// too so the move doesn't drop in-flight wal frames.
foreach (var suffix in new[] { "-wal", "-shm" })
{
var src = legacy + suffix;
if (File.Exists(src)) File.Copy(src, newDbPath + suffix, overwrite: true);
}
}
catch { /* best-effort — fresh db will be created on first server boot */ }
}
private static void AddSqliteColumnIfMissing(
SqliteConnection conn,
string table,