mirror of https://github.com/hak5/overseer.git
add change detection to file fetcher, cleanup code, fix spellings, swap example code to use file fetcher
parent
ef5fb99e14
commit
2e4decb22e
|
@ -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...
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
Fetcher: &fetcher.File{Path: "my_app_next"},
|
||||
Debug: false, //display log of overseer actions
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
// 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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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/
|
||||
|
|
Loading…
Reference in New Issue