rewrite complete, mostly working now

master
Jaime Pillora 2016-02-08 12:06:54 +11:00
parent 8f42cc8800
commit f623267271
16 changed files with 341 additions and 270 deletions

View File

@ -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
View File

@ -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

34
cmd/bootstrap/main.go Normal file
View File

@ -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,
},
})
}

1
cmd/go-upgrade/TODO Normal file
View File

@ -0,0 +1 @@
placeholder for the goupgrade binary tool

View File

@ -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

View File

@ -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

73
example/example.sh Executable file
View File

@ -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...

41
example/main.go Normal file
View File

@ -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,
},
})
}

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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++

View File

@ -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")
}

View File

@ -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

View File

@ -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...)
}
}

View File

@ -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...)
}
}