implement appcenter and minor fix

This commit is contained in:
HolographicHat
2022-07-03 14:06:41 +08:00
parent 468b4b91ea
commit fb8c941b57
27 changed files with 817 additions and 23 deletions

View File

@@ -12,7 +12,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.21.1" />
<PackageReference Include="Google.Protobuf" Version="3.21.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2-beta1" />
</ItemGroup>

View File

@@ -0,0 +1,106 @@
using Newtonsoft.Json;
using System.Net;
using YaeAchievement.AppCenterSDK.Models;
using YaeAchievement.AppCenterSDK.Models.Serialization;
namespace YaeAchievement.AppCenterSDK;
#pragma warning disable CA1416, CA2211
public static class AppCenter {
private const string LogCache = "./cache/bf18159fb833715i.miko";
private const string AppSecret = "648b83bf-d439-49bd-97f4-e1e506bdfe39";
private const string ApiUrl = "https://in.appcenter.ms/logs?api-version=1.0.0";
// ReSharper disable InconsistentNaming
public static Guid? SessionID;
public static readonly string DeviceID;
public static readonly Device DeviceInfo;
private static List<Log> Queue;
private static readonly Lazy<HttpClient> httpClient = new(() => new HttpClient(new HttpClientHandler {
Proxy = GlobalVars.DebugProxy ? new WebProxy("http://127.0.0.1:8888") : null
}) {
DefaultRequestHeaders = {
{ "Install-ID", DeviceID },
{ "App-Secret", AppSecret }
}
});
static AppCenter() {
Queue = new List<Log>();
DeviceID = DeviceHelper.GetDeviceID();
DeviceInfo = new Device();
var running = true;
Task.Run(() => {
// ReSharper disable once LoopVariableIsNeverChangedInsideLoop
while (running) {
Upload();
Thread.Sleep(5 * 1000);
}
});
AppDomain.CurrentDomain.ProcessExit += (_, _) => {
running = false;
if (Queue.Count > 0) {
Directory.CreateDirectory("cache");
File.WriteAllText(LogCache, Queue.ToJson());
}
};
LogSerializer.AddLogType(PageLog.JsonIdentifier, typeof(PageLog));
LogSerializer.AddLogType(EventLog.JsonIdentifier, typeof(EventLog));
LogSerializer.AddLogType(HandledErrorLog.JsonIdentifier, typeof(HandledErrorLog));
LogSerializer.AddLogType(ManagedErrorLog.JsonIdentifier, typeof(ManagedErrorLog));
LogSerializer.AddLogType(StartServiceLog.JsonIdentifier, typeof(StartServiceLog));
LogSerializer.AddLogType(StartSessionLog.JsonIdentifier, typeof(StartSessionLog));
if (Directory.Exists("./cache") && File.Exists(LogCache)) {
var list = File.ReadAllText(LogCache).FromJson()?.Logs;
if (list != null) {
Queue.AddRange(list);
}
File.Delete(LogCache);
}
}
// ReSharper restore InconsistentNaming
public static void Upload() {
if (Queue.Count == 0) return;
var uploadStatus = "";
do {
Queue = Queue.Select(log => {
log.Status = LogStatus.Uploading;
return log;
}).ToList();
using var uploadContent = new StringContent(Queue.ToJson());
try {
using var response = httpClient.Value.PostAsync(ApiUrl, uploadContent).Result;
var result = response.Content.ReadAsStringAsync().Result;
uploadStatus = JsonConvert.DeserializeObject<LogUploadResult>(result)!.Status;
} catch (Exception) {
// ignored
}
} while (uploadStatus != "Success");
Queue.RemoveAll(log => log.Status == LogStatus.Uploading);
}
public static void Init() {
new StartServiceLog("Analytics", "Crashes").Enqueue();
SessionID = Guid.NewGuid();
new StartSessionLog().Enqueue();
}
public static void TrackCrash(Exception exception, bool fatal = true) {
new ManagedErrorLog(exception, fatal).Enqueue();
}
public static void Enqueue(this Log log) {
Queue.Add(log);
}
private static string ToJson(this IEnumerable<Log> queue) {
return LogSerializer.Serialize(new LogContainer(queue));
}
private static LogContainer? FromJson(this string text) {
return LogSerializer.DeserializeLogs(text);
}
}
#pragma warning restore CA1416, CA2211

