├── .github └── workflows │ └── build.yml ├── .project ├── .smalltalkci └── .unit-tests.ston ├── LICENSE ├── README.md └── src ├── .properties ├── BaselineOfScheduler ├── BaselineOfScheduler.class.st └── package.st ├── Scheduler-Core-Tests ├── SchedulerTest.class.st └── package.st ├── Scheduler-Core ├── ManifestSchedulerCore.class.st ├── MillisecondClockOffset.class.st ├── ScheduledTask.class.st ├── TaskScheduler.class.st └── package.st └── Scheduler-Magritte ├── ScheduledTask.extension.st ├── TaskScheduler.extension.st └── package.st /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push,pull_request,workflow_dispatch] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | smalltalk: [ Pharo64-7.0, Pharo64-8.0, Pharo64-9.0, Pharo64-10, Pharo64-11 ] 11 | name: ${{ matrix.smalltalk }} 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Smalltalk CI 15 | uses: hpi-swa/setup-smalltalkCI@v1 16 | with: 17 | smalltalk-image: ${{ matrix.smalltalk }} 18 | - name: Load Image and Run Tests 19 | run: smalltalkci -s ${{ matrix.smalltalk }} .smalltalkci/.unit-tests.ston 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | timeout-minutes: 70 23 | - name: Upload coverage to Codecov 24 | uses: codecov/codecov-action@v1 25 | with: 26 | name: ${{matrix.os}}-${{matrix.smalltalk}} 27 | token: ${{ secrets.CODECOV_TOKEN }} 28 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | { 2 | 'srcDirectory' : 'src' 3 | } -------------------------------------------------------------------------------- /.smalltalkci/.unit-tests.ston: -------------------------------------------------------------------------------- 1 | SmalltalkCISpec { 2 | #loading : [ 3 | SCIMetacelloLoadSpec { 4 | #baseline : 'Scheduler', 5 | #directory : '../src', 6 | #load : [ 'CI' ], 7 | #platforms : [ #pharo ] 8 | } 9 | ], 10 | #testing : { 11 | #coverage : { 12 | #packages : [ 'Scheduler*' ], 13 | #format: #lcov 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 pharo-contributions 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scheduler 2 | 3 | An easy-to-use **task scheduler** for [Pharo](https://www.pharo.org) that can run arbitrary blocks 4 | 5 | [![Unit Tests](https://github.com/pharo-contributions/Scheduler/workflows/Build/badge.svg?branch=master)](https://github.com/pharo-contributions/Scheduler/actions?query=workflow%3ABuild) 6 | [![Coverage Status](https://codecov.io/github/pharo-contributions/Scheduler/coverage.svg?branch=master)](https://codecov.io/gh/pharo-contributions/Scheduler/branch/master) 7 | 8 | [![Pharo 7](https://img.shields.io/badge/Pharo-7.0-%23aac9ff.svg)](https://pharo.org/download) 9 | [![Pharo 8](https://img.shields.io/badge/Pharo-8.0-%23aac9ff.svg)](https://pharo.org/download) 10 | [![Pharo 9](https://img.shields.io/badge/Pharo-9.0-%23aac9ff.svg)](https://pharo.org/download) 11 | [![Pharo 10](https://img.shields.io/badge/Pharo-10-%23aac9ff.svg)](https://pharo.org/download) 12 | [![Pharo 11](https://img.shields.io/badge/Pharo-11-%23aac9ff.svg)](https://pharo.org/download) 13 | 14 | 15 | ## Quick start 16 | 17 | ```Smalltalk 18 | Metacello new 19 | repository: 'github://pharo-contributions/Scheduler/src'; 20 | baseline: 'Scheduler'; 21 | load 22 | ``` 23 | 24 | [![Pharo Scheduler](https://img.youtube.com/vi/NOr32YgdujI/0.jpg)](https://www.youtube.com/watch?v=NOr32YgdujI) 25 | 26 | ## Overview 27 | 28 | An easy-to-use task scheduler that can run arbitrary blocks: 29 | 30 | Every so often (e.g. every hour starting now) 31 | 32 | - Daily at a given time 33 | - Periodically starting at a given time (e.g. every other hour starting a noon) 34 | - Per a provide schedule (e.g. using Schedule instance you can run tasks every Monday and Friday) 35 | - A one time task at some point in the future 36 | 37 | For ease of use tasks can be blocks passed to the scheduler (or any object that understands #value). 38 | 39 | ### Example 1 40 | 41 | ```Smalltalk 42 | "Start a new task scheduler and keep it around" 43 | scheduler := TaskScheduler new. 44 | 45 | scheduler start. 46 | ``` 47 | 48 | ### Example 2 49 | 50 | ```Smalltalk 51 | "Let's save the image every hour" 52 | scheduler 53 | do: [Smalltalk snapshot: true andQuit: false] 54 | every: 60 minutes. 55 | ``` 56 | 57 | ### Example 3 58 | 59 | ```Smalltalk 60 | "Let's run a backup at 2am every day" 61 | scheduler 62 | do: ["backup code here"] 63 | at: '2am' 64 | ``` 65 | 66 | ### Example 4 67 | 68 | ```Smalltalk 69 | "Let's perform a bank transfer every other hour starting at 1pm" 70 | scheduler 71 | do: ["swiss bank account transfer code"] 72 | at: '1pm' 73 | every: 2 hours. 74 | ``` 75 | 76 | ### Example 5 77 | 78 | ```Smalltalk 79 | "Let's do a one time email reminder" 80 | scheduler 81 | doOnce: ["email reminder to go on honeymoon"] 82 | at: '2005-1-15T8:00' 83 | ``` 84 | 85 | ### Example 6 86 | 87 | ```Smalltalk 88 | "Let's do a one time email reminder" 89 | "You can delete tasks by sending #delete to them" 90 | (scheduler taskAt: 1) delete 91 | ``` 92 | 93 | ### Example 7 94 | 95 | ```Smalltalk 96 | "Stop the scheduler from running -- but don't delete its tasks" 97 | scheduler stop. 98 | ``` 99 | Read the provided tests for more examples. 100 | -------------------------------------------------------------------------------- /src/.properties: -------------------------------------------------------------------------------- 1 | { 2 | #format : #tonel 3 | } -------------------------------------------------------------------------------- /src/BaselineOfScheduler/BaselineOfScheduler.class.st: -------------------------------------------------------------------------------- 1 | " 2 | Baseline for [https://github.com/pharo-contributions/Scheduler](https://github.com/pharo-contributions/Scheduler) 3 | " 4 | Class { 5 | #name : #BaselineOfScheduler, 6 | #superclass : #BaselineOf, 7 | #category : #BaselineOfScheduler 8 | } 9 | 10 | { #category : #baseline } 11 | BaselineOfScheduler >> baseline: spec [ 12 | 13 | spec 14 | for: #common do: [ 15 | "Packages" 16 | spec 17 | package: 'Scheduler-Core'; 18 | package: 'Scheduler-Core-Tests' with: [ spec requires: #('Scheduler-Core') ]; 19 | package: 'SchedulerMagritte' with: [ spec requires: #('Scheduler-Core') ]. 20 | 21 | "Groups" 22 | spec 23 | group: 'Core' with: #('Scheduler-Core'); 24 | group: 'Tests' with: #('Scheduler-Core-Tests'); 25 | group: 'CI' with: #('Tests'); 26 | group: 'all' with: #('Core' 'Tests'); 27 | group: 'default' with: #('all') ] 28 | ] 29 | -------------------------------------------------------------------------------- /src/BaselineOfScheduler/package.st: -------------------------------------------------------------------------------- 1 | Package { #name : #BaselineOfScheduler } 2 | -------------------------------------------------------------------------------- /src/Scheduler-Core-Tests/SchedulerTest.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #SchedulerTest, 3 | #superclass : #TestCase, 4 | #instVars : [ 5 | 'scheduler' 6 | ], 7 | #category : #'Scheduler-Core-Tests-Base' 8 | } 9 | 10 | { #category : #running } 11 | SchedulerTest >> setUp [ 12 | 13 | super setUp. 14 | scheduler := TaskScheduler new. 15 | scheduler start 16 | ] 17 | 18 | { #category : #running } 19 | SchedulerTest >> tearDown [ 20 | 21 | scheduler stop. 22 | super tearDown 23 | ] 24 | 25 | { #category : #tests } 26 | SchedulerTest >> testDailyTaskDoesntGetsAheadIfRunExplicitly [ 27 | 28 | | task | 29 | 30 | task := scheduler do: ['task ran'] at: DateAndTime now + 10 seconds. 31 | 32 | self assert: task nextRunTime asDate equals: Date today. 33 | 34 | task run. "run explicitly before scheduled time" 35 | 36 | self assert: task nextRunTime asDate equals: Date today 37 | ] 38 | 39 | { #category : #tests } 40 | SchedulerTest >> testNewTaskScheduledToRunEarlierInDayGetsRescheduledLaterInDay [ 41 | 42 | | message when task | 43 | 44 | when := (DateAndTime now - 1 minute) asTime. 45 | 46 | task := scheduler do: [message := 'task ran'] at: when. 47 | 48 | 1.1 seconds asDelay wait. 49 | 50 | self assert: message isNil. 51 | self assert: scheduler taskCount equals: 1. 52 | self assert: task nextRunTime equals: (DateAndTime date: (Date today addDays: 1) time: when) 53 | ] 54 | 55 | { #category : #tests } 56 | SchedulerTest >> testRunAtSomeTimeInTheFuture [ 57 | 58 | | task timeToRun | 59 | timeToRun := DateAndTime now + 2 second. 60 | task := scheduler do: [] at: timeToRun every: 1 hour. 61 | 62 | "set enough time that even on slow computers like CI the test can pass" 63 | 3 seconds asDelay wait. 64 | 65 | self assert: task nextRunTime equals: (timeToRun + 1 hour). 66 | self assert: task schedule schedule first equals: 1 hour. 67 | self assert: scheduler taskCount equals: 1 68 | 69 | 70 | 71 | ] 72 | 73 | { #category : #tests } 74 | SchedulerTest >> testRunDailyTask [ 75 | 76 | | message when task | 77 | 78 | when := (DateAndTime now + 1 second) asTime. 79 | 80 | task := scheduler do: [message := 'task ran'] at: when. 81 | 82 | 1.1 seconds asDelay wait. 83 | 84 | self assert: message equals: 'task ran'. 85 | self assert: scheduler taskCount equals: 1. 86 | self assert: task nextRunTime equals: (DateAndTime date: (Date today addDays: 1) time: when) 87 | ] 88 | 89 | { #category : #tests } 90 | SchedulerTest >> testRunDailyTaskNeverGetsBehind [ 91 | 92 | | task | 93 | 94 | scheduler stop. 95 | task := scheduler do: ['task ran'] at: DateAndTime now. 96 | 97 | task startDateAndTime: (task nextRunTime - 2 days) duration: 1 day. 98 | scheduler start. 99 | 1 second asDelay wait. 100 | 101 | self assert: task nextRunTime asDate equals: Date tomorrow. 102 | 103 | task startDateAndTime: ((DateAndTime now + 180 seconds) - 1 day) duration: 1 day. 104 | 1 second asDelay wait. 105 | 106 | self assert: task nextRunTime asDate equals: Date today 107 | ] 108 | 109 | { #category : #tests } 110 | SchedulerTest >> testRunEveryOtherHourOnTheHour [ 111 | 112 | | currentTime task | 113 | scheduler stop. 114 | currentTime := DateAndTime now asTime. 115 | 116 | task := scheduler do: [] at: currentTime hour asString every: 2 hours. 117 | task run. 118 | 119 | self assert: task nextRunTime asTime hour equals: (currentTime addSeconds: 2 * 60 * 60) hour. 120 | 121 | self assert: task nextRunTime > DateAndTime now 122 | ] 123 | 124 | { #category : #tests } 125 | SchedulerTest >> testRunEveryQuarterSecond [ 126 | 127 | | runCount | 128 | runCount := 0. 129 | 130 | scheduler do: [runCount := runCount + 1] every: 250 milliSeconds. 131 | 132 | 900 milliSeconds asDelay wait. 133 | 134 | self assert: runCount equals: 4. 135 | self assert: scheduler taskCount equals: 1 136 | ] 137 | 138 | { #category : #tests } 139 | SchedulerTest >> testRunEverySecond [ 140 | 141 | | runCount | 142 | runCount := 0. 143 | 144 | scheduler do: [runCount := runCount + 1] every: 1 second. 145 | 146 | 3.1 seconds asDelay wait. 147 | 148 | self assert: runCount equals: 4. 149 | self assert: scheduler taskCount equals: 1 150 | ] 151 | 152 | { #category : #tests } 153 | SchedulerTest >> testRunInASecond [ 154 | 155 | | ranWhen now | 156 | now := DateAndTime now. 157 | 158 | scheduler do: [ranWhen := DateAndTime now] at: (now + 1 seconds). 159 | 160 | 1.1 seconds asDelay wait. 161 | 162 | "Time's nanosecond precision is too fine to do an exact comparision" 163 | self assert: ranWhen > (now + 1 second). 164 | self assert: ranWhen < (now + 1.5 seconds) 165 | ] 166 | 167 | { #category : #tests } 168 | SchedulerTest >> testRunOnceInPastRunsOnce [ 169 | 170 | | message | 171 | scheduler doOnce: [message := 'task ran'] at: (DateAndTime now - 1 second). 172 | 173 | 1 second asDelay wait. 174 | 175 | self assert: message equals: 'task ran'. 176 | self assert: scheduler taskCount equals: 0 177 | ] 178 | 179 | { #category : #tests } 180 | SchedulerTest >> testRunOnlyOnceWithFullyQualifiedDateAndTime [ 181 | 182 | | runCount | 183 | runCount := 0. 184 | scheduler doOnce: [runCount := runCount + 1] at: (DateAndTime now + 1 seconds). 185 | 186 | 1.1 seconds asDelay wait. 187 | 188 | self assert: runCount equals: 1. 189 | self assert: scheduler taskCount equals: 0 190 | ] 191 | 192 | { #category : #tests } 193 | SchedulerTest >> testRunOnlyOnceWithOnlyTimeSpecified [ 194 | 195 | | runCount | 196 | runCount := 0. 197 | scheduler doOnce: [runCount := runCount + 1] at: ((DateAndTime now + 1 seconds) asTime). 198 | 199 | 1.1 seconds asDelay wait. 200 | 201 | self assert: runCount equals: 1. 202 | self assert: scheduler taskCount equals: 0 203 | ] 204 | 205 | { #category : #tests } 206 | SchedulerTest >> testRunSeveralTasks [ 207 | 208 | | task1 task2 now | 209 | now := DateAndTime now. 210 | 211 | scheduler do: [task1 := 'task 1'] at: (now + 1 second). 212 | scheduler do: [task2 := 'task 2'] at: (now + 1 second). 213 | 214 | 1.1 seconds asDelay wait. 215 | 216 | self assert: task1 equals: 'task 1'. 217 | self assert: task2 equals: 'task 2'. 218 | ] 219 | 220 | { #category : #tests } 221 | SchedulerTest >> testRunUsingCustomSchedule [ 222 | 223 | | runCount schedule | 224 | runCount := 0. 225 | 226 | schedule := Schedule starting: DateAndTime now duration: 1 minute. 227 | schedule schedule: {1 second. 2 seconds. 20 seconds}. 228 | 229 | scheduler do: [runCount := runCount + 1] following: schedule. 230 | "set enough time that even on slow computers like CI the test can pass" 231 | 4 seconds asDelay wait. 232 | 233 | self assert: runCount equals: 2. 234 | self assert: scheduler taskCount equals: 1 235 | 236 | ] 237 | 238 | { #category : #tests } 239 | SchedulerTest >> testRunUsingCustomScheduleThatEnds [ 240 | 241 | | runCount schedule | 242 | runCount := 0. 243 | 244 | schedule := Schedule starting: (DateAndTime now + 1 second) duration: 4 seconds. 245 | schedule schedule: {1 second. 2 seconds}. 246 | 247 | scheduler do: [runCount := runCount + 1] following: schedule. 248 | 5.1 seconds asDelay wait. 249 | 250 | self assert: runCount equals: 3. 251 | self assert: scheduler taskCount equals: 0 252 | 253 | ] 254 | -------------------------------------------------------------------------------- /src/Scheduler-Core-Tests/package.st: -------------------------------------------------------------------------------- 1 | Package { #name : #'Scheduler-Core-Tests' } 2 | -------------------------------------------------------------------------------- /src/Scheduler-Core/ManifestSchedulerCore.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I store metadata for this package. These meta data are used by other tools such as the SmalllintManifestChecker and the critics Browser 3 | " 4 | Class { 5 | #name : #ManifestSchedulerCore, 6 | #superclass : #PackageManifest, 7 | #category : #'Scheduler-Core-Manifest' 8 | } 9 | 10 | { #category : #'code-critics' } 11 | ManifestSchedulerCore class >> ruleBadMessageRule2V1FalsePositive [ 12 | ^ #(#(#(#RGMethodDefinition #(#TaskScheduler #start #false)) #'2020-11-16T01:31:29.237471+01:00') #(#(#RGMethodDefinition #(#ScheduledTask #startDateAndTime:duration: #false)) #'2020-11-16T01:32:22.69332+01:00') ) 13 | ] 14 | -------------------------------------------------------------------------------- /src/Scheduler-Core/MillisecondClockOffset.class.st: -------------------------------------------------------------------------------- 1 | " 2 | Utility class to handle clock offset in millisecond 3 | " 4 | Class { 5 | #name : #MillisecondClockOffset, 6 | #superclass : #Object, 7 | #classVars : [ 8 | 'MilliSecondOffset' 9 | ], 10 | #category : #'Scheduler-Core-Utilities' 11 | } 12 | 13 | { #category : #accessing } 14 | MillisecondClockOffset class >> getMilliSecondOffset [ 15 | 16 | | lastSecond | 17 | lastSecond := Time totalSeconds. 18 | [ lastSecond = Time totalSeconds ] whileTrue: [ 19 | (Delay forMilliseconds: 1) wait ]. 20 | 21 | ^ Time millisecondClockValue \\ 1000 22 | ] 23 | 24 | { #category : #'class initialization' } 25 | MillisecondClockOffset class >> initialize [ 26 | 27 | Smalltalk addToStartUpList: self 28 | ] 29 | 30 | { #category : #accessing } 31 | MillisecondClockOffset class >> milliSeconds [ 32 | 33 | ^ MilliSecondOffset 34 | ifNil: [ 0 ] 35 | ifNotNil: [ Time millisecondClockValue - MilliSecondOffset \\ 1000 ] 36 | ] 37 | 38 | { #category : #'system startup' } 39 | MillisecondClockOffset class >> startUp: resuming [ 40 | 41 | resuming ifFalse: [ ^ self ]. 42 | 43 | MilliSecondOffset := nil. 44 | [ MilliSecondOffset := self getMilliSecondOffset ] fork 45 | ] 46 | -------------------------------------------------------------------------------- /src/Scheduler-Core/ScheduledTask.class.st: -------------------------------------------------------------------------------- 1 | " 2 | A schedules task 3 | " 4 | Class { 5 | #name : #ScheduledTask, 6 | #superclass : #Object, 7 | #instVars : [ 8 | 'description', 9 | 'scheduler', 10 | 'task', 11 | 'runHistory', 12 | 'workerProcess', 13 | 'schedule', 14 | 'nextRunTime' 15 | ], 16 | #category : #'Scheduler-Core-Base' 17 | } 18 | 19 | { #category : #private } 20 | ScheduledTask >> convertToTimeParameter: parameter [ 21 | 22 | ^ [ parameter asDateAndTime ] 23 | on: Error 24 | do: [ Time readFrom: parameter readStream ] 25 | ] 26 | 27 | { #category : #executing } 28 | ScheduledTask >> delete [ 29 | 30 | scheduler removeTask: self 31 | ] 32 | 33 | { #category : #accessing } 34 | ScheduledTask >> description [ 35 | 36 | ^ description 37 | ] 38 | 39 | { #category : #accessing } 40 | ScheduledTask >> description: aString [ 41 | 42 | description := aString 43 | ] 44 | 45 | { #category : #initialization } 46 | ScheduledTask >> initialize [ 47 | 48 | runHistory := OrderedCollection new 49 | ] 50 | 51 | { #category : #testing } 52 | ScheduledTask >> isActive [ 53 | 54 | ^ scheduler isRunning 55 | ] 56 | 57 | { #category : #testing } 58 | ScheduledTask >> isRunning [ 59 | 60 | workerProcess ifNil: [ ^ false ]. 61 | 62 | ^ workerProcess isTerminated not 63 | ] 64 | 65 | { #category : #testing } 66 | ScheduledTask >> isSuspended [ 67 | 68 | workerProcess ifNil: [ ^ false ]. 69 | 70 | ^ workerProcess isTerminated not and: [ workerProcess isSuspended ] 71 | ] 72 | 73 | { #category : #accessing } 74 | ScheduledTask >> nextRunTime [ 75 | 76 | ^ nextRunTime 77 | ] 78 | 79 | { #category : #printing } 80 | ScheduledTask >> printOn: aStream [ 81 | 82 | aStream nextPutAll: 'Scheduled Task'; cr. 83 | self description ifNotNil: [aStream nextPutAll: self description; cr]. 84 | 85 | self isRunning ifTrue: [aStream nextPutAll: 'CURRENTLY RUNNING !!']. 86 | self isSuspended ifTrue: [aStream nextPutAll: ' (suspended)']. 87 | 88 | aStream 89 | cr; 90 | nextPutAll: 'Next Run Time : '; 91 | nextPutAll: self nextRunTime asString; 92 | cr; 93 | nextPutAll: 'Last Run Time : '; 94 | nextPutAll: (runHistory isEmpty 95 | ifTrue: [''] 96 | ifFalse: [runHistory last asString]) 97 | ] 98 | 99 | { #category : #executing } 100 | ScheduledTask >> resume [ 101 | 102 | self isSuspended ifTrue: [ workerProcess resume ] 103 | ] 104 | 105 | { #category : #executing } 106 | ScheduledTask >> run [ 107 | 108 | workerProcess := [[task value] ensure: [runHistory add: DateAndTime now]] fork. 109 | 110 | self wasRun 111 | ] 112 | 113 | { #category : #accessing } 114 | ScheduledTask >> runHistory [ 115 | 116 | ^ runHistory 117 | ] 118 | 119 | { #category : #private } 120 | ScheduledTask >> runIfNecessary [ 121 | "For TaskScheduler only. To run manually, send #run" 122 | 123 | (DateAndTime now >= self nextRunTime and: [ self isRunning not ]) 124 | ifTrue: [ self run ] 125 | ] 126 | 127 | { #category : #accessing } 128 | ScheduledTask >> schedule [ 129 | 130 | ^ schedule 131 | ] 132 | 133 | { #category : #accessing } 134 | ScheduledTask >> schedule: aSchedule [ 135 | 136 | schedule := aSchedule. 137 | nextRunTime := schedule start. 138 | 139 | self setNextRunTime 140 | 141 | ] 142 | 143 | { #category : #private } 144 | ScheduledTask >> scheduleHasOnlyZeroDuration [ 145 | 146 | ^ schedule schedule size = 1 and: [ 147 | schedule schedule first = Duration zero ] 148 | ] 149 | 150 | { #category : #accessing } 151 | ScheduledTask >> scheduler [ 152 | 153 | ^ scheduler 154 | ] 155 | 156 | { #category : #accessing } 157 | ScheduledTask >> scheduler: aTaskScheduler [ 158 | 159 | scheduler := aTaskScheduler 160 | ] 161 | 162 | { #category : #private } 163 | ScheduledTask >> setNextRunTime [ 164 | 165 | self scheduleHasOnlyZeroDuration ifTrue: [^ self]. 166 | 167 | schedule scheduleDo: 168 | [:each | (each > DateAndTime now) ifTrue: [nextRunTime := each. ^ self]]. 169 | 170 | self delete "delete if no more scheduled tasks" 171 | ] 172 | 173 | { #category : #accessing } 174 | ScheduledTask >> startDateAndTime: aDateAndTime duration: aDuration [ 175 | 176 | | timeParameter | 177 | timeParameter := self convertToTimeParameter: aDateAndTime. 178 | 179 | nextRunTime := ((timeParameter isKindOf: Time) 180 | ifTrue: 181 | [DateAndTime date: Date today time: timeParameter] 182 | ifFalse: 183 | [timeParameter]). 184 | 185 | schedule := Schedule starting: self nextRunTime duration: 1000 weeks. 186 | schedule schedule: {aDuration}. 187 | 188 | self setNextRunTime 189 | 190 | 191 | ] 192 | 193 | { #category : #accessing } 194 | ScheduledTask >> statusDescription [ 195 | 196 | self isActive ifFalse: [ ^ 'Scheduler stopped' ]. 197 | self isRunning ifTrue: [ ^ 'Running' ]. 198 | self isSuspended ifTrue: [ ^ 'Suspended' ]. 199 | ^ 'Waiting for next run' 200 | ] 201 | 202 | { #category : #executing } 203 | ScheduledTask >> stop [ 204 | 205 | self isRunning ifTrue: [ workerProcess terminate ] 206 | ] 207 | 208 | { #category : #executing } 209 | ScheduledTask >> suspend [ 210 | 211 | self isRunning ifTrue: [ workerProcess suspend ] 212 | ] 213 | 214 | { #category : #accessing } 215 | ScheduledTask >> task [ 216 | 217 | ^ task 218 | ] 219 | 220 | { #category : #accessing } 221 | ScheduledTask >> task: aBlock [ 222 | 223 | task := aBlock 224 | ] 225 | 226 | { #category : #private } 227 | ScheduledTask >> wasRun [ 228 | 229 | self scheduleHasOnlyZeroDuration ifTrue: [ self delete ]. 230 | 231 | self setNextRunTime 232 | ] 233 | -------------------------------------------------------------------------------- /src/Scheduler-Core/TaskScheduler.class.st: -------------------------------------------------------------------------------- 1 | " 2 | A task scheduler is scheduling tasks 3 | " 4 | Class { 5 | #name : #TaskScheduler, 6 | #superclass : #Object, 7 | #instVars : [ 8 | 'running', 9 | 'tasks', 10 | 'accessProtect' 11 | ], 12 | #category : #'Scheduler-Core-Base' 13 | } 14 | 15 | { #category : #scheduling } 16 | TaskScheduler >> addTask: aScheduledTask [ 17 | 18 | ^ accessProtect critical: [ tasks add: aScheduledTask ] 19 | ] 20 | 21 | { #category : #scheduling } 22 | TaskScheduler >> do: aBlock at: when [ 23 | 24 | ^ self do: aBlock at: when every: 1 day 25 | ] 26 | 27 | { #category : #scheduling } 28 | TaskScheduler >> do: aBlock at: when every: aDuration [ 29 | 30 | | task | 31 | task := ScheduledTask new 32 | scheduler: self; 33 | task: aBlock; 34 | startDateAndTime: when duration: aDuration; 35 | yourself. 36 | ^ self addTask: task 37 | ] 38 | 39 | { #category : #scheduling } 40 | TaskScheduler >> do: aBlock every: aDuration [ 41 | 42 | | task | 43 | task := self do: aBlock at: Time now every: aDuration. 44 | ^ task 45 | run; 46 | yourself 47 | ] 48 | 49 | { #category : #scheduling } 50 | TaskScheduler >> do: aBlock following: aSchedule [ 51 | 52 | | task | 53 | task := ScheduledTask new 54 | scheduler: self; 55 | task: aBlock; 56 | schedule: aSchedule; 57 | yourself. 58 | ^ self addTask: task 59 | ] 60 | 61 | { #category : #scheduling } 62 | TaskScheduler >> doOnce: aBlock at: when [ 63 | 64 | ^ self do: aBlock at: when every: Duration zero 65 | ] 66 | 67 | { #category : #initialization } 68 | TaskScheduler >> initialize [ 69 | 70 | running := false. 71 | tasks := OrderedCollection new. 72 | accessProtect := Semaphore forMutualExclusion 73 | ] 74 | 75 | { #category : #testing } 76 | TaskScheduler >> isRunning [ 77 | 78 | ^ running. 79 | ] 80 | 81 | { #category : #printing } 82 | TaskScheduler >> printOn: aStream [ 83 | 84 | aStream 85 | nextPutAll: 'Task scheduler is '; 86 | nextPutAll: (running 87 | ifTrue: [ 'running' ] 88 | ifFalse: [ 'stopped' ]); 89 | nextPutAll: ' with '; 90 | nextPutAll: self taskCount asWords; 91 | nextPutAll: ' tasks' 92 | ] 93 | 94 | { #category : #scheduling } 95 | TaskScheduler >> removeTask: aScheduledTask [ 96 | 97 | accessProtect critical: [ tasks remove: aScheduledTask ] 98 | ] 99 | 100 | { #category : #executing } 101 | TaskScheduler >> start [ 102 | 103 | | process | 104 | running ifTrue: [^ self]. 105 | 106 | running := true. 107 | 108 | process := [ 109 | [running] whileTrue: 110 | [0.25 seconds asDelay wait. 111 | self tasksDo: [:each | each runIfNecessary]]] newProcess. 112 | 113 | (process respondsTo: #name:) ifTrue: [ process name: 'taskScheduler' ]. 114 | 115 | process priority: Processor systemBackgroundPriority. 116 | 117 | process resume. 118 | 119 | ] 120 | 121 | { #category : #executing } 122 | TaskScheduler >> stop [ 123 | 124 | running := false 125 | ] 126 | 127 | { #category : #accessing } 128 | TaskScheduler >> taskAt: aNumber [ 129 | 130 | ^ accessProtect critical: [ tasks at: aNumber ] 131 | ] 132 | 133 | { #category : #accessing } 134 | TaskScheduler >> taskCount [ 135 | 136 | ^ accessProtect critical: [ tasks size ] 137 | ] 138 | 139 | { #category : #private } 140 | TaskScheduler >> tasks [ 141 | 142 | accessProtect critical: [ ^ tasks copy ] 143 | ] 144 | 145 | { #category : #accessing } 146 | TaskScheduler >> tasksDo: aOneArgBlock [ 147 | 148 | ^ self tasks do: aOneArgBlock 149 | ] 150 | -------------------------------------------------------------------------------- /src/Scheduler-Core/package.st: -------------------------------------------------------------------------------- 1 | Package { #name : #'Scheduler-Core' } 2 | -------------------------------------------------------------------------------- /src/Scheduler-Magritte/ScheduledTask.extension.st: -------------------------------------------------------------------------------- 1 | Extension { #name : #ScheduledTask } 2 | 3 | { #category : #'*Scheduler-Magritte-actions' } 4 | ScheduledTask >> descriptionDelete [ 5 | 6 | 7 | ^ MAActionDescription new 8 | selector: #delete; 9 | yourself 10 | ] 11 | 12 | { #category : #'*Scheduler-Magritte' } 13 | ScheduledTask >> descriptionDescription [ 14 | 15 | 16 | ^ MAStringDescription new 17 | accessor: #description; 18 | label: 'Description'; 19 | priority: 100; 20 | yourself. 21 | ] 22 | 23 | { #category : #'*Scheduler-Magritte' } 24 | ScheduledTask >> descriptionIsActive [ 25 | 26 | 27 | ^ MABooleanDescription new 28 | accessor: #isActive; 29 | label: 'Active'; 30 | priority: 200; 31 | beReadonly; 32 | yourself. 33 | ] 34 | 35 | { #category : #'*Scheduler-Magritte' } 36 | ScheduledTask >> descriptionNextRunTime [ 37 | 38 | 39 | "Not sure how this should work. For now, just show the printString" 40 | ^ MADateAndTimeDescription new 41 | accessor: #nextRunTime; 42 | label: 'Next Run Time'; 43 | priority: 600; 44 | beReadonly; 45 | display: [ :e | e asDate mmddyyyy, ' @ ', e asTime hhmm24 ]; 46 | yourself. 47 | ] 48 | 49 | { #category : #'*Scheduler-Magritte-actions' } 50 | ScheduledTask >> descriptionResume [ 51 | 52 | 53 | ^ MAActionDescription new 54 | selector: #resume; 55 | enableBlock: [ :e | e isSuspended ]; 56 | yourself 57 | ] 58 | 59 | { #category : #'*Scheduler-Magritte-actions' } 60 | ScheduledTask >> descriptionRun [ 61 | 62 | 63 | ^ MAActionDescription new 64 | selector: #run; 65 | enableBlock: [ :e | e isRunning not ]; 66 | yourself 67 | ] 68 | 69 | { #category : #'*Scheduler-Magritte' } 70 | ScheduledTask >> descriptionRunHistory [ 71 | 72 | 73 | ^ MAToManyRelationDescription new 74 | accessor: #runHistory; 75 | label: 'History'; 76 | priority: 400; 77 | display: [ :e | e asDate mmddyyyy, ' @ ', e asTime hhmm24 ]; 78 | beReadonly; 79 | yourself. 80 | ] 81 | 82 | { #category : #'*Scheduler-Magritte' } 83 | ScheduledTask >> descriptionSchedule [ 84 | 85 | 86 | "Not sure how this should work. For now, just show the printString" 87 | ^ MAStringDescription new 88 | accessor: #schedule; 89 | label: 'Schedule'; 90 | priority: 500; 91 | beReadonly; 92 | yourself. 93 | ] 94 | 95 | { #category : #'*Scheduler-Magritte' } 96 | ScheduledTask >> descriptionStatus [ 97 | 98 | 99 | ^ MAStringDescription new 100 | accessor: #statusDescription; 101 | label: 'Status'; 102 | priority: 200; 103 | beReadonly; 104 | yourself. 105 | ] 106 | 107 | { #category : #'*Scheduler-Magritte-actions' } 108 | ScheduledTask >> descriptionStop [ 109 | 110 | 111 | ^ MAActionDescription new 112 | selector: #stop; 113 | enableBlock: [ :e | e isRunning ]; 114 | yourself 115 | ] 116 | 117 | { #category : #'*Scheduler-Magritte-actions' } 118 | ScheduledTask >> descriptionSuspend [ 119 | 120 | 121 | ^ MAActionDescription new 122 | selector: #suspend; 123 | enableBlock: [ :e | e isRunning ]; 124 | yourself. 125 | ] 126 | 127 | { #category : #'*Scheduler-Magritte' } 128 | ScheduledTask >> descriptionTask [ 129 | 130 | 131 | ^ MAStringDescription new 132 | accessor: #task; 133 | label: 'Task'; 134 | priority: 200; 135 | beReadonly; 136 | display: [ :e | e asString allButFirst allButLast ]; 137 | yourself. 138 | ] 139 | -------------------------------------------------------------------------------- /src/Scheduler-Magritte/TaskScheduler.extension.st: -------------------------------------------------------------------------------- 1 | Extension { #name : #TaskScheduler } 2 | 3 | { #category : #'*Scheduler-Magritte' } 4 | TaskScheduler >> descriptionTasks [ 5 | 6 | 7 | ^ MAToManyRelationDescription new 8 | accessor: #tasks; 9 | label: 'Tasks'; 10 | priority: 400; 11 | beReadonly; 12 | yourself. 13 | ] 14 | -------------------------------------------------------------------------------- /src/Scheduler-Magritte/package.st: -------------------------------------------------------------------------------- 1 | Package { #name : #'Scheduler-Magritte' } 2 | --------------------------------------------------------------------------------