MODULE TaskScheduler; (** AUTHOR "staubesv"; PURPOSE "Simple task scheduler"; *) IMPORT Streams, Modules, Kernel, Locks, Dates, Strings, Files, Commands; CONST (* task.repeatType *) Unknown* = -1; Once* = 0; (** default *) EverySecond* = 1; EveryMinute* = 2; Hourly* = 3; Daily* = 4; Weekly* = 5; Monthly* = 6; Yearly* = 7; NameLength* = 64; DescriptionLength* = 256; CommandLength* = 256; ImageNameLength* = 256; TYPE TaskInfo* = RECORD name* : ARRAY NameLength OF CHAR; description* : ARRAY DescriptionLength OF CHAR; command* : ARRAY CommandLength OF CHAR; image* : ARRAY ImageNameLength OF CHAR; repeatType* : LONGINT; trigger* : Dates.DateTime; END; TYPE Task* = OBJECT VAR id- : LONGINT; timestamp- : LONGINT; info : TaskInfo; user* : ANY; handled : BOOLEAN; week, weekDay : LONGINT; list- : TaskList; next : Task; PROCEDURE &Init*; BEGIN id := GetId(); info.name := ""; info.description := ""; info.command := ""; info.image := ""; info.repeatType := Unknown; timestamp := 0; user := NIL; handled := FALSE; list := NIL; next := NIL; END Init; PROCEDURE SetInfo*(CONST info : TaskInfo); BEGIN {EXCLUSIVE} SELF.info := info; INC(timestamp); END SetInfo; PROCEDURE GetInfo*() : TaskInfo; BEGIN {EXCLUSIVE} RETURN info; END GetInfo; PROCEDURE ToStream(out : Streams.Writer); VAR string : ARRAY 128 OF CHAR; BEGIN {EXCLUSIVE} ASSERT(out # NIL); Strings.FormatDateTime("dd.mm.yyyy hh:nn:ss", info.trigger, string); out.String(string); out.String(" "); TypeToStream(out, info.repeatType); out.String(' "'); out.String(info.name); out.String('" "'); out.String(info.description); out.String('" "'); out.String(info.command); out.String('" "'); out.String(info.image); out.String('"'); out.Ln; END ToStream; PROCEDURE FromStream(in : Streams.Reader) : BOOLEAN; VAR string : ARRAY 2048 OF CHAR; BEGIN {EXCLUSIVE} ASSERT(in # NIL); in.SkipWhitespace; in.String(string); Strings.StrToDate(string, info.trigger); in.SkipWhitespace; in.String(string); Strings.StrToTime(string, info.trigger); info.repeatType := TypeFromStream(in); in.SkipWhitespace; in.String(info.name); in.SkipWhitespace; in.String(info.description); in.SkipWhitespace; in.String(info.command); in.SkipWhitespace; in.String(info.image); SetTriggerX(info.trigger, info.repeatType); RETURN TRUE; END FromStream; PROCEDURE Confirm*; BEGIN IF (list # NIL) THEN list.ConfirmTask(SELF); END; END Confirm; (* Returns time left until next time triggered in seconds or 0 if not triggered anymore *) PROCEDURE Left*(VAR days, hours, minutes, seconds : LONGINT); VAR currentTime : Dates.DateTime; BEGIN currentTime := Dates.Now(); LeftFrom(currentTime, days, hours, minutes, seconds); END Left; PROCEDURE LeftFrom*(CONST dt : Dates.DateTime; VAR days, hours, minutes, seconds : LONGINT); BEGIN {EXCLUSIVE} IF (Dates.CompareDateTime(dt, info.trigger) = -1) THEN Dates.TimeDifference(dt, info.trigger, days, hours, minutes, seconds); ELSE days := 0; hours := 0; minutes := 0; seconds := 0; END; END LeftFrom; PROCEDURE SetTrigger*(dt : Dates.DateTime; type : LONGINT); BEGIN {EXCLUSIVE} SetTriggerX(dt, type); END SetTrigger; PROCEDURE SetTriggerX(dt : Dates.DateTime; repeatType : LONGINT); VAR currentTime : Dates.DateTime; ignoreYear : LONGINT; BEGIN ASSERT(Dates.ValidDateTime(dt)); INC(timestamp); info.repeatType := repeatType; info.trigger := dt; IF (repeatType # Once) THEN currentTime := Dates.Now(); IF (repeatType = EverySecond) THEN WHILE (Dates.CompareDateTime(info.trigger, currentTime) # 1) DO Dates.AddSeconds(info.trigger, 1); END; ELSIF (repeatType = EveryMinute) THEN WHILE (Dates.CompareDateTime(info.trigger, currentTime) # 1) DO Dates.AddMinutes(info.trigger, 1); END; ELSIF (repeatType = Hourly) THEN WHILE (Dates.CompareDateTime(info.trigger, currentTime) # 1) DO Dates.AddHours(info.trigger, 1); END; ELSIF (repeatType = Weekly) THEN WHILE (Dates.CompareDateTime(info.trigger, currentTime) # 1) DO Dates.AddDays(info.trigger, 7); END; ELSIF (repeatType = Monthly) THEN WHILE (Dates.CompareDateTime(info.trigger, currentTime) # 1) DO Dates.AddMonths(info.trigger, 1); END; ELSIF (repeatType = Yearly) THEN WHILE (Dates.CompareDateTime(info.trigger, currentTime) # 1) DO Dates.AddYears(info.trigger, 1); END; END; END; Dates.WeekDate(info.trigger, ignoreYear, week, weekDay); END SetTriggerX; PROCEDURE GetTrigger*() : Dates.DateTime; BEGIN {EXCLUSIVE} RETURN info.trigger; END GetTrigger; PROCEDURE TriggerNow*; VAR msg: ARRAY 256 OF CHAR; res:WORD; BEGIN IF Strings.Length(info.command)>0 THEN Commands.Call(info.command, {}, res, msg); END; END TriggerNow; PROCEDURE Check(time : Dates.DateTime; VAR left : LONGINT); BEGIN IF (left = 0) & ~handled THEN IF (info.repeatType = Once) THEN END; END; END Check; END Task; TaskArray* = POINTER TO ARRAY OF Task; TYPE SelectorProcedure* = PROCEDURE {DELEGATE} (time : Dates.DateTime; task : Task) : BOOLEAN; EnumeratorProcedure* = PROCEDURE {DELEGATE} (time : Dates.DateTime; task : Task); TaskList* = OBJECT VAR head : Task; nofTasks : LONGINT; lock : Locks.RWLock; PROCEDURE &Init*; BEGIN head := NIL; nofTasks := 0; NEW(lock); END Init; PROCEDURE Load*(CONST filename : ARRAY OF CHAR) : BOOLEAN; VAR file : Files.File; in : Files.Reader; task : Task; succeeded : BOOLEAN; BEGIN file := Files.Old(filename); IF (file # NIL) THEN succeeded := TRUE; Files.OpenReader(in, file, 0); lock.AcquireWrite; in.SkipWhitespace; WHILE succeeded & (in.Available() > 0) & (in.res = Streams.Ok) DO NEW(task); succeeded := task.FromStream(in); IF succeeded THEN Add(task); END; in.SkipWhitespace; END; succeeded := succeeded OR ~((in.res # Streams.Ok) & (in.res # Streams.EOF)); lock.ReleaseWrite; ELSE succeeded := FALSE; END; RETURN succeeded; END Load; PROCEDURE Store*(CONST filename : ARRAY OF CHAR) : BOOLEAN; VAR file : Files.File; out : Files.Writer; task : Task; BEGIN file := Files.New(filename); IF (file # NIL) THEN Files.OpenWriter(out, file, 0); lock.AcquireRead; task := head; WHILE (task # NIL) & ~task.handled DO task.ToStream(out); task := task.next; END; out.Update; Files.Register(file); lock.ReleaseRead; RETURN TRUE; ELSE RETURN FALSE; END; END Store; PROCEDURE Reset*; BEGIN lock.AcquireWrite; head := NIL; nofTasks := 0; lock.ReleaseWrite; END Reset; PROCEDURE ConfirmTask*(task : Task); BEGIN ASSERT(task # NIL); lock.AcquireWrite; Remove(task); (* TODO: Check race condition... *) IF (task.info.repeatType # Once) & (task.info.repeatType # Unknown) THEN task.SetTrigger(task.info.trigger, task.info.repeatType); Add(task); (* trigger will be adapted -> add again so that the list remains sorted *) END; lock.ReleaseWrite; END ConfirmTask; PROCEDURE FindById*(id : LONGINT) : Task; VAR task : Task; BEGIN lock.AcquireRead; task := head; WHILE (task # NIL) & (task.id # id) DO task := task.next; END; lock.ReleaseRead; RETURN task; END FindById; PROCEDURE Select*(selector : SelectorProcedure; CONST dt : Dates.DateTime; VAR tasks : TaskArray; VAR nofSelectedTasks, nofTasks : LONGINT); VAR task : Task; i : LONGINT; BEGIN Clear(tasks); lock.AcquireRead; (* first count number of selected tasks *) nofTasks := SELF.nofTasks; nofSelectedTasks := 0; task := head; WHILE (task # NIL) DO IF selector(dt, task) THEN INC(nofSelectedTasks); END; task := task.next; END; IF (nofSelectedTasks > 0) THEN IF (tasks = NIL) OR (nofSelectedTasks > LEN(tasks)) THEN NEW(tasks, nofSelectedTasks); END; i := 0; task := head; WHILE (task # NIL) DO IF selector(dt, task) THEN tasks[i] := task; INC(i); END; task := task.next; END; END; lock.ReleaseRead; END Select; PROCEDURE Enumerate*(time : Dates.DateTime; proc : EnumeratorProcedure); VAR task : Task; BEGIN ASSERT(proc # NIL); ASSERT(lock.HasReadLock()); task := head; WHILE (task # NIL) DO proc(time, task); task := task.next; END; END Enumerate; PROCEDURE Add*(task : Task); VAR t : Task; BEGIN ASSERT((task # NIL) & (task.list = NIL)); task.id := GetId(); lock.AcquireWrite; IF (head = NIL) OR (Dates.CompareDateTime(task.GetTrigger(), head.GetTrigger()) = -1) THEN task.next := head; head := task; ELSE t := head; WHILE (t # NIL) & (t.next # NIL) & (Dates.CompareDateTime(task.GetTrigger(), t.next.GetTrigger()) = 1) DO t := t.next; END; task.next := t.next; t.next := task; END; task.list := SELF; INC(nofTasks); lock.ReleaseWrite; END Add; PROCEDURE Remove*(task : Task); VAR t : Task; BEGIN ASSERT((task # NIL) & (task.list = SELF)); lock.AcquireWrite; IF (head = task) THEN head := head.next; DEC(nofTasks); ELSE t := head; WHILE (t # NIL) & (t.next # task) DO t := t.next; END; IF (t.next # NIL) THEN t.next := t.next.next; DEC(nofTasks); END; END; task.list := NIL; lock.ReleaseWrite; END Remove; (** Returns total number of tasks in this list *) PROCEDURE GetNofTasks*() : LONGINT; BEGIN RETURN nofTasks; END GetNofTasks; END TaskList; TYPE Scheduler = OBJECT VAR sleepHint : LONGINT; alive, dead : BOOLEAN; timer : Kernel.Timer; PROCEDURE &Init; BEGIN sleepHint := 1000; alive := TRUE; dead := FALSE; NEW(timer); END Init; PROCEDURE Stop; BEGIN alive := FALSE; timer.Wakeup; BEGIN {EXCLUSIVE} AWAIT(dead); END; END Stop; PROCEDURE Update; BEGIN sleepHint := 500; timer.Wakeup; END Update; PROCEDURE CheckTask(time : Dates.DateTime; task : Task); VAR hint : LONGINT; BEGIN task.Check(time, hint); IF (hint > 0) & (hint < sleepHint) THEN sleepHint := hint; END; END CheckTask; BEGIN {ACTIVE} WHILE alive DO sleepHint := MAX(LONGINT); taskList.lock.AcquireRead; taskList.Enumerate(Dates.Now(), CheckTask); taskList.lock.ReleaseRead; IF alive THEN timer.Sleep(sleepHint); END; END; BEGIN {EXCLUSIVE} dead := TRUE; END; END Scheduler; VAR taskList : TaskList; scheduler : Scheduler; id : LONGINT; StrNoName-, StrNoDescription-, StrNoCommand-, StrNoImage-: Strings.String; PROCEDURE TypeToStream(out : Streams.Writer; repeatType : LONGINT); BEGIN ASSERT(out # NIL); CASE repeatType OF |Unknown: out.String("Unknown"); |Once: out.String("Once"); |EverySecond: out.String("EverySecond"); |EveryMinute: out.String("EveryMinute"); |Hourly: out.String("Hourly"); |Daily: out.String("Daily"); |Weekly: out.String("Weekly"); |Monthly: out.String("Monthly"); |Yearly: out.String("Yearly"); ELSE out.String("Unknown"); END; END TypeToStream; PROCEDURE TypeFromStream(in : Streams.Reader) : LONGINT; VAR repeatType : LONGINT; string : ARRAY 32 OF CHAR; BEGIN ASSERT(in # NIL); repeatType := Unknown; in.SkipWhitespace; in.String(string); IF (string = "Once") THEN repeatType := Once; ELSIF (string = "EverySecond") THEN repeatType := EverySecond; ELSIF (string = "EveryMinute") THEN repeatType := EveryMinute; ELSIF (string = "Hourly") THEN repeatType := Hourly; ELSIF (string = "Daily") THEN repeatType := Daily; ELSIF (string = "Weekly") THEN repeatType := Weekly; ELSIF (string = "Monthly") THEN repeatType := Monthly; ELSIF (string = "Yearly") THEN repeatType := Yearly; END; RETURN repeatType; END TypeFromStream; PROCEDURE GetId() : LONGINT; BEGIN {EXCLUSIVE} INC(id); RETURN id; END GetId; PROCEDURE GetTaskList*() : TaskList; VAR taskList : TaskList; BEGIN NEW(taskList); RETURN taskList; END GetTaskList; (** Helper functions *) PROCEDURE GetRepeatTypeString*(repeatType : LONGINT; VAR string : ARRAY OF CHAR); BEGIN CASE repeatType OF |Unknown: string := "Unknown"; |Once: string := "Once"; |EverySecond: string := "Each Second"; |EveryMinute: string := "Each Minute"; |Hourly: string := "Hourly"; |Daily: string := "Daily"; |Weekly: string := "Weekly"; |Monthly: string := "Monthly"; |Yearly: string := "Yearly"; ELSE string := "Unknown"; END; END GetRepeatTypeString; (** TaskArray helper functions *) (** Returns TRUE if both task arrays contain exactly the same tasks in the same order, FALSE otherwise *) PROCEDURE IsEqual*(tasks1, tasks2 : TaskArray) : BOOLEAN; VAR i : LONGINT; PROCEDURE SameElement(t1, t2 : Task) : BOOLEAN; BEGIN RETURN ((t1 = NIL) & (t2 = NIL)) OR ((t1 # NIL) & (t2 # NIL) & (t1.id = t2.id)); END SameElement; BEGIN ASSERT((tasks1 # NIL) & (tasks2 # NIL)); IF (LEN(tasks1) = LEN(tasks2)) THEN i := 0; WHILE (i < LEN(tasks1)) DO IF ~SameElement(tasks1[i], tasks2[i]) THEN RETURN FALSE; END; INC(i); END; RETURN TRUE; END; RETURN FALSE; END IsEqual; PROCEDURE Copy*(from : TaskArray; VAR to : TaskArray); VAR i : LONGINT; BEGIN ASSERT(from # NIL); IF (to = NIL) OR (LEN(to) < LEN(from)) THEN NEW(to, LEN(from)); END; FOR i := 0 TO LEN(from)-1 DO to[i] := from[i]; END; END Copy; PROCEDURE Clear*(tasks : TaskArray); VAR i : LONGINT; BEGIN ASSERT(tasks # NIL); FOR i := 0 TO LEN(tasks)-1 DO tasks[i] := NIL; END; END Clear; PROCEDURE InitStrings; BEGIN StrNoName := Strings.NewString("NoName"); StrNoDescription := Strings.NewString("NoDescription"); StrNoCommand := Strings.NewString("NoCommand"); StrNoImage := Strings.NewString("NoImage"); END InitStrings; PROCEDURE Cleanup; BEGIN scheduler.Stop; END Cleanup; BEGIN InitStrings; NEW(taskList); NEW(scheduler); Modules.InstallTermHandler(Cleanup); END TaskScheduler. System.Free WMTaskScheduler TaskScheduler ~