/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2015 - ROLI Ltd. Permission is granted to use this software under the terms of either: a) the GPL v2 (or any later version) b) the Affero GPL v3 Details of these licenses can be found at: www.gnu.org/licenses JUCE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. ------------------------------------------------------------------------------ To release a closed-source product which uses JUCE, commercial licenses are available: visit www.juce.com for more information. ============================================================================== */ #include "../jucer_Headers.h" #include "../Application/jucer_Application.h" #include "../Project Saving/jucer_ProjectExporter.h" #include "projucer_MessageIDs.h" #include "projucer_CppHelpers.h" #include "projucer_SourceCodeRange.h" #include "projucer_ClassDatabase.h" #include "projucer_DiagnosticMessage.h" #include "projucer_ProjectBuildInfo.h" #include "projucer_ClientServerMessages.h" #include "projucer_CompileEngineClient.h" #include "../LiveBuildEngine/projucer_CompileEngineServer.h" #ifndef RUN_CLANG_IN_CHILD_PROCESS #error #endif //============================================================================== namespace ProjectProperties { const Identifier liveSettingsType ("LIVE_SETTINGS"); #if JUCE_MAC const Identifier liveSettingsSubtype ("OSX"); #elif JUCE_WINDOWS const Identifier liveSettingsSubtype ("WINDOWS"); #elif JUCE_LINUX const Identifier liveSettingsSubtype ("LINUX"); #endif static ValueTree getLiveSettings (Project& project) { return project.getProjectRoot().getOrCreateChildWithName (liveSettingsType, nullptr) .getOrCreateChildWithName (liveSettingsSubtype, nullptr); } static const ValueTree getLiveSettingsConst (Project& project) { return project.getProjectRoot().getChildWithName (liveSettingsType) .getChildWithName (liveSettingsSubtype); } static Value getLiveSetting (Project& p, const Identifier& i) { return getLiveSettings (p).getPropertyAsValue (i, p.getUndoManagerFor (getLiveSettings (p))); } static var getLiveSettingVar (Project& p, const Identifier& i) { return getLiveSettingsConst (p) [i]; } static Value getUserHeaderPathValue (Project& p) { return getLiveSetting (p, Ids::headerPath); } static String getUserHeaderPathString (Project& p) { return getLiveSettingVar (p, Ids::headerPath); } static Value getSystemHeaderPathValue (Project& p) { return getLiveSetting (p, Ids::systemHeaderPath); } static String getSystemHeaderPathString (Project& p) { return getLiveSettingVar (p, Ids::systemHeaderPath); } static Value getExtraDLLsValue (Project& p) { return getLiveSetting (p, Ids::extraDLLs); } static String getExtraDLLsString (Project& p) { return getLiveSettingVar (p, Ids::extraDLLs); } static Value getExtraCompilerFlagsValue (Project& p) { return getLiveSetting (p, Ids::extraCompilerFlags); } static String getExtraCompilerFlagsString (Project& p) { return getLiveSettingVar (p, Ids::extraCompilerFlags); } static Value getExtraPreprocessorDefsValue (Project& p) { return getLiveSetting (p, Ids::defines); } static String getExtraPreprocessorDefsString (Project& p) { return getLiveSettingVar (p, Ids::defines); } static File getProjucerTempFolder() { #if JUCE_MAC return File ("~/Library/Caches/com.juce.projucer"); #else return File::getSpecialLocation (File::tempDirectory).getChildFile ("com.juce.projucer"); #endif } static File getCacheLocation (Project& project) { String cacheFolderName = project.getProjectFilenameRoot() + "_" + project.getProjectUID(); #if JUCE_DEBUG cacheFolderName += "_debug"; #endif return getProjucerTempFolder() .getChildFile ("Intermediate Files") .getChildFile (cacheFolderName); } } //============================================================================== void LiveBuildProjectSettings::getLiveSettings (Project& project, PropertyListBuilder& props) { using namespace ProjectProperties; props.addSearchPathProperty (getUserHeaderPathValue (project), "User header paths", "User header search paths."); props.addSearchPathProperty (getSystemHeaderPathValue (project), "System header paths", "System header search paths."); props.add (new TextPropertyComponent (getExtraPreprocessorDefsValue (project), "Preprocessor Definitions", 32768, true), "Extra preprocessor definitions. Use the form \"NAME1=value NAME2=value\", using whitespace or commas " "to separate the items - to include a space or comma in a definition, precede it with a backslash."); props.add (new TextPropertyComponent (getExtraCompilerFlagsValue (project), "Extra compiler flags", 2048, true), "Extra command-line flags to be passed to the compiler. This string can contain references to preprocessor" " definitions in the form ${NAME_OF_DEFINITION}, which will be replaced with their values."); props.add (new TextPropertyComponent (getExtraDLLsValue (project), "Extra dynamic libraries", 2048, true), "Extra dynamic libs that the running code may require. Use new-lines or commas to separate the items"); } void LiveBuildProjectSettings::updateNewlyOpenedProject (Project&) { /* placeholder */ } bool LiveBuildProjectSettings::isBuildDisabled (Project& p) { const bool defaultBuildDisabled = true; return p.getStoredProperties().getBoolValue ("buildDisabled", defaultBuildDisabled); } void LiveBuildProjectSettings::setBuildDisabled (Project& p, bool b) { p.getStoredProperties().setValue ("buildDisabled", b); } bool LiveBuildProjectSettings::areWarningsDisabled (Project& p) { return p.getStoredProperties().getBoolValue ("warningsDisabled"); } void LiveBuildProjectSettings::setWarningsDisabled (Project& p, bool b) { p.getStoredProperties().setValue ("warningsDisabled", b); } //============================================================================== class ClientIPC : public MessageHandler, private InterprocessConnection, private Timer { public: ClientIPC (CompileEngineChildProcess& cp) : InterprocessConnection (true), owner (cp) { launchServer(); } ~ClientIPC() { #if RUN_CLANG_IN_CHILD_PROCESS if (childProcess.isRunning()) killServerPolitely(); #endif } void launchServer() { DBG ("Client: Launching Server..."); const String pipeName ("ipc_" + String::toHexString (Random().nextInt64())); const String command (createCommandLineForLaunchingServer (pipeName, owner.project.getProjectUID(), ProjectProperties::getCacheLocation (owner.project))); #if RUN_CLANG_IN_CHILD_PROCESS if (! childProcess.start (command)) { jassertfalse; } #else server = createClangServer (command); #endif bool ok = connectToPipe (pipeName, 10000); jassert (ok); if (ok) MessageTypes::sendPing (*this); startTimer (serverKeepAliveTimeout); } void killServerPolitely() { DBG ("Client: Killing Server..."); MessageTypes::sendQuit (*this); disconnect(); stopTimer(); #if RUN_CLANG_IN_CHILD_PROCESS childProcess.waitForProcessToFinish (5000); #endif killServerWithoutMercy(); } void killServerWithoutMercy() { disconnect(); stopTimer(); #if RUN_CLANG_IN_CHILD_PROCESS childProcess.kill(); #else destroyClangServer (server); server = nullptr; #endif } void connectionMade() { DBG ("Client: connected"); stopTimer(); } void connectionLost() { DBG ("Client: disconnected"); startTimer (100); } bool sendMessage (const ValueTree& m) { return InterprocessConnection::sendMessage (MessageHandler::convertMessage (m)); } void messageReceived (const MemoryBlock& message) { #if RUN_CLANG_IN_CHILD_PROCESS startTimer (serverKeepAliveTimeout); #else stopTimer(); #endif MessageTypes::dispatchToClient (owner, MessageHandler::convertMessage (message)); } enum { serverKeepAliveTimeout = 10000 }; private: CompileEngineChildProcess& owner; #if RUN_CLANG_IN_CHILD_PROCESS ChildProcess childProcess; #else void* server; #endif void timerCallback() { owner.handleCrash (String()); } }; //============================================================================== class CompileEngineChildProcess::ChildProcess : private ValueTree::Listener, private Timer { public: ChildProcess (CompileEngineChildProcess& proc, Project& p) : owner (proc), project (p) { projectRoot = project.getProjectRoot(); restartServer(); projectRoot.addListener (this); openedOk = true; } ~ChildProcess() { projectRoot.removeListener (this); if (isRunningApp && server != nullptr) server->killServerWithoutMercy(); server = nullptr; } void restartServer() { server = nullptr; server = new ClientIPC (owner); sendRebuild(); } void sendRebuild() { stopTimer(); ProjectBuildInfo build; if (! doesProjectMatchSavedHeaderState (project)) { MessageTypes::sendNewBuild (*server, build); owner.errorList.resetToError ("Project structure does not match the saved headers! " "Please re-save your project to enable compilation"); return; } if (areAnyModulesMissing (project)) { MessageTypes::sendNewBuild (*server, build); owner.errorList.resetToError ("Some of your JUCE modules can't be found! " "Please check that all the module paths are correct"); return; } build.setSystemIncludes (getSystemIncludePaths()); build.setUserIncludes (getUserIncludes()); build.setGlobalDefs (getGlobalDefs (project)); build.setCompileFlags (ProjectProperties::getExtraCompilerFlagsString (project).trim()); build.setExtraDLLs (getExtraDLLs()); build.setJuceModulesFolder (EnabledModuleList::findDefaultModulesFolder (project).getFullPathName()); build.setUtilsCppInclude (project.getAppIncludeFile().getFullPathName()); scanForProjectFiles (project, build); owner.updateAllEditors(); MessageTypes::sendNewBuild (*server, build); } void cleanAll() { MessageTypes::sendCleanAll (*server); sendRebuild(); } void reinstantiatePreviews() { MessageTypes::sendReinstantiate (*server); } bool launchApp() { MessageTypes::sendLaunchApp (*server); return true; } ScopedPointer server; bool openedOk = false; bool isRunningApp = false; private: CompileEngineChildProcess& owner; Project& project; ValueTree projectRoot; void projectStructureChanged() { startTimer (100); } void timerCallback() override { sendRebuild(); } void valueTreePropertyChanged (ValueTree&, const Identifier&) override { projectStructureChanged(); } void valueTreeChildAdded (ValueTree&, ValueTree&) override { projectStructureChanged(); } void valueTreeChildRemoved (ValueTree&, ValueTree&, int) override { projectStructureChanged(); } void valueTreeParentChanged (ValueTree&) override { projectStructureChanged(); } void valueTreeChildOrderChanged (ValueTree&, int, int) override {} static String getGlobalDefs (Project& proj) { String defs (ProjectProperties::getExtraPreprocessorDefsString (proj)); for (Project::ExporterIterator exporter (proj); exporter.next();) if (exporter->canLaunchProject()) defs << " " << exporter->getExporterIdentifierMacro() << "=1"; return defs; } static void scanProjectItem (const Project::Item& projectItem, Array& compileUnits, Array& userFiles) { if (projectItem.isGroup()) { for (int i = 0; i < projectItem.getNumChildren(); ++i) scanProjectItem (projectItem.getChild(i), compileUnits, userFiles); return; } if (projectItem.shouldBeCompiled()) { const File f (projectItem.getFile()); if (f.exists()) compileUnits.add (f); } if (projectItem.shouldBeAddedToTargetProject() && ! projectItem.shouldBeAddedToBinaryResources()) { const File f (projectItem.getFile()); if (f.exists()) userFiles.add (f); } } void scanForProjectFiles (Project& proj, ProjectBuildInfo& build) { Array compileUnits, userFiles; scanProjectItem (proj.getMainGroup(), compileUnits, userFiles); { OwnedArray modules; proj.getModules().createRequiredModules (modules); for (Project::ExporterIterator exporter (proj); exporter.next();) { if (exporter->canLaunchProject()) { for (const LibraryModule* m : modules) { const File localModuleFolder = proj.getModules().shouldCopyModuleFilesLocally (m->moduleInfo.getID()).getValue() ? proj.getLocalModuleFolder (m->moduleInfo.getID()) : m->moduleInfo.getFolder(); m->findAndAddCompiledUnits (*exporter, nullptr, compileUnits); } break; } } } for (int i = 0; ; ++i) { const File binaryDataCpp (proj.getBinaryDataCppFile (i)); if (! binaryDataCpp.exists()) break; compileUnits.add (binaryDataCpp); } for (int i = compileUnits.size(); --i >= 0;) if (compileUnits.getReference(i).hasFileExtension (".r")) compileUnits.remove (i); build.setFiles (compileUnits, userFiles); } static bool doesProjectMatchSavedHeaderState (Project& project) { ValueTree liveModules (project.getProjectRoot().getChildWithName (Ids::MODULES)); ScopedPointer xml (XmlDocument::parse (project.getFile())); if (xml == nullptr || ! xml->hasTagName (Ids::JUCERPROJECT.toString())) return false; ValueTree diskModules (ValueTree::fromXml (*xml).getChildWithName (Ids::MODULES)); return liveModules.isEquivalentTo (diskModules); } static bool areAnyModulesMissing (Project& project) { OwnedArray modules; project.getModules().createRequiredModules (modules); for (auto* module : modules) if (! module->getFolder().isDirectory()) return true; return false; } StringArray getUserIncludes() { StringArray paths; paths.add (project.getGeneratedCodeFolder().getFullPathName()); paths.addArray (getSearchPathsFromString (ProjectProperties::getUserHeaderPathString (project))); return convertSearchPathsToAbsolute (paths); } StringArray getSystemIncludePaths() { StringArray paths; paths.addArray (getSearchPathsFromString (ProjectProperties::getSystemHeaderPathString (project))); if (project.getProjectType().isAudioPlugin()) { paths.add (getAppSettings().getGlobalPath (Ids::vst3Path, TargetOS::getThisOS()).toString()); } OwnedArray modules; project.getModules().createRequiredModules (modules); for (auto* module : modules) paths.addIfNotAlreadyThere (module->getFolder().getParentDirectory().getFullPathName()); return convertSearchPathsToAbsolute (paths); } StringArray convertSearchPathsToAbsolute (const StringArray& paths) const { StringArray s; const File root (project.getProjectFolder()); for (String p : paths) s.add (root.getChildFile (p).getFullPathName()); return s; } StringArray getExtraDLLs() { StringArray dlls; dlls.addTokens (ProjectProperties::getExtraDLLsString (project), "\n\r,", StringRef()); dlls.trim(); dlls.removeEmptyStrings(); return dlls; } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ChildProcess) }; //============================================================================== CompileEngineChildProcess::CompileEngineChildProcess (Project& p) : project (p), continuousRebuild (false) { ProjucerApplication::getApp().openDocumentManager.addListener (this); createProcess(); errorList.setWarningsEnabled (! LiveBuildProjectSettings::areWarningsDisabled (project)); } CompileEngineChildProcess::~CompileEngineChildProcess() { ProjucerApplication::getApp().openDocumentManager.removeListener (this); process = nullptr; lastComponentList.clear(); } void CompileEngineChildProcess::createProcess() { jassert (process == nullptr); process = new ChildProcess (*this, project); if (! process->openedOk) process = nullptr; updateAllEditors(); } void CompileEngineChildProcess::cleanAll() { if (process != nullptr) process->cleanAll(); } void CompileEngineChildProcess::openPreview (const ClassDatabase::Class& comp) { if (process != nullptr) { MainWindow* projectWindow = nullptr; OwnedArray& windows = ProjucerApplication::getApp().mainWindowList.windows; for (int i = 0; i < windows.size(); ++i) { if (MainWindow* w = windows[i]) { if (w->getProject() == &project) { projectWindow = w; break; } } } Rectangle mainWindowRect; if (projectWindow != nullptr) mainWindowRect = projectWindow->getBounds(); MessageTypes::sendOpenPreview (*process->server, comp, mainWindowRect); } } void CompileEngineChildProcess::reinstantiatePreviews() { if (process != nullptr) process->reinstantiatePreviews(); } void CompileEngineChildProcess::processActivationChanged (bool isForeground) { if (process != nullptr) MessageTypes::sendProcessActivationState (*process->server, isForeground); } //============================================================================== bool CompileEngineChildProcess::canLaunchApp() const { return process != nullptr && runningAppProcess == nullptr && activityList.getNumActivities() == 0 && errorList.getNumErrors() == 0 && project.getProjectType().isGUIApplication(); } void CompileEngineChildProcess::launchApp() { if (process != nullptr) process->launchApp(); } bool CompileEngineChildProcess::canKillApp() const { return runningAppProcess != nullptr; } void CompileEngineChildProcess::killApp() { runningAppProcess = nullptr; } void CompileEngineChildProcess::handleAppLaunched() { runningAppProcess = process; runningAppProcess->isRunningApp = true; createProcess(); } void CompileEngineChildProcess::handleAppQuit() { DBG ("handleAppQuit"); runningAppProcess = nullptr; } //============================================================================== struct CompileEngineChildProcess::Editor : private CodeDocument::Listener, private Timer { Editor (CompileEngineChildProcess& ccp, const File& f, CodeDocument& doc) : owner (ccp), file (f), document (doc), transactionTimer (doc) { sendFullUpdate(); document.addListener (this); } ~Editor() { document.removeListener (this); } void codeDocumentTextInserted (const String& newText, int insertIndex) override { CodeChange (Range (insertIndex, insertIndex), newText).addToList (pendingChanges); startEditorChangeTimer(); transactionTimer.stopTimer(); owner.lastComponentList.globalNamespace .nudgeAllCodeRanges (file.getFullPathName(), insertIndex, newText.length()); } void codeDocumentTextDeleted (int start, int end) override { CodeChange (Range (start, end), String()).addToList (pendingChanges); startEditorChangeTimer(); transactionTimer.stopTimer(); owner.lastComponentList.globalNamespace .nudgeAllCodeRanges (file.getFullPathName(), start, start - end); } void sendFullUpdate() { reset(); if (owner.process != nullptr) MessageTypes::sendFileContentFullUpdate (*owner.process->server, file, document.getAllContent()); } bool flushEditorChanges() { if (pendingChanges.size() > 0) { if (owner.process != nullptr && owner.process->server != nullptr) MessageTypes::sendFileChanges (*owner.process->server, pendingChanges, file); reset(); return true; } stopTimer(); return false; } void reset() { stopTimer(); pendingChanges.clear(); } void startTransactionTimer() { transactionTimer.startTimer (1000); } void startEditorChangeTimer() { startTimer (200); } CompileEngineChildProcess& owner; File file; CodeDocument& document; private: Array pendingChanges; void timerCallback() override { if (owner.continuousRebuild) flushEditorChanges(); else stopTimer(); } struct TransactionTimer : public Timer { TransactionTimer (CodeDocument& doc) : document (doc) {} void timerCallback() override { stopTimer(); document.newTransaction(); } CodeDocument& document; }; TransactionTimer transactionTimer; }; void CompileEngineChildProcess::editorOpened (const File& file, CodeDocument& document) { editors.add (new Editor (*this, file, document)); } bool CompileEngineChildProcess::documentAboutToClose (OpenDocumentManager::Document* document) { for (int i = editors.size(); --i >= 0;) { if (document->getFile() == editors.getUnchecked(i)->file) { const File f (editors.getUnchecked(i)->file); editors.remove (i); if (process != nullptr) MessageTypes::sendHandleFileReset (*process->server, f); } } return true; } void CompileEngineChildProcess::updateAllEditors() { for (int i = editors.size(); --i >= 0;) editors.getUnchecked(i)->sendFullUpdate(); } //============================================================================== void CompileEngineChildProcess::handleCrash (const String& message) { Logger::writeToLog ("*** Child process crashed: " + message); if (crashHandler != nullptr) crashHandler (message); } void CompileEngineChildProcess::handleNewDiagnosticList (const ValueTree& l) { errorList.setList (l); } void CompileEngineChildProcess::handleActivityListChanged (const StringArray& l) { activityList.setList (l); } void CompileEngineChildProcess::handleCloseIDE() { if (JUCEApplication* app = JUCEApplication::getInstance()) app->systemRequestedQuit(); } void CompileEngineChildProcess::handleMissingSystemHeaders() { if (ProjectContentComponent* p = findProjectContentComponent()) p->handleMissingSystemHeaders(); } void CompileEngineChildProcess::handleKeyPress (const String& className, const KeyPress& key) { ApplicationCommandManager& commandManager = ProjucerApplication::getCommandManager(); CommandID command = commandManager.getKeyMappings()->findCommandForKeyPress (key); if (command == StandardApplicationCommandIDs::undo) { handleUndoInEditor (className); } else if (command == StandardApplicationCommandIDs::redo) { handleRedoInEditor (className); } else if (ApplicationCommandTarget* const target = ApplicationCommandManager::findTargetForComponent (findProjectContentComponent())) { commandManager.setFirstCommandTarget (target); commandManager.getKeyMappings()->keyPressed (key, findProjectContentComponent()); commandManager.setFirstCommandTarget (nullptr); } } void CompileEngineChildProcess::handleUndoInEditor (const String& /*className*/) { } void CompileEngineChildProcess::handleRedoInEditor (const String& /*className*/) { } void CompileEngineChildProcess::handleClassListChanged (const ValueTree& newList) { lastComponentList = ClassDatabase::ClassList::fromValueTree (newList); activityList.sendClassListChangedMessage (lastComponentList); } void CompileEngineChildProcess::handleBuildFailed() { if (errorList.getNumErrors() > 0) ProjucerApplication::getCommandManager().invokeDirectly (CommandIDs::showBuildTab, true); ProjucerApplication::getCommandManager().commandStatusChanged(); } void CompileEngineChildProcess::handleChangeCode (const SourceCodeRange& location, const String& newText) { if (Editor* ed = getOrOpenEditorFor (location.file)) { if (ed->flushEditorChanges()) return; // client-side editor changes were pending, so deal with them first, and discard // the incoming change, whose position may now be wrong. ed->document.deleteSection (location.range.getStart(), location.range.getEnd()); ed->document.insertText (location.range.getStart(), newText); // deliberately clear the messages that we just added, to avoid these changes being // sent to the server (which will already have processed the same ones locally) ed->reset(); ed->startTransactionTimer(); } } void CompileEngineChildProcess::handlePing() { } //============================================================================== void CompileEngineChildProcess::setContinuousRebuild (bool b) { continuousRebuild = b; } void CompileEngineChildProcess::flushEditorChanges() { for (Editor* ed : editors) ed->flushEditorChanges(); } ProjectContentComponent* CompileEngineChildProcess::findProjectContentComponent() const { for (MainWindow* mw : ProjucerApplication::getApp().mainWindowList.windows) if (mw->getProject() == &project) return mw->getProjectContentComponent(); return nullptr; } CompileEngineChildProcess::Editor* CompileEngineChildProcess::getOrOpenEditorFor (const File& file) { for (Editor* ed : editors) if (ed->file == file) return ed; if (ProjectContentComponent* pcc = findProjectContentComponent()) if (pcc->showEditorForFile (file, false)) return getOrOpenEditorFor (file); return nullptr; } void CompileEngineChildProcess::handleHighlightCode (const SourceCodeRange& location) { ProjectContentComponent* pcc = findProjectContentComponent(); if (pcc != nullptr && pcc->showEditorForFile (location.file, false)) { SourceCodeEditor* sce = dynamic_cast (pcc->getEditorComponent()); if (sce != nullptr && sce->editor != nullptr) { sce->highlight (location.range, true); Process::makeForegroundProcess(); CodeEditorComponent& ed = *sce->editor; ed.getTopLevelComponent()->toFront (false); ed.grabKeyboardFocus(); } } } void CompileEngineChildProcess::cleanAllCachedFilesForProject (Project& p) { File cacheFolder (ProjectProperties::getCacheLocation (p)); if (cacheFolder.isDirectory()) cacheFolder.deleteRecursively(); }