├── .fsw.yml ├── .gitignore ├── .gopack.yml ├── .goreleaser.yml ├── .travis.yml ├── LICENSE ├── README.md ├── README_ZH.md ├── assets_dev.go ├── assets_generate.go ├── atomic64.go ├── broadcast.go ├── buffer_pool.go ├── build_remote.sh ├── build_standalone.sh ├── cmds.go ├── config.go ├── distributed.go ├── docs ├── gosuv.gif └── states.png ├── fsm.go ├── fsm_test.go ├── get.sh ├── glide.yaml ├── gops ├── gops.go └── gops_test.go ├── gosuv.go ├── hipchat └── hipchat.go ├── merge_write.go ├── pushover └── pushover.go ├── res ├── bootstrap-3.3.5 │ ├── css │ │ ├── bootstrap-theme.css │ │ ├── bootstrap-theme.css.map │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ └── bootstrap.min.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ └── js │ │ ├── bootstrap.js │ │ ├── bootstrap.min.js │ │ └── npm.js ├── css │ └── style.css ├── font-awesome-4.6.3 │ ├── HELP-US-OUT.txt │ ├── css │ │ ├── font-awesome.css │ │ └── font-awesome.min.css │ └── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 ├── images │ ├── favicon.ico │ └── favicon.png ├── index.html ├── js │ ├── clipboard-1.5.12.min.js │ ├── common.js │ ├── dropzone.js │ ├── echarts.min.js │ ├── index.js │ ├── jquery-3.1.0.min.js │ ├── jquery.qrcode.js │ ├── jquery.scrollUp.min.js │ ├── moment.min.js │ ├── promise-polyfill.min.js │ ├── qrcode.js │ ├── settings.js │ ├── showdown-1.4.2.min.js │ ├── ua-parser.min.js │ ├── underscore-min.js │ └── vue-1.0.min.js └── setting.html ├── sigchld_unix.go ├── utils.go └── web.go /.fsw.yml: -------------------------------------------------------------------------------- 1 | desc: Auto generated by fswatch [gosuv] 2 | triggers: 3 | - name: "" 4 | pattens: 5 | - '**/*.go' 6 | - '**/*.c' 7 | - '**/*.py' 8 | env: 9 | DEBUG: "1" 10 | cmd: sh ./build.sh 11 | shell: true 12 | delay: 100ms 13 | signal: TERM 14 | watch_paths: 15 | - . 16 | watch_depth: 0 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | dist/ 27 | output/ 28 | gosuv 29 | bindata_assetfs.go 30 | assets_vfsdata.go 31 | 32 | .vscode/ 33 | vendor/* 34 | *.un~ 35 | -------------------------------------------------------------------------------- /.gopack.yml: -------------------------------------------------------------------------------- 1 | author: codeskyblue 2 | description: Port of python supervisor 3 | os: darwin linux 4 | includes: [] 5 | excludes: 6 | - \.git 7 | script: 8 | - go build -tags bindata -ldflags "-X main.Version=$(git describe --tags --dirty --always)" 9 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - 3 | goos: 4 | - linux 5 | - darwin 6 | - windows 7 | goarch: 8 | - amd64 9 | - arm 10 | goarm: 11 | - 6 12 | - 7 13 | ignore: 14 | - goos: darwin 15 | goarch: arm 16 | flags: -tags vfs 17 | hooks: 18 | pre: go generate -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | go: 4 | - 1.7 5 | install: 6 | - go get -t -v 7 | script: 8 | - go test -v 9 | after_success: 10 | - test -n "$TRAVIS_TAG" && curl -sL https://git.io/goreleaser | bash 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 shengxiang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gosuv 2 | [![Build Status](https://travis-ci.org/codeskyblue/gosuv.svg)](https://travis-ci.org/codeskyblue/gosuv) 3 | 4 | [中文README](README_ZH.md) 不是很全,能看懂英文的还是尽量看英文 5 | 6 | ## current is in beta 7 | Process management writtern by golang, inspired by python-supervisor 8 | 9 | ## So why write another supervisor? 10 | I have been using python-supervisor for many years and there are something uncomfortable feelings. 11 | 12 | 1. Log can't contains ANSI color chars 13 | 1. The configuration file can add on the web, often forgot some settings. 14 | 1. `supervisorctl reload` will cause supervisord restarted 15 | 1. Hard to set status change to fatal notifications. 16 | 1. No process performance monitor page. 17 | 1. Program starts with no common environ, eg, missing HOME and USER variable 18 | 1. Kill process default is not group kill which make sub process still running. 19 | 1. More... will added when I think of it. 20 | 21 | ## Features 22 | 23 | * [x] Web control page 24 | 25 | * [x] Start, Stop, Tail, Reload 26 | * [x] Realtime log 27 | * [x] Add program support 28 | * [x] Edit support 29 | * [x] Delete support 30 | * [x] Memory and CPU monitor 31 | * [ ] Path auto complete 32 | 33 | * [x] HTTP Basic auth 34 | * [x] Github webhook 35 | * [ ] Single log page, include search support 36 | * [ ] 中文文档 37 | 38 | ## Requirements 39 | Go version at least `1.6+` 40 | 41 | ## Installation 42 | ### Binaries 43 | The fastest way is run with. Default install location is `/usr/local/bin`, change env-var `BINDIR` will also change install location. 44 | 45 | ``` 46 | curl https://raw.githubusercontent.com/codeskyblue/gosuv/master/get.sh | bash 47 | ``` 48 | 49 | Or just download binaries 50 | 51 | 52 | 53 | Thanks to [goreleaser](https://github.com/goreleaser/goreleaser) which makes publish binaries automaticly. 54 | 55 | ### Build from source 56 | ```sh 57 | go get -d github.com/codeskyblue/gosuv 58 | cd $GOPATH/src/github.com/codeskyblue/gosuv 59 | go generate # package html resources into go 60 | go build -tags vfs 61 | ``` 62 | 63 | ## Quick start 64 | After you installed gosuv, the first thing is to start server. 65 | 66 | ```sh 67 | gosuv start-server 68 | ``` 69 | 70 | Basic operations 71 | 72 | ```sh 73 | $ gosuv status 74 | PROGRAM NAME STATUS 75 | test running 76 | test_again stopped 77 | 78 | $ gosuv stop test 79 | $ gosuv start test 80 | ``` 81 | 82 | Open web to see the manager page. And follow the gif to add a program to gosuv. 83 | 84 | 85 | ![gosuv web](docs/gosuv.gif) 86 | 87 | ## Configuration 88 | Default config file stored in directory `$HOME/.gosuv/`, Set env-var `GOSUV_HOME_DIR` can change config file store directory. 89 | 90 | - file `programs.yml` contains all program settings. 91 | - file `config.yml` contains server config 92 | 93 | File `config.yml` can be generated by `gosuv conftest` 94 | 95 | Example config.yaml 96 | 97 | ``` 98 | server: 99 | httpauth: 100 | enabled: true 101 | username: admin 102 | password: admin 103 | addr: :8083 104 | name: 10.10.99.177 105 | master: "" 106 | notifications: 107 | dingtalk: 108 | groups: 109 | - secret: c1b8032******************aa736a 110 | mobile: 111 | - "153********" 112 | client: 113 | server_url: http://admin:admin@localhost:8083 114 | ``` 115 | 116 | master 的支持来自 https://github.com/ihaiker/distributed-gosuv 117 | Logs can be found in `$HOME/.gosuv/log/` 118 | 119 | Edit config file(default located in `$HOME/.gosuv/programs.yml`) and run `gosuv reload` will take effect immediately. 120 | 121 | ## Design 122 | HTTP is follow the RESTFul guide. 123 | 124 | Get or Update program 125 | 126 | ` /api/programs/:name` 127 | 128 | Add new program 129 | 130 | `POST /api/programs` 131 | 132 | Del program 133 | 134 | `DELETE /api/programs/:name` 135 | 136 | ## State 137 | Only 4 states. [ref](http://supervisord.org/subprocess.html#process-states) 138 | 139 | ![states](docs/states.png) 140 | 141 | ## Notification 142 | Configuration example 143 | 144 | ```yaml 145 | - demo-program: 146 | command: ... 147 | notifications: 148 | pushover: 149 | api_key: [token] 150 | users: 151 | - [user1] 152 | - [user2] 153 | ``` 154 | 155 | Now only support [pushover](https://pushover.net/api), and only status change to fatal will get notified. 156 | 157 | ## Integrate with github (todo) 158 | This is feature that will helps update your deployment environment once your updated in the github. 159 | 160 | This part is set in the `programs.yml`, take look the example 161 | 162 | ```yml 163 | - demo-program: 164 | command: python app.py 165 | directory: /opt/demo 166 | webhook: 167 | github: 168 | secret: 123456 169 | command: git pull origin master 170 | ``` 171 | 172 | ## Alternative 173 | - Go implementation of supervisor 174 | 175 | ## LICENSE 176 | [MIT](LICENSE) 177 | -------------------------------------------------------------------------------- /README_ZH.md: -------------------------------------------------------------------------------- 1 | # 概述 2 | GoSuv是GO语言重写的类supervisor的一个进程管理程序,简单易用,界面美感十足且对用户友好 3 | 4 | ## 使用 5 | * 启动服务 6 | 7 | ``` 8 | gosuv start-server 9 | ``` 10 | 11 | 查看服务状态 12 | 13 | ``` 14 | $ gosuv status 15 | Server is running 16 | ``` 17 | 18 | 默认端口 11113 本机测试请使用[http://localhost:11313](http://localhost:11313) 19 | 20 | ![RunImage](docs/gosuv.gif) 21 | 22 | ## 配置 23 | 默认配置文件都放在 `$HOME/.gosuv/` 24 | 25 | * 项目文件名 : programs.yml 26 | * 服务器配置文件名: config.yml 27 | 28 | 验证信息配置 29 | 30 | ```yml 31 | server: 32 | httpauth: 33 | enabled: false 34 | username: uu 35 | password: pp 36 | addr: :11313 37 | client: 38 | server_url: http://localhost:11313 39 | ``` 40 | 41 | ## 默认日志文件位置 42 | `$HOME/.gosuv/log/` 43 | 44 | ## 待续 45 | 内容不是很多,还是推荐能看懂英语的去看[英文的README](README.md) 46 | 47 | ## 贡献人 48 | - [Docking](http://miaomia.com) -------------------------------------------------------------------------------- /assets_dev.go: -------------------------------------------------------------------------------- 1 | // +build !vfs 2 | //go:generate go run assets_generate.go 3 | 4 | package main 5 | 6 | import "net/http" 7 | 8 | // Assets contains project assets. 9 | var Assets http.FileSystem = http.Dir("res") 10 | -------------------------------------------------------------------------------- /assets_generate.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "log" 7 | "net/http" 8 | 9 | "github.com/shurcooL/vfsgen" 10 | ) 11 | 12 | func main() { 13 | var fs http.FileSystem = http.Dir("res") 14 | 15 | err := vfsgen.Generate(fs, vfsgen.Options{ 16 | PackageName: "main", 17 | BuildTags: "vfs", 18 | VariableName: "Assets", 19 | }) 20 | if err != nil { 21 | log.Fatalln(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /atomic64.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "sync/atomic" 4 | 5 | type Int64 struct { 6 | v int64 7 | } 8 | 9 | func (a *Int64) Get() int64 { 10 | return atomic.LoadInt64(&a.v) 11 | } 12 | 13 | func (a *Int64) Set(v int64) { 14 | atomic.StoreInt64(&a.v, v) 15 | } 16 | 17 | func (a *Int64) CompareAndSwap(o, n int64) bool { 18 | return atomic.CompareAndSwapInt64(&a.v, o, n) 19 | } 20 | 21 | func (a *Int64) Swap(v int64) int64 { 22 | return atomic.SwapInt64(&a.v, v) 23 | } 24 | 25 | func (a *Int64) Add(v int64) int64 { 26 | return atomic.AddInt64(&a.v, v) 27 | } 28 | 29 | func (a *Int64) Sub(v int64) int64 { 30 | return a.Add(-v) 31 | } 32 | 33 | func (a *Int64) Incr() int64 { 34 | return a.Add(1) 35 | } 36 | 37 | func (a *Int64) Decr() int64 { 38 | return a.Add(-1) 39 | } 40 | -------------------------------------------------------------------------------- /broadcast.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "sync" 7 | "time" 8 | 9 | "github.com/glycerine/rbuf" 10 | "github.com/qiniu/log" 11 | ) 12 | 13 | // The new broadcast 14 | type StreamWriter struct { 15 | wc io.WriteCloser 16 | stream string 17 | } 18 | 19 | type WriteBroadcaster struct { 20 | sync.Mutex 21 | buf *rbuf.FixedSizeRingBuf 22 | writers map[StreamWriter]bool 23 | closed bool 24 | } 25 | 26 | func NewWriteBroadcaster(size int) *WriteBroadcaster { 27 | if size <= 0 { 28 | size = 4 * 1024 29 | } 30 | bc := &WriteBroadcaster{ 31 | writers: make(map[StreamWriter]bool), 32 | buf: rbuf.NewFixedSizeRingBuf(size), 33 | closed: false, 34 | } 35 | return bc 36 | } 37 | 38 | func (wb *WriteBroadcaster) Closed() bool { 39 | return wb.closed 40 | } 41 | 42 | // this is main func 43 | func (wb *WriteBroadcaster) NewChanString(name string) chan string { 44 | wb.Lock() 45 | defer wb.Unlock() 46 | 47 | wr := NewChanStrWriter() 48 | if wb.closed { 49 | wr.Close() 50 | return nil 51 | } 52 | sw := StreamWriter{wc: wr, stream: name} 53 | wb.writers[sw] = true 54 | wr.Write(wb.buf.Bytes()) 55 | return wr.C 56 | } 57 | 58 | func (wb *WriteBroadcaster) Bytes() []byte { 59 | return wb.buf.Bytes() 60 | } 61 | 62 | func (w *WriteBroadcaster) Write(p []byte) (n int, err error) { 63 | w.Lock() 64 | defer w.Unlock() 65 | 66 | // write with advance 67 | w.buf.WriteAndMaybeOverwriteOldestData(p) 68 | 69 | for sw := range w.writers { 70 | // set write timeout 71 | err = GoTimeout(func() error { 72 | if _, err := sw.wc.Write(p); err != nil { //|| n != len(p) { 73 | return errors.New("broadcast to " + sw.stream + " error") 74 | } 75 | return nil 76 | }, time.Second*1) 77 | if err != nil { 78 | // On error, evict the writer 79 | log.Warnf("broadcase write error: %s, %s", sw.stream, err) 80 | sw.wc.Close() 81 | delete(w.writers, sw) 82 | } 83 | } 84 | return len(p), nil 85 | } 86 | 87 | func (w *WriteBroadcaster) CloseWriter(name string) { 88 | for sw := range w.writers { 89 | if sw.stream == name { 90 | sw.wc.Close() 91 | } 92 | } 93 | } 94 | 95 | func (w *WriteBroadcaster) CloseWriters() error { 96 | w.Lock() 97 | defer w.Unlock() 98 | for sw := range w.writers { 99 | sw.wc.Close() 100 | } 101 | w.writers = make(map[StreamWriter]bool) 102 | w.closed = true 103 | return nil 104 | } 105 | 106 | // // nop writer 107 | // type NopWriter struct{} 108 | 109 | // func (*NopWriter) Write(buf []byte) (int, error) { 110 | // return len(buf), nil 111 | // } 112 | 113 | // type nopWriteCloser struct { 114 | // io.Writer 115 | // } 116 | 117 | // func (w *nopWriteCloser) Close() error { return nil } 118 | 119 | // func NopWriteCloser(w io.Writer) io.WriteCloser { 120 | // return &nopWriteCloser{w} 121 | // } 122 | 123 | // chan string writer 124 | type chanStrWriter struct { 125 | C chan string 126 | closed bool 127 | mu sync.Mutex 128 | } 129 | 130 | func (c *chanStrWriter) Write(data []byte) (n int, err error) { 131 | c.mu.Lock() 132 | defer c.mu.Unlock() 133 | if c.closed { 134 | return 0, errors.New("chan writer closed") 135 | } 136 | c.C <- string(data) // write timeout 137 | return len(data), nil 138 | } 139 | 140 | func (c *chanStrWriter) Close() error { 141 | c.mu.Lock() 142 | defer c.mu.Unlock() 143 | if !c.closed { 144 | c.closed = true 145 | close(c.C) 146 | } 147 | return nil 148 | } 149 | 150 | func NewChanStrWriter() *chanStrWriter { 151 | return &chanStrWriter{ 152 | C: make(chan string, 10), 153 | } 154 | } 155 | 156 | // quick loss writer 157 | type QuickLossBroadcastWriter struct { 158 | *WriteBroadcaster 159 | bufC chan string 160 | closed bool 161 | } 162 | 163 | func (w *QuickLossBroadcastWriter) Write(buf []byte) (int, error) { 164 | select { 165 | case w.bufC <- string(buf): 166 | default: 167 | } 168 | return len(buf), nil 169 | } 170 | 171 | func (w *QuickLossBroadcastWriter) Close() error { 172 | if !w.closed { 173 | w.closed = true 174 | close(w.bufC) 175 | w.WriteBroadcaster.CloseWriters() 176 | } 177 | return nil 178 | } 179 | 180 | func (w *QuickLossBroadcastWriter) drain() { 181 | for data := range w.bufC { 182 | w.WriteBroadcaster.Write([]byte(data)) 183 | } 184 | } 185 | 186 | func NewQuickLossBroadcastWriter(size int) *QuickLossBroadcastWriter { 187 | qlw := &QuickLossBroadcastWriter{ 188 | WriteBroadcaster: NewWriteBroadcaster(size), 189 | bufC: make(chan string, 20), 190 | } 191 | go qlw.drain() 192 | return qlw 193 | } 194 | -------------------------------------------------------------------------------- /buffer_pool.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | // BufferPool implements a pool of bytes.Buffers in the form of a bounded 8 | // channel. 9 | type BufferPool struct { 10 | c chan *bytes.Buffer 11 | } 12 | 13 | // NewBufferPool creates a new BufferPool bounded to the given size. 14 | func NewBufferPool(size int) (bp *BufferPool) { 15 | return &BufferPool{ 16 | c: make(chan *bytes.Buffer, size), 17 | } 18 | } 19 | 20 | // Get gets a Buffer from the BufferPool, or creates a new one if none are 21 | // available in the pool. 22 | func (bp *BufferPool) Get() (b *bytes.Buffer) { 23 | select { 24 | case b = <-bp.c: 25 | // reuse existing buffer 26 | default: 27 | // create new buffer 28 | b = bytes.NewBuffer([]byte{}) 29 | } 30 | return 31 | } 32 | 33 | // Put returns the given Buffer to the BufferPool. 34 | func (bp *BufferPool) Put(b *bytes.Buffer) { 35 | b.Reset() 36 | select { 37 | case bp.c <- b: 38 | default: // Discard the buffer if the pool is full. 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /build_remote.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash - 2 | # 3 | 4 | set -e 5 | 6 | TARGET=build_tmp/src/github.com/codeskyblue/gosuv 7 | HOST="pi3-0" 8 | ssh pi@$HOST mkdir -p $TARGET 9 | 10 | rsync -avz -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress \ 11 | --exclude gosuv --exclude dist --exclude .git \ 12 | --delete \ 13 | . pi@$HOST:$TARGET 14 | 15 | echo "Build remotely ..." 16 | ssh pi@$HOST bash $TARGET/build_standalone.sh 17 | echo "Build finished, copying ..." 18 | scp pi@$HOST:$TARGET/gosuv ./dist/gosuv-linux-arm 19 | echo "All finished" 20 | -------------------------------------------------------------------------------- /build_standalone.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash - 2 | # 3 | 4 | set -e 5 | set -o pipefail 6 | 7 | cd $(dirname $0) 8 | 9 | if test $(whoami) = "pi" 10 | then 11 | export GOPATH=/home/pi/build_tmp 12 | export GOROOT=$HOME/go 13 | export PATH=$PATH:$GOROOT/bin 14 | fi 15 | #[[ -f $HOME/.bash_profile ]] && source "$HOME/.bash_profile" 16 | #[[ -f $HOME/.bashrc ]] && source "$HOME/.bashrc" 17 | 18 | go generate 19 | exec go build -tags vfs "$@" 20 | -------------------------------------------------------------------------------- /cmds.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "os/exec" 12 | "path/filepath" 13 | "strconv" 14 | "strings" 15 | "time" 16 | 17 | // "github.com/franela/goreq" 18 | "github.com/goji/httpauth" 19 | "github.com/imroc/req" 20 | "github.com/urfave/cli" 21 | ) 22 | 23 | func postForm(pathname string, data url.Values) (r JSONResponse, err error) { 24 | resp, err := http.PostForm(cfg.Client.ServerURL+pathname, data) 25 | if err != nil { 26 | return r, err 27 | } 28 | defer resp.Body.Close() 29 | body, err := ioutil.ReadAll(resp.Body) 30 | if err != nil { 31 | return r, err 32 | } 33 | err = json.Unmarshal(body, &r) 34 | if err != nil { 35 | return r, fmt.Errorf("POST %v %v", strconv.Quote(pathname), string(body)) 36 | } 37 | return r, nil 38 | } 39 | 40 | func actionStartServer(c *cli.Context) error { 41 | suv, hdlr, err := newSupervisorHandler() 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | 46 | if c.Bool("foreground") { 47 | if err = newDistributed(suv, hdlr); err != nil { 48 | log.Fatal(err) 49 | } 50 | } 51 | 52 | auth := cfg.Server.HttpAuth 53 | if auth.Enabled { 54 | hdlr = httpauth.SimpleBasicAuth(auth.User, auth.Password)(hdlr) 55 | } 56 | http.Handle("/", hdlr) 57 | 58 | addr := cfg.Server.Addr 59 | if c.Bool("foreground") { 60 | suv.AutoStartPrograms() 61 | log.Printf("server listen on %v", addr) 62 | log.Fatal(http.ListenAndServe(addr, nil)) 63 | } else { 64 | if checkServerStatus() == nil { 65 | fmt.Println("server is already running") 66 | return nil 67 | } 68 | logPath := filepath.Join(defaultGosuvDir, "gosuv.log") 69 | logFd, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) 70 | if err != nil { 71 | log.Fatalf("create file %s failed: %v", logPath, err) 72 | } 73 | cmd := exec.Command(os.Args[0], "start-server", "-f") 74 | cmd.Stdout = logFd 75 | cmd.Stderr = logFd 76 | err = cmd.Start() 77 | if err != nil { 78 | log.Fatal(err) 79 | } 80 | if err := ioutil.WriteFile("/var/run/gosuv.pid", []byte(strconv.Itoa(cmd.Process.Pid)), 0644); err != nil { 81 | log.Fatalln(err) 82 | } 83 | select { 84 | case err = <-GoFunc(cmd.Wait): 85 | log.Fatalf("server started failed, %v", err) 86 | case <-time.After(200 * time.Millisecond): 87 | showAddr := addr 88 | if strings.HasPrefix(addr, ":") { 89 | showAddr = "0.0.0.0" + addr 90 | } 91 | fmt.Printf("server started, listening on %s\n", showAddr) 92 | } 93 | } 94 | return nil 95 | } 96 | 97 | func actionStatus(c *cli.Context) error { 98 | // res, err := goreq.Request{ 99 | // Uri: cfg.Client.ServerURL + "/api/programs", 100 | // }.Do() 101 | res, err := req.Get(cfg.Client.ServerURL + "/api/programs") 102 | if err != nil { 103 | return err 104 | } 105 | var programs = make([]struct { 106 | Program Program `json:"program"` 107 | Status string `json:"status"` 108 | }, 0) 109 | // if err := res.Body.FromJsonTo(&programs); err != nil { 110 | // return err 111 | // } 112 | if err := res.ToJSON(&programs); err != nil { 113 | return err 114 | } 115 | format := "%-23s\t%-8s\n" 116 | fmt.Printf(format, "PROGRAM NAME", "STATUS") 117 | for _, p := range programs { 118 | fmt.Printf(format, p.Program.Name, p.Status) 119 | } 120 | return nil 121 | } 122 | 123 | // cmd: 124 | func programOperate(cmd, name string) (err error, success bool) { 125 | // res, err := goreq.Request{ 126 | // Method: "POST", 127 | // Uri: cfg.Client.ServerURL + "/api/programs/" + name + "/" + cmd, 128 | // }.Do() 129 | 130 | res, err := req.Post(cfg.Client.ServerURL + "/api/programs/" + name + "/" + cmd) 131 | if err != nil { 132 | return 133 | } 134 | var v = struct { 135 | Status int `json:"status"` 136 | }{} 137 | // if err = res.Body.FromJsonTo(&v); err != nil { 138 | // return 139 | // } 140 | if err = res.ToJSON(&v); err != nil { 141 | return 142 | } 143 | success = v.Status == 0 144 | return 145 | } 146 | 147 | func actionStart(c *cli.Context) (err error) { 148 | name := c.Args().First() 149 | err, success := programOperate("start", name) 150 | if err != nil { 151 | return 152 | } 153 | if success { 154 | fmt.Println("Started") 155 | } else { 156 | fmt.Println("Start failed") 157 | } 158 | return nil 159 | } 160 | 161 | func actionStop(c *cli.Context) (err error) { 162 | name := c.Args().First() 163 | err, success := programOperate("stop", name) 164 | if err != nil { 165 | return 166 | } 167 | if !success { 168 | fmt.Println("Stop failed") 169 | } 170 | return nil 171 | } 172 | 173 | func actionShutdown(c *cli.Context) error { 174 | restart := c.Bool("restart") 175 | if restart { 176 | log.Fatal("Restart not implemented.") 177 | } 178 | ret, err := postForm("/api/shutdown", nil) 179 | if err != nil { 180 | log.Fatal(err) 181 | } 182 | fmt.Println(ret.Value) 183 | return nil 184 | } 185 | 186 | func actionUpdateSelf(c *cli.Context) error { 187 | return githubUpdate(c.Bool("yes")) 188 | } 189 | 190 | func actionEdit(c *cli.Context) error { 191 | cmd := exec.Command("vim", filepath.Join(os.Getenv("HOME"), ".gosuv/programs.yml")) 192 | cmd.Stdout = os.Stdout 193 | cmd.Stdin = os.Stdin 194 | cmd.Stderr = os.Stderr 195 | return cmd.Run() 196 | } 197 | 198 | func actionVersion(c *cli.Context) error { 199 | fmt.Printf("gosuv version %s\n", version) 200 | return nil 201 | } 202 | 203 | func actionReload(c *cli.Context) error { 204 | ret, err := postForm("/api/reload", nil) 205 | if err != nil { 206 | log.Fatal(err) 207 | } 208 | fmt.Println(ret.Value) 209 | return nil 210 | } 211 | 212 | func actionConfigTest(c *cli.Context) error { 213 | if _, _, err := newSupervisorHandler(); err != nil { 214 | log.Fatal(err) 215 | } 216 | log.Println("test is successful") 217 | return nil 218 | } 219 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/go-yaml/yaml" 9 | ) 10 | 11 | type Configuration struct { 12 | Server struct { 13 | HttpAuth struct { 14 | Enabled bool `yaml:"enabled"` 15 | User string `yaml:"username"` 16 | Password string `yaml:"password"` 17 | } `yaml:"httpauth"` 18 | Addr string `yaml:"addr"` 19 | Name string `yaml:"name"` 20 | Master string `yaml:"master"` 21 | } `yaml:"server,omitempty"` 22 | Notifications Notifications `yaml:"notifications,omitempty" json:"-"` 23 | 24 | Client struct { 25 | ServerURL string `yaml:"server_url"` 26 | } 27 | } 28 | 29 | func readConf(filename string) (c Configuration, err error) { 30 | // initial default value 31 | c.Server.Addr = ":11313" // in memory of 08-31 13:13 32 | c.Client.ServerURL = "http://localhost:11313" 33 | 34 | data, err := ioutil.ReadFile(filename) 35 | if err != nil { 36 | data = []byte("") 37 | } 38 | err = yaml.Unmarshal(data, &c) 39 | if err != nil { 40 | return 41 | } 42 | cfgDir := filepath.Dir(filename) 43 | if !IsDir(cfgDir) { 44 | os.MkdirAll(cfgDir, 0755) 45 | } 46 | data, _ = yaml.Marshal(c) 47 | err = ioutil.WriteFile(filename, data, 0644) 48 | return 49 | } 50 | -------------------------------------------------------------------------------- /distributed.go: -------------------------------------------------------------------------------- 1 | /* 2 | 分布式实现 3 | 话外:作者的整体代码结构对于我的分布式修改太合适了 4 | */ 5 | package main 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "io/ioutil" 11 | "net/http" 12 | "net/url" 13 | "sort" 14 | "strconv" 15 | "strings" 16 | "time" 17 | 18 | "github.com/bluele/gcache" 19 | "github.com/gorilla/mux" 20 | "github.com/gorilla/websocket" 21 | "github.com/qiniu/log" 22 | ) 23 | 24 | type Cluster struct { 25 | slaves gcache.Cache 26 | client *http.Client 27 | suv *Supervisor 28 | } 29 | 30 | func (cluster *Cluster) join() { 31 | data := url.Values{"slave": []string{cfg.Server.Addr}} 32 | request, err := http.NewRequest(http.MethodPost, "http://"+cfg.Server.Master+"/distributed/join", strings.NewReader(data.Encode())) 33 | request.Header.Add("Content-Type", "application/x-www-form-urlencoded") 34 | request.Header.Add("Content-Length", strconv.Itoa(len(data.Encode()))) 35 | 36 | if err != nil { 37 | log.Errorf("join cluster %s : %s", cfg.Server.Master, err) 38 | return 39 | } 40 | cluster.auth(request) 41 | resp, err := cluster.client.Do(request) 42 | if err != nil { 43 | log.Errorf("join cluster %s : %s", cfg.Server.Master, err) 44 | return 45 | } 46 | if resp.StatusCode == http.StatusOK { 47 | log.Debugf("join to master %s", cfg.Server.Master) 48 | } else { 49 | log.Debugf("join to master %s error: %d", cfg.Server.Master, resp.StatusCode) 50 | } 51 | } 52 | 53 | func (cluster *Cluster) auth(request *http.Request) { 54 | if cfg.Server.HttpAuth.Enabled { 55 | request.SetBasicAuth(cfg.Server.HttpAuth.User, cfg.Server.HttpAuth.Password) 56 | } 57 | } 58 | 59 | func (cluster *Cluster) dialWebSocket(wsUrl string) (*websocket.Conn, *http.Response, error) { 60 | var dialer *websocket.Dialer 61 | if cfg.Server.HttpAuth.Enabled { 62 | dialer = &websocket.Dialer{Proxy: func(r *http.Request) (*url.URL, error) { 63 | cluster.auth(r) 64 | return websocket.DefaultDialer.Proxy(r) 65 | }} 66 | } else { 67 | dialer = websocket.DefaultDialer 68 | } 69 | return dialer.Dial(wsUrl, nil) 70 | } 71 | 72 | func (cluster *Cluster) requestSlave(url, method string, bodyBuffer *bytes.Buffer) ([]byte, error) { 73 | request, err := http.NewRequest(method, url, nil) 74 | if err != nil { 75 | return nil, err 76 | } 77 | cluster.auth(request) 78 | resp, err := cluster.client.Do(request) 79 | if err != nil { 80 | return nil, err 81 | } 82 | defer resp.Body.Close() 83 | return ioutil.ReadAll(resp.Body) 84 | } 85 | 86 | func (cluster *Cluster) cmdJoinCluster(w http.ResponseWriter, r *http.Request) { 87 | slave := r.PostFormValue("slave") 88 | if slave == "" { 89 | w.WriteHeader(http.StatusForbidden) 90 | return 91 | } 92 | if strings.HasPrefix(slave, ":") { 93 | idx := strings.LastIndex(r.RemoteAddr, ":") 94 | slave = r.RemoteAddr[:idx] + slave 95 | } 96 | log.Debugf("%s join cluster.", slave) 97 | if out, err := cluster.slaves.Get(slave); err != nil || out == nil { 98 | cluster.suv.broadcastEvent("new slave : " + slave) 99 | } 100 | cluster.slaves.Set(slave, slave) 101 | w.WriteHeader(http.StatusOK) 102 | } 103 | 104 | //获取分布式系统下所有的内容 105 | func (cluster *Cluster) cmdQueryDistributedPrograms(w http.ResponseWriter, r *http.Request) { 106 | 107 | w.Header().Set("Content-Type", "application/json") 108 | slaves := []string{} 109 | for _, v := range cluster.slaves.GetALL() { 110 | if slave, ok := v.(string); ok { 111 | slaves = append(slaves, slave) 112 | } 113 | } 114 | sort.Strings(slaves) 115 | jsonOut := "{" 116 | idx := 0 117 | for _, slave := range slaves { 118 | reqUrl := fmt.Sprintf("http://%s/api/programs", slave) 119 | if body, err := cluster.requestSlave(reqUrl, http.MethodGet, nil); err == nil { 120 | jsonOut += fmt.Sprintf("\"%s\":%s", slave, body) 121 | } 122 | if idx < cluster.slaves.Len()-1 { 123 | jsonOut += "," 124 | } 125 | idx += 1 126 | } 127 | jsonOut += "}" 128 | w.Write([]byte(jsonOut)) 129 | } 130 | 131 | func (cluster *Cluster) cmdSetting(w http.ResponseWriter, r *http.Request) { 132 | name := mux.Vars(r)["name"] 133 | slave := mux.Vars(r)["slave"] 134 | cluster.suv.renderHTML(w, "setting", map[string]string{ 135 | "Name": name, 136 | "Slave": slave, 137 | }) 138 | } 139 | 140 | func (cluster *Cluster) cmdWebSocketProxy(w http.ResponseWriter, r *http.Request) { 141 | sock, err := upgrader.Upgrade(w, r, nil) 142 | if err != nil { 143 | log.Error("upgrade:", err) 144 | return 145 | } 146 | defer sock.Close() 147 | 148 | slave := mux.Vars(r)["slave"] 149 | slaveUri := strings.Replace(r.RequestURI, "/distributed/"+slave, "", 1) 150 | wsUrl := fmt.Sprintf("ws://%s%s", slave, slaveUri) 151 | log.Infof("proxy websocket :%s", wsUrl) 152 | 153 | ws, _, err := cluster.dialWebSocket(wsUrl) 154 | if err != nil { 155 | log.Error("dial:", err) 156 | return 157 | } 158 | defer ws.Close() 159 | 160 | for { 161 | messageType, data, err := ws.ReadMessage() 162 | if err != nil { 163 | log.Error("read message:", err) 164 | return 165 | } 166 | if messageType == websocket.CloseMessage { 167 | log.Infof("close socket") 168 | return 169 | } 170 | w, err := sock.NextWriter(messageType) 171 | if err != nil { 172 | log.Error("write err:", err) 173 | return 174 | } 175 | _, err = w.Write(data) 176 | if err != nil { 177 | log.Error("read:", err) 178 | return 179 | } 180 | } 181 | } 182 | 183 | func (cluster *Cluster) slaveHttpProxy(w http.ResponseWriter, r *http.Request) { 184 | slave := mux.Vars(r)["slave"] 185 | slaveUri := strings.Replace(r.RequestURI, "/distributed/"+slave, "", 1) 186 | requestUrl := fmt.Sprintf("http://%s%s", slave, slaveUri) 187 | log.Infof("proxy :%s %s", r.Method, requestUrl) 188 | 189 | request, err := http.NewRequest(r.Method, requestUrl, r.Body) 190 | for k, v := range r.Header { 191 | request.Header.Set(k, strings.Join(v, ",")) 192 | } 193 | if err != nil { 194 | log.Error(err) 195 | } 196 | cluster.auth(request) 197 | resp, err := cluster.client.Do(request) 198 | if err != nil { 199 | log.Error(err) 200 | } 201 | defer resp.Body.Close() 202 | 203 | if body, err := ioutil.ReadAll(resp.Body); err != nil { 204 | w.WriteHeader(http.StatusInternalServerError) 205 | w.Write([]byte(err.Error())) 206 | } else { 207 | for k, v := range resp.Header { 208 | w.Header().Set(k, strings.Join(v, ",")) 209 | } 210 | w.Write(body) 211 | cluster.suv.broadcastEvent("execute ok : " + slaveUri) 212 | } 213 | } 214 | 215 | func newDistributed(suv *Supervisor, hdlr http.Handler) error { 216 | cluster.suv = suv 217 | 218 | r := hdlr.(*mux.Router) 219 | r.HandleFunc("/distributed/join", cluster.cmdJoinCluster).Methods("POST") 220 | r.HandleFunc("/distributed/api/programs", cluster.cmdQueryDistributedPrograms).Methods("GET") 221 | r.HandleFunc("/distributed/{slave}/settings/{name}", cluster.cmdSetting) 222 | for _, path := range []string{ 223 | "/distributed/{slave}/api/programs", "/distributed/{slave}/api/programs/{name}", 224 | "/distributed/{slave}/api/programs/{name}/start", "/distributed/{slave}/api/programs/{name}/stop", 225 | } { 226 | r.HandleFunc(path, cluster.slaveHttpProxy) 227 | } 228 | r.HandleFunc("/distributed/{slave}/ws/logs/{name}", cluster.cmdWebSocketProxy) 229 | r.HandleFunc("/distributed/{slave}/ws/perfs/{name}", cluster.cmdWebSocketProxy) 230 | 231 | if cfg.Server.Master != "" { 232 | go func() { 233 | t1 := time.NewTimer(time.Second) 234 | for { 235 | select { 236 | case <-t1.C: 237 | cluster.join() 238 | t1.Reset(time.Second) 239 | } 240 | } 241 | }() 242 | } 243 | return nil 244 | } 245 | 246 | var cluster = Cluster{ 247 | slaves: gcache.New(1000).LRU().Expiration(time.Second * 3).Build(), 248 | client: new(http.Client), 249 | } 250 | -------------------------------------------------------------------------------- /docs/gosuv.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gosuv/3744b1b28ce4589d3c84880e22fb5b9f0e6727eb/docs/gosuv.gif -------------------------------------------------------------------------------- /docs/states.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gosuv/3744b1b28ce4589d3c84880e22fb5b9f0e6727eb/docs/states.png -------------------------------------------------------------------------------- /fsm.go: -------------------------------------------------------------------------------- 1 | // 研究了一天的状态机,天气也热,正当我写的即将昏迷之际,我突然醒悟了,原来状态机是这么一回事 2 | // - 状态比喻成 数据结构 3 | // - 事件比喻成 用户输入 4 | // - 状态转移则是函数调用 5 | // 如此依赖写成函数,也就是 (Orz 原来如此) 6 | // type FSM struct { 7 | // State FSMState 8 | // TransformFuncs map[FSMState] func() 9 | // } 10 | 11 | // func (f *FSM) UserAction(action FSMAction) { 12 | // ... 13 | // } 14 | 15 | package main 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "io" 21 | "io/ioutil" 22 | "os" 23 | "os/user" 24 | "path/filepath" 25 | "runtime" 26 | "strconv" 27 | "strings" 28 | "sync" 29 | "syscall" 30 | "time" 31 | 32 | "github.com/axgle/pinyin" 33 | "github.com/kennygrant/sanitize" 34 | "github.com/lunny/dingtalk_webhook" 35 | "github.com/natefinch/lumberjack" 36 | "github.com/qiniu/log" 37 | "github.com/soopsio/gosuv/pushover" 38 | "github.com/soopsio/kexec" 39 | ) 40 | 41 | type FSMState string 42 | type FSMEvent string 43 | type FSMHandler func() 44 | 45 | type FSM struct { 46 | mu sync.Mutex 47 | state FSMState 48 | handlers map[FSMState]map[FSMEvent]FSMHandler 49 | 50 | StateChange func(oldState, newState FSMState) 51 | } 52 | 53 | func (f *FSM) AddHandler(state FSMState, event FSMEvent, hdlr FSMHandler) *FSM { 54 | _, ok := f.handlers[state] 55 | if !ok { 56 | f.handlers[state] = make(map[FSMEvent]FSMHandler) 57 | } 58 | if _, ok = f.handlers[state][event]; ok { 59 | log.Fatalf("set twice for state(%s) event(%s)", state, event) 60 | } 61 | f.handlers[state][event] = hdlr 62 | return f 63 | } 64 | 65 | func (f *FSM) State() FSMState { 66 | return f.state 67 | } 68 | 69 | func (f *FSM) SetState(newState FSMState) { 70 | if f.StateChange != nil { 71 | f.StateChange(f.state, newState) 72 | } 73 | f.state = newState 74 | } 75 | 76 | func (f *FSM) Operate(event FSMEvent) FSMState { 77 | f.mu.Lock() 78 | defer f.mu.Unlock() 79 | 80 | eventMap := f.handlers[f.State()] 81 | if eventMap == nil { 82 | return f.State() 83 | } 84 | if fn, ok := eventMap[event]; ok { 85 | fn() 86 | } 87 | return f.State() 88 | } 89 | 90 | func NewFSM(initState FSMState) *FSM { 91 | return &FSM{ 92 | state: initState, 93 | handlers: make(map[FSMState]map[FSMEvent]FSMHandler), 94 | } 95 | } 96 | 97 | // Only 4 states now is enough, I think 98 | // 2016-09-18 now five 99 | var ( 100 | Running = FSMState("running") 101 | Stopped = FSMState("stopped") 102 | Fatal = FSMState("fatal") 103 | RetryWait = FSMState("retry wait") 104 | Stopping = FSMState("stopping") 105 | 106 | StartEvent = FSMEvent("start") 107 | StopEvent = FSMEvent("stop") 108 | RestartEvent = FSMEvent("restart") 109 | ) 110 | 111 | type Program struct { 112 | Name string `yaml:"name" json:"name"` 113 | Command string `yaml:"command" json:"command"` 114 | Environ []string `yaml:"environ" json:"environ"` 115 | Dir string `yaml:"directory" json:"directory"` 116 | StartAuto bool `yaml:"start_auto" json:"startAuto"` 117 | StartRetries int `yaml:"start_retries" json:"startRetries"` 118 | StartSeconds int `yaml:"start_seconds,omitempty" json:"startSeconds"` 119 | StopTimeout int `yaml:"stop_timeout,omitempty" json:"stopTimeout"` 120 | retryCount int 121 | User string `yaml:"user,omitempty" json:"user"` 122 | Notifications Notifications `yaml:"notifications,omitempty" json:"-"` 123 | WebHook WebHook `yaml:"webhook,omitempty" json:"-"` 124 | } 125 | 126 | type Notifications struct { 127 | Pushover struct { 128 | ApiKey string `yaml:"api_key"` 129 | Users []string `yaml:"users"` 130 | } `yaml:"pushover,omitempty"` 131 | 132 | Dingtalk struct { 133 | Groups []struct { 134 | Secret string `yaml:"secret"` 135 | Mobiles []string `yaml:"mobile"` 136 | } `yaml:"groups"` 137 | } `yaml:"dingtalk,omitempty"` 138 | } 139 | 140 | type WebHook struct { 141 | Github struct { 142 | Secret string `yaml:"secret"` 143 | } `yaml:"github"` 144 | Command string `yaml:"command"` 145 | Timeout int `yaml:"timeout"` 146 | } 147 | 148 | func (p *Program) Check() error { 149 | if p.Name == "" { 150 | return errors.New("Program name empty") 151 | } 152 | if p.Command == "" { 153 | return errors.New("Program command empty") 154 | } 155 | // Disable check, for Dir may contains env-vars 156 | //if p.Dir != "" && !IsDir(p.Dir) { 157 | // return fmt.Errorf("Program dir(%s) not exists", p.Dir) 158 | //} 159 | return nil 160 | } 161 | 162 | func (p *Program) RunNotification(state FSMState) { 163 | notis := []Notifications{} 164 | notis = append(notis, cfg.Notifications) 165 | t := time.Now().Format("2006-01-02 15:04:05") 166 | host := "" 167 | if cfg.Server.Name != "" { 168 | host = cfg.Server.Name 169 | } else { 170 | host, _ = os.Hostname() 171 | } 172 | msg := fmt.Sprintf("[%s] %s: \"%s\" changed: \"%s\"", t, host, p.Name, state) 173 | if state == RetryWait { 174 | msg += " retryCount:" + strconv.Itoa(p.retryCount) 175 | } 176 | for _, noti := range notis { 177 | po := noti.Pushover 178 | if po.ApiKey != "" && len(po.Users) > 0 { 179 | for _, user := range po.Users { 180 | err := pushover.Notify(pushover.Params{ 181 | Token: po.ApiKey, 182 | User: user, 183 | Title: "gosuv", 184 | Message: msg, 185 | }) 186 | if err != nil { 187 | log.Warnf("pushover error: %v", err) 188 | } 189 | } 190 | } 191 | 192 | pw := noti.Dingtalk 193 | if len(pw.Groups) > 0 { 194 | for _, group := range pw.Groups { 195 | ding := dingtalk.NewWebhook(group.Secret) 196 | err := ding.SendTextMsg(msg, false, group.Mobiles...) 197 | if err != nil { 198 | log.Error("钉钉通知失败:", msg, err) 199 | } 200 | } 201 | } 202 | } 203 | } 204 | 205 | func IsRoot() bool { 206 | u, err := user.Current() 207 | return err == nil && u.Username == "root" 208 | } 209 | 210 | type Process struct { 211 | *FSM `json:"-"` 212 | Program `json:"program"` 213 | cmd *kexec.KCommand 214 | Stdout *QuickLossBroadcastWriter `json:"-"` 215 | Stderr *QuickLossBroadcastWriter `json:"-"` 216 | Output *QuickLossBroadcastWriter `json:"-"` 217 | OutputFile io.WriteCloser `json:"-"` 218 | stopC chan syscall.Signal 219 | retryLeft int 220 | Status string `json:"status"` 221 | 222 | mu sync.Mutex 223 | } 224 | 225 | // FIXME(ssx): maybe need to return error 226 | func (p *Process) buildCommand() *kexec.KCommand { 227 | cmd := kexec.CommandString(p.Command) 228 | // cmd := kexec.Command(p.Command[0], p.Command[1:]...) 229 | logDir := filepath.Join(defaultGosuvDir, "log", sanitize.Name(pinyin.Convert(p.Name))) 230 | if !IsDir(logDir) { 231 | os.MkdirAll(logDir, 0755) 232 | } 233 | var fout io.Writer 234 | var err error 235 | // p.OutputFile, err = os.OpenFile(filepath.Join(logDir, "output.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) 236 | // p.OutputFile = NewRotate(filepath.Join(logDir, "output.log")) 237 | p.OutputFile = &lumberjack.Logger{ 238 | Filename: filepath.Join(logDir, "output.log"), 239 | MaxSize: 1024, 240 | MaxAge: 14, 241 | MaxBackups: 14, 242 | Compress: false, 243 | LocalTime: true, 244 | } 245 | 246 | if err != nil { 247 | log.Warn("create stdout log failed:", err) 248 | fout = ioutil.Discard 249 | } else { 250 | fout = p.OutputFile 251 | } 252 | cmd.Stdout = io.MultiWriter(p.Stdout, p.Output, fout) 253 | cmd.Stderr = io.MultiWriter(p.Stderr, p.Output, fout) 254 | // config environ 255 | cmd.Env = os.Environ() // inherit current vars 256 | environ := map[string]string{} 257 | if p.User != "" { 258 | if !IsRoot() { 259 | log.Warnf("detect not root, can not switch user") 260 | } else if err := cmd.SetUser(p.User); err != nil { 261 | log.Warnf("[%s] chusr to %s failed, %v", p.Name, p.User, err) 262 | } else { 263 | var homeDir string 264 | switch runtime.GOOS { 265 | case "linux": 266 | homeDir = "/home/" + p.User // FIXME(ssx): maybe there is a better way 267 | case "darwin": 268 | homeDir = "/Users/" + p.User 269 | } 270 | cmd.Env = append(cmd.Env, "HOME="+homeDir, "USER="+p.User) 271 | environ["HOME"] = homeDir 272 | environ["USER"] = p.User 273 | } 274 | } 275 | cmd.Env = append(cmd.Env, p.Environ...) 276 | mapping := func(key string) string { 277 | val := os.Getenv(key) 278 | if val != "" { 279 | return val 280 | } 281 | return environ[key] 282 | } 283 | cmd.Dir = os.Expand(p.Dir, mapping) 284 | if strings.HasPrefix(cmd.Dir, "~") { 285 | cmd.Dir = mapping("HOME") + cmd.Dir[1:] 286 | } 287 | log.Infof("[%s] use dir: %s\n", p.Name, cmd.Dir) 288 | return cmd 289 | } 290 | 291 | func (p *Process) waitNextRetry() { 292 | if p.OutputFile != nil { 293 | p.OutputFile.Close() 294 | } 295 | p.SetState(RetryWait) 296 | if p.retryLeft <= 0 { 297 | p.retryLeft = p.StartRetries 298 | p.SetState(Fatal) 299 | p.cmd = nil 300 | return 301 | } 302 | p.retryLeft -= 1 303 | select { 304 | case <-time.After(2 * time.Second): // TODO: need put it into Program 305 | p.startCommand() 306 | case <-p.stopC: 307 | p.stopCommand() 308 | } 309 | } 310 | 311 | func (p *Process) stopCommand() { 312 | p.mu.Lock() 313 | defer p.mu.Unlock() 314 | defer p.SetState(Stopped) 315 | if p.cmd == nil { 316 | return 317 | } 318 | 319 | p.SetState(Stopping) 320 | if p.cmd.Process != nil { 321 | stopch := make(chan bool) 322 | go func() { 323 | p.cmd.Process.Signal(syscall.SIGTERM) 324 | stopch <- true 325 | }() 326 | select { 327 | case <-stopch: // TODO(ssx): add it to config 328 | log.Println(p.Name, "停止完成") 329 | case <-time.After(10 * time.Second): 330 | log.Println(p.Name, "停止超时,强制 kill") 331 | p.cmd.Process.Signal(syscall.SIGKILL) 332 | } 333 | } 334 | select { 335 | case <-GoFunc(p.cmd.Wait): 336 | p.RunNotification(FSMState("quit normally")) 337 | log.Printf("program(%s) quit normally", p.Name) 338 | case <-time.After(time.Duration(p.StopTimeout) * time.Second): // TODO: add 3s to config 339 | p.RunNotification(FSMState("terminate all")) 340 | log.Printf("program(%s) terminate all", p.Name) 341 | p.cmd.Terminate(syscall.SIGKILL) // cleanup 342 | } 343 | err := p.cmd.Wait() // This is OK, because Signal KILL will definitely work 344 | prefixStr := "\n--- GOSUV LOG " + time.Now().Format("2006-01-02 15:04:05") 345 | if err == nil { 346 | io.WriteString(p.cmd.Stderr, fmt.Sprintf("%s exit success ---\n\n", prefixStr)) 347 | } else { 348 | io.WriteString(p.cmd.Stderr, fmt.Sprintf("%s exit %v ---\n\n", prefixStr, err)) 349 | } 350 | if p.OutputFile != nil { 351 | p.OutputFile.Close() 352 | p.OutputFile = nil 353 | } 354 | p.cmd = nil 355 | } 356 | 357 | func (p *Process) IsRunning() bool { 358 | return p.State() == Running || p.State() == RetryWait 359 | } 360 | 361 | func (p *Process) startCommand() { 362 | // p.Stdout.Reset() 363 | // p.Stderr.Reset() 364 | // p.Output.Reset() // Donot reset because log is still needed. 365 | log.Printf("start cmd(%s): %s", p.Name, p.Command) 366 | p.cmd = p.buildCommand() 367 | 368 | p.SetState(Running) 369 | if err := p.cmd.Start(); err != nil { 370 | log.Warnf("program %s start failed: %v", p.Name, err) 371 | p.SetState(Fatal) 372 | p.cmd = nil 373 | return 374 | } 375 | // 如果是running状态,重置 retryLeft 376 | p.retryLeft = p.StartRetries 377 | go func() { 378 | errC := GoFunc(p.cmd.Wait) 379 | startTime := time.Now() 380 | select { 381 | case <-errC: 382 | // if p.cmd.Wait() returns, it means program and its sub process all quited. no need to kill again 383 | // func Wait() will only return when program session finishs. (Only Tested on mac) 384 | log.Printf("program(%s) finished, time used %v", p.Name, time.Since(startTime)) 385 | if time.Since(startTime) < time.Duration(p.StartSeconds)*time.Second { 386 | if p.retryLeft == p.StartRetries { // If first time quit so fast, just set to fatal 387 | p.SetState(Fatal) 388 | p.cmd = nil 389 | p.RunNotification(Fatal) 390 | log.Printf("program(%s) exit too quick, status -> fatal", p.Name) 391 | return 392 | } 393 | } 394 | 395 | p.waitNextRetry() 396 | case <-p.stopC: 397 | log.Println("recv stop command") 398 | p.stopCommand() // clean up all process 399 | } 400 | 401 | }() 402 | } 403 | 404 | func NewProcess(pg Program) *Process { 405 | outputBufferSize := 24 * 1024 // 24K 406 | pr := &Process{ 407 | FSM: NewFSM(Stopped), 408 | Program: pg, 409 | stopC: make(chan syscall.Signal), 410 | retryLeft: pg.StartRetries, 411 | Status: string(Stopped), 412 | Output: NewQuickLossBroadcastWriter(outputBufferSize), 413 | Stdout: NewQuickLossBroadcastWriter(outputBufferSize), 414 | Stderr: NewQuickLossBroadcastWriter(outputBufferSize), 415 | } 416 | pr.StateChange = func(_, newStatus FSMState) { 417 | pr.Status = string(newStatus) 418 | // TODO: status need to filter with config, not hard coded. 419 | // if newStatus == Fatal { 420 | if newStatus == RetryWait { 421 | pr.Program.retryCount++ 422 | } 423 | go pr.Program.RunNotification(newStatus) 424 | // } 425 | } 426 | if pr.StartSeconds <= 0 { 427 | pr.StartSeconds = 3 428 | } 429 | if pr.StopTimeout <= 0 { 430 | pr.StopTimeout = 3 431 | } 432 | 433 | pr.AddHandler(Stopped, StartEvent, func() { 434 | pr.retryLeft = pr.StartRetries 435 | pr.startCommand() 436 | }) 437 | pr.AddHandler(Fatal, StartEvent, pr.startCommand) 438 | 439 | pr.AddHandler(Running, StopEvent, func() { 440 | select { 441 | case pr.stopC <- syscall.SIGTERM: 442 | case <-time.After(200 * time.Millisecond): 443 | } 444 | }).AddHandler(Running, RestartEvent, func() { 445 | go func() { 446 | pr.Operate(StopEvent) 447 | // TODO: start laterly 448 | time.Sleep(1 * time.Second) 449 | pr.Operate(StartEvent) 450 | }() 451 | }) 452 | return pr 453 | } 454 | 455 | func init() { 456 | // pg := Program{ 457 | // Name: "demo", 458 | // Command: "echo hello world && sleep 1 && echo end", 459 | // } 460 | // proc := NewProcess(pg) 461 | // log.Println(proc.State()) 462 | 463 | // proc.Operate(StartEvent) 464 | // log.Println(proc.State()) 465 | // time.Sleep(2 * time.Second) 466 | // log.Println(proc.State()) 467 | // proc.Operate(StopEvent) 468 | // time.Sleep(1 * time.Second) 469 | // log.Println(proc.State()) 470 | // log.Println(light.State) 471 | // light.AddHandler(Opened, Close, func() { 472 | // log.Println("Close light") 473 | // light.State = Closed 474 | // }) 475 | // light.Operate(Close) 476 | // log.Println(light.State) 477 | } 478 | -------------------------------------------------------------------------------- /fsm_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "strconv" 7 | "testing" 8 | "time" 9 | 10 | . "github.com/smartystreets/goconvey/convey" 11 | ) 12 | 13 | func findProcess(name string) bool { 14 | name = fmt.Sprintf("[%c]%s", name[0], name[1:]) 15 | c := exec.Command("bash", "-c", fmt.Sprintf("ps -eo command | grep %s", strconv.Quote(name))) 16 | output, err := c.CombinedOutput() 17 | if err == nil { 18 | So(string(output), ShouldNotEqual, "") 19 | return true 20 | } 21 | return false 22 | } 23 | 24 | func TestStopCommand(t *testing.T) { 25 | Convey("Stop command should clean up all program", t, func() { 26 | p := NewProcess(Program{ 27 | Name: "sleep", 28 | Command: "(echo hello; sleep 17&); exit 1", 29 | StopTimeout: 1, 30 | }) 31 | p.startCommand() 32 | time.Sleep(100 * time.Millisecond) 33 | p.stopCommand() 34 | So(p.cmd, ShouldBeNil) 35 | exists := findProcess("sleep 17") 36 | So(exists, ShouldBeFalse) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /get.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | # Code generated by godownloader. DO NOT EDIT. 4 | # 5 | 6 | usage() { 7 | this=$1 8 | cat </dev/null 118 | } 119 | uname_os() { 120 | os=$(uname -s | tr '[:upper:]' '[:lower:]') 121 | echo "$os" 122 | } 123 | uname_arch() { 124 | arch=$(uname -m) 125 | case $arch in 126 | x86_64) arch="amd64" ;; 127 | x86) arch="386" ;; 128 | i686) arch="386" ;; 129 | i386) arch="386" ;; 130 | aarch64) arch="arm64" ;; 131 | armv5*) arch="armv5" ;; 132 | armv6*) arch="armv6" ;; 133 | armv7*) arch="armv7" ;; 134 | esac 135 | echo ${arch} 136 | } 137 | uname_os_check() { 138 | os=$(uname_os) 139 | case "$os" in 140 | darwin) return 0 ;; 141 | dragonfly) return 0 ;; 142 | freebsd) return 0 ;; 143 | linux) return 0 ;; 144 | android) return 0 ;; 145 | nacl) return 0 ;; 146 | netbsd) return 0 ;; 147 | openbsd) return 0 ;; 148 | plan9) return 0 ;; 149 | solaris) return 0 ;; 150 | windows) return 0 ;; 151 | esac 152 | echo "$0: uname_os_check: internal error '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" 153 | return 1 154 | } 155 | uname_arch_check() { 156 | arch=$(uname_arch) 157 | case "$arch" in 158 | 386) return 0 ;; 159 | amd64) return 0 ;; 160 | arm64) return 0 ;; 161 | armv5) return 0 ;; 162 | armv6) return 0 ;; 163 | armv7) return 0 ;; 164 | ppc64) return 0 ;; 165 | ppc64le) return 0 ;; 166 | mips) return 0 ;; 167 | mipsle) return 0 ;; 168 | mips64) return 0 ;; 169 | mips64le) return 0 ;; 170 | s390x) return 0 ;; 171 | amd64p32) return 0 ;; 172 | esac 173 | echo "$0: uname_arch_check: internal error '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" 174 | return 1 175 | } 176 | untar() { 177 | tarball=$1 178 | case "${tarball}" in 179 | *.tar.gz | *.tgz) tar -xzf "${tarball}" ;; 180 | *.tar) tar -xf "${tarball}" ;; 181 | *.zip) unzip "${tarball}" ;; 182 | *) 183 | echo "Unknown archive format for ${tarball}" 184 | return 1 185 | ;; 186 | esac 187 | } 188 | mktmpdir() { 189 | test -z "$TMPDIR" && TMPDIR="$(mktemp -d)" 190 | mkdir -p "${TMPDIR}" 191 | echo "${TMPDIR}" 192 | } 193 | http_download() { 194 | local_file=$1 195 | source_url=$2 196 | header=$3 197 | headerflag='' 198 | destflag='' 199 | if is_command curl; then 200 | cmd='curl --fail -sSL' 201 | destflag='-o' 202 | headerflag='-H' 203 | elif is_command wget; then 204 | cmd='wget -q' 205 | destflag='-O' 206 | headerflag='--header' 207 | else 208 | echo "http_download: unable to find wget or curl" 209 | return 1 210 | fi 211 | if [ -z "$header" ]; then 212 | $cmd $destflag "$local_file" "$source_url" 213 | else 214 | $cmd $headerflag "$header" $destflag "$local_file" "$source_url" 215 | fi 216 | } 217 | github_api() { 218 | local_file=$1 219 | source_url=$2 220 | header="" 221 | case "$source_url" in 222 | https://api.github.com*) 223 | test -z "$GITHUB_TOKEN" || header="Authorization: token $GITHUB_TOKEN" 224 | ;; 225 | esac 226 | http_download "$local_file" "$source_url" "$header" 227 | } 228 | github_last_release() { 229 | owner_repo=$1 230 | giturl="https://api.github.com/repos/${owner_repo}/releases/latest" 231 | html=$(github_api - "$giturl") 232 | version=$(echo "$html" | grep -m 1 "\"tag_name\":" | cut -f4 -d'"') 233 | test -z "$version" && return 1 234 | echo "$version" 235 | } 236 | hash_sha256() { 237 | TARGET=${1:-/dev/stdin} 238 | if is_command gsha256sum; then 239 | hash=$(gsha256sum "$TARGET") || return 1 240 | echo "$hash" | cut -d ' ' -f 1 241 | elif is_command sha256sum; then 242 | hash=$(sha256sum "$TARGET") || return 1 243 | echo "$hash" | cut -d ' ' -f 1 244 | elif is_command shasum; then 245 | hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1 246 | echo "$hash" | cut -d ' ' -f 1 247 | elif is_command openssl; then 248 | hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1 249 | echo "$hash" | cut -d ' ' -f a 250 | else 251 | echo "hash_sha256: unable to find command to compute sha-256 hash" 252 | return 1 253 | fi 254 | } 255 | hash_sha256_verify() { 256 | TARGET=$1 257 | checksums=$2 258 | if [ -z "$checksums" ]; then 259 | echo "hash_sha256_verify: checksum file not specified in arg2" 260 | return 1 261 | fi 262 | BASENAME=${TARGET##*/} 263 | want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1) 264 | if [ -z "$want" ]; then 265 | echo "hash_sha256_verify: unable to find checksum for '${TARGET}' in '${checksums}'" 266 | return 1 267 | fi 268 | got=$(hash_sha256 "$TARGET") 269 | if [ "$want" != "$got" ]; then 270 | echo "hash_sha256_verify: checksum for '$TARGET' did not verify ${want} vs $got" 271 | return 1 272 | fi 273 | } 274 | cat /dev/null < 0 { 125 | index := bytes.IndexByte(p, '\n') 126 | if index != -1 { 127 | // 写完完整的一行 128 | _, err = b.Buffer.Write(p[0 : index+1]) 129 | if err != nil { 130 | log.Error(err, "Writer Buffer failed") 131 | return n, err 132 | } 133 | 134 | // 将buffer转移到merge中 135 | b.merge.WriteLine(b.Buffer) 136 | 137 | // 分配:写入新数据 138 | b.Buffer = bufferPool.Get() 139 | b.Buffer.WriteString(time.Now().Format("15:04:05") + b.prefix) 140 | p = p[index+1:] 141 | } else { 142 | // 剩下不足一行,一口气全部写入 143 | _, err = b.Buffer.Write(p) 144 | if err != nil { 145 | log.Error(err, "Writer Buffer failed") 146 | return n, err 147 | } 148 | break 149 | } 150 | } 151 | return n, nil 152 | } 153 | -------------------------------------------------------------------------------- /pushover/pushover.go: -------------------------------------------------------------------------------- 1 | package pushover 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "net/url" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | const ( 15 | // API is the Pushover API endpoint. 16 | API = "https://api.pushover.net/1/messages.json" 17 | ) 18 | 19 | type apiResponse struct { 20 | Info string `json:"info"` 21 | Status int `json:"status"` 22 | Request string `json:"request"` 23 | Errors []string `json:"errors"` 24 | Token string `json:"token"` 25 | } 26 | 27 | type Params struct { 28 | Token string 29 | User string 30 | Title string 31 | Message string 32 | } 33 | 34 | // Notify sends a push request to the Pushover API. 35 | func Notify(p Params) error { 36 | vals := make(url.Values) 37 | vals.Set("token", p.Token) 38 | vals.Set("user", p.User) 39 | vals.Set("message", p.Message) 40 | vals.Set("title", p.Title) 41 | 42 | log.Println(vals.Encode()) 43 | webClient := &http.Client{Timeout: 30 * time.Second} 44 | resp, err := webClient.PostForm(API, vals) 45 | if err != nil { 46 | return err 47 | } 48 | defer resp.Body.Close() 49 | log.Println("posted") 50 | 51 | var r apiResponse 52 | if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { 53 | return fmt.Errorf("decoding response: %s", err) 54 | } 55 | 56 | if r.Status != 1 { 57 | return errors.New(strings.Join(r.Errors, ": ")) //noti.APIError{Site: "Pushover", Msg: strings.Join(r.Errors, ": ")} 58 | } else if strings.Contains(r.Info, "no active devices") { 59 | return errors.New(r.Info) //noti.APIError{Site: "Pushover", Msg: r.Info} 60 | } 61 | 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /res/bootstrap-3.3.5/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.5 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-danger.disabled,.btn-danger[disabled],.btn-default.disabled,.btn-default[disabled],.btn-info.disabled,.btn-info[disabled],.btn-primary.disabled,.btn-primary[disabled],.btn-success.disabled,.btn-success[disabled],.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-danger,fieldset[disabled] .btn-default,fieldset[disabled] .btn-info,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-success,fieldset[disabled] .btn-warning{-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.btn.active,.btn:active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:focus,.btn-default:hover{background-color:#e0e0e0;background-position:0 -15px}.btn-default.active,.btn-default:active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:focus,.btn-primary:hover{background-color:#265a88;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:focus,.btn-success:hover{background-color:#419641;background-position:0 -15px}.btn-success.active,.btn-success:active{background-color:#419641;border-color:#3e8f3e}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:focus,.btn-info:hover{background-color:#2aabd2;background-position:0 -15px}.btn-info.active,.btn-info:active{background-color:#2aabd2;border-color:#28a4c9}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:focus,.btn-warning:hover{background-color:#eb9316;background-position:0 -15px}.btn-warning.active,.btn-warning:active{background-color:#eb9316;border-color:#e38d13}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:focus,.btn-danger:hover{background-color:#c12e2a;background-position:0 -15px}.btn-danger.active,.btn-danger:active{background-color:#c12e2a;border-color:#b92c28}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#c12e2a;background-image:none}.img-thumbnail,.thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-color:#2e6da4;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} -------------------------------------------------------------------------------- /res/bootstrap-3.3.5/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gosuv/3744b1b28ce4589d3c84880e22fb5b9f0e6727eb/res/bootstrap-3.3.5/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /res/bootstrap-3.3.5/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gosuv/3744b1b28ce4589d3c84880e22fb5b9f0e6727eb/res/bootstrap-3.3.5/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /res/bootstrap-3.3.5/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gosuv/3744b1b28ce4589d3c84880e22fb5b9f0e6727eb/res/bootstrap-3.3.5/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /res/bootstrap-3.3.5/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gosuv/3744b1b28ce4589d3c84880e22fb5b9f0e6727eb/res/bootstrap-3.3.5/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /res/bootstrap-3.3.5/js/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') -------------------------------------------------------------------------------- /res/css/style.css: -------------------------------------------------------------------------------- 1 | /* */ 2 | 3 | body { 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 5 | } 6 | 7 | .navbar { 8 | border-radius: 0px; 9 | background-color: #1b926c; 10 | border-color: #1fa67a; 11 | } 12 | 13 | .navbar .navbar-brand { 14 | color: white; 15 | } 16 | 17 | .navbar ul.navbar-nav>li>a { 18 | color: white; 19 | } 20 | 21 | .realtime-log { 22 | height: 40em; 23 | } 24 | 25 | .status { 26 | background-color: gray; 27 | border-radius: 0.3em; 28 | padding-left: 0.5em; 29 | padding-right: 0.5em; 30 | color: white; 31 | } 32 | 33 | small.user { 34 | color: #cccccc; 35 | } 36 | 37 | .color-red { 38 | color: red; 39 | } 40 | 41 | .panel-body{ 42 | padding: 0px; 43 | } 44 | 45 | .table { 46 | margin: 0px; 47 | } 48 | -------------------------------------------------------------------------------- /res/font-awesome-4.6.3/HELP-US-OUT.txt: -------------------------------------------------------------------------------- 1 | I hope you love Font Awesome. If you've found it useful, please do me a favor and check out my latest project, 2 | Fort Awesome (https://fortawesome.com). It makes it easy to put the perfect icons on your website. Choose from our awesome, 3 | comprehensive icon sets or copy and paste your own. 4 | 5 | Please. Check it out. 6 | 7 | -Dave Gandy 8 | -------------------------------------------------------------------------------- /res/font-awesome-4.6.3/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gosuv/3744b1b28ce4589d3c84880e22fb5b9f0e6727eb/res/font-awesome-4.6.3/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /res/font-awesome-4.6.3/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gosuv/3744b1b28ce4589d3c84880e22fb5b9f0e6727eb/res/font-awesome-4.6.3/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /res/font-awesome-4.6.3/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gosuv/3744b1b28ce4589d3c84880e22fb5b9f0e6727eb/res/font-awesome-4.6.3/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /res/font-awesome-4.6.3/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gosuv/3744b1b28ce4589d3c84880e22fb5b9f0e6727eb/res/font-awesome-4.6.3/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /res/font-awesome-4.6.3/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gosuv/3744b1b28ce4589d3c84880e22fb5b9f0e6727eb/res/font-awesome-4.6.3/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /res/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gosuv/3744b1b28ce4589d3c84880e22fb5b9f0e6727eb/res/images/favicon.ico -------------------------------------------------------------------------------- /res/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeskyblue/gosuv/3744b1b28ce4589d3c84880e22fb5b9f0e6727eb/res/images/favicon.png -------------------------------------------------------------------------------- /res/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Distributed Go Supervisor 1.0 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 44 |
45 |
46 | 49 |
50 | 57 |
58 |
59 |
60 | Master 61 | 62 | 63 | 64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 90 | 106 | 107 | 108 |
NameStatusViewCommand
80 | 83 | 84 | Profiles 85 | 86 | 89 | 91 | 95 | 99 | 102 | 105 |
109 |
110 |
111 |
112 |
113 | {{slave}} 114 | 115 | 119 | 120 |
121 |
122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 146 | 162 | 163 | 164 |
NameStatusViewCommand
136 | 139 | 140 | Profiles 141 | 142 | 145 | 147 | 151 | 155 | 158 | 161 |
165 |
166 |
167 |
168 |
169 | 179 |
180 | 181 | 182 | 231 | 232 | 276 | 277 | 298 |
299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | -------------------------------------------------------------------------------- /res/js/clipboard-1.5.12.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * clipboard.js v1.5.12 3 | * https://zenorocha.github.io/clipboard.js 4 | * 5 | * Licensed MIT © Zeno Rocha 6 | */ 7 | !function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.Clipboard=t()}}(function(){var t,e,n;return function t(e,n,o){function i(a,c){if(!n[a]){if(!e[a]){var s="function"==typeof require&&require;if(!c&&s)return s(a,!0);if(r)return r(a,!0);var l=new Error("Cannot find module '"+a+"'");throw l.code="MODULE_NOT_FOUND",l}var u=n[a]={exports:{}};e[a][0].call(u.exports,function(t){var n=e[a][1][t];return i(n?n:t)},u,u.exports,t,e,n,o)}return n[a].exports}for(var r="function"==typeof require&&require,a=0;ao;o++)n[o].fn.apply(n[o].ctx,e);return this},off:function(t,e){var n=this.e||(this.e={}),o=n[t],i=[];if(o&&e)for(var r=0,a=o.length;a>r;r++)o[r].fn!==e&&o[r].fn._!==e&&i.push(o[r]);return i.length?n[t]=i:delete n[t],this}},e.exports=o},{}],8:[function(e,n,o){!function(i,r){if("function"==typeof t&&t.amd)t(["module","select"],r);else if("undefined"!=typeof o)r(n,e("select"));else{var a={exports:{}};r(a,i.select),i.clipboardAction=a.exports}}(this,function(t,e){"use strict";function n(t){return t&&t.__esModule?t:{"default":t}}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}var i=n(e),r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol?"symbol":typeof t},a=function(){function t(t,e){for(var n=0;n" + text + ""; 237 | }; 238 | switch (value) { 239 | case "stopping": 240 | return makeColorText(value, "#996633"); 241 | case "running": 242 | return makeColorText(value, "green"); 243 | case "fatal": 244 | return makeColorText(value, "red"); 245 | default: 246 | return makeColorText(value, "gray"); 247 | } 248 | }); 249 | 250 | Vue.directive('disable', function (value) { 251 | this.el.disabled = !!value 252 | }); 253 | 254 | $(function () { 255 | vm.refresh(); 256 | 257 | function newEventWatcher() { 258 | W.events = newWebsocket("/ws/events", { 259 | onopen: function (evt) { 260 | vm.isConnectionAlive = true; 261 | }, 262 | onmessage: function (evt) { 263 | console.log("response:" + evt.data); 264 | vm.refresh(); 265 | }, 266 | onclose: function (evt) { 267 | W.events = null; 268 | vm.isConnectionAlive = false; 269 | console.log("Reconnect after 3s"); 270 | setTimeout(newEventWatcher, 3000) 271 | } 272 | }); 273 | }; 274 | 275 | newEventWatcher(); 276 | 277 | // cancel follow log if people want to see the original data 278 | $(".realtime-log").bind('mousewheel', function (evt) { 279 | if (evt.originalEvent.wheelDelta >= 0) { 280 | vm.log.follow = false; 281 | } 282 | }); 283 | $('#modalTailf').on('hidden.bs.modal', function () { 284 | // do something… 285 | console.log("Hiddeen"); 286 | if (W.wsLog) { 287 | console.log("wsLog closed"); 288 | W.wsLog.close() 289 | } 290 | }) 291 | }); -------------------------------------------------------------------------------- /res/js/jquery.qrcode.js: -------------------------------------------------------------------------------- 1 | (function( $ ){ 2 | $.fn.qrcode = function(options) { 3 | // if options is string, 4 | if( typeof options === 'string' ){ 5 | options = { text: options }; 6 | } 7 | 8 | // set default values 9 | // typeNumber < 1 for automatic calculation 10 | options = $.extend( {}, { 11 | render : "canvas", 12 | width : 256, 13 | height : 256, 14 | typeNumber : -1, 15 | correctLevel : QRErrorCorrectLevel.H, 16 | background : "#ffffff", 17 | foreground : "#000000" 18 | }, options); 19 | 20 | var createCanvas = function(){ 21 | // create the qrcode itself 22 | var qrcode = new QRCode(options.typeNumber, options.correctLevel); 23 | qrcode.addData(options.text); 24 | qrcode.make(); 25 | 26 | // create canvas element 27 | var canvas = document.createElement('canvas'); 28 | canvas.width = options.width; 29 | canvas.height = options.height; 30 | var ctx = canvas.getContext('2d'); 31 | 32 | // compute tileW/tileH based on options.width/options.height 33 | var tileW = options.width / qrcode.getModuleCount(); 34 | var tileH = options.height / qrcode.getModuleCount(); 35 | 36 | // draw in the canvas 37 | for( var row = 0; row < qrcode.getModuleCount(); row++ ){ 38 | for( var col = 0; col < qrcode.getModuleCount(); col++ ){ 39 | ctx.fillStyle = qrcode.isDark(row, col) ? options.foreground : options.background; 40 | var w = (Math.ceil((col+1)*tileW) - Math.floor(col*tileW)); 41 | var h = (Math.ceil((row+1)*tileW) - Math.floor(row*tileW)); 42 | ctx.fillRect(Math.round(col*tileW),Math.round(row*tileH), w, h); 43 | } 44 | } 45 | // return just built canvas 46 | return canvas; 47 | } 48 | 49 | // from Jon-Carlos Rivera (https://github.com/imbcmdth) 50 | var createTable = function(){ 51 | // create the qrcode itself 52 | var qrcode = new QRCode(options.typeNumber, options.correctLevel); 53 | qrcode.addData(options.text); 54 | qrcode.make(); 55 | 56 | // create table element 57 | var $table = $('
') 58 | .css("width", options.width+"px") 59 | .css("height", options.height+"px") 60 | .css("border", "0px") 61 | .css("border-collapse", "collapse") 62 | .css('background-color', options.background); 63 | 64 | // compute tileS percentage 65 | var tileW = options.width / qrcode.getModuleCount(); 66 | var tileH = options.height / qrcode.getModuleCount(); 67 | 68 | // draw in the table 69 | for(var row = 0; row < qrcode.getModuleCount(); row++ ){ 70 | var $row = $('').css('height', tileH+"px").appendTo($table); 71 | 72 | for(var col = 0; col < qrcode.getModuleCount(); col++ ){ 73 | $('') 74 | .css('width', tileW+"px") 75 | .css('background-color', qrcode.isDark(row, col) ? options.foreground : options.background) 76 | .appendTo($row); 77 | } 78 | } 79 | // return just built canvas 80 | return $table; 81 | } 82 | 83 | 84 | return this.each(function(){ 85 | var element = options.render == "canvas" ? createCanvas() : createTable(); 86 | $(element).appendTo(this); 87 | }); 88 | }; 89 | })( jQuery ); 90 | -------------------------------------------------------------------------------- /res/js/jquery.scrollUp.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * scrollup v2.4.1 3 | * Url: http://markgoodyear.com/labs/scrollup/ 4 | * Copyright (c) Mark Goodyear — @markgdyr — http://markgoodyear.com 5 | * License: MIT 6 | */ 7 | !function(l,o,e){"use strict";l.fn.scrollUp=function(o){l.data(e.body,"scrollUp")||(l.data(e.body,"scrollUp",!0),l.fn.scrollUp.init(o))},l.fn.scrollUp.init=function(r){var s,t,c,i,n,a,d,p=l.fn.scrollUp.settings=l.extend({},l.fn.scrollUp.defaults,r),f=!1;switch(d=p.scrollTrigger?l(p.scrollTrigger):l("",{id:p.scrollName,href:"#top"}),p.scrollTitle&&d.attr("title",p.scrollTitle),d.appendTo("body"),p.scrollImg||p.scrollTrigger||d.html(p.scrollText),d.css({display:"none",position:"fixed",zIndex:p.zIndex}),p.activeOverlay&&l("
",{id:p.scrollName+"-active"}).css({position:"absolute",top:p.scrollDistance+"px",width:"100%",borderTop:"1px dotted"+p.activeOverlay,zIndex:p.zIndex}).appendTo("body"),p.animation){case"fade":s="fadeIn",t="fadeOut",c=p.animationSpeed;break;case"slide":s="slideDown",t="slideUp",c=p.animationSpeed;break;default:s="show",t="hide",c=0}i="top"===p.scrollFrom?p.scrollDistance:l(e).height()-l(o).height()-p.scrollDistance,n=l(o).scroll(function(){l(o).scrollTop()>i?f||(d[s](c),f=!0):f&&(d[t](c),f=!1)}),p.scrollTarget?"number"==typeof p.scrollTarget?a=p.scrollTarget:"string"==typeof p.scrollTarget&&(a=Math.floor(l(p.scrollTarget).offset().top)):a=0,d.click(function(o){o.preventDefault(),l("html, body").animate({scrollTop:a},p.scrollSpeed,p.easingType)})},l.fn.scrollUp.defaults={scrollName:"scrollUp",scrollDistance:300,scrollFrom:"top",scrollSpeed:300,easingType:"linear",animation:"fade",animationSpeed:200,scrollTrigger:!1,scrollTarget:!1,scrollText:"Scroll to top",scrollTitle:!1,scrollImg:!1,activeOverlay:!1,zIndex:2147483647},l.fn.scrollUp.destroy=function(r){l.removeData(e.body,"scrollUp"),l("#"+l.fn.scrollUp.settings.scrollName).remove(),l("#"+l.fn.scrollUp.settings.scrollName+"-active").remove(),l.fn.jquery.split(".")[1]>=7?l(o).off("scroll",r):l(o).unbind("scroll",r)},l.scrollUp=l.fn.scrollUp}(jQuery,window,document); -------------------------------------------------------------------------------- /res/js/promise-polyfill.min.js: -------------------------------------------------------------------------------- 1 | /* Disable minification (remove `.min` from URL path) for more info */ 2 | 3 | (function(undefined) {!function(){function e(e){var n,t=!1,r=[];if("function"==typeof e.next)for(;!t&&(n=e.next(),n.hasOwnProperty("value")&&n.hasOwnProperty("done"));){if(n.done===!0){t=!0;break}if(n.done!==!1)break;r.push(n.value)}return!!t&&r}Object.defineProperty(Array,"from",{configurable:!0,value:function(n){if(n===undefined||null===n)throw new TypeError(n+" is not an object");if(1 in arguments&&!(arguments[1]instanceof Function))throw new TypeError(arguments[1]+" is not a function");var t,r,o="string"==typeof n?n.split(""):Object(n),i=arguments[1],a=arguments[2],f=[],u=-1,c=Math.min(Math.max(Number(o.length)||0,0),9007199254740991);r=e(o),"function"==typeof o.entries&&"function"==typeof o.values&&("Set"===o.constructor.name&&"values"in Set.prototype&&(r=e(o.values())),"Map"===o.constructor.name&&"entries"in Map.prototype&&(r=e(o.entries()))),r&&(o=r,c=r.length);for(;++u=e.length)return{done:!0,value:undefined};var r=e[n++];return{done:!1,value:"key"===t?r.name:"value"===t?r.value:[r.name,r.value]}}}function l(t,n){function r(){var e=l.href.replace(/#$|\?$|\?(?=#)/g,"");l.href!==e&&(l.href=e)}function u(){m._setList(l.search?a(l.search.substring(1)):[]),m._update_steps()}if(!(this instanceof e.URL))throw new TypeError("Failed to construct 'URL': Please use the 'new' operator.");n&&(t=function(){if(c)return new s(t,n).href;var e;if(document.implementation&&document.implementation.createHTMLDocument?e=document.implementation.createHTMLDocument(""):document.implementation&&document.implementation.createDocument?(e=document.implementation.createDocument("http://www.w3.org/1999/xhtml","html",null),e.documentElement.appendChild(e.createElement("head")),e.documentElement.appendChild(e.createElement("body"))):window.ActiveXObject&&(e=new window.ActiveXObject("htmlfile"),e.write(""),e.close()),!e)throw Error("base not supported");var r=e.createElement("base");r.href=n,e.getElementsByTagName("head")[0].appendChild(r);var a=e.createElement("a");return a.href=t,a.href}());var l=i(t||""),f=function(){if(!("defineProperties"in Object))return!1;try{var e={};return Object.defineProperties(e,{prop:{get:function(){return!0}}}),e.prop}catch(t){return!1}}(),h=f?this:document.createElement("a"),m=new o(l.search?l.search.substring(1):null);return m._url_object=h,Object.defineProperties(h,{href:{get:function(){return l.href},set:function(e){l.href=e,r(),u()},enumerable:!0,configurable:!0},origin:{get:function(){return"origin"in l?l.origin:this.protocol+"//"+this.host},enumerable:!0,configurable:!0},protocol:{get:function(){return l.protocol},set:function(e){l.protocol=e},enumerable:!0,configurable:!0},username:{get:function(){return l.username},set:function(e){l.username=e},enumerable:!0,configurable:!0},password:{get:function(){return l.password},set:function(e){l.password=e},enumerable:!0,configurable:!0},host:{get:function(){var e={"http:":/:80$/,"https:":/:443$/,"ftp:":/:21$/}[l.protocol];return e?l.host.replace(e,""):l.host},set:function(e){l.host=e},enumerable:!0,configurable:!0},hostname:{get:function(){return l.hostname},set:function(e){l.hostname=e},enumerable:!0,configurable:!0},port:{get:function(){return l.port},set:function(e){l.port=e},enumerable:!0,configurable:!0},pathname:{get:function(){return"/"!==l.pathname.charAt(0)?"/"+l.pathname:l.pathname},set:function(e){l.pathname=e},enumerable:!0,configurable:!0},search:{get:function(){return l.search},set:function(e){l.search!==e&&(l.search=e,r(),u())},enumerable:!0,configurable:!0},searchParams:{get:function(){return m},enumerable:!0,configurable:!0},hash:{get:function(){return l.hash},set:function(e){l.hash=e,r()},enumerable:!0,configurable:!0},toString:{value:function(){return l.toString()},enumerable:!1,configurable:!0},valueOf:{value:function(){return l.valueOf()},enumerable:!1,configurable:!0}}),h}var c,s=e.URL;try{if(s){if("searchParams"in(c=new e.URL("http://example.com")))return;"href"in c||(c=undefined)}}catch(h){}if(Object.defineProperties(o.prototype,{append:{value:function(e,t){this._list.push({name:e,value:t}),this._update_steps()},writable:!0,enumerable:!0,configurable:!0},"delete":{value:function(e){for(var t=0;t1?arguments[1]:undefined;this._list.forEach(function(n,r){e.call(t,n.value,n.name)})},writable:!0,enumerable:!0,configurable:!0},toString:{value:function(){return r(this._list)},writable:!0,enumerable:!1,configurable:!0}}),"Symbol"in e&&"iterator"in e.Symbol&&(Object.defineProperty(o.prototype,e.Symbol.iterator,{value:o.prototype.entries,writable:!0,enumerable:!0,configurable:!0}),Object.defineProperty(u.prototype,e.Symbol.iterator,{value:function(){return this},writable:!0,enumerable:!0,configurable:!0})),s)for(var f in s)s.hasOwnProperty(f)&&"function"==typeof s[f]&&(l[f]=s[f]);e.URL=l,e.URLSearchParams=o}(),function(){if("1"!==new e.URLSearchParams([["a",1]]).get("a")||"1"!==new e.URLSearchParams({a:1}).get("a")){var r=e.URLSearchParams;e.URLSearchParams=function(e){if(e&&"object"==typeof e&&t(e)){var a=new r;return n(e).forEach(function(e){if(!t(e))throw TypeError();var r=n(e);if(2!==r.length)throw TypeError();a.append(r[0],r[1])}),a}return e&&"object"==typeof e?(a=new r,Object.keys(e).forEach(function(t){a.set(t,e[t])}),a):new r(e)}}}()}(self);}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); -------------------------------------------------------------------------------- /res/js/settings.js: -------------------------------------------------------------------------------- 1 | /* javascript */ 2 | var vm = new Vue({ 3 | el: '#app', 4 | data: { 5 | name: name, 6 | pid: '-', 7 | childPids: [], 8 | } 9 | }); 10 | 11 | var maxDataCount = 30; 12 | var requstUrl = "/ws/perfs/" + name; 13 | if ("" !== slave) { 14 | requstUrl = "/distributed/" + slave + requstUrl; 15 | } 16 | var ws = newWebsocket(requstUrl, { 17 | onopen: function(evt) { 18 | console.log(evt); 19 | }, 20 | onmessage: function(evt) { 21 | var data = JSON.parse(evt.data); 22 | vm.pid = data.pid; 23 | vm.childPids = data.pids; 24 | console.log("pid", data.pid, data); //evt.data.pid); 25 | if (memData && data.rss) { 26 | memData.push({ 27 | value: [new Date(), data.rss], 28 | }) 29 | if (memData.length > maxDataCount) { 30 | memData.shift(); 31 | } 32 | chartMem.setOption({ 33 | series: [{ 34 | data: memData, 35 | }] 36 | }); 37 | } 38 | if (cpuData && data.pcpu !== undefined) { 39 | cpuData.push({ 40 | value: [new Date(), data.pcpu], 41 | }) 42 | if (cpuData.length > maxDataCount) { 43 | cpuData.shift(); 44 | } 45 | chartCpu.setOption({ 46 | series: [{ 47 | data: cpuData, 48 | }] 49 | }) 50 | } 51 | } 52 | }) -------------------------------------------------------------------------------- /res/js/ua-parser.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UAParser.js v0.7.3 3 | * Lightweight JavaScript-based User-Agent string parser 4 | * https://github.com/faisalman/ua-parser-js 5 | * 6 | * Copyright © 2012-2014 Faisal Salman 7 | * Dual licensed under GPLv2 & MIT 8 | */ 9 | (function(window,undefined){"use strict";var LIBVERSION="0.7.3",EMPTY="",UNKNOWN="?",FUNC_TYPE="function",UNDEF_TYPE="undefined",OBJ_TYPE="object",MAJOR="major",MODEL="model",NAME="name",TYPE="type",VENDOR="vendor",VERSION="version",ARCHITECTURE="architecture",CONSOLE="console",MOBILE="mobile",TABLET="tablet",SMARTTV="smarttv",WEARABLE="wearable",EMBEDDED="embedded";var util={extend:function(regexes,extensions){for(var i in extensions){if("browser cpu device engine os".indexOf(i)!==-1&&extensions[i].length%2===0){regexes[i]=extensions[i].concat(regexes[i])}}return regexes},has:function(str1,str2){if(typeof str1==="string"){return str2.toLowerCase().indexOf(str1.toLowerCase())!==-1}},lowerize:function(str){return str.toLowerCase()}};var mapper={rgx:function(){var result,i=0,j,k,p,q,matches,match,args=arguments;while(i0){if(q.length==2){if(typeof q[1]==FUNC_TYPE){result[q[0]]=q[1].call(this,match)}else{result[q[0]]=q[1]}}else if(q.length==3){if(typeof q[1]===FUNC_TYPE&&!(q[1].exec&&q[1].test)){result[q[0]]=match?q[1].call(this,match,q[2]):undefined}else{result[q[0]]=match?match.replace(q[1],q[2]):undefined}}else if(q.length==4){result[q[0]]=match?q[3].call(this,match.replace(q[1],q[2])):undefined}}else{result[q]=match?match:undefined}}}}i+=2}return result},str:function(str,map){for(var i in map){if(typeof map[i]===OBJ_TYPE&&map[i].length>0){for(var j=0;j=0&&o>i;i+=n){var a=u?u[i]:i;e=r(e,t[a],a,t)}return e}return function(r,e,u,i){e=b(e,i,4);var o=!k(r)&&m.keys(r),a=(o||r).length,c=n>0?0:a-1;return arguments.length<3&&(u=r[o?o[c]:c],c+=n),t(r,e,u,o,c,a)}}function t(n){return function(t,r,e){r=x(r,e);for(var u=O(t),i=n>0?0:u-1;i>=0&&u>i;i+=n)if(r(t[i],i,t))return i;return-1}}function r(n,t,r){return function(e,u,i){var o=0,a=O(e);if("number"==typeof i)n>0?o=i>=0?i:Math.max(i+a,o):a=i>=0?Math.min(i+1,a):i+a+1;else if(r&&i&&a)return i=r(e,u),e[i]===u?i:-1;if(u!==u)return i=t(l.call(e,o,a),m.isNaN),i>=0?i+o:-1;for(i=n>0?o:a-1;i>=0&&a>i;i+=n)if(e[i]===u)return i;return-1}}function e(n,t){var r=I.length,e=n.constructor,u=m.isFunction(e)&&e.prototype||a,i="constructor";for(m.has(n,i)&&!m.contains(t,i)&&t.push(i);r--;)i=I[r],i in n&&n[i]!==u[i]&&!m.contains(t,i)&&t.push(i)}var u=this,i=u._,o=Array.prototype,a=Object.prototype,c=Function.prototype,f=o.push,l=o.slice,s=a.toString,p=a.hasOwnProperty,h=Array.isArray,v=Object.keys,g=c.bind,y=Object.create,d=function(){},m=function(n){return n instanceof m?n:this instanceof m?void(this._wrapped=n):new m(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=m),exports._=m):u._=m,m.VERSION="1.8.3";var b=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}},x=function(n,t,r){return null==n?m.identity:m.isFunction(n)?b(n,t,r):m.isObject(n)?m.matcher(n):m.property(n)};m.iteratee=function(n,t){return x(n,t,1/0)};var _=function(n,t){return function(r){var e=arguments.length;if(2>e||null==r)return r;for(var u=1;e>u;u++)for(var i=arguments[u],o=n(i),a=o.length,c=0;a>c;c++){var f=o[c];t&&r[f]!==void 0||(r[f]=i[f])}return r}},j=function(n){if(!m.isObject(n))return{};if(y)return y(n);d.prototype=n;var t=new d;return d.prototype=null,t},w=function(n){return function(t){return null==t?void 0:t[n]}},A=Math.pow(2,53)-1,O=w("length"),k=function(n){var t=O(n);return"number"==typeof t&&t>=0&&A>=t};m.each=m.forEach=function(n,t,r){t=b(t,r);var e,u;if(k(n))for(e=0,u=n.length;u>e;e++)t(n[e],e,n);else{var i=m.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},m.map=m.collect=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=Array(u),o=0;u>o;o++){var a=e?e[o]:o;i[o]=t(n[a],a,n)}return i},m.reduce=m.foldl=m.inject=n(1),m.reduceRight=m.foldr=n(-1),m.find=m.detect=function(n,t,r){var e;return e=k(n)?m.findIndex(n,t,r):m.findKey(n,t,r),e!==void 0&&e!==-1?n[e]:void 0},m.filter=m.select=function(n,t,r){var e=[];return t=x(t,r),m.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e},m.reject=function(n,t,r){return m.filter(n,m.negate(x(t)),r)},m.every=m.all=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(!t(n[o],o,n))return!1}return!0},m.some=m.any=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(t(n[o],o,n))return!0}return!1},m.contains=m.includes=m.include=function(n,t,r,e){return k(n)||(n=m.values(n)),("number"!=typeof r||e)&&(r=0),m.indexOf(n,t,r)>=0},m.invoke=function(n,t){var r=l.call(arguments,2),e=m.isFunction(t);return m.map(n,function(n){var u=e?t:n[t];return null==u?u:u.apply(n,r)})},m.pluck=function(n,t){return m.map(n,m.property(t))},m.where=function(n,t){return m.filter(n,m.matcher(t))},m.findWhere=function(n,t){return m.find(n,m.matcher(t))},m.max=function(n,t,r){var e,u,i=-1/0,o=-1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],e>i&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(u>o||u===-1/0&&i===-1/0)&&(i=n,o=u)});return i},m.min=function(n,t,r){var e,u,i=1/0,o=1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],i>e&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(o>u||1/0===u&&1/0===i)&&(i=n,o=u)});return i},m.shuffle=function(n){for(var t,r=k(n)?n:m.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=m.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},m.sample=function(n,t,r){return null==t||r?(k(n)||(n=m.values(n)),n[m.random(n.length-1)]):m.shuffle(n).slice(0,Math.max(0,t))},m.sortBy=function(n,t,r){return t=x(t,r),m.pluck(m.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var F=function(n){return function(t,r,e){var u={};return r=x(r,e),m.each(t,function(e,i){var o=r(e,i,t);n(u,e,o)}),u}};m.groupBy=F(function(n,t,r){m.has(n,r)?n[r].push(t):n[r]=[t]}),m.indexBy=F(function(n,t,r){n[r]=t}),m.countBy=F(function(n,t,r){m.has(n,r)?n[r]++:n[r]=1}),m.toArray=function(n){return n?m.isArray(n)?l.call(n):k(n)?m.map(n,m.identity):m.values(n):[]},m.size=function(n){return null==n?0:k(n)?n.length:m.keys(n).length},m.partition=function(n,t,r){t=x(t,r);var e=[],u=[];return m.each(n,function(n,r,i){(t(n,r,i)?e:u).push(n)}),[e,u]},m.first=m.head=m.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:m.initial(n,n.length-t)},m.initial=function(n,t,r){return l.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},m.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:m.rest(n,Math.max(0,n.length-t))},m.rest=m.tail=m.drop=function(n,t,r){return l.call(n,null==t||r?1:t)},m.compact=function(n){return m.filter(n,m.identity)};var S=function(n,t,r,e){for(var u=[],i=0,o=e||0,a=O(n);a>o;o++){var c=n[o];if(k(c)&&(m.isArray(c)||m.isArguments(c))){t||(c=S(c,t,r));var f=0,l=c.length;for(u.length+=l;l>f;)u[i++]=c[f++]}else r||(u[i++]=c)}return u};m.flatten=function(n,t){return S(n,t,!1)},m.without=function(n){return m.difference(n,l.call(arguments,1))},m.uniq=m.unique=function(n,t,r,e){m.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=x(r,e));for(var u=[],i=[],o=0,a=O(n);a>o;o++){var c=n[o],f=r?r(c,o,n):c;t?(o&&i===f||u.push(c),i=f):r?m.contains(i,f)||(i.push(f),u.push(c)):m.contains(u,c)||u.push(c)}return u},m.union=function(){return m.uniq(S(arguments,!0,!0))},m.intersection=function(n){for(var t=[],r=arguments.length,e=0,u=O(n);u>e;e++){var i=n[e];if(!m.contains(t,i)){for(var o=1;r>o&&m.contains(arguments[o],i);o++);o===r&&t.push(i)}}return t},m.difference=function(n){var t=S(arguments,!0,!0,1);return m.filter(n,function(n){return!m.contains(t,n)})},m.zip=function(){return m.unzip(arguments)},m.unzip=function(n){for(var t=n&&m.max(n,O).length||0,r=Array(t),e=0;t>e;e++)r[e]=m.pluck(n,e);return r},m.object=function(n,t){for(var r={},e=0,u=O(n);u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},m.findIndex=t(1),m.findLastIndex=t(-1),m.sortedIndex=function(n,t,r,e){r=x(r,e,1);for(var u=r(t),i=0,o=O(n);o>i;){var a=Math.floor((i+o)/2);r(n[a])i;i++,n+=r)u[i]=n;return u};var E=function(n,t,r,e,u){if(!(e instanceof t))return n.apply(r,u);var i=j(n.prototype),o=n.apply(i,u);return m.isObject(o)?o:i};m.bind=function(n,t){if(g&&n.bind===g)return g.apply(n,l.call(arguments,1));if(!m.isFunction(n))throw new TypeError("Bind must be called on a function");var r=l.call(arguments,2),e=function(){return E(n,e,t,this,r.concat(l.call(arguments)))};return e},m.partial=function(n){var t=l.call(arguments,1),r=function(){for(var e=0,u=t.length,i=Array(u),o=0;u>o;o++)i[o]=t[o]===m?arguments[e++]:t[o];for(;e=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=m.bind(n[r],n);return n},m.memoize=function(n,t){var r=function(e){var u=r.cache,i=""+(t?t.apply(this,arguments):e);return m.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},m.delay=function(n,t){var r=l.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},m.defer=m.partial(m.delay,m,1),m.throttle=function(n,t,r){var e,u,i,o=null,a=0;r||(r={});var c=function(){a=r.leading===!1?0:m.now(),o=null,i=n.apply(e,u),o||(e=u=null)};return function(){var f=m.now();a||r.leading!==!1||(a=f);var l=t-(f-a);return e=this,u=arguments,0>=l||l>t?(o&&(clearTimeout(o),o=null),a=f,i=n.apply(e,u),o||(e=u=null)):o||r.trailing===!1||(o=setTimeout(c,l)),i}},m.debounce=function(n,t,r){var e,u,i,o,a,c=function(){var f=m.now()-o;t>f&&f>=0?e=setTimeout(c,t-f):(e=null,r||(a=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,o=m.now();var f=r&&!e;return e||(e=setTimeout(c,t)),f&&(a=n.apply(i,u),i=u=null),a}},m.wrap=function(n,t){return m.partial(t,n)},m.negate=function(n){return function(){return!n.apply(this,arguments)}},m.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},m.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},m.before=function(n,t){var r;return function(){return--n>0&&(r=t.apply(this,arguments)),1>=n&&(t=null),r}},m.once=m.partial(m.before,2);var M=!{toString:null}.propertyIsEnumerable("toString"),I=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"];m.keys=function(n){if(!m.isObject(n))return[];if(v)return v(n);var t=[];for(var r in n)m.has(n,r)&&t.push(r);return M&&e(n,t),t},m.allKeys=function(n){if(!m.isObject(n))return[];var t=[];for(var r in n)t.push(r);return M&&e(n,t),t},m.values=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},m.mapObject=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=u.length,o={},a=0;i>a;a++)e=u[a],o[e]=t(n[e],e,n);return o},m.pairs=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},m.invert=function(n){for(var t={},r=m.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},m.functions=m.methods=function(n){var t=[];for(var r in n)m.isFunction(n[r])&&t.push(r);return t.sort()},m.extend=_(m.allKeys),m.extendOwn=m.assign=_(m.keys),m.findKey=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=0,o=u.length;o>i;i++)if(e=u[i],t(n[e],e,n))return e},m.pick=function(n,t,r){var e,u,i={},o=n;if(null==o)return i;m.isFunction(t)?(u=m.allKeys(o),e=b(t,r)):(u=S(arguments,!1,!1,1),e=function(n,t,r){return t in r},o=Object(o));for(var a=0,c=u.length;c>a;a++){var f=u[a],l=o[f];e(l,f,o)&&(i[f]=l)}return i},m.omit=function(n,t,r){if(m.isFunction(t))t=m.negate(t);else{var e=m.map(S(arguments,!1,!1,1),String);t=function(n,t){return!m.contains(e,t)}}return m.pick(n,t,r)},m.defaults=_(m.allKeys,!0),m.create=function(n,t){var r=j(n);return t&&m.extendOwn(r,t),r},m.clone=function(n){return m.isObject(n)?m.isArray(n)?n.slice():m.extend({},n):n},m.tap=function(n,t){return t(n),n},m.isMatch=function(n,t){var r=m.keys(t),e=r.length;if(null==n)return!e;for(var u=Object(n),i=0;e>i;i++){var o=r[i];if(t[o]!==u[o]||!(o in u))return!1}return!0};var N=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof m&&(n=n._wrapped),t instanceof m&&(t=t._wrapped);var u=s.call(n);if(u!==s.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}var i="[object Array]"===u;if(!i){if("object"!=typeof n||"object"!=typeof t)return!1;var o=n.constructor,a=t.constructor;if(o!==a&&!(m.isFunction(o)&&o instanceof o&&m.isFunction(a)&&a instanceof a)&&"constructor"in n&&"constructor"in t)return!1}r=r||[],e=e||[];for(var c=r.length;c--;)if(r[c]===n)return e[c]===t;if(r.push(n),e.push(t),i){if(c=n.length,c!==t.length)return!1;for(;c--;)if(!N(n[c],t[c],r,e))return!1}else{var f,l=m.keys(n);if(c=l.length,m.keys(t).length!==c)return!1;for(;c--;)if(f=l[c],!m.has(t,f)||!N(n[f],t[f],r,e))return!1}return r.pop(),e.pop(),!0};m.isEqual=function(n,t){return N(n,t)},m.isEmpty=function(n){return null==n?!0:k(n)&&(m.isArray(n)||m.isString(n)||m.isArguments(n))?0===n.length:0===m.keys(n).length},m.isElement=function(n){return!(!n||1!==n.nodeType)},m.isArray=h||function(n){return"[object Array]"===s.call(n)},m.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},m.each(["Arguments","Function","String","Number","Date","RegExp","Error"],function(n){m["is"+n]=function(t){return s.call(t)==="[object "+n+"]"}}),m.isArguments(arguments)||(m.isArguments=function(n){return m.has(n,"callee")}),"function"!=typeof/./&&"object"!=typeof Int8Array&&(m.isFunction=function(n){return"function"==typeof n||!1}),m.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},m.isNaN=function(n){return m.isNumber(n)&&n!==+n},m.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===s.call(n)},m.isNull=function(n){return null===n},m.isUndefined=function(n){return n===void 0},m.has=function(n,t){return null!=n&&p.call(n,t)},m.noConflict=function(){return u._=i,this},m.identity=function(n){return n},m.constant=function(n){return function(){return n}},m.noop=function(){},m.property=w,m.propertyOf=function(n){return null==n?function(){}:function(t){return n[t]}},m.matcher=m.matches=function(n){return n=m.extendOwn({},n),function(t){return m.isMatch(t,n)}},m.times=function(n,t,r){var e=Array(Math.max(0,n));t=b(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},m.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},m.now=Date.now||function(){return(new Date).getTime()};var B={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},T=m.invert(B),R=function(n){var t=function(t){return n[t]},r="(?:"+m.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};m.escape=R(B),m.unescape=R(T),m.result=function(n,t,r){var e=null==n?void 0:n[t];return e===void 0&&(e=r),m.isFunction(e)?e.call(n):e};var q=0;m.uniqueId=function(n){var t=++q+"";return n?n+t:t},m.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var K=/(.)^/,z={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\u2028|\u2029/g,L=function(n){return"\\"+z[n]};m.template=function(n,t,r){!t&&r&&(t=r),t=m.defaults({},t,m.templateSettings);var e=RegExp([(t.escape||K).source,(t.interpolate||K).source,(t.evaluate||K).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,o,a){return i+=n.slice(u,a).replace(D,L),u=a+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":o&&(i+="';\n"+o+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var o=new Function(t.variable||"obj","_",i)}catch(a){throw a.source=i,a}var c=function(n){return o.call(this,n,m)},f=t.variable||"obj";return c.source="function("+f+"){\n"+i+"}",c},m.chain=function(n){var t=m(n);return t._chain=!0,t};var P=function(n,t){return n._chain?m(t).chain():t};m.mixin=function(n){m.each(m.functions(n),function(t){var r=m[t]=n[t];m.prototype[t]=function(){var n=[this._wrapped];return f.apply(n,arguments),P(this,r.apply(m,n))}})},m.mixin(m),m.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=o[n];m.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],P(this,r)}}),m.each(["concat","join","slice"],function(n){var t=o[n];m.prototype[n]=function(){return P(this,t.apply(this._wrapped,arguments))}}),m.prototype.value=function(){return this._wrapped},m.prototype.valueOf=m.prototype.toJSON=m.prototype.value,m.prototype.toString=function(){return""+this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return m})}).call(this); 6 | //# sourceMappingURL=underscore-min.map -------------------------------------------------------------------------------- /res/setting.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | gosuv 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 37 |
38 |
39 |