View File

@@ -0,0 +1,68 @@
using Microsoft.Win32;
using YaeAchievement.Win32;
namespace YaeAchievement.AppCenterSDK;
#pragma warning disable CA1416
public static class DeviceHelper {
public static string? GetOem() {
using var root = Registry.LocalMachine;
using var sub = root.OpenSubKey("HARDWARE\\DESCRIPTION\\System\\BIOS");
var oem = sub?.GetValue("SystemManufacturer") as string;
return oem == "System manufacturer" ? null : oem;
}
public static string? GetModel() {
using var root = Registry.LocalMachine;
using var sub = root.OpenSubKey("HARDWARE\\DESCRIPTION\\System\\BIOS");
var model = sub?.GetValue("SystemProductName") as string;
return model == "System Product Name" ? null : model;
}
public static string GetScreenSize() {
var desktop = Native.GetDC(IntPtr.Zero);
var size = $"{Native.GetDeviceCaps(desktop, 118)}x{Native.GetDeviceCaps(desktop, 117)}";
Native.ReleaseDC(IntPtr.Zero, desktop);
return size;
}
public static string? GetCountry() {
using var root = Registry.CurrentUser;
using var sub = root.OpenSubKey("Control Panel\\International\\Geo");
return sub?.GetValue("Name") as string;
}
public static string GetSystemVersion() {
using var root = Registry.LocalMachine;
using var sub = root.OpenSubKey("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion");
var majorVersion = sub?.GetValue("CurrentMajorVersionNumber");
if (majorVersion != null) {
var minorVersion = sub?.GetValue("CurrentMinorVersionNumber", "0");
var buildNumber = sub?.GetValue("CurrentBuildNumber", "0");
return $"{majorVersion}.{minorVersion}.{buildNumber}";
} else {
var version = sub?.GetValue("CurrentVersion", "0.0");
var buildNumber = sub?.GetValue("CurrentBuild", "0");
return $"{version}.{buildNumber}";
}
}
public static int GetSystemBuild() {
using var root = Registry.LocalMachine;
using var sub = root.OpenSubKey("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion");
return (int) (sub?.GetValue("UBR") ?? 0);
}
// ReSharper disable once InconsistentNaming
public static string GetDeviceID() {
using var sdk = Registry.CurrentUser.OpenSubKey("SOFTWARE\\miHoYoSDK");
if (sdk?.GetValue("MIHOYOSDK_DEVICE_ID") is not string id) {
id = $"{Random.Shared.NextInt64().ToString().SHA1Hash().ToLower()}{DateTimeOffset.Now.ToUnixTimeMilliseconds()}";
sdk?.SetValue("MIHOYOSDK_DEVICE_ID", id);
}
id = id.MD5Hash().ToLower();
return $"{id[..8]}-{id[8..12]}-{id[12..16]}-{id[16..20]}-{id[20..]}";
}
}
#pragma warning restore CA1416

View File

@@ -0,0 +1,25 @@
using YaeAchievement.AppCenterSDK.Models;
namespace YaeAchievement.AppCenterSDK;
public static class ErrorLogHelper {
public static MException CreateModelExceptionAndBinaries(Exception exception) {
var modelException = new MException {
Type = exception.GetType().ToString(),
Message = exception.Message,
StackTrace = exception.StackTrace
};
if (exception is AggregateException aggregateException) {
if (aggregateException.InnerExceptions.Count != 0) {
modelException.InnerExceptions = new List<MException>();
foreach (var innerException in aggregateException.InnerExceptions) {
modelException.InnerExceptions.Add(CreateModelExceptionAndBinaries(innerException));
}
}
} else if (exception.InnerException != null) {
modelException.InnerExceptions ??= new List<MException>();
modelException.InnerExceptions.Add(CreateModelExceptionAndBinaries(exception.InnerException));
}
return modelException;
}
}

View File

