| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "fmt" |
| 6 | "log" |
| 7 | "os" |
| 8 | "path/filepath" |
| 9 | "time" |
| 10 | |
| 11 | "github.com/carlosframework/platform/internal/registry" |
| 12 | "github.com/carlosframework/platform/internal/release" |
| 13 | "github.com/carlosframework/platform/internal/rootparticipation" |
| 14 | ) |
| 15 | |
| 16 | var errRestartBinding = errors.Join(errors.New("named-unit binding changed"), os.ErrPermission) |
| 17 | |
| 18 | // A retained census keeps the strict path even after the flag is disabled. |
| 19 | // An unreadable census is never evidence that this is a legacy box. |
| 20 | // The claims flag alone cannot select bound effects without a census. |
| 21 | func legacyRootRestart() (bool, error) { |
| 22 | l, required, err := rootparticipation.OpenForUpdate() |
| 23 | if l.State != nil { |
| 24 | l.State.Close() |
| 25 | } |
| 26 | return !required && err == nil, err |
| 27 | } |
| 28 | |
| 29 | func (c rootRestartChecks) allowLegacy() (bool, error) { |
| 30 | if c.legacy == nil { |
| 31 | return false, nil |
| 32 | } |
| 33 | return c.legacy() |
| 34 | } |
| 35 | |
| 36 | func (c rootRestartChecks) noteLegacy() { |
| 37 | if c.legacyLog != nil { |
| 38 | c.legacyLog.Do(func() { log.Print("adopt: legacy restart (no root census)") }) |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | func restartEffectFailed(err error) bool { |
| 43 | var notUp *release.UnitNotUpError |
| 44 | return errors.As(err, ¬Up) |
| 45 | } |
| 46 | |
| 47 | // This is main's stamped-unit path, including its fallback on Show or |
| 48 | // ExecStart mismatch. Binding cannot drive effects without a root census. |
| 49 | func legacyCheckedUnitRestart(sys unitSystem, optDir string, getenv func(string) string, rt registry.Route, settled func(registry.Route) error, c rootRestartChecks) error { |
| 50 | before, err := sys.Show(rt.Unit) |
| 51 | link := release.LiveLink(optDir, rt.Host) |
| 52 | if err != nil { |
| 53 | log.Printf("adopt: %s: cannot read %s (%v); restart not checked", rt.Host, rt.Unit, err) |
| 54 | return sys.TryRestart(rt.Unit) |
| 55 | } |
| 56 | if before.ExecPath != link { |
| 57 | log.Printf("adopt: binding: %s runs %s, but the unit stamp is on %s; restart not checked", rt.Unit, before.ExecPath, rt.Host) |
| 58 | return sys.TryRestart(rt.Unit) |
| 59 | } |
| 60 | if before.ActiveState != "active" { |
| 61 | return sys.TryRestart(rt.Unit) |
| 62 | } |
| 63 | want, err := filepath.EvalSymlinks(link) |
| 64 | if err != nil { |
| 65 | return sys.TryRestart(rt.Unit) |
| 66 | } |
| 67 | if err := sys.TryRestart(rt.Unit); err != nil { |
| 68 | return err |
| 69 | } |
| 70 | err = settleUnit(sys, rt.Unit, before.InvocationID, legacySettleOptions(optDir, getenv, rt, want, c)) |
| 71 | if errors.Is(err, errUnitStopped) { |
| 72 | log.Printf("adopt: %s: %s was stopped during settle; not judged, not restarted", rt.Host, rt.Unit) |
| 73 | return nil |
| 74 | } |
| 75 | if err == nil { |
| 76 | log.Printf("adopt: %s: %s settled on %s", rt.Host, rt.Unit, filepath.Dir(want)) |
| 77 | if settled != nil { |
| 78 | if e := settled(rt); e != nil { |
| 79 | log.Print(e) |
| 80 | } |
| 81 | } |
| 82 | } |
| 83 | return err |
| 84 | } |
| 85 | |
| 86 | func legacyRollbackRestart(sys unitSystem, optDir string, getenv func(string) string, rt registry.Route, c rootRestartChecks) error { |
| 87 | before, _ := sys.Show(rt.Unit) |
| 88 | if err := sys.ResetFailed(rt.Unit); err != nil { |
| 89 | log.Printf("adopt: %s: reset-failed %s: %v (continuing)", rt.Host, rt.Unit, err) |
| 90 | } |
| 91 | if err := sys.Restart(rt.Unit); err != nil { |
| 92 | return err |
| 93 | } |
| 94 | want, err := filepath.EvalSymlinks(release.LiveLink(optDir, rt.Host)) |
| 95 | if err != nil { |
| 96 | return err |
| 97 | } |
| 98 | err = settleUnit(sys, rt.Unit, before.InvocationID, legacySettleOptions(optDir, getenv, rt, want, c)) |
| 99 | if errors.Is(err, errUnitStopped) { |
| 100 | log.Printf("adopt: %s: %s was stopped during the rollback's settle; not judged, not restarted", rt.Host, rt.Unit) |
| 101 | return nil |
| 102 | } |
| 103 | if err != nil { |
| 104 | return fmt.Errorf("rollback did not come up either: %w", err) |
| 105 | } |
| 106 | return nil |
| 107 | } |
| 108 | |
| 109 | func legacySettleOptions(optDir string, getenv func(string) string, rt registry.Route, want string, c rootRestartChecks) settleOpts { |
| 110 | now, sleep := c.now, c.sleep |
| 111 | if now == nil { |
| 112 | now = time.Now |
| 113 | } |
| 114 | if sleep == nil { |
| 115 | sleep = time.Sleep |
| 116 | } |
| 117 | ready := routeReady(rt, 2*time.Second) |
| 118 | if c.legacyReady != nil { |
| 119 | ready = c.legacyReady(rt) |
| 120 | } |
| 121 | return settleOpts{Timeout: unitSettleTimeout(getenv), Poll: 500 * time.Millisecond, Hold: 5 * time.Second, WantDir: filepath.Dir(want), Ready: ready, Now: now, Sleep: sleep} |
| 122 | } |
| 123 | |