Refactor file handing for self-update.

* Write new file payload to a temp file before touching the original
binary. Minimizes the possibility of failing mid-write and corrupting
the binary.
* On Windows, move the original binary out to a temp file rather than
removing it as the running binary is locked. Fixes issue #2248.
This commit is contained in:
Matt LaPlante 2021-08-23 12:21:09 -05:00 committed by Michael Eischer
parent 7d55b4f95e
commit 0ba9d4ced7
4 changed files with 69 additions and 24 deletions

View file

@ -0,0 +1,8 @@
Bugfix: Support self-update on Windows
Restic self-update would fail in situations where the operating system
locks running binaries, including Windows. The new behavior works around
this by renaming the running file and swapping the updated file in place.
https://github.com/restic/restic/issues/2248
https://github.com/restic/restic/pull/3675

View file

@ -10,6 +10,7 @@ import (
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"runtime"
@ -40,14 +41,6 @@ func findHash(buf []byte, filename string) (hash []byte, err error) {
}
func extractToFile(buf []byte, filename, target string, printf func(string, ...interface{})) error {
var mode = os.FileMode(0755)
// get information about the target file
fi, err := os.Lstat(target)
if err == nil {
mode = fi.Mode()
}
var rd io.Reader = bytes.NewReader(buf)
switch filepath.Ext(filename) {
case ".bz2":
@ -74,33 +67,44 @@ func extractToFile(buf []byte, filename, target string, printf func(string, ...i
rd = file
}
err = os.Remove(target)
if os.IsNotExist(err) {
err = nil
}
if err != nil {
return fmt.Errorf("unable to remove target file: %v", err)
}
dest, err := os.OpenFile(target, os.O_CREATE|os.O_EXCL|os.O_WRONLY, mode)
// Write everything to a temp file
dir := filepath.Dir(target)
new, err := ioutil.TempFile(dir, "restic")
if err != nil {
return err
}
n, err := io.Copy(dest, rd)
n, err := io.Copy(new, rd)
if err != nil {
_ = dest.Close()
_ = os.Remove(dest.Name())
_ = new.Close()
_ = os.Remove(new.Name())
return err
}
if err = new.Sync(); err != nil {
return err
}
if err = new.Close(); err != nil {
return err
}
err = dest.Close()
if err != nil {
mode := os.FileMode(0755)
// attempt to find the original mode
if fi, err := os.Lstat(target); err == nil {
mode = fi.Mode()
}
// Remove the original binary.
if err := removeResticBinary(dir, target); err != nil {
return err
}
printf("saved %d bytes in %v\n", n, dest.Name())
return nil
// Rename the temp file to the final location atomically.
if err := os.Rename(new.Name(), target); err != nil {
return err
}
printf("saved %d bytes in %v\n", n, target)
return os.Chmod(target, mode)
}
// DownloadLatestStableRelease downloads the latest stable released version of

View file

@ -0,0 +1,10 @@
//go:build !windows
// +build !windows
package selfupdate
// Remove the target binary.
func removeResticBinary(dir, target string) error {
// removed on rename on this platform
return nil
}

View file

@ -0,0 +1,23 @@
//go:build windows
// +build windows
package selfupdate
import (
"fmt"
"os"
"path/filepath"
)
// Rename (rather than remove) the running version. The running binary will be locked
// on Windows and cannot be removed while still executing.
func removeResticBinary(dir, target string) error {
backup := filepath.Join(dir, filepath.Base(target)+".bak")
if _, err := os.Stat(backup); err == nil {
_ = os.Remove(backup)
}
if err := os.Rename(target, backup); err != nil {
return fmt.Errorf("unable to rename target file: %v", err)
}
return nil
}