@@ -0,0 +1,51 @@
using System.Globalization;
using System.Reflection;
using Newtonsoft.Json;
namespace YaeAchievement.AppCenterSDK.Models;
public class Device {
[JsonProperty(PropertyName = "sdkName")]
public string SdkName { get; set; } = "appcenter.wpf.netcore";
[JsonProperty(PropertyName = "sdkVersion")]
public string SdkVersion { get; set; } = "4.5.0";
[JsonProperty(PropertyName = "osName")]
public string OsName { get; set; } = "WINDOWS";
[JsonProperty(PropertyName = "osVersion")]
public string OsVersion { get; set; } = DeviceHelper.GetSystemVersion();
[JsonProperty(PropertyName = "osBuild")]
public string OsBuild { get; set; } = $"{DeviceHelper.GetSystemVersion()}.{DeviceHelper.GetSystemBuild()}";
[JsonProperty(PropertyName = "model")]
public string? Model { get; set; } = DeviceHelper.GetModel();
[JsonProperty(PropertyName = "oemName")]
public string? OemName { get; set; } = DeviceHelper.GetOem();
[JsonProperty(PropertyName = "screenSize")]
public string ScreenSize { get; set; } = DeviceHelper.GetScreenSize();
[JsonProperty(PropertyName = "carrierCountry")]
public string Country { get; set; } = DeviceHelper.GetCountry() ?? "CN";
[JsonProperty(PropertyName = "locale")]
public string Locale { get; set; } = CultureInfo.CurrentCulture.Name;
[JsonProperty(PropertyName = "timeZoneOffset")]
public int TimeZoneOffset { get; set; } = (int) TimeZoneInfo.Local.BaseUtcOffset.TotalMinutes;
[JsonProperty(PropertyName = "appVersion")]
public string AppVersion { get; set; } = GlobalVars.AppVersionName;
[JsonProperty(PropertyName = "appBuild")]
public string AppBuild { get; set; } = GlobalVars.AppVersionCode.ToString();
[JsonProperty(PropertyName = "appNamespace")]
public string AppNamespace { get; set; } = Assembly.GetEntryAssembly()?.EntryPoint?.DeclaringType?.Namespace ?? string.Empty;
}

View File

@@ -0,0 +1,20 @@
using Newtonsoft.Json;
namespace YaeAchievement.AppCenterSDK.Models;
[JsonObject(JsonIdentifier)]
public class EventLog : LogWithProperties {
public const string JsonIdentifier = "event";
public EventLog(string name) {
Name = name;
}
[JsonProperty(PropertyName = "id")]
private Guid Id { get; set; } = Guid.NewGuid();
[JsonProperty(PropertyName = "name")]
private string Name { get; set; }
}

View File

@@ -0,0 +1,21 @@
using Newtonsoft.Json;
namespace YaeAchievement.AppCenterSDK.Models;
[JsonObject(JsonIdentifier)]
public class HandledErrorLog : LogWithProperties {
public const string JsonIdentifier = "handledError";
public HandledErrorLog(MException exception) {
Id = Guid.NewGuid();
Exception = exception;
}
[JsonProperty(PropertyName = "id")]
public Guid? Id { get; set; }
[JsonProperty(PropertyName = "exception")]
public MException Exception { get; set; }
}

View File

@@ -0,0 +1,23 @@
using Newtonsoft.Json;
namespace YaeAchievement.AppCenterSDK.Models;
public abstract class Log {
[JsonProperty(PropertyName = "sid")]
private Guid? Session { get; set; } = AppCenter.SessionID;
[JsonProperty(PropertyName = "timestamp")]
private DateTime Timestamp { get; set; } = DateTime.UtcNow;
[JsonProperty(PropertyName = "device")]
private Device Device { get; set; } = AppCenter.DeviceInfo;
[JsonIgnore]
internal LogStatus Status = LogStatus.Pending;
}
public enum LogStatus {
Pending, Uploading, Uploaded
}

View File

@@ -0,0 +1,14 @@
using Newtonsoft.Json;
namespace YaeAchievement.AppCenterSDK.Models;
public class LogContainer {
public LogContainer(IEnumerable<Log> logs) {
Logs = logs;
}
[JsonProperty(PropertyName = "logs")]
public IEnumerable<Log> Logs { get; set; }
}

View File