{{name}}

40 |

41 | pid: {{pid}} 42 |
sub pids: {{childPids.join(',')}} 43 |

44 |

45 |
status: running 46 |
time: 2016/09/07 12:00:00 47 |

48 |
49 |
50 |
51 |
52 |
53 |
54 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /sigchld_unix.go: -------------------------------------------------------------------------------- 1 | // +build linux darwin 2 | 3 | package main 4 | 5 | import ( 6 | "log" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | ) 11 | 12 | func init() { 13 | go watchChildSignal() 14 | } 15 | 16 | func watchChildSignal() { 17 | var sigs = make(chan os.Signal, 3) 18 | signal.Notify(sigs, syscall.SIGCHLD) 19 | 20 | for { 21 | <-sigs 22 | reapChildren() 23 | } 24 | } 25 | 26 | func reapChildren() { 27 | for { 28 | var wstatus syscall.WaitStatus 29 | wpid, err := syscall.Wait4(-1, &wstatus, syscall.WNOHANG, nil) 30 | if err != nil { 31 | log.Printf("syscall.Wait4 call failed: %v", err) 32 | break 33 | } 34 | 35 | if wpid != 0 { 36 | log.Printf("reap dead child: %d, wstatus: %#08x", wpid, wstatus) 37 | } else { 38 | break 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "runtime" 8 | "strings" 9 | "time" 10 | 11 | "github.com/qiniu/log" 12 | ) 13 | 14 | var ErrGoTimeout = errors.New("GoTimeoutFunc") 15 | 16 | func GoFunc(f func() error) chan error { 17 | ch := make(chan error) 18 | go func() { 19 | ch <- f() 20 | }() 21 | return ch 22 | } 23 | 24 | func GoTimeoutFunc(timeout time.Duration, f func() error) chan error { 25 | ch := make(chan error) 26 | go func() { 27 | var err error 28 | select { 29 | case err = <-GoFunc(f): 30 | ch <- err 31 | case <-time.After(timeout): 32 | log.Debugf("timeout: %v", f) 33 | ch <- ErrGoTimeout 34 | } 35 | }() 36 | return ch 37 | } 38 | 39 | func GoTimeout(f func() error, timeout time.Duration) (err error) { 40 | done := make(chan bool) 41 | go func() { 42 | err = f() 43 | done <- true 44 | }() 45 | select { 46 | case <-time.After(timeout): 47 | return ErrGoTimeout 48 | case <-done: 49 | return 50 | } 51 | } 52 | 53 | func IsDir(dir string) bool { 54 | fi, err := os.Stat(dir) 55 | return err == nil && fi.IsDir() 56 | } 57 | 58 | func UserHomeDir() string { 59 | if runtime.GOOS == "windows" { 60 | home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") 61 | if home == "" { 62 | home = os.Getenv("USERPROFILE") 63 | } 64 | return home 65 | } 66 | return os.Getenv("HOME") 67 | } 68 | 69 | // askForConfirmation uses Scanln to parse user input. A user must type in "yes" or "no" and 70 | // then press enter. It has fuzzy matching, so "y", "Y", "yes", "YES", and "Yes" all count as 71 | // confirmations. If the input is not recognized, it will ask again. The function does not return 72 | // until it gets a valid response from the user. Typically, you should use fmt to print out a question 73 | // before calling askForConfirmation. E.g. fmt.Println("WARNING: Are you sure? (yes/no)") 74 | func askForConfirmation(prompt string, _default bool) bool { 75 | var response string 76 | fmt.Print(prompt) 77 | _, err := fmt.Scanln(&response) 78 | if err != nil { 79 | return _default 80 | } 81 | okayResponses := []string{"y", "Y", "yes", "Yes", "YES"} 82 | nokayResponses := []string{"n", "N", "no", "No", "NO"} 83 | if containsString(okayResponses, response) { 84 | return true 85 | } else if containsString(nokayResponses, response) { 86 | return false 87 | } else { 88 | return askForConfirmation(prompt, _default) 89 | } 90 | } 91 | 92 | // You might want to put the following two functions in a separate utility package. 93 | 94 | // posString returns the first index of element in slice. 95 | // If slice does not contain element, returns -1. 96 | func posString(slice []string, element string) int { 97 | for index, elem := range slice { 98 | if elem == element { 99 | return index 100 | } 101 | } 102 | return -1 103 | } 104 | 105 | // containsString returns true iff slice contains element 106 | func containsString(slice []string, element string) bool { 107 | return !(posString(slice, element) == -1) 108 | } 109 | 110 | func StringFormat(format string, m map[string]interface{}) string { 111 | for k, v := range m { 112 | format = strings.Replace(format, "{"+k+"}", fmt.Sprintf("%v", v), -1) 113 | } 114 | return format 115 | } 116 | -------------------------------------------------------------------------------- /web.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "html/template" 8 | "io" 9 | "io/ioutil" 10 | "net/http" 11 | "os" 12 | "os/signal" 13 | "os/user" 14 | "path/filepath" 15 | "reflect" 16 | "strconv" 17 | "sync" 18 | "syscall" 19 | "time" 20 | 21 | "github.com/go-yaml/yaml" 22 | "github.com/gorilla/mux" 23 | "github.com/gorilla/websocket" 24 | "github.com/qiniu/log" 25 | _ "github.com/shurcooL/vfsgen" 26 | "github.com/soopsio/gosuv/gops" 27 | "github.com/soopsio/kexec" 28 | ) 29 | 30 | var defaultGosuvDir string 31 | 32 | func init() { 33 | defaultGosuvDir = os.Getenv("GOSUV_HOME_DIR") 34 | if defaultGosuvDir == "" { 35 | defaultGosuvDir = filepath.Join(UserHomeDir(), ".gosuv") 36 | } 37 | http.Handle("/res/", http.StripPrefix("/res/", http.FileServer(Assets))) // http.StripPrefix("/res/", Assets)) 38 | } 39 | 40 | type Supervisor struct { 41 | ConfigDir string 42 | 43 | names []string // order of programs 44 | pgMap map[string]Program 45 | procMap map[string]*Process 46 | mu sync.Mutex 47 | eventB *WriteBroadcaster 48 | } 49 | 50 | func (s *Supervisor) programs() []Program { 51 | pgs := make([]Program, 0, len(s.names)) 52 | for _, name := range s.names { 53 | pgs = append(pgs, s.pgMap[name]) 54 | } 55 | return pgs 56 | } 57 | 58 | func (s *Supervisor) procs() []*Process { 59 | ps := make([]*Process, 0, len(s.names)) 60 | for _, name := range s.names { 61 | ps = append(ps, s.procMap[name]) 62 | } 63 | return ps 64 | } 65 | 66 | func (s *Supervisor) programPath() string { 67 | return filepath.Join(s.ConfigDir, "programs.yml") 68 | } 69 | 70 | func (s *Supervisor) newProcess(pg Program) *Process { 71 | p := NewProcess(pg) 72 | origFunc := p.StateChange 73 | p.StateChange = func(oldState, newState FSMState) { 74 | s.broadcastEvent(fmt.Sprintf("%s state: %s -> %s", p.Name, string(oldState), string(newState))) 75 | origFunc(oldState, newState) 76 | } 77 | return p 78 | } 79 | 80 | func (s *Supervisor) broadcastEvent(event string) { 81 | s.eventB.Write([]byte(event)) 82 | } 83 | 84 | func (s *Supervisor) addStatusChangeListener(c chan string) { 85 | sChan := s.eventB.NewChanString(fmt.Sprintf("%d", time.Now().UnixNano())) 86 | go func() { 87 | for msg := range sChan { 88 | c <- msg 89 | } 90 | }() 91 | } 92 | 93 | // Send Stop signal and wait program stops 94 | func (s *Supervisor) stopAndWait(name string) error { 95 | p, ok := s.procMap[name] 96 | if !ok { 97 | return errors.New("No such program") 98 | } 99 | if !p.IsRunning() { 100 | return nil 101 | } 102 | c := make(chan string, 0) 103 | s.addStatusChangeListener(c) 104 | // p.stopCommand() 105 | // 停止任务 106 | p.Operate(StopEvent) 107 | for { 108 | select { 109 | case <-c: 110 | if !p.IsRunning() { 111 | return nil 112 | } 113 | case <-time.After(1 * time.Second): // In case some event not catched 114 | if !p.IsRunning() { 115 | return nil 116 | } 117 | } 118 | } 119 | } 120 | 121 | func (s *Supervisor) addOrUpdateProgram(pg Program) error { 122 | // defer s.broadcastEvent(pg.Name + " add or update") 123 | if err := pg.Check(); err != nil { 124 | return err 125 | } 126 | origPg, ok := s.pgMap[pg.Name] 127 | if ok { 128 | if reflect.DeepEqual(origPg, pg) { 129 | return nil 130 | } 131 | s.broadcastEvent(pg.Name + " update") 132 | log.Println("Update:", pg.Name) 133 | origProc := s.procMap[pg.Name] 134 | isRunning := origProc.IsRunning() 135 | go func() { 136 | s.stopAndWait(origProc.Name) 137 | 138 | newProc := s.newProcess(pg) 139 | s.procMap[pg.Name] = newProc 140 | s.pgMap[pg.Name] = pg // update origin 141 | if isRunning { 142 | newProc.Operate(StartEvent) 143 | } 144 | s.saveDB() 145 | }() 146 | } else { 147 | s.names = append(s.names, pg.Name) 148 | s.pgMap[pg.Name] = pg 149 | s.procMap[pg.Name] = s.newProcess(pg) 150 | s.broadcastEvent(pg.Name + " added") 151 | } 152 | return nil 153 | } 154 | 155 | // Check 156 | // - Yaml format 157 | // - Duplicated program 158 | func (s *Supervisor) readConfigFromDB() (pgs []Program, err error) { 159 | data, err := ioutil.ReadFile(s.programPath()) 160 | if err != nil { 161 | data = []byte("") 162 | } 163 | pgs = make([]Program, 0) 164 | if err = yaml.Unmarshal(data, &pgs); err != nil { 165 | return nil, err 166 | } 167 | visited := map[string]bool{} 168 | for _, pg := range pgs { 169 | if visited[pg.Name] { 170 | return nil, fmt.Errorf("Duplicated program name: %s", pg.Name) 171 | } 172 | visited[pg.Name] = true 173 | } 174 | return 175 | } 176 | 177 | func (s *Supervisor) loadDB() error { 178 | s.mu.Lock() 179 | defer s.mu.Unlock() 180 | pgs, err := s.readConfigFromDB() 181 | if err != nil { 182 | return err 183 | } 184 | // add or update program 185 | visited := map[string]bool{} 186 | names := make([]string, 0, len(pgs)) 187 | for _, pg := range pgs { 188 | names = append(names, pg.Name) 189 | visited[pg.Name] = true 190 | s.addOrUpdateProgram(pg) 191 | } 192 | s.names = names 193 | // delete not exists program 194 | for _, pg := range s.pgMap { 195 | if visited[pg.Name] { 196 | continue 197 | } 198 | s.removeProgram(pg.Name) 199 | // name := pg.Name 200 | // log.Printf("stop before delete program: %s", name) 201 | // s.stopAndWait(name) 202 | // delete(s.procMap, name) 203 | // delete(s.pgMap, name) 204 | // s.broadcastEvent(name + " deleted") 205 | } 206 | return nil 207 | } 208 | 209 | func (s *Supervisor) saveDB() error { 210 | s.mu.Lock() 211 | defer s.mu.Unlock() 212 | data, err := yaml.Marshal(s.programs()) 213 | if err != nil { 214 | return err 215 | } 216 | return ioutil.WriteFile(s.programPath(), data, 0644) 217 | } 218 | 219 | func (s *Supervisor) removeProgram(name string) { 220 | names := make([]string, 0, len(s.names)) 221 | for _, pName := range s.names { 222 | if pName == name { 223 | continue 224 | } 225 | names = append(names, pName) 226 | } 227 | s.names = names 228 | log.Printf("stop before delete program: %s", name) 229 | s.stopAndWait(name) 230 | delete(s.procMap, name) 231 | delete(s.pgMap, name) 232 | s.broadcastEvent(name + " deleted") 233 | } 234 | 235 | type WebConfig struct { 236 | User string 237 | Version string 238 | } 239 | 240 | func (s *Supervisor) renderHTML(w http.ResponseWriter, name string, data interface{}) { 241 | file, err := Assets.Open(name + ".html") 242 | if err != nil { 243 | panic(err) 244 | } 245 | defer file.Close() 246 | body, _ := ioutil.ReadAll(file) 247 | 248 | if data == nil { 249 | wc := WebConfig{} 250 | wc.Version = version 251 | user, err := user.Current() 252 | if err == nil { 253 | wc.User = user.Username 254 | } 255 | data = wc 256 | } 257 | w.Header().Set("Content-Type", "text/html") 258 | template.Must(template.New("t").Delims("[[", "]]").Parse(string(body))).Execute(w, data) 259 | } 260 | 261 | type JSONResponse struct { 262 | Status int `json:"status"` 263 | Value interface{} `json:"value"` 264 | } 265 | 266 | func (s *Supervisor) renderJSON(w http.ResponseWriter, data JSONResponse) { 267 | w.Header().Set("Content-Type", "application/json") 268 | bytes, _ := json.Marshal(data) 269 | w.Write(bytes) 270 | } 271 | 272 | func (s *Supervisor) hIndex(w http.ResponseWriter, r *http.Request) { 273 | s.renderHTML(w, "index", nil) 274 | } 275 | 276 | func (s *Supervisor) hSetting(w http.ResponseWriter, r *http.Request) { 277 | name := mux.Vars(r)["name"] 278 | s.renderHTML(w, "setting", map[string]string{ 279 | "Name": name, 280 | }) 281 | } 282 | 283 | func (s *Supervisor) hStatus(w http.ResponseWriter, r *http.Request) { 284 | w.Header().Set("Content-Type", "application/json") 285 | data, _ := json.Marshal(map[string]interface{}{ 286 | "status": 0, 287 | "value": "server is running", 288 | }) 289 | w.Write(data) 290 | } 291 | 292 | func (s *Supervisor) hShutdown(w http.ResponseWriter, r *http.Request) { 293 | s.Close() 294 | s.renderJSON(w, JSONResponse{ 295 | Status: 0, 296 | Value: "gosuv server has been shutdown", 297 | }) 298 | go func() { 299 | time.Sleep(500 * time.Millisecond) 300 | os.Exit(0) 301 | }() 302 | } 303 | 304 | func (s *Supervisor) hReload(w http.ResponseWriter, r *http.Request) { 305 | err := s.loadDB() 306 | log.Println("reload config file") 307 | if err == nil { 308 | s.renderJSON(w, JSONResponse{ 309 | Status: 0, 310 | Value: "load config success", 311 | }) 312 | } else { 313 | s.renderJSON(w, JSONResponse{ 314 | Status: 1, 315 | Value: err.Error(), 316 | }) 317 | } 318 | } 319 | 320 | func (s *Supervisor) hGetProgramList(w http.ResponseWriter, r *http.Request) { 321 | data, err := json.Marshal(s.procs()) 322 | if err != nil { 323 | http.Error(w, err.Error(), http.StatusInternalServerError) 324 | return 325 | } 326 | w.Header().Set("Content-Type", "application/json") 327 | w.Write(data) 328 | } 329 | 330 | func (s *Supervisor) hGetProgram(w http.ResponseWriter, r *http.Request) { 331 | name := mux.Vars(r)["name"] 332 | proc, ok := s.procMap[name] 333 | if !ok { 334 | s.renderJSON(w, JSONResponse{ 335 | Status: 1, 336 | Value: "program not exists", 337 | }) 338 | return 339 | } else { 340 | s.renderJSON(w, JSONResponse{ 341 | Status: 0, 342 | Value: proc, 343 | }) 344 | } 345 | } 346 | 347 | func (s *Supervisor) hAddProgram(w http.ResponseWriter, r *http.Request) { 348 | retries, err := strconv.Atoi(r.FormValue("retries")) 349 | if err != nil { 350 | http.Error(w, err.Error(), http.StatusForbidden) 351 | return 352 | } 353 | pg := Program{ 354 | Name: r.FormValue("name"), 355 | Command: r.FormValue("command"), 356 | Dir: r.FormValue("dir"), 357 | User: r.FormValue("user"), 358 | StartAuto: r.FormValue("autostart") == "on", 359 | StartRetries: retries, 360 | // TODO: missing other values 361 | } 362 | if pg.Dir == "" { 363 | pg.Dir = "/" 364 | } 365 | if err := pg.Check(); err != nil { 366 | http.Error(w, err.Error(), http.StatusBadRequest) 367 | return 368 | } 369 | 370 | w.Header().Set("Content-Type", "application/json") 371 | var data []byte 372 | if _, ok := s.pgMap[pg.Name]; ok { 373 | data, _ = json.Marshal(map[string]interface{}{ 374 | "status": 1, 375 | "error": fmt.Sprintf("Program %s already exists", strconv.Quote(pg.Name)), 376 | }) 377 | } else { 378 | if err := s.addOrUpdateProgram(pg); err != nil { 379 | data, _ = json.Marshal(map[string]interface{}{ 380 | "status": 1, 381 | "error": err.Error(), 382 | }) 383 | } else { 384 | s.saveDB() 385 | data, _ = json.Marshal(map[string]interface{}{ 386 | "status": 0, 387 | }) 388 | } 389 | } 390 | w.Write(data) 391 | } 392 | 393 | func (s *Supervisor) hUpdateProgram(w http.ResponseWriter, r *http.Request) { 394 | // name := mux.Vars(r)["name"] 395 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 396 | pg := Program{} 397 | err := json.NewDecoder(r.Body).Decode(&pg) 398 | if err != nil { 399 | json.NewEncoder(w).Encode(map[string]interface{}{ 400 | "status": 1, 401 | "error": err.Error(), 402 | }) 403 | return 404 | } 405 | err = s.addOrUpdateProgram(pg) 406 | if err != nil { 407 | json.NewEncoder(w).Encode(map[string]interface{}{ 408 | "status": 2, 409 | "error": err.Error(), 410 | }) 411 | return 412 | } 413 | json.NewEncoder(w).Encode(map[string]interface{}{ 414 | "status": 0, 415 | "description": "program updated", 416 | }) 417 | } 418 | 419 | func (s *Supervisor) hDelProgram(w http.ResponseWriter, r *http.Request) { 420 | name := mux.Vars(r)["name"] 421 | 422 | w.Header().Set("Content-Type", "application/json") 423 | var data []byte 424 | if _, ok := s.pgMap[name]; !ok { 425 | data, _ = json.Marshal(map[string]interface{}{ 426 | "status": 1, 427 | "error": fmt.Sprintf("Program %s not exists", strconv.Quote(name)), 428 | }) 429 | } else { 430 | s.removeProgram(name) 431 | s.saveDB() 432 | data, _ = json.Marshal(map[string]interface{}{ 433 | "status": 0, 434 | }) 435 | } 436 | w.Write(data) 437 | } 438 | 439 | func (s *Supervisor) hStartProgram(w http.ResponseWriter, r *http.Request) { 440 | name := mux.Vars(r)["name"] 441 | proc, ok := s.procMap[name] 442 | var data []byte 443 | if !ok { 444 | data, _ = json.Marshal(map[string]interface{}{ 445 | "status": 1, 446 | "error": fmt.Sprintf("Process %s not exists", strconv.Quote(name)), 447 | }) 448 | } else { 449 | proc.Operate(StartEvent) 450 | data, _ = json.Marshal(map[string]interface{}{ 451 | "status": 0, 452 | "name": name, 453 | }) 454 | } 455 | w.Write(data) 456 | } 457 | 458 | func (s *Supervisor) hStopProgram(w http.ResponseWriter, r *http.Request) { 459 | name := mux.Vars(r)["name"] 460 | proc, ok := s.procMap[name] 461 | var data []byte 462 | if !ok { 463 | data, _ = json.Marshal(map[string]interface{}{ 464 | "status": 1, 465 | "error": fmt.Sprintf("Process %s not exists", strconv.Quote(name)), 466 | }) 467 | } else { 468 | proc.Operate(StopEvent) 469 | data, _ = json.Marshal(map[string]interface{}{ 470 | "status": 0, 471 | "name": name, 472 | }) 473 | } 474 | w.Write(data) 475 | } 476 | 477 | func (s *Supervisor) hWebhook(w http.ResponseWriter, r *http.Request) { 478 | vars := mux.Vars(r) 479 | name, category := vars["name"], vars["category"] 480 | proc, ok := s.procMap[name] 481 | if !ok { 482 | http.Error(w, fmt.Sprintf("proc %s not exist", strconv.Quote(name)), http.StatusForbidden) 483 | return 484 | } 485 | hook := proc.Program.WebHook 486 | if category == "github" { 487 | gh := hook.Github 488 | _ = gh.Secret 489 | isRunning := proc.IsRunning() 490 | s.stopAndWait(name) 491 | go func() { 492 | cmd := kexec.CommandString(hook.Command) 493 | cmd.Dir = proc.Program.Dir 494 | cmd.Stdout = proc.Output 495 | cmd.Stderr = proc.Output 496 | err := GoTimeout(cmd.Run, time.Duration(hook.Timeout)*time.Second) 497 | if err == ErrGoTimeout { 498 | cmd.Terminate(syscall.SIGTERM) 499 | } 500 | if err != nil { 501 | log.Warnf("webhook command error: %v", err) 502 | // Trigger pushover notification 503 | } 504 | if isRunning { 505 | proc.Operate(StartEvent) 506 | } 507 | }() 508 | io.WriteString(w, "success triggered") 509 | } else { 510 | log.Warnf("unknown webhook category: %v", category) 511 | } 512 | } 513 | 514 | var upgrader = websocket.Upgrader{} 515 | 516 | func (s *Supervisor) wsEvents(w http.ResponseWriter, r *http.Request) { 517 | c, err := upgrader.Upgrade(w, r, nil) 518 | if err != nil { 519 | log.Print("upgrade:", err) 520 | return 521 | } 522 | defer c.Close() 523 | 524 | ch := make(chan string, 0) 525 | s.addStatusChangeListener(ch) 526 | go func() { 527 | _, _ = <-ch // ignore the history messages 528 | for message := range ch { 529 | // Question: type 1 ? 530 | c.WriteMessage(1, []byte(message)) 531 | } 532 | // s.eventB.RemoveListener(ch) 533 | }() 534 | for { 535 | mt, message, err := c.ReadMessage() 536 | if err != nil { 537 | log.Println("read:", mt, err) 538 | break 539 | } 540 | log.Printf("recv: %v %s", mt, message) 541 | err = c.WriteMessage(mt, message) 542 | if err != nil { 543 | log.Println("write:", err) 544 | break 545 | } 546 | } 547 | } 548 | 549 | func (s *Supervisor) wsLog(w http.ResponseWriter, r *http.Request) { 550 | name := mux.Vars(r)["name"] 551 | log.Println(name) 552 | proc, ok := s.procMap[name] 553 | if !ok { 554 | log.Println("No such process") 555 | // TODO: raise error here? 556 | return 557 | } 558 | 559 | c, err := upgrader.Upgrade(w, r, nil) 560 | if err != nil { 561 | log.Print("upgrade:", err) 562 | return 563 | } 564 | defer c.Close() 565 | 566 | for data := range proc.Output.NewChanString(r.RemoteAddr) { 567 | err := c.WriteMessage(1, []byte(data)) 568 | if err != nil { 569 | proc.Output.CloseWriter(r.RemoteAddr) 570 | break 571 | } 572 | } 573 | } 574 | 575 | // Performance 576 | func (s *Supervisor) wsPerf(w http.ResponseWriter, r *http.Request) { 577 | c, err := upgrader.Upgrade(w, r, nil) 578 | if err != nil { 579 | log.Print("upgrade:", err) 580 | return 581 | } 582 | defer c.Close() 583 | 584 | name := mux.Vars(r)["name"] 585 | proc, ok := s.procMap[name] 586 | if !ok { 587 | log.Println("No such process") 588 | // TODO: raise error here? 589 | return 590 | } 591 | for { 592 | // c.SetWriteDeadline(time.Now().Add(3 * time.Second)) 593 | if proc.cmd == nil || proc.cmd.Process == nil { 594 | log.Println("process not running") 595 | return 596 | } 597 | pid := proc.cmd.Process.Pid 598 | ps, err := gops.NewProcess(pid) 599 | if err != nil { 600 | break 601 | } 602 | mainPinfo, err := ps.ProcInfo() 603 | if err != nil { 604 | break 605 | } 606 | pi := ps.ChildrenProcInfo(true) 607 | pi.Add(mainPinfo) 608 | 609 | err = c.WriteJSON(pi) 610 | if err != nil { 611 | break 612 | } 613 | time.Sleep(700 * time.Millisecond) 614 | } 615 | } 616 | 617 | func (s *Supervisor) Close() { 618 | for _, proc := range s.procMap { 619 | s.stopAndWait(proc.Name) 620 | } 621 | log.Println("server closed") 622 | } 623 | 624 | func (s *Supervisor) catchExitSignal() { 625 | sigC := make(chan os.Signal, 1) 626 | signal.Notify(sigC, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) 627 | go func() { 628 | for sig := range sigC { 629 | if sig == syscall.SIGHUP { 630 | log.Println("Receive SIGHUP, just ignore") 631 | continue 632 | } 633 | log.Printf("Got signal: %v, stopping all running process\n", sig) 634 | s.Close() 635 | break 636 | } 637 | os.Exit(0) 638 | }() 639 | } 640 | 641 | func (s *Supervisor) AutoStartPrograms() { 642 | for _, proc := range s.procMap { 643 | if proc.Program.StartAuto { 644 | log.Printf("auto start %s", strconv.Quote(proc.Name)) 645 | proc.Operate(StartEvent) 646 | } 647 | } 648 | } 649 | 650 | func newSupervisorHandler() (suv *Supervisor, hdlr http.Handler, err error) { 651 | suv = &Supervisor{ 652 | ConfigDir: filepath.Join(defaultGosuvDir, "conf"), 653 | pgMap: make(map[string]Program, 0), 654 | procMap: make(map[string]*Process, 0), 655 | eventB: NewWriteBroadcaster(4 * 1024), 656 | } 657 | if err = suv.loadDB(); err != nil { 658 | return 659 | } 660 | suv.catchExitSignal() 661 | 662 | r := mux.NewRouter() 663 | r.HandleFunc("/", suv.hIndex) 664 | r.HandleFunc("/settings/{name}", suv.hSetting) 665 | 666 | r.HandleFunc("/api/status", suv.hStatus) 667 | r.HandleFunc("/api/shutdown", suv.hShutdown).Methods("POST") 668 | r.HandleFunc("/api/reload", suv.hReload).Methods("POST") 669 | 670 | r.HandleFunc("/api/programs", suv.hGetProgramList).Methods("GET") 671 | r.HandleFunc("/api/programs/{name}", suv.hGetProgram).Methods("GET") 672 | r.HandleFunc("/api/programs/{name}", suv.hDelProgram).Methods("DELETE") 673 | r.HandleFunc("/api/programs/{name}", suv.hUpdateProgram).Methods("PUT") 674 | r.HandleFunc("/api/programs", suv.hAddProgram).Methods("POST") 675 | r.HandleFunc("/api/programs/{name}/start", suv.hStartProgram).Methods("POST") 676 | r.HandleFunc("/api/programs/{name}/stop", suv.hStopProgram).Methods("POST") 677 | 678 | r.HandleFunc("/ws/events", suv.wsEvents) 679 | r.HandleFunc("/ws/logs/{name}", suv.wsLog) 680 | r.HandleFunc("/ws/perfs/{name}", suv.wsPerf) 681 | 682 | r.HandleFunc("/webhooks/{name}/{category}", suv.hWebhook).Methods("POST") 683 | 684 | return suv, r, nil 685 | } 686 | --------------------------------------------------------------------------------