Files
UnrealEngineUWP/Engine/Source/Developer/Windows/LiveCoding/Private/External/LC_ClientStartupThread.cpp
Ben Marsh e1fe0cc030 Integrating live coding feature (aka Live++) into UE4.
Allows fast iteration of C++ changes without restarting the application. To use, select the "Live Coding (Experimental)" mode from the drop down menu next to the editor's compile button, or type "LiveCoding" into the console for a monolithic build. Press Ctrl+Alt+F11 to find changes and compile.

Changes vs standalone Live++ version:

* UBT is used to execute builds. This allows standard UE4 adaptive unity mode, allows us to reuse object files when we do regular builds, supports using any build executor allowed by UBT (XGE, SNDBS, etc..).
* Adding new source files is supported.
* Custom visualizer for FNames is supported via a weakly linked symbol in a static library (Engine/Extras/NatvisHelpers).
* Settings are exposed in the editor's project settings dialog.
* Standalone application has been rewritten as a Slate app ("LiveCodingConsole"). There is an additional option to start the program as hidden, where it will not be visible until Ctrl+Alt+F11 is hit. Similarly, closing the window will hide it instead of closing the application.
* Does not require a standalone licensed version of Live++.

Known issues:

* Does not currently support class layout changes / object reinstancing

#rb none
#fyi Marc.Audy, Stefan.Boberg, Nick.Penwarden
#jira

[CL 5304722 by Ben Marsh in 4.22 branch]
2019-03-05 15:54:02 -05:00

486 lines
14 KiB
C++