@@ -0,0 +1,19 @@
using Newtonsoft.Json;
namespace YaeAchievement.AppCenterSDK.Models;
public class LogUploadResult {
[JsonProperty(PropertyName = "status")]
public string Status { get; set; } = null!;
[JsonProperty(PropertyName = "validDiagnosticsIds")]
public Guid[] ValidDiagnosticsIds { get; set; } = Array.Empty<Guid>();
[JsonProperty(PropertyName = "throttledDiagnosticsIds")]
public Guid[] ThrottledDiagnosticsIds { get; set; } = Array.Empty<Guid>();
[JsonProperty(PropertyName = "correlationId")]
public Guid CorrelationId { get; set; }
}

View File

@@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace YaeAchievement.AppCenterSDK.Models;
public class LogWithProperties : Log {
[JsonProperty(PropertyName = "properties")]
public IDictionary<string, string> Properties { get; set; } = new Dictionary<string, string>();
}

View File

@@ -0,0 +1,22 @@
using Newtonsoft.Json;
namespace YaeAchievement.AppCenterSDK.Models;
public class MException {
[JsonProperty(PropertyName = "type")]
public string Type { get; set; } = "UnknownType";
[JsonProperty(PropertyName = "message")]
public string? Message { get; set; }
[JsonProperty(PropertyName = "stackTrace")]
public string? StackTrace { get; set; }
[JsonProperty(PropertyName = "frames")]
public IList<StackFrame>? Frames { get; set; }
[JsonProperty(PropertyName = "innerExceptions")]
public IList<MException>? InnerExceptions { get; set; }
}

View File

@@ -0,0 +1,50 @@
using System.Diagnostics;
using Newtonsoft.Json;
namespace YaeAchievement.AppCenterSDK.Models;
[JsonObject(JsonIdentifier)]
public class ManagedErrorLog : Log {
public const string JsonIdentifier = "managedError";
public ManagedErrorLog(
Exception exception,
bool fatal = true
) {
var p = Process.GetCurrentProcess();
Id = Guid.NewGuid();
Fatal = fatal;
UserId = AppCenter.DeviceID;
ProcessId = p.Id;
Exception = ErrorLogHelper.CreateModelExceptionAndBinaries(exception);
ProcessName = p.ProcessName;
Architecture = Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE");
AppLaunchTimestamp = p.StartTime.ToUniversalTime();
}
[JsonProperty(PropertyName = "id")]
public Guid Id { get; set; }
[JsonProperty(PropertyName = "userId")]
public string? UserId { get; set; }
[JsonProperty(PropertyName = "processId")]
public int ProcessId { get; set; }
[JsonProperty(PropertyName = "processName")]
public string ProcessName { get; set; }
[JsonProperty(PropertyName = "fatal")]
public bool Fatal { get; set; }
[JsonProperty(PropertyName = "appLaunchTimestamp")]
public DateTime? AppLaunchTimestamp { get; set; }
[JsonProperty(PropertyName = "architecture")]
public string? Architecture { get; set; }
[JsonProperty(PropertyName = "exception")]
public MException Exception { get; set; }
}

View File

@@ -0,0 +1,17 @@
using Newtonsoft.Json;
namespace YaeAchievement.AppCenterSDK.Models;
[JsonObject(JsonIdentifier)]
public class PageLog : LogWithProperties {
public const string JsonIdentifier = "page";
public PageLog(string name) {
Name = name;
}
[JsonProperty(PropertyName = "name")]
public string Name { get; set; }
}

View File

