- add: Service think

This commit is contained in:
2024-12-22 16:38:47 +03:00
parent d9161f837b
commit 4d64c995f1
38 changed files with 4625 additions and 80 deletions

View File

@@ -0,0 +1,142 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Nebula.Launcher.Utils;
public sealed class BandwidthStream : Stream
{
private const int NumSeconds = 8;
private const int BucketDivisor = 2;
private const int BucketsPerSecond = 2 << BucketDivisor;
// TotalBuckets MUST be power of two!
private const int TotalBuckets = NumSeconds * BucketsPerSecond;
private readonly Stream _baseStream;
private readonly long[] _buckets;
private readonly Stopwatch _stopwatch;
private long _bucketIndex;
public BandwidthStream(Stream baseStream)
{
_stopwatch = Stopwatch.StartNew();
_baseStream = baseStream;
_buckets = new long[TotalBuckets];
}
public override bool CanRead => _baseStream.CanRead;
public override bool CanSeek => _baseStream.CanSeek;
public override bool CanWrite => _baseStream.CanWrite;
public override long Length => _baseStream.Length;
public override long Position
{
get => _baseStream.Position;
set => _baseStream.Position = value;
}
private void TrackBandwidth(long value)
{
const int bucketMask = TotalBuckets - 1;
var bucketIdx = CurBucketIdx();
// Increment to bucket idx, clearing along the way.
if (bucketIdx != _bucketIndex)
{
var diff = bucketIdx - _bucketIndex;
if (diff > TotalBuckets)
for (var i = _bucketIndex; i < bucketIdx; i++)
_buckets[i & bucketMask] = 0;
else
// We managed to skip so much time the whole buffer is empty.
Array.Clear(_buckets);
_bucketIndex = bucketIdx;
}
// Write value.
_buckets[bucketIdx & bucketMask] += value;
}
private long CurBucketIdx()
{
var elapsed = _stopwatch.Elapsed.TotalSeconds;
return (long)(elapsed / BucketsPerSecond);
}
public long CalcCurrentAvg()
{
var sum = 0L;
for (var i = 0; i < TotalBuckets; i++) sum += _buckets[i];
return sum >> BucketDivisor;
}
public override void Flush()
{
_baseStream.Flush();
}
public override Task FlushAsync(CancellationToken cancellationToken)
{
return _baseStream.FlushAsync(cancellationToken);
}
protected override void Dispose(bool disposing)
{
if (disposing)
_baseStream.Dispose();
}
public override ValueTask DisposeAsync()
{
return _baseStream.DisposeAsync();
}
public override int Read(byte[] buffer, int offset, int count)
{
var read = _baseStream.Read(buffer, offset, count);
TrackBandwidth(read);
return read;
}
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
var read = await base.ReadAsync(buffer, cancellationToken);
TrackBandwidth(read);
return read;
}
public override long Seek(long offset, SeekOrigin origin)
{
return _baseStream.Seek(offset, origin);
}
public override void SetLength(long value)
{
_baseStream.SetLength(value);
}
public override void Write(byte[] buffer, int offset, int count)
{
_baseStream.Write(buffer, offset, count);
TrackBandwidth(count);
}
public override async ValueTask WriteAsync(
ReadOnlyMemory<byte> buffer,
CancellationToken cancellationToken = default)
{
await _baseStream.WriteAsync(buffer, cancellationToken);
TrackBandwidth(buffer.Length);
}
}

View File

