1 // Ryzom - MMORPG Framework <http://dev.ryzom.com/projects/ryzom/>
2 // Copyright (C) 2010 Winch Gate Property Limited
4 // This program is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU Affero General Public License as
6 // published by the Free Software Foundation, either version 3 of the
7 // License, or (at your option) any later version.
9 // This program is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU Affero General Public License for more details.
14 // You should have received a copy of the GNU Affero General Public License
15 // along with this program. If not, see <http://www.gnu.org/licenses/>.
17 // connection_stats.cpp : Defines the entry point for the DLL application.
22 #define LOG_ANALYSER_PLUGIN_EXPORTS
23 #include "connection_stats.h"
28 BOOL APIENTRY
DllMain( HANDLE hModule
,
29 DWORD ul_reason_for_call
,
33 switch (ul_reason_for_call
)
35 case DLL_PROCESS_ATTACH
:
36 case DLL_THREAD_ATTACH
:
37 case DLL_THREAD_DETACH
:
38 case DLL_PROCESS_DETACH
:
45 #include <nel/misc/debug.h>
46 using namespace NLMISC
;
54 time_t LogBeginTime
= 0, LogEndTime
= 0;
55 bool ContinuationOfStatInPreviousFile
= true;
59 string
timeToStr( const time_t& ts
)
61 //return string(ctime( &ts )).substr( 0, 24 );
62 return IDisplayer::dateToHumanString( ts
);
67 string
toHourStr( uint totalSec
)
69 uint hour
= totalSec
/ 3600;
70 uint remMin
= totalSec
% 3600;
71 string res
= toString( "%uh%02u'%02u\"", hour
, remMin
/ 60, remMin
% 60 );
96 vector
<TSession
> Sessions
;
103 TPlayerStat() : UserId(0), Name("?"), Average(0), Sum(0), Min(0), Max(0) {}
106 bool beginSession( const time_t& ts
, uint clientId
, uint userId
, const string
& name
)
108 if ( Sessions
.empty() || (Name
== "?") )
110 init( userId
, name
);
113 if ( Sessions
.empty() || (Sessions
.back().EndTime
!= 0) )
115 // Open a new session
117 s
.ClientId
= clientId
;
121 Sessions
.push_back( s
);
126 // Occurs if two clients have the same userId
127 nlwarning( "Opening a session for user %u (%s) at %s while previous session at %s not closed", UserId
, Name
.c_str(), timeToStr( ts
).c_str(), timeToStr( Sessions
.back().BeginTime
).c_str() );
128 //Sessions.back().ClientId = clientId; // cumulate times
131 // Close the previous session
133 endSession( ts
, Sessions
.back().ClientId
, userId
, name
, &userMissing
, false );
135 // Open a new session
137 s
.ClientId
= clientId
;
141 Sessions
.push_back( s
);
148 * Return true if the disconnection is valid, false if ignored. If true, set userMissing if connection of user not found in the log
149 * but was done before writing into this stat file.
151 bool endSession( const time_t& ts
, uint clientId
, uint userId
, const string
& name
, bool *userMissing
, bool closed
=true )
153 if ( Sessions
.empty() || (Name
== "?") )
155 init( userId
, name
);
158 if ( Sessions
.empty() )
160 // User was already connected at beginning of log
161 if ( ContinuationOfStatInPreviousFile
)
163 nldebug( "User %u (%s): disconnection at %s: connection before beginning of stat detected", userId
, name
.c_str(), timeToStr( ts
).c_str() );
165 s
.ClientId
= clientId
;
169 Sessions
.push_back( s
);
175 nldebug( "User %u (%s): ignoring disconnection at %s (could not be connected before stat)", userId
, name
.c_str(), timeToStr( ts
).c_str() );
181 // Close the current session
182 if ( clientId
== Sessions
.back().ClientId
)
184 if ( Sessions
.back().EndTime
== 0 )
186 Sessions
.back().EndTime
= ts
;
187 Sessions
.back().Closed
= closed
;
188 *userMissing
= false;
193 nlwarning( "Detected two successive disconnections of user %u (%s) without reconnection (second ignored) at %s", userId
, name
.c_str(), timeToStr( ts
).c_str() );
199 // Occurs if two clients have the same userId
200 nlwarning( "Closing a session for user %u (%s) with invalid client (ignored)", userId
, name
.c_str() );
207 sint
calcSessionTime( uint numSession
, const time_t& testEndTime
)
209 if ( numSession
< Sessions
.size() )
211 if ( Sessions
[numSession
].BeginTime
== 0 )
213 Sessions
[numSession
].BeginTime
= LogBeginTime
;
214 nlinfo( "User %u %s already connected at beginning of log (session end at %s)", UserId
, Name
.c_str(), timeToStr( Sessions
[numSession
].EndTime
).c_str() );
216 if ( Sessions
[numSession
].EndTime
== 0 )
218 Sessions
[numSession
].EndTime
= LogEndTime
;
219 nlinfo( "User %u %s still connected at end of log (session begin at %s)", UserId
, Name
.c_str(), timeToStr( Sessions
[numSession
].BeginTime
).c_str() );
222 Sessions
[numSession
].Duration
= (int)difftime( Sessions
[numSession
].EndTime
, Sessions
[numSession
].BeginTime
);
223 return Sessions
[numSession
].Duration
;
232 void init( uint userId
, const string
& name
)
244 struct TInstantNbPlayers
256 typedef std::map
< uint
, TPlayerStat
> TPlayerMap
;
257 typedef std::deque
< TInstantNbPlayers
> TNbPlayersSeries
;
258 TPlayerMap PlayerMap
;
259 TNbPlayersSeries NbPlayersSeries
;
262 float TotalTimeInDays
;
271 NbPlayersSeries
.clear();
278 void addConnectionEvent( const time_t& ts
, uint userId
)
281 TInstantNbPlayers inp
;
288 NbPlayersSeries
.push_back( inp
);
292 nldebug( "Inserting connection of user %u at beginning", userId
);
293 // Insert at front and increment every other number
294 for ( TNbPlayersSeries::iterator iv
=NbPlayersSeries
.begin(); iv
!=NbPlayersSeries
.end(); ++iv
)
297 inp
.Timestamp
= LogBeginTime
;
298 NbPlayersSeries
.push_front( inp
);
304 void addDisconnectionEvent( const time_t& ts
, uint userId
)
307 TInstantNbPlayers inp
;
312 NbPlayersSeries
.push_back( inp
);
317 void addConnection( const time_t& ts
, uint clientId
, uint userId
, const string
& name
)
319 if ( PlayerMap
[userId
].beginSession( ts
, clientId
, userId
, name
) )
321 addConnectionEvent( ts
, userId
);
327 void addDisconnection( const time_t& ts
, uint clientId
, uint userId
)
330 if ( PlayerMap
[userId
].endSession( ts
, clientId
, userId
, "?", &userMissing
) )
334 // Add connection at beginning if the server was started at a date anterior to the beginning of this stat file
335 // (otherwise, just discard the disconnection, it could be a stat file corruption transformed
336 // into server reset)
337 addConnectionEvent( 0, userId
);
340 addDisconnectionEvent( ts
, userId
);
346 void resetConnections( const time_t& shutdownTs
, const time_t& restartTs
)
348 ContinuationOfStatInPreviousFile
= false;
350 TPlayerMap::iterator ipm
;
351 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
353 if ( ! (*ipm
).second
.Sessions
.empty() )
355 if ( (*ipm
).second
.Sessions
.back().EndTime
== 0 )
357 addDisconnection( shutdownTs
, (*ipm
).second
.Sessions
.back().ClientId
, (*ipm
).second
.UserId
);
358 nlwarning( "Resetting connection of user %u because of server shutdown at %s (restart at %s)", (*ipm
).second
.UserId
, timeToStr( shutdownTs
).c_str(), timeToStr( restartTs
).c_str() );
366 void fillUserNamesInEvents()
368 TNbPlayersSeries::iterator iv
;
369 for ( iv
=NbPlayersSeries
.begin(); iv
!=NbPlayersSeries
.end(); ++iv
)
371 (*iv
).Event
+= PlayerMap
[(*iv
).UserId
].Name
;
377 void extractTime( const string
& line
, time_t& ts
)
380 t
.tm_isdst
= -1; // auto-detect Daylight Saving Time
383 sscanf( line
.c_str(), "%d/%d/%d %d:%d:%d", &t
.tm_year
, &t
.tm_mon
, &t
.tm_mday
, &t
.tm_hour
, &t
.tm_min
, &t
.tm_sec
);
385 t
.tm_mon
-= 1; // 0..11
388 if ( ts
== (time_t)-1 )
391 s.Format( "%d/%d/%d %d:%d:%d (%d)", t.tm_year, t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, ts );
400 void calcStats( string
& res
)
402 TPlayerMap::iterator ipm
;
403 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
405 uint sum
= 0, themax
= 0, themin
= ~0;
406 for ( uint i
= 0; i
!=(*ipm
).second
.Sessions
.size(); ++i
)
408 sum
+= (*ipm
).second
.Sessions
[i
].Duration
;
409 if ( (uint
)(*ipm
).second
.Sessions
[i
].Duration
< themin
)
410 themin
= (*ipm
).second
.Sessions
[i
].Duration
;
411 if ( (uint
)(*ipm
).second
.Sessions
[i
].Duration
> themax
)
412 themax
= (*ipm
).second
.Sessions
[i
].Duration
;
414 (*ipm
).second
.Sum
= sum
;
415 (*ipm
).second
.Average
= sum
/ (*ipm
).second
.Sessions
.size();
416 (*ipm
).second
.Min
= themin
;
417 (*ipm
).second
.Max
= themax
;
422 // Return date for filename such as 2003-06-24
423 string
extractDateFilename( time_t date
)
425 string dateStr
= timeToStr( date
);
426 string::size_type pos
;
427 for ( pos
=0; pos
!=dateStr
.size(); ++pos
)
429 if ( dateStr
[pos
] == '/' )
431 if ( dateStr
[pos
] == ' ' )
433 dateStr
= dateStr
.substr( 0, pos
);
440 string::size_type slashPos = dateStr.rfind( '/' );
441 if ( slashPos == string::npos )
443 string year = dateStr.substr( slashPos+1, slashPos+5 );
444 slashPos = dateStr.rfind( '/', slashPos-1 );
445 if ( slashPos == string::npos )
447 string month = dateStr.substr( slashPos+1, slashPos+3 );
448 string day = dateStr.substr( 0, 2 );
449 return year + "-" + month + "-" + day;*/
453 enum TMainStatEnum
{ MSNb
, MSAverage
, MSSum
, MSMin
, MSMax
};
456 /// Return stats in float 'days'
457 void getValuesStatsAndClearValues( vector
<float>& values
, string
& res
, bool isTimeInMinute
, TMainStatEnum msEnum
)
459 float sum
= 0.0f
, themax
= 0.0f
, themin
= 60.0f
*24.0f
*365.25f
*100.0f
; // 1 century should be enough
460 vector
<float>::const_iterator iv
;
461 for ( iv
=values
.begin(); iv
!=values
.end(); ++iv
)
464 if ( (*iv
) < themin
)
466 if ( (*iv
) > themax
)
469 if ( isTimeInMinute
)
471 res
+= toString( "\t%g", sum
/ (float)values
.size() / (24.0f
*60.0f
) ) +
472 toString( "\t%g", sum
/ (24.0f
*60.0f
) ) +
473 toString( "\t%g", themin
/ (24.0f
*60.0f
) ) +
474 toString( "\t%g", themax
/ (24.0f
*60.0f
) );
480 MainStats
+= toString( "\t%g", sum
/ (float)values
.size() / (24.0f
*60.0f
) / TotalTimeInDays
) +
481 toString( "\t%g", sum
/ (24.0f
*60.0f
) / TotalTimeInDays
) +
482 toString( "\t%g", themax
/ (24.0f
*60.0f
) / TotalTimeInDays
);
493 res
+= "\t" + toString( "%g", sum
/ (float)values
.size() ) +
494 "\t" + toString( "%g", sum
) +
495 "\t" + toString( "%g", themin
) +
496 "\t" + toString( "%g", themax
);
502 /// Return stats in float 'days'
503 void getValuesStatsAndClearValues( vector
<float>& values
, vector
< vector
<string
> >& table
, bool isTimeInMinute
, TMainStatEnum msEnum
)
505 float sum
= 0.0f
, themax
= 0.0f
, themin
= 60.0f
*24.0f
*365.25f
*100.0f
; // 1 century should be enough
506 vector
<float>::const_iterator iv
;
507 for ( iv
=values
.begin(); iv
!=values
.end(); ++iv
)
510 if ( (*iv
) < themin
)
512 if ( (*iv
) > themax
)
515 if ( isTimeInMinute
)
517 table
.back().push_back( toString( "%g", sum
/ (float)values
.size() / (24.0f
*60.0f
) ) );
518 table
.back().push_back( toString( "%g", sum
/ (24.0f
*60.0f
) ) );
519 table
.back().push_back( toString( "%g", themin
/ (24.0f
*60.0f
) ) );
520 table
.back().push_back( toString( "%g", themax
/ (24.0f
*60.0f
) ) );
526 MainStats
+= toString( "\t%g", sum
/ (float)values
.size() / (24.0f
*60.0f
) / TotalTimeInDays
) +
527 toString( "\t%g", sum
/ (24.0f
*60.0f
) / TotalTimeInDays
) +
528 toString( "\t%g", themax
/ (24.0f
*60.0f
) / TotalTimeInDays
);
539 table
.back().push_back( toString( "%g", sum
/ (float)values
.size() ) );
540 table
.back().push_back( toString( "%g", sum
) );
541 table
.back().push_back( toString( "%g", themin
) );
542 table
.back().push_back( toString( "%g", themax
) );
548 /// (Note: main stats only for minutes)
549 uint
getSessionDurations( string
& res
, time_t endTime
, bool convertToMinutes
, bool inColumnsWithDetail
)
552 if ( inColumnsWithDetail
)
555 TPlayerMap::iterator ipm
;
556 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
558 s1
+= toString( "%u", (*ipm
).second
.UserId
) + "\t";
559 s2
+= (*ipm
).second
.Name
+ "\t";
561 res
+= s1
+ "\r\n" + s2
+ "\r\n";
566 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
568 sint duration
= (*ipm
).second
.calcSessionTime( sessionNum
, endTime
);
571 res
+= (convertToMinutes
? toString( "%.2f", (float)duration
/60.0f
) : toString( "%d", duration
)) + "\t";
575 res
+= string(convertToMinutes
? "0" : "") + "\t";
583 while ( timeSum
!= 0 );
588 if ( ! convertToMinutes
)
591 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
593 res
+= toString( "%u\t", (*ipm
).second
.Sessions
.size() );
596 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
598 res
+= toString( "%u\t", (*ipm
).second
.Average
);
601 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
603 res
+= toString( "%u\t", (*ipm
).second
.Sum
);
606 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
608 res
+= toString( "%u\t", (*ipm
).second
.Min
);
611 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
613 res
+= toString( "%u\t", (*ipm
).second
.Max
);
620 vector
<float> values
;
621 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
623 values
.push_back( (float)((*ipm
).second
.Sessions
.size()) );
624 res
+= toString( "%u\t", (*ipm
).second
.Sessions
.size() );
626 getValuesStatsAndClearValues( values
, res
, false, MSNb
);
628 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
630 values
.push_back( (float)((*ipm
).second
.Average
)/60.0f
);
631 res
+= toString( "%.2f\t", values
.back() );
633 getValuesStatsAndClearValues( values
, res
, true, MSAverage
);
635 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
637 values
.push_back( (float)((*ipm
).second
.Sum
)/60.0f
);
638 res
+= toString( "%.2f\t", values
.back() );
640 getValuesStatsAndClearValues( values
, res
, true, MSSum
);
642 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
644 values
.push_back( (float)((*ipm
).second
.Min
)/60.0f
);
645 res
+= toString( "%.2f\t", values
.back() );
647 getValuesStatsAndClearValues( values
, res
, true, MSMin
);
649 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
651 values
.push_back( (float)((*ipm
).second
.Max
)/60.0f
);
652 res
+= toString( "%.2f\t", values
.back() );
654 getValuesStatsAndClearValues( values
, res
, true, MSMax
);
660 vector
< vector
<string
> > table
;
665 TPlayerMap::iterator ipm
;
666 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
668 table
[0].push_back( toString( "%u", (*ipm
).second
.UserId
) ); //+ "\t";
669 table
[1].push_back( (*ipm
).second
.Name
); //+ "\t";
671 //res += s1 + "\r\n" + s2 + "\r\n";
676 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
678 sint duration
= (*ipm
).second
.calcSessionTime( sessionNum
, endTime
);
683 while ( timeSum
!= 0 );
689 if ( ! convertToMinutes
)
693 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
695 table
.back().push_back( toString( "%u", (*ipm
).second
.Sessions
.size() ) );
699 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
701 table
.back().push_back( toString( "%u", (*ipm
).second
.Average
) );
705 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
707 table
.back().push_back( toString( "%u", (*ipm
).second
.Sum
) );
711 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
713 table
.back().push_back( toString( "%u", (*ipm
).second
.Min
) );
717 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
719 table
.back().push_back( toString( "%u", (*ipm
).second
.Max
) );
726 vector
<float> values
;
728 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
730 values
.push_back( (float)((*ipm
).second
.Sessions
.size()) );
731 table
.back().push_back( toString( "%u", (*ipm
).second
.Sessions
.size() ) );
733 getValuesStatsAndClearValues( values
, table
, false, MSNb
);
736 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
738 values
.push_back( (float)((*ipm
).second
.Average
)/60.0f
);
739 table
.back().push_back( toString( "%.2f", values
.back() ) );
741 getValuesStatsAndClearValues( values
, table
, true, MSAverage
);
744 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
746 values
.push_back( (float)((*ipm
).second
.Sum
)/60.0f
);
747 table
.back().push_back( toString( "%.2f", values
.back() ) );
749 getValuesStatsAndClearValues( values
, table
, true, MSSum
);
752 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
754 values
.push_back( (float)((*ipm
).second
.Min
)/60.0f
);
755 table
.back().push_back( toString( "%.2f", values
.back() ) );
757 getValuesStatsAndClearValues( values
, table
, true, MSMin
);
760 for ( ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
762 values
.push_back( (float)((*ipm
).second
.Max
)/60.0f
);
763 table
.back().push_back( toString( "%.2f", values
.back() ) );
765 getValuesStatsAndClearValues( values
, table
, true, MSMax
);
770 /*for ( uint j=0; j!=table.size(); ++j )
772 for ( uint i=0; i!=table[j].size(); ++i )
774 res += table[j][i] + "\t";
781 for ( uint j
=0; j
!=table
.size(); ++j
)
783 if ( table
[j
].size() > maxI
)
784 maxI
= table
[j
].size();
786 for ( uint i
=0; i
!=maxI
; ++i
)
788 for ( uint j
=0; j
!=table
.size(); ++j
)
790 if ( i
< table
[j
].size() )
791 res
+= table
[j
][i
] + "\t";
808 LOG_ANALYSER_PLUGIN_API
std::string
getInfoString()
810 return "Input log: connection.stat or frontend_service.log.\n\nProduces tab-separated connection stats for Excel.";
817 LOG_ANALYSER_PLUGIN_API
bool doAnalyse( const std::vector
<const char *>& vec
, std::string
& res
, std::string
& log
)
819 NLMISC::createDebug();
820 CMemDisplayer memdisp
;
821 NLMISC::DebugLog
->addDisplayer( &memdisp
, true );
822 NLMISC::InfoLog
->addDisplayer( &memdisp
, true );
823 NLMISC::WarningLog
->addDisplayer( &memdisp
, true );
824 NLMISC::ErrorLog
->addDisplayer( &memdisp
, true );
825 NLMISC::AssertLog
->addDisplayer( &memdisp
, true );
829 // Begin and end time
833 sint quicklines
= min( vec
.size(), 100 );
834 while ( (l
< quicklines
) && ((string(vec
[l
]).size()<5) || (vec
[l
][4] != '/')) )
836 if ( l
< quicklines
)
837 extractTime( string(vec
[l
]), LogBeginTime
);
839 l
= ((sint
)vec
.size())-1;
840 quicklines
= max( ((sint
)vec
.size())-100, 0 );
841 while ( (l
>= quicklines
) && ((string(vec
[l
]).size()<5) || (vec
[l
][4] != '/')) )
843 if ( l
>= quicklines
)
844 extractTime( string(vec
[l
]), LogEndTime
);
846 res
+= "Log begin time\t\t" + timeToStr( LogBeginTime
) + "\r\n";
847 res
+= "Log end time\t\t" + timeToStr( LogEndTime
) + "\r\n";
848 MainStats
+= timeToStr( LogEndTime
);
849 TotalTimeInDays
= ((float)(LogEndTime
-LogBeginTime
)) / 3600.0f
/ 24.0f
;
852 uint nbPossibleCorruptions
= 0;
854 for ( i
=0; i
!=vec
.size(); ++i
)
856 string line
= string(vec
[i
]);
862 // Auto-detect file corruption (version for connections.stat)
865 bool corrupted
= false;
867 // Search for beginning not being a date or 'Log Starting"
868 if ( (line
.size() < 20) || (line
[10]!=' ') || (line
[13]!=':')
869 || (line
[16]!=':') || (line
[19]!=' ') || (line
.substr( 20 ).find( " : ") == string::npos
) )
871 if ( line
.find( "Log Starting [" ) != 0 )
876 // Search for year not at beginning. Ex: "2003/" (it does not work when the year changes in the log!)
877 p
= line
.substr( 1 ).find( timeToStr( LogBeginTime
).substr( 0, 5 ) );
878 if ( p
!= string::npos
)
880 ++p
; // because searched from pos 1
882 // Search for date/time
883 if ( (line
.size()>p
+20) && (line
[p
+10]==' ') && (line
[p
+13]==':')
884 && (line
[p
+16]==':') && (line
[p
+19]==' ') )
886 // Search for the two next blank characters. The second is followed by ": ".
887 // (Date Time ThreadId Machine/Service : User-defined log line)
889 string::size_type sp
;
890 for ( sp
=p
+20; sp
!=line
.size(); ++sp
)
897 if ( (nbBlank
==2) && (line
[sp
+1]==':') && (line
[sp
+2]==' ') )
906 ++nbPossibleCorruptions
;
907 nlwarning( "Found possible file corruption at line %u: %s", i
, line
.c_str() );
911 // Detect connections/disconnections
912 p
= line
.find( "Adding client" );
913 if ( p
!= string::npos
)
915 extractTime( line
, ts
);
917 sscanf( line
.substr( p
).c_str(), "Adding client %u (uid %u name %s", &clientId
, &userId
, &name
);
919 addConnection( ts
, clientId
, userId
, sname
/*sname.substr( 0, sname.size()-1 )*/ ); // now name format is "name priv ''".
922 p
= line
.find( "Sent CL_DISCONNECT for client" );
923 if ( p
!= string::npos
)
925 extractTime( line
, ts
);
926 sscanf( line
.substr( p
).c_str(), "Sent CL_DISCONNECT for client %u (uid %u)", &clientId
, &userId
);
927 addDisconnection( ts
, clientId
, userId
);
930 p
= line
.find( "Log Starting [" );
931 if ( p
!= string::npos
)
933 uint hs
= string("Log Starting [").size();
934 line
= line
.substr( hs
, line
.size() - hs
- 1 ); // remove ] to get the timestamp
936 extractTime( line
, restartTs
);
937 // Go back to find the last time of log
938 sint quicklines
= max( ((sint
)i
)-10, 0 );
939 sint l
= ((sint
)i
)-1;
940 while ( (l
>= quicklines
) && ((string(vec
[l
]).size()<5) || (vec
[l
][4] != '/')) )
942 if ( l
>= quicklines
)
943 extractTime( vec
[l
], ts
);
946 resetConnections( ts
, restartTs
);
949 fillUserNamesInEvents();
953 uint maxNbSession
= getSessionDurations( sd
, LogEndTime
, true, false );
954 res
+= "Number of accounts\t" + toString( "%u", PlayerMap
.size() ) + "\r\n";
955 res
+= "Max number of session\t" + toString( "%u", maxNbSession
) + "\r\n";
956 res
+= "\r\nTime of sessions\r\n";
958 res
+= "Connection events\r\n";
959 MainStats
+= toString( "\t%u", PlayerMap
.size() );
962 time_t prevTimestamp
= 0;
963 sint durationSum
= 0;
964 int prevPlayerNb
= -1;
965 uint maxPlayerNb
= 0;
966 for ( i
=0; i
!=NbPlayersSeries
.size(); ++i
)
968 sint duration
= (prevTimestamp
!=0) ? (int)difftime( NbPlayersSeries
[i
].Timestamp
, prevTimestamp
) : 0;
969 prevTimestamp
= NbPlayersSeries
[i
].Timestamp
;
970 durationSum
+= duration
;
971 if ( prevPlayerNb
!= -1 )
972 res
+= "\t" + toString( "%d", durationSum
) + "\t" + timeToStr( NbPlayersSeries
[i
].Timestamp
) + "\t" + toString( "%u", prevPlayerNb
) + "\t" + "\r\n";
973 res
+= toString( "%d", duration
) + "\t" + toString( "%d", durationSum
) + "\t" + timeToStr( NbPlayersSeries
[i
].Timestamp
) + "\t" + toString( "%u", NbPlayersSeries
[i
].Nb
) + "\t" + NbPlayersSeries
[i
].Event
+ "\r\n";
974 prevPlayerNb
= NbPlayersSeries
[i
].Nb
;
975 if ( NbPlayersSeries
[i
].Nb
> maxPlayerNb
)
976 maxPlayerNb
= NbPlayersSeries
[i
].Nb
;
978 MainStats
+= toString( "\t%u", maxPlayerNb
) + toString( "\t\t(%g days)", TotalTimeInDays
);
979 if ( nbPossibleCorruptions
== 0 )
980 MainStats
+= toString( "\t\tStat file OK" );
982 MainStats
+= toString( "\t\tFound %u possible stat file corruptions (edit the stat file to replace them with server reset if time too long, see log.log)", nbPossibleCorruptions
);
985 res
+= "\r\n\nStats per user (hrs)\r\n\nName\tUserId\tSessions\tCumulated\tAverage\tMin\tMax";
986 for ( TPlayerMap::const_iterator ipm
=PlayerMap
.begin(); ipm
!=PlayerMap
.end(); ++ipm
)
989 const TPlayerStat
& playerStat
= (*ipm
).second
;
990 res
+= toString( "%s\tUser %u\t%u\t%s\t%s\t%s\t%s",
991 playerStat
.Name
.c_str(), playerStat
.UserId
, playerStat
.Sessions
.size(),
992 toHourStr(playerStat
.Sum
).c_str(), toHourStr(playerStat
.Average
).c_str(), toHourStr(playerStat
.Min
).c_str(), toHourStr(playerStat
.Max
).c_str() );
993 for ( uint i
=0; i
!=playerStat
.Sessions
.size(); ++i
)
996 const TSession
& sess
= playerStat
.Sessions
[i
];
997 string status
= sess
.Closed
? "OK" : "Not closed";
998 res
+= timeToStr( sess
.BeginTime
) + "\t" + timeToStr( sess
.EndTime
) + "\t" + status
+ "\t" + toHourStr( sess
.Duration
);
1002 string dateStr
= " " + extractDateFilename( LogEndTime
);
1003 res
= dateStr
+ "\r\nDate\tAvg per player\tTotal time\tMax per player\tNb Players\tSimult. Pl.\r\n" + MainStats
+ "\r\n" + res
;
1005 memdisp
.write( log
);
1012 s.Format( "Found C=%u U=%u N=%s in %s", clientId, userId, name, line.substr( p ).c_str() );
1013 AfxMessageBox( s );*/