diff --git a/example/example.sh b/example/example.sh index 1e870c4..56de9da 100755 --- a/example/example.sh +++ b/example/example.sh @@ -3,17 +3,12 @@ #NOTE: DONT CTRL+C OR CLEANUP WONT OCCUR # ENSURE PORTS 5001,5002 ARE UNUSED -#http file server -go get github.com/jpillora/serve -serve --port 5002 --quiet . & -SERVEPID=$! - #initial build -go build -ldflags '-X main.BUILD_ID=1' -o myapp +go build -ldflags '-X main.BuildID=1' -o my_app echo "BUILT APP (1)" #run! echo "RUNNING APP" -./myapp & +./my_app & APPPID=$! sleep 1 @@ -24,7 +19,7 @@ sleep 1 #request during an update curl localhost:5001?d=5s & -go build -ldflags '-X main.BUILD_ID=2' -o myappnew +go build -ldflags '-X main.BuildID=2' -o my_app_next echo "BUILT APP (2)" sleep 2 @@ -35,7 +30,7 @@ sleep 1 #request during an update curl localhost:5001?d=5s & -go build -ldflags '-X main.BUILD_ID=3' -o myappnew +go build -ldflags '-X main.BuildID=3' -o my_app_next echo "BUILT APP (3)" sleep 2 @@ -48,28 +43,25 @@ curl localhost:5001 sleep 1 #end demo - cleanup -kill $SERVEPID kill $APPPID -rm myapp* 2> /dev/null +rm my_app* 2> /dev/null -# Expected output: -# serving . on port 4000 +# Expected output (hashes will vary across OS/arch/go-versions): # BUILT APP (1) # RUNNING APP -# app#1 (96015cccdebcec119adad34f49b93e02552f3ad9) listening... -# app#1 (96015cccdebcec119adad34f49b93e02552f3ad9) says hello -# app#1 (96015cccdebcec119adad34f49b93e02552f3ad9) says hello +# app#1 (9ba12be7d6f581835c6947845aa742cc05515365) listening... +# app#1 (9ba12be7d6f581835c6947845aa742cc05515365) says hello +# app#1 (9ba12be7d6f581835c6947845aa742cc05515365) says hello # BUILT APP (2) -# app#2 (ccc073a1c8e94fd4f2d76ebefb2bbc96790cb795) listening... -# app#2 (ccc073a1c8e94fd4f2d76ebefb2bbc96790cb795) says hello -# app#2 (ccc073a1c8e94fd4f2d76ebefb2bbc96790cb795) says hello -# app#1 (96015cccdebcec119adad34f49b93e02552f3ad9) says hello -# app#1 (96015cccdebcec119adad34f49b93e02552f3ad9) exiting... +# app#2 (180d6284b53f9618b92a2a4c0450521c93d767b7) listening... +# app#2 (180d6284b53f9618b92a2a4c0450521c93d767b7) says hello +# app#2 (180d6284b53f9618b92a2a4c0450521c93d767b7) says hello +# app#1 (9ba12be7d6f581835c6947845aa742cc05515365) says hello +# app#1 (9ba12be7d6f581835c6947845aa742cc05515365) exiting... # BUILT APP (3) -# app#3 (286848c2aefcd3f7321a65b5e4efae987fb17911) listening... -# app#3 (286848c2aefcd3f7321a65b5e4efae987fb17911) says hello -# app#3 (286848c2aefcd3f7321a65b5e4efae987fb17911) says hello -# app#2 (ccc073a1c8e94fd4f2d76ebefb2bbc96790cb795) says hello -# app#2 (ccc073a1c8e94fd4f2d76ebefb2bbc96790cb795) exiting... -# app#3 (286848c2aefcd3f7321a65b5e4efae987fb17911) says hello -# app#3 (286848c2aefcd3f7321a65b5e4efae987fb17911) exiting... +# app#3 (df4f68714724a856d24a08e44102fe41bbf9ee9f) listening... +# app#3 (df4f68714724a856d24a08e44102fe41bbf9ee9f) says hello +# app#3 (df4f68714724a856d24a08e44102fe41bbf9ee9f) says hello +# app#3 (df4f68714724a856d24a08e44102fe41bbf9ee9f) says hello +# app#2 (180d6284b53f9618b92a2a4c0450521c93d767b7) says hello +# app#2 (180d6284b53f9618b92a2a4c0450521c93d767b7) exiting... diff --git a/example/main.go b/example/main.go index 8a9c477..d64c19d 100644 --- a/example/main.go +++ b/example/main.go @@ -11,19 +11,20 @@ import ( //see example.sh for the use-case -var BUILD_ID = "0" +// BuildID is compile-time variable +var BuildID = "0" //convert your 'main()' into a 'prog(state)' //'prog()' is run in a child process func prog(state overseer.State) { - fmt.Printf("app#%s (%s) listening...\n", BUILD_ID, state.ID) + fmt.Printf("app#%s (%s) listening...\n", BuildID, state.ID) http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { d, _ := time.ParseDuration(r.URL.Query().Get("d")) time.Sleep(d) - fmt.Fprintf(w, "app#%s (%s) says hello\n", BUILD_ID, state.ID) + fmt.Fprintf(w, "app#%s (%s) says hello\n", BuildID, state.ID) })) http.Serve(state.Listener, nil) - fmt.Printf("app#%s (%s) exiting...\n", BUILD_ID, state.ID) + fmt.Printf("app#%s (%s) exiting...\n", BuildID, state.ID) } //then create another 'main' which runs the upgrades @@ -32,10 +33,7 @@ func main() { overseer.Run(overseer.Config{ Program: prog, Address: ":5001", - Fetcher: &fetcher.HTTP{ - URL: "http://localhost:5002/myappnew", - Interval: 1 * time.Second, - }, - Debug: false, //display log of overseer actions + Fetcher: &fetcher.File{Path: "my_app_next"}, + Debug: false, //display log of overseer actions }) } diff --git a/fetcher/fetcher.go b/fetcher/fetcher.go index fcd6311..d46cdaa 100644 --- a/fetcher/fetcher.go +++ b/fetcher/fetcher.go @@ -2,6 +2,7 @@ package fetcher import "io" +// Interface defines the required fetcher functions type Interface interface { //Init should perform validation on fields. For //example, ensure the appropriate URLs or keys @@ -12,12 +13,12 @@ type Interface interface { //binary to fetch, and then stream it back the //form of an io.Reader. If io.Reader is nil, //then it is assumed there are no updates. Fetch - //will be run repeatly and forever. It is up the + //will be run repeatedly and forever. It is up the //implementation to throttle the fetch frequency. Fetch() (io.Reader, error) } -//Converts a fetch function into interface +// Func converts a fetch function into the fetcher interface func Func(fn func() (io.Reader, error)) Interface { return &fetcher{fn} } diff --git a/fetcher/fetcher_file.go b/fetcher/fetcher_file.go index b4ca49e..40da20f 100644 --- a/fetcher/fetcher_file.go +++ b/fetcher/fetcher_file.go @@ -1,82 +1,100 @@ package fetcher import ( + "errors" "fmt" "io" "os" "time" ) -// File is used to check new version +// File checks the provided Path, at the provided +// Interval for new Go binaries. When a new binary +// is found it will replace the currently running +// binary. type File struct { Path string Interval time.Duration - // file modify time and its size makes up its hash - uniqHash string - delay bool + // hash is the file modify time and its size + hash string + delay bool } -// Init interval and lastHash +// Init sets the Path and Interval options func (f *File) Init() error { if f.Path == "" { return fmt.Errorf("Path required") } - if f.Interval == 0 { - f.Interval = 10 * time.Second + if f.Interval < 1*time.Second { + f.Interval = 1 * time.Second } - if err := f.updateHash(); err != nil { return err } - f.delay = false - return nil } -// Fetch file +// Fetch file from the specified Path func (f *File) Fetch() (io.Reader, error) { - //delay fetches after first + //only delay after first fetch if f.delay { time.Sleep(f.Interval) } f.delay = true - - lastHash := f.uniqHash + lastHash := f.hash if err := f.updateHash(); err != nil { return nil, err } // no change - if lastHash == f.uniqHash { + if lastHash == f.hash { return nil, nil } - + // changed! file, err := os.Open(f.Path) if err != nil { return nil, err } - + //check every 1/4s for 5s to + //ensure its not mid-copy + const rate = 250 * time.Millisecond + const total = int(5 * time.Second / rate) + attempt := 1 + for { + if attempt == total { + file.Close() + return nil, errors.New("file is currently being changed") + } + attempt++ + //sleep + time.Sleep(rate) + //check hash! + if err := f.updateHash(); err != nil { + file.Close() + return nil, err + } + //check until no longer changing + if lastHash == f.hash { + break + } + lastHash = f.hash + } return file, nil } func (f *File) updateHash() error { file, err := os.Open(f.Path) if err != nil { - // new version not exist, return + //binary does not exist, skip if os.IsNotExist(err) { return nil } return fmt.Errorf("Open file error: %s", err) } defer file.Close() - - state, err := file.Stat() + s, err := file.Stat() if err != nil { - return fmt.Errorf("Get file state error: %s", err) + return fmt.Errorf("Get file stat error: %s", err) } - - n := state.ModTime().UnixNano() - s := state.Size() - f.uniqHash = fmt.Sprintf("%d%d", n, s) - + f.hash = fmt.Sprintf("%d|%d", s.ModTime().UnixNano(), s.Size()) return nil } diff --git a/fetcher/fetcher_github.go b/fetcher/fetcher_github.go index a161e30..ceaa932 100644 --- a/fetcher/fetcher_github.go +++ b/fetcher/fetcher_github.go @@ -25,7 +25,7 @@ type Github struct { //By default a file will match if it contains //both GOOS and GOARCH. Asset func(filename string) bool - //interal state + //internal state releaseURL string delay bool lastETag string @@ -42,6 +42,7 @@ func (h *Github) defaultAsset(filename string) bool { return strings.Contains(filename, runtime.GOOS) && strings.Contains(filename, runtime.GOARCH) } +// Init validates the provided config func (h *Github) Init() error { //apply defaults if h.User == "" { @@ -62,6 +63,7 @@ func (h *Github) Init() error { return nil } +// Fetch the binary from the provided Repository func (h *Github) Fetch() (io.Reader, error) { //delay fetches after first if h.delay { @@ -105,7 +107,7 @@ func (h *Github) Fetch() (io.Reader, error) { return nil, fmt.Errorf("release location request failed (status code %d)", resp.StatusCode) } s3URL := resp.Header.Get("Location") - //psuedo-HEAD request + //pseudo-HEAD request req, err = http.NewRequest("GET", s3URL, nil) if err != nil { return nil, fmt.Errorf("release location url error (%s)", err) diff --git a/fetcher/fetcher_http.go b/fetcher/fetcher_http.go index 64ff400..e950839 100644 --- a/fetcher/fetcher_http.go +++ b/fetcher/fetcher_http.go @@ -9,7 +9,7 @@ import ( "time" ) -//HTTPFetcher uses HEAD requests to poll the status of a given +//HTTP fetcher uses HEAD requests to poll the status of a given //file. If it detects this file has been updated, it will fetch //and return its io.Reader stream. type HTTP struct { @@ -17,7 +17,7 @@ type HTTP struct { URL string Interval time.Duration CheckHeaders []string - //interal state + //internal state delay bool lasts map[string]string } @@ -25,6 +25,7 @@ type HTTP struct { //if any of these change, the binary has been updated var defaultHTTPCheckHeaders = []string{"ETag", "If-Modified-Since", "Last-Modified", "Content-Length"} +// Init validates the provided config func (h *HTTP) Init() error { //apply defaults if h.URL == "" { @@ -40,6 +41,7 @@ func (h *HTTP) Init() error { return nil } +// Fetch the binary from the provided URL func (h *HTTP) Fetch() (io.Reader, error) { //delay fetches after first if h.delay { diff --git a/fetcher/fetcher_s3.go b/fetcher/fetcher_s3.go index 9248f2e..409ff74 100644 --- a/fetcher/fetcher_s3.go +++ b/fetcher/fetcher_s3.go @@ -31,6 +31,7 @@ type S3 struct { lastETag string } +// Init validates the provided config func (s *S3) Init() error { if s.Bucket == "" { return errors.New("S3 bucket not set") @@ -58,6 +59,7 @@ func (s *S3) Init() error { return nil } +// Fetch the binary from S3 func (s *S3) Fetch() (io.Reader, error) { //delay fetches after first if s.delay { diff --git a/overseer.go b/overseer.go index f161ebb..8eb400c 100644 --- a/overseer.go +++ b/overseer.go @@ -1,4 +1,5 @@ -// Daemonizable self-upgrading binaries in Go (golang). +// Package overseer implements daemonizable +// self-upgrading binaries in Go (golang). package overseer import ( @@ -22,6 +23,7 @@ const ( envBinCheckLegacy = "GO_UPGRADE_BIN_CHECK" ) +// Config defines overseer's run-time configuration type Config struct { //Required will prevent overseer from fallback to running //running the program in the main process on failure. @@ -42,7 +44,7 @@ type Config struct { //This helps to prevent unwieldy fetch.Interfaces from hogging //too many resources. Defaults to 1 second. MinFetchInterval time.Duration - //PreUpgrade runs after a binary has been retreived, user defined checks + //PreUpgrade runs after a binary has been retrieved, user defined checks //can be run here and returning an error will cancel the upgrade. PreUpgrade func(tempBinaryPath string) error //Debug enables all [overseer] logs. @@ -91,7 +93,7 @@ func RunErr(c Config) error { } //Run executes overseer, if an error is -//encounted, overseer fallsback to running +//encountered, overseer fallsback to running //the program directly (unless Required is set). func Run(c Config) { err := runErr(&c) diff --git a/proc_master.go b/proc_master.go index 7e235ba..929b4a2 100644 --- a/proc_master.go +++ b/proc_master.go @@ -394,7 +394,7 @@ func (mp *master) fork() error { } mp.debugf("prog exited with %d", code) //if a restarts are disabled or if it was an - //unexpected creash, proxy this exit straight + //unexpected crash, proxy this exit straight //through to the main process if mp.NoRestart || !mp.restarting { os.Exit(code) diff --git a/proc_slave.go b/proc_slave.go index 05f1c4b..0116c66 100644 --- a/proc_slave.go +++ b/proc_slave.go @@ -18,6 +18,7 @@ var ( DisabledState = State{Enabled: false} ) +// State contains the current run-time state of overseer type State struct { //whether overseer is running enabled. When enabled, //this program will be running in a child process and diff --git a/sys_windows.go b/sys_windows.go index d5464bb..32b39a2 100644 --- a/sys_windows.go +++ b/sys_windows.go @@ -27,7 +27,7 @@ func move(dst, src string) error { return nil } //HACK: we're shelling out to move because windows - //throws errors when crossing device boundaryes. + //throws errors when crossing device boundaries. // https://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/move.mspx?mfr=true // https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/