diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1321af2..7a36c27 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,6 +9,9 @@ on: permissions: contents: write +env: + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} + jobs: build-release: runs-on: windows-latest @@ -18,7 +21,7 @@ jobs: - uses: actions/setup-dotnet@v4 with: - dotnet-version: 9.0.x + dotnet-version: 10.0.x - name: Read version id: version @@ -54,3 +57,55 @@ jobs: files: | artifacts/MikuSB-win-x64.zip artifacts/MikuSB-win-x64.zip.sha256 + + build-release-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Publish server + run: | + dotnet publish ./MikuSB/MikuSB.csproj -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true --property:PublishDir=../artifacts/dist + + - name: Read version + id: version + shell: bash + run: | + line=$(head -n 1 version.txt | xargs) + VERSION=${line#v=} + + SHORT_HASH="$(git rev-parse --short=7 HEAD)" + if [ -z "$VERSION" ]; then + echo "version.txt is empty." + VERSION=${SHORT_HASH} + fi + if [[ "${{ env.BRANCH_NAME }}" == "main" ]]; then + echo "tag=v${VERSION}" >> $GITHUB_OUTPUT + else + SAFE_NAME=$(echo "${{ env.BRANCH_NAME }}" | tr '/' '-') + echo "tag=${SAFE_NAME}-v${VERSION}-${SHORT_HASH}" >> $GITHUB_OUTPUT + fi + + - name: Assemble release package + run: | + packageDir="./artifacts/dist/" + zipFile="MikuSB-linux-x64.zip" + cp ./version.txt ${packageDir} + pushd ${packageDir} + zip -r ../${zipFile} . + popd + sha256sum ./artifacts/${zipFile} > ./artifacts/${zipFile}.sha256 + + - name: Create release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.version.outputs.tag }} + name: ${{ steps.version.outputs.tag }} + generate_release_notes: true + files: | + ./artifacts/MikuSB-linux-x64.zip + ./artifacts/MikuSB-linux-x64.zip.sha256 diff --git a/Common/Common.csproj b/Common/Common.csproj index 024f2ef..64ce4a9 100644 --- a/Common/Common.csproj +++ b/Common/Common.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable false @@ -21,7 +21,6 @@ - diff --git a/Common/Database/DatabaseHelper.cs b/Common/Database/DatabaseHelper.cs index 1a9dc5f..cbaf4b0 100644 --- a/Common/Database/DatabaseHelper.cs +++ b/Common/Database/DatabaseHelper.cs @@ -14,7 +14,10 @@ public class DatabaseHelper public static readonly ConcurrentDictionary> UidInstanceMap = []; public static readonly List ToSaveUidList = []; public static long LastSaveTick = DateTime.UtcNow.Ticks; - public static Thread? SaveThread; + + private static int _saving = 0; + private static Task? _saveTask; + private static CancellationTokenSource? _cts; public static bool LoadAccount; public static bool LoadAllData; @@ -77,15 +80,11 @@ public class DatabaseHelper while (!res.IsCompleted) { + Thread.Sleep(100); } - LastSaveTick = DateTime.UtcNow.Ticks; - - SaveThread = new Thread(() => - { - while (true) CalcSaveDatabase(); - }); - SaveThread.Start(); + _cts = new CancellationTokenSource(); + _saveTask = RunAutoSave(_cts.Token); LoadAllData = true; } @@ -248,6 +247,35 @@ public class DatabaseHelper ToSaveUidList.RemoveAll(x => x == key); } + public static void Stop() + { + _cts?.Cancel(); + } + + public static async Task WaitAsync() + { + if (_saveTask != null) + await _saveTask; + } + + private static async Task RunAutoSave(CancellationToken token) + { + LastSaveTick = DateTime.UtcNow.Ticks; + try + { + while (!token.IsCancellationRequested) + { + CalcSaveDatabase(); + await Task.Delay(100, token); + } + } + catch (OperationCanceledException) + { + // exit normally + // Console.WriteLine($"RunAutoSave exit! - OperationCanceledException"); + } + } + // Auto save per 5 min public static void CalcSaveDatabase() { @@ -257,6 +285,10 @@ public class DatabaseHelper public static void SaveDatabase() { + // ensure only one SaveDatabase() runnig + if (Interlocked.Exchange(ref _saving, 1) == 1) + return; + try { var prev = DateTime.Now; @@ -281,11 +313,18 @@ public class DatabaseHelper Math.Round(t, 2).ToString(CultureInfo.InvariantCulture))); ToSaveUidList.Clear(); + + // Thread.Sleep(5000); // for test if saving process taking too long } catch (Exception e) { logger.Error("An error occurred while saving the database", e); } + finally + { + // release lock + Volatile.Write(ref _saving, 0); + } LastSaveTick = DateTime.UtcNow.Ticks; } diff --git a/Common/Util/IConsole.cs b/Common/Util/IConsole.cs index ac0c0e9..395561e 100644 --- a/Common/Util/IConsole.cs +++ b/Common/Util/IConsole.cs @@ -53,7 +53,7 @@ public class IConsole Console.WriteLine(); Input = []; CursorIndex = 0; - if (InputHistory.Count >= HistoryMaxCount) + if (InputHistory.Count >= HistoryMaxCount) InputHistory.RemoveAt(0); InputHistory.Add(input); HistoryIndex = InputHistory.Count; @@ -80,7 +80,7 @@ public class IConsole public static void HandleUpArrow() { if (InputHistory.Count == 0) return; - + if (HistoryIndex > 0) { HistoryIndex--; @@ -94,7 +94,7 @@ public class IConsole public static void HandleDownArrow() { if (HistoryIndex >= InputHistory.Count) return; - + HistoryIndex++; if (HistoryIndex >= InputHistory.Count) { @@ -102,7 +102,7 @@ public class IConsole Input = []; CursorIndex = 0; } - else + else { var history = InputHistory[HistoryIndex]; Input = [.. history]; @@ -114,7 +114,7 @@ public class IConsole public static void HandleLeftArrow() { if (CursorIndex <= 0) return; - + var (left, _) = Console.GetCursorPosition(); CursorIndex--; Console.SetCursorPosition(left - GetWidth(Input[CursorIndex].ToString()), Console.CursorTop); @@ -123,7 +123,7 @@ public class IConsole public static void HandleRightArrow() { if (CursorIndex >= Input.Count) return; - + var (left, _) = Console.GetCursorPosition(); CursorIndex++; Console.SetCursorPosition(left + GetWidth(Input[CursorIndex - 1].ToString()), Console.CursorTop); @@ -148,13 +148,29 @@ public class IConsole #endregion - public static string ListenConsole() + public static async Task ListenConsole(CancellationToken exitToken) { - while (true) + while (!exitToken.IsCancellationRequested) { ConsoleKeyInfo keyInfo; - try { keyInfo = Console.ReadKey(true); } - catch (InvalidOperationException) { continue; } + try + { + if (!Console.KeyAvailable) + { + await Task.Delay(10, exitToken); + continue; + } + keyInfo = Console.ReadKey(true); + } + catch (OperationCanceledException) + { + break; + } + catch (InvalidOperationException) + { + await Task.Delay(50, exitToken); + continue; + } switch (keyInfo.Key) { diff --git a/GameServer/GameServer.csproj b/GameServer/GameServer.csproj index 6600304..ead8405 100644 --- a/GameServer/GameServer.csproj +++ b/GameServer/GameServer.csproj @@ -2,7 +2,7 @@ Library - net9.0 + net10.0 enable enable false @@ -22,7 +22,6 @@ - diff --git a/MikuSB.Updater/MikuSB.Updater.csproj b/MikuSB.Updater/MikuSB.Updater.csproj index 84f8afc..f4640d8 100644 --- a/MikuSB.Updater/MikuSB.Updater.csproj +++ b/MikuSB.Updater/MikuSB.Updater.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 win-x64 enable enable diff --git a/MikuSB/MikuSB.csproj b/MikuSB/MikuSB.csproj index bfcdc01..19c3b36 100644 --- a/MikuSB/MikuSB.csproj +++ b/MikuSB/MikuSB.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable false diff --git a/MikuSB/Program/LoaderManager.cs b/MikuSB/Program/LoaderManager.cs index 5d60ce0..04bfc29 100644 --- a/MikuSB/Program/LoaderManager.cs +++ b/MikuSB/Program/LoaderManager.cs @@ -160,7 +160,7 @@ public class LoaderManager : MikuSB } } - public static void InitCommand() + public static async Task InitCommand(CancellationToken exitToken) { // Register the command handlers try @@ -178,6 +178,6 @@ public class LoaderManager : MikuSB IConsole.OnConsoleExcuteCommand += CommandExecutor.ConsoleExcuteCommand; CommandExecutor.OnRunCommand += (sender, e) => { CommandManager.HandleCommand(e, sender); }; - IConsole.ListenConsole(); + await IConsole.ListenConsole(exitToken); } } \ No newline at end of file diff --git a/MikuSB/Program/MikuSB.cs b/MikuSB/Program/MikuSB.cs index d20e124..ff779d5 100644 --- a/MikuSB/Program/MikuSB.cs +++ b/MikuSB/Program/MikuSB.cs @@ -18,6 +18,10 @@ public class MikuSB public static readonly Listener Listener = new(); public static readonly CommandManager CommandManager = new(); + // for exit signal + private static readonly CancellationTokenSource _cts = new(); + private static int _exitCode = 0; + public static async Task Main() { var time = DateTime.Now; @@ -50,44 +54,58 @@ public class MikuSB ResourceManager.IsLoaded = true; HandbookGenerator.GenerateAll(); - LoaderManager.InitCommand(); + var consoleTask = Task.Run(() => LoaderManager.InitCommand(_cts.Token), _cts.Token); var elapsed = DateTime.Now - time; Logger.Info(I18NManager.Translate("Server.ServerInfo.ServerStarted", Math.Round(elapsed.TotalSeconds, 2).ToString(CultureInfo.InvariantCulture))); + + await consoleTask; + + await ProcessExit(Volatile.Read(ref _exitCode)); } - # region Exit + #region Exit private static void RegisterExitEvent() { AppDomain.CurrentDomain.ProcessExit += (_, _) => { Logger.Info(I18NManager.Translate("Server.ServerInfo.Shutdown")); - ProcessExit(); + RequestShutdown(0); }; AppDomain.CurrentDomain.UnhandledException += (obj, arg) => { Logger.Error(I18NManager.Translate("Server.ServerInfo.UnhandledException", obj.GetType().Name), (Exception)arg.ExceptionObject); Logger.Info(I18NManager.Translate("Server.ServerInfo.Shutdown")); - ProcessExit(); - Environment.Exit(1); + RequestShutdown(1); }; Console.CancelKeyPress += (_, eventArgs) => { Logger.Info(I18NManager.Translate("Server.ServerInfo.CancelKeyPressed")); eventArgs.Cancel = true; - Environment.Exit(0); + RequestShutdown(0); }; } - private static void ProcessExit() + private static void RequestShutdown(int exitCode) + { + Interlocked.Exchange(ref _exitCode, exitCode); + _cts.Cancel(); + } + + private static async Task ProcessExit(int exitCode) { SocketListener.Connections.Values.ToList().ForEach(x => x.Stop(true)); - DatabaseHelper.SaveThread?.Interrupt(); - DatabaseHelper.SaveDatabase(); + + DatabaseHelper.Stop(); // notify stop + await DatabaseHelper.WaitAsync(); // wait AutoSave thread exit + + DatabaseHelper.SaveDatabase(); // final flush + + Environment.Exit(exitCode); } # endregion diff --git a/MikuSB/Properties/PublishProfiles/MikuSB-Win64-Debug.pubxml b/MikuSB/Properties/PublishProfiles/MikuSB-Win64-Debug.pubxml index b46a1e0..d1148d7 100644 --- a/MikuSB/Properties/PublishProfiles/MikuSB-Win64-Debug.pubxml +++ b/MikuSB/Properties/PublishProfiles/MikuSB-Win64-Debug.pubxml @@ -9,7 +9,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. bin\MikuSB-Win64-Debug FileSystem <_TargetId>Folder - net9.0 + net10.0 win-x64 false false diff --git a/MikuSB/Properties/PublishProfiles/MikuSB-Win64-MultiFile.pubxml b/MikuSB/Properties/PublishProfiles/MikuSB-Win64-MultiFile.pubxml index d51f9a6..ab03f5c 100644 --- a/MikuSB/Properties/PublishProfiles/MikuSB-Win64-MultiFile.pubxml +++ b/MikuSB/Properties/PublishProfiles/MikuSB-Win64-MultiFile.pubxml @@ -9,7 +9,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. bin\MikuSB-MultiFile\ FileSystem <_TargetId>Folder - net9.0 + net10.0 win-x64 false false diff --git a/MikuSB/Properties/PublishProfiles/MikuSB-Win64-OneFile.pubxml b/MikuSB/Properties/PublishProfiles/MikuSB-Win64-OneFile.pubxml index 9ac6ab9..d55e2e3 100644 --- a/MikuSB/Properties/PublishProfiles/MikuSB-Win64-OneFile.pubxml +++ b/MikuSB/Properties/PublishProfiles/MikuSB-Win64-OneFile.pubxml @@ -9,7 +9,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. bin\MikuSB-OneFile\ FileSystem <_TargetId>Folder - net9.0 + net10.0 win-x64 true true diff --git a/Proto/Proto.csproj b/Proto/Proto.csproj index 4fe010f..7ff33df 100644 --- a/Proto/Proto.csproj +++ b/Proto/Proto.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable false diff --git a/Proxy/Proxy.csproj b/Proxy/Proxy.csproj index c33e878..73aa14a 100644 --- a/Proxy/Proxy.csproj +++ b/Proxy/Proxy.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 enable enable MikuSB.Proxy diff --git a/Proxy/ProxyCertificateAuthority.cs b/Proxy/ProxyCertificateAuthority.cs index 1c92295..2e4f68a 100644 --- a/Proxy/ProxyCertificateAuthority.cs +++ b/Proxy/ProxyCertificateAuthority.cs @@ -30,7 +30,8 @@ public sealed class ProxyCertificateAuthority RootCerPath); } - public string RootCerPath => Path.Combine(CertificateDirectory, "MikuSB.Proxy.Root.cer"); + public string RootCerPath => Path.Join(CertificateDirectory, "MikuSB.Proxy.Root.cer"); + public string RootCerPemPath => Path.Join(CertificateDirectory, "MikuSB.Proxy.Root.pem"); private static string CertificateDirectory => Path.Combine(AppContext.BaseDirectory, "proxy-certs"); @@ -55,6 +56,12 @@ public sealed class ProxyCertificateAuthority if (!File.Exists(RootCerPath)) File.WriteAllBytes(RootCerPath, existing.Export(X509ContentType.Cert)); + if (!File.Exists(RootCerPemPath)) + { + string pemString = existing.ExportCertificatePem(); + File.WriteAllText(RootCerPemPath, pemString); + } + return existing; } @@ -78,6 +85,9 @@ public sealed class ProxyCertificateAuthority File.WriteAllBytes(pfxPath, exportable.Export(X509ContentType.Pfx, Password)); File.WriteAllBytes(RootCerPath, exportable.Export(X509ContentType.Cert)); _logger.LogInformation("Created MikuSB proxy root certificate at {CertificatePath}", RootCerPath); + + File.WriteAllText(RootCerPemPath, exportable.ExportCertificatePem()); + _logger.LogInformation("Created MikuSB proxy root certificate (PEM) at {CertificatePath}", RootCerPemPath); return exportable; } diff --git a/Proxy/ProxyServer.cs b/Proxy/ProxyServer.cs index e27f995..109c13e 100644 --- a/Proxy/ProxyServer.cs +++ b/Proxy/ProxyServer.cs @@ -5,6 +5,7 @@ using System.Net.Sockets; using System.Security.Authentication; using System.Text; using MikuSB.Configuration; +using MikuSB.Util; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -15,7 +16,7 @@ public sealed class ProxyServer( IOptions options, ProxyCertificateAuthority certificateAuthority, HttpClient httpClient, - ILogger logger) : BackgroundService + Logger logger) : BackgroundService { private const string ListenAddress = "127.0.0.1"; private const string ServerHost = "127.0.0.1"; @@ -53,14 +54,14 @@ public sealed class ProxyServer( { if (!_options.Enabled) { - logger.LogInformation("MikuSB proxy is disabled"); + logger.Info("MikuSB proxy is disabled"); return; } var address = IPAddress.Parse(ListenAddress); _listener = new TcpListener(address, _options.Port); _listener.Start(); - logger.LogInformation("MikuSB proxy listening on {Address}:{Port}", ListenAddress, _options.Port); + logger.Info($"MikuSB proxy listening on {ListenAddress}:{_options.Port}"); try { @@ -85,6 +86,7 @@ public sealed class ProxyServer( { using (client) { + logger.Info($"Proxy New client: {client.Client.RemoteEndPoint}"); try { await HandleClientCoreAsync(client, cancellationToken); @@ -100,12 +102,13 @@ public sealed class ProxyServer( } catch (AuthenticationException ex) { - logger.LogWarning(ex, "Proxy TLS authentication failed"); + logger.Warn($"Proxy TLS authentication failed: {ex}"); } catch (Exception ex) { - logger.LogWarning(ex, "Proxy client failed"); + logger.Warn($"Proxy client failed {ex}"); } + logger.Info($"Proxy client close: {client.Client.RemoteEndPoint}"); } } @@ -187,7 +190,7 @@ public sealed class ProxyServer( { var pathAndQuery = request.GetPathAndQuery(); var uri = new Uri($"http://{ServerHost}:{_options.ServerHttpPort}{pathAndQuery}"); - logger.LogInformation("[Proxy] Redirect: {Method} {Host}{Path} -> {Uri}", request.Method, request.HostOverride ?? request.Host, pathAndQuery, uri); + logger.Info($"Redirect: {request.Method} {request.HostOverride ?? request.Host}{pathAndQuery} -> {uri}"); await SendHttpRequestAsync(clientStream, request, uri, true, cancellationToken); } @@ -202,7 +205,7 @@ public sealed class ProxyServer( if (IsSelfReference(uri)) { - logger.LogWarning("[Proxy] Self-reference blocked: {Method} {Uri}", request.Method, uri); + logger.Info($"Self-reference blocked: {request.Method} {uri}"); await WriteSimpleResponseAsync(clientStream, HttpStatusCode.LoopDetected, "Proxy self-reference detected", cancellationToken); return; } @@ -351,7 +354,10 @@ public sealed class ProxyServer( public string GetPathAndQuery() { - if (Uri.TryCreate(Target, UriKind.Absolute, out var uri)) + // "/query?version=a.b.c&platform=PC" + // => Uri.TryCreate() return true && uri.Scheme == "file" + // => will return "/query%3Fversion=a.b.c&platform=PC" cause 404 + if (Uri.TryCreate(Target, UriKind.Absolute, out var uri) && uri.IsAbsoluteUri && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)) return uri.PathAndQuery; if (string.IsNullOrEmpty(Target)) diff --git a/README_linux.md b/README_linux.md new file mode 100644 index 0000000..36b910b --- /dev/null +++ b/README_linux.md @@ -0,0 +1,66 @@ +# MikuSB on Linux + + +## Config + +### setup steam launch options as following + +`HTTP_PROXY="http://127.0.0.1:8888" HTTPS_PROXY="http://127.0.0.1:8888" ALL_PROXY="http://127.0.0.1:8888" %command%` + +### start local server and keep it running + +``` +./MikuSB +``` + +### find root CA cert, and create ca bundle + +root CA cert, should in the path: `proxy-certs/MikuSB.Proxy.Root.pem` + + +### setup root CA for proton/wine + +not sure, even I remove Proton PFX (Wine prefix) folder, without redo this step, still no cert issue. + +`Proton Hotfix` is the proton version which selected in steam `Force the use of a specific Steam Play compatibility tool` + +```bash +APPID= +STEAM_COMPAT_DATA_PATH=~/.steam/steam/steamapps/compatdata/$APPID/pfx +STEAM_WINE_PATH="$HOME/.steam/steam/steamapps/common/Proton Hotfix/files/bin/wine" +WINEPREFIX=$STEAM_COMPAT_DATA_PATH $STEAM_WINE_PATH certutil -addstore -f Root proxy-certs/MikuSB.Proxy.Root.pem +``` + +### start the game and enjoy + + +## development + +1. Restore dependencies and build. + +```bash +dotnet build +``` + +2. run it + +```bash +dotnet run --project ./MikuSB +``` + +## release build + +```bash +DOTNET_CLI_UI_LANGUAGE=en time dotnet publish ./MikuSB/MikuSB.csproj -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true --property:PublishDir=../publish + +# output will in ./publish/* +cd ./publish + +# start server +./MikuSB +``` + +## TODO: + +* [ ] tool/script for CA cert create and install to proton/wine +* [ ] automatic done in main program diff --git a/SdkServer/SdkServer.cs b/SdkServer/SdkServer.cs index 6955abe..bc9fed6 100644 --- a/SdkServer/SdkServer.cs +++ b/SdkServer/SdkServer.cs @@ -17,17 +17,17 @@ public static class SdkServer { public static void Start(string[] args) { - BuildWebHost(args).RunAsync(); - } + var builder = Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder + .UseStartup() + .ConfigureLogging((_, logging) => { logging.ClearProviders(); }) + .UseUrls(ConfigManager.Config.HttpServer.GetDisplayAddress()); + }); - private static IWebHost BuildWebHost(string[] args) - { - var builder = WebHost.CreateDefaultBuilder(args) - .UseStartup() - .ConfigureLogging((_, logging) => { logging.ClearProviders(); }) - .UseUrls(ConfigManager.Config.HttpServer.GetDisplayAddress()); - - return builder.Build(); + var host = builder.Build(); + host.RunAsync(); } } @@ -91,7 +91,7 @@ public class Startup { options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; }); - services.AddSingleton(_ => new Logger("HttpServer")); + services.AddSingleton(_ => new Logger("Proxy")); services.AddMikuSbProxy(ConfigManager.Config.Proxy); } } diff --git a/SdkServer/SdkServer.csproj b/SdkServer/SdkServer.csproj index d7d4a54..effc4cb 100644 --- a/SdkServer/SdkServer.csproj +++ b/SdkServer/SdkServer.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 enable enable MikuSB.SdkServer diff --git a/TcpSharp/TcpSharp.csproj b/TcpSharp/TcpSharp.csproj index 68eb1cf..004e7a3 100644 --- a/TcpSharp/TcpSharp.csproj +++ b/TcpSharp/TcpSharp.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable false @@ -11,7 +11,6 @@ -