-
Notifications
You must be signed in to change notification settings - Fork 0
/
awecron.go
293 lines (281 loc) · 9.51 KB
/
awecron.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
package main
import (
"bytes"
"context"
"fmt"
"log"
"os"
"os/exec"
"os/user"
"path"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/pelletier/go-toml/v2"
)
// global awecron config type
type cfgType struct {
Max int
Min int
Timeout int
}
// sets the logging format
func setLog() {
var logPrefix string
// get current user, or error
if curUser, err := user.Current(); err == nil {
logPrefix = fmt.Sprintf("awecron (%s) ", curUser.Username)
} else {
log.Printf("awecron [ERROR]: failed to get current user for logging")
logPrefix = "awecron "
}
// set the logging prefix
log.SetFlags(log.LstdFlags | log.Lmsgprefix)
log.SetPrefix(logPrefix)
}
// gets global configuration directory path
// HACK: this function may need improvements
func getCfgDir() string {
// config in $XDG_CONFIG_DIR/awecron or $HOME/.config/awecron
// get user config directory, check if file/directory exists, check if its a directory
if userCfgDir, err := os.UserConfigDir(); err == nil {
if cfgDirInfo, err := os.Stat(userCfgDir + "/awecron"); err == nil {
if cfgDirInfo.IsDir() {
// return if successful
return userCfgDir + "/awecron"
} else {
log.Fatalf("[FATAL]: global config directory %s is not a directory", userCfgDir+"/awecron")
}
}
}
// config in /etc/awecron
// check if awecron file/directory exists, check if its a directory
if cfgDirInfo, err := os.Stat("/etc/awecron"); err == nil {
if cfgDirInfo.IsDir() {
// return if successful
return "/etc/awecron"
} else {
log.Fatalf("[FATAL]: global config directory %s is not a directory", "/etc/awecron")
}
}
// could not find any matching directories
log.Fatalf("[FATAL]: global config directory does not exist")
return ""
}
// gets global awecron configuration
func getCfg(cfgDir *string, cfg *cfgType) {
cfgData, err := os.ReadFile(*cfgDir + "/cfg")
if err != nil {
log.Fatalf("[FATAL]: problem reading global config file cfgDir/cfg and saving as global config data cfgData")
}
err = toml.Unmarshal(cfgData, cfg)
if err != nil {
log.Fatalf("[FATAL]: problem unmarshalling global config data cfgData as struct cfg{}")
}
if cfg.Max <= 0 || cfg.Min <= 0 || cfg.Timeout <= 0 {
log.Fatalf("[FATAL]: global config values cfg{} should be greater than zero")
}
}
// gets cronjob directory paths
func getCjDirs(cfgDir *string) (cjDirs []string) {
cjTmrs, err := filepath.Glob(*cfgDir + "/*/tmr")
if err != nil {
log.Fatalf("[FATAL]: problem matching cfgDir/*/tmr and getting an array of cronjob timers cjTmrs")
}
// removing the /tmr end
for t := 0; t < len(cjTmrs); t++ {
cjDirs = append(cjDirs, strings.TrimSuffix(cjTmrs[t], "/tmr"))
}
return cjDirs
}
// check if its time to run the cronjob
func checkCj(cjDir *string) (bool, int) {
// getting last modification date of tmr file
cjTmrInfo, err := os.Stat(*cjDir + "/tmr")
if err != nil {
log.Printf("[ERROR] {%s}: problem getting last modification date of cjDir/tmr file as file info cjTmrInfo", path.Base(*cjDir))
// the 0 returned for cjSchedule is fixed later in main()
// this also applies to all returns in runCj and scheduleCj
return false, 0
}
cjSchedule := cjTmrInfo.ModTime().Unix()
// check if its time to run the cronjob
if cjSchedule < time.Now().Unix() {
return true, 0
} else {
return false, int(cjSchedule)
}
}
// run the cronjob
func runCj(cjDir *string, cjTimeout *int) bool {
// remove tmr file to disable cronjob in case of errors
err := os.Remove(*cjDir + "/tmr")
if err != nil {
// fatal error because if it fails to disable the cronjob due to a problem then there may be an infinite loop
log.Fatalf("[FATAL] {%s}: problem deleting cjDir/tmr file", path.Base(*cjDir))
}
// declaring context timeout
cjCtx, cjCtxCancel := context.WithTimeout(context.Background(), time.Duration(*cjTimeout)*time.Second)
defer cjCtxCancel()
// creating the cmd struct with context timeout
cjCmd := exec.CommandContext(cjCtx, *cjDir+"/run")
// modifying function which will be used to stop the cronjob if it times out
// so that it contains the log message that cronjob has timed out
cjCmd.Cancel = func() (err error) {
// stopping the cronjob
err = cjCmd.Process.Kill()
if err != nil {
// non fatal error because if cjCmd.Process.Kill() will fail to stop the process
// cjCmd.Run() will exit and forward this error, which will say that cronjob returned an error
// so it won't reenable the cronjob and there is no persistent problem
log.Printf("[ERROR] {%s}: failed to stop the timed out cronjob", path.Base(*cjDir))
return err
}
// log that the cronjob has timed out
log.Printf("[INFO] {%s}: cronjob run has timed out, stopping", path.Base(*cjDir))
return nil
}
// recording stderr
// I could've used cjCmd.CombinedOutput() but I am not interested in recording stdout
var cjStderr bytes.Buffer
cjCmd.Stderr = &cjStderr
// running the executable
err = cjCmd.Run()
// if successful run
if err == nil {
// log everything
log.Printf("[INFO] {%s} [%d]: cronjob run is successful", path.Base(*cjDir), cjCmd.ProcessState.ExitCode())
return true
} else {
// log exit status
log.Printf("[ERROR] {%s} [%d]: cronjob run returned an error", path.Base(*cjDir), cjCmd.ProcessState.ExitCode())
// log stderr if it is not empty
if cjStderr.String() != "" {
log.Printf("[INFO] {%s}: cronjob run stderr output:\n==========\n%s\n==========", path.Base(*cjDir), cjStderr.String())
}
return false
}
}
// schedule the next run of the cronjob
func scheduleCj(cjDir *string) int {
// getting the plain text interval configuration
// its also possible to do it with fmt.Fscanf, but I've chosen this option
cjCfgData, err := os.ReadFile(*cjDir + "/cfg")
if err != nil {
log.Printf("[ERROR] {%s}: problem reading cronjob config file cjDir/cfg and saving as cronjob config data cjCfgData", path.Base(*cjDir))
return 0
}
// conversion
cjCfg, err := strconv.Atoi(strings.TrimSpace(string(cjCfgData)))
if err != nil {
log.Printf("[ERROR] {%s}: problem converting cronjob config data cjCfgData into cronjob config integer cjCfg", path.Base(*cjDir))
return 0
}
// make sure its greater than zero
if cjCfg <= 0 {
log.Printf("[ERROR] {%s}: cronjob config cjCfg should be greater than zero", path.Base(*cjDir))
return 0
}
// create tmr file again
cjTmr, err := os.Create(*cjDir + "/tmr")
// all fatal errors because I am not risking with tmr file
// because it might result in an infinite loop for whatever reason
if err != nil {
log.Fatalf("[FATAL] {%s}: problem creating cjDir/tmr file", path.Base(*cjDir))
}
// closing cjTmr file
err = cjTmr.Close()
if err != nil {
log.Fatalf("[FATAL] {%s}: problem closing tmr file cjTmr", path.Base(*cjDir))
}
// get next run time
cjSchedule := time.Now().Unix() + int64(cjCfg)
// set the next run time as last modification time
err = os.Chtimes(*cjDir+"/tmr", time.Time{}, time.Unix(cjSchedule, int64(0)))
if err != nil {
log.Fatalf("[FATAL] {%s}: problem setting last modification time of tmr file", path.Base(*cjDir))
}
return int(cjSchedule)
}
// get optimal sleep time until next cronjob
func getSleepTime(cjSchedules *[]int, cfg *cfgType) (sleepTime int) {
// if there is no cronjobs sleep max time
if len(*cjSchedules) == 0 {
log.Printf("[INFO]: no enabled cronjobs found, sleeping max time")
return cfg.Max
}
// get the smallest unix time stamp from cronjob schedules
minCjSchedule := (*cjSchedules)[0]
for _, cjSchedule := range (*cjSchedules)[1:] {
if cjSchedule < minCjSchedule {
minCjSchedule = cjSchedule
}
}
// get the sleep time
sleepTime = minCjSchedule - int(time.Now().Unix())
// apply the limits
if sleepTime < cfg.Min {
sleepTime = cfg.Min
} else if sleepTime > cfg.Max {
sleepTime = cfg.Max
}
// return the optimal sleep time
return sleepTime
}
func main() {
// setting the logging format
setLog()
// getting the config directory
cfgDir := getCfgDir()
// global awecron config
var cfg cfgType
// getting global awecron configuration
getCfg(&cfgDir, &cfg)
// infinite loop
for {
// getting cronjob directories
cjDirs := getCjDirs(&cfgDir)
// array of unix time stamps until next cronjob run
var cjSchedules []int
// create mutex for managing above array inside of goroutines
var cjMutex sync.Mutex
// create wait group for goroutines
var cjWG sync.WaitGroup
// loop through cjDirs
for _, cjDir := range cjDirs {
// add one goroutine to wait group
cjWG.Add(1)
// initialize goroutine
go func() {
defer cjWG.Done()
// in awecron.sh I run a separate function for dynamic sleep feature to determine how much for awecron to sleep, which is pretty inefficient
// here instead I will make existing functions return necessary values
var cjSchedule int
var checkCjReturn bool
// check if its necessary to run the cronjob
if checkCjReturn, cjSchedule = checkCj(&cjDir); checkCjReturn {
// run the cronjob
if runCj(&cjDir, &cfg.Timeout) {
// schedule the cronjob for next run
cjSchedule = scheduleCj(&cjDir)
}
}
// if the function fails it has to return something as cjSchedule, so it returns 0
// so if its 0 it won't add it to the array of schedules at all
if cjSchedule != 0 {
// mutex lock while appending cjSchedule to an array, then unlock
cjMutex.Lock()
// append the next run time to the array of schedules
cjSchedules = append(cjSchedules, cjSchedule)
cjMutex.Unlock()
}
}()
}
// wait until all cronjobs finish
cjWG.Wait()
// get optimal sleep time and sleep for that number of seconds
time.Sleep(time.Duration(getSleepTime(&cjSchedules, &cfg)) * time.Second)
}
}