// Copyright 2011-2020 Molecular Matters GmbH, all rights reserved. #if LC_VERSION == 1 // BEGIN EPIC MOD //#include PCH_INCLUDE // END EPIC MOD #include "LC_ServerCommandThread.h" #include "LC_Commands.h" // BEGIN EPIC MOD //#include "LC_MainFrame.h" // END EPIC MOD #include "LC_Telemetry.h" #include "LC_Symbols.h" #include "LC_Filesystem.h" #include "LC_Process.h" #include "LC_Compiler.h" #include "LC_StringUtil.h" #include "LC_CommandMap.h" #include "LC_FileAttributeCache.h" // BEGIN EPIC MOD //#include "LC_App.h" // END EPIC MOD #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" // BEGIN EPIC MOD //#include "LC_ExceptionHandlerDialog.h" //#include "LC_Resource.h" // END EPIC MOD #include "LC_PrimitiveNames.h" #include "LC_MemoryStream.h" #include "LC_NamedSharedMemory.h" #include "LC_VisualStudioAutomation.h" #include "LC_Thread.h" #include // BEGIN EPIC MOD #include "LC_AppSettings.h" #include "LC_Allocators.h" #include "LC_DuplexPipeClient.h" #include "LiveCodingServer.h" #include "Containers/UnrealString.h" // END EPIC MOD // 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, Filesystem::OpenMode::READ); 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() // BEGIN EPIC MOD , m_liveModuleToAdditionalLibraries() // END EPIC MOD , m_restartCS() , m_restartJob(nullptr) , m_restartedProcessCount(0u) // BEGIN EPIC MOD #if WITH_VISUALSTUDIO_DTE // END EPIC MOD , m_restartedProcessIdToDebugger() // BEGIN EPIC MOD #endif // END EPIC MOD { // BEGIN EPIC MOD #if WITH_VISUALSTUDIO_DTE // END EPIC MOD visualStudio::Startup(); // BEGIN EPIC MOD #endif // END EPIC MOD // BEGIN EPIC MOD m_serverThread = Thread::CreateFromMemberFunction("Live coding server", 64u * 1024u, this, &ServerCommandThread::ServerThread); m_compileThread = Thread::CreateFromMemberFunction("Live coding compilation", 64u * 1024u, this, &ServerCommandThread::CompileThread); // END EPIC MOD 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; // BEGIN EPIC MOD #if WITH_VISUALSTUDIO_DTE // END EPIC MOD visualStudio::Shutdown(); // BEGIN EPIC MOD #endif // END EPIC MOD } 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); } // BEGIN EPIC MOD if (liveProcess->IsReinstancingFlowEnabled()) { --m_reinstancingProcessCount; } if (liveProcess->IsDisableCompileFinishNotification()) { --m_disableCompileFinishNotificationProcessCount; } // END EPIC MOD 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; // BEGIN EPIC MOD #if WITH_VISUALSTUDIO_DTE // END EPIC MOD // check if a VS debugger is currently attached to the process about to restart const Process::Id processId = liveProcess->GetProcessId(); EnvDTE::DebuggerPtr debugger = visualStudio::FindDebuggerAttachedToProcess(processId); if (debugger) { m_restartedProcessIdToDebugger.emplace(+processId, debugger); } // BEGIN EPIC MOD #endif // END EPIC MOD } } // exit all successfully prepared processes for (size_t i = 0u; i < count; ++i) { LiveProcess* liveProcess = m_liveProcesses[i]; liveProcess->WaitForExitBeforeRestart(); } // restart all successfully prepared processes for (size_t i = 0u; i < count; ++i) { LiveProcess* liveProcess = m_liveProcesses[i]; // BEGIN EPIC MOD liveProcess->Restart(m_restartJob); // END EPIC MOD } // 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 } void ServerCommandThread::CompileChanges(void) { // protect against accepting this command while compilation is already in progress CriticalSection::ScopedLock lock(&m_actionCS); m_manualRecompileTriggered = true; } 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()).GetString(); } scheduler::Task* ServerCommandThread::LoadModule(Process::Id 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 = Filesystem::NormalizePath(givenModulePath).GetString(); 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 Filesystem::PathAttributes attributes = Filesystem::GetAttributes(modulePath.c_str()); const uint64_t size = Filesystem::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(Process::Id 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 = Filesystem::NormalizePath(givenModulePath).GetString(); 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()(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 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()); } } Thread::ReturnValue ServerCommandThread::ServerThread(void) { // keep named shared memory alive so that restarted processes don't try spawning new Live++ instances Process::NamedSharedMemory* sharedMemory = Process::CreateNamedSharedMemory(primitiveNames::StartupNamedSharedMemory(m_processGroupName).c_str(), 4096u); Process::WriteNamedSharedMemory(sharedMemory, ::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 // BEGIN EPIC MOD context->commandThread = Thread::CreateFromMemberFunction("Live coding client command communication", 64u * 1024u, this, &ServerCommandThread::CommandThread, &context->pipe, context->readyEvent); context->exceptionCommandThread = Thread::CreateFromMemberFunction("Live coding client exception command communication", 64u * 1024u, this, &ServerCommandThread::ExceptionCommandThread, &context->exceptionPipe); // END EPIC MOD // register this connection { CriticalSection::ScopedLock lock(&m_connectionCS); m_commandThreads.push_back(context); } } Process::DestroyNamedSharedMemory(sharedMemory); return Thread::ReturnValue(0u); } // BEGIN EPIC MOD bool ServerCommandThread::HasReinstancingProcess() { return m_reinstancingProcessCount.load() != 0; } // END EPIC MOD // BEGIN EPIC MOD bool ServerCommandThread::ShowCompileFinishNotification() { return m_disableCompileFinishNotificationProcessCount.load() == 0; } // END EPIC MOD // BEGIN EPIC MOD - Focus application windows on patch complete BOOL CALLBACK FocusApplicationWindows(HWND WindowHandle, LPARAM Lparam) { DWORD WindowProcessId; GetWindowThreadProcessId(WindowHandle, &WindowProcessId); const types::vector& Processes = *(const types::vector*)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 m_enableModules; Thread::Handle m_threadHandle; ClientProxyThread(LiveProcess* process, DuplexPipeClient* pipe, const std::vector enableModules) : m_process(process) , m_pipe(pipe) , m_enableModules(enableModules) { m_threadHandle = Thread::Create(64u * 1024u, &StaticEntryPoint, this); Thread::Current::SetName("Live coding client proxy"); } ~ClientProxyThread() { Thread::Join(m_threadHandle); Thread::Close(m_threadHandle); } static unsigned int __stdcall StaticEntryPoint(void* context) { static_cast(context)->EntryPoint(); return 0; } void EntryPoint() { std::vector 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(); commandMap.HandleCommands(m_pipe, m_process); m_pipe->SendCommandAndWaitForAck(commands::FinishedLazyLoadingModules(), nullptr, 0); } }; bool ServerCommandThread::EnableRequiredModules(const TArray& RequiredModules) { bool bEnabledModule = false; for (LiveProcess* liveProcess : m_liveProcesses) { types::vector LoadModuleFileNames; for (const FString& RequiredModule : RequiredModules) { std::wstring ModuleFileName = Filesystem::NormalizePath(*RequiredModule).GetString(); 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(); commandMap.RegisterAction(); commandMap.HandleCommands(&ServerPipe, this); for (const std::wstring& loadModuleFileName : LoadModuleFileNames) { liveProcess->SetLazyLoadedModuleAsLoaded(loadModuleFileName); } bEnabledModule = true; } } return bEnabledModule; } // END EPIC MOD // BEGIN EPIC MOD void ServerCommandThread::CompileChanges(bool didAllProcessesMakeProgress, commands::PostCompileResult& postCompileResult) // END EPIC MOD { // 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 Targets; for (LiveProcess* liveProcess : m_liveProcesses) { Targets.AddUnique(liveProcess->GetBuildArguments()); } GLiveCodingServer->GetStatusChangeDelegate().ExecuteIfBound(L"Compiling changes for live coding..."); // Keep retrying the compile until we've added all the required modules FModuleToModuleFiles ModuleToModuleFiles; ELiveCodingCompileReason CompileReason = ELiveCodingCompileReason::Initial; for (;;) { // Build a list of modules which are enabled for live coding TArray ValidModules; for (LiveModule* liveModule : m_liveModules) { ValidModules.Add(liveModule->GetModuleName().c_str()); } // Build a list of loaded modules which are not enabled TSet LazyLoadModules; for (LiveProcess* liveProcess : m_liveProcesses) { const std::unordered_map& lazyLoadedModules = liveProcess->GetLazyLoadedModules(); for (const std::unordered_map::value_type& kvp : lazyLoadedModules) { if (!kvp.second.m_loaded) { LazyLoadModules.Add(FString(kvp.first.c_str())); } } } // Execute the compile TArray RequiredModules; ELiveCodingCompileResult CompileResult = CompileDelegate.Execute(Targets, ValidModules, LazyLoadModules, RequiredModules, ModuleToModuleFiles, CompileReason); if (CompileResult == ELiveCodingCompileResult::Success) { break; } else if (CompileResult == ELiveCodingCompileResult::Canceled) { postCompileResult = commands::PostCompileResult::Cancelled; GLiveCodingServer->GetCompileFinishedDelegate().ExecuteIfBound(ELiveCodingResult::Error, L"Compilation canceled."); return; } else if (CompileResult == ELiveCodingCompileResult::Failure) { postCompileResult = commands::PostCompileResult::Failure; GLiveCodingServer->GetCompileFinishedDelegate().ExecuteIfBound(ELiveCodingResult::Error, L"Compilation error."); return; } // Enable any lazy-loaded modules that we need if (!RequiredModules.IsEmpty() && !EnableRequiredModules(RequiredModules)) { postCompileResult = commands::PostCompileResult::Failure; GLiveCodingServer->GetCompileFinishedDelegate().ExecuteIfBound(ELiveCodingResult::Error, L"Compilation error."); return; } CompileReason = ELiveCodingCompileReason::Retry; } // Reset the unity file cache symbols::ResetCachedUnityManifests(); // Build up a list of all the modified object files in each module types::unordered_map EnabledModulesByName; for (const LiveModule* liveModule : m_liveModules) { EnabledModulesByName[liveModule->GetModuleName()] = liveModule; } for(const TPair& Pair : ModuleToModuleFiles) { const LiveModule* liveModule = nullptr; std::wstring ModuleFileName = Filesystem::Devirtualize(Filesystem::NormalizePath(*Pair.Key).GetString()); types::unordered_map::iterator ix = EnabledModulesByName.find(ModuleFileName); if (ix == EnabledModulesByName.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 = Filesystem::GetFilename(ModuleFileName.c_str()).GetString(); for (const LiveModule* testLiveModule : m_liveModules) { if (ModuleFileNameOnly == Filesystem::GetFilename(testLiveModule->GetModuleName().c_str()).GetString()) { ModuleFileName = testLiveModule->GetModuleName(); liveModule = testLiveModule; break; } } if (liveModule == nullptr) { LC_WARNING_USER("The module '%S' has not been loaded by any process. Changes will be ignored.", ModuleFileName.c_str()); continue; } } else { liveModule = ix->second; } types::vector ObjectFiles; for (const FString& ObjectFile : Pair.Value.Objects) { std::wstring NormalizedObjectFile = Filesystem::NormalizePath(*ObjectFile).GetString(); if (!liveModule->IsModifiedSource(Filesystem::Devirtualize(NormalizedObjectFile).c_str())) { continue; } // 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 = UE_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 Filesystem::PathAttributes OriginalFileAttributes = Filesystem::GetAttributes(OriginalObjectFile.c_str()); if (Filesystem::DoesExist(OriginalFileAttributes)) { std::wstring OriginalObjectFileBackup = OriginalObjectFile + L".lctmp"; m_restoreFiles.push_back(std::make_pair(OriginalObjectFileBackup, OriginalObjectFile)); Filesystem::DeleteIfExists(OriginalObjectFileBackup.c_str()); Filesystem::Move(OriginalObjectFile.c_str(), OriginalObjectFileBackup.c_str()); } // Move the new file into place m_restoreFiles.push_back(std::make_pair(OriginalObjectFile, NormalizedObjectFile)); Filesystem::Move(NormalizedObjectFile.c_str(), OriginalObjectFile.c_str()); NormalizedObjectFile = OriginalObjectFile; } // Add the file to the list of modifications symbols::ModifiedObjFile ModifiedObjFile; ModifiedObjFile.objPath = NormalizedObjectFile; ObjectFiles.push_back(std::move(ModifiedObjFile)); } m_liveModuleToModifiedOrNewObjFiles.insert(std::make_pair(ModuleFileName, std::move(ObjectFiles))); types::vector AdditionalLibraries; for (const FString& ObjectFile : Pair.Value.Libraries) { AdditionalLibraries.push_back(Filesystem::NormalizePath(*ObjectFile).GetString()); } m_liveModuleToAdditionalLibraries.insert(std::make_pair(ModuleFileName, std::move(AdditionalLibraries))); } } 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; // BEGIN EPIC MOD // For Epic, don't even bother calling update if we have nothing that has been reported as modified/new. // This prevents the automatic change detection code from running which has issues with unity file changes. static const types::vector emptyLibFiles; for (size_t i = 0u; i < count; ++i) { LiveModule* liveModule = m_liveModules[i]; // 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() || objFilesIt->second.empty()) { // no .objs for this module, ignore continue; } const auto libFilesIt = m_liveModuleToAdditionalLibraries.find(liveModule->GetModuleName()); const types::vector& libFiles = libFilesIt != m_liveModuleToAdditionalLibraries.end() ? libFilesIt->second : emptyLibFiles; // build a patch with the given list of .objs for this module const types::vector& objFiles = objFilesIt->second; LiveModule::ErrorType::Enum moduleUpdateError = liveModule->Update(&fileCache, m_directoryCache, updateType, objFiles, libFiles); // 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; } } // END EPIC MOD // 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) { // BEGIN EPIC MOD GLiveCodingServer->GetBringToFrontDelegate().ExecuteIfBound(); // END EPIC MOD } // 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) { // BEGIN EPIC MOD GLiveCodingServer->GetBringToFrontDelegate().ExecuteIfBound(); // END EPIC MOD } // 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 bool setFocus = false; switch (updateError) { case LiveModule::ErrorType::NO_CHANGE: postCompileResult = commands::PostCompileResult::NoChanges; GLiveCodingServer->GetCompileFinishedDelegate().ExecuteIfBound(ELiveCodingResult::Success, L"No changes detected."); setFocus = !ShowCompileFinishNotification(); break; case LiveModule::ErrorType::COMPILE_ERROR: postCompileResult = commands::PostCompileResult::Failure; GLiveCodingServer->GetCompileFinishedDelegate().ExecuteIfBound(ELiveCodingResult::Error, L"Compilation error."); break; case LiveModule::ErrorType::LINK_ERROR: postCompileResult = commands::PostCompileResult::Failure; GLiveCodingServer->GetCompileFinishedDelegate().ExecuteIfBound(ELiveCodingResult::Error, L"Linker error."); break; case LiveModule::ErrorType::LOAD_PATCH_ERROR: postCompileResult = commands::PostCompileResult::Failure; GLiveCodingServer->GetCompileFinishedDelegate().ExecuteIfBound(ELiveCodingResult::Error, L"Could not load patch image."); break; case LiveModule::ErrorType::ACTIVATE_PATCH_ERROR: postCompileResult = commands::PostCompileResult::Failure; GLiveCodingServer->GetCompileFinishedDelegate().ExecuteIfBound(ELiveCodingResult::Error, L"Could not activate patch."); break; case LiveModule::ErrorType::SUCCESS: postCompileResult = commands::PostCompileResult::Success; GLiveCodingServer->GetCompileFinishedDelegate().ExecuteIfBound(ELiveCodingResult::Success, L"Patch creation successful."); setFocus = true; break; default: postCompileResult = commands::PostCompileResult::Success; GLiveCodingServer->GetCompileFinishedDelegate().ExecuteIfBound(ELiveCodingResult::Success, L"Finished."); break; } if (setFocus) { EnumWindows(FocusApplicationWindows, (LPARAM)&m_liveProcesses); } // END EPIC MOD LC_LOG_USER("---------- Finished (%.3fs) ----------", scope.ReadSeconds()); // EPIC REMOVED: g_theApp.GetMainFrame()->ResetStatusBarText(); // EPIC REMOVED: g_theApp.GetMainFrame()->SetBusy(false); } // BEGIN EPIC MOD - Add the ability for pre and post compile notifications void ServerCommandThread::CallPrecompileHooks(bool didAllProcessesMakeProgress) { // 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; if (updateType == LiveModule::UpdateType::NO_CLIENT_COMMUNICATION) { return; } const size_t count = m_liveProcesses.size(); for (size_t i = 0u; i < count; ++i) { LiveProcess* liveProcess = m_liveProcesses[i]; liveProcess->GetPipe()->SendCommandAndWaitForAck(commands::PreCompile{}, nullptr, 0u); } } void ServerCommandThread::CallPostcompileHooks(bool didAllProcessesMakeProgress, commands::PostCompileResult postCompileResult) { // 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; if (updateType == LiveModule::UpdateType::NO_CLIENT_COMMUNICATION) { return; } const size_t count = m_liveProcesses.size(); for (size_t i = 0u; i < count; ++i) { LiveProcess* liveProcess = m_liveProcesses[i]; liveProcess->GetPipe()->SendCommandAndWaitForAck(commands::PostCompile{ postCompileResult }, nullptr, 0u); } } // END EPIC MOD Thread::ReturnValue 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(appSettings::g_continuousCompilationTimeout->GetValue()); const bool foundAnyModification = changeNotification.CheckOnce(); if (isShortcutPressed || foundAnyModification || m_manualRecompileTriggered) { // clear the log if desired by the user if (appSettings::g_clearLogOnRecompile->GetValue()) { // BEGIN EPIC MOD GLiveCodingServer->GetClearOutputDelegate().ExecuteIfBound(); // END EPIC MOD } if (foundAnyModification) { LC_SUCCESS_USER("Detected file modification, re-checking until timeout (%d ms)", changeNotificationTimeout); changeNotification.CheckNext(changeNotificationTimeout); } else if (isShortcutPressed) { // BEGIN EPIC MOD LC_SUCCESS_USER("Accepted Live coding shortcut"); // END EPIC MDO } else if (m_manualRecompileTriggered) { LC_SUCCESS_USER("Manual recompile triggered"); } } 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); } // BEGIN EPIC MOD if (liveProcess->IsReinstancingFlowEnabled()) { --m_reinstancingProcessCount; } if (liveProcess->IsDisableCompileFinishNotification()) { --m_disableCompileFinishNotificationProcessCount; } // END EPIC MOD 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) { // BEGIN EPIC MOD LC_SUCCESS_USER("Possible debugger detected, can take a while to freeze threads"); LC_SUCCESS_USER("Do not continue the execution of the process until the threads have been thawed"); // END EPIC MOD // 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(); // bring Live++ to front on shortcut trigger if (appSettings::g_receiveFocusOnRecompile->GetValue() == appSettings::FocusOnRecompile::ON_SHORTCUT) { // BEGIN EPIC MOD GLiveCodingServer->GetBringToFrontDelegate().ExecuteIfBound(); // END EPIC MOD } // BEGIN EPIC MOD - Add the ability for pre and post compile notifications CallPrecompileHooks(didAllProcessesMakeProgress); // END EPIC MOD // BEGIN EPIC MOD commands::PostCompileResult postCompileResult = commands::PostCompileResult::Success; CompileChanges(didAllProcessesMakeProgress, postCompileResult); // END EPIC MOD // BEGIN EPIC MOD - Add the ability for pre and post compile notifications CallPostcompileHooks(didAllProcessesMakeProgress, postCompileResult); // END EPIC MOD // BEGIN EPIC MOD - Non-destructive compile for (std::vector>::reverse_iterator it = m_restoreFiles.rbegin(); it != m_restoreFiles.rend(); it++) { Filesystem::DeleteIfExists(it->second.c_str()); Filesystem::Move(it->first.c_str(), it->second.c_str()); } m_restoreFiles.clear(); // END EPIC MOD RemoveVirtualDrive(); if (!didAllProcessesMakeProgress) { // BEGIN EPIC MOD LC_SUCCESS_USER("Possible debugger detected, can take a while to thaw threads"); // END EPIC MOD 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(); // BEGIN EPIC MOD LC_SUCCESS_USER("Thawing process has completed. You may now resume process execution"); // END EPIC MOD } 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(); // BEGIN EPIC MOD m_liveModuleToAdditionalLibraries.clear(); // END EPIC MOD } m_restartCS.Leave(); Thread::Current::SleepMilliSeconds(10u); } return Thread::ReturnValue(0u); } Thread::ReturnValue ServerCommandThread::CommandThread(DuplexPipeServer* pipe, Event* readyEvent) { // handle incoming commands CommandMap commandMap; commandMap.RegisterAction(); commandMap.RegisterAction(); commandMap.RegisterAction(); commandMap.RegisterAction(); commandMap.RegisterAction(); commandMap.RegisterAction(); commandMap.RegisterAction(); commandMap.RegisterAction(); commandMap.RegisterAction(); commandMap.RegisterAction(); commandMap.RegisterAction(); commandMap.RegisterAction(); // BEGIN EPIC MOD - Adding ShowConsole command commandMap.RegisterAction(); // END EPIC MOD // BEGIN EPIC MOD - Adding SetVisible command commandMap.RegisterAction(); // END EPIC MOD // BEGIN EPIC MOD - Adding SetActive command commandMap.RegisterAction(); // END EPIC MOD // BEGIN EPIC MOD - Adding SetBuildArguments command commandMap.RegisterAction(); // END EPIC MOD // BEGIN EPIC MOD - Support for lazy-loading modules commandMap.RegisterAction(); // END EPIC MOD // BEGIN EPIC MOD commandMap.RegisterAction(); commandMap.RegisterAction(); // END EPIC MOD // BEGIN EPIC MOD commandMap.RegisterAction(); // 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 Thread::ReturnValue(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 Thread::ReturnValue(0u); } Thread::ReturnValue ServerCommandThread::ExceptionCommandThread(DuplexPipeServer* exceptionPipe) { // handle incoming exception commands CommandMap commandMap; commandMap.RegisterAction(); for (;;) { const bool success = commandMap.HandleCommands(exceptionPipe, this); if ((!success) || (!exceptionPipe->IsValid())) { // pipe was closed or is broken, bail out return Thread::ReturnValue(1u); } } return Thread::ReturnValue(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(Process::Id 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(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::TriggerRestart::Execute(const CommandType*, const DuplexPipe* pipe, void* context, const void*, size_t) { ServerCommandThread* commandThread = static_cast(context); // protect against accepting this command while compilation is already in progress CriticalSection::ScopedLock lock(&commandThread->m_actionCS); pipe->SendAck(); commandThread->RestartTargets(); return true; } bool ServerCommandThread::actions::LogMessage::Execute(const CommandType*, const DuplexPipe* pipe, void*, const void* payload, size_t) { LC_LOG_USER("%S", static_cast(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(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(); const symbols::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(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(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(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(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(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 = Filesystem::NormalizePath(command->fileName).GetString(); 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 // BEGIN EPIC MOD bool ServerCommandThread::actions::SetReinstancingFlow::Execute(const CommandType* command, const DuplexPipe* pipe, void* context, const void*, size_t) { ServerCommandThread* commandThread = static_cast(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) { if (command->enable) { if (!process->IsReinstancingFlowEnabled()) { ++commandThread->m_reinstancingProcessCount; } } else { if (process->IsReinstancingFlowEnabled()) { --commandThread->m_reinstancingProcessCount; } } process->SetReinstancingFlow(command->enable); } } pipe->SendAck(); return true; } // END EPIC MOD // BEGIN EPIC MOD bool ServerCommandThread::actions::DisableCompileFinishNotification::Execute(const CommandType* command, const DuplexPipe* pipe, void* context, const void*, size_t) { ServerCommandThread* commandThread = static_cast(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) { ++commandThread->m_disableCompileFinishNotificationProcessCount; process->DisableCompileFinishNotification(); } } pipe->SendAck(); 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(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).GetString(); 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()) { // BEGIN EPIC MOD std::wstring caption(L"Live coding - Registering process "); // END EPIC MOD caption += Filesystem::GetFilename(processPath.c_str()).GetString(); 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(payload, 0u); const wchar_t* commandLine = pointer::Offset(imagePath, command->imagePathSize); const wchar_t* workingDirectory = pointer::Offset(commandLine, command->commandLineSize); const void* environment = pointer::Offset(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 == Process::Id(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); // BEGIN EPIC MOD #if WITH_VISUALSTUDIO_DTE // END EPIC MOD // 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); } } // BEGIN EPIC MOD #endif // END EPIC MOD --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(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*> 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(); scheduler::Task* 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* 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::Current::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; } // BEGIN EPIC MOD bool ServerCommandThread::actions::EnableModulesEx::Execute(const CommandType* command, const DuplexPipe* pipe, void* context, const void* payload, size_t payloadSize) { pipe->SendAck(); ServerCommandThread* commandThread = static_cast(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 ex"); // Locate the process LiveProcess* liveProcess = nullptr; for (LiveProcess* process : commandThread->m_liveProcesses) { if (process->GetProcessId() == command->processId) { liveProcess = process; break; } } // set up virtual drives before loading anything, otherwise files won't be detected and therefore discarded AddVirtualDrive(); scheduler::TaskBase* rootTask = scheduler::CreateEmptyTask(); types::vector*> 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(); scheduler::Task* task = commandThread->LoadModule(command->processId, moduleData.base, moduleData.path, rootTask); // the module could have failed to load if (task) { loadModuleTasks.push_back(task); } } const unsigned int lazyModuleCount = command->lazyLoadModuleCount; unsigned int loadedLazyModules = 0; for (unsigned int i = 0u; i < lazyModuleCount; ++i) { const commands::ModuleData moduleData = payloadStream.Read(); // 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. bool isEnabled = false; const std::wstring modulePath = Filesystem::NormalizePath(moduleData.path).GetString(); for (LiveModule* module : commandThread->m_liveModules) { if (module->GetModuleName() == modulePath) { scheduler::Task* task = commandThread->LoadModule(command->processId, moduleData.base, moduleData.path, rootTask); if (task) { loadModuleTasks.push_back(task); } isEnabled = true; break; } } if (!isEnabled && liveProcess != nullptr) { liveProcess->AddLazyLoadedModule(modulePath, static_cast(moduleData.base)); LC_LOG_DEV("Registered module %S for lazy-loading", modulePath.c_str()); ++loadedLazyModules; } } const unsigned int reservedPagesCount = command->reservedPagesCount; for (unsigned int i = 0u; i < reservedPagesCount; ++i) { const uintptr_t page = payloadStream.Read(); if (liveProcess != nullptr) { liveProcess->AddPage(reinterpret_cast(page)); } } // 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* 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(); } if (+commandThread->m_compileThread == nullptr || Thread::Current::GetId() != Thread::GetId(commandThread->m_compileThread)) { if (loadModuleTaskCount > 0u) { LC_SUCCESS_USER("Loaded %zu module(s), %u lazy load module(s), and %u reserved page ranges (%.3fs, %zu translation units)", loadModuleTaskCount, loadedLazyModules, reservedPagesCount, moduleLoadingScope.ReadSeconds(), loadedTranslationUnits); } // 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()); } } } // remove virtual drives once we're finished RemoveVirtualDrive(); // tell server we are finished pipe->SendCommandAndWaitForAck(commands::EnableModulesFinished{ command->token }, nullptr, 0u); return true; } // END EPIC MOD 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(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(); 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 #endif