@@ -3,7 +3,6 @@ package cli
3
3
import (
4
4
"bufio"
5
5
"context"
6
- "encoding/json"
7
6
"fmt"
8
7
"io"
9
8
"net"
@@ -30,9 +29,8 @@ func (r *RootCmd) stdioHTTPCommand() *serpent.Command {
30
29
Use :"stdio-http <command> [args...]" ,
31
30
Short :"Run command and expose stdin/stdout/stderr over HTTP" ,
32
31
Long :`Start an HTTP server that runs a command and exposes its stdio streams:
33
- - POST requests to /stdin send data to the command's stdin
34
- - GET requests to /stdout stream the command's stdout as Server-Sent Events
35
- - GET requests to /stderr stream the command's stderr as Server-Sent Events` ,
32
+ - POST requests to / send data to the command's stdin
33
+ - GET requests to / receive Server-Sent Events with stdout and stderr output` ,
36
34
Handler :func (inv * serpent.Invocation )error {
37
35
if len (inv .Args )== 0 {
38
36
return xerrors .Errorf ("command is required" )
@@ -110,6 +108,8 @@ func handleStdioHTTP(inv *serpent.Invocation, cmdName string, cmdArgs []string,
110
108
}
111
109
112
110
// Start the command
111
+ cmdCtx ,cmdCancel := context .WithCancel (ctx )
112
+ defer cmdCancel ()
113
113
if err := server .startCommand (cmdName ,cmdArgs );err != nil {
114
114
return xerrors .Errorf ("failed to start command: %w" ,err )
115
115
}
@@ -123,9 +123,7 @@ func handleStdioHTTP(inv *serpent.Invocation, cmdName string, cmdArgs []string,
123
123
124
124
// Setup HTTP server
125
125
mux := http .NewServeMux ()
126
- mux .HandleFunc ("/stdin" ,server .handleStdin )
127
- mux .HandleFunc ("/stdout" ,server .handleStdout )
128
- mux .HandleFunc ("/stderr" ,server .handleStderr )
126
+ mux .HandleFunc ("/" ,server .handleRoot )
129
127
130
128
addr := net .JoinHostPort (host ,port )
131
129
httpServer := & http.Server {
@@ -136,9 +134,8 @@ func handleStdioHTTP(inv *serpent.Invocation, cmdName string, cmdArgs []string,
136
134
cliui .Infof (inv .Stderr ,"Starting HTTP server on http://%s" ,addr )
137
135
cliui .Infof (inv .Stderr ,"Command: %s %s" ,cmdName ,strings .Join (cmdArgs ," " ))
138
136
cliui .Infof (inv .Stderr ,"Endpoints:" )
139
- cliui .Infof (inv .Stderr ," POST /stdin - Send data to command stdin" )
140
- cliui .Infof (inv .Stderr ," GET /stdout - Stream command stdout (SSE)" )
141
- cliui .Infof (inv .Stderr ," GET /stderr - Stream command stderr (SSE)" )
137
+ cliui .Infof (inv .Stderr ," POST / - Send data to command stdin" )
138
+ cliui .Infof (inv .Stderr ," GET / - Stream command output (SSE with stdout/stderr events)" )
142
139
143
140
// Start HTTP server in goroutine
144
141
errCh := make (chan error ,1 )
@@ -166,16 +163,16 @@ func handleStdioHTTP(inv *serpent.Invocation, cmdName string, cmdArgs []string,
166
163
167
164
// Wait for command to finish
168
165
if server .cmd .Process != nil {
169
- if err := server .cmd .Wait ();err != nil {
170
- cliui . Warnf ( inv . Stderr , "Command finished with error: %v " ,err )
166
+ if err := server .cmd .( );err != nil {
167
+ return xerrors . Errorf ( "kill command error: %w " ,err )
171
168
}
172
169
}
173
170
174
171
return nil
175
172
}
176
173
177
- func (s * stdioHTTPServer )startCommand (cmdName string ,cmdArgs []string )error {
178
- s .cmd = exec .CommandContext (s . ctx ,cmdName ,cmdArgs ... )
174
+ func (s * stdioHTTPServer )startCommand (ctx context. Context , cmdName string ,cmdArgs []string )error {
175
+ s .cmd = exec .CommandContext (ctx ,cmdName ,cmdArgs ... )
179
176
180
177
var err error
181
178
s .stdin ,err = s .cmd .StdinPipe ()
@@ -285,94 +282,55 @@ func (s *stdioHTTPServer) distributeStderr() {
285
282
}
286
283
}
287
284
288
- func (s * stdioHTTPServer )handleStdin (w http.ResponseWriter ,r * http.Request ) {
289
- if r .Method != http .MethodPost {
290
- http .Error (w ,"Method not allowed" ,http .StatusMethodNotAllowed )
291
- return
292
- }
293
-
294
- body ,err := io .ReadAll (r .Body )
295
- if err != nil {
296
- http .Error (w ,"Failed to read body" ,http .StatusBadRequest )
297
- return
298
- }
299
-
300
- if s .stdin == nil {
301
- http .Error (w ,"Command stdin not available" ,http .StatusServiceUnavailable )
302
- return
303
- }
304
-
305
- _ ,err = s .stdin .Write (body )
306
- if err != nil {
307
- http .Error (w ,"Failed to write to command stdin" ,http .StatusInternalServerError )
308
- return
309
- }
310
-
311
- w .Header ().Set ("Content-Type" ,"application/json" )
312
- w .WriteHeader (http .StatusOK )
313
- json .NewEncoder (w ).Encode (map [string ]interface {}{
314
- "status" :"ok" ,
315
- "bytes_written" :len (body ),
316
- })
317
- }
318
-
319
- func (s * stdioHTTPServer )handleStdout (w http.ResponseWriter ,r * http.Request ) {
320
- if r .Method != http .MethodGet {
321
- http .Error (w ,"Method not allowed" ,http .StatusMethodNotAllowed )
322
- return
323
- }
324
-
325
- s .setupSSE (w )
326
-
327
- ch := make (chan []byte ,10 )
328
- s .mu .Lock ()
329
- s .stdoutSubscribers [ch ]= true
330
- s .mu .Unlock ()
331
-
332
- defer func () {
333
- s .mu .Lock ()
334
- delete (s .stdoutSubscribers ,ch )
335
- s .mu .Unlock ()
336
- close (ch )
337
- }()
338
-
339
- flusher ,ok := w .(http.Flusher )
340
- if ! ok {
341
- http .Error (w ,"Streaming not supported" ,http .StatusInternalServerError )
342
- return
343
- }
285
+ func (s * stdioHTTPServer )handleRoot (w http.ResponseWriter ,r * http.Request ) {
286
+ switch r .Method {
287
+ case http .MethodPost :
288
+ // Read stdin data first
289
+ body ,err := io .ReadAll (r .Body )
290
+ if err != nil {
291
+ http .Error (w ,"Failed to read body" ,http .StatusBadRequest )
292
+ return
293
+ }
344
294
345
- for {
346
- select {
347
- case data := <- ch :
348
- fmt .Fprintf (w ,"data: %s\n \n " ,string (data ))
349
- flusher .Flush ()
350
- case <- r .Context ().Done ():
295
+ if s .stdin == nil {
296
+ http .Error (w ,"Command stdin not available" ,http .StatusServiceUnavailable )
351
297
return
352
- case <- s .ctx .Done ():
298
+ }
299
+
300
+ // Write to stdin
301
+ _ ,err = s .stdin .Write (body )
302
+ if err != nil {
303
+ http .Error (w ,"Failed to write to command stdin" ,http .StatusInternalServerError )
353
304
return
354
305
}
355
- }
356
- }
357
306
358
- func (s * stdioHTTPServer )handleStderr (w http.ResponseWriter ,r * http.Request ) {
359
- if r .Method != http .MethodGet {
307
+ // Start streaming SSE
308
+ s .handleStream (w ,r )
309
+ case http .MethodGet :
310
+ s .handleStream (w ,r )
311
+ default :
360
312
http .Error (w ,"Method not allowed" ,http .StatusMethodNotAllowed )
361
- return
362
313
}
314
+ }
363
315
316
+ func (s * stdioHTTPServer )handleStream (w http.ResponseWriter ,r * http.Request ) {
364
317
s .setupSSE (w )
365
318
366
- ch := make (chan []byte ,10 )
319
+ stdoutCh := make (chan []byte ,10 )
320
+ stderrCh := make (chan []byte ,10 )
321
+
367
322
s .mu .Lock ()
368
- s .stderrSubscribers [ch ]= true
323
+ s .stdoutSubscribers [stdoutCh ]= true
324
+ s .stderrSubscribers [stderrCh ]= true
369
325
s .mu .Unlock ()
370
326
371
327
defer func () {
372
328
s .mu .Lock ()
373
- delete (s .stderrSubscribers ,ch )
329
+ delete (s .stdoutSubscribers ,stdoutCh )
330
+ delete (s .stderrSubscribers ,stderrCh )
374
331
s .mu .Unlock ()
375
- close (ch )
332
+ close (stdoutCh )
333
+ close (stderrCh )
376
334
}()
377
335
378
336
flusher ,ok := w .(http.Flusher )
@@ -383,8 +341,11 @@ func (s *stdioHTTPServer) handleStderr(w http.ResponseWriter, r *http.Request) {
383
341
384
342
for {
385
343
select {
386
- case data := <- ch :
387
- fmt .Fprintf (w ,"data: %s\n \n " ,string (data ))
344
+ case data := <- stdoutCh :
345
+ fmt .Fprintf (w ,"event: stdout\n data: %s\n \n " ,string (data ))
346
+ flusher .Flush ()
347
+ case data := <- stderrCh :
348
+ fmt .Fprintf (w ,"event: stderr\n data: %s\n \n " ,string (data ))
388
349
flusher .Flush ()
389
350
case <- r .Context ().Done ():
390
351
return