tge/engine/console/telnetDebugger.cc
2025-02-17 23:17:30 -06:00

823 lines
23 KiB
C++
Executable File

//-----------------------------------------------------------------------------
// Torque Game Engine
// Copyright (C) GarageGames.com, Inc.
//-----------------------------------------------------------------------------
#include "platform/platform.h"
#include "console/console.h"
#include "console/telnetDebugger.h"
#include "platform/event.h"
#include "core/stringTable.h"
#include "console/consoleInternal.h"
#include "console/ast.h"
#include "console/compiler.h"
#include "platform/gameInterface.h"
//
// Enhanced TelnetDebugger for Torsion
// http://www.sickheadgames.com/torsion
//
//
// Debugger commands:
//
// CEVAL console line - evaluate the console line
// output: none
//
// BRKVARSET varName passct expr - NOT IMPLEMENTED!
// output: none
//
// BRKVARCLR varName - NOT IMPLEMENTED!
// output: none
//
// BRKSET file line clear passct expr - set a breakpoint on the file,line
// it must pass passct times for it to break and if clear is true, it
// clears when hit
// output:
//
// BRKNEXT - stop execution at the next breakable line.
// output: none
//
// BRKCLR file line - clear a breakpoint on the file,line
// output: none
//
// BRKCLRALL - clear all breakpoints
// output: none
//
// CONTINUE - continue execution
// output: RUNNING
//
// STEPIN - run until next statement
// output: RUNNING
//
// STEPOVER - run until next break <= current frame
// output: RUNNING
//
// STEPOUT - run until next break <= current frame - 1
// output: RUNNING
//
// EVAL tag frame expr - evaluate the expr in the console, on the frame'th stack frame
// output: EVALOUT tag exprResult
//
// FILELIST - list script files loaded
// output: FILELISTOUT file1 file2 file3 file4 ...
//
// BREAKLIST file - get a list of breakpoint-able lines in the file
// output: BREAKLISTOUT file skipBreakPairs skiplinecount breaklinecount skiplinecount breaklinecount ...
//
//
// Other output:
//
// BREAK file1 line1 function1 file2 line2 function2 ... - Sent when the debugger hits a
// breakpoint. It lists out one file/line/function triplet for each stack level.
// The first one is the top of the stack.
//
// COUT console-output - echo of console output from engine
//
// BRKMOV file line newline - sent when a breakpoint is moved to a breakable line.
//
// BRKCLR file line - sent when a breakpoint cannot be moved to a breakable line on the client.
//
ConsoleFunction( dbgSetParameters, void, 3, 4, "(int port, string password, bool waitForClient)"
"Open a debug server port on the specified port, requiring the specified password, "
"and optionally waiting for the debug client to connect.")
{
if (TelDebugger)
TelDebugger->setDebugParameters(dAtoi(argv[1]), argv[2], argc > 3 ? dAtob(argv[3]) : false );
}
static void debuggerConsumer(ConsoleLogEntry::Level level, const char *line)
{
level;
if (TelDebugger)
TelDebugger->processConsoleLine(line);
}
TelnetDebugger::TelnetDebugger()
{
Con::addConsumer(debuggerConsumer);
mAcceptPort = -1;
mAcceptSocket = InvalidSocket;
mDebugSocket = InvalidSocket;
mState = NotConnected;
mCurPos = 0;
mBreakpoints = NULL;
mBreakOnNextStatement = false;
mStackPopBreakIndex = -1;
mProgramPaused = false;
mWaitForClient = false;
// Add the version number in a global so that
// scripts can detect the presence of the
// "enhanced" debugger features.
Con::evaluatef( "$dbgVersion = %d;", Version );
}
TelnetDebugger::Breakpoint **TelnetDebugger::findBreakpoint(StringTableEntry fileName, S32 lineNumber)
{
Breakpoint **walk = &mBreakpoints;
Breakpoint *cur;
while((cur = *walk) != NULL)
{
if(cur->fileName == fileName && cur->lineNumber == U32(lineNumber))
return walk;
walk = &cur->next;
}
return NULL;
}
TelnetDebugger::~TelnetDebugger()
{
Con::removeConsumer(debuggerConsumer);
if(mAcceptSocket != InvalidSocket)
Net::closeSocket(mAcceptSocket);
if(mDebugSocket != InvalidSocket)
Net::closeSocket(mDebugSocket);
}
TelnetDebugger *TelDebugger = NULL;
void TelnetDebugger::create()
{
TelDebugger = new TelnetDebugger;
}
void TelnetDebugger::destroy()
{
delete TelDebugger;
TelDebugger = NULL;
}
void TelnetDebugger::send(const char *str)
{
Net::send(mDebugSocket, (const unsigned char*)str, dStrlen(str));
}
void TelnetDebugger::setDebugParameters(S32 port, const char *password, bool waitForClient)
{
// Don't bail if same port... we might just be wanting to change
// the password.
// if(port == mAcceptPort)
// return;
if(mAcceptSocket != InvalidSocket)
{
Net::closeSocket(mAcceptSocket);
mAcceptSocket = InvalidSocket;
}
mAcceptPort = port;
if(mAcceptPort != -1 && mAcceptPort != 0)
{
mAcceptSocket = Net::openSocket();
Net::bind(mAcceptSocket, mAcceptPort);
Net::listen(mAcceptSocket, 4);
Net::setBlocking(mAcceptSocket, false);
}
dStrncpy(mDebuggerPassword, password, PasswordMaxLength);
mWaitForClient = waitForClient;
if ( !mWaitForClient )
return;
// Wait for the client to fully connect.
while ( mState != Connected )
{
Platform::sleep(10);
process();
}
}
void TelnetDebugger::processConsoleLine(const char *consoleLine)
{
if(mState != NotConnected)
{
send("COUT ");
send(consoleLine);
send("\r\n");
}
}
void TelnetDebugger::process()
{
NetAddress address;
if(mAcceptSocket != InvalidSocket)
{
// ok, see if we have any new connections:
NetSocket newConnection;
newConnection = Net::accept(mAcceptSocket, &address);
if(newConnection != InvalidSocket && mDebugSocket == InvalidSocket)
{
Con::printf ("Debugger connection from %i.%i.%i.%i",
address.netNum[0], address.netNum[1], address.netNum[2], address.netNum[3]);
mState = PasswordTry;
mDebugSocket = newConnection;
Net::setBlocking(newConnection, false);
}
else if(newConnection != InvalidSocket)
Net::closeSocket(newConnection);
}
// see if we have any input to process...
if(mDebugSocket == InvalidSocket)
return;
checkDebugRecv();
if(mDebugSocket == InvalidSocket)
removeAllBreakpoints();
}
void TelnetDebugger::checkDebugRecv()
{
for (;;)
{
// Process all the complete commands in the buffer.
while ( mCurPos > 0 )
{
// Remove leading whitespace.
while ( mCurPos > 0 && ( mLineBuffer[0] == 0 || mLineBuffer[0] == '\r' || mLineBuffer[0] == '\n' ) )
{
mCurPos--;
dMemmove(mLineBuffer, mLineBuffer + 1, mCurPos);
}
// Look for a complete command.
bool gotCmd = false;
for(S32 i = 0; i < mCurPos; i++)
{
if( mLineBuffer[i] == 0 )
mLineBuffer[i] = '_';
else if ( mLineBuffer[i] == '\r' || mLineBuffer[i] == '\n' )
{
// Send this command to be processed.
mLineBuffer[i] = '\n';
processLineBuffer(i+1);
// Remove the command from the buffer.
mCurPos -= i + 1;
dMemmove(mLineBuffer, mLineBuffer + i + 1, mCurPos);
gotCmd = true;
break;
}
}
// If we didn't find a command in this pass
// then we have an incomplete buffer.
if ( !gotCmd )
break;
}
// found no <CR> or <LF>
if(mCurPos == MaxCommandSize) // this shouldn't happen
{
Net::closeSocket(mDebugSocket);
mDebugSocket = InvalidSocket;
mState = NotConnected;
return;
}
S32 numBytes;
Net::Error err = Net::recv(mDebugSocket, (unsigned char*)(mLineBuffer + mCurPos), MaxCommandSize - mCurPos, &numBytes);
if((err != Net::NoError && err != Net::WouldBlock) || numBytes == 0)
{
Net::closeSocket(mDebugSocket);
mDebugSocket = InvalidSocket;
mState = NotConnected;
return;
}
if(err == Net::WouldBlock)
return;
mCurPos += numBytes;
}
}
void TelnetDebugger::executionStopped(CodeBlock *code, U32 lineNumber)
{
if(mProgramPaused)
return;
if(mBreakOnNextStatement)
{
setBreakOnNextStatement( false );
breakProcess();
return;
}
Breakpoint **bp = findBreakpoint(code->name, lineNumber);
if(!bp)
return;
Breakpoint *brk = *bp;
mProgramPaused = true;
Con::evaluatef("$Debug::result = %s;", brk->testExpression);
if(Con::getBoolVariable("$Debug::result"))
{
brk->curCount++;
if(brk->curCount >= brk->passCount)
{
brk->curCount = 0;
if(brk->clearOnHit)
removeBreakpoint(code->name, lineNumber);
breakProcess();
}
}
mProgramPaused = false;
}
void TelnetDebugger::pushStackFrame()
{
if(mState == NotConnected)
return;
if(mBreakOnNextStatement && mStackPopBreakIndex > -1 &&
gEvalState.stack.size() > mStackPopBreakIndex)
setBreakOnNextStatement( false );
}
void TelnetDebugger::popStackFrame()
{
if(mState == NotConnected)
return;
if(mStackPopBreakIndex > -1 && gEvalState.stack.size()-1 <= mStackPopBreakIndex)
setBreakOnNextStatement( true );
}
void TelnetDebugger::breakProcess()
{
mProgramPaused = true;
// echo out the break
send("BREAK");
char buffer[MaxCommandSize];
char scope[MaxCommandSize];
for(S32 i = (S32) gEvalState.stack.size() - 1; i >= 0; i--)
{
CodeBlock *code = gEvalState.stack[i]->code;
const char *file = code->name;
if ((! file) || (! file[0]))
file = "<none>";
Namespace *ns = gEvalState.stack[i]->scopeNamespace;
scope[0] = 0;
if ( ns ) {
if ( ns->mParent && ns->mParent->mPackage && ns->mParent->mPackage[0] ) {
dStrcat( scope, ns->mParent->mPackage );
dStrcat( scope, "::" );
}
if ( ns->mName && ns->mName[0] ) {
dStrcat( scope, ns->mName );
dStrcat( scope, "::" );
}
}
const char *function = gEvalState.stack[i]->scopeName;
if ((!function) || (!function[0]))
function = "<none>";
dStrcat( scope, function );
U32 line, inst;
U32 ip = gEvalState.stack[i]->ip;
code->findBreakLine(ip, line, inst);
dSprintf(buffer, MaxCommandSize, " %s %d %s", file, line, scope);
send(buffer);
}
send("\r\n");
while(mProgramPaused)
{
Platform::sleep(10);
checkDebugRecv();
if(mDebugSocket == InvalidSocket)
{
mProgramPaused = false;
removeAllBreakpoints();
debugContinue();
return;
}
}
}
void TelnetDebugger::processLineBuffer(S32 cmdLen)
{
if (mState == PasswordTry)
{
if(dStrncmp(mLineBuffer, mDebuggerPassword, cmdLen-1))
{
// failed password:
send("PASS WrongPassword.\r\n");
Net::closeSocket(mDebugSocket);
mDebugSocket = InvalidSocket;
mState = NotConnected;
}
else
{
send("PASS Connected.\r\n");
mState = mWaitForClient ? Initialize : Connected;
}
return;
}
else
{
char evalBuffer[MaxCommandSize];
char varBuffer[MaxCommandSize];
char fileBuffer[MaxCommandSize];
char clear[MaxCommandSize];
S32 passCount, line, frame;
if(dSscanf(mLineBuffer, "CEVAL %[^\n]", evalBuffer) == 1)
{
ConsoleEvent postEvent;
dStrcpy(postEvent.data, evalBuffer);
postEvent.size = ConsoleEventHeaderSize + dStrlen(evalBuffer) + 1;
Game->postEvent(postEvent);
}
else if(dSscanf(mLineBuffer, "BRKVARSET %s %d %[^\n]", varBuffer, &passCount, evalBuffer) == 3)
addVariableBreakpoint(varBuffer, passCount, evalBuffer);
else if(dSscanf(mLineBuffer, "BRKVARCLR %s", varBuffer) == 1)
removeVariableBreakpoint(varBuffer);
else if(dSscanf(mLineBuffer, "BRKSET %s %d %s %d %[^\n]", fileBuffer,&line,&clear,&passCount,evalBuffer) == 5)
addBreakpoint(fileBuffer, line, dAtob(clear), passCount, evalBuffer);
else if(dSscanf(mLineBuffer, "BRKCLR %s %d", fileBuffer, &line) == 2)
removeBreakpoint(fileBuffer, line);
else if(!dStrncmp(mLineBuffer, "BRKCLRALL\n", cmdLen))
removeAllBreakpoints();
else if(!dStrncmp(mLineBuffer, "BRKNEXT\n", cmdLen))
debugBreakNext();
else if(!dStrncmp(mLineBuffer, "CONTINUE\n", cmdLen))
debugContinue();
else if(!dStrncmp(mLineBuffer, "STEPIN\n", cmdLen))
debugStepIn();
else if(!dStrncmp(mLineBuffer, "STEPOVER\n", cmdLen))
debugStepOver();
else if(!dStrncmp(mLineBuffer, "STEPOUT\n", cmdLen))
debugStepOut();
else if(dSscanf(mLineBuffer, "EVAL %s %d %[^\n]", varBuffer, &frame, evalBuffer) == 3)
evaluateExpression(varBuffer, frame, evalBuffer);
else if(!dStrncmp(mLineBuffer, "FILELIST\n", cmdLen))
dumpFileList();
else if(dSscanf(mLineBuffer, "BREAKLIST %s", fileBuffer) == 1)
dumpBreakableList(fileBuffer);
else
{
// invalid stuff.
send("DBGERR Invalid command!\r\n");
}
}
}
void TelnetDebugger::addVariableBreakpoint(const char*, S32, const char*)
{
send("addVariableBreakpoint\r\n");
}
void TelnetDebugger::removeVariableBreakpoint(const char*)
{
send("removeVariableBreakpoint\r\n");
}
void TelnetDebugger::addAllBreakpoints(CodeBlock *code)
{
if(mState == NotConnected)
return;
// Find the breakpoints for this code block and attach them.
Breakpoint **walk = &mBreakpoints;
Breakpoint *cur;
while((cur = *walk) != NULL)
{
if(cur->fileName == code->name)
{
cur->code = code;
// Find the fist breakline starting from and
// including the requested breakline.
S32 newLine = code->findFirstBreakLine(cur->lineNumber);
if (newLine <= 0)
{
walk = &cur->next;
char buffer[MaxCommandSize];
dSprintf(buffer, MaxCommandSize, "BRKCLR %s %d\r\n", cur->fileName, cur->lineNumber);
send(buffer);
removeBreakpoint(cur->fileName, cur->lineNumber);
continue;
}
// If the requested breakline does not match
// the actual break line we need to inform
// the client.
if (newLine != cur->lineNumber)
{
char buffer[MaxCommandSize];
// If we already have a line at this breapoint then
// tell the client to clear the breakpoint.
if ( findBreakpoint(cur->fileName, newLine) ) {
walk = &cur->next;
dSprintf(buffer, MaxCommandSize, "BRKCLR %s %d\r\n", cur->fileName, cur->lineNumber);
send(buffer);
removeBreakpoint(cur->fileName, cur->lineNumber);
continue;
}
// We're moving the breakpoint to new line... inform the
// client so it can update it's view.
dSprintf(buffer, MaxCommandSize, "BRKMOV %s %d %d\r\n", cur->fileName, cur->lineNumber, newLine);
send(buffer);
cur->lineNumber = newLine;
}
code->setBreakpoint(cur->lineNumber);
}
walk = &cur->next;
}
// Enable all breaks if a break next was set.
if (mBreakOnNextStatement)
code->setAllBreaks();
}
void TelnetDebugger::addBreakpoint(const char *fileName, S32 line, bool clear, S32 passCount, const char *evalString)
{
fileName = StringTable->insert(fileName);
Breakpoint **bp = findBreakpoint(fileName, line);
if(bp)
{
// trying to add the same breakpoint...
Breakpoint *brk = *bp;
dFree(brk->testExpression);
brk->testExpression = dStrdup(evalString);
brk->passCount = passCount;
brk->clearOnHit = clear;
brk->curCount = 0;
}
else
{
// Note that if the code block is not already
// loaded it is handled by addAllBreakpoints.
CodeBlock* code = CodeBlock::find(fileName);
if (code)
{
// Find the fist breakline starting from and
// including the requested breakline.
S32 newLine = code->findFirstBreakLine(line);
if (newLine <= 0)
{
char buffer[MaxCommandSize];
dSprintf(buffer, MaxCommandSize, "BRKCLR %s %d\r\n", fileName, line);
send(buffer);
return;
}
// If the requested breakline does not match
// the actual break line we need to inform
// the client.
if (newLine != line)
{
char buffer[MaxCommandSize];
// If we already have a line at this breapoint then
// tell the client to clear the breakpoint.
if ( findBreakpoint(fileName, newLine) ) {
dSprintf(buffer, MaxCommandSize, "BRKCLR %s %d\r\n", fileName, line);
send(buffer);
return;
}
// We're moving the breakpoint to new line... inform the client.
dSprintf(buffer, MaxCommandSize, "BRKMOV %s %d %d\r\n", fileName, line, newLine);
send(buffer);
line = newLine;
}
code->setBreakpoint(line);
}
Breakpoint *brk = new Breakpoint;
brk->code = code;
brk->fileName = fileName;
brk->lineNumber = line;
brk->passCount = passCount;
brk->clearOnHit = clear;
brk->curCount = 0;
brk->testExpression = dStrdup(evalString);
brk->next = mBreakpoints;
mBreakpoints = brk;
}
}
void TelnetDebugger::removeBreakpointsFromCode(CodeBlock *code)
{
Breakpoint **walk = &mBreakpoints;
Breakpoint *cur;
while((cur = *walk) != NULL)
{
if(cur->code == code)
{
dFree(cur->testExpression);
*walk = cur->next;
delete walk;
}
else
walk = &cur->next;
}
}
void TelnetDebugger::removeBreakpoint(const char *fileName, S32 line)
{
fileName = StringTable->insert(fileName);
Breakpoint **bp = findBreakpoint(fileName, line);
if(bp)
{
Breakpoint *brk = *bp;
*bp = brk->next;
if ( brk->code )
brk->code->clearBreakpoint(brk->lineNumber);
dFree(brk->testExpression);
delete brk;
}
}
void TelnetDebugger::removeAllBreakpoints()
{
Breakpoint *walk = mBreakpoints;
while(walk)
{
Breakpoint *temp = walk->next;
if ( walk->code )
walk->code->clearBreakpoint(walk->lineNumber);
dFree(walk->testExpression);
delete walk;
walk = temp;
}
mBreakpoints = NULL;
}
void TelnetDebugger::debugContinue()
{
if (mState == Initialize) {
mState = Connected;
return;
}
setBreakOnNextStatement( false );
mStackPopBreakIndex = -1;
mProgramPaused = false;
send("RUNNING\r\n");
}
void TelnetDebugger::setBreakOnNextStatement( bool enabled )
{
if ( enabled )
{
// Apply breaks on all the code blocks.
for(CodeBlock *walk = CodeBlock::getCodeBlockList(); walk; walk = walk->nextFile)
walk->setAllBreaks();
mBreakOnNextStatement = true;
}
else if ( !enabled )
{
// Clear all the breaks on the codeblocks
// then go reapply the breakpoints.
for(CodeBlock *walk = CodeBlock::getCodeBlockList(); walk; walk = walk->nextFile)
walk->clearAllBreaks();
for(Breakpoint *w = mBreakpoints; w; w = w->next)
{
if ( w->code )
w->code->setBreakpoint(w->lineNumber);
}
mBreakOnNextStatement = false;
}
}
void TelnetDebugger::debugBreakNext()
{
if (mState != Connected)
return;
if ( !mProgramPaused )
setBreakOnNextStatement( true );
}
void TelnetDebugger::debugStepIn()
{
// Note that step in is allowed during
// the initialize state, so that we can
// break on the first script line executed.
setBreakOnNextStatement( true );
mStackPopBreakIndex = -1;
mProgramPaused = false;
// Don't bother sending this to the client
// if it's in the initialize state. It will
// just be ignored as the client knows it
// is in a running state when it connects.
if (mState != Initialize)
send("RUNNING\r\n");
else
mState = Connected;
}
void TelnetDebugger::debugStepOver()
{
if (mState != Connected)
return;
setBreakOnNextStatement( true );
mStackPopBreakIndex = gEvalState.stack.size();
mProgramPaused = false;
send("RUNNING\r\n");
}
void TelnetDebugger::debugStepOut()
{
if (mState != Connected)
return;
setBreakOnNextStatement( false );
mStackPopBreakIndex = gEvalState.stack.size() - 1;
if ( mStackPopBreakIndex == 0 )
mStackPopBreakIndex = -1;
mProgramPaused = false;
send("RUNNING\r\n");
}
void TelnetDebugger::evaluateExpression(const char *tag, S32 frame, const char *evalBuffer)
{
// Make sure we're passing a valid frame to the eval.
if ( frame > gEvalState.stack.size() )
frame = gEvalState.stack.size() - 1;
if ( frame < 0 )
frame = 0;
// Gotta have a statement to execute.
char buffer[MaxCommandSize];
dSprintf(buffer, MaxCommandSize, "$Debug::result = %s;", evalBuffer);
CodeBlock *newCodeBlock = new CodeBlock();
const char *result = newCodeBlock->compileExec(NULL, buffer, false, frame);
dSprintf(buffer, MaxCommandSize, "EVALOUT %s %s\r\n", tag, result[0] ? result : "\"\"");
send(buffer);
}
void TelnetDebugger::dumpFileList()
{
send("FILELISTOUT ");
for(CodeBlock *walk = CodeBlock::getCodeBlockList(); walk; walk = walk->nextFile)
{
send(walk->name);
if(walk->nextFile)
send(" ");
}
send("\r\n");
}
void TelnetDebugger::dumpBreakableList(const char *fileName)
{
fileName = StringTable->insert(fileName);
CodeBlock *file = CodeBlock::find(fileName);
char buffer[MaxCommandSize];
if(file)
{
dSprintf(buffer, MaxCommandSize, "BREAKLISTOUT %s %d", fileName, file->breakListSize >> 1);
send(buffer);
for(U32 i = 0; i < file->breakListSize; i += 2)
{
dSprintf(buffer, MaxCommandSize, " %d %d", file->breakList[i], file->breakList[i+1]);
send(buffer);
}
send("\r\n");
}
else
send("DBGERR No Such file!");
}