luajitos

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

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