Files
UnrealEngineUWP/Engine/Source/Developer/Windows/LiveCodingServer/Private/External/LC_ServerCommandThread.cpp
ben marsh 8360edbfd1 LiveCoding: More fixes for object reconstruction when reapplying Live Coding patches. Compiland IDs are now assigned from a central location, which can be queried at any time. The unity file mapping output from UBT is read on demand and used to populate the cache.
Also fix an issue where symbols in anonymous namespace would not use the correct name if an external build system is enabled.

#rb none
#jira UE-79095
#rnx

#ROBOMERGE-SOURCE: CL 8358810 in //UE4/Release-4.23/...
#ROBOMERGE-BOT: RELEASE (Release-4.23 -> Main) (v401-8057353)

[CL 8358816 by ben marsh in Main branch]
2019-08-27 21:09:33 -04:00

1867 lines
61 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 "Containers/UnrealString.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 "LC_DuplexPipeClient.h"
#include "LC_MemoryStream.h"
#include "LC_NamedSharedMemory.h"
#include "LC_VisualStudioAutomation.h"
#include <mmsystem.h>
// unreachable code
#pragma warning (disable : 4702)
// BEGIN EPIC MODS
#pragma warning(push)
#pragma warning(disable:6031) // warning C6031: Return value ignored: 'CoInitialize'.
// END EPIC MODS
namespace
{
static telemetry::Accumulator g_loadedModuleSize("Module size");
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_manualRecompileTriggered(false)
, m_liveModuleToModifiedOrNewObjFiles()
, m_restartCS()
, m_restartJob(nullptr)
, m_restartedProcessCount(0u)
#if WITH_VISUALSTUDIO_DTE
, m_restartedProcessIdToDebugger()
#endif
{
#if WITH_VISUALSTUDIO_DTE
visualStudio::Startup();
#endif
m_serverThread = thread::Create("Live coding server", 64u * 1024u, &ServerCommandThread::ServerThread, this);
m_compileThread = thread::Create("Live coding compilation", 64u * 1024u, &ServerCommandThread::CompileThread, 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 almost *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.
delete m_directoryCache;
#if WITH_VISUALSTUDIO_DTE
visualStudio::Shutdown();
#endif
}
void ServerCommandThread::RestartTargets(void)
{
// protect against concurrent compilation
m_restartCS.Enter();
// EPIC REMOVED: g_theApp.GetMainFrame()->SetBusy(true);
// EPIC REMOVED: g_theApp.GetMainFrame()->ChangeStatusBarText(L"Restarting target applications...");
LC_LOG_USER("---------- Restarting target applications ----------");
// prevent current Live++ instance from shutting down by associating it with a new job object to keep it alive
if (!m_restartJob)
{
m_restartJob = ::CreateJobObjectW(NULL, primitiveNames::JobGroup(m_processGroupName).c_str());
::AssignProcessToJobObject(m_restartJob, ::GetCurrentProcess());
}
// protect against m_liveProcesses being accessed when processes restart and register themselves with this Live++ instance
CriticalSection::ScopedLock lock(&m_actionCS);
// remove processes that were successfully restarted last time
for (auto processIt = m_liveProcesses.begin(); processIt != m_liveProcesses.end(); /* nothing */)
{
LiveProcess* liveProcess = *processIt;
if (liveProcess->WasSuccessfulRestart())
{
process::Handle processHandle = liveProcess->GetProcessHandle();
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
{
++processIt;
}
}
// try preparing all processes for a restart
const size_t count = m_liveProcesses.size();
for (size_t i = 0u; i < count; ++i)
{
LiveProcess* liveProcess = m_liveProcesses[i];
const bool success = liveProcess->PrepareForRestart();
if (success)
{
++m_restartedProcessCount;
#if WITH_VISUALSTUDIO_DTE
// check if a VS debugger is currently attached to the process about to restart
const unsigned int processId = liveProcess->GetProcessId();
EnvDTE::DebuggerPtr debugger = visualStudio::FindDebuggerAttachedToProcess(processId);
if (debugger)
{
m_restartedProcessIdToDebugger.emplace(processId, debugger);
}
#endif
}
}
// restart all successfully prepared processes
for (size_t i = 0u; i < count; ++i)
{
LiveProcess* liveProcess = m_liveProcesses[i];
liveProcess->Restart(m_restartJob);
}
// BEGIN EPIC MOD - Prevent orphaned console instances if processes fail to restart. Job object will be duplicated into child process.
if (m_restartJob != nullptr)
{
CloseHandle(m_restartJob);
m_restartJob = nullptr;
}
// END EPIC MOD
}
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());
}
scheduler::Task<LiveModule*>* ServerCommandThread::LoadModule(unsigned int processId, void* moduleBase, const wchar_t* givenModulePath, scheduler::TaskBase* taskRoot)
{
// note that the path we get from the client 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(givenModulePath);
const executable::Header imageHeader = GetImageHeader(modulePath.c_str());
if (!executable::IsValidHeader(imageHeader))
{
return nullptr;
}
LiveProcess* liveProcess = FindProcessById(processId);
LC_ASSERT(liveProcess, "Invalid process ID.");
if (liveProcess->TriedToLoadImage(imageHeader))
{
// tried loading this module into this process already
return nullptr;
}
// find any other process ID that tried to load this module already
{
const size_t count = m_liveProcesses.size();
for (size_t i = 0u; i < count; ++i)
{
LiveProcess* otherLiveProcess = 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(), processId);
LiveModule* liveModule = m_imageHeaderToLiveModule[imageHeader];
if (liveModule)
{
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 nullptr;
}
}
}
symbols::Provider* moduleProvider = symbols::OpenEXE(modulePath.c_str(), symbols::OpenOptions::ACCUMULATE_SIZE);
if (!moduleProvider)
{
return nullptr;
}
liveProcess->AddLoadedImage(imageHeader);
// accumulate module info
{
const file::Attributes attributes = file::GetAttributes(modulePath.c_str());
const uint64_t size = file::GetSize(attributes);
g_loadedModuleSize.Accumulate(size);
g_loadedModuleSize.Print();
g_loadedModuleSize.ResetCurrent();
LC_LOG_USER("Loading module %S (%.3f MB)", modulePath.c_str(), size / 1048576.0f);
}
// create a task to load the module of this batch concurrently
LiveModule* liveModule = new LiveModule(modulePath.c_str(), imageHeader, m_runMode);
m_imageHeaderToLiveModule.emplace(imageHeader, liveModule);
auto task = scheduler::CreateTask(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);
return task;
}
bool ServerCommandThread::UnloadModule(unsigned int processId, const wchar_t* givenModulePath)
{
// note that the path we get from the client 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(givenModulePath);
const executable::Header imageHeader = GetImageHeader(modulePath.c_str());
if (!executable::IsValidHeader(imageHeader))
{
return false;
}
LiveProcess* liveProcess = FindProcessById(processId);
LC_ASSERT(liveProcess, "Invalid process ID.");
if (!liveProcess->TriedToLoadImage(imageHeader))
{
// this module was never loaded
return false;
}
LC_LOG_USER("Unloading module %S", modulePath.c_str());
liveProcess->RemoveLoadedImage(imageHeader);
m_imageHeaderToLiveModule.erase(imageHeader);
for (auto it = m_liveModules.begin(); it != m_liveModules.end(); /* nothing */)
{
LiveModule* liveModule = *it;
if (std::equal_to<executable::Header>()(liveModule->GetImageHeader(), imageHeader))
{
liveModule->Unload();
delete liveModule;
it = m_liveModules.erase(it);
return true;
}
else
{
++it;
}
}
return false;
}
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);
if (uniquePaths.size() != 0u)
{
LC_SUCCESS_USER("Prewarmed compiler/linker environment cache (%.3fs, %zu executables)", scope.ReadSeconds(), uniquePaths.size());
}
}
unsigned int ServerCommandThread::ServerThread(void)
{
// keep named shared memory alive so that restarted processes don't try spawning new Live++ instances
NamedSharedMemory sharedMemory(primitiveNames::StartupNamedSharedMemory(m_processGroupName).c_str());
sharedMemory.Write(::GetCurrentProcessId());
// 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->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("Live coding client command communication", 64u * 1024u, &ServerCommandThread::CommandThread, this, &context->pipe, context->readyEvent);
context->exceptionCommandThread = thread::Create("Live coding client exception command communication", 64u * 1024u, &ServerCommandThread::ExceptionCommandThread, this, &context->exceptionPipe);
// register this connection
{
CriticalSection::ScopedLock lock(&m_connectionCS);
m_commandThreads.push_back(context);
}
}
return 0u;
}
// BEGIN EPIC MOD - Focus application windows on patch complete
BOOL CALLBACK FocusApplicationWindows(HWND WindowHandle, LPARAM Lparam)
{
DWORD WindowProcessId;
GetWindowThreadProcessId(WindowHandle, &WindowProcessId);
const types::vector<LiveProcess*>& Processes = *(const types::vector<LiveProcess*>*)Lparam;
for (LiveProcess* Process : Processes)
{
if (Process->GetProcessId() == WindowProcessId && IsWindowVisible(WindowHandle))
{
SetForegroundWindow(WindowHandle);
}
}
return Windows::TRUE;
}
// END EPIC MOD
// BEGIN EPIC MOD - Support for lazy-loading modules
bool ServerCommandThread::actions::FinishedLazyLoadingModules::Execute(const CommandType* command, const DuplexPipe* pipe, void*, const void*, size_t)
{
pipe->SendAck();
return false;
}
struct ClientProxyThread
{
struct ProxyEnableModulesFinishedAction
{
typedef commands::EnableModulesFinished CommandType;
static bool Execute(CommandType* command, const DuplexPipe* pipe, void*, const void*, size_t)
{
pipe->SendAck();
return false;
}
};
LiveProcess* m_process;
DuplexPipeClient* m_pipe;
std::vector<std::wstring> m_enableModules;
thread::Handle m_threadHandle;
ClientProxyThread(LiveProcess* process, DuplexPipeClient* pipe, const std::vector<std::wstring> enableModules)
: m_process(process)
, m_pipe(pipe)
, m_enableModules(enableModules)
{
m_threadHandle = thread::Create(64u * 1024u, &StaticEntryPoint, this);
thread::SetName("Live coding client proxy");
}
~ClientProxyThread()
{
thread::Join(m_threadHandle);
thread::Close(m_threadHandle);
}
static unsigned int __stdcall StaticEntryPoint(void* context)
{
static_cast<ClientProxyThread*>(context)->EntryPoint();
return 0;
}
void EntryPoint()
{
std::vector<commands::ModuleData> modules;
modules.resize(m_enableModules.size());
for (size_t Idx = 0; Idx < m_enableModules.size(); Idx++)
{
commands::ModuleData& module = modules[Idx];
module.base = m_process->GetLazyLoadedModuleBase(m_enableModules[Idx].c_str());
wcscpy_s(module.path, m_enableModules[Idx].c_str());
}
commands::EnableModules enableModulesCommand;
enableModulesCommand.processId = m_process->GetProcessId();
enableModulesCommand.moduleCount = m_enableModules.size();
enableModulesCommand.token = nullptr;
m_pipe->SendCommandAndWaitForAck(enableModulesCommand, modules.data(), modules.size() * sizeof(commands::ModuleData));
CommandMap commandMap;
commandMap.RegisterAction<ProxyEnableModulesFinishedAction>();
commandMap.HandleCommands(m_pipe, m_process);
m_pipe->SendCommandAndWaitForAck(commands::FinishedLazyLoadingModules(), nullptr, 0);
}
};
bool ServerCommandThread::EnableRequiredModules(const TArray<FString>& RequiredModules)
{
bool bEnabledModule = false;
for (LiveProcess* liveProcess : m_liveProcesses)
{
types::vector<std::wstring> LoadModuleFileNames;
for (const FString& RequiredModule : RequiredModules)
{
std::wstring ModuleFileName = file::NormalizePath(*RequiredModule);
if (liveProcess->IsPendingLazyLoadedModule(ModuleFileName))
{
LoadModuleFileNames.push_back(ModuleFileName);
}
}
if (LoadModuleFileNames.size() > 0)
{
const std::wstring PipeName = primitiveNames::Pipe(m_processGroupName + L"_ClientProxy");
DuplexPipeServer ServerPipe;
ServerPipe.Create(PipeName.c_str());
DuplexPipeClient ClientPipe;
ClientPipe.Connect(PipeName.c_str());
ClientProxyThread ClientThread(liveProcess, &ClientPipe, LoadModuleFileNames);
CommandMap commandMap;
commandMap.RegisterAction<actions::EnableModules>();
commandMap.RegisterAction<actions::FinishedLazyLoadingModules>();
commandMap.HandleCommands(&ServerPipe, this);
for (const std::wstring& loadModuleFileName : LoadModuleFileNames)
{
liveProcess->SetLazyLoadedModuleAsLoaded(loadModuleFileName);
}
bEnabledModule = true;
}
}
return bEnabledModule;
}
// END EPIC MOD
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...");
telemetry::Scope scope("Creating patch");
// EPIC REMOVED: g_theApp.GetMainFrame()->OnCompilationStart();
LC_LOG_USER("---------- Creating patch ----------");
// BEGIN EPIC MOD - Hook for the compiler
GLiveCodingServer->GetCompileStartedDelegate().ExecuteIfBound();
const ILiveCodingServer::FCompileDelegate& CompileDelegate = GLiveCodingServer->GetCompileDelegate();
if (CompileDelegate.IsBound())
{
// Get the list of arguments for building each target, and use the delegate to pass them to UBT
TArray<FString> Targets;
for (LiveProcess* liveProcess : m_liveProcesses)
{
Targets.Add(liveProcess->GetBuildArguments());
}
GLiveCodingServer->GetStatusChangeDelegate().ExecuteIfBound(L"Compiling changes for live coding...");
// Keep retrying the compile until we've added all the required modules
TMap<FString, TArray<FString>> ModuleToObjectFiles;
for (;;)
{
// Build a list of modules which are enabled for live coding
TArray<FString> ValidModules;
for (LiveModule* liveModule : m_liveModules)
{
ValidModules.Add(liveModule->GetModuleName().c_str());
}
// Execute the compile
TArray<FString> RequiredModules;
if (CompileDelegate.Execute(Targets, ValidModules, RequiredModules, ModuleToObjectFiles))
{
break;
}
// Enable any lazy-loaded modules that we need
if (!EnableRequiredModules(RequiredModules))
{
GLiveCodingServer->GetCompileFinishedDelegate().ExecuteIfBound(ELiveCodingResult::Error, L"Compilation error.");
return;
}
}
// Reset the unity file cache
symbols::ResetCachedUnityManifests();
// Build up a list of all the modified object files in each module
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())
{
// We couldn't find this exact module filename, but this could be a staged executable. See if we can just match the name.
std::wstring ModuleFileNameOnly = file::GetFilename(ModuleFileName);
bool bFoundNameMatch = false;
for (const LiveModule* liveModule : m_liveModules)
{
if (ModuleFileNameOnly == file::GetFilename(liveModule->GetModuleName()))
{
ModuleFileName = liveModule->GetModuleName();
bFoundNameMatch = true;
break;
}
}
if (!bFoundNameMatch)
{
LC_ERROR_USER("Live coding is not enabled for %S.", ModuleFileName.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"), ModuleFileName.c_str()));
return;
}
}
types::vector<LiveModule::ModifiedObjFile> ObjectFiles;
for(const FString& ObjectFile : Pair.Value)
{
std::wstring NormalizedObjectFile = file::NormalizePath(*ObjectFile);
// If this file has a .lc.obj suffix, temporarily replace the original .obj file while generating the patch.
// It'd be nice to track this explicitly inside Live++ and just load the new file, but it requires a lot of changes and would make upgrades difficult.
static const TCHAR Suffix[] = TEXT(".lc.obj");
static const size_t SuffixLen = ARRAY_COUNT(Suffix) - 1;
if (NormalizedObjectFile.length() >= SuffixLen && _wcsicmp(NormalizedObjectFile.c_str() + NormalizedObjectFile.length() - SuffixLen, Suffix) == 0)
{
// Get the original filename
std::wstring OriginalObjectFile(NormalizedObjectFile.c_str(), NormalizedObjectFile.c_str() + NormalizedObjectFile.length() - SuffixLen);
OriginalObjectFile += L".obj";
// Back up the original file, if it exists
file::Attributes OriginalFileAttributes = file::GetAttributes(OriginalObjectFile.c_str());
if (file::DoesExist(OriginalFileAttributes))
{
std::wstring OriginalObjectFileBackup = OriginalObjectFile + L".lctmp";
m_restoreFiles.push_back(std::make_pair(OriginalObjectFileBackup, OriginalObjectFile));
file::DeleteIfExists(OriginalObjectFileBackup.c_str());
file::Move(OriginalObjectFile.c_str(), OriginalObjectFileBackup.c_str());
}
// Move the new file into place
m_restoreFiles.push_back(std::make_pair(OriginalObjectFile, NormalizedObjectFile));
file::Move(NormalizedObjectFile.c_str(), OriginalObjectFile.c_str());
NormalizedObjectFile = OriginalObjectFile;
}
// Add the file to the list of modifications
LiveModule::ModifiedObjFile ModifiedObjFile;
ModifiedObjFile.objPath = NormalizedObjectFile;
ObjectFiles.push_back(std::move(ModifiedObjFile));
}
m_liveModuleToModifiedOrNewObjFiles.insert(std::make_pair(ModuleFileName, std::move(ObjectFiles)));
}
}
GLiveCodingServer->GetStatusChangeDelegate().ExecuteIfBound(L"Creating patch...");
// END EPIC MOD
// 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 types::vector<LiveModule::ModifiedObjFile>& objFiles = objFilesIt->second;
moduleUpdateError = liveModule->Update(&fileCache, m_directoryCache, updateType, objFiles);
}
}
else
{
// no optional .objs were given, update all live modules regularly
types::vector<LiveModule::ModifiedObjFile> 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.");
EnumWindows(FocusApplicationWindows, (LPARAM)&m_liveProcesses);
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 (;;)
{
// protect against concurrent restarts
m_restartCS.Enter();
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)
{
// not all processes made progress.
// this usually means that at least one of them is currently being debugged.
// let each process handle this.
const size_t processCount = m_liveProcesses.size();
for (size_t i = 0u; i < processCount; ++i)
{
LiveProcess* liveProcess = m_liveProcesses[i];
liveProcess->HandleDebuggingPreCompile();
}
// 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.
{
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);
// BEGIN EPIC MOD - Non-destructive compile
for (std::vector<std::pair<std::wstring, std::wstring>>::reverse_iterator it = m_restoreFiles.rbegin(); it != m_restoreFiles.rend(); it++)
{
file::DeleteIfExists(it->second.c_str());
file::Move(it->first.c_str(), it->second.c_str());
}
m_restoreFiles.clear();
// END EPIC MOD
RemoveVirtualDrive();
if (!didAllProcessesMakeProgress)
{
const size_t processCount = m_liveProcesses.size();
for (size_t i = 0u; i < processCount; ++i)
{
LiveProcess* liveProcess = m_liveProcesses[i];
liveProcess->HandleDebuggingPostCompile();
}
// 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();
}
m_restartCS.Leave();
thread::Sleep(10u);
}
return 0u;
}
unsigned int ServerCommandThread::CommandThread(DuplexPipeServer* pipe, Event* readyEvent)
{
// handle incoming commands
CommandMap commandMap;
commandMap.RegisterAction<actions::TriggerRecompile>();
commandMap.RegisterAction<actions::LogMessage>();
commandMap.RegisterAction<actions::BuildPatch>();
commandMap.RegisterAction<actions::ReadyForCompilation>();
commandMap.RegisterAction<actions::DisconnectClient>();
commandMap.RegisterAction<actions::RegisterProcess>();
commandMap.RegisterAction<actions::EnableModules>();
commandMap.RegisterAction<actions::DisableModules>();
commandMap.RegisterAction<actions::ApplySettingBool>();
commandMap.RegisterAction<actions::ApplySettingInt>();
commandMap.RegisterAction<actions::ApplySettingString>();
// BEGIN EPIC MOD - Adding ShowConsole command
commandMap.RegisterAction<actions::ShowConsole>();
// END EPIC MOD
// BEGIN EPIC MOD - Adding SetVisible command
commandMap.RegisterAction<actions::SetVisible>();
// END EPIC MOD
// BEGIN EPIC MOD - Adding SetActive command
commandMap.RegisterAction<actions::SetActive>();
// END EPIC MOD
// BEGIN EPIC MOD - Adding SetBuildArguments command
commandMap.RegisterAction<actions::SetBuildArguments>();
// END EPIC MOD
// BEGIN EPIC MOD - Support for lazy-loading modules
commandMap.RegisterAction<actions::EnableLazyLoadedModule>();
// END EPIC MOD
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 {}, nullptr, 0u);
}
RemoveCommandThread(pipe);
return 0u;
}
unsigned int ServerCommandThread::ExceptionCommandThread(DuplexPipeServer* exceptionPipe)
{
// handle incoming exception commands
CommandMap commandMap;
commandMap.RegisterAction<actions::HandleException>();
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::actions::TriggerRecompile::Execute(const CommandType*, const DuplexPipe* pipe, void* context, const void*, size_t)
{
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::actions::LogMessage::Execute(const CommandType*, const DuplexPipe* pipe, void*, const void* payload, size_t)
{
LC_LOG_USER("%S", static_cast<const wchar_t*>(payload));
pipe->SendAck();
return true;
}
bool ServerCommandThread::actions::BuildPatch::Execute(const CommandType* command, const DuplexPipe* pipe, void* context, const void* payload, size_t payloadSize)
{
pipe->SendAck();
ServerCommandThread* commandThread = static_cast<ServerCommandThread*>(context);
// protect against accepting this command while compilation is already in progress
CriticalSection::ScopedLock lock(&commandThread->m_actionCS);
memoryStream::Reader payloadStream(payload, payloadSize);
for (unsigned int i = 0u; i < command->fileCount; ++i)
{
const commands::BuildPatch::PatchData patchData = payloadStream.Read<commands::BuildPatch::PatchData>();
const LiveModule::ModifiedObjFile modifiedObjFile = { patchData.objPath, patchData.amalgamatedObjPath };
commandThread->m_liveModuleToModifiedOrNewObjFiles[patchData.moduleName].push_back(modifiedObjFile);
}
commandThread->m_manualRecompileTriggered = true;
return true;
}
bool ServerCommandThread::actions::HandleException::Execute(const CommandType* command, const DuplexPipe* pipe, void* context, const void*, size_t)
{
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 }, nullptr, 0u);
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 }, nullptr, 0u);
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 }, nullptr, 0u);
return true;
}
// signal client that we handled the exception and there's nothing left to do
pipe->SendCommandAndWaitForAck(commands::HandleExceptionFinished { nullptr, nullptr, nullptr, true }, nullptr, 0u);
#endif
// END EPIC MOD
return true;
}
bool ServerCommandThread::actions::ReadyForCompilation::Execute(const CommandType*, const DuplexPipe* pipe, void*, const void*, size_t)
{
pipe->SendAck();
// don't continue execution
return false;
}
bool ServerCommandThread::actions::DisconnectClient::Execute(const CommandType*, const DuplexPipe* pipe, void* context, const void*, size_t)
{
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::actions::ShowConsole::Execute(const CommandType*, const DuplexPipe* pipe, void* context, const void*, size_t)
{
pipe->SendAck();
GLiveCodingServer->GetShowConsoleDelegate().ExecuteIfBound();
return true;
}
// END EPIC MOD
// BEGIN EPIC MOD - Adding SetVisible command
bool ServerCommandThread::actions::SetVisible::Execute(const CommandType* command, const DuplexPipe* pipe, void* context, const void*, size_t)
{
pipe->SendAck();
GLiveCodingServer->GetSetVisibleDelegate().ExecuteIfBound(command->visible);
return true;
}
// END EPIC MOD
// BEGIN EPIC MOD - Adding SetActive command
bool ServerCommandThread::actions::SetActive::Execute(const CommandType* command, const DuplexPipe* pipe, void* context, const void*, size_t)
{
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::actions::SetBuildArguments::Execute(const CommandType* command, const DuplexPipe* pipe, void* context, const void*, size_t)
{
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
// BEGIN EPIC MOD - Support for lazy-loading modules
bool ServerCommandThread::actions::EnableLazyLoadedModule::Execute(const CommandType* command, const DuplexPipe* pipe, void* context, const void*, size_t)
{
ServerCommandThread* commandThread = static_cast<ServerCommandThread*>(context);
// protect against accepting this command while compilation is already in progress
CriticalSection::ScopedLock lock(&commandThread->m_actionCS);
// Check if this module is already enabled - it may have been lazy-loaded, then fully loaded, by a restarted process. If so, translate this into a call to EnableModules.
const std::wstring modulePath = file::NormalizePath(command->fileName);
for (LiveModule* module : commandThread->m_liveModules)
{
if(module->GetModuleName() == modulePath)
{
EnableModules::CommandType EnableCmd = { };
EnableCmd.moduleCount = 1;
EnableCmd.processId = command->processId;
EnableCmd.token = command->token;
commands::ModuleData Module;
Module.base = command->moduleBase;
wcscpy_s(Module.path, command->fileName);
return EnableModules::Execute(&EnableCmd, pipe, context, &Module, sizeof(Module));
}
}
// Acknowledge the command
pipe->SendAck();
// Register the module for lazy loading
for (LiveProcess* process : commandThread->m_liveProcesses)
{
if (process->GetProcessId() == command->processId)
{
process->AddLazyLoadedModule(modulePath, command->moduleBase);
LC_LOG_DEV("Registered module %S for lazy-loading", modulePath.c_str());
}
}
// Tell the client we're done
pipe->SendCommandAndWaitForAck(commands::EnableModulesFinished { command->token }, nullptr, 0u);
return true;
}
// END EPIC MOD
bool ServerCommandThread::actions::RegisterProcess::Execute(const CommandType* command, const DuplexPipe* pipe, void* context, const void* payload, size_t)
{
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)
{
const wchar_t* imagePath = pointer::Offset<const wchar_t*>(payload, 0u);
const wchar_t* commandLine = pointer::Offset<const wchar_t*>(imagePath, command->imagePathSize);
const wchar_t* workingDirectory = pointer::Offset<const wchar_t*>(commandLine, command->commandLineSize);
const void* environment = pointer::Offset<const wchar_t*>(workingDirectory, command->workingDirectorySize);
LiveProcess* liveProcess = new LiveProcess(processHandle, command->processId, command->threadId, command->jumpToSelf, pipe, imagePath, commandLine, workingDirectory, environment, command->environmentSize);
commandThread->m_liveProcesses.push_back(liveProcess);
// BEGIN EPIC MOD - No built-in UI
// commandThread->m_mainFrame->UpdateWindowTitle();
// END EPIC MOD
if (command->restartedProcessId == 0u)
{
// this is a new process
LC_SUCCESS_USER("Registered process %S (PID: %d)", processPath.c_str(), command->processId);
}
else
{
// this process was restarted
LC_SUCCESS_USER("Registered restarted process %S (PID: %d, previous PID: %d)", processPath.c_str(), command->processId, command->restartedProcessId);
#if WITH_VISUALSTUDIO_DTE
// reattach the debugger in case the previous process had a debugger attached
{
auto it = commandThread->m_restartedProcessIdToDebugger.find(command->restartedProcessId);
if (it != commandThread->m_restartedProcessIdToDebugger.end())
{
LC_LOG_USER("Reattaching debugger to PID %d", command->processId);
const EnvDTE::DebuggerPtr& debugger = it->second;
const bool success = visualStudio::AttachToProcess(debugger, command->processId);
if (!success)
{
LC_ERROR_USER("Failed to reattach debugger to PID %d", command->processId);
}
commandThread->m_restartedProcessIdToDebugger.erase(it);
}
}
#endif
--commandThread->m_restartedProcessCount;
if (commandThread->m_restartedProcessCount == 0u)
{
// finished restarting, remove the job that kept this instance alive
if (commandThread->m_restartJob)
{
::CloseHandle(commandThread->m_restartJob);
commandThread->m_restartJob = nullptr;
commandThread->m_restartCS.Leave();
LC_LOG_USER("---------- Restarting finished ----------");
// EPIC REMOVED: g_theApp.GetMainFrame()->ResetStatusBarText();
// EPIC REMOVED: g_theApp.GetMainFrame()->SetBusy(false);
}
// BEGIN EPIC MOD - Prevent orphaned console instances if processes fail to restart. Job object will be duplicated into child process.
else
{
commandThread->m_restartCS.Leave();
LC_LOG_USER("---------- Restarting finished ----------");
}
// END EPIC MOD
}
}
}
// tell client we are finished
pipe->SendCommandAndWaitForAck(commands::RegisterProcessFinished { registeredSuccessfully }, nullptr, 0u);
}
return true;
}
bool ServerCommandThread::actions::EnableModules::Execute(const CommandType* command, const DuplexPipe* pipe, void* context, const void* payload, size_t payloadSize)
{
pipe->SendAck();
// EPIC REMOVED: g_theApp.GetMainFrame()->ChangeStatusBarText(L"Loading modules...");
ServerCommandThread* commandThread = static_cast<ServerCommandThread*>(context);
// protect against several client DLLs calling into this action at the same time.
// this ensures that all modules are loaded serialized per process.
CriticalSection::ScopedLock lock(&commandThread->m_actionCS);
telemetry::Scope moduleLoadingScope("Module loading");
// set up virtual drives before loading anything, otherwise files won't be detected and therefore discarded
AddVirtualDrive();
scheduler::TaskBase* rootTask = scheduler::CreateEmptyTask();
types::vector<scheduler::Task<LiveModule*>*> loadModuleTasks;
const unsigned int moduleCount = command->moduleCount;
loadModuleTasks.reserve(moduleCount);
memoryStream::Reader payloadStream(payload, payloadSize);
for (unsigned int i = 0u; i < moduleCount; ++i)
{
const commands::ModuleData moduleData = payloadStream.Read<commands::ModuleData>();
scheduler::Task<LiveModule*>* task = commandThread->LoadModule(command->processId, moduleData.base, moduleData.path, rootTask);
// the module could have failed to load
if (task)
{
loadModuleTasks.push_back(task);
}
}
// wait for all tasks to finish
scheduler::RunTask(rootTask);
scheduler::WaitForTask(rootTask);
const size_t loadModuleTaskCount = loadModuleTasks.size();
commandThread->m_liveModules.reserve(loadModuleTaskCount);
size_t loadedTranslationUnits = 0u;
// update all live modules loaded by the tasks
for (size_t i = 0u; i < loadModuleTaskCount; ++i)
{
scheduler::Task<LiveModule*>* task = loadModuleTasks[i];
LiveModule* liveModule = task->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
loadedTranslationUnits += liveModule->GetCompilandDatabase()->compilands.size();
}
scheduler::DestroyTasks(loadModuleTasks);
scheduler::DestroyTask(rootTask);
// dump memory statistics
{
LC_LOG_INDENT_TELEMETRY;
g_symbolAllocator.PrintStats();
g_immutableStringAllocator.PrintStats();
g_contributionAllocator.PrintStats();
g_compilandAllocator.PrintStats();
g_dependencyAllocator.PrintStats();
}
// BEGIN EPIC MOD - Suppress output when lazy loading modules
if (commandThread->m_compileThread == nullptr || thread::GetId() != thread::GetId(commandThread->m_compileThread))
{
if (loadModuleTaskCount > 0u)
{
LC_SUCCESS_USER("Loaded %zu module(s) (%.3fs, %zu translation units)", loadModuleTaskCount, moduleLoadingScope.ReadSeconds(), loadedTranslationUnits);
}
// EPIC REMOVED commandThread->PrewarmCompilerEnvironmentCache();
// tell user we are ready, but only once to not clutter the log
{
static bool showedOnce = false;
if (!showedOnce)
{
showedOnce = true;
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());
}
}
}
// END EPIC MOD
// remove virtual drives once we're finished
RemoveVirtualDrive();
// tell server we are finished
pipe->SendCommandAndWaitForAck(commands::EnableModulesFinished { command->token }, nullptr, 0u);
// EPIC REMOVED: g_theApp.GetMainFrame()->ResetStatusBarText();
return true;
}
bool ServerCommandThread::actions::DisableModules::Execute(const CommandType* command, const DuplexPipe* pipe, void* context, const void* payload, size_t payloadSize)
{
pipe->SendAck();
// EPIC REMOVED: g_theApp.GetMainFrame()->ChangeStatusBarText(L"Unloading modules...");
ServerCommandThread* commandThread = static_cast<ServerCommandThread*>(context);
// protect against several client DLLs calling into this action at the same time.
// this ensures that all modules are loaded serialized per process.
CriticalSection::ScopedLock lock(&commandThread->m_actionCS);
telemetry::Scope moduleUnloadingScope("Module unloading");
unsigned int unloadedModules = 0u;
const unsigned int moduleCount = command->moduleCount;
memoryStream::Reader payloadStream(payload, payloadSize);
for (unsigned int i = 0u; i < moduleCount; ++i)
{
const commands::ModuleData moduleData = payloadStream.Read<commands::ModuleData>();
const bool success = commandThread->UnloadModule(command->processId, moduleData.path);
if (success)
{
++unloadedModules;
}
}
if (unloadedModules > 0u)
{
LC_SUCCESS_USER("Unloaded %u module(s) (%.3fs)", unloadedModules, moduleUnloadingScope.ReadSeconds());
}
// tell server we are finished
pipe->SendCommandAndWaitForAck(commands::DisableModulesFinished { command->token }, nullptr, 0u);
// EPIC REMOVED: g_theApp.GetMainFrame()->ResetStatusBarText();
return true;
}
bool ServerCommandThread::actions::ApplySettingBool::Execute(const CommandType* command, const DuplexPipe* pipe, void*, const void*, size_t)
{
pipe->SendAck();
appSettings::ApplySettingBool(command->settingName, (command->settingValue == 0) ? false : true);
return true;
}
bool ServerCommandThread::actions::ApplySettingInt::Execute(const CommandType* command, const DuplexPipe* pipe, void*, const void*, size_t)
{
pipe->SendAck();
appSettings::ApplySettingInt(command->settingName, command->settingValue);
return true;
}
bool ServerCommandThread::actions::ApplySettingString::Execute(const CommandType* command, const DuplexPipe* pipe, void*, const void*, size_t)
{
pipe->SendAck();
appSettings::ApplySettingString(command->settingName, command->settingValue);
return true;
}
// BEGIN EPIC MODS
#pragma warning(pop)
// END EPIC MODS