agi.websocket.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. package agi
  2. import (
  3. "fmt"
  4. "net/http"
  5. "sync"
  6. "sync/atomic"
  7. "time"
  8. "github.com/gorilla/websocket"
  9. "github.com/robertkrimen/otto"
  10. uuid "github.com/satori/go.uuid"
  11. "imuslab.com/arozos/mod/info/logger"
  12. user "imuslab.com/arozos/mod/user"
  13. )
  14. /*
  15. AJGI WebSocket Request Library
  16. This is a library for allowing AGI based connection upgrade to WebSocket.
  17. Different from other agi modules, this does not use the register lib interface
  18. due to its special nature.
  19. New functions exposed to AGI scripts:
  20. websocket.upgrade(timeoutSec)
  21. Upgrades the connection and overrides delay() with a message-pumping version
  22. so that websocket.onMessage fires naturally during pauses.
  23. websocket.send(text) --> bool
  24. websocket.read(timeoutMs?) --> string | null | false
  25. timeoutMs = 0 / omitted --> block until message arrives or connection closes
  26. timeoutMs > 0 --> return null on timeout (connection still open)
  27. returns false --> connection is closed
  28. websocket.available() --> int
  29. Number of messages currently waiting in the inbound buffer. Non-blocking.
  30. websocket.isClosed() --> bool
  31. true when the connection is no longer active.
  32. websocket.onMessage --> assign function(msg) to receive messages
  33. msg = { data: string, timestamp: int64 ms, type: int }
  34. Fired inside delay() on the script's own goroutine — Otto-safe.
  35. websocket.close()
  36. Author: tobychui
  37. */
  38. var upgrader = websocket.Upgrader{
  39. ReadBufferSize: 1024,
  40. WriteBufferSize: 1024,
  41. CheckOrigin: func(r *http.Request) bool {
  42. return true
  43. },
  44. }
  45. // wsMsg is a single inbound WebSocket frame delivered to the AGI script.
  46. type wsMsg struct {
  47. Data string // text payload
  48. Timestamp int64 // arrival time as unix milliseconds
  49. Type int // gorilla message type (1 = text, 2 = binary)
  50. }
  51. // wsConn wraps a gorilla websocket.Conn with a buffered inbound message channel.
  52. // All fields shared across goroutines are accessed via atomics or the channel itself.
  53. // The Otto VM is NEVER touched from any goroutine other than the main script goroutine.
  54. type wsConn struct {
  55. conn *websocket.Conn
  56. msgChan chan wsMsg // filled by the background reader goroutine
  57. closed int32 // 1 when closed; use atomic load/store
  58. lastOprTime int64 // unix seconds of last activity; use atomic load/store
  59. }
  60. func newWsConn(c *websocket.Conn) *wsConn {
  61. wsc := &wsConn{
  62. conn: c,
  63. msgChan: make(chan wsMsg, 128),
  64. }
  65. atomic.StoreInt64(&wsc.lastOprTime, time.Now().Unix())
  66. return wsc
  67. }
  68. func (w *wsConn) isClosed() bool { return atomic.LoadInt32(&w.closed) == 1 }
  69. func (w *wsConn) markClosed() { atomic.StoreInt32(&w.closed, 1) }
  70. func (w *wsConn) touchLastOpr() { atomic.StoreInt64(&w.lastOprTime, time.Now().Unix()) }
  71. func (w *wsConn) getLastOpr() int64 { return atomic.LoadInt64(&w.lastOprTime) }
  72. var connections = sync.Map{}
  73. // checkWebSocketConnectionUpgradeStatus returns whether the current VM has an
  74. // active WebSocket connection. Returns (active, connID, *wsConn).
  75. func checkWebSocketConnectionUpgradeStatus(vm *otto.Otto) (bool, string, *wsConn) {
  76. value, err := vm.Get("_websocket_conn_id")
  77. if err != nil || value.IsUndefined() || value.IsNull() {
  78. return false, "", nil
  79. }
  80. connId, err := value.ToString()
  81. if err != nil || connId == "" {
  82. return false, "", nil
  83. }
  84. raw, ok := connections.Load(connId)
  85. if !ok {
  86. return false, "", nil
  87. }
  88. wsc := raw.(*wsConn)
  89. if wsc.isClosed() {
  90. return false, connId, nil
  91. }
  92. return true, connId, wsc
  93. }
  94. // cleanupWsConn sends a close frame, closes the raw connection, and removes the
  95. // connection from both the sync.Map and the VM.
  96. // MUST be called from the main (script) goroutine so vm.Set is safe.
  97. func cleanupWsConn(vm *otto.Otto, connID string, wsc *wsConn) {
  98. if !wsc.isClosed() {
  99. wsc.markClosed()
  100. wsc.conn.WriteMessage(
  101. websocket.CloseMessage,
  102. websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""),
  103. )
  104. time.Sleep(150 * time.Millisecond)
  105. wsc.conn.Close()
  106. }
  107. vm.Set("_websocket_conn_id", otto.UndefinedValue())
  108. connections.Delete(connID)
  109. }
  110. // dispatchOnMessage calls websocket.onMessage(msg) on the script goroutine.
  111. // Passing data through vm.Set avoids the complexities of otto.Value.Call.
  112. // MUST be called from the main (script) goroutine.
  113. func dispatchOnMessage(vm *otto.Otto, msg wsMsg) {
  114. vm.Set("_ws_incoming", map[string]interface{}{
  115. "data": msg.Data,
  116. "timestamp": msg.Timestamp,
  117. "type": msg.Type,
  118. })
  119. if _, err := vm.Run(`
  120. if (typeof websocket !== 'undefined' && typeof websocket.onMessage === 'function') {
  121. websocket.onMessage(_ws_incoming);
  122. }
  123. `); err != nil {
  124. logger.PrintAndLog("Agi", fmt.Sprint("*AGI WebSocket* onMessage handler error:", err), nil)
  125. }
  126. vm.Set("_ws_incoming", otto.UndefinedValue())
  127. }
  128. func (g *Gateway) injectWebSocketFunctions(vm *otto.Otto, u *user.User, w http.ResponseWriter, r *http.Request) {
  129. // ── websocket.upgrade(timeoutSeconds) ────────────────────────────────────
  130. vm.Set("_websocket_upgrade", func(call otto.FunctionCall) otto.Value {
  131. timeout, err := call.Argument(0).ToInteger()
  132. if err != nil || timeout <= 0 {
  133. timeout = 300
  134. }
  135. if connState, _, _ := checkWebSocketConnectionUpgradeStatus(vm); connState {
  136. return otto.TrueValue() // already upgraded
  137. }
  138. c, err := upgrader.Upgrade(w, r, nil)
  139. if err != nil {
  140. logger.PrintAndLog("Agi", fmt.Sprint("*AGI WebSocket* upgrade failed:", err), nil)
  141. return otto.FalseValue()
  142. }
  143. wsc := newWsConn(c)
  144. connUUID := uuid.NewV4().String()
  145. connections.Store(connUUID, wsc)
  146. vm.Set("_websocket_conn_id", connUUID)
  147. // Background reader — feeds all inbound frames into msgChan.
  148. // Never touches the Otto VM; only updates wsc atomics and the channel.
  149. go func() {
  150. defer func() {
  151. wsc.markClosed()
  152. close(wsc.msgChan)
  153. // Do NOT call vm.Set here — Otto is not goroutine-safe.
  154. }()
  155. for {
  156. msgType, message, err := c.ReadMessage()
  157. if err != nil {
  158. return // connection closed or error
  159. }
  160. wsc.touchLastOpr()
  161. select {
  162. case wsc.msgChan <- wsMsg{
  163. Data: string(message),
  164. Timestamp: time.Now().UnixMilli(),
  165. Type: msgType,
  166. }:
  167. default:
  168. logger.PrintAndLog("Agi", "*AGI WebSocket* inbound buffer full, dropping frame", nil)
  169. }
  170. }
  171. }()
  172. // Idle-timeout watcher — closes the raw connection when no activity.
  173. // Does NOT touch the VM; closing the connection causes the reader to exit.
  174. go func() {
  175. ticker := time.NewTicker(1 * time.Second)
  176. defer ticker.Stop()
  177. for range ticker.C {
  178. if wsc.isClosed() {
  179. return
  180. }
  181. if time.Now().Unix()-wsc.getLastOpr() > timeout {
  182. logger.PrintAndLog("Agi", "*AGI WebSocket* idle timeout — closing connection", nil)
  183. c.Close()
  184. return
  185. }
  186. }
  187. }()
  188. // Override delay() so that websocket.onMessage callbacks fire naturally
  189. // inside pauses without the user needing an explicit poll() call.
  190. vm.Run(`
  191. var _ws_orig_delay = (typeof delay === 'function') ? delay : function(ms){};
  192. delay = function(ms){ _websocket_pump_messages(ms); };
  193. `)
  194. return otto.TrueValue()
  195. })
  196. // ── websocket.send(text) ─────────────────────────────────────────────────
  197. vm.Set("_websocket_send", func(call otto.FunctionCall) otto.Value {
  198. content, err := call.Argument(0).ToString()
  199. if err != nil {
  200. g.RaiseError(err)
  201. return otto.FalseValue()
  202. }
  203. connState, connID, wsc := checkWebSocketConnectionUpgradeStatus(vm)
  204. if !connState {
  205. return otto.FalseValue()
  206. }
  207. if err := wsc.conn.WriteMessage(websocket.TextMessage, []byte(content)); err != nil {
  208. cleanupWsConn(vm, connID, wsc)
  209. return otto.FalseValue()
  210. }
  211. wsc.touchLastOpr()
  212. return otto.TrueValue()
  213. })
  214. // ── websocket.read(timeout ms) ───────────────────────────────────────────
  215. // timeoutMs = 0 or omitted --> block until a message arrives or socket closes
  216. // timeoutMs > 0 --> return null if no message within that many ms
  217. // Returns: string on message · null on timeout · false if connection closed
  218. vm.Set("_websocket_read", func(call otto.FunctionCall) otto.Value {
  219. timeoutMs, _ := call.Argument(0).ToInteger()
  220. connState, connID, wsc := checkWebSocketConnectionUpgradeStatus(vm)
  221. if !connState {
  222. if connID != "" {
  223. // Stale entry — tidy up from the main goroutine
  224. vm.Set("_websocket_conn_id", otto.UndefinedValue())
  225. connections.Delete(connID)
  226. }
  227. return otto.FalseValue()
  228. }
  229. var msg wsMsg
  230. var ok bool
  231. if timeoutMs > 0 {
  232. select {
  233. case msg, ok = <-wsc.msgChan:
  234. case <-time.After(time.Duration(timeoutMs) * time.Millisecond):
  235. return otto.NullValue() // timed out; connection still open
  236. }
  237. } else {
  238. msg, ok = <-wsc.msgChan // block until message or channel close
  239. }
  240. if !ok {
  241. // Channel closed — background reader exited (connection gone)
  242. cleanupWsConn(vm, connID, wsc)
  243. return otto.FalseValue()
  244. }
  245. wsc.touchLastOpr()
  246. v, err := otto.ToValue(msg.Data)
  247. if err != nil {
  248. return otto.NullValue()
  249. }
  250. return v
  251. })
  252. // ── websocket.available() ────────────────────────────────────────────────
  253. // Returns the number of messages currently waiting in the inbound buffer.
  254. // Non-blocking — safe to poll in a tight loop.
  255. vm.Set("_websocket_available", func(call otto.FunctionCall) otto.Value {
  256. _, _, wsc := checkWebSocketConnectionUpgradeStatus(vm)
  257. count := 0
  258. if wsc != nil {
  259. count = len(wsc.msgChan)
  260. }
  261. v, _ := otto.ToValue(count)
  262. return v
  263. })
  264. // ── websocket.isClosed() ─────────────────────────────────────────────────
  265. // Returns true when the WebSocket connection is no longer active.
  266. vm.Set("_websocket_is_closed", func(call otto.FunctionCall) otto.Value {
  267. connState, _, _ := checkWebSocketConnectionUpgradeStatus(vm)
  268. if connState {
  269. return otto.FalseValue()
  270. }
  271. return otto.TrueValue()
  272. })
  273. // ── _websocket_pump_messages(ms) ─────────────────────────────────────────
  274. // Replaces delay() after upgrade. Sleeps for ms milliseconds while
  275. // dispatching any queued messages to websocket.onMessage.
  276. // All JS execution happens on this (main script) goroutine — Otto-safe.
  277. //
  278. // Important: messages are only consumed from the buffer when onMessage is
  279. // actually a function. When it is null/undefined the function falls back to
  280. // a plain sleep so that websocket.available() / websocket.read() can still
  281. // see the queued frames afterwards (Mode 2 / manual-read patterns).
  282. vm.Set("_websocket_pump_messages", func(call otto.FunctionCall) otto.Value {
  283. ms, err := call.Argument(0).ToInteger()
  284. if err != nil || ms < 0 {
  285. ms = 0
  286. }
  287. _, _, wsc := checkWebSocketConnectionUpgradeStatus(vm)
  288. if wsc == nil {
  289. // No active WebSocket — fall back to plain sleep
  290. if ms > 0 {
  291. time.Sleep(time.Duration(ms) * time.Millisecond)
  292. }
  293. return otto.UndefinedValue()
  294. }
  295. // Check once whether a handler is registered. We evaluate in JS so
  296. // that the typeof check is unambiguous regardless of Otto internals.
  297. hasHandlerVal, _ := vm.Run(`typeof websocket !== 'undefined' && typeof websocket.onMessage === 'function'`)
  298. hasHandler, _ := hasHandlerVal.ToBoolean()
  299. if !hasHandler {
  300. // No handler — plain sleep; leave messages in the buffer untouched.
  301. if ms > 0 {
  302. time.Sleep(time.Duration(ms) * time.Millisecond)
  303. }
  304. return otto.UndefinedValue()
  305. }
  306. const tickSize = 20 * time.Millisecond
  307. deadline := time.Now().Add(time.Duration(ms) * time.Millisecond)
  308. for {
  309. remaining := time.Until(deadline)
  310. if remaining <= 0 {
  311. break
  312. }
  313. tick := tickSize
  314. if remaining < tick {
  315. tick = remaining
  316. }
  317. select {
  318. case msg, ok := <-wsc.msgChan:
  319. if !ok {
  320. // Connection closed while waiting — stop pumping
  321. return otto.UndefinedValue()
  322. }
  323. wsc.touchLastOpr()
  324. dispatchOnMessage(vm, msg)
  325. case <-time.After(tick):
  326. // No message in this 20 ms slice — keep waiting
  327. }
  328. }
  329. return otto.UndefinedValue()
  330. })
  331. // ── websocket.close() ────────────────────────────────────────────────────
  332. vm.Set("_websocket_close", func(call otto.FunctionCall) otto.Value {
  333. connState, connID, wsc := checkWebSocketConnectionUpgradeStatus(vm)
  334. if !connState {
  335. return otto.FalseValue()
  336. }
  337. cleanupWsConn(vm, connID, wsc)
  338. return otto.TrueValue()
  339. })
  340. // ── JS wrapper ───────────────────────────────────────────────────────────
  341. vm.Run(`
  342. var websocket = {};
  343. websocket.upgrade = _websocket_upgrade;
  344. websocket.send = _websocket_send;
  345. websocket.read = _websocket_read;
  346. websocket.close = _websocket_close;
  347. websocket.available = _websocket_available;
  348. websocket.isClosed = _websocket_is_closed;
  349. // Assign a function(msg) here to receive messages asynchronously.
  350. // msg = { data: string, timestamp: number, type: number }
  351. // The handler fires inside delay() after websocket.upgrade() is called.
  352. websocket.onMessage = null;
  353. `)
  354. }