diff --git a/README.md b/README.md index 0e8e9fb..9e3fe5b 100644 --- a/README.md +++ b/README.md @@ -17,18 +17,11 @@ https://defold.com/extension-websocket/websocket_api/ ## Debugging -In order to make it easier to debug this extension, we provide a `game.project` setting `websocket.debug` (edit `game.project` as text and add): +In order to make it easier to debug this extension, we provide a `game.project` setting `websocket.debug`. Set it to: -``` -[websocket] -debug = level -``` - -Set it to: - -* `0` to disable debugging (i.e. no debug output). -* `1` to display state changes. -* `2` to display the messages sent and received. +* `disabled` to disable debugging (i.e. no debug output). +* `state` to display state changes. +* `verbose` to display the messages, state changes and more. ## External resources diff --git a/examples/websocket.gui b/examples/websocket.gui index aecb3b6..9a5edb5 100644 --- a/examples/websocket.gui +++ b/examples/websocket.gui @@ -7,767 +7,176 @@ textures { name: "ui" texture: "/examples/assets/ui.atlas" } -background_color { - x: 0.0 - y: 0.0 - z: 0.0 - w: 1.0 -} nodes { position { x: 10.0 y: 949.0 - z: 0.0 - w: 1.0 - } - rotation { - x: 0.0 - y: 0.0 - z: 0.0 - w: 1.0 - } - scale { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 } size { x: 620.0 y: 600.0 - z: 0.0 - w: 1.0 - } - color { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 } type: TYPE_TEXT - blend_mode: BLEND_MODE_ALPHA - text: "" font: "example" id: "log" - xanchor: XANCHOR_NONE - yanchor: YANCHOR_NONE pivot: PIVOT_NW outline { x: 1.0 y: 1.0 z: 1.0 - w: 1.0 } shadow { x: 1.0 y: 1.0 z: 1.0 - w: 1.0 } - adjust_mode: ADJUST_MODE_FIT line_break: true - layer: "" inherit_alpha: true - alpha: 1.0 - outline_alpha: 1.0 - shadow_alpha: 1.0 - template_node_child: false - text_leading: 1.0 - text_tracking: 0.0 } nodes { position { x: 214.0 - y: 314.0 - z: 0.0 - w: 1.0 - } - rotation { - x: 0.0 - y: 0.0 - z: 0.0 - w: 1.0 - } - scale { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } - size { - x: 200.0 - y: 100.0 - z: 0.0 - w: 1.0 - } - color { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 + y: 262.0 } type: TYPE_TEMPLATE id: "connect_ws" - layer: "" inherit_alpha: true - alpha: 1.0 template: "/examples/assets/button.gui" - template_node_child: false } nodes { - position { - x: 0.0 - y: 0.0 - z: 0.0 - w: 1.0 - } - rotation { - x: 0.0 - y: 0.0 - z: 0.0 - w: 1.0 - } - scale { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } - size { - x: 200.0 - y: 45.0 - z: 0.0 - w: 1.0 - } - color { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } type: TYPE_BOX - blend_mode: BLEND_MODE_ALPHA - texture: "ui/green_button08" id: "connect_ws/button" - xanchor: XANCHOR_NONE - yanchor: YANCHOR_NONE - pivot: PIVOT_CENTER - adjust_mode: ADJUST_MODE_FIT parent: "connect_ws" - layer: "below" - inherit_alpha: true - slice9 { - x: 8.0 - y: 8.0 - z: 8.0 - w: 8.0 - } - clipping_mode: CLIPPING_MODE_NONE - clipping_visible: true - clipping_inverted: false - alpha: 1.0 template_node_child: true - size_mode: SIZE_MODE_MANUAL } nodes { - position { - x: 0.0 - y: 0.0 - z: 0.0 - w: 1.0 - } - rotation { - x: 0.0 - y: 0.0 - z: 0.0 - w: 1.0 - } - scale { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } - size { - x: 200.0 - y: 40.0 - z: 0.0 - w: 1.0 - } - color { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } type: TYPE_TEXT - blend_mode: BLEND_MODE_ALPHA text: "CONNECT ws://" - font: "example" id: "connect_ws/label" - xanchor: XANCHOR_NONE - yanchor: YANCHOR_NONE - pivot: PIVOT_CENTER - outline { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } - shadow { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } - adjust_mode: ADJUST_MODE_FIT - line_break: false parent: "connect_ws/button" - layer: "text" - inherit_alpha: true - alpha: 1.0 - outline_alpha: 1.0 - shadow_alpha: 1.0 overridden_fields: 8 template_node_child: true - text_leading: 1.0 - text_tracking: 0.0 } nodes { position { x: 320.0 - y: 184.0 - z: 0.0 - w: 1.0 - } - rotation { - x: 0.0 - y: 0.0 - z: 0.0 - w: 1.0 - } - scale { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } - size { - x: 200.0 - y: 100.0 - z: 0.0 - w: 1.0 - } - color { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 + y: 132.0 } type: TYPE_TEMPLATE id: "close" - layer: "" inherit_alpha: true - alpha: 1.0 template: "/examples/assets/button.gui" - template_node_child: false } nodes { - position { - x: 0.0 - y: 0.0 - z: 0.0 - w: 1.0 - } - rotation { - x: 0.0 - y: 0.0 - z: 0.0 - w: 1.0 - } - scale { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } - size { - x: 200.0 - y: 45.0 - z: 0.0 - w: 1.0 - } - color { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } type: TYPE_BOX - blend_mode: BLEND_MODE_ALPHA - texture: "ui/green_button08" id: "close/button" - xanchor: XANCHOR_NONE - yanchor: YANCHOR_NONE - pivot: PIVOT_CENTER - adjust_mode: ADJUST_MODE_FIT parent: "close" - layer: "below" - inherit_alpha: true - slice9 { - x: 8.0 - y: 8.0 - z: 8.0 - w: 8.0 - } - clipping_mode: CLIPPING_MODE_NONE - clipping_visible: true - clipping_inverted: false - alpha: 1.0 template_node_child: true - size_mode: SIZE_MODE_MANUAL } nodes { - position { - x: 0.0 - y: 0.0 - z: 0.0 - w: 1.0 - } - rotation { - x: 0.0 - y: 0.0 - z: 0.0 - w: 1.0 - } - scale { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } - size { - x: 200.0 - y: 40.0 - z: 0.0 - w: 1.0 - } - color { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } type: TYPE_TEXT - blend_mode: BLEND_MODE_ALPHA text: "CLOSE" - font: "example" id: "close/label" - xanchor: XANCHOR_NONE - yanchor: YANCHOR_NONE - pivot: PIVOT_CENTER - outline { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } - shadow { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } - adjust_mode: ADJUST_MODE_FIT - line_break: false parent: "close/button" - layer: "text" - inherit_alpha: true - alpha: 1.0 - outline_alpha: 1.0 - shadow_alpha: 1.0 overridden_fields: 8 template_node_child: true - text_leading: 1.0 - text_tracking: 0.0 } nodes { position { x: 320.0 - y: 249.0 - z: 0.0 - w: 1.0 - } - rotation { - x: 0.0 - y: 0.0 - z: 0.0 - w: 1.0 - } - scale { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } - size { - x: 200.0 - y: 100.0 - z: 0.0 - w: 1.0 - } - color { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 + y: 197.0 } type: TYPE_TEMPLATE id: "send" - layer: "" inherit_alpha: true - alpha: 1.0 template: "/examples/assets/button.gui" - template_node_child: false } nodes { - position { - x: 0.0 - y: 0.0 - z: 0.0 - w: 1.0 - } - rotation { - x: 0.0 - y: 0.0 - z: 0.0 - w: 1.0 - } - scale { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } - size { - x: 200.0 - y: 45.0 - z: 0.0 - w: 1.0 - } - color { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } type: TYPE_BOX - blend_mode: BLEND_MODE_ALPHA - texture: "ui/green_button08" id: "send/button" - xanchor: XANCHOR_NONE - yanchor: YANCHOR_NONE - pivot: PIVOT_CENTER - adjust_mode: ADJUST_MODE_FIT parent: "send" - layer: "below" - inherit_alpha: true - slice9 { - x: 8.0 - y: 8.0 - z: 8.0 - w: 8.0 - } - clipping_mode: CLIPPING_MODE_NONE - clipping_visible: true - clipping_inverted: false - alpha: 1.0 template_node_child: true - size_mode: SIZE_MODE_MANUAL } nodes { - position { - x: 0.0 - y: 0.0 - z: 0.0 - w: 1.0 - } - rotation { - x: 0.0 - y: 0.0 - z: 0.0 - w: 1.0 - } - scale { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } - size { - x: 200.0 - y: 40.0 - z: 0.0 - w: 1.0 - } - color { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } type: TYPE_TEXT - blend_mode: BLEND_MODE_ALPHA text: "SEND" - font: "example" id: "send/label" - xanchor: XANCHOR_NONE - yanchor: YANCHOR_NONE - pivot: PIVOT_CENTER - outline { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } - shadow { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } - adjust_mode: ADJUST_MODE_FIT - line_break: false parent: "send/button" - layer: "text" - inherit_alpha: true - alpha: 1.0 - outline_alpha: 1.0 - shadow_alpha: 1.0 overridden_fields: 8 template_node_child: true - text_leading: 1.0 - text_tracking: 0.0 } nodes { position { x: 429.0 - y: 314.0 - z: 0.0 - w: 1.0 - } - rotation { - x: 0.0 - y: 0.0 - z: 0.0 - w: 1.0 - } - scale { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } - size { - x: 200.0 - y: 100.0 - z: 0.0 - w: 1.0 - } - color { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 + y: 262.0 } type: TYPE_TEMPLATE id: "connect_wss" - layer: "" inherit_alpha: true - alpha: 1.0 template: "/examples/assets/button.gui" - template_node_child: false } nodes { - position { - x: 0.0 - y: 0.0 - z: 0.0 - w: 1.0 - } - rotation { - x: 0.0 - y: 0.0 - z: 0.0 - w: 1.0 - } - scale { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } - size { - x: 200.0 - y: 45.0 - z: 0.0 - w: 1.0 - } - color { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } type: TYPE_BOX - blend_mode: BLEND_MODE_ALPHA - texture: "ui/green_button08" id: "connect_wss/button" - xanchor: XANCHOR_NONE - yanchor: YANCHOR_NONE - pivot: PIVOT_CENTER - adjust_mode: ADJUST_MODE_FIT parent: "connect_wss" - layer: "below" - inherit_alpha: true - slice9 { - x: 8.0 - y: 8.0 - z: 8.0 - w: 8.0 - } - clipping_mode: CLIPPING_MODE_NONE - clipping_visible: true - clipping_inverted: false - alpha: 1.0 template_node_child: true - size_mode: SIZE_MODE_MANUAL } nodes { - position { - x: 0.0 - y: 0.0 - z: 0.0 - w: 1.0 - } - rotation { - x: 0.0 - y: 0.0 - z: 0.0 - w: 1.0 - } - scale { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } - size { - x: 200.0 - y: 40.0 - z: 0.0 - w: 1.0 - } - color { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } type: TYPE_TEXT - blend_mode: BLEND_MODE_ALPHA text: "CONNECT wss://" - font: "example" id: "connect_wss/label" - xanchor: XANCHOR_NONE - yanchor: YANCHOR_NONE - pivot: PIVOT_CENTER - outline { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } - shadow { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 - } - adjust_mode: ADJUST_MODE_FIT - line_break: false parent: "connect_wss/button" - layer: "text" - inherit_alpha: true - alpha: 1.0 - outline_alpha: 1.0 - shadow_alpha: 1.0 overridden_fields: 8 template_node_child: true - text_leading: 1.0 - text_tracking: 0.0 } nodes { position { x: 320.0 - y: 314.0 - z: 0.0 - w: 1.0 - } - rotation { - x: 0.0 - y: 0.0 - z: 0.0 - w: 1.0 - } - scale { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 + y: 312.0 } size { x: 200.0 - y: 100.0 - z: 0.0 - w: 1.0 - } - color { - x: 1.0 - y: 1.0 - z: 1.0 - w: 1.0 + y: 40.0 } type: TYPE_TEXT - blend_mode: BLEND_MODE_ALPHA text: "" font: "example" id: "connection_text" - xanchor: XANCHOR_NONE - yanchor: YANCHOR_NONE - pivot: PIVOT_CENTER outline { x: 1.0 y: 1.0 z: 1.0 - w: 1.0 } shadow { x: 1.0 y: 1.0 z: 1.0 - w: 1.0 } - adjust_mode: ADJUST_MODE_FIT - line_break: false - layer: "" inherit_alpha: true - alpha: 1.0 - outline_alpha: 1.0 - shadow_alpha: 1.0 - template_node_child: false - text_leading: 1.0 - text_tracking: 0.0 +} +nodes { + position { + x: 320.0 + y: 69.0 + } + type: TYPE_TEMPLATE + id: "reboot" + inherit_alpha: true + template: "/examples/assets/button.gui" +} +nodes { + type: TYPE_BOX + id: "reboot/button" + parent: "reboot" + template_node_child: true +} +nodes { + type: TYPE_TEXT + text: "REBOOT" + id: "reboot/label" + parent: "reboot/button" + overridden_fields: 8 + template_node_child: true } layers { name: "below" @@ -783,4 +192,3 @@ layers { } material: "/builtins/materials/gui.material" adjust_reference: ADJUST_REFERENCE_PARENT -max_nodes: 512 diff --git a/examples/websocket.gui_script b/examples/websocket.gui_script index a1a7e84..813dc2b 100644 --- a/examples/websocket.gui_script +++ b/examples/websocket.gui_script @@ -1,46 +1,64 @@ local URL = "echo.websocket.org" local function click_button(node, action) + if type(node) == "string" then + node = gui.get_node(node) + end return gui.is_enabled(node) and action.pressed and gui.pick_node(node, action.x, action.y) end +local function hide(node) + if type(node) == "string" then + node = gui.get_node(node) + end + gui.set_enabled(node, false) +end +local function show(node) + if type(node) == "string" then + node = gui.get_node(node) + end + gui.set_enabled(node, true) +end local function update_gui(self) if self.connection then - gui.set_enabled(self.connect_ws_node, false) - gui.set_enabled(self.connect_wss_node, false) - gui.set_enabled(self.send_node, true) - gui.set_enabled(self.close_node, true) - gui.set_enabled(self.connection_text, true) - gui.set_text(self.connection_text, "Connected to " .. self.url) + hide("connect_ws/button") + hide("connect_wss/button") + show("send/button") + show("close/button") + show("connection_text") + gui.set_text(gui.get_node("connection_text"), "Connected to " .. self.url) else - gui.set_enabled(self.connect_ws_node, true) - gui.set_enabled(self.connect_wss_node, true) - gui.set_enabled(self.send_node, false) - gui.set_enabled(self.close_node, false) - gui.set_enabled(self.connection_text, false) + show("connect_ws/button") + show("connect_wss/button") + hide("send/button") + hide("close/button") + show("connection_text") + gui.set_text(gui.get_node("connection_text"), "Connect to " .. URL) end end +local LOG_LINES = {} local function log(...) - local text = "" + local text = os.date("%H:%M:%S") .. " " local len = select("#", ...) for i=1,len do text = text .. tostring(select(i, ...)) .. (i == len and "" or ", ") end - - print(text) + table.insert(LOG_LINES, text) + if #LOG_LINES > 20 then + table.remove(LOG_LINES, 1) + end + local s = "" + for _,line in ipairs(LOG_LINES) do + s = s .. line .. "\n" + end local node = gui.get_node("log") - gui.set_text(node, gui.get_text(node) .. "\n" .. text) + gui.set_text(node, s) end function init(self) msg.post(".", "acquire_input_focus") msg.post("@render:", "clear_color", { color = vmath.vector4(0.2, 0.4, 0.8, 1.0) }) - self.connect_ws_node = gui.get_node("connect_ws/button") - self.connect_wss_node = gui.get_node("connect_wss/button") - self.send_node = gui.get_node("send/button") - self.close_node = gui.get_node("close/button") - self.connection_text = gui.get_node("connection_text") self.connection = nil update_gui(self) end @@ -49,12 +67,6 @@ function final(self) msg.post(".", "release_input_focus") end -function update(self, dt) - if self.ws then - self.ws.step() - end -end - local function websocket_callback(self, conn, data) if data.event == websocket.EVENT_DISCONNECTED then log("Disconnected: " .. tostring(conn) .. " Code: " .. data.code .. " Message: " .. tostring(data.message)) @@ -66,11 +78,21 @@ local function websocket_callback(self, conn, data) elseif data.event == websocket.EVENT_ERROR then log("Error: '" .. tostring(data.message) .. "'") if data.handshake_response then - log("Handshake response status: '" .. tostring(data.handshake_response.status) .. "'") - for key, value in pairs(data.handshake_response.headers) do + local response = data.handshake_response + log("Handshake response status: '" .. tostring(response.status) .. "'") + for key, value in pairs(response.headers) do log("Handshake response header: '" .. key .. ": " .. value .. "'") end - log("Handshake response body: '" .. tostring(data.handshake_response.response) .. "'") + log("Handshake response body: '" .. tostring(response.response) .. "'") + + local status = response.status + if status == 301 or status == 302 or status == 303 or status == 307 or status == 308 then + local location = response.headers.location + URL = location:match("https+://(.*)/+") + log("Handshake redirect to:", response.headers.location) + update_gui(self) + return + end end elseif data.event == websocket.EVENT_MESSAGE then log("Receiving: '" .. tostring(data.message) .. "'") @@ -94,18 +116,19 @@ end function on_input(self, action_id, action) - if click_button(self.connect_ws_node, action) then + if click_button("connect_ws/button", action) then connect(self, "ws") - elseif click_button(self.connect_wss_node, action) then + elseif click_button("connect_wss/button", action) then connect(self, "wss") - elseif click_button(self.close_node, action) then + elseif click_button("close/button", action) then disconnect(self) - elseif click_button(gui.get_node("send/button"), action) then + elseif click_button("send/button", action) then local message_to_send = 'sending to server' local ok, was_clean, code, reason = websocket.send(self.connection, message_to_send) log("Sending '" .. message_to_send .. "'", ok, was_clean, code, reason) - elseif click_button(gui.get_node("close/button"), action) then + elseif click_button("close/button", action) then log("Closing") - self.ws:close() + elseif click_button("reboot/button", action) then + sys.reboot() end end diff --git a/game.project b/game.project index eadc24d..da9d393 100755 --- a/game.project +++ b/game.project @@ -14,3 +14,7 @@ _dependencies = https://github.com/GameAnalytics/defold-openssl/archive/1.0.0.zi [library] include_dirs = websocket + +[websocket] +debug = verbose + diff --git a/websocket/ext.properties b/websocket/ext.properties index 81fb565..d2576e5 100644 --- a/websocket/ext.properties +++ b/websocket/ext.properties @@ -1,18 +1,21 @@ [websocket] group = Runtime help = Websocket extension settings +title = Websocket buffer_size.type = integer +buffer_size.help = Size of the receive buffer buffer_size.default = 65536 -socket_timeout.type = integer -socket_timeout.default = 500000 -socket_timeout.help = Timeout for the underlying socket connection, in microseconds - max_connections.type = integer +max_connections.help = Maximum number of concurrent connections max_connections.default = 2 +debug.type = string +debug.options = disabled:Disabled,state_changes:State changes,verbose:Verbose +debug.help = The debug level +debug.default = disabled -debug.type = integer -debug.default = 0 -debug.options = 0:Disabled, 1:State Changes, 2:Messages Sent and Received +socket_timeout.type = integer +socket_timeout.default = 500000 +socket_timeout.help = Timeout for the underlying socket connection, in microseconds diff --git a/websocket/src/emscripten_callbacks.cpp b/websocket/src/emscripten_callbacks.cpp index 80ddb4e..e8f4572 100644 --- a/websocket/src/emscripten_callbacks.cpp +++ b/websocket/src/emscripten_callbacks.cpp @@ -5,28 +5,28 @@ namespace dmWebsocket { EM_BOOL Emscripten_WebSocketOnOpen(int eventType, const EmscriptenWebSocketOpenEvent *websocketEvent, void *userData) { - DebugLog(1, "WebSocket OnOpen"); + DebugLog(dmWebsocket::DEBUG_STATE_CHANGES, "WebSocket OnOpen"); WebsocketConnection* conn = (WebsocketConnection*)userData; SetState(conn, STATE_CONNECTED); HandleCallback(conn, EVENT_CONNECTED, 0, 0); return EM_TRUE; } EM_BOOL Emscripten_WebSocketOnError(int eventType, const EmscriptenWebSocketErrorEvent *websocketEvent, void *userData) { - DebugLog(1, "WebSocket OnError"); + DebugLog(dmWebsocket::DEBUG_STATE_CHANGES, "WebSocket OnError"); WebsocketConnection* conn = (WebsocketConnection*)userData; conn->m_Status = RESULT_ERROR; SetState(conn, STATE_DISCONNECTED); return EM_TRUE; } EM_BOOL Emscripten_WebSocketOnClose(int eventType, const EmscriptenWebSocketCloseEvent *websocketEvent, void *userData) { - DebugLog(1, "WebSocket OnClose"); + DebugLog(dmWebsocket::DEBUG_STATE_CHANGES, "WebSocket OnClose"); WebsocketConnection* conn = (WebsocketConnection*)userData; int length = strlen(websocketEvent->reason); PushMessage(conn, MESSAGE_TYPE_CLOSE, length, (uint8_t*)websocketEvent->reason, websocketEvent->code); return EM_TRUE; } EM_BOOL Emscripten_WebSocketOnMessage(int eventType, const EmscriptenWebSocketMessageEvent *websocketEvent, void *userData) { - DebugLog(1, "WebSocket OnMessage"); + DebugLog(dmWebsocket::DEBUG_STATE_CHANGES, "WebSocket OnMessage"); WebsocketConnection* conn = (WebsocketConnection*)userData; int length = websocketEvent->numBytes; if (websocketEvent->isText) diff --git a/websocket/src/handshake.cpp b/websocket/src/handshake.cpp index a00ee98..04e3fd6 100644 --- a/websocket/src/handshake.cpp +++ b/websocket/src/handshake.cpp @@ -3,6 +3,8 @@ #include #include // tolower +#if !defined(__EMSCRIPTEN__) + namespace dmWebsocket { @@ -102,22 +104,10 @@ Result SendClientHandshake(WebsocketConnection* conn) return SetStatus(conn, RESULT_HANDSHAKE_FAILED, "Connection not ready for sending data: %s", dmSocket::ResultToString(sr)); } -// In emscripten, the sockets are actually already websockets, so no handshake necessary -#if defined(__EMSCRIPTEN__) - return RESULT_OK; -#else return SendClientHandshakeHeaders(conn); -#endif } -#if defined(__EMSCRIPTEN__) -Result ReceiveHeaders(WebsocketConnection* conn) -{ - return RESULT_OK; -} - -#else Result ReceiveHeaders(WebsocketConnection* conn) { dmSocket::Result sr = WaitForSocket(conn, dmSocket::SELECTOR_KIND_READ, SOCKET_WAIT_TIMEOUT); @@ -125,7 +115,7 @@ Result ReceiveHeaders(WebsocketConnection* conn) { if (dmSocket::RESULT_WOULDBLOCK == sr) { - DebugLog(2, "Waiting for socket to be available for reading"); + DebugLog(dmWebsocket::DEBUG_VERBOSE, "Waiting for socket to be available for reading"); return RESULT_WOULDBLOCK; } @@ -169,7 +159,6 @@ Result ReceiveHeaders(WebsocketConnection* conn) return RESULT_WOULDBLOCK; } -#endif static void HandleVersion(void* user_data, int major, int minor, int status, const char* status_str) { @@ -207,13 +196,13 @@ bool ValidateSecretKey(WebsocketConnection* conn, const char* server_key) dmCrypt::Base64Encode(conn->m_Key, sizeof(conn->m_Key), client_key, &client_key_len); client_key[client_key_len] = 0; - DebugLog(2, "Secret key (base64): %s", client_key); + DebugLog(dmWebsocket::DEBUG_VERBOSE, "Secret key (base64): %s", client_key); memcpy(client_key + client_key_len, RFC_MAGIC, strlen(RFC_MAGIC)); client_key_len += strlen(RFC_MAGIC); client_key[client_key_len] = 0; - DebugLog(2, "Secret key + RFC_MAGIC: %s", client_key); + DebugLog(dmWebsocket::DEBUG_VERBOSE, "Secret key + RFC_MAGIC: %s", client_key); uint8_t client_key_sha1[20]; dmCrypt::HashSha1(client_key, client_key_len, client_key_sha1); @@ -223,19 +212,13 @@ bool ValidateSecretKey(WebsocketConnection* conn, const char* server_key) client_key_len = sizeof(client_key); dmCrypt::Base64Encode(client_key_sha1, sizeof(client_key_sha1), client_key, &client_key_len); client_key[client_key_len] = 0; - DebugLog(2, "Client key (base64): %s", client_key); - DebugLog(2, "Server key (base64): %s", server_key); + DebugLog(dmWebsocket::DEBUG_VERBOSE, "Client key (base64): %s", client_key); + DebugLog(dmWebsocket::DEBUG_VERBOSE, "Server key (base64): %s", server_key); return strcmp(server_key, (const char*)client_key) == 0; } -#if defined(__EMSCRIPTEN__) -Result VerifyHeaders(WebsocketConnection* conn) -{ - return RESULT_OK; -} -#else Result VerifyHeaders(WebsocketConnection* conn) { char* r = conn->m_Buffer; @@ -289,6 +272,7 @@ Result VerifyHeaders(WebsocketConnection* conn) conn->m_HasHandshakeData = conn->m_BufferSize != 0 ? 1 : 0; return RESULT_OK; } -#endif } // namespace + +#endif \ No newline at end of file diff --git a/websocket/src/websocket.cpp b/websocket/src/websocket.cpp index 1ecff5e..b2ac55a 100644 --- a/websocket/src/websocket.cpp +++ b/websocket/src/websocket.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include // isprint et al #if defined(__EMSCRIPTEN__) @@ -53,7 +54,6 @@ const char* ResultToString(Result err) const char* StateToString(State err) { switch(err) { - STRING_CASE(STATE_CREATE); STRING_CASE(STATE_CONNECTING); STRING_CASE(STATE_HANDSHAKE_WRITE); STRING_CASE(STATE_HANDSHAKE_READ); @@ -132,11 +132,12 @@ void DebugPrint(int level, const char* msg, const void* _bytes, uint32_t num_byt dmLogWarning("%s", buffer); } - -#define CLOSE_CONN(...) \ - SetStatus(conn, RESULT_ERROR, __VA_ARGS__); \ - CloseConnection(conn); - +// has the connect procedure taken too long? +static bool CheckConnectTimeout(WebsocketConnection* conn) +{ + uint64_t t = dmTime::GetMonotonicTime(); + return t >= conn->m_ConnectTimeout; +} void SetState(WebsocketConnection* conn, State state) { @@ -144,11 +145,53 @@ void SetState(WebsocketConnection* conn, State state) if (prev_state != state) { conn->m_State = state; - DebugLog(1, "%s -> %s", StateToString(prev_state), StateToString(conn->m_State)); + DebugLog(dmWebsocket::DEBUG_STATE_CHANGES, "%s -> %s", StateToString(prev_state), StateToString(conn->m_State)); + } +} + +static bool IsConnectionValid(WebsocketConnection* conn) +{ + if (conn) + { + for (int i = 0; i < g_Websocket.m_Connections.Size(); ++i ) + { + if (g_Websocket.m_Connections[i] == conn) + return true; + } + } + return false; +} + +static void CloseConnection(WebsocketConnection* conn) +{ + // we want it to send this message in the polling + if (conn->m_State == STATE_CONNECTED) + { +#if defined(HAVE_WSLAY) + WSL_Close(conn->m_Ctx); +#else + // start disconnecting by closing the WebSocket through the JS API + // we transition to the DISCONNECTED state when we receive the + // Emscripten callback that the connection has closed + emscripten_websocket_close(conn->m_WS, 1000, "CloseConnection"); + SetState(conn, STATE_DISCONNECTING); +#endif } + +#if defined(HAVE_WSLAY) + // close the connection and immediately transition to the DISCONNECTED + // state + SetState(conn, STATE_DISCONNECTED); +#endif } +#define CLOSE_CONN(...) \ + SetStatus(conn, RESULT_ERROR, __VA_ARGS__); \ + CloseConnection(conn); + + + Result SetStatus(WebsocketConnection* conn, Result status, const char* format, ...) { if (conn->m_Status == RESULT_OK) @@ -160,14 +203,195 @@ Result SetStatus(WebsocketConnection* conn, Result status, const char* format, . va_end(lst); conn->m_Status = status; - DebugLog(1, "STATUS: '%s' len: %u", conn->m_Buffer, conn->m_BufferSize); + DebugLog(dmWebsocket::DEBUG_STATE_CHANGES, "STATUS: '%s' len: %u", conn->m_Buffer, conn->m_BufferSize); } return status; } + // *************************************************************************************************** // LUA functions +#if defined(__EMSCRIPTEN__) +static void CreateConnectionEmscripten(WebsocketConnection* conn) +{ + char uri_buffer[dmURI::MAX_URI_LEN]; + const char* uri; + bool no_path = conn->m_Url.m_Path[0] == '\0'; + bool no_port = conn->m_Url.m_Port == -1; + if (no_path && no_port) + { + dmSnPrintf(uri_buffer, sizeof(uri_buffer), "%s://%s", conn->m_Url.m_Scheme, conn->m_Url.m_Hostname); + } + else if (no_port) + { + dmSnPrintf(uri_buffer, sizeof(uri_buffer), "%s://%s%s", conn->m_Url.m_Scheme, conn->m_Url.m_Hostname, conn->m_Url.m_Path); + } + else if (no_path) + { + dmSnPrintf(uri_buffer, sizeof(uri_buffer), "%s://%s:%d", conn->m_Url.m_Scheme, conn->m_Url.m_Hostname, conn->m_Url.m_Port); + } + else { + dmSnPrintf(uri_buffer, sizeof(uri_buffer), "%s://%s:%d%s", conn->m_Url.m_Scheme, conn->m_Url.m_Hostname, conn->m_Url.m_Port, conn->m_Url.m_Path); + } + uri = uri_buffer; + + EmscriptenWebSocketCreateAttributes ws_attrs = { + uri, + conn->m_Protocol, + EM_TRUE + }; + EMSCRIPTEN_WEBSOCKET_T ws = emscripten_websocket_new(&ws_attrs); + if (ws < 0) + { + conn->m_WS = 0; + CLOSE_CONN("Failed to connect to '%s:%d': %d", conn->m_Url.m_Hostname, (int)conn->m_Url.m_Port, ws); + SetState(conn, STATE_DISCONNECTED); + return; + } + conn->m_WS = ws; + + emscripten_websocket_set_onopen_callback(ws, conn, Emscripten_WebSocketOnOpen); + emscripten_websocket_set_onerror_callback(ws, conn, Emscripten_WebSocketOnError); + emscripten_websocket_set_onclose_callback(ws, conn, Emscripten_WebSocketOnClose); + emscripten_websocket_set_onmessage_callback(ws, conn, Emscripten_WebSocketOnMessage); + SetState(conn, STATE_CONNECTING); +} +#endif + +#if defined(HAVE_WSLAY) +static void ConnectionWorker(void* _conn) +{ + WebsocketConnection* conn = (WebsocketConnection*)_conn; + + // create and connect socket + SetState(conn, STATE_CONNECTING); + dmSocket::Result sr; + dmConnectionPool::Result pool_result = dmConnectionPool::Dial(g_Websocket.m_Pool, conn->m_Url.m_Hostname, conn->m_Url.m_Port, conn->m_SSL, g_Websocket.m_Timeout, &conn->m_Connection, &sr); + if (dmConnectionPool::RESULT_OK != pool_result) + { + CLOSE_CONN("Failed to open connection: %s", dmSocket::ResultToString(sr)); + return; + } + if (CheckConnectTimeout(conn)) + { + CLOSE_CONN("Connect sequence timed out"); + return; + } + if (!IsConnectionValid(conn)) + { + return; + } + + // socket configuration + conn->m_Socket = dmConnectionPool::GetSocket(g_Websocket.m_Pool, conn->m_Connection); + conn->m_SSLSocket = dmConnectionPool::GetSSLSocket(g_Websocket.m_Pool, conn->m_Connection); + dmSocket::SetNoDelay(conn->m_Socket, true); + dmSocket::SetBlocking(conn->m_Socket, false); + // Don't go lower than 1000 microseconds since this value will be divided by 1000 + // in the socket code and we'll then get a timeout of 0 which means the socket + // will block indefinitely + dmSocket::SetReceiveTimeout(conn->m_Socket, 1000); + if (conn->m_SSLSocket) + { + dmSSLSocket::SetReceiveTimeout(conn->m_SSLSocket, 1000); + } + + // send handshake + SetState(conn, STATE_HANDSHAKE_WRITE); + while (true) + { + Result result = SendClientHandshake(conn); + if (RESULT_OK == result) + { + break; + } + if (RESULT_WOULDBLOCK != result) + { + CLOSE_CONN("Failed sending handshake: %d", result); + return; + } + if (CheckConnectTimeout(conn)) + { + CLOSE_CONN("Connect sequence timed out"); + return; + } + if (!IsConnectionValid(conn)) + { + return; + } + dmTime::Sleep(10*1000); + } + + // read handshake + SetState(conn, STATE_HANDSHAKE_READ); + while (true) + { + Result result = ReceiveHeaders(conn); + if (RESULT_OK == result) + { + break; + } + if (RESULT_WOULDBLOCK != result) + { + CLOSE_CONN("Failed receiving handshake headers. %d", result); + return; + } + if (CheckConnectTimeout(conn)) + { + CLOSE_CONN("Connect sequence timed out"); + return; + } + if (!IsConnectionValid(conn)) + { + return; + } + dmTime::Sleep(10*1000); + } + + // verify handshake headers + Result result = VerifyHeaders(conn); + if (RESULT_OK != result) + { + CLOSE_CONN("Failed verifying handshake headers:\n%s\n\n", conn->m_Buffer); + return; + } + + // initialise wslay + int r = WSL_Init(&conn->m_Ctx, g_Websocket.m_BufferSize, (void*)conn); + if (0 != r) + { + CLOSE_CONN("Failed initializing wslay: %s", WSL_ResultToString(r)); + return; + } + + // poll for messages + SetState(conn, STATE_CONNECTED); + while ((STATE_CONNECTED == conn->m_State) || (STATE_DISCONNECTING == conn->m_State)) + { + int r = WSL_Poll(conn->m_Ctx); + if (0 != r) + { + CLOSE_CONN("Websocket closing for %s (%s)", conn->m_Url.m_Hostname, WSL_ResultToString(r)); + return; + } + if (!IsConnectionValid(conn)) + { + return; + } + dmTime::Sleep(10*1000); + } + + SetState(conn, STATE_DISCONNECTED); +} + +static void CreateConnectionWslay(WebsocketConnection* conn) +{ + conn->m_Ctx = 0; + conn->m_ConnectionThread = dmThread::New((dmThread::ThreadStart)ConnectionWorker, 0x80000, conn, "WSConnect"); +} +#endif + static WebsocketConnection* CreateConnection(const char* url) { WebsocketConnection* conn = new WebsocketConnection; @@ -176,6 +400,7 @@ static WebsocketConnection* CreateConnection(const char* url) conn->m_Buffer[0] = 0; conn->m_BufferSize = 0; conn->m_ConnectTimeout = 0; + conn->m_Mutex = dmMutex::New(); dmURI::Parse(url, &conn->m_Url); @@ -183,7 +408,6 @@ static WebsocketConnection* CreateConnection(const char* url) strcpy(conn->m_Url.m_Scheme, "wss"); conn->m_SSL = strcmp(conn->m_Url.m_Scheme, "wss") == 0 ? 1 : 0; - conn->m_State = STATE_CREATE; conn->m_Callback = 0; conn->m_Connection = 0; @@ -195,10 +419,11 @@ static WebsocketConnection* CreateConnection(const char* url) conn->m_ConnectionThread = 0; #if defined(HAVE_WSLAY) - conn->m_Ctx = 0; + CreateConnectionWslay(conn); #endif + #if defined(__EMSCRIPTEN__) - conn->m_WS = 0; + CreateConnectionEmscripten(conn); #endif return conn; @@ -206,32 +431,36 @@ static WebsocketConnection* CreateConnection(const char* url) static void DestroyConnection(WebsocketConnection* conn) { + { + DM_MUTEX_SCOPED_LOCK(conn->m_Mutex); + conn->m_State = STATE_DISCONNECTED; + #if defined(HAVE_WSLAY) - if (conn->m_Ctx) - WSL_Exit(conn->m_Ctx); + if (conn->m_Ctx) + WSL_Exit(conn->m_Ctx); #endif - free((void*)conn->m_CustomHeaders); - free((void*)conn->m_Protocol); + free((void*)conn->m_CustomHeaders); + free((void*)conn->m_Protocol); - if (conn->m_Callback) - dmScript::DestroyCallback(conn->m_Callback); + if (conn->m_Callback) + dmScript::DestroyCallback(conn->m_Callback); #if defined(__EMSCRIPTEN__) - if (conn->m_WS) - { - emscripten_websocket_delete(conn->m_WS); - } + if (conn->m_WS) + { + emscripten_websocket_delete(conn->m_WS); + } #else - if (conn->m_Connection) - dmConnectionPool::Close(g_Websocket.m_Pool, conn->m_Connection); + if (conn->m_Connection) + dmConnectionPool::Close(g_Websocket.m_Pool, conn->m_Connection); #endif - if (conn->m_HandshakeResponse) - delete conn->m_HandshakeResponse; - + if (conn->m_HandshakeResponse) + delete conn->m_HandshakeResponse; - free((void*)conn->m_Buffer); + free((void*)conn->m_Buffer); + } if (conn->m_ConnectionThread) { @@ -239,46 +468,13 @@ static void DestroyConnection(WebsocketConnection* conn) conn->m_ConnectionThread = 0; } + dmMutex::Delete(conn->m_Mutex); + delete conn; - DebugLog(2, "DestroyConnection: %p", conn); + DebugLog(dmWebsocket::DEBUG_VERBOSE, "DestroyConnection: %p", conn); } -static void CloseConnection(WebsocketConnection* conn) -{ - // we want it to send this message in the polling - if (conn->m_State == STATE_CONNECTED) { -#if defined(HAVE_WSLAY) - WSL_Close(conn->m_Ctx); -#else - // start disconnecting by closing the WebSocket through the JS API - // we transition to the DISCONNECTED state when we receive the - // Emscripten callback that the connection has closed - emscripten_websocket_close(conn->m_WS, 1000, "CloseConnection"); - SetState(conn, STATE_DISCONNECTING); -#endif - } - -#if defined(HAVE_WSLAY) - // close the connection and immediately transition to the DISCONNECTED - // state - SetState(conn, STATE_DISCONNECTED); -#endif -} - -static bool IsConnectionValid(WebsocketConnection* conn) -{ - if (conn) - { - for (int i = 0; i < g_Websocket.m_Connections.Size(); ++i ) - { - if (g_Websocket.m_Connections[i] == conn) - return true; - } - } - return false; -} - /*# * */ @@ -331,6 +527,7 @@ static int LuaDisconnect(lua_State* L) if (IsConnectionValid(conn)) { + DM_MUTEX_SCOPED_LOCK(conn->m_Mutex); CloseConnection(conn); } return 0; @@ -540,9 +737,19 @@ static dmExtension::Result AppInitialize(dmExtension::AppParams* params) pool_params.m_MaxConnections = dmConfigFile::GetInt(params->m_ConfigFile, "websocket.max_connections", 2); dmConnectionPool::Result result = dmConnectionPool::New(&pool_params, &g_Websocket.m_Pool); - g_DebugWebSocket = dmConfigFile::GetInt(params->m_ConfigFile, "websocket.debug", 0); - if (g_DebugWebSocket) - dmLogInfo("dmWebSocket::g_DebugWebSocket == %d", g_DebugWebSocket); + const char* debug_level = dmConfigFile::GetString(params->m_ConfigFile, "websocket.debug", "disabled"); + if (strcmp("state_changes", debug_level) == 0) + { + g_DebugWebSocket = dmWebsocket::DEBUG_STATE_CHANGES; + } + else if (strcmp("verbose", debug_level) == 0) + { + g_DebugWebSocket = dmWebsocket::DEBUG_VERBOSE; + } + else + { + g_DebugWebSocket = dmWebsocket::DEBUG_DISABLED; + } if (dmConnectionPool::RESULT_OK != result) { @@ -582,8 +789,13 @@ static dmExtension::Result Finalize(dmExtension::Params* params) return dmExtension::RESULT_OK; } +// called from wslay_callbacks and empscripten_callbacks Result PushMessage(WebsocketConnection* conn, MessageType type, int length, const uint8_t* buffer, uint16_t code) { + DM_MUTEX_SCOPED_LOCK(conn->m_Mutex); + // we should only push messages when in the connected or disconnecting state + assert((STATE_CONNECTED == conn->m_State) || (STATE_DISCONNECTING == conn->m_State)); + if (conn->m_Messages.Full()) conn->m_Messages.OffsetCapacity(4); @@ -610,25 +822,6 @@ Result PushMessage(WebsocketConnection* conn, MessageType type, int length, cons return dmWebsocket::RESULT_OK; } -// has the connect procedure taken too long? -static bool CheckConnectTimeout(WebsocketConnection* conn) -{ - uint64_t t = dmTime::GetTime(); - return t >= conn->m_ConnectTimeout; -} - -static void ConnectionWorker(void* _conn) -{ - WebsocketConnection* conn = (WebsocketConnection*)_conn; - dmSocket::Result sr; - dmConnectionPool::Result pool_result = dmConnectionPool::Dial(g_Websocket.m_Pool, conn->m_Url.m_Hostname, conn->m_Url.m_Port, conn->m_SSL, g_Websocket.m_Timeout, &conn->m_Connection, &sr); - if (dmConnectionPool::RESULT_OK != pool_result) - { - CLOSE_CONN("Failed to open connection: %s", dmSocket::ResultToString(sr)); - return; - } - SetState(conn, STATE_HANDSHAKE_WRITE); -} static dmExtension::Result OnUpdate(dmExtension::Params* params) { @@ -640,14 +833,19 @@ static dmExtension::Result OnUpdate(dmExtension::Params* params) if (STATE_DISCONNECTED == conn->m_State) { - if (RESULT_OK != conn->m_Status) { - HandleCallback(conn, EVENT_ERROR, 0, conn->m_BufferSize); - conn->m_BufferSize = 0; + DM_MUTEX_SCOPED_LOCK(conn->m_Mutex); + if (RESULT_OK != conn->m_Status) + { + HandleCallback(conn, EVENT_ERROR, 0, conn->m_BufferSize); + HandleCallback(conn, EVENT_DISCONNECTED, 0, 0); + } + else + { + HandleCallback(conn, EVENT_DISCONNECTED, 0, conn->m_BufferSize); + } } - HandleCallback(conn, EVENT_DISCONNECTED, 0, conn->m_BufferSize); - g_Websocket.m_Connections.EraseSwap(i); --i; --size; @@ -655,14 +853,12 @@ static dmExtension::Result OnUpdate(dmExtension::Params* params) } else if ((STATE_CONNECTED == conn->m_State) || (STATE_DISCONNECTING == conn->m_State)) { -#if defined(HAVE_WSLAY) - int r = WSL_Poll(conn->m_Ctx); - if (0 != r) + DM_MUTEX_SCOPED_LOCK(conn->m_Mutex); + // transitioned to the connected state? + if ((conn->m_State != conn->m_PreviousState) && (STATE_CONNECTED == conn->m_State)) { - CLOSE_CONN("Websocket closing for %s (%s)", conn->m_Url.m_Hostname, WSL_ResultToString(r)); - continue; + HandleCallback(conn, EVENT_CONNECTED, 0, 0); } -#endif uint32_t offset = 0; for (uint32_t i = 0; i < conn->m_Messages.Size(); ++i) @@ -685,133 +881,6 @@ static dmExtension::Result OnUpdate(dmExtension::Params* params) conn->m_Messages.SetSize(0); conn->m_BufferSize = 0; } - else if (STATE_HANDSHAKE_READ == conn->m_State) - { - if (CheckConnectTimeout(conn)) - { - CLOSE_CONN("Connect sequence timed out"); - continue; - } - - Result result = ReceiveHeaders(conn); - if (RESULT_WOULDBLOCK == result) - { - continue; - } - - if (RESULT_OK != result) - { - CLOSE_CONN("Failed receiving handshake headers. %d", result); - continue; - } - - // Verifies headers, and also stages any initial sent data - result = VerifyHeaders(conn); - if (RESULT_OK != result) - { - CLOSE_CONN("Failed verifying handshake headers:\n%s\n\n", conn->m_Buffer); - continue; - } - -#if defined(HAVE_WSLAY) - int r = WSL_Init(&conn->m_Ctx, g_Websocket.m_BufferSize, (void*)conn); - if (0 != r) - { - CLOSE_CONN("Failed initializing wslay: %s", WSL_ResultToString(r)); - continue; - } - - dmSocket::SetNoDelay(conn->m_Socket, true); - // Don't go lower than 1000 since some platforms might not have that good precision - dmSocket::SetReceiveTimeout(conn->m_Socket, 1000); - if (conn->m_SSLSocket) - dmSSLSocket::SetReceiveTimeout(conn->m_SSLSocket, 1000); - - dmSocket::SetBlocking(conn->m_Socket, false); -#endif - SetState(conn, STATE_CONNECTED); - HandleCallback(conn, EVENT_CONNECTED, 0, 0); - } - else if (STATE_HANDSHAKE_WRITE == conn->m_State) - { - if (CheckConnectTimeout(conn)) - { - CLOSE_CONN("Connect sequence timed out"); - continue; - } - - if (conn->m_ConnectionThread) - { - dmThread::Join(conn->m_ConnectionThread); - conn->m_ConnectionThread = 0; - } - conn->m_Socket = dmConnectionPool::GetSocket(g_Websocket.m_Pool, conn->m_Connection); - conn->m_SSLSocket = dmConnectionPool::GetSSLSocket(g_Websocket.m_Pool, conn->m_Connection); - Result result = SendClientHandshake(conn); - if (RESULT_WOULDBLOCK == result) - { - continue; - } - if (RESULT_OK != result) - { - CLOSE_CONN("Failed sending handshake: %d", result); - continue; - } - - SetState(conn, STATE_HANDSHAKE_READ); - } - else if (STATE_CREATE == conn->m_State) - { - if (CheckConnectTimeout(conn)) - { - CLOSE_CONN("Connect sequence timed out"); - continue; - } - -#if defined(__EMSCRIPTEN__) - char uri_buffer[dmURI::MAX_URI_LEN]; - const char* uri; - bool no_path = conn->m_Url.m_Path[0] == '\0'; - bool no_port = conn->m_Url.m_Port == -1; - if (no_path && no_port) - { - dmSnPrintf(uri_buffer, sizeof(uri_buffer), "%s://%s", conn->m_Url.m_Scheme, conn->m_Url.m_Hostname); - } - else if (no_port) - { - dmSnPrintf(uri_buffer, sizeof(uri_buffer), "%s://%s%s", conn->m_Url.m_Scheme, conn->m_Url.m_Hostname, conn->m_Url.m_Path); - } - else if (no_path) - { - dmSnPrintf(uri_buffer, sizeof(uri_buffer), "%s://%s:%d", conn->m_Url.m_Scheme, conn->m_Url.m_Hostname, conn->m_Url.m_Port); - } - else { - dmSnPrintf(uri_buffer, sizeof(uri_buffer), "%s://%s:%d%s", conn->m_Url.m_Scheme, conn->m_Url.m_Hostname, conn->m_Url.m_Port, conn->m_Url.m_Path); - } - uri = uri_buffer; - - EmscriptenWebSocketCreateAttributes ws_attrs = { - uri, - conn->m_Protocol, - EM_TRUE - }; - EMSCRIPTEN_WEBSOCKET_T ws = emscripten_websocket_new(&ws_attrs); - if (ws < 0) - { - CLOSE_CONN("Failed to connect to '%s:%d': %d", conn->m_Url.m_Hostname, (int)conn->m_Url.m_Port, ws); - continue; - } - conn->m_WS = ws; - - emscripten_websocket_set_onopen_callback(ws, conn, Emscripten_WebSocketOnOpen); - emscripten_websocket_set_onerror_callback(ws, conn, Emscripten_WebSocketOnError); - emscripten_websocket_set_onclose_callback(ws, conn, Emscripten_WebSocketOnClose); - emscripten_websocket_set_onmessage_callback(ws, conn, Emscripten_WebSocketOnMessage); -#else - conn->m_ConnectionThread = dmThread::New((dmThread::ThreadStart)ConnectionWorker, 0x80000, conn, "WSConnect"); -#endif - SetState(conn, STATE_CONNECTING); - } else if (STATE_CONNECTING == conn->m_State) { #if defined(__EMSCRIPTEN__) @@ -822,6 +891,8 @@ static dmExtension::Result OnUpdate(dmExtension::Params* params) } #endif } + + conn->m_PreviousState = conn->m_State; } return dmExtension::RESULT_OK; diff --git a/websocket/src/websocket.h b/websocket/src/websocket.h index cd7d1cb..36c5aa8 100644 --- a/websocket/src/websocket.h +++ b/websocket/src/websocket.h @@ -37,9 +37,15 @@ namespace dmWebsocket // Maximum time to wait for a socket static const int SOCKET_WAIT_TIMEOUT = 4*1000; + enum DebugLevel + { + DEBUG_DISABLED, + DEBUG_STATE_CHANGES, + DEBUG_VERBOSE + }; + enum State { - STATE_CREATE, STATE_CONNECTING, STATE_HANDSHAKE_WRITE, STATE_HANDSHAKE_READ, @@ -121,11 +127,13 @@ namespace dmWebsocket dmSSLSocket::Socket m_SSLSocket; dmThread::Thread m_ConnectionThread; dmArray m_Messages; // lengths of the messages in the data buffer + dmMutex::HMutex m_Mutex; // lock m_Messages and m_Buffer uint64_t m_ConnectTimeout; uint8_t m_Key[16]; const char* m_Protocol; const char* m_CustomHeaders; State m_State; + State m_PreviousState; char* m_Buffer; int m_BufferSize; uint32_t m_BufferCapacity; diff --git a/websocket/src/wslay_callbacks.cpp b/websocket/src/wslay_callbacks.cpp index 21ba816..0d648dd 100644 --- a/websocket/src/wslay_callbacks.cpp +++ b/websocket/src/wslay_callbacks.cpp @@ -67,6 +67,7 @@ int WSL_Poll(wslay_event_context_ptr ctx) ssize_t WSL_RecvCallback(wslay_event_context_ptr ctx, uint8_t *buf, size_t len, int flags, void *user_data) { WebsocketConnection* conn = (WebsocketConnection*)user_data; + DM_MUTEX_SCOPED_LOCK(conn->m_Mutex); int r = -1; // received bytes if >=0, error if < 0 @@ -99,6 +100,7 @@ ssize_t WSL_RecvCallback(wslay_event_context_ptr ctx, uint8_t *buf, size_t len, ssize_t WSL_SendCallback(wslay_event_context_ptr ctx, const uint8_t *data, size_t len, int flags, void *user_data) { WebsocketConnection* conn = (WebsocketConnection*)user_data; + DM_MUTEX_SCOPED_LOCK(conn->m_Mutex); int sent_bytes = 0; dmSocket::Result socket_result = Send(conn, (const char*)data, len, &sent_bytes); @@ -118,10 +120,13 @@ ssize_t WSL_SendCallback(wslay_event_context_ptr ctx, const uint8_t *data, size_ void WSL_OnMsgRecvCallback(wslay_event_context_ptr ctx, const struct wslay_event_on_msg_recv_arg *arg, void *user_data) { WebsocketConnection* conn = (WebsocketConnection*)user_data; + DM_MUTEX_SCOPED_LOCK(conn->m_Mutex); + if (arg->opcode == WSLAY_TEXT_FRAME || arg->opcode == WSLAY_BINARY_FRAME) { PushMessage(conn, MESSAGE_TYPE_NORMAL, arg->msg_length, arg->msg, 0); - } else if (arg->opcode == WSLAY_CONNECTION_CLOSE) + } + else if (arg->opcode == WSLAY_CONNECTION_CLOSE) { // The first two bytes is the close code const uint8_t* reason = (const uint8_t*)""; @@ -142,8 +147,7 @@ void WSL_OnMsgRecvCallback(wslay_event_context_ptr ctx, const struct wslay_event wslay_event_queue_close(ctx, arg->status_code, (const uint8_t*)buffer, len); } - DebugLog(1, "%s", buffer); - + DebugLog(dmWebsocket::DEBUG_STATE_CHANGES, "%s", buffer); } }