返回 DeepSeek-Reasonix
serve_frontend.go
根目录 / internal / cli / serve_frontend.go
1 package cli
2
3 import (
4 "context"
5 "fmt"
6 "io"
7 "net"
8 "os"
9 "os/signal"
10 "strings"
11 "syscall"
12
13 "reasonix/internal/agent"
14 "reasonix/internal/config"
15 "reasonix/internal/control"
16 "reasonix/internal/i18n"
17 "reasonix/internal/serve"
18 )
19
20 // runServe exposes the controller's HTTP and SSE frontend.
21 func runServe(args []string) int {
22 return runServeWithOptions(args, serveRunOptions{command: "serve"})
23 }
24
25 // runWeb adds local browser lifecycle behavior to the Serve frontend.
26 func runWeb(args []string) int {
27 return runServeWithOptions(args, serveRunOptions{command: "web", openBrowser: true})
28 }
29
30 type serveRunOptions struct {
31 command string
32 openBrowser bool
33 }
34
35 type serveFrontendOptions struct {
36 command string
37 address string
38 portFile string
39 tokenFile string
40 pidFile string
41 openBrowser bool
42 hasSession bool
43 // launchTokenPath names where the launch token is, so the
44 // terminal shows a path rather than the secret.
45 launchTokenPath string
46 }
47
48 type serveFrontendResources struct {
49 listener net.Listener
50 displayAddr string
51 artifacts []string
52 registration *webInstanceRegistration
53 }
54
55 func prepareServeFrontend(opts serveFrontendOptions) (_ *serveFrontendResources, err error) {
56 resources := &serveFrontendResources{displayAddr: opts.address}
57 defer func() {
58 if err != nil {
59 resources.release(true)
60 }
61 }()
62
63 if opts.portFile != "" || opts.command == "web" || opts.openBrowser {
64 if opts.command == "web" {
65 resources.listener, err = listenWebWithPortRetry(opts.address)
66 } else {
67 resources.listener, err = net.Listen("tcp", opts.address)
68 }
69 if err != nil {
70 return nil, err
71 }
72 resources.displayAddr = resources.listener.Addr().String()
73 requested, selected := requestedPort(opts.address), requestedPort(resources.displayAddr)
74 if opts.command == "web" && requested != 0 && requested != selected {
75 fmt.Fprintf(os.Stderr, " port %d is in use; using %d instead\n", requested, selected)
76 }
77 if opts.portFile != "" {
78 if err = writeServeAddrFile(opts.portFile, resources.displayAddr); err != nil {
79 return nil, err
80 }
81 resources.artifacts = append(resources.artifacts, opts.portFile)
82 }
83 }
84 if opts.pidFile != "" {
85 if err = writeServePidFile(opts.pidFile); err != nil {
86 return nil, err
87 }
88 resources.artifacts = append(resources.artifacts, opts.pidFile)
89 }
90 if opts.command == "web" {
91 resources.registration, err = registerWebInstance(config.ReasonixHomeDir(), resources.displayAddr)
92 if err != nil {
93 return nil, err
94 }
95 }
96 return resources, nil
97 }
98
99 func (r *serveFrontendResources) release(closeListener bool) {
100 if closeListener && r.listener != nil {
101 _ = r.listener.Close()
102 }
103 if r.registration != nil {
104 r.registration.Release()
105 }
106 for _, path := range r.artifacts {
107 _ = os.Remove(path)
108 }
109 }
110
111 func runServeFrontend(ctrl *control.Controller, srv *serve.Server, cfg config.ServeConfig, opts serveFrontendOptions) int {
112 resources, err := prepareServeFrontend(opts)
113 if err != nil {
114 fmt.Fprintln(os.Stderr, i18n.M.ErrorPrefix, err)
115 return 1
116 }
117 defer resources.release(false)
118 srv.EnableProviderSetupForListener(resources.displayAddr)
119 if srv.AuthMode() == "none" || srv.AuthMode() == "token" {
120 if opts.launchTokenPath, err = launchTokenLocation(srv.AuthToken(), opts, resources); err != nil {
121 fmt.Fprintln(os.Stderr, i18n.M.ErrorPrefix, err)
122 return 1
123 }
124 }
125 reportServeFrontend(ctrl, srv, cfg, resources.displayAddr, opts)
126 startServeBalanceDiagnostics(ctrl)
127 return serveFrontendLoop(ctrl, srv, resources, opts)
128 }
129
130 func reportServeFrontend(ctrl *control.Controller, srv *serve.Server, cfg config.ServeConfig, address string, opts serveFrontendOptions) {
131 fmt.Printf("reasonix %s — %s on http://%s\n", opts.command, ctrl.Label(), address)
132 reportServeAuth(os.Stdout, srv, address, opts)
133 if warning := serve.PlainHTTPAuthWarning(cfg, address); warning != "" {
134 fmt.Fprintf(os.Stderr, " %s\n", warning)
135 }
136 }
137
138 // reportServeAuth names the file holding the launch token, never the token: a
139 // sandboxed command can read back a terminal multiplexer's scrollback.
140 func reportServeAuth(w io.Writer, srv *serve.Server, address string, opts serveFrontendOptions) {
141 switch srv.AuthMode() {
142 case "token":
143 fmt.Fprintln(w, " auth: token")
144 fmt.Fprintf(w, " share: http://%s/#token=<token> (token in %s)\n", address, opts.launchTokenPath)
145 case "password":
146 fmt.Fprintf(w, " auth: password (login at http://%s/login)\n", address)
147 case "none":
148 fmt.Fprintln(w, " auth: none (changes and approvals still require the launch token)")
149 fmt.Fprintf(w, " approvals: http://%s/#token=<token> (token in %s)\n", address, opts.launchTokenPath)
150 }
151 }
152
153 func startServeBalanceDiagnostics(ctrl *control.Controller) {
154 go func() {
155 if balance, err := ctrl.Balance(context.Background()); err != nil {
156 fmt.Fprintf(os.Stderr, " balance: error — %v\n", err)
157 } else if balance == nil {
158 fmt.Fprintln(os.Stderr, " balance: not configured (no balance_url for this provider)")
159 } else {
160 fmt.Printf(" balance: %s\n", balance.Display())
161 }
162 }()
163 }
164
165 func serveFrontendLoop(ctrl *control.Controller, srv *serve.Server, resources *serveFrontendResources, opts serveFrontendOptions) int {
166 ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
167 defer stop()
168 if resources.listener == nil {
169 if err := srv.RunGraceful(ctx, opts.address); err != nil {
170 fmt.Fprintln(os.Stderr, i18n.M.ErrorPrefix, err)
171 return 1
172 }
173 return 0
174 }
175 var serveErr error
176 if opts.openBrowser {
177 sessionID := ""
178 if opts.hasSession {
179 sessionID = agent.BranchID(ctrl.SessionPath())
180 }
181 serveErr = runServeListenerAfterReady(ctx, srv, resources.listener, resources.displayAddr, func() {
182 browserURL, err := launchWebBrowser(srv, resources.displayAddr, sessionID)
183 if err != nil {
184 fmt.Fprintf(os.Stderr, " browser: could not open %s — %v\n", withoutFragment(browserURL), err)
185 } else {
186 fmt.Printf(" browser: %s\n", withoutFragment(browserURL))
187 }
188 })
189 } else {
190 serveErr = srv.RunGracefulListener(ctx, resources.listener)
191 }
192 if serveErr != nil {
193 fmt.Fprintln(os.Stderr, i18n.M.ErrorPrefix, serveErr)
194 return 1
195 }
196 return 0
197 }
198
199 // withoutFragment drops a #token= fragment before a URL reaches the terminal.
200 func withoutFragment(raw string) string {
201 before, _, _ := strings.Cut(raw, "#")
202 return before
203 }
204
204 lines GO