/** * @file llaisapi.cpp * @brief classes and functions for interfacing with the v3+ ais inventory service. * * $LicenseInfo:firstyear=2013&license=viewerlgpl$ * Second Life Viewer Source Code * Copyright (C) 2013, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License only. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA * $/LicenseInfo$ * */ #include "llviewerprecompiledheaders.h" #include "llaisapi.h" #include "llagent.h" #include "llappviewer.h" #include "llcallbacklist.h" #include "llinventorymodel.h" #include "llsdutil.h" #include "llviewerregion.h" #include "llvoavatar.h" #include "llvoavatarself.h" #include "llinventoryobserver.h" #include "llviewercontrol.h" ///---------------------------------------------------------------------------- /// Classes for AISv3 support. ///---------------------------------------------------------------------------- //========================================================================= const std::string AISAPI::INVENTORY_CAP_NAME("InventoryAPIv3"); const std::string AISAPI::LIBRARY_CAP_NAME("LibraryAPIv3"); std::list AISAPI::sPostponedQuery; const S32 MAX_SIMULTANEOUS_COROUTINES = 2048; //------------------------------------------------------------------------- /*static*/ bool AISAPI::isAvailable() { if (gAgent.getRegion()) { return gAgent.getRegion()->isCapabilityAvailable(INVENTORY_CAP_NAME); } return false; } /*static*/ void AISAPI::getCapNames(LLSD& capNames) { capNames.append(INVENTORY_CAP_NAME); capNames.append(LIBRARY_CAP_NAME); } /*static*/ std::string AISAPI::getInvCap() { if (gAgent.getRegion()) { return gAgent.getRegion()->getCapability(INVENTORY_CAP_NAME); } return std::string(); } /*static*/ std::string AISAPI::getLibCap() { if (gAgent.getRegion()) { return gAgent.getRegion()->getCapability(LIBRARY_CAP_NAME); } return std::string(); } /*static*/ void AISAPI::CreateInventory(const LLUUID& parentId, const LLSD& newInventory, completion_t callback) { std::string cap = getInvCap(); if (cap.empty()) { LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL; if (callback) { callback(LLUUID::null); } return; } LLUUID tid; tid.generate(); std::string url = cap + std::string("/category/") + parentId.asString() + "?tid=" + tid.asString(); LL_DEBUGS("Inventory") << "url: " << url << " parentID " << parentId << " newInventory " << newInventory << LL_ENDL; // I may be suffering from golden hammer here, but the first part of this bind // is actually a static cast for &HttpCoroutineAdapter::postAndSuspend so that // the compiler can identify the correct signature to select. // // Reads as follows: // LLSD - method returning LLSD // (LLCoreHttpUtil::HttpCoroutineAdapter::*) - pointer to member function of HttpCoroutineAdapter // (LLCore::HttpRequest::ptr_t, const std::string &, const LLSD &, LLCore::HttpOptions::ptr_t, LLCore::HttpHeaders::ptr_t) - signature of method // invokationFn_t postFn = boost::bind( // Humans ignore next line. It is just a cast. static_cast //---- // _1 -> httpAdapter // _2 -> httpRequest // _3 -> url // _4 -> body // _5 -> httpOptions // _6 -> httpHeaders (&LLCoreHttpUtil::HttpCoroutineAdapter::postAndSuspend), _1, _2, _3, _4, _5, _6); LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro, _1, postFn, url, parentId, newInventory, callback, CREATEINVENTORY)); EnqueueAISCommand("CreateInventory", proc); } /*static*/ void AISAPI::SlamFolder(const LLUUID& folderId, const LLSD& newInventory, completion_t callback) { std::string cap = getInvCap(); if (cap.empty()) { LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL; if (callback) { callback(LLUUID::null); } return; } LLUUID tid; tid.generate(); std::string url = cap + std::string("/category/") + folderId.asString() + "/links?tid=" + tid.asString(); // see comment above in CreateInventoryCommand invokationFn_t putFn = boost::bind( // Humans ignore next line. It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload. static_cast //---- // _1 -> httpAdapter // _2 -> httpRequest // _3 -> url // _4 -> body // _5 -> httpOptions // _6 -> httpHeaders (&LLCoreHttpUtil::HttpCoroutineAdapter::putAndSuspend), _1, _2, _3, _4, _5, _6); LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro, _1, putFn, url, folderId, newInventory, callback, SLAMFOLDER)); EnqueueAISCommand("SlamFolder", proc); } void AISAPI::RemoveCategory(const LLUUID &categoryId, completion_t callback) { std::string cap; cap = getInvCap(); if (cap.empty()) { LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL; if (callback) { callback(LLUUID::null); } return; } std::string url = cap + std::string("/category/") + categoryId.asString(); LL_DEBUGS("Inventory") << "url: " << url << LL_ENDL; invokationFn_t delFn = boost::bind( // Humans ignore next line. It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload. static_cast //---- // _1 -> httpAdapter // _2 -> httpRequest // _3 -> url // _4 -> body // _5 -> httpOptions // _6 -> httpHeaders (&LLCoreHttpUtil::HttpCoroutineAdapter::deleteAndSuspend), _1, _2, _3, _5, _6); LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro, _1, delFn, url, categoryId, LLSD(), callback, REMOVECATEGORY)); EnqueueAISCommand("RemoveCategory", proc); } /*static*/ void AISAPI::RemoveItem(const LLUUID &itemId, completion_t callback) { std::string cap; cap = getInvCap(); if (cap.empty()) { LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL; if (callback) { callback(LLUUID::null); } return; } std::string url = cap + std::string("/item/") + itemId.asString(); LL_DEBUGS("Inventory") << "url: " << url << LL_ENDL; invokationFn_t delFn = boost::bind( // Humans ignore next line. It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload. static_cast //---- // _1 -> httpAdapter // _2 -> httpRequest // _3 -> url // _4 -> body // _5 -> httpOptions // _6 -> httpHeaders (&LLCoreHttpUtil::HttpCoroutineAdapter::deleteAndSuspend), _1, _2, _3, _5, _6); LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro, _1, delFn, url, itemId, LLSD(), callback, REMOVEITEM)); EnqueueAISCommand("RemoveItem", proc); } void AISAPI::CopyLibraryCategory(const LLUUID& sourceId, const LLUUID& destId, bool copySubfolders, completion_t callback) { std::string cap; cap = getLibCap(); if (cap.empty()) { LL_WARNS("Inventory") << "Library cap not found!" << LL_ENDL; if (callback) { callback(LLUUID::null); } return; } LL_DEBUGS("Inventory") << "Copying library category: " << sourceId << " => " << destId << LL_ENDL; LLUUID tid; tid.generate(); std::string url = cap + std::string("/category/") + sourceId.asString() + "?tid=" + tid.asString(); if (!copySubfolders) { url += ",depth=0"; } LL_INFOS() << url << LL_ENDL; std::string destination = destId.asString(); invokationFn_t copyFn = boost::bind( // Humans ignore next line. It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload. static_cast //---- // _1 -> httpAdapter // _2 -> httpRequest // _3 -> url // _4 -> body // _5 -> httpOptions // _6 -> httpHeaders (&LLCoreHttpUtil::HttpCoroutineAdapter::copyAndSuspend), _1, _2, _3, destination, _5, _6); LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro, _1, copyFn, url, destId, LLSD(), callback, COPYLIBRARYCATEGORY)); EnqueueAISCommand("CopyLibraryCategory", proc); } /*static*/ void AISAPI::PurgeDescendents(const LLUUID &categoryId, completion_t callback) { std::string cap; cap = getInvCap(); if (cap.empty()) { LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL; if (callback) { callback(LLUUID::null); } return; } std::string url = cap + std::string("/category/") + categoryId.asString() + "/children"; LL_DEBUGS("Inventory") << "url: " << url << LL_ENDL; invokationFn_t delFn = boost::bind( // Humans ignore next line. It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload. static_cast //---- // _1 -> httpAdapter // _2 -> httpRequest // _3 -> url // _4 -> body // _5 -> httpOptions // _6 -> httpHeaders (&LLCoreHttpUtil::HttpCoroutineAdapter::deleteAndSuspend), _1, _2, _3, _5, _6); LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro, _1, delFn, url, categoryId, LLSD(), callback, PURGEDESCENDENTS)); EnqueueAISCommand("PurgeDescendents", proc); } /*static*/ void AISAPI::UpdateCategory(const LLUUID &categoryId, const LLSD &updates, completion_t callback) { std::string cap; cap = getInvCap(); if (cap.empty()) { LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL; if (callback) { callback(LLUUID::null); } return; } std::string url = cap + std::string("/category/") + categoryId.asString(); invokationFn_t patchFn = boost::bind( // Humans ignore next line. It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload. static_cast //---- // _1 -> httpAdapter // _2 -> httpRequest // _3 -> url // _4 -> body // _5 -> httpOptions // _6 -> httpHeaders (&LLCoreHttpUtil::HttpCoroutineAdapter::patchAndSuspend), _1, _2, _3, _4, _5, _6); LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro, _1, patchFn, url, categoryId, updates, callback, UPDATECATEGORY)); EnqueueAISCommand("UpdateCategory", proc); } /*static*/ void AISAPI::UpdateItem(const LLUUID &itemId, const LLSD &updates, completion_t callback) { std::string cap; cap = getInvCap(); if (cap.empty()) { LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL; if (callback) { callback(LLUUID::null); } return; } std::string url = cap + std::string("/item/") + itemId.asString(); invokationFn_t patchFn = boost::bind( // Humans ignore next line. It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload. static_cast //---- // _1 -> httpAdapter // _2 -> httpRequest // _3 -> url // _4 -> body // _5 -> httpOptions // _6 -> httpHeaders (&LLCoreHttpUtil::HttpCoroutineAdapter::patchAndSuspend), _1, _2, _3, _4, _5, _6); LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro, _1, patchFn, url, itemId, updates, callback, UPDATEITEM)); EnqueueAISCommand("UpdateItem", proc); } /*static*/ void AISAPI::FetchItem(const LLUUID &itemId, ITEM_TYPE type, completion_t callback) { std::string cap; cap = (type == INVENTORY) ? getInvCap() : getLibCap(); if (cap.empty()) { LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL; if (callback) { callback(LLUUID::null); } return; } std::string url = cap + std::string("/item/") + itemId.asString(); invokationFn_t getFn = boost::bind( // Humans ignore next line. It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload. static_cast //---- // _1 -> httpAdapter // _2 -> httpRequest // _3 -> url // _4 -> body // _5 -> httpOptions // _6 -> httpHeaders (&LLCoreHttpUtil::HttpCoroutineAdapter::getAndSuspend), _1, _2, _3, _5, _6); LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro, _1, getFn, url, itemId, LLSD(), callback, FETCHITEM)); EnqueueAISCommand("FetchItem", proc); } /*static*/ void AISAPI::FetchCategoryChildren(const LLUUID &catId, ITEM_TYPE type, bool recursive, completion_t callback, S32 depth) { std::string cap; cap = (type == INVENTORY) ? getInvCap() : getLibCap(); if (cap.empty()) { LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL; if (callback) { callback(LLUUID::null); } return; } std::string url = cap + std::string("/category/") + catId.asString() + "/children"; if (recursive) { // can specify depth=*, but server side is going to cap requests // and reject everything 'over the top',. depth = 50; } else { depth = llmax(depth, 50); } url += "?depth=" + std::to_string(depth); invokationFn_t getFn = boost::bind( // Humans ignore next line. It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload. static_cast //---- // _1 -> httpAdapter // _2 -> httpRequest // _3 -> url // _4 -> body // _5 -> httpOptions // _6 -> httpHeaders (&LLCoreHttpUtil::HttpCoroutineAdapter::getAndSuspend), _1, _2, _3, _5, _6); // get doesn't use body, can pass additional data LLSD body; body["depth"] = depth; LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro, _1, getFn, url, catId, body, callback, FETCHCATEGORYCHILDREN)); EnqueueAISCommand("FetchCategoryChildren", proc); } // some folders can be requested by name, like // animatn | bodypart | clothing | current | favorite | gesture | inbox | landmark | lsltext // lstndfnd | my_otfts | notecard | object | outbox | root | snapshot | sound | texture | trash void AISAPI::FetchCategoryChildren(const std::string &identifier, bool recursive, completion_t callback, S32 depth) { std::string cap; cap = getInvCap(); if (cap.empty()) { LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL; if (callback) { callback(LLUUID::null); } return; } std::string url = cap + std::string("/category/") + identifier + "/children"; if (recursive) { // can specify depth=*, but server side is going to cap requests // and reject everything 'over the top',. depth = 50; } else { depth = llmax(depth, 50); } url += "?depth=" + std::to_string(depth); invokationFn_t getFn = boost::bind( // Humans ignore next line. It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload. static_cast //---- // _1 -> httpAdapter // _2 -> httpRequest // _3 -> url // _4 -> body // _5 -> httpOptions // _6 -> httpHeaders (&LLCoreHttpUtil::HttpCoroutineAdapter::getAndSuspend), _1, _2, _3, _5, _6); // get doesn't use body, can pass additional data LLSD body; body["depth"] = depth; LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro, _1, getFn, url, LLUUID::null, body, callback, FETCHCATEGORYCHILDREN)); EnqueueAISCommand("FetchCategoryChildren", proc); } /*static*/ void AISAPI::FetchCategoryCategories(const LLUUID &catId, ITEM_TYPE type, bool recursive, completion_t callback, S32 depth) { std::string cap; cap = (type == INVENTORY) ? getInvCap() : getLibCap(); if (cap.empty()) { LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL; if (callback) { callback(LLUUID::null); } return; } std::string url = cap + std::string("/category/") + catId.asString() + "/categories"; if (recursive) { // can specify depth=*, but server side is going to cap requests // and reject everything 'over the top',. depth = 50; } else { depth = llmax(depth, 50); } url += "?depth=" + std::to_string(depth); invokationFn_t getFn = boost::bind( // Humans ignore next line. It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload. static_cast //---- // _1 -> httpAdapter // _2 -> httpRequest // _3 -> url // _4 -> body // _5 -> httpOptions // _6 -> httpHeaders (&LLCoreHttpUtil::HttpCoroutineAdapter::getAndSuspend), _1, _2, _3, _5, _6); // get doesn't use body, can pass additional data LLSD body; body["depth"] = depth; LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro, _1, getFn, url, catId, body, callback, FETCHCATEGORYCATEGORIES)); EnqueueAISCommand("FetchCategoryCategories", proc); } /*static*/ void AISAPI::FetchOrphans(completion_t callback) { std::string cap = getInvCap(); if (cap.empty()) { LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL; if (callback) { callback(LLUUID::null); } return; } std::string url = cap + std::string("/orphans"); invokationFn_t getFn = boost::bind( // Humans ignore next line. It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload. static_cast //---- // _1 -> httpAdapter // _2 -> httpRequest // _3 -> url // _4 -> body // _5 -> httpOptions // _6 -> httpHeaders (&LLCoreHttpUtil::HttpCoroutineAdapter::getAndSuspend) , _1 , _2 , _3 , _5 , _6); LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro , _1 , getFn , url , LLUUID::null , LLSD() , callback , FETCHORPHANS)); EnqueueAISCommand("FetchOrphans" , proc); } /*static*/ void AISAPI::EnqueueAISCommand(const std::string &procName, LLCoprocedureManager::CoProcedure_t proc) { LLCoprocedureManager &inst = LLCoprocedureManager::instance(); S32 pending_in_pool = inst.countPending("AIS"); std::string procFullName = "AIS(" + procName + ")"; if (pending_in_pool < MAX_SIMULTANEOUS_COROUTINES) { inst.enqueueCoprocedure("AIS", procFullName, proc); } else { // As I understand it, coroutines have built-in 'pending' pool // but unfortunately it has limited size which inventory often goes over // so this is a workaround to not overfill it. if (sPostponedQuery.empty()) { sPostponedQuery.push_back(ais_query_item_t(procFullName, proc)); gIdleCallbacks.addFunction(onIdle, NULL); } else { sPostponedQuery.push_back(ais_query_item_t(procFullName, proc)); } } } /*static*/ void AISAPI::onIdle(void *userdata) { if (!sPostponedQuery.empty()) { LLCoprocedureManager &inst = LLCoprocedureManager::instance(); S32 pending_in_pool = inst.countPending("AIS"); while (pending_in_pool < MAX_SIMULTANEOUS_COROUTINES && !sPostponedQuery.empty()) { ais_query_item_t &item = sPostponedQuery.front(); inst.enqueueCoprocedure("AIS", item.first, item.second); sPostponedQuery.pop_front(); pending_in_pool++; } } if (sPostponedQuery.empty()) { // Nothing to do anymore gIdleCallbacks.deleteFunction(onIdle, NULL); } } /*static*/ void AISAPI::onUpdateReceived(const std::string& context, const LLSD& update, COMMAND_TYPE type, const LLSD& request_body) { LLTimer timer; if ( (type == UPDATECATEGORY || type == UPDATEITEM) && gSavedSettings.getBOOL("DebugAvatarAppearanceMessage")) { dump_sequential_xml(gAgentAvatarp->getFullname() + "_ais_update", update); } bool is_fetch = (type == FETCHITEM) || (type == FETCHCATEGORYCHILDREN) || (type == FETCHCATEGORYCATEGORIES) || (type == FETCHORPHANS); // parse update llsd into stuff to do or parse received items. S32 depth = 0; if (is_fetch && request_body.has("depth")) { depth = request_body["depth"].asInteger(); } AISUpdate ais_update(update, is_fetch, depth); ais_update.doUpdate(); // execute the updates in the appropriate order. LL_DEBUGS("Inventory", "AIS3") << "Elapsed processing: " << timer.getElapsedTimeF32() << LL_ENDL; } /*static*/ void AISAPI::InvokeAISCommandCoro(LLCoreHttpUtil::HttpCoroutineAdapter::ptr_t httpAdapter, invokationFn_t invoke, std::string url, LLUUID targetId, LLSD body, completion_t callback, COMMAND_TYPE type) { if (gDisconnected) { if (callback) { callback(LLUUID::null); } return; } LLCore::HttpOptions::ptr_t httpOptions(new LLCore::HttpOptions); LLCore::HttpRequest::ptr_t httpRequest(new LLCore::HttpRequest()); LLCore::HttpHeaders::ptr_t httpHeaders; httpOptions->setTimeout(180); LL_DEBUGS("Inventory") << "Request url: " << url << LL_ENDL; LLSD result; LLSD httpResults; LLCore::HttpStatus status; if (debugLoggingEnabled("Inventory")) { LLTimer ais_timer; ais_timer.start(); result = invoke(httpAdapter , httpRequest , url , body , httpOptions , httpHeaders); httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS]; status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults); F32MillisecondsImplicit elapsed_time = ais_timer.getElapsedTimeF32(); LL_DEBUGS("Inventory") << "Request type: " << (S32)type << " \nRequest target: " << targetId << " \nElapsed time ince request: " << elapsed_time << " \nstatus: " << status.toULong() << LL_ENDL; } else { result = invoke(httpAdapter , httpRequest , url , body , httpOptions , httpHeaders); httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS]; status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults); } if (!status || !result.isMap()) { if (!result.isMap()) { status = LLCore::HttpStatus(HTTP_INTERNAL_ERROR, "Malformed response contents"); } else if (status.getType() == 410) //GONE { // Item does not exist or was already deleted from server. // parent folder is out of sync if (type == REMOVECATEGORY) { LLViewerInventoryCategory *cat = gInventory.getCategory(targetId); if (cat) { LL_WARNS("Inventory") << "Purge failed for '" << cat->getName() << "' local version:" << cat->getVersion() << " since folder no longer exists at server. Descendent count: server == " << cat->getDescendentCount() << ", viewer == " << cat->getViewerDescendentCount() << LL_ENDL; gInventory.fetchDescendentsOf(cat->getParentUUID()); // Note: don't delete folder here - contained items will be deparented (or deleted) // and since we are clearly out of sync we can't be sure we won't get rid of something we need. // For example folder could have been moved or renamed with items intact, let it fetch first. } } else if (type == REMOVEITEM) { LLViewerInventoryItem *item = gInventory.getItem(targetId); if (item) { LL_WARNS("Inventory") << "Purge failed for '" << item->getName() << "' since item no longer exists at server." << LL_ENDL; gInventory.fetchDescendentsOf(item->getParentUUID()); // since item not on the server and exists at viewer, so it needs an update at the least, // so delete it, in worst case item will be refetched with new params. gInventory.onObjectDeletedFromServer(targetId); } } } else if (status.getType() == 403) { if (type == FETCHCATEGORYCHILDREN) { LL_DEBUGS("Inventory") << "Fetch failed, content is over imit" << LL_ENDL; } } LL_WARNS("Inventory") << "Inventory error: " << status.toString() << LL_ENDL; LL_WARNS("Inventory") << ll_pretty_print_sd(result) << LL_ENDL; } LL_DEBUGS("Inventory", "AIS3") << "Result: " << result << LL_ENDL; onUpdateReceived("AISCommand", result, type, body); if (callback && !callback.empty()) { bool needs_callback = true; LLUUID id(LLUUID::null); switch (type) { case COPYLIBRARYCATEGORY: case FETCHCATEGORYCATEGORIES: case FETCHCATEGORYCHILDREN: if (result.has("category_id")) { id = result["category_id"]; } break; case FETCHITEM: if (result.has("item_id")) { id = result["item_id"]; } if (result.has("linked_id")) { id = result["linked_id"]; } break; case CREATEINVENTORY: // CREATEINVENTORY can have multiple callbacks if (result.has("_created_categories")) { LLSD& cats = result["_created_categories"]; LLSD::array_const_iterator cat_iter; for (cat_iter = cats.beginArray(); cat_iter != cats.endArray(); ++cat_iter) { LLUUID cat_id = *cat_iter; callback(cat_id); needs_callback = false; } } if (result.has("_created_items")) { LLSD& items = result["_created_items"]; LLSD::array_const_iterator item_iter; for (item_iter = items.beginArray(); item_iter != items.endArray(); ++item_iter) { LLUUID item_id = *item_iter; callback(item_id); needs_callback = false; } } break; default: break; } if (needs_callback) { // Call callback at least once regardless of failure. // UPDATEITEM doesn't expect an id callback(id); } } } //------------------------------------------------------------------------- AISUpdate::AISUpdate(const LLSD& update, bool fetch, S32 depth) : mFetch(fetch) , mFetchDepth(depth) { parseUpdate(update); } void AISUpdate::clearParseResults() { mCatDescendentDeltas.clear(); mCatDescendentsKnown.clear(); mCatVersionsUpdated.clear(); mItemsCreated.clear(); mItemsLost.clear(); mItemsUpdated.clear(); mCategoriesCreated.clear(); mCategoriesUpdated.clear(); mObjectsDeletedIds.clear(); mItemIds.clear(); mCategoryIds.clear(); } void AISUpdate::parseUpdate(const LLSD& update) { clearParseResults(); parseMeta(update); parseContent(update); } void AISUpdate::parseMeta(const LLSD& update) { // parse _categories_removed -> mObjectsDeletedIds uuid_list_t cat_ids; parseUUIDArray(update,"_categories_removed",cat_ids); for (uuid_list_t::const_iterator it = cat_ids.begin(); it != cat_ids.end(); ++it) { LLViewerInventoryCategory *cat = gInventory.getCategory(*it); if(cat) { mCatDescendentDeltas[cat->getParentUUID()]--; mObjectsDeletedIds.insert(*it); } else { LL_WARNS("Inventory") << "removed category not found " << *it << LL_ENDL; } } // parse _categories_items_removed -> mObjectsDeletedIds uuid_list_t item_ids; parseUUIDArray(update,"_category_items_removed",item_ids); parseUUIDArray(update,"_removed_items",item_ids); for (uuid_list_t::const_iterator it = item_ids.begin(); it != item_ids.end(); ++it) { LLViewerInventoryItem *item = gInventory.getItem(*it); if(item) { mCatDescendentDeltas[item->getParentUUID()]--; mObjectsDeletedIds.insert(*it); } else { LL_WARNS("Inventory") << "removed item not found " << *it << LL_ENDL; } } // parse _broken_links_removed -> mObjectsDeletedIds uuid_list_t broken_link_ids; parseUUIDArray(update,"_broken_links_removed",broken_link_ids); for (uuid_list_t::const_iterator it = broken_link_ids.begin(); it != broken_link_ids.end(); ++it) { LLViewerInventoryItem *item = gInventory.getItem(*it); if(item) { mCatDescendentDeltas[item->getParentUUID()]--; mObjectsDeletedIds.insert(*it); } else { LL_WARNS("Inventory") << "broken link not found " << *it << LL_ENDL; } } // parse _created_items parseUUIDArray(update,"_created_items",mItemIds); // parse _created_categories parseUUIDArray(update,"_created_categories",mCategoryIds); // Parse updated category versions. const std::string& ucv = "_updated_category_versions"; if (update.has(ucv)) { for(LLSD::map_const_iterator it = update[ucv].beginMap(), end = update[ucv].endMap(); it != end; ++it) { const LLUUID id((*it).first); S32 version = (*it).second.asInteger(); mCatVersionsUpdated[id] = version; } } } void AISUpdate::parseContent(const LLSD& update) { if (update.has("linked_id")) { parseLink(update); } else if (update.has("item_id")) { parseItem(update); } if (update.has("category_id")) { parseCategory(update, mFetchDepth); } else { if (update.has("_embedded")) { parseEmbedded(update["_embedded"], mFetchDepth); } } } void AISUpdate::parseItem(const LLSD& item_map) { LLUUID item_id = item_map["item_id"].asUUID(); LLPointer new_item(new LLViewerInventoryItem); LLViewerInventoryItem *curr_item = gInventory.getItem(item_id); if (curr_item) { // Default to current values where not provided. new_item->copyViewerItem(curr_item); } BOOL rv = new_item->unpackMessage(item_map); if (rv) { if (mFetch) { mItemsCreated[item_id] = new_item; mCatDescendentDeltas[new_item->getParentUUID()]; new_item->setComplete(true); if (new_item->getParentUUID().isNull()) { mItemsLost[item_id] = new_item; } } else if (curr_item) { mItemsUpdated[item_id] = new_item; // This statement is here to cause a new entry with 0 // delta to be created if it does not already exist; // otherwise has no effect. mCatDescendentDeltas[new_item->getParentUUID()]; } else { mItemsCreated[item_id] = new_item; mCatDescendentDeltas[new_item->getParentUUID()]++; new_item->setComplete(true); } } else { // *TODO: Wow, harsh. Should we just complain and get out? LL_ERRS() << "unpack failed" << LL_ENDL; } } void AISUpdate::parseLink(const LLSD& link_map) { LLUUID item_id = link_map["item_id"].asUUID(); LLPointer new_link(new LLViewerInventoryItem); LLViewerInventoryItem *curr_link = gInventory.getItem(item_id); if (curr_link) { // Default to current values where not provided. new_link->copyViewerItem(curr_link); } BOOL rv = new_link->unpackMessage(link_map); if (rv) { const LLUUID& parent_id = new_link->getParentUUID(); if (mFetch) { LLPermissions default_perms; default_perms.init(gAgent.getID(), gAgent.getID(), LLUUID::null, LLUUID::null); default_perms.initMasks(PERM_NONE, PERM_NONE, PERM_NONE, PERM_NONE, PERM_NONE); new_link->setPermissions(default_perms); LLSaleInfo default_sale_info; new_link->setSaleInfo(default_sale_info); //LL_DEBUGS("Inventory") << "creating link from llsd: " << ll_pretty_print_sd(link_map) << LL_ENDL; mItemsCreated[item_id] = new_link; mCatDescendentDeltas[parent_id]; new_link->setComplete(true); if (new_link->getParentUUID().isNull()) { mItemsLost[item_id] = new_link; } } else if (curr_link) { mItemsUpdated[item_id] = new_link; // This statement is here to cause a new entry with 0 // delta to be created if it does not already exist; // otherwise has no effect. mCatDescendentDeltas[parent_id]; } else { LLPermissions default_perms; default_perms.init(gAgent.getID(),gAgent.getID(),LLUUID::null,LLUUID::null); default_perms.initMasks(PERM_NONE,PERM_NONE,PERM_NONE,PERM_NONE,PERM_NONE); new_link->setPermissions(default_perms); LLSaleInfo default_sale_info; new_link->setSaleInfo(default_sale_info); //LL_DEBUGS("Inventory") << "creating link from llsd: " << ll_pretty_print_sd(link_map) << LL_ENDL; mItemsCreated[item_id] = new_link; mCatDescendentDeltas[parent_id]++; new_link->setComplete(true); } } else { // *TODO: Wow, harsh. Should we just complain and get out? LL_ERRS() << "unpack failed" << LL_ENDL; } } void AISUpdate::parseCategory(const LLSD& category_map, S32 depth) { LLUUID category_id = category_map["category_id"].asUUID(); S32 version = LLViewerInventoryCategory::VERSION_UNKNOWN; if (category_map.has("version")) { version = category_map["version"].asInteger(); } LLViewerInventoryCategory *curr_cat = gInventory.getCategory(category_id); if (curr_cat && curr_cat->getVersion() > LLViewerInventoryCategory::VERSION_UNKNOWN && version > LLViewerInventoryCategory::VERSION_UNKNOWN && version < curr_cat->getVersion()) { LL_WARNS() << "Got stale folder, known: " << curr_cat->getVersion() << ", received: " << version << LL_ENDL; return; } // Check descendent count first, as it may be needed // to populate newly created categories if (category_map.has("_embedded")) { parseDescendentCount(category_id, category_map["_embedded"]); } LLPointer new_cat; if (curr_cat) { // Default to current values where not provided. new_cat = new LLViewerInventoryCategory(curr_cat); } else { if (category_map.has("agent_id")) { new_cat = new LLViewerInventoryCategory(category_map["agent_id"].asUUID()); } else { LL_DEBUGS() << "No owner provided, folder might be assigned wrong owner" << LL_ENDL; new_cat = new LLViewerInventoryCategory(LLUUID::null); } } BOOL rv = new_cat->unpackMessage(category_map); // *NOTE: unpackMessage does not unpack version or descendent count. if (rv) { if (mFetch) { // set version only if previous one was already known // or if we are sure this update has full data and embeded items (depth 0+) // since bulk fetch uses this to decide what still needs fetching if (version > LLViewerInventoryCategory::VERSION_UNKNOWN && (depth >= 0 || (curr_cat && curr_cat->getVersion() > LLViewerInventoryCategory::VERSION_UNKNOWN))) { // Set version/descendents for newly fetched categories. LL_DEBUGS("Inventory") << "Setting version to " << version << " for category " << category_id << LL_ENDL; new_cat->setVersion(version); } uuid_int_map_t::const_iterator lookup_it = mCatDescendentsKnown.find(category_id); if (mCatDescendentsKnown.end() != lookup_it) { S32 descendent_count = lookup_it->second; LL_DEBUGS("Inventory") << "Setting descendents count to " << descendent_count << " for category " << category_id << LL_ENDL; new_cat->setDescendentCount(descendent_count); } mCategoriesCreated[category_id] = new_cat; mCatDescendentDeltas[new_cat->getParentUUID()]; } else if (curr_cat) { mCategoriesUpdated[category_id] = new_cat; // This statement is here to cause a new entry with 0 // delta to be created if it does not already exist; // otherwise has no effect. mCatDescendentDeltas[new_cat->getParentUUID()]; // Capture update for the category itself as well. mCatDescendentDeltas[category_id]; } else { // Set version/descendents for newly created categories. if (category_map.has("version")) { S32 version = category_map["version"].asInteger(); LL_DEBUGS("Inventory") << "Setting version to " << version << " for new category " << category_id << LL_ENDL; new_cat->setVersion(version); } uuid_int_map_t::const_iterator lookup_it = mCatDescendentsKnown.find(category_id); if (mCatDescendentsKnown.end() != lookup_it) { S32 descendent_count = lookup_it->second; LL_DEBUGS("Inventory") << "Setting descendents count to " << descendent_count << " for new category " << category_id << LL_ENDL; new_cat->setDescendentCount(descendent_count); } mCategoriesCreated[category_id] = new_cat; mCatDescendentDeltas[new_cat->getParentUUID()]++; } } else { // *TODO: Wow, harsh. Should we just complain and get out? LL_ERRS() << "unpack failed" << LL_ENDL; } // Check for more embedded content. if (category_map.has("_embedded")) { parseEmbedded(category_map["_embedded"], depth - 1); } } void AISUpdate::parseDescendentCount(const LLUUID& category_id, const LLSD& embedded) { // We can only determine true descendent count if this contains all descendent types. if (embedded.has("categories") && embedded.has("links") && embedded.has("items")) { mCatDescendentsKnown[category_id] = embedded["categories"].size(); mCatDescendentsKnown[category_id] += embedded["links"].size(); mCatDescendentsKnown[category_id] += embedded["items"].size(); } } void AISUpdate::parseEmbedded(const LLSD& embedded, S32 depth) { if (embedded.has("links")) // _embedded in a category { parseEmbeddedLinks(embedded["links"]); } if (embedded.has("items")) // _embedded in a category { parseEmbeddedItems(embedded["items"]); } if (embedded.has("item")) // _embedded in a link { parseEmbeddedItem(embedded["item"]); } if (embedded.has("categories")) // _embedded in a category { parseEmbeddedCategories(embedded["categories"], depth); } if (embedded.has("category")) // _embedded in a link { parseEmbeddedCategory(embedded["category"], depth); } } void AISUpdate::parseUUIDArray(const LLSD& content, const std::string& name, uuid_list_t& ids) { if (content.has(name)) { for(LLSD::array_const_iterator it = content[name].beginArray(), end = content[name].endArray(); it != end; ++it) { ids.insert((*it).asUUID()); } } } void AISUpdate::parseEmbeddedLinks(const LLSD& links) { for(LLSD::map_const_iterator linkit = links.beginMap(), linkend = links.endMap(); linkit != linkend; ++linkit) { const LLUUID link_id((*linkit).first); const LLSD& link_map = (*linkit).second; if (!mFetch && mItemIds.end() == mItemIds.find(link_id)) { LL_DEBUGS("Inventory") << "Ignoring link not in items list " << link_id << LL_ENDL; } else { parseLink(link_map); } } } void AISUpdate::parseEmbeddedItem(const LLSD& item) { // a single item (_embedded in a link) if (item.has("item_id")) { if (mFetch || mItemIds.end() != mItemIds.find(item["item_id"].asUUID())) { parseItem(item); } } } void AISUpdate::parseEmbeddedItems(const LLSD& items) { // a map of items (_embedded in a category) for(LLSD::map_const_iterator itemit = items.beginMap(), itemend = items.endMap(); itemit != itemend; ++itemit) { const LLUUID item_id((*itemit).first); const LLSD& item_map = (*itemit).second; if (!mFetch && mItemIds.end() == mItemIds.find(item_id)) { LL_DEBUGS("Inventory") << "Ignoring item not in items list " << item_id << LL_ENDL; } else { parseItem(item_map); } } } void AISUpdate::parseEmbeddedCategory(const LLSD& category, S32 depth) { // a single category (_embedded in a link) if (category.has("category_id")) { if (mFetch || mCategoryIds.end() != mCategoryIds.find(category["category_id"].asUUID())) { parseCategory(category, depth); } } } void AISUpdate::parseEmbeddedCategories(const LLSD& categories, S32 depth) { // a map of categories (_embedded in a category) for(LLSD::map_const_iterator categoryit = categories.beginMap(), categoryend = categories.endMap(); categoryit != categoryend; ++categoryit) { const LLUUID category_id((*categoryit).first); const LLSD& category_map = (*categoryit).second; if (!mFetch && mCategoryIds.end() == mCategoryIds.find(category_id)) { LL_DEBUGS("Inventory") << "Ignoring category not in categories list " << category_id << LL_ENDL; } else { parseCategory(category_map, depth); } } } void AISUpdate::doUpdate() { // Do version/descendant accounting. for (std::map::const_iterator catit = mCatDescendentDeltas.begin(); catit != mCatDescendentDeltas.end(); ++catit) { LL_DEBUGS("Inventory") << "descendant accounting for " << catit->first << LL_ENDL; const LLUUID cat_id(catit->first); // Don't account for update if we just created this category. if (mCategoriesCreated.find(cat_id) != mCategoriesCreated.end()) { LL_DEBUGS("Inventory") << "Skipping version increment for new category " << cat_id << LL_ENDL; continue; } // Don't account for update unless AIS told us it updated that category. if (mCatVersionsUpdated.find(cat_id) == mCatVersionsUpdated.end()) { LL_DEBUGS("Inventory") << "Skipping version increment for non-updated category " << cat_id << LL_ENDL; continue; } // If we have a known descendant count, set that now. LLViewerInventoryCategory* cat = gInventory.getCategory(cat_id); if (cat) { S32 descendent_delta = catit->second; S32 old_count = cat->getDescendentCount(); LL_DEBUGS("Inventory") << "Updating descendant count for " << cat->getName() << " " << cat_id << " with delta " << descendent_delta << " from " << old_count << " to " << (old_count+descendent_delta) << LL_ENDL; LLInventoryModel::LLCategoryUpdate up(cat_id, descendent_delta); gInventory.accountForUpdate(up); } else { LL_DEBUGS("Inventory") << "Skipping version accounting for unknown category " << cat_id << LL_ENDL; } } // CREATE CATEGORIES for (deferred_category_map_t::const_iterator create_it = mCategoriesCreated.begin(); create_it != mCategoriesCreated.end(); ++create_it) { LLUUID category_id(create_it->first); LLPointer new_category = create_it->second; gInventory.updateCategory(new_category, LLInventoryObserver::CREATE); LL_DEBUGS("Inventory") << "created category " << category_id << LL_ENDL; } // UPDATE CATEGORIES for (deferred_category_map_t::const_iterator update_it = mCategoriesUpdated.begin(); update_it != mCategoriesUpdated.end(); ++update_it) { LLUUID category_id(update_it->first); LLPointer new_category = update_it->second; // Since this is a copy of the category *before* the accounting update, above, // we need to transfer back the updated version/descendant count. LLViewerInventoryCategory* curr_cat = gInventory.getCategory(new_category->getUUID()); if (!curr_cat) { LL_WARNS("Inventory") << "Failed to update unknown category " << new_category->getUUID() << LL_ENDL; } else { new_category->setVersion(curr_cat->getVersion()); new_category->setDescendentCount(curr_cat->getDescendentCount()); gInventory.updateCategory(new_category); LL_DEBUGS("Inventory") << "updated category " << new_category->getName() << " " << category_id << LL_ENDL; } } // LOST ITEMS if (!mItemsLost.empty()) { LL_INFOS("Inventory") << "Received " << (S32)mItemsLost.size() << " items without a parent" << LL_ENDL; const LLUUID lost_uuid(gInventory.findCategoryUUIDForType(LLFolderType::FT_LOST_AND_FOUND)); if (lost_uuid.notNull()) { for (deferred_item_map_t::const_iterator lost_it = mItemsLost.begin(); lost_it != mItemsLost.end(); ++lost_it) { LLPointer new_item = lost_it->second; new_item->setParent(lost_uuid); new_item->updateParentOnServer(FALSE); } } } // CREATE ITEMS for (deferred_item_map_t::const_iterator create_it = mItemsCreated.begin(); create_it != mItemsCreated.end(); ++create_it) { LLUUID item_id(create_it->first); LLPointer new_item = create_it->second; // FIXME risky function since it calls updateServer() in some // cases. Maybe break out the update/create cases, in which // case this is create. LL_DEBUGS("Inventory") << "created item " << item_id << LL_ENDL; gInventory.updateItem(new_item, LLInventoryObserver::CREATE); } // UPDATE ITEMS for (deferred_item_map_t::const_iterator update_it = mItemsUpdated.begin(); update_it != mItemsUpdated.end(); ++update_it) { LLUUID item_id(update_it->first); LLPointer new_item = update_it->second; // FIXME risky function since it calls updateServer() in some // cases. Maybe break out the update/create cases, in which // case this is update. LL_DEBUGS("Inventory") << "updated item " << item_id << LL_ENDL; //LL_DEBUGS("Inventory") << ll_pretty_print_sd(new_item->asLLSD()) << LL_ENDL; gInventory.updateItem(new_item); } // DELETE OBJECTS for (uuid_list_t::const_iterator del_it = mObjectsDeletedIds.begin(); del_it != mObjectsDeletedIds.end(); ++del_it) { LL_DEBUGS("Inventory") << "deleted item " << *del_it << LL_ENDL; gInventory.onObjectDeletedFromServer(*del_it, false, false, false); } // TODO - how can we use this version info? Need to be sure all // changes are going through AIS first, or at least through // something with a reliable responder. for (uuid_int_map_t::iterator ucv_it = mCatVersionsUpdated.begin(); ucv_it != mCatVersionsUpdated.end(); ++ucv_it) { const LLUUID id = ucv_it->first; S32 version = ucv_it->second; LLViewerInventoryCategory *cat = gInventory.getCategory(id); LL_DEBUGS("Inventory") << "cat version update " << cat->getName() << " to version " << cat->getVersion() << LL_ENDL; if (cat->getVersion() != version) { LL_WARNS() << "Possible version mismatch for category " << cat->getName() << ", viewer version " << cat->getVersion() << " AIS version " << version << " !!!Adjusting local version!!!" << LL_ENDL; // the AIS version should be considered the true version. Adjust // our local category model to reflect this version number. Otherwise // it becomes possible to get stuck with the viewer being out of // sync with the inventory system. Under normal circumstances // inventory COF is maintained on the viewer through calls to // LLInventoryModel::accountForUpdate when a changing operation // is performed. This occasionally gets out of sync however. if (version != LLViewerInventoryCategory::VERSION_UNKNOWN) { cat->setVersion(version); } else { // We do not account for update if version is UNKNOWN, so we shouldn't rise version // either or viewer will get stuck on descendants count -1, try to refetch folder instead cat->fetch(); } } } gInventory.notifyObservers(); }