mirror of https://github.com/hak5/overseer.git
rewrite complete, mostly working now
parent
8f42cc8800
commit
f623267271
3
LICENSE
3
LICENSE
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Jaime Pillora
|
||||
Copyright (c) 2016 Jaime Pillora
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
|
137
README.md
137
README.md
|
@ -1,8 +1,10 @@
|
|||
# go-upgrade
|
||||
|
||||
Self-upgrading binaries in Go (Golang)
|
||||
[![GoDoc](https://godoc.org/github.com/jpillora/go-upgrade?status.svg)](https://godoc.org/github.com/jpillora/go-upgrade)
|
||||
|
||||
:warning: This is beta software
|
||||
Daemonizable self-upgrading binaries in Go (golang).
|
||||
|
||||
The main goal of this project is to facilitate the creation of self-upgrading binaries which play nice with standard process managers. The secondary goal is user simplicity. :warning: This is beta software.
|
||||
|
||||
### Install
|
||||
|
||||
|
@ -16,92 +18,97 @@ go get github.com/jpillora/go-upgrade
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/jpillora/go-upgrade"
|
||||
"github.com/jpillora/go-upgrade/fetcher"
|
||||
)
|
||||
|
||||
var VERSION = "0.0.0" //set with ldflags
|
||||
|
||||
//change your 'main' into a 'prog'
|
||||
func prog() {
|
||||
log.Printf("Running version %s...", VERSION)
|
||||
select {}
|
||||
//convert your 'main()' into a 'prog(state)'
|
||||
//'prog()' is run in a child process
|
||||
func prog(state upgrade.State) {
|
||||
log.Printf("app (%s) listening...", state.ID)
|
||||
http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "app (%s) says hello\n", state.ID)
|
||||
}))
|
||||
http.Serve(state.Listener, nil)
|
||||
}
|
||||
|
||||
//then create another 'main' which runs the upgrades
|
||||
//then create another 'main()' which runs the upgrades
|
||||
//'main()' is run in the initial process
|
||||
func main() {
|
||||
upgrade.Run(upgrade.Config{
|
||||
Program: prog,
|
||||
Version: VERSION,
|
||||
Fetcher: upgrade.BasicFetcher(
|
||||
"http://localhost:3000/myapp_{{.Version}}",
|
||||
),
|
||||
FetchInterval: 2 * time.Hour,
|
||||
Signal: os.Interrupt,
|
||||
Address: ":3000",
|
||||
Fetcher: &fetcher.HTTP{
|
||||
URL: "http://localhost:4000/binaries/myapp",
|
||||
Interval: 1 * time.Second,
|
||||
},
|
||||
// Log: false, //display log of go-upgrade actions
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### How it works
|
||||
```sh
|
||||
$ cd example/
|
||||
$ sh example.sh
|
||||
serving . on port 4000
|
||||
BUILT APP (1)
|
||||
RUNNING APP
|
||||
app#1 (96015cccdebcec119adad34f49b93e02552f3ad9) listening...
|
||||
app#1 (96015cccdebcec119adad34f49b93e02552f3ad9) says hello
|
||||
app#1 (96015cccdebcec119adad34f49b93e02552f3ad9) 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...
|
||||
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...
|
||||
```
|
||||
|
||||
* `go-upgrade` uses the main process to check for and install upgrades and a child process to run `Program`
|
||||
* If the current binary cannot be found or written to, `go-upgrade` will be disabled and `Program` will run in the main process
|
||||
* On load `Program` will be run in a child process
|
||||
* All standard pipes are connected to the child process
|
||||
* All signals received are forwarded through to the child process
|
||||
* Every `CheckInterval` the fetcher's `Fetch` method is called with the current version
|
||||
* The `BasicFetcher` requires a URL with a version placeholder. On `Fetch`, the current version will be incremented and result URL will be requested (raw bytes, `.tar.gz`, `.gz`, `.zip` binary releases are supported), if successful, the binary is returned.
|
||||
* When the binary is returned, its version is tested and checked against the current version, if it differs the upgrade is considered successful and the desired `Signal` will be sent to the child process.
|
||||
* When the child process exits, the main process will exit with the same code (except for upgrade restarts).
|
||||
* Upgrade restarts are performed once after an upgrade - any subsequence exits will also cause the main process to exit - **so `go-upgrade` is not a process manager**.
|
||||
### Documentation
|
||||
|
||||
* [Core `upgrade` package](https://godoc.org/github.com/jpillora/go-upgrade)
|
||||
* [Common `fetcher.Interface`](https://godoc.org/github.com/jpillora/go-upgrade/fetcher#Interface)
|
||||
* [Basic `fetcher.HTTP` fetcher type](https://godoc.org/github.com/jpillora/go-upgrade/fetcher#HTTP)
|
||||
|
||||
### Fetchers
|
||||
### Architecture overview
|
||||
|
||||
#### Basic
|
||||
|
||||
**Performs a simple web request at the desired URL**
|
||||
|
||||
When performing the version increment step, the current version will be parsed and each of the numerical sections will be grouped. One at a time, from right to left, each will be incremented and the new URL will requested. So, in the example above, `0.5.1` will be tried, then `0.6.0`, then `1.5.0`. Numerical semantic versions aren't required, you could also simply have `v1`, which then would be incremented to `v2`.
|
||||
|
||||
#### Github
|
||||
|
||||
**Uses Github releases to locate and download the newest version**
|
||||
|
||||
*TODO*
|
||||
*. `go-upgrade` uses the main process to check for and install upgrades and a child process to run `Program`
|
||||
*. All child process pipes are connected back to the main process
|
||||
*. All signals received on the main process are forwarded through to the child process
|
||||
*. The provided `fetcher.Interface` will be used to `Fetch()` the latest build of the binary
|
||||
*. The `fetcher.HTTP` accepts a `URL`, it polls this URL with HEAD requests and until it detects a change. On change, we `GET` the `URL` and stream it back out to `go-upgrade`.
|
||||
*. Once a binary is received, it is run with a simple echo token to confirm it is a `go-upgrade` binary.
|
||||
* Except for scheduled upgrades, the child process exiting will cause the main process to exit with the same code. So, **`go-upgrade` is not a process manager**.
|
||||
|
||||
### Alternatives
|
||||
|
||||
* https://github.com/sanbornm/go-selfupdate
|
||||
* https://github.com/inconshreveable/go-update
|
||||
|
||||
### Todo
|
||||
### TODO
|
||||
|
||||
* Delta updates with https://github.com/kr/binarydist
|
||||
* Signed binaries (in the meantime, use HTTPS where possible)
|
||||
|
||||
#### MIT License
|
||||
|
||||
Copyright © 2015 Jaime Pillora <dev@jpillora.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
* Github fetcher (given a repo)
|
||||
* S3 fetcher (given a bucket and credentials)
|
||||
* etcd fetcher (given a cluster, watch key)
|
||||
* `go-upgrade` CLI tool
|
||||
* Calculate delta updates with https://github.com/kr/binarydist ([courgette](http://dev.chromium.org/developers/design-documents/software-updates-courgette) would be nice)
|
||||
* Signed binaries and updates *(use HTTPS where in the meantime)*
|
||||
* Create signing ECDSA private and private key, store locally
|
||||
* Build binaries and include public key with `-ldflags "-X github.com/jpillora/go-upgrade/fetcher.PublicKey=A" -o myapp`
|
||||
* Only accept future updates with binaries signed by the matching private key
|
||||
* `upgrade` package
|
||||
* Execute and verify calculated delta updates with https://github.com/kr/binarydist
|
||||
* [Omaha](https://coreos.com/docs/coreupdate/custom-apps/coreupdate-protocol/) client support
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/jpillora/go-upgrade"
|
||||
"github.com/jpillora/go-upgrade/fetcher"
|
||||
"github.com/jpillora/opts"
|
||||
)
|
||||
|
||||
func main() {
|
||||
c := struct {
|
||||
URL string `type:"arg" help:"<url> of where to GET the binary"`
|
||||
Port int `help:"listening port"`
|
||||
Log bool `help:"enable logging"`
|
||||
}{
|
||||
Port: 3000,
|
||||
Log: true,
|
||||
}
|
||||
opts.Parse(&c)
|
||||
upgrade.Run(upgrade.Config{
|
||||
Log: c.Log,
|
||||
Program: func(state upgrade.State) {
|
||||
//noop
|
||||
select {}
|
||||
},
|
||||
Address: ":" + strconv.Itoa(c.Port),
|
||||
Fetcher: &fetcher.HTTP{
|
||||
URL: c.URL,
|
||||
Interval: 1 * time.Second,
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
placeholder for the goupgrade binary tool
|
|
@ -1,32 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
#NOTE: DONT CTRL+C OR CLEANUP WONT OCCUR
|
||||
|
||||
#binary hosting server (any file server)
|
||||
# go get github.com/jpillora/serve
|
||||
serve &
|
||||
|
||||
#initial build
|
||||
echo "BUILDING APP: A"
|
||||
go build -ldflags "-X main.FOO=A" -o myapp
|
||||
|
||||
#run!
|
||||
echo "RUNNING APP"
|
||||
./myapp &
|
||||
|
||||
sleep 3
|
||||
|
||||
echo "BUILDING APP: B"
|
||||
go build -ldflags "-X main.FOO=B" -o newmyapp
|
||||
|
||||
sleep 4
|
||||
|
||||
echo "BUILDING APP: C"
|
||||
go build -ldflags "-X main.FOO=C" -o newmyapp
|
||||
|
||||
sleep 4
|
||||
|
||||
#end demo - cleanup
|
||||
killall serve
|
||||
killall myapp
|
||||
rm myapp* 2> /dev/null
|
|
@ -1,39 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/jpillora/go-upgrade"
|
||||
"github.com/jpillora/go-upgrade/fetcher"
|
||||
)
|
||||
|
||||
var VAR = "" //set manually or with with ldflags
|
||||
|
||||
//convert your 'main()' into a 'prog(state)'
|
||||
func prog(state upgrade.State) {
|
||||
log.Printf("app (%s) listening...", state.ID)
|
||||
http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(10 * time.Second)
|
||||
fmt.Fprintf(w, "Var is: %s", VAR)
|
||||
}))
|
||||
err := http.Serve(state.Listener, nil)
|
||||
log.Printf("app (%s) exiting: %v", state.ID, err)
|
||||
}
|
||||
|
||||
//then create another 'main' which runs the upgrades
|
||||
func main() {
|
||||
upgrade.Run(upgrade.Config{
|
||||
Program: prog,
|
||||
Address: "0.0.0.0:3000",
|
||||
Fetcher: &fetcher.HTTP{
|
||||
URL: "http://localhost:4000/myapp2",
|
||||
Interval: 1 * time.Second,
|
||||
},
|
||||
Logging: true, //display log of go-upgrade actions
|
||||
})
|
||||
}
|
||||
|
||||
//then see example.sh for upgrade workflow
|
|
@ -0,0 +1,73 @@
|
|||
#!/bin/bash
|
||||
|
||||
#NOTE: DONT CTRL+C OR CLEANUP WONT OCCUR
|
||||
# ENSURE PORTS 3000,4000 ARE UNUSED
|
||||
|
||||
#http file server
|
||||
go get github.com/jpillora/serve
|
||||
serve --port 4000 --quiet . &
|
||||
SERVEPID=$!
|
||||
|
||||
#initial build
|
||||
go build -ldflags '-X main.BUILD_ID=1' -o myapp
|
||||
echo "BUILT APP (1)"
|
||||
#run!
|
||||
echo "RUNNING APP"
|
||||
./myapp &
|
||||
APPPID=$!
|
||||
|
||||
sleep 1
|
||||
curl localhost:3000
|
||||
sleep 1
|
||||
curl localhost:3000
|
||||
sleep 1
|
||||
#request during an update
|
||||
curl localhost:3000?d=5s &
|
||||
|
||||
go build -ldflags '-X main.BUILD_ID=2' -o myappnew
|
||||
echo "BUILT APP (2)"
|
||||
|
||||
sleep 2
|
||||
curl localhost:3000
|
||||
sleep 1
|
||||
curl localhost:3000
|
||||
sleep 1
|
||||
#request during an update
|
||||
curl localhost:3000?d=5s &
|
||||
|
||||
go build -ldflags '-X main.BUILD_ID=3' -o myappnew
|
||||
echo "BUILT APP (3)"
|
||||
|
||||
sleep 2
|
||||
curl localhost:3000
|
||||
sleep 1
|
||||
curl localhost:3000
|
||||
sleep 1
|
||||
curl localhost:3000
|
||||
|
||||
#end demo - cleanup
|
||||
kill $SERVEPID
|
||||
kill $APPPID
|
||||
rm myapp* 2> /dev/null
|
||||
|
||||
# Expected output:
|
||||
# serving . on port 4000
|
||||
# BUILT APP (1)
|
||||
# RUNNING APP
|
||||
# app#1 (96015cccdebcec119adad34f49b93e02552f3ad9) listening...
|
||||
# app#1 (96015cccdebcec119adad34f49b93e02552f3ad9) says hello
|
||||
# app#1 (96015cccdebcec119adad34f49b93e02552f3ad9) 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...
|
||||
# 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...
|
|
@ -0,0 +1,41 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/jpillora/go-upgrade"
|
||||
"github.com/jpillora/go-upgrade/fetcher"
|
||||
)
|
||||
|
||||
//see example.sh for the use-case
|
||||
|
||||
var BUILD_ID = "0"
|
||||
|
||||
//convert your 'main()' into a 'prog(state)'
|
||||
//'prog()' is run in a child process
|
||||
func prog(state upgrade.State) {
|
||||
fmt.Printf("app#%s (%s) listening...\n", BUILD_ID, 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)
|
||||
}))
|
||||
http.Serve(state.Listener, nil)
|
||||
fmt.Printf("app#%s (%s) exiting...\n", BUILD_ID, state.ID)
|
||||
}
|
||||
|
||||
//then create another 'main' which runs the upgrades
|
||||
//'main()' is run in the initial process
|
||||
func main() {
|
||||
upgrade.Run(upgrade.Config{
|
||||
Log: false, //display log of go-upgrade actions
|
||||
Program: prog,
|
||||
Address: ":3000",
|
||||
Fetcher: &fetcher.HTTP{
|
||||
URL: "http://localhost:4000/myappnew",
|
||||
Interval: 1 * time.Second,
|
||||
},
|
||||
})
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
#NOTE: DONT CTRL+C OR CLEANUP WONT OCCUR
|
||||
|
||||
#upgrade server (any http server)
|
||||
# go get github.com/jpillora/serve
|
||||
serve &
|
||||
|
||||
#initial build
|
||||
echo "BUILDING APP 0.3.0"
|
||||
go build -ldflags "-X main.VERSION 0.3.0" -o myapp
|
||||
|
||||
#run!
|
||||
echo "RUNNING APP"
|
||||
./myapp &
|
||||
|
||||
sleep 3
|
||||
|
||||
echo "BUILDING AND ARCHIVING APP 0.4.0"
|
||||
gox -osarch "darwin/amd64" -ldflags "-X main.VERSION 0.4.0" -output "myapp_{{.OS}}_{{.Arch}}_0.4.0"
|
||||
for f in myapp_*; do
|
||||
tar czvf $f.tar.gz l.txt $f n.txt
|
||||
rm $f
|
||||
done
|
||||
|
||||
sleep 10
|
||||
|
||||
#end demo - cleanup
|
||||
killall serve
|
||||
killall myapp
|
||||
rm myapp* 2> /dev/null
|
|
@ -1,34 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/jpillora/go-upgrade"
|
||||
)
|
||||
|
||||
var VERSION = "0.0.0" //set with ldflags
|
||||
|
||||
//change your 'main' into a 'prog'
|
||||
func prog() {
|
||||
log.Printf("Running version %s...", VERSION)
|
||||
select {}
|
||||
}
|
||||
|
||||
//then create another 'main' which runs the upgrades
|
||||
func main() {
|
||||
upgrade.Run(upgrade.Config{
|
||||
Program: prog,
|
||||
Version: VERSION,
|
||||
Fetcher: upgrade.BasicFetcher(
|
||||
"http://localhost:3000/myapp_{{.OS}}_{{.Arch}}_{{.Version}}.tar.gz",
|
||||
),
|
||||
FetchInterval: 2 * time.Second,
|
||||
Signal: os.Interrupt,
|
||||
//display logs of actions
|
||||
Logging: true,
|
||||
})
|
||||
}
|
||||
|
||||
//then see example.sh for upgrade workflow
|
|
@ -6,7 +6,9 @@ type Interface interface {
|
|||
//Fetch should check if there is an updated
|
||||
//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.
|
||||
//then it is assumed there are no updates. Fetch
|
||||
//will be run repeatly and forever. It is up the
|
||||
//implementation to throttle the fetch frequency.
|
||||
Fetch() (io.Reader, error)
|
||||
}
|
||||
|
||||
|
|
|
@ -12,21 +12,28 @@ import (
|
|||
//and stream out to the binary writer.
|
||||
type HTTP struct {
|
||||
//URL to poll for new binaries
|
||||
URL string
|
||||
Interval time.Duration
|
||||
URL string
|
||||
Interval time.Duration
|
||||
CheckHeaders []string
|
||||
//interal state
|
||||
delay bool
|
||||
lasts map[string]string
|
||||
}
|
||||
|
||||
//if any of these change, the binary has been updated
|
||||
var httpHeaders = []string{"ETag", "If-Modified-Since", "Last-Modified", "Content-Length"}
|
||||
var defaultHTTPCheckHeaders = []string{"ETag", "If-Modified-Since", "Last-Modified", "Content-Length"}
|
||||
|
||||
func (h *HTTP) Fetch() (io.Reader, error) {
|
||||
//apply defaults
|
||||
if h.URL == "" {
|
||||
return nil, fmt.Errorf("fetcher.HTTP requires a URL")
|
||||
}
|
||||
if h.Interval == 0 {
|
||||
h.Interval = 5 * time.Minute
|
||||
}
|
||||
if h.CheckHeaders == nil {
|
||||
h.CheckHeaders = defaultHTTPCheckHeaders
|
||||
}
|
||||
if h.lasts == nil {
|
||||
h.lasts = map[string]string{}
|
||||
}
|
||||
|
@ -46,7 +53,7 @@ func (h *HTTP) Fetch() (io.Reader, error) {
|
|||
}
|
||||
//if all headers match, skip update
|
||||
matches, total := 0, 0
|
||||
for _, header := range httpHeaders {
|
||||
for _, header := range h.CheckHeaders {
|
||||
if curr := resp.Header.Get(header); curr != "" {
|
||||
if last, ok := h.lasts[header]; ok && last == curr {
|
||||
matches++
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// Daemonizable self-upgrading binaries in Go (golang).
|
||||
package upgrade
|
||||
|
||||
import (
|
||||
|
@ -19,7 +20,7 @@ const (
|
|||
|
||||
type Config struct {
|
||||
//Optional allows go-upgrade to fallback to running
|
||||
//running the program with
|
||||
//running the program in the main process.
|
||||
Optional bool
|
||||
//Program's main function
|
||||
Program func(state State)
|
||||
|
@ -34,10 +35,17 @@ type Config struct {
|
|||
//wait for the program to terminate itself. After this
|
||||
//timeout, go-upgrade will issue a SIGKILL.
|
||||
TerminateTimeout time.Duration
|
||||
//Restarts will be throttled by this duration.
|
||||
ThrottleRestarts time.Duration
|
||||
//Logging enables [go-upgrade] logs to be sent to stdout.
|
||||
Logging bool
|
||||
//MinFetchInterval defines the smallest duration between Fetch()s.
|
||||
//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
|
||||
//can be run here and returning an error will cancel the upgrade.
|
||||
PreUpgrade func(tempBinaryPath string) error
|
||||
//NoRestart disables automatic restarts after each upgrade.
|
||||
NoRestart bool
|
||||
//Log enables [go-upgrade] logs to be sent to stdout.
|
||||
Log bool
|
||||
//Fetcher will be used to fetch binaries.
|
||||
Fetcher fetcher.Interface
|
||||
}
|
||||
|
@ -68,6 +76,9 @@ func Run(c Config) {
|
|||
if c.TerminateTimeout == 0 {
|
||||
c.TerminateTimeout = 30 * time.Second
|
||||
}
|
||||
if c.MinFetchInterval == 0 {
|
||||
c.MinFetchInterval = 1 * time.Second
|
||||
}
|
||||
if c.Fetcher == nil {
|
||||
fatalf("upgrade.Config.Fetcher required")
|
||||
}
|
|
@ -47,6 +47,7 @@ func (l *upListener) Accept() (net.Conn, error) {
|
|||
return uconn, nil
|
||||
}
|
||||
|
||||
//non-blocking trigger close
|
||||
func (l *upListener) release(timeout time.Duration) {
|
||||
//stop accepting connections - release fd
|
||||
l.closeError = l.Listener.Close()
|
||||
|
@ -66,6 +67,7 @@ func (l *upListener) release(timeout time.Duration) {
|
|||
}()
|
||||
}
|
||||
|
||||
//blocking wait for close
|
||||
func (l *upListener) Close() error {
|
||||
l.wg.Wait()
|
||||
return l.closeError
|
||||
|
@ -78,6 +80,7 @@ func (l *upListener) File() *os.File {
|
|||
return fl
|
||||
}
|
||||
|
||||
//notifying on close net.Conn
|
||||
type upConn struct {
|
||||
net.Conn
|
||||
wg *sync.WaitGroup
|
||||
|
|
123
proc_master.go
123
proc_master.go
|
@ -14,6 +14,7 @@ import (
|
|||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
|
@ -30,9 +31,11 @@ type master struct {
|
|||
binPath string
|
||||
binPerms os.FileMode
|
||||
binHash []byte
|
||||
restartMux sync.Mutex
|
||||
restarting bool
|
||||
restartedAt time.Time
|
||||
restarted chan bool
|
||||
awaitingUSR1 bool
|
||||
descriptorsReleased chan bool
|
||||
signalledAt time.Time
|
||||
signals chan os.Signal
|
||||
|
@ -105,35 +108,40 @@ func (mp *master) setupSignalling() {
|
|||
signal.Notify(mp.signals)
|
||||
go func() {
|
||||
for s := range mp.signals {
|
||||
|
||||
if s.String() == "child exited" {
|
||||
continue
|
||||
}
|
||||
|
||||
//**during a restart** a SIGUSR1 signals
|
||||
//to the master process that, the file
|
||||
//descriptors have been released
|
||||
if mp.restarting && s == syscall.SIGUSR1 {
|
||||
mp.descriptorsReleased <- true
|
||||
continue
|
||||
}
|
||||
|
||||
if mp.slaveCmd != nil && mp.slaveCmd.Process != nil {
|
||||
mp.logf("proxy signal (%s)", s)
|
||||
if err := mp.slaveCmd.Process.Signal(s); err != nil {
|
||||
mp.logf("proxy signal failed (%s)", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else if s == syscall.SIGINT {
|
||||
mp.logf("interupt with no slave")
|
||||
os.Exit(1)
|
||||
} else {
|
||||
mp.logf("signal discarded (%s), no slave process", s)
|
||||
}
|
||||
mp.handleSignal(s)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (mp *master) handleSignal(s os.Signal) {
|
||||
if s.String() == "child exited" {
|
||||
// will occur on every restart
|
||||
} else
|
||||
//**during a restart** a SIGUSR1 signals
|
||||
//to the master process that, the file
|
||||
//descriptors have been released
|
||||
if mp.awaitingUSR1 && s == syscall.SIGUSR1 {
|
||||
mp.awaitingUSR1 = false
|
||||
mp.descriptorsReleased <- true
|
||||
} else
|
||||
//while the slave process is running, proxy
|
||||
//all signals through
|
||||
if mp.slaveCmd != nil && mp.slaveCmd.Process != nil {
|
||||
mp.logf("proxy signal (%s)", s)
|
||||
if err := mp.slaveCmd.Process.Signal(s); err != nil {
|
||||
mp.logf("proxy signal failed (%s)", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else
|
||||
//otherwise if not running, kill on CTRL+c
|
||||
if s == syscall.SIGINT {
|
||||
mp.logf("interupt with no slave")
|
||||
os.Exit(1)
|
||||
} else {
|
||||
mp.logf("signal discarded (%s), no slave process", s)
|
||||
}
|
||||
}
|
||||
|
||||
func (mp *master) retreiveFileDescriptors() {
|
||||
mp.slaveExtraFiles = make([]*os.File, len(mp.Config.Addresses))
|
||||
for i, addr := range mp.Config.Addresses {
|
||||
|
@ -156,16 +164,17 @@ func (mp *master) retreiveFileDescriptors() {
|
|||
}
|
||||
}
|
||||
|
||||
//fetchLoop is run in a goroutine
|
||||
func (mp *master) fetchLoop() {
|
||||
minDelay := time.Second
|
||||
time.Sleep(minDelay)
|
||||
min := mp.Config.MinFetchInterval
|
||||
time.Sleep(min)
|
||||
for {
|
||||
t0 := time.Now()
|
||||
mp.fetch()
|
||||
diff := time.Now().Sub(t0)
|
||||
if diff < minDelay {
|
||||
delay := minDelay - diff
|
||||
//ensures at least minDelay
|
||||
if diff < min {
|
||||
delay := min - diff
|
||||
//ensures at least MinFetchInterval delay.
|
||||
//should be throttled by the fetcher!
|
||||
time.Sleep(delay)
|
||||
}
|
||||
|
@ -173,6 +182,9 @@ func (mp *master) fetchLoop() {
|
|||
}
|
||||
|
||||
func (mp *master) fetch() {
|
||||
if mp.restarting {
|
||||
return //skip if restarting
|
||||
}
|
||||
mp.logf("checking for updates...")
|
||||
reader, err := mp.Fetcher.Fetch()
|
||||
if err != nil {
|
||||
|
@ -180,6 +192,7 @@ func (mp *master) fetch() {
|
|||
return
|
||||
}
|
||||
if reader == nil {
|
||||
mp.logf("no updates")
|
||||
return //fetcher has explicitly said there are no updates
|
||||
}
|
||||
//optional closer
|
||||
|
@ -191,11 +204,14 @@ func (mp *master) fetch() {
|
|||
mp.logf("failed to open temp binary: %s", err)
|
||||
return
|
||||
}
|
||||
defer os.Remove(tmpBinPath)
|
||||
defer func() {
|
||||
tmpBin.Close()
|
||||
os.Remove(tmpBinPath)
|
||||
}()
|
||||
//tee off to sha1
|
||||
hash := sha1.New()
|
||||
reader = io.TeeReader(reader, hash)
|
||||
//write to temp
|
||||
//write to a temp file
|
||||
_, err = io.Copy(tmpBin, reader)
|
||||
if err != nil {
|
||||
mp.logf("failed to write temp binary: %s", err)
|
||||
|
@ -204,6 +220,7 @@ func (mp *master) fetch() {
|
|||
//compare hash
|
||||
newHash := hash.Sum(nil)
|
||||
if bytes.Equal(mp.binHash, newHash) {
|
||||
mp.logf("hash match - skip")
|
||||
return
|
||||
}
|
||||
//copy permissions
|
||||
|
@ -212,7 +229,13 @@ func (mp *master) fetch() {
|
|||
return
|
||||
}
|
||||
tmpBin.Close()
|
||||
//sanity check
|
||||
if mp.Config.PreUpgrade != nil {
|
||||
if err := mp.Config.PreUpgrade(tmpBinPath); err != nil {
|
||||
mp.logf("user cancelled upgrade: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
//go-upgrade sanity check, dont replace our good binary with a text file
|
||||
buff := make([]byte, 8)
|
||||
rand.Read(buff)
|
||||
tokenIn := hex.EncodeToString(buff)
|
||||
|
@ -234,17 +257,21 @@ func (mp *master) fetch() {
|
|||
}
|
||||
mp.logf("upgraded binary (%x -> %x)", mp.binHash[:12], newHash[:12])
|
||||
mp.binHash = newHash
|
||||
//binary successfully replaced, perform graceful restart
|
||||
mp.restarting = true
|
||||
mp.signalledAt = time.Now()
|
||||
mp.signals <- mp.Config.Signal //ask nicely to terminate
|
||||
select {
|
||||
case <-mp.restarted:
|
||||
//success
|
||||
case <-time.After(mp.TerminateTimeout):
|
||||
//times up process, we did ask nicely!
|
||||
mp.logf("graceful timeout, forcing exit")
|
||||
mp.signals <- syscall.SIGKILL
|
||||
//binary successfully replaced
|
||||
if !mp.Config.NoRestart && mp.slaveCmd != nil {
|
||||
//if running, perform graceful restart
|
||||
mp.restarting = true
|
||||
mp.awaitingUSR1 = true
|
||||
mp.signalledAt = time.Now()
|
||||
mp.signals <- mp.Config.Signal //ask nicely to terminate
|
||||
select {
|
||||
case <-mp.restarted:
|
||||
//success
|
||||
case <-time.After(mp.TerminateTimeout):
|
||||
//times up process, we did ask nicely!
|
||||
mp.logf("graceful timeout, forcing exit")
|
||||
mp.signals <- syscall.SIGKILL
|
||||
}
|
||||
}
|
||||
//and keep fetching...
|
||||
return
|
||||
|
@ -268,14 +295,15 @@ func (mp *master) fork() {
|
|||
e = append(e, envIsSlave+"=1")
|
||||
e = append(e, envNumFDs+"="+strconv.Itoa(len(mp.Config.Addresses)))
|
||||
cmd.Env = e
|
||||
//inherit master args/stdfiles
|
||||
cmd.Args = os.Args
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
//include socket files
|
||||
cmd.ExtraFiles = mp.slaveExtraFiles
|
||||
if err := cmd.Start(); err != nil {
|
||||
fatalf("failed to fork: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if mp.restarting {
|
||||
mp.restartedAt = time.Now()
|
||||
|
@ -312,12 +340,13 @@ func (mp *master) fork() {
|
|||
//if descriptors are released, the program
|
||||
//has yielded control of its sockets and
|
||||
//a new instance should be started to pick
|
||||
//them up
|
||||
//them up. The previous cmd.Wait() will still
|
||||
//be consumed though it will be discarded.
|
||||
}
|
||||
}
|
||||
|
||||
func (mp *master) logf(f string, args ...interface{}) {
|
||||
if mp.Logging {
|
||||
if mp.Log {
|
||||
log.Printf("[go-upgrade master] "+f, args...)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,10 +38,10 @@ type State struct {
|
|||
|
||||
type slave struct {
|
||||
Config
|
||||
listeners []*upListener
|
||||
ppid int
|
||||
pproc *os.Process
|
||||
state State
|
||||
listeners []*upListener
|
||||
masterPid int
|
||||
masterProc *os.Process
|
||||
state State
|
||||
}
|
||||
|
||||
func (sp *slave) run() {
|
||||
|
@ -57,16 +57,16 @@ func (sp *slave) run() {
|
|||
}
|
||||
|
||||
func (sp *slave) watchParent() {
|
||||
sp.ppid = os.Getppid()
|
||||
proc, err := os.FindProcess(sp.ppid)
|
||||
sp.masterPid = os.Getppid()
|
||||
proc, err := os.FindProcess(sp.masterPid)
|
||||
if err != nil {
|
||||
fatalf("parent process %s", err)
|
||||
}
|
||||
sp.pproc = proc
|
||||
sp.masterProc = proc
|
||||
go func() {
|
||||
for {
|
||||
//sending signal 0 should not error as long as the process is alive
|
||||
if err := sp.pproc.Signal(syscall.Signal(0)); err != nil {
|
||||
if err := sp.masterProc.Signal(syscall.Signal(0)); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
|
@ -111,14 +111,14 @@ func (sp *slave) watchSignal() {
|
|||
}
|
||||
sp.logf("released")
|
||||
//signal released fds
|
||||
sp.pproc.Signal(syscall.SIGUSR1)
|
||||
sp.masterProc.Signal(syscall.SIGUSR1)
|
||||
sp.logf("notify USR1")
|
||||
//listeners should be waiting on connections to close...
|
||||
}()
|
||||
}
|
||||
|
||||
func (sp *slave) logf(f string, args ...interface{}) {
|
||||
if sp.Logging {
|
||||
if sp.Log {
|
||||
log.Printf("[go-upgrade slave] "+f, args...)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue