Files
CC-Framework.CrashReport/Editor/BuglyAndroidSymbolUtility.cs

652 lines
26 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using UnityEditor;
using UnityEngine;
using Debug = UnityEngine.Debug;
namespace CCFramework.CrashReport.Editor
{
public static class BuglyAndroidSymbolUtility
{
public delegate void LogHandler(string message, LogType type = LogType.Log);
public const string BuglyDownloadUrl = "https://bugly.tds.tencent.com/docs/assets/files/bugly_symbol_oversea_v3.4.23-a449c7a62d7792de76fca1af623584f5.zip";
public const string BuglyToolGuideUrl = "https://bugly.tds.tencent.com/docs/tutorial/symbol/tool/";
private const string SymbolFolderSuffix = "_bugly_symbols";
private const string SymbolPackageSuffix = "_bugly_symbols.zip";
private const string UploadStagingDirectory = "Library/BuglySymbolUpload";
public static void ApplyAndroidSymbolSettings(BuildProfile profile, CrashReportBuglyProfileSettings settings, LogHandler log)
{
if (!IsAndroidProfile(profile) || settings == null || !settings.enableAndroidSymbolArchive)
{
return;
}
EditorUserBuildSettings.androidCreateSymbols = AndroidCreateSymbols.Debugging;
WriteLog(log, "已启用 Android Debugging 符号表生成,构建后会产出 native-debug-symbols.zip。");
}
public static BuglySymbolArchiveResult ArchiveAfterBuild(
BuildProfile profile,
CrashReportBuglyProfileSettings settings,
string buildOutputPath,
LogHandler log)
{
BuglySymbolArchiveResult result = new BuglySymbolArchiveResult();
if (!IsAndroidProfile(profile) || settings == null || !settings.enableAndroidSymbolArchive)
{
result.message = "当前不是 Android 构建或未启用 Bugly 符号表归档。";
WriteLog(log, result.message);
return result;
}
string resolvedBuildOutputPath = ResolveFullPath(buildOutputPath);
string buildDirectory = Path.GetDirectoryName(resolvedBuildOutputPath);
string buildName = Path.GetFileNameWithoutExtension(resolvedBuildOutputPath);
string archiveDirectory = Path.Combine(buildDirectory, buildName + SymbolFolderSuffix);
Directory.CreateDirectory(archiveDirectory);
string nativeSymbolPath = FindNativeDebugSymbols(profile);
string mappingPath = FindMappingFile(profile, resolvedBuildOutputPath);
if (!string.IsNullOrWhiteSpace(nativeSymbolPath))
{
result.nativeSymbolPath = Path.Combine(archiveDirectory, buildName + "_native-debug-symbols.zip");
File.Copy(nativeSymbolPath, result.nativeSymbolPath, true);
WriteLog(log, $"已归档 Bugly SO 符号表:{result.nativeSymbolPath}");
}
else
{
WriteLog(log, "未找到 native-debug-symbols.zipNative Crash 可能无法还原。", LogType.Warning);
}
if (!string.IsNullOrWhiteSpace(mappingPath))
{
result.mappingPath = Path.Combine(archiveDirectory, buildName + "_mapping.txt");
File.Copy(mappingPath, result.mappingPath, true);
WriteLog(log, $"已归档 Bugly mapping 文件:{result.mappingPath}");
}
result.archiveDirectory = archiveDirectory;
result.manifestPath = WriteManifest(profile, resolvedBuildOutputPath, archiveDirectory, result);
result.packagePath = CreatePackage(archiveDirectory, Path.Combine(buildDirectory, buildName + SymbolPackageSuffix));
result.hasSymbols = File.Exists(result.nativeSymbolPath) || File.Exists(result.mappingPath);
result.message = result.hasSymbols ? "Bugly 符号表归档完成。" : "Bugly 符号表归档完成,但没有找到可上传的符号表文件。";
WriteLog(log, $"Bugly 符号表包:{result.packagePath}");
return result;
}
public static BuglySymbolUploadResult UploadPreparedSymbols(
BuildProfile profile,
CrashReportBuglyProfileSettings settings,
BuglySymbolArchiveResult archive,
LogHandler log)
{
if (profile == null || settings == null)
{
return FailUpload("构建配置为空,无法上传 Bugly 符号表。", log);
}
if (archive == null || !archive.hasSymbols)
{
return FailUpload("没有可上传的 Bugly 符号表文件。", log);
}
string javaPath = string.IsNullOrWhiteSpace(settings.buglyJavaPath) ? "java" : settings.buglyJavaPath.Trim();
string toolPath = ResolveSymbolToolPath(settings);
if (string.IsNullOrWhiteSpace(settings.buglyAppId))
{
return FailUpload("Bugly App ID 为空。", log);
}
if (string.IsNullOrWhiteSpace(settings.buglyAppKey))
{
return FailUpload("Bugly App Key 为空。", log);
}
if (string.IsNullOrWhiteSpace(toolPath) || !File.Exists(toolPath))
{
return FailUpload($"Bugly 符号表工具不存在:{toolPath}", log);
}
BuglySymbolArchiveResult uploadArchive = PrepareUploadInputs(profile, archive, log);
List<string> arguments = new List<string>
{
"-jar",
Quote(toolPath),
"-appid",
Quote(settings.buglyAppId.Trim()),
"-appkey",
Quote(settings.buglyAppKey.Trim()),
"-version",
Quote(profile.version),
"-buildNo",
Quote(profile.buildNumber.ToString()),
"-platform",
"Android"
};
if (PathExists(uploadArchive.nativeSymbolPath))
{
arguments.Add("-inputSymbol");
arguments.Add(Quote(uploadArchive.nativeSymbolPath));
}
if (File.Exists(uploadArchive.mappingPath))
{
arguments.Add("-inputMapping");
arguments.Add(Quote(uploadArchive.mappingPath));
}
string argumentText = string.Join(" ", arguments);
WriteLog(log, $"开始上传 Bugly 符号表:{javaPath} {MaskUploadArguments(argumentText)}");
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = javaPath,
Arguments = argumentText,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
StandardOutputEncoding = GetBuglyToolOutputEncoding(),
StandardErrorEncoding = GetBuglyToolOutputEncoding()
};
try
{
using (Process process = Process.Start(startInfo))
{
string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();
BuglySymbolUploadResult result = new BuglySymbolUploadResult
{
success = process.ExitCode == 0 && IsBuglyUploadOutputSuccessful(output, error),
exitCode = process.ExitCode,
output = output,
error = error,
command = javaPath + " " + MaskUploadArguments(argumentText)
};
result.logPath = WriteUploadLog(archive.archiveDirectory, result);
if (result.success)
{
WriteLog(log, $"Bugly 符号表上传完成,日志:{result.logPath}");
}
else
{
WriteLog(log, $"Bugly 符号表上传失败ExitCode={result.exitCode},日志:{result.logPath}", LogType.Error);
}
return result;
}
}
catch (Exception e)
{
return FailUpload($"执行 Bugly 符号表上传失败:{e.Message}", log);
}
}
public static string BuildUploadCommandTemplate(BuildProfile profile, CrashReportBuglyProfileSettings settings, bool maskSecret)
{
if (profile == null || settings == null)
{
return string.Empty;
}
string buildOutputPath = ResolveFullPath(BuildPipelineCore.GetExpectedBuildOutputPath(profile));
string buildDirectory = Path.GetDirectoryName(buildOutputPath);
string buildName = Path.GetFileNameWithoutExtension(buildOutputPath);
string archiveDirectory = Path.Combine(buildDirectory, buildName + SymbolFolderSuffix);
string appKey = string.IsNullOrWhiteSpace(settings.buglyAppKey) ? "<Bugly App Key>" : settings.buglyAppKey.Trim();
if (maskSecret && !string.IsNullOrWhiteSpace(settings.buglyAppKey))
{
appKey = "******";
}
return (string.IsNullOrWhiteSpace(settings.buglyJavaPath) ? "java" : settings.buglyJavaPath.Trim()) +
" -jar " + Quote(ResolveSymbolToolPath(settings)) +
" -appid " + Quote(string.IsNullOrWhiteSpace(settings.buglyAppId) ? "<Bugly App ID>" : settings.buglyAppId.Trim()) +
" -appkey " + Quote(appKey) +
" -version " + Quote(profile.version) +
" -buildNo " + Quote(profile.buildNumber.ToString()) +
" -platform Android" +
" -inputSymbol " + Quote(Path.Combine(archiveDirectory, buildName + "_native-debug-symbols.zip")) +
" -inputMapping " + Quote(Path.Combine(archiveDirectory, buildName + "_mapping.txt"));
}
public static string ResolveSymbolToolPath(CrashReportBuglyProfileSettings settings)
{
if (settings != null && !string.IsNullOrWhiteSpace(settings.buglySymbolToolPath))
{
string configuredPath = ResolveFullPath(settings.buglySymbolToolPath);
if (File.Exists(configuredPath))
{
return configuredPath;
}
}
string bundledRoot = Path.Combine(GetProjectRoot(), "Tools", "BuglySymbolTool");
if (!Directory.Exists(bundledRoot))
{
return string.Empty;
}
string newestPath = string.Empty;
DateTime newestWriteTime = DateTime.MinValue;
foreach (string jarPath in Directory.GetFiles(bundledRoot, "*.jar", SearchOption.AllDirectories))
{
DateTime writeTime = File.GetLastWriteTimeUtc(jarPath);
if (writeTime > newestWriteTime)
{
newestPath = jarPath;
newestWriteTime = writeTime;
}
}
return newestPath;
}
public static string ToProfilePath(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return string.Empty;
}
string fullPath = ResolveFullPath(path);
string projectRoot = GetProjectRoot().TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
string rootPrefix = projectRoot + Path.DirectorySeparatorChar;
if (fullPath.StartsWith(rootPrefix, StringComparison.OrdinalIgnoreCase))
{
return fullPath.Substring(rootPrefix.Length).Replace(Path.DirectorySeparatorChar, '/');
}
return path;
}
private static bool IsAndroidProfile(BuildProfile profile)
{
if (profile == null)
{
return false;
}
Type profileType = profile.GetType();
string targetPlatform = GetStringField(profileType, profile, "targetPlatform");
string buildTargetGroup = GetStringField(profileType, profile, "buildTargetGroup");
string buildTarget = GetStringField(profileType, profile, "buildTarget");
bool hasExplicitPlatformValue =
!string.IsNullOrWhiteSpace(targetPlatform) ||
!string.IsNullOrWhiteSpace(buildTargetGroup) ||
!string.IsNullOrWhiteSpace(buildTarget);
return IsPlatformValue(targetPlatform, "Android") ||
IsPlatformValue(buildTargetGroup, "Android") ||
IsPlatformValue(buildTarget, "Android") ||
(!hasExplicitPlatformValue && EditorUserBuildSettings.activeBuildTarget == BuildTarget.Android);
}
private static string GetStringField(Type ownerType, object instance, string fieldName)
{
FieldInfo field = ownerType.GetField(fieldName, BindingFlags.Public | BindingFlags.Instance);
return field?.GetValue(instance) as string ?? string.Empty;
}
private static bool IsPlatformValue(string value, string expectedValue)
{
return !string.IsNullOrWhiteSpace(value) &&
string.Equals(value, expectedValue, StringComparison.OrdinalIgnoreCase);
}
private static string FindNativeDebugSymbols(BuildProfile profile)
{
string buildType = profile != null && profile.isDevelopment ? "debug" : "release";
string preferredPath = Path.Combine(
GetProjectRoot(),
"Library",
"Bee",
"Android",
"Prj",
"IL2CPP",
"Gradle",
"launcher",
"build",
"outputs",
"native-debug-symbols",
buildType,
"native-debug-symbols.zip");
return File.Exists(preferredPath)
? preferredPath
: FindNewestFile(Path.Combine(GetProjectRoot(), "Library", "Bee", "Android"), "native-debug-symbols.zip");
}
private static string FindMappingFile(BuildProfile profile, string buildOutputPath)
{
string buildDirectory = Path.GetDirectoryName(buildOutputPath);
string newestOutputMapping = FindNewestFile(buildDirectory, "*mapping*.txt");
if (!string.IsNullOrEmpty(newestOutputMapping))
{
return newestOutputMapping;
}
string buildType = profile != null && profile.isDevelopment ? "debug" : "release";
string preferredRoot = Path.Combine(
GetProjectRoot(),
"Library",
"Bee",
"Android",
"Prj",
"IL2CPP",
"Gradle",
"launcher",
"build",
"outputs",
"mapping",
buildType);
string preferredMapping = FindNewestFile(preferredRoot, "mapping.txt");
return !string.IsNullOrEmpty(preferredMapping)
? preferredMapping
: FindNewestFile(Path.Combine(GetProjectRoot(), "Library", "Bee", "Android"), "mapping.txt");
}
private static string FindNewestFile(string root, string fileName)
{
if (string.IsNullOrEmpty(root) || !Directory.Exists(root))
{
return string.Empty;
}
string newestPath = string.Empty;
DateTime newestWriteTime = DateTime.MinValue;
foreach (string file in Directory.GetFiles(root, fileName, SearchOption.AllDirectories))
{
DateTime writeTime = File.GetLastWriteTimeUtc(file);
if (writeTime > newestWriteTime)
{
newestPath = file;
newestWriteTime = writeTime;
}
}
return newestPath;
}
private static string WriteManifest(BuildProfile profile, string buildOutputPath, string archiveDirectory, BuglySymbolArchiveResult result)
{
BuglySymbolManifest manifest = new BuglySymbolManifest
{
productName = profile.productName,
bundleIdentifier = profile.bundleIdentifier,
version = profile.version,
buildNumber = profile.buildNumber,
buildOutputPath = buildOutputPath,
buildOutputSha256 = File.Exists(buildOutputPath) ? GetSHA256Hash(buildOutputPath) : string.Empty,
nativeSymbolPath = result.nativeSymbolPath,
nativeSymbolSha256 = File.Exists(result.nativeSymbolPath) ? GetSHA256Hash(result.nativeSymbolPath) : string.Empty,
mappingPath = result.mappingPath,
mappingSha256 = File.Exists(result.mappingPath) ? GetSHA256Hash(result.mappingPath) : string.Empty,
createdAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")
};
string manifestPath = Path.Combine(archiveDirectory, "bugly-symbol-manifest.json");
File.WriteAllText(manifestPath, JsonUtility.ToJson(manifest, true), Encoding.UTF8);
return manifestPath;
}
private static string CreatePackage(string archiveDirectory, string packagePath)
{
if (File.Exists(packagePath))
{
File.Delete(packagePath);
}
ZipFile.CreateFromDirectory(
archiveDirectory,
packagePath,
System.IO.Compression.CompressionLevel.Optimal,
false);
return packagePath;
}
private static BuglySymbolArchiveResult PrepareUploadInputs(BuildProfile profile, BuglySymbolArchiveResult archive, LogHandler log)
{
string stagingRoot = Path.Combine(GetProjectRoot(), UploadStagingDirectory.Replace("/", Path.DirectorySeparatorChar.ToString()));
string stagingDirectory = Path.Combine(stagingRoot, BuildSafeStagingName(profile) + "_" + DateTime.Now.ToString("yyyyMMddHHmmss"));
Directory.CreateDirectory(stagingDirectory);
BuglySymbolArchiveResult uploadArchive = new BuglySymbolArchiveResult
{
hasSymbols = archive.hasSymbols,
archiveDirectory = archive.archiveDirectory,
packagePath = archive.packagePath,
manifestPath = archive.manifestPath,
message = archive.message
};
if (File.Exists(archive.nativeSymbolPath))
{
string symbolDirectory = Path.Combine(stagingDirectory, "symbols");
Directory.CreateDirectory(symbolDirectory);
ZipFile.ExtractToDirectory(archive.nativeSymbolPath, symbolDirectory);
uploadArchive.nativeSymbolPath = symbolDirectory;
WriteLog(log, $"已解压 Bugly SO 符号表到英文临时目录:{symbolDirectory}");
}
if (File.Exists(archive.mappingPath))
{
uploadArchive.mappingPath = Path.Combine(stagingDirectory, "mapping.txt");
File.Copy(archive.mappingPath, uploadArchive.mappingPath, true);
}
return uploadArchive;
}
private static string BuildSafeStagingName(BuildProfile profile)
{
string rawName = profile == null ? "bugly" : profile.profileName + "_" + profile.version + "_" + profile.buildNumber;
StringBuilder builder = new StringBuilder(rawName.Length);
foreach (char ch in rawName)
{
builder.Append(char.IsLetterOrDigit(ch) || ch == '_' || ch == '-' || ch == '.' ? ch : '_');
}
return builder.ToString();
}
private static string WriteUploadLog(string archiveDirectory, BuglySymbolUploadResult result)
{
string logDirectory = Directory.Exists(archiveDirectory) ? archiveDirectory : GetProjectRoot();
string logPath = Path.Combine(logDirectory, "bugly-symbol-upload.log");
File.WriteAllText(
logPath,
$"Time: {DateTime.Now:yyyy-MM-dd HH:mm:ss}\nCommand: {result.command}\nExitCode: {result.exitCode}\n--- Output ---\n{result.output}\n--- Error ---\n{result.error}",
Encoding.UTF8);
return logPath;
}
private static BuglySymbolUploadResult FailUpload(string message, LogHandler log)
{
WriteLog(log, message, LogType.Error);
return new BuglySymbolUploadResult
{
success = false,
exitCode = -1,
error = message
};
}
private static bool PathExists(string path)
{
return File.Exists(path) || Directory.Exists(path);
}
private static Encoding GetBuglyToolOutputEncoding()
{
try
{
return Encoding.GetEncoding("GB18030");
}
catch
{
return Encoding.Default;
}
}
private static bool IsBuglyUploadOutputSuccessful(string output, string error)
{
string combinedText = (output ?? string.Empty) + "\n" + (error ?? string.Empty);
return combinedText.IndexOf("plugin execute result status: failure", StringComparison.OrdinalIgnoreCase) < 0 &&
combinedText.IndexOf("event_res:failure", StringComparison.OrdinalIgnoreCase) < 0 &&
combinedText.IndexOf("##[error]", StringComparison.OrdinalIgnoreCase) < 0 &&
combinedText.IndexOf("errorMsg is", StringComparison.OrdinalIgnoreCase) < 0;
}
private static string ResolveFullPath(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return string.Empty;
}
string normalizedPath = path.Replace("{inproject}: ", string.Empty);
return Path.IsPathRooted(normalizedPath)
? Path.GetFullPath(normalizedPath)
: Path.GetFullPath(Path.Combine(GetProjectRoot(), normalizedPath));
}
private static string GetProjectRoot()
{
return Directory.GetParent(Application.dataPath).FullName;
}
private static string Quote(string value)
{
return string.IsNullOrEmpty(value) ? "\"\"" : "\"" + value.Replace("\"", "\\\"") + "\"";
}
private static string MaskUploadArguments(string arguments)
{
return MaskArgumentValue(arguments, "-appkey");
}
private static string MaskArgumentValue(string arguments, string key)
{
int keyIndex = arguments.IndexOf(key, StringComparison.OrdinalIgnoreCase);
if (keyIndex < 0)
{
return arguments;
}
int valueStart = keyIndex + key.Length;
while (valueStart < arguments.Length && char.IsWhiteSpace(arguments[valueStart]))
{
valueStart++;
}
if (valueStart >= arguments.Length)
{
return arguments;
}
int tokenEnd = valueStart;
if (arguments[valueStart] == '"')
{
tokenEnd = arguments.IndexOf('"', valueStart + 1);
return tokenEnd > valueStart
? arguments.Substring(0, valueStart + 1) + "******" + arguments.Substring(tokenEnd)
: arguments;
}
while (tokenEnd < arguments.Length && !char.IsWhiteSpace(arguments[tokenEnd]))
{
tokenEnd++;
}
return arguments.Substring(0, valueStart) + "******" + arguments.Substring(tokenEnd);
}
private static string GetSHA256Hash(string filePath)
{
using (SHA256 sha256 = SHA256.Create())
using (FileStream stream = File.OpenRead(filePath))
{
return BitConverter.ToString(sha256.ComputeHash(stream)).Replace("-", "").ToLowerInvariant();
}
}
private static void WriteLog(LogHandler log, string message, LogType type = LogType.Log)
{
if (log != null)
{
log(message, type);
return;
}
switch (type)
{
case LogType.Error:
case LogType.Exception:
Debug.LogError(message);
break;
case LogType.Warning:
Debug.LogWarning(message);
break;
default:
Debug.Log(message);
break;
}
}
}
[Serializable]
public class BuglySymbolArchiveResult
{
public bool hasSymbols;
public string archiveDirectory;
public string packagePath;
public string nativeSymbolPath;
public string mappingPath;
public string manifestPath;
public string message;
}
[Serializable]
public class BuglySymbolUploadResult
{
public bool success;
public int exitCode;
public string command;
public string output;
public string error;
public string logPath;
}
[Serializable]
public class BuglySymbolManifest
{
public string productName;
public string bundleIdentifier;
public string version;
public int buildNumber;
public string buildOutputPath;
public string buildOutputSha256;
public string nativeSymbolPath;
public string nativeSymbolSha256;
public string mappingPath;
public string mappingSha256;
public string createdAt;
}
}