/*
** Command & Conquer Generals Zero Hour(tm)
** Copyright 2025 Electronic Arts Inc.
**
** 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 3 of the License, or
** (at your option) any later version.
**
** 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, see .
*/
/////////////////////////////////////////////////////////////////////////EA-V1
// $File: //depot/GeneralsMD/Staging/code/Libraries/Source/debug/debug_io_flat.cpp $
// $Author: mhoffe $
// $Revision: #1 $
// $DateTime: 2003/07/03 11:55:26 $
//
// ©2003 Electronic Arts
//
// Debug I/O class flat (flat or split log file)
//////////////////////////////////////////////////////////////////////////////
#include "_pch.h"
#include
#include // needed for placement new prototype
DebugIOFlat::OutputStream::OutputStream(const char *filename, unsigned maxSize):
m_bufferUsed(0), m_nextChar(0)
{
m_fileName=(char *)DebugAllocMemory(strlen(filename)+1);
strcpy(m_fileName,filename);
m_limitedFileSize=maxSize>0;
m_bufferSize=m_limitedFileSize?maxSize:0x10000;
m_buffer=(char *)DebugAllocMemory(m_bufferSize);
if (!m_limitedFileSize)
m_fileHandle=CreateFile(m_fileName,GENERIC_WRITE,0,NULL,CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL|FILE_FLAG_WRITE_THROUGH,
NULL);
}
DebugIOFlat::OutputStream::~OutputStream()
{
}
DebugIOFlat::OutputStream *DebugIOFlat::OutputStream::Create(const char *filename, unsigned maxSize)
{
return new (DebugAllocMemory(sizeof(OutputStream))) OutputStream(filename,maxSize);
}
void DebugIOFlat::OutputStream::Delete(const char *path)
{
Flush();
if (!m_limitedFileSize)
CloseHandle(m_fileHandle);
if (path&&*path)
{
// copy file to given path
int run=-1;
char *ext=strrchr(m_fileName,'.');
if (!ext)
ext=m_fileName+strlen(m_fileName);
char *fileNameOnly=strrchr(m_fileName,'\\');
fileNameOnly=fileNameOnly?fileNameOnly+1:m_fileName;
unsigned pathLen=strlen(path);
for (;;)
{
// absolute path?
char help[512];
if (path[0]&&(path[1]==':'||(path[0]=='\\'&&path[1]=='\\')))
{
strcpy(help,path);
strcpy(help+pathLen,fileNameOnly);
help[ext-fileNameOnly+pathLen]=0;
}
else
{
// no, relative path given
strcpy(help,m_fileName);
strcpy(help+(fileNameOnly-m_fileName),path);
strcpy(help+(fileNameOnly-m_fileName)+pathLen,fileNameOnly);
help[ext-fileNameOnly+pathLen+(fileNameOnly-m_fileName)]=0;
}
if (++run)
wsprintf(help+strlen(help),"(%i)%s",run,ext);
else
strcat(help,ext);
if (CopyFile(m_fileName,help,TRUE))
break;
if (GetLastError()!=ERROR_FILE_EXISTS)
break;
}
}
DebugFreeMemory(m_buffer);
DebugFreeMemory(m_fileName);
this->~OutputStream();
DebugFreeMemory(this);
}
void DebugIOFlat::OutputStream::Write(const char *src)
{
if (!src)
{
// flush request, flush only if unlimited file size
if (!m_limitedFileSize)
Flush();
}
else
{
unsigned len=strlen(src);
while (len>m_bufferSize)
{
InternalWrite(src,m_bufferSize);
src+=m_bufferSize;
len-=m_bufferSize;
}
InternalWrite(src,len);
}
}
void DebugIOFlat::OutputStream::InternalWrite(const char *src, unsigned len)
{
__ASSERT(len<=m_bufferSize);
// unlimited log file length?
if (!m_limitedFileSize)
{
if (m_bufferUsed+len>m_bufferSize)
Flush();
memcpy(m_buffer+m_bufferUsed,src,len);
m_bufferUsed+=len;
}
else
{
// just write to ring buffer
if ((m_bufferUsed+=len)>m_bufferSize)
m_bufferUsed=m_bufferSize;
while (len)
{
unsigned toWrite;
if (m_nextChar+len>m_bufferSize)
toWrite=m_bufferSize-m_nextChar;
else
toWrite=len;
memcpy(m_buffer+m_nextChar,src,toWrite);
if ((m_nextChar+=toWrite)>=m_bufferSize)
m_nextChar=0;
src+=toWrite;
len-=toWrite;
}
}
}
void DebugIOFlat::OutputStream::Flush(void)
{
if (!m_limitedFileSize)
{
// simple flush to file
DWORD written;
WriteFile(m_fileHandle,m_buffer,m_bufferUsed,&written,NULL);
m_bufferUsed=0;
}
else
{
// create file, write ring buffer
m_fileHandle=CreateFile(m_fileName,GENERIC_WRITE,0,NULL,CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL|FILE_FLAG_WRITE_THROUGH,
NULL);
DWORD written;
if (m_bufferUsed250)
src="*eMN";
// non-magic name?
if (*src!='*')
{
// just return input name
if (splitName)
{
// must jam in split name before extension
const char *p=strrchr(src,'.');
if (!p)
p=src+strlen(src);
strncpy(buf,src,p-src);
buf[p-src]='-';
strcpy(buf+(p-src)+1,splitName);
strcat(buf,p);
}
else
strcpy(buf,src);
return;
}
// translate magic name
src++;
char *dst=buf;
while (*src)
{
if (dst-buf>250)
break;
if (*src>='A'&&*src<='Z'&&(*src!='N'||splitName))
*dst++='-';
char help[256];
DWORD size=sizeof(help);
*help=0;
switch(*src++)
{
case 'e':
case 'E':
GetModuleFileName(NULL,help,sizeof(help));
break;
case 'm':
case 'M':
GetComputerName(help,&size);
break;
case 'u':
case 'U':
GetUserName(help,&size);
break;
case 't':
case 'T':
{
SYSTEMTIME systime;
GetLocalTime(&systime);
wsprintf(help,"%04i%02i%02i-%02i%02i-%02i",
systime.wYear,systime.wMonth,systime.wDay,
systime.wHour,systime.wMinute,systime.wSecond);
}
break;
case 'n':
case 'N':
if (splitName&&strlen(splitName)<250)
strcpy(help,splitName);
break;
default:
*dst++=src[-1];
}
unsigned len=strlen(help);
if (dst-buf+len>250)
break;
strcpy(dst,help);
dst+=len;
}
strcpy(dst,".log");
}
DebugIOFlat::DebugIOFlat(void):
m_firstStream(NULL), m_firstSplit(NULL),
m_lastStreamPtr(&m_firstStream), m_lastSplitPtr(&m_firstSplit)
{
*m_copyDir=0;
}
DebugIOFlat::~DebugIOFlat()
{
for (SplitListEntry *cur=m_firstSplit;cur;)
{
SplitListEntry *kill=cur;
cur=cur->next;
DebugFreeMemory(kill);
}
m_firstSplit=NULL;
for (StreamListEntry *stream=m_firstStream;stream;)
{
StreamListEntry *kill=stream;
stream=stream->next;
kill->stream->Delete(m_copyDir);
DebugFreeMemory(kill);
}
}
void DebugIOFlat::Write(StringType type, const char *src, const char *str)
{
for (SplitListEntry *cur=m_firstSplit;cur;cur=cur->next)
{
if (!(cur->stringTypes&(1<items))
continue;
cur->stream->Write(str);
break;
}
if (!cur)
m_firstStream->stream->Write(str);
}
void DebugIOFlat::EmergencyFlush(void)
{
for (StreamListEntry *cur=m_firstStream;cur;cur=cur->next)
cur->stream->Flush();
}
void DebugIOFlat::Execute(class Debug& dbg, const char *cmd, bool structuredCmd,
unsigned argn, const char * const * argv)
{
if (!cmd||!strcmp(cmd,"help"))
{
if (!argn)
dbg << "flat I/O help:\n"
"The following I/O commands are defined:\n"
" add, copy, splitadd, splitview, splitremove\n"
"Type in debug.io flat help for a detailed command help.\n";
else if (!strcmp(argv[0],"add"))
dbg <<
"add [ [ ] ]\n\n"
"Create flat file I/O (optionally specifying file name and file size).\n"
"If a filename is specified all output is written to that file. Otherwise\n"
"the magic filename '*eMN' is automatically used. Any existing files with\n"
"that name are overwritten.\n"
"\n"
"Instead of a real file name a 'magic' file name can be used by starting\n"
"the file name with a '*' followed by any number of special characters:\n"
"- 'e': inserts EXE name\n"
"- 'm': inserts machine name\n"
"- 'u': inserts username\n"
"- 't': inserts timestamp\n"
"- 'n': inserts split name (empty if main log file)\n"
"- '-': inserts '-'\n"
"- 'E', 'M', 'U', 'T', 'N': same as above but with '-' in front \n"
" (for 'N': empty if main log file)\n"
".log is automatically appended if using magic file names.\n"
"\n"
"If a size is specified then all data is internally written to a fixed \n"
"size memory based ring buffer. This data is flushed out once the \n"
"program exits. If no size is given then the size of the log file is not \n"
"limited and any log data is written out immediately.\n";
else if (!strcmp(argv[0],"copy"))
dbg <<
"copy \n\n"
"Copies generated log file(s) into the given directory if the program\n"
"exists or crashes. If there is already a log file with the same\n"
"name a unique number is appended to the current log files' name.\n";
else if (!strcmp(argv[0],"splitadd"))
dbg <<
"splitadd [ ]\n\n"
"Splits off part of the log data. Multiple splits can be defined. They \n"
"are written out to the first matching split file.\n"
"\n"
"'types' defines one or more string types which should be split off:\n"
"- a: asserts\n"
"- c: checks\n"
"- l: logs\n"
"- h: crash\n"
"- x: exceptions\n"
"- r: replies from commands\n"
"- o: other messages \n"
"\n"
"Next a filter is specified that determines which items are to be \n"
"filtered (only for string types a, c and l). Items can be listed with\n"
"then 'list' command. The filter is exactly specified as in that command.\n"
"\n"
"The third parameter defines a name for this split. If there is \n"
"already a split with the same name then both will write to the same \n"
"destination file.\n"
"\n"
"Note: If splits are used and the filename given for 'add' is static \n"
"or does not contain 'n' or 'N' then the split name is automatically \n"
"appended to the log file name (before the extension).\n"
"\n"
"If a size is specified then all data is internally written to a \n"
"fixed size memory based ring buffer. This data is flushed out once \n"
"the program exits.\n"
"\n"
"If no size is given then the size of the log file is not limited and \n"
"any log data is written out immediately.\n";
else if (!strcmp(argv[0],"splitview"))
dbg << "splitview\n\n"
"Shows all existing splits in the order they are evaluated.";
else if (!strcmp(argv[0],"splitremove"))
dbg << "splitremove \n\n"
"Removes all active splits matching the given name pattern.";
else
dbg << "Unknown flat I/O command";
}
else if (!strcmp(cmd,"add"))
{
// add [ [ ] ]
__ASSERT(m_firstStream==NULL);
strcpy(m_baseFilename,argn?argv[0]:"*eMN");
char fn[256];
ExpandMagic(m_baseFilename,NULL,fn);
m_firstStream=(StreamListEntry *)DebugAllocMemory(sizeof(StreamListEntry));
m_firstStream->next=NULL;
m_firstStream->stream=OutputStream::Create(fn,argn>1?atoi(argv[1])*1024:0);
m_lastStreamPtr=&m_firstStream->next;
}
else if (!strcmp(cmd,"copy"))
{
// copy
if (argn)
{
strncpy(m_copyDir,argv[0],sizeof(m_copyDir)-1);
m_copyDir[sizeof(m_copyDir)-1]=0;
}
}
else if (!strcmp(cmd,"splitadd"))
{
// splitadd [ ]
if (argn>=3)
{
// add entry
SplitListEntry *cur=(SplitListEntry *)DebugAllocMemory(sizeof(SplitListEntry));
cur->next=*m_lastSplitPtr;
m_lastSplitPtr=&cur->next;
if (!m_firstSplit)
m_firstSplit=cur;
cur->stringTypes=0;
for (const char *p=argv[0];*p;++p)
{
switch(*p)
{
case 'a': cur->stringTypes|=1<stringTypes|=1<stringTypes|=1<stringTypes|=1<stringTypes|=1<stringTypes|=1<stringTypes|=1<stringTypes)
cur->stringTypes=0xffffffff;
strncpy(cur->items,argv[1],sizeof(cur->items)-1);
cur->items[sizeof(cur->items)-1]=0;
strncpy(cur->name,argv[2],sizeof(cur->name)-1);
cur->name[sizeof(cur->name)-1]=0;
// create our filename, search for stream with same filename
char fn[256];
ExpandMagic(m_baseFilename,cur->name,fn);
for (StreamListEntry *stream=m_firstStream;stream;stream=stream->next)
if (!strcmp(stream->stream->GetFilename(),fn))
break;
if (!stream)
{
// must create new stream
stream=(StreamListEntry *)DebugAllocMemory(sizeof(StreamListEntry));
stream->next=NULL;
*m_lastStreamPtr=stream;
m_lastStreamPtr=&stream->next;
stream->stream=OutputStream::Create(fn,argn>3?atoi(argv[3])*1024:0);
}
cur->stream=stream->stream;
}
}
else if (!strcmp(cmd,"splitview"))
{
// splitview
for (SplitListEntry *cur=m_firstSplit;cur;cur=cur->next)
{
for (StringType t=Assert;tstringTypes&(1<items << " " << cur->name << "\n";
}
}
else if (!strcmp(cmd,"splitremove"))
{
// splitremove
const char *pattern=argn<1?"*":argv[0];
for (SplitListEntry **entryPtr=&m_firstSplit;*entryPtr;)
{
if ( Debug::SimpleMatch((*entryPtr)->name,pattern) )
{
// remove this entry
SplitListEntry *cur=*entryPtr;
*entryPtr=cur->next;
DebugFreeMemory(cur);
}
else
entryPtr=&((*entryPtr)->next);
}
// must fixup m_lastSplitPtr now
if (m_firstSplit)
{
for (SplitListEntry *cur=m_firstSplit;cur->next;cur=cur->next);
m_firstSplit=cur;
}
else
m_firstSplit=NULL;
}
}
DebugIOInterface *DebugIOFlat::Create(void)
{
return new (DebugAllocMemory(sizeof(DebugIOFlat))) DebugIOFlat();
}
void DebugIOFlat::Delete(void)
{
this->~DebugIOFlat();
DebugFreeMemory(this);
}