KUMOS/Client/Includes/AppRuntime.sbi
2026-05-02 15:49:06 +02:00

409 lines
14 KiB
Plaintext

Module AppRuntime
EnableExplicit
;- Instance structure
Structure Instance
InstanceID.i ; stable unique ID — used as the JS↔SB bridge key
Window.i
View.i
AppID.s
Perms.s
EndStructure
Global NewList Instances.Instance()
Global _NextID = 1 ; auto-incrementing ID counter
;- Pending async operations
; One pending op of each type at a time (JS is single-threaded).
Global _PL_ID = -1 : Global _PL_KMID.s = "" ; list
Global _PS_ID = -1 : Global _PS_KMID.s = "" ; stat (standalone)
Global _PSR_ID = -1 : Global _PSR_KMID.s = "" ; stat-before-read/delete
Global _PR_ID = -1 : Global _PR_KMID.s = "" ; read
Global _PW_ID = -1 : Global _PW_KMID.s = "" ; write / mkdir / delete
Global _PC_ID = -1 : Global _PC_KMID.s = "" ; confirm dialog
;- Private declarations
Declare _FindByID(ID)
Declare _FindByWindow(Win)
Declare _HasPerm(ID, Token.s)
Declare _SendResponse(ID, KMID.s, Success, DataString.s)
Declare.s _StoragePath(ID, RelPath.s)
Declare _Remove(ID)
Declare _CloseByID(ID)
Declare _Dispatch(ID, RawMsg.s)
Declare _DoList(ID, KMID.s, Path.s)
Declare _DoStat(ID, KMID.s, Path.s)
Declare _DoStatForRead(ID, KMID.s, Path.s)
Declare _DoRead(ID, KMID.s, FileID)
Declare _DoWrite(ID, KMID.s, Path.s, Content.s)
Declare _DoMkdir(ID, KMID.s, Path.s)
Declare _DoDelete(ID, KMID.s, Path.s)
Declare _ListCallback(Success, DataString.s)
Declare _StatCallback(Success, DataString.s)
Declare _StatForReadCallback(Success, DataString.s)
Declare _ReadCallback(Success, DataString.s)
Declare _WriteCallback(Success, DataString.s)
Declare _ConfirmCallback(Result)
Declare Handler_Resize()
Declare Handler_Close()
;- Public procedures
; Call once from Desktop::Open.
Procedure Init()
!window._kumos_rt = { instances: {} };
!window.addEventListener('message', function(e) {
! var inst;
! for (var id in window._kumos_rt.instances) {
! inst = window._kumos_rt.instances[id];
! if (inst && inst.frameEl && inst.frameEl.contentWindow === e.source) {
! if (typeof e.data === 'string') {
! appruntime$f__dispatch(inst.id, e.data);
! }
! return;
! }
! }
!});
EndProcedure
; Open a new sandboxed app window.
Procedure Launch(AppID.s, ManifestJSON.s, Permissions.s)
Protected Count, W, H, Win, View, ID
Protected AppName.s, N.s, Src.s
AppName = AppID
If ParseJSON(0, ManifestJSON)
N = GetJSONString(GetJSONMember(JSONValue(0), "name"))
If N <> "" : AppName = N : EndIf
FreeJSON(0)
EndIf
Count = ListSize(Instances())
W = 640
H = 480
Win = OpenWindow(#PB_Any, 80 + (Count % 8) * 24, 80 + (Count % 8) * 24, W, H, AppName, #PB_Window_TitleBar | #PB_Window_SizeGadget | #PB_Window_SystemMenu)
View = WebGadget(#PB_Any, 0, 0, W, H, "about:blank")
Src = "/api/apps/" + AppID + "/index.html"
ID = _NextID
_NextID + 1
!(function(){
! var frames = document.querySelectorAll('iframe[src="about:blank"]:not([data-kumos-instance])');
! var f = frames[frames.length - 1];
! if (!f) return;
! f.setAttribute('data-kumos-instance', String(v_id));
! // No allow-same-origin -> cross-origin isolation even from same host.
! f.setAttribute('sandbox', 'allow-scripts allow-forms allow-modals allow-downloads allow-pointer-lock');
! window._kumos_rt.instances[v_id] = { id: v_id, frameEl: f, appId: v_appid };
! f.src = v_src;
!})();
AddElement(Instances())
Instances()\InstanceID = ID
Instances()\Window = Win
Instances()\View = View
Instances()\AppID = AppID
Instances()\Perms = Permissions
BindEvent(#PB_Event_SizeWindow, @Handler_Resize(), Win)
BindEvent(#PB_Event_CloseWindow, @Handler_Close(), Win)
Desktop::Register(AppName, Win, "📦")
EndProcedure
;- Private procedures
; Leaves list cursor on the found element. Returns #True if found.
Procedure _FindByID(ID)
ForEach Instances()
If Instances()\InstanceID = ID : ProcedureReturn #True : EndIf
Next
ProcedureReturn #False
EndProcedure
Procedure _FindByWindow(Win)
ForEach Instances()
If Instances()\Window = Win : ProcedureReturn #True : EndIf
Next
ProcedureReturn #False
EndProcedure
Procedure _HasPerm(ID, Token.s)
If Not _FindByID(ID) : ProcedureReturn #False : EndIf
ProcedureReturn Bool(FindString(Instances()\Perms, ~"\"" + Token + ~"\"") > 0)
EndProcedure
Procedure _SendResponse(ID, KMID.s, Success, DataString.s)
!var inst = window._kumos_rt.instances[v_id];
!if (!inst || !inst.frameEl) return;
!var resp = { kmid: v_kmid, success: !!v_success };
!if (v_success) {
! try { resp.data = JSON.parse(v_datastring); }
! catch(e) { resp.data = v_datastring || null; }
!} else {
! resp.error = v_datastring || 'Error';
!}
!try { inst.frameEl.contentWindow.postMessage(JSON.stringify(resp), '*'); } catch(e) {}
EndProcedure
Procedure.s _StoragePath(ID, RelPath.s)
Protected Base.s
If Not _FindByID(ID) : ProcedureReturn "" : EndIf
Base = "/.apps/" + Instances()\AppID + "/"
If RelPath = "" Or RelPath = "/" : ProcedureReturn Base : EndIf
If Left(RelPath, 1) = "/" : RelPath = Mid(RelPath, 2) : EndIf
ProcedureReturn Base + RelPath
EndProcedure
Procedure _Remove(ID)
If Not _FindByID(ID) : ProcedureReturn : EndIf
!delete window._kumos_rt.instances[v_id];
UnbindEvent(#PB_Event_SizeWindow, @Handler_Resize(), Instances()\Window)
UnbindEvent(#PB_Event_CloseWindow, @Handler_Close(), Instances()\Window)
Desktop::Unregister(Instances()\Window)
DeleteElement(Instances())
EndProcedure
Procedure _CloseByID(ID)
_Remove(ID)
EndProcedure
;- Message dispatcher
Procedure _Dispatch(ID, RawMsg.s)
Protected Root, Args, TType
Protected KMID.s, Action.s, Path.s, Content.s, Title_.s, Msg.s, MsgType.s
If Not _FindByID(ID) : ProcedureReturn : EndIf
If Not ParseJSON(0, RawMsg) : ProcedureReturn : EndIf
Root = JSONValue(0)
KMID = GetJSONString(GetJSONMember(Root, "kmid"))
Action = GetJSONString(GetJSONMember(Root, "action"))
Args = GetJSONMember(Root, "args")
If KMID = "" : FreeJSON(0) : ProcedureReturn : EndIf
Path = GetJSONString(GetJSONMember(Args, "path"))
Content = GetJSONString(GetJSONMember(Args, "content"))
Title_ = GetJSONString(GetJSONMember(Args, "title"))
Msg = GetJSONString(GetJSONMember(Args, "message"))
MsgType = GetJSONString(GetJSONMember(Args, "type"))
FreeJSON(0)
Select Action
Case "window.ready"
_FindByID(ID)
_SendResponse(ID, KMID, #True, ~"{\"app_id\":\"" + ReplaceString(Instances()\AppID, ~"\"", ~"\\\"") + ~"\"}")
Case "window.setTitle"
_FindByID(ID)
SetWindowTitle(Instances()\Window, Title_)
_SendResponse(ID, KMID, #True, "")
Case "window.close"
_SendResponse(ID, KMID, #True, "")
!setTimeout(function(){ appruntime$f__closebyid(v_id); }, 50);
Case "notify.toast"
If Not _HasPerm(ID, "notify") : _SendResponse(ID, KMID, #False, "Permission denied: notify") : ProcedureReturn : EndIf
TType = Notify::#Info
If MsgType = "success" : TType = Notify::#Success
ElseIf MsgType = "warning" : TType = Notify::#Warning
ElseIf MsgType = "error" : TType = Notify::#Error
EndIf
Notify::Toast(Msg, TType)
_SendResponse(ID, KMID, #True, "")
Case "notify.confirm"
If Not _HasPerm(ID, "notify") : _SendResponse(ID, KMID, #False, "Permission denied: notify") : ProcedureReturn : EndIf
If _PC_ID >= 0 : _SendResponse(ID, KMID, #False, "A dialog is already open") : ProcedureReturn : EndIf
_PC_ID = ID : _PC_KMID = KMID
Notify::Confirm(Title_, Msg, @_ConfirmCallback())
Case "storage.list" : _DoList(ID, KMID, _StoragePath(ID, Path))
Case "storage.stat" : _DoStat(ID, KMID, _StoragePath(ID, Path))
Case "storage.read" : _DoStatForRead(ID, KMID, _StoragePath(ID, Path))
Case "storage.write" : _DoWrite(ID, KMID, _StoragePath(ID, Path), Content)
Case "storage.mkdir" : _DoMkdir(ID, KMID, _StoragePath(ID, Path))
Case "storage.delete" : _DoDelete(ID, KMID, _StoragePath(ID, Path))
Case "fs.list"
If Not _HasPerm(ID, "fs.read") : _SendResponse(ID, KMID, #False, "Permission denied: fs.read") : ProcedureReturn : EndIf
_DoList(ID, KMID, Path)
Case "fs.stat"
If Not _HasPerm(ID, "fs.read") : _SendResponse(ID, KMID, #False, "Permission denied: fs.read") : ProcedureReturn : EndIf
_DoStat(ID, KMID, Path)
Case "fs.read"
If Not _HasPerm(ID, "fs.read") : _SendResponse(ID, KMID, #False, "Permission denied: fs.read") : ProcedureReturn : EndIf
_DoStatForRead(ID, KMID, Path)
Case "fs.write"
If Not _HasPerm(ID, "fs.write") : _SendResponse(ID, KMID, #False, "Permission denied: fs.write") : ProcedureReturn : EndIf
_DoWrite(ID, KMID, Path, Content)
Default
_SendResponse(ID, KMID, #False, "Unknown action: " + Action)
EndSelect
EndProcedure
;- Async FS helpers
Procedure _DoList(ID, KMID.s, Path.s)
If _PL_ID >= 0 : _SendResponse(ID, KMID, #False, "Busy") : ProcedureReturn : EndIf
_PL_ID = ID : _PL_KMID = KMID
HTTPRequest(#PB_HTTP_Get, "/api/fs/list?path=" + URLEncoder(Path, #PB_UTF8), "", @_ListCallback())
EndProcedure
Procedure _DoStat(ID, KMID.s, Path.s)
If _PS_ID >= 0 : _SendResponse(ID, KMID, #False, "Busy") : ProcedureReturn : EndIf
_PS_ID = ID : _PS_KMID = KMID
HTTPRequest(#PB_HTTP_Get, "/api/fs/stat?path=" + URLEncoder(Path, #PB_UTF8), "", @_StatCallback())
EndProcedure
Procedure _DoStatForRead(ID, KMID.s, Path.s)
If _PSR_ID >= 0 : _SendResponse(ID, KMID, #False, "Busy") : ProcedureReturn : EndIf
_PSR_ID = ID : _PSR_KMID = KMID
HTTPRequest(#PB_HTTP_Get, "/api/fs/stat?path=" + URLEncoder(Path, #PB_UTF8), "", @_StatForReadCallback())
EndProcedure
Procedure _DoRead(ID, KMID.s, FileID)
If _PR_ID >= 0 : _SendResponse(ID, KMID, #False, "Busy") : ProcedureReturn : EndIf
_PR_ID = ID : _PR_KMID = KMID
HTTPRequest(#PB_HTTP_Get, "/api/fs/read?id=" + FileID, "", @_ReadCallback())
EndProcedure
Procedure _DoWrite(ID, KMID.s, Path.s, Content.s)
If _PW_ID >= 0 : _SendResponse(ID, KMID, #False, "Busy") : ProcedureReturn : EndIf
_PW_ID = ID : _PW_KMID = KMID
HTTPRequest(#PB_HTTP_Post, "/api/fs/write", "path=" + URLEncoder(Path, #PB_UTF8) + "&content=" + URLEncoder(Content, #PB_UTF8), @_WriteCallback())
EndProcedure
Procedure _DoMkdir(ID, KMID.s, Path.s)
If _PW_ID >= 0 : _SendResponse(ID, KMID, #False, "Busy") : ProcedureReturn : EndIf
_PW_ID = ID : _PW_KMID = KMID
HTTPRequest(#PB_HTTP_Post, "/api/fs/mkdir", "path=" + URLEncoder(Path, #PB_UTF8), @_WriteCallback())
EndProcedure
Procedure _DoDelete(ID, KMID.s, Path.s)
If _PSR_ID >= 0 : _SendResponse(ID, KMID, #False, "Busy") : ProcedureReturn : EndIf
_PSR_ID = ID : _PSR_KMID = "DEL:" + KMID
HTTPRequest(#PB_HTTP_Get, "/api/fs/stat?path=" + URLEncoder(Path, #PB_UTF8), "", @_StatForReadCallback())
EndProcedure
;- Callbacks
Procedure _ListCallback(Success, DataString.s)
Protected ID
Protected KMID.s
If _PL_ID < 0 : ProcedureReturn : EndIf
ID = _PL_ID : KMID = _PL_KMID
_PL_ID = -1 : _PL_KMID = ""
_SendResponse(ID, KMID, Success, DataString)
EndProcedure
Procedure _StatCallback(Success, DataString.s)
Protected ID
Protected KMID.s
If _PS_ID < 0 : ProcedureReturn : EndIf
ID = _PS_ID : KMID = _PS_KMID
_PS_ID = -1 : _PS_KMID = ""
_SendResponse(ID, KMID, Success, DataString)
EndProcedure
Procedure _StatForReadCallback(Success, DataString.s)
Protected ID, IsDelete, IsDir, FileID, Root
Protected Tag.s, KMID.s
If _PSR_ID < 0 : ProcedureReturn : EndIf
ID = _PSR_ID
Tag = _PSR_KMID
_PSR_ID = -1 : _PSR_KMID = ""
IsDelete = Bool(Left(Tag, 4) = "DEL:")
If IsDelete : KMID = Mid(Tag, 5) : Else : KMID = Tag : EndIf
If Not Success : _SendResponse(ID, KMID, #False, "Not found") : ProcedureReturn : EndIf
If Not ParseJSON(0, DataString) : _SendResponse(ID, KMID, #False, "Bad stat response") : ProcedureReturn : EndIf
Root = JSONValue(0)
IsDir = GetJSONInteger(GetJSONMember(Root, "is_dir"))
FileID = GetJSONInteger(GetJSONMember(Root, "id"))
FreeJSON(0)
If FileID <= 0 : _SendResponse(ID, KMID, #False, "Not found") : ProcedureReturn : EndIf
If IsDelete
If _PW_ID >= 0 : _SendResponse(ID, KMID, #False, "Busy") : ProcedureReturn : EndIf
_PW_ID = ID : _PW_KMID = KMID
HTTPRequest(#PB_HTTP_Post, "/api/fs/delete", "id=" + FileID, @_WriteCallback())
Else
If IsDir : _SendResponse(ID, KMID, #False, "Is a directory") : ProcedureReturn : EndIf
_DoRead(ID, KMID, FileID)
EndIf
EndProcedure
Procedure _ReadCallback(Success, DataString.s)
Protected ID
Protected KMID.s
If _PR_ID < 0 : ProcedureReturn : EndIf
ID = _PR_ID : KMID = _PR_KMID
_PR_ID = -1 : _PR_KMID = ""
_SendResponse(ID, KMID, Success, DataString)
EndProcedure
Procedure _WriteCallback(Success, DataString.s)
Protected ID, OK
Protected KMID.s
If _PW_ID < 0 : ProcedureReturn : EndIf
ID = _PW_ID : KMID = _PW_KMID
_PW_ID = -1 : _PW_KMID = ""
If Not Success : _SendResponse(ID, KMID, #False, "Request failed") : ProcedureReturn : EndIf
If ParseJSON(0, DataString)
OK = GetJSONBoolean(GetJSONMember(JSONValue(0), "success"))
FreeJSON(0)
_SendResponse(ID, KMID, OK, "")
Else
_SendResponse(ID, KMID, #False, "Bad server response")
EndIf
EndProcedure
Procedure _ConfirmCallback(Result)
Protected ID
Protected KMID.s, BoolStr.s
If _PC_ID < 0 : ProcedureReturn : EndIf
ID = _PC_ID : KMID = _PC_KMID
_PC_ID = -1 : _PC_KMID = ""
BoolStr = "false"
If Result : BoolStr = "true" : EndIf
_SendResponse(ID, KMID, #True, BoolStr)
EndProcedure
;- Window event handlers
Procedure Handler_Resize()
If Not _FindByWindow(EventWindow()) : ProcedureReturn : EndIf
ResizeGadget(Instances()\View, 0, 0, WindowWidth(Instances()\Window), WindowHeight(Instances()\Window))
EndProcedure
Procedure Handler_Close()
If Not _FindByWindow(EventWindow()) : ProcedureReturn : EndIf
_Remove(Instances()\InstanceID)
EndProcedure
EndModule
; IDE Options = SpiderBasic 3.20 (Windows - x86)
; CursorPosition = 2
; Folding = BAAA9
; iOSAppOrientation = 0
; AndroidAppCode = 0
; AndroidAppOrientation = 0
; EnableXP
; DPIAware
; CompileSourceDirectory