@@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System.Reflection;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace YaeAchievement.AppCenterSDK.Models.Serialization;
#pragma warning disable CS8604, CS8765
public class LogJsonConverter : JsonConverter {
private readonly Dictionary<string, Type> _logTypes = new ();
private readonly object _jsonConverterLock = new ();
private static readonly JsonSerializerSettings SerializationSettings;
static LogJsonConverter() {
SerializationSettings = new JsonSerializerSettings {
Formatting = Formatting.None,
NullValueHandling = NullValueHandling.Ignore,
DateFormatHandling = DateFormatHandling.IsoDateFormat,
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
ReferenceLoopHandling = ReferenceLoopHandling.Serialize
};
}
public void AddLogType(string typeName, Type type) {
lock (_jsonConverterLock) {
_logTypes[typeName] = type;
}
}
public override bool CanConvert(Type objectType) {
return typeof(Log).IsAssignableFrom(objectType);
}
public override object? ReadJson(JsonReader reader, Type t, object o, JsonSerializer s) {
Type logType;
var jsonObject = JObject.Load(reader);
var typeName = jsonObject.GetValue("type")?.ToString();
lock (_jsonConverterLock) {
if (typeName == null || !_logTypes.ContainsKey(typeName)) {
throw new JsonReaderException("Could not identify type of log");
}
logType = _logTypes[typeName];
jsonObject.Remove("type");
}
return jsonObject.ToObject(logType);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
var info = value.GetType().GetTypeInfo();
if (info.GetCustomAttribute(typeof(JsonObjectAttribute)) is not JsonObjectAttribute attribute) {
throw new JsonWriterException("Log type is missing JsonObjectAttribute");
}
var jsonObject = JObject.FromObject(value, JsonSerializer.CreateDefault(SerializationSettings));
jsonObject.Add("type", JToken.FromObject(attribute.Id));
writer.WriteRawValue(jsonObject.ToString(Formatting.None));
}
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using Newtonsoft.Json;
namespace YaeAchievement.AppCenterSDK.Models.Serialization;
#pragma warning disable CS8604, CS8765, CS8603
public static class LogSerializer {
private static readonly JsonSerializerSettings SerializationSettings;
private static readonly LogJsonConverter Converter = new ();
static LogSerializer() {
SerializationSettings = new JsonSerializerSettings {
Formatting = Formatting.None,
NullValueHandling = NullValueHandling.Ignore,
DateFormatHandling = DateFormatHandling.IsoDateFormat,
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
Converters = { Converter }
};
}
public static void AddLogType(string typeName, Type type) {
Converter.AddLogType(typeName, type);
}
public static string Serialize(LogContainer logContainer) {
return JsonConvert.SerializeObject(logContainer, SerializationSettings);
}
public static string Serialize(Log log) {
return JsonConvert.SerializeObject(log, SerializationSettings);
}
public static LogContainer? DeserializeLogs(string json) {
return JsonConvert.DeserializeObject<LogContainer>(json, SerializationSettings);
}
}

View File

@@ -0,0 +1,34 @@
using Newtonsoft.Json;
namespace YaeAchievement.AppCenterSDK.Models;
public class StackFrame {
public StackFrame(string address, string code, string className, string methodName, int? lineNumber, string fileName) {
Address = address;
Code = code;
ClassName = className;
MethodName = methodName;
LineNumber = lineNumber;
FileName = fileName;
}
[JsonProperty(PropertyName = "address")]
public string Address { get; set; }
[JsonProperty(PropertyName = "code")]
public string Code { get; set; }
[JsonProperty(PropertyName = "className")]
public string ClassName { get; set; }
[JsonProperty(PropertyName = "methodName")]
public string MethodName { get; set; }
[JsonProperty(PropertyName = "lineNumber")]
public int? LineNumber { get; set; }
[JsonProperty(PropertyName = "fileName")]
public string FileName { get; set; }
}

View File

@@ -0,0 +1,17 @@
using Newtonsoft.Json;
namespace YaeAchievement.AppCenterSDK.Models;
[JsonObject(JsonIdentifier)]
public class StartServiceLog : Log {
public const string JsonIdentifier = "startService";
public StartServiceLog(params string[] services) {
Services = services;
}
[JsonProperty(PropertyName = "services")]
public string[] Services { get; set; }
}

View File

@@ -0,0 +1,8 @@
using Newtonsoft.Json;
namespace YaeAchievement.AppCenterSDK.Models;
[JsonObject(JsonIdentifier)]
public class StartSessionLog : Log {
public const string JsonIdentifier = "startSession";
}

View File

@@ -171,5 +171,4 @@ public static class Export {
var b = Utils.GetBucketFileAsByteArray("schicksal/metadata");
return AchievementInfo.Parser.ParseFrom(b);
}
}

106
src/Extensions.cs Normal file
View File

