1
0
mirror of https://github.com/gusaul/grpcox.git synced 2024-12-26 10:50:11 +00:00

save and reuse active connections

This commit is contained in:
gusaul 2019-03-13 10:38:30 +07:00
parent 02a4feb17b
commit f44b1a7f78
8 changed files with 274 additions and 32 deletions

View File

@ -16,7 +16,8 @@ import (
// GrpCox - main object // GrpCox - main object
type GrpCox struct { type GrpCox struct {
KeepAlive float64 KeepAlive float64
PlainText bool
activeConn map[string]*Resource
// TODO : utilize below args // TODO : utilize below args
headers []string headers []string
@ -30,14 +31,28 @@ type GrpCox struct {
isUnixSocket func() bool isUnixSocket func() bool
} }
// InitGrpCox constructor
func InitGrpCox() *GrpCox {
return &GrpCox{
activeConn: make(map[string]*Resource),
}
}
// GetResource - open resource to targeted grpc server // 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 var err error
r := new(Resource) r := new(Resource)
h := append(g.headers, g.reflectHeaders...) h := append(g.headers, g.reflectHeaders...)
md := grpcurl.MetadataFromHeaders(h) md := grpcurl.MetadataFromHeaders(h)
refCtx := metadata.NewOutgoingContext(ctx, md) refCtx := metadata.NewOutgoingContext(ctx, md)
r.clientConn, err = g.dial(ctx, target) r.clientConn, err = g.dial(ctx, target, plainText)
if err != nil { if err != nil {
return nil, err 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.descSource = grpcurl.DescriptorSourceFromServer(ctx, r.refClient)
r.headers = h r.headers = h
g.activeConn[target] = r
return r, nil 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 dialTime := 10 * time.Second
ctx, cancel := context.WithTimeout(ctx, dialTime) ctx, cancel := context.WithTimeout(ctx, dialTime)
defer cancel() defer cancel()
@ -69,7 +114,7 @@ func (g *GrpCox) dial(ctx context.Context, target string) (*grpc.ClientConn, err
} }
var creds credentials.TransportCredentials var creds credentials.TransportCredentials
if !g.PlainText { if !plainText {
var err error var err error
creds, err = grpcurl.ClientTransportCredentials(g.insecure, g.cacert, g.cert, g.key) creds, err = grpcurl.ClientTransportCredentials(g.insecure, g.cacert, g.cert, g.key)
if err != nil { if err != nil {

View File

@ -119,7 +119,7 @@ func (r *Resource) Describe(symbol string) (string, string, error) {
// Invoke - invoking gRPC function // Invoke - invoking gRPC function
func (r *Resource) Invoke(ctx context.Context, symbol string, in io.Reader) (string, time.Duration, error) { 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 // so we stub the Stdout using os.Pipe
backUpStdout := os.Stdout backUpStdout := os.Stdout
defer func() { defer func() {

View File

@ -6,12 +6,25 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/gusaul/grpcox/core" "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) body := new(bytes.Buffer)
err := indexHTML.Execute(body, make(map[string]string)) err := indexHTML.Execute(body, make(map[string]string))
if err != nil { if err != nil {
@ -23,7 +36,27 @@ func index(w http.ResponseWriter, r *http.Request) {
w.Write(body.Bytes()) 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) vars := mux.Vars(r)
host := vars["host"] host := vars["host"]
if host == "" { if host == "" {
@ -33,16 +66,14 @@ func getLists(w http.ResponseWriter, r *http.Request) {
service := vars["serv_name"] service := vars["serv_name"]
g := new(core.GrpCox)
useTLS, _ := strconv.ParseBool(r.Header.Get("use_tls")) 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 { if err != nil {
writeError(w, err) writeError(w, err)
return return
} }
defer res.Close()
result, err := res.List(service) result, err := res.List(service)
if err != nil { if err != nil {
@ -53,7 +84,7 @@ func getLists(w http.ResponseWriter, r *http.Request) {
response(w, result) 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) vars := mux.Vars(r)
host := vars["host"] host := vars["host"]
if host == "" { if host == "" {
@ -67,16 +98,13 @@ func describeFunction(w http.ResponseWriter, r *http.Request) {
return return
} }
g := new(core.GrpCox)
useTLS, _ := strconv.ParseBool(r.Header.Get("use_tls")) 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 { if err != nil {
writeError(w, err) writeError(w, err)
return return
} }
defer res.Close()
// get param // get param
result, _, err := res.Describe(funcName) 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) vars := mux.Vars(r)
host := vars["host"] host := vars["host"]
if host == "" { if host == "" {
@ -123,16 +151,13 @@ func invokeFunction(w http.ResponseWriter, r *http.Request) {
return return
} }
g := new(core.GrpCox)
useTLS, _ := strconv.ParseBool(r.Header.Get("use_tls")) 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 { if err != nil {
writeError(w, err) writeError(w, err)
return return
} }
defer res.Close()
// get param // get param
result, timer, err := res.Invoke(context.Background(), funcName, r.Body) result, timer, err := res.Invoke(context.Background(), funcName, r.Body)

View File

@ -8,18 +8,26 @@ import (
// Init - routes initialization // Init - routes initialization
func Init(router *mux.Router) { func Init(router *mux.Router) {
router.HandleFunc("/", index) h := InitHandler()
router.HandleFunc("/", h.index)
ajaxRoute := router.PathPrefix("/server/{host}").Subrouter() ajaxRoute := router.PathPrefix("/server/{host}").Subrouter()
ajaxRoute.HandleFunc("/services", corsHandler(getLists)).Methods(http.MethodGet, http.MethodOptions) ajaxRoute.HandleFunc("/services", corsHandler(h.getLists)).Methods(http.MethodGet, http.MethodOptions)
ajaxRoute.HandleFunc("/service/{serv_name}/functions", corsHandler(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(describeFunction)).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(invokeFunction)).Methods(http.MethodPost, 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" assetsPath := "index"
router.PathPrefix("/css/").Handler(http.StripPrefix("/css/", http.FileServer(http.Dir(assetsPath+"/css/")))) 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("/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("/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 { func corsHandler(h http.HandlerFunc) http.HandlerFunc {

View File

@ -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 */ /* loading spinner */
.spinner { .spinner {
margin: 10px auto; margin: 10px auto;

BIN
index/img/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="x-ua-compatible" content="ie=edge"> <meta http-equiv="x-ua-compatible" content="ie=edge">
<title>gRPCox - gRPC Testing Environment</title> <title>gRPCox - gRPC Testing Environment</title>
<link rel="icon" href="/img/favicon.png" type="image/x-icon" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
<link href="css/bootstrap.min.css" rel="stylesheet"> <link href="css/bootstrap.min.css" rel="stylesheet">
<link href="css/mdb.min.css" rel="stylesheet"> <link href="css/mdb.min.css" rel="stylesheet">
@ -23,8 +24,8 @@
</div> </div>
</div> </div>
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="use-tls"> <input type="checkbox" class="custom-control-input" id="restart-conn">
<label class="custom-control-label" for="use-tls">Use TLS</label> <label class="custom-control-label" for="restart-conn">Restart Connection</label>
</div> </div>
<div class="other-elem" id="choose-service" style="display: none"> <div class="other-elem" id="choose-service" style="display: none">
@ -92,6 +93,16 @@
</svg> </svg>
</a> </a>
<div class="connections">
<div class="title">
<svg class="dots" expanded = "true" height = "100px" width = "100px"><circle cx = "50%" cy = "50%" r = "7px"></circle><circle class = "pulse" cx = "50%" cy = "50%" r = "10px"></circle></svg>
<span></span> Active Connection(s)
</div>
<div id="conn-list-template" style="display:none"><li><i class="fa fa-close"></i> <span class="ip"></span></li></div>
<ul class="nav">
</ul>
</div>
<div class="spinner" style="display: none"> <div class="spinner" style="display: none">
<div class="rect1"></div> <div class="rect1"></div>
<div class="rect2"></div> <div class="rect2"></div>

View File

@ -4,15 +4,20 @@ $('#get-services').click(function(){
var t = get_valid_target(); var t = get_valid_target();
if (target != t) { if (target != t) {
target = t; target = t;
use_tls = $('#use-tls').is(":checked"); use_tls = "false";
} else { } else {
return false; return false;
} }
var restart = "0"
if($('#restart-conn').is(":checked")) {
restart = "1"
}
$('.other-elem').hide(); $('.other-elem').hide();
var button = $(this).html(); var button = $(this).html();
$.ajax({ $.ajax({
url: "server/"+target+"/services", url: "server/"+target+"/services?restart="+restart,
global: true, global: true,
method: "GET", method: "GET",
success: function(res){ success: function(res){
@ -38,6 +43,7 @@ $('#get-services').click(function(){
show_loading(); show_loading();
}, },
complete: function(){ complete: function(){
applyConnCount();
$(this).html(button); $(this).html(button);
hide_loading(); hide_loading();
} }
@ -187,3 +193,55 @@ function show_loading() {
function hide_loading() { function hide_loading() {
$('.spinner').hide(); $('.spinner').hide();
} }
$(".connections ul").on("click", "i", function(){
$parent = $(this).parent("li");
var ip = $(this).siblings("span").text();
$.ajax({
url: "/active/close/" + ip,
global: true,
method: "DELETE",
success: function(res){
if(res.data.success) {
$parent.remove();
updateCountNum();
}
},
error: err,
beforeSend: function(xhr){
$(this).attr("class", "fa fa-spinner");
},
});
});
function updateCountNum() {
$(".connections .title span").html($(".connections ul li").length);
}
function applyConnCount() {
$.ajax({
url: "active/get",
global: true,
method: "GET",
success: function(res){
$(".connections .title span").html(res.data.length);
$(".connections .nav").html("");
res.data.forEach(function(item){
$list = $("#conn-list-template").clone();
$list.find(".ip").html(item);
$(".connections .nav").append($list.html());
});
},
error: err,
});
}
function refreshConnCount() {
applyConnCount();
setTimeout(refreshConnCount, 5000);
}
$(document).ready(function(){
refreshConnCount();
});