Changeset 706:4ffbc9f1e922 in freeDiameter for libfdproto/sessions.c
- Timestamp:
- Feb 9, 2011, 3:26:58 PM (13 years ago)
- Branch:
- default
- Phase:
- public
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
libfdproto/sessions.c
r691 r706 69 69 int eyec; /* An eye catcher also used to ensure the object is valid, must be SH_EYEC */ 70 70 int id; /* A unique integer to identify this handler */ 71 void (*cleanup)(session_state *, char *, void *); /* The cleanup function to be called for cleaning a state */71 void (*cleanup)(session_state *, os0_t, void *); /* The cleanup function to be called for cleaning a state */ 72 72 void *opaque; /* a value that is passed as is to the cleanup callback */ 73 73 }; … … 84 84 union { 85 85 struct session_handler *hdl; /* The handler for which this state was registered */ 86 char *sid; /* For deleted state, the sid of the session it belong to */86 os0_t sid; /* For deleted state, the sid of the session it belong to */ 87 87 }; 88 88 }; … … 92 92 int eyec; /* Eyecatcher, SI_EYEC */ 93 93 94 char * sid; /* The \0-terminated Session-Id */ 94 os0_t sid; /* The \0-terminated Session-Id */ 95 size_t sidlen; /* cached length of sid */ 95 96 uint32_t hash; /* computed hash of sid */ 96 97 struct fd_list chain_h;/* chaining in the hash table of sessions. */ … … 102 103 struct fd_list states; /* Sentinel for the list of states of this session. */ 103 104 int msg_cnt;/* Reference counter for the messages pointing to this session */ 105 int is_destroyed; /* boolean telling if fd_sess_detroy has been called on this */ 104 106 }; 105 107 106 108 /* Sessions hash table, to allow fast sid to session retrieval */ 107 109 static struct { 108 struct fd_list sentinel; /* sentinel element for this sublist */110 struct fd_list sentinel; /* sentinel element for this sublist. The sublist is ordered by hash value, then fd_os_cmp(sid). */ 109 111 pthread_mutex_t lock; /* the mutex for this sublist -- we might probably change it to rwlock for a little optimization */ 110 112 } sess_hash [ 1 << SESS_HASH_SIZE ] ; … … 132 134 /********************************************************************************************************/ 133 135 134 /* Initialize a session object. It is not linked now. sid must be already malloc'ed. */135 static struct session * new_session( char * sid, size_t sidlen)136 /* Initialize a session object. It is not linked now. sid must be already malloc'ed. The hash has already been computed. */ 137 static struct session * new_session(os0_t sid, size_t sidlen, uint32_t hash) 136 138 { 137 139 struct session * sess; 138 140 139 TRACE_ENTRY("%p % d", sid, sidlen);141 TRACE_ENTRY("%p %zd", sid, sidlen); 140 142 CHECK_PARAMS_DO( sid && sidlen, return NULL ); 141 143 … … 146 148 147 149 sess->sid = sid; 148 sess->hash = fd_hash(sid, sidlen); 150 sess->sidlen = sidlen; 151 sess->hash = hash; 149 152 fd_list_init(&sess->chain_h, sess); 150 153 … … 157 160 158 161 return sess; 162 } 163 164 /* destroy the session object. It should really be already unlinked... */ 165 static void del_session(struct session * s) 166 { 167 ASSERT(FD_IS_LIST_EMPTY(&s->states)); 168 free(s->sid); 169 fd_list_unlink(&s->chain_h); 170 fd_list_unlink(&s->expire); 171 CHECK_POSIX_DO( pthread_mutex_destroy(&s->stlock), /* continue */ ); 172 free(s); 159 173 } 160 174 … … 250 264 TRACE_ENTRY(""); 251 265 CHECK_FCT_DO( fd_thr_term(&exp_thr), /* continue */ ); 266 267 /* Destroy all sessions in the hash table, and the hash table itself? -- How to do it without a race condition ? */ 268 252 269 return; 253 270 } 254 271 255 272 /* Create a new handler */ 256 int fd_sess_handler_create_internal ( struct session_handler ** handler, void (*cleanup)(session_state * state, char * sid, void * opaque), void * opaque )273 int fd_sess_handler_create_internal ( struct session_handler ** handler, void (*cleanup)(session_state *, os0_t, void *), void * opaque ) 257 274 { 258 275 struct session_handler *new; … … 299 316 CHECK_POSIX( pthread_mutex_lock(&sess_hash[i].lock) ); 300 317 301 for (li_si = sess_hash[i].sentinel.next; li_si != &sess_hash[i].sentinel; li_si = li_si->next) { 318 for (li_si = sess_hash[i].sentinel.next; li_si != &sess_hash[i].sentinel; li_si = li_si->next) { /* for each session in the hash line */ 302 319 struct fd_list * li_st; 303 320 struct session * sess = (struct session *)(li_si->o); 304 321 CHECK_POSIX( pthread_mutex_lock(&sess->stlock) ); 305 for (li_st = sess->states.next; li_st != &sess->states; li_st = li_st->next) { 322 for (li_st = sess->states.next; li_st != &sess->states; li_st = li_st->next) { /* for each state in this session */ 306 323 struct state * st = (struct state *)(li_st->o); 307 324 /* The list is ordered */ … … 311 328 /* This state belongs to the handler we are deleting, move the item to the deleted_states list */ 312 329 fd_list_unlink(&st->chain); 313 CHECK_MALLOC( st->sid = strdup(sess->sid) );330 st->sid = sess->sid; 314 331 fd_list_insert_before(&deleted_states, &st->chain); 315 332 } … … 326 343 TRACE_DEBUG(FULL, "Calling cleanup handler for session '%s' and data %p", st->sid, st->state); 327 344 (*del->cleanup)(st->state, st->sid, del->opaque); 328 free(st->sid);329 345 fd_list_unlink(&st->chain); 330 346 free(st); … … 343 359 344 360 /* Create a new session object with the default timeout value, and link it */ 345 int fd_sess_new ( struct session ** session, char * diamId, char* opt, size_t optlen )346 { 347 char *sid = NULL;361 int fd_sess_new ( struct session ** session, DiamId_t diamid, size_t diamidlen, uint8_t * opt, size_t optlen ) 362 { 363 os0_t sid = NULL; 348 364 size_t sidlen; 365 uint32_t hash; 349 366 struct session * sess; 350 367 struct fd_list * li; 351 368 int found = 0; 352 353 TRACE_ENTRY("%p %p %p %d", session, diamId, opt, optlen); 354 CHECK_PARAMS( session && (diamId || opt) ); 355 369 int ret = 0; 370 371 TRACE_ENTRY("%p %p %zd %p %zd", session, diamid, diamidlen, opt, optlen); 372 CHECK_PARAMS( session && (diamid || opt) ); 373 374 if (diamid) { 375 if (!diamidlen) { 376 diamidlen = strlen(diamid); 377 } 378 /* We check if the string is a valid DiameterIdentity */ 379 CHECK_PARAMS( fd_os_is_valid_DiameterIdentity((uint8_t *)diamid, diamidlen) ); 380 } else { 381 diamidlen = 0; 382 } 383 if (opt) { 384 if (!optlen) { 385 optlen = strlen((char *)opt); 386 } else { 387 CHECK_PARAMS( fd_os_is_valid_os0(opt, optlen) ); 388 } 389 } else { 390 optlen = 0; 391 } 392 356 393 /* Ok, first create the identifier for the string */ 357 if (diam Id == NULL) {394 if (diamid == NULL) { 358 395 /* opt is the full string */ 359 if (optlen) { 360 CHECK_MALLOC( sid = malloc(optlen + 1) ); 361 strncpy(sid, opt, optlen); 362 sid[optlen] = '\0'; 363 sidlen = optlen; 364 } else { 365 CHECK_MALLOC( sid = strdup(opt) ); 366 sidlen = strlen(sid); 367 } 396 CHECK_MALLOC( sid = os0dup(opt, optlen) ); 397 sidlen = optlen; 368 398 } else { 369 399 uint32_t sid_h_cpy; 370 400 uint32_t sid_l_cpy; 371 401 /* "<diamId>;<high32>;<low32>[;opt]" */ 372 sidlen = strlen(diamId);402 sidlen = diamidlen; 373 403 sidlen += 22; /* max size of ';<high32>;<low32>' */ 374 404 if (opt) 375 sidlen += 1 + (optlen ?: strlen(opt)) ;405 sidlen += 1 + optlen; /* ';opt' */ 376 406 sidlen++; /* space for the final \0 also */ 377 407 CHECK_MALLOC( sid = malloc(sidlen) ); … … 385 415 386 416 if (opt) { 387 if (optlen) 388 sidlen = snprintf(sid, sidlen, "%s;%u;%u;%.*s", diamId, sid_h_cpy, sid_l_cpy, (int)optlen, opt); 389 else 390 sidlen = snprintf(sid, sidlen, "%s;%u;%u;%s", diamId, sid_h_cpy, sid_l_cpy, opt); 417 sidlen = snprintf((char*)sid, sidlen, "%.*s;%u;%u;%.*s", (int)diamidlen, diamid, sid_h_cpy, sid_l_cpy, (int)optlen, opt); 391 418 } else { 392 sidlen = snprintf( sid, sidlen, "%s;%u;%u", diamId, sid_h_cpy, sid_l_cpy);419 sidlen = snprintf((char*)sid, sidlen, "%.*s;%u;%u", (int)diamidlen, diamid, sid_h_cpy, sid_l_cpy); 393 420 } 394 421 } 395 422 396 /* Initialize the session object now, to spend less time inside locked section later. 397 * Cons: we malloc then free if there is already a session with same SID; we could malloc later to avoid this. */ 398 CHECK_MALLOC( sess = new_session(sid, sidlen) ); 423 hash = fd_os_hash(sid, sidlen); 399 424 400 425 /* Now find the place to add this object in the hash table. */ 401 CHECK_POSIX( pthread_mutex_lock( H_LOCK( sess->hash) ) );402 pthread_cleanup_push( fd_cleanup_mutex, H_LOCK( sess->hash) );403 404 for (li = H_LIST( sess->hash)->next; li != H_LIST(sess->hash); li = li->next) {426 CHECK_POSIX( pthread_mutex_lock( H_LOCK(hash) ) ); 427 pthread_cleanup_push( fd_cleanup_mutex, H_LOCK(hash) ); 428 429 for (li = H_LIST(hash)->next; li != H_LIST(hash); li = li->next) { 405 430 int cmp; 406 431 struct session * s = (struct session *)(li->o); 407 432 408 433 /* The list is ordered by hash and sid (in case of collisions) */ 409 if (s->hash < sess->hash)434 if (s->hash < hash) 410 435 continue; 411 if (s->hash > sess->hash)436 if (s->hash > hash) 412 437 break; 413 438 414 cmp = strcasecmp(s->sid, sess->sid);439 cmp = fd_os_cmp(s->sid, s->sidlen, sid, sidlen); 415 440 if (cmp < 0) 416 441 continue; … … 424 449 } 425 450 426 /* If the session did not exist, we can link it in global tables */451 /* If the session did not exist, we can create it & link it in global tables */ 427 452 if (!found) { 453 CHECK_MALLOC_DO(sess = new_session(sid, sidlen, hash), 454 { 455 ret = ENOMEM; 456 goto out; 457 } ); 458 428 459 fd_list_insert_before(li, &sess->chain_h); /* hash table */ 429 430 /* We must also insert in the expiry list */ 431 CHECK_POSIX( pthread_mutex_lock( &exp_lock ) ); 432 pthread_cleanup_push( fd_cleanup_mutex, &exp_lock ); 433 434 /* Find the position in that list. We take it in reverse order */ 435 for (li = exp_sentinel.prev; li != &exp_sentinel; li = li->prev) { 436 struct session * s = (struct session *)(li->o); 437 if (TS_IS_INFERIOR( &s->timeout, &sess->timeout ) ) 438 break; 460 } else { 461 /* it was found: was it previously destroyed? */ 462 if ((*session)->is_destroyed == 0) { 463 ret = EALREADY; 464 goto out; 465 } else { 466 /* the session was marked destroyed, let's re-activate it. */ 467 TODO("Re-creating a deleted session. Should investigate if this can lead to an issue... (need more feedback)"); 468 sess = *session; 469 470 /* update the expiry time */ 471 CHECK_SYS_DO( clock_gettime(CLOCK_REALTIME, &sess->timeout), { ASSERT(0); } ); 472 sess->timeout.tv_sec += SESS_DEFAULT_LIFETIME; 439 473 } 440 fd_list_insert_after( li, &sess->expire ); 441 442 /* We added a new expiring element, we must signal */ 443 if (li == &exp_sentinel) { 444 CHECK_POSIX_DO( pthread_cond_signal(&exp_cond), { ASSERT(0); } ); /* if it fails, we might not pop the cleanup handlers, but this should not happen -- and we'd have a serious problem otherwise */ 445 } 446 447 #if 0 448 if (TRACE_BOOL(ANNOYING)) { 449 TRACE_DEBUG(FULL, "-- Updated session expiry list --"); 450 for (li = exp_sentinel.next; li != &exp_sentinel; li = li->next) { 451 struct session * s = (struct session *)(li->o); 452 fd_sess_dump(FULL, s); 453 } 454 TRACE_DEBUG(FULL, "-- end of expiry list --"); 455 } 456 #endif 457 458 /* We're done */ 459 pthread_cleanup_pop(0); 460 CHECK_POSIX_DO( pthread_mutex_unlock( &exp_lock ), { ASSERT(0); } ); /* if it fails, we might not pop the cleanup handler, but this should not happen -- and we'd have a serious problem otherwise */ 461 } 462 474 } 475 476 /* We must insert in the expiry list */ 477 CHECK_POSIX( pthread_mutex_lock( &exp_lock ) ); 478 pthread_cleanup_push( fd_cleanup_mutex, &exp_lock ); 479 480 /* Find the position in that list. We take it in reverse order */ 481 for (li = exp_sentinel.prev; li != &exp_sentinel; li = li->prev) { 482 struct session * s = (struct session *)(li->o); 483 if (TS_IS_INFERIOR( &s->timeout, &sess->timeout ) ) 484 break; 485 } 486 fd_list_insert_after( li, &sess->expire ); 487 488 /* We added a new expiring element, we must signal */ 489 if (li == &exp_sentinel) { 490 CHECK_POSIX_DO( pthread_cond_signal(&exp_cond), { ASSERT(0); } ); /* if it fails, we might not pop the cleanup handlers, but this should not happen -- and we'd have a serious problem otherwise */ 491 } 492 493 /* We're done with the locked part */ 463 494 pthread_cleanup_pop(0); 464 CHECK_POSIX( pthread_mutex_unlock( H_LOCK(sess->hash) ) ); 465 466 /* If a session already existed, we must destroy the new element */ 467 if (found) { 468 CHECK_FCT( fd_sess_destroy( &sess ) ); /* we could avoid locking this time for optimization */ 469 return EALREADY; 470 } 495 CHECK_POSIX_DO( pthread_mutex_unlock( &exp_lock ), { ASSERT(0); } ); /* if it fails, we might not pop the cleanup handler, but this should not happen -- and we'd have a serious problem otherwise */ 496 497 out: 498 ; 499 pthread_cleanup_pop(0); 500 CHECK_POSIX( pthread_mutex_unlock( H_LOCK(hash) ) ); 501 502 if (ret) /* in case of error */ 503 return ret; 471 504 472 505 *session = sess; … … 475 508 476 509 /* Find or create a session */ 477 int fd_sess_fromsid ( char* sid, size_t len, struct session ** session, int * new)510 int fd_sess_fromsid ( uint8_t * sid, size_t len, struct session ** session, int * new) 478 511 { 479 512 int ret; … … 482 515 CHECK_PARAMS( sid && session ); 483 516 517 if (!fd_os_is_valid_os0(sid,len)) { 518 TRACE_DEBUG(INFO, "Warning: a Session-Id value contains \\0 chars... (len:%zd, begin:'%.*s')\n => Debug messages may be truncated.", len, len, sid); 519 } 520 484 521 /* All the work is done in sess_new */ 485 ret = fd_sess_new ( session, NULL, sid, len );522 ret = fd_sess_new ( session, NULL, 0, sid, len ); 486 523 switch (ret) { 487 524 case 0: … … 500 537 501 538 /* Get the sid of a session */ 502 int fd_sess_getsid ( struct session * session, char ** sid)539 int fd_sess_getsid ( struct session * session, os0_t * sid, size_t * sidlen ) 503 540 { 504 541 TRACE_ENTRY("%p %p", session, sid); … … 507 544 508 545 *sid = session->sid; 546 if (sidlen) 547 *sidlen = session->sidlen; 509 548 510 549 return 0; … … 543 582 } 544 583 545 #if 0546 if (TRACE_BOOL(ANNOYING)) {547 TRACE_DEBUG(FULL, "-- Updated session expiry list --");548 for (li = exp_sentinel.next; li != &exp_sentinel; li = li->next) {549 struct session * s = (struct session *)(li->o);550 fd_sess_dump(FULL, s);551 }552 TRACE_DEBUG(FULL, "-- end of expiry list --");553 }554 #endif555 556 584 /* We're done */ 557 585 pthread_cleanup_pop(0); … … 561 589 } 562 590 563 /* Destroy a session immediatly*/591 /* Destroy the states associated to a session, and mark it destroyed. */ 564 592 int fd_sess_destroy ( struct session ** session ) 565 593 { 566 594 struct session * sess; 595 int destroy_now; 596 os0_t sid; 597 int ret = 0; 598 599 /* place to save the list of states to be cleaned up. We do it after finding them to avoid deadlocks. the "o" field becomes a copy of the sid. */ 600 struct fd_list deleted_states = FD_LIST_INITIALIZER( deleted_states ); 567 601 568 602 TRACE_ENTRY("%p", session); … … 572 606 *session = NULL; 573 607 574 /* Unlink and invalidate */608 /* Lock the hash line */ 575 609 CHECK_POSIX( pthread_mutex_lock( H_LOCK(sess->hash) ) ); 576 610 pthread_cleanup_push( fd_cleanup_mutex, H_LOCK(sess->hash) ); 611 612 /* Unlink from the expiry list */ 577 613 CHECK_POSIX_DO( pthread_mutex_lock( &exp_lock ), { ASSERT(0); /* otherwise cleanup handler is not pop'd */ } ); 578 fd_list_unlink( &sess->chain_h );579 614 fd_list_unlink( &sess->expire ); /* no need to signal the condition here */ 580 sess->eyec = 0xdead;581 615 CHECK_POSIX_DO( pthread_mutex_unlock( &exp_lock ), { ASSERT(0); /* otherwise cleanup handler is not pop'd */ } ); 582 pthread_cleanup_pop(0); 583 CHECK_POSIX( pthread_mutex_unlock( H_LOCK(sess->hash) ) ); 584 585 /* Now destroy all states associated -- we don't take the lock since nobody can access this session anymore (in theory) */ 616 617 /* Now move all states associated to this session into deleted_states */ 618 CHECK_POSIX_DO( pthread_mutex_lock( &sess->stlock ), { ASSERT(0); /* otherwise cleanup handler is not pop'd */ } ); 586 619 while (!FD_IS_LIST_EMPTY(&sess->states)) { 587 620 struct state * st = (struct state *)(sess->states.next->o); 588 621 fd_list_unlink(&st->chain); 589 TRACE_DEBUG(FULL, "Calling handler %p cleanup for state registered with session '%s'", st->hdl, sess->sid); 590 (*st->hdl->cleanup)(st->state, sess->sid, st->hdl->opaque); 622 fd_list_insert_before(&deleted_states, &st->chain); 623 } 624 CHECK_POSIX_DO( pthread_mutex_unlock( &sess->stlock ), { ASSERT(0); /* otherwise cleanup handler is not pop'd */ } ); 625 626 /* Mark the session as destroyed */ 627 destroy_now = (sess->msg_cnt == 0); 628 if (destroy_now) { 629 fd_list_unlink( &sess->chain_h ); 630 sid = sess->sid; 631 } else { 632 sess->is_destroyed = 1; 633 CHECK_MALLOC_DO( sid = os0dup(sess->sid, sess->sidlen), ret = ENOMEM ); 634 } 635 pthread_cleanup_pop(0); 636 CHECK_POSIX( pthread_mutex_unlock( H_LOCK(sess->hash) ) ); 637 638 if (ret) 639 return ret; 640 641 /* Now, really delete the states */ 642 while (!FD_IS_LIST_EMPTY(&deleted_states)) { 643 struct state * st = (struct state *)(deleted_states.next->o); 644 fd_list_unlink(&st->chain); 645 TRACE_DEBUG(FULL, "Calling handler %p cleanup for state %p registered with session '%s'", st->hdl, st, sid); 646 (*st->hdl->cleanup)(st->state, sid, st->hdl->opaque); 591 647 free(st); 592 648 } 593 649 594 /* Finally, destroy the session itself */ 595 free(sess->sid); 596 free(sess); 650 /* Finally, destroy the session itself, if it is not referrenced by any message anymore */ 651 if (destroy_now) { 652 del_session(sess); 653 } else { 654 free(sid); 655 } 597 656 598 657 return 0; … … 604 663 struct session * sess; 605 664 uint32_t hash; 665 int destroy_now = 0; 606 666 607 667 TRACE_ENTRY("%p", session); … … 612 672 *session = NULL; 613 673 614 CHECK_POSIX( pthread_mutex_lock( H_LOCK(sess->hash) ) ); 615 pthread_cleanup_push( fd_cleanup_mutex, H_LOCK(sess->hash) ); 674 CHECK_POSIX( pthread_mutex_lock( H_LOCK(hash) ) ); 675 pthread_cleanup_push( fd_cleanup_mutex, H_LOCK(hash) ); 676 CHECK_POSIX_DO( pthread_mutex_lock( &sess->stlock ), { ASSERT(0); /* otherwise, cleanup not poped on FreeBSD */ } ); 677 pthread_cleanup_push( fd_cleanup_mutex, &sess->stlock ); 616 678 CHECK_POSIX_DO( pthread_mutex_lock( &exp_lock ), { ASSERT(0); /* otherwise, cleanup not poped on FreeBSD */ } ); 679 680 /* We only do something if the states list is empty */ 617 681 if (FD_IS_LIST_EMPTY(&sess->states)) { 618 fd_list_unlink( &sess->chain_h );682 /* In this case, we do as in destroy */ 619 683 fd_list_unlink( &sess->expire ); 620 sess->eyec = 0xdead; 621 free(sess->sid); 622 free(sess); 623 } 684 destroy_now = (sess->msg_cnt == 0); 685 if (destroy_now) { 686 fd_list_unlink(&sess->chain_h); 687 } else { 688 /* just mark it as destroyed, it will be freed when the last message stops referencing it */ 689 sess->is_destroyed = 1; 690 } 691 } 692 624 693 CHECK_POSIX_DO( pthread_mutex_unlock( &exp_lock ), { ASSERT(0); /* otherwise, cleanup not poped on FreeBSD */ } ); 625 694 pthread_cleanup_pop(0); 695 CHECK_POSIX_DO( pthread_mutex_unlock( &sess->stlock ), { ASSERT(0); /* otherwise, cleanup not poped on FreeBSD */ } ); 696 pthread_cleanup_pop(0); 626 697 CHECK_POSIX( pthread_mutex_unlock( H_LOCK(hash) ) ); 698 699 if (destroy_now) 700 del_session(sess); 627 701 628 702 return 0; … … 638 712 639 713 TRACE_ENTRY("%p %p %p", handler, session, state); 640 CHECK_PARAMS( handler && VALIDATE_SH(handler) && session && VALIDATE_SI(session) && state );714 CHECK_PARAMS( handler && VALIDATE_SH(handler) && session && VALIDATE_SI(session) && (!session->is_destroyed) && state ); 641 715 642 716 /* Lock the session state list */ … … 720 794 721 795 /* For the messages module */ 722 int fd_sess_fromsid_msg ( u nsigned char* sid, size_t len, struct session ** session, int * new)796 int fd_sess_fromsid_msg ( uint8_t * sid, size_t len, struct session ** session, int * new) 723 797 { 724 798 TRACE_ENTRY("%p %zd %p %p", sid, len, session, new); … … 726 800 727 801 /* Get the session object */ 728 CHECK_FCT( fd_sess_fromsid ( (char *)sid, len, session, new) );802 CHECK_FCT( fd_sess_fromsid ( sid, len, session, new) ); 729 803 730 804 /* Increase count */ … … 786 860 } else { 787 861 788 fd_log_debug("\t %*s sid '%s' , hash %x\n", level, "", session->sid, session->hash);862 fd_log_debug("\t %*s sid '%s'(%zd), hash %x\n", level, "", session->sid, session->sidlen, session->hash); 789 863 790 864 strftime(buf, sizeof(buf), "%D,%T", localtime_r( &session->timeout.tv_sec , &tm ));
Note: See TracChangeset
for help on using the changeset viewer.