| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/sha256" |
| 6 | "encoding/hex" |
| 7 | "encoding/json" |
| 8 | "errors" |
| 9 | "io" |
| 10 | "log" |
| 11 | "os" |
| 12 | "path/filepath" |
| 13 | "strings" |
| 14 | "sync" |
| 15 | "time" |
| 16 | |
| 17 | "github.com/carlosframework/platform/internal/activator" |
| 18 | "github.com/carlosframework/platform/internal/hostwrite" |
| 19 | "github.com/carlosframework/platform/internal/registry" |
| 20 | "github.com/carlosframework/platform/internal/release" |
| 21 | "github.com/carlosframework/platform/internal/rootstate" |
| 22 | ) |
| 23 | |
| 24 | type rootRestartChecks struct { |
| 25 | unitShape func(context.Context, string, *registry.Route) error |
| 26 | preserveOutcome bool |
| 27 | history func() (*release.RootHistoryPass, error) |
| 28 | readLive func(string) (string, error) |
| 29 | legacy func() (bool, error) |
| 30 | legacyLog *sync.Once |
| 31 | legacyReady func(registry.Route) func() error |
| 32 | legacyStart func(string) error |
| 33 | targetDigest string |
| 34 | targetFile func(string) (*os.File, error) |
| 35 | dispatch func(context.Context, registry.Route) (bool, error) |
| 36 | resolve proofBindingResolver |
| 37 | started *sync.Map |
| 38 | attempts func() (rootstate.Store, error) |
| 39 | rollback func(context.Context, proofBinding) (proofBinding, error) |
| 40 | prepareRollback func(context.Context, proofBinding) (proofBinding, error) |
| 41 | recovered func(context.Context, proofBinding) (proofBinding, error) |
| 42 | identity hostwrite.Identity |
| 43 | effect func(func() error) error |
| 44 | verifyOnly bool |
| 45 | host func(context.Context, string) (proofBinding, error) |
| 46 | ready func(registry.Route) func() error |
| 47 | now func() time.Time |
| 48 | sleep func(time.Duration) |
| 49 | } |
| 50 | |
| 51 | func productionRootRestartChecks(sys unitSystem, optDir string) rootRestartChecks { |
| 52 | var sourceMu sync.Mutex |
| 53 | var cached *proofBindingSource |
| 54 | var sourceErr error |
| 55 | var opened bool |
| 56 | source := func(ctx context.Context) (*proofBindingSource, error) { |
| 57 | sourceMu.Lock() |
| 58 | defer sourceMu.Unlock() |
| 59 | if opened { |
| 60 | return cached, sourceErr |
| 61 | } |
| 62 | opened = true |
| 63 | s, err := openDeployStore(ctx) |
| 64 | sourceErr = err |
| 65 | if err != nil { |
| 66 | return nil, err |
| 67 | } |
| 68 | cached = productionProofBindingSource(s, sys, optDir) |
| 69 | return cached, nil |
| 70 | } |
| 71 | return rootRestartChecks{unitShape: rootClaimRestartShape, legacy: legacyRootRestart, legacyLog: &sync.Once{}, targetFile: rootstate.OpenTrustedFile, started: &sync.Map{}, readLive: rootstate.ReadTrustedLink, |
| 72 | recovered: func(ctx context.Context, b proofBinding) (proofBinding, error) { |
| 73 | s, err := source(ctx) |
| 74 | if err != nil { |
| 75 | return proofBinding{}, err |
| 76 | } |
| 77 | return s.recovered(ctx, b) |
| 78 | }, |
| 79 | history: func() (*release.RootHistoryPass, error) { |
| 80 | s, err := openDeployStore(context.Background()) |
| 81 | if err != nil { |
| 82 | return nil, err |
| 83 | } |
| 84 | return openRootHistory(s, optDir) |
| 85 | }, |
| 86 | dispatch: func(ctx context.Context, rt registry.Route) (bool, error) { |
| 87 | s, err := source(ctx) |
| 88 | if err != nil { |
| 89 | return false, err |
| 90 | } |
| 91 | return s.needsRestart(ctx, rt) |
| 92 | }, |
| 93 | attempts: func() (rootstate.Store, error) { |
| 94 | if err := rootstate.EnsurePublicDir(release.RootHealthyDir); err != nil { |
| 95 | return nil, err |
| 96 | } |
| 97 | return rootstate.Open(rootRestartState, true, false) |
| 98 | }, |
| 99 | prepareRollback: func(ctx context.Context, b proofBinding) (proofBinding, error) { |
| 100 | s, err := source(ctx) |
| 101 | if err != nil { |
| 102 | return proofBinding{}, err |
| 103 | } |
| 104 | return s.prepareRollback(ctx, b) |
| 105 | }, |
| 106 | rollback: func(ctx context.Context, b proofBinding) (proofBinding, error) { |
| 107 | s, err := source(ctx) |
| 108 | if err != nil { |
| 109 | return proofBinding{}, err |
| 110 | } |
| 111 | return s.rollback(ctx, b) |
| 112 | }, |
| 113 | resolve: func(ctx context.Context, rt registry.Route) (proofBinding, error) { |
| 114 | s, err := source(ctx) |
| 115 | if err != nil { |
| 116 | return proofBinding{}, err |
| 117 | } |
| 118 | return s.resolve(ctx, rt) |
| 119 | }, |
| 120 | host: func(ctx context.Context, host string) (proofBinding, error) { |
| 121 | s, err := source(ctx) |
| 122 | if err != nil { |
| 123 | return proofBinding{}, err |
| 124 | } |
| 125 | return s.forHost(ctx, host) |
| 126 | }, |
| 127 | ready: func(rt registry.Route) func() error { return proofRouteReady(rt, 2*time.Second) }, |
| 128 | now: time.Now, sleep: time.Sleep, |
| 129 | } |
| 130 | } |
| 131 | |
| 132 | func (c rootRestartChecks) runningVersion(ctx context.Context, rt registry.Route) (string, error) { |
| 133 | if rt.Unit == "" || rt.Unit == activator.UnitName(rt.Host) { |
| 134 | return "", nil |
| 135 | } |
| 136 | // Merge ruling 6: without a census the binding never chooses an |
| 137 | // effect, and the rollback target is one. Main's cur stands. |
| 138 | legacy, err := c.allowLegacy() |
| 139 | if err != nil { |
| 140 | return "", err |
| 141 | } |
| 142 | if legacy { |
| 143 | return "", nil |
| 144 | } |
| 145 | b, err := c.resolve(ctx, rt) |
| 146 | if err != nil { |
| 147 | return "", err |
| 148 | } |
| 149 | if b.PID > 0 { |
| 150 | return b.Version, nil |
| 151 | } |
| 152 | return "", nil |
| 153 | } |
| 154 | |
| 155 | func checkedUnitRestart(sys unitSystem, optDir string, getenv func(string) string, settled func(registry.Route) error, checks ...rootRestartChecks) func(registry.Route) error { |
| 156 | c := productionRootRestartChecks(sys, optDir) |
| 157 | if len(checks) == 1 { |
| 158 | c = checks[0] |
| 159 | } |
| 160 | return func(candidate registry.Route) error { |
| 161 | c := c |
| 162 | legacy, err := c.allowLegacy() |
| 163 | if err != nil { |
| 164 | return err |
| 165 | } |
| 166 | if legacy { |
| 167 | c.noteLegacy() |
| 168 | return legacyCheckedUnitRestart(sys, optDir, getenv, candidate, settled, c) |
| 169 | } |
| 170 | if err := c.checkUnitShape(context.Background(), candidate.Host, &candidate); err != nil { |
| 171 | return err |
| 172 | } |
| 173 | if c.attempts == nil { |
| 174 | return os.ErrPermission |
| 175 | } |
| 176 | s, err := c.attempts() |
| 177 | if err != nil { |
| 178 | return err |
| 179 | } |
| 180 | defer s.Close() |
| 181 | unlock, err := s.Lock(context.Background(), rollbackHostKey(candidate.Host)+".lock") |
| 182 | if err != nil { |
| 183 | return err |
| 184 | } |
| 185 | defer unlock() |
| 186 | binding, err := c.resolve(context.Background(), candidate) |
| 187 | if err != nil { |
| 188 | return err |
| 189 | } |
| 190 | before, err := sys.Show(binding.Route.Unit) |
| 191 | if err != nil { |
| 192 | return err |
| 193 | } |
| 194 | if before.ActiveState == "inactive" { |
| 195 | return errUnitStopped |
| 196 | } |
| 197 | if !pointerBoundUnit(before) && (before.ActiveState != "active" || before.MainPID <= 0 || before.MainPID != binding.PID || before.InvocationID != binding.Invocation) { |
| 198 | return os.ErrPermission |
| 199 | } |
| 200 | target, err := c.live(optDir, candidate.Host) |
| 201 | if err != nil { |
| 202 | return err |
| 203 | } |
| 204 | rel, err := filepath.Rel(filepath.Join(optDir, "versions"), target) |
| 205 | if err != nil { |
| 206 | return err |
| 207 | } |
| 208 | version := strings.Split(rel, string(filepath.Separator))[0] |
| 209 | if !release.ValidVersion(version) || target != release.VersionPath(optDir, binding.Route.Account, version, binding.Route.App) || c.targetFile == nil { |
| 210 | return os.ErrPermission |
| 211 | } |
| 212 | f, err := c.targetFile(target) |
| 213 | if err != nil { |
| 214 | return err |
| 215 | } |
| 216 | hash := sha256.New() |
| 217 | _, err = io.Copy(hash, f) |
| 218 | f.Close() |
| 219 | if err != nil { |
| 220 | return err |
| 221 | } |
| 222 | targetDigest := hex.EncodeToString(hash.Sum(nil)) |
| 223 | c.targetDigest = targetDigest |
| 224 | captured := binding |
| 225 | if pointerBoundUnit(before) { |
| 226 | previous, e := filepath.EvalSymlinks(filepath.Join(optDir, "previous", candidate.Host)) |
| 227 | if e == nil && previous != target { |
| 228 | rel, e := filepath.Rel(filepath.Join(optDir, "versions"), previous) |
| 229 | if e != nil { |
| 230 | return e |
| 231 | } |
| 232 | version := strings.Split(rel, string(filepath.Separator))[0] |
| 233 | if !release.ValidVersion(version) || previous != release.VersionPath(optDir, binding.Route.Account, version, binding.Route.App) { |
| 234 | return os.ErrPermission |
| 235 | } |
| 236 | f, e := c.targetFile(previous) |
| 237 | if e != nil { |
| 238 | return e |
| 239 | } |
| 240 | h := sha256.New() |
| 241 | _, e = io.Copy(h, f) |
| 242 | f.Close() |
| 243 | if e != nil { |
| 244 | return e |
| 245 | } |
| 246 | captured.Version, captured.Digest = version, hex.EncodeToString(h.Sum(nil)) |
| 247 | captured, e = c.prepareRollback(context.Background(), captured) |
| 248 | if e != nil { |
| 249 | return e |
| 250 | } |
| 251 | } else if e != nil && !errors.Is(e, os.ErrNotExist) { |
| 252 | return e |
| 253 | } |
| 254 | } |
| 255 | if err := writeRollbackState(s, rollbackHostKey(candidate.Host)+".previous", captured); err != nil { |
| 256 | return err |
| 257 | } |
| 258 | attempt, err := historyRestartAttempt(s, captured, c, target, getenv) |
| 259 | if err != nil { |
| 260 | return err |
| 261 | } |
| 262 | if c.started != nil { |
| 263 | c.started.Store(candidate.Host, attempt.ID) |
| 264 | } |
| 265 | err = restartBoundUnit(sys, optDir, getenv, binding, false, settled, c) |
| 266 | if err == nil { |
| 267 | return settleRollbackAttempt(s, attempt) |
| 268 | } |
| 269 | if errors.Is(err, errUnitStopped) { |
| 270 | return errors.Join(err, settleRollbackAttempt(s, attempt)) |
| 271 | } |
| 272 | if errors.Is(err, errRestartBinding) && !restartEffectFailed(err) { |
| 273 | return errors.Join(err, settleRollbackAttempt(s, attempt)) |
| 274 | } |
| 275 | attempt.Failed = true |
| 276 | var notUp *release.UnitNotUpError |
| 277 | if !errors.As(err, ¬Up) { |
| 278 | err = errors.Join(&release.UnitNotUpError{Unit: binding.Route.Unit, Reason: err.Error()}, err) |
| 279 | } |
| 280 | return errors.Join(err, writeRollbackState(s, rollbackJournalKey(candidate.Host), attempt)) |
| 281 | } |
| 282 | } |
| 283 | |
| 284 | func restartBoundUnit(sys unitSystem, optDir string, getenv func(string) string, binding proofBinding, rollback bool, settled func(registry.Route) error, c rootRestartChecks) error { |
| 285 | rt := binding.Route |
| 286 | if binding.Source == "" || !binding.Network || c.ready == nil { |
| 287 | return errRestartBinding |
| 288 | } |
| 289 | ready := c.ready(rt) |
| 290 | if ready == nil { |
| 291 | return errRestartBinding |
| 292 | } |
| 293 | before, err := sys.Show(rt.Unit) |
| 294 | if err != nil { |
| 295 | return errors.Join(errRestartBinding, err) |
| 296 | } |
| 297 | if before.ExecPath != proofExecPath(binding, optDir) { |
| 298 | return errRestartBinding |
| 299 | } |
| 300 | if before.ActiveState == "inactive" { |
| 301 | return errUnitStopped |
| 302 | } |
| 303 | want, err := c.live(optDir, rt.Host) |
| 304 | if err != nil { |
| 305 | return err |
| 306 | } |
| 307 | effect := c.effect |
| 308 | if effect == nil { |
| 309 | effect = func(fn func() error) error { return fn() } |
| 310 | } |
| 311 | if c.verifyOnly { |
| 312 | before.InvocationID = "" |
| 313 | } else if err := effect(func() error { |
| 314 | if rollback { |
| 315 | current, err := c.rollback(context.Background(), binding) |
| 316 | if err != nil || current.Source != binding.Source { |
| 317 | return errRestartBinding |
| 318 | } |
| 319 | if err := sys.ResetFailed(rt.Unit); err != nil { |
| 320 | return err |
| 321 | } |
| 322 | if err := sys.Restart(rt.Unit); err != nil { |
| 323 | return err |
| 324 | } |
| 325 | } else { |
| 326 | if !pointerBoundUnit(before) && (before.ActiveState != "active" || before.MainPID != binding.PID || before.InvocationID != binding.Invocation) { |
| 327 | return errRestartBinding |
| 328 | } |
| 329 | if before.MainPID > 0 { |
| 330 | start, err := sys.StartTime(before.MainPID) |
| 331 | if err != nil || start != binding.Start { |
| 332 | return errRestartBinding |
| 333 | } |
| 334 | } |
| 335 | current, err := c.host(context.Background(), rt.Host) |
| 336 | if err != nil || current.Source != binding.Source || current.Version != binding.Version || current.Digest != binding.Digest || current.PID != binding.PID || current.Invocation != binding.Invocation { |
| 337 | return errRestartBinding |
| 338 | } |
| 339 | if pointerBoundUnit(before) { |
| 340 | if err := sys.ResetFailed(rt.Unit); err != nil { |
| 341 | return err |
| 342 | } |
| 343 | return sys.Restart(rt.Unit) |
| 344 | } |
| 345 | if err := sys.TryRestart(rt.Unit); err != nil { |
| 346 | return err |
| 347 | } |
| 348 | } |
| 349 | return nil |
| 350 | }); err != nil { |
| 351 | return err |
| 352 | } |
| 353 | var observed proofBinding |
| 354 | observe := func() (proofBinding, error) { |
| 355 | if rollback && c.recovered != nil { |
| 356 | return c.recovered(context.Background(), binding) |
| 357 | } |
| 358 | return c.host(context.Background(), rt.Host) |
| 359 | } |
| 360 | var bindingErr error |
| 361 | checkedReady := func() error { |
| 362 | first, err := observe() |
| 363 | if err != nil || first.Source != binding.Source || want != release.VersionPath(optDir, first.Route.Account, first.Version, first.Route.App) { |
| 364 | bindingErr = errors.Join(errRestartBinding, err) |
| 365 | return bindingErr |
| 366 | } |
| 367 | if c.targetDigest != "" && first.Digest != c.targetDigest { |
| 368 | bindingErr = errRestartBinding |
| 369 | return bindingErr |
| 370 | } |
| 371 | if rollback && (first.Version != binding.Version || first.Digest != binding.Digest) { |
| 372 | bindingErr = errRestartBinding |
| 373 | return bindingErr |
| 374 | } |
| 375 | if err := ready(); err != nil { |
| 376 | return err |
| 377 | } |
| 378 | last, err := observe() |
| 379 | if err != nil || last.Source != first.Source || last.Version != first.Version || last.Digest != first.Digest || last.Invocation != first.Invocation || last.PID != first.PID || last.Start != first.Start { |
| 380 | bindingErr = errors.Join(errRestartBinding, err) |
| 381 | return bindingErr |
| 382 | } |
| 383 | observed = last |
| 384 | return nil |
| 385 | } |
| 386 | err = settleUnit(sys, rt.Unit, before.InvocationID, settleOpts{ |
| 387 | Timeout: unitSettleTimeout(getenv), Poll: 500 * time.Millisecond, Hold: 5 * time.Second, |
| 388 | WantDir: filepath.Dir(want), Ready: checkedReady, Now: c.now, Sleep: c.sleep, RequireExe: true, |
| 389 | }) |
| 390 | if err != nil { |
| 391 | if bindingErr != nil && !restartEffectFailed(err) { |
| 392 | if c.preserveOutcome { |
| 393 | return errors.Join(err, bindingErr) |
| 394 | } |
| 395 | return bindingErr |
| 396 | } |
| 397 | return err |
| 398 | } |
| 399 | current, err := observe() |
| 400 | if err != nil { |
| 401 | return errors.Join(errRestartBinding, err) |
| 402 | } |
| 403 | if current.Invocation != observed.Invocation || current.PID != observed.PID || current.Start != observed.Start || current.Source != binding.Source || want != release.VersionPath(optDir, current.Route.Account, current.Version, current.Route.App) || (rollback && (current.Version != binding.Version || current.Digest != binding.Digest)) { |
| 404 | return errRestartBinding |
| 405 | } |
| 406 | if settled != nil { |
| 407 | if err := effect(func() error { return settled(binding.Route) }); err != nil { |
| 408 | log.Print(err) |
| 409 | } |
| 410 | } |
| 411 | return nil |
| 412 | } |
| 413 | |
| 414 | func (c rootRestartChecks) live(optDir, host string) (string, error) { |
| 415 | if c.readLive != nil { |
| 416 | return c.readLive(release.LiveLink(optDir, host)) |
| 417 | } |
| 418 | return filepath.EvalSymlinks(release.LiveLink(optDir, host)) |
| 419 | } |
| 420 | |
| 421 | func rollbackUnitRestart(reg *registry.Registry, sys unitSystem, optDir string, getenv func(string) string, checks ...rootRestartChecks) func(string) error { |
| 422 | c := productionRootRestartChecks(sys, optDir) |
| 423 | if len(checks) == 1 { |
| 424 | c = checks[0] |
| 425 | } |
| 426 | |
| 427 | return func(host string) (result error) { |
| 428 | c := c |
| 429 | rt, routeErr := reg.Get(host) |
| 430 | if err := c.checkUnitShape(context.Background(), host, rt); err != nil { |
| 431 | return err |
| 432 | } |
| 433 | if rt != nil && (rt.Unit == "" || (c.legacyStart != nil && rt.Hibernate)) { |
| 434 | return nil |
| 435 | } |
| 436 | if rt != nil && rt.Unit == activator.UnitName(host) { |
| 437 | if c.legacyStart != nil { |
| 438 | return c.legacyStart(rt.Unit) |
| 439 | } |
| 440 | return legacyRollbackRestart(sys, optDir, getenv, *rt, c) |
| 441 | } |
| 442 | legacy, err := c.allowLegacy() |
| 443 | if err != nil { |
| 444 | return err |
| 445 | } |
| 446 | if legacy { |
| 447 | c.noteLegacy() |
| 448 | if routeErr != nil || rt == nil { |
| 449 | return nil |
| 450 | } |
| 451 | if c.legacyStart != nil { |
| 452 | return c.legacyStart(rt.Unit) |
| 453 | } |
| 454 | return legacyRollbackRestart(sys, optDir, getenv, *rt, c) |
| 455 | } |
| 456 | if c.attempts == nil || c.rollback == nil { |
| 457 | return os.ErrPermission |
| 458 | } |
| 459 | s, err := c.attempts() |
| 460 | if err != nil { |
| 461 | return err |
| 462 | } |
| 463 | defer s.Close() |
| 464 | unlock, err := s.Lock(context.Background(), rollbackHostKey(host)+".lock") |
| 465 | if err != nil { |
| 466 | return err |
| 467 | } |
| 468 | defer unlock() |
| 469 | attempt, binding, err := loadRollback(s, host, c.now()) |
| 470 | if attempt.ID != "" { |
| 471 | defer func() { |
| 472 | result = errors.Join(result, settleRollbackAttempt(s, attempt)) |
| 473 | }() |
| 474 | } |
| 475 | if err != nil { |
| 476 | if !errors.Is(err, os.ErrNotExist) && !attempt.Settled && (attempt.ID == "" || !c.now().After(attempt.Deadline.Add(rollbackMargin))) { |
| 477 | return err |
| 478 | } |
| 479 | raw, readErr := s.Read(rollbackHostKey(host) + ".previous") |
| 480 | if errors.Is(readErr, os.ErrNotExist) && c.host != nil { |
| 481 | binding, readErr = c.host(context.Background(), host) |
| 482 | } else if readErr == nil && json.Unmarshal(raw, &binding) != nil { |
| 483 | readErr = os.ErrPermission |
| 484 | } |
| 485 | if readErr != nil || binding.Route.Host != host { |
| 486 | return errors.Join(err, readErr) |
| 487 | } |
| 488 | before, showErr := sys.Show(binding.Route.Unit) |
| 489 | if showErr != nil || before.ActiveState != "failed" || before.MainPID != 0 { |
| 490 | return errors.Join(err, showErr) |
| 491 | } |
| 492 | } |
| 493 | c.targetDigest = binding.Digest |
| 494 | binding, err = c.rollback(context.Background(), binding) |
| 495 | if err != nil { |
| 496 | return err |
| 497 | } |
| 498 | before, err := sys.Show(binding.Route.Unit) |
| 499 | if err != nil { |
| 500 | return err |
| 501 | } |
| 502 | if before.ActiveState == "inactive" { |
| 503 | return errUnitStopped |
| 504 | } |
| 505 | if before.ActiveState != "failed" && before.MainPID != 0 && !attempt.Failed { |
| 506 | return os.ErrPermission |
| 507 | } |
| 508 | if attempt.Recovering && before.ActiveState == "active" && before.MainPID > 0 { |
| 509 | c.verifyOnly = true |
| 510 | } |
| 511 | attempt.Recovering = true |
| 512 | if err := writeRollbackState(s, rollbackJournalKey(host), attempt); err != nil { |
| 513 | return err |
| 514 | } |
| 515 | if err := restartBoundUnit(sys, optDir, getenv, binding, true, nil, c); err != nil { |
| 516 | return err |
| 517 | } |
| 518 | attempt.Verified = true |
| 519 | return c.settleHistory(context.Background(), attempt, true) |
| 520 | } |
| 521 | |
| 522 | } |
| 523 | |
| 524 | func rootStartRoute(reg *registry.Registry, optDir string, checks rootRestartChecks) func(string) error { |
| 525 | checks.legacyStart = systemctlStartUnit |
| 526 | return rollbackUnitRestart(reg, systemdUnits{}, optDir, os.Getenv, checks) |
| 527 | } |
| 528 | |
| 529 | func rootSelectedRestart(reg *registry.Registry, sys unitSystem, optDir string, getenv func(string) string, settled func(registry.Route) error, checks rootRestartChecks, rollback bool) hostwrite.Callback { |
| 530 | return func(ctx context.Context, selected hostwrite.Selection) error { |
| 531 | if selected.Work == nil { |
| 532 | return hostwrite.ErrUnavailable |
| 533 | } |
| 534 | c := checks |
| 535 | c.identity = selected.Identity |
| 536 | c.effect = func(fn func() error) error { return selected.Work.With(ctx, selected.Identity, fn) } |
| 537 | if rollback { |
| 538 | return rollbackUnitRestart(reg, sys, optDir, getenv, c)(selected.Host) |
| 539 | } |
| 540 | return rootRestartRouteWith(reg, sys, optDir, getenv, settled, c)(selected.Host) |
| 541 | } |
| 542 | } |
| 543 | |
| 544 | func rootRestartRouteWith(reg *registry.Registry, sys unitSystem, optDir string, getenv func(string) string, settled func(registry.Route) error, checks rootRestartChecks) func(string) error { |
| 545 | restart := checkedUnitRestart(sys, optDir, getenv, settled, checks) |
| 546 | return func(host string) error { |
| 547 | rt, err := reg.Get(host) |
| 548 | if shapeErr := checks.checkUnitShape(context.Background(), host, rt); shapeErr != nil { |
| 549 | return shapeErr |
| 550 | } |
| 551 | if err != nil || rt == nil || rt.Unit == "" { |
| 552 | return restartOrdinaryUnit(sys, host) |
| 553 | } |
| 554 | if rt.Unit == activator.UnitName(host) { |
| 555 | return legacyCheckedUnitRestart(sys, optDir, getenv, *rt, settled, checks) |
| 556 | } |
| 557 | legacy, err := checks.allowLegacy() |
| 558 | if err != nil { |
| 559 | return err |
| 560 | } |
| 561 | if legacy { |
| 562 | checks.noteLegacy() |
| 563 | return legacyCheckedUnitRestart(sys, optDir, getenv, *rt, settled, checks) |
| 564 | } |
| 565 | if checks.dispatch != nil { |
| 566 | need, err := checks.dispatch(context.Background(), *rt) |
| 567 | if err != nil { |
| 568 | return err |
| 569 | } |
| 570 | if !need { |
| 571 | return restartVerifiedOrdinaryUnit(sys, optDir, host, checks.targetFile) |
| 572 | } |
| 573 | } |
| 574 | err = restart(*rt) |
| 575 | if errors.Is(err, errUnitStopped) { |
| 576 | return nil |
| 577 | } |
| 578 | return err |
| 579 | } |
| 580 | } |
| 581 | |
| 582 | func restartOrdinaryUnit(sys unitSystem, host string) error { |
| 583 | if err := sys.TryRestart(activator.UnitName(host)); err != nil { |
| 584 | log.Print(err) |
| 585 | } |
| 586 | return nil |
| 587 | } |
| 588 | |
| 589 | func restartVerifiedOrdinaryUnit(sys unitSystem, optDir, host string, open func(string) (*os.File, error)) error { |
| 590 | unit := activator.UnitName(host) |
| 591 | p, err := sys.Show(unit) |
| 592 | if err != nil || p.Name != unit || p.ExecPath != release.LiveLink(optDir, host) || p.ActiveState != "active" || open == nil { |
| 593 | return nil |
| 594 | } |
| 595 | file, err := open(p.FragmentPath) |
| 596 | if err != nil { |
| 597 | return err |
| 598 | } |
| 599 | file.Close() |
| 600 | if err := sys.TryRestart(unit); err != nil { |
| 601 | log.Print(err) |
| 602 | } |
| 603 | return nil |
| 604 | } |
| 605 | |