diff --git a/Help/manual/cmake-server.7.rst b/Help/manual/cmake-server.7.rst index f66212522..afd4e2b26 100644 --- a/Help/manual/cmake-server.7.rst +++ b/Help/manual/cmake-server.7.rst @@ -194,6 +194,49 @@ are of type "signal", have an empty "cookie" and "inReplyTo" field and always have a "name" set to show which signal was sent. +Specific Signals +---------------- + +The cmake server may sent signals with the following names: + +"dirty" Signal +^^^^^^^^^^^^^^ + +The "dirty" signal is sent whenever the server determines that the configuration +of the project is no longer up-to-date. This happens when any of the files that have +an influence on the build system is changed. + +The "dirty" signal may look like this:: + + [== CMake Server ==[ + { + "cookie":"", + "inReplyTo":"", + "name":"dirty", + "type":"signal"} + ]== CMake Server ==] + + +"fileChange" Signal +^^^^^^^^^^^^^^^^^^^ + +The "fileChange" signal is sent whenever a watched file is changed. It contains +the "path" that has changed and a list of "properties" with the kind of change +that was detected. Possible changes are "change" and "rename". + +The "fileChange" signal looks like this:: + + [== CMake Server ==[ + { + "cookie":"", + "inReplyTo":"", + "name":"fileChange", + "path":"/absolute/CMakeLists.txt", + "properties":["change"], + "type":"signal"} + ]== CMake Server ==] + + Specific Message Types ---------------------- @@ -635,3 +678,26 @@ CMake will respond with the following output:: The output can be limited to a list of keys by passing an array of key names to the "keys" optional field of the "cache" request. + + +Type "fileSystemWatchers" +^^^^^^^^^^^^^^^^^^^^^^^^^ + +The server can watch the filesystem for changes. The "fileSystemWatchers" +command will report on the files and directories watched. + +Example:: + + [== CMake Server ==] + {"type":"fileSystemWatchers"} + [== CMake Server ==] + +CMake will respond with the following output:: + + [== CMake Server ==] + { + "cookie":"","inReplyTo":"fileSystemWatchers","type":"reply", + "watchedFiles": [ "/absolute/path" ], + "watchedDirectories": [ "/absolute" ] + } + [== CMake Server ==] diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 264138171..ec49481e0 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -780,6 +780,7 @@ target_link_libraries(cmake CMakeLib) if(CMake_ENABLE_SERVER_MODE) add_library(CMakeServerLib + cmFileMonitor.cxx cmFileMonitor.h cmServer.cxx cmServer.h cmServerConnection.cxx cmServerConnection.h cmServerProtocol.cxx cmServerProtocol.h diff --git a/Source/cmFileMonitor.cxx b/Source/cmFileMonitor.cxx new file mode 100644 index 000000000..b97590b86 --- /dev/null +++ b/Source/cmFileMonitor.cxx @@ -0,0 +1,389 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#include "cmFileMonitor.h" + +#include + +#include +#include +#include +#include + +namespace { +void on_directory_change(uv_fs_event_t* handle, const char* filename, + int events, int status); +void on_handle_close(uv_handle_t* handle); +} // namespace + +class cmIBaseWatcher +{ +public: + cmIBaseWatcher() = default; + virtual ~cmIBaseWatcher() = default; + + virtual void Trigger(const std::string& pathSegment, int events, + int status) const = 0; + virtual std::string Path() const = 0; + virtual uv_loop_t* Loop() const = 0; + + virtual void StartWatching() = 0; + virtual void StopWatching() = 0; + + virtual std::vector WatchedFiles() const = 0; + virtual std::vector WatchedDirectories() const = 0; +}; + +class cmVirtualDirectoryWatcher : public cmIBaseWatcher +{ +public: + ~cmVirtualDirectoryWatcher() + { + for (auto i : this->Children) { + delete i.second; + } + } + + cmIBaseWatcher* Find(const std::string& ps) + { + const auto i = this->Children.find(ps); + return (i == this->Children.end()) ? nullptr : i->second; + } + + void Trigger(const std::string& pathSegment, int events, + int status) const final + { + if (pathSegment.empty()) { + for (const auto& i : this->Children) { + i.second->Trigger(std::string(), events, status); + } + } else { + const auto i = this->Children.find(pathSegment); + if (i != this->Children.end()) { + i->second->Trigger(std::string(), events, status); + } + } + } + + void StartWatching() override + { + for (const auto& i : this->Children) { + i.second->StartWatching(); + } + } + + void StopWatching() override + { + for (const auto& i : this->Children) { + i.second->StopWatching(); + } + } + + std::vector WatchedFiles() const final + { + std::vector result; + for (const auto& i : this->Children) { + for (const auto& j : i.second->WatchedFiles()) { + result.push_back(j); + } + } + return result; + } + + std::vector WatchedDirectories() const override + { + std::vector result; + for (const auto& i : this->Children) { + for (const auto& j : i.second->WatchedDirectories()) { + result.push_back(j); + } + } + return result; + } + + void Reset() + { + for (auto c : this->Children) { + delete c.second; + } + this->Children.clear(); + } + + void AddChildWatcher(const std::string& ps, cmIBaseWatcher* watcher) + { + assert(!ps.empty()); + assert(this->Children.find(ps) == this->Children.end()); + assert(watcher); + + this->Children.emplace(std::make_pair(ps, watcher)); + } + +private: + std::unordered_map Children; // owned! +}; + +// Root of all the different (on windows!) root directories: +class cmRootWatcher : public cmVirtualDirectoryWatcher +{ +public: + cmRootWatcher(uv_loop_t* loop) + : mLoop(loop) + { + assert(loop); + } + + std::string Path() const final + { + assert(false); + return std::string(); + } + uv_loop_t* Loop() const final { return this->mLoop; } + +private: + uv_loop_t* const mLoop; // no ownership! +}; + +// Real directories: +class cmRealDirectoryWatcher : public cmVirtualDirectoryWatcher +{ +public: + cmRealDirectoryWatcher(cmVirtualDirectoryWatcher* p, const std::string& ps) + : Parent(p) + , PathSegment(ps) + { + assert(p); + assert(!ps.empty()); + + p->AddChildWatcher(ps, this); + } + + ~cmRealDirectoryWatcher() + { + // Handle is freed via uv_handle_close callback! + } + + void StartWatching() final + { + if (!this->Handle) { + this->Handle = new uv_fs_event_t; + + uv_fs_event_init(this->Loop(), this->Handle); + this->Handle->data = this; + uv_fs_event_start(this->Handle, &on_directory_change, Path().c_str(), 0); + } + cmVirtualDirectoryWatcher::StartWatching(); + } + + void StopWatching() final + { + if (this->Handle) { + uv_fs_event_stop(this->Handle); + uv_close(reinterpret_cast(this->Handle), &on_handle_close); + this->Handle = nullptr; + } + cmVirtualDirectoryWatcher::StopWatching(); + } + + uv_loop_t* Loop() const final { return this->Parent->Loop(); } + + std::vector WatchedDirectories() const override + { + std::vector result = { Path() }; + for (const auto& j : cmVirtualDirectoryWatcher::WatchedDirectories()) { + result.push_back(j); + } + return result; + } + +protected: + cmVirtualDirectoryWatcher* const Parent; + const std::string PathSegment; + +private: + uv_fs_event_t* Handle = nullptr; // owner! +}; + +// Root directories: +class cmRootDirectoryWatcher : public cmRealDirectoryWatcher +{ +public: + cmRootDirectoryWatcher(cmRootWatcher* p, const std::string& ps) + : cmRealDirectoryWatcher(p, ps) + { + } + + std::string Path() const final { return this->PathSegment; } +}; + +// Normal directories below root: +class cmDirectoryWatcher : public cmRealDirectoryWatcher +{ +public: + cmDirectoryWatcher(cmRealDirectoryWatcher* p, const std::string& ps) + : cmRealDirectoryWatcher(p, ps) + { + } + + std::string Path() const final + { + return this->Parent->Path() + this->PathSegment + "/"; + } +}; + +class cmFileWatcher : public cmIBaseWatcher +{ +public: + cmFileWatcher(cmRealDirectoryWatcher* p, const std::string& ps, + cmFileMonitor::Callback cb) + : Parent(p) + , PathSegment(ps) + , CbList({ cb }) + { + assert(p); + assert(!ps.empty()); + p->AddChildWatcher(ps, this); + } + + void StartWatching() final {} + + void StopWatching() final {} + + void AppendCallback(cmFileMonitor::Callback cb) { CbList.push_back(cb); } + + std::string Path() const final + { + return this->Parent->Path() + this->PathSegment; + } + + std::vector WatchedDirectories() const final { return {}; } + + std::vector WatchedFiles() const final + { + return { this->Path() }; + } + + void Trigger(const std::string& ps, int events, int status) const final + { + assert(ps.empty()); + assert(status == 0); + static_cast(ps); + + const std::string path = this->Path(); + for (const auto& cb : this->CbList) { + cb(path, events, status); + } + } + + uv_loop_t* Loop() const final { return this->Parent->Loop(); } + +private: + cmRealDirectoryWatcher* Parent; + const std::string PathSegment; + std::vector CbList; +}; + +namespace { + +void on_directory_change(uv_fs_event_t* handle, const char* filename, + int events, int status) +{ + const cmIBaseWatcher* const watcher = + static_cast(handle->data); + const std::string pathSegment(filename); + watcher->Trigger(pathSegment, events, status); +} + +void on_handle_close(uv_handle_t* handle) +{ + delete (reinterpret_cast(handle)); +} + +} // namespace + +cmFileMonitor::cmFileMonitor(uv_loop_t* l) + : Root(new cmRootWatcher(l)) +{ +} + +cmFileMonitor::~cmFileMonitor() +{ + delete this->Root; +} + +void cmFileMonitor::MonitorPaths(const std::vector& paths, + Callback cb) +{ + for (const auto& p : paths) { + std::vector pathSegments; + cmsys::SystemTools::SplitPath(p, pathSegments, true); + + const size_t segmentCount = pathSegments.size(); + if (segmentCount < 2) { // Expect at least rootdir and filename + continue; + } + cmVirtualDirectoryWatcher* currentWatcher = this->Root; + for (size_t i = 0; i < segmentCount; ++i) { + assert(currentWatcher); + + const bool fileSegment = (i == segmentCount - 1); + const bool rootSegment = (i == 0); + assert( + !(fileSegment && + rootSegment)); // Can not be both filename and root part of the path! + + const std::string& currentSegment = pathSegments[i]; + + cmIBaseWatcher* nextWatcher = currentWatcher->Find(currentSegment); + if (!nextWatcher) { + if (rootSegment) { // Root part + assert(currentWatcher == this->Root); + nextWatcher = new cmRootDirectoryWatcher(this->Root, currentSegment); + assert(currentWatcher->Find(currentSegment) == nextWatcher); + } else if (fileSegment) { // File part + assert(currentWatcher != this->Root); + nextWatcher = new cmFileWatcher( + dynamic_cast(currentWatcher), + currentSegment, cb); + assert(currentWatcher->Find(currentSegment) == nextWatcher); + } else { // Any normal directory in between + nextWatcher = new cmDirectoryWatcher( + dynamic_cast(currentWatcher), + currentSegment); + assert(currentWatcher->Find(currentSegment) == nextWatcher); + } + } else { + if (fileSegment) { + auto filePtr = dynamic_cast(nextWatcher); + assert(filePtr); + filePtr->AppendCallback(cb); + continue; + } + } + currentWatcher = dynamic_cast(nextWatcher); + } + } + this->Root->StartWatching(); +} + +void cmFileMonitor::StopMonitoring() +{ + this->Root->StopWatching(); + this->Root->Reset(); +} + +std::vector cmFileMonitor::WatchedFiles() const +{ + std::vector result; + if (this->Root) { + result = this->Root->WatchedFiles(); + } + return result; +} + +std::vector cmFileMonitor::WatchedDirectories() const +{ + std::vector result; + if (this->Root) { + result = this->Root->WatchedDirectories(); + } + return result; +} diff --git a/Source/cmFileMonitor.h b/Source/cmFileMonitor.h new file mode 100644 index 000000000..e05f48dc4 --- /dev/null +++ b/Source/cmFileMonitor.h @@ -0,0 +1,28 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file Copyright.txt or https://cmake.org/licensing for details. */ +#pragma once + +#include +#include +#include + +#include "cm_uv.h" + +class cmRootWatcher; + +class cmFileMonitor +{ +public: + cmFileMonitor(uv_loop_t* l); + ~cmFileMonitor(); + + using Callback = std::function; + void MonitorPaths(const std::vector& paths, Callback cb); + void StopMonitoring(); + + std::vector WatchedFiles() const; + std::vector WatchedDirectories() const; + +private: + cmRootWatcher* Root; +}; diff --git a/Source/cmServer.cxx b/Source/cmServer.cxx index ba1bd9d7a..51a363f03 100644 --- a/Source/cmServer.cxx +++ b/Source/cmServer.cxx @@ -237,6 +237,11 @@ bool cmServer::Serve(std::string* errorMessage) return Connection->ProcessEvents(errorMessage); } +cmFileMonitor* cmServer::FileMonitor() const +{ + return Connection->FileMonitor(); +} + void cmServer::WriteJsonObject(const Json::Value& jsonValue, const DebugInfo* debug) const { diff --git a/Source/cmServer.h b/Source/cmServer.h index 796db8e66..7f29e3221 100644 --- a/Source/cmServer.h +++ b/Source/cmServer.h @@ -13,6 +13,7 @@ #include #include +class cmFileMonitor; class cmServerConnection; class cmServerProtocol; class cmServerRequest; @@ -28,6 +29,8 @@ public: bool Serve(std::string* errorMessage); + cmFileMonitor* FileMonitor() const; + private: void RegisterProtocol(cmServerProtocol* protocol); diff --git a/Source/cmServerConnection.cxx b/Source/cmServerConnection.cxx index 89ee6d8e5..c62ca3c3e 100644 --- a/Source/cmServerConnection.cxx +++ b/Source/cmServerConnection.cxx @@ -4,7 +4,8 @@ #include "cmServerDictionary.h" -#include +#include "cmFileMonitor.h" +#include "cmServer.h" #include @@ -64,10 +65,16 @@ public: : Connection(connection) { Connection->mLoop = uv_default_loop(); + if (Connection->mLoop) { + Connection->mFileMonitor = new cmFileMonitor(Connection->mLoop); + } } ~LoopGuard() { + if (Connection->mFileMonitor) { + delete Connection->mFileMonitor; + } uv_loop_close(Connection->mLoop); Connection->mLoop = nullptr; } diff --git a/Source/cmServerConnection.h b/Source/cmServerConnection.h index 16b1d5c01..78842e7cf 100644 --- a/Source/cmServerConnection.h +++ b/Source/cmServerConnection.h @@ -10,6 +10,7 @@ #endif class cmServer; +class cmFileMonitor; class LoopGuard; class cmServerConnection @@ -29,6 +30,8 @@ public: virtual void Connect(uv_stream_t* server) { (void)(server); } + cmFileMonitor* FileMonitor() const { return this->mFileMonitor; } + protected: virtual bool DoSetup(std::string* errorMessage) = 0; virtual void TearDown() = 0; @@ -46,6 +49,7 @@ protected: private: uv_loop_t* mLoop = nullptr; + cmFileMonitor* mFileMonitor = nullptr; cmServer* Server = nullptr; friend class LoopGuard; diff --git a/Source/cmServerDictionary.h b/Source/cmServerDictionary.h index c811b83af..c82274ab6 100644 --- a/Source/cmServerDictionary.h +++ b/Source/cmServerDictionary.h @@ -6,12 +6,16 @@ // Vocabulary: +static const std::string kDIRTY_SIGNAL = "dirty"; +static const std::string kFILE_CHANGE_SIGNAL = "fileChange"; + static const std::string kCACHE_TYPE = "cache"; static const std::string kCMAKE_INPUTS_TYPE = "cmakeInputs"; static const std::string kCODE_MODEL_TYPE = "codemodel"; static const std::string kCOMPUTE_TYPE = "compute"; static const std::string kCONFIGURE_TYPE = "configure"; static const std::string kERROR_TYPE = "error"; +static const std::string kFILESYSTEM_WATCHERS_TYPE = "fileSystemWatchers"; static const std::string kGLOBAL_SETTINGS_TYPE = "globalSettings"; static const std::string kHANDSHAKE_TYPE = "handshake"; static const std::string kMESSAGE_TYPE = "message"; @@ -80,6 +84,11 @@ static const std::string kVALUE_KEY = "value"; static const std::string kWARN_UNINITIALIZED_KEY = "warnUninitialized"; static const std::string kWARN_UNUSED_CLI_KEY = "warnUnusedCli"; static const std::string kWARN_UNUSED_KEY = "warnUnused"; +static const std::string kWATCHED_DIRECTORIES_KEY = "watchedDirectories"; +static const std::string kWATCHED_FILES_KEY = "watchedFiles"; static const std::string kSTART_MAGIC = "[== CMake Server ==["; static const std::string kEND_MAGIC = "]== CMake Server ==]"; + +static const std::string kRENAME_PROPERTY_VALUE = "rename"; +static const std::string kCHANGE_PROPERTY_VALUE = "change"; diff --git a/Source/cmServerProtocol.cxx b/Source/cmServerProtocol.cxx index f083e49fe..a2bdf49e2 100644 --- a/Source/cmServerProtocol.cxx +++ b/Source/cmServerProtocol.cxx @@ -4,6 +4,7 @@ #include "cmCacheManager.h" #include "cmExternalMakefileProjectGenerator.h" +#include "cmFileMonitor.h" #include "cmGeneratorTarget.h" #include "cmGlobalGenerator.h" #include "cmListFileCache.h" @@ -214,6 +215,11 @@ bool cmServerProtocol::Activate(cmServer* server, return result; } +cmFileMonitor* cmServerProtocol::FileMonitor() const +{ + return this->m_Server ? this->m_Server->FileMonitor() : nullptr; +} + void cmServerProtocol::SendSignal(const std::string& name, const Json::Value& data) const { @@ -365,6 +371,30 @@ bool cmServerProtocol1_0::DoActivate(const cmServerRequest& request, return true; } +void cmServerProtocol1_0::HandleCMakeFileChanges(const std::string& path, + int event, int status) +{ + assert(status == 0); + static_cast(status); + + if (!m_isDirty) { + m_isDirty = true; + SendSignal(kDIRTY_SIGNAL, Json::objectValue); + } + Json::Value obj = Json::objectValue; + obj[kPATH_KEY] = path; + Json::Value properties = Json::arrayValue; + if (event & UV_RENAME) { + properties.append(kRENAME_PROPERTY_VALUE); + } + if (event & UV_CHANGE) { + properties.append(kCHANGE_PROPERTY_VALUE); + } + + obj[kPROPERTIES_KEY] = properties; + SendSignal(kFILE_CHANGE_SIGNAL, obj); +} + const cmServerResponse cmServerProtocol1_0::Process( const cmServerRequest& request) { @@ -385,6 +415,9 @@ const cmServerResponse cmServerProtocol1_0::Process( if (request.Type == kCONFIGURE_TYPE) { return this->ProcessConfigure(request); } + if (request.Type == kFILESYSTEM_WATCHERS_TYPE) { + return this->ProcessFileSystemWatchers(request); + } if (request.Type == kGLOBAL_SETTINGS_TYPE) { return this->ProcessGlobalSettings(request); } @@ -862,6 +895,8 @@ cmServerResponse cmServerProtocol1_0::ProcessConfigure( return request.ReportError("This instance is inactive."); } + FileMonitor()->StopMonitoring(); + // Make sure the types of cacheArguments matches (if given): std::vector cacheArgs; bool cacheArgumentsError = false; @@ -938,7 +973,17 @@ cmServerResponse cmServerProtocol1_0::ProcessConfigure( if (ret < 0) { return request.ReportError("Configuration failed."); } + + std::vector toWatchList; + getCMakeInputs(gg, std::string(), buildDir, nullptr, &toWatchList, nullptr); + + FileMonitor()->MonitorPaths(toWatchList, + [this](const std::string& p, int e, int s) { + this->HandleCMakeFileChanges(p, e, s); + }); + m_State = STATE_CONFIGURED; + m_isDirty = false; return request.Reply(Json::Value()); } @@ -1011,3 +1056,22 @@ cmServerResponse cmServerProtocol1_0::ProcessSetGlobalSettings( return request.Reply(Json::Value()); } + +cmServerResponse cmServerProtocol1_0::ProcessFileSystemWatchers( + const cmServerRequest& request) +{ + const cmFileMonitor* const fm = FileMonitor(); + Json::Value result = Json::objectValue; + Json::Value files = Json::arrayValue; + for (const auto& f : fm->WatchedFiles()) { + files.append(f); + } + Json::Value directories = Json::arrayValue; + for (const auto& d : fm->WatchedDirectories()) { + directories.append(d); + } + result[kWATCHED_FILES_KEY] = files; + result[kWATCHED_DIRECTORIES_KEY] = directories; + + return request.Reply(result); +} diff --git a/Source/cmServerProtocol.h b/Source/cmServerProtocol.h index d672a60b6..5238d5db1 100644 --- a/Source/cmServerProtocol.h +++ b/Source/cmServerProtocol.h @@ -13,6 +13,7 @@ #include class cmake; +class cmFileMonitor; class cmServer; class cmServerRequest; @@ -81,6 +82,7 @@ public: bool Activate(cmServer* server, const cmServerRequest& request, std::string* errorMessage); + cmFileMonitor* FileMonitor() const; void SendSignal(const std::string& name, const Json::Value& data) const; protected: @@ -107,6 +109,8 @@ private: bool DoActivate(const cmServerRequest& request, std::string* errorMessage) override; + void HandleCMakeFileChanges(const std::string& path, int event, int status); + // Handle requests: cmServerResponse ProcessCache(const cmServerRequest& request); cmServerResponse ProcessCMakeInputs(const cmServerRequest& request); @@ -115,6 +119,7 @@ private: cmServerResponse ProcessConfigure(const cmServerRequest& request); cmServerResponse ProcessGlobalSettings(const cmServerRequest& request); cmServerResponse ProcessSetGlobalSettings(const cmServerRequest& request); + cmServerResponse ProcessFileSystemWatchers(const cmServerRequest& request); enum State { @@ -124,4 +129,6 @@ private: STATE_COMPUTED }; State m_State = STATE_INACTIVE; + + bool m_isDirty = false; };