@@ -0,0 +1,118 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text;
namespace Nebula.Launcher.Utils;
public class ManifestReader : StreamReader
{
public const int BufferSize = 128;
public ManifestReader(Stream stream) : base(stream)
{
ReadManifestVersion();
}
public ManifestReader(Stream stream, bool detectEncodingFromByteOrderMarks) : base(stream,
detectEncodingFromByteOrderMarks)
{
ReadManifestVersion();
}
public ManifestReader(Stream stream, Encoding encoding) : base(stream, encoding)
{
ReadManifestVersion();
}
public ManifestReader(Stream stream, Encoding encoding, bool detectEncodingFromByteOrderMarks) : base(stream,
encoding, detectEncodingFromByteOrderMarks)
{
ReadManifestVersion();
}
public ManifestReader(Stream stream, Encoding encoding, bool detectEncodingFromByteOrderMarks, int bufferSize) :
base(stream, encoding, detectEncodingFromByteOrderMarks, bufferSize)
{
ReadManifestVersion();
}
public ManifestReader(Stream stream, Encoding? encoding = null, bool detectEncodingFromByteOrderMarks = true,
int bufferSize = -1, bool leaveOpen = false) : base(stream, encoding, detectEncodingFromByteOrderMarks,
bufferSize, leaveOpen)
{
ReadManifestVersion();
}
public ManifestReader(string path) : base(path)
{
ReadManifestVersion();
}
public ManifestReader(string path, bool detectEncodingFromByteOrderMarks) : base(path,
detectEncodingFromByteOrderMarks)
{
ReadManifestVersion();
}
public ManifestReader(string path, FileStreamOptions options) : base(path, options)
{
ReadManifestVersion();
}
public ManifestReader(string path, Encoding encoding) : base(path, encoding)
{
ReadManifestVersion();
}
public ManifestReader(string path, Encoding encoding, bool detectEncodingFromByteOrderMarks) : base(path, encoding,
detectEncodingFromByteOrderMarks)
{
ReadManifestVersion();
}
public ManifestReader(string path, Encoding encoding, bool detectEncodingFromByteOrderMarks, int bufferSize) : base(
path, encoding, detectEncodingFromByteOrderMarks, bufferSize)
{
ReadManifestVersion();
}
public ManifestReader(string path, Encoding encoding, bool detectEncodingFromByteOrderMarks,
FileStreamOptions options) : base(path, encoding, detectEncodingFromByteOrderMarks, options)
{
ReadManifestVersion();
}
public string ManifestVersion { get; private set; } = "";
public int CurrentId { get; private set; }
private void ReadManifestVersion()
{
ManifestVersion = ReadLine();
}
public RobustManifestItem? ReadItem()
{
var line = ReadLine();
if (line == null) return null;
var splited = line.Split(" ");
return new RobustManifestItem(splited[0], line.Substring(splited[0].Length + 1), CurrentId++);
}
public bool TryReadItem([NotNullWhen(true)] out RobustManifestItem? item)
{
item = ReadItem();
return item != null;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
CurrentId = 0;
}
public new void DiscardBufferedData()
{
base.DiscardBufferedData();
CurrentId = 0;
}
}

View File

@@ -0,0 +1,121 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Nebula.Launcher.Utils;
public static class RidUtility
{
public static string? FindBestRid(ICollection<string> runtimes, string? currentRid = null)
{
var catalog = LoadRidCatalog();
if (currentRid == null)
{
var reportedRid = RuntimeInformation.RuntimeIdentifier;
if (!catalog.Runtimes.ContainsKey(reportedRid))
{
currentRid = GuessRid();
Console.WriteLine(".NET reported unknown RID: {0}, guessing: {1}", reportedRid, currentRid);
}
else
{
currentRid = reportedRid;
}
}
// Breadth-first search.
var q = new Queue<string>();
if (!catalog.Runtimes.TryGetValue(currentRid, out var root))
// RID doesn't exist in catalog???
return null;
root.Discovered = true;
q.Enqueue(currentRid);
while (q.TryDequeue(out var v))
{
if (runtimes.Contains(v)) return v;
foreach (var w in catalog.Runtimes[v].Imports)
{
var r = catalog.Runtimes[w];
if (!r.Discovered)
{
q.Enqueue(w);
r.Discovered = true;
}
}
}
return null;
}
private static string GuessRid()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return RuntimeInformation.ProcessArchitecture switch
{
Architecture.X86 => "linux-x86",
Architecture.X64 => "linux-x64",
Architecture.Arm => "linux-arm",
Architecture.Arm64 => "linux-arm64",
_ => "unknown"
};
if (RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD))
return RuntimeInformation.ProcessArchitecture switch
{
Architecture.X64 => "freebsd-x64",
_ => "unknown"
};
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return RuntimeInformation.ProcessArchitecture switch
{
Architecture.X86 => "win-x86",
Architecture.X64 => "win-x64",
Architecture.Arm => "win-arm",
Architecture.Arm64 => "win-arm64",
_ => "unknown"
};
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return RuntimeInformation.ProcessArchitecture switch
{
Architecture.X64 => "osx-x64",
Architecture.Arm64 => "osx-arm64",
_ => "unknown"
};
return "unknown";
}
private static RidCatalog LoadRidCatalog()
{
using var stream = typeof(RidCatalog).Assembly.GetManifestResourceStream("Utility.runtime.json")!;
var ms = new MemoryStream();
stream.CopyTo(ms);
return JsonSerializer.Deserialize<RidCatalog>(ms.GetBuffer().AsSpan(0, (int)ms.Length))!;
}
#pragma warning disable 649
private sealed class RidCatalog
{
[JsonInclude] [JsonPropertyName("runtimes")]
public Dictionary<string, Runtime> Runtimes = default!;
public class Runtime
{
public bool Discovered;
[JsonInclude] [JsonPropertyName("#import")]
public string[] Imports = default!;
}
}
#pragma warning restore 649
}

View File

