func (u *Updater) update() error { path, err := osext.Executable() if err != nil { return err } old, err := os.Open(path) if err != nil { return err } defer old.Close() err = u.fetchInfo() if err != nil { return err } if u.info.Version == Version { return nil } bin, err := u.fetchAndVerifyPatch(old) if err != nil { switch err { case ErrNoPatchAvailable: log.Println("update: no patch available, falling back to full binary") case ErrHashMismatch: log.Println("update: hash mismatch from patched binary") default: log.Println("update: patching binary,", err) } bin, err = u.fetchAndVerifyFullBin() if err != nil { if err == ErrHashMismatch { log.Println("update: hash mismatch from full binary") } else { log.Println("update: fetching full binary,", err) } return err } } // close the old binary before installing because on windows // it can't be renamed if a handle to the file is still open old.Close() err, errRecover := update.FromStream(bytes.NewBuffer(bin)) if errRecover != nil { return fmt.Errorf("update and recovery errors: %q %q", err, errRecover) } if err != nil { return err } log.Printf("Updated v%s -> v%s.", Version, u.info.Version) return nil }
// FromStream reads the contents of the supplied io.Reader newBinary // and uses them to update the current program's executable file. // // FromStream performs the following actions to ensure a cross-platform safe // update: // // - Creates a new file, /path/to/.program-name.new with mode 0755 and copies // the contents of newBinary into the file // // - Renames the current program's executable file from /path/to/program-name // to /path/to/.program-name.old // // - Renames /path/to/.program-name.new to /path/to/program-name // // - If the rename is successful, it erases /path/to/.program.old. If this operation // fails, no error is reported. // // - If the rename is unsuccessful, it attempts to rename /path/to/.program-name.old // back to /path/to/program-name. If this operation fails, the error is not reported // in order to not mask the error that caused the rename recovery attempt. func FromStream(newBinary io.Reader) (err error, errRecover error) { // get the path to the executable thisExecPath, err := osext.Executable() if err != nil { return } // get the directory the executable exists in execDir := filepath.Dir(thisExecPath) execName := filepath.Base(thisExecPath) // Copy the contents of of newbinary to a the new executable file newExecPath := filepath.Join(execDir, fmt.Sprintf(".%s.new", execName)) fp, err := os.OpenFile(newExecPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) if err != nil { return } defer fp.Close() _, err = io.Copy(fp, newBinary) // if we don't call fp.Close(), windows won't let us move the new executable // because the file will still be "in use" fp.Close() // this is where we'll move the executable to so that we can swap in the updated replacement oldExecPath := filepath.Join(execDir, fmt.Sprintf(".%s.old", execName)) // delete any existing old exec file - this is necessary on Windows for two reasons: // 1. after a successful update, windows can't remove the .old file because the process is still running // 2. windows rename operations fail if the destination file already exists _ = os.Remove(oldExecPath) // move the existing executable to a new file in the same directory err = os.Rename(thisExecPath, oldExecPath) if err != nil { return } // move the new exectuable in to become the new program err = os.Rename(newExecPath, thisExecPath) if err != nil { // copy unsuccessful errRecover = os.Rename(oldExecPath, thisExecPath) } else { // copy successful, remove the old binary _ = os.Remove(oldExecPath) } return }
func (u *Updater) backgroundRun() { os.MkdirAll(u.dir, 0777) if u.wantUpdate() { if err := update.SanityCheck(); err != nil { // fail return } self, err := osext.Executable() if err != nil { // fail update, couldn't figure out path to self return } // TODO(bgentry): logger isn't on Windows. Replace w/ proper error reports. l := exec.Command("logger", "-thk") c := exec.Command(self, "update") if w, err := l.StdinPipe(); err == nil && l.Start() == nil { c.Stdout = w c.Stderr = w } c.Start() } }
// SanityCheck() attempts to determine whether an in-place executable update could // succeed by performing preliminary checks (to establish valid permissions, etc). // This helps avoid downloading updates when we know the update can't be successfully // applied later. func SanityCheck() (err error) { // get the path to the executable thisExecPath, err := osext.Executable() if err != nil { return } // get the directory the executable exists in execDir := filepath.Dir(thisExecPath) execName := filepath.Base(thisExecPath) // attempt to open a file in the executable's directory newExecPath := filepath.Join(execDir, fmt.Sprintf(".%s.new", execName)) fp, err := os.OpenFile(newExecPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) if err != nil { return } fp.Close() _ = os.Remove(newExecPath) return }