@@ -13,6 +13,7 @@ import (
1313 "net/http"
1414 "net/url"
1515 "os"
16+ "os/exec"
1617 "path/filepath"
1718 "runtime"
1819 "strconv"
@@ -24,10 +25,11 @@ import (
2425)
2526
2627const (
27- githubRepo = "https-cert/deploy"
28- githubAPIURL = "https://api.github.com/repos/" + githubRepo + "/releases/latest"
29- downloadTimeout = 10 * time .Minute
30- downloadRetries = 3
28+ githubRepo = "https-cert/deploy"
29+ githubAPIURL = "https://api.github.com/repos/" + githubRepo + "/releases/latest"
30+ downloadTimeout = 10 * time .Minute
31+ downloadRetries = 3
32+ smokeTestTimeout = 15 * time .Second
3133)
3234
3335// 常见的 GitHub 镜像加速服务
@@ -136,13 +138,12 @@ func PerformUpdate(ctx context.Context, info *UpdateInfo) error {
136138 logger .Info ("下载更新中..." , "version" , info .LatestVersion )
137139
138140 // 获取当前可执行文件路径
139- execPath , err := os . Executable ()
141+ execPath , err := currentExecutablePath ()
140142 if err != nil {
141- return fmt . Errorf ( "获取可执行文件路径失败: %w" , err )
143+ return err
142144 }
143- execPath , err = filepath .EvalSymlinks (execPath )
144- if err != nil {
145- return fmt .Errorf ("解析可执行文件路径失败: %w" , err )
145+ if err := checkExecutableWritable (execPath ); err != nil {
146+ return err
146147 }
147148
148149 // 创建临时目录
@@ -174,6 +175,8 @@ func PerformUpdate(ctx context.Context, info *UpdateInfo) error {
174175 return fmt .Errorf ("文件校验失败: %w" , err )
175176 }
176177 }
178+ } else {
179+ logger .Warn ("未找到 checksum 文件,跳过下载文件校验" , "binary" , info .BinaryName )
177180 }
178181
179182 // 设置可执行权限(Unix 系统)
@@ -182,25 +185,156 @@ func PerformUpdate(ctx context.Context, info *UpdateInfo) error {
182185 return fmt .Errorf ("设置可执行权限失败: %w" , err )
183186 }
184187 }
188+ if err := smokeTestBinary (ctx , newBinaryPath ); err != nil {
189+ return err
190+ }
185191
186192 // 备份当前版本
187- backupPath := execPath + ".backup"
188- if err := copyFile ( execPath , backupPath ); err != nil {
189- return fmt . Errorf ( "备份当前版本失败: %w" , err )
193+ backupPath , err := backupExecutable ( execPath )
194+ if err != nil {
195+ return err
190196 }
191197
192198 // 替换可执行文件
193199 if err := replaceExecutable (newBinaryPath , execPath ); err != nil {
194- // 恢复备份
195- if restoreErr := os .Rename (backupPath , execPath ); restoreErr != nil {
200+ if restoreErr := restoreBackup (backupPath , execPath ); restoreErr != nil {
196201 return fmt .Errorf ("替换失败且恢复备份失败: %w, 恢复错误: %v" , err , restoreErr )
197202 }
198203 return fmt .Errorf ("替换可执行文件失败: %w" , err )
199204 }
200205
201- // 删除备份
202- os .Remove (backupPath )
206+ logger .Info ("更新成功,已保留上一版本备份" , "backup" , backupPath )
207+
208+ return nil
209+ }
210+
211+ // Rollback 回滚到上一次更新保留的备份版本。
212+ func Rollback () error {
213+ execPath , err := currentExecutablePath ()
214+ if err != nil {
215+ return err
216+ }
217+ if err := checkExecutableWritable (execPath ); err != nil {
218+ return err
219+ }
220+
221+ backupPath := backupPathFor (execPath )
222+ if _ , err := os .Stat (backupPath ); err != nil {
223+ if os .IsNotExist (err ) {
224+ return fmt .Errorf ("未找到可回滚备份: %s" , backupPath )
225+ }
226+ return fmt .Errorf ("检查回滚备份失败: %w" , err )
227+ }
228+
229+ if err := restoreBackup (backupPath , execPath ); err != nil {
230+ return fmt .Errorf ("回滚失败: %w" , err )
231+ }
232+ logger .Info ("回滚成功" , "backup" , backupPath )
233+ return nil
234+ }
235+
236+ // currentExecutablePath 返回当前可执行文件的真实路径。
237+ func currentExecutablePath () (string , error ) {
238+ execPath , err := os .Executable ()
239+ if err != nil {
240+ return "" , fmt .Errorf ("获取可执行文件路径失败: %w" , err )
241+ }
242+ execPath , err = filepath .EvalSymlinks (execPath )
243+ if err != nil {
244+ return "" , fmt .Errorf ("解析可执行文件路径失败: %w" , err )
245+ }
246+ return execPath , nil
247+ }
248+
249+ // checkExecutableWritable 检查当前二进制和所在目录是否可写。
250+ func checkExecutableWritable (execPath string ) error {
251+ if strings .TrimSpace (execPath ) == "" {
252+ return fmt .Errorf ("可执行文件路径不能为空" )
253+ }
203254
255+ info , err := os .Stat (execPath )
256+ if err != nil {
257+ return fmt .Errorf ("检查当前二进制失败: %w" , err )
258+ }
259+ if info .IsDir () {
260+ return fmt .Errorf ("当前二进制路径不是文件: %s" , execPath )
261+ }
262+
263+ file , err := os .Open (execPath )
264+ if err != nil {
265+ return fmt .Errorf ("当前二进制不可读: %w" , err )
266+ }
267+ if err := file .Close (); err != nil {
268+ return fmt .Errorf ("关闭当前二进制失败: %w" , err )
269+ }
270+
271+ parentDir := filepath .Dir (execPath )
272+ probe , err := os .CreateTemp (parentDir , ".anssl-update-write-*" )
273+ if err != nil {
274+ return fmt .Errorf ("当前二进制所在目录不可写: %w" , err )
275+ }
276+ probePath := probe .Name ()
277+ if err := probe .Close (); err != nil {
278+ os .Remove (probePath )
279+ return fmt .Errorf ("关闭写权限探测文件失败: %w" , err )
280+ }
281+ if err := os .Remove (probePath ); err != nil {
282+ return fmt .Errorf ("删除写权限探测文件失败: %w" , err )
283+ }
284+ return nil
285+ }
286+
287+ // smokeTestBinary 通过 version 命令验证新二进制可执行。
288+ func smokeTestBinary (ctx context.Context , binaryPath string ) error {
289+ smokeCtx , cancel := context .WithTimeout (ctx , smokeTestTimeout )
290+ defer cancel ()
291+
292+ cmd := exec .CommandContext (smokeCtx , binaryPath , "version" )
293+ output , err := cmd .CombinedOutput ()
294+ if smokeCtx .Err () == context .DeadlineExceeded {
295+ return fmt .Errorf ("新二进制 smoke test 超时" )
296+ }
297+ if err != nil {
298+ return fmt .Errorf ("新二进制 smoke test 失败: %w\n %s" , err , strings .TrimSpace (string (output )))
299+ }
300+ return nil
301+ }
302+
303+ // backupPathFor 返回当前二进制的备份路径。
304+ func backupPathFor (execPath string ) string {
305+ return execPath + ".backup"
306+ }
307+
308+ // backupExecutable 备份当前二进制,成功更新后仍保留该备份供 rollback 使用。
309+ func backupExecutable (execPath string ) (string , error ) {
310+ backupPath := backupPathFor (execPath )
311+ if err := copyFile (execPath , backupPath ); err != nil {
312+ return "" , fmt .Errorf ("备份当前版本失败: %w" , err )
313+ }
314+ return backupPath , nil
315+ }
316+
317+ // restoreBackup 使用备份恢复当前二进制,同时保留备份文件。
318+ func restoreBackup (backupPath , execPath string ) error {
319+ restorePath := execPath + ".restore"
320+ os .Remove (restorePath )
321+ if err := copyFile (backupPath , restorePath ); err != nil {
322+ return fmt .Errorf ("准备恢复文件失败: %w" , err )
323+ }
324+ if _ , err := os .Stat (execPath ); os .IsNotExist (err ) {
325+ if err := copyFile (restorePath , execPath ); err != nil {
326+ os .Remove (restorePath )
327+ return err
328+ }
329+ return os .Remove (restorePath )
330+ } else if err != nil {
331+ os .Remove (restorePath )
332+ return err
333+ }
334+ if err := replaceExecutable (restorePath , execPath ); err != nil {
335+ os .Remove (restorePath )
336+ return err
337+ }
204338 return nil
205339}
206340
0 commit comments