You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
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
#ROBOMERGE-SOURCE: CL 5304722 in //UE4/Release-4.22/...
#ROBOMERGE-BOT: RELEASE (Release-4.22 -> Main)
[CL 5309051 by ben marsh in Main branch]
1799 lines
56 KiB
C++
1799 lines
56 KiB
C++
// Copyright 2011-2019 Molecular Matters GmbH, all rights reserved.
|
|
|
|
#include "LC_ServerCommandThread.h"
|
|
#include "LC_Commands.h"
|
|
#include "LC_Telemetry.h"
|
|
#include "LC_Symbols.h"
|
|
#include "LC_FileUtil.h"
|
|
#include "LC_Process.h"
|
|
#include "LC_Compiler.h"
|
|
#include "LC_StringUtil.h"
|
|
#include "LC_CommandMap.h"
|
|
#include "LC_FileAttributeCache.h"
|
|
#include "LiveCodingServer.h"
|
|
#include "LC_Shortcut.h"
|
|
#include "LC_Key.h"
|
|
#include "LC_ChangeNotification.h"
|
|
#include "LC_DirectoryCache.h"
|
|
#include "LC_VirtualDrive.h"
|
|
#include "LC_LiveModule.h"
|
|
#include "LC_LiveProcess.h"
|
|
#include "LC_CodeCave.h"
|
|
#include "LC_PrimitiveNames.h"
|
|
#include "LC_AppSettings.h"
|
|
#include "LC_Allocators.h"
|
|
#include <mmsystem.h>
|
|
|
|
// unreachable code
|
|
#pragma warning (disable : 4702)
|
|
|
|
|
|
namespace
|
|
{
|
|
static telemetry::Accumulator g_loadedModuleSize("Module size");
|
|
|
|
struct InitializeCOM
|
|
{
|
|
InitializeCOM(void)
|
|
{
|
|
::CoInitialize(NULL);
|
|
}
|
|
|
|
~InitializeCOM(void)
|
|
{
|
|
::CoUninitialize();
|
|
}
|
|
};
|
|
|
|
|
|
static void AddVirtualDrive(void)
|
|
{
|
|
const std::wstring virtualDriveLetter = appSettings::g_virtualDriveLetter->GetValue();
|
|
const std::wstring virtualDrivePath = appSettings::g_virtualDrivePath->GetValue();
|
|
if ((virtualDriveLetter.size() != 0) && (virtualDrivePath.size() != 0))
|
|
{
|
|
virtualDrive::Add(virtualDriveLetter.c_str(), virtualDrivePath.c_str());
|
|
}
|
|
}
|
|
|
|
static void RemoveVirtualDrive(void)
|
|
{
|
|
const std::wstring virtualDriveLetter = appSettings::g_virtualDriveLetter->GetValue();
|
|
const std::wstring virtualDrivePath = appSettings::g_virtualDrivePath->GetValue();
|
|
if ((virtualDriveLetter.size() != 0) && (virtualDrivePath.size() != 0))
|
|
{
|
|
virtualDrive::Remove(virtualDriveLetter.c_str(), virtualDrivePath.c_str());
|
|
}
|
|
}
|
|
|
|
static executable::Header GetImageHeader(const wchar_t* path)
|
|
{
|
|
executable::Image* image = executable::OpenImage(path, file::OpenMode::READ_ONLY);
|
|
if (!image)
|
|
{
|
|
return executable::Header {};
|
|
}
|
|
|
|
const executable::Header& imageHeader = executable::GetHeader(image);
|
|
executable::CloseImage(image);
|
|
|
|
return imageHeader;
|
|
}
|
|
}
|
|
|
|
|
|
ServerCommandThread::ServerCommandThread(MainFrame* mainFrame, const wchar_t* const processGroupName, RunMode::Enum runMode)
|
|
: m_processGroupName(processGroupName)
|
|
, m_runMode(runMode)
|
|
, m_mainFrame(mainFrame)
|
|
, m_serverThread()
|
|
, m_compileThread()
|
|
, m_liveModules()
|
|
, m_liveProcesses()
|
|
, m_imageHeaderToLiveModule()
|
|
, m_actionCS()
|
|
, m_exceptionCS()
|
|
, m_inExceptionHandlerEvent(nullptr, Event::Type::MANUAL_RESET)
|
|
, m_handleCommandsEvent(nullptr, Event::Type::MANUAL_RESET)
|
|
, m_directoryCache(new DirectoryCache(2048u))
|
|
, m_connectionCS()
|
|
, m_commandThreads()
|
|
, m_moduleBatchScope("Module loading")
|
|
, m_loadedCompilandCountInBatchScope(0u)
|
|
, m_manualRecompileTriggered(false)
|
|
, m_liveModuleToModifiedOrNewObjFiles()
|
|
{
|
|
m_serverThread = thread::Create(64u * 1024u, &ServerThreadProxy, this);
|
|
m_compileThread = thread::Create(64u * 1024u, &CompileThreadProxy, this);
|
|
|
|
m_liveModules.reserve(256u);
|
|
m_liveProcesses.reserve(8u);
|
|
m_imageHeaderToLiveModule.reserve(256u);
|
|
|
|
m_commandThreads.reserve(8u);
|
|
}
|
|
|
|
|
|
ServerCommandThread::~ServerCommandThread(void)
|
|
{
|
|
// note that we deliberately do *nothing* here.
|
|
// this is only called when Live++ is being torn down anyway, so we leave cleanup to the OS.
|
|
// otherwise we could run into races when trying to terminate the thread that might currently be doing
|
|
// some intensive work.
|
|
}
|
|
|
|
|
|
std::wstring ServerCommandThread::GetProcessImagePath(void) const
|
|
{
|
|
// there must be at least one registered process.
|
|
// in case the EXE was erroneously started directly, no process will be registered.
|
|
// handle this case gracefully.
|
|
if (m_liveProcesses.size() == 0u)
|
|
{
|
|
return L"Unknown";
|
|
}
|
|
|
|
return process::GetImagePath(m_liveProcesses[0]->GetProcessHandle());
|
|
}
|
|
|
|
|
|
void ServerCommandThread::LoadModule(const wchar_t* givenModulePath, const DuplexPipe* pipe, TaskContext* tasks, unsigned int processId)
|
|
{
|
|
// EPIC REMOVED: g_theApp.GetMainFrame()->ChangeStatusBarText(L"Loading modules...");
|
|
|
|
const std::wstring& modulePath = file::NormalizePath(givenModulePath);
|
|
const executable::Header imageHeader = GetImageHeader(modulePath.c_str());
|
|
if (!executable::IsValidHeader(imageHeader))
|
|
{
|
|
return;
|
|
}
|
|
|
|
LiveProcess* liveProcess = FindProcessById(processId);
|
|
LC_ASSERT(liveProcess, "Invalid process ID.");
|
|
|
|
if (liveProcess->TriedToLoadImage(imageHeader))
|
|
{
|
|
// tried loading this module into this process already
|
|
return;
|
|
}
|
|
|
|
{
|
|
CommandMap commandMap;
|
|
commandMap.RegisterAction<GetModuleInfoAction>();
|
|
|
|
// defer loading of the module to make sure that we get the correct module base address,
|
|
// no matter if .exe or .dll.
|
|
{
|
|
commands::GetModule cmd = {};
|
|
cmd.loadImports = false;
|
|
cmd.taskContext = tasks;
|
|
wcscpy_s(cmd.path, modulePath.c_str());
|
|
pipe->SendCommandAndWaitForAck(cmd);
|
|
}
|
|
|
|
// handle commands that return module info
|
|
commandMap.HandleCommands(pipe, this);
|
|
}
|
|
|
|
liveProcess->AddLoadedImage(imageHeader);
|
|
}
|
|
|
|
|
|
void ServerCommandThread::LoadAllModules(const wchar_t* givenModulePath, const DuplexPipe* pipe, TaskContext* tasks, unsigned int processId)
|
|
{
|
|
// EPIC REMOVED: g_theApp.GetMainFrame()->ChangeStatusBarText(L"Loading modules...");
|
|
|
|
const std::wstring& modulePath = file::NormalizePath(givenModulePath);
|
|
const executable::Header imageHeader = GetImageHeader(modulePath.c_str());
|
|
if (!executable::IsValidHeader(imageHeader))
|
|
{
|
|
return;
|
|
}
|
|
|
|
LiveProcess* liveProcess = FindProcessById(processId);
|
|
LC_ASSERT(liveProcess, "Invalid process ID.");
|
|
|
|
if (liveProcess->TriedToLoadImage(imageHeader))
|
|
{
|
|
// tried loading this module into this process already
|
|
return;
|
|
}
|
|
|
|
symbols::Provider* provider = symbols::OpenEXE(modulePath.c_str(), symbols::OpenOptions::ACCUMULATE_SIZE);
|
|
if (!provider)
|
|
{
|
|
liveProcess->AddLoadedImage(imageHeader);
|
|
return;
|
|
}
|
|
|
|
// grab DIA compilands first. this is very fast, and needed in order to gather modules next
|
|
symbols::DiaCompilandDB* diaCompilandDb = symbols::GatherDiaCompilands(provider);
|
|
symbols::ModuleDB* moduleDB = symbols::GatherModules(diaCompilandDb);
|
|
|
|
// now that we have a list of modules, load them all concurrently, starting with the main executable, followed
|
|
// by all DLLs.
|
|
{
|
|
CommandMap commandMap;
|
|
commandMap.RegisterAction<GetModuleInfoAction>();
|
|
{
|
|
commands::GetModule cmd = {};
|
|
cmd.loadImports = false;
|
|
cmd.taskContext = tasks;
|
|
wcscpy_s(cmd.path, modulePath.c_str());
|
|
pipe->SendCommandAndWaitForAck(cmd);
|
|
}
|
|
|
|
commandMap.HandleCommands(pipe, this);
|
|
|
|
const size_t count = moduleDB->modules.size();
|
|
for (size_t i = 0u; i < count; ++i)
|
|
{
|
|
const std::wstring& path = moduleDB->modules[i];
|
|
|
|
// all we have is a relative path to the DLL. get the full path from the modules loaded into the main process
|
|
{
|
|
// because DLLs might also have import DLLs, load all those as well
|
|
commands::GetModule cmd = {};
|
|
cmd.loadImports = true;
|
|
cmd.taskContext = tasks;
|
|
wcscpy_s(cmd.path, path.c_str());
|
|
pipe->SendCommandAndWaitForAck(cmd);
|
|
}
|
|
|
|
// handle commands that return module info
|
|
commandMap.HandleCommands(pipe, this);
|
|
}
|
|
}
|
|
|
|
symbols::DestroyDiaCompilandDB(diaCompilandDb);
|
|
symbols::DestroyModuleDB(moduleDB);
|
|
|
|
symbols::Close(provider);
|
|
|
|
liveProcess->AddLoadedImage(imageHeader);
|
|
}
|
|
|
|
|
|
void ServerCommandThread::UnloadModule(const wchar_t* givenModulePath, const DuplexPipe* pipe, unsigned int processId)
|
|
{
|
|
// EPIC REMOVED: g_theApp.GetMainFrame()->ChangeStatusBarText(L"Unloading modules...");
|
|
|
|
const std::wstring& modulePath = file::NormalizePath(givenModulePath);
|
|
const executable::Header imageHeader = GetImageHeader(modulePath.c_str());
|
|
if (!executable::IsValidHeader(imageHeader))
|
|
{
|
|
return;
|
|
}
|
|
|
|
LiveProcess* liveProcess = FindProcessById(processId);
|
|
LC_ASSERT(liveProcess, "Invalid process ID.");
|
|
|
|
if (!liveProcess->TriedToLoadImage(imageHeader))
|
|
{
|
|
// this module was never loaded
|
|
return;
|
|
}
|
|
|
|
{
|
|
CommandMap commandMap;
|
|
commandMap.RegisterAction<GetModuleInfoAction>();
|
|
|
|
// defer unloading of the module to make sure that we get the correct module base address,
|
|
// no matter if .exe or .dll.
|
|
{
|
|
commands::GetModule cmd = {};
|
|
cmd.loadImports = false;
|
|
cmd.taskContext = nullptr;
|
|
wcscpy_s(cmd.path, modulePath.c_str());
|
|
pipe->SendCommandAndWaitForAck(cmd);
|
|
}
|
|
|
|
// handle commands that return module info
|
|
commandMap.HandleCommands(pipe, this);
|
|
}
|
|
|
|
liveProcess->RemoveLoadedImage(imageHeader);
|
|
}
|
|
|
|
|
|
void ServerCommandThread::UnloadAllModules(const wchar_t* givenModulePath, const DuplexPipe* pipe, unsigned int processId)
|
|
{
|
|
// EPIC REMOVED: g_theApp.GetMainFrame()->ChangeStatusBarText(L"Unloading modules...");
|
|
|
|
const std::wstring& modulePath = file::NormalizePath(givenModulePath);
|
|
const executable::Header imageHeader = GetImageHeader(modulePath.c_str());
|
|
if (!executable::IsValidHeader(imageHeader))
|
|
{
|
|
return;
|
|
}
|
|
|
|
LiveProcess* liveProcess = FindProcessById(processId);
|
|
LC_ASSERT(liveProcess, "Invalid process ID.");
|
|
|
|
if (!liveProcess->TriedToLoadImage(imageHeader))
|
|
{
|
|
// this module was never loaded
|
|
return;
|
|
}
|
|
|
|
symbols::Provider* provider = symbols::OpenEXE(modulePath.c_str(), symbols::OpenOptions::ACCUMULATE_SIZE);
|
|
if (!provider)
|
|
{
|
|
liveProcess->RemoveLoadedImage(imageHeader);
|
|
return;
|
|
}
|
|
|
|
// grab DIA compilands first. this is very fast, and needed in order to gather modules next
|
|
symbols::DiaCompilandDB* diaCompilandDb = symbols::GatherDiaCompilands(provider);
|
|
symbols::ModuleDB* moduleDB = symbols::GatherModules(diaCompilandDb);
|
|
|
|
// now that we have a list of modules, load them all concurrently, starting with the main executable, followed
|
|
// by all DLLs.
|
|
{
|
|
CommandMap commandMap;
|
|
commandMap.RegisterAction<GetModuleInfoAction>();
|
|
{
|
|
commands::GetModule cmd = {};
|
|
cmd.loadImports = false;
|
|
cmd.taskContext = nullptr;
|
|
wcscpy_s(cmd.path, modulePath.c_str());
|
|
pipe->SendCommandAndWaitForAck(cmd);
|
|
}
|
|
|
|
commandMap.HandleCommands(pipe, this);
|
|
|
|
const size_t count = moduleDB->modules.size();
|
|
for (size_t i = 0u; i < count; ++i)
|
|
{
|
|
const std::wstring& path = moduleDB->modules[i];
|
|
|
|
// all we have is a relative path to the DLL. get the full path from the modules loaded into the main process
|
|
{
|
|
// because DLLs might also have import DLLs, load all those as well
|
|
commands::GetModule cmd = {};
|
|
cmd.loadImports = true;
|
|
cmd.taskContext = nullptr;
|
|
wcscpy_s(cmd.path, path.c_str());
|
|
pipe->SendCommandAndWaitForAck(cmd);
|
|
}
|
|
|
|
// handle commands that return module info
|
|
commandMap.HandleCommands(pipe, this);
|
|
}
|
|
}
|
|
|
|
symbols::DestroyDiaCompilandDB(diaCompilandDb);
|
|
symbols::DestroyModuleDB(moduleDB);
|
|
|
|
symbols::Close(provider);
|
|
|
|
liveProcess->RemoveLoadedImage(imageHeader);
|
|
}
|
|
|
|
|
|
void ServerCommandThread::PrewarmCompilerEnvironmentCache(void)
|
|
{
|
|
// EPIC REMOVED: g_theApp.GetMainFrame()->ChangeStatusBarText(L"Prewarming compiler/linker environment cache...");
|
|
|
|
telemetry::Scope scope("Prewarming compiler/linker environment cache");
|
|
|
|
// fetch unique compiler and linker paths from all modules
|
|
types::StringSet uniquePaths;
|
|
|
|
// compiler and linker paths can be overridden, so we need to make sure that we pre-warm the
|
|
// cache for all compilers and linkers involved, depending on the UI settings.
|
|
// there are 3 options:
|
|
// - the path is not overridden: fetch only the paths from the compilands
|
|
// - the paths are overridden, but only used as fallback: fetch the paths from the compilands
|
|
// as well as the overridden ones. we might need both, depending on which file we compile
|
|
// - the paths are overridden, and always used: fetch only the overridden paths, we're only using those
|
|
|
|
// fetch all compiler paths involved.
|
|
// the compiler is only used in default mode, NOT when using an external build system.
|
|
const bool useCompilerEnvironment = appSettings::g_useCompilerEnvironment->GetValue();
|
|
if (useCompilerEnvironment && (m_runMode == RunMode::DEFAULT))
|
|
{
|
|
const std::wstring overriddenPath = appSettings::GetCompilerPath();
|
|
const bool useOverriddenPathAsFallback = appSettings::g_useCompilerOverrideAsFallback->GetValue();
|
|
|
|
// always prewarm for overridden compiler path if it is available
|
|
const bool prewarmOverridenPath = (overriddenPath.length() != 0u);
|
|
|
|
const bool prewarmCompilandCompilerPath = prewarmOverridenPath
|
|
? useOverriddenPathAsFallback // overridden path is set. only prewarm compiland compiler paths if the override is only used as fallback
|
|
: true; // no override is set, always prewarm
|
|
|
|
if (prewarmCompilandCompilerPath)
|
|
{
|
|
const size_t count = m_liveModules.size();
|
|
for (size_t i = 0u; i < count; ++i)
|
|
{
|
|
const LiveModule* liveModule = m_liveModules[i];
|
|
|
|
const symbols::CompilandDB* compilandDB = liveModule->GetCompilandDatabase();
|
|
for (auto it = compilandDB->compilands.begin(); it != compilandDB->compilands.end(); ++it)
|
|
{
|
|
const symbols::Compiland* compiland = it->second;
|
|
LC_ASSERT(compiland->compilerPath.c_str(), "Invalid compiler path.");
|
|
|
|
if (compiland->compilerPath.GetLength() != 0u)
|
|
{
|
|
uniquePaths.insert(compiland->compilerPath);
|
|
}
|
|
else
|
|
{
|
|
LC_WARNING_USER("Not prewarming environment cache for empty compiler in module %S", liveModule->GetModuleName().c_str());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (prewarmOverridenPath)
|
|
{
|
|
uniquePaths.insert(string::ToUtf8String(overriddenPath));
|
|
}
|
|
}
|
|
|
|
// fetch all linker paths involved
|
|
const bool useLinkerEnvironment = appSettings::g_useLinkerEnvironment->GetValue();
|
|
if (useLinkerEnvironment)
|
|
{
|
|
const std::wstring overriddenPath = appSettings::GetLinkerPath();
|
|
const bool useOverriddenPathAsFallback = appSettings::g_useLinkerOverrideAsFallback->GetValue();
|
|
|
|
// always prewarm for overridden linker path if it is available
|
|
const bool prewarmOverridenPath = (overriddenPath.length() != 0u);
|
|
|
|
const bool prewarmLinkerPath = prewarmOverridenPath
|
|
? useOverriddenPathAsFallback // overridden path is set. only prewarm linker paths if the override is only used as fallback
|
|
: true; // no override is set, always prewarm
|
|
|
|
if (prewarmLinkerPath)
|
|
{
|
|
const size_t count = m_liveModules.size();
|
|
for (size_t i = 0u; i < count; ++i)
|
|
{
|
|
const LiveModule* liveModule = m_liveModules[i];
|
|
|
|
const symbols::LinkerDB* linkerDB = liveModule->GetLinkerDatabase();
|
|
if (linkerDB->linkerPath.GetLength() != 0u)
|
|
{
|
|
uniquePaths.insert(linkerDB->linkerPath);
|
|
}
|
|
else
|
|
{
|
|
LC_WARNING_USER("Not prewarming environment cache for empty linker in module %S", liveModule->GetModuleName().c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (prewarmOverridenPath)
|
|
{
|
|
uniquePaths.insert(string::ToUtf8String(overriddenPath));
|
|
}
|
|
}
|
|
|
|
// grab environment blocks for all unique compilers/linkers concurrently
|
|
auto taskRoot = scheduler::CreateEmptyTask();
|
|
|
|
types::vector<scheduler::TaskBase*> tasks;
|
|
tasks.reserve(uniquePaths.size());
|
|
|
|
for (auto it = uniquePaths.begin(); it != uniquePaths.end(); ++it)
|
|
{
|
|
auto task = scheduler::CreateTask(taskRoot, [it]()
|
|
{
|
|
const ImmutableString& path = *it;
|
|
compiler::UpdateEnvironmentCache(string::ToWideString(path).c_str());
|
|
|
|
return true;
|
|
});
|
|
scheduler::RunTask(task);
|
|
|
|
tasks.emplace_back(task);
|
|
}
|
|
|
|
// wait for all tasks to end
|
|
scheduler::RunTask(taskRoot);
|
|
scheduler::WaitForTask(taskRoot);
|
|
|
|
// destroy all tasks
|
|
scheduler::DestroyTasks(tasks);
|
|
scheduler::DestroyTask(taskRoot);
|
|
|
|
LC_SUCCESS_USER("Prewarmed compiler/linker environment cache (%.3fs, %zu)", scope.ReadSeconds(), uniquePaths.size());
|
|
}
|
|
|
|
|
|
unsigned int __stdcall ServerCommandThread::ServerThreadProxy(void* context)
|
|
{
|
|
thread::SetName("Live coding server");
|
|
|
|
ServerCommandThread* instance = static_cast<ServerCommandThread*>(context);
|
|
return instance->ServerThread();
|
|
}
|
|
|
|
|
|
unsigned int ServerCommandThread::ServerThread(void)
|
|
{
|
|
InitializeCOM initCOM;
|
|
|
|
// inter process event for telling client that server is ready
|
|
Event serverReadyEvent(primitiveNames::ServerReadyEvent(m_processGroupName).c_str(), Event::Type::AUTO_RESET);
|
|
|
|
// run separate pipe servers for all incoming connections
|
|
for (;;)
|
|
{
|
|
CommandThreadContext* context = new CommandThreadContext;
|
|
context->instance = this;
|
|
|
|
context->pipe.Create(primitiveNames::Pipe(m_processGroupName).c_str());
|
|
context->exceptionPipe.Create(primitiveNames::ExceptionPipe(m_processGroupName).c_str());
|
|
|
|
context->readyEvent = new Event(nullptr, Event::Type::AUTO_RESET);
|
|
|
|
// tell other processes that a new server is ready
|
|
serverReadyEvent.Signal();
|
|
|
|
// wait until any client connects, blocking
|
|
context->pipe.WaitForClient();
|
|
context->exceptionPipe.WaitForClient();
|
|
|
|
// a new client has connected, open a new thread for communication
|
|
context->commandThread = thread::Create(64u * 1024u, &CommandThreadProxy, context);
|
|
context->exceptionCommandThread = thread::Create(64u * 1024u, &ExceptionCommandThreadProxy, context);
|
|
|
|
// register this connection
|
|
{
|
|
CriticalSection::ScopedLock lock(&m_connectionCS);
|
|
m_commandThreads.push_back(context);
|
|
}
|
|
}
|
|
|
|
return 0u;
|
|
}
|
|
|
|
|
|
unsigned int __stdcall ServerCommandThread::CompileThreadProxy(void* context)
|
|
{
|
|
thread::SetName("Live coding compilation");
|
|
|
|
ServerCommandThread* instance = static_cast<ServerCommandThread*>(context);
|
|
return instance->CompileThread();
|
|
}
|
|
|
|
|
|
void ServerCommandThread::CompileChanges(bool didAllProcessesMakeProgress)
|
|
{
|
|
// recompile files, if any
|
|
// EPIC REMOVED: g_theApp.GetMainFrame()->SetBusy(true);
|
|
// EPIC REMOVED: g_theApp.GetMainFrame()->ChangeStatusBarText(L"Creating patch...");
|
|
|
|
// BEGIN EPIC MOD - Hook for the compiler
|
|
GLiveCodingServer->GetCompileStartedDelegate().ExecuteIfBound();
|
|
|
|
const ILiveCodingServer::FCompileDelegate& CompileDelegate = GLiveCodingServer->GetCompileDelegate();
|
|
if (CompileDelegate.IsBound())
|
|
{
|
|
LC_LOG_USER("---------- Starting build ----------");
|
|
|
|
TArray<FString> Targets;
|
|
for (LiveProcess* liveProcess : m_liveProcesses)
|
|
{
|
|
Targets.Add(liveProcess->GetBuildArguments());
|
|
}
|
|
|
|
GLiveCodingServer->GetStatusChangeDelegate().ExecuteIfBound(L"Compiling changes for live coding...");
|
|
|
|
TMap<FString, TArray<FString>> ModuleToObjectFiles;
|
|
if (!CompileDelegate.Execute(Targets, ModuleToObjectFiles))
|
|
{
|
|
GLiveCodingServer->GetCompileFinishedDelegate().ExecuteIfBound(ELiveCodingResult::Error, L"Compilation error.");
|
|
return;
|
|
}
|
|
|
|
types::unordered_set<std::wstring> ValidModuleFileNames;
|
|
for (const LiveModule* liveModule : m_liveModules)
|
|
{
|
|
ValidModuleFileNames.insert(liveModule->GetModuleName());
|
|
}
|
|
|
|
for(const TPair<FString, TArray<FString>>& Pair : ModuleToObjectFiles)
|
|
{
|
|
std::wstring ModuleFileName = file::NormalizePath(*Pair.Key);
|
|
if(ValidModuleFileNames.find(ModuleFileName) == ValidModuleFileNames.end())
|
|
{
|
|
std::wstring ModuleName = file::GetFilename(ModuleFileName);
|
|
LC_ERROR_USER("Live coding is not enabled for %S.", ModuleName.c_str());
|
|
LC_ERROR_USER("Configure the list of enabled modules from the Live Coding section of the editor preferences window.");
|
|
GLiveCodingServer->GetCompileFinishedDelegate().ExecuteIfBound(ELiveCodingResult::Error, *FString::Printf(TEXT("Live coding not enabled for %s"), ModuleName.c_str()));
|
|
return;
|
|
}
|
|
|
|
types::vector<std::wstring> ObjectFiles;
|
|
for(const FString& ObjectFile : Pair.Value)
|
|
{
|
|
std::wstring NormalizedObjectFile = file::NormalizePath(*ObjectFile);
|
|
ObjectFiles.push_back(std::move(NormalizedObjectFile));
|
|
}
|
|
|
|
m_liveModuleToModifiedOrNewObjFiles.insert(std::make_pair(ModuleFileName, std::move(ObjectFiles)));
|
|
}
|
|
}
|
|
|
|
GLiveCodingServer->GetStatusChangeDelegate().ExecuteIfBound(L"Creating patch...");
|
|
// END EPIC MOD
|
|
|
|
telemetry::Scope scope("Creating patch");
|
|
|
|
// EPIC REMOVED: g_theApp.GetMainFrame()->OnCompilationStart();
|
|
|
|
LC_LOG_USER("---------- Creating patch ----------");
|
|
|
|
// recompile files, if any
|
|
const size_t count = m_liveModules.size();
|
|
if (count == 0u)
|
|
{
|
|
LC_LOG_USER("No live modules enabled");
|
|
}
|
|
|
|
LiveModule::ErrorType::Enum updateError = LiveModule::ErrorType::NO_CHANGE;
|
|
|
|
// check directory notifications first to prune file changes based on directories
|
|
m_directoryCache->PrimeNotifications();
|
|
|
|
FileAttributeCache fileCache;
|
|
|
|
// when all processes made progress, none of them is being held in the debugger which means it is safe to
|
|
// communicate with the client, call hooks, use synchronization points, etc.
|
|
// however, when a process was held in the debugger and now spins inside the code cave, we are not allowed
|
|
// to call any of these functions, because that might lead to a deadlock.
|
|
// similarly, if we're currently handling an exception, calling any of the client-provided functions could be fatal.
|
|
const bool inExceptionHandler = m_inExceptionHandlerEvent.WaitTimeout(0u);
|
|
const LiveModule::UpdateType::Enum updateType = (didAllProcessesMakeProgress && !inExceptionHandler)
|
|
? LiveModule::UpdateType::DEFAULT
|
|
: LiveModule::UpdateType::NO_CLIENT_COMMUNICATION;
|
|
|
|
// has the user given us at least one modified or new .obj file for at least one of the modules?
|
|
const bool hasAtLeastOneOptionalObj = (m_liveModuleToModifiedOrNewObjFiles.size() != 0u);
|
|
for (size_t i = 0u; i < count; ++i)
|
|
{
|
|
LiveModule* liveModule = m_liveModules[i];
|
|
|
|
LiveModule::ErrorType::Enum moduleUpdateError = LiveModule::ErrorType::SUCCESS;
|
|
if (hasAtLeastOneOptionalObj)
|
|
{
|
|
// try to find the list of modified or new .objs for this module
|
|
const auto objFilesIt = m_liveModuleToModifiedOrNewObjFiles.find(liveModule->GetModuleName());
|
|
if (objFilesIt == m_liveModuleToModifiedOrNewObjFiles.end())
|
|
{
|
|
// no .objs for this module, ignore
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
// build a patch with the given list of .objs for this module
|
|
const std::vector<std::wstring>& objFiles = objFilesIt->second;
|
|
moduleUpdateError = liveModule->Update(&fileCache, m_directoryCache, updateType, objFiles);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// no optional .objs were given, update all live modules regularly
|
|
std::vector<std::wstring> emptyObjs;
|
|
moduleUpdateError = liveModule->Update(&fileCache, m_directoryCache, updateType, emptyObjs);
|
|
}
|
|
|
|
// only accept new error conditions for this module if there haven't been any updates until now.
|
|
// this ensures that error conditions are kept and can be shown when updating several modules at once.
|
|
if (updateError == LiveModule::ErrorType::NO_CHANGE)
|
|
{
|
|
updateError = moduleUpdateError;
|
|
}
|
|
}
|
|
|
|
// restart directory notifications for next compilation
|
|
m_directoryCache->RestartNotifications();
|
|
|
|
//EPIC REMOVED: g_theApp.GetMainFrame()->OnCompilationEnd();
|
|
|
|
if (updateError == LiveModule::ErrorType::SUCCESS)
|
|
{
|
|
// bring Live++ to front on success
|
|
if (appSettings::g_receiveFocusOnRecompile->GetValue() == appSettings::FocusOnRecompile::ON_SUCCESS)
|
|
{
|
|
GLiveCodingServer->GetBringToFrontDelegate().ExecuteIfBound();
|
|
}
|
|
|
|
// play sound on success
|
|
const std::wstring soundOnSuccess = appSettings::g_playSoundOnSuccess->GetValue();
|
|
if (soundOnSuccess.size() != 0u)
|
|
{
|
|
// first finish any sound that might still be playing, then play the real sound
|
|
::PlaySoundW(NULL, NULL, 0u);
|
|
::PlaySoundW(soundOnSuccess.c_str(), NULL, SND_FILENAME | SND_ASYNC | SND_NODEFAULT);
|
|
}
|
|
}
|
|
|
|
if ((updateError == LiveModule::ErrorType::COMPILE_ERROR) ||
|
|
(updateError == LiveModule::ErrorType::LINK_ERROR) ||
|
|
(updateError == LiveModule::ErrorType::LOAD_PATCH_ERROR) ||
|
|
(updateError == LiveModule::ErrorType::ACTIVATE_PATCH_ERROR))
|
|
{
|
|
// bring Live++ to front on failure
|
|
if (appSettings::g_receiveFocusOnRecompile->GetValue() == appSettings::FocusOnRecompile::ON_ERROR)
|
|
{
|
|
GLiveCodingServer->GetBringToFrontDelegate().ExecuteIfBound();
|
|
}
|
|
|
|
// play sound on error
|
|
const std::wstring soundOnError = appSettings::g_playSoundOnError->GetValue();
|
|
if (soundOnError.size() != 0u)
|
|
{
|
|
// first finish any sound that might still be playing, then play the real sound
|
|
::PlaySoundW(NULL, NULL, 0u);
|
|
::PlaySoundW(soundOnError.c_str(), NULL, SND_FILENAME | SND_ASYNC | SND_NODEFAULT);
|
|
}
|
|
}
|
|
|
|
// BEGIN EPIC MOD - Custom hooks for finishing compile
|
|
switch (updateError)
|
|
{
|
|
case LiveModule::ErrorType::NO_CHANGE:
|
|
GLiveCodingServer->GetCompileFinishedDelegate().ExecuteIfBound(ELiveCodingResult::Success, L"No changes detected.");
|
|
break;
|
|
|
|
case LiveModule::ErrorType::COMPILE_ERROR:
|
|
GLiveCodingServer->GetCompileFinishedDelegate().ExecuteIfBound(ELiveCodingResult::Error, L"Compilation error.");
|
|
break;
|
|
|
|
case LiveModule::ErrorType::LINK_ERROR:
|
|
GLiveCodingServer->GetCompileFinishedDelegate().ExecuteIfBound(ELiveCodingResult::Error, L"Linker error.");
|
|
break;
|
|
|
|
case LiveModule::ErrorType::LOAD_PATCH_ERROR:
|
|
GLiveCodingServer->GetCompileFinishedDelegate().ExecuteIfBound(ELiveCodingResult::Error, L"Could not load patch image.");
|
|
break;
|
|
|
|
case LiveModule::ErrorType::ACTIVATE_PATCH_ERROR:
|
|
GLiveCodingServer->GetCompileFinishedDelegate().ExecuteIfBound(ELiveCodingResult::Error, L"Could not activate patch.");
|
|
break;
|
|
|
|
case LiveModule::ErrorType::SUCCESS:
|
|
GLiveCodingServer->GetCompileFinishedDelegate().ExecuteIfBound(ELiveCodingResult::Success, L"Patch creation successful.");
|
|
break;
|
|
|
|
default:
|
|
GLiveCodingServer->GetCompileFinishedDelegate().ExecuteIfBound(ELiveCodingResult::Success, L"Finished.");
|
|
break;
|
|
}
|
|
// END EPIC MOD
|
|
|
|
LC_LOG_USER("---------- Finished (%.3fs) ----------", scope.ReadSeconds());
|
|
|
|
// EPIC REMOVED: g_theApp.GetMainFrame()->ResetStatusBarText();
|
|
// EPIC REMOVED: g_theApp.GetMainFrame()->SetBusy(false);
|
|
}
|
|
|
|
|
|
unsigned int ServerCommandThread::CompileThread(void)
|
|
{
|
|
input::Key keyControl(VK_CONTROL);
|
|
input::Key keyAlt(VK_MENU);
|
|
input::Key keyShift(VK_SHIFT);
|
|
input::Key keyShortcut(VK_F11);
|
|
|
|
Event compilationEvent(primitiveNames::CompilationEvent(m_processGroupName).c_str(), Event::Type::MANUAL_RESET);
|
|
|
|
ChangeNotification changeNotification;
|
|
|
|
if (appSettings::g_continuousCompilationEnabled->GetValue())
|
|
{
|
|
changeNotification.Create(appSettings::g_continuousCompilationPath->GetValue());
|
|
}
|
|
|
|
for (;;)
|
|
{
|
|
const int shortcutValue = appSettings::g_compileShortcut->GetValue();
|
|
keyShortcut.AssignCode(shortcut::GetVirtualKeyCode(shortcutValue));
|
|
|
|
keyControl.Clear();
|
|
keyAlt.Clear();
|
|
keyShift.Clear();
|
|
keyShortcut.Clear();
|
|
|
|
keyControl.Update();
|
|
keyAlt.Update();
|
|
keyShift.Update();
|
|
keyShortcut.Update();
|
|
|
|
// BEGIN EPIC MOD - Adding SetActive command
|
|
if(!m_active)
|
|
{
|
|
keyShortcut.Clear();
|
|
}
|
|
// END EPIC MOD
|
|
|
|
const bool control = shortcut::ContainsControl(shortcutValue) ? keyControl.IsPressed() : !keyControl.IsPressed();
|
|
const bool alt = shortcut::ContainsAlt(shortcutValue) ? keyAlt.IsPressed() : !keyAlt.IsPressed();
|
|
const bool shift = shortcut::ContainsShift(shortcutValue) ? keyShift.IsPressed() : !keyShift.IsPressed();
|
|
const bool isShortcutPressed = (control && alt && shift && keyShortcut.WentDown());
|
|
|
|
// did anything change in the watched directory?
|
|
const unsigned int changeNotificationTimeout = static_cast<unsigned int>(appSettings::g_continuousCompilationTimeout->GetValue());
|
|
|
|
const bool foundAnyModification = changeNotification.CheckOnce();
|
|
if (foundAnyModification)
|
|
{
|
|
// clear the log if desired by the user
|
|
if (appSettings::g_clearLogOnRecompile->GetValue())
|
|
{
|
|
GLiveCodingServer->GetClearOutputDelegate().ExecuteIfBound();
|
|
}
|
|
|
|
LC_SUCCESS_USER("Detected file modification, re-checking until timeout (%d ms)", changeNotificationTimeout);
|
|
changeNotification.CheckNext(changeNotificationTimeout);
|
|
}
|
|
|
|
if (isShortcutPressed || foundAnyModification || m_manualRecompileTriggered)
|
|
{
|
|
// forbid command thread to handle commands through the pipe
|
|
m_handleCommandsEvent.Reset();
|
|
|
|
// tell clients that we're about to compile.
|
|
// clients will send a command to say that they're ready. this command will let the command thread
|
|
// rest until we signal the event again.
|
|
compilationEvent.Signal();
|
|
|
|
// remove inactive/disconnected processes
|
|
{
|
|
for (auto processIt = m_liveProcesses.begin(); processIt != m_liveProcesses.end(); /* nothing */)
|
|
{
|
|
LiveProcess* liveProcess = *processIt;
|
|
process::Handle processHandle = liveProcess->GetProcessHandle();
|
|
if (!process::IsActive(processHandle))
|
|
{
|
|
LC_WARNING_USER("Process %d is no longer valid, disconnecting", liveProcess->GetProcessId());
|
|
|
|
process::Close(processHandle);
|
|
|
|
// tell live modules to remove this process
|
|
const size_t moduleCount = m_liveModules.size();
|
|
for (size_t j = 0u; j < moduleCount; ++j)
|
|
{
|
|
LiveModule* liveModule = m_liveModules[j];
|
|
liveModule->UnregisterProcess(liveProcess);
|
|
}
|
|
|
|
delete liveProcess;
|
|
|
|
processIt = m_liveProcesses.erase(processIt);
|
|
}
|
|
else
|
|
{
|
|
// update process heart beats to know whether it made some progress
|
|
liveProcess->ReadHeartBeatDelta(m_processGroupName.c_str());
|
|
|
|
++processIt;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool didAllProcessesMakeProgress = true;
|
|
{
|
|
const size_t processCount = m_liveProcesses.size();
|
|
for (size_t i = 0u; i < processCount; ++i)
|
|
{
|
|
LiveProcess* liveProcess = m_liveProcesses[i];
|
|
didAllProcessesMakeProgress &= liveProcess->MadeProgress();
|
|
}
|
|
}
|
|
|
|
if (!didAllProcessesMakeProgress)
|
|
{
|
|
// install a code cave for all processes.
|
|
// this ensures that if a process is currently being held in the debugger, the process will
|
|
// not make progress in terms of new instructions being executed after continuing it in the debugger.
|
|
const size_t processCount = m_liveProcesses.size();
|
|
for (size_t i = 0u; i < processCount; ++i)
|
|
{
|
|
LiveProcess* liveProcess = m_liveProcesses[i];
|
|
liveProcess->InstallCodeCave();
|
|
}
|
|
|
|
// don't allow the exception handler dialog to be shown when continuing in the debugger with F5
|
|
m_exceptionCS.Enter();
|
|
}
|
|
|
|
// wait until all command threads/clients are ready to go. we might not be getting commands
|
|
// from a client because it is being held in the debugger.
|
|
{
|
|
if (didAllProcessesMakeProgress)
|
|
{
|
|
LC_SUCCESS_USER("Waiting for client(s)");
|
|
}
|
|
else
|
|
{
|
|
LC_SUCCESS_USER("Waiting for client(s), hit 'Continue' (F5) if being held in the debugger");
|
|
}
|
|
|
|
CriticalSection::ScopedLock lock(&m_connectionCS);
|
|
|
|
const size_t count = m_commandThreads.size();
|
|
for (size_t i = 0u; i < count; ++i)
|
|
{
|
|
CommandThreadContext* threadContext = m_commandThreads[i];
|
|
threadContext->readyEvent->Wait();
|
|
}
|
|
}
|
|
|
|
// do not let other processes register new modules during compilation
|
|
CriticalSection::ScopedLock actionLock(&m_actionCS);
|
|
|
|
// setup the same virtual drive we had when loading the project
|
|
AddVirtualDrive();
|
|
|
|
if (isShortcutPressed || m_manualRecompileTriggered)
|
|
{
|
|
// clear the log if desired by the user
|
|
if (appSettings::g_clearLogOnRecompile->GetValue())
|
|
{
|
|
GLiveCodingServer->GetClearOutputDelegate().ExecuteIfBound();
|
|
}
|
|
|
|
if (isShortcutPressed)
|
|
{
|
|
LC_SUCCESS_USER("Accepted live coding shortcut");
|
|
}
|
|
else if (m_manualRecompileTriggered)
|
|
{
|
|
LC_SUCCESS_USER("Manual recompile triggered");
|
|
}
|
|
}
|
|
|
|
// bring Live++ to front on shortcut trigger
|
|
if (appSettings::g_receiveFocusOnRecompile->GetValue() == appSettings::FocusOnRecompile::ON_SHORTCUT)
|
|
{
|
|
GLiveCodingServer->GetBringToFrontDelegate().ExecuteIfBound();
|
|
}
|
|
|
|
CompileChanges(didAllProcessesMakeProgress);
|
|
|
|
RemoveVirtualDrive();
|
|
|
|
if (!didAllProcessesMakeProgress)
|
|
{
|
|
// remove all code caves
|
|
const size_t processCount = m_liveProcesses.size();
|
|
for (size_t i = 0u; i < processCount; ++i)
|
|
{
|
|
LiveProcess* liveProcess = m_liveProcesses[i];
|
|
liveProcess->UninstallCodeCave();
|
|
}
|
|
|
|
// remove the lock on the exception handler dialog
|
|
m_exceptionCS.Leave();
|
|
}
|
|
|
|
compilationEvent.Reset();
|
|
|
|
m_handleCommandsEvent.Signal();
|
|
|
|
// clear change notifications that might have happened while compiling
|
|
changeNotification.Check(0u);
|
|
|
|
// clear API recompiles
|
|
m_manualRecompileTriggered = false;
|
|
m_liveModuleToModifiedOrNewObjFiles.clear();
|
|
}
|
|
else
|
|
{
|
|
// nothing to do for now, go to sleep a bit
|
|
thread::Sleep(10u);
|
|
}
|
|
}
|
|
|
|
return 0u;
|
|
}
|
|
|
|
|
|
unsigned int __stdcall ServerCommandThread::CommandThreadProxy(void* context)
|
|
{
|
|
thread::SetName("Live coding client command communication");
|
|
|
|
CommandThreadContext* realContext = static_cast<CommandThreadContext*>(context);
|
|
return realContext->instance->CommandThread(&realContext->pipe, realContext->readyEvent);
|
|
}
|
|
|
|
|
|
unsigned int ServerCommandThread::CommandThread(DuplexPipeServer* pipe, Event* readyEvent)
|
|
{
|
|
// handle incoming commands
|
|
CommandMap commandMap;
|
|
commandMap.RegisterAction<TriggerRecompileAction>();
|
|
commandMap.RegisterAction<BuildPatchAction>();
|
|
commandMap.RegisterAction<ReadyForCompilationAction>();
|
|
commandMap.RegisterAction<DisconnectClientAction>();
|
|
// BEGIN EPIC MOD - Adding ShowConsole command
|
|
commandMap.RegisterAction<ShowConsoleAction>();
|
|
// END EPIC MOD
|
|
// BEGIN EPIC MOD - Adding SetVisible command
|
|
commandMap.RegisterAction<SetVisibleAction>();
|
|
// END EPIC MOD
|
|
// BEGIN EPIC MOD - Adding SetActive command
|
|
commandMap.RegisterAction<SetActiveAction>();
|
|
// END EPIC MOD
|
|
// BEGIN EPIC MOD - Adding SetBuildArguments command
|
|
commandMap.RegisterAction<SetBuildArgumentsAction>();
|
|
// END EPIC MOD
|
|
commandMap.RegisterAction<RegisterProcessAction>();
|
|
commandMap.RegisterAction<EnableModuleBatchBeginAction>();
|
|
commandMap.RegisterAction<EnableModuleBatchEndAction>();
|
|
commandMap.RegisterAction<DisableModuleBatchBeginAction>();
|
|
commandMap.RegisterAction<DisableModuleBatchEndAction>();
|
|
commandMap.RegisterAction<EnableModuleAction>();
|
|
commandMap.RegisterAction<EnableAllModulesAction>();
|
|
commandMap.RegisterAction<DisableModuleAction>();
|
|
commandMap.RegisterAction<DisableAllModulesAction>();
|
|
commandMap.RegisterAction<ApplySettingBoolAction>();
|
|
commandMap.RegisterAction<ApplySettingIntAction>();
|
|
commandMap.RegisterAction<ApplySettingStringAction>();
|
|
|
|
for (;;)
|
|
{
|
|
const bool success = commandMap.HandleCommands(pipe, this);
|
|
|
|
// we must have received a ReadyForCompilation command to get here, or the pipe is broken.
|
|
// in any case, let the main server thread responsible for compilation know that this client is ready.
|
|
// this is needed to always let the compilation thread advance, even when a client might have disconnected.
|
|
readyEvent->Signal();
|
|
|
|
if ((!success) || (!pipe->IsValid()))
|
|
{
|
|
// pipe was closed or is broken, bail out.
|
|
// remove ourselves from the array of threads first.
|
|
RemoveCommandThread(pipe);
|
|
return 1u;
|
|
}
|
|
|
|
// wait until we're allowed to handle commands again
|
|
m_handleCommandsEvent.Wait();
|
|
|
|
// tell client that compilation has finished
|
|
pipe->SendCommandAndWaitForAck(commands::CompilationFinished {});
|
|
}
|
|
|
|
RemoveCommandThread(pipe);
|
|
return 0u;
|
|
}
|
|
|
|
|
|
unsigned int __stdcall ServerCommandThread::ExceptionCommandThreadProxy(void* context)
|
|
{
|
|
thread::SetName("Live coding client exception command communication");
|
|
|
|
CommandThreadContext* realContext = static_cast<CommandThreadContext*>(context);
|
|
return realContext->instance->ExceptionCommandThread(&realContext->exceptionPipe);
|
|
}
|
|
|
|
|
|
unsigned int ServerCommandThread::ExceptionCommandThread(DuplexPipeServer* exceptionPipe)
|
|
{
|
|
// handle incoming exception commands
|
|
CommandMap commandMap;
|
|
commandMap.RegisterAction<HandleExceptionAction>();
|
|
|
|
for (;;)
|
|
{
|
|
const bool success = commandMap.HandleCommands(exceptionPipe, this);
|
|
if ((!success) || (!exceptionPipe->IsValid()))
|
|
{
|
|
// pipe was closed or is broken, bail out
|
|
return 1u;
|
|
}
|
|
}
|
|
|
|
return 0u;
|
|
}
|
|
|
|
|
|
void ServerCommandThread::RemoveCommandThread(const DuplexPipe* pipe)
|
|
{
|
|
CriticalSection::ScopedLock lock(&m_connectionCS);
|
|
|
|
const size_t count = m_commandThreads.size();
|
|
for (size_t i = 0u; i < count; ++i)
|
|
{
|
|
CommandThreadContext* threadContext = m_commandThreads[i];
|
|
if (&threadContext->pipe == pipe)
|
|
{
|
|
// don't bother cleaning up the context, just remove it
|
|
auto it = m_commandThreads.begin();
|
|
std::advance(it, i);
|
|
m_commandThreads.erase(it);
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
LiveProcess* ServerCommandThread::FindProcessById(unsigned int processId)
|
|
{
|
|
const size_t count = m_liveProcesses.size();
|
|
for (size_t i = 0u; i < count; ++i)
|
|
{
|
|
LiveProcess* process = m_liveProcesses[i];
|
|
if (process->GetProcessId() == processId)
|
|
{
|
|
return process;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
bool ServerCommandThread::TriggerRecompileAction::Execute(CommandType*, const DuplexPipe* pipe, void* context)
|
|
{
|
|
ServerCommandThread* commandThread = static_cast<ServerCommandThread*>(context);
|
|
|
|
// protect against accepting this command while compilation is already in progress
|
|
CriticalSection::ScopedLock lock(&commandThread->m_actionCS);
|
|
|
|
pipe->SendAck();
|
|
|
|
commandThread->m_manualRecompileTriggered = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool ServerCommandThread::BuildPatchAction::Execute(CommandType* command, const DuplexPipe* pipe, void* context)
|
|
{
|
|
ServerCommandThread* commandThread = static_cast<ServerCommandThread*>(context);
|
|
|
|
// protect against accepting this command while compilation is already in progress
|
|
CriticalSection::ScopedLock lock(&commandThread->m_actionCS);
|
|
|
|
pipe->SendAck();
|
|
|
|
// receive module names and .obj paths
|
|
for (unsigned int i = 0u; i < command->count; ++i)
|
|
{
|
|
uint32_t id = 0u;
|
|
pipe->ReceiveCommandId(&id);
|
|
|
|
commands::BuildPatchPacket packetCommand = {};
|
|
pipe->ReceiveCommand(&packetCommand);
|
|
|
|
pipe->SendAck();
|
|
|
|
commandThread->m_liveModuleToModifiedOrNewObjFiles[packetCommand.moduleName].push_back(packetCommand.objPath);
|
|
}
|
|
|
|
commandThread->m_manualRecompileTriggered = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool ServerCommandThread::HandleExceptionAction::Execute(CommandType* command, const DuplexPipe* pipe, void* context)
|
|
{
|
|
pipe->SendAck();
|
|
|
|
// BEGIN EPIC MOD - Using internal CrashReporter
|
|
#if 0
|
|
ServerCommandThread* commandThread = static_cast<ServerCommandThread*>(context);
|
|
|
|
// protect against several processes showing a dialog at the same time.
|
|
// protect against showing the exception handler dialog while compilation is already in progress.
|
|
CriticalSection::ScopedLock lock(&commandThread->m_exceptionCS);
|
|
|
|
LiveProcess* liveProcess = commandThread->FindProcessById(command->processId);
|
|
if (!liveProcess)
|
|
{
|
|
// signal client we did not handle the exception
|
|
pipe->SendCommandAndWaitForAck(commands::HandleExceptionFinished { nullptr, nullptr, nullptr, false });
|
|
return true;
|
|
}
|
|
|
|
// let the compile thread know that we're currently handling an exception.
|
|
// this is needed to ensure that no hooks or synchronization points are called during compilation.
|
|
commandThread->m_inExceptionHandlerEvent.Signal();
|
|
|
|
// hold all processes in place
|
|
const size_t processCount = commandThread->m_liveProcesses.size();
|
|
for (size_t i = 0u; i < processCount; ++i)
|
|
{
|
|
commandThread->m_liveProcesses[i]->InstallCodeCave();
|
|
}
|
|
|
|
ExceptionHandlerDialog dialog(commandThread->m_processGroupName, liveProcess, command->threadId, command->exception, command->context, command->clientContextPtr);
|
|
const INT_PTR result = dialog.DoModal();
|
|
|
|
// release processes from the cave
|
|
for (size_t i = 0u; i < processCount; ++i)
|
|
{
|
|
commandThread->m_liveProcesses[i]->UninstallCodeCave();
|
|
}
|
|
|
|
// remove our signal saying that we're handling an exception
|
|
commandThread->m_inExceptionHandlerEvent.Reset();
|
|
|
|
if (result == IDC_EXCEPTION_HANDLER_LEAVE)
|
|
{
|
|
// tell the client that it needs to unwind its stack and continue at the return address
|
|
const ExceptionHandlerDialog::ParentFrameData& frameData = dialog.GetParentFrameData();
|
|
pipe->SendCommandAndWaitForAck(commands::HandleExceptionFinished { frameData.returnAddress, frameData.framePointer, frameData.stackPointer, true });
|
|
return true;
|
|
}
|
|
else if (result == IDC_EXCEPTION_HANDLER_IGNORE)
|
|
{
|
|
// tell the client that we ignored the exception
|
|
pipe->SendCommandAndWaitForAck(commands::HandleExceptionFinished { nullptr, nullptr, nullptr, false });
|
|
return true;
|
|
}
|
|
|
|
// signal client that we handled the exception and there's nothing left to do
|
|
pipe->SendCommandAndWaitForAck(commands::HandleExceptionFinished { nullptr, nullptr, nullptr, true });
|
|
#endif
|
|
// END EPIC MOD
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool ServerCommandThread::ReadyForCompilationAction::Execute(CommandType*, const DuplexPipe* pipe, void*)
|
|
{
|
|
pipe->SendAck();
|
|
|
|
// don't continue execution
|
|
return false;
|
|
}
|
|
|
|
|
|
bool ServerCommandThread::DisconnectClientAction::Execute(CommandType*, const DuplexPipe* pipe, void* context)
|
|
{
|
|
ServerCommandThread* instance = static_cast<ServerCommandThread*>(context);
|
|
|
|
// unregister this connection
|
|
{
|
|
instance->RemoveCommandThread(pipe);
|
|
|
|
CriticalSection::ScopedLock lock(&instance->m_connectionCS);
|
|
if (instance->m_commandThreads.size() == 0u)
|
|
{
|
|
// BEGIN EPIC MOD - No built-in UI
|
|
// // this was the last client to disconnect, remove the system tray
|
|
// g_theApp.GetMainFrame()->GetSystemTray()->Destroy();
|
|
// END EPIC MOD
|
|
}
|
|
}
|
|
|
|
pipe->SendAck();
|
|
|
|
return true;
|
|
}
|
|
|
|
// BEGIN EPIC MOD - Adding ShowConsole command
|
|
bool ServerCommandThread::ShowConsoleAction::Execute(CommandType* command, const DuplexPipe* pipe, void* context)
|
|
{
|
|
pipe->SendAck();
|
|
|
|
GLiveCodingServer->GetShowConsoleDelegate().ExecuteIfBound();
|
|
|
|
return true;
|
|
}
|
|
// END EPIC MOD
|
|
|
|
// BEGIN EPIC MOD - Adding SetVisible command
|
|
bool ServerCommandThread::SetVisibleAction::Execute(CommandType* command, const DuplexPipe* pipe, void* context)
|
|
{
|
|
pipe->SendAck();
|
|
|
|
GLiveCodingServer->GetSetVisibleDelegate().ExecuteIfBound(command->visible);
|
|
|
|
return true;
|
|
}
|
|
// END EPIC MOD
|
|
|
|
// BEGIN EPIC MOD - Adding SetActive command
|
|
bool ServerCommandThread::SetActiveAction::Execute(CommandType* command, const DuplexPipe* pipe, void* context)
|
|
{
|
|
ServerCommandThread* commandThread = static_cast<ServerCommandThread*>(context);
|
|
|
|
// protect against accepting this command while compilation is already in progress
|
|
CriticalSection::ScopedLock lock(&commandThread->m_actionCS);
|
|
|
|
pipe->SendAck();
|
|
|
|
commandThread->m_active = command->active;
|
|
|
|
return true;
|
|
}
|
|
// END EPIC MOD
|
|
|
|
// BEGIN EPIC MOD - Adding SetBuildArguments command
|
|
bool ServerCommandThread::SetBuildArgumentsAction::Execute(CommandType* command, const DuplexPipe* pipe, void* context)
|
|
{
|
|
ServerCommandThread* commandThread = static_cast<ServerCommandThread*>(context);
|
|
|
|
// protect against accepting this command while compilation is already in progress
|
|
CriticalSection::ScopedLock lock(&commandThread->m_actionCS);
|
|
|
|
for (LiveProcess* process : commandThread->m_liveProcesses)
|
|
{
|
|
if (process->GetProcessId() == command->processId)
|
|
{
|
|
process->SetBuildArguments(command->arguments);
|
|
}
|
|
}
|
|
|
|
pipe->SendAck();
|
|
|
|
return true;
|
|
}
|
|
// END EPIC MOD
|
|
|
|
bool ServerCommandThread::RegisterProcessAction::Execute(CommandType* command, const DuplexPipe* pipe, void* context)
|
|
{
|
|
pipe->SendAck();
|
|
|
|
ServerCommandThread* commandThread = static_cast<ServerCommandThread*>(context);
|
|
|
|
// protect against several client DLLs calling into this action at the same time
|
|
CriticalSection::ScopedLock lock(&commandThread->m_actionCS);
|
|
|
|
process::Handle processHandle = process::Open(command->processId);
|
|
|
|
// check if any live module in this process group has patches installed already
|
|
{
|
|
const std::wstring& processPath = process::GetImagePath(processHandle);
|
|
|
|
bool registeredSuccessfully = true;
|
|
if (!appSettings::g_installCompiledPatchesMultiProcess->GetValue())
|
|
{
|
|
// we are not allowed to install any compiled patches when a new executable is spawned
|
|
bool processGroupHasPatches = false;
|
|
const size_t count = commandThread->m_liveModules.size();
|
|
for (size_t i = 0u; i < count; ++i)
|
|
{
|
|
LiveModule* liveModule = commandThread->m_liveModules[i];
|
|
if (liveModule->HasInstalledPatches())
|
|
{
|
|
std::wstring caption(L"Live coding - Registering process ");
|
|
caption += file::GetFilename(processPath);
|
|
|
|
processGroupHasPatches = true;
|
|
// BEGIN EPIC MOD - Using non-modal error dialog
|
|
GLiveCodingServer->GetLogOutputDelegate().ExecuteIfBound(ELiveCodingLogVerbosity::Failure, L"This process cannot be added to the existing process group, because at least one module already has installed patches. Live coding is disabled for this process.");
|
|
// END EPIC MD
|
|
break;
|
|
}
|
|
}
|
|
|
|
registeredSuccessfully = !processGroupHasPatches;
|
|
}
|
|
|
|
if (registeredSuccessfully)
|
|
{
|
|
LiveProcess* liveProcess = new LiveProcess(processHandle, command->processId, command->threadId, pipe);
|
|
commandThread->m_liveProcesses.push_back(liveProcess);
|
|
// BEGIN EPIC MOD - No built-in UI
|
|
// commandThread->m_mainFrame->UpdateWindowTitle();
|
|
// END EPIC MOD
|
|
|
|
LC_SUCCESS_USER("Registered process %S (PID: %d)", processPath.c_str(), command->processId);
|
|
}
|
|
|
|
// tell client we are finished
|
|
pipe->SendCommandAndWaitForAck(commands::RegisterProcessFinished { registeredSuccessfully });
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool ServerCommandThread::EnableModuleBatchBeginAction::Execute(CommandType*, const DuplexPipe* pipe, void* context)
|
|
{
|
|
ServerCommandThread* commandThread = static_cast<ServerCommandThread*>(context);
|
|
|
|
// protect against several client DLLs calling into this action at the same time.
|
|
// we hold this critical section until we get the BatchEnd signal.
|
|
// this ensures that all modules are loaded serialized per process.
|
|
commandThread->m_actionCS.Enter();
|
|
|
|
commandThread->m_moduleBatchScope.Restart();
|
|
commandThread->m_loadedCompilandCountInBatchScope = 0u;
|
|
|
|
// set up virtual drives before loading anything, otherwise files won't be detected and therefore discarded
|
|
const std::wstring virtualDriveLetter = appSettings::g_virtualDriveLetter->GetValue();
|
|
const std::wstring virtualDrivePath = appSettings::g_virtualDrivePath->GetValue();
|
|
if ((virtualDriveLetter.size() != 0) && (virtualDrivePath.size() != 0))
|
|
{
|
|
virtualDrive::Add(virtualDriveLetter.c_str(), virtualDrivePath.c_str());
|
|
}
|
|
|
|
pipe->SendAck();
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool ServerCommandThread::EnableModuleBatchEndAction::Execute(CommandType*, const DuplexPipe* pipe, void* context)
|
|
{
|
|
ServerCommandThread* commandThread = static_cast<ServerCommandThread*>(context);
|
|
|
|
commandThread->m_moduleBatchScope.End();
|
|
LC_SUCCESS_USER("Successfully loaded modules (%.3fs, %zu translation units)", commandThread->m_moduleBatchScope.ReadSeconds(), commandThread->m_loadedCompilandCountInBatchScope);
|
|
|
|
// EPIC REMOVED: commandThread->PrewarmCompilerEnvironmentCache();
|
|
|
|
// EPIC REMOVED: g_theApp.GetMainFrame()->ResetStatusBarText();
|
|
|
|
// tell user we are ready
|
|
{
|
|
const int shortcut = appSettings::g_compileShortcut->GetValue();
|
|
const std::wstring& shortcutText = shortcut::ConvertShortcutToText(shortcut);
|
|
LC_SUCCESS_USER("Live coding ready - Save changes and press %S to re-compile code", shortcutText.c_str());
|
|
}
|
|
|
|
// remove virtual drives once we're finished
|
|
const std::wstring virtualDriveLetter = appSettings::g_virtualDriveLetter->GetValue();
|
|
const std::wstring virtualDrivePath = appSettings::g_virtualDrivePath->GetValue();
|
|
if ((virtualDriveLetter.size() != 0) && (virtualDrivePath.size() != 0))
|
|
{
|
|
virtualDrive::Remove(virtualDriveLetter.c_str(), virtualDrivePath.c_str());
|
|
}
|
|
|
|
pipe->SendAck();
|
|
|
|
// protect against several client DLLs calling into this action at the same time
|
|
commandThread->m_actionCS.Leave();
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool ServerCommandThread::DisableModuleBatchBeginAction::Execute(CommandType*, const DuplexPipe* pipe, void* context)
|
|
{
|
|
ServerCommandThread* commandThread = static_cast<ServerCommandThread*>(context);
|
|
|
|
// protect against several client DLLs calling into this action at the same time.
|
|
// we hold this critical section until we get the BatchEnd signal.
|
|
// this ensures that all modules are unloaded serialized per process.
|
|
commandThread->m_actionCS.Enter();
|
|
|
|
pipe->SendAck();
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool ServerCommandThread::DisableModuleBatchEndAction::Execute(CommandType*, const DuplexPipe* pipe, void* context)
|
|
{
|
|
// EPIC REMOVED: g_theApp.GetMainFrame()->ResetStatusBarText();
|
|
|
|
ServerCommandThread* commandThread = static_cast<ServerCommandThread*>(context);
|
|
|
|
pipe->SendAck();
|
|
|
|
// protect against several client DLLs calling into this action at the same time
|
|
commandThread->m_actionCS.Leave();
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool ServerCommandThread::EnableModuleAction::Execute(CommandType* command, const DuplexPipe* pipe, void* context)
|
|
{
|
|
pipe->SendAck();
|
|
|
|
ServerCommandThread* commandThread = static_cast<ServerCommandThread*>(context);
|
|
|
|
TaskContext taskContext = {};
|
|
taskContext.taskRoot = scheduler::CreateEmptyTask();
|
|
commandThread->LoadModule(command->path, pipe, &taskContext, command->processId);
|
|
|
|
// wait for all tasks to finish
|
|
scheduler::RunTask(taskContext.taskRoot);
|
|
scheduler::WaitForTask(taskContext.taskRoot);
|
|
|
|
// add all live modules loaded by the tasks
|
|
for (size_t i = 0u; i < taskContext.tasks.size(); ++i)
|
|
{
|
|
LiveModule* liveModule = taskContext.tasks[i]->GetResult();
|
|
commandThread->m_liveModules.push_back(liveModule);
|
|
|
|
// update directory cache for this live module
|
|
liveModule->UpdateDirectoryCache(commandThread->m_directoryCache);
|
|
|
|
// update the number of loaded translation units during this batch
|
|
commandThread->m_loadedCompilandCountInBatchScope += liveModule->GetCompilandDatabase()->compilands.size();
|
|
}
|
|
|
|
scheduler::DestroyTasks(taskContext.tasks);
|
|
scheduler::DestroyTask(taskContext.taskRoot);
|
|
|
|
// tell client we are finished
|
|
pipe->SendCommandAndWaitForAck(commands::EnableModuleFinished { command->token });
|
|
|
|
// dump memory statistics
|
|
{
|
|
LC_LOG_INDENT_TELEMETRY;
|
|
g_symbolAllocator.PrintStats();
|
|
g_immutableStringAllocator.PrintStats();
|
|
g_contributionAllocator.PrintStats();
|
|
g_compilandAllocator.PrintStats();
|
|
g_dependencyAllocator.PrintStats();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool ServerCommandThread::EnableAllModulesAction::Execute(CommandType* command, const DuplexPipe* pipe, void* context)
|
|
{
|
|
pipe->SendAck();
|
|
|
|
ServerCommandThread* commandThread = static_cast<ServerCommandThread*>(context);
|
|
|
|
TaskContext taskContext = {};
|
|
taskContext.taskRoot = scheduler::CreateEmptyTask();
|
|
commandThread->LoadAllModules(command->path, pipe, &taskContext, command->processId);
|
|
|
|
// wait for all tasks to finish
|
|
scheduler::RunTask(taskContext.taskRoot);
|
|
scheduler::WaitForTask(taskContext.taskRoot);
|
|
|
|
// add all live modules loaded by the tasks
|
|
for (size_t i = 0u; i < taskContext.tasks.size(); ++i)
|
|
{
|
|
LiveModule* liveModule = taskContext.tasks[i]->GetResult();
|
|
commandThread->m_liveModules.push_back(liveModule);
|
|
|
|
// update directory cache for this live module
|
|
liveModule->UpdateDirectoryCache(commandThread->m_directoryCache);
|
|
|
|
// update the number of loaded translation units during this batch
|
|
commandThread->m_loadedCompilandCountInBatchScope += liveModule->GetCompilandDatabase()->compilands.size();
|
|
}
|
|
|
|
scheduler::DestroyTasks(taskContext.tasks);
|
|
scheduler::DestroyTask(taskContext.taskRoot);
|
|
|
|
// tell server we are finished
|
|
pipe->SendCommandAndWaitForAck(commands::EnableAllModulesFinished { command->token });
|
|
|
|
// dump memory statistics
|
|
{
|
|
LC_LOG_INDENT_TELEMETRY;
|
|
g_symbolAllocator.PrintStats();
|
|
g_immutableStringAllocator.PrintStats();
|
|
g_contributionAllocator.PrintStats();
|
|
g_compilandAllocator.PrintStats();
|
|
g_dependencyAllocator.PrintStats();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool ServerCommandThread::DisableModuleAction::Execute(CommandType* command, const DuplexPipe* pipe, void* context)
|
|
{
|
|
pipe->SendAck();
|
|
|
|
ServerCommandThread* commandThread = static_cast<ServerCommandThread*>(context);
|
|
commandThread->UnloadModule(command->path, pipe, command->processId);
|
|
|
|
// tell server we are finished
|
|
pipe->SendCommandAndWaitForAck(commands::DisableModuleFinished { command->token });
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool ServerCommandThread::DisableAllModulesAction::Execute(CommandType* command, const DuplexPipe* pipe, void* context)
|
|
{
|
|
pipe->SendAck();
|
|
|
|
ServerCommandThread* commandThread = static_cast<ServerCommandThread*>(context);
|
|
commandThread->UnloadAllModules(command->path, pipe, command->processId);
|
|
|
|
// tell server we are finished
|
|
pipe->SendCommandAndWaitForAck(commands::DisableAllModulesFinished { command->token });
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool ServerCommandThread::GetModuleInfoAction::Execute(CommandType* command, const DuplexPipe* pipe, void* context)
|
|
{
|
|
pipe->SendAck();
|
|
|
|
if (!command->moduleBase)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// note that the path we get back from the DLL might not be normalized, depending on how the executable was launched.
|
|
// it is crucial to normalize the path again, otherwise we could load already loaded modules into the same
|
|
// Live++ instance, which would wreak havoc
|
|
const std::wstring modulePath = file::NormalizePath(file::RelativeToAbsolutePath(command->path).c_str());
|
|
|
|
ServerCommandThread* commandThread = static_cast<ServerCommandThread*>(context);
|
|
TaskContext* taskContext = static_cast<TaskContext*>(command->taskContext);
|
|
|
|
// a task context is provided for loading modules
|
|
const bool shouldLoad = (taskContext != nullptr);
|
|
|
|
if (command->loadImports)
|
|
{
|
|
if (shouldLoad)
|
|
{
|
|
// load this module and all its import DLLs as well
|
|
commandThread->LoadAllModules(modulePath.c_str(), pipe, taskContext, command->processId);
|
|
}
|
|
else
|
|
{
|
|
// unload this module and all its import DLLs as well
|
|
commandThread->UnloadAllModules(modulePath.c_str(), pipe, command->processId);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
LiveProcess* liveProcess = commandThread->FindProcessById(command->processId);
|
|
LC_ASSERT(liveProcess, "Invalid process ID.");
|
|
|
|
const executable::Header imageHeader = GetImageHeader(modulePath.c_str());
|
|
if (shouldLoad)
|
|
{
|
|
if (liveProcess->TriedToLoadImage(imageHeader))
|
|
{
|
|
// tried loading this module into this process already
|
|
return false;
|
|
}
|
|
|
|
// find any other process ID that tried to load this module already (if any)
|
|
{
|
|
const size_t count = commandThread->m_liveProcesses.size();
|
|
for (size_t i = 0u; i < count; ++i)
|
|
{
|
|
LiveProcess* otherLiveProcess = commandThread->m_liveProcesses[i];
|
|
if (otherLiveProcess->TriedToLoadImage(imageHeader))
|
|
{
|
|
// some *other* process loaded this module already
|
|
LC_LOG_USER("Registering module %S (PID: %d)", modulePath.c_str(), command->processId);
|
|
|
|
LiveModule* liveModule = commandThread->m_imageHeaderToLiveModule[imageHeader];
|
|
if (liveModule)
|
|
{
|
|
const unsigned int processId = command->processId;
|
|
void* moduleBase = command->moduleBase;
|
|
|
|
liveModule->RegisterProcess(liveProcess, moduleBase, modulePath);
|
|
liveModule->DisableControlFlowGuard(liveProcess, moduleBase);
|
|
|
|
const bool installedPatchesSuccessfully = liveModule->InstallCompiledPatches(liveProcess, moduleBase);
|
|
if (!installedPatchesSuccessfully)
|
|
{
|
|
LC_ERROR_USER("Compiled patches could not be installed (PID: %d)", processId);
|
|
liveModule->UnregisterProcess(liveProcess);
|
|
}
|
|
|
|
liveProcess->AddLoadedImage(imageHeader);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
symbols::Provider* moduleProvider = symbols::OpenEXE(modulePath.c_str(), symbols::OpenOptions::ACCUMULATE_SIZE);
|
|
if (!moduleProvider)
|
|
{
|
|
liveProcess->AddLoadedImage(imageHeader);
|
|
return false;
|
|
}
|
|
|
|
// this live module hasn't been loaded yet by any process
|
|
void* moduleBase = command->moduleBase;
|
|
|
|
// accumulate module info
|
|
const file::Attributes attributes = file::GetAttributes(modulePath.c_str());
|
|
const uint64_t size = file::GetSize(attributes);
|
|
g_loadedModuleSize.Accumulate(size);
|
|
|
|
{
|
|
// create a task to load the module of this batch concurrently
|
|
LC_LOG_USER("Loading module %S (%.3f MB)", modulePath.c_str(), size / 1048576.0f);
|
|
|
|
LiveModule* liveModule = new LiveModule(modulePath.c_str(), imageHeader, commandThread->m_runMode);
|
|
commandThread->m_imageHeaderToLiveModule.emplace(imageHeader, liveModule);
|
|
|
|
auto task = scheduler::CreateTask(taskContext->taskRoot, [liveModule, liveProcess, modulePath, moduleBase, moduleProvider]()
|
|
{
|
|
telemetry::Scope scope("Loading module");
|
|
|
|
symbols::DiaCompilandDB* moduleDiaCompilandDb = symbols::GatherDiaCompilands(moduleProvider);
|
|
|
|
liveModule->Load(moduleProvider, moduleDiaCompilandDb);
|
|
liveModule->RegisterProcess(liveProcess, moduleBase, modulePath);
|
|
liveModule->DisableControlFlowGuard(liveProcess, moduleBase);
|
|
|
|
symbols::DestroyDiaCompilandDB(moduleDiaCompilandDb);
|
|
symbols::Close(moduleProvider);
|
|
|
|
return liveModule;
|
|
});
|
|
scheduler::RunTask(task);
|
|
|
|
taskContext->tasks.emplace_back(task);
|
|
}
|
|
|
|
g_loadedModuleSize.Print();
|
|
g_loadedModuleSize.ResetCurrent();
|
|
|
|
liveProcess->AddLoadedImage(imageHeader);
|
|
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
LC_LOG_USER("Unloading module %S", modulePath.c_str());
|
|
|
|
liveProcess->RemoveLoadedImage(imageHeader);
|
|
commandThread->m_imageHeaderToLiveModule.erase(imageHeader);
|
|
|
|
for (auto it = commandThread->m_liveModules.begin(); it != commandThread->m_liveModules.end(); /* nothing */)
|
|
{
|
|
LiveModule* liveModule = *it;
|
|
if (std::equal_to<executable::Header>()(liveModule->GetImageHeader(), imageHeader))
|
|
{
|
|
liveModule->Unload();
|
|
delete liveModule;
|
|
|
|
it = commandThread->m_liveModules.erase(it);
|
|
}
|
|
else
|
|
{
|
|
++it;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
bool ServerCommandThread::ApplySettingBoolAction::Execute(CommandType* command, const DuplexPipe* pipe, void*)
|
|
{
|
|
pipe->SendAck();
|
|
|
|
appSettings::ApplySettingBool(command->settingName, (command->settingValue == 0) ? false : true);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool ServerCommandThread::ApplySettingIntAction::Execute(CommandType* command, const DuplexPipe* pipe, void*)
|
|
{
|
|
pipe->SendAck();
|
|
|
|
appSettings::ApplySettingInt(command->settingName, command->settingValue);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool ServerCommandThread::ApplySettingStringAction::Execute(CommandType* command, const DuplexPipe* pipe, void*)
|
|
{
|
|
pipe->SendAck();
|
|
|
|
appSettings::ApplySettingString(command->settingName, command->settingValue);
|
|
|
|
return true;
|
|
}
|