| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | |
| 6 | "reasonix/internal/boot" |
| 7 | "reasonix/internal/control" |
| 8 | "reasonix/internal/session" |
| 9 | ) |
| 10 | |
| 11 | func desktopLegacyImportOptions(workspaceRoot string) session.CreateOptions { |
| 12 | cwd := canonicalRuntimeRoot(workspaceRoot) |
| 13 | if cwd == "" { |
| 14 | cwd = globalWorkspaceRoot() |
| 15 | } |
| 16 | return session.CreateOptions{ |
| 17 | CWD: cwd, Origin: session.SessionOriginLegacyImport, |
| 18 | } |
| 19 | } |
| 20 | |
| 21 | // sessionBinding is intentionally smaller than the public desktop control |
| 22 | // surface. It lets rebuild code preserve the exact host-owned Runtime without |
| 23 | // making legacy test controllers implement the final identity API. |
| 24 | type persistedSessionBinding interface { |
| 25 | SessionBinding() (*session.Service, *session.Runtime, bool) |
| 26 | } |
| 27 | |
| 28 | func exclusiveSessionBinding(ctrl control.SessionAPI) (*session.Service, *session.Runtime, bool) { |
| 29 | bound, ok := ctrl.(persistedSessionBinding) |
| 30 | if !ok || bound == nil { |
| 31 | return nil, nil, false |
| 32 | } |
| 33 | service, runtime, exclusive := bound.SessionBinding() |
| 34 | return service, runtime, exclusive && service != nil && runtime != nil |
| 35 | } |
| 36 | |
| 37 | // buildDesktopControllerReplacement uses boot.Rebuild for an already-bound |
| 38 | // final-format session. That path keeps the SessionRuntime and its writer while |
| 39 | // replacing only the Agent/configuration graph. Legacy controllers retain the |
| 40 | // old build-and-resume path until they cross the explicit migration boundary. |
| 41 | func buildDesktopControllerReplacement(ctx context.Context, old control.SessionAPI, opts boot.Options) (control.SessionAPI, bool, error) { |
| 42 | concrete, ok := old.(*control.Controller) |
| 43 | if !ok || concrete == nil { |
| 44 | ctrl, err := boot.Build(ctx, opts) |
| 45 | return ctrl, false, err |
| 46 | } |
| 47 | if opts.SessionService == nil { |
| 48 | ctrl, err := boot.Build(ctx, opts) |
| 49 | return ctrl, false, err |
| 50 | } |
| 51 | if opts.SessionTemp == nil { |
| 52 | opts.SessionTemp = concrete.SessionTemp() |
| 53 | } |
| 54 | if _, _, bound := exclusiveSessionBinding(old); !bound { |
| 55 | opts.SessionCreateOptions = desktopLegacyImportOptions(opts.WorkspaceRoot) |
| 56 | } |
| 57 | result, err := boot.Rebuild(ctx, concrete, opts) |
| 58 | if err != nil { |
| 59 | return nil, true, err |
| 60 | } |
| 61 | return result.Controller, true, nil |
| 62 | } |
| 63 | |
| 64 | // retireReplacedController must not close a SessionRuntime still used by its |
| 65 | // replacement. ReleaseResources tears down only the retired Agent generation. |
| 66 | func retireReplacedController(old, replacement control.SessionAPI) { |
| 67 | if old == nil || old == replacement { |
| 68 | return |
| 69 | } |
| 70 | _, oldRuntime, oldExclusive := exclusiveSessionBinding(old) |
| 71 | _, newRuntime, newExclusive := exclusiveSessionBinding(replacement) |
| 72 | if oldExclusive && newExclusive && oldRuntime == newRuntime { |
| 73 | if concrete, ok := replacement.(*control.Controller); ok { |
| 74 | concrete.ActivateGoalDriverAfterRebuild() |
| 75 | } |
| 76 | if concrete, ok := old.(*control.Controller); ok { |
| 77 | concrete.ReleaseResources() |
| 78 | return |
| 79 | } |
| 80 | } |
| 81 | old.Close() |
| 82 | } |
| 83 | |
| 84 | // activateReplacementController is called only inside the host's final |
| 85 | // compare-and-publish critical section. It transfers an exclusive Session |
| 86 | // Runtime without letting construction-time candidates steal Stop or commit |
| 87 | // authority from the controller still visible in the tab. |
| 88 | func activateReplacementController(old, replacement control.SessionAPI) error { |
| 89 | return control.ActivateSessionAPIReplacement(old, replacement) |
| 90 | } |
| 91 | |
| 92 | func discardReplacementController(candidate, current control.SessionAPI) { |
| 93 | if candidate == nil || candidate == current { |
| 94 | return |
| 95 | } |
| 96 | _, candidateRuntime, candidateExclusive := exclusiveSessionBinding(candidate) |
| 97 | _, currentRuntime, currentExclusive := exclusiveSessionBinding(current) |
| 98 | if candidateExclusive && currentExclusive && candidateRuntime == currentRuntime { |
| 99 | if concrete, ok := candidate.(*control.Controller); ok { |
| 100 | concrete.ReleaseResources() |
| 101 | return |
| 102 | } |
| 103 | } |
| 104 | candidate.Close() |
| 105 | } |
| 106 |