github.com/gogf/selfupdate@v0.0.0-20231215043001-5c48c528462f/apply.go (about) 1 package selfupdate 2 3 import ( 4 "bytes" 5 "crypto" 6 "errors" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "os" 11 "path/filepath" 12 13 "github.com/gogf/selfupdate/internal/osext" 14 ) 15 16 // Apply performs an update of the current executable or opts.TargetFile, with 17 // the contents of the given io.Reader. When the update fails, it is unlikely 18 // that old executable is corrupted, but still, applications need to check the 19 // returned error with RollbackError() and notify the user of the bad news and 20 // ask them to recover manually. 21 func Apply(update io.Reader, opts Options) error { 22 err := PrepareAndCheckBinary(update, opts) 23 if err != nil { 24 return err 25 } 26 return CommitBinary(opts) 27 } 28 29 // PrepareAndCheckBinary reads the new binary content from io.Reader and performs the following actions: 30 // 1. If configured, applies the contents of the update io.Reader as a binary patch. 31 // 2. If configured, computes the checksum of the executable and verifies it matches. 32 // 3. If configured, verifies the signature with a public key. 33 // 4. Creates a new file, /path/to/.target.new with the TargetMode with the contents of the updated file 34 func PrepareAndCheckBinary(update io.Reader, opts Options) error { 35 // get target path 36 targetPath, err := opts.getPath() 37 if err != nil { 38 return err 39 } 40 41 var newBytes []byte 42 if opts.Patcher != nil { 43 if newBytes, err = opts.applyPatch(update, targetPath); err != nil { 44 return err 45 } 46 } else { 47 // no patch to apply, go on through 48 if newBytes, err = ioutil.ReadAll(update); err != nil { 49 return err 50 } 51 } 52 53 // verify checksum if requested 54 if opts.Checksum != nil { 55 if err = opts.verifyChecksum(newBytes); err != nil { 56 return err 57 } 58 } 59 60 if opts.Verifier != nil { 61 if err = opts.Verifier.Verify(newBytes); err != nil { 62 return err 63 } 64 } 65 66 // get the directory the executable exists in 67 updateDir := filepath.Dir(targetPath) 68 filename := filepath.Base(targetPath) 69 70 // Copy the contents of newbinary to a new executable file 71 newPath := filepath.Join(updateDir, fmt.Sprintf(".%s.new", filename)) 72 fp, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, opts.getMode()) 73 if err != nil { 74 return err 75 } 76 defer fp.Close() 77 78 _, err = io.Copy(fp, bytes.NewReader(newBytes)) 79 if err != nil { 80 return err 81 } 82 83 // if we don't call fp.Close(), windows won't let us move the new executable 84 // because the file will still be "in use" 85 fp.Close() 86 return nil 87 } 88 89 // CommitBinary moves the new executable to the location of the current executable or opts.TargetPath 90 // if specified. It performs the following operations: 91 // 1. Renames /path/to/target to /path/to/.target.old 92 // 2. Renames /path/to/.target.new to /path/to/target 93 // 3. If the final rename is successful, deletes /path/to/.target.old, returns no error. On Windows, 94 // the removal of /path/to/target.old always fails, so instead Apply hides the old file instead. 95 // 4. If the final rename fails, attempts to roll back by renaming /path/to/.target.old 96 // back to /path/to/target. 97 // 98 // If the roll back operation fails, the file system is left in an inconsistent state where there is 99 // no new executable file and the old executable file could not be be moved to its original location. 100 // In this case you should notify the user of the bad news and ask them to recover manually. Applications 101 // can determine whether the rollback failed by calling RollbackError, see the documentation on that function 102 // for additional detail. 103 func CommitBinary(opts Options) error { 104 // get the directory the file exists in 105 targetPath, err := opts.getPath() 106 if err != nil { 107 return err 108 } 109 110 updateDir := filepath.Dir(targetPath) 111 filename := filepath.Base(targetPath) 112 newPath := filepath.Join(updateDir, fmt.Sprintf(".%s.new", filename)) 113 114 // this is where we'll move the executable to so that we can swap in the updated replacement 115 oldPath := opts.OldSavePath 116 removeOld := opts.OldSavePath == "" 117 if removeOld { 118 oldPath = filepath.Join(updateDir, fmt.Sprintf(".%s.old", filename)) 119 } 120 121 // delete any existing old exec file - this is necessary on Windows for two reasons: 122 // 1. after a successful update, Windows can't remove the .old file because the process is still running 123 // 2. windows rename operations fail if the destination file already exists 124 _ = os.Remove(oldPath) 125 126 // move the existing executable to a new file in the same directory 127 err = os.Rename(targetPath, oldPath) 128 if err != nil { 129 return err 130 } 131 132 // move the new exectuable in to become the new program 133 err = os.Rename(newPath, targetPath) 134 135 if err != nil { 136 // move unsuccessful 137 // 138 // The filesystem is now in a bad state. We have successfully 139 // moved the existing binary to a new location, but we couldn't move the new 140 // binary to take its place. That means there is no file where the current executable binary 141 // used to be! 142 // Try to rollback by restoring the old binary to its original path. 143 rerr := os.Rename(oldPath, targetPath) 144 if rerr != nil { 145 return &rollbackErr{err, rerr} 146 } 147 148 return err 149 } 150 151 // move successful, remove the old binary if needed 152 if removeOld { 153 errRemove := os.Remove(oldPath) 154 155 // windows has trouble with removing old binaries, so hide it instead 156 if errRemove != nil { 157 _ = hideFile(oldPath) 158 } 159 } 160 161 return nil 162 } 163 164 // RollbackError takes an error value returned by Apply and returns the error, if any, 165 // that occurred when attempting to roll back from a failed update. Applications should 166 // always call this function on any non-nil errors returned by Apply. 167 // 168 // If no rollback was needed or if the rollback was successful, RollbackError returns nil, 169 // otherwise it returns the error encountered when trying to roll back. 170 func RollbackError(err error) error { 171 if err == nil { 172 return nil 173 } 174 if rerr, ok := err.(*rollbackErr); ok { 175 return rerr.rollbackErr 176 } 177 return nil 178 } 179 180 type rollbackErr struct { 181 error // original error 182 rollbackErr error // error encountered while rolling back 183 } 184 185 type Options struct { 186 // TargetPath defines the path to the file to update. 187 // The emptry string means 'the executable file of the running program'. 188 TargetPath string 189 190 // Create TargetPath replacement with this file mode. If zero, defaults to 0755. 191 TargetMode os.FileMode 192 193 // Checksum of the new binary to verify against. If nil, no checksum or signature verification is done. 194 Checksum []byte 195 196 // Verifier for signature verification. If nil, no signature verification is done. 197 Verifier *Verifier 198 199 // Use this hash function to generate the checksum. If not set, SHA256 is used. 200 Hash crypto.Hash 201 202 // If nil, treat the update as a complete replacement for the contents of the file at TargetPath. 203 // If non-nil, treat the update contents as a patch and use this object to apply the patch. 204 Patcher Patcher 205 206 // Store the old executable file at this path after a successful update. 207 // The empty string means the old executable file will be removed after the update. 208 OldSavePath string 209 } 210 211 // CheckPermissions determines whether the process has the correct permissions to 212 // perform the requested update. If the update can proceed, it returns nil, otherwise 213 // it returns the error that would occur if an update were attempted. 214 func (o *Options) CheckPermissions() error { 215 // get the directory the file exists in 216 path, err := o.getPath() 217 if err != nil { 218 return err 219 } 220 221 fileDir := filepath.Dir(path) 222 fileName := filepath.Base(path) 223 224 // attempt to open a file in the file's directory 225 newPath := filepath.Join(fileDir, fmt.Sprintf(".%s.check-perm", fileName)) 226 fp, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, o.getMode()) 227 if err != nil { 228 return err 229 } 230 fp.Close() 231 232 _ = os.Remove(newPath) 233 return nil 234 } 235 236 func (o *Options) getPath() (string, error) { 237 if o.TargetPath == "" { 238 return osext.Executable() 239 } else { 240 return o.TargetPath, nil 241 } 242 } 243 244 func (o *Options) getMode() os.FileMode { 245 if o.TargetMode == 0 { 246 return 0755 247 } 248 return o.TargetMode 249 } 250 251 func (o *Options) getHash() crypto.Hash { 252 if o.Hash == 0 { 253 o.Hash = crypto.SHA256 254 } 255 return o.Hash 256 } 257 258 func (o *Options) applyPatch(patch io.Reader, targetPath string) ([]byte, error) { 259 // open the file to patch 260 old, err := os.Open(targetPath) 261 if err != nil { 262 return nil, err 263 } 264 defer old.Close() 265 266 // apply the patch 267 var applied bytes.Buffer 268 if err = o.Patcher.Patch(old, &applied, patch); err != nil { 269 return nil, err 270 } 271 272 return applied.Bytes(), nil 273 } 274 275 func (o *Options) verifyChecksum(updated []byte) error { 276 checksum, err := checksumFor(o.getHash(), updated) 277 if err != nil { 278 return err 279 } 280 281 if !bytes.Equal(o.Checksum, checksum) { 282 return fmt.Errorf("Updated file has wrong checksum. Expected: %x, got: %x", o.Checksum, checksum) 283 } 284 return nil 285 } 286 287 func checksumFor(h crypto.Hash, payload []byte) ([]byte, error) { 288 if !h.Available() { 289 return nil, errors.New("requested hash function not available") 290 } 291 hash := h.New() 292 hash.Write(payload) // guaranteed not to error 293 return hash.Sum([]byte{}), nil 294 }