mirror of
https://github.com/gusaul/grpcox.git
synced 2024-12-26 02:40:10 +00:00
save and reuse active connections
This commit is contained in:
parent
02a4feb17b
commit
f44b1a7f78
|
@ -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 {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
BIN
index/img/favicon.png
Normal file
BIN
index/img/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
|
@ -5,6 +5,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<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 href="css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="css/mdb.min.css" rel="stylesheet">
|
||||
|
@ -23,8 +24,8 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="use-tls">
|
||||
<label class="custom-control-label" for="use-tls">Use TLS</label>
|
||||
<input type="checkbox" class="custom-control-input" id="restart-conn">
|
||||
<label class="custom-control-label" for="restart-conn">Restart Connection</label>
|
||||
</div>
|
||||
|
||||
<div class="other-elem" id="choose-service" style="display: none">
|
||||
|
@ -92,6 +93,16 @@
|
|||
</svg>
|
||||
</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="rect1"></div>
|
||||
<div class="rect2"></div>
|
||||
|
|
|
@ -4,15 +4,20 @@ $('#get-services').click(function(){
|
|||
var t = get_valid_target();
|
||||
if (target != t) {
|
||||
target = t;
|
||||
use_tls = $('#use-tls').is(":checked");
|
||||
use_tls = "false";
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
var restart = "0"
|
||||
if($('#restart-conn').is(":checked")) {
|
||||
restart = "1"
|
||||
}
|
||||
|
||||
$('.other-elem').hide();
|
||||
var button = $(this).html();
|
||||
$.ajax({
|
||||
url: "server/"+target+"/services",
|
||||
url: "server/"+target+"/services?restart="+restart,
|
||||
global: true,
|
||||
method: "GET",
|
||||
success: function(res){
|
||||
|
@ -38,6 +43,7 @@ $('#get-services').click(function(){
|
|||
show_loading();
|
||||
},
|
||||
complete: function(){
|
||||
applyConnCount();
|
||||
$(this).html(button);
|
||||
hide_loading();
|
||||
}
|
||||
|
@ -187,3 +193,55 @@ function show_loading() {
|
|||
function hide_loading() {
|
||||
$('.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();
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue
Block a user