From 098851592ca5b3363760a74b2f66bf50e3d04aa0 Mon Sep 17 00:00:00 2001 From: drbob Date: Mon, 16 Jun 2008 20:37:48 +0000 Subject: [PATCH] * Added isOnline to rspeers.h interface Various improvements to get p3distrib Messages working: * Defensive programming in cachestrapper (forcing ownId on save). * Changed CONFIG_CACHE_ID to last -> to force correct loading order at startup This should be Config Files, Local Cache Files, then Remote Caches. * Hack to maintain existing Cache Files. * Improvments to forum_test. Lots of work on p3distrib: * fixed nextCacheSubId. * Added new Msgs to Config Save. * Fixed Cache reload issues. * Overall: Enabled Forum Msg publication. git-svn-id: http://svn.code.sf.net/p/retroshare/code/trunk@611 b45a01b8-16f6-495d-af2f-9b41ad6348cc --- libretroshare/src/dbase/cachestrapper.cc | 6 +- libretroshare/src/pqi/p3cfgmgr.cc | 11 + libretroshare/src/pqi/p3cfgmgr.h | 7 +- libretroshare/src/rsiface/rspeers.h | 1 + libretroshare/src/rsserver/p3face-startup.cc | 1 - libretroshare/src/rsserver/p3peers.cc | 17 ++ libretroshare/src/rsserver/p3peers.h | 1 + libretroshare/src/services/forum_test.cc | 31 ++- libretroshare/src/services/p3distrib.cc | 263 +++++++++++++++---- libretroshare/src/services/p3distrib.h | 14 +- 10 files changed, 298 insertions(+), 54 deletions(-) diff --git a/libretroshare/src/dbase/cachestrapper.cc b/libretroshare/src/dbase/cachestrapper.cc index 490ab1f87..d3979a28c 100644 --- a/libretroshare/src/dbase/cachestrapper.cc +++ b/libretroshare/src/dbase/cachestrapper.cc @@ -733,7 +733,11 @@ std::list CacheStrapper::saveList(bool &cleanup) { RsCacheConfig *rscc = new RsCacheConfig(); - rscc->pid = cit->pid; + // Fixup lazy behaviour in clients... + // This ensures correct loading later. + // (used to be: rscc->pid = cit->pid;) + rscc->pid = ownId; + //rscc->pname = cit->pname; rscc->cachetypeid = cit->cid.type; rscc->cachesubid = cit->cid.subid; diff --git a/libretroshare/src/pqi/p3cfgmgr.cc b/libretroshare/src/pqi/p3cfgmgr.cc index 4d391e1f4..d0ae506d4 100644 --- a/libretroshare/src/pqi/p3cfgmgr.cc +++ b/libretroshare/src/pqi/p3cfgmgr.cc @@ -318,6 +318,17 @@ void p3ConfigMgr::loadConfiguration() uint32_t confId = atoi(it->key.c_str()); std::string hashin = it->value; + /*********************** HACK TO CHANGE CACHE CONFIG ID ********* + * REMOVE IN A MONTH OR TWO + */ + + if (confId == CONFIG_TYPE_CACHE_OLDID) + { + confId = CONFIG_TYPE_CACHE; + } + + /*********************** HACK TO CHANGE CACHE CONFIG ID *********/ + std::map::iterator cit; cit = configs.find(confId); if (cit != configs.end()) diff --git a/libretroshare/src/pqi/p3cfgmgr.h b/libretroshare/src/pqi/p3cfgmgr.h index e666203a4..238a7fcf7 100644 --- a/libretroshare/src/pqi/p3cfgmgr.h +++ b/libretroshare/src/pqi/p3cfgmgr.h @@ -62,7 +62,7 @@ const uint32_t CONFIG_TYPE_GENERAL = 0x0001; const uint32_t CONFIG_TYPE_PEERS = 0x0002; const uint32_t CONFIG_TYPE_FSERVER = 0x0003; const uint32_t CONFIG_TYPE_MSGS = 0x0004; -const uint32_t CONFIG_TYPE_CACHE = 0x0005; +const uint32_t CONFIG_TYPE_CACHE_OLDID = 0x0005; const uint32_t CONFIG_TYPE_RANK_LINK = 0x0011; @@ -70,6 +70,11 @@ const uint32_t CONFIG_TYPE_QBLOG = 0x0012; const uint32_t CONFIG_TYPE_FORUMS = 0x0013; +/* CACHE ID Must be at the END so that other configurations + * are loaded First (Cache Config --> Cache Loading) + */ +const uint32_t CONFIG_TYPE_CACHE = 0xff01; + class p3ConfigMgr; class p3AuthMgr; diff --git a/libretroshare/src/rsiface/rspeers.h b/libretroshare/src/rsiface/rspeers.h index 2556ce0e7..a92717f57 100644 --- a/libretroshare/src/rsiface/rspeers.h +++ b/libretroshare/src/rsiface/rspeers.h @@ -125,6 +125,7 @@ virtual bool getOnlineList(std::list &ids) = 0; virtual bool getFriendList(std::list &ids) = 0; virtual bool getOthersList(std::list &ids) = 0; +virtual bool isOnline(std::string id) = 0; virtual bool isFriend(std::string id) = 0; virtual std::string getPeerName(std::string id) = 0; virtual bool getPeerDetails(std::string id, RsPeerDetails &d) = 0; diff --git a/libretroshare/src/rsserver/p3face-startup.cc b/libretroshare/src/rsserver/p3face-startup.cc index d5ab846e4..98fd54deb 100644 --- a/libretroshare/src/rsserver/p3face-startup.cc +++ b/libretroshare/src/rsserver/p3face-startup.cc @@ -80,7 +80,6 @@ #define RS_RELEASE 1 - /**************** PQI_USE_XPGP ******************/ #if defined(PQI_USE_XPGP) #include "pqi/authxpgp.h" diff --git a/libretroshare/src/rsserver/p3peers.cc b/libretroshare/src/rsserver/p3peers.cc index a0926ac91..b694871b8 100644 --- a/libretroshare/src/rsserver/p3peers.cc +++ b/libretroshare/src/rsserver/p3peers.cc @@ -196,6 +196,23 @@ bool p3Peers::getOthersList(std::list &ids) return true; } +bool p3Peers::isOnline(std::string id) +{ +#ifdef P3PEERS_DEBUG + std::cerr << "p3Peers::isOnline() " << id; + std::cerr << std::endl; +#endif + + /* get from mConnectMgr */ + peerConnectState state; + if (mConnMgr->getFriendNetStatus(id, state) && + (state.state & RS_PEER_S_CONNECTED)) + { + return true; + } + return false; +} + bool p3Peers::isFriend(std::string id) { #ifdef P3PEERS_DEBUG diff --git a/libretroshare/src/rsserver/p3peers.h b/libretroshare/src/rsserver/p3peers.h index 4ccdeb32d..4692f26cb 100644 --- a/libretroshare/src/rsserver/p3peers.h +++ b/libretroshare/src/rsserver/p3peers.h @@ -48,6 +48,7 @@ virtual bool getOnlineList(std::list &ids); virtual bool getFriendList(std::list &ids); virtual bool getOthersList(std::list &ids); +virtual bool isOnline(std::string id); virtual bool isFriend(std::string id); virtual std::string getPeerName(std::string id); virtual bool getPeerDetails(std::string id, RsPeerDetails &d); diff --git a/libretroshare/src/services/forum_test.cc b/libretroshare/src/services/forum_test.cc index 6f8703d1f..694fb7dad 100644 --- a/libretroshare/src/services/forum_test.cc +++ b/libretroshare/src/services/forum_test.cc @@ -53,6 +53,12 @@ int main(int argc, char **argv) testForums(forum); + int i; + for(i = 0; i < 10; i++) + { + sleep(1); + forum->tick(); + } /* cleanup */ CRYPTO_cleanup_all_ex_data(); @@ -68,8 +74,31 @@ int main(int argc, char **argv) int testForums(p3Forums *forum) { std::string fId1 = forum->createForum(L"Forum 1", L"first forum", RS_DISTRIB_PUBLIC); + + forum->tick(); /* expect group publish */ + sleep(1); + forum->tick(); + sleep(1); + std::string fId2 = forum->createForum(L"Forum 2", L"next first forum", RS_DISTRIB_PRIVATE); - forum -> tick(); + forum->tick(); /* expect group publish */ + sleep(1); + forum->tick(); + sleep(1); + + std::string mId1 = forum->createForumMsg(fId2, "", L"Forum 2 Msg 1", L"first forum msg"); + + forum->tick(); /* expect msg publish */ + sleep(1); + forum->tick(); + sleep(1); + + std::string mId2 = forum->createForumMsg(fId2, "", L"Forum 2 Msg 2", L"second forum msg"); + + forum->tick(); /* expect msg publish */ + sleep(1); + forum->tick(); + sleep(1); } diff --git a/libretroshare/src/services/p3distrib.cc b/libretroshare/src/services/p3distrib.cc index 66ae975d1..5849b33d6 100644 --- a/libretroshare/src/services/p3distrib.cc +++ b/libretroshare/src/services/p3distrib.cc @@ -51,17 +51,15 @@ p3GroupDistrib::p3GroupDistrib(uint16_t subtype, CacheStore(subtype, true, cs, cft, storedir), p3Config(configId), nullService(subtype), mStorePeriod(storePeriod), - mPubPeriod(pubPeriod) + mPubPeriod(pubPeriod), + mLastPublishTime(0), + mMaxCacheSubId(1) { /* not much yet */ time_t now = time(NULL); - /* set this a little in the future -> so we can - * adjust the publish point if needed - */ - - mNextPublishTime = now + mPubPeriod / 4; - mGroupsRepublish = false; + /* force publication of groups (cleared if local cache file found) */ + mGroupsRepublish = true; return; } @@ -75,23 +73,20 @@ int p3GroupDistrib::tick() std::cerr << std::endl; #endif -#if 0 time_t now = time(NULL); bool toPublish; { RsStackMutex stack(distribMtx); /**** STACK LOCKED MUTEX ****/ - toPublish = (now > mNextPublishTime); + toPublish = (mPendingPublish.size() > 0) && (now > mPubPeriod + mLastPublishTime); } if (toPublish) { RsStackMutex stack(distribMtx); /**** STACK LOCKED MUTEX ****/ - locked_publishPendingMsgs(); - mNextPublishTime = now + mPubPeriod; + locked_publishPendingMsgs(); /* flags taken care of in here */ } -#endif bool toPublishGroups; { @@ -121,7 +116,7 @@ int p3GroupDistrib::tick() int p3GroupDistrib::loadAnyCache(const CacheData &data, bool local) { - /* if subtype = 0 -> FileGroup, else -> FileMsgs */ + /* if subtype = 1 -> FileGroup, else -> FileMsgs */ std::string file = data.path; file += "/"; @@ -133,13 +128,13 @@ int p3GroupDistrib::loadAnyCache(const CacheData &data, bool local) std::cerr << "Cid: " << data.cid.type << ":" << data.cid.subid << std::endl; #endif - if (data.cid.subid == 0) + if (data.cid.subid == 1) { loadFileGroups(file, data.pid, local); } else { - loadFileMsgs(file, data.cid.subid, data.pid, local); + loadFileMsgs(file, data.cid.subid, data.pid, data.recvd, local); } return true; } @@ -151,7 +146,16 @@ int p3GroupDistrib::loadCache(const CacheData &data) std::cerr << std::endl; #endif - return loadAnyCache(data, false); + loadAnyCache(data, false); + + if (data.size > 0) + { + CacheStore::lockData(); /***** LOCK ****/ + locked_storeCacheEntry(data); + CacheStore::unlockData(); /***** UNLOCK ****/ + } + + return 1; } bool p3GroupDistrib::loadLocalCache(const CacheData &data) @@ -161,7 +165,14 @@ bool p3GroupDistrib::loadLocalCache(const CacheData &data) std::cerr << std::endl; #endif - return loadAnyCache(data, true); + loadAnyCache(data, true); + + if (data.size > 0) + { + refreshCache(data); + } + + return true; } @@ -222,11 +233,20 @@ void p3GroupDistrib::loadFileGroups(std::string filename, std::string src, bool delete streamer; + + + /* clear publication of groups if local cache file found */ + RsStackMutex stack(distribMtx); /******* STACK LOCKED MUTEX ***********/ + if (local) + { + mGroupsRepublish = false; + } + return; } -void p3GroupDistrib::loadFileMsgs(std::string filename, uint16_t cacheSubId, std::string src, bool local) +void p3GroupDistrib::loadFileMsgs(std::string filename, uint16_t cacheSubId, std::string src, uint32_t ts, bool local) { #ifdef DISTRIB_DEBUG @@ -271,6 +291,39 @@ void p3GroupDistrib::loadFileMsgs(std::string filename, uint16_t cacheSubId, std streamer->tick(); } + + if (local) + { + /* now we create a map of time -> subid + * This is used to determine the newest and the oldest items + */ +#ifdef DISTRIB_DEBUG + std::cerr << "p3GroupDistrib::loadFileMsgs() Updating Local TimeStamps"; + std::cerr << std::endl; + std::cerr << "p3GroupDistrib::loadFileMsgs() CacheSubId: " << cacheSubId << " recvd: " << ts; + std::cerr << std::endl; +#endif + + mLocalCacheTs[ts] = cacheSubId; + if (cacheSubId > mMaxCacheSubId) + { +#ifdef DISTRIB_DEBUG + std::cerr << "p3GroupDistrib::loadFileMsgs() New Max CacheSubId"; + std::cerr << std::endl; +#endif + mMaxCacheSubId = cacheSubId; + } + + if ((ts < now) && (ts > mLastPublishTime)) + { +#ifdef DISTRIB_DEBUG + std::cerr << "p3GroupDistrib::loadFileMsgs() New LastPublishTime"; + std::cerr << std::endl; +#endif + mLastPublishTime = ts; + } + } + delete streamer; return; } @@ -532,6 +585,7 @@ void p3GroupDistrib::loadMsg(RsDistribSignedMsg *newMsg, std::string src, bool l std::cerr << "p3GroupDistrib::loadMsg() unpack failed" << std::endl; std::cerr << std::endl; #endif + delete newMsg; return; } @@ -541,6 +595,7 @@ void p3GroupDistrib::loadMsg(RsDistribSignedMsg *newMsg, std::string src, bool l std::cerr << "p3GroupDistrib::loadMsg() check failed" << std::endl; std::cerr << std::endl; #endif + delete newMsg; delete msg; return; } @@ -554,9 +609,22 @@ void p3GroupDistrib::loadMsg(RsDistribSignedMsg *newMsg, std::string src, bool l #endif /* else if group = subscribed | listener -> publish */ - if (git->second.flags & (RS_DISTRIB_SUBSCRIBED)) + /* if it has come from us... then it has been published already */ + if ((!local) && (git->second.flags & (RS_DISTRIB_SUBSCRIBED))) { - locked_toPublishMsg(msg); +#ifdef DISTRIB_DEBUG + std::cerr << "p3GroupDistrib::loadMsg() To be Published!"; + std::cerr << std::endl; +#endif + locked_toPublishMsg(newMsg); + } + else + { +#ifdef DISTRIB_DEBUG + std::cerr << "p3GroupDistrib::loadMsg() Deleted Original Msg (No Publish)"; + std::cerr << std::endl; +#endif + delete newMsg; } locked_notifyGroupChanged(git->second); @@ -568,24 +636,76 @@ void p3GroupDistrib::loadMsg(RsDistribSignedMsg *newMsg, std::string src, bool l /****************** create/mod Cache Content **********************************/ /***************************************************************************************/ /***************************************************************************************/ -uint16_t p3GroupDistrib::determineCacheSubId() + +void p3GroupDistrib::locked_toPublishMsg(RsDistribSignedMsg *msg) { - return 1; +#ifdef DISTRIB_DEBUG + std::cerr << "p3GroupDistrib::locked_toPublishMsg() Adding to PendingPublish List"; + std::cerr << std::endl; +#endif + mPendingPublish.push_back(msg); + if (msg->PeerId() == mOwnId) + { + +#ifdef DISTRIB_DEBUG + std::cerr << "p3GroupDistrib::locked_toPublishMsg() Local -> ConfigSave Requested"; + std::cerr << std::endl; +#endif + /* we need to trigger Configuration save */ + IndicateConfigChanged(); /**** INDICATE CONFIG CHANGED! *****/ + } } -void p3GroupDistrib::locked_toPublishMsg(RsDistribMsg *msg) + +uint16_t p3GroupDistrib::locked_determineCacheSubId() { - mPendingPublish.push_back(msg); + /* if oldest cache is previous to StorePeriod - use that */ + time_t now = time(NULL); + uint16_t id = 1; + + uint32_t oldest = now; + if (mLocalCacheTs.size() > 0) + { + oldest = mLocalCacheTs.begin()->first; + } + + if (oldest < now - mStorePeriod) + { + /* clear it out, return id */ + id = mLocalCacheTs.begin()->second; + mLocalCacheTs.erase(mLocalCacheTs.begin()); + +#ifdef DISTRIB_DEBUG + std::cerr << "p3GroupDistrib::locked_determineCacheSubId() Replacing Old CacheId: " << id; + std::cerr << std::endl; +#endif + return id; + } + + mMaxCacheSubId++; + +#ifdef DISTRIB_DEBUG + std::cerr << "p3GroupDistrib::locked_determineCacheSubId() Returning new Id: " << mMaxCacheSubId; + std::cerr << std::endl; +#endif + /* else return maximum */ + return mMaxCacheSubId; } + void p3GroupDistrib::locked_publishPendingMsgs() { +#ifdef DISTRIB_DEBUG + std::cerr << "p3GroupDistrib::locked_publishPendingMsgs()"; + std::cerr << std::endl; +#endif /* get the next message id */ CacheData newCache; + time_t now = time(NULL); newCache.pid = mOwnId; newCache.cid.type = CacheSource::getCacheType(); - newCache.cid.subid = determineCacheSubId(); // NOT fixed - should rotate. + newCache.cid.subid = locked_determineCacheSubId(); /* create filename */ std::string path = CacheSource::getCacheDir(); @@ -597,20 +717,34 @@ void p3GroupDistrib::locked_publishPendingMsgs() BinInterface *bio = new BinFileInterface(filename.c_str(), BIN_FLAGS_WRITEABLE | BIN_FLAGS_HASH_DATA); - pqistreamer *streamer = createStreamer(bio, mOwnId, - BIN_FLAGS_NO_DELETE); + pqistreamer *streamer = createStreamer(bio, mOwnId, 0); /* messages are deleted! */ - - std::list::iterator it; + bool resave = false; + std::list::iterator it; for(it = mPendingPublish.begin(); it != mPendingPublish.end(); it++) { - streamer->SendItem(*it); /* doesnt delete it */ +#ifdef DISTRIB_DEBUG + std::cerr << "p3GroupDistrib::locked_publishPendingMsgs() Publishing:"; + std::cerr << std::endl; + (*it)->print(std::cerr, 10); + std::cerr << std::endl; +#endif + if ((*it)->PeerId() == mOwnId) + { + +#ifdef DISTRIB_DEBUG + std::cerr << "p3GroupDistrib::locked_publishPendingMsgs() Own Publish"; + std::cerr << std::endl; +#endif + resave = true; + } + + streamer->SendItem(*it); /* deletes it */ streamer->tick(); + } - /* cleanup */ - mPendingPublish.clear(); - delete streamer; + streamer->tick(); /* once more for good luck! */ /* Extract File Information from pqistreamer */ newCache.path = path; @@ -618,13 +752,27 @@ void p3GroupDistrib::locked_publishPendingMsgs() newCache.hash = bio->gethash(); newCache.size = bio->bytecount(); - newCache.recvd = time(NULL); + newCache.recvd = now; + + /* cleanup */ + mPendingPublish.clear(); + delete streamer; + + /* indicate not to save for a while */ + mLastPublishTime = now; /* push file to CacheSource */ refreshCache(newCache); - /* flag to store config (saying we've published messages) */ - IndicateConfigChanged(); /**** INDICATE CONFIG CHANGED! *****/ + if (resave) + { +#ifdef DISTRIB_DEBUG + std::cerr << "p3GroupDistrib::locked_publishPendingMsgs() Indicate Save Data Changed"; + std::cerr << std::endl; +#endif + /* flag to store config (saying we've published messages) */ + IndicateConfigChanged(); /**** INDICATE CONFIG CHANGED! *****/ + } } @@ -635,12 +783,12 @@ void p3GroupDistrib::publishDistribGroups() std::cerr << std::endl; #endif - /* set subid = 0 */ + /* set subid = 1 */ CacheData newCache; newCache.pid = mOwnId; newCache.cid.type = CacheSource::getCacheType(); - newCache.cid.subid = 0; + newCache.cid.subid = 1; /* create filename */ std::string path = CacheSource::getCacheDir(); @@ -1086,6 +1234,15 @@ std::list p3GroupDistrib::saveList(bool &cleanup) } + std::list::iterator mit; + for(mit = mPendingPublish.begin(); mit != mPendingPublish.end(); mit++) + { + if ((*mit)->PeerId() == mOwnId) + { + saveData.push_back(*mit); + } + } + return saveData; } @@ -1113,6 +1270,7 @@ bool p3GroupDistrib::loadList(std::list load) RsDistribGrp *newGrp = NULL; RsDistribGrpKey *newKey = NULL; + RsDistribSignedMsg *newMsg = NULL; if ((newGrp = dynamic_cast(*lit))) { @@ -1134,6 +1292,11 @@ bool p3GroupDistrib::loadList(std::list load) { loadGroupKey(newKey); } + else if ((newMsg = dynamic_cast(*lit))) + { + newMsg->PeerId(mOwnId); + loadMsg(newMsg, mOwnId, false); /* false so it'll pushed to PendingPublish list */ + } } /* no need to republish until something new comes in */ @@ -1545,8 +1708,12 @@ std::string p3GroupDistrib::publishMsg(RsDistribMsg *msg, bool personalSign) std::cerr << std::endl; #endif - /* load proper */ - loadMsg(signedMsg, "ownId", true); + /* load proper - + * If we pretend it is coming from an alternative source + * it'll automatically get published with other msgs + */ + signedMsg->PeerId(mOwnId); + loadMsg(signedMsg, mOwnId, false); /* done */ return msgId; @@ -2262,14 +2429,20 @@ RsDistribMsg *p3GroupDistrib::unpackDistribSignedMsg(RsDistribSignedMsg *newMsg) /* transfer data that is not in the serialiser */ distribMsg->msgId = newMsg->msgId; - distribMsg->publishSignature = newMsg->publishSignature; - distribMsg->personalSignature = newMsg->personalSignature; - newMsg->publishSignature.ShallowClear(); - newMsg->personalSignature.ShallowClear(); + /* Full copies required ? */ + + distribMsg->publishSignature.keyId = newMsg->publishSignature.keyId; + distribMsg->publishSignature.signData.setBinData( + newMsg->publishSignature.signData.bin_data, + newMsg->publishSignature.signData.bin_len); + + distribMsg->personalSignature.keyId = newMsg->personalSignature.keyId; + distribMsg->personalSignature.signData.setBinData( + newMsg->personalSignature.signData.bin_data, + newMsg->personalSignature.signData.bin_len); } - delete newMsg; delete serialType; return distribMsg; diff --git a/libretroshare/src/services/p3distrib.h b/libretroshare/src/services/p3distrib.h index df5b082e9..2c140410e 100644 --- a/libretroshare/src/services/p3distrib.h +++ b/libretroshare/src/services/p3distrib.h @@ -221,7 +221,7 @@ int loadAnyCache(const CacheData &data, bool local); /* load cache files */ void loadFileGroups(std::string filename, std::string src, bool local); -void loadFileMsgs(std::string filename, uint16_t cacheSubId, std::string src, bool local); +void loadFileMsgs(std::string filename, uint16_t cacheSubId, std::string src, uint32_t ts, bool local); protected: /* load cache msgs */ @@ -303,13 +303,13 @@ virtual int tick(); /* overloaded form pqiService */ protected: /* create/mod cache content */ -void locked_toPublishMsg(RsDistribMsg *msg); +void locked_toPublishMsg(RsDistribSignedMsg *msg); void publishPendingMsgs(); void publishDistribGroups(); void clear_local_caches(time_t now); void locked_publishPendingMsgs(); -uint16_t determineCacheSubId(); +uint16_t locked_determineCacheSubId(); /***************************************************************************************/ @@ -382,14 +382,18 @@ void locked_notifyGroupChanged(GroupInfo &info); std::list mLocalCaches; std::map mGroups; uint32_t mStorePeriod, mPubPeriod; - time_t mNextPublishTime; - std::list mPendingPublish; + /* Message Publishing */ + std::list mPendingPublish; + time_t mLastPublishTime; + std::map mLocalCacheTs; + uint16_t mMaxCacheSubId; bool mGroupsChanged; bool mGroupsRepublish; std::list saveCleanupList; /* TEMPORARY LIST WHEN SAVING */ + };