Implement controlled app launch (T-401)

This commit is contained in:
ila
2026-07-19 21:10:59 +08:00
parent d0cf333394
commit 87083c387f
32 changed files with 1802 additions and 31 deletions
+1
View File
@@ -48,6 +48,7 @@ type ExtractResult struct {
Files int
Bytes int64
EntrypointPath string
WorkingDir string
PayloadFiles []ExtractedFile
}
+31 -4
View File
@@ -14,6 +14,11 @@ var (
type HealthCheck func(currentPath string) error
// PreSwitchCheck runs immediately before replacing an existing current
// directory. It is intentionally core-only so callers can inject process
// state without bringing platform APIs into the transaction.
type PreSwitchCheck func() error
type switchStep string
const (
@@ -42,13 +47,27 @@ func (err *RollbackError) Unwrap() []error {
// Switcher activates a verified staging directory and runs an injected check.
type Switcher struct {
health HealthCheck
afterStep func(switchStep) error
durability durabilityFence
health HealthCheck
preSwitchCheck PreSwitchCheck
afterStep func(switchStep) error
durability durabilityFence
}
func NewSwitcher(health HealthCheck) *Switcher {
return &Switcher{health: health, durability: defaultDurability()}
return NewSwitcherWithPreSwitchCheck(health, nil)
}
// NewSwitcherWithPreSwitchCheck creates a switcher that performs the optional
// check only for updates with an existing current directory.
func NewSwitcherWithPreSwitchCheck(
health HealthCheck,
preSwitchCheck PreSwitchCheck,
) *Switcher {
return &Switcher{
health: health,
preSwitchCheck: preSwitchCheck,
durability: defaultDurability(),
}
}
func (switcher *Switcher) Switch(root string) error {
@@ -75,6 +94,14 @@ func (switcher *Switcher) Switch(root string) error {
if state.backup {
return ErrBackupExists
}
if state.current && switcher.preSwitchCheck != nil {
if err := switcher.preSwitchCheck(); err != nil {
if cleanupErr := removeManagedDirectoryWithFence(layout, layout.staging, fence); cleanupErr != nil {
return errors.Join(err, cleanupErr)
}
return err
}
}
record := newTransaction(phasePrepared, state.current)
if err := writeTransactionWithFence(layout, record, fence); err != nil {
+35
View File
@@ -57,6 +57,41 @@ func TestSwitcherRemovesFailedInitialInstall(t *testing.T) {
assertMissing(t, filepath.Join(root, transactionFileName))
}
func TestSwitcherPreSwitchCheckPreventsUpdateAndCleansStaging(t *testing.T) {
root := makeInstallRoot(t, "old", "new")
checkErr := errors.New("target is running")
switcher := NewSwitcherWithPreSwitchCheck(
func(string) error { return nil },
func() error {
assertVersion(t, filepath.Join(root, "current"), "old")
assertVersion(t, filepath.Join(root, "staging"), "new")
return checkErr
},
)
err := switcher.Switch(root)
if !errors.Is(err, checkErr) {
t.Fatalf("Switch() error = %v, want %v", err, checkErr)
}
assertVersion(t, filepath.Join(root, "current"), "old")
assertMissing(t, filepath.Join(root, "staging"))
assertMissing(t, filepath.Join(root, "backup"))
assertMissing(t, filepath.Join(root, transactionFileName))
}
func TestSwitcherSkipsPreSwitchCheckForInitialInstall(t *testing.T) {
root := makeInstallRoot(t, "", "new")
switcher := NewSwitcherWithPreSwitchCheck(
func(string) error { return nil },
func() error { return errors.New("must not run") },
)
if err := switcher.Switch(root); err != nil {
t.Fatalf("Switch() error = %v", err)
}
assertVersion(t, filepath.Join(root, "current"), "new")
}
func TestRecoverInterruptedSwitch(t *testing.T) {
tests := []struct {
name string
+1
View File
@@ -197,6 +197,7 @@ func (extractor Extractor) ExtractVerifiedFileWithCheck(
if err != nil {
return ExtractResult{}, packageError(PackageStageExtract, err)
}
result.WorkingDir = manifest.WorkingDir
return result, nil
}