├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── bin └── go-cron.go ├── crane.yml ├── go-cron.go ├── httpserver.go └── test └── run /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | 25 | dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This code is based on the work of Michał Rączka and forked from the 2 | michaloo/go-cron Github project. All extensions were made under same 3 | MIT license. 4 | 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2014 Jan Nabbefeld 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TOKEN = `cat .token` 2 | REPO := go-cron 3 | USER := odise 4 | VERSION := "v0.0.4" 5 | 6 | build: 7 | mkdir -p out/darwin out/linux 8 | GOOS=darwin go build -o out/darwin/go-cron -ldflags "-X main.build `git rev-parse --short HEAD`" bin/go-cron.go 9 | GOOS=linux go build -o out/linux/go-cron -ldflags "-X main.build `git rev-parse --short HEAD`" bin/go-cron.go 10 | 11 | release: build 12 | rm -f out/darwin/go-cron-osx.gz 13 | gzip -c out/darwin/go-cron > out/darwin/go-cron-osx.gz 14 | rm -f out/linux/go-cron-linux.gz 15 | gzip -c out/linux/go-cron > out/linux/go-cron-linux.gz 16 | 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-cron 2 | ========= 3 | 4 | Simple golang wrapper over `github.com/robfig/cron` and `os/exec` as a cron replacement. 5 | Additionally the application opens a HTTP port that can be used as a healthcheck. 6 | 7 | ## usage 8 | 9 | `go-cron -s "* * * * * *" -p 8080 -- /bin/bash -c "echo 1"` 10 | 11 | Check the healthcheck: 12 | 13 | ``` 14 | $ curl -v localhost:18080 15 | * Rebuilt URL to: localhost:18080/ 16 | * Hostname was NOT found in DNS cache 17 | * Trying ::1... 18 | * Connected to localhost (::1) port 18080 (#0) 19 | > GET / HTTP/1.1 20 | > User-Agent: curl/7.37.1 21 | > Host: localhost:18080 22 | > Accept: */* 23 | > 24 | < HTTP/1.1 200 OK 25 | < Content-Type: application/json 26 | < Date: Wed, 11 Mar 2015 12:59:07 GMT 27 | < Content-Length: 237 28 | < 29 | { 30 | "Running": {}, 31 | "Last": { 32 | "Exit_status": 0, 33 | "Stdout": "1\n", 34 | "Stderr": "", 35 | "ExitTime": "2015-03-11T13:59:05+01:00", 36 | "Pid": 14420, 37 | "StartingTime": "2015-03-11T13:59:05+01:00" 38 | }, 39 | "Schedule": "*/5 * * * *" 40 | * Connection #0 to host localhost left intact 41 | } 42 | ``` 43 | -------------------------------------------------------------------------------- /bin/go-cron.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | gocron "github.com/odise/go-cron" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | ) 11 | 12 | var build string 13 | 14 | func main() { 15 | flagArgs, execArgs := splitArgs() 16 | os.Args = flagArgs 17 | 18 | var ( 19 | help = flag.Bool("h", false, "display usage") 20 | port = flag.String("p", "18080", "bind healthcheck to a specific port, set to 0 to not open HTTP port at all") 21 | schedule = flag.String("s", "* * * * *", "schedule the task the cron style") 22 | ) 23 | 24 | flag.Parse() 25 | 26 | if *help { 27 | println("Usage of", os.Args[0], "(build", build, ")") 28 | println(os.Args[0], " [ OPTIONS ] -- [ COMMAND ]") 29 | flag.PrintDefaults() 30 | os.Exit(1) 31 | } 32 | log.Println("Running version:", build) 33 | 34 | c, wg := gocron.Create(*schedule, execArgs[0], execArgs[1:len(execArgs)]) 35 | 36 | go gocron.Start(c) 37 | if *port != "0" { 38 | go gocron.Http_server(*port) 39 | } 40 | 41 | ch := make(chan os.Signal, 1) 42 | signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) 43 | println(<-ch) 44 | gocron.Stop(c, wg) 45 | } 46 | 47 | func splitArgs() (flagArgs []string, execArgs []string) { 48 | 49 | split := len(os.Args) 50 | 51 | for idx, e := range os.Args { 52 | 53 | if e == "--" { 54 | split = idx 55 | break 56 | } 57 | 58 | } 59 | 60 | flagArgs = os.Args[0:split] 61 | 62 | if split < len(os.Args) { 63 | execArgs = os.Args[split+1 : len(os.Args)] 64 | } else { 65 | execArgs = []string{} 66 | } 67 | 68 | return flagArgs, execArgs 69 | 70 | } 71 | -------------------------------------------------------------------------------- /crane.yml: -------------------------------------------------------------------------------- 1 | containers: 2 | go-cron: 3 | image: michaloo/golangdev 4 | run: 5 | volume: 6 | - ".:/go-cron" 7 | interactive: true 8 | tty: true 9 | workdir: "/go-cron" 10 | entrypoint: /bin/bash 11 | cmd: 12 | - -c 13 | - "bash" 14 | rm: true 15 | -------------------------------------------------------------------------------- /go-cron.go: -------------------------------------------------------------------------------- 1 | package gocron 2 | 3 | import ( 4 | "github.com/robfig/cron" 5 | "io" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "strconv" 10 | "strings" 11 | "sync" 12 | "syscall" 13 | "time" 14 | ) 15 | 16 | type LastRun struct { 17 | Exit_status int 18 | Stdout string 19 | Stderr string 20 | ExitTime string 21 | Pid int 22 | StartingTime string 23 | } 24 | 25 | type CurrentState struct { 26 | Running map[string]*LastRun 27 | Last *LastRun 28 | Schedule string 29 | } 30 | 31 | var Current_state CurrentState 32 | 33 | func copyOutput(out *string, src io.ReadCloser, pid int) { 34 | buf := make([]byte, 1024) 35 | for { 36 | n, err := src.Read(buf) 37 | if n != 0 { 38 | s := string(buf[:n]) 39 | *out = *out + s 40 | log.Printf("%d: %v", pid, s) 41 | } 42 | if err != nil { 43 | break 44 | } 45 | } 46 | } 47 | 48 | func execute(command string, args []string) { 49 | 50 | cmd := exec.Command(command, args...) 51 | 52 | run := new(LastRun) 53 | run.StartingTime = time.Now().Format(time.RFC3339) 54 | 55 | stdout, err := cmd.StdoutPipe() 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | stderr, err := cmd.StderrPipe() 60 | if err != nil { 61 | log.Fatal(err) 62 | } 63 | if err := cmd.Start(); err != nil { 64 | log.Fatalf("cmd.Start: %v", err) 65 | } 66 | 67 | run.Pid = cmd.Process.Pid 68 | Current_state.Running[strconv.Itoa(run.Pid)] = run 69 | 70 | go copyOutput(&run.Stdout, stdout, run.Pid) 71 | go copyOutput(&run.Stderr, stderr, run.Pid) 72 | 73 | log.Println(run.Pid, "cmd:", command, strings.Join(args, " ")) 74 | 75 | if err := cmd.Wait(); err != nil { 76 | if exiterr, ok := err.(*exec.ExitError); ok { 77 | // The program has exited with an exit code != 0 78 | // so set the error code to tremporary value 79 | run.Exit_status = 127 80 | if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { 81 | run.Exit_status = status.ExitStatus() 82 | log.Printf("%d Exit Status: %d", run.Pid, run.Exit_status) 83 | } 84 | } else { 85 | log.Fatalf("cmd.Wait: %v", err) 86 | } 87 | } 88 | 89 | run.ExitTime = time.Now().Format(time.RFC3339) 90 | 91 | delete(Current_state.Running, strconv.Itoa(run.Pid)) 92 | //run.Pid = 0 93 | Current_state.Last = run 94 | } 95 | 96 | func Create(schedule string, command string, args []string) (cr *cron.Cron, wgr *sync.WaitGroup) { 97 | 98 | wg := &sync.WaitGroup{} 99 | 100 | c := cron.New() 101 | Current_state = CurrentState{map[string]*LastRun{}, &LastRun{}, schedule} 102 | log.Println("new cron:", schedule) 103 | 104 | c.AddFunc(schedule, func() { 105 | wg.Add(1) 106 | execute(command, args) 107 | wg.Done() 108 | }) 109 | 110 | return c, wg 111 | } 112 | 113 | func Start(c *cron.Cron) { 114 | c.Start() 115 | } 116 | 117 | func Stop(c *cron.Cron, wg *sync.WaitGroup) { 118 | log.Println("Stopping") 119 | c.Stop() 120 | log.Println("Waiting") 121 | wg.Wait() 122 | log.Println("Exiting") 123 | os.Exit(0) 124 | } 125 | -------------------------------------------------------------------------------- /httpserver.go: -------------------------------------------------------------------------------- 1 | package gocron 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | func handler(w http.ResponseWriter, r *http.Request) { 10 | w.Header().Set("Content-Type", "application/json") 11 | 12 | js, err := json.MarshalIndent(Current_state, "", " ") 13 | if err != nil { 14 | http.Error(w, err.Error(), http.StatusInternalServerError) 15 | return 16 | } 17 | 18 | if Current_state.Last.Exit_status != 0 { 19 | w.WriteHeader(http.StatusServiceUnavailable) 20 | } 21 | 22 | w.Write(js) 23 | } 24 | 25 | func Http_server(port string) { 26 | log.Println("Opening port", port, "for health checking") 27 | http.HandleFunc("/", handler) 28 | err := http.ListenAndServe(":"+port, nil) 29 | if err != nil { 30 | log.Fatal("ListenAndServe: ", err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | go run ./go-cron.go "* * * * * *" /bin/bash -c "echo 1;" 4 | --------------------------------------------------------------------------------