summaryrefslogtreecommitdiff
path: root/indra/newview/llaisapi.cpp
diff options
context:
space:
mode:
authorNat Goodspeed <nat@lindenlab.com>2024-04-25 09:13:23 -0400
committerNat Goodspeed <nat@lindenlab.com>2024-04-25 09:13:23 -0400
commitf162693a23fe5cfda8dab3857718624033812d30 (patch)
tree0768f9ea570b248b48e4caa33103e3d55c625466 /indra/newview/llaisapi.cpp
parentd8931c9269a90cd01f6f6ff4de83b8fb41df11d3 (diff)
parentd98fc504a1d4bc292ba86acdda053c8b4598a193 (diff)
Merge Maint YZ branch 'main' into DRTVWR-588-cleanup-timers
Diffstat (limited to 'indra/newview/llaisapi.cpp')
-rw-r--r--indra/newview/llaisapi.cpp879
1 files changed, 793 insertions, 86 deletions
diff --git a/indra/newview/llaisapi.cpp b/indra/newview/llaisapi.cpp
index 005259bcb8..f23ce13608 100644
--- a/indra/newview/llaisapi.cpp
+++ b/indra/newview/llaisapi.cpp
@@ -29,11 +29,15 @@
#include "llaisapi.h"
#include "llagent.h"
+#include "llappviewer.h"
#include "llcallbacklist.h"
#include "llinventorymodel.h"
+#include "llinventoryobserver.h"
+#include "llnotificationsutil.h"
#include "llsdutil.h"
#include "llviewerregion.h"
-#include "llinventoryobserver.h"
+#include "llvoavatar.h"
+#include "llvoavatarself.h"
#include "llviewercontrol.h"
///----------------------------------------------------------------------------
@@ -43,11 +47,16 @@
//=========================================================================
const std::string AISAPI::INVENTORY_CAP_NAME("InventoryAPIv3");
const std::string AISAPI::LIBRARY_CAP_NAME("LibraryAPIv3");
+const S32 AISAPI::HTTP_TIMEOUT = 180;
std::list<AISAPI::ais_query_item_t> AISAPI::sPostponedQuery;
const S32 MAX_SIMULTANEOUS_COROUTINES = 2048;
+// AIS3 allows '*' requests, but in reality those will be cut at some point
+// Specify own depth to be able to anticipate it and mark folders as incomplete
+const S32 MAX_FOLDER_DEPTH_REQUEST = 50;
+
//-------------------------------------------------------------------------
/*static*/
bool AISAPI::isAvailable()
@@ -93,6 +102,10 @@ void AISAPI::CreateInventory(const LLUUID& parentId, const LLSD& newInventory, c
if (cap.empty())
{
LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL;
+ if (callback)
+ {
+ callback(LLUUID::null);
+ }
return;
}
@@ -100,7 +113,7 @@ void AISAPI::CreateInventory(const LLUUID& parentId, const LLSD& newInventory, c
tid.generate();
std::string url = cap + std::string("/category/") + parentId.asString() + "?tid=" + tid.asString();
- LL_DEBUGS("Inventory") << "url: " << url << LL_ENDL;
+ 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
@@ -124,7 +137,7 @@ void AISAPI::CreateInventory(const LLUUID& parentId, const LLSD& newInventory, c
(&LLCoreHttpUtil::HttpCoroutineAdapter::postAndSuspend), _1, _2, _3, _4, _5, _6);
LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro,
- _1, postFn, url, parentId, newInventory, callback, COPYINVENTORY));
+ _1, postFn, url, parentId, newInventory, callback, CREATEINVENTORY));
EnqueueAISCommand("CreateInventory", proc);
}
@@ -135,6 +148,10 @@ void AISAPI::SlamFolder(const LLUUID& folderId, const LLSD& newInventory, comple
if (cap.empty())
{
LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL;
+ if (callback)
+ {
+ callback(LLUUID::null);
+ }
return;
}
@@ -170,6 +187,10 @@ void AISAPI::RemoveCategory(const LLUUID &categoryId, completion_t callback)
if (cap.empty())
{
LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL;
+ if (callback)
+ {
+ callback(LLUUID::null);
+ }
return;
}
@@ -203,6 +224,10 @@ void AISAPI::RemoveItem(const LLUUID &itemId, completion_t callback)
if (cap.empty())
{
LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL;
+ if (callback)
+ {
+ callback(LLUUID::null);
+ }
return;
}
@@ -235,6 +260,10 @@ void AISAPI::CopyLibraryCategory(const LLUUID& sourceId, const LLUUID& destId, b
if (cap.empty())
{
LL_WARNS("Inventory") << "Library cap not found!" << LL_ENDL;
+ if (callback)
+ {
+ callback(LLUUID::null);
+ }
return;
}
@@ -279,6 +308,10 @@ void AISAPI::PurgeDescendents(const LLUUID &categoryId, completion_t callback)
if (cap.empty())
{
LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL;
+ if (callback)
+ {
+ callback(LLUUID::null);
+ }
return;
}
@@ -313,6 +346,10 @@ void AISAPI::UpdateCategory(const LLUUID &categoryId, const LLSD &updates, compl
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();
@@ -345,6 +382,10 @@ void AISAPI::UpdateItem(const LLUUID &itemId, const LLSD &updates, completion_t
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();
@@ -368,6 +409,380 @@ void AISAPI::UpdateItem(const LLUUID &itemId, const LLSD &updates, completion_t
}
/*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<LLSD(LLCoreHttpUtil::HttpCoroutineAdapter::*)(LLCore::HttpRequest::ptr_t, const std::string &, LLCore::HttpOptions::ptr_t, LLCore::HttpHeaders::ptr_t)>
+ //----
+ // _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 = MAX_FOLDER_DEPTH_REQUEST;
+ }
+ else
+ {
+ depth = llmin(depth, MAX_FOLDER_DEPTH_REQUEST);
+ }
+
+ 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<LLSD(LLCoreHttpUtil::HttpCoroutineAdapter::*)(LLCore::HttpRequest::ptr_t, const std::string &, LLCore::HttpOptions::ptr_t, LLCore::HttpHeaders::ptr_t)>
+ //----
+ // _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 = MAX_FOLDER_DEPTH_REQUEST;
+ }
+ else
+ {
+ depth = llmin(depth, MAX_FOLDER_DEPTH_REQUEST);
+ }
+
+ 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<LLSD(LLCoreHttpUtil::HttpCoroutineAdapter::*)(LLCore::HttpRequest::ptr_t, const std::string &, LLCore::HttpOptions::ptr_t, LLCore::HttpHeaders::ptr_t)>
+ //----
+ // _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 = MAX_FOLDER_DEPTH_REQUEST;
+ }
+ else
+ {
+ depth = llmin(depth, MAX_FOLDER_DEPTH_REQUEST);
+ }
+
+ 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<LLSD(LLCoreHttpUtil::HttpCoroutineAdapter::*)(LLCore::HttpRequest::ptr_t, const std::string &, LLCore::HttpOptions::ptr_t, LLCore::HttpHeaders::ptr_t)>
+ //----
+ // _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);
+}
+
+void AISAPI::FetchCategorySubset(const LLUUID& catId,
+ const uuid_vec_t specificChildren,
+ ITEM_TYPE type,
+ bool recursive,
+ completion_t callback,
+ S32 depth)
+{
+ std::string cap = (type == INVENTORY) ? getInvCap() : getLibCap();
+ if (cap.empty())
+ {
+ LL_WARNS("Inventory") << "Inventory cap not found!" << LL_ENDL;
+ if (callback)
+ {
+ callback(LLUUID::null);
+ }
+ return;
+ }
+ if (specificChildren.empty())
+ {
+ LL_WARNS("Inventory") << "Empty request!" << LL_ENDL;
+ if (callback)
+ {
+ callback(LLUUID::null);
+ }
+ return;
+ }
+ // category/any_folder_id/children?depth=*&children=child_id1,child_id2,child_id3
+ std::string url = cap + std::string("/category/") + catId.asString() + "/children";
+
+ if (recursive)
+ {
+ depth = MAX_FOLDER_DEPTH_REQUEST;
+ }
+ else
+ {
+ depth = llmin(depth, MAX_FOLDER_DEPTH_REQUEST);
+ }
+
+ uuid_vec_t::const_iterator iter = specificChildren.begin();
+ uuid_vec_t::const_iterator end = specificChildren.end();
+
+ url += "?depth=" + std::to_string(depth) + "&children=" + iter->asString();
+ iter++;
+
+ while (iter != end)
+ {
+ url += "," + iter->asString();
+ iter++;
+ }
+
+ const S32 MAX_URL_LENGH = 2000; // RFC documentation specifies a maximum length of 2048
+ if (url.length() > MAX_URL_LENGH)
+ {
+ LL_WARNS("Inventory") << "Request url is too long, url: " << url << LL_ENDL;
+ }
+
+ invokationFn_t getFn = boost::bind(
+ // Humans ignore next line. It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload.
+ static_cast<LLSD(LLCoreHttpUtil::HttpCoroutineAdapter::*)(LLCore::HttpRequest::ptr_t, const std::string&, LLCore::HttpOptions::ptr_t, LLCore::HttpHeaders::ptr_t)>
+ //----
+ // _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, FETCHCATEGORYSUBSET));
+
+ EnqueueAISCommand("FetchCategorySubset", proc);
+}
+
+/*static*/
+// Will get COF folder, links in it and items those links point to
+void AISAPI::FetchCOF(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("/category/current/links");
+
+ invokationFn_t getFn = boost::bind(
+ // Humans ignore next line. It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload.
+ static_cast<LLSD(LLCoreHttpUtil::HttpCoroutineAdapter::*)(LLCore::HttpRequest::ptr_t, const std::string&, LLCore::HttpOptions::ptr_t, LLCore::HttpHeaders::ptr_t)>
+ //----
+ // _1 -> httpAdapter
+ // _2 -> httpRequest
+ // _3 -> url
+ // _4 -> body
+ // _5 -> httpOptions
+ // _6 -> httpHeaders
+ (&LLCoreHttpUtil::HttpCoroutineAdapter::getAndSuspend), _1, _2, _3, _5, _6);
+
+ LLSD body;
+ // Only cof folder will be full, but cof can contain an outfit
+ // link with embedded outfit folder for request to parse
+ body["depth"] = 0;
+ LLCoprocedureManager::CoProcedure_t proc(boost::bind(&AISAPI::InvokeAISCommandCoro,
+ _1, getFn, url, LLUUID::null, body, callback, FETCHCOF));
+
+ EnqueueAISCommand("FetchCOF", proc);
+}
+
+void AISAPI::FetchCategoryLinks(const LLUUID &catId, 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("/category/") + catId.asString() + "/links";
+
+ invokationFn_t getFn = boost::bind(
+ // Humans ignore next line. It is just a cast to specify which LLCoreHttpUtil::HttpCoroutineAdapter routine overload.
+ static_cast<LLSD (LLCoreHttpUtil::HttpCoroutineAdapter::*)(LLCore::HttpRequest::ptr_t, const std::string &,
+ LLCore::HttpOptions::ptr_t, LLCore::HttpHeaders::ptr_t)>
+ //----
+ // _1 -> httpAdapter
+ // _2 -> httpRequest
+ // _3 -> url
+ // _4 -> body
+ // _5 -> httpOptions
+ // _6 -> httpHeaders
+ (&LLCoreHttpUtil::HttpCoroutineAdapter::getAndSuspend),
+ _1, _2, _3, _5, _6);
+
+ LLSD body;
+ body["depth"] = 0;
+ LLCoprocedureManager::CoProcedure_t proc(
+ boost::bind(&AISAPI::InvokeAISCommandCoro, _1, getFn, url, LLUUID::null, body, callback, FETCHCATEGORYLINKS));
+
+ EnqueueAISCommand("FetchCategoryLinks", 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<LLSD(LLCoreHttpUtil::HttpCoroutineAdapter::*)(LLCore::HttpRequest::ptr_t , const std::string& , LLCore::HttpOptions::ptr_t , LLCore::HttpHeaders::ptr_t)>
+ //----
+ // _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();
@@ -418,21 +833,49 @@ void AISAPI::onIdle(void *userdata)
}
/*static*/
+void AISAPI::onUpdateReceived(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);
+ }
+
+ AISUpdate ais_update(update, type, request_body);
+ 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(LLCoreHttpUtil::HTTP_REQUEST_EXPIRY_SECS);
+ httpOptions->setTimeout(HTTP_TIMEOUT);
- LL_DEBUGS("Inventory") << "url: " << url << LL_ENDL;
+ LL_DEBUGS("Inventory") << "Request url: " << url << LL_ENDL;
+
+ LLSD result;
+ LLSD httpResults;
+ LLCore::HttpStatus status;
- LLSD result = invoke(httpAdapter, httpRequest, url, body, httpOptions, httpHeaders);
- LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS];
- LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults);
+ result = invoke(httpAdapter , httpRequest , url , body , httpOptions , httpHeaders);
+ httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS];
+ status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults);
if (!status || !result.isMap())
{
@@ -474,29 +917,127 @@ void AISAPI::InvokeAISCommandCoro(LLCoreHttpUtil::HttpCoroutineAdapter::ptr_t ht
}
}
}
+ else if (status == LLCore::HttpStatus(HTTP_FORBIDDEN) /*403*/)
+ {
+ if (type == FETCHCATEGORYCHILDREN)
+ {
+ if (body.has("depth") && body["depth"].asInteger() == 0)
+ {
+ // Can't fetch a single folder with depth 0, folder is too big.
+ static bool first_call = true;
+ if (first_call)
+ {
+ first_call = false;
+ LLNotificationsUtil::add("InventoryLimitReachedAISAlert");
+ }
+ else
+ {
+ LLNotificationsUtil::add("InventoryLimitReachedAIS");
+ }
+ LL_WARNS("Inventory") << "Fetch failed, content is over limit, url: " << url << LL_ENDL;
+ }
+ else
+ {
+ // Result was too big, but situation is recoverable by requesting with lower depth
+ LL_DEBUGS("Inventory") << "Fetch failed, content is over limit, url: " << url << LL_ENDL;
+ }
+ }
+ }
LL_WARNS("Inventory") << "Inventory error: " << status.toString() << LL_ENDL;
LL_WARNS("Inventory") << ll_pretty_print_sd(result) << LL_ENDL;
}
- gInventory.onAISUpdateReceived("AISCommand", result);
+ LL_DEBUGS("Inventory", "AIS3") << "Result: " << result << LL_ENDL;
+ onUpdateReceived(result, type, body);
if (callback && !callback.empty())
- {
+ {
+ bool needs_callback = true;
LLUUID id(LLUUID::null);
- if (result.has("category_id") && (type == COPYLIBRARYCATEGORY))
- {
- id = result["category_id"];
- }
+ switch (type)
+ {
+ case COPYLIBRARYCATEGORY:
+ case FETCHCATEGORYCATEGORIES:
+ case FETCHCATEGORYCHILDREN:
+ case FETCHCATEGORYSUBSET:
+ case FETCHCATEGORYLINKS:
+ case FETCHCOF:
+ if (result.has("category_id"))
+ {
+ id = result["category_id"];
+ }
+ break;
+ case FETCHITEM:
+ if (result.has("item_id"))
+ {
+ // Error message might contain an 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;
+ }
- callback(id);
+ if (needs_callback)
+ {
+ // Call callback at least once regardless of failure.
+ // UPDATEITEM doesn't expect an id
+ callback(id);
+ }
}
}
//-------------------------------------------------------------------------
-AISUpdate::AISUpdate(const LLSD& update)
+AISUpdate::AISUpdate(const LLSD& update, AISAPI::COMMAND_TYPE type, const LLSD& request_body)
+: mType(type)
{
+ mFetch = (type == AISAPI::FETCHITEM)
+ || (type == AISAPI::FETCHCATEGORYCHILDREN)
+ || (type == AISAPI::FETCHCATEGORYCATEGORIES)
+ || (type == AISAPI::FETCHCATEGORYSUBSET)
+ || (type == AISAPI::FETCHCOF)
+ || (type == AISAPI::FETCHCATEGORYLINKS)
+ || (type == AISAPI::FETCHORPHANS);
+ // parse update llsd into stuff to do or parse received items.
+ mFetchDepth = MAX_FOLDER_DEPTH_REQUEST;
+ if (mFetch && request_body.has("depth"))
+ {
+ mFetchDepth = request_body["depth"].asInteger();
+ }
+
+ mTimer.setTimerExpirySec(AIS_EXPIRY_SECONDS);
+ mTimer.start();
parseUpdate(update);
}
@@ -506,6 +1047,7 @@ void AISUpdate::clearParseResults()
mCatDescendentsKnown.clear();
mCatVersionsUpdated.clear();
mItemsCreated.clear();
+ mItemsLost.clear();
mItemsUpdated.clear();
mCategoriesCreated.clear();
mCategoriesUpdated.clear();
@@ -514,6 +1056,16 @@ void AISUpdate::clearParseResults()
mCategoryIds.clear();
}
+void AISUpdate::checkTimeout()
+{
+ if (mTimer.hasExpired())
+ {
+ llcoro::suspend();
+ LLCoros::checkStop();
+ mTimer.setTimerExpirySec(AIS_EXPIRY_SECONDS);
+ }
+}
+
void AISUpdate::parseUpdate(const LLSD& update)
{
clearParseResults();
@@ -601,24 +1153,37 @@ void AISUpdate::parseMeta(const LLSD& update)
void AISUpdate::parseContent(const LLSD& update)
{
- if (update.has("linked_id"))
+ // Errors from a fetch request might contain id without
+ // full item or folder.
+ // Todo: Depending on error we might want to do something,
+ // like removing a 404 item or refetching parent folder
+ if (update.has("linked_id") && update.has("parent_id"))
{
- parseLink(update);
+ parseLink(update, mFetchDepth);
}
- else if (update.has("item_id"))
+ else if (update.has("item_id") && update.has("parent_id"))
{
parseItem(update);
}
- if (update.has("category_id"))
- {
- parseCategory(update);
- }
+ if (mType == AISAPI::FETCHCATEGORYSUBSET)
+ {
+ // initial category is incomplete, don't process it,
+ // go for content instead
+ if (update.has("_embedded"))
+ {
+ parseEmbedded(update["_embedded"], mFetchDepth - 1);
+ }
+ }
+ else if (update.has("category_id") && update.has("parent_id"))
+ {
+ parseCategory(update, mFetchDepth);
+ }
else
{
if (update.has("_embedded"))
{
- parseEmbedded(update["_embedded"]);
+ parseEmbedded(update["_embedded"], mFetchDepth);
}
}
}
@@ -636,7 +1201,17 @@ void AISUpdate::parseItem(const LLSD& item_map)
BOOL rv = new_item->unpackMessage(item_map);
if (rv)
{
- if (curr_item)
+ if (mFetch)
+ {
+ mItemsCreated[item_id] = new_item;
+ 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
@@ -648,6 +1223,7 @@ void AISUpdate::parseItem(const LLSD& item_map)
{
mItemsCreated[item_id] = new_item;
mCatDescendentDeltas[new_item->getParentUUID()]++;
+ new_item->setComplete(true);
}
}
else
@@ -657,7 +1233,7 @@ void AISUpdate::parseItem(const LLSD& item_map)
}
}
-void AISUpdate::parseLink(const LLSD& link_map)
+void AISUpdate::parseLink(const LLSD& link_map, S32 depth)
{
LLUUID item_id = link_map["item_id"].asUUID();
LLPointer<LLViewerInventoryItem> new_link(new LLViewerInventoryItem);
@@ -671,7 +1247,24 @@ void AISUpdate::parseLink(const LLSD& link_map)
if (rv)
{
const LLUUID& parent_id = new_link->getParentUUID();
- if (curr_link)
+ 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;
+ 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
@@ -690,7 +1283,13 @@ void AISUpdate::parseLink(const LLSD& link_map)
//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 (link_map.has("_embedded"))
+ {
+ parseEmbedded(link_map["_embedded"], depth);
+ }
}
else
{
@@ -700,19 +1299,30 @@ void AISUpdate::parseLink(const LLSD& link_map)
}
-void AISUpdate::parseCategory(const LLSD& category_map)
+void AISUpdate::parseCategory(const LLSD& category_map, S32 depth)
{
- LLUUID category_id = category_map["category_id"].asUUID();
+ LLUUID category_id = category_map["category_id"].asUUID();
+ S32 version = LLViewerInventoryCategory::VERSION_UNKNOWN;
- // 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"]);
- }
+ 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
+ && curr_cat->getDescendentCount() != LLViewerInventoryCategory::DESCENDENT_COUNT_UNKNOWN
+ && version > LLViewerInventoryCategory::VERSION_UNKNOWN
+ && version < curr_cat->getVersion())
+ {
+ LL_WARNS() << "Got stale folder, known: " << curr_cat->getVersion()
+ << ", received: " << version << LL_ENDL;
+ return;
+ }
LLPointer<LLViewerInventoryCategory> new_cat;
- LLViewerInventoryCategory *curr_cat = gInventory.getCategory(category_id);
if (curr_cat)
{
// Default to current values where not provided.
@@ -732,13 +1342,58 @@ void AISUpdate::parseCategory(const LLSD& category_map)
}
BOOL rv = new_cat->unpackMessage(category_map);
// *NOTE: unpackMessage does not unpack version or descendent count.
- //if (category_map.has("version"))
- //{
- // mCatVersionsUpdated[category_id] = category_map["version"].asInteger();
- //}
if (rv)
{
- if (curr_cat)
+ // Check descendent count first, as it may be needed
+ // to populate newly created categories
+ if (category_map.has("_embedded"))
+ {
+ parseDescendentCount(category_id, new_cat->getPreferredType(), category_map["_embedded"]);
+ }
+
+ if (mFetch)
+ {
+ 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);
+
+ // set version only if we are sure this update has full data and embeded items
+ // since viewer uses version to decide if folder and content still need fetching
+ if (version > LLViewerInventoryCategory::VERSION_UNKNOWN
+ && depth >= 0)
+ {
+ if (curr_cat && curr_cat->getVersion() > version)
+ {
+ LL_WARNS("Inventory") << "Version was " << curr_cat->getVersion()
+ << ", but fetch returned version " << version
+ << " for category " << category_id << LL_ENDL;
+ }
+ else
+ {
+ LL_DEBUGS("Inventory") << "Setting version to " << version
+ << " for category " << category_id << LL_ENDL;
+ }
+
+ new_cat->setVersion(version);
+ }
+ }
+ else if (curr_cat
+ && curr_cat->getVersion() > LLViewerInventoryCategory::VERSION_UNKNOWN
+ && version > curr_cat->getVersion())
+ {
+ // Potentially should new_cat->setVersion(unknown) here,
+ // but might be waiting for a callback that would increment
+ LL_DEBUGS("Inventory") << "Category " << category_id
+ << " is stale. Known version: " << curr_cat->getVersion()
+ << " server version: " << version << LL_ENDL;
+ }
+ mCategoriesCreated[category_id] = new_cat;
+ }
+ else if (curr_cat)
{
mCategoriesUpdated[category_id] = new_cat;
// This statement is here to cause a new entry with 0
@@ -751,20 +1406,22 @@ void AISUpdate::parseCategory(const LLSD& category_map)
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);
+ 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);
+
+ // Don't set version unles correct children count is present
+ 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);
+ }
}
mCategoriesCreated[category_id] = new_cat;
mCatDescendentDeltas[new_cat->getParentUUID()]++;
@@ -779,28 +1436,35 @@ void AISUpdate::parseCategory(const LLSD& category_map)
// Check for more embedded content.
if (category_map.has("_embedded"))
{
- parseEmbedded(category_map["_embedded"]);
+ parseEmbedded(category_map["_embedded"], depth - 1);
}
}
-void AISUpdate::parseDescendentCount(const LLUUID& category_id, const LLSD& embedded)
+void AISUpdate::parseDescendentCount(const LLUUID& category_id, LLFolderType::EType type, 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();
- }
+ // 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();
+ }
+ else if (mFetch && embedded.has("links") && (type == LLFolderType::FT_CURRENT_OUTFIT || type == LLFolderType::FT_OUTFIT))
+ {
+ // COF and outfits contain links only
+ mCatDescendentsKnown[category_id] = embedded["links"].size();
+ }
}
-void AISUpdate::parseEmbedded(const LLSD& embedded)
+void AISUpdate::parseEmbedded(const LLSD& embedded, S32 depth)
{
+ checkTimeout();
+
if (embedded.has("links")) // _embedded in a category
{
- parseEmbeddedLinks(embedded["links"]);
+ parseEmbeddedLinks(embedded["links"], depth);
}
if (embedded.has("items")) // _embedded in a category
{
@@ -812,11 +1476,11 @@ void AISUpdate::parseEmbedded(const LLSD& embedded)
}
if (embedded.has("categories")) // _embedded in a category
{
- parseEmbeddedCategories(embedded["categories"]);
+ parseEmbeddedCategories(embedded["categories"], depth);
}
if (embedded.has("category")) // _embedded in a link
{
- parseEmbeddedCategory(embedded["category"]);
+ parseEmbeddedCategory(embedded["category"], depth);
}
}
@@ -833,7 +1497,7 @@ void AISUpdate::parseUUIDArray(const LLSD& content, const std::string& name, uui
}
}
-void AISUpdate::parseEmbeddedLinks(const LLSD& links)
+void AISUpdate::parseEmbeddedLinks(const LLSD& links, S32 depth)
{
for(LLSD::map_const_iterator linkit = links.beginMap(),
linkend = links.endMap();
@@ -841,13 +1505,13 @@ void AISUpdate::parseEmbeddedLinks(const LLSD& links)
{
const LLUUID link_id((*linkit).first);
const LLSD& link_map = (*linkit).second;
- if (mItemIds.end() == mItemIds.find(link_id))
+ 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);
+ parseLink(link_map, depth);
}
}
}
@@ -857,7 +1521,7 @@ void AISUpdate::parseEmbeddedItem(const LLSD& item)
// a single item (_embedded in a link)
if (item.has("item_id"))
{
- if (mItemIds.end() != mItemIds.find(item["item_id"].asUUID()))
+ if (mFetch || mItemIds.end() != mItemIds.find(item["item_id"].asUUID()))
{
parseItem(item);
}
@@ -873,7 +1537,7 @@ void AISUpdate::parseEmbeddedItems(const LLSD& items)
{
const LLUUID item_id((*itemit).first);
const LLSD& item_map = (*itemit).second;
- if (mItemIds.end() == mItemIds.find(item_id))
+ if (!mFetch && mItemIds.end() == mItemIds.find(item_id))
{
LL_DEBUGS("Inventory") << "Ignoring item not in items list " << item_id << LL_ENDL;
}
@@ -884,19 +1548,19 @@ void AISUpdate::parseEmbeddedItems(const LLSD& items)
}
}
-void AISUpdate::parseEmbeddedCategory(const LLSD& category)
+void AISUpdate::parseEmbeddedCategory(const LLSD& category, S32 depth)
{
// a single category (_embedded in a link)
if (category.has("category_id"))
{
- if (mCategoryIds.end() != mCategoryIds.find(category["category_id"].asUUID()))
+ if (mFetch || mCategoryIds.end() != mCategoryIds.find(category["category_id"].asUUID()))
{
- parseCategory(category);
+ parseCategory(category, depth);
}
}
}
-void AISUpdate::parseEmbeddedCategories(const LLSD& categories)
+void AISUpdate::parseEmbeddedCategories(const LLSD& categories, S32 depth)
{
// a map of categories (_embedded in a category)
for(LLSD::map_const_iterator categoryit = categories.beginMap(),
@@ -905,19 +1569,21 @@ void AISUpdate::parseEmbeddedCategories(const LLSD& categories)
{
const LLUUID category_id((*categoryit).first);
const LLSD& category_map = (*categoryit).second;
- if (mCategoryIds.end() == mCategoryIds.find(category_id))
+ 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);
+ parseCategory(category_map, depth);
}
}
}
void AISUpdate::doUpdate()
{
+ checkTimeout();
+
// Do version/descendant accounting.
for (std::map<LLUUID,S32>::const_iterator catit = mCatDescendentDeltas.begin();
catit != mCatDescendentDeltas.end(); ++catit)
@@ -959,6 +1625,7 @@ void AISUpdate::doUpdate()
}
// CREATE CATEGORIES
+ const S32 MAX_UPDATE_BACKLOG = 50; // stall prevention
for (deferred_category_map_t::const_iterator create_it = mCategoriesCreated.begin();
create_it != mCategoriesCreated.end(); ++create_it)
{
@@ -967,6 +1634,13 @@ void AISUpdate::doUpdate()
gInventory.updateCategory(new_category, LLInventoryObserver::CREATE);
LL_DEBUGS("Inventory") << "created category " << category_id << LL_ENDL;
+
+ // fetching can receive massive amount of items and folders
+ if (gInventory.getChangedIDs().size() > MAX_UPDATE_BACKLOG)
+ {
+ gInventory.notifyObservers();
+ checkTimeout();
+ }
}
// UPDATE CATEGORIES
@@ -991,6 +1665,24 @@ void AISUpdate::doUpdate()
}
}
+ // 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<LLViewerInventoryItem> 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)
@@ -1003,6 +1695,13 @@ void AISUpdate::doUpdate()
// case this is create.
LL_DEBUGS("Inventory") << "created item " << item_id << LL_ENDL;
gInventory.updateItem(new_item, LLInventoryObserver::CREATE);
+
+ // fetching can receive massive amount of items and folders
+ if (gInventory.getChangedIDs().size() > MAX_UPDATE_BACKLOG)
+ {
+ gInventory.notifyObservers();
+ checkTimeout();
+ }
}
// UPDATE ITEMS
@@ -1039,10 +1738,6 @@ void AISUpdate::doUpdate()
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
@@ -1052,17 +1747,29 @@ void AISUpdate::doUpdate()
// is performed. This occasionally gets out of sync however.
if (version != LLViewerInventoryCategory::VERSION_UNKNOWN)
{
+ LL_WARNS() << "Possible version mismatch for category " << cat->getName()
+ << ", viewer version " << cat->getVersion()
+ << " AIS version " << version << " !!!Adjusting local version!!!" << LL_ENDL;
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();
+ //
+ // Todo: proper backoff?
+
+ LL_WARNS() << "Possible version mismatch for category " << cat->getName()
+ << ", viewer version " << cat->getVersion()
+ << " AIS version " << version << " !!!Rerequesting category!!!" << LL_ENDL;
+ const S32 LONG_EXPIRY = 360;
+ cat->fetch(LONG_EXPIRY);
}
}
}
+ checkTimeout();
+
gInventory.notifyObservers();
}