zworld-em/Plugins/sentry-unreal/Source/SentryEditor/Private/SentrySettingsCustomization.cpp
2025-05-11 22:07:21 +08:00

637 lines
19 KiB
C++

// Copyright (c) 2022 Sentry. All Rights Reserved.
#include "SentrySettingsCustomization.h"
#include "SentrySettings.h"
#include "SentryModule.h"
#include "SentrySubsystem.h"
#include "SentrySymToolsDownloader.h"
#include "DetailCategoryBuilder.h"
#include "DetailLayoutBuilder.h"
#include "DetailWidgetRow.h"
#include "IUATHelperModule.h"
#include "Engine/Engine.h"
#include "Misc/Paths.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/EngineVersionComparison.h"
#include "PropertyHandle.h"
#include "Framework/Notifications/NotificationManager.h"
#include "HAL/FileManager.h"
#include "Interfaces/IPluginManager.h"
#include "Runtime/Launch/Resources/Version.h"
#include "Widgets/Text/SRichTextBlock.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Layout/SBorder.h"
#include "Widgets/Layout/SWidgetSwitcher.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Notifications/SNotificationList.h"
#include "Widgets/Images/SImage.h"
#if UE_VERSION_OLDER_THAN(5, 0, 0)
#include "EditorStyleSet.h"
#else
#include "Styling/AppStyle.h"
#endif
#if UE_VERSION_OLDER_THAN(5, 0, 0)
#include "HAL/PlatformFilemanager.h"
#else
#include "HAL/PlatformFileManager.h"
#endif
const FString FSentrySettingsCustomization::DefaultCrcEndpoint = TEXT("https://datarouter.ol.epicgames.com/datarouter/api/v1/public/data");
void OnDocumentationLinkClicked(const FSlateHyperlinkRun::FMetadata& Metadata);
FSentrySettingsCustomization::FSentrySettingsCustomization()
: CliDownloader(MakeShareable(new FSentrySymToolsDownloader()))
, IsCompilingLinuxBinaries(false)
{
}
FSentrySettingsCustomization::~FSentrySettingsCustomization()
{
}
TSharedRef<IDetailCustomization> FSentrySettingsCustomization::MakeInstance()
{
return MakeShareable(new FSentrySettingsCustomization);
}
void FSentrySettingsCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
{
DrawGeneralNotice(DetailBuilder);
DrawDebugSymbolsNotice(DetailBuilder);
DrawCrashReporterNotice(DetailBuilder);
SetPropertiesUpdateHandler(DetailBuilder);
}
void FSentrySettingsCustomization::DrawGeneralNotice(IDetailLayoutBuilder& DetailBuilder)
{
IDetailCategoryBuilder& GeneralCategory = DetailBuilder.EditCategory(TEXT("General"));
TSharedRef<SWidget> GeneralMissingDsnWidget = MakeGeneralSettingsStatusRow(FName(TEXT("SettingsEditor.WarningIcon")),
FText::FromString(TEXT("Sentry DSN is not configured.")), FText());
TSharedRef<SWidget> GeneralModifiedWidget = MakeGeneralSettingsStatusRow(FName(TEXT("SettingsEditor.WarningIcon")),
FText::FromString(TEXT("Sentry settings were modified.")), FText::FromString(TEXT("Apply")));
TSharedRef<SWidget> GeneralConfiguredWidget = MakeGeneralSettingsStatusRow(FName(TEXT("SettingsEditor.GoodIcon")),
FText::FromString(TEXT("Sentry is configured.")), FText());
#if UE_VERSION_OLDER_THAN(5, 0, 0)
const ISlateStyle& Style = FEditorStyle::Get();
#else
const ISlateStyle& Style = FAppStyle::Get();
#endif
GeneralCategory.AddCustomRow(FText::FromString(TEXT("General")), false)
.WholeRowWidget
[
SNew(SBorder)
.Padding(8.0f)
[
SNew(SWidgetSwitcher)
.WidgetIndex(this, &FSentrySettingsCustomization::GetGeneralSettingsStatusAsInt)
+SWidgetSwitcher::Slot()
[
GeneralMissingDsnWidget
]
+SWidgetSwitcher::Slot()
[
GeneralModifiedWidget
]
+SWidgetSwitcher::Slot()
[
GeneralConfiguredWidget
]
]
];
#if PLATFORM_WINDOWS
if (FSentryModule::Get().IsMarketplaceVersion())
{
TSharedRef<SWidget> LinuxBinariesMissingWidget = MakeLinuxBinariesStatusRow(FName(TEXT("SettingsEditor.WarningIcon")),
FText::FromString(TEXT("Sentry Linux pre-compiled binaries are missing.")), FText::FromString(TEXT("Compile")));
TSharedRef<SWidget> LinuxBinariesCompilingWidget = MakeLinuxBinariesStatusRow(FName(TEXT("SettingsEditor.WarningIcon")),
FText::FromString(TEXT("Compiling Sentry for Linux...")), FText());
TSharedRef<SWidget> LinuxBinariesConfiguredWidget = MakeLinuxBinariesStatusRow(FName(TEXT("SettingsEditor.GoodIcon")),
FText::FromString(TEXT("Sentry Linux pre-compiled binaries are ready.")), FText());
GeneralCategory.AddCustomRow(FText::FromString(TEXT("General")), false)
.WholeRowWidget
[
SNew(SBorder)
.Padding(8.0f)
[
SNew(SWidgetSwitcher)
.WidgetIndex(this, &FSentrySettingsCustomization::GetLinuxBinariesStatusAsInt)
+SWidgetSwitcher::Slot()
[
LinuxBinariesMissingWidget
]
+SWidgetSwitcher::Slot()
[
LinuxBinariesCompilingWidget
]
+SWidgetSwitcher::Slot()
[
LinuxBinariesConfiguredWidget
]
]
];
}
#endif
}
void FSentrySettingsCustomization::DrawCrashReporterNotice(IDetailLayoutBuilder& DetailBuilder)
{
IDetailCategoryBuilder& CrashReporterCategory = DetailBuilder.EditCategory(TEXT("Crash Reporter"));
TSharedPtr<IPropertyHandle> CrashReporterUrlHandle = DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(USentrySettings, CrashReporterUrl));
#if UE_VERSION_OLDER_THAN(5, 0, 0)
const ISlateStyle& Style = FEditorStyle::Get();
#else
const ISlateStyle& Style = FAppStyle::Get();
#endif
CrashReporterCategory.AddCustomRow(FText::FromString(TEXT("CrashReporter")), false)
.WholeRowWidget
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.Padding(1)
.AutoHeight()
[
SNew(SBorder)
.Padding(1)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.Padding(FMargin(10, 10, 10, 10))
.FillWidth(1.0f)
[
SNew(SRichTextBlock)
.Text(FText::FromString(TEXT("In order to configure Crash Reporter use Sentry's Unreal Engine Endpoint from the Client Keys settings page. "
"This will include which project within Sentry you want to see the crashes arriving in real time. "
"Note that it's accomplished by modifying the `CrashReportClient` section in the global <RichTextBlock.TextHighlight>DefaultEngine.ini</> file. "
"Changing the engine is necessary for this to work!")))
.TextStyle(Style, "MessageLog")
.DecoratorStyleSet(&Style)
.AutoWrapText(true)
]
]
]
+ SVerticalBox::Slot()
.Padding(FMargin(0, 10, 0, 10))
.VAlign(VAlign_Top)
[
SNew(SRichTextBlock)
.Text(FText::FromString(TEXT("<a id=\"browser\" href=\"https://docs.sentry.io/platforms/unreal/setup-crashreporter/\">View the Crash Reporter setup documentation -></>")))
.AutoWrapText(true)
.DecoratorStyleSet(&FCoreStyle::Get())
+ SRichTextBlock::HyperlinkDecorator(TEXT("browser"), FSlateHyperlinkRun::FOnClick::CreateStatic(&OnDocumentationLinkClicked))
]
+ SVerticalBox::Slot()
.Padding(FMargin(0, 10, 0, 10))
.AutoHeight()
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(FMargin(0, 0, 5, 0))
[
SNew(SButton)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
.ContentPadding(FMargin(8, 2))
.OnClicked_Lambda([this, CrashReporterUrlHandle]() -> FReply
{
FString CrcEndpoint;
CrashReporterUrlHandle->GetValue(CrcEndpoint);
UpdateCrcConfig(CrcEndpoint);
return FReply::Handled();
})
.Text(FText::FromString(TEXT("Update global settings")))
.ToolTipText(FText::FromString(TEXT("Update global crash reporter settings in DefaultEngine.ini configuration file.")))
]
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(FMargin(5, 0, 5, 0))
[
SNew(SButton)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
.ContentPadding(FMargin(8, 2))
.OnClicked_Lambda([this]() -> FReply
{
UpdateCrcConfig(DefaultCrcEndpoint);
return FReply::Handled();
})
.Text(FText::FromString("Reset"))
.ToolTipText(FText::FromString(TEXT("Reset crash reporter settings to defaults.")))
]
]
];
}
void FSentrySettingsCustomization::DrawDebugSymbolsNotice(IDetailLayoutBuilder& DetailBuilder)
{
IDetailCategoryBuilder& DebugSymbolsCategory = DetailBuilder.EditCategory(TEXT("Debug Symbols"));
TSharedRef<SWidget> CliMissingWidget = MakeSentryCliStatusRow(FName(TEXT("SettingsEditor.WarningIcon")),
FText::FromString(TEXT("Sentry symbol upload tools are not configured.")), FText::FromString(TEXT("Configure Now")));
TSharedRef<SWidget> CliDownloadingWidget = MakeSentryCliStatusRow(FName(TEXT("SettingsEditor.WarningIcon")),
FText::FromString(TEXT("Downloading Sentry symbol upload tools...")), FText());
TSharedRef<SWidget> CliConfiguredWidget = MakeSentryCliStatusRow(FName(TEXT("SettingsEditor.GoodIcon")),
FText::FromString(TEXT("Sentry symbol upload tools are configured.")), FText::FromString(TEXT("Reload")));
#if UE_VERSION_OLDER_THAN(5, 0, 0)
const ISlateStyle& Style = FEditorStyle::Get();
#else
const ISlateStyle& Style = FAppStyle::Get();
#endif
DebugSymbolsCategory.AddCustomRow(FText::FromString(TEXT("DebugSymbols")), false)
.WholeRowWidget
[
SNew(SBorder)
.Padding(8.0f)
[
SNew(SWidgetSwitcher)
.WidgetIndex(this, &FSentrySettingsCustomization::GetSentryCliStatusAsInt)
+SWidgetSwitcher::Slot()
[
CliMissingWidget
]
+SWidgetSwitcher::Slot()
[
CliDownloadingWidget
]
+SWidgetSwitcher::Slot()
[
CliConfiguredWidget
]
]
];
DebugSymbolsCategory.AddCustomRow(FText::FromString(TEXT("DebugSymbols")), false)
.WholeRowWidget
[
SNew(SBorder)
.Padding(1)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.Padding(FMargin(10, 10, 10, 10))
.FillWidth(1.0f)
[
SNew(SRichTextBlock)
.Text(FText::FromString(TEXT("Note that the Sentry SDK creates a <RichTextBlock.TextHighlight>sentry.properties</> file at project root to store the configuration, "
"that should <RichTextBlock.TextHighlight>NOT</> be made publicly available.")))
.TextStyle(Style, "MessageLog")
.DecoratorStyleSet(&Style)
.AutoWrapText(true)
]
]
];
}
void FSentrySettingsCustomization::SetPropertiesUpdateHandler(IDetailLayoutBuilder& DetailBuilder)
{
const FSimpleDelegate OnUpdateProjectName = FSimpleDelegate::CreateSP(this, &FSentrySettingsCustomization::UpdateProjectName);
ProjectNameHandle = DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(USentrySettings, ProjectName));
ProjectNameHandle->SetOnPropertyValueChanged(OnUpdateProjectName);
const FSimpleDelegate OnUpdateOrganizationName = FSimpleDelegate::CreateSP(this, &FSentrySettingsCustomization::UpdateOrganizationName);
OrganizationNameHandle = DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(USentrySettings, OrgName));
OrganizationNameHandle->SetOnPropertyValueChanged(OnUpdateOrganizationName);
const FSimpleDelegate OnUpdateAuthToken = FSimpleDelegate::CreateSP(this, &FSentrySettingsCustomization::UpdateAuthToken);
AuthTokenHandle = DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(USentrySettings, AuthToken));
AuthTokenHandle->SetOnPropertyValueChanged(OnUpdateAuthToken);
}
TSharedRef<SWidget> FSentrySettingsCustomization::MakeGeneralSettingsStatusRow(FName IconName, FText Message, FText ButtonMessage)
{
TSharedRef<SHorizontalBox> Result = SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
[
SNew(SImage)
#if UE_VERSION_OLDER_THAN(5, 0, 0)
.Image(FEditorStyle::Get().GetBrush(IconName))
#else
.Image(FAppStyle::Get().GetBrush(IconName))
#endif
]
+SHorizontalBox::Slot()
.FillWidth(1.0f)
.Padding(16.0f, 0.0f)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.ColorAndOpacity(FLinearColor::White)
.ShadowColorAndOpacity(FLinearColor::Black)
.ShadowOffset(FVector2D::UnitVector)
.Text(Message)
];
if (!ButtonMessage.IsEmpty())
{
Result->AddSlot()
.AutoWidth()
.VAlign(VAlign_Center)
[
SNew(SButton)
.OnClicked_Lambda([this]() -> FReply
{
USentrySubsystem* SentrySubsystem = GEngine->GetEngineSubsystem<USentrySubsystem>();
SentrySubsystem->Close();
SentrySubsystem->Initialize();
USentrySettings* Settings = FSentryModule::Get().GetSettings();
Settings->ClearDirtyFlag();
return FReply::Handled();
})
.Text(ButtonMessage)
];
}
return Result;
}
TSharedRef<SWidget> FSentrySettingsCustomization::MakeLinuxBinariesStatusRow(FName IconName, FText Message, FText ButtonMessage)
{
TSharedRef<SHorizontalBox> Result = SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
[
SNew(SImage)
#if UE_VERSION_OLDER_THAN(5, 0, 0)
.Image(FEditorStyle::Get().GetBrush(IconName))
#else
.Image(FAppStyle::Get().GetBrush(IconName))
#endif
]
+SHorizontalBox::Slot()
.FillWidth(1.0f)
.Padding(16.0f, 0.0f)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.ColorAndOpacity(FLinearColor::White)
.ShadowColorAndOpacity(FLinearColor::Black)
.ShadowOffset(FVector2D::UnitVector)
.Text(Message)
];
if (!ButtonMessage.IsEmpty())
{
Result->AddSlot()
.AutoWidth()
.VAlign(VAlign_Center)
[
SNew(SButton)
.OnClicked_Lambda([this]() -> FReply
{
IsCompilingLinuxBinaries = true;
// In case the plugin installed via Epic Games launcher it's supposed to be <EngineDir>/Plugins/Marketplace/Sentry
const FString PluginPath = IPluginManager::Get().FindPlugin(TEXT("Sentry"))->GetBaseDir();
const FString TempLinuxBinariesPath = FPaths::ConvertRelativePathToFull(FPaths::Combine(FPaths::ProjectDir(), TEXT("Intermediate"), TEXT("SentryLinuxBinaries")));
FString CommandLine = FString::Printf(TEXT("BuildPlugin -Plugin=\"%s/Sentry.uplugin\" -Package=\"%s\" -CreateSubFolder -TargetPlatforms=Linux"), *PluginPath, *TempLinuxBinariesPath);
IUATHelperModule::Get().CreateUatTask(CommandLine, FText::FromString("Windows"),
FText::FromString("Compiling Sentry for Linux"),
FText::FromString("Compile Sentry Linux"),
#if UE_VERSION_OLDER_THAN(5, 1, 0)
FEditorStyle::GetBrush(TEXT("MainFrame.CookContent")),
#else
FAppStyle::GetBrush(TEXT("MainFrame.CookContent")),
nullptr,
#endif
[this, TempLinuxBinariesPath](FString result, double X)
{
if (result.Equals(TEXT("Completed")))
{
FPlatformFileManager::Get().GetPlatformFile().CopyDirectoryTree(*GetLinuxBinariesDirPath(),
*FPaths::Combine(TempLinuxBinariesPath, TEXT("Intermediate"), TEXT("Build"), TEXT("Linux")), true);
}
IsCompilingLinuxBinaries = false;
});
return FReply::Handled();
})
.Text(ButtonMessage)
];
}
return Result;
}
TSharedRef<SWidget> FSentrySettingsCustomization::MakeSentryCliStatusRow(FName IconName, FText Message, FText ButtonMessage)
{
TSharedRef<SHorizontalBox> Result = SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
[
SNew(SImage)
#if UE_VERSION_OLDER_THAN(5, 0, 0)
.Image(FEditorStyle::Get().GetBrush(IconName))
#else
.Image(FAppStyle::Get().GetBrush(IconName))
#endif
]
+SHorizontalBox::Slot()
.FillWidth(1.0f)
.Padding(16.0f, 0.0f)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.ColorAndOpacity(FLinearColor::White)
.ShadowColorAndOpacity(FLinearColor::Black)
.ShadowOffset(FVector2D::UnitVector)
.Text(Message)
];
if (!ButtonMessage.IsEmpty())
{
Result->AddSlot()
.AutoWidth()
.VAlign(VAlign_Center)
[
SNew(SButton)
.OnClicked_Lambda([this]() -> FReply
{
if(CliDownloader.IsValid() && CliDownloader->GetStatus() != ESentrySymToolsStatus::Downloading)
{
CliDownloader->Download([](bool Result)
{
FNotificationInfo Info(FText::FromString(Result
? TEXT("Sentry symbol upload tools was configured successfully.")
: TEXT("Sentry symbol upload tools configuration failed.")));
Info.ExpireDuration = 3.0f;
Info.bUseSuccessFailIcons = true;
TSharedPtr<SNotificationItem> EditorNotification = FSlateNotificationManager::Get().AddNotification(Info);
EditorNotification->SetCompletionState(Result ? SNotificationItem::CS_Success : SNotificationItem::CS_Fail);
});
}
return FReply::Handled();
})
.Text(ButtonMessage)
];
}
return Result;
}
void FSentrySettingsCustomization::UpdateProjectName()
{
FString Value;
ProjectNameHandle->GetValue(Value);
UpdatePropertiesFile(TEXT("defaults.project"), Value);
}
void FSentrySettingsCustomization::UpdateOrganizationName()
{
FString Value;
OrganizationNameHandle->GetValue(Value);
UpdatePropertiesFile(TEXT("defaults.org"), Value);
}
void FSentrySettingsCustomization::UpdateAuthToken()
{
FString Value;
AuthTokenHandle->GetValue(Value);
UpdatePropertiesFile(TEXT("auth.token"), Value);
}
void FSentrySettingsCustomization::UpdatePropertiesFile(const FString& PropertyName, const FString& PropertyValue)
{
const FString PropertiesFilePath = FPaths::Combine(FPaths::ProjectDir(), TEXT("sentry.properties"));
FConfigFile PropertiesFile;
if (FPaths::FileExists(PropertiesFilePath))
{
PropertiesFile.Read(PropertiesFilePath);
}
PropertiesFile.SetString(TEXT("Sentry"), *PropertyName, *PropertyValue);
PropertiesFile.Write(PropertiesFilePath);
}
void FSentrySettingsCustomization::UpdateCrcConfig(const FString& Url)
{
if (Url.IsEmpty())
{
return;
}
const FString CrcConfigFilePath = GetCrcConfigPath();
if (!FPaths::FileExists(CrcConfigFilePath))
{
return;
}
FConfigCacheIni CrcConfigFile(EConfigCacheType::DiskBacked);
CrcConfigFile.LoadFile(CrcConfigFilePath);
const FString CrcSectionName = FString(TEXT("CrashReportClient"));
const FString DataRouterUrlKey = FString(TEXT("DataRouterUrl"));
const FString DataRouterUrlValue = Url;
CrcConfigFile.SetString(*CrcSectionName, *DataRouterUrlKey, *DataRouterUrlValue, CrcConfigFilePath);
}
FString FSentrySettingsCustomization::GetCrcConfigPath() const
{
return FPaths::Combine(FPaths::EngineDir(), TEXT("Programs"), TEXT("CrashReportClient"), TEXT("Config"), TEXT("DefaultEngine.ini"));
}
FString FSentrySettingsCustomization::GetLinuxBinariesDirPath() const
{
const FString PluginPath = IPluginManager::Get().FindPlugin(TEXT("Sentry"))->GetBaseDir();
return FPaths::Combine(PluginPath, TEXT("Intermediate"), TEXT("Build"), TEXT("Linux"));
}
int32 FSentrySettingsCustomization::GetGeneralSettingsStatusAsInt() const
{
USentrySubsystem* SentrySubsystem = GEngine->GetEngineSubsystem<USentrySubsystem>();
USentrySettings* Settings = FSentryModule::Get().GetSettings();
if (Settings->Dsn.IsEmpty())
{
return static_cast<int32>(ESentrySettingsStatus::DsnMissing);
}
if (Settings->IsDirty() && SentrySubsystem->IsEnabled())
{
return static_cast<int32>(ESentrySettingsStatus::Modified);
}
return static_cast<int32>(ESentrySettingsStatus::Configured);
}
int32 FSentrySettingsCustomization::GetLinuxBinariesStatusAsInt() const
{
if (IsCompilingLinuxBinaries)
{
return static_cast<int32>(ESentryLinuxBinariesStatus::Compiling);
}
if (IFileManager::Get().DirectoryExists(*GetLinuxBinariesDirPath()))
{
return static_cast<int32>(ESentryLinuxBinariesStatus::Configured);
}
return static_cast<int32>(ESentryLinuxBinariesStatus::Missing);
}
int32 FSentrySettingsCustomization::GetSentryCliStatusAsInt() const
{
if (CliDownloader.IsValid())
{
return static_cast<int32>(CliDownloader->GetStatus());
}
return 0;
}
void OnDocumentationLinkClicked(const FSlateHyperlinkRun::FMetadata& Metadata)
{
const FString* UrlPtr = Metadata.Find(TEXT("href"));
if (UrlPtr)
{
FPlatformProcess::LaunchURL(**UrlPtr, nullptr, nullptr);
}
}