agi.scheduler.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. package agi
  2. import (
  3. "fmt"
  4. "time"
  5. "github.com/robertkrimen/otto"
  6. "imuslab.com/arozos/mod/agi/static"
  7. "imuslab.com/arozos/mod/info/logger"
  8. )
  9. /*
  10. AGI Scheduler Library
  11. author: tobychui
  12. Exposes task scheduler functionality to AGI scripts via the "scheduler" library.
  13. Load it with: requirelib("scheduler");
  14. The cron script (cron.agi by default) must live inside the webapp's own folder,
  15. next to init.agi — NOT in user virtual storage:
  16. ./web/MyApp/
  17. init.agi
  18. cron.agi ← this is what gets scheduled
  19. index.html
  20. Usage pattern (typically placed in a backend AGI called on first launch):
  21. requirelib("scheduler");
  22. var APP = "MyApp"; // must match your module folder name
  23. var TASK = "MyApp_DailySync";
  24. if (!scheduler.hasPermission()) {
  25. // The frontend should call ao_module_requestSchedulerPermission()
  26. // to ask the user to grant this permission first.
  27. sendResp("no_permission");
  28. } else if (scheduler.registered(TASK, APP)) {
  29. sendResp("already_registered");
  30. } else {
  31. // scriptName defaults to "cron.agi" if omitted
  32. var ok = scheduler.register(TASK, APP, 86400, "Daily sync", "cron.agi");
  33. sendResp(ok ? "registered" : "error");
  34. }
  35. // To unregister (e.g. on user opt-out):
  36. scheduler.unregister(TASK);
  37. */
  38. // SchedulerCallbacks holds function pointers to the scheduler's core operations.
  39. // This avoids a circular import between the agi and scheduler packages.
  40. type SchedulerCallbacks struct {
  41. // RegisterJob registers a new job; returns error string or ""
  42. RegisterJob func(creator, appName, taskName, scriptVpath, fshID, description string, interval, baseTime int64) error
  43. // UnregisterJob removes a job by name for the given creator (or admin)
  44. UnregisterJob func(creator, taskName string) error
  45. // JobExists checks whether a job with the given appName+creator+taskName is registered
  46. AppJobExists func(appName, creator, taskName string) bool
  47. // CanCreate checks whether the given username has cron creation permission
  48. CanCreate func(username string) bool
  49. }
  50. // RegisterSchedulerLib registers the AGI "scheduler" library after the scheduler is running.
  51. // Must be called after the Scheduler is initialized, passing callback functions
  52. // so that the agi package does not import the scheduler package directly.
  53. func (g *Gateway) RegisterSchedulerLib(callbacks *SchedulerCallbacks) {
  54. err := g.RegisterLib("scheduler", func(payload *static.AgiLibInjectionPayload) {
  55. g.injectSchedulerLibFunctions(payload, callbacks)
  56. })
  57. if err != nil {
  58. // Library already registered – not fatal, just warn
  59. logger.PrintAndLog("Agi", fmt.Sprint("[AGI] scheduler lib already registered:", err), nil)
  60. }
  61. }
  62. func (g *Gateway) injectSchedulerLibFunctions(payload *static.AgiLibInjectionPayload, cb *SchedulerCallbacks) {
  63. vm := payload.VM
  64. u := payload.User
  65. /*
  66. scheduler.hasPermission() => bool
  67. Returns true when the current user is allowed to create cron jobs.
  68. */
  69. vm.Set("_scheduler_hasPermission", func(call otto.FunctionCall) otto.Value {
  70. if cb == nil || cb.CanCreate == nil {
  71. return otto.FalseValue()
  72. }
  73. canCreate := cb.CanCreate(u.Username)
  74. if canCreate {
  75. return otto.TrueValue()
  76. }
  77. return otto.FalseValue()
  78. })
  79. /*
  80. scheduler.registered(taskName) => bool
  81. Returns true when the task with the given name is already registered for this user+app.
  82. */
  83. vm.Set("_scheduler_registered", func(call otto.FunctionCall) otto.Value {
  84. taskName, err := call.Argument(0).ToString()
  85. if err != nil || taskName == "undefined" {
  86. g.RaiseError(err)
  87. return otto.FalseValue()
  88. }
  89. appName, err := call.Argument(1).ToString()
  90. if err != nil || appName == "undefined" {
  91. appName = ""
  92. }
  93. if cb == nil || cb.AppJobExists == nil {
  94. return otto.FalseValue()
  95. }
  96. exists := cb.AppJobExists(appName, u.Username, taskName)
  97. if exists {
  98. return otto.TrueValue()
  99. }
  100. return otto.FalseValue()
  101. })
  102. /*
  103. scheduler.register(taskName, appName, intervalSecs, description, scriptName) => bool
  104. Registers a cron job for the calling app's own cron script.
  105. - taskName: unique name for the job (max 32 chars)
  106. - appName: the webapp's module folder name (must match the folder in ./web/)
  107. - intervalSecs: execution interval in seconds
  108. - description: optional human-readable description
  109. - scriptName: script filename inside the app folder, default "cron.agi"
  110. Returns true on success, false on error.
  111. */
  112. vm.Set("_scheduler_register", func(call otto.FunctionCall) otto.Value {
  113. taskName, err := call.Argument(0).ToString()
  114. if err != nil || taskName == "undefined" {
  115. g.RaiseError(err)
  116. return otto.FalseValue()
  117. }
  118. appName, err := call.Argument(1).ToString()
  119. if err != nil || appName == "undefined" || appName == "" {
  120. g.RaiseError(err)
  121. return otto.FalseValue()
  122. }
  123. intervalSecs, err := call.Argument(2).ToInteger()
  124. if err != nil {
  125. g.RaiseError(err)
  126. return otto.FalseValue()
  127. }
  128. description, _ := call.Argument(3).ToString()
  129. if description == "undefined" {
  130. description = ""
  131. }
  132. scriptName, _ := call.Argument(4).ToString()
  133. if scriptName == "undefined" || scriptName == "" {
  134. scriptName = "cron.agi"
  135. }
  136. if cb == nil || cb.RegisterJob == nil {
  137. g.RaiseError(errExitcall)
  138. return otto.FalseValue()
  139. }
  140. baseTime := time.Now().Unix()
  141. // scriptName is app-relative ("cron.agi") — RegisterJobFromAGI detects this
  142. // because it has no ":" separator and appName is set.
  143. regErr := cb.RegisterJob(u.Username, appName, taskName, scriptName, "", description, intervalSecs, baseTime)
  144. if regErr != nil {
  145. g.RaiseError(regErr)
  146. return otto.FalseValue()
  147. }
  148. return otto.TrueValue()
  149. })
  150. /*
  151. scheduler.unregister(taskName, appName) => bool
  152. Removes a previously registered cron job.
  153. Returns true on success, false on error.
  154. */
  155. vm.Set("_scheduler_unregister", func(call otto.FunctionCall) otto.Value {
  156. taskName, err := call.Argument(0).ToString()
  157. if err != nil || taskName == "undefined" {
  158. g.RaiseError(err)
  159. return otto.FalseValue()
  160. }
  161. if cb == nil || cb.UnregisterJob == nil {
  162. g.RaiseError(errExitcall)
  163. return otto.FalseValue()
  164. }
  165. unregErr := cb.UnregisterJob(u.Username, taskName)
  166. if unregErr != nil {
  167. g.RaiseError(unregErr)
  168. return otto.FalseValue()
  169. }
  170. return otto.TrueValue()
  171. })
  172. // Expose as a "scheduler" object in the JS vm
  173. vm.Run(`
  174. var scheduler = {
  175. hasPermission: function() {
  176. return _scheduler_hasPermission();
  177. },
  178. registered: function(taskName, appName) {
  179. appName = appName || "";
  180. return _scheduler_registered(taskName, appName);
  181. },
  182. // register(taskName, appName, intervalSecs [, description [, scriptName]])
  183. // scriptName defaults to "cron.agi" inside the app's own folder.
  184. register: function(taskName, appName, intervalSecs, description, scriptName) {
  185. description = description || "";
  186. scriptName = scriptName || "cron.agi";
  187. return _scheduler_register(taskName, appName, intervalSecs, description, scriptName);
  188. },
  189. unregister: function(taskName) {
  190. return _scheduler_unregister(taskName);
  191. }
  192. };
  193. `)
  194. }