diff options
| -rw-r--r-- | http_server.go | 50 | ||||
| -rw-r--r-- | http_server_test.go | 149 | ||||
| -rw-r--r-- | testdata/http_server/execute/cancel.aww | 1 | ||||
| -rw-r--r-- | testdata/http_server/execute/cancel_test.data | 55 |
4 files changed, 255 insertions, 0 deletions
diff --git a/http_server.go b/http_server.go index c45cde2..2a01332 100644 --- a/http_server.go +++ b/http_server.go @@ -192,6 +192,19 @@ func (httpd *httpServer) registerEndpoints() (err error) { return fmt.Errorf("%s: %w", logp, err) } + // Register endpoint to cancel execution. + + err = httpd.RegisterEndpoint(&libhttp.Endpoint{ + Method: libhttp.RequestMethodDelete, + Path: pathAwwanAPIExecute, + RequestType: libhttp.RequestTypeJSON, + ResponseType: libhttp.ResponseTypeJSON, + Call: httpd.ExecuteCancel, + }) + if err != nil { + return fmt.Errorf(`%s: %w`, logp, err) + } + // Register Server-sent events to tail the execution state and // output. @@ -728,6 +741,43 @@ func (httpd *httpServer) Execute(epr *libhttp.EndpointRequest) (resb []byte, err return resb, nil } +// ExecuteCancel cancel execution by its ID. +// +// Request format, +// +// DELETE /awwan/api/execute?id=<string> +// +// If the ID is exist, the execution will be cancelled and return HTTP +// status 200 with the following body, +// +// Content-Type: application/json +// { +// "code": 200, +// } +// +// Otherwise it will return HTTP status 404 with error message. +func (httpd *httpServer) ExecuteCancel(epr *libhttp.EndpointRequest) (resb []byte, err error) { + var ( + endRes = &libhttp.EndpointResponse{} + execID = epr.HttpRequest.Form.Get(paramNameID) + ctxDoCancel = httpd.idContextCancel[execID] + ) + + if ctxDoCancel == nil { + endRes.Code = http.StatusNotFound + endRes.Message = fmt.Sprintf(`execution ID not found: %q`, execID) + return nil, endRes + } + + ctxDoCancel() + + endRes.Code = http.StatusOK + endRes.Message = fmt.Sprintf(`execution ID %q has been cancelled`, execID) + + resb, err = json.Marshal(endRes) + return resb, err +} + // ExecuteTail fetch the latest output of execution using Server-sent // events. // diff --git a/http_server_test.go b/http_server_test.go index 9a7603f..7ed09d3 100644 --- a/http_server_test.go +++ b/http_server_test.go @@ -13,6 +13,7 @@ import ( "math/big" "net/http" "net/http/httptest" + "net/url" "testing" "time" @@ -324,6 +325,154 @@ func TestHttpServer_Execute(t *testing.T) { test.Assert(t, `Execute tail`, string(tdata.Output[`local:/local.aww:1-:tail`]), buf.String()) } +func TestHttpServer_ExecuteCancel(t *testing.T) { + var ( + baseDir = `testdata/http_server/execute` + + tdata *test.Data + err error + ) + + tdata, err = test.LoadData(baseDir + `/cancel_test.data`) + if err != nil { + t.Fatal(err) + } + + var aww *Awwan + + aww, err = New(baseDir) + if err != nil { + t.Fatal(err) + } + + var ( + address = testGenerateServerAddress() + isDev = false + ) + + go func() { + err = aww.Serve(address, isDev) + if err != nil { + log.Fatal(err) + } + }() + + err = libnet.WaitAlive(`tcp`, address, 10*time.Second) + if err != nil { + t.Fatal(err) + } + + var ( + clientOpts = libhttp.ClientOptions{ + ServerUrl: fmt.Sprintf(`http://%s`, address), + } + reqJSON = tdata.Input[`local:/cancel.aww:1-`] + + execRequest ExecRequest + cl *libhttp.Client + ) + + cl = libhttp.NewClient(&clientOpts) + + err = json.Unmarshal(reqJSON, &execRequest) + if err != nil { + t.Fatal(err) + } + + var ( + resBody []byte + buf bytes.Buffer + ) + + _, resBody, err = cl.PostJSON(pathAwwanAPIExecute, nil, &execRequest) + if err != nil { + t.Fatal(err) + } + + json.Indent(&buf, resBody, ``, ` `) + + var expResp = string(tdata.Output[`local:/cancel.aww:1-`]) + + test.Assert(t, `First response`, expResp, buf.String()) + + var ( + res libhttp.EndpointResponse + execResp ExecResponse + ) + + res.Data = &execResp + + err = json.Unmarshal(resBody, &res) + if err != nil { + t.Fatal(err) + } + + // Tail the execution output. + + var ssec = sseclient.Client{ + Endpoint: fmt.Sprintf(`http://%s%s?id=%s`, address, pathAwwanAPIExecuteTail, execResp.ID), + } + + err = ssec.Connect(nil) + if err != nil { + t.Fatal(err) + } + + var ( + timeWait = time.NewTimer(1 * time.Second) + ever = true + + ev sseclient.Event + ) + buf.Reset() + for ever { + select { + case ev = <-ssec.C: + if len(ev.Type) != 0 { + fmt.Fprintf(&buf, "event: %s\n", ev.Type) + } + if len(ev.Data) != 0 { + fmt.Fprintf(&buf, "data: %q\n", ev.Data) + } + if len(ev.ID) != 0 { + fmt.Fprintf(&buf, "id: %s\n", ev.ID) + } + buf.WriteByte('\n') + + if ev.ID == `1` { + testDoExecuteCancel(t, tdata, cl, execResp.ID) + } + + if ev.Type == "end" { + ever = false + break + } + case <-timeWait.C: + break + } + } + + test.Assert(t, `Execute cancel`, string(tdata.Output[`local:/cancel.aww:1-:tail`]), buf.String()) +} + +func testDoExecuteCancel(t *testing.T, tdata *test.Data, cl *libhttp.Client, execID string) { + var ( + params = url.Values{} + + resBody []byte + err error + ) + + params.Set(paramNameID, execID) + + _, resBody, err = cl.Delete(pathAwwanAPIExecute, nil, params) + if err != nil { + t.Fatal(err) + } + + test.Assert(t, `Cancel response`, string(tdata.Output[`local:/cancel.aww:1-:response`]), string(resBody)) +} + func TestHttpServer_FSGet(t *testing.T) { var ( tdata *test.Data diff --git a/testdata/http_server/execute/cancel.aww b/testdata/http_server/execute/cancel.aww new file mode 100644 index 0000000..46a443f --- /dev/null +++ b/testdata/http_server/execute/cancel.aww @@ -0,0 +1 @@ +sleep 300 diff --git a/testdata/http_server/execute/cancel_test.data b/testdata/http_server/execute/cancel_test.data new file mode 100644 index 0000000..4611da6 --- /dev/null +++ b/testdata/http_server/execute/cancel_test.data @@ -0,0 +1,55 @@ +Test cancelling Execute using HTTP API. +The SSE data is quoted to make the string viewable. + +>>> local:/cancel.aww:1- +{ + "mode": "local", + "script": "/cancel.aww", + "line_range": "1-" +} + +<<< local:/cancel.aww:1- +{ + "data": { + "mode": "local", + "script": "/cancel.aww", + "line_range": "1-", + "id": "local:/cancel.aww:1-:1701012060", + "begin_at": "2023-11-26T15:21:00Z", + "end_at": "", + "error": "", + "output": [] + }, + "code": 200 +} + +<<< local:/cancel.aww:1-:tail +event: open + +event: begin +data: "2023-11-26T15:21:00Z" + +event: message +data: "----/--/-- --:--:-- === BEGIN: local /cancel.aww 1-\n" +id: 0 + +event: message +data: "----/--/-- --:--:-- --> 1: sleep 300\n" +id: 1 + +event: message +data: "----/--/-- --:--:-- !!! ExecLocal: signal: killed\n" +id: 2 + +event: message +data: "Local: ExecLocal: signal: killed" +id: 2 + +event: end +data: "2023-11-26T15:21:00Z" +id: 2 + + + +<<< local:/cancel.aww:1-:response +{"message":"execution ID \"local:/cancel.aww:1-:1701012060\" has been cancelled","code":200} |
