/* DBGp client functions - v1.1
* Enables scripts to debug other scripts via DBGp.
* Requires AutoHotkey v1.1.21+
*/
/*
Public functions:
DBGp_StartListening(localAddress="127.0.0.1", localPort=9000) returns socket
DBGp_OnBegin(func) ; func(session, initPacket)
DBGp_OnBreak(func) ; func(session, responsePacket)
DBGp_OnStream(func) ; func(session, streamPacket)
DBGp_OnEnd(func) ; func(session)
DBGp_StopListening(socket)
DBGp(session, command [, args, ByRef response])
DBGp_Send(session, command [, args, responseHandler])
DBGp_Receive(session, ByRef packet)
DBGp_Base64UTF8Decode(ByRef base64) returns decoded string
DBGp_Base64UTF8Encode(ByRef textdata) returns encoded string
DBGp_EncodeFileURI(filename) returns fileuri
DBGp_DecodeFileURI(fileuri) returns filename
DBGp_GetSessionSocket(session) -> session.Socket
DBGp_GetSessionIDEKey(session) -> session.IDEKey
DBGp_GetSessionCookie(session) -> session.Cookie
DBGp_GetSessionThread(session) -> session.Thread
DBGp_GetSessionFile(session) -> session.File
*/
class DBGp_Session
{
;public:
static __Call := Func("DBGp")
Send := Func("DBGp_Send")
Receive := Func("DBGp_Receive")
Close := Func("DBGp_CloseSession")
;internal:
static OnBegin, OnBreak, OnStream, OnEnd
static sockets := {}
static callQueue := []
responseQueue := []
handlers := {}
lastID := 0
__New() {
ObjSetCapacity(this, "buf", 4096)
this.bufLen := 0
}
class WaitHandler {
static Call := Func("_DBGp_WaitHandler_Call")
}
class QueueHandler {
static Call := Func("_DBGp_QueueHandler_Call")
static __New := Func("_DBGp_QueueHandler_New")
}
}
; Start listening for debugger connections. Must be called before any debugger may connect.
DBGp_StartListening(localAddress="127.0.0.1", localPort=9000)
{
static AF_INET:=2, SOCK_STREAM:=1, IPPROTO_TCP:=6
, FD_ACCEPT:=8, FD_READ:=1, FD_CLOSE:=0x20
static wsaData := ""
if !VarSetCapacity(wsaData)
{ ; Initialize Winsock to version 2.2.
VarSetCapacity(wsaData, 402)
wsaError := DllCall("ws2_32\WSAStartup", "ushort", 0x202, "ptr", &wsaData)
if wsaError
return DBGp_WSAE(wsaError)
}
; Create socket to be used to listen for connections.
s := DllCall("ws2_32\socket", "int", AF_INET, "int", SOCK_STREAM, "int", IPPROTO_TCP, "ptr")
if s = -1
return DBGp_WSAE()
; Bind to specific local interface, or any/all.
VarSetCapacity(sockaddr_in, 16, 0)
NumPut(AF_INET, sockaddr_in, 0, "ushort")
NumPut(DllCall("ws2_32\htons", "ushort", localPort, "ushort"), sockaddr_in, 2, "ushort")
NumPut(DllCall("ws2_32\inet_addr", "astr", localAddress), sockaddr_in, 4)
if DllCall("ws2_32\bind", "ptr", s, "ptr", &sockaddr_in, "int", 16) = 0 ; no error
; Request window message-based notification of network events.
&& DllCall("ws2_32\WSAAsyncSelect", "ptr", s, "ptr", DBGp_hwnd(), "uint", 0x8000, "int", FD_ACCEPT|FD_READ|FD_CLOSE) = 0 ; no error
&& DllCall("ws2_32\listen", "ptr", s, "int", 4) = 0 ; no error
return s
; An error occurred.
e := DllCall("ws2_32\WSAGetLastError")
DllCall("ws2_32\closesocket", "ptr", s)
DBGp_WSAE(e)
}
; Set the function to be called when a debugger connection is accepted.
DBGp_OnBegin(fn)
{
; Subject to change - do not use this property directly:
DBGp_Session.OnBegin := fn ? new DBGp_Session.QueueHandler(fn) : ""
}
; Set the function to be called when a response to a continuation command is received.
DBGp_OnBreak(fn)
{
; Subject to change - do not use this property directly:
DBGp_Session.OnBreak := fn ? new DBGp_Session.QueueHandler(fn) : ""
}
; Set the function to be called when a stream packet is received.
DBGp_OnStream(fn)
{
; Subject to change - do not use this property directly:
DBGp_Session.OnStream := fn ? new DBGp_Session.QueueHandler(fn) : ""
}
; Set the function to be called when a debugger connection is lost.
DBGp_OnEnd(fn)
{
; Subject to change - do not use this property directly:
DBGp_Session.OnEnd := fn ? new DBGp_Session.QueueHandler(fn) : ""
}
; Stops listening for debugger connections. Does not disconnect debuggers, but prevents more debuggers from connecting.
DBGp_StopListening(socket)
{
return DllCall("ws2_32\closesocket", "ptr", socket) = -1 ? DBGp_WSAE() : 0
}
; Execute a DBGp command.
DBGp(session, command, args="", ByRef response="")
{
response := ""
handler := ""
; If OnBreak has been set and this is a continuation command,
; call OnBreak when the response is received instead of waiting.
if InStr(" run step_into step_over step_out ", " " command " ")
handler := DBGp_Session.OnBreak
if wait := !handler
handler := new DBGp_Session.WaitHandler
if (r := _DBGp_SendEx(session, command, args, handler)) = 0
{
if wait
{
handler.cmd := command ;dbg
; Wait for and return a response.
r := _DBGp_WaitHandler_Wait(handler, session, response)
}
}
return r
}
; Send a command.
DBGp_Send(session, command, args="", responseHandler="")
{
if responseHandler
responseHandler := new DBGp_Session.QueueHandler(responseHandler)
return _DBGp_SendEx(session, command, args, responseHandler)
}
_DBGp_SendEx(session, command, args, responseHandler)
{
; Format command line (insert -i transaction_id).
transaction_id := ++session.lastID
packet := command " -i " transaction_id
if (args != "")
packet .= " " args
; Convert to UTF-8 (regardless of ANSI vs Unicode).
VarSetCapacity(packetData, packetLen := StrPut(packet, "UTF-8"))
StrPut(packet, &packetData, "UTF-8")
; Set the handler first to avoid a possible race condition.
if responseHandler
session.handlers[transaction_id] := responseHandler
if DllCall("ws2_32\send", "ptr", session.Socket, "ptr", &packetData, "int", packetLen, "int", 0) = -1
{
; Remove the handler, since it is unlikely to be called. This
; may be unnecessary since it's likely the session is ending.
if responseHandler
session.handlers.Delete(transaction_id)
return DBGp_WSAE()
}
return 0
}
; Retrieve the next .
DBGp_Receive(session, ByRef packet)
{
return _DBGp_WaitHandler_Wait(session.responseQueue, session, packet)
}
; ## SESSION API ##
DBGp_GetSessionSocket(session)
{
return session.Socket
}
DBGp_GetSessionIDEKey(session)
{
return session.IDEKey
}
DBGp_GetSessionCookie(session)
{
return session.Cookie
}
DBGp_GetSessionThread(session)
{
return session.Thread
}
DBGp_GetSessionFile(session)
{
return session.File
}
DBGp_CloseSession(session)
{
return DllCall("ws2_32\closesocket", "ptr", session.Socket) = -1 ? DBGp_WSAE() : 0
}
; ## UTILITY FUNCTIONS ##
DBGp_Base64UTF8Decode(ByRef base64) {
if (base64 = "")
return
cp := DBGp_StringToBinary(result, base64, 1)
return StrGet(&result, cp, "utf-8")
}
DBGp_Base64UTF8Encode(ByRef textdata) {
if (textdata = "")
return
VarSetCapacity(rawdata, StrPut(textdata, "utf-8")), sz := StrPut(textdata, &rawdata, "utf-8") - 1
return DBGp_BinaryToString(rawdata, sz, 0x40000001)
}
;http://www.autohotkey.com/forum/viewtopic.php?p=238120#238120
DBGp_BinaryToString(ByRef bin, sz=0, fmt=12) { ; return base64 or formatted-hex
n := sz>0 ? sz : VarSetCapacity(bin)
DllCall("Crypt32.dll\CryptBinaryToString", "ptr",&bin, "uint",n, "uint",fmt, "ptr",0, "uint*",cp:=0) ; get size
VarSetCapacity(str, cp*(A_IsUnicode ? 2:1))
DllCall("Crypt32.dll\CryptBinaryToString", "ptr",&bin, "uint",n, "uint",fmt, "str",str, "uint*",cp)
return str
}
DBGp_StringToBinary(ByRef bin, hex, fmt=12) { ; return length, result in bin
DllCall("Crypt32.dll\CryptStringToBinary", "ptr",&hex, "uint",StrLen(hex), "uint",fmt, "ptr",0, "uint*",cp:=0, "ptr",0,"ptr",0) ; get size
VarSetCapacity(bin, cp)
DllCall("Crypt32.dll\CryptStringToBinary", "ptr",&hex, "uint",StrLen(hex), "uint",fmt, "ptr",&bin, "uint*",cp, "ptr",0,"ptr",0)
return cp
}
; Convert file path to URI
; Rewritten by fincs to support Unicode paths
DBGp_EncodeFileURI(s)
{
len := DllCall("GetFullPathName", "str", s, "uint", 0, "ptr", 0, "ptr", 0)
VarSetCapacity(buf, len*2)
DllCall("GetFullPathName", "str", s, "uint", len, "str", buf, "ptr", 0)
s := StrReplace(StrReplace(s, "\", "/"), "%", "%25")
VarSetCapacity(h, 4)
regex := (A_AhkVersion >= "2." ? "" : "O)") "[^\w\-.!~*'()/%]"
while RegExMatch(s, regex, c)
{
StrPut(c[0], &h, "UTF-8")
r := ""
while n := NumGet(h, A_Index-1, "UChar")
r .= Format("%{:02X}", n)
s := StrReplace(s, c[0], r)
}
return s
}
; Convert URI to file path
; Rewritten by fincs to support Unicode paths
DBGp_DecodeFileURI(s)
{
if SubStr(s, 1, 8) = "file:///"
s := SubStr(s, 9)
s := StrReplace(s, "/", "\")
VarSetCapacity(buf, StrLen(s)+1)
i := 0, o := 0
while i <= StrLen(s)
{
c := NumGet(s, i * (A_IsUnicode ? 2 : 1), A_IsUnicode ? "UShort" : "UChar")
if (c = Ord("%"))
c := "0x" SubStr(s, i+2, 2), i += 2
NumPut(c, buf, o, "UChar")
i++, o++
}
return StrGet(&buf, "UTF-8")
}
; Replace XML entities with the appropriate characters.
DBGp_DecodeXmlEntities(s)
{
; Replace XML entities which may be returned by AutoHotkey_L (e.g. in ide_key attribute of init packet if DBGp_IDEKEY env var contains one of "&'<>).
s := StrReplace(s, """, Chr(34))
s := StrReplace(s, "&", "&")
s := StrReplace(s, "'", "'")
s := StrReplace(s, "<", "<")
s := StrReplace(s, ">", ">")
return s
}
; ## INTERNAL FUNCTIONS ##
; Internal: Window procedure for handling WSAAsyncSelect notifications.
DBGp_HandleWindowMessage(hwnd, uMsg, wParam, lParam)
{
static FD_ACCEPT:=8, FD_READ:=1, FD_CLOSE:=0x20
; Must not be interrupted by FD_READ while processing FD_ACCEPT
; (e.g. setting up the session which FD_READ may be received for)
; or FD_READ (still processing previous data).
Critical 10000
uMsg &= 0xFFFFFFFF
if uMsg != 0x8000
return DllCall("DefWindowProc", "ptr", hwnd, "uint", uMsg, "ptr", wParam, "ptr", lParam)
event := lParam & 0xffff
; error := (lParam >> 16) & 0xffff
if (event = FD_ACCEPT)
{
; Accept incoming connection.
s := DllCall("ws2_32\accept", "ptr", wParam, "uint", 0, "uint", 0, "ptr")
if s = -1
{
DBGp_WSAE()
return 0
}
; Create object to store information about this debugging session.
session := new DBGp_Session
session.Socket := s
DBGp_AddSession(session)
}
else if (event = FD_READ) ; Receiving data.
{
if !(session := DBGp_FindSessionBySocket(wParam))
return 0
DBGp_HandleIncomingData(session)
}
else if (event = FD_CLOSE) ; Connection closed.
{
if !(session := DBGp_FindSessionBySocket(wParam))
return 0
DBGp_CallHandler(DBGp_Session.OnEnd, session)
DBGp_RemoveSession(session), session.Socket := -1
DllCall("ws2_32\closesocket", "ptr", wParam)
}
return 0
}
DBGp_HandleIncomingData(session)
{
cap := ObjGetCapacity(session, "buf")
ptr := ObjGetAddress(session, "buf")
len := session.bufLen
; Copy available data into the buffer.
r := DllCall("ws2_32\recv", "ptr", session.Socket
, "ptr", ptr + len, "int", cap - len, "int", 0)
; Be tolerant of errors because WSAEWOULDBLOCK is expected in some
; cases, and even if some other error occurs, there may be data in
; our buffer that we can try to process.
if (r != -1)
session.bufLen := (len += r)
if (packetLen := session.packetLen) = ""
{
; Each message begins with the length of the message body
; encoded as a null-terminated numeric string.
; Ensure the data is null-terminated.
NumPut(0, ptr+0, len, "char")
headerLen := DllCall("lstrlenA", "ptr", ptr)
; If we've received the complete string, len must include the
; null-terminator. Otherwise, the data is invalid/incomplete.
; This case should be very rare:
if (headerLen = len)
{
; Haven't seen the null-terminator yet.
if (len < 20)
return
; This section can only execute if we've received >= 20
; bytes and still don't have a null-terminated string.
; No valid message length would be >= 20 characters.
packetLen := "invalid"
}
else
{
; The most common case: we've received the complete header.
packetLen := StrGet(ptr, headerLen, "utf-8")
}
if packetLen is not integer
{
; Recovering from invalid data doesn't seem very useful in
; this context, so just shutdown and wait for the other end
; to close the connection.
DllCall("ws2_32\shutdown", "ptr", session.Socket, "int", 2)
return DBGp_E("invalid message header")
}
; Let packetLen include the null-terminator.
packetLen += 1
; Discard the null-terminated header.
headerLen += 1
len -= headerLen
DllCall("RtlMoveMemory", "ptr", ptr, "ptr", ptr + headerLen, "ptr", len)
; Ensure the buffer is large enough for the complete packet.
if (cap < packetLen)
{
; Grow exponentially to avoid incrementally reallocating.
while (cap < packetLen)
cap *= 2
if !(cap := ObjSetCapacity(session, "buf", cap))
throw Exception("Insufficient memory")
ptr := ObjGetAddress(session, "buf")
}
; Update session object.
session.bufLen := len
session.packetLen := packetLen
}
if (len >= packetLen) ; We have a complete packet.
{
; Retrieve and decode the packet.
packet := StrGet(ptr, packetLen, "utf-8")
; Remove it from the buffer.
session.bufLen := (len -= packetLen)
DllCall("RtlMoveMemory", "ptr", ptr, "ptr", ptr + packetLen, "ptr", len)
session.packetLen := ""
if len
{
; Post a message so this function will be called again to
; process the rest of the data. Unlike loop/goto, this
; method allows data to be received and processed while one
; of the handlers called below is still running.
DllCall("PostMessage", "ptr", DBGp_hwnd(), "uint", 0x8000
, "ptr", session.Socket, "ptr", 1)
}
; Call the appropriate handler.
if !RegExMatch(packet, "<\K\w+", packetType)
DBGp_E("invalid packet")
else if (packetType = "response")
DBGp_HandleResponsePacket(session, packet)
else if (packetType = "stream")
DBGp_HandleStreamPacket(session, packet)
else if (packetType = "init")
DBGp_HandleInitPacket(session, packet)
else
DBGp_E("unknown packet type: " packetType)
}
}
DBGp_CallHandler(handler, session="", ByRef packet="")
{
handler.Call(session, packet)
}
_DBGp_QueueHandler_Call(handler, session, ByRef packet)
{
DBGp_Session.callQueue.Push([handler.fn, session, packet])
; Using a single timer ensures that each handler finishes before
; the next is called, and that each runs in its own thread.
SetTimer _DBGp_DispatchTimer, -1
}
_DBGp_DispatchTimer()
{
; Call exactly one handler per new thread.
if next := DBGp_Session.callQueue.RemoveAt(1)
fn := next[1], %fn%(next[2], next[3])
; If the queue is not empty, reset the timer.
if DBGp_Session.callQueue.Length()
SetTimer _DBGp_DispatchTimer, -1
}
_DBGp_QueueHandler_New(handler, fn)
{
handler.fn := IsObject(fn) ? fn : Func(fn)
}
_DBGp_WaitHandler_Call(handler, session, ByRef response)
{
ObjPush(handler, response)
}
_DBGp_WaitHandler_Wait(handler, session, ByRef response)
{
WasCritical := A_IsCritical
Critical Off ; Must be Off to allow data to be received.
try
{
Loop
{
Sleep -1
if ObjLength(handler)
break
if session.Socket = -1
return DBGp_E("Disconnected")
DllCall("WaitMessage")
}
response := ObjRemoveAt(handler, 1)
if RegExMatch(response, " then returns an empty string.
DBGp_WSAE(n="")
{
if (n = "")
n := DllCall("ws2_32\WSAGetLastError")
if n
ErrorLevel := "WSAE:" n
else
ErrorLevel := 0
}
; Internal: Sets ErrorLevel then returns an empty string or DBGp error code.
DBGp_E(n)
{
ErrorLevel := n
if ErrorLevel is integer
return ErrorLevel ; Return DBGp error code.
; Empty/no return value indicates an internal/protocol error.
}