diff --git a/commands.lua b/commands.lua new file mode 100644 index 0000000..e943357 --- /dev/null +++ b/commands.lua @@ -0,0 +1,608 @@ +local _p = emu.log +local function print(data) + _p(data .. "") +end + +return function(server) + local me = {} + + local function responseCheckpointInfo(requestId, checkpt, hit) + local r = {} + + r[#r+1] = server.uint32ToLittleEndian(checkpt.num) + r[#r+1] = server.boolToLittleEndian(hit) + + r[#r+1] = server.uint16ToLittleEndian(checkpt.start) + r[#r+1] = server.uint16ToLittleEndian(checkpt.finish) + r[#r+1] = server.boolToLittleEndian(checkpt.stop) + r[#r+1] = server.boolToLittleEndian(checkpt.enabled) + r[#r+1] = server.uint8ToLittleEndian(checkpt.op) + r[#r+1] = server.boolToLittleEndian(checkpt.temp) + + r[#r+1] = server.uint32ToLittleEndian(checkpt.hitCount) + r[#r+1] = server.uint32ToLittleEndian(checkpt.ignoreCount) + r[#r+1] = server.boolToLittleEndian(checkpt.condition ~= nil) + r[#r+1] = server.uint32ToLittleEndian(checkpt.memspace) + + server.response(server.responseType.CHECKPOINT_INFO, server.errorType.OK, requestId, table.concat(r)) + end + + local regMeta = { + a = { name = "A", id = 0, size = 1 }, + x = { name = "X", id = 1, size = 1 }, + y = { name = "Y", id = 2, size = 1 }, + pc = { name = "PC", id = 3, size = 2 }, + sp = { name = "SP", id = 4, size = 1 }, + status = { name = "FL", id = 5, size = 1 }, + } + + local function responseRegisterInfo(requestId) + if requestId == nil then + error("request id was nil", 2) + end + + local r = {} + local regs = emu.getState().cpu + + local count = 0 + for name, meta in pairs(regMeta) do + count = count + 1 + end + + r[#r+1] = server.uint16ToLittleEndian(count) + + local itemSize = 3 + for name, meta in pairs(regMeta) do + r[#r+1] = server.uint8ToLittleEndian(itemSize) + r[#r+1] = server.uint8ToLittleEndian(meta.id) + r[#r+1] = server.uint16ToLittleEndian(regs[name]) + end + + server.response(server.responseType.REGISTER_INFO, server.errorType.OK, requestId, table.concat(r)) + end + + local function responseStopped() + local pc = emu.getState().cpu.pc + + server.response(server.responseType.STOPPED, server.errorType.OK, server.EVENT_ID, server.uint16ToLittleEndian(pc)) + end + + local function responseResumed() + local pc = emu.getState().cpu.pc + + server.response(server.responseType.RESUMED, server.errorType.OK, server.EVENT_ID, server.uint16ToLittleEndian(pc)) + end + + function me.monitorOpened() + print("Monitor opened") + responseRegisterInfo(server.EVENT_ID) + responseStopped(server.EVENT_ID) + end + + function me.monitorClosed() + print("Monitor closed") + responseRegisterInfo(server.EVENT_ID) + responseResumed(server.EVENT_ID) + end + + local function processAdvanceInstructions(command) + if command.length < 3 then + server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) + return + end + + local stepOverSubroutines = server.readBool(command.body, 1) + local count = server.readUint16(command.body, 2) + + server.stepping = true + if stepOverSubroutines then + -- FIXME + emu.execute(count, emu.executeCountType.cpuInstructions) + else + emu.execute(count, emu.executeCountType.cpuInstructions) + end + + server.running = true + + server.response(server.responseType.ADVANCE_INSTRUCTIONS, server.errorType.OK, command.requestId, nil) + + me.monitorClosed() + end + + local function processPing(command) + server.response(server.responseType.PING, server.errorType.OK, command.requestId, nil) + end + + local function processExit(command) + server.running = true + + server.response(server.responseType.EXIT, server.errorType.OK, command.requestId, nil) + + me.monitorClosed() + end + + local function processReset(command) + server.running = true + emu.reset() + + server.response(server.responseType.RESET, server.errorType.OK, command.requestId, nil) + + me.monitorClosed() + end + + local memspaces = { + MAIN = 0, + INVALID = -1, + } + + local function getRequestedMemspace(requestedMemspace) + if requestedMemspace == 0 then + return memspaces.MAIN + else + return memspaces.INVALID + end + end + + local banks = { + default = 0, + cpu = 1, + ppu = 2, + palette = 3, + oam = 4, + secondaryOam = 5 + } + + local function validateBanknum(memspace, banknum) + return memspace == memspaces.MAIN and banknum >= banks.default and banknum <= banks.secondaryOam + end + + local function processMemorySet(command) + local newSidefx = server.readBool(command.body, 1) + + local startAddress = server.readUint16(command.body, 2) + local endAddress = server.readUint16(command.body, 4) + + if startAddress > endAddress then + server.errorResponse(server.errorType.INVALID_PARAMETER, command.requestId) + return + end + + local length = endAddress - startAddress + 1 + + if command.length < length + 8 then + server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) + return + end + + local requestedMemspace = server.readUint8(command.body, 6) + local requestedBanknum = server.readUint16(command.body, 7) + + local memspace = getRequestedMemspace(requestedMemspace) + + if memspace == memspaces.INVALID then + server.errorResponse(server.errorType.INVALID_MEMSPACE, command.requestId) + return + end + + if not validateBanknum(memspace, requestedBanknum) then + server.errorResponse(server.errorType.INVALID_PARAMETER, command.requestId) + return + end + + local banknum = requestedBanknum + + if banknum == banks.default then + banknum = banks.cpu + end + + if not newSidefx and ( banknum == banks.cpu or banknum == banks.ppu ) then + banknum = banknum + 0x100 + end + + banknum = banknum - 1 + + for i = 0,length - 1,1 do + local val = command.body:byte(8 + i + 1) + print(val) + emu.write(startAddress + i, val, banknum) + end + + server.response(server.responseType.MEM_SET, server.errorType.OK, command.requestId, nil) + end + + local function processMemoryGet(command) + if command.length < 8 then + server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) + return + end + + local newSidefx = server.readBool(command.body, 1) + + local startAddress = server.readUint16(command.body, 2) + local endAddress = server.readUint16(command.body, 4) + + if startAddress > endAddress then + server.errorResponse(server.errorType.INVALID_PARAMETER, command.requestId) + return + end + + local requestedMemspace = server.readUint8(command.body, 6) + local memspace = getRequestedMemspace(requestedMemspace) + + if memspace == memspaces.INVALID then + server.errorResponse(server.errorType.INVALID_MEMSPACE, command.requestId) + return + end + + local length = endAddress - startAddress + 1 + + local requestedBanknum = server.readUint16(command.body, 7) + + if not validateBanknum(memspace, requestedBanknum) then + server.errorResponse(server.errorType.INVALID_PARAMETER, command.requestId) + return + end + + local banknum = requestedBanknum + + local r = {} + + r[#r+1] = server.uint16ToLittleEndian(length) + + if banknum == banks.default then + banknum = banks.cpu + end + + if not newSidefx and ( banknum == banks.cpu or banknum == banks.ppu ) then + banknum = banknum + 0x100 + end + + banknum = banknum - 1 + + local remainingByte = length % 2 ~= 0 + for addr=startAddress,endAddress-1,2 do + r[#r+1] = server.uint16ToLittleEndian(emu.readWord(addr, banknum)) + end + if remainingByte then + r[#r+1] = server.uint8ToLittleEndian(emu.read(addr, banknum)) + end + + server.response(server.responseType.MEM_GET, server.errorType.OK, command.requestId, table.concat(r)) + end + + nextTrap = 1 + traps = {} + local operation = { + READ = 0x01, + WRITE = 0x02, + EXEC = 0x04, + } + + local trapHandle + + local function addCheckpoint(start, finish, stop, enabled, op, temp) + local num = nextTrap + nextTrap = nextTrap+1 + local trap = { + start = start, + finish = finish, + stop = stop, + enabled = enabled, + op = op, + temp = temp, + hitCount = 0, + ignoreCount = 0, + condition = nil, + memspace = 0, + num = num, + } + traps[#traps+1] = trap + + if trap.op & operation.EXEC ~= 0 then + trap.execRegistration = emu.addMemoryCallback(function() + if trap.enabled then + trapHandle(trap) + end + end, emu.memCallbackType.cpuExec, trap.start, trap.finish) + end + if trap.op & operation.READ ~= 0 then + trap.readRegistration = emu.addMemoryCallback(function() + if trap.enabled then + trapHandle(trap) + end + end, emu.memCallbackType.cpuRead, trap.start, trap.finish) + end + if trap.op & operation.WRITE ~= 0 then + trap.writeRegistration = emu.addMemoryCallback(function() + if trap.enabled then + trapHandle(trap) + end + end, emu.memCallbackType.cpuRead, trap.start, trap.finish) + end + return trap + end + + local function removeCheckpoint(num) + local trap + for i=#traps,1,-1 do + trap = traps[i] + if trap.num == num then + table.remove(traps, i) + break + end + trap = nil + end + + if trap == nil then + return false + end + + if trap.execRegistration ~= nil then + emu.removeMemoryCallback(trap.execRegistration, emu.memCallbackType.cpuExec, trap.start, trap.finish) + end + if trap.readRegistration ~= nil then + emu.removeMemoryCallback(trap.readRegistration, emu.memCallbackType.cpuRead, trap.start, trap.finish) + end + if trap.writeRegistration ~= nil then + emu.removeMemoryCallback(trap.writeRegistration, emu.memCallbackType.cpuWrite, trap.start, trap.finish) + end + + return true + end + + local function toggleCheckpoint(enable, num) + for i=#traps,1,-1 do + trap = traps[i] + if trap.num == num then + break + end + trap = nil + end + + if trap == nil then + return false + end + + trap.enabled = enable + + return true + end + + local function processCheckpointGet(command) + if command.length < 4 then + server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) + return + end + + local num = server.readUint32(command.body, 1) + + for i=#traps,1,-1 do + trap = traps[i] + if trap.num == num then + break + end + trap = nil + end + + if not trap then + server.errorResponse(server.errorType.OBJECT_MISSING, command.requestId) + return + end + + responseCheckpointInfo(command.requestId, trap, false) + end + + local function processCheckpointSet(command) + if command.length < 8 then + server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) + return + end + + -- Ignore the memspace - byte 9 + local checkpt = addCheckpoint( + server.readUint16(command.body, 1), + server.readUint16(command.body, 3), + server.readBool(command.body, 5), + server.readBool(command.body, 6), + server.readUint8(command.body, 7), + server.readBool(command.body, 8) + ) + + responseCheckpointInfo(command.requestId, checkpt, 0) + end + + local function processCheckpointDelete(command) + if command.length < 4 then + server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) + return + end + + local brkNum = server.readUint32(command.body, 1) + local success = removeCheckpoint(brkNum) + + if not success then + server.errorResponse(server.errorType.OBJECT_MISSING, command.requestId) + return + end + + server.response(server.responseType.CHECKPOINT_DELETE, server.errorType.OK, command.requestId, nil) + end + + local function processCheckpointList(command) + for i = 1,#traps,1 do + responseCheckpointInfo(command.requestId, traps[i], false) + end + + local r = server.uint32ToLittleEndian(#traps) + + server.response(server.responseType.CHECKPOINT_LIST, server.errorType.OK, command.requestId, r) + end + + local function processCheckpointToggle(command) + if command.length < 5 then + server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) + return + end + + local num = server.readUint32(command.body, 1) + local enable = server.readBool(command.body, 5) + + if not toggleCheckpoint(enable, num) then + server.errorResponse(server.errorType.OBJECT_MISSING, command.requestId) + return + end + + server.response(server.responseType.CHECKPOINT_TOGGLE, server.errorType.OK, command.requestId, nil) + end + + local function validateRegister(memspace, regId) + return memspace == memspaces.MAIN and regId >= regMeta.a.id and regId <= regMeta.status.id + end + + local function processRegistersGet(command) + if command.length < 1 then + server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) + return + end + + local requestedMemspace = server.readUint8(command.body, 1) + local memspace = getRequestedMemspace(requestedMemspace) + + if memspace == memspaces.INVALID then + server.errorResponse(server.errorType.INVALID_MEMSPACE, command.requestId) + return + end + + responseRegisterInfo(command.requestId, memspace) + end + + local function processRegistersSet(command) + local headerSize = 3 + local count = server.readUint16(command.body, 2) + + if command.length < headerSize + count * (3 + 1) then + server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) + return + end + + local requestedMemspace = server.readUint8(command.body, 1) + local memspace = getRequestedMemspace(requestedMemspace) + + if memspace == memspaces.INVALID then + server.errorResponse(server.errorType.INVALID_MEMSPACE, command.requestId) + return + end + + local bodyCursor = headerSize + 1 + + local state = emu.getState() + for i=1,count do + local itemSize = server.readUint8(command.body, bodyCursor + 0) + local regId = server.readUint8(command.body, bodyCursor + 1) + local regVal = server.readUint16(command.body, bodyCursor + 2) + + if itemSize < 3 then + server.errorResponse(server.errorType.CMD_INVALID_LENGTH, command.requestId) + return + end + + if not validateRegister(memspace, regId) then + server.errorResponse(server.errorType.OBJECT_MISSING, command.requestId) + return + end + + for name, meta in pairs(regMeta) do + if meta.id == regId then + state.cpu[name] = regVal + break + end + end + + bodyCursor = bodyCursor + itemSize + 1 + end + + emu.setState(state) + + responseRegisterInfo(command.requestId, memspace) + end + + function me.processCommand(apiVersion, bodyLength, remainingHeader, body) + local command = {} + command.apiVersion = apiVersion + if command.apiVersion < 0x01 or command.apiVersion > 0x02 then + server.errorResponse(server.errorType.INVALID_API_VERSION, command.requestId) + end + + command.length = bodyLength + + command.requestId = server.readUint32(remainingHeader, 1) + command.type = server.readUint8(remainingHeader, 5) + command.body = body + + print(string.format("Command start: %02x", command.type)) + + local ct = command.type + + if ct == server.commandType.MEM_GET then + processMemoryGet(command) + elseif ct == server.commandType.MEM_SET then + processMemorySet(command) + + elseif ct == server.commandType.CHECKPOINT_GET then + processCheckpointGet(command) + elseif ct == server.commandType.CHECKPOINT_SET then + processCheckpointSet(command) + elseif ct == server.commandType.CHECKPOINT_DELETE then + processCheckpointDelete(command) + elseif ct == server.commandType.CHECKPOINT_LIST then + processCheckpointList(command) + elseif ct == server.commandType.CHECKPOINT_TOGGLE then + processCheckpointToggle(command) + + elseif ct == server.commandType.REGISTERS_GET then + processRegistersGet(command) + elseif ct == server.commandType.REGISTERS_SET then + processRegistersSet(command) + + elseif ct == server.commandType.ADVANCE_INSTRUCTIONS then + processAdvanceInstructions(command) + + elseif ct == server.commandType.PING then + processPing(command) + + elseif ct == server.commandType.EXIT then + processExit(command) + elseif ct == server.commandType.RESET then + processReset(command) + else + server.errorResponse(server.errorType.CMD_INVALID_TYPE, command.requestId) + print(string.format("unknown command: %d, skipping command length %d", command.type, command.length)) + end + + print(string.format("Command finished: %02x", command.type)) + end + + function trapHandle(trap) + print("Trap") + + if conn == nil then + return + end + + if trap.stop then + me.monitorOpened() + end + responseCheckpointInfo(requestId, trap, true) + if not trap.stop then + return + end + + server.running = false + + server.deregisterFrameCallback() + emu.breakExecution() + server.registerFrameCallback() + end + + return me +end \ No newline at end of file diff --git a/main.lua b/main.lua index 5ba8090..c26380d 100644 --- a/main.lua +++ b/main.lua @@ -3,881 +3,11 @@ local function print(data) _p(data .. "") end -local socket = require("socket.core") +local baseDir = os.getenv("MESEN_REMOTE_BASEDIR") -local running = true - -local regMeta = { - a = { name = "A", id = 0, size = 1 }, - x = { name = "X", id = 1, size = 1 }, - y = { name = "Y", id = 2, size = 1 }, - pc = { name = "PC", id = 3, size = 2 }, - sp = { name = "SP", id = 4, size = 1 }, - status = { name = "FL", id = 5, size = 1 }, -} +local server = dofile(baseDir.."/server.lua") local host = os.getenv("MESEN_REMOTE_HOST") or "localhost" local port = os.getenv("MESEN_REMOTE_PORT") or 9355 -print("Binding to host '" ..host.. "' and port " ..port.. "...") - -local server = assert(socket.tcp()) -assert(server:bind(host, port)) -assert(server:listen(32)) -server:settimeout(0) - -local i, p = server:getsockname() -assert(i, p) - -print("Waiting for connection on " .. i .. ":" .. p .. "...") -local conn = nil - -local function boolToLittleEndian(b) - local value = 0 - if b then - value = 1 - end - return string.char(value & 0xff) -end - -local function uint8ToLittleEndian(value) - return string.char(value & 0xff) -end - -local function uint16ToLittleEndian(value) - return string.char(value & 0xff, (value >> 8) & 0xff) -end - -local function uint32ToLittleEndian(value) - return string.char(value & 0xff, (value >> 8) & 0xff, (value >> 16) & 0xff, (value >> 24) & 0xff) -end - -local function readUint32(data, index) - return data:byte(index) + (data:byte(index + 1) << 8) + (data:byte(index + 2) << 16) + (data:byte(index + 3) << 24) -end - -local function readUint16(data, index) - return data:byte(index) + (data:byte(index + 1) << 8) -end - -local function readUint8(data, index) - return data:byte(index) -end - -local function readBool(data, index) - return data:byte(index) > 0 -end - -local errorType = { - OK = 0x00, - OBJECT_MISSING = 0x01, - INVALID_MEMSPACE = 0x02, - CMD_INVALID_LENGTH = 0x80, - INVALID_PARAMETER = 0x81, - CMD_INVALID_API_VERSION = 0x82, - CMD_INVALID_TYPE = 0x83, - CMD_FAILURE = 0x8f, -}; - -local commandType = { - INVALID = 0x00, - - MEM_GET = 0x01, - MEM_SET = 0x02, - - CHECKPOINT_GET = 0x11, - CHECKPOINT_SET = 0x12, - CHECKPOINT_DELETE = 0x13, - CHECKPOINT_LIST = 0x14, - CHECKPOINT_TOGGLE = 0x15, - - CONDITION_SET = 0x22, - - REGISTERS_GET = 0x31, - REGISTERS_SET = 0x32, - - DUMP = 0x41, - UNDUMP = 0x42, - - RESOURCE_GET = 0x51, - RESOURCE_SET = 0x52, - - ADVANCE_INSTRUCTIONS = 0x71, - KEYBOARD_FEED = 0x72, - EXECUTE_UNTIL_RETURN = 0x73, - - PING = 0x81, - BANKS_AVAILABLE = 0x82, - REGISTERS_AVAILABLE = 0x83, - DISPLAY_GET = 0x84, - VICE_INFO = 0x85, - - PALETTE_GET = 0x91, - - EXIT = 0xaa, - QUIT = 0xbb, - RESET = 0xcc, - AUTOSTART = 0xdd, -} - -local responseType = { - MEM_GET = 0x01, - MEM_SET = 0x02, - - CHECKPOINT_INFO = 0x11, - - CHECKPOINT_DELETE = 0x13, - CHECKPOINT_LIST = 0x14, - CHECKPOINT_TOGGLE = 0x15, - - CONDITION_SET = 0x22, - - REGISTER_INFO = 0x31, - - DUMP = 0x41, - UNDUMP = 0x42, - - RESOURCE_GET = 0x51, - RESOURCE_SET = 0x52, - - JAM = 0x61, - STOPPED = 0x62, - RESUMED = 0x63, - - ADVANCE_INSTRUCTIONS = 0x71, - KEYBOARD_FEED = 0x72, - EXECUTE_UNTIL_RETURN = 0x73, - - PING = 0x81, - BANKS_AVAILABLE = 0x82, - REGISTERS_AVAILABLE = 0x83, - DISPLAY_GET = 0x84, - VICE_INFO = 0x85, - - PALETTE_GET = 0x91, - - EXIT = 0xaa, - QUIT = 0xbb, - RESET = 0xcc, - AUTOSTART = 0xdd, -} - -local API_VERSION = 0x02 - -local EVENT_ID = 0xffffffff - -local function response(responseType, errorCode, requestId, body) - if conn == nil then - return - end - - local r = {} - r[#r+1] = string.char(0x02) - r[#r+1] = uint8ToLittleEndian(API_VERSION) - if body ~= nil then - r[#r+1] = uint32ToLittleEndian(body:len()) - else - r[#r+1] = uint32ToLittleEndian(0) - end - r[#r+1] = uint8ToLittleEndian(responseType) - r[#r+1] = uint8ToLittleEndian(errorCode) - r[#r+1] = uint32ToLittleEndian(requestId) - conn:send(table.concat(r)) - - if body ~= nil then - conn:send(body) - end -end - -local function responseCheckpointInfo(requestId, checkpt, hit) - local r = {} - - r[#r+1] = uint32ToLittleEndian(checkpt.num) - r[#r+1] = boolToLittleEndian(hit) - - r[#r+1] = uint16ToLittleEndian(checkpt.start) - r[#r+1] = uint16ToLittleEndian(checkpt.finish) - r[#r+1] = boolToLittleEndian(checkpt.stop) - r[#r+1] = boolToLittleEndian(checkpt.enabled) - r[#r+1] = uint8ToLittleEndian(checkpt.op) - r[#r+1] = boolToLittleEndian(checkpt.temp) - - r[#r+1] = uint32ToLittleEndian(checkpt.hitCount) - r[#r+1] = uint32ToLittleEndian(checkpt.ignoreCount) - r[#r+1] = boolToLittleEndian(checkpt.condition ~= nil) - r[#r+1] = uint32ToLittleEndian(checkpt.memspace) - - response(responseType.CHECKPOINT_INFO, errorType.OK, requestId, table.concat(r)) -end - -local function responseRegisterInfo(requestId) - local r = {} - local regs = emu.getState().cpu - - local count = 0 - for name, meta in pairs(regMeta) do - count = count + 1 - end - - r[#r+1] = uint16ToLittleEndian(count) - - local itemSize = 3 - for name, meta in pairs(regMeta) do - r[#r+1] = uint8ToLittleEndian(itemSize) - r[#r+1] = uint8ToLittleEndian(meta.id) - r[#r+1] = uint16ToLittleEndian(regs[name]) - end - - response(responseType.REGISTER_INFO, errorType.OK, requestId, table.concat(r)) -end - -local function responseStopped() - local pc = emu.getState().cpu.pc - - response(responseType.STOPPED, errorType.OK, EVENT_ID, uint16ToLittleEndian(pc)) -end - -local function responseResumed() - local pc = emu.getState().cpu.pc - - response(responseType.RESUMED, errorType.OK, EVENT_ID, uint16ToLittleEndian(pc)) -end - -local function monitorOpened() - print("Monitor opened") - responseRegisterInfo(EVENT_ID) - responseStopped(EVENT_ID) -end - -local function monitorClosed() - print("Monitor closed") - responseRegisterInfo(EVENT_ID) - responseResumed(EVENT_ID) -end - -local function errorResponse(errorCode, requestId) - response(0, errorCode, requestId, nil) -end - -local stepping = false -local function processAdvanceInstructions(command) - if command.length < 3 then - errorResponse(errorType.CMD_INVALID_LENGTH, command.requestId) - return - end - - local stepOverSubroutines = readBool(command.body, 1); - local count = readUint16(command.body, 2); - - stepping = true - if stepOverSubroutines then - -- FIXME - emu.execute(count, emu.executeCountType.cpuInstructions) - else - emu.execute(count, emu.executeCountType.cpuInstructions) - end - - running = true - - response(responseType.ADVANCE_INSTRUCTIONS, errorType.OK, command.requestId, nil); - - monitorClosed() -end - -local function processPing(command) - response(responseType.PING, errorType.OK, command.requestId, nil) -end - -local function processExit(command) - running = true - - response(responseType.EXIT, errorType.OK, command.requestId, nil) - - monitorClosed() -end - -local function processReset(command) - running = true - emu.reset() - - response(responseType.RESET, errorType.OK, command.requestId, nil) - - monitorClosed() -end - -local memspaces = { - MAIN = 0, - INVALID = -1, -} - -local function getRequestedMemspace(requestedMemspace) - if requestedMemspace == 0 then - return memspaces.MAIN - else - return memspaces.INVALID - end -end - -local banks = { - default = 0, - cpu = 1, - ppu = 2, - palette = 3, - oam = 4, - secondaryOam = 5 -} - -local function validateBanknum(memspace, banknum) - return memspace == memspaces.MAIN and banknum >= banks.default and banknum <= banks.secondaryOam -end - -local function processMemorySet(command) - local newSidefx = readBool(command.body, 1) - - local startAddress = readUint16(command.body, 2); - local endAddress = readUint16(command.body, 4); - - if startAddress > endAddress then - errorResponse(errorType.INVALID_PARAMETER, command.requestId) - return - end - - local length = endAddress - startAddress + 1; - - if command.length < length + 8 then - errorResponse(errorType.CMD_INVALID_LENGTH, command.requestId) - return - end - - local requestedMemspace = readUint8(command.body, 6); - local requestedBanknum = readUint16(command.body, 7); - - local memspace = getRequestedMemspace(requestedMemspace); - - if memspace == memspaces.INVALID then - errorResponse(errorType.INVALID_MEMSPACE, command.requestId) - return - end - - if not validateBanknum(memspace, requestedBanknum) then - errorResponse(errorType.INVALID_PARAMETER, command.requestId) - return - end - - local banknum = requestedBanknum; - - if banknum == banks.default then - banknum = banks.cpu - end - - if not newSidefx and ( banknum == banks.cpu or banknum == banks.ppu ) then - banknum = banknum + 0x100 - end - - banknum = banknum - 1 - - for i = 0,length - 1,1 do - local val = command.body:byte(8 + i + 1) - print(val) - emu.write(startAddress + i, val, banknum) - end - - response(responseType.MEM_SET, errorType.OK, command.requestId, nil); -end - -local function processMemoryGet(command) - if command.length < 8 then - errorResponse(errorType.CMD_INVALID_LENGTH, command.requestId) - return - end - - local newSidefx = readBool(command.body, 1) - - local startAddress = readUint16(command.body, 2); - local endAddress = readUint16(command.body, 4); - - if startAddress > endAddress then - errorResponse(errorType.INVALID_PARAMETER, command.requestId) - return - end - - local requestedMemspace = readUint8(command.body, 6); - local memspace = getRequestedMemspace(requestedMemspace); - - if memspace == memspaces.INVALID then - errorResponse(errorType.INVALID_MEMSPACE, command.requestId) - return - end - - local length = endAddress - startAddress + 1; - - local requestedBanknum = readUint16(command.body, 7); - - if not validateBanknum(memspace, requestedBanknum) then - errorResponse(errorType.INVALID_PARAMETER, command.requestId) - return - end - - local banknum = requestedBanknum; - - local r = {} - - r[#r+1] = uint16ToLittleEndian(length); - - if banknum == banks.default then - banknum = banks.cpu - end - - if not newSidefx and ( banknum == banks.cpu or banknum == banks.ppu ) then - banknum = banknum + 0x100 - end - - banknum = banknum - 1 - - local remainingByte = length % 2 ~= 0 - for addr=startAddress,endAddress-1,2 do - r[#r+1] = uint16ToLittleEndian(emu.readWord(addr, banknum)) - end - if remainingByte then - r[#r+1] = uint8ToLittleEndian(emu.read(addr, banknum)) - end - - response(responseType.MEM_GET, errorType.OK, command.requestId, table.concat(r)) -end - -nextTrap = 1 -traps = {} -local operation = { - READ = 0x01, - WRITE = 0x02, - EXEC = 0x04, -} - -local trapHandle - -local function addCheckpoint(start, finish, stop, enabled, op, temp) - local num = nextTrap - nextTrap = nextTrap+1 - local trap = { - start = start, - finish = finish, - stop = stop, - enabled = enabled, - op = op, - temp = temp, - hitCount = 0, - ignoreCount = 0, - condition = nil, - memspace = 0, - num = num, - } - traps[#traps+1] = trap - - if trap.op & operation.EXEC ~= 0 then - trap.execRegistration = emu.addMemoryCallback(function() - if trap.enabled then - trapHandle(trap) - end - end, emu.memCallbackType.cpuExec, trap.start, trap.finish) - end - if trap.op & operation.READ ~= 0 then - trap.readRegistration = emu.addMemoryCallback(function() - if trap.enabled then - trapHandle(trap) - end - end, emu.memCallbackType.cpuRead, trap.start, trap.finish) - end - if trap.op & operation.WRITE ~= 0 then - trap.writeRegistration = emu.addMemoryCallback(function() - if trap.enabled then - trapHandle(trap) - end - end, emu.memCallbackType.cpuRead, trap.start, trap.finish) - end - return trap -end - -local function removeCheckpoint(num) - local trap - for i=#traps,1,-1 do - trap = traps[i] - if trap.num == num then - table.remove(traps, i) - break - end - trap = nil - end - - if trap == nil then - return false - end - - if trap.execRegistration ~= nil then - emu.removeMemoryCallback(trap.execRegistration, emu.memCallbackType.cpuExec, trap.start, trap.finish) - end - if trap.readRegistration ~= nil then - emu.removeMemoryCallback(trap.readRegistration, emu.memCallbackType.cpuRead, trap.start, trap.finish) - end - if trap.writeRegistration ~= nil then - emu.removeMemoryCallback(trap.writeRegistration, emu.memCallbackType.cpuWrite, trap.start, trap.finish) - end - - return true -end - -local function toggleCheckpoint(enable, num) - for i=#traps,1,-1 do - trap = traps[i] - if trap.num == num then - break - end - trap = nil - end - - if trap == nil then - return false - end - - trap.enabled = enable - - return true -end - -local function processCheckpointGet(command) - if command.length < 4 then - errorResponse(errorType.CMD_INVALID_LENGTH, command.requestId) - return - end - - local num = readUint32(command.body, 1); - - for i=#traps,1,-1 do - trap = traps[i] - if trap.num == num then - break - end - trap = nil - end - - if not trap then - errorResponse(errorType.OBJECT_MISSING, command.requestId) - return - end - - responseCheckpointInfo(command.requestId, trap, false); -end - -local function processCheckpointSet(command) - if command.length < 8 then - errorResponse(errorType.CMD_INVALID_LENGTH, command.requestId) - return - end - - -- Ignore the memspace - byte 9 - local checkpt = addCheckpoint( - readUint16(command.body, 1), - readUint16(command.body, 3), - readBool(command.body, 5), - readBool(command.body, 6), - readUint8(command.body, 7), - readBool(command.body, 8) - ) - - responseCheckpointInfo(command.requestId, checkpt, 0) -end - -local function processCheckpointDelete(command) - if command.length < 4 then - errorResponse(errorType.CMD_INVALID_LENGTH, command.requestId) - return - end - - local brkNum = readUint32(command.body, 1) - local success = removeCheckpoint(brkNum) - - if not success then - errorResponse(errorType.OBJECT_MISSING, command.requestId) - return - end - - response(responseType.CHECKPOINT_DELETE, errorType.OK, command.requestId, nil) -end - -local function processCheckpointList(command) - for i = 1,#traps,1 do - responseCheckpointInfo(command.requestId, traps[i], false); - end - - local r = uint32ToLittleEndian(#traps) - - response(responseType.CHECKPOINT_LIST, errorType.OK, command.requestId, r); -end - -local function processCheckpointToggle(command) - if command.length < 5 then - errorResponse(errorType.CMD_INVALID_LENGTH, command.requestId) - return - end - - local num = readUint32(command.body, 1); - local enable = readBool(command.body, 5); - - if not toggleCheckpoint(enable, num) then - errorResponse(errorType.OBJECT_MISSING, command.requestId) - return - end - - response(responseType.CHECKPOINT_TOGGLE, errorType.OK, command.requestId, nil); -end - -local function validateRegister(memspace, regId) - return memspace == memspaces.MAIN and regId >= regMeta.a.id and regId <= regMeta.status.id -end - -local function processRegistersGet(command) - if command.length < 1 then - errorResponse(errorType.CMD_INVALID_LENGTH, command.requestId) - return - end - - local requestedMemspace = readUint8(command.body, 1); - local memspace = getRequestedMemspace(requestedMemspace); - - if memspace == memspaces.INVALID then - errorResponse(errorType.INVALID_MEMSPACE, command.requestId) - return - end - - responseRegisterInfo(command.requestId, memspace); -end - -local function processRegistersSet(command) - local headerSize = 3 - local count = readUint16(command.body, 2); - - if command.length < headerSize + count * (3 + 1) then - errorResponse(errorType.CMD_INVALID_LENGTH, command.requestId) - return - end - - local requestedMemspace = readUint8(command.body, 1); - local memspace = getRequestedMemspace(requestedMemspace); - - if memspace == memspaces.INVALID then - errorResponse(errorType.INVALID_MEMSPACE, command.requestId) - return - end - - local bodyCursor = headerSize + 1; - - local state = emu.getState() - for i=1,count do - local itemSize = readUint8(command.body, bodyCursor + 0) - local regId = readUint8(command.body, bodyCursor + 1) - local regVal = readUint16(command.body, bodyCursor + 2) - - if itemSize < 3 then - errorResponse(errorType.CMD_INVALID_LENGTH, command.requestId) - return - end - - if not validateRegister(memspace, regId) then - errorResponse(errorType.OBJECT_MISSING, command.requestId) - return - end - - for name, meta in pairs(regMeta) do - if meta.id == regId then - state.cpu[name] = regVal - break - end - end - - bodyCursor = bodyCursor + itemSize + 1; - end - - emu.setState(state) - - responseRegisterInfo(command.requestId, memspace); -end - -local function processCommand(apiVersion, bodyLength, remainingHeader, body) - local command = {} - command.apiVersion = apiVersion - if command.apiVersion < 0x01 or command.apiVersion > 0x02 then - errorResponse(errorType.INVALID_API_VERSION, command.requestId) - end - - command.length = bodyLength - - command.requestId = readUint32(remainingHeader, 1) - command.type = readUint8(remainingHeader, 5) - command.body = body - - print(string.format("Command start: %02x", command.type)) - - local ct = command.type - - if ct == commandType.MEM_GET then - processMemoryGet(command) - elseif ct == commandType.MEM_SET then - processMemorySet(command) - - elseif ct == commandType.CHECKPOINT_GET then - processCheckpointGet(command) - elseif ct == commandType.CHECKPOINT_SET then - processCheckpointSet(command) - elseif ct == commandType.CHECKPOINT_DELETE then - processCheckpointDelete(command) - elseif ct == commandType.CHECKPOINT_LIST then - processCheckpointList(command) - elseif ct == commandType.CHECKPOINT_TOGGLE then - processCheckpointToggle(command) - - elseif ct == commandType.REGISTERS_GET then - processRegistersGet(command) - elseif ct == commandType.REGISTERS_SET then - processRegistersSet(command) - - elseif ct == commandType.ADVANCE_INSTRUCTIONS then - processAdvanceInstructions(command) - - elseif ct == commandType.PING then - processPing(command) - - elseif ct == commandType.EXIT then - processExit(command) - elseif ct == commandType.RESET then - processReset(command) - else - errorResponse(errorType.CMD_INVALID_TYPE, command.requestId) - print(string.format("unknown command: %d, skipping command length %d", command.type, command.length)) - end - - print(string.format("Command finished: %02x", command.type)) -end - -local function prepareCommand() - if conn == nil then - running = true - return - end - - if running then - conn:settimeout(0) - else - conn:settimeout(-1) - end - - local data, status, partial = conn:receive(1) - if status == "timeout" then - return - elseif status == "closed" then - conn = nil - running = true - return - end - - if data:byte(1) ~= 0x02 then - return - end - - if running then - running = false - monitorOpened() - end - conn:settimeout(-1) - - data, status = conn:receive(1 + 4) - - local apiVersion = readUint8(data, 1) - - local bodyLength = readUint32(data, 2) - - local remainingHeaderSize = 5 - - local remainingHeader, status = conn:receive(remainingHeaderSize) - - local body - if bodyLength > 0 then - body, status = conn:receive(bodyLength) - end - - processCommand(apiVersion, bodyLength, remainingHeader, body) -end - -local function initConnection() - if conn == nil then - conn, err = server:accept() - if conn == nil or err ~= nil then - return - end - end -end - -local deregisterFrameCallback -local registerFrameCallback -local function frameHandle() - initConnection() - - if conn == nil then - return - end - - prepareCommand() - if not running then - deregisterFrameCallback() - emu.breakExecution() - registerFrameCallback() - end -end - -local frameCallback = nil -function registerFrameCallback() - frameCallback = emu.addEventCallback(frameHandle, emu.eventType.inputPolled) -end - -function deregisterFrameCallback() - emu.removeEventCallback(frameCallback, emu.eventType.inputPolled) -end - -function trapHandle(trap) - print("Trap") - - if conn == nil then - return - end - - if trap.stop then - monitorOpened() - end - responseCheckpointInfo(requestId, trap, true) - if not trap.stop then - return - end - - running = false - - deregisterFrameCallback() - emu.breakExecution() - registerFrameCallback() -end - -local function breakHandle() - print("Break") - - if stepping then - stepping = false - monitorOpened() - end - - repeat - prepareCommand() - until running - - emu.resume() -end - -registerFrameCallback() - -emu.addEventCallback(breakHandle, emu.eventType.codeBreak) \ No newline at end of file +server.start(host, port) \ No newline at end of file diff --git a/server.lua b/server.lua new file mode 100644 index 0000000..af12a4e --- /dev/null +++ b/server.lua @@ -0,0 +1,312 @@ +local _p = emu.log +local function print(data) + _p(data .. "") +end + +local baseDir = os.getenv("MESEN_REMOTE_BASEDIR") + +local socket = require("socket.core") +local me = {} +local commands = dofile(baseDir.."/commands.lua")(me) + +me.running = false +me.stepping = false +me.conn = nil +me.server = nil + +function me.boolToLittleEndian(b) + local value = 0 + if b then + value = 1 + end + return string.char(value & 0xff) +end + +function me.uint8ToLittleEndian(value) + if value == nil then + error("value is nil", 2) + end + return string.char(value & 0xff) +end + +function me.uint16ToLittleEndian(value) + if value == nil then + error("value is nil", 2) + end + return string.char(value & 0xff, (value >> 8) & 0xff) +end + +function me.uint32ToLittleEndian(value) + if value == nil then + error("value is nil", 2) + end + return string.char(value & 0xff, (value >> 8) & 0xff, (value >> 16) & 0xff, (value >> 24) & 0xff) +end + +function me.readUint32(data, index) + return data:byte(index) + (data:byte(index + 1) << 8) + (data:byte(index + 2) << 16) + (data:byte(index + 3) << 24) +end + +function me.readUint16(data, index) + return data:byte(index) + (data:byte(index + 1) << 8) +end + +function me.readUint8(data, index) + return data:byte(index) +end + +function me.readBool(data, index) + return data:byte(index) > 0 +end + +me.errorType = { + OK = 0x00, + OBJECT_MISSING = 0x01, + INVALID_MEMSPACE = 0x02, + CMD_INVALID_LENGTH = 0x80, + INVALID_PARAMETER = 0x81, + CMD_INVALID_API_VERSION = 0x82, + CMD_INVALID_TYPE = 0x83, + CMD_FAILURE = 0x8f, +}; + +me.commandType = { + INVALID = 0x00, + + MEM_GET = 0x01, + MEM_SET = 0x02, + + CHECKPOINT_GET = 0x11, + CHECKPOINT_SET = 0x12, + CHECKPOINT_DELETE = 0x13, + CHECKPOINT_LIST = 0x14, + CHECKPOINT_TOGGLE = 0x15, + + CONDITION_SET = 0x22, + + REGISTERS_GET = 0x31, + REGISTERS_SET = 0x32, + + DUMP = 0x41, + UNDUMP = 0x42, + + RESOURCE_GET = 0x51, + RESOURCE_SET = 0x52, + + ADVANCE_INSTRUCTIONS = 0x71, + KEYBOARD_FEED = 0x72, + EXECUTE_UNTIL_RETURN = 0x73, + + PING = 0x81, + BANKS_AVAILABLE = 0x82, + REGISTERS_AVAILABLE = 0x83, + DISPLAY_GET = 0x84, + VICE_INFO = 0x85, + + PALETTE_GET = 0x91, + + EXIT = 0xaa, + QUIT = 0xbb, + RESET = 0xcc, + AUTOSTART = 0xdd, +} + +me.responseType = { + MEM_GET = 0x01, + MEM_SET = 0x02, + + CHECKPOINT_INFO = 0x11, + + CHECKPOINT_DELETE = 0x13, + CHECKPOINT_LIST = 0x14, + CHECKPOINT_TOGGLE = 0x15, + + CONDITION_SET = 0x22, + + REGISTER_INFO = 0x31, + + DUMP = 0x41, + UNDUMP = 0x42, + + RESOURCE_GET = 0x51, + RESOURCE_SET = 0x52, + + JAM = 0x61, + STOPPED = 0x62, + RESUMED = 0x63, + + ADVANCE_INSTRUCTIONS = 0x71, + KEYBOARD_FEED = 0x72, + EXECUTE_UNTIL_RETURN = 0x73, + + PING = 0x81, + BANKS_AVAILABLE = 0x82, + REGISTERS_AVAILABLE = 0x83, + DISPLAY_GET = 0x84, + VICE_INFO = 0x85, + + PALETTE_GET = 0x91, + + EXIT = 0xaa, + QUIT = 0xbb, + RESET = 0xcc, + AUTOSTART = 0xdd, +} + +me.API_VERSION = 0x02 + +me.EVENT_ID = 0xffffffff + +function me.response(responseType, errorCode, requestId, body) + if responseType == nil or errorCode == nil or requestId == nil then + error("A required response value was nil", 2) + end + + if me.conn == nil then + return + end + + local r = {} + r[#r+1] = string.char(0x02) + r[#r+1] = me.uint8ToLittleEndian(me.API_VERSION) + if body ~= nil then + r[#r+1] = me.uint32ToLittleEndian(body:len()) + else + r[#r+1] = me.uint32ToLittleEndian(0) + end + r[#r+1] = me.uint8ToLittleEndian(responseType) + r[#r+1] = me.uint8ToLittleEndian(errorCode) + r[#r+1] = me.uint32ToLittleEndian(requestId) + me.conn:send(table.concat(r)) + + if body ~= nil then + me.conn:send(body) + end +end + +function me.errorResponse(errorCode, requestId) + me.response(0, errorCode, requestId, nil) +end + +local function initConnection() + if me.conn == nil then + me.conn, err = me.server:accept() + if me.conn == nil or err ~= nil then + return + end + print("Connected") + end +end + +local function prepareCommand() + if me.conn == nil then + me.running = true + return + end + + if me.running then + me.conn:settimeout(0) + else + me.conn:settimeout(-1) + end + + local data, status, partial = me.conn:receive(1) + if status == "timeout" then + return + elseif status == "closed" then + me.conn = nil + me.running = true + return + end + + if data:byte(1) ~= 0x02 then + return + end + + if me.running then + me.running = false + commands.monitorOpened() + end + me.conn:settimeout(-1) + + data, status = me.conn:receive(1 + 4) + + local apiVersion = me.readUint8(data, 1) + + local bodyLength = me.readUint32(data, 2) + + local remainingHeaderSize = 5 + + local remainingHeader, status = me.conn:receive(remainingHeaderSize) + + local body + if bodyLength > 0 then + body, status = me.conn:receive(bodyLength) + end + + commands.processCommand(apiVersion, bodyLength, remainingHeader, body) +end + +me.deregisterFrameCallback = nil +me.registerFrameCallback = nil +local function frameHandle() + initConnection() + + if me.conn == nil then + return + end + + prepareCommand() + if not me.running then + me.deregisterFrameCallback() + emu.breakExecution() + me.registerFrameCallback() + end +end + +local frameCallback = nil +function me.registerFrameCallback() + frameCallback = emu.addEventCallback(frameHandle, emu.eventType.inputPolled) +end + +function me.deregisterFrameCallback() + emu.removeEventCallback(frameCallback, emu.eventType.inputPolled) +end + +local function breakHandle() + print("Break") + + if me.stepping then + me.stepping = false + commands.monitorOpened() + end + + repeat + prepareCommand() + until me.running + + emu.resume() +end + +function me.start(host, port) + me.stepping = false + me.running = true + + print("Binding to host '" ..host.. "' and port " ..port.. "...") + + me.server = assert(socket.tcp()) + assert(me.server:bind(host, port)) + assert(me.server:listen(32)) + me.server:settimeout(0) + + local i, p = me.server:getsockname() + assert(i, p) + + print("Waiting for connection on " .. i .. ":" .. p .. "...") + + me.registerFrameCallback() + + emu.addEventCallback(breakHandle, emu.eventType.codeBreak) +end + +return me \ No newline at end of file