From 364ff6e6d45087653a302b96ccd8c9e02db2b9d1 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Tue, 28 Apr 2026 10:23:44 -0700 Subject: Refactor the LLSyntaxIDLSL/Lua into single LLSyntaxDefCache, use new cap when available to retrieve all language definition files and cache them locally. Retrieve the default definition files from the LSL definitions precompiled library. --- indra/newview/llsyntaxid.cpp | 517 +++++++++++++++++++++++++++---------------- 1 file changed, 330 insertions(+), 187 deletions(-) (limited to 'indra/newview/llsyntaxid.cpp') diff --git a/indra/newview/llsyntaxid.cpp b/indra/newview/llsyntaxid.cpp index 16274f59bc..3d0a9d80b4 100644 --- a/indra/newview/llsyntaxid.cpp +++ b/indra/newview/llsyntaxid.cpp @@ -38,37 +38,53 @@ //----------------------------------------------------------------------------- // LLSyntaxIdLSL //----------------------------------------------------------------------------- -const std::string SYNTAX_ID_CAPABILITY_NAME = "LSLSyntax"; -const std::string SYNTAX_ID_SIMULATOR_FEATURE = "LSLSyntaxId"; -const std::string FILENAME_DEFAULT_LSL = "keywords_lsl_default.xml"; -const std::string FILENAME_DEFAULT_LUA = "keywords_lua_default.xml"; - -/** - * @brief LLSyntaxIdLSL constructor - */ -LLSyntaxIdLSL::LLSyntaxIdLSL() -: mKeywordsXml(LLSD()) -, mCapabilityURL(std::string()) -, mFilePath(LL_PATH_APP_SETTINGS) -, mSyntaxId(LLUUID()) -, mInitialized(false) +namespace +{ + const std::string SYNTAX_ID_CAPABILITY_NAME = "LSLSyntax"; + const std::string SYNTAX_DEF_CAPABILITY_NAME = "ScriptDefinitions"; + const std::string SYNTAX_ID_SIMULATOR_FEATURE = "LSLSyntaxId"; + const std::string FILENAME_INTERNAL_LSL = "lsl_keywords.xml"; + const std::string FILENAME_INTERNAL_LUA = "slua_keywords.xml"; + + constexpr U32 LLSD_SYNTAX_LSL_VERSION_EXPECTED = 2; + const std::string LLSD_SYNTAX_LSL_VERSION_KEY("llsd-lsl-syntax-version"); + + const std::unordered_set MEMCACHED_LLSD = { + FILENAME_INTERNAL_LSL, + FILENAME_INTERNAL_LUA + }; +} // namespace + +//======================================================================== +void LLSyntaxDefCache::initSingleton() { - loadDefaultKeywordsIntoLLSD(); - mRegionChangedCallback = gAgent.addRegionChangedCallback(boost::bind(&LLSyntaxIdLSL::handleRegionChanged, this)); + buildDefaultCache(); + loadKeywordsIntoLLSD(); + mRegionChangedCallback = gAgent.addRegionChangedCallback(boost::bind(&LLSyntaxDefCache::handleRegionChanged, this)); handleRegionChanged(); // Kick off an initial caps query and fetch } -void LLSyntaxIdLSL::buildFullFileSpec() +void LLSyntaxDefCache::cleanupSingleton() { - ELLPath path = mSyntaxId.isNull() ? LL_PATH_APP_SETTINGS : LL_PATH_CACHE; - const std::string filename = mSyntaxId.isNull() ? FILENAME_DEFAULT_LSL : "keywords_lsl_" + mSyntaxId.asString() + ".llsd.xml"; - mFullFileSpec = gDirUtilp->getExpandedFilename(path, filename); + gAgent.removeRegionChangedCallback(mRegionChangedCallback); + mLSLKeywords = LLSD(); + mLuaKeywords = LLSD(); + mCapabilityURL = std::string(); + mSyntaxId = LLUUID(); + mFileCachePaths.clear(); } -//----------------------------------------------------------------------------- -// syntaxIdChange() -//----------------------------------------------------------------------------- -bool LLSyntaxIdLSL::syntaxIdChanged() +boost::signals2::connection LLSyntaxDefCache::addSyntaxIDCallback(const syntax_id_changed_signal_t::slot_type& cb) +{ + return mSyntaxIDChangedSignal.connect(cb); +} + +//======================================================================== +// checkSyntaxId() +// Checks the current region for the LSLSyntaxId feature and capability, and +// if found checks the syntax ID against the one we have. If they differ, +// updates the syntax ID and returns true. Otherwise returns false. +bool LLSyntaxDefCache::updateSyntaxId() { LLViewerRegion* region = gAgent.getRegion(); @@ -83,51 +99,97 @@ bool LLSyntaxIdLSL::syntaxIdChanged() { // get and check the hash LLUUID new_syntax_id = sim_features[SYNTAX_ID_SIMULATOR_FEATURE].asUUID(); - mCapabilityURL = region->getCapability(SYNTAX_ID_CAPABILITY_NAME); + + // *Note* the syntax ID may not have changed, but the region almost certainly has. + // update the cap URL + mCapabilityURL = region->getCapability(SYNTAX_DEF_CAPABILITY_NAME); + mUseDefsCap = !mCapabilityURL.empty(); + if (!mUseDefsCap) + { + mCapabilityURL = region->getCapability(SYNTAX_ID_CAPABILITY_NAME); + } LL_DEBUGS("SyntaxLSL") << SYNTAX_ID_SIMULATOR_FEATURE << " capability URL: " << mCapabilityURL << LL_ENDL; + if (new_syntax_id != mSyntaxId) { LL_DEBUGS("SyntaxLSL") << "New SyntaxID '" << new_syntax_id << "' found." << LL_ENDL; mSyntaxId = new_syntax_id; return true; } - else - LL_DEBUGS("SyntaxLSL") << "SyntaxID matches what we have." << LL_ENDL; + + LL_DEBUGS("SyntaxLSL") << "SyntaxID has not changed. Still " << mSyntaxId << LL_ENDL; } } else { - region->setCapabilitiesReceivedCallback(boost::bind(&LLSyntaxIdLSL::handleCapsReceived, this, _1)); + region->setCapabilitiesReceivedCallback(boost::bind(&LLSyntaxDefCache::handleCapsReceived, this, _1)); LL_DEBUGS("SyntaxLSL") << "Region has not received capabilities. Waiting for caps..." << LL_ENDL; } } return false; } +void LLSyntaxDefCache::handleRegionChanged() +{ + if (updateSyntaxId()) + { + if (!checkCacheAndLoad(mSyntaxId)) + { + fetchKeywords(); + } + } +} + +void LLSyntaxDefCache::handleCapsReceived(const LLUUID& region_uuid) +{ + LLViewerRegion* current_region = gAgent.getRegion(); + + if (region_uuid.notNull() && current_region->getRegionID() == region_uuid) + { + updateSyntaxId(); + if (!checkCacheAndLoad(mSyntaxId)) + { + fetchKeywords(); + } + } +} + //----------------------------------------------------------------------------- -// fetchKeywordsFile -//----------------------------------------------------------------------------- -void LLSyntaxIdLSL::fetchKeywordsFile(const std::string& filespec) +// fetchKeywords +// Initiates a fetch of the current language definitions from the region caps. +void LLSyntaxDefCache::fetchKeywords() { - LLCoros::instance().launch("LLSyntaxIdLSL::fetchKeywordsFileCoro", - boost::bind(&LLSyntaxIdLSL::fetchKeywordsFileCoro, this, mCapabilityURL, filespec)); - LL_DEBUGS("SyntaxLSL") << "LSLSyntaxId capability URL is: " << mCapabilityURL << ". Filename to use is: '" << filespec << "'." << LL_ENDL; + if (mCapabilityURL.empty()) + { + LL_WARNS("SyntaxLSL") << "No capability URL for fetching syntax definitions." << LL_ENDL; + return; + } + if (mUseDefsCap) + { + LLCoros::instance().launch("LLSyntaxIdLSL::fetchKeywordsDefsCoro", + boost::bind(&LLSyntaxDefCache::fetchKeywordsDefsCoro, this, mCapabilityURL, mSyntaxId)); + } + else + { + LLCoros::instance().launch("LLSyntaxIdLSL::fetchKeywordsFileCoro", + boost::bind(&LLSyntaxDefCache::fetchKeywordsFileCoro, this, mCapabilityURL, mSyntaxId)); + } } //----------------------------------------------------------------------------- // fetchKeywordsFileCoro -//----------------------------------------------------------------------------- -void LLSyntaxIdLSL::fetchKeywordsFileCoro(std::string url, std::string fileSpec) +// This uses the legacy languge cap which only sends the LSL keywords file. +void LLSyntaxDefCache::fetchKeywordsFileCoro(std::string url, LLUUID syntax_id) { LLCore::HttpRequest::policy_t httpPolicy(LLCore::HttpRequest::DEFAULT_POLICY_ID); LLCoreHttpUtil::HttpCoroutineAdapter::ptr_t httpAdapter = std::make_shared("fetchKeywordsFileCoro", httpPolicy); LLCore::HttpRequest::ptr_t httpRequest = std::make_shared(); - std::pair::iterator, bool> insrt = mInflightFetches.insert(fileSpec); + auto insrt = mInflightFetches.insert(syntax_id); if (!insrt.second) { - LL_WARNS("SyntaxLSL") << "Already downloading keyword file called \"" << fileSpec << "\"." << LL_ENDL; + LL_WARNS("SyntaxLSL") << "Already downloading keyword file for syntax ID \"" << syntax_id << "\"." << LL_ENDL; return; } @@ -136,11 +198,11 @@ void LLSyntaxIdLSL::fetchKeywordsFileCoro(std::string url, std::string fileSpec) LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS]; LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults); - mInflightFetches.erase(fileSpec); + mInflightFetches.erase(syntax_id); if (!status) { - LL_WARNS("SyntaxLSL") << "Failed to fetch syntax file \"" << fileSpec << "\"" << LL_ENDL; + LL_WARNS("SyntaxLSL") << "Failed to fetch syntax file for syntax ID \"" << syntax_id << "\"" << LL_ENDL; return; } @@ -148,13 +210,38 @@ void LLSyntaxIdLSL::fetchKeywordsFileCoro(std::string url, std::string fileSpec) if (isSupportedVersion(result)) { - // Shuttle this task to the main coro/worker. - // loadKeywordsIntoLLSD will attempt to get a mutex which is not coro aware. - LLAppViewer::instance()->postToMainCoro([this, result, fileSpec]() + std::string path = buildCacheDirectoryName(syntax_id); + + if (!LLFile::exists(path)) + { + LL_DEBUGS("SyntaxLSL") << "Cache directory '" << path << "' does not exist. Attempting to create." << LL_ENDL; + if (LLFile::mkdir(path)) { - setKeywordsXml(result); - cacheFile(fileSpec, result); - loadKeywordsIntoLLSD(); + LL_WARNS("SyntaxLSL") << "Failed to create cache directory '" << path << "'. Cannot cache syntax defs file." << LL_ENDL; + return; + } + } + + // Note that after this call the file cache will have all well known files pointing + // to the default versions, so below, where we get the path to the lua keywords + // well be loading the default version. + buildDefaultCache(); + + // The LSL keywords we just received + std::string full_path = gDirUtilp->add(path, FILENAME_INTERNAL_LSL); + if (writeCacheFile(full_path, result)) + { + mFileCachePaths.addNamePath(FILENAME_INTERNAL_LSL, full_path); + } + // We need to manually load the Lua keywords + full_path = mFileCachePaths.getPath(FILENAME_INTERNAL_LUA); + LLSD lua_defs = loadDeserializedCacheFile(full_path); + + setKeywords(result, lua_defs); + + // Shuttle this task to the main coro/worker. + LLAppViewer::instance()->postToMainCoro([this]() { + mSyntaxIDChangedSignal(); }); } else @@ -164,73 +251,196 @@ void LLSyntaxIdLSL::fetchKeywordsFileCoro(std::string url, std::string fileSpec) } -//----------------------------------------------------------------------------- -// cacheFile -//----------------------------------------------------------------------------- -void LLSyntaxIdLSL::cacheFile(const std::string &fileSpec, const LLSD& content_ref) +void LLSyntaxDefCache::fetchKeywordsDefsCoro(std::string url, LLUUID syntax_id) { - std::stringstream str; - LLSDSerialize::toXML(content_ref, str); - const std::string xml = str.str(); + LLCore::HttpRequest::policy_t httpPolicy(LLCore::HttpRequest::DEFAULT_POLICY_ID); + LLCoreHttpUtil::HttpCoroutineAdapter::ptr_t httpAdapter = + std::make_shared("fetchKeywordsDefsCoro", httpPolicy); + LLCore::HttpRequest::ptr_t httpRequest = std::make_shared(); - // save the str to disk, usually to the cache. - llofstream file(fileSpec.c_str(), std::ios_base::out); - file.write(xml.c_str(), str.str().size()); - file.close(); + static std::set inflightDefsFetches; + auto insrt = inflightDefsFetches.insert(syntax_id); + //auto insrt = mInflightFetches.insert(syntax_id); + if (!insrt.second) + { + LL_WARNS("SyntaxLSL") << "Already downloading keyword defs for \"" << syntax_id << "\"." << LL_ENDL; + return; + } - LL_DEBUGS("SyntaxLSL") << "Syntax file received, saving as: '" << fileSpec << "'" << LL_ENDL; -} + LLSD result = httpAdapter->getAndSuspend(httpRequest, url); -//----------------------------------------------------------------------------- -// initialize -//----------------------------------------------------------------------------- -void LLSyntaxIdLSL::initialize() -{ - if(mInitialized) return; - if (mSyntaxId.isNull()) + LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS]; + LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults); + + inflightDefsFetches.erase(syntax_id); + + if (!status) { - loadDefaultKeywordsIntoLLSD(); + LL_WARNS("SyntaxLSL") << "Failed to fetch syntax file \"" << syntax_id << "\"" << LL_ENDL; + result.erase(LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS); + return; } - else if (!mCapabilityURL.empty()) + + // Now we need to walk through the returned LLSD. It consists of a map keyed on the file name containing a binary + // blob of the actual file contents. + LLSD files = result["files"]; + LLSD memcached_keywords; + if (files.isMap()) { - LL_DEBUGS("SyntaxLSL") << "LSL version has changed, getting appropriate file." << LL_ENDL; + std::string path = buildCacheDirectoryName(syntax_id); - // Need a full spec regardless of file source, so build it now. - buildFullFileSpec(); - if (mSyntaxId.notNull()) + if (!LLFile::exists(path)) { - if (!gDirUtilp->fileExists(mFullFileSpec)) - { // Does not exist, so fetch it from the capability - LL_DEBUGS("SyntaxLSL") << "LSL syntax not cached, attempting download." << LL_ENDL; - fetchKeywordsFile(mFullFileSpec); - } - else + LL_DEBUGS("SyntaxLSL") << "Cache directory '" << path << "' does not exist. Attempting to create." << LL_ENDL; + if (LLFile::mkdir(path)) { - LL_DEBUGS("SyntaxLSL") << "Found cached Syntax file: " << mFullFileSpec << " Loading keywords." << LL_ENDL; - loadKeywordsIntoLLSD(); + LL_WARNS("SyntaxLSL") << "Failed to create cache directory '" << path << "'. Cannot cache syntax defs file." << LL_ENDL; + return; } } - else + + buildDefaultCache(); + + for (const auto &[filename, contents] : llsd::inMap(files)) { - LL_DEBUGS("SyntaxLSL") << "LSLSyntaxId is null. Loading default values" << LL_ENDL; - loadDefaultKeywordsIntoLLSD(); + std::string full_path = gDirUtilp->add(path, filename); + + if (MEMCACHED_LLSD.find(filename) != MEMCACHED_LLSD.end()) + { // Maintain some keyword LLSDs internally, LSL and Lua + memcached_keywords[filename] = contents; + } + + if (writeCacheFile(full_path, contents)) + { + mFileCachePaths.addNamePath(filename, full_path); + } } } else { - LL_DEBUGS("SyntaxLSL") << "LSLSyntaxId capability URL is empty." << LL_ENDL; - loadDefaultKeywordsIntoLLSD(); + LL_WARNS("SyntaxLSL") << "Malformed syntax defs response, missing 'files' map." << LL_ENDL; } - mInitialized = true; + + result.erase(LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS); + + setKeywords(memcached_keywords["lsl_keywords.xml"], memcached_keywords["lua_keywords.xml"]); + LLAppViewer::instance()->postToMainCoro( + [this]() + { + mSyntaxIDChangedSignal(); + }); } +//----------------------------------------------------------------------------- +// buildCache +// Constructs the cache file paths for the given syntax ID. If the syntax ID is null, +// constructs the default cache file paths. +void LLSyntaxDefCache::buildCachePaths(const LLUUID &syntax_id) +{ + if (syntax_id.notNull()) + { // Initialize the cache files to point to the default files + buildCachePaths(LLUUID::null); + } + else + { + mFileCachePaths.clear(); + } + + std::string cache_dir = buildCacheDirectoryName(syntax_id); + if (!LLFile::exists(cache_dir)) + { + LL_DEBUGS("SyntaxLSL") << "Cache directory '" << cache_dir << "' does not exist." << LL_ENDL; + return; + } + + auto files = gDirUtilp->getFilesInDir(cache_dir); + for (const auto &file : files) + { + mFileCachePaths.addNamePath(file, gDirUtilp->add(cache_dir, file)); + } +} + +bool LLSyntaxDefCache::writeCacheFile(const std::string &fileSpec, const LLSD& content_ref) +{ + bool binary(content_ref.isBinary()); + std::ofstream file(fileSpec.c_str(), (binary)? std::ios_base::binary : 0); + + if (!file.is_open()) + { + LL_WARNS("SyntaxLSL") << "Failed to open file for writing: '" << fileSpec << "'" << LL_ENDL; + return false; + } + + if (binary) + { + LL_DEBUGS("SyntaxLSL") << "Caching raw content to '" << fileSpec << "'" << LL_ENDL; + file.write((const char*)content_ref.asBinary().data(), content_ref.asBinary().size()); + } + else + { + LL_DEBUGS("SyntaxLSL") << "Caching XML content to '" << fileSpec << "'" << LL_ENDL; + LLSDSerialize::serialize(content_ref, file, LLSDSerialize::LLSD_XML, LLSDFormatter::OPTIONS_PRETTY); + } + file.close(); + + if (!file.good()) + { + LL_WARNS("SyntaxLSL") << "Failed to write content to file: '" << fileSpec << "'" << LL_ENDL; + return false; + } + return true; +} + +//----------------------------------------------------------------------------- +// checkCacheAndLoad +// Tests the local cache for the given syntax ID. If found it loads the keywords +// from the cache into LLSD and returns true. Otherwise returns false. +bool LLSyntaxDefCache::checkCacheAndLoad(const LLUUID& syntax_id) +{ + if (checkLocalCache(syntax_id)) + { + buildCachePaths(syntax_id); + loadKeywordsIntoLLSD(); + return true; + } + return false; +} + +std::string LLSyntaxDefCache::buildCacheDirectoryName(const LLUUID& syntax_id) +{ + if (syntax_id.isNull()) + { + LL_DEBUGS("SyntaxLSL") << "No SyntaxID, using app settings directory." << LL_ENDL; + return gDirUtilp->getExpandedFilename(LL_PATH_CACHE, "syntax_default"); + } + else + { + LL_DEBUGS("SyntaxLSL") << "Using cache directory for SyntaxID '" << syntax_id << "'." << LL_ENDL; + std::string cache_dir_name = "syntax_" + syntax_id.asString(); + + return gDirUtilp->getExpandedFilename(LL_PATH_APP_SETTINGS, cache_dir_name); + } +} + +bool LLSyntaxDefCache::checkLocalCache(const LLUUID& syntax_id) const +{ + if (syntax_id.isNull()) + { // Cache check will always fail if we don't have a valid SyntaxID, so skip it in that case. + LL_DEBUGS("SyntaxLSL") << "No SyntaxID, skipping local cache check." << LL_ENDL; + return false; + } + + // Check for the existence of the cache directory for this syntax ID. If it doesn't exist, then we don't have a cached file. + std::string cache_dir = buildCacheDirectoryName(syntax_id); + return gDirUtilp->fileExists(cache_dir); +} + + //----------------------------------------------------------------------------- // isSupportedVersion //----------------------------------------------------------------------------- -const U32 LLSD_SYNTAX_LSL_VERSION_EXPECTED = 2; -const std::string LLSD_SYNTAX_LSL_VERSION_KEY("llsd-lsl-syntax-version"); -bool LLSyntaxIdLSL::isSupportedVersion(const LLSD& content) +bool LLSyntaxDefCache::isSupportedVersion(const LLSD& content) { bool is_valid = false; /* @@ -256,128 +466,61 @@ bool LLSyntaxIdLSL::isSupportedVersion(const LLSD& content) } //----------------------------------------------------------------------------- -// loadDefaultKeywordsIntoLLSD() -//----------------------------------------------------------------------------- -void LLSyntaxIdLSL::loadDefaultKeywordsIntoLLSD() -{ - mSyntaxId.setNull(); - buildFullFileSpec(); - loadKeywordsIntoLLSD(); -} - -//----------------------------------------------------------------------------- -// loadKeywordsFileIntoLLSD +// loadKeywordsIntoLLSD //----------------------------------------------------------------------------- /** * @brief Load xml serialized LLSD - * @desc Opens the specified filespec and attempts to deserializes the - * contained data to the specified LLSD object. indicate success/failure with - * sLoaded/sLoadFailed members. + * @desc Open the internal lsl keywords files and deserialize them into the correct + * members. */ -void LLSyntaxIdLSL::loadKeywordsIntoLLSD() +void LLSyntaxDefCache::loadKeywordsIntoLLSD() { - LLSD content; - llifstream file; - file.open(mFullFileSpec.c_str()); - if (file.is_open()) + for (auto& filename : MEMCACHED_LLSD) { - if (LLSDSerialize::fromXML(content, file) != LLSDParser::PARSE_FAILURE) + // Note, in the case of the legacy language cap (it only delivers the LSL keywords file) + // The mFileCachePaths will have been initialized in such a way that the Lua keywords + // point to the default file. + std::string full_path = mFileCachePaths.getPath(filename); + if (!full_path.empty()) { - if (isSupportedVersion(content)) + LLSD content = loadDeserializedCacheFile(full_path); + if (!content.isUndefined() && isSupportedVersion(content)) { - LL_DEBUGS("SyntaxLSL") << "Deserialized: " << mFullFileSpec << LL_ENDL; + LL_DEBUGS("SyntaxLSL") << "Deserialized cached file: " << full_path << LL_ENDL; + if (filename == FILENAME_INTERNAL_LSL) + { + mLSLKeywords = content; + } + else if (filename == FILENAME_INTERNAL_LUA) + { + mLuaKeywords = content; + } } else { - LL_WARNS("SyntaxLSL") << "Unknown or unsupported version of syntax file." << LL_ENDL; + LL_WARNS("SyntaxLSL") << "Unknown or unsupported version of syntax file " << full_path << "." << LL_ENDL; } } } - else - { - LL_WARNS("SyntaxLSL") << "Failed to open: " << mFullFileSpec << LL_ENDL; - } - mKeywordsXml = content; - mSyntaxIDChangedSignal(); -} - -bool LLSyntaxIdLSL::keywordFetchInProgress() -{ - return !mInflightFetches.empty(); -} - -void LLSyntaxIdLSL::handleRegionChanged() -{ - if (syntaxIdChanged()) - { - buildFullFileSpec(); - fetchKeywordsFile(mFullFileSpec); - mInitialized = false; - } -} - -void LLSyntaxIdLSL::handleCapsReceived(const LLUUID& region_uuid) -{ - LLViewerRegion* current_region = gAgent.getRegion(); - - if (region_uuid.notNull() - && current_region->getRegionID() == region_uuid) - { - syntaxIdChanged(); - } -} - -boost::signals2::connection LLSyntaxIdLSL::addSyntaxIDCallback(const syntax_id_changed_signal_t::slot_type& cb) -{ - return mSyntaxIDChangedSignal.connect(cb); -} - - - -//----------------------------------------------------------------------------- -// LLSyntaxLua -//----------------------------------------------------------------------------- -LLSyntaxLua::LLSyntaxLua() - : mKeywordsXml(LLSD()) - , mInitialized(false) -{ -} - -void LLSyntaxLua::initialize() -{ - if (mInitialized) return; - loadDefaultKeywordsIntoLLSD(); - loadLuaTypesIntoLLSD(); - mInitialized = true; + mSyntaxIDChangedSignal(); } -void LLSyntaxLua::loadDefaultKeywordsIntoLLSD() +LLSD LLSyntaxDefCache::loadDeserializedCacheFile(const std::string& file_path) { - std::string fullFileSpec = gDirUtilp->getExpandedFilename(LL_PATH_APP_SETTINGS, FILENAME_DEFAULT_LUA); - llifstream file(fullFileSpec.c_str()); - + std::ifstream file(file_path.c_str()); if (file.good()) { LLSD content; - if (LLSDSerialize::fromXML(content, file) != LLSDParser::PARSE_FAILURE) + if (LLSDSerialize::deserialize(content, file, -1)) { - mKeywordsXml = content; + return content; } } -} - -void LLSyntaxLua::loadLuaTypesIntoLLSD() -{ - std::string fullFileSpec = gDirUtilp->getExpandedFilename(LL_PATH_APP_SETTINGS, "types_lua_default.xml"); - llifstream file(fullFileSpec.c_str()); - - if (file.good()) + else { - LLSD content; - if (LLSDSerialize::fromXML(content, file) != LLSDParser::PARSE_FAILURE) - { - mTypesXml = content; - } + LL_WARNS("SyntaxLSL") << "Failed to open cached file: " << file_path << LL_ENDL; } + return LLSD(); } + -- cgit v1.3 From 84e806f71492a76ab28e049ec255df479ffbb339 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Wed, 29 Apr 2026 14:43:08 -0700 Subject: Added support to jsonrpc server for the viewer based syntax cache. --- doc/external-editor-json-rpc.md | 236 +++++++++++++++++++++++++++---------- indra/newview/llscripteditorws.cpp | 89 ++++++++++++++ indra/newview/llscripteditorws.h | 2 + indra/newview/llsyntaxid.cpp | 39 ++++++ indra/newview/llsyntaxid.h | 18 +++ 5 files changed, 324 insertions(+), 60 deletions(-) (limited to 'indra/newview/llsyntaxid.cpp') diff --git a/doc/external-editor-json-rpc.md b/doc/external-editor-json-rpc.md index b8c7fa69e3..8d657b0d33 100644 --- a/doc/external-editor-json-rpc.md +++ b/doc/external-editor-json-rpc.md @@ -1,6 +1,6 @@ # Viewer to External Editor JSON-RPC
Message Interfaces Documentation -This document describes all the message interfaces defined in for WebSocket communication between the Second Life viewer and an external editor such as a VSCode extension. +This document describes all the message interfaces defined for WebSocket communication between the Second Life viewer and an external editor such as a VSCode extension. ## Table of Contents @@ -15,10 +15,13 @@ This document describes all the message interfaces defined in for WebSocket comm - [SyntaxChange](#syntaxchange) - [Language Syntax ID Request](#language-syntax-id-request) - [Language Syntax Request](#language-syntax-request) + - [Language Syntax Cache List](#language-syntax-cache-list) + - [Language Syntax Cache Get](#language-syntax-cache-get) - [Script Subscription Interfaces](#script-subscription-interfaces) - [ScriptSubscribe](#scriptsubscribe) - [ScriptSubscribeResponse](#scriptsubscriberesponse) - [ScriptUnsubscribe](#scriptunsubscribe) + - [ScriptList](#scriptlist) - [Compilation Interfaces](#compilation-interfaces) - [CompilationError](#compilationerror) - [CompilationResult](#compilationresult) @@ -33,7 +36,7 @@ This document describes all the message interfaces defined in for WebSocket comm 1. **Connection Establishment:** - - Viewer sends `session.handshake` notification with `SessionHandshake` data + - Viewer sends `session.handshake` call with `SessionHandshake` data - Extension responds with `SessionHandshakeResponse` - Viewer confirms with `session.ok` notification @@ -41,7 +44,7 @@ This document describes all the message interfaces defined in for WebSocket comm - Extension makes `language.syntax.id` call to get current syntax version - Extension makes `language.syntax` calls with different `kind` parameters to get specific language data - - Viewer responds with `LanguageInfo` data containing the requested information + - Viewer responds with a `LanguageInfo` object containing the requested definitions 3. **Script Subscription Management:** @@ -65,17 +68,23 @@ This document describes all the message interfaces defined in for WebSocket comm | Method | Direction | Type | Interface/Parameters | | ------------------------------- | ------------------ | ------------ | -------------------------- | -| `session.handshake` | Viewer → Extension | Notification | `SessionHandshake` | +| `session.handshake` | Viewer → Extension | Call | `SessionHandshake` | | `session.handshake` (response) | Extension → Viewer | Response | `SessionHandshakeResponse` | | `session.ok` | Viewer → Extension | Notification | _(no interface)_ | | `session.disconnect` | Bidirectional | Notification | `SessionDisconnect` | | `script.subscribe` | Extension → Viewer | Call | `ScriptSubscribe` | | `script.subscribe` (response) | Viewer → Extension | Response | `ScriptSubscribeResponse` | | `script.unsubscribe` | Viewer → Extension | Notification | `ScriptUnsubscribe` | +| `script.list` | Extension → Viewer | Call | _(no parameters)_ | +| `script.list` (response) | Viewer → Extension | Response | `ScriptList` | | `language.syntax.id` | Extension → Viewer | Call | _(no parameters)_ | | `language.syntax.id` (response) | Viewer → Extension | Response | `{ id: string }` | | `language.syntax` | Extension → Viewer | Call | `{ kind: string }` | | `language.syntax` (response) | Viewer → Extension | Response | `LanguageInfo` | +| `language.syntax.cache` | Extension → Viewer | Call | _(no parameters)_ | +| `language.syntax.cache` (response) | Viewer → Extension | Response | `SyntaxCacheList` | +| `language.syntax.get` | Extension → Viewer | Call | `{ filename: string, as_json?: boolean }` | +| `language.syntax.get` (response) | Viewer → Extension | Response | `SyntaxCacheFile` | | `language.syntax.change` | Viewer → Extension | Notification | `SyntaxChange` | | `script.compiled` | Viewer → Extension | Notification | `CompilationResult` | | `runtime.debug` | Viewer → Extension | Notification | `RuntimeDebug` | @@ -85,9 +94,9 @@ This document describes all the message interfaces defined in for WebSocket comm ### SessionHandshake -**JSON-RPC Method:** `session.handshake` (notification from viewer) +**JSON-RPC Method:** `session.handshake` (call from viewer) -The initial handshake message sent by the viewer to establish a connection. +The initial handshake call sent by the viewer to establish a session. ```typescript interface SessionHandshake { @@ -112,10 +121,13 @@ interface SessionHandshake { - `viewer_version`: Version string of the viewer - `agent_id`: Unique identifier for the user/agent - `agent_name`: Human-readable name of the agent -- `challenge` (optional): Security challenge string for authentication -- `languages`: Array of supported scripting languages (e.g., ["lsl", "luau"]) -- `syntax_id`: Current active syntax/language identifier -- `features`: Dictionary of feature flags indicating viewer capabilities +- `challenge` (optional): Path to a temporary file on the local filesystem containing a UUID. The client must read this file and return the UUID as `challenge_response` to authenticate the connection. +- `languages`: Array of supported scripting languages (e.g., `["lsl", "luau"]`) +- `syntax_id`: Current active syntax identifier as a UUID string +- `features`: Dictionary of feature flags indicating viewer capabilities. Known flags: + - `live_sync`: Viewer supports live script synchronisation with the external editor + - `compilation`: Viewer will forward compilation results via `script.compiled` + - `syntax__cache`: Viewer supports `language.syntax.cache` and `language.syntax.get` for retrieving syntax definition files ### SessionHandshakeResponse @@ -131,6 +143,8 @@ interface SessionHandshakeResponse { challenge_response?: string; languages: string[]; features: { [feature: string]: boolean }; + script_name?: string; + script_language?: string; } ``` @@ -139,15 +153,17 @@ interface SessionHandshakeResponse { - `client_name`: Name of the client (VS Code extension) - `client_version`: Fixed version "1.0" of the client - `protocol_version`: Protocol version the client supports -- `challenge_response` (optional): Response to the security challenge if provided +- `challenge_response` (optional): The UUID read from the temporary file identified by the `challenge` field in the handshake. Must be provided if `challenge` was present, otherwise the connection will be closed. - `languages`: Array of languages supported by the client - `features`: Dictionary of features supported by the client +- `script_name` (optional): Name of the script currently open in the editor +- `script_language` (optional): Language of the script currently open in the editor (e.g. `"lsl"`, `"luau"`) ### Session OK **JSON-RPC Method:** `session.ok` (notification from viewer) -Confirmation notification sent by the viewer after successful handshake completion. This interface has no defined structure as it appears to be a simple confirmation message. +Confirmation notification sent by the viewer after successful handshake completion. No parameters are sent with this notification. ### SessionDisconnect @@ -164,7 +180,12 @@ interface SessionDisconnect { **Fields:** -- `reason`: Numeric code indicating the reason for disconnection +- `reason`: Numeric code indicating the reason for disconnection: + - `0`: Normal closure + - `1`: Editor closed + - `2`: Protocol error + - `3`: Connection timeout + - `4`: Internal server error - `message`: Human-readable description of the disconnect reason ## Language and Syntax Interfaces @@ -183,7 +204,7 @@ interface SyntaxChange { **Fields:** -- `id`: Identifier for the new syntax/language +- `id`: UUID string identifying the new syntax version ### Language Syntax ID Request @@ -191,63 +212,130 @@ interface SyntaxChange { Requests the current active language syntax identifier from the viewer. This method takes no parameters. -**Response:** Returns an object with an `id` field containing the current syntax identifier. +**Response:** Returns `{ id: string }` where `id` is the current syntax version as a UUID string. ### Language Syntax Request **JSON-RPC Method:** `language.syntax` (call from extension to viewer) -Requests detailed syntax information for a specific language kind. +Requests the in-memory keyword definitions for a specific language. These definitions are the deserialized, viewer-processed form of the syntax data for the current region. **Parameters:** ```typescript { - kind: string; // The type of syntax information requested + kind: string; // The language whose definitions to retrieve } ``` -**Fields:** +**Valid `kind` values:** -- `kind`: The type of syntax information to retrieve (e.g., "functions", "constants", "events", "types.luau") +| Value | Description | +| ----------- | ----------------------------------------- | +| `"defs.lsl"` | Returns the LSL keyword definitions | +| `"defs.lua"` | Returns the Luau keyword definitions | -**Response:** Returns `LanguageInfo` data containing the requested syntax information: +**Response:** ```typescript interface LanguageInfo { id: string; - lslDefs?: { - controls?: any; - types?: any; - constants?: { [name: string]: ConstantDef }; - events?: { [name: string]: FunctionDef }; - functions?: { [name: string]: FunctionDef }; - }; - luaDefs?: { - modules?: { [name: string]: TypeDef }; - classes?: { [name: string]: TypeDef }; - aliases?: { [name: string]: TypeDef }; - functions?: { [name: string]: FunctionDef }; - }; + defs?: object; // Present only on success + success: boolean; + error?: string; // Present only on failure +} +``` + +**Response Fields:** + +- `id`: The current syntax version identifier +- `defs` (optional): The keyword definitions object. Only present when `success` is `true`. Structure varies by language. +- `success`: Whether the definitions were found and returned successfully +- `error` (optional): Human-readable error description. Only present when `success` is `false` + +**Error cases:** + +- No `kind` parameter supplied: `success: false`, `error: "No syntax category specified"` +- Unknown `kind` value: `success: false`, `error: "Unknown syntax category requested"` + +### Language Syntax Cache List + +**JSON-RPC Method:** `language.syntax.cache` (call from extension to viewer) + +Requests the list of file names currently held in the `LLSyntaxDefCache`. This provides the extension with the available syntax definition file names that can subsequently be retrieved with `language.syntax.get`. This method takes no parameters. + +**Response:** + +```typescript +interface SyntaxCacheList { + files: string[]; // Array of file names (e.g. ["lsl_keywords.xml", "slua_definitions.yaml"]) + success: boolean; } ``` **Response Fields:** -- `id`: Version identifier for the language syntax -- `lslDefs` (optional): LSL-specific language definitions containing: - - `controls` (optional): Control flow and language constructs - - `types` (optional): LSL type definitions - - `constants` (optional): Object containing constant definitions keyed by constant name - - `events` (optional): Object containing event definitions keyed by event name - - `functions` (optional): Object containing function definitions keyed by function name -- `luaDefs` (optional): Lua-specific language definitions containing: - - `modules` (optional): Module type definitions keyed by module name - - `classes` (optional): Class type definitions keyed by class name - - `aliases` (optional): Type alias definitions keyed by alias name - - `functions` (optional): Function definitions keyed by function name - -The specific sections returned depend on the `kind` parameter and the active language context. +- `files`: Array of file name strings, each of which can be passed as the `filename` parameter to `language.syntax.get` +- `success`: Whether the request was handled successfully + +**Known cache files:** + +| File name | Description | +| -------------------------------- | ---------------------------------------------------- | +| `builtins.txt` | LSL built-in keyword list in plain text format | +| `lsl_definitions.yaml` | LSL language definitions in YAML format | +| `lsl_keywords.xml` | LSL keyword definitions in LLSD XML format | +| `lsl_keywords_pretty.xml` | LSL keyword definitions in formatted LLSD XML format | +| `slua_default.d.luau` | Luau type definition file for editor tooling | +| `slua_default.docs.json` | Luau documentation data in JSON format | +| `slua_definitions.yaml` | Luau language definitions in YAML format | +| `slua_keywords.xml` | Luau keyword definitions in LLSD XML format | +| `slua_keywords_pretty.xml` | Luau keyword definitions in formatted LLSD XML format | +| `slua_selene.yml` | Luau Selene linter configuration in YAML format | + +Not all files may be present in every cache — the actual list returned by `language.syntax.cache` reflects only what is available on the viewer's local filesystem at the time of the request. + +### Language Syntax Cache Get + +**JSON-RPC Method:** `language.syntax.get` (call from extension to viewer) + +Requests the content of a specific file from the syntax definition cache. The file name must be one of the names returned by a prior `language.syntax.cache` call. Content is returned either as a raw text string or as a parsed JSON/LLSD object depending on the `as_json` parameter. + +**Parameters:** + +```typescript +{ + filename: string; // The file name to retrieve, as returned by language.syntax.cache + as_json?: boolean; // Optional. If true, content is returned as a parsed object rather than raw text +} +``` + +**Fields:** + +- `filename`: The file name to retrieve (e.g. `"lsl_keywords.xml"`, `"slua_definitions.yaml"`) +- `as_json` (optional): When `true`, the file is deserialized and returned as a structured object in `content`. When omitted or `false`, `content` is the raw text of the file. + +**Response:** + +```typescript +interface SyntaxCacheFile { + content?: string | object; // Present only on success. String if as_json is false/omitted, object if as_json is true + success: boolean; + error?: string; // Present only on failure +} +``` + +**Response Fields:** + +- `content`: The file content. Only present when `success` is `true`. Is a raw text string when `as_json` is omitted or `false`; is a parsed object when `as_json` is `true`. +- `success`: Whether the file was found and read successfully +- `error` (optional): Human-readable error description. Only present when `success` is `false` + +**Error cases:** + +- No `filename` parameter supplied: `success: false`, `error: "No filename specified"` +- Name not found in cache: `success: false`, `error: "Requested syntax cache file not found"` +- File could not be loaded: `success: false`, `error: "Failed to load syntax cache file"` (or `"Failed to load and format syntax cache file."` when `as_json` is `true`) ## Script Subscription Interfaces @@ -283,7 +371,6 @@ interface ScriptSubscribeResponse { success: boolean; status: number; object_id?: string; - object_name?: string; item_id?: string; message?: string; } @@ -293,9 +380,14 @@ interface ScriptSubscribeResponse { - `script_id`: The script identifier that was subscribed to - `success`: Whether the subscription was successful -- `status`: Numeric status code indicating the result -- `object_id` (optional): The in-world ID of the object containing the script -- `object_name` (optional): The name of the object containing the script. +- `status`: Numeric status code indicating the result: + - `0`: Success + - `1`: Invalid editor — the script editor panel is no longer open + - `2`: Invalid subscription — no subscription found for the given `script_id` + - `3`: Already subscribed — another connection is already subscribed to this script + - `4`: Internal server error +- `object_id` (optional): The in-world UUID of the object containing the script +- `item_id` (optional): The inventory item UUID of the script within the object - `message` (optional): Additional information about the subscription result ### ScriptUnsubscribe @@ -314,6 +406,28 @@ interface ScriptUnsubscribe { - `script_id`: Unique identifier for the script to unsubscribe from +### ScriptList + +**JSON-RPC Method:** `script.list` (call from extension to viewer) + +Requests the list of all scripts currently open and tracked by the viewer, along with the viewer's temp directory. This is intended for use by a file watcher tool that needs to discover which script temp files are active without going through the full `script.subscribe` flow. This method takes no parameters. + +**Response:** + +```typescript +interface ScriptList { + temp_dir: string; + script_ids: string[]; + success: boolean; +} +``` + +**Response Fields:** + +- `temp_dir`: The absolute path to the viewer's temp directory where live-sync script files are written. Combined with a `script_id`, the caller can locate the corresponding temp file on disk. +- `script_ids`: Array of script ID strings for all currently subscribed scripts, across all active connections. +- `success`: Always `true`. + ## Compilation Interfaces ### CompilationError @@ -324,17 +438,19 @@ Individual compilation error record. interface CompilationError { row: number; column: number; - level: "ERROR"; + level: string; message: string; + format?: "lsl"; // Present only for LSL compilation errors } ``` **Fields:** -- `row`: Line number where the error occurred (0-based or 1-based depending on context) -- `column`: Column position of the error -- `level`: Severity level (currently only "ERROR" is defined) +- `row`: Line number where the error occurred (1-based for both LSL and Luau) +- `column`: Column position of the error (1-based for LSL; always `0` for Luau as the compiler does not provide column information) +- `level`: Compiler severity string (e.g. `"ERROR"`, `"WARNING"`) - `message`: Error description +- `format` (optional): Present and set to `"lsl"` for LSL compilation errors; absent for Luau errors ### CompilationResult @@ -405,10 +521,10 @@ interface RuntimeError { - `script_id`: Unique identifier for the script that encountered the error - `object_id`: Unique identifier for the object containing the script - `object_name`: Human-readable name of the object -- `message`: Error message description -- `error`: Specific error type or code -- `line`: Line number where the error occurred -- `stack` (optional): Stack trace information if available +- `message`: The full raw chat text of the runtime error message as received from the simulator +- `error`: Extracted error description. Currently always an empty string — runtime error extraction from the simulator's multi-message format is not yet fully implemented. +- `line`: Line number where the error occurred. Currently always `0` for the same reason. +- `stack` (optional): Stack trace lines if they could be extracted from the error message ## Handler and Configuration Interfaces @@ -436,7 +552,7 @@ interface WebSocketHandlers { - `onHandshake`: Handler for initial handshake message, returns handshake response - `onHandshakeOk`: Handler called when handshake is successfully completed - `onDisconnect`: Handler for disconnect notifications -- `onSubscribe`: Handler for script subscription requests from viewer, returns subscription response +- `onSubscribe`: Handler called when the extension sends a `script.subscribe` request, returns subscription response - `onUnsubscribe`: Handler for script unsubscription notifications from viewer - `onSyntaxChange`: Handler for syntax change notifications - `onConnectionClosed`: Handler called when connection is closed diff --git a/indra/newview/llscripteditorws.cpp b/indra/newview/llscripteditorws.cpp index 11aacd791e..3ca9be44bc 100644 --- a/indra/newview/llscripteditorws.cpp +++ b/indra/newview/llscripteditorws.cpp @@ -2,6 +2,9 @@ * @file llscripteditorws.cpp * @brief JSON-RPC 2.0 WebSocket server implementation for external script editor integration * + * For a full description of the JSON-RPC protocol and all supported methods, + * see doc/external-editor-json-rpc.md in the repository root. + * * $LicenseInfo:firstyear=2025&license=viewerlgpl$ * Second Life Viewer Source Code * Copyright (C) 2025, Linden Research, Inc. @@ -272,6 +275,26 @@ void LLScriptEditorWSServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t c } return LLSD(); }); + script_connection->registerMethod("language.syntax.cache", + [that](const std::string&, const LLSD&, const LLSD& params) + { + auto server = that.lock(); + if (server) + { + return server->handleSyntaxCacheRequest(); + } + return LLSD(); + }); + script_connection->registerMethod("language.syntax.get", + [that](const std::string&, const LLSD&, const LLSD& params) + { + auto server = that.lock(); + if (server) + { + return server->handleSyntaxCacheFileRequest(params); + } + return LLSD(); + }); script_connection->registerMethod("script.subscribe", [that, connection_id](const std::string&, const LLSD&, const LLSD& params) -> LLSD { @@ -356,6 +379,71 @@ LLSD LLScriptEditorWSServer::handleSyntaxRequest(const LLSD& params) const return response; } +LLSD LLScriptEditorWSServer::handleSyntaxCacheRequest() const +{ + LLSD response; + // Add array of cached syntax definition files + LLSD syntax_files = LLSD::emptyArray(); + for (const auto& name : LLSyntaxDefCache::instance().getCacheFileNames()) + { + syntax_files.append(name); + } + response["files"] = syntax_files; + response["success"] = true; + return response; +} + +LLSD LLScriptEditorWSServer::handleSyntaxCacheFileRequest(const LLSD& params) const +{ + std::string filename = params["filename"].asString(); + bool as_json = params["as_json"].asBoolean(); + + LLSyntaxDefCache& cache = LLSyntaxDefCache::instance(); + LLSD response; + + if (filename.empty()) + { + response["error"] = "No filename specified"; + response["success"] = false; + return response; + } + if (!cache.hasCacheFile(filename)) + { + response["error"] = "Requested syntax cache file not found"; + response["success"] = false; + return response; + } + bool success = false; + if (as_json) + { + LLSD file_content = cache.loadCacheFileAsLLSD(filename); + if (file_content.isDefined()) + { + response["content"] = file_content; + success = true; + } + else + { + response["error"] = "Failed to load and format syntax cache file."; + } + } + else + { + std::string content = cache.loadCacheFile(filename); + if (!content.empty()) + { + response["content"] = content; + success = true; + } + else + { + response["error"] = "Failed to load syntax cache file"; + } + } + response["success"] = success; + return response; +} + LLSD LLScriptEditorWSServer::handleScriptSubscribe(U32 connection_id, const LLSD& params) { LLSD response(LLSD::emptyMap()); @@ -716,6 +804,7 @@ void LLScriptEditorWSConnection::onOpen() LLSD features; features["live_sync"] = true; features["compilation"] = true; + features["syntax_cache"] = true; handshake["features"] = features; wptr_t that = shared_from_this(); diff --git a/indra/newview/llscripteditorws.h b/indra/newview/llscripteditorws.h index c8898c66a7..765ebe83f2 100644 --- a/indra/newview/llscripteditorws.h +++ b/indra/newview/llscripteditorws.h @@ -192,6 +192,8 @@ protected: LLSD handleLanguageIdRequest() const; LLSD handleSyntaxRequest(const LLSD ¶ms) const; + LLSD handleSyntaxCacheRequest() const; + LLSD handleSyntaxCacheFileRequest(const LLSD& params) const; LLSD handleScriptSubscribe(U32 connection_id, const LLSD& params); LLSD handleScriptUnsubscribe(U32 connection_id, const LLSD& params); LLSD handleFileWatcherFileListRequest() const; diff --git a/indra/newview/llsyntaxid.cpp b/indra/newview/llsyntaxid.cpp index 3d0a9d80b4..3e6fc2d217 100644 --- a/indra/newview/llsyntaxid.cpp +++ b/indra/newview/llsyntaxid.cpp @@ -506,6 +506,16 @@ void LLSyntaxDefCache::loadKeywordsIntoLLSD() mSyntaxIDChangedSignal(); } +std::vector LLSyntaxDefCache::getCacheFileNames() const +{ + std::vector names; + for (const auto& [name, path] : mFileCachePaths) + { + names.push_back(name); + } + return names; +} + LLSD LLSyntaxDefCache::loadDeserializedCacheFile(const std::string& file_path) { std::ifstream file(file_path.c_str()); @@ -524,3 +534,32 @@ LLSD LLSyntaxDefCache::loadDeserializedCacheFile(const std::string& file_path) return LLSD(); } +std::string LLSyntaxDefCache::loadCacheFile(const std::string& name) const +{ + std::string full_path = mFileCachePaths.getPath(name); + if (!full_path.empty()) + { + std::ifstream file(full_path.c_str()); + if (file.good()) + { + std::ostringstream ss; + ss << file.rdbuf(); + return (file.good()) ? ss.str() : std::string(); + } + else + { + LL_WARNS("SyntaxLSL") << "Failed to open cached file: " << full_path << LL_ENDL; + } + } + return std::string(); +} + +LLSD LLSyntaxDefCache::loadCacheFileAsLLSD(const std::string& name) const +{ + std::string full_path = mFileCachePaths.getPath(name); + if (!full_path.empty()) + { + return loadDeserializedCacheFile(full_path); + } + return LLSD(); +} diff --git a/indra/newview/llsyntaxid.h b/indra/newview/llsyntaxid.h index 3bd49378b5..b38d02c10f 100644 --- a/indra/newview/llsyntaxid.h +++ b/indra/newview/llsyntaxid.h @@ -80,6 +80,9 @@ public: void clear() { mNamePathMap.clear(); } bool empty() const { return mNamePathMap.empty(); } + iterator find(const std::string &name) { return mNamePathMap.find(name); } + const_iterator find(const std::string &name) const { return mNamePathMap.find(name); } + private: name_path_map_t mNamePathMap; }; @@ -94,6 +97,21 @@ public: static std::string buildCacheDirectoryName(const LLUUID& syntax_id); + using const_iterator = cache_names_t::const_iterator; + + const_iterator begin() const { return mFileCachePaths.begin(); } + const_iterator end() const { return mFileCachePaths.end(); } + + size_t size() const { return mFileCachePaths.size(); } + bool empty() const { return mFileCachePaths.empty(); } + + const_iterator find(const std::string& name) const { return mFileCachePaths.find(name); } + + std::vector getCacheFileNames() const; + bool hasCacheFile(const std::string& name) const { return mFileCachePaths.hasName(name); } + std::string loadCacheFile(const std::string& name) const; + LLSD loadCacheFileAsLLSD(const std::string& name) const; + protected: void initSingleton() override; void cleanupSingleton() override; -- cgit v1.3 From 89a568167f419b34a7b61ca126c378cbb387bc45 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Wed, 29 Apr 2026 15:09:53 -0700 Subject: Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- doc/external-editor-json-rpc.md | 2 +- indra/newview/llsyntaxid.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'indra/newview/llsyntaxid.cpp') diff --git a/doc/external-editor-json-rpc.md b/doc/external-editor-json-rpc.md index 8d657b0d33..202f1e5b29 100644 --- a/doc/external-editor-json-rpc.md +++ b/doc/external-editor-json-rpc.md @@ -127,7 +127,7 @@ interface SessionHandshake { - `features`: Dictionary of feature flags indicating viewer capabilities. Known flags: - `live_sync`: Viewer supports live script synchronisation with the external editor - `compilation`: Viewer will forward compilation results via `script.compiled` - - `syntax__cache`: Viewer supports `language.syntax.cache` and `language.syntax.get` for retrieving syntax definition files + - `syntax_cache`: Viewer supports `language.syntax.cache` and `language.syntax.get` for retrieving syntax definition files ### SessionHandshakeResponse diff --git a/indra/newview/llsyntaxid.cpp b/indra/newview/llsyntaxid.cpp index 3e6fc2d217..0ded401d3e 100644 --- a/indra/newview/llsyntaxid.cpp +++ b/indra/newview/llsyntaxid.cpp @@ -411,14 +411,14 @@ std::string LLSyntaxDefCache::buildCacheDirectoryName(const LLUUID& syntax_id) if (syntax_id.isNull()) { LL_DEBUGS("SyntaxLSL") << "No SyntaxID, using app settings directory." << LL_ENDL; - return gDirUtilp->getExpandedFilename(LL_PATH_CACHE, "syntax_default"); + return gDirUtilp->getExpandedFilename(LL_PATH_APP_SETTINGS, "syntax_default"); } else { LL_DEBUGS("SyntaxLSL") << "Using cache directory for SyntaxID '" << syntax_id << "'." << LL_ENDL; std::string cache_dir_name = "syntax_" + syntax_id.asString(); - return gDirUtilp->getExpandedFilename(LL_PATH_APP_SETTINGS, cache_dir_name); + return gDirUtilp->getExpandedFilename(LL_PATH_CACHE, cache_dir_name); } } -- cgit v1.3 From 0f8be801753ade9db2c0dad66d124d71f7314b2d Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Wed, 29 Apr 2026 15:22:50 -0700 Subject: trigger the pre-commit. --- indra/newview/llsyntaxid.cpp | 3 ++- indra/newview/llsyntaxid.h | 7 ++----- 2 files changed, 4 insertions(+), 6 deletions(-) (limited to 'indra/newview/llsyntaxid.cpp') diff --git a/indra/newview/llsyntaxid.cpp b/indra/newview/llsyntaxid.cpp index 0ded401d3e..ad10cee3dd 100644 --- a/indra/newview/llsyntaxid.cpp +++ b/indra/newview/llsyntaxid.cpp @@ -35,6 +35,7 @@ #include "llviewerregion.h" #include "llcorehttputil.h" + //----------------------------------------------------------------------------- // LLSyntaxIdLSL //----------------------------------------------------------------------------- @@ -226,7 +227,7 @@ void LLSyntaxDefCache::fetchKeywordsFileCoro(std::string url, LLUUID syntax_id) // to the default versions, so below, where we get the path to the lua keywords // well be loading the default version. buildDefaultCache(); - + // The LSL keywords we just received std::string full_path = gDirUtilp->add(path, FILENAME_INTERNAL_LSL); if (writeCacheFile(full_path, result)) diff --git a/indra/newview/llsyntaxid.h b/indra/newview/llsyntaxid.h index b38d02c10f..acf8020d58 100644 --- a/indra/newview/llsyntaxid.h +++ b/indra/newview/llsyntaxid.h @@ -25,8 +25,7 @@ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA * $/LicenseInfo$ */ -#ifndef LL_SYNTAXID_H -#define LL_SYNTAXID_H +#pragma once #include "llviewerprecompiledheaders.h" @@ -122,7 +121,7 @@ private: static bool isSupportedVersion(const LLSD& content); void handleRegionChanged(); void handleCapsReceived(const LLUUID& region_uuid); - void setKeywords(const LLSD& lsl, const LLSD& lua) { mLSLKeywords = lsl; mLuaKeywords = lua; }; + void setKeywords(const LLSD& lsl, const LLSD& lua) { mLSLKeywords = lsl; mLuaKeywords = lua; }; void loadKeywordsIntoLLSD(); @@ -148,5 +147,3 @@ private: LLSD mLuaKeywords; bool mUseDefsCap{ false }; }; - -#endif // LLSYNTAXID_H -- cgit v1.3 From e932b04e87cefd922800c9ca24382f9a9c461b2d Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Wed, 29 Apr 2026 15:36:21 -0700 Subject: Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- indra/newview/llsyntaxid.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) (limited to 'indra/newview/llsyntaxid.cpp') diff --git a/indra/newview/llsyntaxid.cpp b/indra/newview/llsyntaxid.cpp index ad10cee3dd..6670809c85 100644 --- a/indra/newview/llsyntaxid.cpp +++ b/indra/newview/llsyntaxid.cpp @@ -167,19 +167,19 @@ void LLSyntaxDefCache::fetchKeywords() } if (mUseDefsCap) { - LLCoros::instance().launch("LLSyntaxIdLSL::fetchKeywordsDefsCoro", + LLCoros::instance().launch("LLSyntaxDefCache::fetchKeywordsDefsCoro", boost::bind(&LLSyntaxDefCache::fetchKeywordsDefsCoro, this, mCapabilityURL, mSyntaxId)); } else { - LLCoros::instance().launch("LLSyntaxIdLSL::fetchKeywordsFileCoro", + LLCoros::instance().launch("LLSyntaxDefCache::fetchKeywordsFileCoro", boost::bind(&LLSyntaxDefCache::fetchKeywordsFileCoro, this, mCapabilityURL, mSyntaxId)); } } //----------------------------------------------------------------------------- // fetchKeywordsFileCoro -// This uses the legacy languge cap which only sends the LSL keywords file. +// This uses the legacy language cap which only sends the LSL keywords file. void LLSyntaxDefCache::fetchKeywordsFileCoro(std::string url, LLUUID syntax_id) { LLCore::HttpRequest::policy_t httpPolicy(LLCore::HttpRequest::DEFAULT_POLICY_ID); @@ -324,7 +324,7 @@ void LLSyntaxDefCache::fetchKeywordsDefsCoro(std::string url, LLUUID syntax_id) result.erase(LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS); - setKeywords(memcached_keywords["lsl_keywords.xml"], memcached_keywords["lua_keywords.xml"]); + setKeywords(memcached_keywords[FILENAME_INTERNAL_LSL], memcached_keywords[FILENAME_INTERNAL_LUA]); LLAppViewer::instance()->postToMainCoro( [this]() { @@ -363,8 +363,10 @@ void LLSyntaxDefCache::buildCachePaths(const LLUUID &syntax_id) bool LLSyntaxDefCache::writeCacheFile(const std::string &fileSpec, const LLSD& content_ref) { - bool binary(content_ref.isBinary()); - std::ofstream file(fileSpec.c_str(), (binary)? std::ios_base::binary : 0); + bool binary(content_ref.isBinary()); + std::ios_base::openmode mode(binary ? (std::ios_base::out | std::ios_base::binary) + : std::ios_base::out); + std::ofstream file(fileSpec.c_str(), mode); if (!file.is_open()) { -- cgit v1.3 From a453db20bb49193806fb0cafeb4d50db4a16abe4 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Tue, 5 May 2026 11:05:11 -0700 Subject: Update to new lsl-definitions --- autobuild.xml | 88 ++++++++++++++++++++-------------------- indra/cmake/LSLDefinitions.cmake | 4 +- indra/newview/llsyntaxid.cpp | 2 +- 3 files changed, 47 insertions(+), 47 deletions(-) (limited to 'indra/newview/llsyntaxid.cpp') diff --git a/autobuild.xml b/autobuild.xml index feca45d6d8..3f07900e58 100644 --- a/autobuild.xml +++ b/autobuild.xml @@ -1326,18 +1326,6 @@ lsl_definitions - canonical_repo - https://github.com/secondlife/lsl-definitions - copyright - Copyright (c) 2026, Linden Lab - description - LSL definitions - license - MIT - license_file - LICENSES/lsl_definitions.txt - name - lsl_definitions platforms common @@ -1345,28 +1333,40 @@ archive hash - 6e4e3b86f5daf6dc2e7d7b2b59deaaad293b7232 + 9e8a23b240897ca2e98f6700fae3bec46ad0d0f2 hash_algorithm sha1 url - https://github.com/secondlife/lsl-definitions/releases/download/v0.4.1/lsl_definitions-0.4.1-common-22871771849.tar.zst + https://github.com/secondlife/lsl-definitions/releases/download/v0.6.3/lsl_definitions-0.6.3-common-25336758775.tar.zst name common - source_type - git + license + MIT + license_file + LICENSES/lsl_definitions.txt + copyright + Copyright (c) 2026, Linden Lab + version + 0.6.3 use_scm_version true + name + lsl_definitions vcs_branch refs/tags/v0.4.1 vcs_revision 03006fb488cba2bae502e9baed051bfc8d00144d vcs_url git://github.com/secondlife/lsl-definitions.git - version - 0.4.1 + canonical_repo + https://github.com/secondlife/lsl-definitions + description + LSL definitions + source_type + git meshoptimizer @@ -2473,16 +2473,6 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors webrtc - canonical_repo - https://github.com/secondlife/3p-webrtc-build - copyright - Copyright (c) 2011, The WebRTC project authors. All rights reserved. - license - MIT - license_file - LICENSES/webrtc-license.txt - name - webrtc platforms darwin64 @@ -2528,14 +2518,24 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors windows64 + license + MIT + license_file + LICENSES/webrtc-license.txt + copyright + Copyright (c) 2011, The WebRTC project authors. All rights reserved. + version + m137.7151.04.23.22004231636 + name + webrtc vcs_branch secondlife vcs_revision d3f62d32bac8694d3c7423c731ae30c113bf6a11 vcs_url https://github.com/secondlife/3p-webrtc-build - version - m137.7151.04.23.22004231636 + canonical_repo + https://github.com/secondlife/3p-webrtc-build xxhash @@ -2813,18 +2813,6 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors websocketpp - canonical_repo - https://github.com/secondlife/3p-websocketpp - copyright - Copyright (c) 2014, Peter Thorson - description - WebSocket++ is a C++ header only library for interacting with WebSocket servers and clients. - license - websocketpp - license_file - LICENSES/websocketpp.txt - name - websocketpp platforms common @@ -2842,14 +2830,26 @@ Copyright (c) 2012, 2014, 2015, 2016 nghttp2 contributors common + license + websocketpp + license_file + LICENSES/websocketpp.txt + copyright + Copyright (c) 2014, Peter Thorson + version + 0.8.2.24525603568 + name + websocketpp vcs_branch refs/tags/v0.8.2 vcs_revision bdcf1453101976fc4dc26a62c87bc98c12e9c6dc vcs_url git://github.com/secondlife/3p-websocketpp.git - version - 0.8.2.24525603568 + canonical_repo + https://github.com/secondlife/3p-websocketpp + description + WebSocket++ is a C++ header only library for interacting with WebSocket servers and clients. package_description diff --git a/indra/cmake/LSLDefinitions.cmake b/indra/cmake/LSLDefinitions.cmake index ae0eeebb7e..1a9a477118 100644 --- a/indra/cmake/LSLDefinitions.cmake +++ b/indra/cmake/LSLDefinitions.cmake @@ -3,7 +3,7 @@ include(Prebuilt) use_prebuilt_binary(lsl_definitions) -configure_file("${AUTOBUILD_INSTALL_DIR}/lsl_definitions/lsl_keywords_pretty.xml" +configure_file("${AUTOBUILD_INSTALL_DIR}/lsl_definitions/lsl_keywords.xml" "${CMAKE_SOURCE_DIR}/newview/app_settings/keywords_lsl_default.xml" COPYONLY) -configure_file("${AUTOBUILD_INSTALL_DIR}/lsl_definitions/slua_keywords_pretty.xml" +configure_file("${AUTOBUILD_INSTALL_DIR}/lsl_definitions/lua_keywords.xml" "${CMAKE_SOURCE_DIR}/newview/app_settings/keywords_lua_default.xml" COPYONLY) diff --git a/indra/newview/llsyntaxid.cpp b/indra/newview/llsyntaxid.cpp index 6670809c85..9c1a0ef403 100644 --- a/indra/newview/llsyntaxid.cpp +++ b/indra/newview/llsyntaxid.cpp @@ -45,7 +45,7 @@ namespace const std::string SYNTAX_DEF_CAPABILITY_NAME = "ScriptDefinitions"; const std::string SYNTAX_ID_SIMULATOR_FEATURE = "LSLSyntaxId"; const std::string FILENAME_INTERNAL_LSL = "lsl_keywords.xml"; - const std::string FILENAME_INTERNAL_LUA = "slua_keywords.xml"; + const std::string FILENAME_INTERNAL_LUA = "lua_keywords.xml"; constexpr U32 LLSD_SYNTAX_LSL_VERSION_EXPECTED = 2; const std::string LLSD_SYNTAX_LSL_VERSION_KEY("llsd-lsl-syntax-version"); -- cgit v1.3