1package app
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "os"
8
9 "github.com/bnema/vev/internal/adapters/clock"
10 "github.com/bnema/vev/internal/adapters/ipc"
11 "github.com/bnema/vev/internal/adapters/term"
12 "github.com/bnema/vev/internal/adapters/uidriver"
13 "github.com/bnema/vev/internal/adapters/uiterm"
14 "github.com/bnema/vev/internal/logging"
15 "github.com/bnema/vev/internal/ports"
16 "github.com/bnema/vev/internal/protocol"
17 "github.com/bnema/vev/internal/usecase/client"
18)
19
20// runAttach dials (auto-spawning the daemon if needed) and runs the client
21// attach loop. Logging goes to the shared file: the client must never write
22// to the console while the terminal is raw.
23func runAttach(ctx context.Context, intent uint8, name, remoteTarget string) (retErr error) {
24 if os.Getenv("VEV") != "" {
25 if remoteTarget == "" && intent == protocol.IntentNew {
26 return createDetachedTerminalSession(ctx, name)
27 }
28 return errors.New("sessions should be nested with care; unset VEV to force")
29 }
30 ctx, stop := clientSignalContext(ctx)
31 defer stop()
32 log, logCloser, err := configureLogging(logging.Client, false)
33 if err != nil {
34 return err
35 }
36 defer func() { _ = logCloser.Close() }()
37 clk := clock.New()
38 observer, observerCloser, err := newPerformanceTrace(clk)
39 if err != nil {
40 return fmt.Errorf("performance trace: %w", err)
41 }
42 if observerCloser != nil {
43 defer func() { retErr = errors.Join(retErr, observerCloser.Close()) }()
44 }
45 if observer != nil {
46 observer.ObserveRuntime(ports.NewRuntimeMark("client", ports.RuntimeTransportDiagnostic, 0, true))
47 }
48 terminal := terminalForAttach()
49 intent, preconnected, err := resolveAttachCreationIntent(ctx, intent, name, remoteTarget, terminal)
50 if err != nil {
51 return err
52 }
53 navigation, resolver, err := terminalBrokerNavigation(intent, name, remoteTarget)
54 if err != nil {
55 if preconnected != nil {
56 _ = preconnected.Close()
57 }
58 return err
59 }
60 callbacks := terminalBrokerCallbacks()
61 return runBrokerClient(ctx, brokerClientConfig{
62 Logger: log, Connector: withPreconnected(newProductionBrokerConnector(), preconnected), Terminal: terminal, Clock: clk,
63 InitialNavigation: navigation, ResolveInitialNavigation: resolver,
64 AttachmentEnvironment: outerTerminalAttachmentEnvironment(log), SessionEnvironment: terminalSessionEnvironment(),
65 OnState: callbacks.OnState, OnLifecycle: callbacks.OnLifecycle, OnFailure: callbacks.OnFailure,
66 })
67}
68
69// runAttachWithOptions is the composition path for ordinary interactive
70// clients. Observation is opt-in; the default path remains term.New().
71//
72// The observed path composes the physical terminal, one VT mirror, the UI
73// service over that mirror, and the same broker connector and initial-navigation
74// translation the ordinary terminal attach uses, then delegates the whole run to
75// the shared runBrokerClient. It owns no daemon dialer, no remote carriage, and
76// no session: mirroring observes bytes at the serialized terminal writer and the
77// broker owns every transport.
78func runAttachWithOptions(ctx context.Context, intent uint8, name, remoteTarget string, options interactiveUIOptions) (retErr error) {
79 if !options.enabled() {
80 return runAttach(ctx, intent, name, remoteTarget)
81 }
82 if options.control {
83 options.observe = true
84 }
85 ctx, stop := clientSignalContext(ctx)
86 defer stop()
87 log, logCloser, err := configureLogging(logging.Client, false)
88 if err != nil {
89 return err
90 }
91 defer func() { _ = logCloser.Close() }()
92 clk := clock.New()
93 // The process trace is still created and joined here so an operator's trace
94 // file keeps its exact lifecycle, including a close failure joined into the
95 // run's result. Client-side transport spans are no longer emitted: the
96 // broker owns carriage, so the observed composition keeps no dialer seam.
97 _, observerCloser, err := newPerformanceTrace(clk)
98 if err != nil {
99 return fmt.Errorf("performance trace: %w", err)
100 }
101 if observerCloser != nil {
102 defer func() { retErr = errors.Join(retErr, observerCloser.Close()) }()
103 }
104 physical := term.NewWithFilesAndObservation(os.Stdin, os.Stdout, nil)
105 geometry, err := physical.Geometry()
106 if err != nil {
107 return fmt.Errorf("reading terminal geometry: %w", err)
108 }
109 mirror, err := uiterm.NewMirror(ctx, geometry, "")
110 if err != nil {
111 return fmt.Errorf("create UI mirror: %w", err)
112 }
113 defer mirror.Close()
114 physical = term.NewWithFilesAndObservation(os.Stdin, os.Stdout, mirror)
115 ui := client.NewUI(mirror, clk)
116 var endpoint *uidriver.UnixEndpoint
117 if options.observe {
118 server := uidriver.New(ui, clk)
119 socketPath := options.socket
120 if socketPath == "" {
121 socketPath = uidriver.DefaultSocketPath(ipc.SocketDir(), ui.Handle())
122 }
123 endpoint, err = uidriver.ListenUnix(socketPath, server, func() uidriver.Ready {
124 ready := uidriver.Ready{Attachment: ui.Handle(), Control: options.control, Status: ports.UIStatusPicker}
125 if snapshot, snapshotErr := ui.Capture(ui.Handle()); snapshotErr == nil {
126 ready = uidriverReady(ui, snapshot, options.control)
127 }
128 return ready
129 })
130 if err != nil {
131 return err
132 }
133 fmt.Fprintln(os.Stderr, socketPath)
134 defer func() { _ = endpoint.Close() }()
135 }
136 terminal := observedTerminal{Terminal: physical, UIOutputTransaction: mirror}
137 intent, preconnected, err := resolveAttachCreationIntent(ctx, intent, name, remoteTarget, terminal)
138 if err != nil {
139 return err
140 }
141 navigation, resolver, err := terminalBrokerNavigation(intent, name, remoteTarget)
142 if err != nil {
143 if preconnected != nil {
144 _ = preconnected.Close()
145 }
146 return err
147 }
148 sessionEnv := terminalSessionEnvironment()
149 attachmentEnv := configuredTerminalAttachmentEnvironment(log)
150 attachmentEnv.Cwd = sessionEnv.Cwd
151 return runBrokerClient(ctx, brokerClientConfig{
152 Logger: log,
153 Connector: withPreconnected(newProductionBrokerConnector(), preconnected),
154 Terminal: terminal,
155 Clock: clk,
156 UI: ui,
157 InitialNavigation: navigation,
158 ResolveInitialNavigation: resolver,
159 AttachmentEnvironment: attachmentEnv,
160 SessionEnvironment: sessionEnv,
161 })
162}