Run.lua (92310B)
1 #!/usr/bin/env luajit 2 -- Application Runner Module 3 -- Handles running applications with sandboxing, permissions, and CLI buffer management 4 5 -- Test if osprint is available when module loads 6 if osprint then 7 osprint("RUN MODULE LOADED: osprint is available!\n") 8 end 9 10 local run = {} 11 12 -- Apps are registered in _G.sys.applications (maintained by Sys.lua) 13 -- This provides a unified way to access running apps across the system 14 local function get_app_registry() 15 return (_G.sys and _G.sys.applications) or {} 16 end 17 18 -- Scheduler instance (loaded on demand) 19 local scheduler = nil 20 21 -- Utility functions that are needed for run functionality 22 23 -- Utility: Split string by delimiter 24 local function split(str, delimiter) 25 local result = {} 26 local pattern = string.format("([^%s]+)", delimiter) 27 for match in string.gmatch(str, pattern) do 28 table.insert(result, match) 29 end 30 return result 31 end 32 33 -- Utility: Parse command line arguments 34 local function parse_args(argStr) 35 if not argStr or argStr == "" then 36 return { str = "" } 37 end 38 39 local args = { str = argStr } 40 local current = "" 41 local in_quote = false 42 local raw_args = {} 43 44 -- First pass: split into tokens 45 for i = 1, #argStr do 46 local c = argStr:sub(i, i) 47 if c == '"' then 48 in_quote = not in_quote 49 elseif c == ' ' and not in_quote then 50 if current ~= "" then 51 table.insert(raw_args, current) 52 current = "" 53 end 54 else 55 current = current .. c 56 end 57 end 58 59 if current ~= "" then 60 table.insert(raw_args, current) 61 end 62 63 -- Second pass: parse flags and populate args table 64 local i = 1 65 local positional_index = 1 66 while i <= #raw_args do 67 local arg = raw_args[i] 68 69 -- Check if it's a flag (-X or --X) 70 local flag_name = nil 71 if arg:match("^%-%-(.+)") then 72 -- Long flag: --name 73 flag_name = arg:match("^%-%-(.+)") 74 elseif arg:match("^%-(.+)") then 75 -- Short flag: -n 76 flag_name = arg:match("^%-(.+)") 77 end 78 79 if flag_name then 80 -- It's a flag - check if it's "str" or a number (reserved names) 81 if flag_name ~= "str" and not tonumber(flag_name) then 82 -- Get the next value if it exists and isn't another flag 83 if i + 1 <= #raw_args and not raw_args[i + 1]:match("^%-") then 84 args[flag_name] = raw_args[i + 1] 85 i = i + 1 -- Skip the value in next iteration 86 else 87 -- Flag without value, set to true 88 args[flag_name] = true 89 end 90 end 91 else 92 -- Not a flag, add as positional argument 93 args[positional_index] = arg 94 positional_index = positional_index + 1 95 end 96 97 i = i + 1 98 end 99 100 return args 101 end 102 103 -- Utility: Deep copy a table 104 local function deep_copy(obj) 105 if type(obj) ~= 'table' then 106 return obj 107 end 108 109 local copy = {} 110 for k, v in pairs(obj) do 111 copy[k] = deep_copy(v) 112 end 113 114 return copy 115 end 116 117 -- Utility: Parse manifest (simple Lua code execution) 118 -- Parse manifest content as text (no Lua execution for security) 119 local function parse_manifest(manifest_code) 120 if not manifest_code then 121 return {} 122 end 123 124 -- Remove "return" if present at start 125 manifest_code = string.gsub(manifest_code, "^%s*return%s*", "") 126 127 -- Remove outer braces only if both opening and closing exist as a pair 128 local trimmed = string.gsub(manifest_code, "^%s+", "") 129 trimmed = string.gsub(trimmed, "%s+$", "") 130 if string.sub(trimmed, 1, 1) == "{" and string.sub(trimmed, -1) == "}" then 131 manifest_code = string.sub(trimmed, 2, -2) 132 end 133 134 -- Extract [[long strings]] before removing comments (to preserve their content) 135 local long_strings = {} 136 local placeholder_idx = 0 137 manifest_code = string.gsub(manifest_code, "%[%[(.-)%]%]", function(content) 138 placeholder_idx = placeholder_idx + 1 139 local placeholder = "\001LONGSTR" .. placeholder_idx .. "\001" 140 long_strings[placeholder] = content 141 return placeholder 142 end) 143 144 -- Remove single-line comments (-- to end of line) 145 manifest_code = string.gsub(manifest_code, "%-%-[^\n]*", "") 146 147 -- Normalize whitespace: replace newlines/tabs with spaces, collapse multiple spaces 148 manifest_code = string.gsub(manifest_code, "[\r\n\t]+", " ") 149 manifest_code = string.gsub(manifest_code, " +", " ") 150 151 local manifest = {} 152 153 -- Helper to parse array content (handles [[strings]] in arrays) 154 local function parse_array_item(item) 155 item = string.gsub(item, "^%s+", "") 156 item = string.gsub(item, "%s+$", "") 157 -- Check for long string placeholder 158 if long_strings[item] then 159 return long_strings[item] 160 elseif string.match(item, '^".-"$') then 161 return string.sub(item, 2, -2) 162 elseif string.match(item, "^'.-'$") then 163 return string.sub(item, 2, -2) 164 elseif item ~= "" then 165 return item 166 end 167 return nil 168 end 169 170 -- Match key = value pairs (handles arrays with spaces like { "a", "b" }) 171 -- Pattern: word = (value until next word= or end) 172 local pos = 1 173 while pos <= #manifest_code do 174 -- Skip whitespace, commas, and semicolons 175 local ws_end = string.match(manifest_code, "^[%s,;]*()", pos) 176 if ws_end then pos = ws_end end 177 178 -- Match key = value 179 local key, value_start = string.match(manifest_code, "^([%w_]+)%s*=%s*()", pos) 180 if not key then break end 181 182 pos = value_start 183 184 -- Determine value type and extract it 185 local value 186 local char = string.sub(manifest_code, pos, pos) 187 188 if char == "{" then 189 -- Array: find matching } by counting brace depth 190 local depth = 1 191 local brace_end = pos + 1 192 while brace_end <= #manifest_code and depth > 0 do 193 local c = string.sub(manifest_code, brace_end, brace_end) 194 if c == "{" then 195 depth = depth + 1 196 elseif c == "}" then 197 depth = depth - 1 198 end 199 brace_end = brace_end + 1 200 end 201 brace_end = brace_end - 1 -- Point to the closing } 202 if depth == 0 then 203 local arr_content = string.sub(manifest_code, pos + 1, brace_end - 1) 204 local arr = {} 205 for item in string.gmatch(arr_content, '[^,;]+') do 206 local parsed = parse_array_item(item) 207 if parsed then 208 table.insert(arr, parsed) 209 end 210 end 211 value = arr 212 pos = brace_end + 1 213 else 214 break 215 end 216 elseif char == "\001" then 217 -- Long string placeholder 218 local placeholder = string.match(manifest_code, "^(\001LONGSTR%d+\001)", pos) 219 if placeholder and long_strings[placeholder] then 220 value = long_strings[placeholder] 221 pos = pos + #placeholder 222 else 223 break 224 end 225 elseif char == '"' then 226 -- Double-quoted string 227 local str_end = string.find(manifest_code, '"', pos + 1, true) 228 if str_end then 229 value = string.sub(manifest_code, pos + 1, str_end - 1) 230 pos = str_end + 1 231 else 232 break 233 end 234 elseif char == "'" then 235 -- Single-quoted string 236 local str_end = string.find(manifest_code, "'", pos + 1, true) 237 if str_end then 238 value = string.sub(manifest_code, pos + 1, str_end - 1) 239 pos = str_end + 1 240 else 241 break 242 end 243 else 244 -- Unquoted value (boolean, number, or identifier) 245 local val_str = string.match(manifest_code, "^([^%s,;}]+)", pos) 246 if val_str then 247 if val_str == "true" then 248 value = true 249 elseif val_str == "false" then 250 value = false 251 elseif tonumber(val_str) then 252 value = tonumber(val_str) 253 else 254 value = val_str 255 end 256 pos = pos + #val_str 257 else 258 break 259 end 260 end 261 262 if key and value ~= nil then 263 manifest[key] = value 264 end 265 end 266 267 return manifest 268 end 269 270 -- Utility: Check if directory exists 271 local function dir_exists(path) 272 local ok, err, code = os.rename(path, path) 273 if not ok then 274 if code == 13 then 275 return true -- Permission denied, but exists 276 end 277 end 278 return ok 279 end 280 281 -- Utility: Read file content 282 local function read_file(filepath) 283 local file = io.open(filepath, "rb") 284 if not file then 285 return nil 286 end 287 local content = file:read("*all") 288 file:close() 289 return content 290 end 291 292 -- Helper function to read from ramdisk 293 local function read_from_ramdisk(fsRoot, filepath) 294 if not fsRoot then 295 -- Use CRamdisk functions when no fsRoot available 296 if CRamdiskOpen and CRamdiskRead and CRamdiskClose then 297 local handle = CRamdiskOpen(filepath, "r") 298 if handle then 299 local content = CRamdiskRead(handle) 300 CRamdiskClose(handle) 301 return content 302 end 303 end 304 -- Fallback to regular file reading 305 return read_file(filepath) 306 end 307 308 -- Try fsRoot:traverse first (works with Lua ramdisk implementation) 309 local node = fsRoot:traverse(filepath) 310 if node and node.type == "file" then 311 return node.content 312 end 313 314 -- Fallback to CRamdiskRead if available (C ramdisk API) 315 if CRamdiskRead then 316 -- Strip leading slash for CRamdiskRead 317 local path = filepath 318 if path:sub(1,1) == "/" then 319 path = path:sub(2) 320 end 321 local result = CRamdiskRead(path) 322 return result 323 end 324 325 return nil 326 end 327 328 -- Find installed apps by name 329 local function find_apps(app_name, fsRoot) 330 local apps_dir = "/apps" 331 local matches = {} 332 333 -- Always use CRamdisk functions for finding apps (more reliable than SafeFS) 334 if osprint then 335 osprint("[find_apps] Looking for '" .. app_name .. "' in " .. apps_dir .. "\n") 336 end 337 338 -- Check if direct match (com.dev.appname or com.*.appname) 339 if string.match(app_name, "^com%.") then 340 local app_path = apps_dir .. "/" .. app_name 341 -- Use CRamdiskExists to check if directory exists 342 if CRamdiskExists and CRamdiskExists(app_path) then 343 if osprint then 344 osprint("[find_apps] Direct match found: " .. app_name .. "\n") 345 end 346 table.insert(matches, app_name) 347 end 348 return matches 349 end 350 351 -- Search for matches using CRamdiskList 352 if CRamdiskList then 353 if osprint then 354 osprint("[find_apps] Using CRamdiskList to search...\n") 355 end 356 local entries = CRamdiskList(apps_dir) 357 if entries then 358 if osprint then 359 osprint("[find_apps] Found " .. #entries .. " entries in " .. apps_dir .. "\n") 360 end 361 for _, entry in ipairs(entries) do 362 if osprint then 363 osprint("[find_apps] Entry: '" .. entry.name .. "' type=" .. entry.type .. "\n") 364 end 365 local pattern = "%." .. app_name .. "$" 366 if osprint then 367 osprint("[find_apps] Testing pattern: '" .. pattern .. "' against '" .. entry.name .. "'\n") 368 end 369 if (entry.type == "directory" or entry.type == "dir") and string.match(entry.name, pattern) then 370 if osprint then 371 osprint("[find_apps] MATCH: " .. entry.name .. "\n") 372 end 373 table.insert(matches, entry.name) 374 end 375 end 376 else 377 if osprint then 378 osprint("[find_apps] CRamdiskList returned nil\n") 379 end 380 end 381 else 382 if osprint then 383 osprint("[find_apps] CRamdiskList not available\n") 384 end 385 end 386 387 if osprint then 388 osprint("[find_apps] Total matches: " .. #matches .. "\n") 389 end 390 391 return matches 392 end 393 394 -- Extract embedded manifest from Lua script 395 -- Looks for comment block between ----- MANIFEST START and ----- MANIFEST END 396 -- Returns manifest table or nil if not found 397 local function extract_embedded_manifest(script_content) 398 if not script_content then 399 return nil 400 end 401 402 -- Look for --[[ MANIFEST ... --]] block comment format 403 local manifest_block = string.match(script_content, "%-%-%[%[%s*MANIFEST%s*(.-)%-%-%]%]") 404 405 if not manifest_block then 406 return nil 407 end 408 409 -- Reuse parse_manifest which handles multi-line arrays and text-based parsing 410 local manifest = parse_manifest(manifest_block) 411 412 -- Return nil if manifest is empty 413 local has_keys = false 414 for _ in pairs(manifest) do 415 has_keys = true 416 break 417 end 418 419 return has_keys and manifest or nil 420 end 421 422 -- Run an installed app with sandboxing 423 function run.execute(app_name, fsRoot) 424 -- Use osprint directly in run code (global print doesn't work in bare metal) 425 local run_print = osprint or print 426 427 if osprint then 428 osprint("[RUN.EXECUTE] Called with app_name='" .. tostring(app_name) .. "' fsRoot=" .. tostring(fsRoot) .. "\n") 429 end 430 431 -- Utility functions are now defined locally in this module (no LPM dependency) 432 433 -- Check if this is a direct .lua file execution 434 local is_lua_script = string.match(app_name, "%.lua$") ~= nil 435 local selected_app, init_content, init_file, app_dir, data_dir 436 local permissions = {} 437 local manifest = {} 438 local app_instance = nil 439 440 if is_lua_script then 441 -- Direct .lua file execution 442 run_print("Running Lua script: " .. app_name .. "\n") 443 444 -- If script name has no path separator, look in /scripts/ 445 if not string.match(app_name, "/") then 446 init_file = "/scripts/" .. app_name 447 run_print("Looking for script in: " .. init_file .. "\n") 448 else 449 init_file = app_name 450 end 451 452 -- Read the script file 453 init_content = read_from_ramdisk(fsRoot, init_file) 454 455 if not init_content then 456 local err_msg = "Script file not found: " .. init_file 457 run_print("Error: " .. err_msg .. "\n") 458 return false, err_msg 459 end 460 461 -- Extract embedded manifest 462 local extracted_manifest = extract_embedded_manifest(init_content) 463 464 if extracted_manifest then 465 manifest = extracted_manifest 466 run_print("Found embedded manifest\n") 467 468 -- Parse permissions from embedded manifest 469 if manifest.permission then 470 if type(manifest.permission) == "table" then 471 for _, perm in ipairs(manifest.permission) do 472 if type(perm) == "string" then 473 table.insert(permissions, perm) 474 end 475 end 476 end 477 elseif manifest.permissions then 478 if type(manifest.permissions) == "table" then 479 for _, perm in ipairs(manifest.permissions) do 480 if type(perm) == "string" then 481 table.insert(permissions, perm) 482 end 483 end 484 end 485 end 486 else 487 run_print("No embedded manifest found, running with no permissions\n") 488 manifest = {} -- Ensure manifest is an empty table, not nil 489 end 490 491 -- For standalone scripts, use a simple app name based on the file path 492 selected_app = string.gsub(app_name, "%.lua$", "") 493 selected_app = string.gsub(selected_app, "^/", "") 494 selected_app = string.gsub(selected_app, "/", ".") 495 496 -- Create a hash of the script path for consistent proc directory 497 local script_hash = 0 498 for i = 1, #init_file do 499 script_hash = (script_hash * 31 + string.byte(init_file, i)) % 0x7FFFFFFF 500 end 501 502 -- Use /proc/script_<hash> as the data directory 503 app_dir = "/proc/script_" .. string.format("%08x", script_hash) 504 data_dir = app_dir 505 506 else 507 -- Standard app execution from /apps directory 508 local matches = find_apps(app_name, fsRoot) 509 510 if #matches == 0 then 511 local err_msg = "No app found matching '" .. app_name .. "'" 512 run_print("Error: " .. err_msg .. "\n") 513 return false, err_msg 514 end 515 516 if #matches > 1 then 517 run_print("Multiple apps found:\n") 518 for i, app in ipairs(matches) do 519 run_print(i .. ": " .. app .. "\n") 520 end 521 run_print("Enter selection (1-" .. #matches .. "): \n") 522 local choice = tonumber(io.read()) 523 524 if not choice or choice < 1 or choice > #matches then 525 local err_msg = "Invalid selection" 526 run_print(err_msg .. "\n") 527 return false, err_msg 528 end 529 530 selected_app = matches[choice] 531 else 532 selected_app = matches[1] 533 end 534 535 local apps_dir = "/apps" 536 app_dir = apps_dir .. "/" .. selected_app 537 data_dir = app_dir .. "/data" 538 local manifest_file = app_dir .. "/manifest.lua" 539 540 -- Read manifest to get permissions and entry point 541 local manifest_content = read_from_ramdisk(fsRoot, manifest_file) 542 543 local entry_point = "src/init.lua" -- Default entry point 544 545 if not manifest_content then 546 run_print("Warning: manifest.lua not found in " .. manifest_file .. ", running with no permissions\n") 547 -- Continue with empty permissions and manifest 548 else 549 manifest = parse_manifest(manifest_content) 550 551 -- Parse permissions into array of strings 552 if manifest.permissions then 553 if type(manifest.permissions) == "table" then 554 for _, perm in ipairs(manifest.permissions) do 555 if type(perm) == "string" then 556 table.insert(permissions, perm) 557 end 558 end 559 end 560 end 561 562 -- Get entry point from manifest (default to src/init.lua) 563 -- Entry is always relative to $/src/ 564 if manifest.entry and type(manifest.entry) == "string" then 565 entry_point = "src/" .. manifest.entry 566 end 567 end 568 569 run_print("Running: " .. selected_app .. "\n") 570 run_print("Entry point: " .. entry_point .. "\n") 571 572 -- Build full path to entry file 573 init_file = app_dir .. "/" .. entry_point 574 575 -- Check if entry file exists 576 init_content = read_from_ramdisk(fsRoot, init_file) 577 if not init_content then 578 local err_msg = "Entry file not found: " .. init_file 579 run_print("Error: " .. err_msg .. "\n") 580 return false, err_msg 581 end 582 end 583 584 -- Common code for both script and app execution 585 run_print("Permissions: " .. table.concat(permissions, ", ") .. "\n") 586 587 -- Create sandboxed environment 588 local sandbox_env = { 589 -- Basic Lua functions 590 print = nil, -- Will be set later after app_instance is created 591 osprint = osprint, -- Also expose osprint directly for explicit use 592 tonumber = tonumber, 593 tostring = tostring, 594 type = type, 595 pairs = pairs, 596 ipairs = ipairs, 597 next = next, 598 select = select, 599 assert = assert, 600 error = error, 601 pcall = pcall, 602 xpcall = xpcall, 603 setmetatable = setmetatable, 604 getmetatable = getmetatable, 605 606 -- Safe standard libraries 607 string = string, 608 table = table, 609 math = math, 610 bit = bit, -- LuaJIT bit operations library 611 612 -- Restricted os library (only safe functions) 613 os = { 614 date = os.date, 615 time = os.time, 616 difftime = os.difftime, 617 clock = os.clock, 618 }, 619 620 -- Pre-define optional items as nil so accessing them doesn't throw 621 fs = nil, -- SafeFS instance (set later if filesystem permission granted) 622 } 623 624 -- Add optional globals only if they exist (avoid nil values in sandbox) 625 if _G.Dialog then 626 sandbox_env.Dialog = _G.Dialog 627 end 628 629 -- Check for crypto permission and add crypto object 630 local has_crypto = false 631 for _, perm in ipairs(permissions) do 632 if perm == "crypto" then 633 has_crypto = true 634 break 635 end 636 end 637 if has_crypto and _G.crypto then 638 sandbox_env.crypto = _G.crypto 639 if osprint then 640 osprint("Crypto permission granted - crypto object available\n") 641 end 642 end 643 644 -- Check for system-all permission and add sys object 645 local has_system_all = false 646 for _, perm in ipairs(permissions) do 647 if perm == "system-all" then 648 has_system_all = true 649 break 650 end 651 end 652 653 -- Check for admin permission first 654 local has_admin = false 655 for _, perm in ipairs(permissions) do 656 if perm == "admin" then 657 has_admin = true 658 break 659 end 660 end 661 662 -- Check for system-hook permission 663 local has_system_hook = false 664 for _, perm in ipairs(permissions) do 665 if perm == "system-hook" then 666 has_system_hook = true 667 break 668 end 669 end 670 671 -- Check for system-apps permission 672 local has_system_apps = false 673 for _, perm in ipairs(permissions) do 674 if perm == "system-apps" then 675 has_system_apps = true 676 break 677 end 678 end 679 680 if has_system_all then 681 -- Create a proxy for sys that: 682 -- 1. Blocks hook unless system-hook permission 683 -- 2. Exposes applications as 'apps' only if system-apps permission 684 -- 3. Blocks direct access to 'applications' 685 sandbox_env.sys = setmetatable({}, { 686 __index = function(t, k) 687 if k == "hook" and not has_system_hook then 688 error("sys.hook requires 'system-hook' permission") 689 end 690 if k == "applications" then 691 error("sys.applications is not accessible, use sys.apps with 'system-apps' permission") 692 end 693 if k == "apps" then 694 if has_system_apps then 695 return _G.sys.applications 696 else 697 error("sys.apps requires 'system-apps' permission") 698 end 699 end 700 return _G.sys[k] 701 end, 702 __newindex = function(t, k, v) 703 if k == "hook" and not has_system_hook then 704 error("sys.hook requires 'system-hook' permission") 705 end 706 if k == "applications" or k == "apps" then 707 error("Cannot modify sys.apps") 708 end 709 _G.sys[k] = v 710 end, 711 __pairs = function(t) 712 return function(tbl, k) 713 local nextKey, nextValue = next(_G.sys, k) 714 -- Skip hook if no permission 715 if nextKey == "hook" and not has_system_hook then 716 nextKey, nextValue = next(_G.sys, nextKey) 717 end 718 -- Skip applications (exposed as apps) 719 if nextKey == "applications" then 720 nextKey, nextValue = next(_G.sys, nextKey) 721 end 722 return nextKey, nextValue 723 end, t, nil 724 end, 725 __ipairs = function(t) 726 return ipairs(_G.sys) 727 end, 728 __metatable = false -- Prevent metatable access/modification 729 }) 730 if osprint then 731 osprint("System-all permission granted - sys object available\n") 732 if has_system_hook then osprint(" - sys.hook available\n") end 733 if has_system_apps then osprint(" - sys.apps available\n") end 734 end 735 end 736 737 if has_admin then 738 -- Create admin API object with restricted admin functions 739 sandbox_env.ADMIN_AppAddPermission = _G.sys.ADMIN_AppAddPermission 740 sandbox_env.ADMIN_AppAddPath = _G.sys.ADMIN_AppAddPath 741 sandbox_env.ADMIN_StartPrompt = _G.sys.ADMIN_StartPrompt 742 sandbox_env.ADMIN_FinishPrompt = _G.sys.ADMIN_FinishPrompt 743 if osprint then 744 osprint("Admin permission granted - admin functions available\n") 745 end 746 end 747 748 -- Check for filesystem permission and setup SafeFS 749 -- Also enable if manifest has allowedPaths (needs SafeFS for path access) 750 local has_filesystem = false 751 for _, perm in ipairs(permissions) do 752 if perm == "filesystem" then 753 has_filesystem = true 754 break 755 end 756 end 757 -- Enable SafeFS if manifest has allowedPaths defined 758 if not has_filesystem and manifest and manifest.allowedPaths and #manifest.allowedPaths > 0 then 759 has_filesystem = true 760 end 761 762 -- Load Application module (always load, not just for filesystem permission) 763 local Application = nil 764 local app_module_path = "/os/libs/Application.lua" 765 local app_module_code = nil 766 767 -- Try to load from fsRoot first, then fall back to CRamdisk 768 if fsRoot then 769 app_module_code = read_from_ramdisk(fsRoot, app_module_path) 770 elseif CRamdiskOpen then 771 -- Fall back to direct CRamdisk access when fsRoot is nil 772 local handle = CRamdiskOpen(app_module_path, "r") 773 if handle then 774 app_module_code = CRamdiskRead(handle) 775 CRamdiskClose(handle) 776 end 777 end 778 779 if app_module_code then 780 if osprint then 781 osprint("[run] Application.lua loaded, size=" .. #app_module_code .. " bytes\n") 782 end 783 784 -- Initialize global sys table if it doesn't exist 785 if not _G.sys then 786 _G.sys = { 787 used_pids = {}, 788 fallback_pid = 1000, 789 app_timestamp = 0 790 } 791 end 792 793 -- Load Application module in a temporary environment 794 local app_env = { 795 sys = _G.sys -- Explicitly provide sys to Application.lua 796 } 797 setmetatable(app_env, {__index = _G, __metatable = false}) 798 local app_func, err = load(app_module_code, app_module_path, "t", app_env) 799 800 if app_func then 801 local success, AppModule = pcall(app_func) 802 if success and AppModule then 803 Application = AppModule 804 if osprint then osprint("Application module loaded successfully!\n") end 805 else 806 if osprint then osprint("Warning: Failed to load Application module: " .. tostring(AppModule) .. "\n") end 807 end 808 else 809 if osprint then osprint("Warning: Failed to compile Application module: " .. tostring(err) .. "\n") end 810 end 811 else 812 if osprint then osprint("Warning: Application module not found at " .. app_module_path .. "\n") end 813 end 814 815 -- Create Application instance for this app 816 if Application then 817 if osprint then 818 osprint("[run] Creating Application instance for " .. selected_app .. "\n") 819 end 820 app_instance = Application.new(app_dir, { 821 name = selected_app 822 }) 823 -- Set status to running 824 app_instance:setStatus("running") 825 826 -- Store permissions and allowed paths in app instance 827 app_instance.permissions = permissions 828 app_instance.allowedPaths = (manifest and manifest.allowedPaths) or {} 829 830 -- Store manifest as read-only deep copy to prevent tampering 831 local function deepCopyReadOnly(tbl) 832 if type(tbl) ~= "table" then 833 return tbl 834 end 835 836 local copy = {} 837 for k, v in pairs(tbl) do 838 copy[k] = deepCopyReadOnly(v) 839 end 840 841 -- Make table read-only with metatable 842 return setmetatable({}, { 843 __index = copy, 844 __newindex = function() 845 error("Manifest is read-only and cannot be modified") 846 end, 847 __metatable = false -- Hide metatable 848 }) 849 end 850 851 app_instance.manifest = manifest and deepCopyReadOnly(manifest) or {} 852 853 -- Load application icon 854 local iconLoaded = false 855 local defaultIconPath = "/os/res/default.bmp" 856 857 -- Try loading app-specific icon (PNG first, then JPEG, then BMP) 858 local iconPaths = { 859 app_dir .. "/icon.png", 860 app_dir .. "/icon.jpg", 861 app_dir .. "/icon.jpeg", 862 app_dir .. "/icon.bmp" 863 } 864 865 for _, iconPath in ipairs(iconPaths) do 866 if CRamdiskExists and CRamdiskExists(iconPath) then 867 local handle = CRamdiskOpen(iconPath, "r") 868 if handle then 869 local iconData = CRamdiskRead(handle) 870 CRamdiskClose(handle) 871 872 if iconData then 873 local img = nil 874 if iconPath:match("%.png$") and PNGLoad then 875 img = PNGLoad(iconData) 876 elseif (iconPath:match("%.jpg$") or iconPath:match("%.jpeg$")) and JPEGLoad then 877 img = JPEGLoad(iconData) 878 elseif iconPath:match("%.bmp$") and BMPLoad then 879 img = BMPLoad(iconData) 880 end 881 882 if img then 883 local info = ImageGetInfo and ImageGetInfo(img) 884 if info and info.width and info.height then 885 app_instance.iconBuffer = img 886 app_instance.iconWidth = info.width 887 app_instance.iconHeight = info.height 888 iconLoaded = true 889 if osprint then 890 osprint("[run] Loaded icon: " .. iconPath .. " (" .. info.width .. "x" .. info.height .. ")\n") 891 end 892 break 893 end 894 end 895 end 896 end 897 end 898 end 899 900 -- Fall back to default icon if app icon not found 901 if not iconLoaded and CRamdiskExists and CRamdiskExists(defaultIconPath) then 902 local handle = CRamdiskOpen(defaultIconPath, "r") 903 if handle then 904 local iconData = CRamdiskRead(handle) 905 CRamdiskClose(handle) 906 907 if iconData and BMPLoad then 908 local img = BMPLoad(iconData) 909 if img then 910 local info = ImageGetInfo and ImageGetInfo(img) 911 if info and info.width and info.height then 912 app_instance.iconBuffer = img 913 app_instance.iconWidth = info.width 914 app_instance.iconHeight = info.height 915 if osprint then 916 osprint("[run] Loaded default icon for " .. selected_app .. "\n") 917 end 918 end 919 end 920 end 921 end 922 end 923 924 -- Create /proc/$PID directory with process file 925 if CRamdiskMkdir and CRamdiskWrite then 926 local proc_dir = "/proc/" .. app_instance.pid 927 local success, err = CRamdiskMkdir(proc_dir) 928 929 if success or (err and err:match("already exists")) then 930 -- Create process file containing the app path 931 local process_file = proc_dir .. "/process" 932 local write_success = CRamdiskWrite(process_file, app_dir) 933 934 if write_success then 935 if osprint then 936 osprint("Created " .. process_file .. " -> " .. app_dir .. "\n") 937 end 938 else 939 if osprint then 940 osprint("WARNING: Failed to create " .. process_file .. "\n") 941 end 942 end 943 else 944 if osprint then 945 osprint("WARNING: Failed to create " .. proc_dir .. ": " .. tostring(err) .. "\n") 946 end 947 end 948 end 949 950 -- Register in sys.applications using PID as key 951 if osprint then 952 osprint("[run] Attempting to register app, _G.sys = " .. tostring(_G.sys) .. "\n") 953 if _G.sys then 954 osprint("[run] _G.sys.registerApplication = " .. tostring(_G.sys.registerApplication) .. "\n") 955 end 956 end 957 958 if _G.sys and _G.sys.registerApplication then 959 if osprint then 960 osprint("[run] Calling _G.sys.registerApplication for PID " .. app_instance.pid .. "\n") 961 end 962 _G.sys.registerApplication(app_instance) 963 else 964 if osprint then 965 osprint("[run] ERROR: Cannot register app - sys or registerApplication missing!\n") 966 end 967 end 968 969 -- Create a proxy for app that only exposes safe methods 970 -- This prevents apps from directly modifying exports, windows, etc. 971 local safe_app_methods = { 972 "export", "call", "getExport", "listExports", "getInfo", 973 "getExportCount", "getHelp", "printExports", 974 "writeStdout", "getStdout", "clearStdout", 975 "newWindow", "enterFullscreen", "exitFullscreen" 976 } 977 -- Also expose these read-only properties 978 local safe_app_properties = { 979 "appName", "pid", "startTime", "status", "path" 980 } 981 local app_proxy = {} 982 for _, method in ipairs(safe_app_methods) do 983 if app_instance[method] then 984 app_proxy[method] = function(self, ...) 985 return app_instance[method](app_instance, ...) 986 end 987 end 988 end 989 setmetatable(app_proxy, { 990 __index = function(t, k) 991 -- Allow read-only access to safe properties 992 for _, prop in ipairs(safe_app_properties) do 993 if k == prop then 994 return app_instance[k] 995 end 996 end 997 error("app." .. tostring(k) .. " is not accessible", 2) 998 end, 999 __newindex = function(t, k, v) 1000 -- Allow setting path if app has set-path permission 1001 if k == "path" and permissions["set-path"] then 1002 app_instance.path = v 1003 return 1004 end 1005 error("Cannot modify app." .. tostring(k), 2) 1006 end, 1007 __metatable = false 1008 }) 1009 1010 -- Add to sandbox environment 1011 sandbox_env.app = app_proxy 1012 1013 -- Add Timer API scoped to this app 1014 if _G.Timer then 1015 sandbox_env.Timer = _G.Timer.createAPI(selected_app) 1016 end 1017 1018 -- Create print wrapper that captures output to app stdout 1019 local sandbox_print = function(...) 1020 -- Build output string 1021 local n = select('#', ...) 1022 local output = "" 1023 1024 if n == 0 then 1025 output = "\n" 1026 else 1027 local result = tostring(select(1, ...)) 1028 for i = 2, n do 1029 result = result .. "\t" .. tostring(select(i, ...)) 1030 end 1031 output = result .. "\n" 1032 end 1033 1034 -- Write to app stdout 1035 app_instance:writeStdout(output) 1036 1037 -- Also output to osprint if available (for debugging) 1038 if osprint then 1039 osprint(output) 1040 end 1041 end 1042 1043 -- Set the print function in sandbox 1044 sandbox_env.print = sandbox_print 1045 1046 if osprint then 1047 osprint("Application instance created with PID: " .. app_instance.pid .. "\n") 1048 end 1049 1050 -- Check if parent CLI was passed (from run() function) 1051 local parent_cli = _G.parentCli 1052 local cli 1053 1054 if parent_cli then 1055 -- Create SafeCLI wrapper that only exposes write methods 1056 -- DO NOT expose the parent's buffer directly 1057 if osprint then 1058 osprint("Using parent CLI buffer (safe wrapper)\n") 1059 end 1060 1061 cli = { 1062 -- Safe write method - writes to parent buffer 1063 write = function(text) 1064 if parent_cli and parent_cli.write then 1065 parent_cli.write(text) 1066 end 1067 end, 1068 1069 -- Safe writeLine alias 1070 writeLine = function(text) 1071 if parent_cli and parent_cli.write then 1072 parent_cli.write(text) 1073 end 1074 end, 1075 1076 -- Safe getText method - read-only access to parent's buffer 1077 getText = function() 1078 if parent_cli and parent_cli.getText then 1079 return parent_cli.getText() 1080 end 1081 return "" 1082 end 1083 1084 -- DO NOT expose: buffer, clear 1085 -- buffer - direct access could allow modification 1086 -- clear - would clear parent's buffer 1087 } 1088 else 1089 -- Create new CLI buffer table (always available, not just for CLI mode) 1090 local cli_buffer = { 1091 lines = {}, 1092 max_lines = 100 -- Store last 100 lines 1093 } 1094 1095 -- Create CLI helper table (always available for all apps) 1096 cli = { 1097 buffer = cli_buffer, 1098 1099 -- Add line to buffer 1100 write = function(text) 1101 table.insert(cli_buffer.lines, tostring(text)) 1102 -- Trim buffer if it exceeds max lines 1103 while #cli_buffer.lines > cli_buffer.max_lines do 1104 table.remove(cli_buffer.lines, 1) 1105 end 1106 end, 1107 1108 -- Clear buffer 1109 clear = function() 1110 cli_buffer.lines = {} 1111 end, 1112 1113 -- Get all buffer content as string 1114 getText = function() 1115 return table.concat(cli_buffer.lines, "\n") 1116 end 1117 } 1118 1119 -- Add writeLine as an alias for write 1120 cli.writeLine = cli.write 1121 end 1122 1123 -- Add safe cli table to sandbox environment (for all apps) 1124 sandbox_env.cli = cli 1125 1126 -- Parse command line arguments from _G.args.argStr 1127 local parsed_args = {} 1128 if _G.args and _G.args.argStr then 1129 parsed_args = parse_args(_G.args.argStr) 1130 if osprint then 1131 osprint("Parsed arguments from argStr: " .. _G.args.argStr .. "\n") 1132 end 1133 end 1134 1135 -- Add parsed args to sandbox environment 1136 sandbox_env.args = parsed_args 1137 1138 -- Modify print to also call cli.write 1139 local original_print = sandbox_print 1140 local using_parent_cli = (_G.parentCli ~= nil) 1141 1142 sandbox_env.print = function(...) 1143 -- Only call original print if NOT using parent CLI 1144 -- (to avoid duplicate output to stdout when running from shell) 1145 if not using_parent_cli then 1146 original_print(...) 1147 end 1148 1149 -- Write to CLI buffer (either parent's or own) 1150 local n = select('#', ...) 1151 if n == 0 then 1152 cli.write("") 1153 else 1154 local result = tostring(select(1, ...)) 1155 for i = 2, n do 1156 result = result .. "\t" .. tostring(select(i, ...)) 1157 end 1158 cli.write(result) 1159 end 1160 end 1161 1162 -- Check if app is in CLI mode and set up default draw handler 1163 if manifest and manifest.type == "cli" then 1164 1165 -- If app has draw permission, set up default draw method 1166 local has_draw = false 1167 for _, perm in ipairs(permissions) do 1168 if perm == "draw" then 1169 has_draw = true 1170 break 1171 end 1172 end 1173 1174 if has_draw and app_instance then 1175 -- Get the window if it exists 1176 local window = app_instance.window 1177 if window then 1178 -- Set up default onDraw handler that draws the CLI buffer 1179 window:onDraw(function(gfx) 1180 -- Draw black background 1181 gfx:fillRect(0, 0, window.width, window.height, 0x000000) 1182 1183 -- Draw CLI buffer text 1184 local text = cli.getText() 1185 gfx:drawText(10, 10, text, 0x00FF00) -- Green text on black 1186 end) 1187 1188 if osprint then 1189 osprint("CLI mode enabled with default draw handler\n") 1190 end 1191 end 1192 end 1193 end 1194 else 1195 -- No Application module, create basic print wrapper 1196 local sandbox_print = osprint and function(...) 1197 local n = select('#', ...) 1198 if n == 0 then 1199 osprint("\n") 1200 return 1201 end 1202 1203 local result = tostring(select(1, ...)) 1204 for i = 2, n do 1205 result = result .. "\t" .. tostring(select(i, ...)) 1206 end 1207 osprint(result .. "\n") 1208 end or print 1209 1210 sandbox_env.print = sandbox_print 1211 end 1212 1213 if has_filesystem and fsRoot then 1214 -- Load SafeFS module 1215 local safefs_path = "/os/libs/SafeFS.lua" 1216 local safefs_code = read_from_ramdisk(fsRoot, safefs_path) 1217 1218 if safefs_code then 1219 -- Load SafeFS in a temporary environment 1220 local safefs_env = {} 1221 setmetatable(safefs_env, {__index = _G, __metatable = false}) 1222 local safefs_func, err = load(safefs_code, safefs_path, "t", safefs_env) 1223 1224 if safefs_func then 1225 local success, SafeFS = pcall(safefs_func) 1226 1227 if success and SafeFS then 1228 -- Build list of allowed paths 1229 local allowed_paths = {data_dir .. "/*"} -- Always allow data directory 1230 1231 -- Add additional paths from manifest.allowedPaths 1232 if manifest and manifest.allowedPaths and type(manifest.allowedPaths) == "table" then 1233 for _, path in ipairs(manifest.allowedPaths) do 1234 if type(path) == "string" then 1235 -- Replace $ with app directory 1236 local resolved_path = string.gsub(path, "^%$", app_dir) 1237 table.insert(allowed_paths, resolved_path) 1238 if osprint then osprint("Additional path allowed: " .. resolved_path .. "\n") end 1239 end 1240 end 1241 end 1242 1243 -- Ensure data directory exists using C ramdisk functions 1244 if osprint then osprint("DEBUG: Checking data directory: " .. data_dir .. "\n") end 1245 if osprint then osprint("DEBUG: CRamdiskExists=" .. tostring(CRamdiskExists ~= nil) .. ", CRamdiskMkdir=" .. tostring(CRamdiskMkdir ~= nil) .. "\n") end 1246 if CRamdiskExists and CRamdiskMkdir then 1247 local dir_exists = CRamdiskExists(data_dir) 1248 if osprint then osprint("DEBUG: Directory exists: " .. tostring(dir_exists) .. "\n") end 1249 if not dir_exists then 1250 if osprint then osprint("Creating data directory: " .. data_dir .. "\n") end 1251 -- Create directory structure 1252 local parts = split(data_dir, "/") 1253 local path = "" 1254 for _, part in ipairs(parts) do 1255 if part ~= "" then 1256 path = path .. "/" .. part 1257 if not CRamdiskExists(path) then 1258 -- Create this directory 1259 if osprint then osprint(" Creating: " .. path .. "\n") end 1260 local success, err = CRamdiskMkdir(path) 1261 if not success then 1262 if osprint then osprint(" Failed to create directory: " .. tostring(err) .. "\n") end 1263 else 1264 if osprint then osprint(" Directory created successfully\n") end 1265 -- Check if we can now find it 1266 local exists_after = CRamdiskExists(path) 1267 if osprint then osprint(" Exists after creation: " .. tostring(exists_after) .. "\n") end 1268 1269 -- Try to get the directory node to see what was created 1270 if CRamdiskList then 1271 local items = CRamdiskList(path) 1272 if osprint then 1273 osprint(" Directory listing: " .. tostring(items ~= nil) .. "\n") 1274 if items then 1275 osprint(" Number of items in directory: " .. tostring(#items) .. "\n") 1276 end 1277 end 1278 end 1279 end 1280 else 1281 if osprint then osprint(" Already exists: " .. path .. "\n") end 1282 end 1283 end 1284 end 1285 end 1286 end 1287 1288 -- Create SafeFS instance with all allowed paths 1289 -- Pass selected_app as the app ID for file handler registration 1290 -- Pass app_dir as the app path for $ expansion 1291 local fs_instance = SafeFS.new(fsRoot, allowed_paths, "app", selected_app, app_dir) 1292 1293 if fs_instance then 1294 -- Set default CWD to $/data if it exists in allowed paths 1295 local data_path = "/apps/" .. app_name .. "/data" 1296 for _, path in ipairs(allowed_paths) do 1297 if path == data_path or path == data_path .. "/*" then 1298 fs_instance:setCWD(data_path) 1299 break 1300 end 1301 end 1302 1303 -- Mount disk drives if app has /mnt/* access 1304 for _, path in ipairs(allowed_paths) do 1305 if path == "/mnt/*" or path == "/*" or path:match("^/mnt/hd%d") then 1306 if fs_instance.mountAllDiskDrives then 1307 local ok, msg = fs_instance:mountAllDiskDrives() 1308 if osprint then 1309 osprint("[RUN] Disk mount result: " .. tostring(msg) .. "\n") 1310 end 1311 end 1312 break 1313 end 1314 end 1315 1316 -- Create a proxy table that only exposes safe methods 1317 -- This prevents apps from accessing internal properties like allowedDirs 1318 -- Note: createDiskFSMount and mountAllDiskDrives are NOT exposed - they're internal only 1319 local safe_fs_methods = { 1320 "resolvePath", "getCWD", "setCWD", "read", "write", "open", 1321 "delete", "dirs", "files", "exists", "getType", "mkdir", 1322 "fileName", "parentDir", "join", "relativeTo", "copy", "move", 1323 "createPseudoFile", "createPseudoDir", "addFileHandler", 1324 "removeFileHandler" 1325 } 1326 local fs_proxy = {} 1327 for _, method in ipairs(safe_fs_methods) do 1328 if fs_instance[method] then 1329 fs_proxy[method] = function(self, ...) 1330 return fs_instance[method](fs_instance, ...) 1331 end 1332 end 1333 end 1334 setmetatable(fs_proxy, { 1335 __index = function(t, k) 1336 error("fs." .. tostring(k) .. " is not accessible", 2) 1337 end, 1338 __newindex = function(t, k, v) 1339 error("Cannot modify fs object", 2) 1340 end, 1341 __metatable = false 1342 }) 1343 1344 -- Add to sandbox environment 1345 sandbox_env.fs = fs_proxy 1346 if osprint then 1347 osprint("[RUN] Set sandbox_env.fs (proxied) for " .. app_name .. "\n") 1348 end 1349 else 1350 if osprint then 1351 osprint("Warning: Failed to create SafeFS instance for " .. app_name .. "\n") 1352 end 1353 end 1354 1355 if osprint then 1356 osprint("Filesystem access granted to " .. app_name .. ":\n") 1357 for _, path in ipairs(allowed_paths) do 1358 osprint(" " .. path .. "\n") 1359 end 1360 end 1361 else 1362 if osprint then osprint("Warning: Failed to load SafeFS module: " .. tostring(SafeFS) .. "\n") end 1363 end 1364 else 1365 if osprint then osprint("Warning: Failed to compile SafeFS: " .. tostring(err) .. "\n") end 1366 end 1367 else 1368 if osprint then osprint("Warning: SafeFS module not found at " .. safefs_path .. "\n") end 1369 end 1370 end 1371 1372 -- Check for network permission and setup SafeHTTP 1373 local has_network = false 1374 for _, perm in ipairs(permissions) do 1375 if perm == "network" then 1376 has_network = true 1377 break 1378 end 1379 end 1380 1381 if has_network and fsRoot then 1382 -- Load SafeHTTP module 1383 local safehttp_path = "/os/libs/SafeHTTP.lua" 1384 local safehttp_code = read_from_ramdisk(fsRoot, safehttp_path) 1385 1386 if safehttp_code then 1387 local safehttp_func, err = load(safehttp_code, safehttp_path, "t") 1388 1389 if safehttp_func then 1390 local success, SafeHTTP = pcall(safehttp_func) 1391 1392 if success and SafeHTTP then 1393 -- Get allowed domains from manifest 1394 local allowed_domains = (manifest and manifest.allowedDomains) or {} 1395 1396 -- If no domains specified but network permission granted, allow all (backwards compat) 1397 if #allowed_domains == 0 then 1398 if osprint then 1399 osprint("Warning: Network permission granted but no allowedDomains specified - blocking all HTTP\n") 1400 end 1401 -- Don't create SafeHTTP if no domains allowed 1402 else 1403 -- Get NetworkStack if available 1404 if _G.NetworkStack then 1405 -- Create SafeHTTP instance with allowed domains 1406 local http_instance = SafeHTTP.new(_G.NetworkStack, allowed_domains, { 1407 timeout = 30, 1408 max_size = 10485760, -- 10MB default 1409 user_agent = (manifest and manifest.name or "Script") .. "/" .. (manifest and manifest.version or "1.0") 1410 }) 1411 1412 -- Add to sandbox environment 1413 sandbox_env.http = http_instance 1414 1415 if osprint then 1416 osprint("Network access granted to domains:\n") 1417 for _, domain in ipairs(allowed_domains) do 1418 osprint(" " .. domain .. "\n") 1419 end 1420 end 1421 else 1422 if osprint then 1423 osprint("Warning: NetworkStack not available - network permission ineffective\n") 1424 end 1425 end 1426 end 1427 else 1428 if osprint then osprint("Warning: Failed to load SafeHTTP module: " .. tostring(SafeHTTP) .. "\n") end 1429 end 1430 else 1431 if osprint then osprint("Warning: Failed to compile SafeHTTP: " .. tostring(err) .. "\n") end 1432 end 1433 else 1434 if osprint then osprint("Warning: SafeHTTP module not found at " .. safehttp_path .. "\n") end 1435 end 1436 end 1437 1438 -- Check for import permission and setup apps table 1439 local has_import = false 1440 for _, perm in ipairs(permissions) do 1441 if perm == "import" then 1442 has_import = true 1443 break 1444 end 1445 end 1446 1447 if has_import then 1448 -- Create apps table that provides access to Application instances 1449 local apps_table = {} 1450 1451 setmetatable(apps_table, { 1452 __index = function(t, app_name) 1453 -- app_name is like "com.devname.appname" 1454 -- Find the Application instance in sys.applications 1455 local registry = get_app_registry() 1456 for pid, app_instance in pairs(registry) do 1457 if app_instance.appPath and app_instance.appPath:find(app_name, 1, true) then 1458 -- Return the Application instance (which has :call(), :export(), etc.) 1459 rawset(t, app_name, app_instance) 1460 return app_instance 1461 end 1462 end 1463 1464 -- App not found 1465 return nil 1466 end, 1467 __metatable = false -- Prevent metatable access/modification 1468 }) 1469 1470 sandbox_env.apps = apps_table 1471 1472 if osprint then 1473 osprint("Import permission granted - apps table available\n") 1474 end 1475 end 1476 1477 -- Check for scheduling permission and setup os.schedule API 1478 local has_schedule = false 1479 for _, perm in ipairs(permissions) do 1480 if perm == "scheduling" then 1481 has_schedule = true 1482 break 1483 end 1484 end 1485 1486 if has_schedule then 1487 -- Load scheduler module if not already loaded 1488 if not scheduler and fsRoot then 1489 local scheduler_path = "/os/libs/Scheduler.lua" 1490 local scheduler_code = read_from_ramdisk(fsRoot, scheduler_path) 1491 1492 if scheduler_code then 1493 -- Load scheduler in a temporary environment 1494 local scheduler_env = {} 1495 setmetatable(scheduler_env, {__index = _G, __metatable = false}) 1496 local scheduler_func, err = load(scheduler_code, scheduler_path, "t", scheduler_env) 1497 1498 if scheduler_func then 1499 local success, Scheduler = pcall(scheduler_func) 1500 1501 if success and Scheduler then 1502 scheduler = Scheduler 1503 -- Initialize scheduler 1504 scheduler.init() 1505 if osprint then 1506 osprint("Scheduler module loaded and initialized\n") 1507 end 1508 else 1509 if osprint then 1510 osprint("Warning: Failed to load scheduler module: " .. tostring(Scheduler) .. "\n") 1511 end 1512 end 1513 else 1514 if osprint then 1515 osprint("Warning: Failed to compile scheduler: " .. tostring(err) .. "\n") 1516 end 1517 end 1518 else 1519 if osprint then 1520 osprint("Warning: Scheduler module not found at " .. scheduler_path .. "\n") 1521 end 1522 end 1523 end 1524 1525 -- Create os.schedule API if scheduler is available 1526 if scheduler then 1527 -- Create app context for scheduler API 1528 local app_context = { 1529 _app_path = selected_app -- Store the app path for onNextStartUp/onEveryStartUp 1530 } 1531 1532 -- Parse permissions into a table 1533 local perm_table = {} 1534 for _, perm in ipairs(permissions) do 1535 perm_table[perm] = true 1536 end 1537 1538 -- Create the os.schedule API 1539 local schedule_api = scheduler.create_api(app_context, perm_table) 1540 1541 if schedule_api then 1542 -- Create os table if it doesn't exist 1543 if not sandbox_env.os then 1544 sandbox_env.os = {} 1545 end 1546 1547 -- Add schedule API to os table 1548 sandbox_env.os.schedule = schedule_api 1549 1550 if osprint then 1551 osprint("Schedule permission granted - os.schedule API available\n") 1552 end 1553 end 1554 end 1555 end 1556 1557 -- Check for ramdisk permission and add ramdisk functions 1558 local has_ramdisk = false 1559 for _, perm in ipairs(permissions) do 1560 if perm == "ramdisk" then 1561 has_ramdisk = true 1562 break 1563 end 1564 end 1565 1566 if has_ramdisk then 1567 -- Add ramdisk functions 1568 sandbox_env.CRamdiskOpen = _G.CRamdiskOpen 1569 sandbox_env.CRamdiskRead = _G.CRamdiskRead 1570 sandbox_env.CRamdiskWrite = _G.CRamdiskWrite 1571 sandbox_env.CRamdiskClose = _G.CRamdiskClose 1572 sandbox_env.CRamdiskList = _G.CRamdiskList 1573 sandbox_env.CRamdiskExists = _G.CRamdiskExists 1574 sandbox_env.CRamdiskMkdir = _G.CRamdiskMkdir 1575 1576 -- Add GetManifest helper function (requires ramdisk access) 1577 sandbox_env.GetManifest = function(appId) 1578 if type(appId) ~= "string" or appId == "" then 1579 return nil, "Invalid app ID" 1580 end 1581 1582 local manifestPath = "/apps/" .. appId .. "/manifest.lua" 1583 1584 if not _G.CRamdiskExists or not _G.CRamdiskExists(manifestPath) then 1585 return nil, "Manifest not found" 1586 end 1587 1588 local handle = _G.CRamdiskOpen(manifestPath, "r") 1589 if not handle then 1590 return nil, "Failed to open manifest" 1591 end 1592 1593 local manifestCode = _G.CRamdiskRead(handle) 1594 _G.CRamdiskClose(handle) 1595 1596 if not manifestCode or manifestCode == "" then 1597 return nil, "Failed to read manifest" 1598 end 1599 1600 -- Load and execute manifest using loadstring (available in Run.lua context) 1601 local loadFunc = loadstring or load 1602 if not loadFunc then 1603 return nil, "No load function available" 1604 end 1605 1606 local manifestFunc, loadErr = loadFunc(manifestCode, "manifest_" .. appId) 1607 if not manifestFunc then 1608 return nil, "Failed to load manifest" 1609 end 1610 1611 local success, manifest = pcall(manifestFunc) 1612 if not success then 1613 return nil, "Failed to execute manifest" 1614 end 1615 1616 if type(manifest) ~= "table" then 1617 return nil, "Manifest did not return a table" 1618 end 1619 1620 return manifest 1621 end 1622 1623 if osprint then 1624 osprint("Ramdisk permission granted - ramdisk functions available\n") 1625 end 1626 end 1627 1628 -- Check for draw permission and add graphics/image functions 1629 local has_draw = false 1630 for _, perm in ipairs(permissions) do 1631 if perm == "draw" then 1632 has_draw = true 1633 break 1634 end 1635 end 1636 1637 if has_draw then 1638 -- Add VESA graphics functions 1639 sandbox_env.VESAInit = _G.VESAInit 1640 sandbox_env.VESASetMode = _G.VESASetMode 1641 sandbox_env.VESAClearScreen = _G.VESAClearScreen 1642 sandbox_env.VESADrawPixel = _G.VESADrawPixel 1643 sandbox_env.VESAFillRect = _G.VESAFillRect 1644 sandbox_env.VESADrawRect = _G.VESADrawRect 1645 sandbox_env.VESADrawChar = _G.VESADrawChar 1646 sandbox_env.VESADrawString = _G.VESADrawString 1647 sandbox_env.VESAGetInfo = _G.VESAGetInfo 1648 sandbox_env.VESAInspectBuffer = _G.VESAInspectBuffer 1649 sandbox_env.VESASetRenderTarget = _G.VESASetRenderTarget 1650 sandbox_env.VESAProcessBufferedDrawOps = _G.VESAProcessBufferedDrawOps 1651 sandbox_env.VESABlitWindowBufferRegion = _G.VESABlitWindowBufferRegion 1652 1653 -- Add image loading/drawing functions 1654 sandbox_env.PNGLoad = _G.PNGLoad 1655 sandbox_env.BMPLoad = _G.BMPLoad 1656 sandbox_env.JPEGLoad = _G.JPEGLoad 1657 sandbox_env.ImageDraw = _G.ImageDraw 1658 sandbox_env.ImageDrawScaled = _G.ImageDrawScaled 1659 sandbox_env.ImageGetInfo = _G.ImageGetInfo 1660 sandbox_env.ImageGetWidth = _G.ImageGetWidth 1661 sandbox_env.ImageGetHeight = _G.ImageGetHeight 1662 sandbox_env.ImageGetPixel = _G.ImageGetPixel 1663 sandbox_env.ImageGetBufferBGRA = _G.ImageGetBufferBGRA 1664 sandbox_env.ImageDestroy = _G.ImageDestroy 1665 1666 -- Add partial window update function for efficient drawing (e.g., paint apps) 1667 sandbox_env.updateWindowRegion = _G.updateWindowRegion 1668 1669 if osprint then 1670 osprint("Draw permission granted - graphics functions available\n") 1671 osprint(" BMPLoad in _G: " .. tostring(_G.BMPLoad ~= nil) .. "\n") 1672 osprint(" BMPLoad in sandbox: " .. tostring(sandbox_env.BMPLoad ~= nil) .. "\n") 1673 osprint(" JPEGLoad in _G: " .. tostring(_G.JPEGLoad ~= nil) .. "\n") 1674 osprint(" JPEGLoad in sandbox: " .. tostring(sandbox_env.JPEGLoad ~= nil) .. "\n") 1675 osprint(" ImageGetWidth in _G: " .. tostring(_G.ImageGetWidth ~= nil) .. "\n") 1676 osprint(" ImageGetHeight in _G: " .. tostring(_G.ImageGetHeight ~= nil) .. "\n") 1677 end 1678 end 1679 1680 -- Check for imaging permission and add Image library 1681 local has_imaging = false 1682 for _, perm in ipairs(permissions) do 1683 if perm == "imaging" then 1684 has_imaging = true 1685 break 1686 end 1687 end 1688 1689 if has_imaging and _G.Image then 1690 sandbox_env.Image = _G.Image 1691 if osprint then 1692 osprint("Imaging permission granted - Image library available\n") 1693 end 1694 end 1695 1696 -- Check for run permission and setup run function 1697 local has_run = false 1698 for _, perm in ipairs(permissions) do 1699 if perm == "run" then 1700 has_run = true 1701 break 1702 end 1703 end 1704 1705 if has_run then 1706 -- Create run function that accepts app name and arguments 1707 -- Supports both: run("progname", "arg1 -v") and run("progname", "arg1", "-v") 1708 sandbox_env.run = function(prog_name, ...) 1709 local args = {...} 1710 local argStr = "" 1711 1712 -- Build argument string 1713 if #args == 0 then 1714 argStr = "" 1715 elseif #args == 1 and type(args[1]) == "string" then 1716 -- Single string argument: run("prog", "arg1 -v") 1717 argStr = args[1] 1718 else 1719 -- Multiple arguments: run("prog", "arg1", "-v") 1720 local parts = {} 1721 for i, arg in ipairs(args) do 1722 table.insert(parts, tostring(arg)) 1723 end 1724 argStr = table.concat(parts, " ") 1725 end 1726 1727 -- Set global args for the program being run 1728 if not _G.args then 1729 _G.args = {} 1730 end 1731 _G.args.argStr = argStr 1732 1733 -- Pass parent's CLI buffer to child process 1734 _G.parentCli = sandbox_env.cli 1735 1736 -- Run the program - use _G.fsRoot to ensure we have the correct filesystem root 1737 local success, app = run.execute(prog_name, _G.fsRoot or fsRoot) 1738 1739 -- Clear args and parentCli after running 1740 _G.args.argStr = nil 1741 _G.parentCli = nil 1742 1743 return success, app 1744 end 1745 1746 if osprint then 1747 osprint("Run permission granted - run() function available\n") 1748 end 1749 end 1750 1751 -- Check for load permission and add load function 1752 local has_load = false 1753 for _, perm in ipairs(permissions) do 1754 if perm == "load" then 1755 has_load = true 1756 break 1757 end 1758 end 1759 1760 if has_load then 1761 -- Grant access to load() function for Lua interpreter 1762 -- Create a safe loadstring wrapper that sets the environment 1763 -- Optionally accepts a custom environment table as third parameter 1764 sandbox_env.loadstring = function(code, chunkname, env) 1765 local func, err = _G.loadstring(code, chunkname) 1766 if func then 1767 -- Use provided environment or default to sandbox_env 1768 local ok, setenv_err = pcall(_G.setfenv, func, env or sandbox_env) 1769 if not ok then 1770 return nil, "setfenv failed: " .. tostring(setenv_err) 1771 end 1772 end 1773 return func, err 1774 end 1775 1776 if osprint then 1777 osprint("Load permission granted - loadstring() function available\n") 1778 end 1779 end 1780 1781 -- Check for setfenv permission 1782 local has_setfenv = false 1783 for _, perm in ipairs(permissions) do 1784 if perm == "setfenv" then 1785 has_setfenv = true 1786 break 1787 end 1788 end 1789 1790 if has_setfenv then 1791 -- Grant access to setfenv for environment manipulation 1792 -- Use direct reference (available in Run.lua context) rather than _G 1793 sandbox_env.setfenv = setfenv 1794 sandbox_env.getfenv = getfenv 1795 1796 if osprint then 1797 osprint("Setfenv permission granted - setfenv/getfenv functions available\n") 1798 end 1799 end 1800 1801 -- Check for system permission and setup system information API 1802 local has_system = false 1803 for _, perm in ipairs(permissions) do 1804 if perm == "system" then 1805 has_system = true 1806 break 1807 end 1808 end 1809 1810 if has_system then 1811 -- Create system API with read-only access to system information 1812 sandbox_env.system = { 1813 -- Get list of running applications 1814 getApplications = function() 1815 if _G.sys and _G.sys.getAllApplications then 1816 return _G.sys.getAllApplications() 1817 end 1818 return {} 1819 end 1820 } 1821 1822 if osprint then 1823 osprint("System permission granted - system information API available\n") 1824 end 1825 end 1826 1827 -- Check for global-environment permission (gives full _G access) 1828 local has_global_env = false 1829 for _, perm in ipairs(permissions) do 1830 if perm == "global-environment" then 1831 has_global_env = true 1832 break 1833 end 1834 end 1835 1836 -- SECURITY: Scan and remove dangerous globals that should never be in sandbox 1837 local dangerous_globals = { 1838 "_G", -- Global environment access 1839 "fsRoot", -- Direct filesystem access 1840 "gfx", -- Raw graphics access 1841 "gfx_api", -- Raw graphics API 1842 "run", -- Run module internals (except run() function if granted) 1843 "sys", -- System internals (except system permission API) 1844 "ADMIN_AppAddPermission", -- Admin-only functions 1845 "ADMIN_AppAddPath", 1846 "ADMIN_StartPrompt", 1847 "ADMIN_FinishPrompt", 1848 "VESAInit", -- Raw VESA (only if draw permission not granted) 1849 "VESASetMode", 1850 "VESAClearScreen", 1851 "VESADrawPixel", 1852 "VESADrawRect", 1853 "VESADrawText", 1854 "VESASetPixel", 1855 "VESAGetInfo", 1856 "CRamdiskOpen", -- Raw ramdisk (only if ramdisk permission not granted) 1857 "CRamdiskRead", 1858 "CRamdiskWrite", 1859 "CRamdiskClose", 1860 "CRamdiskList", 1861 "CRamdiskExists", 1862 "CRamdiskMkdir", 1863 "getfenv", -- Environment manipulation 1864 "setfenv", 1865 "rawget", -- Raw access functions (keep rawset for sandbox internals) 1866 "rawequal", 1867 "dofile", -- File execution 1868 "loadfile", 1869 "load" -- Dynamic code loading (unless explicitly added) 1870 } 1871 1872 -- Remove dangerous globals (except those explicitly granted by permissions) 1873 -- Skip this for global-environment permission (full _G access) 1874 if not has_global_env then 1875 for _, key in ipairs(dangerous_globals) do 1876 -- Only remove if it wasn't explicitly set by permission system above 1877 -- Check if it exists and wasn't added intentionally 1878 if sandbox_env[key] ~= nil then 1879 -- Special cases: these are OK if granted by permissions 1880 local is_permitted = false 1881 1882 -- Check if it's a permitted VESA function (draw permission) 1883 if key:match("^VESA") and has_draw then 1884 is_permitted = true 1885 end 1886 1887 -- Check if it's a permitted ramdisk function (ramdisk permission) 1888 if key:match("^CRamdisk") and has_ramdisk then 1889 is_permitted = true 1890 end 1891 1892 -- Check if it's the run() function (run permission) 1893 if key == "run" and has_run then 1894 is_permitted = true 1895 end 1896 1897 -- Check if it's the load() function (load permission) 1898 if key == "load" and has_load then 1899 is_permitted = true 1900 end 1901 1902 -- Check if it's the sys object (system-all permission) 1903 if key == "sys" and has_system_all then 1904 is_permitted = true 1905 end 1906 1907 -- Check if it's an admin function (admin permission) 1908 if (key == "ADMIN_AppAddPermission" or key == "ADMIN_AppAddPath" or 1909 key == "ADMIN_StartPrompt" or key == "ADMIN_FinishPrompt") and has_admin then 1910 is_permitted = true 1911 end 1912 1913 -- Check if it's setfenv/getfenv (setfenv permission) 1914 if (key == "setfenv" or key == "getfenv") and has_setfenv then 1915 is_permitted = true 1916 end 1917 1918 -- Remove if not permitted 1919 if not is_permitted then 1920 sandbox_env[key] = nil 1921 if osprint then 1922 osprint("SECURITY: Removed dangerous global from sandbox: " .. key .. "\n") 1923 end 1924 end 1925 end 1926 end 1927 else 1928 if osprint then 1929 osprint("SECURITY: Skipping dangerous globals removal (global-environment permission)\n") 1930 end 1931 end 1932 1933 -- Add _G reference to sandbox itself (or real _G for global-environment) 1934 if has_global_env then 1935 sandbox_env._G = _G -- Full global access 1936 else 1937 sandbox_env._G = sandbox_env 1938 end 1939 1940 -- Create require function that searches app's src directory first 1941 local app_src_dir = app_dir .. "/src" 1942 local require_cache = {} 1943 1944 -- Track modules currently being loaded to detect circular requires 1945 local loading_modules = {} 1946 1947 sandbox_env.require = function(module_name, use_bytecode) 1948 if type(module_name) ~= "string" or module_name == "" then 1949 error("require: module name must be a non-empty string", 2) 1950 end 1951 1952 if osprint then 1953 osprint("[require] Loading: " .. module_name .. (use_bytecode and " (bytecode)" or "") .. "\n") 1954 end 1955 1956 -- Check cache first 1957 if require_cache[module_name] then 1958 if osprint then 1959 osprint("[require] Cache hit: " .. module_name .. "\n") 1960 end 1961 return require_cache[module_name] 1962 end 1963 1964 -- Check if module is pre-loaded in sandbox (e.g., Dialog, Application) 1965 -- Use rawget to avoid metatable strict checking 1966 local preloaded = rawget(sandbox_env, module_name) 1967 if preloaded ~= nil then 1968 if osprint then 1969 osprint("[require] Using pre-loaded module: " .. module_name .. "\n") 1970 end 1971 require_cache[module_name] = preloaded 1972 return preloaded 1973 end 1974 1975 -- Check for circular require 1976 if loading_modules[module_name] then 1977 error("require: circular dependency detected for '" .. module_name .. "'", 2) 1978 end 1979 1980 -- Mark as loading 1981 loading_modules[module_name] = true 1982 1983 -- Convert module name to path (replace . with /) 1984 local module_path = module_name:gsub("%.", "/") 1985 1986 -- Search paths in order: 1987 -- If use_bytecode is true, check .luac first 1988 -- Otherwise only check .lua files 1989 local search_paths 1990 if use_bytecode then 1991 search_paths = { 1992 app_src_dir .. "/" .. module_path .. ".luac", 1993 app_src_dir .. "/" .. module_path .. ".lua", 1994 "/os/libs/" .. module_path .. ".luac", 1995 "/os/libs/" .. module_path .. ".lua", 1996 } 1997 else 1998 search_paths = { 1999 app_src_dir .. "/" .. module_path .. ".lua", 2000 "/os/libs/" .. module_path .. ".lua", 2001 } 2002 end 2003 2004 local module_content = nil 2005 local found_path = nil 2006 2007 for _, path in ipairs(search_paths) do 2008 if osprint then 2009 osprint("[require] Trying: " .. path .. "\n") 2010 end 2011 module_content = read_from_ramdisk(fsRoot, path) 2012 if module_content then 2013 found_path = path 2014 if osprint then 2015 osprint("[require] Found: " .. path .. " (" .. #module_content .. " bytes)\n") 2016 end 2017 break 2018 end 2019 end 2020 2021 if not module_content then 2022 loading_modules[module_name] = nil 2023 error("require: module '" .. module_name .. "' not found\n searched:\n " .. 2024 table.concat(search_paths, "\n "), 2) 2025 end 2026 2027 -- Load the module in sandbox environment 2028 local loadFunc = loadstring or load 2029 local module_func, load_err 2030 2031 if osprint then 2032 osprint("[require] Compiling: " .. module_name .. " (" .. #module_content .. " bytes)\n") 2033 end 2034 2035 if loadFunc == loadstring then 2036 if osprint then 2037 osprint("[require] Calling loadstring...\n") 2038 end 2039 local compile_ok, compile_result, compile_err = pcall(function() 2040 return loadFunc(module_content, found_path) 2041 end) 2042 if osprint then 2043 osprint("[require] loadstring pcall returned: " .. tostring(compile_ok) .. "\n") 2044 end 2045 if compile_ok then 2046 module_func = compile_result 2047 load_err = compile_err 2048 else 2049 module_func = nil 2050 load_err = compile_result 2051 end 2052 if module_func then 2053 if osprint then 2054 osprint("[require] Setting environment...\n") 2055 end 2056 setfenv(module_func, sandbox_env) 2057 if osprint then 2058 osprint("[require] Environment set\n") 2059 end 2060 end 2061 else 2062 module_func, load_err = loadFunc(module_content, found_path, "t", sandbox_env) 2063 end 2064 2065 if not module_func then 2066 loading_modules[module_name] = nil 2067 error("require: failed to load module '" .. module_name .. "': " .. tostring(load_err), 2) 2068 end 2069 2070 if osprint then 2071 osprint("[require] Executing: " .. module_name .. "\n") 2072 end 2073 2074 -- Execute the module 2075 local success, result = pcall(module_func) 2076 2077 -- Clear loading flag 2078 loading_modules[module_name] = nil 2079 2080 if not success then 2081 error("require: failed to execute module '" .. module_name .. "': " .. tostring(result), 2) 2082 end 2083 2084 if osprint then 2085 osprint("[require] Completed: " .. module_name .. "\n") 2086 end 2087 2088 -- Cache the result (use true if module returns nil) 2089 local cached_value = result 2090 if cached_value == nil then 2091 cached_value = true 2092 end 2093 require_cache[module_name] = cached_value 2094 2095 return result 2096 end 2097 2098 if osprint then 2099 osprint("require() function available (searches " .. app_src_dir .. " first)\n") 2100 end 2101 2102 -- Set up metatable based on permissions 2103 if has_global_env then 2104 -- Global environment permission: give direct access to _G 2105 -- This is for system-level apps like the installer that need full access 2106 setmetatable(sandbox_env, { 2107 __index = _G, 2108 __newindex = function(t, k, v) 2109 rawset(t, k, v) -- Allow setting new globals within sandbox 2110 end, 2111 __metatable = _G -- As requested, metatable = _G 2112 }) 2113 if osprint then 2114 osprint("Global-environment permission granted - full _G access available\n") 2115 end 2116 else 2117 -- Standard sandbox: prevent access to undefined globals 2118 -- Track which keys are allowed (even if nil) vs truly undefined 2119 local allowed_keys = {} 2120 for k, _ in pairs(sandbox_env) do 2121 allowed_keys[k] = true 2122 end 2123 -- Also allow keys that were explicitly set to nil or may be nil 2124 allowed_keys.fs = true 2125 allowed_keys.crypto = true 2126 allowed_keys.sys = true 2127 allowed_keys.window = true 2128 allowed_keys.JPEGLoad = true 2129 allowed_keys.PNGLoad = true 2130 allowed_keys.BMPLoad = true 2131 allowed_keys.ImageGetWidth = true 2132 allowed_keys.ImageGetHeight = true 2133 allowed_keys.ImageGetPixel = true 2134 allowed_keys.ImageGetBufferBGRA = true 2135 allowed_keys.ImageDraw = true 2136 allowed_keys.ImageDrawScaled = true 2137 allowed_keys.ImageDestroy = true 2138 allowed_keys.ImageGetInfo = true 2139 allowed_keys.setfenv = true 2140 allowed_keys.getfenv = true 2141 allowed_keys.loadstring = true 2142 2143 setmetatable(sandbox_env, { 2144 __index = function(t, k) 2145 if allowed_keys[k] then 2146 return nil -- Key is allowed but not set, return nil 2147 end 2148 error("Attempt to access undefined global: " .. tostring(k), 2) 2149 end, 2150 __newindex = function(t, k, v) 2151 allowed_keys[k] = true -- Mark as allowed when set 2152 rawset(t, k, v) 2153 end, 2154 __metatable = false -- Prevent metatable access/modification 2155 }) 2156 end 2157 2158 -- Register sandbox environment with sys (after sandbox is fully set up) 2159 -- Note: app_instance is already defined above, don't redefine it from the proxy 2160 if app_instance and app_instance.pid and _G.sys and _G.sys.registerEnvironment then 2161 if osprint then 2162 osprint("[run] Registering sandbox environment for PID " .. app_instance.pid .. "\n") 2163 end 2164 _G.sys.registerEnvironment(app_instance.pid, sandbox_env) 2165 end 2166 2167 -- Initialize global proc table if needed 2168 if not _G.proc then 2169 _G.proc = {} 2170 end 2171 2172 -- Populate global proc table with sandbox instances 2173 local app = rawget(sandbox_env, "app") 2174 if app and app.pid then 2175 local pid = app.pid 2176 _G.proc[pid] = { 2177 id = pid, 2178 fs = rawget(sandbox_env, "fs"), -- SafeFS instance (may be nil) 2179 gfx = rawget(sandbox_env, "gfx"), -- SafeGfx instance (may be nil) 2180 http = rawget(sandbox_env, "http"), -- SafeHTTP instance (may be nil) 2181 app = app -- Application instance 2182 } 2183 2184 if osprint then 2185 osprint("Registered process " .. pid .. " in global proc table\n") 2186 osprint("[DEBUG] About to load app code with load()...\n") 2187 end 2188 end 2189 2190 -- Check if this is an HTML type app 2191 if manifest and manifest.type == "html" then 2192 if osprint then 2193 osprint("[DEBUG] HTML type app detected, creating HTML window\n") 2194 end 2195 2196 -- Extract window dimensions from HTML meta tags 2197 -- Supports: <meta name="window-width" content="500"> or value="500" 2198 local html_width = manifest.width or 800 2199 local html_height = manifest.height or 600 2200 -- Try content= first, then value= 2201 local meta_width = string.match(init_content, '<meta[^>]+name=["\']window%-width["\'][^>]+content=["\'](%d+)["\']') 2202 or string.match(init_content, '<meta[^>]+name=["\']window%-width["\'][^>]+value=["\'](%d+)["\']') 2203 local meta_height = string.match(init_content, '<meta[^>]+name=["\']window%-height["\'][^>]+content=["\'](%d+)["\']') 2204 or string.match(init_content, '<meta[^>]+name=["\']window%-height["\'][^>]+value=["\'](%d+)["\']') 2205 if meta_width then html_width = tonumber(meta_width) or html_width end 2206 if meta_height then html_height = tonumber(meta_height) or html_height end 2207 2208 -- For HTML apps, init_content is HTML, not Lua 2209 local htmlWindow, htmlErr 2210 2211 if _G.sys and _G.sys.browser then 2212 htmlWindow, htmlErr = _G.sys.browser:newHTMLWindow({ 2213 app = sandbox_env.app, 2214 html = init_content, 2215 file = init_file, 2216 fs = rawget(sandbox_env, "fs"), -- Use rawget to avoid metatable errors 2217 width = html_width, 2218 height = html_height, 2219 title = manifest.name or "HTML App", 2220 Dialog = rawget(sandbox_env, "Dialog"), 2221 loadstring = rawget(sandbox_env, "loadstring"), 2222 showStatusBar = manifest.showStatusBar or false, 2223 onSubmit = function(formData) 2224 if osprint then 2225 osprint("Form submitted: " .. tostring(formData) .. "\n") 2226 end 2227 end, 2228 }) 2229 else 2230 htmlErr = "sys.browser not available" 2231 end 2232 2233 if not htmlWindow then 2234 if osprint then 2235 osprint("ERROR: Failed to create HTML window: " .. tostring(htmlErr) .. "\n") 2236 end 2237 local err_msg = "Failed to create HTML window: " .. tostring(htmlErr) 2238 print("Error: " .. err_msg) 2239 return false, err_msg 2240 end 2241 2242 if osprint then 2243 osprint("HTML window created successfully\n") 2244 end 2245 2246 -- Store HTML window reference 2247 sandbox_env._htmlWindow = htmlWindow 2248 2249 -- Trigger ApplicationStart hook 2250 if _G.sys and _G.sys.hook and sandbox_env.app then 2251 _G.sys.hook:run("ApplicationStart", sandbox_env.app) 2252 end 2253 2254 -- Attach CLI buffer to app instance for proper output handling 2255 if app_instance and app_instance.attachCLI then 2256 app_instance:attachCLI(cli) 2257 if osprint then 2258 osprint("[run] Attached CLI to HTML app instance\n") 2259 end 2260 end 2261 2262 return true, app_instance 2263 end 2264 2265 -- Load and execute the app in the sandbox (Lua apps) 2266 if osprint then 2267 osprint("[DEBUG] Calling load() on init file: " .. init_file .. "\n") 2268 osprint("[DEBUG] init_content length: " .. #init_content .. " bytes\n") 2269 osprint("[DEBUG] sandbox_env type: " .. type(sandbox_env) .. "\n") 2270 osprint("[DEBUG] Testing with small chunk first...\n") 2271 end 2272 2273 -- Test: Try loading a simple string first to verify loadstring works 2274 local testFunc = loadstring("return 1") 2275 if osprint then 2276 if testFunc then 2277 osprint("[DEBUG] Test loadstring succeeded\n") 2278 else 2279 osprint("[DEBUG] Test loadstring FAILED\n") 2280 end 2281 end 2282 2283 if osprint then 2284 osprint("[DEBUG] About to call loadstring on actual content...\n") 2285 osprint("[DEBUG] First 100 chars: " .. init_content:sub(1, 100) .. "\n") 2286 osprint("[DEBUG] Last 100 chars: " .. init_content:sub(-100) .. "\n") 2287 end 2288 2289 -- Try loadstring first (LuaJIT), fallback to load 2290 local loadFunc = loadstring or load 2291 local app_func, err 2292 2293 if loadFunc == loadstring then 2294 if osprint then 2295 osprint("[DEBUG] Using loadstring (LuaJIT)\n") 2296 osprint("[DEBUG] Calling loadstring on full content...\n") 2297 end 2298 2299 -- LuaJIT loadstring doesn't take mode parameter 2300 -- Wrap in pcall to catch any crashes 2301 local success, result1, result2 = pcall(function() 2302 return loadFunc(init_content, init_file) 2303 end) 2304 2305 if osprint then 2306 osprint("[DEBUG] pcall returned, success=" .. tostring(success) .. "\n") 2307 end 2308 2309 if success then 2310 app_func = result1 2311 err = result2 2312 if osprint then 2313 if app_func then 2314 osprint("[DEBUG] loadstring succeeded, app_func=" .. type(app_func) .. "\n") 2315 else 2316 osprint("[DEBUG] loadstring FAILED: " .. tostring(err) .. "\n") 2317 end 2318 end 2319 else 2320 if osprint then 2321 osprint("[DEBUG] loadstring CRASHED: " .. tostring(result1) .. "\n") 2322 end 2323 err = "loadstring crashed: " .. tostring(result1) 2324 end 2325 if app_func then 2326 -- Set environment after loading 2327 if osprint then 2328 osprint("[DEBUG] Setting environment with setfenv...\n") 2329 end 2330 setfenv(app_func, sandbox_env) 2331 if osprint then 2332 osprint("[DEBUG] setfenv completed\n") 2333 end 2334 end 2335 else 2336 if osprint then 2337 osprint("[DEBUG] Using load (Lua 5.2+)\n") 2338 end 2339 -- Standard Lua 5.2+ load function 2340 app_func, err = loadFunc(init_content, init_file, "t", sandbox_env) 2341 end 2342 2343 if osprint then 2344 osprint("[DEBUG] load/loadstring phase completed\n") 2345 end 2346 2347 if not app_func then 2348 if osprint then 2349 osprint("ERROR: Failed to load app: " .. err .. "\n") 2350 end 2351 print("Error: " .. tostring(err)) 2352 return false, tostring(err) 2353 end 2354 2355 if osprint then 2356 osprint("[DEBUG] load() succeeded, app_func created\n") 2357 osprint("[run] Executing app code...\n") 2358 end 2359 2360 local success, result = pcall(app_func) 2361 2362 if osprint then 2363 osprint("[DEBUG] pcall completed, success=" .. tostring(success) .. "\n") 2364 end 2365 2366 -- Trigger ApplicationStart hook (requires admin permission) 2367 if success and sys and sys.hook and sandbox_env.app then 2368 sys.hook:run("ApplicationStart", sandbox_env.app) 2369 end 2370 2371 if not success then 2372 if osprint then 2373 osprint("[run] ERROR: App execution failed: " .. tostring(result) .. "\n") 2374 end 2375 print("Error: App execution failed: " .. result) 2376 2377 -- Mark app as stopped if it exists 2378 if Application and app_instance then 2379 app_instance:setStatus("stopped", "execution_error: " .. tostring(result)) 2380 2381 -- Clean up /proc/$PID directory 2382 local pid = app_instance.pid 2383 if pid and CRamdiskRemove then 2384 local proc_dir = "/proc/" .. pid 2385 CRamdiskRemove(proc_dir .. "/process") 2386 CRamdiskRemove(proc_dir) 2387 if osprint then 2388 osprint("Cleaned up " .. proc_dir .. "\n") 2389 end 2390 end 2391 2392 -- Clean up global proc table 2393 if pid and _G.proc then 2394 _G.proc[pid] = nil 2395 if osprint then 2396 osprint("Removed process " .. pid .. " from proc table\n") 2397 end 2398 end 2399 2400 -- Unregister from sys.applications 2401 if _G.sys and _G.sys.unregisterApplication then 2402 _G.sys.unregisterApplication(pid) 2403 end 2404 end 2405 2406 return false, tostring(result) 2407 end 2408 2409 print("App completed successfully") 2410 2411 -- Check if app should keep running (has windows or callbacks) 2412 local keepRunning = false 2413 if Application and app_instance then 2414 if app_instance.windows and #app_instance.windows > 0 then 2415 keepRunning = true 2416 if osprint then 2417 osprint("App has " .. #app_instance.windows .. " windows, keeping it running\n") 2418 end 2419 end 2420 end 2421 2422 -- Only mark app as stopped and clean up if it has no windows 2423 if not keepRunning and Application and app_instance then 2424 app_instance:setStatus("stopped", "no_windows") 2425 2426 -- Clean up /proc/$PID directory 2427 local pid = app_instance.pid 2428 if pid and CRamdiskRemove then 2429 local proc_dir = "/proc/" .. pid 2430 CRamdiskRemove(proc_dir .. "/process") 2431 CRamdiskRemove(proc_dir) 2432 if osprint then 2433 osprint("Cleaned up " .. proc_dir .. "\n") 2434 end 2435 end 2436 2437 -- Clean up global proc table 2438 if pid and _G.proc then 2439 _G.proc[pid] = nil 2440 if osprint then 2441 osprint("Removed process " .. pid .. " from proc table\n") 2442 end 2443 end 2444 2445 -- Unregister from sys.applications 2446 if _G.sys and _G.sys.unregisterApplication then 2447 _G.sys.unregisterApplication(pid) 2448 end 2449 end 2450 2451 -- Attach CLI buffer to app instance for easy access 2452 if app_instance and sandbox_env.cli then 2453 app_instance.cli = sandbox_env.cli 2454 if osprint then 2455 osprint("[run] Attached CLI to app instance\n") 2456 osprint("[run] CLI has getText: " .. tostring(sandbox_env.cli.getText ~= nil) .. "\n") 2457 osprint("[run] CLI has buffer: " .. tostring(sandbox_env.cli.buffer ~= nil) .. "\n") 2458 if sandbox_env.cli.getText then 2459 local text = sandbox_env.cli.getText() 2460 osprint("[run] CLI getText() returns: " .. tostring(text) .. "\n") 2461 end 2462 end 2463 end 2464 2465 -- Return success and the app instance (so caller can get PID, stdout, etc) 2466 return true, app_instance 2467 end 2468 2469 -- Export to global namespace for easy access 2470 _G.run = run 2471 2472 -- Export module functions 2473 return run