/* -*-mode: C; fill-column: 78; c-basic-offset: 4; -*- */ /* * Copyright 2005-2009 by Eric House (xwords@eehouse.org). All rights * reserved. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include <assert.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <pthread.h> #include "crefmgr.h" #include "cref.h" #include "mlock.h" #include "configs.h" #include "timermgr.h" class SocketStuff { public: SocketStuff( CookieRef* cref ) : m_cref(cref) { pthread_mutex_init( &m_writeMutex, NULL ); } ~SocketStuff() { pthread_mutex_destroy( &m_writeMutex ); } CookieRef* m_cref; pthread_mutex_t m_writeMutex; /* so only one thread writes at a time */ }; static CRefMgr* s_instance = NULL; /* static */ CRefMgr* CRefMgr::Get() { if ( s_instance == NULL ) { s_instance = new CRefMgr(); } return s_instance; } /* Get */ CRefMgr::CRefMgr() : m_nRoomsFilled(0) , m_startTime(time(NULL)) { /* should be using pthread_once() here */ /* pthread_mutex_init( &m_SocketStuffMutex, NULL ); */ pthread_mutex_init( &m_roomsFilledMutex, NULL ); pthread_mutex_init( &m_freeList_mutex, NULL ); pthread_rwlock_init( &m_cookieMapRWLock, NULL ); m_db = DBMgr::Get(); m_cidlock = CidLock::GetInstance(); } CRefMgr::~CRefMgr() { assert( this == s_instance ); delete m_db; delete m_cidlock; pthread_mutex_destroy( &m_freeList_mutex ); pthread_rwlock_destroy( &m_cookieMapRWLock ); s_instance = NULL; } void CRefMgr::CloseAll() { /* Get every cref instance, shut it down */ for ( ; ; ) { CookieRef* cref = NULL; { RWWriteLock rwl( &m_cookieMapRWLock ); CookieMap::iterator iter = m_cookieMap.begin(); if ( iter == m_cookieMap.end() ) { break; } cref = iter->second; { SafeCref scr( cref->GetCid(), false ); /* cref */ scr.Shutdown(); } } } } /* CloseAll */ void CRefMgr::IncrementFullCount( void ) { MutexLock ml( &m_roomsFilledMutex ); ++m_nRoomsFilled; } int CRefMgr::GetNumRoomsFilled( void ) { MutexLock ml(&m_roomsFilledMutex); return m_nRoomsFilled; } int CRefMgr::GetSize( void ) { return m_cookieMap.size(); } void CRefMgr::GetStats( CrefMgrInfo& mgrInfo ) { mgrInfo.m_nRoomsFilled = GetNumRoomsFilled(); mgrInfo.m_startTimeSpawn = m_startTime; if ( 0 == m_ports.length() ) { RelayConfigs* cfg = RelayConfigs::GetConfigs(); vector<int> ints; if ( cfg->GetValueFor( "GAME_PORTS", ints ) ) { vector<int>::const_iterator iter; for ( iter = ints.begin(); ; ) { char buf[8]; snprintf( buf, sizeof(buf), "%d", *iter ); m_ports += buf; ++iter; if ( iter == ints.end() ) { break; } m_ports += ","; } } } mgrInfo.m_ports = m_ports.c_str(); RWReadLock rwl( &m_cookieMapRWLock ); mgrInfo.m_nCrefsCurrent = m_cookieMap.size(); CookieMap::iterator iter; for ( iter = m_cookieMap.begin(); iter != m_cookieMap.end(); ++iter ) { CookieRef* cref = iter->second; CrefInfo info; info.m_cookie = cref->Cookie(); info.m_connName = cref->ConnName(); info.m_cid = cref->GetCid(); info.m_curState = cref->CurState(); info.m_nPlayersSought = cref->GetPlayersSought(); info.m_nPlayersHere = cref->GetPlayersHere(); info.m_startTime = cref->GetStarttime(); info.m_langCode = cref->GetLangCode(); SafeCref sc(cref->GetCid(), false ); sc.GetHostsConnected( &info.m_hostsIds, &info.m_hostSeeds, &info.m_hostIps ); mgrInfo.m_crefInfo.push_back( info ); } } CookieID CRefMgr::cookieIDForConnName( const char* connName ) { CookieID cid = 0; /* for now, just walk the existing data structure and see if the thing's in use. If it isn't, return a new id. */ RWReadLock rwl( &m_cookieMapRWLock ); CookieMap::iterator iter = m_cookieMap.begin(); while ( iter != m_cookieMap.end() ) { CookieRef* cref = iter->second; if ( 0 == strcmp( cref->ConnName(), connName ) ) { cid = iter->first; break; } ++iter; } return cid; } /* cookieIDForConnName */ void CRefMgr::addToFreeList( CookieRef* cref ) { MutexLock ml( &m_freeList_mutex ); m_freeList.push_back( cref ); } CookieRef* CRefMgr::getFromFreeList( void ) { CookieRef* cref = NULL; MutexLock ml( &m_freeList_mutex ); if ( m_freeList.size() > 0 ) { cref = m_freeList.front(); m_freeList.pop_front(); } return cref; } /* connect case */ CidInfo* CRefMgr::getMakeCookieRef( const char* cookie, HostID hid, int socket, int nPlayersH, int nPlayersT, int langCode, int seed, bool wantsPublic, bool makePublic, bool* seenSeed ) { CidInfo* cinfo; /* We have a cookie from a new connection or from a reconnect. This may be the first time it's been seen, or there may be a game currently in the XW_ST_CONNECTING state, or it may be a dupe of a connect packet on the same or a different socket. If there's a game, cool. Otherwise add a new one. Pass the connName which will be used if set, but if not set we'll be generating another later when the game is complete. */ for ( ; ; ) { /* What's this for loop thing. It's to fix a race condition. One thread has "claim" on cid <N>, which is in the DB. Another comes into this function and looks it up in the DB, retrieving <N>, but progress is blocked inside getCookieRef_impl which calls Claim(). The first thread winds up removing <N> from the DB and deleting its cref before calling Relinquish so that when Claim() returns there's no cref. So we test for that case and retry. */ CookieID cid; char connNameBuf[MAX_CONNNAME_LEN+1] = {0}; int alreadyHere = 0; *seenSeed = m_db->SeenSeed( cookie, seed, langCode, nPlayersT, wantsPublic, connNameBuf, sizeof(connNameBuf), &alreadyHere, &cid ); if ( !*seenSeed ) { cid = m_db->FindOpen( cookie, langCode, nPlayersT, nPlayersH, wantsPublic, connNameBuf, sizeof(connNameBuf), &alreadyHere ); } if ( cid > 0 ) { cinfo = m_cidlock->Claim( cid ); if ( NULL == cinfo->GetRef() ) { m_cidlock->Relinquish( cinfo, true ); continue; } else if ( !cinfo->GetRef()->HaveRoom( nPlayersH ) ) { m_cidlock->Relinquish( cinfo, false ); continue; } } else { cinfo = m_cidlock->Claim(); cid = cinfo->GetCid(); CookieRef* cref = AddNew( cookie, connNameBuf, cid, langCode, nPlayersT, alreadyHere ); cinfo->SetRef( cref ); if ( !connNameBuf[0] ) { /* didn't exist in DB */ m_db->AddNew( cookie, cref->ConnName(), cid, langCode, nPlayersT, wantsPublic || makePublic ); } else { if ( !m_db->AddCID( connNameBuf, cid ) ) { m_cidlock->Relinquish( cinfo, true ); continue; } } } break; } assert( cinfo->GetRef() ); return cinfo; } /* getMakeCookieRef */ /* reconnect case */ CidInfo* CRefMgr::getMakeCookieRef( const char* connName, const char* cookie, HostID hid, int socket, int nPlayersH, int nPlayersS, int seed, int langCode, bool isPublic, bool* isDead ) { CookieRef* cref = NULL; CidInfo* cinfo; for ( ; ; ) { /* for: see comment above */ /* fetch these from DB */ char curCookie[MAX_INVITE_LEN+1]; int curLangCode; int nPlayersT = 0; int nAlreadyHere = 0; CookieID cid = m_db->FindGame( connName, curCookie, sizeof(curCookie), &curLangCode, &nPlayersT, &nAlreadyHere, isDead ); if ( 0 != cid ) { /* already open */ cinfo = m_cidlock->Claim( cid ); if ( NULL == cinfo->GetRef() ) { m_cidlock->Relinquish( cinfo, true ); continue; } } else { /* The entry may not even be in the DB, e.g. if it got deleted. Deal with that possibility by taking the caller's word for it. */ cinfo = m_cidlock->Claim(); cid = cinfo->GetCid(); if ( nPlayersT == 0 ) { /* wasn't in the DB */ m_db->AddNew( cookie, connName, cid, langCode, nPlayersS, isPublic ); curLangCode = langCode; nPlayersT = nPlayersS; } else { if ( !m_db->AddCID( connName, cid ) ) { m_cidlock->Relinquish( cinfo, true ); continue; } cookie = curCookie; } cref = AddNew( cookie, connName, cid, curLangCode, nPlayersT, nAlreadyHere ); cinfo->SetRef( cref ); } break; } /* for */ assert( cinfo->GetRef() ); return cinfo; } /* getMakeCookieRef */ CidInfo* CRefMgr::getMakeCookieRef( const char* const connName, bool* isDead ) { CookieRef* cref = NULL; CidInfo* cinfo = NULL; char curCookie[MAX_INVITE_LEN+1]; int curLangCode; int nPlayersT = 0; int nAlreadyHere = 0; for ( ; ; ) { /* for: see comment above */ CookieID cid = m_db->FindGame( connName, curCookie, sizeof(curCookie), &curLangCode, &nPlayersT, &nAlreadyHere, isDead ); if ( 0 != cid ) { /* already open */ cinfo = m_cidlock->Claim( cid ); if ( NULL == cinfo->GetRef() ) { m_cidlock->Relinquish( cinfo, true ); continue; } } else { if ( nPlayersT == 0 ) { /* wasn't in the DB */ /* do nothing; insufficient info to fake it */ } else { cinfo = m_cidlock->Claim(); if ( !m_db->AddCID( connName, cinfo->GetCid() ) ) { m_cidlock->Relinquish( cinfo, true ); continue; } cref = AddNew( curCookie, connName, cinfo->GetCid(), curLangCode, nPlayersT, nAlreadyHere ); cinfo->SetRef( cref ); } } break; } return cinfo; } void CRefMgr::RemoveSocketRefs( int socket ) { { SafeCref scr( socket ); scr.Remove( socket ); } } void CRefMgr::PrintSocketInfo( int socket, string& out ) { SafeCref scr( socket ); const char* name = scr.Cookie(); if ( name != NULL && name[0] != '\0' ) { char buf[64]; snprintf( buf, sizeof(buf), "* socket: %d\n", socket ); out += buf; snprintf( buf, sizeof(buf), " in cookie: %s\n", name ); out += buf; } } CidInfo* CRefMgr::getCookieRef( CookieID cid, bool failOk ) { CidInfo* cinfo = NULL; for ( ; ; ) { cinfo = m_cidlock->Claim( cid ); if ( NULL != cinfo->GetRef() ) { break; } else if ( failOk ) { break; } m_cidlock->Relinquish( cinfo, true ); } return cinfo; } /* getCookieRef */ CidInfo* CRefMgr::getCookieRef( int socket ) { CidInfo* cinfo = m_cidlock->ClaimSocket( socket ); assert( NULL == cinfo || NULL != cinfo->GetRef() ); return cinfo; } /* getCookieRef */ #ifdef RELAY_HEARTBEAT /* static */ void CRefMgr::heartbeatProc( void* closure ) { CRefMgr* self = (CRefMgr*)closure; self->checkHeartbeats( ::uptime() ); } /* heartbeatProc */ #endif CookieRef* CRefMgr::AddNew( const char* cookie, const char* connName, CookieID cid, int langCode, int nPlayers, int nAlreadyHere ) { if ( 0 == connName[0] ) { connName = NULL; } /* PENDING: should this return a locked cref? */ logf( XW_LOGINFO, "%s( cookie=%s, connName=%s, cid=%d)", __func__, cookie, connName, cid ); CookieRef* ref = getFromFreeList(); RWWriteLock rwl( &m_cookieMapRWLock ); logf( XW_LOGINFO, "making new cref: %d", cid ); if ( !!ref ) { ref->ReInit( cookie, connName, cid, langCode, nPlayers, nAlreadyHere ); } else { ref = new CookieRef( cookie, connName, cid, langCode, nPlayers, nAlreadyHere ); } ref->assignConnName(); m_cookieMap.insert( pair<CookieID, CookieRef*>(ref->GetCid(), ref ) ); logf( XW_LOGINFO, "%s: paired cookie %s/connName %s with cid %d", __func__, (cookie?cookie:"NULL"), connName, ref->GetCid() ); #ifdef RELAY_HEARTBEAT if ( m_cookieMap.size() == 1 ) { RelayConfigs* cfg = RelayConfigs::GetConfigs(); int heartbeat; cfg->GetValueFor( "HEARTBEAT", &heartbeat ); TimerMgr::GetTimerMgr()->SetTimer( heartbeat, heartbeatProc, this, heartbeat ); } #endif logf( XW_LOGINFO, "%s=>%p", __func__, ref ); return ref; } /* AddNew */ void CRefMgr::Recycle_locked( CookieRef* cref ) { logf( XW_LOGINFO, "%s(cref=%p,cookie=%s)", __func__, cref, cref->Cookie() ); CookieID cid = cref->GetCid(); DBMgr::Get()->ClearCID( cref->ConnName() ); cref->Clear(); addToFreeList( cref ); cref->Unlock(); /* don't grab this lock until after releasing cref's lock; otherwise deadlock happens. */ RWWriteLock rwl( &m_cookieMapRWLock ); CookieMap::iterator iter = m_cookieMap.begin(); while ( iter != m_cookieMap.end() ) { CookieRef* ref = iter->second; if ( ref == cref ) { logf( XW_LOGINFO, "%s: erasing cref cid %d", __func__, cid ); m_cookieMap.erase( iter ); break; } ++iter; } assert( iter != m_cookieMap.end() ); /* we found something */ #ifdef RELAY_HEARTBEAT if ( m_cookieMap.size() == 0 ) { TimerMgr::GetTimerMgr()->ClearTimer( heartbeatProc, this ); } #endif } /* CRefMgr::Recycle */ void CRefMgr::Recycle( CookieID cid ) { CidInfo* cinfo = getCookieRef( cid ); if ( cinfo != NULL ) { CookieRef* cref = cinfo->GetRef(); cref->Lock(); Recycle_locked( cref ); } } /* Delete */ void CRefMgr::Recycle( const char* connName ) { Recycle( cookieIDForConnName( connName ) ); } /* Delete */ #ifdef RELAY_HEARTBEAT void CRefMgr::checkHeartbeats( time_t now ) { vector<CookieRef*> crefs; { RWReadLock rwl( &m_cookieMapRWLock ); CookieMap::iterator iter = m_cookieMap.begin(); while ( iter != m_cookieMap.end() ) { crefs.push_back(iter->second); ++iter; } } unsigned int ii; for ( ii = 0; ii < crefs.size(); ++ii ) { SafeCref scr( crefs[ii] ); scr.CheckHeartbeats( now ); } } /* checkHeartbeats */ #endif time_t CRefMgr::uptime( void ) { return time(NULL) - m_startTime; } /* static */ CookieMapIterator CRefMgr::GetCookieIterator() { CookieMapIterator iter(&m_cookieMapRWLock); return iter; } CookieMapIterator::CookieMapIterator(pthread_rwlock_t* rwlock) : m_rwl( rwlock ) ,_iter( CRefMgr::Get()->m_cookieMap.begin() ) { } CookieID CookieMapIterator::Next() { CookieID cid = 0; if ( _iter != CRefMgr::Get()->m_cookieMap.end() ) { CookieRef* cref = _iter->second; cid = cref->GetCid(); ++_iter; } return cid; } ////////////////////////////////////////////////////////////////////////////// // SafeCref ////////////////////////////////////////////////////////////////////////////// /* connect case */ SafeCref::SafeCref( const char* cookie, int socket, int clientVersion, int nPlayersH, int nPlayersS, unsigned short gameSeed, int langCode, bool wantsPublic, bool makePublic ) : m_cinfo( NULL ) , m_mgr( CRefMgr::Get() ) , m_clientVersion( clientVersion ) , m_isValid( false ) , m_seenSeed( false ) { CidInfo* cinfo; cinfo = m_mgr->getMakeCookieRef( cookie, 0, socket, nPlayersH, nPlayersS, langCode, gameSeed, wantsPublic, makePublic, &m_seenSeed ); if ( cinfo != NULL ) { CookieRef* cref = cinfo->GetRef(); m_locked = cref->Lock(); m_cinfo = cinfo; m_isValid = true; } } /* REconnect case */ SafeCref::SafeCref( const char* connName, const char* cookie, HostID hid, int socket, int clientVersion, int nPlayersH, int nPlayersS, unsigned short gameSeed, int langCode, bool wantsPublic, bool makePublic ) : m_cinfo( NULL ) , m_mgr( CRefMgr::Get() ) , m_clientVersion( clientVersion ) , m_isValid( false ) { CidInfo* cinfo; assert( hid <= 4 ); /* no more than 4 hosts */ bool isDead = false; cinfo = m_mgr->getMakeCookieRef( connName, cookie, hid, socket, nPlayersH, nPlayersS, gameSeed, langCode, wantsPublic || makePublic, &isDead ); if ( cinfo != NULL ) { assert( cinfo->GetCid() == cinfo->GetRef()->GetCid() ); m_locked = cinfo->GetRef()->Lock(); m_cinfo = cinfo; m_isValid = true; m_dead = isDead; } } /* ConnName case -- must exist (unless DB record's been removed */ SafeCref::SafeCref( const char* const connName ) : m_cinfo( NULL ) , m_mgr( CRefMgr::Get() ) , m_isValid( false ) { bool isDead = false; CidInfo* cinfo = m_mgr->getMakeCookieRef( connName, &isDead ); if ( NULL != cinfo && NULL != cinfo->GetRef() ) { assert( cinfo->GetCid() == cinfo->GetRef()->GetCid() ); m_locked = cinfo->GetRef()->Lock(); m_cinfo = cinfo; m_isValid = true; m_dead = isDead; } } SafeCref::SafeCref( CookieID cid, bool failOk ) : m_cinfo( NULL ) , m_mgr( CRefMgr::Get() ) , m_isValid( false ) , m_locked( false ) { CidInfo* cinfo = m_mgr->getCookieRef( cid, failOk ); if ( cinfo != NULL ) { /* known cookie? */ CookieRef* cref = cinfo->GetRef(); if ( NULL != cref ) { assert( cinfo->GetCid() == cref->GetCid() ); m_locked = cref->Lock(); m_isValid = m_locked && cid == cref->GetCid(); } m_cinfo = cinfo; } } SafeCref::SafeCref( int socket ) : m_cinfo( NULL ) , m_mgr( CRefMgr::Get() ) , m_isValid( false ) { CidInfo* cinfo = m_mgr->getCookieRef( socket ); if ( cinfo != NULL ) { /* known socket? */ CookieRef* cref = cinfo->GetRef(); assert( cinfo->GetCid() == cref->GetCid() ); m_locked = cref->Lock(); m_isValid = m_locked && cref->HasSocket_locked( socket ); m_cinfo = cinfo; } } SafeCref::~SafeCref() { if ( m_cinfo != NULL ) { bool recycle = true; if ( m_locked ) { CookieRef* cref = m_cinfo->GetRef(); assert( m_cinfo->GetCid() == cref->GetCid() ); recycle = cref->ShouldDie(); if ( recycle ) { m_mgr->Recycle_locked( cref ); } else { cref->Unlock(); } } m_mgr->m_cidlock->Relinquish( m_cinfo, recycle ); } }