// Copyright 2011-2019 Molecular Matters GmbH, all rights reserved.
#include "LC_ClientStartupThread.h"
#include "LC_StringUtil.h"
#include "LC_NamedSharedMemory.h"
#include "LC_InterprocessMutex.h"
#include "LC_DuplexPipeClient.h"
#include "LC_CommandMap.h"
#include "LC_ClientCommandActions.h"
#include "LC_ClientCommandThread.h"
#include "LC_ClientUserCommandThread.h"
#include "LC_Event.h"
#include "LC_CriticalSection.h"
#include "LC_PrimitiveNames.h"
#include "LC_Logging.h"
#include "Misc/Paths.h"
#include "Misc/ConfigCacheIni.h"
#include "Misc/App.h"
namespace
{
template <typename T>
static void DeleteAndNull(T*& instance)
{
delete instance;
instance = nullptr;
}
}
ClientStartupThread::ClientStartupThread(HINSTANCE instance)
: m_instance(instance)
, m_thread(INVALID_HANDLE_VALUE)
, m_job(nullptr)
, m_sharedMemory(nullptr)
, m_mainProcessContext(nullptr)
, m_processHandle(nullptr)
, m_successfulInit(false)
, m_pipeClient(nullptr)
, m_exceptionPipeClient(nullptr)
, m_pipeClientCS(nullptr)
, m_commandThread(nullptr)
, m_userCommandThread(nullptr)
, m_startEvent(nullptr)
, m_compilationEvent(nullptr)
{
m_pipeClient = new DuplexPipeClient;
m_exceptionPipeClient = new DuplexPipeClient;
m_commandThread = new ClientCommandThread(m_pipeClient);
m_userCommandThread = new ClientUserCommandThread(m_pipeClient, m_exceptionPipeClient);
}
ClientStartupThread::~ClientStartupThread(void)
{
// close the pipe and then wait for the helper threads to finish.
// closing the pipe bails out the helper threads.
if (m_pipeClient)
{
// give the server a chance to deal with disconnected clients
if (m_pipeClient->IsValid())
{
m_pipeClient->SendCommandAndWaitForAck(commands::DisconnectClient{});
}
m_pipeClient->Close();
}
if (m_exceptionPipeClient)
{
m_exceptionPipeClient->Close();
}
// wait for command thread to finish
if (m_commandThread)
{
m_commandThread->Join();
}
// bail out user command thread and wait for it to finish
if (m_userCommandThread)
{
m_userCommandThread->End();
m_userCommandThread->Join();
}
DeleteAndNull(m_pipeClient);
DeleteAndNull(m_exceptionPipeClient);
DeleteAndNull(m_commandThread);
DeleteAndNull(m_userCommandThread);
DeleteAndNull(m_startEvent);
DeleteAndNull(m_compilationEvent);
DeleteAndNull(m_pipeClientCS);
if (m_mainProcessContext)
{
process::Destroy(m_mainProcessContext);
}
// close job object to make child processes close as well.
// if this is the last handle we close, the Live++ process will be killed as well.
::CloseHandle(m_job);
// clean up interprocess objects
DeleteAndNull(m_sharedMemory);
}
void ClientStartupThread::Start(const char* const groupName, RunMode::Enum runMode)
{
// spawn a thread that does all the initialization work
ThreadContext* context = new ThreadContext;
context->thisInstance = this;
context->processGroupName = string::ToWideString(groupName);
context->runMode = runMode;
m_thread = thread::Create(128u * 1024u, &ThreadProxy, context);
}
void ClientStartupThread::Join(void)
{
if (m_thread != INVALID_HANDLE_VALUE)
{
thread::Join(m_thread);
thread::Close(m_thread);
}
}
void* ClientStartupThread::EnableModule(const wchar_t* const nameOfExeOrDll)
{
return m_userCommandThread->EnableModule(nameOfExeOrDll);
}
void* ClientStartupThread::EnableAllModules(const wchar_t* const nameOfExeOrDll)
{
return m_userCommandThread->EnableAllModules(nameOfExeOrDll);
}
void* ClientStartupThread::DisableModule(const wchar_t* const nameOfExeOrDll)
{
return m_userCommandThread->DisableModule(nameOfExeOrDll);
}
void* ClientStartupThread::DisableAllModules(const wchar_t* const nameOfExeOrDll)
{
return m_userCommandThread->DisableAllModules(nameOfExeOrDll);
}
void ClientStartupThread::WaitForToken(void* token)
{
// we cannot wait for commands in the user command thread as long as startup hasn't finished
Join();
if (m_userCommandThread)
{
m_userCommandThread->WaitForToken(token);
}
}
void ClientStartupThread::TriggerRecompile(void)
{
// we cannot wait for commands in the user command thread as long as startup hasn't finished
Join();
if (m_userCommandThread)
{
m_userCommandThread->TriggerRecompile();
}
}
void ClientStartupThread::BuildPatch(const wchar_t* moduleNames[], const wchar_t* objPaths[], unsigned int count)
{
// we cannot wait for commands in the user command thread as long as startup hasn't finished
Join();
if (m_userCommandThread)
{
m_userCommandThread->BuildPatch(moduleNames, objPaths, count);
}
}
void ClientStartupThread::InstallExceptionHandler(void)
{
// we cannot wait for commands in the user command thread as long as startup hasn't finished
Join();
if (m_userCommandThread)
{
m_userCommandThread->InstallExceptionHandler();
}
}
// BEGIN EPIC MOD - Adding ShowConsole command
void ClientStartupThread::ShowConsole(void)
{
// we cannot wait for commands in the user command thread as long as startup hasn't finished
Join();
if (m_userCommandThread)
{
m_userCommandThread->ShowConsole();
}
}
// END EPIC MOD
// BEGIN EPIC MOD - Adding SetVisible command
void ClientStartupThread::SetVisible(bool visible)
{
// we cannot wait for commands in the user command thread as long as startup hasn't finished
Join();
if (m_userCommandThread)
{
m_userCommandThread->SetVisible(visible);
}
}
// END EPIC MOD
// BEGIN EPIC MOD - Adding SetActive command
void ClientStartupThread::SetActive(bool active)
{
// we cannot wait for commands in the user command thread as long as startup hasn't finished
Join();
if (m_userCommandThread)
{
m_userCommandThread->SetActive(active);
}
}
// END EPIC MOD
// BEGIN EPIC MOD - Adding SetBuildArguments command
void ClientStartupThread::SetBuildArguments(const wchar_t* arguments)
{
// we cannot wait for commands in the user command thread as long as startup hasn't finished
Join();
if (m_userCommandThread)
{
m_userCommandThread->SetBuildArguments(arguments);
}
}
// END EPIC MOD
void ClientStartupThread::ApplySettingBool(const char* const settingName, int value)
{
// we cannot wait for commands in the user command thread as long as startup hasn't finished
Join();
if (m_userCommandThread)
{
m_userCommandThread->ApplySettingBool(settingName, value);
}
}
void ClientStartupThread::ApplySettingInt(const char* const settingName, int value)
{
// we cannot wait for commands in the user command thread as long as startup hasn't finished
Join();
if (m_userCommandThread)
{
m_userCommandThread->ApplySettingInt(settingName, value);
}
}
void ClientStartupThread::ApplySettingString(const char* const settingName, const wchar_t* const value)
{
// we cannot wait for commands in the user command thread as long as startup hasn't finished
Join();
if (m_userCommandThread)
{
m_userCommandThread->ApplySettingString(settingName, value);
}
}
unsigned int __stdcall ClientStartupThread::ThreadProxy(void* context)
{
thread::SetName("Live coding startup");
ThreadContext* realContext = static_cast<ThreadContext*>(context);
// in the context of mutexes, jobs, named shared memory, etc. object names behave similar to
// file names and are not allowed to contain certain characters.
const std::wstring& safeProcessGroupName = string::MakeSafeName(realContext->processGroupName);
const unsigned int exitCode = realContext->thisInstance->ThreadFunction(safeProcessGroupName, realContext->runMode);
delete realContext;
return exitCode;
}
unsigned int ClientStartupThread::ThreadFunction(const std::wstring& processGroupName, RunMode::Enum runMode)
{
// configure all child processes associated with the job to terminate when the parent terminates.
// we create (or open) a process-wide job per process group and register the spawned process with that job.
// when the last handle to the job is closed, it will close the associated process automatically.
// this nicely handles multi-process scenarios where applications can even be restarted and attach to the
// same Live++ instance.
m_job = ::CreateJobObjectW(NULL, primitiveNames::JobGroup(processGroupName).c_str());
JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo = {};
jobInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
::SetInformationJobObject(m_job, JobObjectExtendedLimitInformation, &jobInfo, sizeof(jobInfo));
// lock the interprocess mutex to ensure that only one process can run this code at any time.
// the first one will spawn the Live++ process, all others will connect to the same process.
{
InterprocessMutex initProcessMutex(primitiveNames::StartupMutex(processGroupName).c_str());
initProcessMutex.Lock();
m_sharedMemory = new NamedSharedMemory(primitiveNames::StartupNamedSharedMemory(processGroupName).c_str());
if (m_sharedMemory->IsOwnedByCallingProcess())
{
// BEGIN EPIC MOD - Using LiveCodeConsole
// we are the first DLL. spawn the console.
LC_LOG_USER("First instance in process group \"%S\", spawning console", processGroupName.c_str());
// get the path to the console application
extern FString GLiveCodingConsolePath;
const std::wstring& exePath = *GLiveCodingConsolePath;
std::wstring commandLine;
commandLine += L"-Group=";
commandLine += processGroupName;
// BEGIN EPIC MOD - Additional arguments for console app
extern const wchar_t* GLiveCodingConsoleArguments;
if(GLiveCodingConsoleArguments != nullptr)
{
commandLine += L" ";
commandLine += GLiveCodingConsoleArguments;
}
if (!FApp::IsProjectNameEmpty())
{
commandLine += L" -ProjectName=\"";
commandLine += FApp::GetProjectName();
commandLine += L"\"";
}
// END EPIC MOD
m_mainProcessContext = process::Spawn(exePath.c_str(), nullptr, commandLine.c_str(), nullptr, process::SpawnFlags::NONE);
if (m_mainProcessContext->pi.dwProcessId != 0)
{
m_processHandle = m_mainProcessContext->pi.hProcess;
::AssignProcessToJobObject(m_job, m_processHandle);
// share Live++ process ID with other processes
m_sharedMemory->Write(m_mainProcessContext->pi.dwProcessId);
}
// END EPIC MOD - Using LiveCodeConsole
}
else
{
// the Live++ process is already running. fetch the process ID from shared memory.
const DWORD processId = m_sharedMemory->Read<DWORD>();
LC_LOG_USER("Detected running instance in process group \"%S\", connecting to console process (PID: %d)", processGroupName.c_str(), processId);
if (processId != 0u)
{
m_processHandle = process::Open(processId);
::AssignProcessToJobObject(m_job, m_processHandle);
}
}
initProcessMutex.Unlock();
}
if (!m_processHandle)
{
// we were unable to open the process, bail out
LC_ERROR_USER("Unable to attach to console process");
DeleteAndNull(m_sharedMemory);
return 1u;
}
// wait for server to become ready
{
LC_LOG_USER("Waiting for server");
Event serverReadyEvent(primitiveNames::ServerReadyEvent(processGroupName).c_str(), Event::Type::AUTO_RESET);
serverReadyEvent.Wait();
}
// create a named duplex pipe for communicating between DLL and Live++ process
if (!m_pipeClient->Connect(primitiveNames::Pipe(processGroupName).c_str()))
{
// could not connect to Live++ process
LC_ERROR_USER("Could not connect named pipe to console process");
return 2u;
}
// create a named duplex pipe for communicating exceptions between DLL and Live++ process
if (!m_exceptionPipeClient->Connect(primitiveNames::ExceptionPipe(processGroupName).c_str()))
{
// could not connect to Live++ process
LC_ERROR_USER("Could not connect exception pipe to console process");
return 3u;
}
m_pipeClientCS = new CriticalSection;
// the Live++ server must be ready. create the interprocess event used for signaling that compilation is about to start
m_compilationEvent = new Event(primitiveNames::CompilationEvent(processGroupName).c_str(), Event::Type::MANUAL_RESET);
// create helper threads responsible for handling commands from user calls as well as Live++.
// both threads are not allowed to run until we send them a signal. this ensures that they don't use the
// pipe for communicating as long as we aren't finished with it.
m_startEvent = new Event(nullptr, Event::Type::MANUAL_RESET);
const unsigned int commandThreadId = m_commandThread->Start(processGroupName, m_compilationEvent, m_startEvent, m_pipeClientCS);
m_userCommandThread->Start(processGroupName, m_startEvent, m_pipeClientCS);
// register this process with Live++
m_pipeClient->SendCommandAndWaitForAck(commands::RegisterProcess { process::GetId(), process::GetBase(), commandThreadId });
// handle commands until registration is finished
{
CommandMap commandMap;
commandMap.RegisterAction<actions::RegisterProcessFinished>();
commandMap.HandleCommands(m_pipeClient, &m_successfulInit);
}
if (!m_successfulInit)
{
// process could not be registered, bail out
LC_ERROR_USER("Could not register live coding process");
// close the pipe and then wait for the helper threads to finish.
// closing the pipe bails out the helper threads.
m_pipeClient->Close();
m_exceptionPipeClient->Close();
// let the threads run *after* we've closed the pipe, otherwise they could have tried communicating
// with the server in the mean time.
m_startEvent->Signal();
// bail out command thread and wait for it
m_compilationEvent->Signal();
m_commandThread->Join();
// bail out user command thread and wait for it
m_userCommandThread->End();
m_userCommandThread->Join();
DeleteAndNull(m_pipeClient);
DeleteAndNull(m_exceptionPipeClient);
DeleteAndNull(m_commandThread);
DeleteAndNull(m_userCommandThread);
DeleteAndNull(m_startEvent);
DeleteAndNull(m_compilationEvent);
DeleteAndNull(m_pipeClientCS);
return 3u;
}
LC_LOG_USER("Successfully initialized, removing startup thread");
// helper threads are now allowed to run, we're finished with the pipe
m_startEvent->Signal();
return 0u;
}