diff --git a/core/grpcox.go b/core/grpcox.go index 04d414d..ee09591 100644 --- a/core/grpcox.go +++ b/core/grpcox.go @@ -16,7 +16,8 @@ import ( // GrpCox - main object type GrpCox struct { KeepAlive float64 - PlainText bool + + activeConn map[string]*Resource // TODO : utilize below args headers []string @@ -30,14 +31,28 @@ type GrpCox struct { isUnixSocket func() bool } +// InitGrpCox constructor +func InitGrpCox() *GrpCox { + return &GrpCox{ + activeConn: make(map[string]*Resource), + } +} + // GetResource - open resource to targeted grpc server -func (g *GrpCox) GetResource(ctx context.Context, target string) (*Resource, error) { +func (g *GrpCox) GetResource(ctx context.Context, target string, plainText, isRestartConn bool) (*Resource, error) { + if conn, ok := g.activeConn[target]; ok { + if !isRestartConn && conn.refClient != nil && conn.clientConn != nil { + return conn, nil + } + g.CloseActiveConns(target) + } + var err error r := new(Resource) h := append(g.headers, g.reflectHeaders...) md := grpcurl.MetadataFromHeaders(h) refCtx := metadata.NewOutgoingContext(ctx, md) - r.clientConn, err = g.dial(ctx, target) + r.clientConn, err = g.dial(ctx, target, plainText) if err != nil { return nil, err } @@ -46,10 +61,40 @@ func (g *GrpCox) GetResource(ctx context.Context, target string) (*Resource, err r.descSource = grpcurl.DescriptorSourceFromServer(ctx, r.refClient) r.headers = h + g.activeConn[target] = r return r, nil } -func (g *GrpCox) dial(ctx context.Context, target string) (*grpc.ClientConn, error) { +// GetActiveConns - get all saved active connection +func (g *GrpCox) GetActiveConns(ctx context.Context) []string { + result := make([]string, len(g.activeConn)) + i := 0 + for k := range g.activeConn { + result[i] = k + i++ + } + return result +} + +// CloseActiveConns - close conn by host or all +func (g *GrpCox) CloseActiveConns(host string) error { + if host == "all" { + for k, v := range g.activeConn { + v.Close() + delete(g.activeConn, k) + } + return nil + } + + if v, ok := g.activeConn[host]; ok { + v.Close() + delete(g.activeConn, host) + } + + return nil +} + +func (g *GrpCox) dial(ctx context.Context, target string, plainText bool) (*grpc.ClientConn, error) { dialTime := 10 * time.Second ctx, cancel := context.WithTimeout(ctx, dialTime) defer cancel() @@ -69,7 +114,7 @@ func (g *GrpCox) dial(ctx context.Context, target string) (*grpc.ClientConn, err } var creds credentials.TransportCredentials - if !g.PlainText { + if !plainText { var err error creds, err = grpcurl.ClientTransportCredentials(g.insecure, g.cacert, g.cert, g.key) if err != nil { diff --git a/core/resource.go b/core/resource.go index 1046959..ca01ffb 100644 --- a/core/resource.go +++ b/core/resource.go @@ -119,7 +119,7 @@ func (r *Resource) Describe(symbol string) (string, string, error) { // Invoke - invoking gRPC function func (r *Resource) Invoke(ctx context.Context, symbol string, in io.Reader) (string, time.Duration, error) { - // because of grpcurl directlu fmt.Printf on their invoke function + // because of grpcurl directly fmt.Printf on their invoke function // so we stub the Stdout using os.Pipe backUpStdout := os.Stdout defer func() { diff --git a/handler/handler.go b/handler/handler.go index cc15c01..4e281f5 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -6,12 +6,25 @@ import ( "fmt" "net/http" "strconv" + "strings" "github.com/gorilla/mux" "github.com/gusaul/grpcox/core" ) -func index(w http.ResponseWriter, r *http.Request) { +// Handler hold all handler methods +type Handler struct { + g *core.GrpCox +} + +// InitHandler Constructor +func InitHandler() *Handler { + return &Handler{ + g: core.InitGrpCox(), + } +} + +func (h *Handler) index(w http.ResponseWriter, r *http.Request) { body := new(bytes.Buffer) err := indexHTML.Execute(body, make(map[string]string)) if err != nil { @@ -23,7 +36,27 @@ func index(w http.ResponseWriter, r *http.Request) { w.Write(body.Bytes()) } -func getLists(w http.ResponseWriter, r *http.Request) { +func (h *Handler) getActiveConns(w http.ResponseWriter, r *http.Request) { + response(w, h.g.GetActiveConns(context.TODO())) +} + +func (h *Handler) closeActiveConns(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + host := vars["host"] + if host == "" { + writeError(w, fmt.Errorf("Invalid Host")) + return + } + + err := h.g.CloseActiveConns(strings.Trim(host, " ")) + if err != nil { + writeError(w, err) + return + } + response(w, map[string]bool{"success": true}) +} + +func (h *Handler) getLists(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) host := vars["host"] if host == "" { @@ -33,16 +66,14 @@ func getLists(w http.ResponseWriter, r *http.Request) { service := vars["serv_name"] - g := new(core.GrpCox) useTLS, _ := strconv.ParseBool(r.Header.Get("use_tls")) - g.PlainText = !useTLS + restart, _ := strconv.ParseBool(r.FormValue("restart")) - res, err := g.GetResource(context.Background(), host) + res, err := h.g.GetResource(context.Background(), host, !useTLS, restart) if err != nil { writeError(w, err) return } - defer res.Close() result, err := res.List(service) if err != nil { @@ -53,7 +84,7 @@ func getLists(w http.ResponseWriter, r *http.Request) { response(w, result) } -func describeFunction(w http.ResponseWriter, r *http.Request) { +func (h *Handler) describeFunction(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) host := vars["host"] if host == "" { @@ -67,16 +98,13 @@ func describeFunction(w http.ResponseWriter, r *http.Request) { return } - g := new(core.GrpCox) useTLS, _ := strconv.ParseBool(r.Header.Get("use_tls")) - g.PlainText = !useTLS - res, err := g.GetResource(context.Background(), host) + res, err := h.g.GetResource(context.Background(), host, !useTLS, false) if err != nil { writeError(w, err) return } - defer res.Close() // get param result, _, err := res.Describe(funcName) @@ -109,7 +137,7 @@ func describeFunction(w http.ResponseWriter, r *http.Request) { } -func invokeFunction(w http.ResponseWriter, r *http.Request) { +func (h *Handler) invokeFunction(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) host := vars["host"] if host == "" { @@ -123,16 +151,13 @@ func invokeFunction(w http.ResponseWriter, r *http.Request) { return } - g := new(core.GrpCox) useTLS, _ := strconv.ParseBool(r.Header.Get("use_tls")) - g.PlainText = !useTLS - res, err := g.GetResource(context.Background(), host) + res, err := h.g.GetResource(context.Background(), host, !useTLS, false) if err != nil { writeError(w, err) return } - defer res.Close() // get param result, timer, err := res.Invoke(context.Background(), funcName, r.Body) diff --git a/handler/routes.go b/handler/routes.go index 5c8979b..e05a89f 100644 --- a/handler/routes.go +++ b/handler/routes.go @@ -8,18 +8,26 @@ import ( // Init - routes initialization func Init(router *mux.Router) { - router.HandleFunc("/", index) + h := InitHandler() + + router.HandleFunc("/", h.index) ajaxRoute := router.PathPrefix("/server/{host}").Subrouter() - ajaxRoute.HandleFunc("/services", corsHandler(getLists)).Methods(http.MethodGet, http.MethodOptions) - ajaxRoute.HandleFunc("/service/{serv_name}/functions", corsHandler(getLists)).Methods(http.MethodGet, http.MethodOptions) - ajaxRoute.HandleFunc("/function/{func_name}/describe", corsHandler(describeFunction)).Methods(http.MethodGet, http.MethodOptions) - ajaxRoute.HandleFunc("/function/{func_name}/invoke", corsHandler(invokeFunction)).Methods(http.MethodPost, http.MethodOptions) + ajaxRoute.HandleFunc("/services", corsHandler(h.getLists)).Methods(http.MethodGet, http.MethodOptions) + ajaxRoute.HandleFunc("/service/{serv_name}/functions", corsHandler(h.getLists)).Methods(http.MethodGet, http.MethodOptions) + ajaxRoute.HandleFunc("/function/{func_name}/describe", corsHandler(h.describeFunction)).Methods(http.MethodGet, http.MethodOptions) + ajaxRoute.HandleFunc("/function/{func_name}/invoke", corsHandler(h.invokeFunction)).Methods(http.MethodPost, http.MethodOptions) + + // get list of active connection + router.HandleFunc("/active/get", corsHandler(h.getActiveConns)).Methods(http.MethodGet, http.MethodOptions) + // close active connection + router.HandleFunc("/active/close/{host}", corsHandler(h.closeActiveConns)).Methods(http.MethodDelete, http.MethodOptions) assetsPath := "index" router.PathPrefix("/css/").Handler(http.StripPrefix("/css/", http.FileServer(http.Dir(assetsPath+"/css/")))) router.PathPrefix("/js/").Handler(http.StripPrefix("/js/", http.FileServer(http.Dir(assetsPath+"/js/")))) router.PathPrefix("/font/").Handler(http.StripPrefix("/font/", http.FileServer(http.Dir(assetsPath+"/font/")))) + router.PathPrefix("/img/").Handler(http.StripPrefix("/img/", http.FileServer(http.Dir(assetsPath+"/img/")))) } func corsHandler(h http.HandlerFunc) http.HandlerFunc { diff --git a/index/css/style.css b/index/css/style.css index 76bd808..ae61674 100644 --- a/index/css/style.css +++ b/index/css/style.css @@ -53,6 +53,101 @@ body { } } +/* connections */ +.connections { + font-weight: 100; + background: #efefef; + width: 240px; + height: 320px; + position: fixed; + top: 200px; + z-index: 100; + -webkit-box-shadow: -3px 0px 5px 0px rgba(0,0,0,0.2); + box-shadow: -3px 0px 5px 0px rgba(0,0,0,0.2); + left: -190px; + transition: all .3s; + -webkit-transition: all .3s; + color: #222; +} + +.connections:hover, .connections:focus { + transform: translate3d(190px, 0, 0); + animation-timing-function: 1s ease-in +} + +.connections .title { + top: 50%; + position: absolute; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); + transform: rotate(270deg); + right: -50px; + font-weight: 800; + font-size: 15px +} + +.connections .nav { + position: absolute; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); + font-weight: 100; + overflow-y: scroll; + padding-left: 10px; + padding-right: 30px; + top: 160px; + height: 300px; +} + +.connections .nav li { + padding-bottom: 12px; + list-style-type: none +} + +.connections .nav li i { + color: #a20000; + cursor: pointer; +} + +.connections .nav li a:hover { color: #aaa } + +.dots { + position: absolute; + left: -65px; + top: -39px; + color: #b70000; +} + +circle { + stroke: #ce2828; + fill: #a20000; + stroke-width: 2px; + stroke-opacity: 1; +} + +.pulse { + fill: white; + fill-opacity: 0; + transform-origin: 50% 50%; + animation-duration: 2s; + animation-name: pulse; + animation-iteration-count: infinite; +} + +@keyframes pulse { + from { + stroke-width: 3px; + stroke-opacity: 1; + transform: scale(0.3); + } + to { + stroke-width: 0; + stroke-opacity: 0; + transform: scale(2); + } +} + /* loading spinner */ .spinner { margin: 10px auto; diff --git a/index/img/favicon.png b/index/img/favicon.png new file mode 100644 index 0000000..a50da68 Binary files /dev/null and b/index/img/favicon.png differ diff --git a/index/index.html b/index/index.html index b0d07b5..d874799 100644 --- a/index/index.html +++ b/index/index.html @@ -5,6 +5,7 @@