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