@@ -0,0 +1,106 @@
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Web;
using Newtonsoft.Json;
namespace YaeAchievement;
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public static class Extensions {
// ReSharper disable once InconsistentNaming
private static readonly Lazy<Aes> aes = new (Aes.Create);
// ReSharper disable once InconsistentNaming
private static readonly Lazy<MD5> md5 = new (MD5.Create);
// ReSharper disable once InconsistentNaming
private static readonly Lazy<SHA1> sha1 = new (SHA1.Create);
// ReSharper disable once InconsistentNaming
private static readonly Lazy<HttpClient> defaultClient = new (() => new HttpClient(new HttpClientHandler {
Proxy = GlobalVars.DebugProxy ? new WebProxy("http://127.0.0.1:8888") : null
}) {
DefaultRequestHeaders = {{ "User-Agent", "UnityPlayer/2017.4.30f1 (UnityWebRequest/1.0, libcurl/7.51.0-DEV)" }}
});
public static byte[] ToBytes(this string text) {
return Encoding.UTF8.GetBytes(text);
}
public static string DecodeToString(this byte[] bytes) {
return Encoding.UTF8.GetString(bytes);
}
// ReSharper disable once InconsistentNaming
public static string MD5Hash(this string text) {
return md5.Value.ComputeHash(text.ToBytes()).ToHex();
}
// ReSharper disable once InconsistentNaming
public static string SHA1Hash(this string text, bool base64 = true) {
var bytes = sha1.Value.ComputeHash(text.ToBytes());
return base64 ? bytes.ToBase64() : bytes.ToHex();
}
public static HttpResponseMessage Send(this HttpRequestMessage message, HttpClient? client = null) {
Logger.Trace($"{message.Method} {message.RequestUri?.GetFullPath()}");
return (client ?? defaultClient.Value).Send(message); // dispose message?
}
public static T? Send<T>(this HttpRequestMessage message, HttpClient? client = null, Func<string, string>? onPreDeserialize = null) {
Logger.Trace($"{message.Method} {message.RequestUri?.GetFullPath()}");
using var response = (client ?? defaultClient.Value).Send(message);
var text = response.Content.ReadAsStringAsync().Result;
if (onPreDeserialize != null) {
text = onPreDeserialize.Invoke(text);
}
return JsonConvert.DeserializeObject<T>(text);
}
public static string ToQueryString(this NameValueCollection collection, bool escape = true) {
var items = collection.AllKeys
.Select(key => escape ?
$"{HttpUtility.UrlEncode(key)}={HttpUtility.UrlEncode(collection[key])}" :
$"{key}={collection[key]}");
return string.Join("&", items);
}
public static string ToQueryString(this IEnumerable<KeyValuePair<string, object>> dict, bool escape = true) {
var items = dict
.Select(pair => escape ?
$"{HttpUtility.UrlEncode(pair.Key)}={HttpUtility.UrlEncode(pair.Value.ToString())}" :
$"{pair.Key}={pair.Value}");
return string.Join("&", items);
}
public static string ToJsonString<TKey, TValue>(this Dictionary<TKey, TValue> data) where TKey : notnull {
return JsonConvert.SerializeObject(data);
}
public static string GetFullPath(this Uri uri) {
return $"{uri.Scheme}://{uri.Host}{uri.AbsolutePath}";
}
public static IEnumerable<KeyValuePair<string, string>> ToKeyValuePairs(this NameValueCollection collection) {
return collection.AllKeys
.Select(key => new KeyValuePair<string, string>(key!, collection[key] ?? string.Empty))
.ToArray();
}
public static string UrlEncode(this string text) {
return HttpUtility.UrlEncode(text);
}
public static string ToHex(this byte[] bytes) {
return Convert.ToHexString(bytes);
}
public static string ToBase64(this byte[] bytes) {
return Convert.ToBase64String(bytes);
}
public static byte[] FromBase64(this string text) {
return Convert.FromBase64String(text);
}
}

View File

