/indra/llcommon/llevents.cpp

https://bitbucket.org/lindenlab/viewer-beta/ · C++ · 614 lines · 347 code · 35 blank · 232 comment · 61 complexity · 37403f6e3662bf4533cd129b9480034b MD5 · raw file

  1. /**
  2. * @file llevents.cpp
  3. * @author Nat Goodspeed
  4. * @date 2008-09-12
  5. * @brief Implementation for llevents.
  6. *
  7. * $LicenseInfo:firstyear=2008&license=viewerlgpl$
  8. * Second Life Viewer Source Code
  9. * Copyright (C) 2010, Linden Research, Inc.
  10. *
  11. * This library is free software; you can redistribute it and/or
  12. * modify it under the terms of the GNU Lesser General Public
  13. * License as published by the Free Software Foundation;
  14. * version 2.1 of the License only.
  15. *
  16. * This library is distributed in the hope that it will be useful,
  17. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  19. * Lesser General Public License for more details.
  20. *
  21. * You should have received a copy of the GNU Lesser General Public
  22. * License along with this library; if not, write to the Free Software
  23. * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  24. *
  25. * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
  26. * $/LicenseInfo$
  27. */
  28. // Precompiled header
  29. #include "linden_common.h"
  30. #if LL_WINDOWS
  31. #pragma warning (disable : 4675) // "resolved by ADL" -- just as I want!
  32. #endif
  33. // associated header
  34. #include "llevents.h"
  35. // STL headers
  36. #include <set>
  37. #include <sstream>
  38. #include <algorithm>
  39. // std headers
  40. #include <typeinfo>
  41. #include <cassert>
  42. #include <cmath>
  43. #include <cctype>
  44. // external library headers
  45. #include <boost/range/iterator_range.hpp>
  46. #if LL_WINDOWS
  47. #pragma warning (push)
  48. #pragma warning (disable : 4701) // compiler thinks might use uninitialized var, but no
  49. #endif
  50. #include <boost/lexical_cast.hpp>
  51. #if LL_WINDOWS
  52. #pragma warning (pop)
  53. #endif
  54. // other Linden headers
  55. #include "stringize.h"
  56. #include "llerror.h"
  57. #include "llsdutil.h"
  58. #if LL_MSVC
  59. #pragma warning (disable : 4702)
  60. #endif
  61. /*****************************************************************************
  62. * queue_names: specify LLEventPump names that should be instantiated as
  63. * LLEventQueue
  64. *****************************************************************************/
  65. /**
  66. * At present, we recognize particular requested LLEventPump names as needing
  67. * LLEventQueues. Later on we'll migrate this information to an external
  68. * configuration file.
  69. */
  70. const char* queue_names[] =
  71. {
  72. "placeholder - replace with first real name string"
  73. };
  74. /*****************************************************************************
  75. * If there's a "mainloop" pump, listen on that to flush all LLEventQueues
  76. *****************************************************************************/
  77. struct RegisterFlush : public LLEventTrackable
  78. {
  79. RegisterFlush():
  80. pumps(LLEventPumps::instance())
  81. {
  82. pumps.obtain("mainloop").listen("flushLLEventQueues", boost::bind(&RegisterFlush::flush, this, _1));
  83. }
  84. bool flush(const LLSD&)
  85. {
  86. pumps.flush();
  87. return false;
  88. }
  89. ~RegisterFlush()
  90. {
  91. // LLEventTrackable handles stopListening for us.
  92. }
  93. LLEventPumps& pumps;
  94. };
  95. static RegisterFlush registerFlush;
  96. /*****************************************************************************
  97. * LLEventPumps
  98. *****************************************************************************/
  99. LLEventPumps::LLEventPumps():
  100. // Until we migrate this information to an external config file,
  101. // initialize mQueueNames from the static queue_names array.
  102. mQueueNames(boost::begin(queue_names), boost::end(queue_names))
  103. {
  104. }
  105. LLEventPump& LLEventPumps::obtain(const std::string& name)
  106. {
  107. PumpMap::iterator found = mPumpMap.find(name);
  108. if (found != mPumpMap.end())
  109. {
  110. // Here we already have an LLEventPump instance with the requested
  111. // name.
  112. return *found->second;
  113. }
  114. // Here we must instantiate an LLEventPump subclass.
  115. LLEventPump* newInstance;
  116. // Should this name be an LLEventQueue?
  117. PumpNames::const_iterator nfound = mQueueNames.find(name);
  118. if (nfound != mQueueNames.end())
  119. newInstance = new LLEventQueue(name);
  120. else
  121. newInstance = new LLEventStream(name);
  122. // LLEventPump's constructor implicitly registers each new instance in
  123. // mPumpMap. But remember that we instantiated it (in mOurPumps) so we'll
  124. // delete it later.
  125. mOurPumps.insert(newInstance);
  126. return *newInstance;
  127. }
  128. void LLEventPumps::flush()
  129. {
  130. // Flush every known LLEventPump instance. Leave it up to each instance to
  131. // decide what to do with the flush() call.
  132. for (PumpMap::iterator pmi = mPumpMap.begin(), pmend = mPumpMap.end(); pmi != pmend; ++pmi)
  133. {
  134. pmi->second->flush();
  135. }
  136. }
  137. void LLEventPumps::reset()
  138. {
  139. // Reset every known LLEventPump instance. Leave it up to each instance to
  140. // decide what to do with the reset() call.
  141. for (PumpMap::iterator pmi = mPumpMap.begin(), pmend = mPumpMap.end(); pmi != pmend; ++pmi)
  142. {
  143. pmi->second->reset();
  144. }
  145. }
  146. std::string LLEventPumps::registerNew(const LLEventPump& pump, const std::string& name, bool tweak)
  147. {
  148. std::pair<PumpMap::iterator, bool> inserted =
  149. mPumpMap.insert(PumpMap::value_type(name, const_cast<LLEventPump*>(&pump)));
  150. // If the insert worked, then the name is unique; return that.
  151. if (inserted.second)
  152. return name;
  153. // Here the new entry was NOT inserted, and therefore name isn't unique.
  154. // Unless we're permitted to tweak it, that's Bad.
  155. if (! tweak)
  156. {
  157. throw LLEventPump::DupPumpName(std::string("Duplicate LLEventPump name '") + name + "'");
  158. }
  159. // The passed name isn't unique, but we're permitted to tweak it. Find the
  160. // first decimal-integer suffix not already taken. The insert() attempt
  161. // above will have set inserted.first to the iterator of the existing
  162. // entry by that name. Starting there, walk forward until we reach an
  163. // entry that doesn't start with 'name'. For each entry consisting of name
  164. // + integer suffix, capture the integer suffix in a set. Use a set
  165. // because we're going to encounter string suffixes in the order: name1,
  166. // name10, name11, name2, ... Walking those possibilities in that order
  167. // isn't convenient to detect the first available "hole."
  168. std::set<int> suffixes;
  169. PumpMap::iterator pmi(inserted.first), pmend(mPumpMap.end());
  170. // We already know inserted.first references the existing entry with
  171. // 'name' as the key; skip that one and start with the next.
  172. while (++pmi != pmend)
  173. {
  174. if (pmi->first.substr(0, name.length()) != name)
  175. {
  176. // Found the first entry beyond the entries starting with 'name':
  177. // stop looping.
  178. break;
  179. }
  180. // Here we're looking at an entry that starts with 'name'. Is the rest
  181. // of it an integer?
  182. // Dubious (?) assumption: in the local character set, decimal digits
  183. // are in increasing order such that '9' is the last of them. This
  184. // test deals with 'name' values such as 'a', where there might be a
  185. // very large number of entries starting with 'a' whose suffixes
  186. // aren't integers. A secondary assumption is that digit characters
  187. // precede most common name characters (true in ASCII, false in
  188. // EBCDIC). The test below is correct either way, but it's worth more
  189. // if the assumption holds.
  190. if (pmi->first[name.length()] > '9')
  191. break;
  192. // It should be cheaper to detect that we're not looking at a digit
  193. // character -- and therefore the suffix can't possibly be an integer
  194. // -- than to attempt the lexical_cast and catch the exception.
  195. if (! std::isdigit(pmi->first[name.length()]))
  196. continue;
  197. // Okay, the first character of the suffix is a digit, it's worth at
  198. // least attempting to convert to int.
  199. try
  200. {
  201. suffixes.insert(boost::lexical_cast<int>(pmi->first.substr(name.length())));
  202. }
  203. catch (const boost::bad_lexical_cast&)
  204. {
  205. // If the rest of pmi->first isn't an int, just ignore it.
  206. }
  207. }
  208. // Here we've accumulated in 'suffixes' all existing int suffixes of the
  209. // entries starting with 'name'. Find the first unused one.
  210. int suffix = 1;
  211. for ( ; suffixes.find(suffix) != suffixes.end(); ++suffix)
  212. ;
  213. // Here 'suffix' is not in 'suffixes'. Construct a new name based on that
  214. // suffix, insert it and return it.
  215. std::ostringstream out;
  216. out << name << suffix;
  217. return registerNew(pump, out.str(), tweak);
  218. }
  219. void LLEventPumps::unregister(const LLEventPump& pump)
  220. {
  221. // Remove this instance from mPumpMap
  222. PumpMap::iterator found = mPumpMap.find(pump.getName());
  223. if (found != mPumpMap.end())
  224. {
  225. mPumpMap.erase(found);
  226. }
  227. // If this instance is one we created, also remove it from mOurPumps so we
  228. // won't try again to delete it later!
  229. PumpSet::iterator psfound = mOurPumps.find(const_cast<LLEventPump*>(&pump));
  230. if (psfound != mOurPumps.end())
  231. {
  232. mOurPumps.erase(psfound);
  233. }
  234. }
  235. LLEventPumps::~LLEventPumps()
  236. {
  237. // On destruction, delete every LLEventPump we instantiated (via
  238. // obtain()). CAREFUL: deleting an LLEventPump calls its destructor, which
  239. // calls unregister(), which removes that LLEventPump instance from
  240. // mOurPumps. So an iterator loop over mOurPumps to delete contained
  241. // LLEventPump instances is dangerous! Instead, delete them one at a time
  242. // until mOurPumps is empty.
  243. while (! mOurPumps.empty())
  244. {
  245. delete *mOurPumps.begin();
  246. }
  247. }
  248. /*****************************************************************************
  249. * LLEventPump
  250. *****************************************************************************/
  251. #if LL_WINDOWS
  252. #pragma warning (push)
  253. #pragma warning (disable : 4355) // 'this' used in initializer list: yes, intentionally
  254. #endif
  255. LLEventPump::LLEventPump(const std::string& name, bool tweak):
  256. // Register every new instance with LLEventPumps
  257. mName(LLEventPumps::instance().registerNew(*this, name, tweak)),
  258. mSignal(new LLStandardSignal()),
  259. mEnabled(true)
  260. {}
  261. #if LL_WINDOWS
  262. #pragma warning (pop)
  263. #endif
  264. LLEventPump::~LLEventPump()
  265. {
  266. // Unregister this doomed instance from LLEventPumps
  267. LLEventPumps::instance().unregister(*this);
  268. }
  269. // static data member
  270. const LLEventPump::NameList LLEventPump::empty;
  271. std::string LLEventPump::inventName(const std::string& pfx)
  272. {
  273. static long suffix = 0;
  274. return STRINGIZE(pfx << suffix++);
  275. }
  276. void LLEventPump::reset()
  277. {
  278. mSignal.reset();
  279. mConnections.clear();
  280. //mDeps.clear();
  281. }
  282. LLBoundListener LLEventPump::listen_impl(const std::string& name, const LLEventListener& listener,
  283. const NameList& after,
  284. const NameList& before)
  285. {
  286. // Check for duplicate name before connecting listener to mSignal
  287. ConnectionMap::const_iterator found = mConnections.find(name);
  288. // In some cases the user might disconnect a connection explicitly -- or
  289. // might use LLEventTrackable to disconnect implicitly. Either way, we can
  290. // end up retaining in mConnections a zombie connection object that's
  291. // already been disconnected. Such a connection object can't be
  292. // reconnected -- nor, in the case of LLEventTrackable, would we want to
  293. // try, since disconnection happens with the destruction of the listener
  294. // object. That means it's safe to overwrite a disconnected connection
  295. // object with the new one we're attempting. The case we want to prevent
  296. // is only when the existing connection object is still connected.
  297. if (found != mConnections.end() && found->second.connected())
  298. {
  299. throw DupListenerName(std::string("Attempt to register duplicate listener name '") + name +
  300. "' on " + typeid(*this).name() + " '" + getName() + "'");
  301. }
  302. // Okay, name is unique, try to reconcile its dependencies. Specify a new
  303. // "node" value that we never use for an mSignal placement; we'll fix it
  304. // later.
  305. DependencyMap::node_type& newNode = mDeps.add(name, -1.0, after, before);
  306. // What if this listener has been added, removed and re-added? In that
  307. // case newNode already has a non-negative value because we never remove a
  308. // listener from mDeps. But keep processing uniformly anyway in case the
  309. // listener was added back with different dependencies. Then mDeps.sort()
  310. // would put it in a different position, and the old newNode placement
  311. // value would be wrong, so we'd have to reassign it anyway. Trust that
  312. // re-adding a listener with the same dependencies is the trivial case for
  313. // mDeps.sort(): it can just replay its cache.
  314. DependencyMap::sorted_range sorted_range;
  315. try
  316. {
  317. // Can we pick an order that works including this new entry?
  318. sorted_range = mDeps.sort();
  319. }
  320. catch (const DependencyMap::Cycle& e)
  321. {
  322. // No: the new node's after/before dependencies have made mDeps
  323. // unsortable. If we leave the new node in mDeps, it will continue
  324. // to screw up all future attempts to sort()! Pull it out.
  325. mDeps.remove(name);
  326. throw Cycle(std::string("New listener '") + name + "' on " + typeid(*this).name() +
  327. " '" + getName() + "' would cause cycle: " + e.what());
  328. }
  329. // Walk the list to verify that we haven't changed the order.
  330. float previous = 0.0, myprev = 0.0;
  331. DependencyMap::sorted_iterator mydmi = sorted_range.end(); // need this visible after loop
  332. for (DependencyMap::sorted_iterator dmi = sorted_range.begin();
  333. dmi != sorted_range.end(); ++dmi)
  334. {
  335. // Since we've added the new entry with an invalid placement,
  336. // recognize it and skip it.
  337. if (dmi->first == name)
  338. {
  339. // Remember the iterator belonging to our new node, and which
  340. // placement value was 'previous' at that point.
  341. mydmi = dmi;
  342. myprev = previous;
  343. continue;
  344. }
  345. // If the new node has rearranged the existing nodes, we'll find
  346. // that their placement values are no longer in increasing order.
  347. if (dmi->second < previous)
  348. {
  349. // This is another scenario in which we'd better back out the
  350. // newly-added node from mDeps -- but don't do it yet, we want to
  351. // traverse the existing mDeps to report on it!
  352. // Describe the change to the order of our listeners. Copy
  353. // everything but the newest listener to a vector we can sort to
  354. // obtain the old order.
  355. typedef std::vector< std::pair<float, std::string> > SortNameList;
  356. SortNameList sortnames;
  357. for (DependencyMap::sorted_iterator cdmi(sorted_range.begin()), cdmend(sorted_range.end());
  358. cdmi != cdmend; ++cdmi)
  359. {
  360. if (cdmi->first != name)
  361. {
  362. sortnames.push_back(SortNameList::value_type(cdmi->second, cdmi->first));
  363. }
  364. }
  365. std::sort(sortnames.begin(), sortnames.end());
  366. std::ostringstream out;
  367. out << "New listener '" << name << "' on " << typeid(*this).name() << " '" << getName()
  368. << "' would move previous listener '" << dmi->first << "'\nwas: ";
  369. SortNameList::const_iterator sni(sortnames.begin()), snend(sortnames.end());
  370. if (sni != snend)
  371. {
  372. out << sni->second;
  373. while (++sni != snend)
  374. {
  375. out << ", " << sni->second;
  376. }
  377. }
  378. out << "\nnow: ";
  379. DependencyMap::sorted_iterator ddmi(sorted_range.begin()), ddmend(sorted_range.end());
  380. if (ddmi != ddmend)
  381. {
  382. out << ddmi->first;
  383. while (++ddmi != ddmend)
  384. {
  385. out << ", " << ddmi->first;
  386. }
  387. }
  388. // NOW remove the offending listener node.
  389. mDeps.remove(name);
  390. // Having constructed a description of the order change, inform caller.
  391. throw OrderChange(out.str());
  392. }
  393. // This node becomes the previous one.
  394. previous = dmi->second;
  395. }
  396. // We just got done with a successful mDeps.add(name, ...) call. We'd
  397. // better have found 'name' somewhere in that sorted list!
  398. assert(mydmi != sorted_range.end());
  399. // Four cases:
  400. // 0. name is the only entry: placement 1.0
  401. // 1. name is the first of several entries: placement (next placement)/2
  402. // 2. name is between two other entries: placement (myprev + (next placement))/2
  403. // 3. name is the last entry: placement ceil(myprev) + 1.0
  404. // Since we've cleverly arranged for myprev to be 0.0 if name is the
  405. // first entry, this folds down to two cases. Case 1 is subsumed by
  406. // case 2, and case 0 is subsumed by case 3. So we need only handle
  407. // cases 2 and 3, which means we need only detect whether name is the
  408. // last entry. Increment mydmi to see if there's anything beyond.
  409. if (++mydmi != sorted_range.end())
  410. {
  411. // The new node isn't last. Place it between the previous node and
  412. // the successor.
  413. newNode = (myprev + mydmi->second)/2.0;
  414. }
  415. else
  416. {
  417. // The new node is last. Bump myprev up to the next integer, add
  418. // 1.0 and use that.
  419. newNode = std::ceil(myprev) + 1.0;
  420. }
  421. // Now that newNode has a value that places it appropriately in mSignal,
  422. // connect it.
  423. LLBoundListener bound = mSignal->connect(newNode, listener);
  424. mConnections[name] = bound;
  425. return bound;
  426. }
  427. LLBoundListener LLEventPump::getListener(const std::string& name) const
  428. {
  429. ConnectionMap::const_iterator found = mConnections.find(name);
  430. if (found != mConnections.end())
  431. {
  432. return found->second;
  433. }
  434. // not found, return dummy LLBoundListener
  435. return LLBoundListener();
  436. }
  437. void LLEventPump::stopListening(const std::string& name)
  438. {
  439. ConnectionMap::iterator found = mConnections.find(name);
  440. if (found != mConnections.end())
  441. {
  442. found->second.disconnect();
  443. mConnections.erase(found);
  444. }
  445. // We intentionally do NOT remove this name from mDeps. It may happen that
  446. // the same listener with the same name and dependencies will jump on and
  447. // off this LLEventPump repeatedly. Keeping a cache of dependencies will
  448. // avoid a new dependency sort in such cases.
  449. }
  450. /*****************************************************************************
  451. * LLEventStream
  452. *****************************************************************************/
  453. bool LLEventStream::post(const LLSD& event)
  454. {
  455. if (! mEnabled || !mSignal)
  456. {
  457. return false;
  458. }
  459. // NOTE NOTE NOTE: Any new access to member data beyond this point should
  460. // cause us to move our LLStandardSignal object to a pimpl class along
  461. // with said member data. Then the local shared_ptr will preserve both.
  462. // DEV-43463: capture a local copy of mSignal. We've turned up a
  463. // cross-coroutine scenario (described in the Jira) in which this post()
  464. // call could end up destroying 'this', the LLEventPump subclass instance
  465. // containing mSignal, during the call through *mSignal. So -- capture a
  466. // *stack* instance of the shared_ptr, ensuring that our heap
  467. // LLStandardSignal object will live at least until post() returns, even
  468. // if 'this' gets destroyed during the call.
  469. boost::shared_ptr<LLStandardSignal> signal(mSignal);
  470. // Let caller know if any one listener handled the event. This is mostly
  471. // useful when using LLEventStream as a listener for an upstream
  472. // LLEventPump.
  473. return (*signal)(event);
  474. }
  475. /*****************************************************************************
  476. * LLEventQueue
  477. *****************************************************************************/
  478. bool LLEventQueue::post(const LLSD& event)
  479. {
  480. if (mEnabled)
  481. {
  482. // Defer sending this event by queueing it until flush()
  483. mEventQueue.push_back(event);
  484. }
  485. // Unconditionally return false. We won't know until flush() whether a
  486. // listener claims to have handled the event -- meanwhile, don't block
  487. // other listeners.
  488. return false;
  489. }
  490. void LLEventQueue::flush()
  491. {
  492. if(!mSignal) return;
  493. // Consider the case when a given listener on this LLEventQueue posts yet
  494. // another event on the same queue. If we loop over mEventQueue directly,
  495. // we'll end up processing all those events during the same flush() call
  496. // -- rather like an EventStream. Instead, copy mEventQueue and clear it,
  497. // so that any new events posted to this LLEventQueue during flush() will
  498. // be processed in the *next* flush() call.
  499. EventQueue queue(mEventQueue);
  500. mEventQueue.clear();
  501. // NOTE NOTE NOTE: Any new access to member data beyond this point should
  502. // cause us to move our LLStandardSignal object to a pimpl class along
  503. // with said member data. Then the local shared_ptr will preserve both.
  504. // DEV-43463: capture a local copy of mSignal. See LLEventStream::post()
  505. // for detailed comments.
  506. boost::shared_ptr<LLStandardSignal> signal(mSignal);
  507. for ( ; ! queue.empty(); queue.pop_front())
  508. {
  509. (*signal)(queue.front());
  510. }
  511. }
  512. /*****************************************************************************
  513. * LLListenerOrPumpName
  514. *****************************************************************************/
  515. LLListenerOrPumpName::LLListenerOrPumpName(const std::string& pumpname):
  516. // Look up the specified pumpname, and bind its post() method as our listener
  517. mListener(boost::bind(&LLEventPump::post,
  518. boost::ref(LLEventPumps::instance().obtain(pumpname)),
  519. _1))
  520. {
  521. }
  522. LLListenerOrPumpName::LLListenerOrPumpName(const char* pumpname):
  523. // Look up the specified pumpname, and bind its post() method as our listener
  524. mListener(boost::bind(&LLEventPump::post,
  525. boost::ref(LLEventPumps::instance().obtain(pumpname)),
  526. _1))
  527. {
  528. }
  529. bool LLListenerOrPumpName::operator()(const LLSD& event) const
  530. {
  531. if (! mListener)
  532. {
  533. throw Empty("attempting to call uninitialized");
  534. }
  535. return (*mListener)(event);
  536. }
  537. void LLReqID::stamp(LLSD& response) const
  538. {
  539. if (! (response.isUndefined() || response.isMap()))
  540. {
  541. // If 'response' was previously completely empty, it's okay to
  542. // turn it into a map. If it was already a map, then it should be
  543. // okay to add a key. But if it was anything else (e.g. a scalar),
  544. // assigning a ["reqid"] key will DISCARD the previous value,
  545. // replacing it with a map. That would be Bad.
  546. LL_INFOS("LLReqID") << "stamp(" << mReqid << ") leaving non-map response unmodified: "
  547. << response << LL_ENDL;
  548. return;
  549. }
  550. LLSD oldReqid(response["reqid"]);
  551. if (! (oldReqid.isUndefined() || llsd_equals(oldReqid, mReqid)))
  552. {
  553. LL_INFOS("LLReqID") << "stamp(" << mReqid << ") preserving existing [\"reqid\"] value "
  554. << oldReqid << " in response: " << response << LL_ENDL;
  555. return;
  556. }
  557. response["reqid"] = mReqid;
  558. }
  559. bool sendReply(const LLSD& reply, const LLSD& request, const std::string& replyKey)
  560. {
  561. // If the original request has no value for replyKey, it's pointless to
  562. // construct or send a reply event: on which LLEventPump should we send
  563. // it? Allow that to be optional: if the caller wants to require replyKey,
  564. // it can so specify when registering the operation method.
  565. if (! request.has(replyKey))
  566. {
  567. return false;
  568. }
  569. // Here the request definitely contains replyKey; reasonable to proceed.
  570. // Copy 'reply' to modify it.
  571. LLSD newreply(reply);
  572. // Get the ["reqid"] element from request
  573. LLReqID reqID(request);
  574. // and copy it to 'newreply'.
  575. reqID.stamp(newreply);
  576. // Send reply on LLEventPump named in request[replyKey]. Don't forget to
  577. // send the modified 'newreply' instead of the original 'reply'.
  578. return LLEventPumps::instance().obtain(request[replyKey]).post(newreply);
  579. }