aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2023-12-22 02:57:21 +0700
committerShulhan <ms@kilabit.info>2023-12-22 03:08:04 +0700
commitaa64efe9e8957da6c122f9decaaa78dca83d14ee (patch)
treea6b60b025b369c21d9d4e4efce957c313be38c69
parentc0d336ca456093b2b7c0b585dbe08f62cbc8ca83 (diff)
downloadawwan-aa64efe9e8957da6c122f9decaaa78dca83d14ee.tar.xz
all: implement HTTP API to stop local or play execution
The HTTP API for stopping execution have the following signature, 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. References: https://todo.sr.ht/~shulhan/awwan/9
-rw-r--r--http_server.go50
-rw-r--r--http_server_test.go149
-rw-r--r--testdata/http_server/execute/cancel.aww1
-rw-r--r--testdata/http_server/execute/cancel_test.data55
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}