Files
UnrealEngineUWP/Engine/Source/Programs/LiveCodingConsole/Private/LiveCodingConsole.cpp
tim smith 899eaa25cd Improved notifications in the editor/game for live coding.
Added message that packaging can fail if assets reference new changes.

#rb
#rnx
#jira UE-115558
#preflight 60c39c8e8d00b80001b1e85f

#ROBOMERGE-SOURCE: CL 16645001 in //UE5/Main/...
#ROBOMERGE-BOT: STARSHIP (Main -> Release-Engine-Test) (v833-16641396)

[CL 16645007 by tim smith in ue5-release-engine-test branch]
2021-06-11 14:48:40 -04:00

590 lines
19 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "LiveCodingConsole.h"
#include "RequiredProgramMainCPPInclude.h"
#include "Framework/Application/SlateApplication.h"
#include "StandaloneRenderer.h"
#include "LiveCodingConsoleStyle.h"
#include "HAL/PlatformProcess.h"
#include "SLogWidget.h"
#include "Modules/ModuleManager.h"
#include "ILiveCodingServer.h"
#include "Features/IModularFeatures.h"
#include "Widgets/Notifications/SNotificationList.h"
#include "Widgets/Input/SButton.h"
#include "Framework/Notifications/NotificationManager.h"
#include "Windows/WindowsHWrapper.h"
#include "Misc/MonitoredProcess.h"
#include "LiveCodingManifest.h"
#include "SourceCodeAccess/Public/ISourceCodeAccessModule.h"
#include "Modules/ModuleInterface.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Input/SCheckBox.h"
#include "Widgets/Text/STextBlock.h"
#include "Misc/CompilationResult.h"
#include "Misc/MessageDialog.h"
#define LOCTEXT_NAMESPACE "LiveCodingConsole"
IMPLEMENT_APPLICATION(LiveCodingConsole, "LiveCodingConsole");
static void OnRequestExit()
{
RequestEngineExit(TEXT("LiveCoding console closed"));
}
class FLiveCodingConsoleApp
{
private:
FCriticalSection CriticalSection;
FSlateApplication& Slate;
ILiveCodingServer& Server;
TSharedPtr<SLogWidget> LogWidget;
TSharedPtr<SWindow> Window;
TSharedPtr<SNotificationItem> CompileNotification;
TArray<FSimpleDelegate> MainThreadTasks;
bool bRequestCancel;
bool bDisableLimit;
FDateTime LastPatchTime;
FDateTime NextPatchStartTime;
public:
FLiveCodingConsoleApp(FSlateApplication& InSlate, ILiveCodingServer& InServer)
: Slate(InSlate)
, Server(InServer)
, bRequestCancel(false)
, bDisableLimit(false)
, LastPatchTime(FDateTime::MinValue())
, NextPatchStartTime(FDateTime::MinValue())
{
}
void Run()
{
// open up the app window
LogWidget = SNew(SLogWidget);
// Create the window
Window =
SNew(SWindow)
.Title(GetWindowTitle())
.ClientSize(FVector2D(1200.0f, 600.0f))
.ActivationPolicy(EWindowActivationPolicy::Never)
.IsInitiallyMaximized(false)
[
SNew(SBorder)
.BorderImage(FCoreStyle::Get().GetBrush("ToolPanel.GroupBorder"))
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.FillHeight(1.0f)
[
LogWidget.ToSharedRef()
]
+ SVerticalBox::Slot()
.HAlign(HAlign_Center)
.AutoHeight()
.Padding(0.0f, 6.0f, 0.0f, 4.0f)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.HAlign(HAlign_Center)
[
SNew(SButton)
.Text(LOCTEXT("QuickRestart", "Quick Restart"))
.OnClicked(FOnClicked::CreateRaw(this, &FLiveCodingConsoleApp::RestartTargets))
.ToolTipText_Lambda([this]() { return Server.HasReinstancingProcess() ?
LOCTEXT("DisableQuickRestart", "Quick restarting isn't supported when re-instancing is enabled") :
LOCTEXT("EnableQuickRestart", "Restart all live coding applications"); })
.IsEnabled_Lambda([this]() { return !Server.HasReinstancingProcess(); })
]
+ SHorizontalBox::Slot()
.HAlign(HAlign_Center)
.Padding(5)
[
SNew(SCheckBox)
.IsChecked_Lambda([this]() { return bDisableLimit ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; })
.OnCheckStateChanged_Lambda([this](ECheckBoxState InCheckBoxState) { bDisableLimit = InCheckBoxState == ECheckBoxState::Checked; })
[
SNew(STextBlock)
.Text(LOCTEXT("DisableLimit", "Disable action limit for this session"))
]
]
]
]
];
// Add the window without showing it
Slate.AddWindow(Window.ToSharedRef(), false);
// Show the window without stealling focus
if (!FParse::Param(FCommandLine::Get(), TEXT("Hidden")))
{
HWND ForegroundWindow = GetForegroundWindow();
if (ForegroundWindow != nullptr)
{
::SetWindowPos((HWND)Window->GetNativeWindow()->GetOSWindowHandle(), ForegroundWindow, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
}
Window->ShowWindow();
}
// Get the server interface
Server.GetBringToFrontDelegate().BindRaw(this, &FLiveCodingConsoleApp::BringToFrontAsync);
Server.GetLogOutputDelegate().BindRaw(this, &FLiveCodingConsoleApp::AppendLogLine);
Server.GetShowConsoleDelegate().BindRaw(this, &FLiveCodingConsoleApp::BringToFrontAsync);
Server.GetSetVisibleDelegate().BindRaw(this, &FLiveCodingConsoleApp::SetVisibleAsync);
Server.GetCompileDelegate().BindRaw(this, &FLiveCodingConsoleApp::CompilePatch);
Server.GetCompileStartedDelegate().BindRaw(this, &FLiveCodingConsoleApp::OnCompileStartedAsync);
Server.GetCompileFinishedDelegate().BindLambda([this](ELiveCodingResult Result, const wchar_t* Message){ OnCompileFinishedAsync(Result, Message); });
Server.GetStatusChangeDelegate().BindLambda([this](const wchar_t* Status){ OnStatusChangedAsync(Status); });
// Start the server
FString ProcessGroupName;
if (FParse::Value(FCommandLine::Get(), TEXT("-Group="), ProcessGroupName))
{
Server.Start(*ProcessGroupName);
Window->SetRequestDestroyWindowOverride(FRequestDestroyWindowOverride::CreateLambda([this](const TSharedRef<SWindow>&){ SetVisible(false); }));
}
else
{
LogWidget->AppendLine(GetLogColor(ELiveCodingLogVerbosity::Warning), TEXT("Running in standalone mode. Server is disabled."));
}
// Setting focus seems to have to happen after the Window has been added
Slate.ClearKeyboardFocus(EFocusCause::Cleared);
// loop until the app is ready to quit
while (!IsEngineExitRequested())
{
BeginExitIfRequested();
Slate.PumpMessages();
Slate.Tick();
FPlatformProcess::Sleep(1.0f / 30.0f);
// Execute all the main thread tasks
FScopeLock Lock(&CriticalSection);
for (FSimpleDelegate& MainThreadTask : MainThreadTasks)
{
MainThreadTask.Execute();
}
MainThreadTasks.Empty();
}
// Make sure the window is hidden, because it might take a while for the background thread to finish.
Window->HideWindow();
// Shutdown the server
Server.Stop();
}
private:
FText GetWindowTitle()
{
FString ProjectName;
if (FParse::Value(FCommandLine::Get(), TEXT("-ProjectName="), ProjectName))
{
FFormatNamedArguments Args;
Args.Add(TEXT("ProjectName"), FText::FromString(ProjectName));
return FText::Format(LOCTEXT("WindowTitleWithProject", "{ProjectName} - Live Coding"), Args);
}
return LOCTEXT("WindowTitle", "Live Coding");
}
void BringToFrontAsync()
{
FScopeLock Lock(&CriticalSection);
MainThreadTasks.Add(FSimpleDelegate::CreateRaw(this, &FLiveCodingConsoleApp::BringToFront));
}
void BringToFront()
{
HWND WindowHandle = (HWND)Window->GetNativeWindow()->GetOSWindowHandle();
if (IsIconic(WindowHandle))
{
ShowWindow(WindowHandle, SW_RESTORE);
}
::SetWindowPos(WindowHandle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
::SetWindowPos(WindowHandle, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
}
static FSlateColor GetLogColor(ELiveCodingLogVerbosity Verbosity)
{
switch (Verbosity)
{
case ELiveCodingLogVerbosity::Success:
return FSlateColor(FLinearColor::Green);
case ELiveCodingLogVerbosity::Failure:
return FSlateColor(FLinearColor::Red);
case ELiveCodingLogVerbosity::Warning:
return FSlateColor(FLinearColor::Yellow);
default:
return FSlateColor(FLinearColor::Gray);
}
}
void AppendLogLine(ELiveCodingLogVerbosity Verbosity, const TCHAR* Text)
{
// SLogWidget has its own synchronization
LogWidget->AppendLine(GetLogColor(Verbosity), MoveTemp(Text));
}
ELiveCodingCompileResult CompilePatch(const TArray<FString>& Targets, const TArray<FString>& ValidModules, TArray<FString>& RequiredModules, TMap<FString, TArray<FString>>& ModuleToObjectFiles, ELiveCodingCompileReason CompileReason)
{
// Update the compile start time. This gets copied into the last patch time once a patch has been confirmed to have been applied.
NextPatchStartTime = FDateTime::UtcNow();
// Get the UBT path
FString Executable;
FString Entry;
if( GConfig->GetString( TEXT("PlatformPaths"), TEXT("UnrealBuildTool"), Entry, GEngineIni ))
{
Executable = FPaths::ConvertRelativePathToFull(FPaths::RootDir() / Entry);
}
else
{
Executable = FPaths::EngineDir() / TEXT("Binaries/DotNET/UnrealBuildTool/UnrealBuildTool.exe");
}
FPaths::MakePlatformFilename(Executable);
// Write out the list of lazy-loaded modules for UBT to check
FString ModulesFileName = FPaths::ConvertRelativePathToFull(FPaths::EngineIntermediateDir() / TEXT("LiveCodingModules.txt"));
FFileHelper::SaveStringArrayToFile(ValidModules, *ModulesFileName);
// Delete the output file for non-whitelisted modules
FString ModulesOutputFileName = ModulesFileName + TEXT(".out");
IFileManager::Get().Delete(*ModulesOutputFileName);
// Delete any existing manifest
FString ManifestFileName = FPaths::ConvertRelativePathToFull(FPaths::EngineIntermediateDir() / TEXT("LiveCoding.json"));
IFileManager::Get().Delete(*ManifestFileName);
// Build the argument list
FString Arguments;
for (const FString& Target : Targets)
{
Arguments += FString::Printf(TEXT("-Target=\"%s\" "), *Target.Replace(TEXT("\""), TEXT("\"\"")));
}
Arguments += FString::Printf(TEXT("-LiveCoding -LiveCodingModules=\"%s\" -LiveCodingManifest=\"%s\" -WaitMutex"), *ModulesFileName, *ManifestFileName);
if (!bDisableLimit && CompileReason == ELiveCodingCompileReason::Initial)
{
bool bDisableActionLimit = false;
GConfig->GetBool(TEXT("LiveCoding"), TEXT("bDisableActionLimit"), bDisableActionLimit, GEngineIni);
int ActionLimit = 1;
GConfig->GetInt(TEXT("LiveCoding"), TEXT("ActionLimit"), ActionLimit, GEngineIni);
if (!bDisableActionLimit && ActionLimit > 0)
{
Arguments += FString::Printf(TEXT(" -LiveCodingLimit=%d"), ActionLimit);
}
}
AppendLogLine(ELiveCodingLogVerbosity::Info, *FString::Printf(TEXT("Running %s %s"), *Executable, *Arguments));
// Spawn UBT and wait for it to complete (or the compile button to be pressed)
FMonitoredProcess Process(*Executable, *Arguments, true);
Process.OnOutput().BindLambda([this](const FString& Text){ AppendLogLine(ELiveCodingLogVerbosity::Info, *(FString(TEXT(" ")) + Text)); });
Process.Launch();
while(Process.Update())
{
if (HasCancelledBuild())
{
AppendLogLine(ELiveCodingLogVerbosity::Warning, TEXT("Build cancelled."));
return ELiveCodingCompileResult::Canceled;
}
FPlatformProcess::Sleep(0.1f);
}
int ReturnCode = Process.GetReturnCode();
if (ReturnCode != 0)
{
ELiveCodingCompileResult CompileResult = ELiveCodingCompileResult::Failure;
// If there are missing modules, then we always retry them
if (FPaths::FileExists(ModulesOutputFileName))
{
FFileHelper::LoadFileToStringArray(RequiredModules, *ModulesOutputFileName);
if (!RequiredModules.IsEmpty())
{
CompileResult = ELiveCodingCompileResult::Retry;
}
}
// If we reached the live coding limit, the prompt the user to retry
if (ReturnCode == ECompilationResult::LiveCodingLimitError)
{
const FText Message = LOCTEXT("LimitText", "Live Coding action limit reached. Do you wish to compile anyway?\n\n"
"The limit can be permanently changed or disabled by setting the ActionLimit or DisableActionLimit setting in the engine.");
const FText Title = LOCTEXT("LimitTitle", "Live Coding Action Limit Reached");
EAppReturnType::Type ReturnType = FMessageDialog::Open(EAppMsgType::YesNo, Message, &Title);
CompileResult = ReturnType == EAppReturnType::Yes ? ELiveCodingCompileResult::Retry : ELiveCodingCompileResult::Canceled;
}
if (CompileResult == ELiveCodingCompileResult::Failure)
{
AppendLogLine(ELiveCodingLogVerbosity::Failure, TEXT("Build failed."));
}
return CompileResult;
}
// Read the output manifest
FString ManifestFailReason;
FLiveCodingManifest Manifest;
if (!Manifest.Read(*ManifestFileName, ManifestFailReason))
{
AppendLogLine(ELiveCodingLogVerbosity::Failure, *ManifestFailReason);
return ELiveCodingCompileResult::Failure;
}
// Override the linker path
Server.SetLinkerPath(*Manifest.LinkerPath, Manifest.LinkerEnvironment);
// Strip out all the files that haven't been modified
IFileManager& FileManager = IFileManager::Get();
for(TPair<FString, TArray<FString>>& Pair : Manifest.BinaryToObjectFiles)
{
FDateTime MinTimeStamp = FileManager.GetTimeStamp(*Pair.Key);
if(LastPatchTime > MinTimeStamp)
{
MinTimeStamp = LastPatchTime;
}
for (const FString& ObjectFileName : Pair.Value)
{
if (FileManager.GetTimeStamp(*ObjectFileName) > MinTimeStamp)
{
ModuleToObjectFiles.FindOrAdd(Pair.Key).Add(ObjectFileName);
}
}
}
return ELiveCodingCompileResult::Success;
}
void CancelBuild()
{
FScopeLock Lock(&CriticalSection);
bRequestCancel = true;
}
bool HasCancelledBuild()
{
FScopeLock Lock(&CriticalSection);
return bRequestCancel;
}
FReply RestartTargets()
{
Server.RestartTargets();
return FReply::Handled();
}
void SetVisibleAsync(bool bVisible)
{
FScopeLock Lock(&CriticalSection);
MainThreadTasks.Add(FSimpleDelegate::CreateLambda([this, bVisible](){ SetVisible(bVisible); }));
}
void SetVisible(bool bVisible)
{
if (bVisible)
{
if (!Window->IsVisible())
{
Window->ShowWindow();
}
}
else
{
if (Window->IsVisible())
{
Window->HideWindow();
}
}
}
void ShowConsole()
{
SetVisible(true);
BringToFront();
}
void OnCompileStartedAsync()
{
FScopeLock Lock(&CriticalSection);
MainThreadTasks.Add(FSimpleDelegate::CreateRaw(this, &FLiveCodingConsoleApp::OnCompileStarted));
bRequestCancel = false;
NextPatchStartTime = FDateTime::UtcNow();
}
void OnCompileStarted()
{
if (!CompileNotification.IsValid())
{
ShowConsole();
FNotificationInfo Info(FText::FromString(TEXT("Starting...")));
Info.bFireAndForget = false;
Info.FadeOutDuration = 0.0f;
Info.ExpireDuration = 0.0f;
Info.Hyperlink = FSimpleDelegate::CreateRaw(this, &FLiveCodingConsoleApp::ShowConsole);
Info.HyperlinkText = LOCTEXT("BuildStatusShowConsole", "Show Console");
Info.ButtonDetails.Add(FNotificationButtonInfo(LOCTEXT("BuildStatusCancel", "Cancel"), FText(), FSimpleDelegate::CreateRaw(this, &FLiveCodingConsoleApp::CancelBuild), SNotificationItem::CS_Pending));
CompileNotification = FSlateNotificationManager::Get().AddNotification(Info);
CompileNotification->SetCompletionState(SNotificationItem::CS_Pending);
}
}
void OnCompileFinishedAsync(ELiveCodingResult Result, const FString& Status)
{
FScopeLock Lock(&CriticalSection);
MainThreadTasks.Add(FSimpleDelegate::CreateLambda([this, Result, Status](){ OnCompileFinished(Result, Status); }));
if (Result == ELiveCodingResult::Success)
{
LastPatchTime = NextPatchStartTime;
}
}
void OnCompileFinished(ELiveCodingResult Result, const FString& Status)
{
if(CompileNotification.IsValid())
{
if (Result == ELiveCodingResult::Success)
{
if (Server.ShowCompileFinishNotification())
{
CompileNotification->SetText(FText::FromString(Status));
CompileNotification->SetCompletionState(SNotificationItem::CS_Success);
CompileNotification->SetExpireDuration(1.5f);
CompileNotification->SetFadeOutDuration(0.4f);
}
else
{
CompileNotification->SetExpireDuration(0.0f);
CompileNotification->SetFadeOutDuration(0.1f);
}
}
else if (HasCancelledBuild())
{
CompileNotification->SetExpireDuration(0.0f);
CompileNotification->SetFadeOutDuration(0.1f);
}
else
{
if (Server.ShowCompileFinishNotification())
{
CompileNotification->SetText(FText::FromString(Status));
CompileNotification->SetCompletionState(SNotificationItem::CS_Fail);
CompileNotification->SetExpireDuration(5.0f);
CompileNotification->SetFadeOutDuration(2.0f);
}
else
{
CompileNotification->SetExpireDuration(0.0f);
CompileNotification->SetFadeOutDuration(0.1f);
ShowConsole();
}
}
}
CompileNotification->ExpireAndFadeout();
CompileNotification.Reset();
}
void OnStatusChangedAsync(const FString& Status)
{
FScopeLock Lock(&CriticalSection);
MainThreadTasks.Add(FSimpleDelegate::CreateLambda([this, Status](){ OnCompileStatusChanged(Status); }));
}
void OnCompileStatusChanged(const FString& Status)
{
if (CompileNotification.IsValid())
{
CompileNotification->SetText(FText::FromString(Status));
}
}
};
bool LiveCodingConsoleMain(const TCHAR* CmdLine)
{
// start up the main loop
GEngineLoop.PreInit(CmdLine);
check(GConfig && GConfig->IsReadyForUse());
// Initialize high DPI mode
FSlateApplication::InitHighDPI(true);
{
// Create the platform slate application (what FSlateApplication::Get() returns)
TSharedRef<FSlateApplication> Slate = FSlateApplication::Create(MakeShareable(FPlatformApplicationMisc::CreateApplication()));
{
// Initialize renderer
TSharedRef<FSlateRenderer> SlateRenderer = GetStandardStandaloneRenderer();
// Try to initialize the renderer. It's possible that we launched when the driver crashed so try a few times before giving up.
bool bRendererInitialized = Slate->InitializeRenderer(SlateRenderer, true);
if (!bRendererInitialized)
{
// Close down the Slate application
FSlateApplication::Shutdown();
return false;
}
// Set the normal UE IsEngineExitRequested() when outer frame is closed
Slate->SetExitRequestedHandler(FSimpleDelegate::CreateStatic(&OnRequestExit));
// Prepare the custom Slate styles
FLiveCodingConsoleStyle::Initialize();
// Set the icon
FAppStyle::SetAppStyleSet(FLiveCodingConsoleStyle::Get());
// Load the source code access module
ISourceCodeAccessModule& SourceCodeAccessModule = FModuleManager::LoadModuleChecked<ISourceCodeAccessModule>(FName("SourceCodeAccess"));
// Manually load in the source code access plugins, as standalone programs don't currently support plugins.
#if PLATFORM_MAC
IModuleInterface& XCodeSourceCodeAccessModule = FModuleManager::LoadModuleChecked<IModuleInterface>(FName("XCodeSourceCodeAccess"));
SourceCodeAccessModule.SetAccessor(FName("XCodeSourceCodeAccess"));
#elif PLATFORM_WINDOWS
IModuleInterface& VisualStudioSourceCodeAccessModule = FModuleManager::LoadModuleChecked<IModuleInterface>(FName("VisualStudioSourceCodeAccess"));
SourceCodeAccessModule.SetAccessor(FName("VisualStudioSourceCodeAccess"));
#endif
// Load the server module
FModuleManager::Get().LoadModuleChecked<ILiveCodingServerModule>(TEXT("LiveCodingServer"));
ILiveCodingServer& Server = IModularFeatures::Get().GetModularFeature<ILiveCodingServer>(LIVE_CODING_SERVER_FEATURE_NAME);
// Run the inner application loop
FLiveCodingConsoleApp App(Slate.Get(), Server);
App.Run();
// Unload the server module
FModuleManager::Get().UnloadModule(TEXT("LiveCodingServer"));
// Clean up the custom styles
FLiveCodingConsoleStyle::Shutdown();
}
// Close down the Slate application
FSlateApplication::Shutdown();
}
FEngineLoop::AppPreExit();
FModuleManager::Get().UnloadModulesAtShutdown();
FEngineLoop::AppExit();
return true;
}
int WINAPI WinMain(HINSTANCE hCurrInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
hInstance = hCurrInstance;
return LiveCodingConsoleMain(GetCommandLineW())? 0 : 1;
}
#undef LOCTEXT_NAMESPACE