-
Notifications
You must be signed in to change notification settings - Fork 2
add WebSocket endpoint support #66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
005e946
6fa0377
1d9be51
37b4ea0
6ea9e7d
66655c3
df16361
39a8fee
ac575d0
3fb6208
9d2d81b
5d079af
ca99d38
76c7679
ee79a82
39a960a
68d4b32
e87eff2
dc49adf
6edc04b
d9b2c14
ae4d794
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -61,27 +61,33 @@ | |
| ); | ||
| def i: Int; | ||
| for i = 0, i < elements.getLength(), ++i { | ||
| def uriParams: Array[String] = getModifierParams(elements(i), "uiEndpoint", "منفذ_مرئي"); | ||
| def uriParams: Array[String]; | ||
| extractModifierParams(elements(i), "uiEndpoint", "منفذ_مرئي", uriParams); | ||
| if uriParams.getLength() < 1 { | ||
| System.fail(1, "Invalid @uiEndpoint params"); | ||
| } | ||
|
|
||
| def titleParams: Array[String] = getModifierParams(elements(i), "title", "عنوان"); | ||
| def titleParams: Array[String]; | ||
| extractModifierParams(elements(i), "title", "عنوان", titleParams); | ||
| def title: String; | ||
| if titleParams.getLength() >= 1 title = titleParams(0) else title = "Alusus WebPlatform"; | ||
|
|
||
| def iconParams: Array[String] = getModifierParams(elements(i), "icon", "أيقونة"); | ||
| def iconParams: Array[String]; | ||
| extractModifierParams(elements(i), "icon", "أيقونة", iconParams); | ||
| def icon: String; | ||
| if iconParams.getLength() >= 1 icon = iconParams(0); | ||
|
|
||
| def appParams: Array[String] = getModifierParams(elements(i), "webApp", "تطبيق_ويب"); | ||
| def appParams: Array[String]; | ||
| extractModifierParams(elements(i), "webApp", "تطبيق_ويب" , appParams); | ||
| def appManifest: String; | ||
| if appParams.getLength() > 0 appManifest = appParams(0); | ||
| def appVersion: String; | ||
| if appParams.getLength() > 1 appVersion = appParams(1) else appVersion = "v1"; | ||
|
|
||
| def preCacheFilenames: Array[String] = getModifierParams(elements(i), "preCache", "خزن_مسبق"); | ||
| def dynCacheFilenames: Array[String] = getModifierParams(elements(i), "dynCache", "خزن_تفاعلي"); | ||
| def preCacheFilenames : Array[String]; | ||
| extractModifierParams(elements(i), "preCache", "خزن_مسبق", preCacheFilenames); | ||
| def dynCacheFilenames: Array[String]; | ||
| extractModifierParams(elements(i), "dynCache", "خزن_تفاعلي", dynCacheFilenames); | ||
|
|
||
| generateUiEndpointFiles( | ||
| elements(i), uriParams(0), title, icon, appManifest, appVersion, preCacheFilenames, dynCacheFilenames, | ||
|
|
@@ -215,7 +221,8 @@ | |
| ); | ||
| def i: Int; | ||
| for i = 0, i < elements.getLength(), ++i { | ||
| def uriParams: Array[String] = getModifierParams(elements(i), "uiEndpoint", "منفذ_مرئي"); | ||
| def uriParams: Array[String]; | ||
| extractModifierParams(elements(i), "uiEndpoint", "منفذ_مرئي" ,uriParams); | ||
| if uriParams.getLength() < 1 { | ||
| System.fail(1, "Invalid @uiEndpoint params"); | ||
| } | ||
|
|
@@ -231,18 +238,12 @@ | |
| } | ||
|
|
||
| func generateBeEndpointsCalls (parent: ref[Core.Basic.TiObject]) { | ||
| def elements: Array[ref[Core.Basic.TiObject]] = Spp.astMgr.findElements( | ||
| ast { modifier == "beEndpoint" || modifier == "منفذ_بياني" }, | ||
| parent, | ||
| Spp.SeekerFlags.SKIP_OWNERS | Spp.SeekerFlags.SKIP_USES | Spp.SeekerFlags.SKIP_CHILDREN | ||
| ); | ||
| def elements : Array[ref[Core.Basic.TiObject]] = extractFunctionElements(parent, "beEndpoint", "منفذ_بياني"); | ||
| def i: Int; | ||
| for i = 0, i < elements.getLength(), ++i { | ||
| def modifier: ref[Core.Basic.TiObject](Spp.astMgr.findModifierForElement(elements(i), "beEndpoint")); | ||
| if modifier~ptr == 0 modifier~no_deref = Spp.astMgr.findModifierForElement(elements(i), "منفذ_بياني"); | ||
| def endpointParams: Array[String]; | ||
| if !Spp.astMgr.getModifierStringParams(modifier, endpointParams) | ||
| || endpointParams.getLength() < 2 { | ||
| extractModifierParams(elements(i), "beEndpoint", "منفذ_بياني" , endpointParams); | ||
| if endpointParams.getLength() < 2 { | ||
| System.fail(1, "Invalid BE endpoint params"); | ||
| } | ||
| Spp.astMgr.insertAst( | ||
|
|
@@ -275,7 +276,102 @@ | |
| ); | ||
| } | ||
| } | ||
| func generateWebSocketEndpointChecks (parent: ref[Core.Basic.TiObject]) { | ||
| def classElements: Array[ref[Core.Basic.TiObject]] = Spp.astMgr.findElements( | ||
| ast { modifier == "wsEndpoint" || modifier == "منفذ_مقبس" }, | ||
| parent, | ||
| Spp.SeekerFlags.SKIP_OWNERS | Spp.SeekerFlags.SKIP_USES | Spp.SeekerFlags.SKIP_CHILDREN | ||
| ); | ||
| def i: Int; | ||
| for i = 0, i < classElements.getLength(), ++i { | ||
| def modifier: ref[Core.Basic.TiObject](Spp.astMgr.findModifierForElement(classElements(i), "wsEndpoint")); | ||
| if modifier~ptr == 0 modifier~no_deref = Spp.astMgr.findModifierForElement(classElements(i), "منفذ_مقبس"); | ||
|
|
||
| def wsEndpointParam: Array[String]; | ||
|
|
||
| if !Spp.astMgr.getModifierStringParams(modifier, wsEndpointParam) | ||
| || wsEndpointParam.getLength() != 1 { | ||
| System.fail(1, "Invalid WS endpoint params"); | ||
| } | ||
| Spp.astMgr.insertAst( | ||
| ast { | ||
| if String.isEqual(method, "GET") && String.isEqual(uri, "{{wsEndpointUri}}") { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are we checking specifically for GET requests, rather than all requests hitting this URI?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
So whenever a client wants to start a WebSocket connection, it sends an HTTP GET request to begin the handshake — not any other HTTP method. This is also mandated by RFC 6455 3.1 . |
||
| return 0; | ||
| } | ||
| }, | ||
| AstTemplateMap() | ||
| .set(Srl.String("wsEndpointUri"), Core.Basic.TiStr(wsEndpointParam(0))) | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| function generateWebSockets(modulesRef: ref[Core.Basic.TiObject]) { | ||
| def webSocketClasses : Array[ref[Core.Basic.TiObject]] = extractTypeElements(modulesRef, "wsEndpoint", "منفذ_مقبس"); | ||
|
|
||
| if (webSocketClasses.getLength() > 0) { | ||
| // register a single set of free-function trampolines that forward each | ||
| // callback to the matching bound method on the instance stored in userData, | ||
| // since civetweb's C callbacks can't carry an instance pointer directly | ||
| Spp.astMgr.insertAst( | ||
| ast { | ||
| func trampolineConnection (connection: ptr[Http.Connection], userData: ptr[Void]): Int { | ||
| def rc: ptr[RefCounter] = userData~cast[ptr[RefCounter]]; | ||
| def instance: SrdRef[WsRoute](rc~cnt, rc~cnt.managedObj~cast[ptr[WsRoute]]~cnt); | ||
| return instance.onConnectWrapper(connection, userData); | ||
| } | ||
| func trampolineReady (connection: ptr[Http.Connection], userData: ptr[Void]): Void { | ||
| def rc: ptr[RefCounter] = userData~cast[ptr[RefCounter]]; | ||
| def instance: SrdRef[WsRoute](rc~cnt, rc~cnt.managedObj~cast[ptr[WsRoute]]~cnt); | ||
| instance.onReadyWrapper(connection, userData); | ||
| } | ||
| func trampolineData (connection: ptr[Http.Connection], bits: Int, data: CharsPtr, dataLen: ArchWord, userData: ptr[Void]): Int { | ||
| def rc: ptr[RefCounter] = userData~cast[ptr[RefCounter]]; | ||
| def instance: SrdRef[WsRoute](rc~cnt, rc~cnt.managedObj~cast[ptr[WsRoute]]~cnt); | ||
| return instance.onDataWrapper(connection, bits, data, dataLen, userData); | ||
| } | ||
| func trampolineClose (connection: ptr[Http.Connection], userData: ptr[Void]): Void { | ||
| def rc: ptr[RefCounter] = userData~cast[ptr[RefCounter]]; | ||
| def instance: SrdRef[WsRoute](rc~cnt, rc~cnt.managedObj~cast[ptr[WsRoute]]~cnt); | ||
| instance.onCloseWrapper(connection, userData); | ||
| } | ||
| }, | ||
| AstTemplateMap() | ||
| ); | ||
| } | ||
|
|
||
| def i : Int; | ||
| for i = 0 , i < webSocketClasses.getLength(), i++ { | ||
| def params : Array[String]; | ||
| extractModifierParams(webSocketClasses(i) , "wsEndpoint", "منفذ_مقبس" , params); | ||
|
|
||
| if (params.getLength() != 1) { | ||
| System.fail(1 , "where is the endpoint"); | ||
| } | ||
|
|
||
|
|
||
| Spp.astMgr.insertAst( | ||
| ast { | ||
| def instance: SrdRef[webSocketClass] = SrdRef[webSocketClass].construct(); | ||
| instance.wkThis.assign(instance); | ||
|
|
||
| // keep this route instance alive for the server's lifetime | ||
| instance.refCounter.count++; | ||
| Http.setWebSocketHandler( | ||
| httpContext, | ||
| "{{wsEndpointUri}}", | ||
| trampolineConnection~ptr, | ||
| trampolineReady~ptr, | ||
| trampolineData~ptr, | ||
| trampolineClose~ptr, | ||
| instance.refCounter~ptr~cast[ptr[Void]] | ||
| ) | ||
| }, | ||
| AstTemplateMap() | ||
| .set(Srl.String("wsEndpointUri"), Core.Basic.TiStr(params(0))) | ||
| .set(Srl.String("webSocketClass"), constructElementFullReferenceByName(webSocketClasses(i))) | ||
| ); | ||
| } | ||
| } | ||
| // Querying Functions | ||
|
|
||
| func getAssetsRoutesFromModules (modulesRef: ref[Core.Basic.TiObject]): Array[StaticRoute] { | ||
|
|
@@ -326,6 +422,31 @@ | |
| func startServer [modulesRef: ast_ref = Root] ( | ||
| mainAssetsPath: String, uiEndpointsPath: String, options: Array[CharsPtr], useSrcAssetsPath: Bool | ||
| ): ptr[ServerSession] { | ||
|
|
||
| // Bounds how long a single write (over any TCP connection this | ||
| // server handles — HTTP or WebSocket) can block on an | ||
| // unresponsive/dead peer before giving up — without this, a write can hang for a very long | ||
| // time (OS-level TCP timeout) if left unset. | ||
| if !(hasOption(options , "request_timeout_ms")) { | ||
| options.add("request_timeout_ms"); | ||
| options.add("5000"); | ||
| } | ||
|
|
||
| // How long the read loop waits for incoming data before sending a PING | ||
| // falls back to request_timeout_ms if unset. Used by CivetWeb to detect and | ||
| // close unresponsive connections. | ||
| if !(hasOption(options , "websocket_timeout_ms")) { | ||
| options.add("websocket_timeout_ms"); | ||
| options.add("10000"); | ||
| } | ||
|
|
||
| // Enables CivetWeb's built-in ping/pong handling for WebSocket | ||
| // connections: it auto-replies to client PING frames with PONG | ||
| // and filters PONG frames before they reach our data_handler, | ||
| // so we don't need to implement this ourselves. | ||
| options.add("enable_websocket_ping_pong"); | ||
| options.add("yes"); | ||
|
|
||
| // Http library requires that the last options argument is a zero, to denote | ||
| // the end of options. | ||
| options.add(0); | ||
|
|
@@ -351,6 +472,15 @@ | |
| session~cnt~init(); | ||
| session~cnt.requestCallbackContext = requestCallbackContext; | ||
| session~cnt.httpContext = httpContext; | ||
| preprocess { | ||
| def modules: Array[ref[Core.Basic.TiObject]] = getAllModules(modulesRef~ast); | ||
| if modules.getLength() == 0 { | ||
| Spp.buildMgr.raiseBuildNotice("SPPA1002", 1, modulesRef~ast); | ||
| } | ||
| def i: Int; | ||
| for i = 0, i < modules.getLength(), ++i generateWebSockets(modules(i)); | ||
| }; | ||
|
|
||
| return session; | ||
| } | ||
|
|
||
|
|
@@ -414,7 +544,10 @@ | |
| Spp.buildMgr.raiseBuildNotice("SPPA1002", 1, modulesRef~ast); | ||
| } | ||
| def i: Int; | ||
| for i = 0, i < modules.getLength(), ++i generateBeEndpointsCalls(modules(i)); | ||
| for i = 0, i < modules.getLength(), ++i { | ||
| generateBeEndpointsCalls(modules(i)); | ||
| generateWebSocketEndpointChecks(modules(i)); | ||
| }; | ||
| }; | ||
|
|
||
| if String.isEqual(method, "GET") { | ||
|
|
@@ -474,31 +607,76 @@ | |
|
|
||
| // Helpers | ||
|
|
||
| func hasOption (options: ref[Array[CharsPtr]], key: CharsPtr): Bool { | ||
| def i: Int; | ||
| // options is a flat key/value list — check every even index (just the keys) | ||
| for i = 0, i < options.getLength(), i += 2 { | ||
| if String.isEqual(options(i), key){ | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| function extractFunctionElements (modulesRef: ref[Core.Basic.TiObject], enName: CharsPtr, arName: CharsPtr): Array[ref[Core.Basic.TiObject]] { | ||
| def translations: Map[String, String]; | ||
| translations.set(String(arName), String(enName)); | ||
| return Spp.astMgr.findElements( | ||
| ast { elementType == "function" }, | ||
| modulesRef, | ||
| Spp.SeekerFlags.SKIP_OWNERS | Spp.SeekerFlags.SKIP_USES | Spp.SeekerFlags.SKIP_CHILDREN, | ||
| enName, | ||
| translations | ||
| ); | ||
| } | ||
|
|
||
| function extractTypeElements (modulesRef: ref[Core.Basic.TiObject], enName: CharsPtr, arName: CharsPtr): Array[ref[Core.Basic.TiObject]] { | ||
| def translations: Map[String, String]; | ||
| translations.set(String(arName), String(enName)); | ||
| return Spp.astMgr.findElements( | ||
| ast { elementType == "type" }, | ||
| modulesRef, | ||
| Spp.SeekerFlags.SKIP_OWNERS | Spp.SeekerFlags.SKIP_USES | Spp.SeekerFlags.SKIP_CHILDREN, | ||
| enName, | ||
| translations | ||
| ); | ||
| } | ||
|
|
||
| function extractModifierParams (elementRef : ref[Core.Basic.TiObject], enKwd : CharsPtr, arKwd : CharsPtr, params : ref[Array[String]]){ | ||
|
|
||
| def translations: Map[String, String]; | ||
| translations.set(String(arKwd), String(enKwd)); | ||
|
|
||
| def modifier: ref[Core.Basic.TiObject](Spp.astMgr.findModifierForElement(elementRef, enKwd,translations)); | ||
| Spp.astMgr.getModifierStringParams(modifier, params); | ||
| } | ||
|
|
||
| // refer to an AST element using its pointer instead of referring to that element using an identifier | ||
| // makes generating code dynamically easier | ||
| func constructElementFullReference (element: ref[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { | ||
| return Core.Basic.newSrdObj[Core.Data.Ast.Passage].{ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
| Core.Basic.BindingOf[this].setMember("target", element); | ||
| }; | ||
| } | ||
|
|
||
| func constructElementFullReferenceByName (element: ref[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { | ||
| def node: ref[Core.Data.Node](castRef[element, Core.Data.Node]); | ||
| if node.owner~ptr == 0 return SrdRef[Core.Basic.TiObject](); | ||
| def name: String = Spp.astMgr.getDefinitionName(node); | ||
| def identifier: SrdRef[Core.Data.Ast.Identifier] = Core.Basic.newSrdObj[Core.Data.Ast.Identifier].{ | ||
| value.value = name; | ||
| }; | ||
| def ownerRef: SrdRef[Core.Basic.TiObject] = constructElementFullReference(node.owner.owner); | ||
| def ownerRef: SrdRef[Core.Basic.TiObject] = constructElementFullReferenceByName(node.owner.owner); | ||
| if ownerRef.isNull() return identifier | ||
| else return Core.Basic.newSrdObj[Core.Data.Ast.LinkOperator].{ | ||
| Core.Basic.BindingOf[this].setMember("type", Core.Basic.TiStr(".")); | ||
| Core.Basic.MapContainerOf[this].{ | ||
| setElement("first", ownerRef); | ||
| setElement("second", identifier); | ||
| }; | ||
| }; | ||
| } | ||
|
|
||
| func getModifierParams (element: ref[Core.Basic.TiObject], enKwd: CharsPtr, arKwd: CharsPtr): Array[String] { | ||
| def modifier: ref[Core.Basic.TiObject](Spp.astMgr.findModifierForElement(element, enKwd)); | ||
| if modifier~ptr == 0 modifier~no_deref = Spp.astMgr.findModifierForElement(element, arKwd); | ||
| def endpointParams: Array[String]; | ||
| Spp.astMgr.getModifierStringParams(modifier, endpointParams); | ||
| return endpointParams; | ||
| } | ||
|
|
||
| func getAllModules (astRef: ref[Core.Basic.TiObject]): Array[ref[Core.Basic.TiObject]] { | ||
| def result: Array[ref[Core.Basic.TiObject]]; | ||
| if astRef~ptr == Root~ast~ptr or Core.Basic.isDerivedFrom[astRef, Spp.Ast.Module] { | ||
|
|
@@ -576,4 +754,5 @@ | |
| System.fail(1, String("Invalid asset route; path should end with /: ") + buildPath); | ||
| } | ||
| } | ||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't quit the entire program, raise a build notice instead using
Spp.buildMgr.raiseBuildNoticeso that the developer get a proper build error pointing to the actual line number where the error is.