@@ -0,0 +1,58 @@
using System;
using System.Buffers;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Nebula.Launcher.Utils;
public static class StreamHelper
{
public static async ValueTask<byte[]> ReadExactAsync(this Stream stream, int amount, CancellationToken? cancel)
{
var data = new byte[amount];
await ReadExactAsync(stream, data, cancel);
return data;
}
public static async ValueTask ReadExactAsync(this Stream stream, Memory<byte> into, CancellationToken? cancel)
{
while (into.Length > 0)
{
var read = await stream.ReadAsync(into);
// Check EOF.
if (read == 0)
throw new EndOfStreamException();
into = into[read..];
}
}
public static async Task CopyAmountToAsync(
this Stream stream,
Stream to,
int amount,
int bufferSize,
CancellationToken cancel)
{
var buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
while (amount > 0)
{
Memory<byte> readInto = buffer;
if (amount < readInto.Length)
readInto = readInto[..amount];
var read = await stream.ReadAsync(readInto, cancel);
if (read == 0)
throw new EndOfStreamException();
amount -= read;
readInto = readInto[..read];
await to.WriteAsync(readInto, cancel);
}
}
}

View File

@@ -0,0 +1,135 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Web;
namespace Nebula.Launcher.Utils;
public static class UriHelper
{
public const string SchemeSs14 = "ss14";
// ReSharper disable once InconsistentNaming
public const string SchemeSs14s = "ss14s";
/// <summary>
/// Parses an <c>ss14://</c> or <c>ss14s://</c> URI,
/// defaulting to <c>ss14://</c> if no scheme is specified.
/// </summary>
[Pure]
public static Uri ParseSs14Uri(string address)
{
if (!TryParseSs14Uri(address, out var uri)) throw new FormatException("Not a valid SS14 URI");
return uri;
}
[Pure]
public static bool TryParseSs14Uri(string address, [NotNullWhen(true)] out Uri? uri)
{
if (!address.Contains("://")) address = "ss14://" + address;
if (!Uri.TryCreate(address, UriKind.Absolute, out uri)) return false;
if (uri.Scheme != SchemeSs14 && uri.Scheme != SchemeSs14s) return false;
if (string.IsNullOrWhiteSpace(uri.Host))
return false;
return true;
}
/// <summary>
/// Gets the <c>http://</c> or <c>https://</c> API address for a server address.
/// </summary>
[Pure]
public static Uri GetServerApiAddress(Uri serverAddress)
{
var dataScheme = serverAddress.Scheme switch
{
"ss14" => Uri.UriSchemeHttp,
"ss14s" => Uri.UriSchemeHttps,
_ => throw new ArgumentException($"Wrong URI scheme: {serverAddress.Scheme}")
};
var builder = new UriBuilder(serverAddress)
{
Scheme = dataScheme
};
// No port specified.
// Default port for ss14:// is 1212, for ss14s:// it's 443 (HTTPS)
if (serverAddress.IsDefaultPort && serverAddress.Scheme == SchemeSs14) builder.Port = 1212;
if (!builder.Path.EndsWith('/')) builder.Path += "/";
return builder.Uri;
}
/// <summary>
/// Gets the <c>/status</c> HTTP address for a server address.
/// </summary>
[Pure]
public static Uri GetServerStatusAddress(string serverAddress)
{
return GetServerStatusAddress(ParseSs14Uri(serverAddress));
}
/// <summary>
/// Gets the <c>/status</c> HTTP address for an ss14 uri.
/// </summary>
[Pure]
public static Uri GetServerStatusAddress(Uri serverAddress)
{
return new Uri(GetServerApiAddress(serverAddress), "status");
}
/// <summary>
/// Gets the <c>/info</c> HTTP address for a server address.
/// </summary>
[Pure]
public static Uri GetServerInfoAddress(string serverAddress)
{
return GetServerInfoAddress(ParseSs14Uri(serverAddress));
}
/// <summary>
/// Gets the <c>/info</c> HTTP address for an ss14 uri.
/// </summary>
[Pure]
public static Uri GetServerInfoAddress(Uri serverAddress)
{
return new Uri(GetServerApiAddress(serverAddress), "info");
}
/// <summary>
/// Gets the <c>/client.zip</c> HTTP address for a server address.
/// This is not necessarily the actual client ZIP address.
/// </summary>
[Pure]
public static Uri GetServerSelfhostedClientZipAddress(string serverAddress)
{
return GetServerSelfhostedClientZipAddress(ParseSs14Uri(serverAddress));
}
/// <summary>
/// Gets the <c>/client.zip</c> HTTP address for an ss14 uri.
/// This is not necessarily the actual client ZIP address.
/// </summary>
[Pure]
public static Uri GetServerSelfhostedClientZipAddress(Uri serverAddress)
{
return new Uri(GetServerApiAddress(serverAddress), "client.zip");
}
[Pure]
public static Uri AddParameter(this Uri url, string paramName, string paramValue)
{
var uriBuilder = new UriBuilder(url);
var query = HttpUtility.ParseQueryString(uriBuilder.Query);
query[paramName] = paramValue;
uriBuilder.Query = query.ToString();
return uriBuilder.Uri;
}
}

File diff suppressed because it is too large Load Diff