@@ -78,8 +78,7 @@ func Exec(args []string) int {
7878 return execExitUsage
7979 }
8080
81- url := fmt .Sprintf ("%s/api/sessions/%s/run?wait_ms=%d" , opts .Relay , token , timeout .Milliseconds ())
82- body , status , err := postJSON (ctx , url , payload )
81+ body , status , err := runCommand (ctx , opts .Relay , token , payload , timeout )
8382 if err != nil {
8483 fmt .Fprintln (os .Stderr , "request failed:" , err )
8584 return 1
@@ -237,3 +236,124 @@ func viewTokenFrom(s string) string {
237236 }
238237 return strings .Trim (s , "/" )
239238}
239+
240+ // runCommand prefers the relay's one-shot endpoint and falls back to queueing
241+ // plus a result poll. A relay that predates /run answers 404, and a client that
242+ // gave up there would be useless against any relay not yet carrying it.
243+ func runCommand (ctx context.Context , relayBase , token string , payload map [string ]interface {}, timeout time.Duration ) ([]byte , int , error ) {
244+ runURL := fmt .Sprintf ("%s/api/sessions/%s/run?wait_ms=%d" , relayBase , token , timeout .Milliseconds ())
245+ body , status , err := postJSON (ctx , runURL , payload )
246+ if err != nil {
247+ return nil , 0 , err
248+ }
249+ if status != http .StatusNotFound {
250+ return body , status , nil
251+ }
252+
253+ // A 404 from an unknown session names the token; a 404 from a relay with no
254+ // /run route does not, and that is the case worth retrying.
255+ var probe map [string ]interface {}
256+ if json .Unmarshal (body , & probe ) == nil {
257+ if msg , _ := probe ["error" ].(string ); strings .Contains (msg , "view token" ) {
258+ return body , status , nil
259+ }
260+ }
261+ return queueAndAwait (ctx , relayBase , token , payload , timeout )
262+ }
263+
264+ func queueAndAwait (ctx context.Context , relayBase , token string , payload map [string ]interface {}, timeout time.Duration ) ([]byte , int , error ) {
265+ ackBody , ackStatus , err := postJSON (ctx , fmt .Sprintf ("%s/api/sessions/%s/cmd" , relayBase , token ), payload )
266+ if err != nil {
267+ return nil , 0 , err
268+ }
269+ if ackStatus != http .StatusOK {
270+ return ackBody , ackStatus , nil
271+ }
272+
273+ var ack struct {
274+ CommandID string `json:"command_id"`
275+ }
276+ if err := json .Unmarshal (ackBody , & ack ); err != nil || ack .CommandID == "" {
277+ return ackBody , ackStatus , nil
278+ }
279+
280+ kind , _ := payload ["kind" ].(string )
281+ deadline := time .Now ().Add (timeout )
282+ resultURL := fmt .Sprintf ("%s/api/sessions/%s/cmd/%s" , relayBase , token , ack .CommandID )
283+
284+ for time .Now ().Before (deadline ) {
285+ wait := time .Until (deadline )
286+ if wait > 25 * time .Second {
287+ wait = 25 * time .Second
288+ }
289+ req , err := http .NewRequestWithContext (ctx , http .MethodGet ,
290+ fmt .Sprintf ("%s?wait_ms=%d" , resultURL , wait .Milliseconds ()), nil )
291+ if err != nil {
292+ return nil , 0 , err
293+ }
294+ resp , err := http .DefaultClient .Do (req )
295+ if err != nil {
296+ return nil , 0 , err
297+ }
298+ raw , readErr := io .ReadAll (resp .Body )
299+ resp .Body .Close ()
300+ if readErr != nil {
301+ return nil , 0 , readErr
302+ }
303+
304+ var got map [string ]interface {}
305+ _ = json .Unmarshal (raw , & got )
306+
307+ switch resp .StatusCode {
308+ case http .StatusOK :
309+ // An older relay returns the raw terminal event; reshape it into
310+ // the same envelope /run would have produced.
311+ if inner , isMap := got ["payload" ].(map [string ]interface {}); isMap {
312+ return reshapeResult (inner , kind , ack .CommandID ), http .StatusOK , nil
313+ }
314+ if got ["status" ] == "pending" {
315+ continue
316+ }
317+ return raw , http .StatusOK , nil
318+ case http .StatusGone :
319+ return raw , http .StatusGone , nil
320+ case http .StatusNotFound :
321+ continue
322+ default :
323+ return raw , resp .StatusCode , nil
324+ }
325+ }
326+
327+ pending , _ := json .Marshal (map [string ]interface {}{
328+ "command_id" : ack .CommandID ,
329+ "kind" : kind ,
330+ "status" : "pending" ,
331+ "ok" : false ,
332+ "result_url" : fmt .Sprintf ("/api/sessions/%s/cmd/%s" , token , ack .CommandID ),
333+ })
334+ return pending , http .StatusAccepted , nil
335+ }
336+
337+ func reshapeResult (inner map [string ]interface {}, kind , commandID string ) []byte {
338+ ok , _ := inner ["ok" ].(bool )
339+ out := map [string ]interface {}{
340+ "command_id" : commandID ,
341+ "kind" : kind ,
342+ "ok" : ok ,
343+ "status" : "error" ,
344+ "result" : inner ["result" ],
345+ "error" : inner ["error" ],
346+ "elapsed_ms" : inner ["elapsed_ms" ],
347+ }
348+ if ok {
349+ out ["status" ] = "ok"
350+ }
351+ if detail , present := inner ["detail" ]; present {
352+ out ["detail" ] = detail
353+ }
354+ enc , err := json .Marshal (out )
355+ if err != nil {
356+ return []byte (`{"ok":false,"error":"could not read the relay's result"}` )
357+ }
358+ return enc
359+ }
0 commit comments