├── upx.sh ├── go_supervisord_gui.png ├── zombie_reaper_windows.go ├── daemonize_windows.go ├── rlimit_windows.go ├── supervisor.ini ├── .gitignore ├── process ├── pdeathsig_windows.go ├── set_user_id_windows.go ├── pdeathsig_other.go ├── pdeathsig_linux.go ├── set_user_id.go ├── process_manager_test.go ├── path.go ├── process_change_monitor.go ├── command_parser_test.go ├── command_parser.go └── process_manager.go ├── zombie_reaper.go ├── assets_dev.go ├── config ├── string_expression_test.go ├── process_group_test.go ├── process_sort_test.go ├── string_expression.go ├── process_group.go ├── process_sort.go ├── config_test.go └── config.go ├── pidproxy ├── signal_windows.go ├── signal.go └── pidproxy.go ├── circle.yml ├── logger ├── log_windows.go ├── log_test.go └── log_unix.go ├── Dockerfile ├── daemonize.go ├── version.go ├── webgui.go ├── go.mod ├── Dockerfile.github ├── .travis.yml ├── LICENSE ├── signals ├── signal.go └── signal_windows.go ├── types ├── process-name-sorter.go └── comm-types.go ├── .goreleaser.yml ├── faults └── faults.go ├── util └── util.go ├── webgui ├── css │ ├── bootstrap-dialog.min.css │ ├── bootstrap-reboot.min.css │ ├── bootstrap-reboot.css │ └── bootstrap-table.css └── index.html ├── rlimit.go ├── logtail.go ├── content_checker_test.go ├── config_template.go ├── b0x.yaml ├── xmlrpcclient ├── xml_processor.go └── xmlrpc-client.go ├── content_checker.go ├── main.go ├── rest-rpc.go ├── events └── events_test.go ├── xmlrpc.go ├── README.md ├── go.sum └── ctl.go /upx.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | upx dist/*/supervisord* 4 | -------------------------------------------------------------------------------- /go_supervisord_gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nknorg/supervisord/master/go_supervisord_gui.png -------------------------------------------------------------------------------- /zombie_reaper_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package main 4 | 5 | func ReapZombie() { 6 | } 7 | -------------------------------------------------------------------------------- /daemonize_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package main 4 | 5 | func Deamonize(proc func()) { 6 | proc() 7 | } 8 | -------------------------------------------------------------------------------- /rlimit_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package main 4 | 5 | func (s *Supervisor) checkRequiredResources() error { 6 | return nil 7 | } 8 | -------------------------------------------------------------------------------- /supervisor.ini: -------------------------------------------------------------------------------- 1 | [inet_http_server] 2 | port = :9001 3 | 4 | [program:rig] 5 | directory = /Users/bingoobjca/GitHub/rig 6 | command = ./rig -u -o=false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | supervisord 3 | supervisord.exe 4 | test.log* 5 | .tags* 6 | debug 7 | go-debug.json 8 | vendor 9 | supervisord_linux_amd64 10 | assets_release.go 11 | -------------------------------------------------------------------------------- /process/pdeathsig_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package process 4 | 5 | import ( 6 | "syscall" 7 | ) 8 | 9 | func setDeathsig(_ *syscall.SysProcAttr) { 10 | } 11 | -------------------------------------------------------------------------------- /process/set_user_id_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package process 4 | 5 | import ( 6 | "syscall" 7 | ) 8 | 9 | func setUserID(_ *syscall.SysProcAttr, _ uint32, _ uint32) { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /process/pdeathsig_other.go: -------------------------------------------------------------------------------- 1 | // +build !linux 2 | // +build !windows 3 | 4 | package process 5 | 6 | import ( 7 | "syscall" 8 | ) 9 | 10 | func setDeathsig(sysProcAttr *syscall.SysProcAttr) { 11 | sysProcAttr.Setpgid = true 12 | } 13 | -------------------------------------------------------------------------------- /zombie_reaper.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package main 4 | 5 | import ( 6 | reaper "github.com/ochinchina/go-reaper" 7 | ) 8 | 9 | // ReapZombie reap the zombie child process 10 | func ReapZombie() { 11 | go reaper.Reap() 12 | } 13 | -------------------------------------------------------------------------------- /assets_dev.go: -------------------------------------------------------------------------------- 1 | // +build !release 2 | //go:generate go run github.com/UnnoTed/fileb0x b0x.yaml 3 | 4 | package main 5 | 6 | import ( 7 | "net/http" 8 | ) 9 | 10 | //HTTP auto generated 11 | var HTTP http.FileSystem = http.Dir("./webgui") 12 | -------------------------------------------------------------------------------- /process/pdeathsig_linux.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | 3 | package process 4 | 5 | import ( 6 | "syscall" 7 | ) 8 | 9 | func setDeathsig(sysProcAttr *syscall.SysProcAttr) { 10 | sysProcAttr.Setpgid = true 11 | sysProcAttr.Pdeathsig = syscall.SIGKILL 12 | } 13 | -------------------------------------------------------------------------------- /process/set_user_id.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package process 4 | 5 | import ( 6 | "syscall" 7 | ) 8 | 9 | func setUserID(procAttr *syscall.SysProcAttr, uid uint32, gid uint32) { 10 | procAttr.Credential = &syscall.Credential{Uid: uid, Gid: gid, NoSetGroups: true} 11 | } 12 | -------------------------------------------------------------------------------- /config/string_expression_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestEval(t *testing.T) { 8 | se := NewStringExpression() 9 | 10 | se.Add("var1", "ok").Add("var2", "2") 11 | 12 | r, _ := se.Eval("%(var1)s_test_%(var2)02d") 13 | 14 | if r != "ok_test_02" { 15 | t.Error("fail to replace the environment") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pidproxy/signal_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package main 4 | 5 | import ( 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | ) 10 | 11 | func install_signal(c chan os.Signal) { 12 | signal.Notify(c, syscall.SIGTERM, 13 | syscall.SIGHUP, 14 | syscall.SIGINT, 15 | syscall.SIGQUIT) 16 | } 17 | 18 | func allowForwardSig(_ os.Signal) bool { 19 | return true 20 | } 21 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | deployment: 2 | master: 3 | branch: [master] 4 | commands: 5 | - go version 6 | - go get github.com/mitchellh/gox 7 | - go get github.com/tcnksm/ghr 8 | - gox -output "dist/supervisord_{{.OS}}_{{.Arch}}" -osarch="linux/amd64 linux/386 darwin/amd64" 9 | - ghr -t $GITHUB_TOKEN -u $CIRCLE_PROJECT_USERNAME -r $CIRCLE_PROJECT_REPONAME --replace v1.0.0 dist/ -------------------------------------------------------------------------------- /logger/log_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows plan9 nacl 2 | 3 | package logger 4 | 5 | func NewSysLogger(name string, logEventEmitter LogEventEmitter) *SysLogger { 6 | return &SysLogger{logEventEmitter: logEventEmitter, logWriter: nil} 7 | } 8 | 9 | func NewRemoteSysLogger(name string, config string, logEventEmitter LogEventEmitter) *SysLogger { 10 | return NewSysLogger(name, logEventEmitter) 11 | } 12 | -------------------------------------------------------------------------------- /pidproxy/signal.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package main 4 | 5 | import ( 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | ) 10 | 11 | func installSignal(c chan os.Signal) { 12 | signal.Notify(c, syscall.SIGTERM, 13 | syscall.SIGHUP, 14 | syscall.SIGINT, 15 | syscall.SIGUSR1, 16 | syscall.SIGUSR2, 17 | syscall.SIGQUIT, 18 | syscall.SIGCHLD) 19 | } 20 | 21 | func allowForwardSig(sig os.Signal) bool { 22 | return sig != syscall.SIGCHLD 23 | } 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | 3 | RUN apk add --no-cache --update git 4 | 5 | RUN go get -v -u github.com/ochinchina/supervisord 6 | 7 | RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags "-linkmode external -extldflags -static" -o /usr/local/bin/supervisord github.com/ochinchina/supervisord 8 | 9 | FROM scratch 10 | 11 | COPY --from=builder /usr/local/bin/supervisord /usr/local/bin/supervisord 12 | 13 | ENTRYPOINT ["/usr/local/bin/supervisord"] 14 | -------------------------------------------------------------------------------- /daemonize.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package main 4 | 5 | import ( 6 | daemon "github.com/ochinchina/go-daemon" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // Deamonize run this process in daemon mode 11 | func Deamonize(proc func()) { 12 | context := daemon.Context{LogFileName: "/dev/stdout"} 13 | 14 | child, err := context.Reborn() 15 | if err != nil { 16 | context := daemon.Context{} 17 | child, err = context.Reborn() 18 | if err != nil { 19 | log.WithFields(log.Fields{"err": err}).Fatal("Unable to run") 20 | } 21 | } 22 | if child != nil { 23 | return 24 | } 25 | defer context.Release() 26 | proc() 27 | } 28 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // VERSION the version of supervisor 8 | const VERSION = "v0.6.8" 9 | 10 | // VersionCommand implement the flags.Commander interface 11 | type VersionCommand struct { 12 | } 13 | 14 | var versionCommand VersionCommand 15 | 16 | // Execute implement Execute() method defined in flags.Commander interface, executes the given command 17 | func (v VersionCommand) Execute(args []string) error { 18 | fmt.Println(VERSION) 19 | return nil 20 | } 21 | 22 | func init() { 23 | parser.AddCommand("version", 24 | "show the version of supervisor", 25 | "display the supervisor version", 26 | &versionCommand) 27 | } 28 | -------------------------------------------------------------------------------- /webgui.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gorilla/mux" 7 | ) 8 | 9 | // SupervisorWebgui the interface to show a WEBGUI to control the supervisor 10 | type SupervisorWebgui struct { 11 | router *mux.Router 12 | supervisor *Supervisor 13 | } 14 | 15 | // NewSupervisorWebgui create a new SupervisorWebgui object 16 | func NewSupervisorWebgui(supervisor *Supervisor) *SupervisorWebgui { 17 | router := mux.NewRouter() 18 | return &SupervisorWebgui{router: router, supervisor: supervisor} 19 | } 20 | 21 | // CreateHandler create a http handler to process the request from WEBGUI 22 | func (sw *SupervisorWebgui) CreateHandler() http.Handler { 23 | sw.router.PathPrefix("/").Handler(http.FileServer(HTTP)) 24 | return sw.router 25 | } 26 | -------------------------------------------------------------------------------- /process/process_manager_test.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "github.com/ochinchina/supervisord/config" 5 | "testing" 6 | ) 7 | 8 | var procs *Manager = NewManager() 9 | 10 | func TestProcessMgrAdd(t *testing.T) { 11 | entry := &config.Entry{ConfigDir: ".", Group: "test", Name: "program:test1"} 12 | procs.Clear() 13 | procs.Add("test1", NewProcess("supervisord", entry)) 14 | 15 | if procs.Find("test1") == nil { 16 | t.Error("fail to add process") 17 | } 18 | } 19 | 20 | func TestProcMgrRemove(t *testing.T) { 21 | procs.Clear() 22 | procs.Add("test1", &Process{}) 23 | proc := procs.Remove("test1") 24 | 25 | if proc == nil { 26 | t.Error("fail to remove process") 27 | } 28 | 29 | proc = procs.Remove("test1") 30 | if proc != nil { 31 | t.Error("fail to remove process") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module supervisord 2 | 3 | require ( 4 | github.com/GeertJohan/go.rice v1.0.0 5 | github.com/gorilla/mux v1.7.3 6 | github.com/gorilla/rpc v1.2.0 7 | github.com/jessevdk/go-flags v1.4.0 8 | github.com/ochinchina/filechangemonitor v0.3.1 9 | github.com/ochinchina/go-daemon v0.1.5 10 | github.com/ochinchina/go-ini v1.0.1 11 | github.com/ochinchina/go-reaper v0.0.0-20181016012355-6b11389e79fc 12 | github.com/ochinchina/gorilla-xmlrpc v0.0.0-20171012055324-ecf2fe693a2c 13 | github.com/ochinchina/supervisord v0.6.4 14 | github.com/robfig/cron/v3 v3.0.1 15 | github.com/rogpeppe/go-charset v0.0.0-20190617161244-0dc95cdf6f31 // indirect 16 | github.com/sirupsen/logrus v1.4.2 17 | golang.org/x/net v0.0.0-20180921000356-2f5d2388922f 18 | ) 19 | 20 | replace github.com/ochinchina/supervisord => ./ 21 | 22 | go 1.13 23 | -------------------------------------------------------------------------------- /logger/log_test.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestWriteSingleLog(t *testing.T) { 9 | logger := NewFileLogger("test.log", int64(50), 2, NewNullLogEventEmitter(), NewNullLocker()) 10 | for i := 0; i < 10; i++ { 11 | logger.Write([]byte(fmt.Sprintf("this is a test %d\n", i))) 12 | } 13 | logger.Close() 14 | } 15 | 16 | func TestSplitLogFile(t *testing.T) { 17 | files := splitLogFile(" test1.log, /dev/stdout, test2.log ") 18 | if len(files) != 3 { 19 | t.Error("Fail to split log file") 20 | } 21 | if files[0] != "test1.log" { 22 | t.Error("Fail to get first log file") 23 | } 24 | if files[1] != "/dev/stdout" { 25 | t.Error("Fail to get second log file") 26 | } 27 | if files[2] != "test2.log" { 28 | t.Error("Fail to get third log file") 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Dockerfile.github: -------------------------------------------------------------------------------- 1 | # Use this file when golang.org/go.googlesource.com is blocked. 2 | # 3 | # Build with: 4 | # 5 | # docker build . -f Dockerfile.github -t ochinchina/supervisord:latest 6 | # 7 | FROM golang:alpine as builder 8 | 9 | RUN apk add --no-cache --update git 10 | 11 | RUN mkdir -p $GOPATH/src/golang.org/x && \ 12 | cd $GOPATH/src/golang.org/x && \ 13 | git clone https://github.com/golang/crypto && \ 14 | git clone https://github.com/golang/sys 15 | 16 | # Exit 0 to ignore meta tag complaints 17 | RUN go get -v -u github.com/ochinchina/supervisord; exit 0 18 | 19 | RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags "-extldflags -static" -o /usr/local/bin/supervisord github.com/ochinchina/supervisord 20 | 21 | FROM scratch 22 | 23 | COPY --from=builder /usr/local/bin/supervisord /usr/local/bin/supervisord 24 | 25 | ENTRYPOINT ["/usr/local/bin/supervisord"] 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: true 3 | go: 4 | - 1.13.x 5 | 6 | env: 7 | global: 8 | - GO111MODULE=on 9 | - GORELEASER_VERSION=0.123.3 10 | - UPXVER="3.94" 11 | 12 | before_install: 13 | - wget https://github.com/goreleaser/goreleaser/releases/download/v${GORELEASER_VERSION}/goreleaser_amd64.deb 14 | - sudo dpkg -i goreleaser_amd64.deb 15 | - | 16 | if [[ ! -f "upx/${UPXVER}/upx" ]] 17 | then 18 | echo "Installing upx .." 19 | curl -OL "https://github.com/upx/upx/releases/download/v${UPXVER}/upx-${UPXVER}-amd64_linux.tar.xz" 20 | tar xvf "upx-${UPXVER}-amd64_linux.tar.xz" 21 | mkdir -p upx 22 | mv "upx-${UPXVER}-amd64_linux" "upx/${UPXVER}" 23 | fi 24 | - export PATH="${TRAVIS_BUILD_DIR}/upx/${UPXVER}/:${PATH}" 25 | - upx --version | grep -E '^upx' 26 | - chmod +x upx.sh 27 | - go install github.com/UnnoTed/fileb0x 28 | 29 | script: 30 | - go test -v ./... 31 | - goreleaser --skip-validate --skip-sign --debug 32 | 33 | branches: 34 | only: 35 | - master 36 | -------------------------------------------------------------------------------- /process/path.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "os/user" 5 | "path/filepath" 6 | ) 7 | 8 | func pathSplit(path string) []string { 9 | r := make([]string, 0) 10 | curPath := path 11 | for { 12 | dir, file := filepath.Split(curPath) 13 | if len(file) > 0 { 14 | r = append(r, file) 15 | } 16 | if len(dir) <= 0 { 17 | break 18 | } 19 | curPath = dir[0 : len(dir)-1] 20 | } 21 | for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 { 22 | r[i], r[j] = r[j], r[i] 23 | } 24 | return r 25 | } 26 | 27 | // PathExpand replace the ~ with user home directory 28 | func PathExpand(path string) (string, error) { 29 | pathList := pathSplit(path) 30 | 31 | if len(pathList) > 0 && len(pathList[0]) > 0 && pathList[0][0] == '~' { 32 | var usr *user.User 33 | var err error 34 | 35 | if pathList[0] == "~" { 36 | usr, err = user.Current() 37 | } else { 38 | usr, err = user.Lookup(pathList[0][1:]) 39 | } 40 | 41 | if err != nil { 42 | return "", err 43 | } 44 | pathList[0] = usr.HomeDir 45 | return filepath.Join(pathList...), nil 46 | } 47 | return path, nil 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Steven Ou 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 | -------------------------------------------------------------------------------- /process/process_change_monitor.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ochinchina/filechangemonitor" 6 | ) 7 | 8 | var fileChangeMonitor = filechangemonitor.NewFileChangeMonitor(10) 9 | 10 | // AddProgramChangeMonitor add a program change listener to monitor if the program binary 11 | func AddProgramChangeMonitor(path string, fileChangeCb func(path string, mode filechangemonitor.FileChangeMode)) { 12 | fileChangeMonitor.AddMonitorFile(path, 13 | false, 14 | filechangemonitor.NewExactFileMatcher(path), 15 | filechangemonitor.NewFileChangeCallbackWrapper(fileChangeCb), 16 | filechangemonitor.NewFileMD5CompareInfo()) 17 | } 18 | 19 | // AddConfigChangeMonitor add a program change listener to monitor if any one of its configuration files is changed 20 | func AddConfigChangeMonitor(path string, filePattern string, fileChangeCb func(path string, mode filechangemonitor.FileChangeMode)) { 21 | fmt.Printf("filePattern=%s\n", filePattern) 22 | fileChangeMonitor.AddMonitorFile(path, 23 | true, 24 | filechangemonitor.NewPatternFileMatcher(filePattern), 25 | filechangemonitor.NewFileChangeCallbackWrapper(fileChangeCb), 26 | filechangemonitor.NewFileMD5CompareInfo()) 27 | } 28 | -------------------------------------------------------------------------------- /signals/signal.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package signals 4 | 5 | import ( 6 | "os" 7 | "syscall" 8 | ) 9 | 10 | // ToSignal convert a signal name to signal 11 | func ToSignal(signalName string) (os.Signal, error) { 12 | if signalName == "HUP" { 13 | return syscall.SIGHUP, nil 14 | } else if signalName == "INT" { 15 | return syscall.SIGINT, nil 16 | } else if signalName == "QUIT" { 17 | return syscall.SIGQUIT, nil 18 | } else if signalName == "KILL" { 19 | return syscall.SIGKILL, nil 20 | } else if signalName == "USR1" { 21 | return syscall.SIGUSR1, nil 22 | } else if signalName == "USR2" { 23 | return syscall.SIGUSR2, nil 24 | } else { 25 | return syscall.SIGTERM, nil 26 | 27 | } 28 | 29 | } 30 | 31 | // Kill send signal to the process 32 | // 33 | // Args: 34 | // process - the process which the signal should be sent to 35 | // sig - the signal will be sent 36 | // sigChildren - true if the signal needs to be sent to the children also 37 | // 38 | func Kill(process *os.Process, sig os.Signal, sigChildren bool) error { 39 | localSig := sig.(syscall.Signal) 40 | pid := process.Pid 41 | if sigChildren { 42 | pid = -pid 43 | } 44 | return syscall.Kill(pid, localSig) 45 | } 46 | -------------------------------------------------------------------------------- /types/process-name-sorter.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | ) 7 | 8 | // ProcessNameSorter sort the process info by program name 9 | type ProcessNameSorter struct { 10 | processes []ProcessInfo 11 | } 12 | 13 | // NewwProcessNameSorter create a new ProcessNameSorter object 14 | func NewwProcessNameSorter(processes []ProcessInfo) *ProcessNameSorter { 15 | return &ProcessNameSorter{processes: processes} 16 | } 17 | 18 | // Len return the number of programs 19 | func (pns *ProcessNameSorter) Len() int { 20 | return len(pns.processes) 21 | } 22 | 23 | // Less return true if the program name of ith process is less than the program name of jth process 24 | func (pns *ProcessNameSorter) Less(i, j int) bool { 25 | return strings.Compare(pns.processes[i].Name, pns.processes[j].Name) < 0 26 | } 27 | 28 | // Swap swap the ith program and jth program 29 | func (pns *ProcessNameSorter) Swap(i, j int) { 30 | info := pns.processes[i] 31 | pns.processes[i] = pns.processes[j] 32 | pns.processes[j] = info 33 | } 34 | 35 | // SortProcessInfos sort the process information with program name 36 | func SortProcessInfos(processes []ProcessInfo) { 37 | sorter := NewwProcessNameSorter(processes) 38 | sort.Sort(sorter) 39 | } 40 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go generate ./... 4 | project_name: supervisord 5 | builds: 6 | - id: static 7 | env: 8 | - CGO_ENABLED=1 9 | binary: supervisord_static 10 | goos: 11 | - linux 12 | goarch: 13 | - amd64 14 | ldflags: 15 | - "-linkmode external -extldflags -static" 16 | - env: 17 | - CGO_ENABLED=0 18 | ldflags: 19 | - "-s -w" 20 | binary: supervisord 21 | flags: 22 | - -tags=release 23 | goos: 24 | - windows 25 | - darwin 26 | - linux 27 | goarch: 28 | - "386" 29 | - amd64 30 | - arm 31 | - arm64 32 | goarm: 33 | - "6" 34 | - "7" 35 | hooks: 36 | post: ./upx.sh 37 | archive: 38 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 39 | format: tar.gz 40 | format_overrides: 41 | - goos: windows 42 | format: zip 43 | wrap_in_directory: true 44 | files: 45 | - none* 46 | replacements: 47 | amd64: 64-bit 48 | 386: 32-bit 49 | arm: ARM 50 | arm64: ARM64 51 | darwin: macOS 52 | linux: Linux 53 | windows: Windows 54 | openbsd: OpenBSD 55 | netbsd: NetBSD 56 | freebsd: FreeBSD 57 | release: 58 | github: 59 | owner: ochinchina 60 | name: supervisord 61 | draft: true 62 | prerelease: true 63 | name_template: "{{.ProjectName}}-v{{.Version}}-{{.ShortCommit}}" 64 | -------------------------------------------------------------------------------- /signals/signal_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package signals 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | log "github.com/sirupsen/logrus" 9 | "os" 10 | "os/exec" 11 | "syscall" 12 | ) 13 | 14 | //convert a signal name to signal 15 | func ToSignal(signalName string) (os.Signal, error) { 16 | if signalName == "HUP" { 17 | return syscall.SIGHUP, nil 18 | } else if signalName == "INT" { 19 | return syscall.SIGINT, nil 20 | } else if signalName == "QUIT" { 21 | return syscall.SIGQUIT, nil 22 | } else if signalName == "KILL" { 23 | return syscall.SIGKILL, nil 24 | } else if signalName == "USR1" { 25 | log.Warn("signal USR1 is not supported in windows") 26 | return nil, errors.New("signal USR1 is not supported in windows") 27 | } else if signalName == "USR2" { 28 | log.Warn("signal USR2 is not supported in windows") 29 | return nil, errors.New("signal USR2 is not supported in windows") 30 | } else { 31 | return syscall.SIGTERM, nil 32 | 33 | } 34 | 35 | } 36 | 37 | // 38 | // Args: 39 | // process - the process 40 | // sig - the signal 41 | // sigChildren - ignore in windows system 42 | // 43 | func Kill(process *os.Process, sig os.Signal, sigChilren bool) error { 44 | //Signal command can't kill children processes, call taskkill command to kill them 45 | cmd := exec.Command("taskkill", "/F", "/T", "/PID", fmt.Sprintf("%d", process.Pid)) 46 | err := cmd.Start() 47 | if err == nil { 48 | return cmd.Wait() 49 | } 50 | //if fail to find taskkill, fallback to normal signal 51 | return process.Signal(sig) 52 | } 53 | -------------------------------------------------------------------------------- /faults/faults.go: -------------------------------------------------------------------------------- 1 | package faults 2 | 3 | import ( 4 | xmlrpc "github.com/ochinchina/gorilla-xmlrpc/xml" 5 | ) 6 | 7 | const ( 8 | // UnknownMethod unknown xml rpc method 9 | UnknownMethod = 1 10 | // IncorrectParameters incorrect parameters result code 11 | IncorrectParameters = 2 12 | 13 | // BadArguments Bad argument result code for xml rpc 14 | BadArguments = 3 15 | 16 | // SignatureUnsupported signature unsupported result code for xml rpc 17 | SignatureUnsupported = 4 18 | 19 | // ShutdownState shutdown state result code 20 | ShutdownState = 6 21 | 22 | // BadName bad name result code 23 | BadName = 10 24 | 25 | // BadSignal bad signal result code 26 | BadSignal = 11 27 | // NoFile no such file result code 28 | NoFile = 20 29 | 30 | // NotExecutable not executable result code 31 | NotExecutable = 21 32 | 33 | // Failed failed result code 34 | Failed = 30 35 | 36 | // AbnormalTermination abnormal termination result code 37 | AbnormalTermination = 40 38 | 39 | // SpawnError spawn error result code 40 | SpawnError = 50 41 | 42 | // AlreadyStated already stated result code 43 | AlreadyStated = 60 44 | 45 | // NotRunning not running result code 46 | NotRunning = 70 47 | 48 | // Success sucess result code 49 | Success = 80 50 | 51 | // AlreadyAdded already added result code 52 | AlreadyAdded = 90 53 | 54 | // StillRunning still running result code 55 | StillRunning = 91 56 | 57 | // CantReRead can't re-read result code 58 | CantReRead = 92 59 | ) 60 | 61 | // NewFault create a Fault object as xml rpc result 62 | func NewFault(code int, desc string) error { 63 | return &xmlrpc.Fault{Code: code, String: desc} 64 | } 65 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | // InArray return true if the elem is in the array arr 4 | func InArray(elem interface{}, arr []interface{}) bool { 5 | for _, e := range arr { 6 | if e == elem { 7 | return true 8 | } 9 | } 10 | return false 11 | } 12 | 13 | // HasAllElements return true if the array arr1 contains all elements of array arr2 14 | func HasAllElements(arr1 []interface{}, arr2 []interface{}) bool { 15 | for _, e2 := range arr2 { 16 | if !InArray(e2, arr1) { 17 | return false 18 | } 19 | } 20 | return true 21 | } 22 | 23 | // StringArrayToInterfacArray convert the []string to []interface 24 | func StringArrayToInterfacArray(arr []string) []interface{} { 25 | result := make([]interface{}, 0) 26 | for _, s := range arr { 27 | result = append(result, s) 28 | } 29 | return result 30 | } 31 | 32 | // Sub return all the element in arr1 but not in arr2 33 | func Sub(arr1 []string, arr2 []string) []string { 34 | result := make([]string, 0) 35 | for _, s := range arr1 { 36 | exist := false 37 | for _, s2 := range arr2 { 38 | if s == s2 { 39 | exist = true 40 | } 41 | } 42 | if !exist { 43 | result = append(result, s) 44 | } 45 | } 46 | return result 47 | } 48 | 49 | // IsSameStringArray return true if arr1 and arr2 has exactly same elements without order care 50 | func IsSameStringArray(arr1 []string, arr2 []string) bool { 51 | if len(arr1) != len(arr2) { 52 | return false 53 | } 54 | for _, s := range arr1 { 55 | exist := false 56 | for _, s2 := range arr2 { 57 | if s2 == s { 58 | exist = true 59 | break 60 | } 61 | } 62 | if !exist { 63 | return false 64 | } 65 | } 66 | return true 67 | } 68 | -------------------------------------------------------------------------------- /types/comm-types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // ProcessInfo the running process information 8 | type ProcessInfo struct { 9 | Name string `xml:"name" json:"name"` 10 | Group string `xml:"group" json:"group"` 11 | Description string `xml:"description" json:"description"` 12 | Start int `xml:"start" json:"start"` 13 | Stop int `xml:"stop" json:"stop"` 14 | Now int `xml:"now" json:"now"` 15 | State int `xml:"state" json:"state"` 16 | Statename string `xml:"statename" json:"statename"` 17 | Spawnerr string `xml:"spawnerr" json:"spawnerr"` 18 | Exitstatus int `xml:"exitstatus" json:"exitstatus"` 19 | Logfile string `xml:"logfile" json:"logfile"` 20 | StdoutLogfile string `xml:"stdout_logfile" json:"stdout_logfile"` 21 | StderrLogfile string `xml:"stderr_logfile" json:"stderr_logfile"` 22 | Pid int `xml:"pid" json:"pid"` 23 | } 24 | 25 | // ReloadConfigResult the result of supervisor configuration reloading 26 | type ReloadConfigResult struct { 27 | AddedGroup []string 28 | ChangedGroup []string 29 | RemovedGroup []string 30 | } 31 | 32 | // ProcessSignal process signal includes program name and signal sent to it 33 | type ProcessSignal struct { 34 | Name string 35 | Signal string 36 | } 37 | 38 | // BooleanReply any rpc result with BooleanReply type 39 | type BooleanReply struct { 40 | Success bool 41 | } 42 | 43 | // GetFullName get the full name of program includes group and name 44 | func (pi ProcessInfo) GetFullName() string { 45 | if len(pi.Group) > 0 { 46 | return fmt.Sprintf("%s:%s", pi.Group, pi.Name) 47 | } 48 | return pi.Name 49 | } 50 | -------------------------------------------------------------------------------- /process/command_parser_test.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestEmptyCommandLine(t *testing.T) { 10 | args, err := parseCommand(" ") 11 | if err == nil || len(args) > 0 { 12 | t.Error("fail to parse the empty command line") 13 | } 14 | } 15 | 16 | func TestNormalCommandLine(t *testing.T) { 17 | args, err := parseCommand("program arg1 arg2") 18 | if err != nil { 19 | t.Error("fail to parse normal command line") 20 | } 21 | if args[0] != "program" || args[1] != "arg1" || args[2] != "arg2" { 22 | t.Error("fail to parse normal command line") 23 | } 24 | } 25 | 26 | func TestCommandLineWithQuotationMarks(t *testing.T) { 27 | args, err := parseCommand("program 'this is arg1' args=\"this is arg2\"") 28 | fmt.Printf("%s\n", strings.Join(args, ",")) 29 | if err != nil || len(args) != 3 { 30 | t.Error("fail to parse command line with quotation marks") 31 | } 32 | if args[0] != "program" || args[1] != "this is arg1" || args[2] != "args=\"this is arg2\"" { 33 | t.Error("fail to parse command line with quotation marks") 34 | } 35 | } 36 | 37 | func TestCommandLineArgsIsQuatationMarks(t *testing.T) { 38 | args, err := parseCommand("/home/test/nginx-1.13.0/objs/nginx -p /home/test/nginx-1.13.0 -c conf/nginx.conf -g \"daemon off;\"") 39 | fmt.Printf("%s\n", strings.Join(args, ",")) 40 | if err != nil || len(args) != 7 { 41 | t.Error("fail to pase the command line") 42 | } 43 | if args[0] != "/home/test/nginx-1.13.0/objs/nginx" || 44 | args[1] != "-p" || 45 | args[2] != "/home/test/nginx-1.13.0" || 46 | args[3] != "-c" || 47 | args[4] != "conf/nginx.conf" || 48 | args[5] != "-g" || 49 | args[6] != "daemon off;" { 50 | t.Error("fail to parse command line") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /webgui/css/bootstrap-dialog.min.css: -------------------------------------------------------------------------------- 1 | .bootstrap-dialog .modal-header{border-top-left-radius:4px;border-top-right-radius:4px}.bootstrap-dialog .bootstrap-dialog-title{color:#fff;display:inline-block;font-size:16px}.bootstrap-dialog .bootstrap-dialog-message{font-size:14px}.bootstrap-dialog .bootstrap-dialog-button-icon{margin-right:3px}.bootstrap-dialog .bootstrap-dialog-close-button{font-size:20px;float:right;filter:alpha(opacity=90);-moz-opacity:.9;-khtml-opacity:.9;opacity:.9}.bootstrap-dialog .bootstrap-dialog-close-button:hover{cursor:pointer;filter:alpha(opacity=100);-moz-opacity:1;-khtml-opacity:1;opacity:1}.bootstrap-dialog.type-default .modal-header{background-color:#fff}.bootstrap-dialog.type-default .bootstrap-dialog-title{color:#333}.bootstrap-dialog.type-info .modal-header{background-color:#5bc0de}.bootstrap-dialog.type-primary .modal-header{background-color:#428bca}.bootstrap-dialog.type-success .modal-header{background-color:#5cb85c}.bootstrap-dialog.type-warning .modal-header{background-color:#f0ad4e}.bootstrap-dialog.type-danger .modal-header{background-color:#d9534f}.bootstrap-dialog.size-large .bootstrap-dialog-title{font-size:24px}.bootstrap-dialog.size-large .bootstrap-dialog-close-button{font-size:30px}.bootstrap-dialog.size-large .bootstrap-dialog-message{font-size:18px}.bootstrap-dialog .icon-spin{display:inline-block;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;-webkit-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@-ms-keyframes spin{0%{-ms-transform:rotate(0deg)}100%{-ms-transform:rotate(359deg)}}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}} -------------------------------------------------------------------------------- /rlimit.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "syscall" 8 | ) 9 | 10 | func (s *Supervisor) checkRequiredResources() error { 11 | if minfds, vErr := s.getMinRequiredRes("minfds"); vErr == nil { 12 | return s.checkMinLimit(syscall.RLIMIT_NOFILE, "NOFILE", minfds) 13 | } 14 | if minprocs, vErr := s.getMinRequiredRes("minprocs"); vErr == nil { 15 | //RPROC = 6 16 | return s.checkMinLimit(6, "NPROC", minprocs) 17 | } 18 | return nil 19 | 20 | } 21 | 22 | func (s *Supervisor) getMinRequiredRes(resourceName string) (uint64, error) { 23 | if entry, ok := s.config.GetSupervisord(); ok { 24 | value := uint64(entry.GetInt(resourceName, 0)) 25 | if value > 0 { 26 | return value, nil 27 | } else { 28 | return 0, fmt.Errorf("No such key %s", resourceName) 29 | } 30 | } else { 31 | return 0, fmt.Errorf("No supervisord section") 32 | } 33 | 34 | } 35 | 36 | 37 | func (s *Supervisor) checkMinLimit(resource int, resourceName string, minRequiredSource uint64) error { 38 | var limit syscall.Rlimit 39 | 40 | if syscall.Getrlimit(resource, &limit) != nil { 41 | return fmt.Errorf("fail to get the %s limit", resourceName) 42 | } 43 | 44 | if minRequiredSource > limit.Max { 45 | return fmt.Errorf("%s %d is greater than Hard limit %d", resourceName, minRequiredSource, limit.Max) 46 | } 47 | 48 | if limit.Cur >= minRequiredSource { 49 | return nil 50 | } 51 | 52 | limit.Cur = limit.Max 53 | if syscall.Setrlimit(syscall.RLIMIT_NOFILE, &limit) != nil { 54 | return fmt.Errorf(fmt.Sprintf("fail to set the %s to %d", resourceName, limit.Cur)) 55 | } 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /process/command_parser.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "fmt" 5 | "unicode" 6 | ) 7 | 8 | // find the position of byte ch in the string s start from offset 9 | // 10 | // return: -1 if byte ch is not found, >= offset if the ch is found 11 | // in the string s from offset 12 | func findChar(s string, offset int, ch byte) int { 13 | for i := offset; i < len(s); i++ { 14 | if s[i] == '\\' { 15 | i++ 16 | } else if s[i] == ch { 17 | return i 18 | } 19 | } 20 | return -1 21 | } 22 | 23 | // skip all the white space and return the first position of non-space char 24 | // 25 | // return: the first position of non-space char or -1 if all the char 26 | // from offset are space 27 | func skipSpace(s string, offset int) int { 28 | for i := offset; i < len(s); i++ { 29 | if !unicode.IsSpace(rune(s[i])) { 30 | return i 31 | } 32 | } 33 | return -1 34 | } 35 | 36 | func appendArgument(arg string, args []string) []string { 37 | if arg[0] == '"' || arg[0] == '\'' { 38 | return append(args, arg[1:len(arg)-1]) 39 | } 40 | return append(args, arg) 41 | } 42 | 43 | func parseCommand(command string) ([]string, error) { 44 | args := make([]string, 0) 45 | cmdLen := len(command) 46 | for i := 0; i < cmdLen; { 47 | //find the first non-space char 48 | j := skipSpace(command, i) 49 | if j == -1 { 50 | break 51 | } 52 | i = j 53 | for ; j < cmdLen; j++ { 54 | if unicode.IsSpace(rune(command[j])) { 55 | args = appendArgument(command[i:j], args) 56 | i = j + 1 57 | break 58 | } else if command[j] == '\\' { 59 | j++ 60 | } else if command[j] == '"' || command[j] == '\'' { 61 | k := findChar(command, j+1, command[j]) 62 | if k == -1 { 63 | args = appendArgument(command[i:], args) 64 | i = cmdLen 65 | } else { 66 | args = appendArgument(command[i:k+1], args) 67 | i = k + 1 68 | } 69 | break 70 | } 71 | } 72 | if j >= cmdLen { 73 | args = appendArgument(command[i:], args) 74 | i = cmdLen 75 | } 76 | } 77 | if len(args) <= 0 { 78 | return nil, fmt.Errorf("no command from empty string") 79 | } 80 | return args, nil 81 | } 82 | -------------------------------------------------------------------------------- /logtail.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gorilla/mux" 5 | logger "github.com/ochinchina/supervisord/logger" 6 | "net/http" 7 | ) 8 | 9 | // Logtail tail the process log through http interface 10 | type Logtail struct { 11 | router *mux.Router 12 | supervisor *Supervisor 13 | } 14 | 15 | // NewLogtail create a Logtail object 16 | func NewLogtail(supervisor *Supervisor) *Logtail { 17 | return &Logtail{router: mux.NewRouter(), supervisor: supervisor} 18 | } 19 | 20 | // CreateHandler create http handlers to process the program stdout and stderr through http interface 21 | func (lt *Logtail) CreateHandler() http.Handler { 22 | lt.router.HandleFunc("/logtail/{program}/stdout", lt.getStdoutLog).Methods("GET") 23 | lt.router.HandleFunc("/logtail/{program}/stderr", lt.getStderrLog).Methods("GET") 24 | return lt.router 25 | } 26 | 27 | func (lt *Logtail) getStdoutLog(w http.ResponseWriter, req *http.Request) { 28 | lt.getLog("stdout", w, req) 29 | } 30 | 31 | func (lt *Logtail) getStderrLog(w http.ResponseWriter, req *http.Request) { 32 | lt.getLog("stderr", w, req) 33 | } 34 | 35 | func (lt *Logtail) getLog(logType string, w http.ResponseWriter, req *http.Request) { 36 | vars := mux.Vars(req) 37 | program := vars["program"] 38 | procMgr := lt.supervisor.GetManager() 39 | proc := procMgr.Find(program) 40 | if proc == nil { 41 | w.WriteHeader(http.StatusBadRequest) 42 | } else { 43 | var ok bool = false 44 | var compositeLogger *logger.CompositeLogger = nil 45 | if logType == "stdout" { 46 | compositeLogger, ok = proc.StdoutLog.(*logger.CompositeLogger) 47 | } else { 48 | compositeLogger, ok = proc.StderrLog.(*logger.CompositeLogger) 49 | } 50 | if ok { 51 | w.Header().Set("Transfer-Encoding", "chunked") 52 | w.WriteHeader(http.StatusOK) 53 | flusher, _ := w.(http.Flusher) 54 | ch := make(chan []byte, 100) 55 | chanLogger := logger.NewChanLogger(ch) 56 | compositeLogger.AddLogger(chanLogger) 57 | for { 58 | text, ok := <-ch 59 | if !ok { 60 | break 61 | } 62 | _, err := w.Write(text) 63 | if err != nil { 64 | break 65 | } 66 | flusher.Flush() 67 | } 68 | compositeLogger.RemoveLogger(chanLogger) 69 | chanLogger.Close() 70 | } 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /config/process_group_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/ochinchina/supervisord/util" 5 | "testing" 6 | ) 7 | 8 | func createTestGroup() *ProcessGroup { 9 | group := NewProcessGroup() 10 | 11 | group.Add("group1", "proc1_1") 12 | group.Add("group1", "proc1_2") 13 | group.Add("group2", "proc2_1") 14 | group.Add("group2", "proc2_2") 15 | group.Add("group2", "proc2_3") 16 | 17 | return group 18 | } 19 | 20 | func TestGetAllGroup(t *testing.T) { 21 | group := createTestGroup() 22 | 23 | groups := group.GetAllGroup() 24 | if len(groups) != 2 || !util.HasAllElements(util.StringArrayToInterfacArray(groups), []interface{}{"group1", "group2"}) { 25 | t.Fail() 26 | } 27 | 28 | } 29 | 30 | func TestGetAllProcessInGroup(t *testing.T) { 31 | group := createTestGroup() 32 | 33 | procs := group.GetAllProcess("group1") 34 | 35 | if len(procs) != 2 || !util.HasAllElements(util.StringArrayToInterfacArray(procs), []interface{}{"proc1_1", "proc1_2"}) { 36 | t.Fail() 37 | } 38 | 39 | procs = group.GetAllProcess("group10") 40 | if len(procs) != 0 { 41 | t.Fail() 42 | } 43 | } 44 | 45 | func TestInGroup(t *testing.T) { 46 | group := createTestGroup() 47 | 48 | if !group.InGroup("proc2_2", "group2") || group.InGroup("proc1_1", "group2") { 49 | t.Fail() 50 | } 51 | } 52 | 53 | func TestRemoveFromGroup(t *testing.T) { 54 | group := createTestGroup() 55 | 56 | group.Remove("proc2_1") 57 | 58 | procs := group.GetAllProcess("group2") 59 | 60 | if len(procs) != 2 || !util.HasAllElements(util.StringArrayToInterfacArray(procs), []interface{}{"proc2_2", "proc2_3"}) { 61 | t.Fail() 62 | } 63 | 64 | } 65 | 66 | func TestGroupDiff(t *testing.T) { 67 | group1 := NewProcessGroup() 68 | group1.Add("group-1", "proc-11") 69 | group1.Add("group-1", "proc-12") 70 | group1.Add("group-2", "proc-21") 71 | 72 | group2 := NewProcessGroup() 73 | group2.Add("group-1", "proc-11") 74 | group2.Add("group-1", "proc-12") 75 | group2.Add("group-1", "proc-13") 76 | group2.Add("group-3", "proc-31") 77 | 78 | added, changed, removed := group2.Sub(group1) 79 | if len(added) != 1 || added[0] != "group-3" { 80 | t.Error("Fail to get the Added groups") 81 | } 82 | if len(changed) != 1 || changed[0] != "group-1" { 83 | t.Error("Fail to get changed groups") 84 | } 85 | 86 | if len(removed) != 1 || removed[0] != "group-2" { 87 | t.Error("Fail to get removed groups") 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /config/process_sort_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | // 9 | // check if program1 is before the program2 in the Entry 10 | // 11 | func isProgramBefore(entries []*Entry, program1 string, program2 string) bool { 12 | order := 0 13 | program1Order := -1 14 | progam2Order := -1 15 | 16 | for _, entry := range entries { 17 | if entry.IsProgram() { 18 | if entry.GetProgramName() == program1 { 19 | program1Order = order 20 | } else if entry.GetProgramName() == program2 { 21 | progam2Order = order 22 | } 23 | order++ 24 | } 25 | } 26 | 27 | fmt.Printf("%s Order=%d, %s Order=%d\n", program1, program1Order, program2, progam2Order) 28 | 29 | return program1Order >= 0 && program1Order < progam2Order 30 | } 31 | func TestSortProgram(t *testing.T) { 32 | entries := make([]*Entry, 0) 33 | entry := NewEntry(".") 34 | entry.Group = "group:group-1" 35 | entry.Name = "program:prog-1" 36 | entry.keyValues["depends_on"] = "prog-3" 37 | 38 | entries = append(entries, entry) 39 | 40 | entry = NewEntry(".") 41 | entry.Group = "group:group-1" 42 | entry.Name = "program:prog-2" 43 | entry.keyValues["depends_on"] = "prog-1" 44 | 45 | entries = append(entries, entry) 46 | 47 | entry = NewEntry(".") 48 | entry.Group = "group:group-1" 49 | entry.Name = "program:prog-3" 50 | entry.keyValues["depends_on"] = "prog-4,prog-5" 51 | 52 | entries = append(entries, entry) 53 | 54 | entry = NewEntry(".") 55 | entry.Group = "group:group-1" 56 | entry.Name = "program:prog-5" 57 | 58 | entries = append(entries, entry) 59 | 60 | entry = NewEntry(".") 61 | entry.Group = "group:group-1" 62 | entry.Name = "program:prog-4" 63 | 64 | entries = append(entries, entry) 65 | 66 | entry = NewEntry(".") 67 | entry.Group = "group:group-1" 68 | entry.Name = "program:prog-6" 69 | entry.keyValues["priority"] = "100" 70 | 71 | entries = append(entries, entry) 72 | 73 | entry = NewEntry(".") 74 | entry.Group = "group:group-1" 75 | entry.Name = "program:prog-7" 76 | entry.keyValues["priority"] = "99" 77 | 78 | entries = append(entries, entry) 79 | 80 | result := sortProgram(entries) 81 | for _, e := range result { 82 | fmt.Printf("%s\n", e.GetProgramName()) 83 | } 84 | 85 | if !isProgramBefore(result, "prog-5", "prog-3") || 86 | !isProgramBefore(result, "prog-3", "prog-1") || 87 | !isProgramBefore(result, "prog-1", "prog-2") || 88 | !isProgramBefore(result, "prog-7", "prog-6") { 89 | t.Error("Program sort is incorrect") 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /config/string_expression.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // StringExpression replace the python String like "%(var)s" to string 11 | type StringExpression struct { 12 | env map[string]string // the environment variable used to replace the var in the python expression 13 | } 14 | 15 | // NewStringExpression create a new StringExpression with the environment variables 16 | func NewStringExpression(envs ...string) *StringExpression { 17 | se := &StringExpression{env: make(map[string]string)} 18 | 19 | for _, env := range os.Environ() { 20 | t := strings.Split(env, "=") 21 | se.env["ENV_"+t[0]] = t[1] 22 | } 23 | n := len(envs) 24 | for i := 0; i+1 < n; i += 2 { 25 | se.env[envs[i]] = envs[i+1] 26 | } 27 | 28 | hostname, err := os.Hostname() 29 | if err == nil { 30 | se.env["host_node_name"] = hostname 31 | } 32 | 33 | return se 34 | 35 | } 36 | 37 | // Add add the environment variable (key,value) 38 | func (se *StringExpression) Add(key string, value string) *StringExpression { 39 | se.env[key] = value 40 | return se 41 | } 42 | 43 | // Eval evaluate the expression include "%(var)s" and return the string after replacing the var 44 | func (se *StringExpression) Eval(s string) (string, error) { 45 | for { 46 | //find variable start indicator 47 | start := strings.Index(s, "%(") 48 | 49 | if start == -1 { 50 | return s, nil 51 | } 52 | 53 | end := start + 1 54 | n := len(s) 55 | 56 | //find variable end indicator 57 | for end < n && s[end] != ')' { 58 | end++ 59 | } 60 | 61 | //find the type of the variable 62 | typ := end + 1 63 | for typ < n && !((s[typ] >= 'a' && s[typ] <= 'z') || (s[typ] >= 'A' && s[typ] <= 'Z')) { 64 | typ++ 65 | } 66 | 67 | //evaluate the variable 68 | if typ < n { 69 | varName := s[start+2 : end] 70 | 71 | varValue, ok := se.env[varName] 72 | 73 | if !ok { 74 | return "", fmt.Errorf("fail to find the environment variable %s", varName) 75 | } 76 | if s[typ] == 'd' { 77 | i, err := strconv.Atoi(varValue) 78 | if err != nil { 79 | return "", fmt.Errorf("can't convert %s to integer", varValue) 80 | } 81 | s = s[0:start] + fmt.Sprintf("%"+s[end+1:typ+1], i) + s[typ+1:] 82 | } else if s[typ] == 's' { 83 | s = s[0:start] + varValue + s[typ+1:] 84 | } else { 85 | return "", fmt.Errorf("not implement type:%v", s[typ]) 86 | } 87 | } else { 88 | return "", fmt.Errorf("invalid string expression format") 89 | } 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /content_checker_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestBaseCheckOk(t *testing.T) { 11 | checker := NewBaseChecker([]string{"Hello", "world"}, 10) 12 | 13 | go func() { 14 | checker.Write([]byte("this is a world")) 15 | time.Sleep(2 * time.Second) 16 | checker.Write([]byte("Hello, how are you?")) 17 | }() 18 | if !checker.Check() { 19 | t.Fail() 20 | } 21 | } 22 | 23 | func TestBaseCheckFail(t *testing.T) { 24 | checker := NewBaseChecker([]string{"Hello", "world"}, 2) 25 | 26 | go func() { 27 | checker.Write([]byte("this is a world")) 28 | }() 29 | if checker.Check() { 30 | t.Fail() 31 | } 32 | } 33 | 34 | func TestTcpCheckOk(t *testing.T) { 35 | go func() { 36 | listener, err := net.Listen("tcp", ":8999") 37 | if err == nil { 38 | defer listener.Close() 39 | conn, err := listener.Accept() 40 | if err == nil { 41 | defer conn.Close() 42 | conn.Write([]byte("this is a world")) 43 | time.Sleep(3 * time.Second) 44 | conn.Write([]byte("Hello, how are you?")) 45 | } 46 | } 47 | }() 48 | checker := NewTCPChecker("127.0.0.1", 8999, []string{"Hello", "world"}, 10) 49 | if !checker.Check() { 50 | t.Fail() 51 | } 52 | } 53 | 54 | func TestTcpCheckFail(t *testing.T) { 55 | go func() { 56 | listener, err := net.Listen("tcp", ":8989") 57 | if err == nil { 58 | conn, err := listener.Accept() 59 | if err == nil { 60 | conn.Write([]byte("this is a world")) 61 | time.Sleep(3 * time.Second) 62 | conn.Close() 63 | } 64 | listener.Close() 65 | } 66 | }() 67 | checker := NewTCPChecker("127.0.0.1", 8989, []string{"Hello", "world"}, 2) 68 | if checker.Check() { 69 | t.Fail() 70 | } 71 | } 72 | 73 | func TestHttpCheckOk(t *testing.T) { 74 | go func() { 75 | listener, err := net.Listen("tcp", ":8999") 76 | if err == nil { 77 | 78 | http.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 79 | defer listener.Close() 80 | w.WriteHeader(http.StatusOK) 81 | w.Write([]byte("this is an response")) 82 | })) 83 | 84 | } 85 | }() 86 | checker := NewHTTPChecker("http://127.0.0.1:8999", 2) 87 | if !checker.Check() { 88 | t.Fail() 89 | } 90 | } 91 | 92 | func TestHttpCheckFail(t *testing.T) { 93 | go func() { 94 | listener, err := net.Listen("tcp", ":8999") 95 | if err == nil { 96 | http.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 97 | defer listener.Close() 98 | w.WriteHeader(http.StatusNotFound) 99 | w.Write([]byte("not found")) 100 | })) 101 | 102 | } 103 | }() 104 | checker := NewHTTPChecker("http://127.0.0.1:8999", 2) 105 | if checker.Check() { 106 | t.Fail() 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /pidproxy/pidproxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "syscall" 9 | "time" 10 | ) 11 | 12 | func installSignalAndForward(pidfile string, exitIfDaemonStopped bool) { 13 | c := make(chan os.Signal, 1) 14 | installSignal(c) 15 | 16 | timer := time.After(5 * time.Second) 17 | for { 18 | select { 19 | case sig := <-c: 20 | fmt.Printf("Get a signal %v\n", sig) 21 | if allowForwardSig(sig) { 22 | forwardSignal(sig, pidfile) 23 | } 24 | 25 | if sig == syscall.SIGTERM || 26 | sig == syscall.SIGINT || 27 | sig == syscall.SIGQUIT { 28 | os.Exit(0) 29 | } 30 | case <-timer: 31 | timer = time.After(5 * time.Second) 32 | pid, err := readPid(pidfile) 33 | if err == nil && !isProcessAlive(pid) { 34 | fmt.Printf("Process %d is not alive\n", pid) 35 | if exitIfDaemonStopped { 36 | os.Exit(1) 37 | } 38 | } 39 | 40 | } 41 | } 42 | } 43 | 44 | func isProcessAlive(pid int) bool { 45 | proc, err := os.FindProcess(pid) 46 | if err != nil { 47 | return false 48 | } 49 | return proc.Signal(syscall.Signal(0)) == nil 50 | } 51 | 52 | func forwardSignal(sig os.Signal, pidfile string) { 53 | pid, err := readPid(pidfile) 54 | 55 | if err == nil { 56 | fmt.Printf("Read pid %d from file %s\n", pid, pidfile) 57 | proc, err := os.FindProcess(pid) 58 | if err == nil { 59 | err = proc.Signal(sig) 60 | if err == nil { 61 | fmt.Printf("Succeed to send signal %v to process %d\n", sig, pid) 62 | return 63 | } 64 | } 65 | fmt.Printf("Fail to send signal %v to process %d with error:%v\n", sig, pid, err) 66 | } else { 67 | fmt.Printf("Fail to read pid from file %s with error:%v\n", pidfile, err) 68 | } 69 | } 70 | 71 | func readPid(pidfile string) (int, error) { 72 | file, err := os.Open(pidfile) 73 | if err == nil { 74 | defer file.Close() 75 | pid := 0 76 | n, err := fmt.Fscanf(file, "%d", &pid) 77 | if err == nil { 78 | if n != 1 { 79 | return pid, errors.New("Fail to get pid from file") 80 | } 81 | return pid, nil 82 | } 83 | } 84 | return 0, err 85 | } 86 | 87 | func startApplication(command string, args []string) { 88 | cmd := exec.Command(command) 89 | for _, arg := range args { 90 | cmd.Args = append(cmd.Args, arg) 91 | } 92 | 93 | err := cmd.Start() 94 | 95 | if err == nil { 96 | err = cmd.Wait() 97 | if err == nil { 98 | fmt.Printf("Succeed to start program:%s\n", command) 99 | return 100 | } 101 | 102 | } 103 | fmt.Printf("Fail to start program with error %v\n", err) 104 | os.Exit(1) 105 | } 106 | 107 | func printUsage() { 108 | fmt.Println("Usage: pidproxy [-exit-daemon-stop] [args...]") 109 | fmt.Println("exit-daemon-stop exit this pidproxy if the started daemon exits") 110 | } 111 | func main() { 112 | var args []string 113 | exitIfDaemonStopped := false 114 | if os.Args[1] == "-exit-daemon-stop" { 115 | exitIfDaemonStopped = true 116 | args = os.Args[2:] 117 | } else { 118 | args = os.Args[1:] 119 | } 120 | 121 | if len(args) < 2 { 122 | printUsage() 123 | } else { 124 | pidfile := args[0] 125 | command := args[1] 126 | 127 | startApplication(command, args[2:]) 128 | installSignalAndForward(pidfile, exitIfDaemonStopped) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /config_template.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | var configTemplate = `[unix_http_server] 9 | file=/tmp/supervisord.sock 10 | #chmod=not support 11 | #chown=not support 12 | username=test1 13 | password={SHA}82ab876d1387bfafe46cc1c8a2ef074eae50cb1d 14 | 15 | [inet_http_server] 16 | port=127.0.0.1:9001 17 | username=test1 18 | password=thepassword 19 | 20 | [supervisord] 21 | logfile=%(here)s/supervisord.log 22 | logfile_maxbytes=50MB 23 | logfile_backups=10 24 | loglevel=info 25 | pidfile=%(here)s/supervisord.pid 26 | #umask=not support 27 | #nodaemon=not support 28 | #minfds=not support 29 | #minprocs=not support 30 | #nocleanup=not support 31 | #childlogdir=not support 32 | #user=not support 33 | #directory=not support 34 | #strip_ansi=not support 35 | #environment=not support 36 | identifier=supervisor 37 | 38 | [program:x] 39 | command=/bin/cat 40 | process_name=%(program_name)s 41 | numprocs=1 42 | #numprocs_start=not support 43 | autostart=true 44 | startsecs=3 45 | startretries=3 46 | autorestart=true 47 | exitcodes=0,2 48 | stopsignal=TERM 49 | stopwaitsecs=10 50 | stopasgroup=true 51 | killasgroup=true 52 | user=user1 53 | redirect_stderr=false 54 | stdout_logfile=AUTO 55 | stdout_logfile_maxbytes=50MB 56 | stdout_logfile_backups=10 57 | stdout_capture_maxbytes=0 58 | stdout_events_enabled=true 59 | stderr_logfile=AUTO 60 | stderr_logfile_maxbytes=50MB 61 | stderr_logfile_backups=10 62 | stderr_capture_maxbytes=0 63 | stderr_events_enabled=false 64 | environment=KEY="val",KEY2="val2" 65 | directory=/tmp 66 | #umask=not support 67 | serverurl=AUTO 68 | 69 | [include] 70 | files=/an/absolute/filename.conf /an/absolute/*.conf foo.conf config??.conf 71 | 72 | [group:x] 73 | programs=bar,baz 74 | priority=999 75 | 76 | [eventlistener:x] 77 | command=/bin/eventlistener 78 | process_name=%(program_name)s 79 | numprocs=1 80 | #numprocs_start=not support 81 | autostart=true 82 | startsecs=3 83 | startretries=3 84 | autorestart=true 85 | exitcodes=0,2 86 | stopsignal=TERM 87 | stopwaitsecs=10 88 | #stopasgroup=not support 89 | #killasgroup=not support 90 | user=user1 91 | redirect_stderr=false 92 | stdout_logfile=AUTO 93 | stdout_logfile_maxbytes=50MB 94 | stdout_logfile_backups=10 95 | stdout_capture_maxbytes=0 96 | stdout_events_enabled=true 97 | stderr_logfile=AUTO 98 | stderr_logfile_maxbytes=50MB 99 | stderr_logfile_backups=10 100 | stderr_capture_maxbytes=0 101 | stderr_events_enabled=false 102 | environment=KEY="val",KEY2="val2" 103 | directory=/tmp 104 | #umask=not support 105 | serverurl=AUTO 106 | buffer_size=10240 107 | events=PROCESS_STATE 108 | #result_handler=not support 109 | 110 | [supervisorctl] 111 | serverurl = unix:///tmp/supervisor.sock 112 | username = chris 113 | password = 123 114 | #prompt = not support 115 | ` 116 | 117 | // InitTemplateCommand implemnts flags.Commander interface 118 | type InitTemplateCommand struct { 119 | OutFile string `short:"o" long:"output" description:"the output file name" required:"true"` 120 | } 121 | 122 | var initTemplateCommand InitTemplateCommand 123 | 124 | // Execute execute the init command 125 | func (x *InitTemplateCommand) Execute(args []string) error { 126 | f, err := os.Create(x.OutFile) 127 | if err != nil { 128 | return err 129 | } 130 | defer f.Close() 131 | return GenTemplate(f) 132 | } 133 | 134 | // GenTemplate generate the template 135 | func GenTemplate(writer io.Writer) error { 136 | _, err := writer.Write([]byte(configTemplate)) 137 | return err 138 | } 139 | 140 | func init() { 141 | parser.AddCommand("init", 142 | "initialize a template", 143 | "The init subcommand writes the supported configurations to specified file", 144 | &initTemplateCommand) 145 | 146 | } 147 | -------------------------------------------------------------------------------- /config/process_group.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "github.com/ochinchina/supervisord/util" 6 | "strings" 7 | ) 8 | 9 | // ProcessGroup manage the program and its group mapping 10 | type ProcessGroup struct { 11 | //mapping between the program and its group 12 | processGroup map[string]string 13 | } 14 | 15 | // NewProcessGroup create a ProcessGroup object 16 | func NewProcessGroup() *ProcessGroup { 17 | return &ProcessGroup{processGroup: make(map[string]string)} 18 | } 19 | 20 | // Clone clone the process group 21 | func (pg *ProcessGroup) Clone() *ProcessGroup { 22 | newPg := NewProcessGroup() 23 | for k, v := range pg.processGroup { 24 | newPg.processGroup[k] = v 25 | } 26 | return newPg 27 | } 28 | 29 | // Sub remove all the programs in other ProcessGroup from this ProcessGroup 30 | func (pg *ProcessGroup) Sub(other *ProcessGroup) (added []string, changed []string, removed []string) { 31 | thisGroup := pg.GetAllGroup() 32 | otherGroup := other.GetAllGroup() 33 | added = util.Sub(thisGroup, otherGroup) 34 | changed = make([]string, 0) 35 | removed = util.Sub(otherGroup, thisGroup) 36 | 37 | for _, group := range thisGroup { 38 | proc1 := pg.GetAllProcess(group) 39 | proc2 := other.GetAllProcess(group) 40 | if len(proc2) > 0 && !util.IsSameStringArray(proc1, proc2) { 41 | changed = append(changed, group) 42 | } 43 | } 44 | return 45 | } 46 | 47 | //Add add a process to a group 48 | func (pg *ProcessGroup) Add(group string, procName string) { 49 | pg.processGroup[procName] = group 50 | } 51 | 52 | //Remove remove a process 53 | func (pg *ProcessGroup) Remove(procName string) { 54 | delete(pg.processGroup, procName) 55 | } 56 | 57 | //GetAllGroup get all the groups 58 | func (pg *ProcessGroup) GetAllGroup() []string { 59 | groups := make(map[string]bool) 60 | for _, group := range pg.processGroup { 61 | groups[group] = true 62 | } 63 | 64 | result := make([]string, 0) 65 | for group := range groups { 66 | result = append(result, group) 67 | } 68 | return result 69 | } 70 | 71 | // GetAllProcess get all the processes in a group 72 | func (pg *ProcessGroup) GetAllProcess(group string) []string { 73 | result := make([]string, 0) 74 | for procName, groupName := range pg.processGroup { 75 | if group == groupName { 76 | result = append(result, procName) 77 | } 78 | } 79 | return result 80 | } 81 | 82 | // InGroup check if a process belongs to a group or not 83 | func (pg *ProcessGroup) InGroup(procName string, group string) bool { 84 | groupName, ok := pg.processGroup[procName] 85 | if ok && group == groupName { 86 | return true 87 | } 88 | return false 89 | } 90 | 91 | // ForEachProcess iterate all the processes and process it with procFunc 92 | func (pg *ProcessGroup) ForEachProcess(procFunc func(group string, procName string)) { 93 | for procName, groupName := range pg.processGroup { 94 | procFunc(groupName, procName) 95 | } 96 | } 97 | 98 | // GetGroup get the group name of process. If fail to find the group by 99 | // procName, set its group to defGroup and return this defGroup 100 | func (pg *ProcessGroup) GetGroup(procName string, defGroup string) string { 101 | group, ok := pg.processGroup[procName] 102 | 103 | if ok { 104 | return group 105 | } 106 | pg.processGroup[procName] = defGroup 107 | return defGroup 108 | } 109 | 110 | // String convert the process and its group mapping to human readable string 111 | func (pg *ProcessGroup) String() string { 112 | buf := bytes.NewBuffer(make([]byte, 0)) 113 | for _, group := range pg.GetAllGroup() { 114 | buf.WriteString(group) 115 | buf.WriteString(":") 116 | buf.WriteString(strings.Join(pg.GetAllProcess(group), ",")) 117 | buf.WriteString(";") 118 | } 119 | return buf.String() 120 | } 121 | -------------------------------------------------------------------------------- /b0x.yaml: -------------------------------------------------------------------------------- 1 | # all folders and files are relative to the path 2 | # where fileb0x was run at! 3 | 4 | # default: main 5 | pkg: main 6 | 7 | # destination 8 | dest: "." 9 | 10 | # gofmt 11 | # type: bool 12 | # default: false 13 | fmt: true 14 | 15 | # build tags for the main b0x.go file 16 | tags: "release" 17 | 18 | # updater allows you to update a b0x in a running server 19 | # without having to restart it 20 | updater: 21 | # disabled by default 22 | enabled: false 23 | 24 | # empty mode creates a empty b0x file with just the 25 | # server and the filesystem, then you'll have to upload 26 | # the files later using the cmd: 27 | # fileb0x -update=http://server.com:port b0x.yaml 28 | # 29 | # it avoids long compile time 30 | empty: false 31 | 32 | # amount of uploads at the same time 33 | workers: 3 34 | 35 | # to get a username and password from a env variable 36 | # leave username and password blank (username: "") 37 | # then set your username and password in the env vars 38 | # (no caps) -> fileb0x_username and fileb0x_password 39 | username: "user" 40 | password: "pass" 41 | port: 8041 42 | 43 | # compress files 44 | # at the moment, only supports gzip 45 | # 46 | # type: object 47 | compression: 48 | # activates the compression 49 | # 50 | # type: bool 51 | # default: false 52 | compress: true 53 | 54 | # valid values are: 55 | # -> "NoCompression" 56 | # -> "BestSpeed" 57 | # -> "BestCompression" 58 | # -> "DefaultCompression" or "" 59 | # 60 | # type: string 61 | # default: "DefaultCompression" # when: Compress == true && Method == "" 62 | method: "DefaultCompression" 63 | 64 | # true = do it yourself (the file is written as gzip compressed file into the memory file system) 65 | # false = decompress files at run time (while writing file into memory file system) 66 | # 67 | # type: bool 68 | # default: false 69 | keep: false 70 | 71 | # --------------- 72 | # -- DANGEROUS -- 73 | # --------------- 74 | # 75 | # cleans the destination folder (only b0xfiles) 76 | # you should use this when using the spread function 77 | # type: bool 78 | # default: false 79 | clean: false 80 | 81 | # default: ab0x.go 82 | output: "assets_release.go" 83 | 84 | # [noprefix] disables adding "a" prefix to output 85 | # type: bool 86 | # default: false 87 | noprefix: true 88 | 89 | # [unexporTed] builds non-exporTed functions, variables and types... 90 | # type: bool 91 | # default: false 92 | unexporTed: false 93 | 94 | # [spread] means it will make a file to hold all fileb0x data 95 | # and each file into a separaTed .go file 96 | # 97 | # example: 98 | # theres 2 files in the folder assets, they're: hello.json and world.txt 99 | # when spread is activaTed, fileb0x will make a file: 100 | # b0x.go or [output]'s data, assets_hello.json.go and assets_world.txt.go 101 | # 102 | # 103 | # type: bool 104 | # default: false 105 | spread: false 106 | 107 | # [lcf] log changed files when spread is active 108 | lcf: true 109 | 110 | # [debug] is a debug mode where the files are read directly from the file 111 | # sytem. Useful for web dev when files change when the server is running. 112 | # type: bool 113 | # default: false 114 | debug: false 115 | 116 | # type: array of objects 117 | custom: 118 | 119 | # type: array of strings 120 | - base: "./webgui" 121 | files: 122 | - "./webgui/index.html" 123 | - "./webgui/js/jquery-3.3.1.min.js" 124 | - "./webgui/js/popper.min.js" 125 | - "./webgui/js/bootstrap.min.js" 126 | - "./webgui/js/bootstrap-table.min.js" 127 | - "./webgui/js/bootstrap-dialog.min.js" 128 | - "./webgui/css/bootstrap.min.css" 129 | - "./webgui/css/bootstrap-table.css" 130 | - "./webgui/css/bootstrap-dialog.min.css" 131 | 132 | 133 | -------------------------------------------------------------------------------- /webgui/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.1.3 (https://getbootstrap.com/) 3 | * Copyright 2011-2018 The Bootstrap Authors 4 | * Copyright 2011-2018 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /xmlrpcclient/xml_processor.go: -------------------------------------------------------------------------------- 1 | package xmlrpcclient 2 | 3 | import ( 4 | "encoding/xml" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | // XMLPath represent the XML path in array 10 | type XMLPath struct { 11 | ElemNames []string 12 | } 13 | 14 | // NewXMLPath create a new XMLPath object 15 | func NewXMLPath() *XMLPath { 16 | return &XMLPath{ElemNames: make([]string, 0)} 17 | } 18 | 19 | // AddChildren append paths to the XMLPath 20 | func (xp *XMLPath) AddChildren(names ...string) { 21 | for _, name := range names { 22 | xp.ElemNames = append(xp.ElemNames, name) 23 | } 24 | } 25 | 26 | // AddChild add a child to the path 27 | func (xp *XMLPath) AddChild(elemName string) { 28 | xp.ElemNames = append(xp.ElemNames, elemName) 29 | } 30 | 31 | // RemoveLast remove the last element from path 32 | func (xp *XMLPath) RemoveLast() { 33 | if len(xp.ElemNames) > 0 { 34 | xp.ElemNames = xp.ElemNames[0 : len(xp.ElemNames)-1] 35 | } 36 | } 37 | 38 | // Equals check if this XMLPath object equals other XMLPath object 39 | func (xp *XMLPath) Equals(other *XMLPath) bool { 40 | if len(xp.ElemNames) != len(other.ElemNames) { 41 | return false 42 | } 43 | 44 | for i := len(xp.ElemNames) - 1; i >= 0; i-- { 45 | if xp.ElemNames[i] != other.ElemNames[i] { 46 | return false 47 | } 48 | } 49 | return true 50 | } 51 | 52 | // String convert the XMLPath to string 53 | func (xp *XMLPath) String() string { 54 | return strings.Join(xp.ElemNames, "/") 55 | } 56 | 57 | // XMLLeafProcessor the XML leaf element process function 58 | type XMLLeafProcessor func(value string) 59 | 60 | // XMLNonLeafProcessor the non-leaf element process function 61 | type XMLNonLeafProcessor func() 62 | 63 | // XMLProcessorManager the xml processor based on the XMLPath 64 | type XMLProcessorManager struct { 65 | leafProcessors map[string]XMLLeafProcessor 66 | nonLeafProcessors map[string]XMLNonLeafProcessor 67 | } 68 | 69 | // NewXMLProcessorManager create a new XMLProcessorManager object 70 | func NewXMLProcessorManager() *XMLProcessorManager { 71 | return &XMLProcessorManager{leafProcessors: make(map[string]XMLLeafProcessor), 72 | nonLeafProcessors: make(map[string]XMLNonLeafProcessor)} 73 | } 74 | 75 | // AddLeafProcessor add a leaf processor for the xml path 76 | func (xpm *XMLProcessorManager) AddLeafProcessor(path string, processor XMLLeafProcessor) { 77 | xpm.leafProcessors[path] = processor 78 | } 79 | 80 | // AddNonLeafProcessor add a non-leaf processor for the xml path 81 | func (xpm *XMLProcessorManager) AddNonLeafProcessor(path string, processor XMLNonLeafProcessor) { 82 | xpm.nonLeafProcessors[path] = processor 83 | } 84 | 85 | // ProcessLeafNode process the leaf element with xml path and its value 86 | func (xpm *XMLProcessorManager) ProcessLeafNode(path string, data string) { 87 | if processor, ok := xpm.leafProcessors[path]; ok { 88 | processor(data) 89 | } 90 | } 91 | 92 | // ProcessNonLeafNode process the non-leaf element based on the xml path 93 | func (xpm *XMLProcessorManager) ProcessNonLeafNode(path string) { 94 | if processor, ok := xpm.nonLeafProcessors[path]; ok { 95 | processor() 96 | } 97 | } 98 | 99 | // ProcessXML read the xml from reader and process it 100 | func (xpm *XMLProcessorManager) ProcessXML(reader io.Reader) { 101 | decoder := xml.NewDecoder(reader) 102 | var curData xml.CharData 103 | curPath := NewXMLPath() 104 | 105 | for { 106 | tk, err := decoder.Token() 107 | if err != nil { 108 | break 109 | } 110 | 111 | switch tk.(type) { 112 | case xml.StartElement: 113 | startElem, _ := tk.(xml.StartElement) 114 | curPath.AddChild(startElem.Name.Local) 115 | curData = nil 116 | case xml.CharData: 117 | data, _ := tk.(xml.CharData) 118 | curData = data.Copy() 119 | case xml.EndElement: 120 | if curData != nil { 121 | xpm.ProcessLeafNode(curPath.String(), string(curData)) 122 | } else { 123 | xpm.ProcessNonLeafNode(curPath.String()) 124 | } 125 | curPath.RemoveLast() 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /logger/log_unix.go: -------------------------------------------------------------------------------- 1 | // +build !windows,!nacl,!plan9 2 | 3 | package logger 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "log/syslog" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | // NewSysLogger create a local syslog 14 | func NewSysLogger(name string, logEventEmitter LogEventEmitter) *SysLogger { 15 | writer, err := syslog.New(syslog.LOG_DEBUG, name) 16 | logger := &SysLogger{logEventEmitter: logEventEmitter} 17 | if err == nil { 18 | logger.logWriter = writer 19 | } 20 | return logger 21 | } 22 | 23 | // BackendSysLogWriter a syslog writer to write the log to syslog in background 24 | type BackendSysLogWriter struct { 25 | network string 26 | raddr string 27 | priority syslog.Priority 28 | tag string 29 | logChannel chan []byte 30 | } 31 | 32 | // NewBackendSysLogWriter create a backgroud running syslog writer 33 | func NewBackendSysLogWriter(network, raddr string, priority syslog.Priority, tag string) *BackendSysLogWriter { 34 | bs := &BackendSysLogWriter{network: network, raddr: raddr, priority: priority, tag: tag, logChannel: make(chan []byte)} 35 | bs.start() 36 | return bs 37 | } 38 | 39 | func (bs *BackendSysLogWriter) start() { 40 | go func() { 41 | var writer *syslog.Writer = nil 42 | for { 43 | b, ok := <-bs.logChannel 44 | // if channel is closed 45 | if !ok { 46 | if writer != nil { 47 | writer.Close() 48 | } 49 | break 50 | } 51 | //if not connect to syslog, try to connect to it 52 | if writer == nil { 53 | writer, _ = syslog.Dial(bs.network, bs.raddr, bs.priority, bs.tag) 54 | } 55 | if writer != nil { 56 | writer.Write(b) 57 | } 58 | 59 | } 60 | }() 61 | } 62 | 63 | // Write write data to the backend syslog writer 64 | func (bs *BackendSysLogWriter) Write(b []byte) (int, error) { 65 | bs.logChannel <- b 66 | return len(b), nil 67 | } 68 | 69 | // Close close the backgroup write channel 70 | func (bs *BackendSysLogWriter) Close() error { 71 | close(bs.logChannel) 72 | return nil 73 | } 74 | 75 | // parse the configuration for syslog 76 | // the configure should be in following format: 77 | // [protocol:]host[:port] 78 | // 79 | // - protocol, should be tcp or udp 80 | // - port, if missing, for tcp it should be 6514 and for udp it should be 514 81 | // 82 | func parseSysLogConfig(config string) (protocol string, host string, port int, err error) { 83 | fields := strings.Split(config, ":") 84 | host = "" 85 | protocol = "" 86 | port = 0 87 | err = nil 88 | switch len(fields) { 89 | case 1: 90 | host = fields[0] 91 | port = 514 92 | protocol = "udp" 93 | case 2: 94 | switch fields[0] { 95 | case "tcp": 96 | host = fields[1] 97 | protocol = "tcp" 98 | port = 6514 99 | case "udp": 100 | host = fields[1] 101 | protocol = "udp" 102 | port = 514 103 | default: 104 | protocol = "udp" 105 | host = fields[0] 106 | port, err = strconv.Atoi(fields[1]) 107 | if err != nil { 108 | return 109 | } 110 | } 111 | case 3: 112 | protocol = fields[0] 113 | host = fields[1] 114 | port, err = strconv.Atoi(fields[2]) 115 | if err != nil { 116 | return 117 | } 118 | default: 119 | err = errors.New("invalid format") 120 | } 121 | return 122 | 123 | } 124 | 125 | // NewRemoteSysLogger create a network syslog 126 | func NewRemoteSysLogger(name string, config string, logEventEmitter LogEventEmitter) *SysLogger { 127 | if len(config) <= 0 { 128 | return NewSysLogger(name, logEventEmitter) 129 | } 130 | 131 | protocol, host, port, err := parseSysLogConfig(config) 132 | if err != nil { 133 | return NewSysLogger(name, logEventEmitter) 134 | } 135 | writer, err := syslog.Dial(protocol, fmt.Sprintf("%s:%d", host, port), syslog.LOG_LOCAL7|syslog.LOG_DEBUG, name) 136 | logger := &SysLogger{logEventEmitter: logEventEmitter} 137 | if writer != nil && err == nil { 138 | logger.logWriter = writer 139 | } else { 140 | logger.logWriter = NewBackendSysLogWriter(protocol, fmt.Sprintf("%s:%d", host, port), syslog.LOG_LOCAL7|syslog.LOG_DEBUG, name) 141 | } 142 | return logger 143 | 144 | } 145 | -------------------------------------------------------------------------------- /content_checker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "os/exec" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | // ContentChecker define the check interface 13 | type ContentChecker interface { 14 | Check() bool 15 | } 16 | 17 | // BaseChecker basic implementation of ContentChecker 18 | type BaseChecker struct { 19 | data string 20 | includes []string 21 | //timeout in second 22 | timeoutTime time.Time 23 | notifyChannel chan string 24 | } 25 | 26 | // NewBaseChecker create a BaseChecker object 27 | func NewBaseChecker(includes []string, timeout int) *BaseChecker { 28 | return &BaseChecker{data: "", 29 | includes: includes, 30 | timeoutTime: time.Now().Add(time.Duration(timeout) * time.Second), 31 | notifyChannel: make(chan string, 1)} 32 | } 33 | 34 | // Write write the data to the checker 35 | func (bc *BaseChecker) Write(b []byte) (int, error) { 36 | bc.notifyChannel <- string(b) 37 | return len(b), nil 38 | } 39 | 40 | func (bc *BaseChecker) isReady() bool { 41 | allFound := true 42 | for _, include := range bc.includes { 43 | if strings.Index(bc.data, include) == -1 { 44 | allFound = false 45 | break 46 | } 47 | } 48 | return allFound 49 | } 50 | 51 | // Check check the content of input data 52 | func (bc *BaseChecker) Check() bool { 53 | d := bc.timeoutTime.Sub(time.Now()) 54 | if d < 0 { 55 | return false 56 | } 57 | timeoutSignal := time.After(d) 58 | 59 | for { 60 | select { 61 | case data := <-bc.notifyChannel: 62 | bc.data = bc.data + data 63 | if bc.isReady() { 64 | return true 65 | } 66 | case <-timeoutSignal: 67 | return false 68 | } 69 | } 70 | } 71 | 72 | // ScriptChecker implement ContentChecker by calling external script 73 | type ScriptChecker struct { 74 | args []string 75 | } 76 | 77 | // NewScriptChecker create a ScriptChecker object 78 | func NewScriptChecker(args []string) *ScriptChecker { 79 | return &ScriptChecker{args: args} 80 | } 81 | 82 | // Check check the the return code of script. If the return code is 0, the 83 | // check is success 84 | func (sc *ScriptChecker) Check() bool { 85 | cmd := exec.Command(sc.args[0]) 86 | if len(sc.args) > 1 { 87 | cmd.Args = sc.args 88 | } 89 | err := cmd.Run() 90 | return err == nil && cmd.ProcessState != nil && cmd.ProcessState.Success() 91 | } 92 | 93 | // TCPChecker check by TCP protocol 94 | type TCPChecker struct { 95 | host string 96 | port int 97 | conn net.Conn 98 | baseChecker *BaseChecker 99 | } 100 | 101 | // NewTCPChecker create a TCPChecker object 102 | func NewTCPChecker(host string, port int, includes []string, timeout int) *TCPChecker { 103 | checker := &TCPChecker{host: host, 104 | port: port, 105 | baseChecker: NewBaseChecker(includes, timeout)} 106 | checker.start() 107 | return checker 108 | } 109 | 110 | func (tc *TCPChecker) start() { 111 | go func() { 112 | b := make([]byte, 1024) 113 | var err error 114 | for { 115 | tc.conn, err = net.Dial("tcp", fmt.Sprintf("%s:%d", tc.host, tc.port)) 116 | if err == nil || tc.baseChecker.timeoutTime.Before(time.Now()) { 117 | break 118 | } 119 | } 120 | 121 | if err == nil { 122 | for { 123 | n, err := tc.conn.Read(b) 124 | if err != nil { 125 | break 126 | } 127 | tc.baseChecker.Write(b[0:n]) 128 | } 129 | } 130 | }() 131 | } 132 | 133 | // Check check if it is ready by reading the tcp data 134 | func (tc *TCPChecker) Check() bool { 135 | ret := tc.baseChecker.Check() 136 | if tc.conn != nil { 137 | tc.conn.Close() 138 | } 139 | return ret 140 | } 141 | 142 | // HTTPChecker implements the ContentChcker by HTTP protocol 143 | type HTTPChecker struct { 144 | url string 145 | timeoutTime time.Time 146 | } 147 | 148 | // NewHTTPChecker create a HTTPChecker object 149 | func NewHTTPChecker(url string, timeout int) *HTTPChecker { 150 | return &HTTPChecker{url: url, 151 | timeoutTime: time.Now().Add(time.Duration(timeout) * time.Second)} 152 | } 153 | 154 | // Check check the content of HTTP response 155 | func (hc *HTTPChecker) Check() bool { 156 | for { 157 | if hc.timeoutTime.After(time.Now()) { 158 | resp, err := http.Get(hc.url) 159 | if err == nil { 160 | return resp.StatusCode >= 200 && resp.StatusCode < 300 161 | } 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "github.com/jessevdk/go-flags" 7 | log "github.com/sirupsen/logrus" 8 | "os" 9 | "os/signal" 10 | "path/filepath" 11 | "runtime" 12 | "strings" 13 | "syscall" 14 | "unicode" 15 | ) 16 | 17 | // Options the command line options 18 | type Options struct { 19 | Configuration string `short:"c" long:"configuration" description:"the configuration file"` 20 | Daemon bool `short:"d" long:"daemon" description:"run as daemon"` 21 | EnvFile string `long:"env-file" description:"the environment file"` 22 | } 23 | 24 | func init() { 25 | log.SetOutput(os.Stdout) 26 | if runtime.GOOS == "windows" { 27 | log.SetFormatter(&log.TextFormatter{DisableColors: true, FullTimestamp: true}) 28 | } else { 29 | log.SetFormatter(&log.TextFormatter{DisableColors: false, FullTimestamp: true}) 30 | } 31 | log.SetLevel(log.DebugLevel) 32 | } 33 | 34 | func initSignals(s *Supervisor) { 35 | sigs := make(chan os.Signal, 1) 36 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 37 | go func() { 38 | sig := <-sigs 39 | log.WithFields(log.Fields{"signal": sig}).Info("receive a signal to stop all process & exit") 40 | s.procMgr.StopAllProcesses() 41 | os.Exit(-1) 42 | }() 43 | 44 | } 45 | 46 | var options Options 47 | var parser = flags.NewParser(&options, flags.Default & ^flags.PrintErrors) 48 | 49 | func loadEnvFile() { 50 | if len(options.EnvFile) <= 0 { 51 | return 52 | } 53 | //try to open the environment file 54 | f, err := os.Open(options.EnvFile) 55 | if err != nil { 56 | log.WithFields(log.Fields{"file": options.EnvFile}).Error("Fail to open environment file") 57 | return 58 | } 59 | defer f.Close() 60 | reader := bufio.NewReader(f) 61 | for { 62 | //for each line 63 | line, err := reader.ReadString('\n') 64 | if err != nil { 65 | break 66 | } 67 | //if line starts with '#', it is a comment line, ignore it 68 | line = strings.TrimSpace(line) 69 | if len(line) > 0 && line[0] == '#' { 70 | continue 71 | } 72 | //if environment variable is exported with "export" 73 | if strings.HasPrefix(line, "export") && len(line) > len("export") && unicode.IsSpace(rune(line[len("export")])) { 74 | line = strings.TrimSpace(line[len("export"):]) 75 | } 76 | //split the environment variable with "=" 77 | pos := strings.Index(line, "=") 78 | if pos != -1 { 79 | k := strings.TrimSpace(line[0:pos]) 80 | v := strings.TrimSpace(line[pos+1:]) 81 | //if key and value are not empty, put it into the environment 82 | if len(k) > 0 && len(v) > 0 { 83 | os.Setenv(k, v) 84 | } 85 | } 86 | } 87 | } 88 | 89 | // find the supervisord.conf in following order: 90 | // 91 | // 1. $CWD/supervisord.conf 92 | // 2. $CWD/etc/supervisord.conf 93 | // 3. /etc/supervisord.conf 94 | // 4. /etc/supervisor/supervisord.conf (since Supervisor 3.3.0) 95 | // 5. ../etc/supervisord.conf (Relative to the executable) 96 | // 6. ../supervisord.conf (Relative to the executable) 97 | func findSupervisordConf() (string, error) { 98 | possibleSupervisordConf := []string{options.Configuration, 99 | "./supervisord.conf", 100 | "./etc/supervisord.conf", 101 | "/etc/supervisord.conf", 102 | "/etc/supervisor/supervisord.conf", 103 | "../etc/supervisord.conf", 104 | "../supervisord.conf"} 105 | 106 | for _, file := range possibleSupervisordConf { 107 | if _, err := os.Stat(file); err == nil { 108 | absFile, err := filepath.Abs(file) 109 | if err == nil { 110 | return absFile, nil 111 | } 112 | return file, nil 113 | } 114 | } 115 | 116 | return "", fmt.Errorf("fail to find supervisord.conf") 117 | } 118 | 119 | func runServer() { 120 | // infinite loop for handling Restart ('reload' command) 121 | loadEnvFile() 122 | for true { 123 | options.Configuration, _ = findSupervisordConf() 124 | s := NewSupervisor(options.Configuration) 125 | initSignals(s) 126 | if _, _, _, sErr := s.Reload(); sErr != nil { 127 | panic(sErr) 128 | } 129 | s.WaitForExit() 130 | } 131 | } 132 | 133 | func main() { 134 | ReapZombie() 135 | 136 | if _, err := parser.Parse(); err != nil { 137 | flagsErr, ok := err.(*flags.Error) 138 | if ok { 139 | switch flagsErr.Type { 140 | case flags.ErrHelp: 141 | fmt.Fprintln(os.Stdout, err) 142 | os.Exit(0) 143 | case flags.ErrCommandRequired: 144 | if options.Daemon { 145 | Deamonize(runServer) 146 | } else { 147 | runServer() 148 | } 149 | default: 150 | fmt.Fprintf(os.Stderr, "error when parsing command: %s\n", err) 151 | os.Exit(1) 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /config/process_sort.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | ) 7 | 8 | // ProgramByPriority sort program by its priority 9 | type ProgramByPriority []*Entry 10 | 11 | // Len number of programs 12 | func (p ProgramByPriority) Len() int { 13 | return len(p) 14 | } 15 | 16 | // Swap swap program i and program j 17 | func (p ProgramByPriority) Swap(i, j int) { 18 | p[i], p[j] = p[j], p[i] 19 | } 20 | 21 | // Less return true if the priority ith program is less than the priority of jth program 22 | func (p ProgramByPriority) Less(i, j int) bool { 23 | return p[i].GetInt("priority", 999) < p[j].GetInt("priority", 999) 24 | } 25 | 26 | // ProcessSorter sort the program by its priority 27 | type ProcessSorter struct { 28 | dependsOnGraph map[string][]string 29 | procsWithooutDepends []*Entry 30 | } 31 | 32 | // NewProcessSorter create a sorter 33 | func NewProcessSorter() *ProcessSorter { 34 | return &ProcessSorter{dependsOnGraph: make(map[string][]string), 35 | procsWithooutDepends: make([]*Entry, 0)} 36 | } 37 | 38 | func (p *ProcessSorter) initDepends(programConfigs []*Entry) { 39 | //sort by dependsOn 40 | for _, config := range programConfigs { 41 | if config.IsProgram() && config.HasParameter("depends_on") { 42 | dependsOn := config.GetString("depends_on", "") 43 | progName := config.GetProgramName() 44 | for _, dependsOnProg := range strings.Split(dependsOn, ",") { 45 | dependsOnProg = strings.TrimSpace(dependsOnProg) 46 | if dependsOnProg != "" { 47 | if _, ok := p.dependsOnGraph[progName]; !ok { 48 | p.dependsOnGraph[progName] = make([]string, 0) 49 | } 50 | p.dependsOnGraph[progName] = append(p.dependsOnGraph[progName], dependsOnProg) 51 | 52 | } 53 | } 54 | } 55 | } 56 | 57 | } 58 | 59 | func (p *ProcessSorter) initProgramWithoutDepends(programConfigs []*Entry) { 60 | dependsOnPrograms := p.getDependsOnInfo() 61 | for _, config := range programConfigs { 62 | if config.IsProgram() { 63 | if _, ok := dependsOnPrograms[config.GetProgramName()]; !ok { 64 | p.procsWithooutDepends = append(p.procsWithooutDepends, config) 65 | } 66 | } 67 | } 68 | } 69 | 70 | func (p *ProcessSorter) getDependsOnInfo() map[string]string { 71 | dependsOnPrograms := make(map[string]string) 72 | 73 | for k, v := range p.dependsOnGraph { 74 | dependsOnPrograms[k] = k 75 | for _, t := range v { 76 | dependsOnPrograms[t] = t 77 | } 78 | } 79 | 80 | return dependsOnPrograms 81 | } 82 | 83 | func (p *ProcessSorter) sortDepends() []string { 84 | finishedPrograms := make(map[string]string) 85 | progsWithDependsInfo := p.getDependsOnInfo() 86 | progsStartOrder := make([]string, 0) 87 | 88 | //get all process without depends 89 | for progName := range progsWithDependsInfo { 90 | if _, ok := p.dependsOnGraph[progName]; !ok { 91 | finishedPrograms[progName] = progName 92 | progsStartOrder = append(progsStartOrder, progName) 93 | } 94 | } 95 | 96 | for len(finishedPrograms) < len(progsWithDependsInfo) { 97 | for progName := range p.dependsOnGraph { 98 | if _, ok := finishedPrograms[progName]; !ok && p.inFinishedPrograms(progName, finishedPrograms) { 99 | finishedPrograms[progName] = progName 100 | progsStartOrder = append(progsStartOrder, progName) 101 | } 102 | } 103 | } 104 | 105 | return progsStartOrder 106 | } 107 | 108 | func (p *ProcessSorter) inFinishedPrograms(programName string, finishedPrograms map[string]string) bool { 109 | if dependsOn, ok := p.dependsOnGraph[programName]; ok { 110 | for _, dependProgram := range dependsOn { 111 | if _, finished := finishedPrograms[dependProgram]; !finished { 112 | return false 113 | } 114 | } 115 | } 116 | return true 117 | } 118 | 119 | /*func (p *ProcessSorter) SortProcess(procs []*Process) []*Process { 120 | prog_configs := make([]*Entry, 0) 121 | for _, proc := range procs { 122 | if proc.config.IsProgram() { 123 | prog_configs = append(prog_configs, proc.config) 124 | } 125 | } 126 | 127 | result := make([]*Process, 0) 128 | for _, config := range p.SortProgram(prog_configs) { 129 | for _, proc := range procs { 130 | if proc.config == config { 131 | result = append(result, proc) 132 | } 133 | } 134 | } 135 | 136 | return result 137 | }*/ 138 | 139 | // SortProgram sort the program and return the result 140 | func (p *ProcessSorter) SortProgram(programConfigs []*Entry) []*Entry { 141 | p.initDepends(programConfigs) 142 | p.initProgramWithoutDepends(programConfigs) 143 | result := make([]*Entry, 0) 144 | 145 | for _, prog := range p.sortDepends() { 146 | for _, config := range programConfigs { 147 | if config.IsProgram() && config.GetProgramName() == prog { 148 | result = append(result, config) 149 | } 150 | } 151 | } 152 | 153 | sort.Sort(ProgramByPriority(p.procsWithooutDepends)) 154 | for _, p := range p.procsWithooutDepends { 155 | result = append(result, p) 156 | } 157 | return result 158 | } 159 | 160 | /*func sortProcess(procs []*Process) []*Process { 161 | return NewProcessSorter().SortProcess(procs) 162 | }*/ 163 | 164 | func sortProgram(configs []*Entry) []*Entry { 165 | return NewProcessSorter().SortProgram(configs) 166 | } 167 | -------------------------------------------------------------------------------- /rest-rpc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/gorilla/mux" 6 | "github.com/ochinchina/supervisord/types" 7 | "io/ioutil" 8 | "net/http" 9 | ) 10 | 11 | // SupervisorRestful the restful interface to control the programs defined in configuration file 12 | type SupervisorRestful struct { 13 | router *mux.Router 14 | supervisor *Supervisor 15 | } 16 | 17 | // NewSupervisorRestful create a new SupervisorRestful object 18 | func NewSupervisorRestful(supervisor *Supervisor) *SupervisorRestful { 19 | return &SupervisorRestful{router: mux.NewRouter(), supervisor: supervisor} 20 | } 21 | 22 | // CreateProgramHandler create http handler to process program related restful request 23 | func (sr *SupervisorRestful) CreateProgramHandler() http.Handler { 24 | sr.router.HandleFunc("/program/list", sr.ListProgram).Methods("GET") 25 | sr.router.HandleFunc("/program/start/{name}", sr.StartProgram).Methods("POST", "PUT") 26 | sr.router.HandleFunc("/program/stop/{name}", sr.StopProgram).Methods("POST", "PUT") 27 | sr.router.HandleFunc("/program/log/{name}/stdout", sr.ReadStdoutLog).Methods("GET") 28 | sr.router.HandleFunc("/program/startPrograms", sr.StartPrograms).Methods("POST", "PUT") 29 | sr.router.HandleFunc("/program/stopPrograms", sr.StopPrograms).Methods("POST", "PUT") 30 | return sr.router 31 | } 32 | 33 | // CreateSupervisorHandler create http rest interface to control supervisor itself 34 | func (sr *SupervisorRestful) CreateSupervisorHandler() http.Handler { 35 | sr.router.HandleFunc("/supervisor/shutdown", sr.Shutdown).Methods("PUT", "POST") 36 | sr.router.HandleFunc("/supervisor/reload", sr.Reload).Methods("PUT", "POST") 37 | return sr.router 38 | } 39 | 40 | // ListProgram list the status of all the programs 41 | // 42 | // json array to present the status of all programs 43 | func (sr *SupervisorRestful) ListProgram(w http.ResponseWriter, req *http.Request) { 44 | result := struct{ AllProcessInfo []types.ProcessInfo }{make([]types.ProcessInfo, 0)} 45 | if sr.supervisor.GetAllProcessInfo(nil, nil, &result) == nil { 46 | json.NewEncoder(w).Encode(result.AllProcessInfo) 47 | } else { 48 | r := map[string]bool{"success": false} 49 | json.NewEncoder(w).Encode(r) 50 | } 51 | } 52 | 53 | // StartProgram start the given program through restful interface 54 | func (sr *SupervisorRestful) StartProgram(w http.ResponseWriter, req *http.Request) { 55 | defer req.Body.Close() 56 | params := mux.Vars(req) 57 | success, err := sr._startProgram(params["name"]) 58 | r := map[string]bool{"success": err == nil && success} 59 | json.NewEncoder(w).Encode(&r) 60 | } 61 | 62 | func (sr *SupervisorRestful) _startProgram(program string) (bool, error) { 63 | startArgs := StartProcessArgs{Name: program, Wait: true} 64 | result := struct{ Success bool }{false} 65 | err := sr.supervisor.StartProcess(nil, &startArgs, &result) 66 | return result.Success, err 67 | } 68 | 69 | // StartPrograms start one or more programs through restful interface 70 | func (sr *SupervisorRestful) StartPrograms(w http.ResponseWriter, req *http.Request) { 71 | defer req.Body.Close() 72 | var b []byte 73 | var err error 74 | 75 | if b, err = ioutil.ReadAll(req.Body); err != nil { 76 | w.WriteHeader(400) 77 | w.Write([]byte("not a valid request")) 78 | return 79 | } 80 | 81 | var programs []string 82 | if err = json.Unmarshal(b, &programs); err != nil { 83 | w.WriteHeader(400) 84 | w.Write([]byte("not a valid request")) 85 | } else { 86 | for _, program := range programs { 87 | sr._startProgram(program) 88 | } 89 | w.Write([]byte("Success to start the programs")) 90 | } 91 | } 92 | 93 | // StopProgram stop a program through the restful interface 94 | func (sr *SupervisorRestful) StopProgram(w http.ResponseWriter, req *http.Request) { 95 | defer req.Body.Close() 96 | 97 | params := mux.Vars(req) 98 | success, err := sr._stopProgram(params["name"]) 99 | r := map[string]bool{"success": err == nil && success} 100 | json.NewEncoder(w).Encode(&r) 101 | } 102 | 103 | func (sr *SupervisorRestful) _stopProgram(programName string) (bool, error) { 104 | stopArgs := StartProcessArgs{Name: programName, Wait: true} 105 | result := struct{ Success bool }{false} 106 | err := sr.supervisor.StopProcess(nil, &stopArgs, &result) 107 | return result.Success, err 108 | } 109 | 110 | // StopPrograms stop programs through the restful interface 111 | func (sr *SupervisorRestful) StopPrograms(w http.ResponseWriter, req *http.Request) { 112 | defer req.Body.Close() 113 | 114 | var programs []string 115 | var b []byte 116 | var err error 117 | if b, err = ioutil.ReadAll(req.Body); err != nil { 118 | w.WriteHeader(400) 119 | w.Write([]byte("not a valid request")) 120 | return 121 | } 122 | 123 | if err := json.Unmarshal(b, &programs); err != nil { 124 | w.WriteHeader(400) 125 | w.Write([]byte("not a valid request")) 126 | } else { 127 | for _, program := range programs { 128 | sr._stopProgram(program) 129 | } 130 | w.Write([]byte("Success to stop the programs")) 131 | } 132 | 133 | } 134 | 135 | // ReadStdoutLog read the stdout of given program 136 | func (sr *SupervisorRestful) ReadStdoutLog(w http.ResponseWriter, req *http.Request) { 137 | } 138 | 139 | // Shutdown shutdown the supervisor itself 140 | func (sr *SupervisorRestful) Shutdown(w http.ResponseWriter, req *http.Request) { 141 | defer req.Body.Close() 142 | 143 | reply := struct{ Ret bool }{false} 144 | sr.supervisor.Shutdown(nil, nil, &reply) 145 | w.Write([]byte("Shutdown...")) 146 | } 147 | 148 | // Reload reload the supervisor configuration file through rest interface 149 | func (sr *SupervisorRestful) Reload(w http.ResponseWriter, req *http.Request) { 150 | defer req.Body.Close() 151 | 152 | reply := struct{ Ret bool }{false} 153 | sr.supervisor.Reload() 154 | r := map[string]bool{"success": reply.Ret} 155 | json.NewEncoder(w).Encode(&r) 156 | } 157 | -------------------------------------------------------------------------------- /process/process_manager.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "sync" 7 | 8 | "github.com/ochinchina/supervisord/config" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // Manager manage all the process in the supervisor 13 | type Manager struct { 14 | procs map[string]*Process 15 | eventListeners map[string]*Process 16 | lock sync.Mutex 17 | } 18 | 19 | // NewManager create a new Manager object 20 | func NewManager() *Manager { 21 | return &Manager{procs: make(map[string]*Process), 22 | eventListeners: make(map[string]*Process), 23 | } 24 | } 25 | 26 | // CreateProcess create a process (program or event listener) and add to this manager 27 | func (pm *Manager) CreateProcess(supervisorID string, config *config.Entry) *Process { 28 | pm.lock.Lock() 29 | defer pm.lock.Unlock() 30 | if config.IsProgram() { 31 | return pm.createProgram(supervisorID, config) 32 | } else if config.IsEventListener() { 33 | return pm.createEventListener(supervisorID, config) 34 | } else { 35 | return nil 36 | } 37 | } 38 | 39 | // StartAutoStartPrograms start all the program if its autostart is true 40 | func (pm *Manager) StartAutoStartPrograms() { 41 | pm.ForEachProcess(func(proc *Process) { 42 | if proc.isAutoStart() { 43 | proc.Start(false) 44 | } 45 | }) 46 | } 47 | 48 | func (pm *Manager) createProgram(supervisorID string, config *config.Entry) *Process { 49 | procName := config.GetProgramName() 50 | 51 | proc, ok := pm.procs[procName] 52 | 53 | if !ok { 54 | proc = NewProcess(supervisorID, config) 55 | pm.procs[procName] = proc 56 | } 57 | log.Info("create process:", procName) 58 | return proc 59 | } 60 | 61 | func (pm *Manager) createEventListener(supervisorID string, config *config.Entry) *Process { 62 | eventListenerName := config.GetEventListenerName() 63 | 64 | evtListener, ok := pm.eventListeners[eventListenerName] 65 | 66 | if !ok { 67 | evtListener = NewProcess(supervisorID, config) 68 | pm.eventListeners[eventListenerName] = evtListener 69 | } 70 | log.Info("create event listener:", eventListenerName) 71 | return evtListener 72 | } 73 | 74 | // Add add the process to this process manager 75 | func (pm *Manager) Add(name string, proc *Process) { 76 | pm.lock.Lock() 77 | defer pm.lock.Unlock() 78 | pm.procs[name] = proc 79 | log.Info("add process:", name) 80 | } 81 | 82 | // Remove remove the process from the manager 83 | // 84 | // Arguments: 85 | // name - the name of program 86 | // 87 | // Return the process or nil 88 | func (pm *Manager) Remove(name string) *Process { 89 | pm.lock.Lock() 90 | defer pm.lock.Unlock() 91 | proc, _ := pm.procs[name] 92 | delete(pm.procs, name) 93 | log.Info("remove process:", name) 94 | return proc 95 | } 96 | 97 | // Find find process by program name return process if found or nil if not found 98 | func (pm *Manager) Find(name string) *Process { 99 | procs := pm.FindMatch(name) 100 | if len(procs) == 1 { 101 | if procs[0].GetName() == name || name == fmt.Sprintf("%s:%s", procs[0].GetGroup(), procs[0].GetName()) { 102 | return procs[0] 103 | } 104 | } 105 | return nil 106 | } 107 | 108 | // FindMatch find the program with one of following format: 109 | // - group:program 110 | // - group:* 111 | // - program 112 | func (pm *Manager) FindMatch(name string) []*Process { 113 | result := make([]*Process, 0) 114 | if pos := strings.Index(name, ":"); pos != -1 { 115 | groupName := name[0:pos] 116 | programName := name[pos+1:] 117 | pm.ForEachProcess(func(p *Process) { 118 | if p.GetGroup() == groupName { 119 | if programName == "*" || programName == p.GetName() { 120 | result = append(result, p) 121 | } 122 | } 123 | }) 124 | } else { 125 | pm.lock.Lock() 126 | defer pm.lock.Unlock() 127 | proc, ok := pm.procs[name] 128 | if ok { 129 | result = append(result, proc) 130 | } 131 | } 132 | if len(result) <= 0 { 133 | log.Info("fail to find process:", name) 134 | } 135 | return result 136 | } 137 | 138 | // Clear clear all the processes 139 | func (pm *Manager) Clear() { 140 | pm.lock.Lock() 141 | defer pm.lock.Unlock() 142 | pm.procs = make(map[string]*Process) 143 | } 144 | 145 | // ForEachProcess process each process in sync mode 146 | func (pm *Manager) ForEachProcess(procFunc func(p *Process)) { 147 | pm.lock.Lock() 148 | defer pm.lock.Unlock() 149 | 150 | procs := pm.getAllProcess() 151 | for _, proc := range procs { 152 | procFunc(proc) 153 | } 154 | } 155 | 156 | // AsyncForEachProcess handle each process in async mode 157 | // Args: 158 | // - procFunc, the function to handle the process 159 | // - done, signal the process is completed 160 | // Returns: number of total processes 161 | func (pm *Manager) AsyncForEachProcess(procFunc func(p *Process), done chan *Process) int { 162 | pm.lock.Lock() 163 | defer pm.lock.Unlock() 164 | 165 | procs := pm.getAllProcess() 166 | 167 | for _, proc := range procs { 168 | go forOneProcess(proc, procFunc, done) 169 | } 170 | return len(procs) 171 | } 172 | 173 | func forOneProcess(proc *Process, action func(p *Process), done chan *Process) { 174 | action(proc) 175 | done <- proc 176 | } 177 | 178 | func (pm *Manager) getAllProcess() []*Process { 179 | tmpProcs := make([]*Process, 0) 180 | for _, proc := range pm.procs { 181 | tmpProcs = append(tmpProcs, proc) 182 | } 183 | return sortProcess(tmpProcs) 184 | } 185 | 186 | // StopAllProcesses stop all the processes managed by this manager 187 | func (pm *Manager) StopAllProcesses() { 188 | var wg sync.WaitGroup 189 | 190 | pm.ForEachProcess(func(proc *Process) { 191 | wg.Add(1) 192 | 193 | go func(wg *sync.WaitGroup) { 194 | defer wg.Done() 195 | 196 | proc.Stop(true) 197 | }(&wg) 198 | }) 199 | 200 | wg.Wait() 201 | } 202 | 203 | func sortProcess(procs []*Process) []*Process { 204 | progConfigs := make([]*config.Entry, 0) 205 | for _, proc := range procs { 206 | if proc.config.IsProgram() { 207 | progConfigs = append(progConfigs, proc.config) 208 | } 209 | } 210 | 211 | result := make([]*Process, 0) 212 | p := config.NewProcessSorter() 213 | for _, config := range p.SortProgram(progConfigs) { 214 | for _, proc := range procs { 215 | if proc.config == config { 216 | result = append(result, proc) 217 | } 218 | } 219 | } 220 | 221 | return result 222 | } 223 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "regexp" 9 | "testing" 10 | ) 11 | 12 | func createTmpFile() (string, error) { 13 | f, err := ioutil.TempFile("", "tmp") 14 | if err == nil { 15 | f.Close() 16 | return f.Name(), err 17 | } 18 | return "", err 19 | } 20 | 21 | func saveToTmpFile(b []byte) (string, error) { 22 | f, err := createTmpFile() 23 | 24 | if err != nil { 25 | return "", err 26 | } 27 | 28 | ioutil.WriteFile(f, b, os.ModePerm) 29 | 30 | return f, nil 31 | } 32 | 33 | func parse(b []byte) (*Config, error) { 34 | fileName, err := saveToTmpFile(b) 35 | if err != nil { 36 | return nil, err 37 | } 38 | config := NewConfig(fileName) 39 | _, err = config.Load() 40 | 41 | if err != nil { 42 | return nil, err 43 | } 44 | os.Remove(fileName) 45 | return config, nil 46 | } 47 | 48 | func TestProgramConfig(t *testing.T) { 49 | config, err := parse([]byte("[program:test]\ncommand=/bin/ls")) 50 | if err != nil { 51 | t.Error("Fail to parse program") 52 | return 53 | } 54 | 55 | progs := config.GetPrograms() 56 | 57 | if len(progs) != 1 || config.GetProgram("test") == nil || config.GetProgram("app") != nil { 58 | t.Error("Fail to parse the test program") 59 | } 60 | } 61 | 62 | func TestGetBoolValueFromConfig(t *testing.T) { 63 | config, _ := parse([]byte("[program:test]\na=true\nb=false\n")) 64 | entry := config.GetProgram("test") 65 | if entry.GetBool("a", false) == false || entry.GetBool("b", true) == true || entry.GetBool("c", false) != false { 66 | t.Error("Fail to get boolean value") 67 | } 68 | } 69 | 70 | func TestGetIntValueFromConfig(t *testing.T) { 71 | config, _ := parse([]byte("[program:test]\na=1\nb=2\n")) 72 | entry := config.GetProgram("test") 73 | if entry.GetInt("a", 0) == 0 || entry.GetInt("b", 0) == 0 || entry.GetInt("c", 9) != 9 { 74 | t.Error("Fail to get integer value") 75 | } 76 | } 77 | 78 | func TestGetStringValueFromConfig(t *testing.T) { 79 | config, _ := parse([]byte("[program:test]\na=test\nb=hello\n")) 80 | entry := config.GetProgram("test") 81 | if entry.GetString("a", "") != "test" || entry.GetString("b", "") != "hello" || entry.GetString("c", "") != "" { 82 | t.Error("Fail to get string value") 83 | } 84 | } 85 | 86 | func TestGetEnvValueFromConfig(t *testing.T) { 87 | config, _ := parse([]byte("[program:test]\na=A=\"env1\",B=env2")) 88 | entry := config.GetProgram("test") 89 | envs := entry.GetEnv("a") 90 | if len(envs) != 2 || envs[0] != "A=env1" || envs[1] != "B=env2" { 91 | t.Error("Fail to get env value") 92 | } 93 | 94 | config, _ = parse([]byte("[program:test]\na=A=env1,B=\"env2\"")) 95 | entry = config.GetProgram("test") 96 | envs = entry.GetEnv("a") 97 | if len(envs) != 2 || envs[0] != "A=env1" || envs[1] != "B=env2" { 98 | t.Error("Fail to get env value") 99 | } 100 | 101 | } 102 | 103 | func TestGetBytesFromConfig(t *testing.T) { 104 | config, _ := parse([]byte("[program:test]\nA=1024\nB=2KB\nC=3MB\nD=4GB\nE=test")) 105 | entry := config.GetProgram("test") 106 | 107 | if entry.GetBytes("A", 0) != 1024 || entry.GetBytes("B", 0) != 2048 || entry.GetBytes("C", 0) != 3*1024*1024 || entry.GetBytes("D", 0) != 4*1024*1024*1024 || entry.GetBytes("E", 0) != 0 || entry.GetBytes("F", -1) != -1 { 108 | t.Error("Fail to get bytes") 109 | } 110 | 111 | } 112 | 113 | func TestGetUnitHttpServer(t *testing.T) { 114 | config, _ := parse([]byte("[program:test]\nA=1024\nB=2KB\nC=3MB\nD=4GB\nE=test\n[unix_http_server]\n")) 115 | 116 | entry, ok := config.GetUnixHTTPServer() 117 | 118 | if !ok || entry == nil { 119 | t.Error("Fail to get the unix_http_server") 120 | } 121 | 122 | if entry.GetProgramName() != "" { 123 | t.Error("there should be no program name in unix_http_server") 124 | } 125 | } 126 | 127 | func TestProgramInGroup(t *testing.T) { 128 | config, _ := parse([]byte("[program:test1]\nA=123\n[group:test]\nprograms=test1,test2\n[program:test2]\nB=hello\n[program:test3]\nC=tt")) 129 | if config.GetProgram("test1").Group != "test" { //|| config.GetProgram( "test2" ).Group != "test" || config.GetProgram( "test3" ).Group == "test" { 130 | t.Error("fail to test the program in a group") 131 | } 132 | } 133 | 134 | func TestToRegex(t *testing.T) { 135 | pattern := toRegexp("/an/absolute/*.conf") 136 | matched, err := regexp.MatchString(pattern, "/an/absolute/ab.conf") 137 | if !matched || err != nil { 138 | t.Error("fail to match the file") 139 | } 140 | 141 | matched, err = regexp.MatchString(pattern, "/an/absolute/abconf") 142 | 143 | if matched && err == nil { 144 | t.Error("fail to match the file") 145 | } 146 | 147 | pattern = toRegexp("/an/absolute/??.conf") 148 | matched, err = regexp.MatchString(pattern, "/an/absolute/ab.conf") 149 | if !matched || err != nil { 150 | t.Error("fail to match the file") 151 | } 152 | 153 | matched, err = regexp.MatchString(pattern, "/an/absolute/abconf") 154 | if matched && err == nil { 155 | t.Error("fail to match the file") 156 | } 157 | 158 | matched, err = regexp.MatchString(pattern, "/an/absolute/abc.conf") 159 | if matched && err == nil { 160 | t.Error("fail to match the file") 161 | } 162 | 163 | } 164 | 165 | func TestConfigWithInclude(t *testing.T) { 166 | dir, _ := ioutil.TempDir("", "tmp") 167 | 168 | ioutil.WriteFile(filepath.Join(dir, "file1"), []byte("[program:cat]\ncommand=pwd\nA=abc\n[include]\nfiles=*.conf"), os.ModePerm) 169 | ioutil.WriteFile(filepath.Join(dir, "file2.conf"), []byte("[program:ls]\ncommand=ls\n"), os.ModePerm) 170 | 171 | fmt.Println(filepath.Join(dir, "file1")) 172 | config := NewConfig(filepath.Join(dir, "file1")) 173 | config.Load() 174 | 175 | os.RemoveAll(filepath.Join(dir)) 176 | 177 | entry := config.GetProgram("ls") 178 | 179 | if entry == nil { 180 | t.Error("fail to include section test") 181 | } 182 | 183 | } 184 | 185 | func TestDefaultParams(t *testing.T) { 186 | config, _ := parse([]byte("[program:test]\nautorestart=true\ntest=1\n[program-default]\ncommand=/usr/bin/ls\nrestart=true\nautorestart=false")) 187 | entry := config.GetProgram("test") 188 | if entry.GetString("command", "") != "/usr/bin/ls" { 189 | t.Error("fail to get command of program") 190 | } 191 | if entry.GetString("restart", "") != "true" { 192 | t.Error("Fail to get restart value") 193 | } 194 | 195 | if entry.GetInt("test", 0) != 1 { 196 | t.Error("Fail to get test value") 197 | } 198 | if entry.GetString("autorestart", "") != "true" { 199 | t.Error("autorestart value should be true") 200 | } 201 | 202 | } 203 | -------------------------------------------------------------------------------- /webgui/css/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.1.3 (https://getbootstrap.com/) 3 | * Copyright 2011-2018 The Bootstrap Authors 4 | * Copyright 2011-2018 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */ 8 | *, 9 | *::before, 10 | *::after { 11 | box-sizing: border-box; 12 | } 13 | 14 | html { 15 | font-family: sans-serif; 16 | line-height: 1.15; 17 | -webkit-text-size-adjust: 100%; 18 | -ms-text-size-adjust: 100%; 19 | -ms-overflow-style: scrollbar; 20 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 21 | } 22 | 23 | @-ms-viewport { 24 | width: device-width; 25 | } 26 | 27 | article, aside, figcaption, figure, footer, header, hgroup, main, nav, section { 28 | display: block; 29 | } 30 | 31 | body { 32 | margin: 0; 33 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 34 | font-size: 1rem; 35 | font-weight: 400; 36 | line-height: 1.5; 37 | color: #212529; 38 | text-align: left; 39 | background-color: #fff; 40 | } 41 | 42 | [tabindex="-1"]:focus { 43 | outline: 0 !important; 44 | } 45 | 46 | hr { 47 | box-sizing: content-box; 48 | height: 0; 49 | overflow: visible; 50 | } 51 | 52 | h1, h2, h3, h4, h5, h6 { 53 | margin-top: 0; 54 | margin-bottom: 0.5rem; 55 | } 56 | 57 | p { 58 | margin-top: 0; 59 | margin-bottom: 1rem; 60 | } 61 | 62 | abbr[title], 63 | abbr[data-original-title] { 64 | text-decoration: underline; 65 | -webkit-text-decoration: underline dotted; 66 | text-decoration: underline dotted; 67 | cursor: help; 68 | border-bottom: 0; 69 | } 70 | 71 | address { 72 | margin-bottom: 1rem; 73 | font-style: normal; 74 | line-height: inherit; 75 | } 76 | 77 | ol, 78 | ul, 79 | dl { 80 | margin-top: 0; 81 | margin-bottom: 1rem; 82 | } 83 | 84 | ol ol, 85 | ul ul, 86 | ol ul, 87 | ul ol { 88 | margin-bottom: 0; 89 | } 90 | 91 | dt { 92 | font-weight: 700; 93 | } 94 | 95 | dd { 96 | margin-bottom: .5rem; 97 | margin-left: 0; 98 | } 99 | 100 | blockquote { 101 | margin: 0 0 1rem; 102 | } 103 | 104 | dfn { 105 | font-style: italic; 106 | } 107 | 108 | b, 109 | strong { 110 | font-weight: bolder; 111 | } 112 | 113 | small { 114 | font-size: 80%; 115 | } 116 | 117 | sub, 118 | sup { 119 | position: relative; 120 | font-size: 75%; 121 | line-height: 0; 122 | vertical-align: baseline; 123 | } 124 | 125 | sub { 126 | bottom: -.25em; 127 | } 128 | 129 | sup { 130 | top: -.5em; 131 | } 132 | 133 | a { 134 | color: #007bff; 135 | text-decoration: none; 136 | background-color: transparent; 137 | -webkit-text-decoration-skip: objects; 138 | } 139 | 140 | a:hover { 141 | color: #0056b3; 142 | text-decoration: underline; 143 | } 144 | 145 | a:not([href]):not([tabindex]) { 146 | color: inherit; 147 | text-decoration: none; 148 | } 149 | 150 | a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus { 151 | color: inherit; 152 | text-decoration: none; 153 | } 154 | 155 | a:not([href]):not([tabindex]):focus { 156 | outline: 0; 157 | } 158 | 159 | pre, 160 | code, 161 | kbd, 162 | samp { 163 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 164 | font-size: 1em; 165 | } 166 | 167 | pre { 168 | margin-top: 0; 169 | margin-bottom: 1rem; 170 | overflow: auto; 171 | -ms-overflow-style: scrollbar; 172 | } 173 | 174 | figure { 175 | margin: 0 0 1rem; 176 | } 177 | 178 | img { 179 | vertical-align: middle; 180 | border-style: none; 181 | } 182 | 183 | svg { 184 | overflow: hidden; 185 | vertical-align: middle; 186 | } 187 | 188 | table { 189 | border-collapse: collapse; 190 | } 191 | 192 | caption { 193 | padding-top: 0.75rem; 194 | padding-bottom: 0.75rem; 195 | color: #6c757d; 196 | text-align: left; 197 | caption-side: bottom; 198 | } 199 | 200 | th { 201 | text-align: inherit; 202 | } 203 | 204 | label { 205 | display: inline-block; 206 | margin-bottom: 0.5rem; 207 | } 208 | 209 | button { 210 | border-radius: 0; 211 | } 212 | 213 | button:focus { 214 | outline: 1px dotted; 215 | outline: 5px auto -webkit-focus-ring-color; 216 | } 217 | 218 | input, 219 | button, 220 | select, 221 | optgroup, 222 | textarea { 223 | margin: 0; 224 | font-family: inherit; 225 | font-size: inherit; 226 | line-height: inherit; 227 | } 228 | 229 | button, 230 | input { 231 | overflow: visible; 232 | } 233 | 234 | button, 235 | select { 236 | text-transform: none; 237 | } 238 | 239 | button, 240 | html [type="button"], 241 | [type="reset"], 242 | [type="submit"] { 243 | -webkit-appearance: button; 244 | } 245 | 246 | button::-moz-focus-inner, 247 | [type="button"]::-moz-focus-inner, 248 | [type="reset"]::-moz-focus-inner, 249 | [type="submit"]::-moz-focus-inner { 250 | padding: 0; 251 | border-style: none; 252 | } 253 | 254 | input[type="radio"], 255 | input[type="checkbox"] { 256 | box-sizing: border-box; 257 | padding: 0; 258 | } 259 | 260 | input[type="date"], 261 | input[type="time"], 262 | input[type="datetime-local"], 263 | input[type="month"] { 264 | -webkit-appearance: listbox; 265 | } 266 | 267 | textarea { 268 | overflow: auto; 269 | resize: vertical; 270 | } 271 | 272 | fieldset { 273 | min-width: 0; 274 | padding: 0; 275 | margin: 0; 276 | border: 0; 277 | } 278 | 279 | legend { 280 | display: block; 281 | width: 100%; 282 | max-width: 100%; 283 | padding: 0; 284 | margin-bottom: .5rem; 285 | font-size: 1.5rem; 286 | line-height: inherit; 287 | color: inherit; 288 | white-space: normal; 289 | } 290 | 291 | progress { 292 | vertical-align: baseline; 293 | } 294 | 295 | [type="number"]::-webkit-inner-spin-button, 296 | [type="number"]::-webkit-outer-spin-button { 297 | height: auto; 298 | } 299 | 300 | [type="search"] { 301 | outline-offset: -2px; 302 | -webkit-appearance: none; 303 | } 304 | 305 | [type="search"]::-webkit-search-cancel-button, 306 | [type="search"]::-webkit-search-decoration { 307 | -webkit-appearance: none; 308 | } 309 | 310 | ::-webkit-file-upload-button { 311 | font: inherit; 312 | -webkit-appearance: button; 313 | } 314 | 315 | output { 316 | display: inline-block; 317 | } 318 | 319 | summary { 320 | display: list-item; 321 | cursor: pointer; 322 | } 323 | 324 | template { 325 | display: none; 326 | } 327 | 328 | [hidden] { 329 | display: none !important; 330 | } 331 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /events/events_test.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "strconv" 8 | "strings" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestEventSerial(t *testing.T) { 14 | v1 := nextEventSerial() 15 | v2 := nextEventSerial() 16 | if v2 < v1 { 17 | t.Error("Fail to get next serial") 18 | } 19 | } 20 | 21 | func TestEventPoolSerial(t *testing.T) { 22 | val := eventPoolSerial.nextSerial("test1") 23 | if val != 1 { 24 | t.Error("Fail to get next serial") 25 | } 26 | 27 | val = eventPoolSerial.nextSerial("test1") 28 | if val != 2 { 29 | t.Error("Fail to get next serial") 30 | } 31 | 32 | val = eventPoolSerial.nextSerial("test2") 33 | if val != 1 { 34 | t.Error("Fail to get next serial") 35 | } 36 | 37 | } 38 | 39 | func readEvent(reader *bufio.Reader) (string, string) { 40 | header, err := reader.ReadString('\n') 41 | if err != nil { 42 | return "", "" 43 | } 44 | tmp := strings.Split(header[0:len(header)-1], ":") 45 | len, _ := strconv.Atoi(tmp[len(tmp)-1]) 46 | b := make([]byte, len) 47 | io.ReadFull(reader, b) 48 | return header, string(b) 49 | } 50 | 51 | func TestEventListener(t *testing.T) { 52 | r1, w1 := io.Pipe() 53 | r2, w2 := io.Pipe() 54 | reader := bufio.NewReader(r1) 55 | 56 | listener := NewEventListener("pool-1", 57 | "supervisor", 58 | r2, 59 | w1, 60 | 10) 61 | eventListenerManager.registerEventListener("pool-1", 62 | []string{"REMOTE_COMMUNICATION"}, 63 | listener) 64 | EmitEvent(NewRemoteCommunicationEvent("type-1", "this is a remote communication event test")) 65 | fmt.Printf("start to write READY\n") 66 | w2.Write([]byte("READY\n")) 67 | _, body := readEvent(reader) 68 | if body != "type:type-1\nthis is a remote communication event test" { 69 | t.Error("The body is not expect") 70 | } 71 | w2.Write([]byte("RESULT 4\nFAIL")) 72 | w2.Write([]byte("READY\n")) 73 | _, body = readEvent(reader) 74 | if body != "type:type-1\nthis is a remote communication event test" { 75 | t.Error("The body is not expect") 76 | } 77 | w2.Write([]byte("RESULT 2\nOK")) 78 | time.Sleep(2 * time.Second) 79 | w2.Close() 80 | r2.Close() 81 | r1.Close() 82 | w1.Close() 83 | 84 | eventListenerManager.unregisterEventListener("pool-1") 85 | } 86 | 87 | func TestProcCommEventCapture(t *testing.T) { 88 | r1, w1 := io.Pipe() 89 | r2, w2 := io.Pipe() 90 | reader := bufio.NewReader(r1) 91 | 92 | captureReader, captureWriter := io.Pipe() 93 | eventCapture := NewProcCommEventCapture(captureReader, 94 | 10240, 95 | "PROCESS_COMMUNICATION_STDOUT", 96 | "proc-1", 97 | "group-1") 98 | eventCapture.SetPid(99) 99 | listener := NewEventListener("pool-1", 100 | "supervisor", 101 | r2, 102 | w1, 103 | 10) 104 | eventListenerManager.registerEventListener("pool-1", 105 | []string{"PROCESS_COMMUNICATION"}, 106 | listener) 107 | w2.Write([]byte("READY\n")) 108 | captureWriter.Write([]byte(`this is unuseful information, seems it is very 109 | long and not useful, just used for testing purpose. 110 | let's input more unuseful information, ok..... 111 | haha...this is a proc event test also 112 | add some other unuseful`)) 113 | _, body := readEvent(reader) 114 | expectBody := "processname:proc-1 groupname:group-1 pid:99\nthis is a proc event test" 115 | if body != expectBody { 116 | t.Error("Fail to get the process communication event") 117 | } 118 | w2.Close() 119 | r2.Close() 120 | r1.Close() 121 | w1.Close() 122 | } 123 | 124 | func TestProcessStartingEvent(t *testing.T) { 125 | event := CreateProcessStartingEvent("proc-1", "group-1", "STOPPED", 0) 126 | if event.GetType() != "PROCESS_STATE_STARTING" { 127 | t.Error("Fail to creating the process starting event") 128 | } 129 | fmt.Printf( "%s\n", event.GetBody() ) 130 | if event.GetBody() != "processname:proc-1 groupname:group-1 from_state:STOPPED tries:0" { 131 | t.Error("Fail to encode the process starting event") 132 | } 133 | } 134 | 135 | func TestProcessRunningEvent(t *testing.T) { 136 | event := CreateProcessRunningEvent("proc-1", "group-1", "STARTING", 2766) 137 | if event.GetType() != "PROCESS_STATE_RUNNING" { 138 | t.Error("Fail to creating the process running event") 139 | } 140 | if event.GetBody() != "processname:proc-1 groupname:group-1 from_state:STARTING pid:2766" { 141 | t.Error("Fail to encode the process running event") 142 | } 143 | } 144 | 145 | func TestProcessBackoffEvent(t *testing.T) { 146 | event := CreateProcessBackoffEvent("proc-1", "group-1", "STARTING", 1) 147 | if event.GetType() != "PROCESS_STATE_BACKOFF" { 148 | t.Error("Fail to creating the process backoff event") 149 | } 150 | if event.GetBody() != "processname:proc-1 groupname:group-1 from_state:STARTING tries:1" { 151 | t.Error("Fail to encode the process backoff event") 152 | } 153 | } 154 | 155 | func TestProcessStoppingEvent(t *testing.T) { 156 | event := CreateProcessStoppingEvent("proc-1", "group-1", "STARTING", 2766) 157 | if event.GetType() != "PROCESS_STATE_STOPPING" { 158 | t.Error("Fail to creating the process stopping event") 159 | } 160 | if event.GetBody() != "processname:proc-1 groupname:group-1 from_state:STARTING pid:2766" { 161 | t.Error("Fail to encode the process stopping event") 162 | } 163 | } 164 | 165 | func TestProcessExitedEvent(t *testing.T) { 166 | event := CreateProcessExitedEvent("proc-1", "group-1", "RUNNING", 1, 2766) 167 | if event.GetType() != "PROCESS_STATE_EXITED" { 168 | t.Error("Fail to creating the process exited event") 169 | } 170 | if event.GetBody() != "processname:proc-1 groupname:group-1 from_state:RUNNING expected:1 pid:2766" { 171 | t.Error("Fail to encode the process exited event") 172 | } 173 | } 174 | 175 | func TestProcessStoppedEvent(t *testing.T) { 176 | event := CreateProcessStoppedEvent("proc-1", "group-1", "STOPPING", 2766) 177 | if event.GetType() != "PROCESS_STATE_STOPPED" { 178 | t.Error("Fail to creating the process stopped event") 179 | } 180 | if event.GetBody() != "processname:proc-1 groupname:group-1 from_state:STOPPING pid:2766" { 181 | t.Error("Fail to encode the process stopped event") 182 | } 183 | } 184 | 185 | func TestProcessFatalEvent(t *testing.T) { 186 | event := CreateProcessFatalEvent("proc-1", "group-1", "BACKOFF") 187 | if event.GetType() != "PROCESS_STATE_FATAL" { 188 | t.Error("Fail to creating the process fatal event") 189 | } 190 | if event.GetBody() != "processname:proc-1 groupname:group-1 from_state:BACKOFF" { 191 | t.Error("Fail to encode the process fatal event") 192 | } 193 | } 194 | 195 | func TestProcessUnknownEvent(t *testing.T) { 196 | event := CreateProcessUnknownEvent("proc-1", "group-1", "BACKOFF") 197 | if event.GetType() != "PROCESS_STATE_UNKNOWN" { 198 | t.Error("Fail to creating the process unknown event") 199 | } 200 | if event.GetBody() != "processname:proc-1 groupname:group-1 from_state:BACKOFF" { 201 | t.Error("Fail to encode the process unknown event") 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /xmlrpc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/hex" 6 | "io" 7 | "net" 8 | "net/http" 9 | "os" 10 | "strings" 11 | 12 | "github.com/gorilla/rpc" 13 | "github.com/ochinchina/gorilla-xmlrpc/xml" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | // XMLRPC mange the XML RPC servers 18 | // start XML RPC servers to accept the XML RPC request from client side 19 | type XMLRPC struct { 20 | // all the listeners to accept the XML RPC request 21 | listeners map[string]net.Listener 22 | } 23 | 24 | type httpBasicAuth struct { 25 | user string 26 | password string 27 | handler http.Handler 28 | } 29 | 30 | // create a new HttpBasicAuth oject with user name, password and the http request handler 31 | func newHTTPBasicAuth(user string, password string, handler http.Handler) *httpBasicAuth { 32 | if user != "" && password != "" { 33 | log.Debug("require authentication") 34 | } 35 | return &httpBasicAuth{user: user, password: password, handler: handler} 36 | } 37 | 38 | func (h *httpBasicAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { 39 | if h.user == "" || h.password == "" { 40 | log.Debug("no auth required") 41 | h.handler.ServeHTTP(w, r) 42 | return 43 | } 44 | username, password, ok := r.BasicAuth() 45 | if ok && username == h.user { 46 | if strings.HasPrefix(h.password, "{SHA}") { 47 | log.Debug("auth with SHA") 48 | hash := sha1.New() 49 | io.WriteString(hash, password) 50 | if hex.EncodeToString(hash.Sum(nil)) == h.password[5:] { 51 | h.handler.ServeHTTP(w, r) 52 | return 53 | } 54 | } else if password == h.password { 55 | log.Debug("Auth with normal password") 56 | h.handler.ServeHTTP(w, r) 57 | return 58 | } 59 | } 60 | w.Header().Set("WWW-Authenticate", "Basic realm=\"supervisor\"") 61 | w.WriteHeader(401) 62 | } 63 | 64 | // NewXMLRPC create a new XML RPC object 65 | func NewXMLRPC() *XMLRPC { 66 | return &XMLRPC{listeners: make(map[string]net.Listener)} 67 | } 68 | 69 | // Stop stop network listening 70 | func (p *XMLRPC) Stop() { 71 | log.Info("stop listening") 72 | for _, listener := range p.listeners { 73 | listener.Close() 74 | } 75 | p.listeners = make(map[string]net.Listener) 76 | } 77 | 78 | // StartUnixHTTPServer start http server on unix domain socket with path listenAddr. If both user and password are not empty, the user 79 | // must provide user and password for basic authentication when making a XML RPC request. 80 | func (p *XMLRPC) StartUnixHTTPServer(user string, password string, listenAddr string, s *Supervisor, startedCb func()) { 81 | os.Remove(listenAddr) 82 | p.startHTTPServer(user, password, "unix", listenAddr, s, startedCb) 83 | } 84 | 85 | // StartInetHTTPServer start http server on tcp with path listenAddr. If both user and password are not empty, the user 86 | // must provide user and password for basic authentication when making a XML RPC request. 87 | func (p *XMLRPC) StartInetHTTPServer(user string, password string, listenAddr string, s *Supervisor, startedCb func()) { 88 | p.startHTTPServer(user, password, "tcp", listenAddr, s, startedCb) 89 | } 90 | 91 | func (p *XMLRPC) isHTTPServerStartedOnProtocol(protocol string) bool { 92 | _, ok := p.listeners[protocol] 93 | return ok 94 | } 95 | 96 | func (p *XMLRPC) startHTTPServer(user string, password string, protocol string, listenAddr string, s *Supervisor, startedCb func()) { 97 | if p.isHTTPServerStartedOnProtocol(protocol) { 98 | startedCb() 99 | return 100 | } 101 | mux := http.NewServeMux() 102 | mux.Handle("/RPC2", newHTTPBasicAuth(user, password, p.createRPCServer(s))) 103 | progRestHandler := NewSupervisorRestful(s).CreateProgramHandler() 104 | mux.Handle("/program/", newHTTPBasicAuth(user, password, progRestHandler)) 105 | supervisorRestHandler := NewSupervisorRestful(s).CreateSupervisorHandler() 106 | mux.Handle("/supervisor/", newHTTPBasicAuth(user, password, supervisorRestHandler)) 107 | logtailHandler := NewLogtail(s).CreateHandler() 108 | mux.Handle("/logtail/", newHTTPBasicAuth(user, password, logtailHandler)) 109 | webguiHandler := NewSupervisorWebgui(s).CreateHandler() 110 | mux.Handle("/", newHTTPBasicAuth(user, password, webguiHandler)) 111 | listener, err := net.Listen(protocol, listenAddr) 112 | if err == nil { 113 | log.WithFields(log.Fields{"addr": listenAddr, "protocol": protocol}).Info("success to listen on address") 114 | p.listeners[protocol] = listener 115 | startedCb() 116 | http.Serve(listener, mux) 117 | } else { 118 | startedCb() 119 | log.WithFields(log.Fields{"addr": listenAddr, "protocol": protocol}).Fatal("fail to listen on address") 120 | } 121 | 122 | } 123 | func (p *XMLRPC) createRPCServer(s *Supervisor) *rpc.Server { 124 | RPC := rpc.NewServer() 125 | xmlrpcCodec := xml.NewCodec() 126 | RPC.RegisterCodec(xmlrpcCodec, "text/xml") 127 | RPC.RegisterService(s, "") 128 | 129 | xmlrpcCodec.RegisterAlias("supervisor.getVersion", "Supervisor.GetVersion") 130 | xmlrpcCodec.RegisterAlias("supervisor.getAPIVersion", "Supervisor.GetVersion") 131 | xmlrpcCodec.RegisterAlias("supervisor.getIdentification", "Supervisor.GetIdentification") 132 | xmlrpcCodec.RegisterAlias("supervisor.getState", "Supervisor.GetState") 133 | xmlrpcCodec.RegisterAlias("supervisor.getPID", "Supervisor.GetPID") 134 | xmlrpcCodec.RegisterAlias("supervisor.readLog", "Supervisor.ReadLog") 135 | xmlrpcCodec.RegisterAlias("supervisor.clearLog", "Supervisor.ClearLog") 136 | xmlrpcCodec.RegisterAlias("supervisor.shutdown", "Supervisor.Shutdown") 137 | xmlrpcCodec.RegisterAlias("supervisor.restart", "Supervisor.Restart") 138 | xmlrpcCodec.RegisterAlias("supervisor.getProcessInfo", "Supervisor.GetProcessInfo") 139 | xmlrpcCodec.RegisterAlias("supervisor.getSupervisorVersion", "Supervisor.GetVersion") 140 | xmlrpcCodec.RegisterAlias("supervisor.getAllProcessInfo", "Supervisor.GetAllProcessInfo") 141 | xmlrpcCodec.RegisterAlias("supervisor.startProcess", "Supervisor.StartProcess") 142 | xmlrpcCodec.RegisterAlias("supervisor.startAllProcesses", "Supervisor.StartAllProcesses") 143 | xmlrpcCodec.RegisterAlias("supervisor.startProcessGroup", "Supervisor.StartProcessGroup") 144 | xmlrpcCodec.RegisterAlias("supervisor.stopProcess", "Supervisor.StopProcess") 145 | xmlrpcCodec.RegisterAlias("supervisor.stopProcessGroup", "Supervisor.StopProcessGroup") 146 | xmlrpcCodec.RegisterAlias("supervisor.stopAllProcesses", "Supervisor.StopAllProcesses") 147 | xmlrpcCodec.RegisterAlias("supervisor.signalProcess", "Supervisor.SignalProcess") 148 | xmlrpcCodec.RegisterAlias("supervisor.signalProcessGroup", "Supervisor.SignalProcessGroup") 149 | xmlrpcCodec.RegisterAlias("supervisor.signalAllProcesses", "Supervisor.SignalAllProcesses") 150 | xmlrpcCodec.RegisterAlias("supervisor.sendProcessStdin", "Supervisor.SendProcessStdin") 151 | xmlrpcCodec.RegisterAlias("supervisor.sendRemoteCommEvent", "Supervisor.SendRemoteCommEvent") 152 | xmlrpcCodec.RegisterAlias("supervisor.reloadConfig", "Supervisor.ReloadConfig") 153 | xmlrpcCodec.RegisterAlias("supervisor.addProcessGroup", "Supervisor.AddProcessGroup") 154 | xmlrpcCodec.RegisterAlias("supervisor.removeProcessGroup", "Supervisor.RemoveProcessGroup") 155 | xmlrpcCodec.RegisterAlias("supervisor.readProcessStdoutLog", "Supervisor.ReadProcessStdoutLog") 156 | xmlrpcCodec.RegisterAlias("supervisor.readProcessStderrLog", "Supervisor.ReadProcessStderrLog") 157 | xmlrpcCodec.RegisterAlias("supervisor.tailProcessStdoutLog", "Supervisor.TailProcessStdoutLog") 158 | xmlrpcCodec.RegisterAlias("supervisor.tailProcessStderrLog", "Supervisor.TailProcessStderrLog") 159 | xmlrpcCodec.RegisterAlias("supervisor.clearProcessLogs", "Supervisor.ClearProcessLogs") 160 | xmlrpcCodec.RegisterAlias("supervisor.clearAllProcessLogs", "Supervisor.ClearAllProcessLogs") 161 | return RPC 162 | } 163 | -------------------------------------------------------------------------------- /webgui/css/bootstrap-table.css: -------------------------------------------------------------------------------- 1 | /** 2 | * @author zhixin wen 3 | * version: 1.12.1 4 | * https://github.com/wenzhixin/bootstrap-table/ 5 | */ 6 | 7 | .bootstrap-table .table { 8 | margin-bottom: 0 !important; 9 | border-bottom: 1px solid #dddddd; 10 | border-collapse: collapse !important; 11 | border-radius: 1px; 12 | } 13 | 14 | .bootstrap-table .table:not(.table-condensed), 15 | .bootstrap-table .table:not(.table-condensed) > tbody > tr > th, 16 | .bootstrap-table .table:not(.table-condensed) > tfoot > tr > th, 17 | .bootstrap-table .table:not(.table-condensed) > thead > tr > td, 18 | .bootstrap-table .table:not(.table-condensed) > tbody > tr > td, 19 | .bootstrap-table .table:not(.table-condensed) > tfoot > tr > td { 20 | padding: 8px; 21 | } 22 | 23 | .bootstrap-table .table.table-no-bordered > thead > tr > th, 24 | .bootstrap-table .table.table-no-bordered > tbody > tr > td { 25 | border-right: 2px solid transparent; 26 | } 27 | 28 | .bootstrap-table .table.table-no-bordered > tbody > tr > td:last-child { 29 | border-right: none; 30 | } 31 | 32 | .fixed-table-container { 33 | position: relative; 34 | clear: both; 35 | border: 1px solid #dddddd; 36 | border-radius: 4px; 37 | -webkit-border-radius: 4px; 38 | -moz-border-radius: 4px; 39 | } 40 | 41 | .fixed-table-container.table-no-bordered { 42 | border: 1px solid transparent; 43 | } 44 | 45 | .fixed-table-footer, 46 | .fixed-table-header { 47 | overflow: hidden; 48 | } 49 | 50 | .fixed-table-footer { 51 | border-top: 1px solid #dddddd; 52 | } 53 | 54 | .fixed-table-body { 55 | overflow-x: auto; 56 | overflow-y: auto; 57 | height: 100%; 58 | } 59 | 60 | .fixed-table-container table { 61 | width: 100%; 62 | } 63 | 64 | .fixed-table-container thead th { 65 | height: 0; 66 | padding: 0; 67 | margin: 0; 68 | border-left: 1px solid #dddddd; 69 | } 70 | 71 | .fixed-table-container thead th:focus { 72 | outline: 0 solid transparent; 73 | } 74 | 75 | .fixed-table-container thead th:first-child:not([data-not-first-th]) { 76 | border-left: none; 77 | border-top-left-radius: 4px; 78 | -webkit-border-top-left-radius: 4px; 79 | -moz-border-radius-topleft: 4px; 80 | } 81 | 82 | .fixed-table-container thead th .th-inner, 83 | .fixed-table-container tbody td .th-inner { 84 | padding: 8px; 85 | line-height: 24px; 86 | vertical-align: top; 87 | overflow: hidden; 88 | text-overflow: ellipsis; 89 | white-space: nowrap; 90 | } 91 | 92 | .fixed-table-container thead th .sortable { 93 | cursor: pointer; 94 | background-position: right; 95 | background-repeat: no-repeat; 96 | padding-right: 30px; 97 | } 98 | 99 | .fixed-table-container thead th .both { 100 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAkElEQVQoz7X QMQ5AQBCF4dWQSJxC5wwax1Cq1e7BAdxD5SL+Tq/QCM1oNiJidwox0355mXnG/DrEtIQ6azioNZQxI0ykPhTQIwhCR+BmBYtlK7kLJYwWCcJA9M4qdrZrd8pPjZWPtOqdRQy320YSV17OatFC4euts6z39GYMKRPCTKY9UnPQ6P+GtMRfGtPnBCiqhAeJPmkqAAAAAElFTkSuQmCC'); 101 | } 102 | 103 | .fixed-table-container thead th .asc { 104 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZ0lEQVQ4y2NgGLKgquEuFxBPAGI2ahhWCsS/gDibUoO0gPgxEP8H4ttArEyuQYxAPBdqEAxPBImTY5gjEL9DM+wTENuQahAvEO9DMwiGdwAxOymGJQLxTyD+jgWDxCMZRsEoGAVoAADeemwtPcZI2wAAAABJRU5ErkJggg=='); 105 | } 106 | 107 | .fixed-table-container thead th .desc { 108 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZUlEQVQ4y2NgGAWjYBSggaqGu5FA/BOIv2PBIPFEUgxjB+IdQPwfC94HxLykus4GiD+hGfQOiB3J8SojEE9EM2wuSJzcsFMG4ttQgx4DsRalkZENxL+AuJQaMcsGxBOAmGvopk8AVz1sLZgg0bsAAAAASUVORK5CYII= '); 109 | } 110 | 111 | .fixed-table-container th.detail { 112 | width: 30px; 113 | } 114 | 115 | .fixed-table-container tbody td { 116 | border-left: 1px solid #dddddd; 117 | } 118 | 119 | .fixed-table-container tbody tr:first-child td { 120 | border-top: none; 121 | } 122 | 123 | .fixed-table-container tbody td:first-child { 124 | border-left: none; 125 | } 126 | 127 | /* the same color with .active */ 128 | .fixed-table-container tbody .selected td { 129 | background-color: #f5f5f5; 130 | } 131 | 132 | .fixed-table-container .bs-checkbox { 133 | text-align: center; 134 | } 135 | 136 | .fixed-table-container input[type="radio"], 137 | .fixed-table-container input[type="checkbox"] { 138 | margin: 0 auto !important; 139 | } 140 | 141 | .fixed-table-container .no-records-found { 142 | text-align: center; 143 | } 144 | 145 | .fixed-table-pagination div.pagination, 146 | .fixed-table-pagination .pagination-detail { 147 | margin-top: 10px; 148 | margin-bottom: 10px; 149 | } 150 | 151 | .fixed-table-pagination div.pagination .pagination { 152 | margin: 0; 153 | } 154 | 155 | .fixed-table-pagination .pagination a { 156 | padding: 6px 12px; 157 | line-height: 1.428571429; 158 | } 159 | 160 | .fixed-table-pagination .pagination-info { 161 | line-height: 34px; 162 | margin-right: 5px; 163 | } 164 | 165 | .fixed-table-pagination .btn-group { 166 | position: relative; 167 | display: inline-block; 168 | vertical-align: middle; 169 | } 170 | 171 | .fixed-table-pagination .dropup .dropdown-menu { 172 | margin-bottom: 0; 173 | } 174 | 175 | .fixed-table-pagination .page-list { 176 | display: inline-block; 177 | } 178 | 179 | .fixed-table-toolbar .columns-left { 180 | margin-right: 5px; 181 | } 182 | 183 | .fixed-table-toolbar .columns-right { 184 | margin-left: 5px; 185 | } 186 | 187 | .fixed-table-toolbar .columns label { 188 | display: block; 189 | padding: 3px 20px; 190 | clear: both; 191 | font-weight: normal; 192 | line-height: 1.428571429; 193 | } 194 | 195 | .fixed-table-toolbar .bs-bars, 196 | .fixed-table-toolbar .search, 197 | .fixed-table-toolbar .columns { 198 | position: relative; 199 | margin-top: 10px; 200 | margin-bottom: 10px; 201 | line-height: 34px; 202 | } 203 | 204 | .fixed-table-pagination li.disabled a { 205 | pointer-events: none; 206 | cursor: default; 207 | } 208 | 209 | .fixed-table-loading { 210 | display: none; 211 | position: absolute; 212 | top: 42px; 213 | right: 0; 214 | bottom: 0; 215 | left: 0; 216 | z-index: 99; 217 | background-color: #fff; 218 | text-align: center; 219 | } 220 | 221 | .fixed-table-body .card-view .title { 222 | font-weight: bold; 223 | display: inline-block; 224 | min-width: 30%; 225 | text-align: left !important; 226 | } 227 | 228 | /* support bootstrap 2 */ 229 | .fixed-table-body thead th .th-inner { 230 | box-sizing: border-box; 231 | } 232 | 233 | .table th, .table td { 234 | vertical-align: middle; 235 | box-sizing: border-box; 236 | } 237 | 238 | .fixed-table-toolbar .dropdown-menu { 239 | text-align: left; 240 | max-height: 300px; 241 | overflow: auto; 242 | } 243 | 244 | .fixed-table-toolbar .btn-group > .btn-group { 245 | display: inline-block; 246 | margin-left: -1px !important; 247 | } 248 | 249 | .fixed-table-toolbar .btn-group > .btn-group > .btn { 250 | border-radius: 0; 251 | } 252 | 253 | .fixed-table-toolbar .btn-group > .btn-group:first-child > .btn { 254 | border-top-left-radius: 4px; 255 | border-bottom-left-radius: 4px; 256 | } 257 | 258 | .fixed-table-toolbar .btn-group > .btn-group:last-child > .btn { 259 | border-top-right-radius: 4px; 260 | border-bottom-right-radius: 4px; 261 | } 262 | 263 | .bootstrap-table .table > thead > tr > th { 264 | vertical-align: bottom; 265 | border-bottom: 1px solid #ddd; 266 | } 267 | 268 | /* support bootstrap 3 */ 269 | .bootstrap-table .table thead > tr > th { 270 | padding: 0; 271 | margin: 0; 272 | } 273 | 274 | .bootstrap-table .fixed-table-footer tbody > tr > td { 275 | padding: 0 !important; 276 | } 277 | 278 | .bootstrap-table .fixed-table-footer .table { 279 | border-bottom: none; 280 | border-radius: 0; 281 | padding: 0 !important; 282 | } 283 | 284 | .bootstrap-table .pull-right .dropdown-menu { 285 | right: 0; 286 | left: auto; 287 | } 288 | 289 | /* calculate scrollbar width */ 290 | p.fixed-table-scroll-inner { 291 | width: 100%; 292 | height: 200px; 293 | } 294 | 295 | div.fixed-table-scroll-outer { 296 | top: 0; 297 | left: 0; 298 | visibility: hidden; 299 | width: 200px; 300 | height: 150px; 301 | overflow: hidden; 302 | } 303 | 304 | /* for get correct heights */ 305 | .fixed-table-toolbar:after, .fixed-table-pagination:after { 306 | content: ""; 307 | display: block; 308 | clear: both; 309 | } 310 | 311 | .fullscreen { 312 | position: fixed; 313 | top: 0; 314 | left: 0; 315 | z-index: 1050; 316 | width: 100%!important; 317 | background: #FFF; 318 | } 319 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Report Card](https://goreportcard.com/badge/github.com/ochinchina/supervisord)](https://goreportcard.com/report/github.com/ochinchina/supervisord) 2 | 3 | # Why this project? 4 | 5 | The python script supervisord is a powerful tool used by a lot of guys to manage the processes. I like the tool supervisord also. 6 | 7 | But this tool requires us to install the big python environment. In some situation, for example in the docker environment, the python is too big for us. 8 | 9 | In this project, the supervisord is re-implemented in go-lang. The compiled supervisord is very suitable for these environment that the python is not installed. 10 | 11 | # Compile the supervisord 12 | 13 | Before compiling the supervisord, make sure the go-lang is installed in your environement. 14 | 15 | To compile the go-lang version supervisord, run following commands (required go 1.11+): 16 | 17 | 1. local: `go build` 18 | 1. linux: `env GOOS=linux GOARCH=amd64 go build -o supervisord_linux_amd64` 19 | 20 | # Run the supervisord 21 | 22 | After the supervisord binary is generated, create a supervisord configuration file and start the supervisord like below: 23 | 24 | ```shell 25 | $ cat supervisor.conf 26 | [program:test] 27 | command = /your/program args 28 | $ supervisord -c supervisor.conf 29 | ``` 30 | # Run as daemon 31 | Add the inet interface in your configuration: 32 | ```ini 33 | [inet_http_server] 34 | port=127.0.0.1:9001 35 | ``` 36 | then run 37 | ```shell 38 | $ supervisord -c supervisor.conf -d 39 | ``` 40 | In order to controll the daemon, you can use `$ supervisord ctl` subcommand, available commands are: `status`, `start`, `stop`, `shutdown`, `reload`. 41 | 42 | ```shell 43 | $ supervisord ctl status 44 | $ supervisord ctl status program-1 program-2... 45 | $ supervisord ctl status group:* 46 | $ supervisord ctl stop program-1 program-2... 47 | $ supervisord ctl stop group:* 48 | $ supervisord ctl stop all 49 | $ supervisord ctl start program-1 program-2... 50 | $ supervisord ctl start group:* 51 | $ supervisord ctl start all 52 | $ supervisord ctl shutdown 53 | $ supervisord ctl reload 54 | $ supervisord ctl signal ... 55 | $ supervisord ctl signal all 56 | $ supervisord ctl pid 57 | $ supervisord ctl fg 58 | ``` 59 | 60 | the URL of supervisord in the "supervisor ctl" subcommand is dected in following order: 61 | 62 | - check if option -s or --serverurl is present, use this url 63 | - check if -c option is present and the "serverurl" in "supervisorctl" section is present, use the "serverurl" in section "supervisorctl" 64 | - return http://localhost:9001 65 | 66 | # Check the version 67 | 68 | command "version" will show the current supervisor version. 69 | 70 | ```shell 71 | $ supervisord version 72 | ``` 73 | 74 | # Supported features 75 | 76 | ## http server 77 | 78 | the unix socket & TCP http server is supported. Basic auth is supported. 79 | 80 | The unix socket setting is in the "unix_http_server" section. 81 | The TCP http server setting is in "inet_http_server" section. 82 | 83 | If both "inet_http_server" and "unix_http_server" is not configured in the configuration file, no http server will be started. 84 | 85 | ## supervisord information 86 | 87 | Following parameters are supported in "supervisord" section: 88 | 89 | - logfile 90 | - logfile_maxbytes 91 | - logfile_backups 92 | - loglevel 93 | - pidfile 94 | - minfds 95 | - minprocs 96 | - identifier 97 | 98 | ## program 99 | 100 | the following features is supported in the "program:x" section: 101 | 102 | - program command 103 | - process name 104 | - numprocs 105 | - numprocs_start 106 | - autostart 107 | - startsecs 108 | - startretries 109 | - autorestart 110 | - exitcodes 111 | - stopsignal 112 | - stopwaitsecs 113 | - stdout_logfile 114 | - stdout_logfile_maxbytes 115 | - stdout_logfile_backups 116 | - redirect_stderr 117 | - stderr_logfile 118 | - stderr_logfile_maxbytes 119 | - stderr_logfile_backups 120 | - environment 121 | - priority 122 | - user 123 | - directory 124 | - stopasgroup 125 | - killasgroup 126 | - restartpause 127 | 128 | ### program extends 129 | 130 | Following new keys are supported by the [program:xxx] section: 131 | 132 | - **depends_on**: define program depends information. If program A depends on program B, C, the program B, C will be started before program A. Example: 133 | 134 | ```ini 135 | [program:A] 136 | depends_on = B, C 137 | 138 | [program:B] 139 | ... 140 | [program:C] 141 | ... 142 | ``` 143 | 144 | - **user**: user in the section "program:xxx" now is extended to support group with format "user[:group]". So "user" can be configured as: 145 | 146 | ```ini 147 | [program:xxx] 148 | user = user_name 149 | ... 150 | ``` 151 | or 152 | ```ini 153 | [program:xxx] 154 | user = user_name:group_name 155 | ... 156 | ``` 157 | - **stopsignal** list 158 | one or more stop signal can be configured. If more than one stopsignal is configured, when stoping the program, the supervisor will send the signals to the program one by one with interval "stopwaitsecs". If the program does not exit after all the signals sent to the program, the supervisor will kill the program 159 | 160 | - **restart_when_binary_changed**: a bool flag to control if the program should be restarted when the executable binary is changed 161 | 162 | - **restart_directory_monitor**: a path to be monitored for restarting purpose 163 | - **restart_file_pattern**: if a file is changed under restart_directory_monitor and the filename matches this pattern, the program will be restarted. 164 | 165 | ## Set default parameters for program 166 | 167 | A section "program-default" is added and the default parameters for programs can be set in this section. This can reduce some parameters for programs. For example both test1 and test2 program have exactly same environment variables VAR1 and VAR2, the environment variable is decalred like: 168 | 169 | ```ini 170 | [program:test1] 171 | ... 172 | environment=VAR1="value1",VAR2="value2" 173 | 174 | [program:test2] 175 | ... 176 | environment=VAR1="value1",VAR2="value2" 177 | ``` 178 | 179 | the VAR1 and VAR2 environment variable can be moved to "program-default" section like: 180 | 181 | ```ini 182 | 183 | [program-default] 184 | environment=VAR1="value1",VAR2="value2" 185 | 186 | [program:test1] 187 | ... 188 | 189 | [program:test2] 190 | ... 191 | 192 | ``` 193 | 194 | 195 | 196 | ## Group 197 | the "group" section is supported and you can set "programs" item 198 | 199 | ## Events 200 | 201 | the supervisor 3.x defined events are supported partially. Now it supports following events: 202 | 203 | - all process state related events 204 | - process communication event 205 | - remote communication event 206 | - tick related events 207 | - process log related events 208 | 209 | ## Logs 210 | 211 | The logs ( field stdout_logfile, stderr_logfile ) from programs managed by the supervisord can be written to: 212 | 213 | ``` 214 | - /dev/null, ignore the log 215 | - /dev/stdout, write log to stdout 216 | - /dev/stderr, write log to stderr 217 | - syslog, write the log to local syslog 218 | - syslog @[protocol:]host[:port], write the log to remote syslog. protocol must be "tcp" or "udp", if missing, "udp" will be used. If port is missing, for "udp" protocol, it's value is 514 and for "tcp" protocol, it's value is 6514. 219 | - file name, write log to a file 220 | ``` 221 | 222 | Mutiple log file can be configured for the stdout_logfile and stderr_logfile with delimeter ',', for example if want to a program write log to both stdout and test.log file, the stdout_logfile for the program can be configured as: 223 | 224 | ```ini 225 | stdout_logfile = test.log, /dev/stdout 226 | ``` 227 | 228 | # Web GUI 229 | 230 | This supervisord has a default web GUI, you can start, stop & check the status of program from the GUI. Following picture shows the default web GUI: 231 | 232 | ![alt text](https://github.com/ochinchina/supervisord/blob/master/go_supervisord_gui.png) 233 | 234 | # Usage from a Docker container 235 | 236 | supervisord is compiled inside a Docker image to be used directly inside another image, from the Docker Hub version. 237 | 238 | ```Dockerfile 239 | FROM debian:latest 240 | COPY --from=ochinchina/supervisord:latest /usr/local/bin/supervisord /usr/local/bin/supervisord 241 | CMD ["/usr/local/bin/supervisord"] 242 | ``` 243 | 244 | # The MIT License (MIT) 245 | 246 | Copyright (c) 247 | 248 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 249 | 250 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 251 | 252 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 253 | -------------------------------------------------------------------------------- /xmlrpcclient/xmlrpc-client.go: -------------------------------------------------------------------------------- 1 | package xmlrpcclient 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "net" 11 | "net/http" 12 | "net/url" 13 | "time" 14 | 15 | "github.com/ochinchina/supervisord/types" 16 | 17 | "github.com/ochinchina/gorilla-xmlrpc/xml" 18 | ) 19 | 20 | // XMLRPCClient the supervisor XML RPC client library 21 | type XMLRPCClient struct { 22 | serverurl string 23 | user string 24 | password string 25 | timeout time.Duration 26 | verbose bool 27 | } 28 | 29 | // VersionReply the version reply message from supervisor 30 | type VersionReply struct { 31 | Value string 32 | } 33 | 34 | // StartStopReply the program start/stop reply message from supervisor 35 | type StartStopReply struct { 36 | Value bool 37 | } 38 | 39 | // ShutdownReply the program shutdown reply message 40 | type ShutdownReply StartStopReply 41 | 42 | // AllProcessInfoReply all the processes information from supervisor 43 | type AllProcessInfoReply struct { 44 | Value []types.ProcessInfo 45 | } 46 | 47 | var emptyReader io.ReadCloser 48 | 49 | func init() { 50 | var buf bytes.Buffer 51 | emptyReader = ioutil.NopCloser(&buf) 52 | } 53 | 54 | // NewXMLRPCClient create a XMLRPCClient object 55 | func NewXMLRPCClient(serverurl string, verbose bool) *XMLRPCClient { 56 | return &XMLRPCClient{serverurl: serverurl, timeout: 0, verbose: verbose} 57 | } 58 | 59 | // SetUser set the user for basic http auth 60 | func (r *XMLRPCClient) SetUser(user string) { 61 | r.user = user 62 | } 63 | 64 | // SetPassword set the password for basic http auth 65 | func (r *XMLRPCClient) SetPassword(password string) { 66 | r.password = password 67 | } 68 | 69 | // SetTimeout set the http request timeout 70 | func (r *XMLRPCClient) SetTimeout(timeout time.Duration) { 71 | r.timeout = timeout 72 | } 73 | 74 | // URL return the RPC url 75 | func (r *XMLRPCClient) URL() string { 76 | return fmt.Sprintf("%s/RPC2", r.serverurl) 77 | } 78 | 79 | func (r *XMLRPCClient) createHTTPRequest(method string, url string, data interface{}) (*http.Request, error) { 80 | buf, _ := xml.EncodeClientRequest(method, data) 81 | req, err := http.NewRequest("POST", url, bytes.NewBuffer(buf)) 82 | if err != nil { 83 | if r.verbose { 84 | fmt.Println("Fail to create request:", err) 85 | } 86 | return nil, err 87 | } 88 | 89 | if len(r.user) > 0 && len(r.password) > 0 { 90 | req.SetBasicAuth(r.user, r.password) 91 | } 92 | 93 | req.Header.Set("Content-Type", "text/xml") 94 | 95 | return req, nil 96 | } 97 | 98 | func (r *XMLRPCClient) processResponse(resp *http.Response, processBody func(io.ReadCloser, error)) { 99 | defer resp.Body.Close() 100 | 101 | if resp.StatusCode/100 != 2 { 102 | if r.verbose { 103 | fmt.Println("Bad Response:", resp.Status) 104 | } 105 | processBody(emptyReader, fmt.Errorf("Bad response with status code %d", resp.StatusCode)) 106 | } else { 107 | processBody(resp.Body, nil) 108 | } 109 | } 110 | 111 | func (r *XMLRPCClient) postInetHTTP(method string, url string, data interface{}, processBody func(io.ReadCloser, error)) { 112 | req, err := r.createHTTPRequest(method, url, data) 113 | if err != nil { 114 | return 115 | } 116 | 117 | if r.timeout > 0 { 118 | ctx, cancel := context.WithTimeout(context.Background(), r.timeout) 119 | defer cancel() 120 | req = req.WithContext(ctx) 121 | } 122 | 123 | resp, err := http.DefaultClient.Do(req) 124 | if err != nil { 125 | if r.verbose { 126 | fmt.Println("Fail to send request to supervisord:", err) 127 | } 128 | return 129 | } 130 | r.processResponse(resp, processBody) 131 | 132 | } 133 | 134 | func (r *XMLRPCClient) postUnixHTTP(method string, path string, data interface{}, processBody func(io.ReadCloser, error)) { 135 | var conn net.Conn 136 | var err error 137 | if r.timeout > 0 { 138 | conn, err = net.DialTimeout("unix", path, r.timeout) 139 | } else { 140 | conn, err = net.Dial("unix", path) 141 | } 142 | if err != nil { 143 | if r.verbose { 144 | fmt.Printf("Fail to connect unix socket path: %s\n", r.serverurl) 145 | } 146 | return 147 | } 148 | defer conn.Close() 149 | 150 | if r.timeout > 0 { 151 | if err := conn.SetDeadline(time.Now().Add(r.timeout)); err != nil { 152 | return 153 | } 154 | } 155 | req, err := r.createHTTPRequest(method, "/RPC2", data) 156 | 157 | if err != nil { 158 | return 159 | } 160 | err = req.Write(conn) 161 | if err != nil { 162 | if r.verbose { 163 | fmt.Printf("Fail to write to unix socket %s\n", r.serverurl) 164 | } 165 | return 166 | } 167 | resp, err := http.ReadResponse(bufio.NewReader(conn), req) 168 | if err != nil { 169 | if r.verbose { 170 | fmt.Printf("Fail to read response %s\n", err) 171 | } 172 | return 173 | } 174 | r.processResponse(resp, processBody) 175 | 176 | } 177 | func (r *XMLRPCClient) post(method string, data interface{}, processBody func(io.ReadCloser, error)) { 178 | url, err := url.Parse(r.serverurl) 179 | if err != nil { 180 | fmt.Printf("Malform url:%s\n", url) 181 | return 182 | } 183 | if url.Scheme == "http" || url.Scheme == "https" { 184 | r.postInetHTTP(method, r.URL(), data, processBody) 185 | } else if url.Scheme == "unix" { 186 | r.postUnixHTTP(method, url.Path, data, processBody) 187 | } else { 188 | fmt.Printf("Unsupported URL scheme:%s\n", url.Scheme) 189 | } 190 | 191 | } 192 | 193 | // GetVersion send get the supervisor http version request 194 | func (r *XMLRPCClient) GetVersion() (reply VersionReply, err error) { 195 | ins := struct{}{} 196 | r.post("supervisor.getVersion", &ins, func(body io.ReadCloser, procError error) { 197 | err = procError 198 | if err == nil { 199 | err = xml.DecodeClientResponse(body, reply) 200 | } 201 | }) 202 | return 203 | } 204 | 205 | // GetAllProcessInfo get all the processes of superisor 206 | func (r *XMLRPCClient) GetAllProcessInfo() (reply AllProcessInfoReply, err error) { 207 | ins := struct{}{} 208 | r.post("supervisor.getAllProcessInfo", &ins, func(body io.ReadCloser, procError error) { 209 | err = procError 210 | if err == nil { 211 | err = xml.DecodeClientResponse(body, &reply) 212 | } 213 | }) 214 | 215 | return 216 | } 217 | 218 | // ChangeProcessState change the proccess state 219 | func (r *XMLRPCClient) ChangeProcessState(change string, processName string) (reply StartStopReply, err error) { 220 | if !(change == "start" || change == "stop") { 221 | err = fmt.Errorf("Incorrect required state") 222 | return 223 | } 224 | 225 | ins := struct{ Value string }{processName} 226 | r.post(fmt.Sprintf("supervisor.%sProcess", change), &ins, func(body io.ReadCloser, procError error) { 227 | err = procError 228 | if err == nil { 229 | err = xml.DecodeClientResponse(body, &reply) 230 | } 231 | }) 232 | 233 | return 234 | } 235 | 236 | // ChangeAllProcessState change all the program to same state( start/stop ) 237 | func (r *XMLRPCClient) ChangeAllProcessState(change string) (reply AllProcessInfoReply, err error) { 238 | if !(change == "start" || change == "stop") { 239 | err = fmt.Errorf("Incorrect required state") 240 | return 241 | } 242 | ins := struct{ Wait bool }{true} 243 | r.post(fmt.Sprintf("supervisor.%sAllProcesses", change), &ins, func(body io.ReadCloser, procError error) { 244 | err = procError 245 | if err == nil { 246 | err = xml.DecodeClientResponse(body, &reply) 247 | } 248 | }) 249 | return 250 | } 251 | 252 | // Shutdown shutdown the supervisor 253 | func (r *XMLRPCClient) Shutdown() (reply ShutdownReply, err error) { 254 | ins := struct{}{} 255 | r.post("supervisor.shutdown", &ins, func(body io.ReadCloser, procError error) { 256 | err = procError 257 | if err == nil { 258 | err = xml.DecodeClientResponse(body, &reply) 259 | } 260 | 261 | }) 262 | 263 | return 264 | } 265 | 266 | // ReloadConfig ask supervisor reload the configuration 267 | func (r *XMLRPCClient) ReloadConfig() (reply types.ReloadConfigResult, err error) { 268 | ins := struct{}{} 269 | 270 | xmlProcMgr := NewXMLProcessorManager() 271 | reply.AddedGroup = make([]string, 0) 272 | reply.ChangedGroup = make([]string, 0) 273 | reply.RemovedGroup = make([]string, 0) 274 | i := -1 275 | hasValue := false 276 | xmlProcMgr.AddNonLeafProcessor("methodResponse/params/param/value/array/data", func() { 277 | if hasValue { 278 | hasValue = false 279 | } else { 280 | i++ 281 | } 282 | }) 283 | xmlProcMgr.AddLeafProcessor("methodResponse/params/param/value/array/data/value", func(value string) { 284 | hasValue = true 285 | i++ 286 | switch i { 287 | case 0: 288 | reply.AddedGroup = append(reply.AddedGroup, value) 289 | case 1: 290 | reply.ChangedGroup = append(reply.ChangedGroup, value) 291 | case 2: 292 | reply.RemovedGroup = append(reply.RemovedGroup, value) 293 | } 294 | }) 295 | r.post("supervisor.reloadConfig", &ins, func(body io.ReadCloser, procError error) { 296 | err = procError 297 | if err == nil { 298 | xmlProcMgr.ProcessXML(body) 299 | } 300 | }) 301 | return 302 | } 303 | 304 | // SignalProcess send signal to program 305 | func (r *XMLRPCClient) SignalProcess(signal string, name string) (reply types.BooleanReply, err error) { 306 | ins := types.ProcessSignal{Name: name, Signal: signal} 307 | r.post("supervisor.signalProcess", &ins, func(body io.ReadCloser, procError error) { 308 | err = procError 309 | if err == nil { 310 | err = xml.DecodeClientResponse(body, &reply) 311 | } 312 | }) 313 | return 314 | } 315 | 316 | // SignalAll send signal to all the programs 317 | func (r *XMLRPCClient) SignalAll(signal string) (reply AllProcessInfoReply, err error) { 318 | ins := struct{ Signal string }{signal} 319 | r.post("supervisor.signalProcess", &ins, func(body io.ReadCloser, procError error) { 320 | err = procError 321 | if err == nil { 322 | err = xml.DecodeClientResponse(body, &reply) 323 | } 324 | }) 325 | 326 | return 327 | } 328 | 329 | // GetProcessInfo get the process information of one program 330 | func (r *XMLRPCClient) GetProcessInfo(process string) (reply types.ProcessInfo, err error) { 331 | ins := struct{ Name string }{process} 332 | result := struct{ Reply types.ProcessInfo }{} 333 | r.post("supervisor.getProcessInfo", &ins, func(body io.ReadCloser, procError error) { 334 | err = procError 335 | if err == nil { 336 | err = xml.DecodeClientResponse(body, &result) 337 | if err == nil { 338 | reply = result.Reply 339 | } else if r.verbose { 340 | fmt.Printf("Fail to decode to types.ProcessInfo\n") 341 | } 342 | } 343 | }) 344 | 345 | return 346 | } 347 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= 4 | github.com/GeertJohan/go.rice v0.0.0-20170420135705-c02ca9a983da h1:UVU3a9pRUyLdnBtn60WjRl0s4SEyJc2ChCY56OAR6wI= 5 | github.com/GeertJohan/go.rice v0.0.0-20170420135705-c02ca9a983da/go.mod h1:DgrzXonpdQbfN3uYaGz1EG4Sbhyum/MMIn6Cphlh2bw= 6 | github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ= 7 | github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= 8 | github.com/UnnoTed/fileb0x v1.1.4 h1:IUgFzgBipF/ujNx9wZgkrKOF3oltUuXMSoaejrBws+A= 9 | github.com/UnnoTed/fileb0x v1.1.4/go.mod h1:X59xXT18tdNk/D6j+KZySratBsuKJauMtVuJ9cgOiZs= 10 | github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= 11 | github.com/bmatcuk/doublestar v1.1.1 h1:YroD6BJCZBYx06yYFEWvUuKVWQn3vLLQAVmDmvTSaiQ= 12 | github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= 13 | github.com/daaku/go.zipexe v1.0.0 h1:VSOgZtH418pH9L16hC/JrgSNJbbAL26pj7lmD1+CGdY= 14 | github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= 15 | github.com/daaku/go.zipexe v1.0.1 h1:wV4zMsDOI2SZ2m7Tdz1Ps96Zrx+TzaK15VbUaGozw0M= 16 | github.com/daaku/go.zipexe v1.0.1/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8= 17 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 18 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 21 | github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc= 22 | github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY= 23 | github.com/gorilla/mux v1.7.1 h1:Dw4jY2nghMMRsh1ol8dv1axHkDwMQK2DHerMNJsIpJU= 24 | github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 25 | github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= 26 | github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 27 | github.com/gorilla/rpc v1.1.0 h1:marKfvVP0Gpd/jHlVBKCQ8RAoUPdX7K1Nuh6l1BNh7A= 28 | github.com/gorilla/rpc v1.1.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ= 29 | github.com/gorilla/rpc v1.2.0 h1:WvvdC2lNeT1SP32zrIce5l0ECBfbAlmrmSBsuc57wfk= 30 | github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ= 31 | github.com/jessevdk/go-flags v1.3.0 h1:QmKsgik/Z5fJ11ZtlcA8F+XW9dNybBNFQ1rngF3MmdU= 32 | github.com/jessevdk/go-flags v1.3.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 33 | github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= 34 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 35 | github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 h1:PJPDf8OUfOK1bb/NeTKd4f1QXZItOX389VN3B6qC8ro= 36 | github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= 37 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= 38 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= 39 | github.com/karrick/godirwalk v1.7.8 h1:VfG72pyIxgtC7+3X9CMHI0AOl4LwyRAg98WAgsvffi8= 40 | github.com/karrick/godirwalk v1.7.8/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34= 41 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 42 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 43 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= 44 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 45 | github.com/labstack/echo v3.2.1+incompatible h1:J2M7YArHx4gi8p/3fDw8tX19SXhBCoRpviyAZSN3I88= 46 | github.com/labstack/echo v3.2.1+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= 47 | github.com/labstack/gommon v0.2.7 h1:2qOPq/twXDrQ6ooBGrn3mrmVOC+biLlatwgIu8lbzRM= 48 | github.com/labstack/gommon v0.2.7/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4= 49 | github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= 50 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 51 | github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= 52 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 53 | github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 54 | github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4= 55 | github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 56 | github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 57 | github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= 58 | github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 59 | github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= 60 | github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc= 61 | github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840= 62 | github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= 63 | github.com/ochinchina/filechangemonitor v0.0.0-20190818103757-2e342dfc0dad h1:j7oyjdPHPvlRgALkG1yMaG8ntAZmKsZyq0tsYV89Py8= 64 | github.com/ochinchina/filechangemonitor v0.0.0-20190818103757-2e342dfc0dad/go.mod h1:OLRTJMpgb3yP1zBKA2g5GMYsKzJUoLq01lNOsReEzbQ= 65 | github.com/ochinchina/filechangemonitor v0.3.1 h1:Fyt8iE44kFwmI3ncNWAi21GZnmRBrAUSlMunpcDlMjQ= 66 | github.com/ochinchina/filechangemonitor v0.3.1/go.mod h1:OLRTJMpgb3yP1zBKA2g5GMYsKzJUoLq01lNOsReEzbQ= 67 | github.com/ochinchina/go-daemon v0.1.5 h1:XZoQ1NUXfeIGkU5rgbAwiNb1sr5btc2NbUqYUXmR5Zs= 68 | github.com/ochinchina/go-daemon v0.1.5/go.mod h1:oqEZ8HaYtoTxjkIpaizQ75VT5PWgpVeIennFIYSIkzQ= 69 | github.com/ochinchina/go-ini v1.0.1 h1:qrKGrgxJjY+4H8aV7B2HPohShzHGrymW+/X1Gx933zU= 70 | github.com/ochinchina/go-ini v1.0.1/go.mod h1:Tqs5+JmccLSNMX1KXbbyG/B3ro4J9uXVYC5U5VOeRE8= 71 | github.com/ochinchina/go-reaper v0.0.0-20181016012355-6b11389e79fc h1:oyaVoTfmN7Xe06URvpaKK8GDZr0YJFKhmKi37rE0a3c= 72 | github.com/ochinchina/go-reaper v0.0.0-20181016012355-6b11389e79fc/go.mod h1:SmX+KYO+b7mEApGBUNwjdJpRQwAdb0Rlzoh8G77K55I= 73 | github.com/ochinchina/gorilla-xmlrpc v0.0.0-20171012055324-ecf2fe693a2c h1:6xgMUqscagnZicBedm1h4T3q6IQHbrrZp7bker+toOI= 74 | github.com/ochinchina/gorilla-xmlrpc v0.0.0-20171012055324-ecf2fe693a2c/go.mod h1:/gFmJ8Das0jFgYxzt/RkvAO62T/ZPcyTaZlOkEBu/jw= 75 | github.com/ochinchina/supervisord v0.6.3 h1:zOuVV3/u7+uYXrVXUOdnOW7l7hZQJFuHjwg8ppFfxEk= 76 | github.com/ochinchina/supervisord v0.6.3/go.mod h1:csl1p3boUYQM+Yh8xkAhjoPD+1qaPx8TBwQ3i4B6toA= 77 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 78 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 79 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 80 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 81 | github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= 82 | github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= 83 | github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4 h1:BN/Nyn2nWMoqGRA7G7paDNDqTXE30mXGqzzybrfo05w= 84 | github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= 85 | github.com/rogpeppe/go-charset v0.0.0-20190617161244-0dc95cdf6f31 h1:DE4LcMKyqAVa6a0CGmVxANbnVb7stzMmPkQiieyNmfQ= 86 | github.com/rogpeppe/go-charset v0.0.0-20190617161244-0dc95cdf6f31/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= 87 | github.com/sirupsen/logrus v0.0.0-20170713114250-a3f95b5c4235 h1:a2XWU6egUZQhD52o2GEKr79zE+OuZmwLybyOQpoqhHQ= 88 | github.com/sirupsen/logrus v0.0.0-20170713114250-a3f95b5c4235/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= 89 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 90 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 91 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 92 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 93 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 94 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 95 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 96 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 97 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 98 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 99 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 100 | github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw= 101 | github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8= 102 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 103 | golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b h1:2b9XGzhjiYsYPnKXoEfL7klWZQIt8IfyRCz62gCqqlQ= 104 | golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 105 | golang.org/x/net v0.0.0-20180921000356-2f5d2388922f h1:QM2QVxvDoW9PFSPp/zy9FgxJLfaWTZlS61KEPtBwacM= 106 | golang.org/x/net v0.0.0-20180921000356-2f5d2388922f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 107 | golang.org/x/sys v0.0.0-20170814044513-c84c1ab9fd18 h1:IoiXxANYbZRybSGnlkI5TZv53JFaYJACyByrcuQnzSk= 108 | golang.org/x/sys v0.0.0-20170814044513-c84c1ab9fd18/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 109 | golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 110 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= 111 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 112 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= 113 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 114 | golang.org/x/sys v0.0.0-20200103143344-a1369afcdac7 h1:/W9OPMnnpmFXHYkcp2rQsbFUbRlRzfECQjmAFiOyHE8= 115 | golang.org/x/sys v0.0.0-20200103143344-a1369afcdac7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 116 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 117 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 118 | gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= 119 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 120 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 121 | -------------------------------------------------------------------------------- /webgui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Go-Supervisor 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 284 | 285 |