@@ -3,21 +3,25 @@ using System.Reflection;
namespace YaeAchievement;
[SuppressMessage("Usage", "CA2211:非常量字段应当不可见")]
[SuppressMessage("ReSharper", "ConvertToConstant.Global")]
[SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Global")]
#pragma warning disable CA2211
public static class GlobalVars {
public static bool Verbose = false;
public static bool DebugProxy = false;
public static bool CheckGamePath = true;
public static bool UnexpectedExit = true;
public static string GamePath = "";
public static string GamePath = null!;
public static Logger.Level LogLevel = Logger.Level.Info;
public static Version AppVersion = Assembly.GetEntryAssembly()!.GetName().Version!;
public const uint AppVersionCode = 77;
public const string AppVersionName = "2.?";
public const uint AppVersionCode = 28;
public const string AppVersionName = "2.0";
public const string LibName = "YaeLib.dll";
public const string PipeName = "YaeAchievementPipe";
public const string BucketHost = "https://cn-cd-1259389942.file.myqcloud.com";
public const string ConfigFileName = "YaeAchievement.runtimeconfig.json";
}
#pragma warning restore CA2211

40
src/Logger.cs Normal file
View File

@@ -0,0 +1,40 @@
namespace YaeAchievement;
public static class Logger {
public enum Level {
Trace, Debug, Info, Warn, Error
}
public static void Error(string msg) {
Log(msg, Level.Error);
}
public static void Warn(string msg) {
Log(msg, Level.Warn);
}
public static void Info(string msg) {
Log(msg, Level.Info);
}
public static void Debug(string msg) {
Log(msg, Level.Debug);
}
public static void Trace(string msg) {
Log(msg, Level.Trace);
}
private static void Log(string msg, Level level) {
if (level >= GlobalVars.LogLevel) {
Console.WriteLine($"{DateTime.Now:MM/dd HH:mm:ss} {level.ToString().ToUpper().PadLeft(5)} : {msg}");
}
}
public static void WriteLog(string msg, Level level = Level.Info) {
if (level >= GlobalVars.LogLevel) {
Console.Write($"{DateTime.Now:MM/dd HH:mm:ss} {level.ToString().ToUpper().PadLeft(5)} : {msg}");
}
}
}

View File

@@ -1,7 +1,10 @@
using YaeAchievement;
using YaeAchievement.AppCenterSDK;
using YaeAchievement.AppCenterSDK.Models;
using static YaeAchievement.Utils;
InstallExitHook();
CheckSelfIsRunning();
TryDisableQuickEdit();
InstallExceptionHook();
CheckGenshinIsRunning();
@@ -13,6 +16,13 @@ Console.WriteLine("----------------------------------------------------");
LoadConfig();
CheckUpdate();
AppCenter.Init();
new EventLog("AppInit") {
Properties = {
{ "AppVersion", GlobalVars.AppVersionName },
{ "SystemVersion", DeviceHelper.GetSystemVersion() }
}
}.Enqueue();
StartAndWaitResult(GlobalVars.GamePath, str => {
GlobalVars.UnexpectedExit = false;
var list = AchievementAllDataNotify.Parser.ParseFrom(Convert.FromBase64String(str));

View File

@@ -9,6 +9,7 @@ using System.Security.Cryptography;
using System.Text;
using System.Text.Json.Nodes;
using Google.Protobuf;
using YaeAchievement.AppCenterSDK;
using YaeAchievement.Win32;
using static YaeAchievement.Win32.OpenFileFlags;
@@ -18,7 +19,7 @@ public static class Utils {
public static readonly Lazy<HttpClient> CHttpClient = new (() => {
var c = new HttpClient(new HttpClientHandler {
Proxy = new WebProxy("http://127.0.0.1:8888"),
Proxy = GlobalVars.DebugProxy ? new WebProxy("http://127.0.0.1:8888") : null,
AutomaticDecompression = DecompressionMethods.Brotli | DecompressionMethods.GZip
}) {
DefaultRequestHeaders = {
@@ -99,10 +100,10 @@ public static class Utils {
public static void LoadConfig() {
var conf = JsonNode.Parse(File.ReadAllText(GlobalVars.ConfigFileName))!;
var path = conf["genshinPath"];
var path = conf["location"];
if (path == null || CheckGamePathValid(path.GetValue<string>())) {
GlobalVars.GamePath = SelectGameExecutable();
conf["genshinPath"] = GlobalVars.GamePath;
conf["location"] = GlobalVars.GamePath;
File.WriteAllText(GlobalVars.ConfigFileName, conf.ToJsonString());
} else {
GlobalVars.GamePath = path.GetValue<string>();
@@ -116,15 +117,15 @@ public static class Utils {
Console.WriteLine($"更新内容: \n{info.Description}");
if (info.EnableAutoDownload) {
Console.WriteLine("正在下载更新包...");
var fullPath = Path.GetFullPath("update.7z");
var fullPath = Path.GetFullPath($"update.{Path.GetExtension(info.PackageLink)}");
File.WriteAllBytes(fullPath, GetBucketFileAsByteArray(info.PackageLink));
Console.WriteLine("下载完毕! 关闭程序后, 将压缩包解压至当前目录即可完成更新.");
Console.WriteLine("关闭程序后, 将压缩包解压至当前目录即可完成更新.");
ShellOpen(fullPath);
Environment.Exit(0);
}
Console.WriteLine($"下载地址: {info.PackageLink}");
if (info.ForceUpdate) {
Console.WriteLine("在完成此次更新前, 程序可能无法正常使用.");
//Console.WriteLine("在完成此次更新前, 程序可能无法正常使用.");
Environment.Exit(0);
}
}
@@ -133,6 +134,18 @@ public static class Utils {
}
}
public static void CheckSelfIsRunning() {
Process.EnterDebugMode();
var cur = Process.GetCurrentProcess();
foreach (var process in Process.GetProcesses().Where(process => process.Id != cur.Id)) {
if (process.ProcessName == cur.ProcessName) {
Logger.Error("另一个实例正在运行,请关闭后重试");
Environment.Exit(302);
}
}
Process.LeaveDebugMode();
}
public static bool ShellOpen(string path) {
return new Process {
StartInfo = {
@@ -144,7 +157,8 @@ public static class Utils {
private static bool CheckGamePathValid(string path) {
var dir = Path.GetDirectoryName(path)!;
return File.Exists($"{dir}/UnityPlayer.dll") && File.Exists($"{dir}/mhypbase.dll");
return !GlobalVars.CheckGamePath ||
File.Exists($"{dir}/UnityPlayer.dll") && File.Exists($"{dir}/mhypbase.dll");
}
private static string SelectGameExecutable() {
@@ -202,9 +216,13 @@ public static class Utils {
Process.LeaveDebugMode();
}
// ReSharper disable once InconsistentNaming
private static Process? proc;
public static void InstallExitHook() {
AppDomain.CurrentDomain.ProcessExit += (_, _) => {
Console.WriteLine("按任意键退出");
proc?.Kill();
Logger.Info("按任意键退出");
Console.ReadKey();
};
}
@@ -212,7 +230,9 @@ public static class Utils {
public static void InstallExceptionHook() {
AppDomain.CurrentDomain.UnhandledException += (_, e) => {
Console.WriteLine(e.ExceptionObject.ToString());
Console.WriteLine("发生错误,请联系开发者以获取帮助");
Logger.Error("正在上报错误信息...");
AppCenter.TrackCrash((Exception) e.ExceptionObject);
AppCenter.Upload();
Environment.Exit(-1);
};
}
@@ -227,10 +247,12 @@ public static class Utils {
Environment.Exit(new Win32Exception().PrintMsgAndReturnErrCode("TerminateProcess fail"));
}
}
var proc = Process.GetProcessById(Convert.ToInt32(pid));
Logger.Info($"原神正在启动 ({pid})");
proc = Process.GetProcessById(Convert.ToInt32(pid));
proc.EnableRaisingEvents = true;
proc.Exited += (_, _) => {
if (GlobalVars.UnexpectedExit) {
proc = null;
Console.WriteLine("游戏进程异常退出");
Environment.Exit(114514);
}

View File

@@ -1,14 +1,13 @@
using System.ComponentModel;
using YaeAchievement.AppCenterSDK;
namespace YaeAchievement.Win32;
public static class Extensions {
public static int PrintMsgAndReturnErrCode(this Win32Exception ex, string msg) {
Console.WriteLine($"{msg}: {ex.Message}");
if (GlobalVars.Verbose) {
Console.WriteLine(ex.StackTrace);
}
Logger.Error($"{msg}: {ex.Message}");
AppCenter.TrackCrash(ex, false);
return ex.NativeErrorCode;
}

View File

@@ -138,4 +138,13 @@ public static class Native {
[DllImport("user32.dll", SetLastError = true)]
public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
[DllImport("gdi32.dll")]
public static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
[DllImport("user32.dll")]
public static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern bool ReleaseDC(IntPtr hWnd, IntPtr hdc);
}