支持分布式

master
wangyj 6 years ago
parent c702388603
commit 02f8a9a370

@ -102,6 +102,7 @@ server:
password: admin
addr: :8083
name: 10.10.99.177
master: ""
notifications:
dingtalk:
groups:
@ -112,6 +113,7 @@ client:
server_url: http://admin:admin@localhost:8083
```
master 的支持来自 https://github.com/ihaiker/distributed-gosuv
Logs can be found in `$HOME/.gosuv/log/`
Edit config file(default located in `$HOME/.gosuv/programs.yml`) and run `gosuv reload` will take effect immediately.

@ -0,0 +1,39 @@
package main
import "sync/atomic"
type Int64 struct {
v int64
}
func (a *Int64) Get() int64 {
return atomic.LoadInt64(&a.v)
}
func (a *Int64) Set(v int64) {
atomic.StoreInt64(&a.v, v)
}
func (a *Int64) CompareAndSwap(o, n int64) bool {
return atomic.CompareAndSwapInt64(&a.v, o, n)
}
func (a *Int64) Swap(v int64) int64 {
return atomic.SwapInt64(&a.v, v)
}
func (a *Int64) Add(v int64) int64 {
return atomic.AddInt64(&a.v, v)
}
func (a *Int64) Sub(v int64) int64 {
return a.Add(-v)
}
func (a *Int64) Incr() int64 {
return a.Add(1)
}
func (a *Int64) Decr() int64 {
return a.Add(-1)
}

@ -0,0 +1,40 @@
package main
import (
"bytes"
)
// BufferPool implements a pool of bytes.Buffers in the form of a bounded
// channel.
type BufferPool struct {
c chan *bytes.Buffer
}
// NewBufferPool creates a new BufferPool bounded to the given size.
func NewBufferPool(size int) (bp *BufferPool) {
return &BufferPool{
c: make(chan *bytes.Buffer, size),
}
}
// Get gets a Buffer from the BufferPool, or creates a new one if none are
// available in the pool.
func (bp *BufferPool) Get() (b *bytes.Buffer) {
select {
case b = <-bp.c:
// reuse existing buffer
default:
// create new buffer
b = bytes.NewBuffer([]byte{})
}
return
}
// Put returns the given Buffer to the BufferPool.
func (bp *BufferPool) Put(b *bytes.Buffer) {
b.Reset()
select {
case bp.c <- b:
default: // Discard the buffer if the pool is full.
}
}

@ -41,6 +41,13 @@ func actionStartServer(c *cli.Context) error {
if err != nil {
log.Fatal(err)
}
if c.Bool("foreground") {
if err = newDistributed(suv, hdlr); err != nil {
log.Fatal(err)
}
}
auth := cfg.Server.HttpAuth
if auth.Enabled {
hdlr = httpauth.SimpleBasicAuth(auth.User, auth.Password)(hdlr)

@ -15,8 +15,9 @@ type Configuration struct {
User string `yaml:"username"`
Password string `yaml:"password"`
} `yaml:"httpauth"`
Addr string `yaml:"addr"`
Name string `yaml:"name"`
Addr string `yaml:"addr"`
Name string `yaml:"name"`
Master string `yaml:"master"`
} `yaml:"server,omitempty"`
Notifications Notifications `yaml:"notifications,omitempty" json:"-"`

@ -0,0 +1,241 @@
/*
*/
package main
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/bluele/gcache"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"github.com/qiniu/log"
)
type Cluster struct {
slaves gcache.Cache
client *http.Client
suv *Supervisor
}
func (cluster *Cluster) join() {
data := url.Values{"slave": []string{cfg.Server.Addr}}
request, err := http.NewRequest(http.MethodPost, "http://"+cfg.Server.Master+"/distributed/join", strings.NewReader(data.Encode()))
request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
request.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
if err != nil {
log.Errorf("join cluster %s : %s", cfg.Server.Master, err)
return
}
cluster.auth(request)
resp, err := cluster.client.Do(request)
if err != nil {
log.Errorf("join cluster %s : %s", cfg.Server.Master, err)
return
}
if resp.StatusCode == http.StatusOK {
log.Debugf("join to master %s", cfg.Server.Master)
} else {
log.Debugf("join to master %s error: %d", cfg.Server.Master, resp.StatusCode)
}
}
func (cluster *Cluster) auth(request *http.Request) {
if cfg.Server.HttpAuth.Enabled {
request.SetBasicAuth(cfg.Server.HttpAuth.User, cfg.Server.HttpAuth.Password)
}
}
func (cluster *Cluster) dialWebSocket(wsUrl string) (*websocket.Conn, *http.Response, error) {
var dialer *websocket.Dialer
if cfg.Server.HttpAuth.Enabled {
dialer = &websocket.Dialer{Proxy: func(r *http.Request) (*url.URL, error) {
cluster.auth(r)
return websocket.DefaultDialer.Proxy(r)
}}
} else {
dialer = websocket.DefaultDialer
}
return dialer.Dial(wsUrl, nil)
}
func (cluster *Cluster) requestSlave(url, method string, bodyBuffer *bytes.Buffer) ([]byte, error) {
request, err := http.NewRequest(method, url, nil)
if err != nil {
return nil, err
}
cluster.auth(request)
resp, err := cluster.client.Do(request)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
func (cluster *Cluster) cmdJoinCluster(w http.ResponseWriter, r *http.Request) {
slave := r.PostFormValue("slave")
if slave == "" {
w.WriteHeader(http.StatusForbidden)
return
}
if strings.HasPrefix(slave, ":") {
idx := strings.LastIndex(r.RemoteAddr, ":")
slave = r.RemoteAddr[:idx] + slave
}
log.Debugf("%s join cluster.", slave)
if out, err := cluster.slaves.Get(slave); err != nil || out == nil {
cluster.suv.broadcastEvent("new slave : " + slave)
}
cluster.slaves.Set(slave, slave)
w.WriteHeader(http.StatusOK)
}
//获取分布式系统下所有的内容
func (cluster *Cluster) cmdQueryDistributedPrograms(w http.ResponseWriter, r *http.Request) {
jsonOut := "{"
idx := 0
for _, v := range cluster.slaves.GetALL() {
slave := v.(string)
reqUrl := fmt.Sprintf("http://%s/api/programs", slave)
if body, err := cluster.requestSlave(reqUrl, http.MethodGet, nil); err == nil {
jsonOut += fmt.Sprintf("\"%s\":%s", slave, body)
}
if idx < cluster.slaves.Len()-1 {
jsonOut += ","
}
idx += 1
}
jsonOut += "}"
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(jsonOut))
}
func (cluster *Cluster) cmdSetting(w http.ResponseWriter, r *http.Request) {
name := mux.Vars(r)["name"]
slave := mux.Vars(r)["slave"]
cluster.suv.renderHTML(w, "setting", map[string]string{
"Name": name,
"Slave": slave,
})
}
func (cluster *Cluster) cmdWebSocketProxy(w http.ResponseWriter, r *http.Request) {
sock, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Error("upgrade:", err)
return
}
defer sock.Close()
slave := mux.Vars(r)["slave"]
slaveUri := strings.Replace(r.RequestURI, "/distributed/"+slave, "", 1)
wsUrl := fmt.Sprintf("ws://%s%s", slave, slaveUri)
log.Infof("proxy websocket :%s", wsUrl)
ws, _, err := cluster.dialWebSocket(wsUrl)
if err != nil {
log.Error("dial:", err)
return
}
defer ws.Close()
for {
messageType, data, err := ws.ReadMessage()
if err != nil {
log.Error("read message:", err)
return
}
if messageType == websocket.CloseMessage {
log.Infof("close socket")
return
}
w, err := sock.NextWriter(messageType)
if err != nil {
log.Error("write err:", err)
return
}
_, err = w.Write(data)
if err != nil {
log.Error("read:", err)
return
}
}
}
func (cluster *Cluster) slaveHttpProxy(w http.ResponseWriter, r *http.Request) {
slave := mux.Vars(r)["slave"]
slaveUri := strings.Replace(r.RequestURI, "/distributed/"+slave, "", 1)
requestUrl := fmt.Sprintf("http://%s%s", slave, slaveUri)
log.Infof("proxy :%s %s", r.Method, requestUrl)
request, err := http.NewRequest(r.Method, requestUrl, r.Body)
for k, v := range r.Header {
request.Header.Set(k, strings.Join(v, ","))
}
if err != nil {
log.Error(err)
}
cluster.auth(request)
resp, err := cluster.client.Do(request)
if err != nil {
log.Error(err)
}
defer resp.Body.Close()
if body, err := ioutil.ReadAll(resp.Body); err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
} else {
for k, v := range resp.Header {
w.Header().Set(k, strings.Join(v, ","))
}
w.Write(body)
cluster.suv.broadcastEvent("execute ok : " + slaveUri)
}
}
func newDistributed(suv *Supervisor, hdlr http.Handler) error {
cluster.suv = suv
r := hdlr.(*mux.Router)
r.HandleFunc("/distributed/join", cluster.cmdJoinCluster).Methods("POST")
r.HandleFunc("/distributed/api/programs", cluster.cmdQueryDistributedPrograms).Methods("GET")
r.HandleFunc("/distributed/{slave}/settings/{name}", cluster.cmdSetting)
for _, path := range []string{
"/distributed/{slave}/api/programs", "/distributed/{slave}/api/programs/{name}",
"/distributed/{slave}/api/programs/{name}/start", "/distributed/{slave}/api/programs/{name}/stop",
} {
r.HandleFunc(path, cluster.slaveHttpProxy)
}
r.HandleFunc("/distributed/{slave}/ws/logs/{name}", cluster.cmdWebSocketProxy)
r.HandleFunc("/distributed/{slave}/ws/perfs/{name}", cluster.cmdWebSocketProxy)
if cfg.Server.Master != "" {
go func() {
t1 := time.NewTimer(time.Second)
for {
select {
case <-t1.C:
cluster.join()
t1.Reset(time.Second)
}
}
}()
}
return nil
}
var cluster = Cluster{
slaves: gcache.New(10).LRU().Expiration(time.Second * 3).Build(),
client: new(http.Client),
}

@ -23,16 +23,17 @@ import (
"os/user"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/codeskyblue/gosuv/pushover"
"github.com/codeskyblue/kexec"
"github.com/kennygrant/sanitize"
"github.com/lunny/dingtalk_webhook"
"github.com/qiniu/log"
"github.com/soopsio/gosuv/pushover"
"github.com/soopsio/kexec"
)
type FSMState string
@ -106,14 +107,15 @@ var (
)
type Program struct {
Name string `yaml:"name" json:"name"`
Command string `yaml:"command" json:"command"`
Environ []string `yaml:"environ" json:"environ"`
Dir string `yaml:"directory" json:"directory"`
StartAuto bool `yaml:"start_auto" json:"startAuto"`
StartRetries int `yaml:"start_retries" json:"startRetries"`
StartSeconds int `yaml:"start_seconds,omitempty" json:"startSeconds"`
StopTimeout int `yaml:"stop_timeout,omitempty" json:"stopTimeout"`
Name string `yaml:"name" json:"name"`
Command string `yaml:"command" json:"command"`
Environ []string `yaml:"environ" json:"environ"`
Dir string `yaml:"directory" json:"directory"`
StartAuto bool `yaml:"start_auto" json:"startAuto"`
StartRetries int `yaml:"start_retries" json:"startRetries"`
StartSeconds int `yaml:"start_seconds,omitempty" json:"startSeconds"`
StopTimeout int `yaml:"stop_timeout,omitempty" json:"stopTimeout"`
retryCount int
User string `yaml:"user,omitempty" json:"user"`
Notifications Notifications `yaml:"notifications,omitempty" json:"-"`
WebHook WebHook `yaml:"webhook,omitempty" json:"-"`
@ -165,9 +167,12 @@ func (p *Program) RunNotification(state FSMState) {
} else {
host, _ = os.Hostname()
}
msg := fmt.Sprintf("[%s] %s: \"%s\" changed: \"%s\"", t, host, p.Name, state)
if state == RetryWait {
msg += " retryCount:" + strconv.Itoa(p.retryCount)
}
for _, noti := range notis {
po := noti.Pushover
msg := fmt.Sprintf("[%s] %s: %s changed: %s", t, host, p.Name, state)
if po.ApiKey != "" && len(po.Users) > 0 {
for _, user := range po.Users {
err := pushover.Notify(pushover.Params{
@ -288,16 +293,20 @@ func (p *Process) waitNextRetry() {
}
func (p *Process) stopCommand() {
fmt.Println("stop 111", p)
p.mu.Lock()
defer p.mu.Unlock()
defer p.SetState(Stopped)
if p.cmd == nil {
return
}
fmt.Println("stop 222", p)
p.SetState(Stopping)
if p.cmd.Process != nil {
p.cmd.Process.Signal(syscall.SIGTERM) // TODO(ssx): add it to config
}
fmt.Println("stop 333", p)
select {
case <-GoFunc(p.cmd.Wait):
p.RunNotification(FSMState("quit normally"))
@ -307,7 +316,9 @@ func (p *Process) stopCommand() {
log.Printf("program(%s) terminate all", p.Name)
p.cmd.Terminate(syscall.SIGKILL) // cleanup
}
fmt.Println("stop 444", p)
err := p.cmd.Wait() // This is OK, because Signal KILL will definitely work
fmt.Println("stop 555", p)
prefixStr := "\n--- GOSUV LOG " + time.Now().Format("2006-01-02 15:04:05")
if err == nil {
io.WriteString(p.cmd.Stderr, fmt.Sprintf("%s exit success ---\n\n", prefixStr))
@ -338,9 +349,13 @@ func (p *Process) startCommand() {
p.SetState(Fatal)
return
}
// 如果是running状态重置 retryLeft
p.retryLeft = p.StartRetries
go func() {
log.Println("开始运行 1111")
errC := GoFunc(p.cmd.Wait)
startTime := time.Now()
log.Println("开始运行 2222")
select {
case <-errC:
// if p.cmd.Wait() returns, it means program and its sub process all quited. no need to kill again
@ -349,6 +364,7 @@ func (p *Process) startCommand() {
if time.Since(startTime) < time.Duration(p.StartSeconds)*time.Second {
if p.retryLeft == p.StartRetries { // If first time quit so fast, just set to fatal
p.SetState(Fatal)
p.RunNotification(Fatal)
log.Printf("program(%s) exit too quick, status -> fatal", p.Name)
return
}
@ -358,6 +374,7 @@ func (p *Process) startCommand() {
log.Println("recv stop command")
p.stopCommand() // clean up all process
}
log.Println("开始运行 3333")
}()
}
@ -377,6 +394,9 @@ func NewProcess(pg Program) *Process {
pr.Status = string(newStatus)
// TODO: status need to filter with config, not hard coded.
// if newStatus == Fatal {
if newStatus == RetryWait {
pr.Program.retryCount++
}
go pr.Program.RunNotification(newStatus)
// }
}

68
glide.lock generated

@ -1,68 +0,0 @@
hash: b098305412c9c0105d572881e395da369b0acf7738d1e89eb816b1e69f731aca
updated: 2018-01-16T10:18:20.068657958+08:00
imports:
- name: github.com/codeskyblue/gosuv
version: 6f18d35abfca2d1b82e522ea868771436341dd8e
subpackages:
- pushover
- name: github.com/codeskyblue/kexec
version: 863094f94c7fb7c235764bf8f0f79cccea78c8eb
- name: github.com/equinox-io/equinox
version: f24972fa72facf59d05c91c848b65eac38815915
- name: github.com/franela/goreq
version: bcd34c9993f899273c74baaa95e15386cd97b6e7
- name: github.com/glycerine/rbuf
version: 54320fe9f6f340f8fbe3f34306e3c32589d97dc4
- name: github.com/go-yaml/yaml
version: d670f9405373e636a5a2765eea47fac0c9bc91a4
- name: github.com/goji/httpauth
version: 2da839ab0f4df05a6db5eb277995589dadbd4fb9
- name: github.com/gorilla/context
version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42
- name: github.com/gorilla/mux
version: 5bbbb5b2b5729b132181cc7f4aa3b3c973e9a0ed
- name: github.com/gorilla/websocket
version: 292fd08b2560ad524ee37396253d71570339a821
- name: github.com/kennygrant/sanitize
version: 2e6820834a1f36c626bf19a253b7d3cc060e9b8b
- name: github.com/lunny/dingtalk_webhook
version: e3534c89ef969912856dfa39e56b09e58c5f5daf
- name: github.com/mitchellh/go-ps
version: 4fdf99ab29366514c69ccccddab5dc58b8d84062
- name: github.com/qiniu/log
version: a304a74568d6982c5b89de1c68ac8fca3add196a
- name: github.com/shirou/gopsutil
version: 6a368fb7cd1221fa6ea90facc9447c9a2234c255
subpackages:
- process
- name: github.com/shurcooL/httpfs
version: 809beceb23714880abc4a382a00c05f89d13b1cc
subpackages:
- vfsutil
- name: github.com/shurcooL/vfsgen
version: bb654eaf43db9a91d1d3201dbd8c4b0423a96872
- name: github.com/urfave/cli
version: 75104e932ac2ddb944a6ea19d9f9f26316ff1145
- name: golang.org/x/net
version: 5ccada7d0a7ba9aeb5d3aca8d3501b4c2a509fec
subpackages:
- html
- html/atom
testImports:
- name: github.com/gopherjs/gopherjs
version: 444abdf920945de5d4a977b572bcc6c674d1e4eb
subpackages:
- js
- name: github.com/jtolds/gls
version: 77f18212c9c7edc9bd6a33d383a7b545ce62f064
- name: github.com/smartystreets/assertions
version: 0b37b35ec7434b77e77a4bb29b79677cced992ea
subpackages:
- internal/go-render/render
- internal/oglematchers
- name: github.com/smartystreets/goconvey
version: e5b2b7c9111590d019a696c7800593f666e1a7f4
subpackages:
- convey
- convey/gotest
- convey/reporting

@ -1,6 +1,6 @@
package: github.com/soopsio/gosuv
import:
- package: github.com/codeskyblue/kexec
- package: github.com/soopsio/kexec
- package: github.com/equinox-io/equinox
- package: github.com/franela/goreq
- package: github.com/glycerine/rbuf

@ -0,0 +1,152 @@
package main
import (
"bytes"
"fmt"
"io"
"time"
log "github.com/qiniu/log"
)
type Bool struct {
c Int64
}
func (b *Bool) Get() bool {
return b.c.Get() != 0
}
func (b *Bool) toInt64(v bool) int64 {
if v {
return 1
} else {
return 0
}
}
func (b *Bool) Set(v bool) {
b.c.Set(b.toInt64(v))
}
func (b *Bool) CompareAndSwap(o, n bool) bool {
return b.c.CompareAndSwap(b.toInt64(o), b.toInt64(n))
}
func (b *Bool) Swap(v bool) bool {
return b.c.Swap(b.toInt64(v)) != 0
}
var bufferPool *BufferPool
func init() {
// 4000行日志缓存
bufferPool = NewBufferPool(4000)
}
type MergeWriter struct {
lines chan *bytes.Buffer
writer io.Writer
closed Bool
}
func NewMergeWriter(writer io.Writer) *MergeWriter {
merger := &MergeWriter{
lines: make(chan *bytes.Buffer, 1000),
writer: writer,
}
merger.closed.Set(false)
merger.drainLines()
return merger
}
func (m *MergeWriter) Close() {
// log.Printf("Close MergeWriter")
if m.closed.CompareAndSwap(false, true) {
// log.Printf("Close lines chan")
close(m.lines)
}
}
func (m *MergeWriter) WriteStrLine(line string) {
if m.closed.Get() {
return
} else {
buffer := bufferPool.Get()
buffer.WriteString(line)
m.lines <- buffer
}
}
func (m *MergeWriter) WriteLine(line *bytes.Buffer) {
if m.closed.Get() {
// 需要回收Buffer
// log.Printf("Write to closed MergeWrite...")
bufferPool.Put(line)
return
} else {
m.lines <- line
}
}
func (m *MergeWriter) drainLines() {
go func() {
for line := range m.lines {
m.writer.Write(line.Bytes())
// 回收
bufferPool.Put(line)
}
}()
}
// 创建新的BufferWriter
func (m *MergeWriter) NewWriter(index int) io.Writer {
writer := &BufferWriter{
merge: m,
prefix: fmt.Sprintf(" [P%02d] ", index),
}
// 分配
writer.Buffer = bufferPool.Get()
writer.Buffer.WriteString(writer.prefix)
return writer
}
type BufferWriter struct {
Buffer *bytes.Buffer
prefix string
merge *MergeWriter
}
func (b *BufferWriter) Write(p []byte) (n int, err error) {
n = len(p)
for len(p) > 0 {
index := bytes.IndexByte(p, '\n')
if index != -1 {
// 写完完整的一行
_, err = b.Buffer.Write(p[0 : index+1])
if err != nil {
log.Error(err, "Writer Buffer failed")
return n, err
}
// 将buffer转移到merge中
b.merge.WriteLine(b.Buffer)
// 分配:写入新数据
b.Buffer = bufferPool.Get()
b.Buffer.WriteString(time.Now().Format("15:04:05") + b.prefix)
p = p[index+1:]
} else {
// 剩下不足一行,一口气全部写入
_, err = b.Buffer.Write(p)
if err != nil {
log.Error(err, "Writer Buffer failed")
return n, err
}
break
}
}
return n, nil
}

@ -37,3 +37,11 @@ small.user {
.color-red {
color: red;
}
.panel-body{
padding: 0px;
}
.table {
margin: 0px;
}

@ -2,251 +2,308 @@
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>GoSUV</title>
<link rel="shortcut icon" type="image/png" href="/res/images/favicon.ico" />
<link rel="stylesheet" type="text/css" href="/res/bootstrap-3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/res/font-awesome-4.6.3/css/font-awesome.min.css">
<link rel="stylesheet" type="text/css" href="/res/css/style.css">
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Distributed Go Supervisor 1.0</title>
<link rel="shortcut icon" type="image/png" href="/res/images/favicon.ico"/>
<link rel="stylesheet" type="text/css" href="/res/bootstrap-3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/res/font-awesome-4.6.3/css/font-awesome.min.css">
<link rel="stylesheet" type="text/css" href="/res/css/style.css">
</head>
<body id="app">
<nav class="navbar navbar-inverse">
<nav class="navbar navbar-inverse">
<div class="container">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-2">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Go Supervisor 2.0 <small class="user">[[.User]]</small></a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-2">
<ul class="nav navbar-nav">
<!-- <li class="hidden-xs">
<a>
<span class="glyphicon glyphicon-qrcode"></span>
</a>
</li> -->
</ul>
<ul id="nav-right-bar" class="nav navbar-nav navbar-right">
</ul>
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
data-target="#bs-example-navbar-collapse-2">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Distributed Go Supervisor 1.0
<small class="user">[[.User]]</small>
</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-2">
<ul class="nav navbar-nav">
<!-- <li class="hidden-xs">
<a>
<span class="glyphicon glyphicon-qrcode"></span>
</a>
</li> -->
</ul>
<ul id="nav-right-bar" class="nav navbar-nav navbar-right">
</ul>
</div>
</div>
</div>
</div>
</nav>
<div class="container">
</nav>
<div class="container">
<div class="col-md-12">
<div class="alert alert-danger" role="alert" v-if="!isConnectionAlive">
<strong>Connection lost</strong> try to reconnect after 3s, or <a href="/" class="alert-link">connect immediately</a>
</div>
<div class="alert alert-danger" role="alert" v-if="!isConnectionAlive">
<strong>Connection lost</strong> try to reconnect after 3s, or <a href="/" class="alert-link">connect immediately</a>
</div>
</div>
<!--
<div class="col-md-12">
<button class="btn btn-default btn-sm" id="launchNewProgram">
<span class="glyphicon glyphicon-plus"></span> New Program
</button>
<button class="btn btn-default btn-sm" v-on:click="refresh">
<span class="glyphicon glyphicon-refresh"></span> Refresh
</button>
<button class="btn btn-default btn-sm" v-on:click="reload">
<span class="glyphicon glyphicon-repeat"></span> Reload
</button>
<button class="btn btn-default btn-sm" v-on:click="test" data-toggle="tooltip" data-placement="bottom" title="Tooltip on bottom">
<span class="glyphicon glyphicon-glass"></span> Test
</button>
<button class="btn btn-primary" v-on:click="refresh">
<span class="glyphicon glyphicon-refresh"></span> Refresh
</button>
</div>
-->
<div class="col-md-12">
<table class="table table-hover">
<thead>
<tr>
<td>Name</td>
<td>Status</td>
<td>View</td>
<td>Command</td>
</tr>
</thead>
<tbody>
<tr v-for="p in programs">
<td v-text="p.program.name"></td>
<td v-html="p.status | colorStatus"></td>
<td>
<button class="btn btn-default btn-xs" v-on:click="cmdTail(p.program.name)">
<span class="fa fa-file-text-o"></span> Log
</button>
<a href="/settings/{{p.program.name}}" class="btn btn-default btn-xs">
<span class="fa fa-bar-chart"></span> Profiles
</a>
<button class="btn btn-default btn-xs" data-toggle="tooltip" title="{{p.program.command}}">
<span class="glyphicon glyphicon-info-sign"></span> Info
</button>
</td>
<td>
<button v-on:click="cmdStart(p.program.name)" class="btn btn-default btn-xs" :disabled='["running", "stopping"].indexOf(p.status) != -1'>
<span class="glyphicon glyphicon-play"></span> Start
</button>
<button class="btn btn-default btn-xs" v-on:click="cmdStop(p.program.name)" :disabled="!canStop(p.status)">
<span class="glyphicon glyphicon-stop"></span> Stop
</button>
<button v-on:click="showEditProgram(p.program)" class="btn btn-default btn-xs">
<span class="glyphicon glyphicon-edit"></span> Edit
</button>
<button class="btn btn-default btn-xs" v-on:click="cmdDelete(p.program.name)">
<span class="color-red glyphicon glyphicon-trash"></span> Delete
</button>
</td>
</tr>
</tbody>
</table>
<div class="panel panel-default">
<div class="panel-heading">
Master
<span class="panel-icon pull-right">
<button class="btn btn-success btn-sm" rel="tooltip" v-on:click="addNewProgram()" data-original-title="Click to add programs"><i class="fa fa-plus"></i>New Program</button>
</span>
</div>
<div class="panel-body">
<table class="table table-hover table-bordered">
<thead>
<tr>
<td>Name</td>
<td>Status</td>
<td>View</td>
<td>Command</td>
</tr>
</thead>
<tbody>
<tr v-for="p in programs">
<td v-text="p.program.name"></td>
<td v-html="p.status | colorStatus"></td>
<td>
<button class="btn btn-default btn-xs" v-on:click="cmdTail(p.program.name)">
<span class="fa fa-file-text-o"></span> Log
</button>
<a href="/settings/{{p.program.name}}" class="btn btn-default btn-xs">
<span class="fa fa-bar-chart"></span> Profiles
</a>
<button class="btn btn-default btn-xs" data-toggle="tooltip" title="{{p.program.command}}">
<span class="glyphicon glyphicon-info-sign"></span> Info
</button>
</td>
<td>
<button v-on:click="cmdStart(p.program.name)" class="btn btn-default btn-xs"
:disabled='["running", "stopping"].indexOf(p.status) != -1'>
<span class="glyphicon glyphicon-play"></span> Start
</button>
<button class="btn btn-default btn-xs" v-on:click="cmdStop(p.program.name)"
:disabled="!canStop(p.status)">
<span class="glyphicon glyphicon-stop"></span> Stop
</button>
<button v-on:click="showEditProgram(p.program)" class="btn btn-default btn-xs">
<span class="glyphicon glyphicon-edit"></span> Edit
</button>
<button class="btn btn-default btn-xs" v-on:click="cmdDelete(p.program.name)">
<span class="color-red glyphicon glyphicon-trash"></span> Delete
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div v-for="(slave,items) in slaves" class="panel panel-default">
<div class="panel-heading">
{{slave}}
<span class="panel-icon pull-right">
<button class="btn btn-success btn-sm" rel="tooltip" v-on:click="addNewProgram(slave)"
data-original-title="Click to add programs">
<i class="fa fa-plus"></i>New Program
</button>
</span>
</div>
<div class="panel-body">
<table class="table table-hover table-bordered">
<thead>
<tr>
<td>Name</td>
<td>Status</td>
<td>View</td>
<td>Command</td>
</tr>
</thead>
<tbody>
<tr v-for="p in items">
<td v-text="p.program.name"></td>
<td v-html="p.status | colorStatus"></td>
<td>
<button class="btn btn-default btn-xs" v-on:click="cmdTail(p.program.name,slave)">
<span class="fa fa-file-text-o"></span> Log
</button>
<a href="/distributed/{{slave}}/settings/{{p.program.name}}" class="btn btn-default btn-xs">
<span class="fa fa-bar-chart"></span> Profiles
</a>
<button class="btn btn-default btn-xs" data-toggle="tooltip" title="{{p.program.command}}">
<span class="glyphicon glyphicon-info-sign"></span> Info
</button>
</td>
<td>
<button v-on:click="cmdStart(p.program.name,slave)" class="btn btn-default btn-xs"
:disabled='["running", "stopping"].indexOf(p.status) != -1'>
<span class="glyphicon glyphicon-play"></span> Start
</button>
<button class="btn btn-default btn-xs" v-on:click="cmdStop(p.program.name,slave)"
:disabled="!canStop(p.status)">
<span class="glyphicon glyphicon-stop"></span> Stop
</button>
<button v-on:click="showEditProgram(p.program,slave)" class="btn btn-default btn-xs">
<span class="glyphicon glyphicon-edit"></span> Edit
</button>
<button class="btn btn-default btn-xs" v-on:click="cmdDelete(p.program.name,slave)">
<span class="color-red glyphicon glyphicon-trash"></span> Delete
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-12">
<div id="footer" class="pull-right" style="margin: 2em 1em">
<a href="https://github.com/codeskyblue/gosuv">gosuv ([[.Version]])</a>, written by <a href="https://github.com/codeskyblue">codeskyblue</a>. 2017. go1.7
</div>
<div id="footer" class="pull-right" style="margin: 2em 1em">
<div>
<a href="https://github.com/codeskyblue/gosuv">gosuv ([[.Version]])</a>, written by <a
href="https://github.com/codeskyblue">codeskyblue</a>. 2017. go1.7
</div>
<div>
<a href="https://github.com/ihaiker/distributed-gosuv">distributed-gosuv ([[.Version]])</a>, written by
<a href="https://github.com/ihaiker">haiker</a>. 2017. go1.8
</div>
</div>
</div>
<!-- panels -->
<!-- modals -->
<div class="modal" id="newProgram">
<div class="modal-dialog">
<div class="modal-content">
<form id="formNewProgram" action="/api/programs" method="post">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">New program</h4>
</div>
<div class="modal-body">
<div class="form-group">
<label>Name</label>
<input type="text" name="name" v-model="program.name" class="form-control" placeholder="name (must be unique)" required>
</div>
<div class="form-group">
<label>Command</label>
<input type="text" name="command" class="form-control" placeholder="shell command, ex: redis-server --port 6379">
</div>
<div class="form-group">
<label>Directory</label>
<input type="text" name="dir" class="form-control" placeholder="directory, default is /">
</div>
<div class="form-group">
<label>User</label>
<input type="text" name="user" class="form-control" placeholder="user, optional">
</div>
<div class="form-group">
<label>Fail Retries</label>
<input style="max-width: 5em" type="number" name="retries" class="form-control" min="0" step="1" value="3">
</div>
<div class="checkbox">
<label>
<input name="autostart" type="checkbox"> Auto start
</label>
</div>
<!-- <button type="submit" class="btn btn-Wdefault">Submit</button> -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Add program</button>
<div class="modal-dialog">
<div class="modal-content">
<form id="formNewProgram" action="" method="post">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">New program</h4>
</div>
<div class="modal-body">
<div class="form-group">
<label>Name</label>
<input type="text" name="name" v-model="program.name" class="form-control"
placeholder="name (must be unique)" required>
</div>
<div class="form-group">
<label>Command</label>
<input type="text" name="command" class="form-control"
placeholder="shell command, ex: redis-server --port 6379">
</div>
<div class="form-group">
<label>Directory</label>
<input type="text" name="dir" class="form-control" placeholder="directory, default is /">
</div>
<div class="form-group">
<label>User</label>
<input type="text" name="user" class="form-control" placeholder="user, optional">
</div>
<div class="form-group">
<label>Fail Retries</label>
<input style="max-width: 5em" type="number" name="retries" class="form-control" min="0"
step="1" value="3">
</div>
<div class="checkbox">
<label>
<input name="autostart" type="checkbox"> Auto start
</label>
</div>
<!-- <button type="submit" class="btn btn-Wdefault">Submit</button> -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" v-on:click="formNewProgram()">Add program</button>
</div>
</form>
</div>
</form>
</div>
</div>
</div>
<!-- model edit -->
<div class="modal" id="programEdit">
<div class="modal-dialog">
<div class="modal-content">
<form v-on:submit.prevent="editProgram">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title"><span class="glyphicon glyphicon-edit"></span> <span v-text="edit.program.name"></span></h4>
<div class="modal-dialog">
<div class="modal-content">
<form v-on:submit.prevent="editProgram">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title"><span class="glyphicon glyphicon-edit"></span> <span
v-text="edit.program.name"></span></h4>
</div>
<div class="modal-body">
<div class="form-group">
<label>Command</label>
<input type="text" name="command" class="form-control" v-model="edit.program.command">
</div>
<div class="form-group">
<label>Directory</label>
<input type="text" name="dir" class="form-control" v-model="edit.program.directory">
</div>
<div class="form-group">
<label>User</label>
<input type="text" name="user" class="form-control" v-model="edit.program.user">
</div>
<div class="form-group">
<label>Fail Retries</label>
<input style="max-width: 5em" type="number" name="retries" class="form-control" min="0"
step="1" v-model="edit.program.startRetries">
</div>
<div class="checkbox">
<label>
<input name="autostart" type="checkbox" v-model="edit.program.startAuto"> Auto start
</label>
</div>
<!-- <button type="submit" class="btn btn-Wdefault">Submit</button> -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Update</button>
</div>
</form>
</div>
<div class="modal-body">
<div class="form-group">
<label>Command</label>
<input type="text" name="command" class="form-control" v-model="edit.program.command">
</div>
<div class="form-group">
<label>Directory</label>
<input type="text" name="dir" class="form-control" v-model="edit.program.directory">
</div>
<div class="form-group">
<label>User</label>
<input type="text" name="user" class="form-control" v-model="edit.program.user">
</div>
<div class="form-group">
<label>Fail Retries</label>
<input style="max-width: 5em" type="number" name="retries" class="form-control" min="0" step="1" v-model="edit.program.startRetries">
</div>
<div class="checkbox">
<label>
<input name="autostart" type="checkbox" v-model="edit.program.startAuto"> Auto start
</label>
</div>
<!-- <button type="submit" class="btn btn-Wdefault">Submit</button> -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Update</button>
</div>
</form>
</div>
</div>
</div>
<!-- /.modal -->
<div class="modal" id="modalTailf">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<form id="formNewProgram" action="/api/programs" method="post">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Tail</h4>
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Tail</h4>
</div>
<div class="modal-body">
<p>Line: {{log.line_count}}</p>
<pre v-html="log.content" class="realtime-log"></pre>
<div class="checkbox text-right">
<label>
<input v-model="log.follow" type="checkbox"> Follow
</label>
</div>
</div>
</div>
<div class="modal-body">
<p>Line: {{log.line_count}}</p>
<pre v-html="log.content" class="realtime-log"></pre>
<div class="checkbox text-right">
<label>
<input v-model="log.follow" type="checkbox"> Follow
</label>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- polyfill make browser support ECMAScript 6th edition -->
<script src="/res/js/promise-polyfill.min.js"></script>
<script src="/res/js/jquery-3.1.0.min.js"></script>
<script src="/res/bootstrap-3.3.5/js/bootstrap.min.js"></script>
<script src="/res/js/moment.min.js"></script>
<script src="/res/js/underscore-min.js"></script>
<script src="/res/js/vue-1.0.min.js"></script>
<script src="/res/js/common.js"></script>
<script src="/res/js/index.js"></script>
<script type="text/javascript">
$(function() {
$("#launchNewProgram").click(function() {
$("#newProgram").modal({
show: true,
// keyboard: false,
backdrop: 'static',
})
});
// $("#modalTailf").modal({
// // show: true,
// // keyboard: false,
// // backdrop: 'static',
// })
});
</script>
</div>
<!-- polyfill make browser support ECMAScript 6th edition -->
<script src="/res/js/promise-polyfill.min.js"></script>
<script src="/res/js/jquery-3.1.0.min.js"></script>
<script src="/res/bootstrap-3.3.5/js/bootstrap.min.js"></script>
<script src="/res/js/moment.min.js"></script>
<script src="/res/js/underscore-min.js"></script>
<script src="/res/js/vue-1.0.min.js"></script>
<script src="/res/js/common.js"></script>
<script src="/res/js/index.js"></script>
</body>
</html>

@ -1,274 +1,291 @@
/* index.js */
var W = {};
var testPrograms = [{
program: {
name: "gggg",
command: "",
dir: "",
autoStart: true,
},
status: "running",
}];
var vm = new Vue({
el: "#app",
data: {
isConnectionAlive: true,
log: {
content: '',
follow: true,
line_count: 0,
},
programs: [],
edit: {
program: null,
}
},
methods: {
addNewProgram: function() {
console.log("Add")
var form = $("#formNewProgram");
form.submit(function(e) {
e.preventDefault();
$("#newProgram").modal('hide')
return false;
});
},
showEditProgram: function(p) {
this.edit.program = Object.assign({}, p); // here require polyfill.min.js
$("#programEdit").modal('show');
},
editProgram: function() {
var p = this.edit.program;
$.ajax({
url: "/api/programs/" + p.name,
method: "PUT",
data: JSON.stringify(p),
})
.then(function(ret) {
console.log(ret);
$("#programEdit").modal('hide');
})
// console.log(JSON.stringify(p));
},
updateBreadcrumb: function() {
var pathname = decodeURI(location.pathname || "/");
var parts = pathname.split('/');
this.breadcrumb = [];
if (pathname == "/") {
return this.breadcrumb;
}
var i = 2;
for (; i <= parts.length; i += 1) {
var name = parts[i - 1];
var path = parts.slice(0, i).join('/');
this.breadcrumb.push({
name: name + (i == parts.length ? ' /' : ''),
path: path
})
}
return this.breadcrumb;
},
refresh: function() {
// ws.send("Hello")
$.ajax({
url: "/api/programs",
success: function(data) {
vm.programs = data;
Vue.nextTick(function() {
$('[data-toggle="tooltip"]').tooltip()
})
}
});
},
reload: function() {
$.ajax({
url: "/api/reload",
method: "POST",
success: function(data) {
if (data.status == 0) {
alert("reload success");
} else {
alert(data.value);
}
}
});
},
test: function() {
console.log("test");
},
cmdStart: function(name) {
console.log(name);
$.ajax({
url: "/api/programs/" + name + "/start",
method: 'post',
success: function(data) {
console.log(data);
}
})
},
cmdStop: function(name) {
$.ajax({
url: "/api/programs/" + name + "/stop",
method: 'post',
success: function(data) {
console.log(data);
}
})
},
cmdTail: function(name) {
var that = this;
if (W.wsLog) {
W.wsLog.close()
}
W.wsLog = newWebsocket("/ws/logs/" + name, {
onopen: function(evt) {
that.log.content = "";
that.log.line_count = 0;
el: "#app",
data: {
isConnectionAlive: true,
log: {
content: '',
follow: true,
line_count: 0,
},
onmessage: function(evt) {
// strip ansi color
// console.log("DT:", evt.data)
that.log.content += evt.data.replace(/\033\[[0-9;]*m/g, "");
that.log.line_count = $.trim(that.log.content).split(/\r\n|\r|\n/).length;
if (that.log.follow) {
var pre = $(".realtime-log")[0];
setTimeout(function() {
pre.scrollTop = pre.scrollHeight - pre.clientHeight;
}, 1);
}
programs: [],
slaves: [],
edit: {
program: null,
}
});
this.log.follow = true;
$("#modalTailf").modal({
show: true,
keyboard: true,
// keyboard: false,
// backdrop: 'static',
})
},
cmdDelete: function(name) {
if (!confirm("Confirm delete \"" + name + "\"")) {
return
}
$.ajax({
url: "/api/programs/" + name,
method: 'delete',
success: function(data) {
console.log(data);
}
})
},
canStop: function(status) {
switch (status) {
case "running":
case "retry wait":
return true;
}
},
}
})
methods: {
addNewProgram: function (slave) {
$("#newProgram").modal({
show: true,
backdrop: 'static',
}).data("slave", slave);
},
formNewProgram: function () {
var url = "/api/programs",
data = $("#formNewProgram").serialize(),
name = $("#formNewProgram").find("[name=name]").val(),
disablechars = "./\\";
if (!name) {
alert("\"" + name + "\" is empty ")
return false
}
if (disablechars.indexOf(name[0]) != -1) {
alert("\"" + name + "\" Can't starts with \".\" \"/\" \"\\\"")
return false
}
var slave = $("#newProgram").data("slave");
if (slave !== undefined && slave !== "") {
url = "/distributed/" + slave + url;
}
$.ajax({
type: "POST",
url: url,
data: data,
success: function (data) {
if (data.status === 0) {
$("#newProgram").modal('hide');
} else {
window.alert(data.error);
}
},
error: function (err) {
alert(err.responseText)
}
});
},
Vue.filter('fromNow', function(value) {
return moment(value).fromNow();
})
showEditProgram: function (p, slave) {
this.edit.program = Object.assign({}, p); // here require polyfill.min.js
$("#programEdit").data("slave",slave).modal('show');
},
Vue.filter('formatBytes', function(value) {
var bytes = parseFloat(value);
if (bytes < 0) return "-";
else if (bytes < 1024) return bytes + " B";
else if (bytes < 1048576) return (bytes / 1024).toFixed(0) + " KB";
else if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + " MB";
else return (bytes / 1073741824).toFixed(1) + " GB";
editProgram: function () {
var p = this.edit.program;
var requestUrl = "/api/programs/" + p.name
var slave = $("#programEdit").data("slave");
if (slave !== undefined && slave !== "") {
requestUrl = "/distributed/" + slave + requestUrl;
}
$.ajax({
url: requestUrl,
method: "PUT",
data: JSON.stringify(p),
}).then(function (ret) {
$("#programEdit").modal('hide');
})
},
updateBreadcrumb: function () {
var pathname = decodeURI(location.pathname || "/");
var parts = pathname.split('/');
this.breadcrumb = [];
if (pathname == "/") {
return this.breadcrumb;
}
var i = 2;
for (; i <= parts.length; i += 1) {
var name = parts[i - 1];
var path = parts.slice(0, i).join('/');
this.breadcrumb.push({
name: name + (i == parts.length ? ' /' : ''),
path: path
})
}
return this.breadcrumb;
},
refresh: function () {
// ws.send("Hello")
$.ajax({
url: "/api/programs",
success: function (data) {
vm.programs = data;
Vue.nextTick(function () {
$('[data-toggle="tooltip"]').tooltip()
})
}
});
$.ajax({
url: "/distributed/api/programs",
success: function (data) {
vm.slaves = data;
Vue.nextTick(function () {
$('[data-toggle="tooltip"]').tooltip()
})
}
});
},
/*reload: function () {
$.ajax({
url: "/api/reload",
method: "POST",
success: function (data) {
if (data.status == 0) {
alert("reload success");
} else {
alert(data.value);
}
}
});
},
test: function () {
console.log("test");
},*/
cmdStart: function (name, slave) {
console.log(name, slave);
requestUrl = "/api/programs/" + name + "/start";
if (slave !== undefined && "" !== slave) {
requestUrl = "/distributed/" + slave + requestUrl;
}
$.ajax({
url: requestUrl,
method: 'post',
success: function (data) {
console.log(data);
}
});
},
cmdStop: function (name, slave) {
requestUrl = "/api/programs/" + name + "/stop";
if (slave !== undefined && "" !== slave) {
requestUrl = "/distributed/" + slave + requestUrl;
}
$.ajax({
url: requestUrl,
method: 'post',
success: function (data) {
console.log(data);
}
})
},
cmdTail: function (name, slave) {
requestUrl = "/ws/logs/" + name;
if (slave !== undefined && "" !== slave) {
requestUrl = "/distributed/" + slave + requestUrl;
}
var that = this;
if (W.wsLog) {
W.wsLog.close()
}
W.wsLog = newWebsocket(requestUrl, {
onopen: function (evt) {
that.log.content = "";
that.log.line_count = 0;
},
onmessage: function (evt) {
that.log.content += evt.data.replace(/\033\[[0-9;]*m/g, "");
that.log.line_count = $.trim(that.log.content).split(/\r\n|\r|\n/).length;
if (that.log.follow) {
var pre = $(".realtime-log")[0];
setTimeout(function () {
pre.scrollTop = pre.scrollHeight - pre.clientHeight;
}, 1);
}
}
});
this.log.follow = true;
$("#modalTailf").modal({
show: true,
keyboard: true,
}).on("hide.bs.modal", function (e) {
W.wsLog.close();
})
},
cmdDelete: function (name, slave) {
if (!confirm("Confirm delete \"" + name + "\"")) {
return
}
requestUrl = "/api/programs/" + name;
if (slave !== undefined && "" !== slave) {
requestUrl = "/distributed/" + slave + requestUrl
}
$.ajax({
url: requestUrl,
method: 'delete',
success: function (data) {
console.log(data);
}
})
},
canStop: function (status) {
switch (status) {
case "running":
case "retry wait":
return true;
}
},
}
})
Vue.filter('colorStatus', function(value) {
var makeColorText = function(text, color) {
return "<span class='status' style='background-color:" + color + "'>" + text + "</span>";
}
switch (value) {
case "stopping":
return makeColorText(value, "#996633");
case "running":
return makeColorText(value, "green");
case "fatal":
return makeColorText(value, "red");
default:
return makeColorText(value, "gray");
}
Vue.filter('fromNow', function (value) {
return moment(value).fromNow();
})
Vue.directive('disable', function(value) {
this.el.disabled = !!value
Vue.filter('formatBytes', function (value) {
var bytes = parseFloat(value);
if (bytes < 0) return "-";
else if (bytes < 1024) return bytes + " B";
else if (bytes < 1048576) return (bytes / 1024).toFixed(0) + " KB";
else if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + " MB";
else return (bytes / 1073741824).toFixed(1) + " GB";
})
$(function() {
vm.refresh();
Vue.filter('colorStatus', function (value) {
var makeColorText = function (text, color) {
return "<span class='status' style='background-color:" + color + "'>" + text + "</span>";
};
switch (value) {
case "stopping":
return makeColorText(value, "#996633");
case "running":
return makeColorText(value, "green");
case "fatal":
return makeColorText(value, "red");
default:
return makeColorText(value, "gray");
}
});
$("#formNewProgram").submit(function(e) {
var url = "/api/programs",
data = $(this).serialize(),
name = $(this).find("[name=name]").val(),
disablechars = "./\\";
Vue.directive('disable', function (value) {
this.el.disabled = !!value
});
if (!name) {
alert("\"" + name + "\" is empty ")
return false
}
if (disablechars.indexOf(name[0]) != -1) {
alert("\"" + name + "\" Can't starts with \".\" \"/\" \"\\\"")
return false
}
$.ajax({
type: "POST",
url: url,
data: data,
success: function(data) {
if (data.status === 0) {
$("#newProgram").modal('hide');
} else {
window.alert(data.error);
}
},
error: function(err) {
console.log(err.responseText);
}
})
e.preventDefault()
});
$(function () {
vm.refresh();
function newEventWatcher() {
W.events = newWebsocket("/ws/events", {
onopen: function(evt) {
vm.isConnectionAlive = true;
},
onmessage: function(evt) {
console.log("response:" + evt.data);
vm.refresh();
},
onclose: function(evt) {
W.events = null;
vm.isConnectionAlive = false;
console.log("Reconnect after 3s")
setTimeout(newEventWatcher, 3000)
}
});
};
function newEventWatcher() {
W.events = newWebsocket("/ws/events", {
onopen: function (evt) {
vm.isConnectionAlive = true;
},
onmessage: function (evt) {
console.log("response:" + evt.data);
vm.refresh();
},
onclose: function (evt) {
W.events = null;
vm.isConnectionAlive = false;
console.log("Reconnect after 3s");
setTimeout(newEventWatcher, 3000)
}
});
};
newEventWatcher();
newEventWatcher();
// cancel follow log if people want to see the original data
$(".realtime-log").bind('mousewheel', function(evt) {
if (evt.originalEvent.wheelDelta >= 0) {
vm.log.follow = false;
}
})
$('#modalTailf').on('hidden.bs.modal', function() {
// do something…
console.log("Hiddeen")
if (W.wsLog) {
console.log("wsLog closed")
W.wsLog.close()
}
})
// cancel follow log if people want to see the original data
$(".realtime-log").bind('mousewheel', function (evt) {
if (evt.originalEvent.wheelDelta >= 0) {
vm.log.follow = false;
}
});
$('#modalTailf').on('hidden.bs.modal', function () {
// do something…
console.log("Hiddeen");
if (W.wsLog) {
console.log("wsLog closed");
W.wsLog.close()
}
})
});

@ -1,48 +1,52 @@
/* javascript */
var vm = new Vue({
el: '#app',
data: {
name: name,
pid: '-',
childPids: [],
}
el: '#app',
data: {
name: name,
pid: '-',
childPids: [],
}
});
var maxDataCount = 30;
var ws = newWebsocket('/ws/perfs/' + name, {
onopen: function(evt) {
console.log(evt);
},
onmessage: function(evt) {
var data = JSON.parse(evt.data);
vm.pid = data.pid;
vm.childPids = data.pids;
console.log("pid", data.pid, data); //evt.data.pid);
if (memData && data.rss) {
memData.push({
value: [new Date(), data.rss],
})
if (memData.length > maxDataCount) {
memData.shift();
}
chartMem.setOption({
series: [{
data: memData,
}]
});
}
if (cpuData && data.pcpu !== undefined) {
cpuData.push({
value: [new Date(), data.pcpu],
})
if (cpuData.length > maxDataCount) {
cpuData.shift();
}
chartCpu.setOption({
series: [{
data: cpuData,
}]
})
var requstUrl = "/ws/perfs/" + name;
if ("" !== slave) {
requstUrl = "/distributed/" + slave + requstUrl;
}
var ws = newWebsocket(requstUrl, {
onopen: function(evt) {
console.log(evt);
},
onmessage: function(evt) {
var data = JSON.parse(evt.data);
vm.pid = data.pid;
vm.childPids = data.pids;
console.log("pid", data.pid, data); //evt.data.pid);
if (memData && data.rss) {
memData.push({
value: [new Date(), data.rss],
})
if (memData.length > maxDataCount) {
memData.shift();
}
chartMem.setOption({
series: [{
data: memData,
}]
});
}
if (cpuData && data.pcpu !== undefined) {
cpuData.push({
value: [new Date(), data.pcpu],
})
if (cpuData.length > maxDataCount) {
cpuData.shift();
}
chartCpu.setOption({
series: [{
data: cpuData,
}]
})
}
}
}
})

@ -2,193 +2,191 @@
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>gosuv</title>
<link rel="shortcut icon" type="image/png" href="/res/images/favicon.ico" />
<link rel="stylesheet" type="text/css" href="/res/bootstrap-3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/res/font-awesome-4.6.3/css/font-awesome.min.css">
<link rel="stylesheet" type="text/css" href="/res/css/style.css">
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>gosuv</title>
<link rel="shortcut icon" type="image/png" href="/res/images/favicon.ico"/>
<link rel="stylesheet" type="text/css" href="/res/bootstrap-3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/res/font-awesome-4.6.3/css/font-awesome.min.css">
<link rel="stylesheet" type="text/css" href="/res/css/style.css">
</head>
<body id="app">
<nav class="navbar navbar-inverse">
<nav class="navbar navbar-inverse">
<div class="container">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-2">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Go Supervisor</a>
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
data-target="#bs-example-navbar-collapse-2">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Go Supervisor</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-2">
<ul class="nav navbar-nav">
</ul>
<ul id="nav-right-bar" class="nav navbar-nav navbar-right">
</ul>
</div>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-2">
<ul class="nav navbar-nav">
</ul>
<ul id="nav-right-bar" class="nav navbar-nav navbar-right">
</ul>
</div>
</div>
</div>
</nav>
<div class="container">
</nav>
<div class="container">
<div class="col-md-12">
<h1>{{name}}</h1>
<p>
pid: {{pid}}
<br> sub pids: {{childPids.join(',')}}
</p>
<p>
<br> status: running
<br> time: 2016/09/07 12:00:00
</p>
<h1>{{name}}</h1>
<p>
pid: {{pid}}
<br> sub pids: {{childPids.join(',')}}
</p>
<p>
<br> status: running
<br> time: 2016/09/07 12:00:00
</p>
</div>
<div class="col-md-12">
<div id="chart-cpu" style="width: 100%;height:250px;"></div>
<div id="chart-mem" style="width: 100%;height:250px;"></div>
<div id="chart-cpu" style="width: 100%;height:250px;"></div>
<div id="chart-mem" style="width: 100%;height:250px;"></div>
</div>
</div>
<script type="text/javascript">
var name = "[[.Name]]";
</script>
<script src="/res/js/jquery-3.1.0.min.js"></script>
<script src="/res/bootstrap-3.3.5/js/bootstrap.min.js"></script>
<!-- <script src="/res/js/moment.min.js"></script> -->
<!-- <script src="/res/js/underscore-min.js"></script> -->
<script src="/res/js/vue-1.0.min.js"></script>
<script src="/res/js/echarts.min.js"></script>
<script src="/res/js/common.js"></script>
<script src="/res/js/settings.js"></script>
<script type="text/javascript">
// 基于准备好的dom初始化echarts实例
var chartCpu = echarts.init(document.getElementById('chart-cpu'));
var chartMem = echarts.init(document.getElementById('chart-mem'));
</div>
<script type="text/javascript">
var name = "[[.Name]]";
var slave = "[[.Slave]]"
</script>
<script src="/res/js/jquery-3.1.0.min.js"></script>
<script src="/res/bootstrap-3.3.5/js/bootstrap.min.js"></script>
<script src="/res/js/vue-1.0.min.js"></script>
<script src="/res/js/echarts.min.js"></script>
<script src="/res/js/common.js"></script>
<script src="/res/js/settings.js"></script>
<script type="text/javascript">
// 基于准备好的dom初始化echarts实例
var chartCpu = echarts.init(document.getElementById('chart-cpu'));
var chartMem = echarts.init(document.getElementById('chart-mem'));
// 指定图表的配置项和数据
// 指定图表的配置项和数据
var cpuData = [];
for (var i = maxDataCount; i > 0; i -= 1) {
cpuData.push({
value: [new Date().getTime() - 1000 * i, 0]
})
}
var cpuData = [];
for (var i = maxDataCount; i > 0; i -= 1) {
cpuData.push({
value: [new Date().getTime() - 1000 * i, 0]
})
}
var option = {
title: {
text: 'CPU'
},
toolbox: {
feature: {
saveAsImage: {}
}
},
tooltip: {
trigger: 'axis',
// formatter: function(params) {
// params = params[0];
// console.log(params)
// var date = new Date(params.value[0]);
// return date + date.getFullYear() + '/' + (date.getMonth() + 1) + '/' + date.getDate() + ' : ' + params.value[1];
// },
axisPointer: {
animation: false
}
},
legend: {
data: ['Total']
},
xAxis: {
type: 'time',
splitLine: {
show: false
}
},
yAxis: {
type: 'value',
min: 0,
max: 100,
axisLabel: {
formatter: '{value} %'
},
},
series: [{
name: 'Total',
type: 'line',
data: cpuData,
animation: false,
smooth: true,
areaStyle: {
normal: {}
},
}]
}
var option = {
title: {
text: 'CPU'
},
toolbox: {
feature: {
saveAsImage: {}
}
},
tooltip: {
trigger: 'axis',
// formatter: function(params) {
// params = params[0];
// console.log(params)
// var date = new Date(params.value[0]);
// return date + date.getFullYear() + '/' + (date.getMonth() + 1) + '/' + date.getDate() + ' : ' + params.value[1];
// },
axisPointer: {
animation: false
}
},
legend: {
data: ['Total']
},
xAxis: {
type: 'time',
splitLine: {
show: false
}
},
yAxis: {
type: 'value',
min: 0,
max: 100,
axisLabel: {
formatter: '{value} %'
},
},
series: [{
name: 'Total',
type: 'line',
data: cpuData,
animation: false,
smooth: true,
areaStyle: {
normal: {}
},
}]
}
chartCpu.setOption(option);
chartCpu.setOption(option);
var memData = [];
for (var i = maxDataCount; i > 0; i -= 1) {
memData.push({
value: [new Date().getTime() - 1000 * i, 0]
})
}
var option = {
title: {
text: 'Memory'
},
toolbox: {
feature: {
saveAsImage: {}
}
},
tooltip: {
trigger: 'axis',
formatter: function(params) {
params = params[0];
// console.log(params)
var date = new Date(params.value[0]);
return formatBytes(params.value[1]);
// return date + date.getFullYear() + '/' + (date.getMonth() + 1) + '/' + date.getDate() + ' : ' + params.value[1];
},
axisPointer: {
animation: false
}
},
legend: {
data: ['RSS']
},
xAxis: {
type: 'time',
splitLine: {
show: false
}
},
yAxis: {
type: 'value',
axisLabel: {
formatter: function(value, index) {
return formatBytes(value);
}
},
},
series: [{
name: 'RSS',
type: 'line',
data: memData,
animation: false,
smooth: true,
areaStyle: {
normal: {
color: "gray",
}
},
}]
}
chartMem.setOption(option);
</script>
var memData = [];
for (var i = maxDataCount; i > 0; i -= 1) {
memData.push({
value: [new Date().getTime() - 1000 * i, 0]
})
}
var option = {
title: {
text: 'Memory'
},
toolbox: {
feature: {
saveAsImage: {}
}
},
tooltip: {
trigger: 'axis',
formatter: function (params) {
params = params[0];
// console.log(params)
var date = new Date(params.value[0]);
return formatBytes(params.value[1]);
// return date + date.getFullYear() + '/' + (date.getMonth() + 1) + '/' + date.getDate() + ' : ' + params.value[1];
},
axisPointer: {
animation: false
}
},
legend: {
data: ['RSS']
},
xAxis: {
type: 'time',
splitLine: {
show: false
}
},
yAxis: {
type: 'value',
axisLabel: {
formatter: function (value, index) {
return formatBytes(value);
}
},
},
series: [{
name: 'RSS',
type: 'line',
data: memData,
animation: false,
smooth: true,
areaStyle: {
normal: {
color: "gray",
}
},
}]
}
chartMem.setOption(option);
</script>
</body>
</header>
</html>

@ -18,13 +18,13 @@ import (
"syscall"
"time"
"github.com/codeskyblue/kexec"
"github.com/go-yaml/yaml"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"github.com/qiniu/log"
_ "github.com/shurcooL/vfsgen"
"github.com/soopsio/gosuv/gops"
"github.com/soopsio/kexec"
)
var defaultGosuvDir string
@ -101,6 +101,8 @@ func (s *Supervisor) stopAndWait(name string) error {
}
c := make(chan string, 0)
s.addStatusChangeListener(c)
// p.stopCommand()
// 停止任务
p.Operate(StopEvent)
for {
select {
@ -183,7 +185,6 @@ func (s *Supervisor) loadDB() error {
visited := map[string]bool{}
names := make([]string, 0, len(pgs))
for _, pg := range pgs {
fmt.Printf("%+v", pg)
names = append(names, pg.Name)
visited[pg.Name] = true
s.addOrUpdateProgram(pg)

Loading…
Cancel
Save