Implement self-update recovery (T-403)
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type healthRecord struct {
|
||||
SchemaVersion int `json:"schema_version"`
|
||||
RequestID string `json:"request_id"`
|
||||
}
|
||||
|
||||
// FileHealthWaiter reads the fixed root health file without accepting a
|
||||
// caller-provided locator.
|
||||
type FileHealthWaiter struct {
|
||||
PollInterval time.Duration
|
||||
}
|
||||
|
||||
// WaitForHealth waits for a matching acknowledgement below target's root.
|
||||
func (waiter FileHealthWaiter) WaitForHealth(ctx context.Context, targetDir, requestID string, timeout time.Duration) error {
|
||||
layout, err := inspectTarget(targetDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !validRequestID(requestID) {
|
||||
return fmt.Errorf("%w: unsafe request ID", ErrInvalidRequest)
|
||||
}
|
||||
interval := waiter.PollInterval
|
||||
if interval <= 0 {
|
||||
interval = 250 * time.Millisecond
|
||||
}
|
||||
timer := time.NewTimer(timeout)
|
||||
defer timer.Stop()
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
record, readErr := readHealth(layout.health)
|
||||
if readErr == nil {
|
||||
if record.RequestID != requestID {
|
||||
return fmt.Errorf("%w: request ID does not match", ErrHealthInvalid)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !os.IsNotExist(readErr) {
|
||||
return readErr
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-timer.C:
|
||||
return ErrHealthTimeout
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AcknowledgeHealthFromExecutable writes the acknowledgement only when the
|
||||
// running executable is exactly <root>/app/SoftBox.exe.
|
||||
func AcknowledgeHealthFromExecutable(executablePath, requestID string, syncer DirectorySyncer) error {
|
||||
if syncer == nil {
|
||||
return fmt.Errorf("%w: directory syncer is required", ErrInvalidRequest)
|
||||
}
|
||||
if !validRequestID(requestID) {
|
||||
return fmt.Errorf("%w: unsafe request ID", ErrInvalidRequest)
|
||||
}
|
||||
if !filepath.IsAbs(executablePath) || filepath.Base(filepath.Clean(executablePath)) != ProductExecutableName {
|
||||
return fmt.Errorf("%w: executable is not SoftBox.exe", ErrUnsafeLayout)
|
||||
}
|
||||
appDir := filepath.Dir(filepath.Clean(executablePath))
|
||||
if filepath.Base(appDir) != appDirectoryName {
|
||||
return fmt.Errorf("%w: executable is outside root/app", ErrUnsafeLayout)
|
||||
}
|
||||
layout, err := inspectTarget(appDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := requireRealDirectory(layout.target, "target"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateRegularFile(executablePath); err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrUnsafeLayout, err)
|
||||
}
|
||||
transaction, exists, err := loadTransaction(layout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists || transaction.RequestID != requestID ||
|
||||
(transaction.Phase != phaseStagingActivated && transaction.Phase != phaseLaunched) {
|
||||
return fmt.Errorf("%w: no matching activated transaction", ErrHealthInvalid)
|
||||
}
|
||||
record := healthRecord{SchemaVersion: transactionSchemaVersion, RequestID: requestID}
|
||||
data, err := json.Marshal(record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = append(data, '\n')
|
||||
return replaceRegularFile(layout.root, layout.health, ".self-update-health-*.tmp", data, syncer)
|
||||
}
|
||||
|
||||
func readHealth(path string) (healthRecord, error) {
|
||||
data, err := readRegularFile(path)
|
||||
if err != nil {
|
||||
return healthRecord{}, err
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
var record healthRecord
|
||||
if err := decoder.Decode(&record); err != nil {
|
||||
return healthRecord{}, fmt.Errorf("%w: %v", ErrHealthInvalid, err)
|
||||
}
|
||||
var extra interface{}
|
||||
if err := decoder.Decode(&extra); err != io.EOF {
|
||||
return healthRecord{}, ErrHealthInvalid
|
||||
}
|
||||
if record.SchemaVersion != transactionSchemaVersion || !validRequestID(record.RequestID) {
|
||||
return healthRecord{}, ErrHealthInvalid
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
Reference in New Issue
Block a user