github.com/influx6/npkg@v0.8.8/ndaemon/daemon.go (about) 1 package ndaemon 2 3 import ( 4 "context" 5 "strings" 6 "sync" 7 "time" 8 9 "github.com/robfig/cron" 10 11 "github.com/influx6/npkg/nerror" 12 "github.com/takama/daemon" 13 14 "github.com/influx6/npkg/njson" 15 ) 16 17 var ( 18 defaultLocation = time.Now().Location() 19 defaultCronParserOption = cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.DowOptional | cron.Descriptor 20 ) 21 22 type Logger interface { 23 Log(json *njson.JSON) 24 } 25 26 type DaemonJob func(ctx context.Context, logger Logger) 27 28 type ctxJob struct { 29 ctx context.Context 30 logger Logger 31 job DaemonJob 32 } 33 34 func (c *ctxJob) Run() { 35 c.job(c.ctx, c.logger) 36 } 37 38 // CronDaemon returns a new cron based daemon which will execute the function 39 // based on a cron schedule. 40 // 41 // See for marker format: http://www.quartz-scheduler.org/documentation/quartz-2.x/tutorials/crontrigger.html 42 // CRON Expression Format 43 // A cron expression represents a set of times, using 5 space-separated fields. 44 // Field name | Mandatory? | Allowed values | Allowed special characters 45 // ---------- | ---------- | -------------- | -------------------------- 46 // Minutes | Yes | 0-59 | * / , - 47 // Hours | Yes | 0-23 | * / , - 48 // Day of month | Yes | 1-31 | * / , - ? 49 // Month | Yes | 1-12 or JAN-DEC | * / , - 50 // Day of week | Yes | 0-6 or SUN-SAT | * / , - ? 51 // Month and Day-of-week field values are case insensitive. "SUN", "Sun", and "sun" are equally accepted. 52 // The specific interpretation of the format is based on the Cron Wikipedia page: 53 // https://en.wikipedia.org/wiki/Cron 54 // 55 func CronDaemon( 56 ctx context.Context, 57 canceler context.CancelFunc, 58 cronExpression string, 59 location *time.Location, 60 overrideParser cron.ParseOption, 61 name string, 62 desc string, 63 logger Logger, 64 kind daemon.Kind, 65 templateOverride string, 66 depends []string, 67 do DaemonJob, 68 ) *ServiceDaemon { 69 var daemonService = ServiceDaemon{ 70 Cancel: canceler, 71 Ctx: ctx, 72 Name: name, 73 Desc: desc, 74 Logger: logger, 75 Kind: kind, 76 DependOnServices: depends, 77 DaemonTemplate: templateOverride, 78 } 79 80 daemonService.Job = func(ctx context.Context, logger Logger) { 81 var logStack = njson.Log(logger) 82 83 var waiter sync.WaitGroup 84 waiter.Add(1) 85 go func() { 86 <-ctx.Done() 87 waiter.Done() 88 }() 89 90 var parser = cron.NewParser(overrideParser) 91 var cronManager = cron.NewWithLocation(location) 92 93 var schedule, err = parser.Parse(cronExpression) 94 if err != nil { 95 var wrapErr = nerror.WrapOnly(err) 96 logStack.New(). 97 LError(). 98 Message("failed to parse cron expression"). 99 String("error", wrapErr.Error()). 100 End() 101 return 102 } 103 104 cronManager.Schedule(schedule, &ctxJob{ 105 ctx: ctx, 106 logger: logger, 107 job: do, 108 }) 109 110 cronManager.Start() 111 112 waiter.Wait() 113 } 114 115 return &daemonService 116 } 117 118 // Cron returns a cron ServiceDaemon which uses default location and 119 // default parser options (e.g "* * * * * *") to define the cron expression. 120 func Cron( 121 ctx context.Context, 122 canceler context.CancelFunc, 123 cronExpression string, 124 name string, 125 desc string, 126 logger Logger, 127 kind daemon.Kind, 128 do DaemonJob, 129 depends ...string, 130 ) *ServiceDaemon { 131 return CronDaemon( 132 ctx, 133 canceler, 134 cronExpression, 135 defaultLocation, 136 defaultCronParserOption, 137 name, 138 desc, 139 logger, 140 kind, 141 "", 142 depends, 143 do, 144 ) 145 } 146 147 type ServiceDaemon struct { 148 Name string 149 Desc string 150 Logger Logger 151 Kind daemon.Kind 152 DependOnServices []string 153 154 // Job function which will be lunched into a goroutine of it's own. 155 // If the job immediately returns then the context attached will be 156 // cancelled and the daemon killed. 157 Job DaemonJob 158 159 /*** 160 DaemonTemplate is a go template string to be used to create the service. 161 This is optional, but can be modified to suit usage. 162 163 [Unit] 164 Description={{.Description}} 165 Requires={{.Dependencies}} 166 After={{.Dependencies}} 167 168 [Service] 169 PIDFile=/var/run/{{.Name}}.pid 170 ExecStartPre=/bin/rm -f /var/run/{{.Name}}.pid 171 ExecStart={{.Path}} {{.Args}} 172 Restart=on-failure 173 174 [Install] 175 WantedBy=multi-user.target 176 */ 177 DaemonTemplate string 178 179 // Context to use for delivering to operation. 180 Ctx context.Context 181 182 // Cancel function to cancel context when application stops 183 // or is removed/uninstalled. 184 Cancel context.CancelFunc 185 } 186 187 func (c *ServiceDaemon) Run(args []string) (string, error) { 188 var logStack = njson.Log(c.Logger) 189 190 var usageHelp = "Usage: " + c.Name + " install | remove | start | stop | status" 191 192 var serviceDaemon, err = daemon.New(c.Name, c.Desc, c.Kind, c.DependOnServices...) 193 if err != nil { 194 return "", nerror.WrapOnly(err) 195 } 196 197 // start service details 198 if len(args) == 0 { 199 var waiter = WaiterForCtxSignal(c.Ctx, c.Cancel) 200 201 go func() { 202 defer c.Cancel() 203 204 c.Job(c.Ctx, c.Logger) 205 }() 206 207 waiter.Wait() 208 return "", nil 209 } 210 211 if len(c.DaemonTemplate) != 0 { 212 if err := serviceDaemon.SetTemplate(c.DaemonTemplate); err != nil { 213 var wrapErr = nerror.WrapOnly(err) 214 logStack.New(). 215 LError(). 216 Message("failed to set daemon template"). 217 String("error", wrapErr.Error()). 218 End() 219 return "", wrapErr 220 } 221 } 222 223 switch strings.ToLower(args[0]) { 224 case "install": 225 var message, opErr = serviceDaemon.Install(args[1:]...) 226 if opErr != nil { 227 var wrapErr = nerror.WrapOnly(opErr) 228 logStack.New(). 229 LError(). 230 Message("failed to complete install operation"). 231 String("message", message). 232 String("error", wrapErr.Error()). 233 End() 234 return "", wrapErr 235 } 236 logStack.New(). 237 LInfo(). 238 Message("installed daemon"). 239 String("message", message). 240 End() 241 return message, nil 242 case "remove": 243 var message, opErr = serviceDaemon.Remove() 244 if opErr != nil { 245 var wrapErr = nerror.WrapOnly(opErr) 246 logStack.New(). 247 LError(). 248 Message("failed to complete remove operation"). 249 String("message", message). 250 String("error", wrapErr.Error()). 251 End() 252 return "", wrapErr 253 } 254 logStack.New(). 255 LInfo(). 256 Message("removed daemon"). 257 String("message", message). 258 End() 259 return message, nil 260 case "start": 261 var message, opErr = serviceDaemon.Start() 262 if opErr != nil { 263 var wrapErr = nerror.WrapOnly(opErr) 264 logStack.New(). 265 LError(). 266 Message("failed to complete start operation"). 267 String("message", message). 268 String("error", wrapErr.Error()). 269 End() 270 return "", wrapErr 271 } 272 logStack.New(). 273 LInfo(). 274 Message("started daemon"). 275 String("message", message). 276 End() 277 return message, nil 278 case "stop": 279 var message, opErr = serviceDaemon.Stop() 280 if opErr != nil { 281 var wrapErr = nerror.WrapOnly(opErr) 282 logStack.New(). 283 LError(). 284 Message("failed to complete start operation"). 285 String("message", message). 286 String("error", wrapErr.Error()). 287 End() 288 return "", wrapErr 289 } 290 logStack.New(). 291 LInfo(). 292 Message("started daemon"). 293 String("message", message). 294 End() 295 return message, nil 296 case "status": 297 var message, opErr = serviceDaemon.Status() 298 if opErr != nil { 299 var wrapErr = nerror.WrapOnly(opErr) 300 logStack.New(). 301 LError(). 302 Message("failed to complete start operation"). 303 String("message", message). 304 String("error", wrapErr.Error()). 305 End() 306 return "", wrapErr 307 } 308 logStack.New(). 309 LInfo(). 310 Message("started daemon"). 311 String("message", message). 312 End() 313 return message, nil 314 } 315 return usageHelp, nil 316 }