Apply downloads the current platform's binary, verifies its sha256 against the published codehamr_checksums.txt, and atomically replaces execPath. The caller is expected to syscall.Exec afterwards so the process becomes the new binary with no user-visible restart. Checksum verification closes the s
(ctx context.Context, execPath string)
| 131 | // ctx governs both fetches; the binary download sets no http.Client.Timeout, so |
| 132 | // ctx's deadline is the only budget. |
| 133 | func Apply(ctx context.Context, execPath string) error { |
| 134 | asset, ok := assetName(runtime.GOOS, runtime.GOARCH) |
| 135 | if !ok { |
| 136 | return fmt.Errorf("unsupported platform: %s/%s", runtime.GOOS, runtime.GOARCH) |
| 137 | } |
| 138 | expected, err := fetchHash(ctx, asset) |
| 139 | if err != nil { |
| 140 | return fmt.Errorf("checksum lookup: %w", err) |
| 141 | } |
| 142 | if expected == "" { |
| 143 | return fmt.Errorf("checksum lookup: no entry for %s in published manifest", asset) |
| 144 | } |
| 145 | tmp, err := os.CreateTemp(filepath.Dir(execPath), ".codehamr-update-*") |
| 146 | if err != nil { |
| 147 | return err |
| 148 | } |
| 149 | tmpPath := tmp.Name() |
| 150 | // Deferred Close/Remove clean up on any early return below. After a |
| 151 | // successful rename tmpPath is gone (Remove's ENOENT ignored); a second |
| 152 | // Close on *os.File is harmless. |
| 153 | defer os.Remove(tmpPath) |
| 154 | defer tmp.Close() |
| 155 | |
| 156 | req, err := http.NewRequestWithContext(ctx, "GET", releaseBase+asset, nil) |
| 157 | if err != nil { |
| 158 | return err |
| 159 | } |
| 160 | resp, err := http.DefaultClient.Do(req) |
| 161 | if err != nil { |
| 162 | return err |
| 163 | } |
| 164 | defer resp.Body.Close() |
| 165 | if resp.StatusCode != http.StatusOK { |
| 166 | return fmt.Errorf("download: status %d", resp.StatusCode) |
| 167 | } |
| 168 | // Stream-hash while writing so verification needs no second read of the |
| 169 | // temp file. |
| 170 | h := sha256.New() |
| 171 | if _, err := io.Copy(io.MultiWriter(tmp, h), resp.Body); err != nil { |
| 172 | return err |
| 173 | } |
| 174 | // Sync before the rename: rename is metadata-only, so without the flush a |
| 175 | // power loss right after Apply can journal the rename while the data never |
| 176 | // hit disk, leaving a truncated binary at execPath that CleanupOld can |
| 177 | // never reach (recoverable only by hand from .old). |
| 178 | if err := tmp.Sync(); err != nil { |
| 179 | return err |
| 180 | } |
| 181 | if err := tmp.Close(); err != nil { |
| 182 | return err |
| 183 | } |
| 184 | got := hex.EncodeToString(h.Sum(nil)) |
| 185 | if !strings.EqualFold(got, expected) { |
| 186 | return fmt.Errorf("checksum mismatch: downloaded %s, expected %s", got, expected) |
| 187 | } |
| 188 | if err := os.Chmod(tmpPath, 0o755); err != nil { |
| 189 | return err |
| 190 | } |