diff --git a/starport/pkg/httpstatuschecker/httpstatuschecker.go b/starport/pkg/httpstatuschecker/httpstatuschecker.go new file mode 100644 index 0000000000..6cdb4e72e2 --- /dev/null +++ b/starport/pkg/httpstatuschecker/httpstatuschecker.go @@ -0,0 +1,56 @@ +// httpstatuschecker is a tool check health of http pages. +package httpstatuschecker + +import ( + "net/http" +) + +type checker struct { + c *http.Client + addr string + method string +} + +// Option used to customize checker. +type Option func(*checker) + +// Client configures http client. +func Client(c *http.Client) Option { + return func(cr *checker) { + cr.c = c + } +} + +// Client configures http method. +func Method(name string) Option { + return func(cr *checker) { + cr.method = name + } +} + +// Check checks if given http addr is alive by applying options. +func Check(addr string, options ...Option) (isAvailable bool, err error) { + cr := &checker{ + c: http.DefaultClient, + addr: addr, + method: http.MethodGet, + } + for _, o := range options { + o(cr) + } + return cr.check() +} + +func (c *checker) check() (bool, error) { + req, err := http.NewRequest(c.method, c.addr, nil) + if err != nil { + return false, err + } + res, err := c.c.Do(req) + if err != nil { + return false, nil + } + defer res.Body.Close() + isOKStatus := res.StatusCode >= http.StatusOK && res.StatusCode < http.StatusMultipleChoices + return isOKStatus, nil +} diff --git a/starport/pkg/httpstatuschecker/httpstatuschecker_test.go b/starport/pkg/httpstatuschecker/httpstatuschecker_test.go new file mode 100644 index 0000000000..29254fe0bd --- /dev/null +++ b/starport/pkg/httpstatuschecker/httpstatuschecker_test.go @@ -0,0 +1,39 @@ +package httpstatuschecker + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCheckStatus(t *testing.T) { + cases := []struct { + name string + returnedStatus int + isAvaiable bool + }{ + {"200 OK", 200, true}, + {"202 Accepted ", 202, true}, + {"404 Not Found", 404, false}, + } + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tt.returnedStatus) + })) + defer ts.Close() + + isAvailable, err := Check(ts.URL) + require.NoError(t, err) + require.Equal(t, tt.isAvaiable, isAvailable) + }) + } +} + +func TestCheckServerUnreachable(t *testing.T) { + isAvailable, err := Check("http://localhost:63257") + require.NoError(t, err) + require.False(t, isAvailable) +} diff --git a/starport/pkg/xhttp/response.go b/starport/pkg/xhttp/response.go new file mode 100644 index 0000000000..d1c56a5a68 --- /dev/null +++ b/starport/pkg/xhttp/response.go @@ -0,0 +1,41 @@ +package xhttp + +import ( + "encoding/json" + "errors" + "net/http" +) + +// ResponseJSON writes a JSON response to w by using status as http status and data +// as payload. +func ResponseJSON(w http.ResponseWriter, status int, data interface{}) error { + bdata, err := json.Marshal(data) + if err != nil { + status = http.StatusInternalServerError + bdata, _ = json.Marshal(NewErrorResponse(errors.New(http.StatusText(status)))) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + w.Write(bdata) + return err +} + +// ErrorResponseBody is the skeleton for error messages that should be sent to +// client. +type ErrorResponseBody struct { + Error ErrorResponse `json:"error"` +} + +// ErrorResponse holds the error message. +type ErrorResponse struct { + Message string `json:"message"` +} + +// NewErrorResponse creates a new http error response from err. +func NewErrorResponse(err error) ErrorResponseBody { + return ErrorResponseBody{ + Error: ErrorResponse{ + Message: err.Error(), + }, + } +} diff --git a/starport/pkg/xhttp/response_test.go b/starport/pkg/xhttp/response_test.go new file mode 100644 index 0000000000..36c574bc57 --- /dev/null +++ b/starport/pkg/xhttp/response_test.go @@ -0,0 +1,34 @@ +package xhttp + +import ( + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestResponseJSON(t *testing.T) { + w := httptest.NewRecorder() + data := map[string]interface{}{"a": 1} + require.NoError(t, ResponseJSON(w, http.StatusCreated, data)) + resp := w.Result() + + require.Equal(t, http.StatusCreated, resp.StatusCode) + require.Equal(t, "application/json", resp.Header.Get("Content-Type")) + + body, _ := ioutil.ReadAll(resp.Body) + dataJSON, _ := json.Marshal(data) + require.Equal(t, dataJSON, body) +} + +func TestNewErrorResponse(t *testing.T) { + require.Equal(t, ErrorResponseBody{ + Error: ErrorResponse{ + Message: "error!", + }, + }, NewErrorResponse(errors.New("error!"))) +} diff --git a/starport/services/serve/dev.go b/starport/services/serve/dev.go index 01d497510f..9a880e4e4f 100644 --- a/starport/services/serve/dev.go +++ b/starport/services/serve/dev.go @@ -1,57 +1,106 @@ package starportserve import ( - "encoding/json" "net/http" "github.com/gobuffalo/packr/v2" "github.com/gorilla/mux" + "github.com/tendermint/starport/starport/pkg/httpstatuschecker" + "github.com/tendermint/starport/starport/pkg/xhttp" + "golang.org/x/sync/errgroup" ) -// newDevHandler creates a new development server handler. -func newDevHandler(app App) http.Handler { +const ( + appNodeInfoEndpoint = "/node_info" +) + +// serviceStatusResponse holds the status of development environment and http services +// needed for development. +type statusResponse struct { + Status serviceStatus `json:"status"` + Env env `json:"env"` +} + +// serviceStatus holds the availibity status of http services. +type serviceStatus struct { + IsConsensusEngineAlive bool `json:"is_consensus_engine_alive"` + IsMyAppBackendAlive bool `json:"is_my_app_backend_alive"` + IsMyAppFrontendAlive bool `json:"is_my_app_frontend_alive"` +} + +// env holds info about development environment. +type env struct { + ChainID string `json:"chain_id"` + NodeJS bool `json:"node_js"` +} + +// development handler builder. +type development struct { + app App + conf Config +} + +// Config used to configure development handler. +type Config struct { + EngineAddr string + AppBackendAddr string + AppFrontendAddr string + DevFrontendAssetsPath string +} + +// newDevHandler creates a new development server handler for app by given conf. +func newDevHandler(app App, conf Config) http.Handler { + dev := &development{app, conf} router := mux.NewRouter() - devUI := packr.New("ui/dist", "../../ui/dist") - router.HandleFunc("/env", func(w http.ResponseWriter, r *http.Request) { - env := Env{app.Name, isCommandAvailable("node")} - js, err := json.Marshal(env) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + router.Handle("/status", dev.statusHandler()).Methods(http.MethodGet) + router.PathPrefix("/").Handler(dev.devAssetsHandler()).Methods(http.MethodGet) + return router +} + +func (d *development) devAssetsHandler() http.Handler { + return http.FileServer(packr.New("ui/dist", d.conf.DevFrontendAssetsPath)) +} + +func (d *development) statusHandler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var ( + engineStatus, + appBackendStatus, + appFrontendStatus bool + ) + g := &errgroup.Group{} + g.Go(func() (err error) { + engineStatus, err = httpstatuschecker.Check(d.conf.EngineAddr) + return + }) + g.Go(func() (err error) { + appBackendStatus, err = httpstatuschecker.Check(d.conf.AppBackendAddr + appNodeInfoEndpoint) + return + }) + g.Go(func() (err error) { + appFrontendStatus, err = httpstatuschecker.Check(d.conf.AppFrontendAddr) + return + }) + if err := g.Wait(); err != nil { + xhttp.ResponseJSON(w, http.StatusInternalServerError, xhttp.NewErrorResponse(err)) return } - w.Header().Set("Content-Type", "application/json") - w.Write(js) - }) - router.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) { - res, err := http.Get("http://localhost:1317/node_info") - if err != nil || res.StatusCode != 200 { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("500 error")) - } else if res.StatusCode == 200 { - w.WriteHeader(http.StatusOK) - w.Write([]byte("200 ok")) - } - }) - router.HandleFunc("/rpc", func(w http.ResponseWriter, r *http.Request) { - res, err := http.Get("http://localhost:26657") - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - } else if res.StatusCode == 200 { - w.WriteHeader(http.StatusOK) - } else { - w.WriteHeader(http.StatusInternalServerError) - } - }) - router.HandleFunc("/frontend", func(w http.ResponseWriter, r *http.Request) { - res, err := http.Get("http://localhost:8080") - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - } else if res.StatusCode == 200 { - w.WriteHeader(http.StatusOK) - } else { - w.WriteHeader(http.StatusInternalServerError) + + resp := statusResponse{ + Env: d.env(), + Status: serviceStatus{ + IsConsensusEngineAlive: engineStatus, + IsMyAppBackendAlive: appBackendStatus, + IsMyAppFrontendAlive: appFrontendStatus, + }, } + xhttp.ResponseJSON(w, http.StatusOK, resp) }) - router.PathPrefix("/").Handler(http.FileServer(devUI)) - return router +} + +func (d *development) env() env { + return env{ + d.app.Name, + isCommandAvailable("node"), + } } diff --git a/starport/services/serve/serve.go b/starport/services/serve/serve.go index d251aa82ee..c94eb47cd7 100644 --- a/starport/services/serve/serve.go +++ b/starport/services/serve/serve.go @@ -59,12 +59,6 @@ func Serve(ctx context.Context, app App, verbose bool) error { return w.Start(time.Millisecond * 1000) } -// Env ... -type Env struct { - ChainID string `json:"chain_id"` - NodeJS bool `json:"node_js"` -} - func startServe(ctx context.Context, app App, verbose bool) error { var ( steps step.Steps @@ -191,7 +185,13 @@ func runDevServer(app App, verbose bool) error { } else { fmt.Printf("\nšŸš€ Get started: http://localhost:12345/\n\n") } - return http.ListenAndServe(":12345", newDevHandler(app)) + conf := Config{ + EngineAddr: "http://localhost:26657", + AppBackendAddr: "http://localhost:1317", + AppFrontendAddr: "http://localhost:8080", + DevFrontendAssetsPath: "../../ui/dist", + } // TODO get vals from const + return http.ListenAndServe(":12345", newDevHandler(app, conf)) } func isCommandAvailable(name string) bool { diff --git a/starport/ui/src/views/Start.vue b/starport/ui/src/views/Start.vue index 3eb369a310..4c6fadad01 100644 --- a/starport/ui/src/views/Start.vue +++ b/starport/ui/src/views/Start.vue @@ -227,34 +227,29 @@ export default { }; }, methods: { - async areServersRunning() { + async setStatusState() { try { - await axios.get("/api"); - this.running.api = true; + const { data } = await axios.get("/status"); + const { status, env } = data; + this.running = { + rpc: status.is_consensus_engine_alive, + api: status.is_my_app_backend_alive, + frontend: status.is_my_app_frontend_alive, + }; + this.env = env; } catch { - this.running.api = false; - } - try { - await axios.get("/rpc"); - this.running.rpc = true; - } catch { - this.running.rpc = false; - } - try { - await axios.get("/frontend"); - this.running.frontend = true; - } catch { - this.running.frontend = false; + this.running = { + rpc: false, + api: false, + frontend: false, + }; } } }, async created() { - this.timer = setInterval(async () => { - this.areServersRunning(); - }, 1000); - axios.get("/api/node_info"); + this.timer = setInterval(this.setStatusState.bind(this), 1000); try { - this.env = (await axios.get("/env")).data; + await this.setStatusState(); } catch { console.log("Can't fetch /env"); }