]> git.ipfire.org Git - thirdparty/squid.git/blame - src/cbdata.cc
Refactored actions for "carp" to Cache Manager singleton API.
[thirdparty/squid.git] / src / cbdata.cc
CommitLineData
365e5b34 1
e1ac4723 2/*
63be0a78 3 * $Id: cbdata.cc,v 1.77 2008/02/26 21:49:34 amosjeffries Exp $
e1ac4723 4 *
5 * DEBUG: section 45 Callback Data Registry
28c60158 6 * ORIGINAL AUTHOR: Duane Wessels
7 * Modified by Moez Mahfoudh (08/12/2000)
e48ed433 8 * History added by Robert Collins (2002-10-25)
e1ac4723 9 *
2b6662ba 10 * SQUID Web Proxy Cache http://www.squid-cache.org/
e25c139f 11 * ----------------------------------------------------------
e1ac4723 12 *
2b6662ba 13 * Squid is the result of efforts by numerous individuals from
14 * the Internet community; see the CONTRIBUTORS file for full
15 * details. Many organizations have provided support for Squid's
16 * development; see the SPONSORS file for full details. Squid is
17 * Copyrighted (C) 2001 by the Regents of the University of
18 * California; see the COPYRIGHT file for full details. Squid
19 * incorporates software developed and/or copyrighted by other
20 * sources; see the CREDITS file for full details.
e1ac4723 21 *
22 * This program is free software; you can redistribute it and/or modify
23 * it under the terms of the GNU General Public License as published by
24 * the Free Software Foundation; either version 2 of the License, or
25 * (at your option) any later version.
26 *
27 * This program is distributed in the hope that it will be useful,
28 * but WITHOUT ANY WARRANTY; without even the implied warranty of
29 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30 * GNU General Public License for more details.
31 *
32 * You should have received a copy of the GNU General Public License
33 * along with this program; if not, write to the Free Software
cbdec147 34 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA.
e25c139f 35 *
e1ac4723 36 */
37
63be0a78 38/**
39 \defgroup CBDATAInternal Callback Data Allocator Internals
40 \ingroup CBDATAAPI
41 *
e1ac4723 42 * These routines manage a set of registered callback data pointers.
63be0a78 43 * One of the easiest ways to make Squid coredump is to issue a
e1ac4723 44 * callback to for some data structure which has previously been
45 * freed. With these routines, we register (add) callback data
46 * pointers, lock them just before registering the callback function,
47 * validate them before issuing the callback, and then free them
48 * when finished.
e1ac4723 49 */
a2172245 50
aa839030 51#include "cbdata.h"
62ee09ca 52#include "CacheManager.h"
e6ccf245 53#include "Store.h"
e48ed433 54#if CBDATA_DEBUG
55#include "Stack.h"
56#endif
57#include "Generic.h"
a2172245 58
b4bab919 59#if WITH_VALGRIND
60#define HASHED_CBDATA 1
61#endif
62
8b9a2d90 63static int cbdataCount = 0;
fa80a8ef 64#if CBDATA_DEBUG
65dlink_list cbdataEntries;
66#endif
8b9a2d90 67
e48ed433 68#if CBDATA_DEBUG
62e76326 69
70class CBDataCall
71{
72
e48ed433 73public:
74 CBDataCall (char const *callLabel, char const *aFile, int aLine) : label(callLabel), file(aFile), line(aLine){}
62e76326 75
e48ed433 76 char const *label;
77 char const *file;
78 int line;
79};
62e76326 80
e48ed433 81#endif
82
63be0a78 83/// \ingroup CBDATAInternal
43ae1d95 84#define OFFSET_OF(TYPE, MEMBER) ((size_t) &(((TYPE) *)0)->(MEMBER))
85
63be0a78 86/// \ingroup CBDATAInternal
43ae1d95 87class cbdata
62e76326 88{
63be0a78 89 /** \todo examine making cbdata templated on this - so we get type
b4bab919 90 * safe access to data - RBC 20030902 */
43ae1d95 91public:
b4bab919 92#if HASHED_CBDATA
93 hash_link hash; // Must be first
94#endif
95
e48ed433 96#if CBDATA_DEBUG
43ae1d95 97
e48ed433 98 void dump(StoreEntry *)const;
99#endif
62e76326 100
b4bab919 101#if !HASHED_CBDATA
f668fcda 102 void *operator new(size_t size, void *where);
e13b11a5 103 void operator delete(void *where, void *where2);
b4bab919 104#else
105 MEMPROXY_CLASS(cndata);
106#endif
f668fcda 107
108 ~cbdata();
3015e83a 109 int valid;
110 int locks;
f668fcda 111 cbdata_type type;
4e3f712d 112#if CBDATA_DEBUG
62e76326 113
91caca83 114 void addHistory(char const *label, char const *file, int line)
62e76326 115 {
91caca83 116 if (calls.size() > 1000)
62e76326 117 return;
118
91caca83 119 calls.push_back(new CBDataCall(label, file, line));
62e76326 120 }
121
fa80a8ef 122 dlink_node link;
4e3f712d 123 const char *file;
124 int line;
91caca83 125 Stack<CBDataCall*> calls;
4e3f712d 126#endif
62e76326 127
43ae1d95 128 /* cookie used while debugging */
129 long cookie;
b4bab919 130 void check(int line) const {assert(cookie == ((long)this ^ Cookie));}
131 static const long Cookie;
43ae1d95 132
b4bab919 133#if !HASHED_CBDATA
43ae1d95 134 size_t dataSize() const { return sizeof(data);}
43ae1d95 135 static long MakeOffset();
136 static const long Offset;
b4bab919 137 /* MUST be the last per-instance member */
138 void *data;
139#endif
140
43ae1d95 141};
142
bf5113eb 143const long cbdata::Cookie((long)0xDEADBEEF);
b4bab919 144#if !HASHED_CBDATA
43ae1d95 145const long cbdata::Offset(MakeOffset());
146
f668fcda 147void *
148cbdata::operator new(size_t size, void *where)
149{
150 // assert (size == sizeof(cbdata));
151 return where;
152}
153
63be0a78 154/**
155 * Only ever invoked when placement new throws
156 * an exception. Used to prevent an incorrect
157 * free.
158 */
f668fcda 159void
160cbdata::operator delete(void *where, void *where2)
cfdb8f88
AJ
161{
162 ; // empty.
f668fcda 163}
164
43ae1d95 165long
166cbdata::MakeOffset()
167{
168 cbdata *zero = (cbdata *)0L;
169 void **dataOffset = &zero->data;
170 return (long)dataOffset;
171}
b4bab919 172#else
cfdb8f88 173MEMPROXY_CLASS_INLINE(cbdata);
b4bab919 174#endif
a2172245 175
db1cd23c 176static OBJH cbdataDump;
e48ed433 177#ifdef CBDATA_DEBUG
178static OBJH cbdataDumpHistory;
179#endif
a2172245 180
63be0a78 181/// \ingroup CBDATAInternal
62e76326 182struct CBDataIndex
183{
b4bab919 184 MemAllocator *pool;
72711e31 185 FREE *free_func;
62e76326 186}
62e76326 187*cbdata_index = NULL;
63be0a78 188
189/// \ingroup CBDATAInternal
28c60158 190int cbdata_types = 0;
191
b4bab919 192#if HASHED_CBDATA
193static hash_table *cbdata_htable = NULL;
194
195static int
196cbdata_cmp(const void *p1, const void *p2)
197{
198 return (char *) p1 - (char *) p2;
199}
200
201static unsigned int
202cbdata_hash(const void *p, unsigned int mod)
203{
204 return ((unsigned long) p >> 8) % mod;
205}
206#endif
207
208
f668fcda 209cbdata::~cbdata()
e48ed433 210{
211#if CBDATA_DEBUG
212 CBDataCall *aCall;
62e76326 213
91caca83 214 while ((aCall = calls.pop()))
62e76326 215 delete aCall;
216
e48ed433 217#endif
62e76326 218
e48ed433 219 FREE *free_func = cbdata_index[type].free_func;
62e76326 220
b4bab919 221#if HASHED_CBDATA
222 void *p = hash.key;
223#else
224 void *p = &data;
225#endif
226
e48ed433 227 if (free_func)
b4bab919 228 free_func(p);
e48ed433 229}
230
fa80a8ef 231static void
232cbdataInternalInitType(cbdata_type type, const char *name, int size, FREE * free_func)
a2172245 233{
28c60158 234 char *label;
aa839030 235 assert (type == cbdata_types + 1);
62e76326 236
aa839030 237 cbdata_index = (CBDataIndex *)xrealloc(cbdata_index, (type + 1) * sizeof(*cbdata_index));
238 memset(&cbdata_index[type], 0, sizeof(*cbdata_index));
239 cbdata_types = type;
62e76326 240
e6ccf245 241 label = (char *)xmalloc(strlen(name) + 20);
62e76326 242
28c60158 243 snprintf(label, strlen(name) + 20, "cbdata %s (%d)", name, (int) type);
62e76326 244
b4bab919 245#if !HASHED_CBDATA
43ae1d95 246 assert((size_t)cbdata::Offset == (sizeof(cbdata) - ((cbdata *)NULL)->dataSize()));
b4bab919 247 size += cbdata::Offset;
248#endif
62e76326 249
04eb0689 250 cbdata_index[type].pool = memPoolCreate(label, size);
62e76326 251
72711e31 252 cbdata_index[type].free_func = free_func;
b4bab919 253
254#if HASHED_CBDATA
255 if (!cbdata_htable)
256 cbdata_htable = hash_create(cbdata_cmp, 1 << 12, cbdata_hash);
257#endif
a2172245 258}
259
28c60158 260cbdata_type
fa80a8ef 261cbdataInternalAddType(cbdata_type type, const char *name, int size, FREE * free_func)
a2172245 262{
28c60158 263 if (type)
62e76326 264 return type;
265
aa839030 266 type = (cbdata_type)(cbdata_types + 1);
62e76326 267
fa80a8ef 268 cbdataInternalInitType(type, name, size, free_func);
62e76326 269
28c60158 270 return type;
a2172245 271}
272
62ee09ca 273void
274cbdataRegisterWithCacheManager(CacheManager & manager)
275{
276 manager.registerAction("cbdata",
277 "Callback Data Registry Contents",
278 cbdataDump, 0, 1);
279#if CBDATA_DEBUG
280
281 manager.registerAction("cbdatahistory",
282 "Detailed call history for all current cbdata contents",
283 cbdataDumpHistory, 0, 1);
284#endif
285}
286
28c60158 287void *
4e3f712d 288#if CBDATA_DEBUG
72711e31 289cbdataInternalAllocDbg(cbdata_type type, const char *file, int line)
4e3f712d 290#else
72711e31 291cbdataInternalAlloc(cbdata_type type)
4e3f712d 292#endif
a2172245 293{
b4bab919 294 cbdata *c;
295 void *p;
aa839030 296 assert(type > 0 && type <= cbdata_types);
297 /* placement new: the pool alloc gives us cbdata + user type memory space
298 * and we init it with cbdata at the start of it
299 */
b4bab919 300#if HASHED_CBDATA
301 c = new cbdata;
302 p = cbdata_index[type].pool->alloc();
303 c->hash.key = p;
304 hash_join(cbdata_htable, &c->hash);
305#else
306 c = new (cbdata_index[type].pool->alloc()) cbdata;
307 p = (void *)&c->data;
308#endif
f668fcda 309
b4bab919 310 c->type = type;
311 c->valid = 1;
312 c->locks = 0;
313 c->cookie = (long) c ^ cbdata::Cookie;
8b9a2d90 314 cbdataCount++;
fa80a8ef 315#if CBDATA_DEBUG
62e76326 316
b4bab919 317 c->file = file;
318 c->line = line;
319 c->calls = Stack<CBDataCall *> ();
320 c->addHistory("Alloc", file, line);
321 dlinkAdd(c, &c->link, &cbdataEntries);
bf8fe701 322 debugs(45, 3, "cbdataAlloc: " << p << " " << file << ":" << line);
fa80a8ef 323#endif
62e76326 324
b4bab919 325 return p;
b8a2718d 326}
327
c29316a4 328void *
fa80a8ef 329#if CBDATA_DEBUG
330cbdataInternalFreeDbg(void *p, const char *file, int line)
331#else
72711e31 332cbdataInternalFree(void *p)
fa80a8ef 333#endif
a2172245 334{
28c60158 335 cbdata *c;
b4bab919 336#if HASHED_CBDATA
337 c = (cbdata *) hash_lookup(cbdata_htable, p);
338#else
43ae1d95 339 c = (cbdata *) (((char *) p) - cbdata::Offset);
b4bab919 340#endif
fa80a8ef 341#if CBDATA_DEBUG
62e76326 342
bf8fe701 343 debugs(45, 3, "cbdataFree: " << p << " " << file << ":" << line);
fa80a8ef 344#else
62e76326 345
bf8fe701 346 debugs(45, 9, "cbdataFree: " << p);
fa80a8ef 347#endif
62e76326 348
06a5ae20 349 c->check(__LINE__);
a2fb868f 350 assert(c->valid);
3015e83a 351 c->valid = 0;
e48ed433 352#if CBDATA_DEBUG
62e76326 353
e48ed433 354 c->addHistory("Free", file, line);
355#endif
62e76326 356
3015e83a 357 if (c->locks) {
bf8fe701 358 debugs(45, 9, "cbdataFree: " << p << " has " << c->locks << " locks, not freeing");
62e76326 359 return NULL;
3015e83a 360 }
62e76326 361
28c60158 362 cbdataCount--;
bf8fe701 363 debugs(45, 9, "cbdataFree: Freeing " << p);
fa80a8ef 364#if CBDATA_DEBUG
62e76326 365
fa80a8ef 366 dlinkDelete(&c->link, &cbdataEntries);
367#endif
62e76326 368
f668fcda 369 /* This is ugly. But: operator delete doesn't get
370 * the type parameter, so we can't use that
371 * to free the memory.
372 * So, we free it ourselves.
373 * Note that this means a non-placement
374 * new would be a seriously bad idea.
375 * Lastly, if we where a templated class,
376 * we could use the normal delete operator
377 * and it would Just Work. RBC 20030902
378 */
b4bab919 379 cbdata_type theType = c->type;
380#if HASHED_CBDATA
381 hash_remove_link(cbdata_htable, &c->hash);
382 delete c;
383 cbdata_index[theType].pool->free((void *)p);
384#else
385 c->cbdata::~cbdata();
b001e822 386 cbdata_index[theType].pool->free(c);
b4bab919 387#endif
c29316a4 388 return NULL;
72711e31 389}
390
a2172245 391void
b7e6c377 392#if CBDATA_DEBUG
fa80a8ef 393cbdataInternalLockDbg(const void *p, const char *file, int line)
b7e6c377 394#else
fa80a8ef 395cbdataInternalLock(const void *p)
b7e6c377 396#endif
a2172245 397{
3015e83a 398 cbdata *c;
62e76326 399
3015e83a 400 if (p == NULL)
62e76326 401 return;
402
b4bab919 403#if HASHED_CBDATA
404 c = (cbdata *) hash_lookup(cbdata_htable, p);
405#else
43ae1d95 406 c = (cbdata *) (((char *) p) - cbdata::Offset);
b4bab919 407#endif
62e76326 408
b7e6c377 409#if CBDATA_DEBUG
62e76326 410
bf8fe701 411 debugs(45, 3, "cbdataLock: " << p << "=" << (c ? c->locks + 1 : -1) << " " << file << ":" << line);
62e76326 412
e48ed433 413 c->addHistory("Reference", file, line);
62e76326 414
fa80a8ef 415#else
62e76326 416
bf8fe701 417 debugs(45, 9, "cbdataLock: " << p << "=" << (c ? c->locks + 1 : -1));
62e76326 418
b7e6c377 419#endif
62e76326 420
06a5ae20 421 c->check(__LINE__);
62e76326 422
e5130cd3 423 assert(c->locks < 65535);
62e76326 424
fa80a8ef 425 c->locks++;
a2172245 426}
427
428void
b7e6c377 429#if CBDATA_DEBUG
fa80a8ef 430cbdataInternalUnlockDbg(const void *p, const char *file, int line)
b7e6c377 431#else
fa80a8ef 432cbdataInternalUnlock(const void *p)
b7e6c377 433#endif
a2172245 434{
3015e83a 435 cbdata *c;
62e76326 436
3015e83a 437 if (p == NULL)
62e76326 438 return;
439
b4bab919 440#if HASHED_CBDATA
441 c = (cbdata *) hash_lookup(cbdata_htable, p);
442#else
43ae1d95 443 c = (cbdata *) (((char *) p) - cbdata::Offset);
b4bab919 444#endif
62e76326 445
fa80a8ef 446#if CBDATA_DEBUG
62e76326 447
bf8fe701 448 debugs(45, 3, "cbdataUnlock: " << p << "=" << (c ? c->locks - 1 : -1) << " " << file << ":" << line);
62e76326 449
e48ed433 450 c->addHistory("Dereference", file, line);
62e76326 451
fa80a8ef 452#else
62e76326 453
bf8fe701 454 debugs(45, 9, "cbdataUnlock: " << p << "=" << (c ? c->locks - 1 : -1));
62e76326 455
fa80a8ef 456#endif
62e76326 457
06a5ae20 458 c->check(__LINE__);
62e76326 459
3015e83a 460 assert(c != NULL);
62e76326 461
3015e83a 462 assert(c->locks > 0);
62e76326 463
3015e83a 464 c->locks--;
62e76326 465
aab9676e 466 if (c->valid || c->locks)
62e76326 467 return;
468
28c60158 469 cbdataCount--;
62e76326 470
bf8fe701 471 debugs(45, 9, "cbdataUnlock: Freeing " << p);
62e76326 472
fa80a8ef 473#if CBDATA_DEBUG
62e76326 474
fa80a8ef 475 dlinkDelete(&c->link, &cbdataEntries);
62e76326 476
fa80a8ef 477#endif
62e76326 478
f668fcda 479 /* This is ugly. But: operator delete doesn't get
480 * the type parameter, so we can't use that
481 * to free the memory.
482 * So, we free it ourselves.
483 * Note that this means a non-placement
484 * new would be a seriously bad idea.
485 * Lastly, if we where a templated class,
486 * we could use the normal delete operator
487 * and it would Just Work. RBC 20030902
488 */
b4bab919 489 cbdata_type theType = c->type;
490#if HASHED_CBDATA
491 hash_remove_link(cbdata_htable, &c->hash);
492 delete c;
493 cbdata_index[theType].pool->free((void *)p);
494#else
495 c->cbdata::~cbdata();
b001e822 496 cbdata_index[theType].pool->free(c);
b4bab919 497#endif
a2172245 498}
499
500int
fa80a8ef 501cbdataReferenceValid(const void *p)
a2172245 502{
3015e83a 503 cbdata *c;
62e76326 504
3015e83a 505 if (p == NULL)
62e76326 506 return 1; /* A NULL pointer cannot become invalid */
507
bf8fe701 508 debugs(45, 9, "cbdataReferenceValid: " << p);
62e76326 509
b4bab919 510#if HASHED_CBDATA
511 c = (cbdata *) hash_lookup(cbdata_htable, p);
512#else
43ae1d95 513 c = (cbdata *) (((char *) p) - cbdata::Offset);
b4bab919 514#endif
62e76326 515
06a5ae20 516 c->check(__LINE__);
62e76326 517
3015e83a 518 assert(c->locks > 0);
62e76326 519
3015e83a 520 return c->valid;
a2172245 521}
522
fa80a8ef 523int
524#if CBDATA_DEBUG
525cbdataInternalReferenceDoneValidDbg(void **pp, void **tp, const char *file, int line)
526#else
527cbdataInternalReferenceDoneValid(void **pp, void **tp)
528#endif
529{
530 void *p = (void *) *pp;
531 int valid = cbdataReferenceValid(p);
532 *pp = NULL;
533#if CBDATA_DEBUG
62e76326 534
fa80a8ef 535 cbdataInternalUnlockDbg(p, file, line);
536#else
62e76326 537
fa80a8ef 538 cbdataInternalUnlock(p);
539#endif
62e76326 540
fa80a8ef 541 if (valid) {
62e76326 542 *tp = p;
543 return 1;
fa80a8ef 544 } else {
62e76326 545 *tp = NULL;
546 return 0;
fa80a8ef 547 }
548}
549
e48ed433 550#if CBDATA_DEBUG
551void
43ae1d95 552cbdata::dump(StoreEntry *sentry) const
e48ed433 553{
b4bab919 554#if HASHED_CBDATA
c692f6b0 555 void *p = (void *)hash.key;
b4bab919 556#else
c692f6b0 557 void *p = (void *)&data;
b4bab919 558#endif
e48ed433 559 storeAppendPrintf(sentry, "%c%p\t%d\t%d\t%20s:%-5d\n", valid ? ' ' :
b4bab919 560 '!', p, type, locks, file, line);
e48ed433 561}
562
43ae1d95 563struct CBDataDumper : public unary_function<cbdata, void>
e48ed433 564{
565 CBDataDumper(StoreEntry *anEntry):where(anEntry){}
62e76326 566
43ae1d95 567 void operator()(cbdata const &x)
62e76326 568 {
569 x.dump(where);
e48ed433 570 }
62e76326 571
e48ed433 572 StoreEntry *where;
573};
62e76326 574
e48ed433 575#endif
fa80a8ef 576
db1cd23c 577static void
3015e83a 578cbdataDump(StoreEntry * sentry)
a2172245 579{
8b9a2d90 580 storeAppendPrintf(sentry, "%d cbdata entries\n", cbdataCount);
fa80a8ef 581#if CBDATA_DEBUG
62e76326 582
fa80a8ef 583 storeAppendPrintf(sentry, "Pointer\tType\tLocks\tAllocated by\n");
e48ed433 584 CBDataDumper dumper(sentry);
585 for_each (cbdataEntries, dumper);
fa80a8ef 586 storeAppendPrintf(sentry, "\n");
587 storeAppendPrintf(sentry, "types\tsize\tallocated\ttotal\n");
62e76326 588
e48ed433 589 for (int i = 1; i < cbdata_types; i++) {
b4bab919 590 MemAllocator *pool = cbdata_index[i].pool;
62e76326 591
592 if (pool) {
b4bab919 593#if HASHED_CBDATA
594 int obj_size = pool->objectSize();
595#else
b001e822 596 int obj_size = pool->objectSize() - cbdata::Offset;
b4bab919 597#endif
06a0702b 598 storeAppendPrintf(sentry, "%s\t%d\t%ld\t%ld\n", pool->objectType() + 7, obj_size, (long int)pool->getMeter().inuse.level, (long int)obj_size * pool->getMeter().inuse.level);
62e76326 599 }
fa80a8ef 600 }
62e76326 601
fa80a8ef 602#else
603 storeAppendPrintf(sentry, "detailed allocation information only available when compiled with CBDATA_DEBUG\n");
62e76326 604
fa80a8ef 605#endif
62e76326 606
fa80a8ef 607 storeAppendPrintf(sentry, "\nsee also \"Memory utilization\" for detailed per type statistics\n");
a2172245 608}
e48ed433 609
aa839030 610CBDATA_CLASS_INIT(generic_cbdata);
611
e48ed433 612#if CBDATA_DEBUG
613
e48ed433 614struct CBDataCallDumper : public unary_function<CBDataCall, void>
615{
616 CBDataCallDumper (StoreEntry *anEntry):where(anEntry){}
62e76326 617
618 void operator()(CBDataCall const &x)
619 {
620 storeAppendPrintf(where, "%s\t%s\t%d\n", x.label, x.file, x.line);
e48ed433 621 }
62e76326 622
e48ed433 623 StoreEntry *where;
624};
625
626struct CBDataHistoryDumper : public CBDataDumper
627{
628 CBDataHistoryDumper(StoreEntry *anEntry):CBDataDumper(anEntry),where(anEntry), callDumper(anEntry){}
62e76326 629
43ae1d95 630 void operator()(cbdata const &x)
62e76326 631 {
632 CBDataDumper::operator()(x);
633 storeAppendPrintf(where, "\n");
634 storeAppendPrintf(where, "Action\tFile\tLine\n");
91caca83 635 for_each (x.calls,callDumper);
62e76326 636 storeAppendPrintf(where, "\n");
e48ed433 637 }
62e76326 638
e48ed433 639 StoreEntry *where;
640 CBDataCallDumper callDumper;
641};
642
643void
644cbdataDumpHistory(StoreEntry *sentry)
645{
646 storeAppendPrintf(sentry, "%d cbdata entries\n", cbdataCount);
647 storeAppendPrintf(sentry, "Pointer\tType\tLocks\tAllocated by\n");
648 CBDataHistoryDumper dumper(sentry);
649 for_each (cbdataEntries, dumper);
650}
62e76326 651
e48ed433 652#endif