You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
Object files generated for Live Coding compiles are now temporarily renamed to their original versions until the Live Coding patch is generated, after which point they are restored. Would be preferable to incorporate this logic in Live++ directly and just read from the correct files, but the number of changes necessary to support it would make it difficult to integrate new versions. #rb none #jira UE-79365, UE-79095, FORT-198753 #ROBOMERGE-OWNER: ben.marsh #ROBOMERGE-AUTHOR: ben.marsh #ROBOMERGE-SOURCE: CL 8310434 via CL 8357056 #ROBOMERGE-BOT: (v401-8057353) [CL 8358588 by ben marsh in Main branch]
1864 lines
61 KiB
C++
1864 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;
|
|
}
|
|
}
|
|
|
|
// 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
|