Go-Supervisor

286 |
287 |

Programs

288 |
289 |
290 | 291 | 292 | 293 | 294 |
295 |
296 |
297 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 |
ProgramStateDescriptionAction
308 |
309 |
310 | 311 | 312 | 329 | 330 | 331 | 332 | 333 | 334 | -------------------------------------------------------------------------------- /ctl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jessevdk/go-flags" 6 | "github.com/ochinchina/supervisord/config" 7 | "github.com/ochinchina/supervisord/types" 8 | "github.com/ochinchina/supervisord/xmlrpcclient" 9 | "net/http" 10 | "os" 11 | "strings" 12 | ) 13 | 14 | // CtlCommand the entry of ctl command 15 | type CtlCommand struct { 16 | ServerURL string `short:"s" long:"serverurl" description:"URL on which supervisord server is listening"` 17 | User string `short:"u" long:"user" description:"the user name"` 18 | Password string `short:"P" long:"password" description:"the password"` 19 | Verbose bool `short:"v" long:"verbose" description:"Show verbose debug information"` 20 | } 21 | 22 | // StatusCommand get the status of all supervisor managed programs 23 | type StatusCommand struct { 24 | } 25 | 26 | // StartCommand start the given program 27 | type StartCommand struct { 28 | } 29 | 30 | // StopCommand stop the given program 31 | type StopCommand struct { 32 | } 33 | 34 | // RestartCommand restart the given program 35 | type RestartCommand struct { 36 | } 37 | 38 | // ShutdownCommand shutdown the supervisor 39 | type ShutdownCommand struct { 40 | } 41 | 42 | // ReloadCommand reload all the programs 43 | type ReloadCommand struct { 44 | } 45 | 46 | // PidCommand get the pid of program 47 | type PidCommand struct { 48 | } 49 | 50 | // SignalCommand send signal of program 51 | type SignalCommand struct { 52 | } 53 | 54 | // LogtailCommand tail the stdout/stderr log of program through http interface 55 | type LogtailCommand struct { 56 | } 57 | 58 | // CmdCheckWrapperCommand A wrapper can be use to check whether 59 | // number of parameters is valid or not 60 | type CmdCheckWrapperCommand struct { 61 | // Original cmd 62 | cmd flags.Commander 63 | // leastNumArgs indicates how many arguments 64 | // this cmd should have at least 65 | leastNumArgs int 66 | // Print usage when arguments not valid 67 | usage string 68 | } 69 | 70 | var ctlCommand CtlCommand 71 | var statusCommand = CmdCheckWrapperCommand{&StatusCommand{}, 0, ""} 72 | var startCommand = CmdCheckWrapperCommand{&StartCommand{}, 0, ""} 73 | var stopCommand = CmdCheckWrapperCommand{&StopCommand{}, 0, ""} 74 | var restartCommand = CmdCheckWrapperCommand{&RestartCommand{}, 0, ""} 75 | var shutdownCommand = CmdCheckWrapperCommand{&ShutdownCommand{}, 0, ""} 76 | var reloadCommand = CmdCheckWrapperCommand{&ReloadCommand{}, 0, ""} 77 | var pidCommand = CmdCheckWrapperCommand{&PidCommand{}, 1, "pid "} 78 | var signalCommand = CmdCheckWrapperCommand{&SignalCommand{}, 2, "signal [...]"} 79 | var logtailCommand = CmdCheckWrapperCommand{&LogtailCommand{}, 1, "logtail "} 80 | 81 | func (x *CtlCommand) getServerURL() string { 82 | options.Configuration, _ = findSupervisordConf() 83 | 84 | if x.ServerURL != "" { 85 | return x.ServerURL 86 | } else if _, err := os.Stat(options.Configuration); err == nil { 87 | config := config.NewConfig(options.Configuration) 88 | config.Load() 89 | if entry, ok := config.GetSupervisorctl(); ok { 90 | serverurl := entry.GetString("serverurl", "") 91 | if serverurl != "" { 92 | return serverurl 93 | } 94 | } 95 | } 96 | return "http://localhost:9001" 97 | } 98 | 99 | func (x *CtlCommand) getUser() string { 100 | options.Configuration, _ = findSupervisordConf() 101 | 102 | if x.User != "" { 103 | return x.User 104 | } else if _, err := os.Stat(options.Configuration); err == nil { 105 | config := config.NewConfig(options.Configuration) 106 | config.Load() 107 | if entry, ok := config.GetSupervisorctl(); ok { 108 | user := entry.GetString("username", "") 109 | return user 110 | } 111 | } 112 | return "" 113 | } 114 | 115 | func (x *CtlCommand) getPassword() string { 116 | options.Configuration, _ = findSupervisordConf() 117 | 118 | if x.Password != "" { 119 | return x.Password 120 | } else if _, err := os.Stat(options.Configuration); err == nil { 121 | config := config.NewConfig(options.Configuration) 122 | config.Load() 123 | if entry, ok := config.GetSupervisorctl(); ok { 124 | password := entry.GetString("password", "") 125 | return password 126 | } 127 | } 128 | return "" 129 | } 130 | 131 | func (x *CtlCommand) createRPCClient() *xmlrpcclient.XMLRPCClient { 132 | rpcc := xmlrpcclient.NewXMLRPCClient(x.getServerURL(), x.Verbose) 133 | rpcc.SetUser(x.getUser()) 134 | rpcc.SetPassword(x.getPassword()) 135 | return rpcc 136 | } 137 | 138 | // Execute implements flags.Commander interface to execute the control commands 139 | func (x *CtlCommand) Execute(args []string) error { 140 | if len(args) == 0 { 141 | return nil 142 | } 143 | 144 | rpcc := x.createRPCClient() 145 | verb := args[0] 146 | 147 | switch verb { 148 | 149 | //////////////////////////////////////////////////////////////////////////////// 150 | // STATUS 151 | //////////////////////////////////////////////////////////////////////////////// 152 | case "status": 153 | x.status(rpcc, args[1:]) 154 | 155 | //////////////////////////////////////////////////////////////////////////////// 156 | // START or STOP 157 | //////////////////////////////////////////////////////////////////////////////// 158 | case "start", "stop": 159 | x.startStopProcesses(rpcc, verb, args[1:]) 160 | 161 | //////////////////////////////////////////////////////////////////////////////// 162 | // SHUTDOWN 163 | //////////////////////////////////////////////////////////////////////////////// 164 | case "shutdown": 165 | x.shutdown(rpcc) 166 | case "reload": 167 | x.reload(rpcc) 168 | case "signal": 169 | sigName, processes := args[1], args[2:] 170 | x.signal(rpcc, sigName, processes) 171 | case "pid": 172 | x.getPid(rpcc, args[1]) 173 | default: 174 | fmt.Println("unknown command") 175 | } 176 | 177 | return nil 178 | } 179 | 180 | // get the status of processes 181 | func (x *CtlCommand) status(rpcc *xmlrpcclient.XMLRPCClient, processes []string) { 182 | processesMap := make(map[string]bool) 183 | for _, process := range processes { 184 | processesMap[process] = true 185 | } 186 | if reply, err := rpcc.GetAllProcessInfo(); err == nil { 187 | x.showProcessInfo(&reply, processesMap) 188 | } else { 189 | os.Exit(1) 190 | } 191 | } 192 | 193 | // start or stop the processes 194 | // verb must be: start or stop 195 | func (x *CtlCommand) startStopProcesses(rpcc *xmlrpcclient.XMLRPCClient, verb string, processes []string) { 196 | state := map[string]string{ 197 | "start": "started", 198 | "stop": "stopped", 199 | } 200 | x._startStopProcesses(rpcc, verb, processes, state[verb], true) 201 | } 202 | 203 | func (x *CtlCommand) _startStopProcesses(rpcc *xmlrpcclient.XMLRPCClient, verb string, processes []string, state string, showProcessInfo bool) { 204 | if len(processes) <= 0 { 205 | fmt.Printf("Please specify process for %s\n", verb) 206 | } 207 | for _, pname := range processes { 208 | if pname == "all" { 209 | reply, err := rpcc.ChangeAllProcessState(verb) 210 | if err == nil { 211 | if showProcessInfo { 212 | x.showProcessInfo(&reply, make(map[string]bool)) 213 | } 214 | } else { 215 | fmt.Printf("Fail to change all process state to %s", state) 216 | } 217 | } else { 218 | if reply, err := rpcc.ChangeProcessState(verb, pname); err == nil { 219 | if showProcessInfo { 220 | fmt.Printf("%s: ", pname) 221 | if !reply.Value { 222 | fmt.Printf("not ") 223 | } 224 | fmt.Printf("%s\n", state) 225 | } 226 | } else { 227 | fmt.Printf("%s: failed [%v]\n", pname, err) 228 | os.Exit(1) 229 | } 230 | } 231 | } 232 | } 233 | 234 | func (x *CtlCommand) restartProcesses(rpcc *xmlrpcclient.XMLRPCClient, processes []string) { 235 | x._startStopProcesses(rpcc, "stop", processes, "stopped", false) 236 | x._startStopProcesses(rpcc, "start", processes, "restarted", true) 237 | } 238 | 239 | // shutdown the supervisord 240 | func (x *CtlCommand) shutdown(rpcc *xmlrpcclient.XMLRPCClient) { 241 | if reply, err := rpcc.Shutdown(); err == nil { 242 | if reply.Value { 243 | fmt.Printf("Shut Down\n") 244 | } else { 245 | fmt.Printf("Hmmm! Something gone wrong?!\n") 246 | } 247 | } else { 248 | os.Exit(1) 249 | } 250 | } 251 | 252 | // reload all the programs in the supervisord 253 | func (x *CtlCommand) reload(rpcc *xmlrpcclient.XMLRPCClient) { 254 | if reply, err := rpcc.ReloadConfig(); err == nil { 255 | 256 | if len(reply.AddedGroup) > 0 { 257 | fmt.Printf("Added Groups: %s\n", strings.Join(reply.AddedGroup, ",")) 258 | } 259 | if len(reply.ChangedGroup) > 0 { 260 | fmt.Printf("Changed Groups: %s\n", strings.Join(reply.ChangedGroup, ",")) 261 | } 262 | if len(reply.RemovedGroup) > 0 { 263 | fmt.Printf("Removed Groups: %s\n", strings.Join(reply.RemovedGroup, ",")) 264 | } 265 | } else { 266 | os.Exit(1) 267 | } 268 | } 269 | 270 | // send signal to one or more processes 271 | func (x *CtlCommand) signal(rpcc *xmlrpcclient.XMLRPCClient, sigName string, processes []string) { 272 | for _, process := range processes { 273 | if process == "all" { 274 | reply, err := rpcc.SignalAll(process) 275 | if err == nil { 276 | x.showProcessInfo(&reply, make(map[string]bool)) 277 | } else { 278 | fmt.Printf("Fail to send signal %s to all process", sigName) 279 | os.Exit(1) 280 | } 281 | } else { 282 | reply, err := rpcc.SignalProcess(sigName, process) 283 | if err == nil && reply.Success { 284 | fmt.Printf("Succeed to send signal %s to process %s\n", sigName, process) 285 | } else { 286 | fmt.Printf("Fail to send signal %s to process %s\n", sigName, process) 287 | os.Exit(1) 288 | } 289 | } 290 | } 291 | } 292 | 293 | // get the pid of running program 294 | func (x *CtlCommand) getPid(rpcc *xmlrpcclient.XMLRPCClient, process string) { 295 | procInfo, err := rpcc.GetProcessInfo(process) 296 | if err != nil { 297 | fmt.Printf("program '%s' not found\n", process) 298 | os.Exit(1) 299 | } else { 300 | fmt.Printf("%d\n", procInfo.Pid) 301 | } 302 | } 303 | 304 | func (x *CtlCommand) getProcessInfo(rpcc *xmlrpcclient.XMLRPCClient, process string) (types.ProcessInfo, error) { 305 | return rpcc.GetProcessInfo(process) 306 | } 307 | 308 | // check if group name should be displayed 309 | func (x *CtlCommand) showGroupName() bool { 310 | val, ok := os.LookupEnv("SUPERVISOR_GROUP_DISPLAY") 311 | if !ok { 312 | return false 313 | } 314 | 315 | val = strings.ToLower(val) 316 | return val == "yes" || val == "true" || val == "y" || val == "t" || val == "1" 317 | } 318 | 319 | func (x *CtlCommand) showProcessInfo(reply *xmlrpcclient.AllProcessInfoReply, processesMap map[string]bool) { 320 | for _, pinfo := range reply.Value { 321 | description := pinfo.Description 322 | if strings.ToLower(description) == "" { 323 | description = "" 324 | } 325 | if x.inProcessMap(&pinfo, processesMap) { 326 | processName := pinfo.GetFullName() 327 | if !x.showGroupName() { 328 | processName = pinfo.Name 329 | } 330 | fmt.Printf("%s%-33s%-10s%s%s\n", x.getANSIColor(pinfo.Statename), processName, pinfo.Statename, description, "\x1b[0m") 331 | } 332 | } 333 | } 334 | 335 | func (x *CtlCommand) inProcessMap(procInfo *types.ProcessInfo, processesMap map[string]bool) bool { 336 | if len(processesMap) <= 0 { 337 | return true 338 | } 339 | for procName := range processesMap { 340 | if procName == procInfo.Name || procName == procInfo.GetFullName() { 341 | return true 342 | } 343 | 344 | // check the wildcast '*' 345 | pos := strings.Index(procName, ":") 346 | if pos != -1 { 347 | groupName := procName[0:pos] 348 | programName := procName[pos+1:] 349 | if programName == "*" && groupName == procInfo.Group { 350 | return true 351 | } 352 | } 353 | } 354 | return false 355 | } 356 | 357 | func (x *CtlCommand) getANSIColor(statename string) string { 358 | if statename == "RUNNING" { 359 | // green 360 | return "\x1b[0;32m" 361 | } else if statename == "BACKOFF" || statename == "FATAL" { 362 | // red 363 | return "\x1b[0;31m" 364 | } else { 365 | // yellow 366 | return "\x1b[1;33m" 367 | } 368 | } 369 | 370 | // Execute implements flags.Commander interface to get status of program 371 | func (sc *StatusCommand) Execute(args []string) error { 372 | ctlCommand.status(ctlCommand.createRPCClient(), args) 373 | return nil 374 | } 375 | 376 | // Execute start the given programs 377 | func (sc *StartCommand) Execute(args []string) error { 378 | ctlCommand.startStopProcesses(ctlCommand.createRPCClient(), "start", args) 379 | return nil 380 | } 381 | 382 | // Execute stop the given programs 383 | func (sc *StopCommand) Execute(args []string) error { 384 | ctlCommand.startStopProcesses(ctlCommand.createRPCClient(), "stop", args) 385 | return nil 386 | } 387 | 388 | // Execute restart the programs 389 | func (rc *RestartCommand) Execute(args []string) error { 390 | ctlCommand.restartProcesses(ctlCommand.createRPCClient(), args) 391 | return nil 392 | } 393 | 394 | // Execute shutdown the supervisor 395 | func (sc *ShutdownCommand) Execute(args []string) error { 396 | ctlCommand.shutdown(ctlCommand.createRPCClient()) 397 | return nil 398 | } 399 | 400 | // Execute stop the running programs and reload the supervisor configuration 401 | func (rc *ReloadCommand) Execute(args []string) error { 402 | ctlCommand.reload(ctlCommand.createRPCClient()) 403 | return nil 404 | } 405 | 406 | // Execute send signal to program 407 | func (rc *SignalCommand) Execute(args []string) error { 408 | sigName, processes := args[0], args[1:] 409 | ctlCommand.signal(ctlCommand.createRPCClient(), sigName, processes) 410 | return nil 411 | } 412 | 413 | // Execute get the pid of program 414 | func (pc *PidCommand) Execute(args []string) error { 415 | ctlCommand.getPid(ctlCommand.createRPCClient(), args[0]) 416 | return nil 417 | } 418 | 419 | // Execute tail the stdout/stderr of a program through http interrface 420 | func (lc *LogtailCommand) Execute(args []string) error { 421 | program := args[0] 422 | go func() { 423 | lc.tailLog(program, "stderr") 424 | }() 425 | return lc.tailLog(program, "stdout") 426 | } 427 | 428 | func (lc *LogtailCommand) tailLog(program string, dev string) error { 429 | _, err := ctlCommand.getProcessInfo(ctlCommand.createRPCClient(), program) 430 | if err != nil { 431 | fmt.Printf("Not exist program %s\n", program) 432 | return err 433 | } 434 | url := fmt.Sprintf("%s/logtail/%s/%s", ctlCommand.getServerURL(), program, dev) 435 | req, err := http.NewRequest("GET", url, nil) 436 | if err != nil { 437 | return err 438 | } 439 | req.SetBasicAuth(ctlCommand.getUser(), ctlCommand.getPassword()) 440 | client := http.Client{} 441 | resp, err := client.Do(req) 442 | if err != nil { 443 | return err 444 | } 445 | buf := make([]byte, 10240) 446 | for { 447 | n, err := resp.Body.Read(buf) 448 | if err != nil { 449 | return err 450 | } 451 | if dev == "stdout" { 452 | os.Stdout.Write(buf[0:n]) 453 | } else { 454 | os.Stderr.Write(buf[0:n]) 455 | } 456 | } 457 | return nil 458 | } 459 | 460 | // Execute check if the number of arguments is ok 461 | func (wc *CmdCheckWrapperCommand) Execute(args []string) error { 462 | if len(args) < wc.leastNumArgs { 463 | err := fmt.Errorf("Invalid arguments.\nUsage: supervisord ctl %v", wc.usage) 464 | fmt.Printf("%v\n", err) 465 | return err 466 | } 467 | return wc.cmd.Execute(args) 468 | } 469 | 470 | func init() { 471 | ctlCmd, _ := parser.AddCommand("ctl", 472 | "Control a running daemon", 473 | "The ctl subcommand resembles supervisorctl command of original daemon.", 474 | &ctlCommand) 475 | ctlCmd.AddCommand("status", 476 | "show program status", 477 | "show all or some program status", 478 | &statusCommand) 479 | ctlCmd.AddCommand("start", 480 | "start programs", 481 | "start one or more programs", 482 | &startCommand) 483 | ctlCmd.AddCommand("stop", 484 | "stop programs", 485 | "stop one or more programs", 486 | &stopCommand) 487 | ctlCmd.AddCommand("restart", 488 | "restart programs", 489 | "restart one or more programs", 490 | &restartCommand) 491 | ctlCmd.AddCommand("shutdown", 492 | "shutdown supervisord", 493 | "shutdown supervisord", 494 | &shutdownCommand) 495 | ctlCmd.AddCommand("reload", 496 | "reload the programs", 497 | "reload the programs", 498 | &reloadCommand) 499 | ctlCmd.AddCommand("signal", 500 | "send signal to program", 501 | "send signal to program", 502 | &signalCommand) 503 | ctlCmd.AddCommand("pid", 504 | "get the pid of specified program", 505 | "get the pid of specified program", 506 | &pidCommand) 507 | ctlCmd.AddCommand("logtail", 508 | "get the standard output&standard error of the program", 509 | "get the standard output&standard error of the program", 510 | &logtailCommand) 511 | 512 | } 513 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "regexp" 10 | "strconv" 11 | "strings" 12 | 13 | ini "github.com/ochinchina/go-ini" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | // Entry standards for a configuration section in supervisor configuration file 18 | type Entry struct { 19 | ConfigDir string 20 | Group string 21 | Name string 22 | keyValues map[string]string 23 | } 24 | 25 | // IsProgram return true if this is a program section 26 | func (c *Entry) IsProgram() bool { 27 | return strings.HasPrefix(c.Name, "program:") 28 | } 29 | 30 | // GetProgramName get the program name 31 | func (c *Entry) GetProgramName() string { 32 | if strings.HasPrefix(c.Name, "program:") { 33 | return c.Name[len("program:"):] 34 | } 35 | return "" 36 | } 37 | 38 | // IsEventListener return true if this section is for event listener 39 | func (c *Entry) IsEventListener() bool { 40 | return strings.HasPrefix(c.Name, "eventlistener:") 41 | } 42 | 43 | // GetEventListenerName get the event listener name 44 | func (c *Entry) GetEventListenerName() string { 45 | if strings.HasPrefix(c.Name, "eventlistener:") { 46 | return c.Name[len("eventlistener:"):] 47 | } 48 | return "" 49 | } 50 | 51 | // IsGroup return true if it is group section 52 | func (c *Entry) IsGroup() bool { 53 | return strings.HasPrefix(c.Name, "group:") 54 | } 55 | 56 | // GetGroupName get the group name if this entry is group 57 | func (c *Entry) GetGroupName() string { 58 | if strings.HasPrefix(c.Name, "group:") { 59 | return c.Name[len("group:"):] 60 | } 61 | return "" 62 | } 63 | 64 | // GetPrograms get the programs from the group 65 | func (c *Entry) GetPrograms() []string { 66 | if c.IsGroup() { 67 | r := c.GetStringArray("programs", ",") 68 | for i, p := range r { 69 | r[i] = strings.TrimSpace(p) 70 | } 71 | return r 72 | } 73 | return make([]string, 0) 74 | } 75 | 76 | func (c *Entry) setGroup(group string) { 77 | c.Group = group 78 | } 79 | 80 | // String dump the configuration as string 81 | func (c *Entry) String() string { 82 | buf := bytes.NewBuffer(make([]byte, 0)) 83 | fmt.Fprintf(buf, "configDir=%s\n", c.ConfigDir) 84 | fmt.Fprintf(buf, "group=%s\n", c.Group) 85 | for k, v := range c.keyValues { 86 | fmt.Fprintf(buf, "%s=%s\n", k, v) 87 | } 88 | return buf.String() 89 | 90 | } 91 | 92 | // Config memory reprentations of supervisor configuration file 93 | type Config struct { 94 | configFile string 95 | //mapping between the section name and the configure 96 | entries map[string]*Entry 97 | 98 | ProgramGroup *ProcessGroup 99 | } 100 | 101 | // NewEntry create a configuration entry 102 | func NewEntry(configDir string) *Entry { 103 | return &Entry{configDir, "", "", make(map[string]string)} 104 | } 105 | 106 | // NewConfig create Config object 107 | func NewConfig(configFile string) *Config { 108 | return &Config{configFile, make(map[string]*Entry), NewProcessGroup()} 109 | } 110 | 111 | //create a new entry or return the already-exist entry 112 | func (c *Config) createEntry(name string, configDir string) *Entry { 113 | entry, ok := c.entries[name] 114 | 115 | if !ok { 116 | entry = NewEntry(configDir) 117 | c.entries[name] = entry 118 | } 119 | return entry 120 | } 121 | 122 | // 123 | // Load load the configuration and return the loaded programs 124 | func (c *Config) Load() ([]string, error) { 125 | ini := ini.NewIni() 126 | c.ProgramGroup = NewProcessGroup() 127 | ini.LoadFile(c.configFile) 128 | 129 | includeFiles := c.getIncludeFiles(ini) 130 | for _, f := range includeFiles { 131 | ini.LoadFile(f) 132 | } 133 | return c.parse(ini), nil 134 | } 135 | 136 | func (c *Config) getIncludeFiles(cfg *ini.Ini) []string { 137 | result := make([]string, 0) 138 | if includeSection, err := cfg.GetSection("include"); err == nil { 139 | key, err := includeSection.GetValue("files") 140 | if err == nil { 141 | env := NewStringExpression("here", c.GetConfigFileDir()) 142 | files := strings.Fields(key) 143 | for _, fRaw := range files { 144 | dir := c.GetConfigFileDir() 145 | f, err := env.Eval(fRaw) 146 | if err != nil { 147 | continue 148 | } 149 | if filepath.IsAbs(f) { 150 | dir = filepath.Dir(f) 151 | } 152 | fileInfos, err := ioutil.ReadDir(dir) 153 | if err == nil { 154 | goPattern := toRegexp(filepath.Base(f)) 155 | for _, fileInfo := range fileInfos { 156 | if matched, err := regexp.MatchString(goPattern, fileInfo.Name()); matched && err == nil { 157 | result = append(result, filepath.Join(dir, fileInfo.Name())) 158 | } 159 | } 160 | } 161 | 162 | } 163 | } 164 | } 165 | return result 166 | 167 | } 168 | 169 | func (c *Config) parse(cfg *ini.Ini) []string { 170 | c.parseGroup(cfg) 171 | loadedPrograms := c.parseProgram(cfg) 172 | 173 | //parse non-group,non-program and non-eventlistener sections 174 | for _, section := range cfg.Sections() { 175 | if !strings.HasPrefix(section.Name, "group:") && !strings.HasPrefix(section.Name, "program:") && !strings.HasPrefix(section.Name, "eventlistener:") { 176 | entry := c.createEntry(section.Name, c.GetConfigFileDir()) 177 | c.entries[section.Name] = entry 178 | entry.parse(section) 179 | } 180 | } 181 | c.setProgramDefaultParams() 182 | return loadedPrograms 183 | } 184 | 185 | // set the default parameteres of programs 186 | func (c *Config) setProgramDefaultParams() { 187 | defParams, ok := c.entries["program-default"] 188 | 189 | if ok { 190 | for _, entry := range c.entries { 191 | if !entry.IsProgram() { 192 | continue 193 | } 194 | for param, value := range defParams.keyValues { 195 | v, exist := entry.keyValues[param] 196 | if !exist || len(v) <= 0 { 197 | entry.keyValues[param] = value 198 | } 199 | } 200 | 201 | } 202 | } 203 | 204 | } 205 | 206 | // GetConfigFileDir get the directory of supervisor configuration file 207 | func (c *Config) GetConfigFileDir() string { 208 | return filepath.Dir(c.configFile) 209 | } 210 | 211 | //convert supervisor file pattern to the go regrexp 212 | func toRegexp(pattern string) string { 213 | tmp := strings.Split(pattern, ".") 214 | for i, t := range tmp { 215 | s := strings.Replace(t, "*", ".*", -1) 216 | tmp[i] = strings.Replace(s, "?", ".", -1) 217 | } 218 | return strings.Join(tmp, "\\.") 219 | } 220 | 221 | // GetUnixHTTPServer get the unix_http_server section 222 | func (c *Config) GetUnixHTTPServer() (*Entry, bool) { 223 | entry, ok := c.entries["unix_http_server"] 224 | 225 | return entry, ok 226 | } 227 | 228 | //GetSupervisord get the supervisord section 229 | func (c *Config) GetSupervisord() (*Entry, bool) { 230 | entry, ok := c.entries["supervisord"] 231 | return entry, ok 232 | } 233 | 234 | // GetInetHTTPServer Get the inet_http_server configuration section 235 | func (c *Config) GetInetHTTPServer() (*Entry, bool) { 236 | entry, ok := c.entries["inet_http_server"] 237 | return entry, ok 238 | } 239 | 240 | // GetSupervisorctl Get the "supervisorctl" section 241 | func (c *Config) GetSupervisorctl() (*Entry, bool) { 242 | entry, ok := c.entries["supervisorctl"] 243 | return entry, ok 244 | } 245 | 246 | // GetEntries get the configuration entries by filter 247 | func (c *Config) GetEntries(filterFunc func(entry *Entry) bool) []*Entry { 248 | result := make([]*Entry, 0) 249 | for _, entry := range c.entries { 250 | if filterFunc(entry) { 251 | result = append(result, entry) 252 | } 253 | } 254 | return result 255 | } 256 | 257 | // GetGroups get entries of all the program groups 258 | func (c *Config) GetGroups() []*Entry { 259 | return c.GetEntries(func(entry *Entry) bool { 260 | return entry.IsGroup() 261 | }) 262 | } 263 | 264 | // GetPrograms get entries of all programs 265 | func (c *Config) GetPrograms() []*Entry { 266 | programs := c.GetEntries(func(entry *Entry) bool { 267 | return entry.IsProgram() 268 | }) 269 | 270 | return sortProgram(programs) 271 | } 272 | 273 | // GetEventListeners get event listeners 274 | func (c *Config) GetEventListeners() []*Entry { 275 | eventListeners := c.GetEntries(func(entry *Entry) bool { 276 | return entry.IsEventListener() 277 | }) 278 | 279 | return eventListeners 280 | } 281 | 282 | // GetProgramNames get all the program names 283 | func (c *Config) GetProgramNames() []string { 284 | result := make([]string, 0) 285 | programs := c.GetPrograms() 286 | 287 | programs = sortProgram(programs) 288 | for _, entry := range programs { 289 | result = append(result, entry.GetProgramName()) 290 | } 291 | return result 292 | } 293 | 294 | // GetProgram return the proram configure entry or nil 295 | func (c *Config) GetProgram(name string) *Entry { 296 | for _, entry := range c.entries { 297 | if entry.IsProgram() && entry.GetProgramName() == name { 298 | return entry 299 | } 300 | } 301 | return nil 302 | } 303 | 304 | // GetBool get value of key as bool 305 | func (c *Entry) GetBool(key string, defValue bool) bool { 306 | value, ok := c.keyValues[key] 307 | 308 | if ok { 309 | b, err := strconv.ParseBool(value) 310 | if err == nil { 311 | return b 312 | } 313 | } 314 | return defValue 315 | } 316 | 317 | // HasParameter check if has parameter 318 | func (c *Entry) HasParameter(key string) bool { 319 | _, ok := c.keyValues[key] 320 | return ok 321 | } 322 | 323 | func toInt(s string, factor int, defValue int) int { 324 | i, err := strconv.Atoi(s) 325 | if err == nil { 326 | return i * factor 327 | } 328 | return defValue 329 | } 330 | 331 | // GetInt get the value of the key as int 332 | func (c *Entry) GetInt(key string, defValue int) int { 333 | value, ok := c.keyValues[key] 334 | 335 | if ok { 336 | return toInt(value, 1, defValue) 337 | } 338 | return defValue 339 | } 340 | 341 | // GetEnv get the value of key as environment setting. An environment string example: 342 | // environment = A="env 1",B="this is a test" 343 | func (c *Entry) GetEnv(key string) []string { 344 | value, ok := c.keyValues[key] 345 | env := make([]string, 0) 346 | 347 | if ok { 348 | start := 0 349 | n := len(value) 350 | var i int 351 | for { 352 | for i = start; i < n && value[i] != '='; { 353 | i++ 354 | } 355 | key := value[start:i] 356 | start = i + 1 357 | if value[start] == '"' { 358 | for i = start + 1; i < n && value[i] != '"'; { 359 | i++ 360 | } 361 | if i < n { 362 | env = append(env, fmt.Sprintf("%s=%s", strings.TrimSpace(key), strings.TrimSpace(value[start+1:i]))) 363 | } 364 | if i+1 < n && value[i+1] == ',' { 365 | start = i + 2 366 | } else { 367 | break 368 | } 369 | } else { 370 | for i = start; i < n && value[i] != ','; { 371 | i++ 372 | } 373 | if i < n { 374 | env = append(env, fmt.Sprintf("%s=%s", strings.TrimSpace(key), strings.TrimSpace(value[start:i]))) 375 | start = i + 1 376 | } else { 377 | env = append(env, fmt.Sprintf("%s=%s", strings.TrimSpace(key), strings.TrimSpace(value[start:]))) 378 | break 379 | } 380 | } 381 | } 382 | } 383 | 384 | result := make([]string, 0) 385 | for i := 0; i < len(env); i++ { 386 | tmp, err := NewStringExpression("program_name", c.GetProgramName(), 387 | "process_num", c.GetString("process_num", "0"), 388 | "group_name", c.GetGroupName(), 389 | "here", c.ConfigDir).Eval(env[i]) 390 | if err == nil { 391 | result = append(result, tmp) 392 | } 393 | } 394 | return result 395 | } 396 | 397 | // GetString get the value of key as string 398 | func (c *Entry) GetString(key string, defValue string) string { 399 | s, ok := c.keyValues[key] 400 | 401 | if ok { 402 | env := NewStringExpression("here", c.ConfigDir) 403 | repS, err := env.Eval(s) 404 | if err == nil { 405 | return repS 406 | } 407 | log.WithFields(log.Fields{ 408 | log.ErrorKey: err, 409 | "program": c.GetProgramName(), 410 | "key": key, 411 | }).Warn("Unable to parse expression") 412 | } 413 | return defValue 414 | } 415 | 416 | //GetStringExpression get the value of key as string and attempt to parse it with StringExpression 417 | func (c *Entry) GetStringExpression(key string, defValue string) string { 418 | s, ok := c.keyValues[key] 419 | if !ok || s == "" { 420 | return "" 421 | } 422 | 423 | hostName, err := os.Hostname() 424 | if err != nil { 425 | hostName = "Unknown" 426 | } 427 | result, err := NewStringExpression("program_name", c.GetProgramName(), 428 | "process_num", c.GetString("process_num", "0"), 429 | "group_name", c.GetGroupName(), 430 | "here", c.ConfigDir, 431 | "host_node_name", hostName).Eval(s) 432 | 433 | if err != nil { 434 | log.WithFields(log.Fields{ 435 | log.ErrorKey: err, 436 | "program": c.GetProgramName(), 437 | "key": key, 438 | }).Warn("unable to parse expression") 439 | return s 440 | } 441 | 442 | return result 443 | } 444 | 445 | // GetStringArray get the string value and split it as array with "sep" 446 | func (c *Entry) GetStringArray(key string, sep string) []string { 447 | s, ok := c.keyValues[key] 448 | 449 | if ok { 450 | return strings.Split(s, sep) 451 | } 452 | return make([]string, 0) 453 | } 454 | 455 | // GetBytes get the value of key as the bytes setting. 456 | // 457 | // logSize=1MB 458 | // logSize=1GB 459 | // logSize=1KB 460 | // logSize=1024 461 | // 462 | func (c *Entry) GetBytes(key string, defValue int) int { 463 | v, ok := c.keyValues[key] 464 | 465 | if ok { 466 | if len(v) > 2 { 467 | lastTwoBytes := v[len(v)-2:] 468 | if lastTwoBytes == "MB" { 469 | return toInt(v[:len(v)-2], 1024*1024, defValue) 470 | } else if lastTwoBytes == "GB" { 471 | return toInt(v[:len(v)-2], 1024*1024*1024, defValue) 472 | } else if lastTwoBytes == "KB" { 473 | return toInt(v[:len(v)-2], 1024, defValue) 474 | } 475 | } 476 | return toInt(v, 1, defValue) 477 | } 478 | return defValue 479 | } 480 | 481 | func (c *Entry) parse(section *ini.Section) { 482 | c.Name = section.Name 483 | for _, key := range section.Keys() { 484 | c.keyValues[key.Name()] = strings.TrimSpace(key.ValueWithDefault("")) 485 | } 486 | } 487 | 488 | func (c *Config) parseGroup(cfg *ini.Ini) { 489 | 490 | //parse the group at first 491 | for _, section := range cfg.Sections() { 492 | if strings.HasPrefix(section.Name, "group:") { 493 | entry := c.createEntry(section.Name, c.GetConfigFileDir()) 494 | entry.parse(section) 495 | groupName := entry.GetGroupName() 496 | programs := entry.GetPrograms() 497 | for _, program := range programs { 498 | c.ProgramGroup.Add(groupName, program) 499 | } 500 | } 501 | } 502 | } 503 | 504 | func (c *Config) isProgramOrEventListener(section *ini.Section) (bool, string) { 505 | //check if it is a program or event listener section 506 | isProgram := strings.HasPrefix(section.Name, "program:") 507 | isEventListener := strings.HasPrefix(section.Name, "eventlistener:") 508 | prefix := "" 509 | if isProgram { 510 | prefix = "program:" 511 | } else if isEventListener { 512 | prefix = "eventlistener:" 513 | } 514 | return isProgram || isEventListener, prefix 515 | } 516 | 517 | // parse the sections starts with "program:" prefix. 518 | // 519 | // Return all the parsed program names in the ini 520 | func (c *Config) parseProgram(cfg *ini.Ini) []string { 521 | loadedPrograms := make([]string, 0) 522 | for _, section := range cfg.Sections() { 523 | 524 | programOrEventListener, prefix := c.isProgramOrEventListener(section) 525 | 526 | //if it is program or event listener 527 | if programOrEventListener { 528 | //get the number of processes 529 | numProcs, err := section.GetInt("numprocs") 530 | programName := section.Name[len(prefix):] 531 | if err != nil { 532 | numProcs = 1 533 | } 534 | procName, err := section.GetValue("process_name") 535 | if numProcs > 1 { 536 | if err != nil || strings.Index(procName, "%(process_num)") == -1 { 537 | log.WithFields(log.Fields{ 538 | "numprocs": numProcs, 539 | "process_name": procName, 540 | }).Error("no process_num in process name") 541 | } 542 | } 543 | originalProcName := programName 544 | if err == nil { 545 | originalProcName = procName 546 | } 547 | 548 | originalCmd := section.GetValueWithDefault("command", "") 549 | 550 | for i := 1; i <= numProcs; i++ { 551 | envs := NewStringExpression("program_name", programName, 552 | "process_num", fmt.Sprintf("%d", i), 553 | "group_name", c.ProgramGroup.GetGroup(programName, programName), 554 | "here", c.GetConfigFileDir()) 555 | cmd, err := envs.Eval(originalCmd) 556 | if err != nil { 557 | log.WithFields(log.Fields{ 558 | log.ErrorKey: err, 559 | "program": programName, 560 | }).Error("get envs failed") 561 | continue 562 | } 563 | section.Add("command", cmd) 564 | 565 | procName, err := envs.Eval(originalProcName) 566 | if err != nil { 567 | log.WithFields(log.Fields{ 568 | log.ErrorKey: err, 569 | "program": programName, 570 | }).Error("get envs failed") 571 | continue 572 | } 573 | 574 | section.Add("process_name", procName) 575 | section.Add("numprocs_start", fmt.Sprintf("%d", (i-1))) 576 | section.Add("process_num", fmt.Sprintf("%d", i)) 577 | entry := c.createEntry(procName, c.GetConfigFileDir()) 578 | entry.parse(section) 579 | entry.Name = prefix + procName 580 | group := c.ProgramGroup.GetGroup(programName, programName) 581 | entry.Group = group 582 | loadedPrograms = append(loadedPrograms, procName) 583 | } 584 | } 585 | } 586 | return loadedPrograms 587 | 588 | } 589 | 590 | // String convert the configuration to string represents 591 | func (c *Config) String() string { 592 | buf := bytes.NewBuffer(make([]byte, 0)) 593 | fmt.Fprintf(buf, "configFile:%s\n", c.configFile) 594 | for k, v := range c.entries { 595 | fmt.Fprintf(buf, "[program:%s]\n", k) 596 | fmt.Fprintf(buf, "%s\n", v.String()) 597 | } 598 | return buf.String() 599 | } 600 | 601 | // RemoveProgram remove a program entry by its name 602 | func (c *Config) RemoveProgram(programName string) { 603 | delete(c.entries, programName) 604 | c.ProgramGroup.Remove(programName) 605 | } 606 | --------------------------------------------------------------------------------