Scheduler.lua (12951B)
1 #!/usr/bin/env luajit 2 -- Task Scheduler Module 3 -- Provides cron-like scheduling and startup task management 4 5 local scheduler = {} 6 7 -- Storage for scheduled tasks 8 local scheduled_tasks = { 9 next_startup = nil, -- Single task to run on next startup 10 every_startup = {}, -- List of tasks to run on every startup 11 cron = {} -- Named cron-style scheduled tasks 12 } 13 14 -- Task storage file path 15 local SCHEDULER_FILE = "/tmp/scheduler_tasks.lua" 16 17 -- Special constant for removing next startup task 18 scheduler.NEXT_START_UP = "NEXT_START_UP" 19 20 --- Parse cron expression: "sec min hour dayOfMonth month dayOfWeek year millisec" 21 --- Format: "1 1 0 * * * * *" means every year at 00:01:01 22 --- Returns table with parsed fields or nil if invalid 23 local function parse_cron(expression) 24 if type(expression) ~= "string" then 25 return nil 26 end 27 28 local parts = {} 29 for part in string.gmatch(expression, "%S+") do 30 table.insert(parts, part) 31 end 32 33 -- Expect 8 fields: sec min hour dayOfMonth month dayOfWeek year millisec 34 if #parts ~= 8 then 35 return nil 36 end 37 38 return { 39 second = parts[1], 40 minute = parts[2], 41 hour = parts[3], 42 day_of_month = parts[4], 43 month = parts[5], 44 day_of_week = parts[6], 45 year = parts[7], 46 millisecond = parts[8] 47 } 48 end 49 50 --- Check if a cron field matches the current time value 51 --- field: cron field value (e.g., "*", "1", "1-5", "*/2") 52 --- value: current time value to match against 53 local function cron_field_matches(field, value) 54 if field == "*" then 55 return true 56 end 57 58 -- Check for exact match 59 if tonumber(field) == value then 60 return true 61 end 62 63 -- Check for range (e.g., "1-5") 64 local range_start, range_end = string.match(field, "^(%d+)%-(%d+)$") 65 if range_start and range_end then 66 range_start = tonumber(range_start) 67 range_end = tonumber(range_end) 68 if value >= range_start and value <= range_end then 69 return true 70 end 71 end 72 73 -- Check for step values (e.g., "*/5") 74 local step = string.match(field, "^%*%/(%d+)$") 75 if step then 76 step = tonumber(step) 77 if value % step == 0 then 78 return true 79 end 80 end 81 82 return false 83 end 84 85 --- Check if a cron expression matches the current time 86 --- cron: parsed cron table 87 --- current_time: table with {year, month, day, hour, min, sec, wday} 88 local function cron_matches(cron, current_time) 89 if not cron or not current_time then 90 return false 91 end 92 93 -- Match each field (ignore milliseconds for now) 94 return cron_field_matches(cron.second, current_time.sec) and 95 cron_field_matches(cron.minute, current_time.min) and 96 cron_field_matches(cron.hour, current_time.hour) and 97 cron_field_matches(cron.day_of_month, current_time.day) and 98 cron_field_matches(cron.month, current_time.month) and 99 cron_field_matches(cron.day_of_week, current_time.wday) and 100 cron_field_matches(cron.year, current_time.year) 101 end 102 103 --- Save scheduled tasks to persistent storage 104 local function save_tasks() 105 local file, err = io.open(SCHEDULER_FILE, "w") 106 if not file then 107 if osprint then 108 osprint("Scheduler: Failed to save tasks: " .. tostring(err) .. "\n") 109 end 110 return false 111 end 112 113 -- Serialize the tasks table 114 file:write("return {\n") 115 116 -- Save next_startup 117 if scheduled_tasks.next_startup then 118 file:write(" next_startup = " .. string.format("%q", scheduled_tasks.next_startup) .. ",\n") 119 end 120 121 -- Save every_startup 122 file:write(" every_startup = {\n") 123 for _, task in ipairs(scheduled_tasks.every_startup) do 124 file:write(" " .. string.format("%q", task) .. ",\n") 125 end 126 file:write(" },\n") 127 128 -- Save cron tasks 129 file:write(" cron = {\n") 130 for name, task in pairs(scheduled_tasks.cron) do 131 file:write(" [" .. string.format("%q", name) .. "] = {\n") 132 file:write(" expression = " .. string.format("%q", task.expression) .. ",\n") 133 file:write(" app_path = " .. string.format("%q", task.app_path) .. ",\n") 134 file:write(" },\n") 135 end 136 file:write(" },\n") 137 138 file:write("}\n") 139 file:close() 140 141 return true 142 end 143 144 --- Load scheduled tasks from persistent storage 145 local function load_tasks() 146 local file, err = io.open(SCHEDULER_FILE, "r") 147 if not file then 148 -- File doesn't exist yet, that's okay 149 return true 150 end 151 152 local content = file:read("*all") 153 file:close() 154 155 if not content or content == "" then 156 return true 157 end 158 159 -- Load the tasks 160 local loaded_tasks, load_err = loadstring(content) 161 if not loaded_tasks then 162 if osprint then 163 osprint("Scheduler: Failed to load tasks: " .. tostring(load_err) .. "\n") 164 end 165 return false 166 end 167 168 local success, result = pcall(loaded_tasks) 169 if success and type(result) == "table" then 170 scheduled_tasks = result 171 if osprint then 172 osprint("Scheduler: Loaded scheduled tasks\n") 173 end 174 return true 175 else 176 if osprint then 177 osprint("Scheduler: Failed to parse tasks: " .. tostring(result) .. "\n") 178 end 179 return false 180 end 181 end 182 183 --- Initialize the scheduler 184 function scheduler.init() 185 if osprint then 186 osprint("Scheduler: Initializing...\n") 187 end 188 189 -- Load existing tasks 190 load_tasks() 191 192 -- Ensure tables exist 193 scheduled_tasks.next_startup = scheduled_tasks.next_startup or nil 194 scheduled_tasks.every_startup = scheduled_tasks.every_startup or {} 195 scheduled_tasks.cron = scheduled_tasks.cron or {} 196 197 if osprint then 198 osprint("Scheduler: Initialized\n") 199 end 200 201 return true 202 end 203 204 --- Run startup tasks 205 function scheduler.run_startup_tasks() 206 if osprint then 207 osprint("Scheduler: Running startup tasks...\n") 208 end 209 210 -- Run next_startup task if it exists 211 if scheduled_tasks.next_startup then 212 local app_path = scheduled_tasks.next_startup 213 if osprint then 214 osprint("Scheduler: Running next_startup task: " .. app_path .. "\n") 215 end 216 217 -- Run the app using run module 218 if run and run.execute then 219 local success, err = pcall(run.execute, app_path) 220 if not success then 221 if osprint then 222 osprint("Scheduler: Failed to run next_startup task: " .. tostring(err) .. "\n") 223 end 224 end 225 end 226 227 -- Clear next_startup after running 228 scheduled_tasks.next_startup = nil 229 save_tasks() 230 end 231 232 -- Run every_startup tasks 233 if #scheduled_tasks.every_startup > 0 then 234 for _, app_path in ipairs(scheduled_tasks.every_startup) do 235 if osprint then 236 osprint("Scheduler: Running every_startup task: " .. app_path .. "\n") 237 end 238 239 if run and run.execute then 240 local success, err = pcall(run.execute, app_path) 241 if not success then 242 if osprint then 243 osprint("Scheduler: Failed to run every_startup task: " .. tostring(err) .. "\n") 244 end 245 end 246 end 247 end 248 end 249 250 if osprint then 251 osprint("Scheduler: Startup tasks complete\n") 252 end 253 end 254 255 --- Check and run cron tasks (should be called periodically) 256 function scheduler.check_cron_tasks(current_time) 257 -- current_time should be a table: {year, month, day, hour, min, sec, wday} 258 if not current_time then 259 return 260 end 261 262 for name, task in pairs(scheduled_tasks.cron) do 263 local cron = parse_cron(task.expression) 264 if cron and cron_matches(cron, current_time) then 265 if osprint then 266 osprint("Scheduler: Running cron task '" .. name .. "': " .. task.app_path .. "\n") 267 end 268 269 if run and run.execute then 270 local success, err = pcall(run.execute, task.app_path) 271 if not success then 272 if osprint then 273 osprint("Scheduler: Failed to run cron task: " .. tostring(err) .. "\n") 274 end 275 end 276 end 277 end 278 end 279 end 280 281 --- Create os.schedule API for applications 282 --- app_context: the application context to add os.schedule to 283 --- app_permissions: the application's permissions table 284 function scheduler.create_api(app_context, app_permissions) 285 -- Check if app has scheduling permission 286 if not app_permissions or not app_permissions.scheduling then 287 -- No scheduling permission, return nil 288 return nil 289 end 290 291 local api = {} 292 293 --- Schedule app to run on next startup 294 --- app_path: path to the application to run (optional, defaults to current app) 295 function api.onNextStartUp(app_path) 296 -- If no app_path provided, use the current app's path 297 if not app_path then 298 app_path = app_context._app_path or error("No app path specified") 299 end 300 301 scheduled_tasks.next_startup = app_path 302 save_tasks() 303 304 if osprint then 305 osprint("Scheduler: Scheduled '" .. app_path .. "' for next startup\n") 306 end 307 308 return true 309 end 310 311 --- Schedule app to run on every startup 312 --- app_path: path to the application to run (optional, defaults to current app) 313 function api.onEveryStartUp(app_path) 314 -- If no app_path provided, use the current app's path 315 if not app_path then 316 app_path = app_context._app_path or error("No app path specified") 317 end 318 319 -- Check if already scheduled 320 for _, existing_path in ipairs(scheduled_tasks.every_startup) do 321 if existing_path == app_path then 322 -- Already scheduled 323 return true 324 end 325 end 326 327 table.insert(scheduled_tasks.every_startup, app_path) 328 save_tasks() 329 330 if osprint then 331 osprint("Scheduler: Scheduled '" .. app_path .. "' for every startup\n") 332 end 333 334 return true 335 end 336 337 --- Schedule app to run on a cron-like schedule 338 --- expression: cron expression string (8 fields) 339 --- name: optional name for this scheduled task (defaults to app path) 340 --- app_path: path to the application to run (optional, defaults to current app) 341 function api.onEvery(expression, name, app_path) 342 -- If no app_path provided, use the current app's path 343 if not app_path then 344 app_path = app_context._app_path or error("No app path specified") 345 end 346 347 -- If no name provided, use the app path 348 if not name or name == "" then 349 name = app_path 350 end 351 352 -- Validate cron expression 353 local cron = parse_cron(expression) 354 if not cron then 355 error("Invalid cron expression: " .. tostring(expression)) 356 end 357 358 -- Store the scheduled task 359 scheduled_tasks.cron[name] = { 360 expression = expression, 361 app_path = app_path 362 } 363 364 save_tasks() 365 366 if osprint then 367 osprint("Scheduler: Scheduled '" .. app_path .. "' with cron '" .. expression .. "' as '" .. name .. "'\n") 368 end 369 370 return true 371 end 372 373 --- Remove a scheduled task 374 --- name: either scheduler.NEXT_START_UP or the name of a cron task 375 function api.remove(name) 376 if name == scheduler.NEXT_START_UP then 377 -- Remove next_startup task 378 if scheduled_tasks.next_startup then 379 scheduled_tasks.next_startup = nil 380 save_tasks() 381 if osprint then 382 osprint("Scheduler: Removed next_startup task\n") 383 end 384 return true 385 else 386 return false 387 end 388 else 389 -- Remove cron task by name 390 if scheduled_tasks.cron[name] then 391 scheduled_tasks.cron[name] = nil 392 save_tasks() 393 if osprint then 394 osprint("Scheduler: Removed cron task '" .. name .. "'\n") 395 end 396 return true 397 else 398 -- Try to remove from every_startup 399 for i, app_path in ipairs(scheduled_tasks.every_startup) do 400 if app_path == name then 401 table.remove(scheduled_tasks.every_startup, i) 402 save_tasks() 403 if osprint then 404 osprint("Scheduler: Removed every_startup task '" .. name .. "'\n") 405 end 406 return true 407 end 408 end 409 return false 410 end 411 end 412 end 413 414 return api 415 end 416 417 return scheduler