├── version └── version.go ├── README.md ├── LICENSE ├── signals └── signals.go ├── logging └── logging.go ├── main.go ├── status └── status.go ├── configuration └── configuration.go ├── application └── application.go ├── rest └── rest.go └── process └── process.go /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | var Version string //set during build 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### Exorsus 2 | 3 | Exorsus is a server system that allows its users to run and control a multiple processes in Docker Containers. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Vladimir Bykovskiy 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 | -------------------------------------------------------------------------------- /signals/signals.go: -------------------------------------------------------------------------------- 1 | package signals 2 | 3 | import ( 4 | "fmt" 5 | "github.com/sirupsen/logrus" 6 | "github.com/vvhq/exorsus/configuration" 7 | "github.com/vvhq/exorsus/logging" 8 | "github.com/vvhq/exorsus/process" 9 | "github.com/vvhq/exorsus/rest" 10 | "os" 11 | "sync" 12 | "syscall" 13 | "time" 14 | ) 15 | 16 | func HandleSignals(wg *sync.WaitGroup, 17 | signalChan chan os.Signal, 18 | procManager *process.Manager, 19 | restService *rest.Service, 20 | timeout int, 21 | config *configuration.Configuration, 22 | logger *logrus.Logger, 23 | loggerHook *logging.FileHook) { 24 | for { 25 | receivedSignal := <-signalChan 26 | logger. 27 | WithField("source", "signals"). 28 | WithField("signal", receivedSignal.String()). 29 | Info("Signal received") 30 | if receivedSignal == syscall.SIGUSR1 { 31 | handleUSR1(logger, loggerHook) 32 | } else if receivedSignal == syscall.SIGHUP { 33 | handleHUP(logger) 34 | } else if receivedSignal == syscall.SIGINT || receivedSignal == syscall.SIGTERM { 35 | handleSTOP(procManager, restService, timeout, logger) 36 | wg.Done() 37 | return 38 | } else { 39 | logger. 40 | WithField("source", "signals"). 41 | WithField("signal", "UNHANDLED"). 42 | Info("Signal received") 43 | } 44 | } 45 | } 46 | 47 | func handleUSR1(logger *logrus.Logger, loggerHook *logging.FileHook) { 48 | loggerHook.Rotate() 49 | logger. 50 | WithField("source", "signals"). 51 | Info("Log rotated") 52 | } 53 | 54 | func handleHUP(logger *logrus.Logger) { 55 | logger. 56 | WithField("source", "signals"). 57 | WithField("signal", "HUP"). 58 | Info("Signal received") 59 | } 60 | 61 | func handleSTOP(procManager *process.Manager, restService *rest.Service, timeout int, logger *logrus.Logger) { 62 | procManager.StopAll() 63 | restService.Stop() 64 | logger. 65 | WithField("source", "signal"). 66 | Infof("Waiting for the processes to complete %d seconds", timeout) 67 | time.Sleep(time.Second * time.Duration(timeout)) 68 | for _, proc := range procManager.List() { 69 | if proc.Zombie() { 70 | logger. 71 | WithField("source", "main"). 72 | WithField("process", proc.Name). 73 | WithField("pid", fmt.Sprintf("%d", proc.GetPid())). 74 | Error("Found zombie process, force exit") 75 | os.Exit(2) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /logging/logging.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "fmt" 5 | "github.com/sirupsen/logrus" 6 | "gopkg.in/natefinch/lumberjack.v2" 7 | "io" 8 | "os" 9 | "path" 10 | "path/filepath" 11 | ) 12 | 13 | type FileHook struct { 14 | logger *logrus.Logger 15 | lumberLogger *lumberjack.Logger 16 | parentLogger *logrus.Logger 17 | } 18 | 19 | func (hook *FileHook) Rotate() { 20 | err := hook.lumberLogger.Rotate() 21 | if err != nil { 22 | hook.parentLogger.Error(err.Error()) 23 | } 24 | } 25 | 26 | func (hook *FileHook) Levels() []logrus.Level { 27 | return logrus.AllLevels 28 | } 29 | 30 | func (hook *FileHook) Fire(entry *logrus.Entry) error { 31 | switch entry.Level { 32 | case logrus.TraceLevel: 33 | hook.logger.WithFields(entry.Data).WithTime(entry.Time).Trace(entry.Message) 34 | case logrus.DebugLevel: 35 | hook.logger.WithFields(entry.Data).WithTime(entry.Time).Debug(entry.Message) 36 | case logrus.InfoLevel: 37 | hook.logger.WithFields(entry.Data).WithTime(entry.Time).Info(entry.Message) 38 | case logrus.WarnLevel: 39 | hook.logger.WithFields(entry.Data).WithTime(entry.Time).Warn(entry.Message) 40 | case logrus.ErrorLevel: 41 | hook.logger.WithFields(entry.Data).WithTime(entry.Time).Error(entry.Message) 42 | case logrus.FatalLevel: 43 | hook.logger.WithFields(entry.Data).WithTime(entry.Time).Fatal(entry.Message) 44 | case logrus.PanicLevel: 45 | hook.logger.WithFields(entry.Data).WithTime(entry.Time).Panic(entry.Message) 46 | default: 47 | hook.logger.WithFields(entry.Data).WithTime(entry.Time).Print(entry.Message) 48 | } 49 | return nil 50 | } 51 | 52 | func NewFileHook(parentLogger *logrus.Logger, logPath string, maxSize int, maxBackups int, maxAge int, localTime bool) (*FileHook, error) { 53 | fileLogger := logrus.New() 54 | fileLogger.SetLevel(parentLogger.Level) 55 | fileLogger.SetFormatter(&logrus.JSONFormatter{}) 56 | fileLogger.SetNoLock() 57 | hostName, err := os.Hostname() 58 | if err == nil { 59 | logDirName, logFileName := filepath.Split(logPath) 60 | logPath = path.Join(logDirName, fmt.Sprintf("%s_%s", hostName, logFileName)) 61 | } else { 62 | return nil, err 63 | } 64 | lumberLogger := &lumberjack.Logger{ 65 | Filename: logPath, 66 | MaxSize: maxSize, 67 | MaxBackups: maxBackups, 68 | MaxAge: maxAge, 69 | LocalTime: localTime, 70 | } 71 | fileLogger.SetOutput(lumberLogger) 72 | return &FileHook{logger: fileLogger, lumberLogger: lumberLogger, parentLogger: parentLogger}, nil 73 | } 74 | 75 | func NewLogger(output io.Writer, level logrus.Level) *logrus.Logger { 76 | logger := logrus.New() 77 | logger.SetLevel(level) 78 | logger.SetFormatter(&logrus.TextFormatter{FullTimestamp: true, DisableLevelTruncation: true}) 79 | logger.SetOutput(output) 80 | logger.SetNoLock() 81 | if logger.Level == logrus.TraceLevel { 82 | logger.SetReportCaller(true) 83 | } 84 | return logger 85 | } 86 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/vvhq/exorsus/application" 7 | "github.com/vvhq/exorsus/configuration" 8 | "github.com/vvhq/exorsus/logging" 9 | "github.com/vvhq/exorsus/process" 10 | "github.com/vvhq/exorsus/rest" 11 | "github.com/vvhq/exorsus/signals" 12 | "github.com/vvhq/exorsus/status" 13 | "github.com/vvhq/exorsus/version" 14 | "io/ioutil" 15 | "os" 16 | "os/signal" 17 | "path" 18 | "strconv" 19 | "sync" 20 | "syscall" 21 | ) 22 | 23 | func main() { 24 | configDir := flag.String("config", "./config/", "application directory path") 25 | printVersion := flag.Bool("version", false, "print version number") 26 | flag.Parse() 27 | if *printVersion { 28 | fmt.Println(version.Version) 29 | os.Exit(0) 30 | } 31 | configDirPath := path.Dir(*configDir) 32 | configPath := path.Join(configDirPath, configuration.DefaultConfigurationFileName) 33 | config := configuration.New(configPath) 34 | 35 | var logger = logging.NewLogger(os.Stdout, config.GetLogLevel()) 36 | loggerHook, err := logging.NewFileHook(logger, 37 | path.Join(path.Dir(config.GetLogPath()), configuration.DefaultLogFileName), 38 | config.LogMaxSize, 39 | config.LogMaxBackups, 40 | config.LogMaxAge, 41 | config.LogLocalTime) 42 | if err != nil { 43 | logger. 44 | WithField("source", "main"). 45 | WithField("error", err.Error()). 46 | Error("Can not set file hook") 47 | } else { 48 | logger.AddHook(loggerHook) 49 | } 50 | logger.WithField("Source", "Main").Trace("Exorsus starting") 51 | maxTimeout := 0 52 | var wg sync.WaitGroup 53 | storage := application.NewStorage(path.Join(configDirPath, configuration.DefaultApplicationsFileName), logger) 54 | procManager := process.NewManager(&wg, logger) 55 | for _, app := range storage.List() { 56 | appClone, err := app.Copy() 57 | if err != nil { 58 | logger. 59 | WithField("source", "main"). 60 | WithField("error", err.Error()). 61 | Error("Skip application due error") 62 | } else { 63 | proc := process.New(appClone, status.New(config.GetMaxStdLines()), &wg, config, logger) 64 | procManager.Append(proc) 65 | if appClone.Timeout > maxTimeout { 66 | maxTimeout = appClone.Timeout 67 | } 68 | } 69 | } 70 | restService := rest.New(config.GetListenPort(), storage, procManager, &wg, config, logger) 71 | procManager.StartAll() 72 | restService.Start() 73 | maxTimeout = maxTimeout + config.GetShutdownTimeout() 74 | signalChan := make(chan os.Signal, 1) 75 | signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1, syscall.SIGHUP) 76 | wg.Add(1) 77 | go signals.HandleSignals(&wg, signalChan, procManager, restService, maxTimeout, config, logger, loggerHook) 78 | logger. 79 | WithField("source", "main"). 80 | Infof("Exorsus stared; Pid: %d", os.Getpid()) 81 | pidPath := path.Join(config.PidPath, config.PidFileName) 82 | err = ioutil.WriteFile(pidPath, []byte(strconv.Itoa(os.Getpid())), 0644) 83 | if err != nil { 84 | logger. 85 | WithField("source", "main"). 86 | Warnf("Can not write PID to file '%s'; Error: %s", pidPath, err.Error()) 87 | } 88 | wg.Wait() 89 | err = ioutil.WriteFile(pidPath, []byte(""), 0644) 90 | if err != nil { 91 | logger. 92 | WithField("source", "main"). 93 | Warnf("Can not write PID to file '%s'; Error: %s", pidPath, err.Error()) 94 | } 95 | logger. 96 | WithField("source", "main"). 97 | Info("Exorsus stopped") 98 | } 99 | -------------------------------------------------------------------------------- /status/status.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "fmt" 5 | "github.com/vvhq/exorsus/configuration" 6 | "sync" 7 | "sync/atomic" 8 | "time" 9 | ) 10 | 11 | type IOStdStore struct { 12 | max int 13 | warehouse []string 14 | lock sync.RWMutex 15 | } 16 | 17 | func (store *IOStdStore) Append(item string) { 18 | item = fmt.Sprintf("%s%s%s %s", 19 | configuration.DefaultStdDatePrefix, 20 | time.Now().Format(configuration.DefaultStdDateLayout), 21 | configuration.DefaultStdDateSuffix, 22 | item) 23 | store.lock.Lock() 24 | defer store.lock.Unlock() 25 | store.warehouse = append(store.warehouse, item) 26 | if len(store.warehouse) > store.max { 27 | shifted := make([]string, store.max) 28 | idx := len(store.warehouse) - store.max 29 | copy(shifted, store.warehouse[idx:]) 30 | store.warehouse = shifted 31 | } 32 | } 33 | 34 | func (store *IOStdStore) List() []string { 35 | store.lock.RLock() 36 | defer store.lock.RUnlock() 37 | if len(store.warehouse) == 0 { 38 | return []string{} 39 | } 40 | var warehouse []string 41 | if len(store.warehouse) > store.max { 42 | warehouse = make([]string, store.max) 43 | copy(warehouse, store.warehouse[(len(store.warehouse)-store.max):]) 44 | } else { 45 | warehouse = make([]string, len(store.warehouse)) 46 | copy(warehouse, store.warehouse) 47 | } 48 | return warehouse 49 | } 50 | 51 | func NewIOStdStore(max int) *IOStdStore { 52 | return &IOStdStore{max: max} 53 | } 54 | 55 | const Stopped int = 0 56 | const Started int = 1 57 | const Stopping int = 2 58 | const Starting int = 3 59 | const Failed int = 4 60 | 61 | type Status struct { 62 | pid int32 63 | code int32 64 | state int32 65 | startupError error 66 | stdOutStore *IOStdStore 67 | stdErrStore *IOStdStore 68 | lock sync.RWMutex 69 | } 70 | 71 | func (status *Status) SetPid(pid int) { 72 | atomic.SwapInt32(&status.pid, int32(pid)) 73 | } 74 | 75 | func (status *Status) GetPid() int { 76 | return int(atomic.LoadInt32(&status.pid)) 77 | } 78 | 79 | func (status *Status) SetExitCode(code int) { 80 | atomic.SwapInt32(&status.code, int32(code)) 81 | } 82 | 83 | func (status *Status) GetExitCode() int { 84 | return int(atomic.LoadInt32(&status.code)) 85 | } 86 | 87 | func (status *Status) SetState(state int) { 88 | atomic.SwapInt32(&status.state, int32(state)) 89 | } 90 | 91 | func (status *Status) GetState() int { 92 | return int(atomic.LoadInt32(&status.state)) 93 | } 94 | 95 | func (status *Status) SetError(startupError error) { 96 | status.lock.Lock() 97 | defer status.lock.Unlock() 98 | status.startupError = startupError 99 | } 100 | 101 | func (status *Status) GetError() error { 102 | status.lock.RLock() 103 | defer status.lock.RUnlock() 104 | return status.startupError 105 | } 106 | 107 | func (status *Status) AddStdOutItem(item string) { 108 | status.stdOutStore.Append(item) 109 | } 110 | 111 | func (status *Status) ListStdOutItems() []string { 112 | return status.stdOutStore.List() 113 | } 114 | 115 | func (status *Status) AddStdErrItem(item string) { 116 | status.stdErrStore.Append(item) 117 | } 118 | 119 | func (status *Status) ListStdErrItems() []string { 120 | return status.stdErrStore.List() 121 | } 122 | 123 | func New(max int) *Status { 124 | return &Status{pid: 0, code: 0, startupError: nil, state: int32(Stopped), stdOutStore: NewIOStdStore(max), stdErrStore: NewIOStdStore(max)} 125 | } 126 | -------------------------------------------------------------------------------- /configuration/configuration.go: -------------------------------------------------------------------------------- 1 | package configuration 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/sirupsen/logrus" 7 | "io/ioutil" 8 | "os" 9 | "path" 10 | "strings" 11 | ) 12 | 13 | const DefaultConfigPath string = "./config/" 14 | const DefaultLogPath string = "./log/" 15 | const DefaultLogLevel string = "info" 16 | const DefaultLogMaxSize int = 10 17 | const DefaultLogMaxBackups int = 30 18 | const DefaultLogMaxAge int = 28 19 | const DefaultLogLocalTime bool = true 20 | const DefaultStdLinesCount int = 500 21 | const DefaultStdDateLayout = "2006-01-02 15:04:05" 22 | const DefaultStdDatePrefix = "[" 23 | const DefaultStdDateSuffix = "]" 24 | const DefaultShutdownTimeout int = 4 25 | const DefaultListenPort int = 5202 26 | const DefaultConfigurationFileName string = "config.json" 27 | const DefaultLogFileName string = "log.json" 28 | const DefaultApplicationsFileName string = "applications.json" 29 | const DefaultPidPath string = "/tmp/" 30 | const DefaultPidFileName string = "exorsus.pid" 31 | 32 | type Configuration struct { 33 | LogPath string 34 | LogLevel string 35 | LogMaxSize int 36 | LogMaxBackups int 37 | LogMaxAge int 38 | LogLocalTime bool 39 | StdLinesCount int 40 | ShutdownTimeout int 41 | ListenPort int 42 | DateLayout string 43 | DatePrefix string 44 | DateSuffix string 45 | PidPath string 46 | PidFileName string 47 | } 48 | 49 | func (config *Configuration) GetLogPath() string { 50 | return config.LogPath 51 | } 52 | 53 | func (config *Configuration) GetLogLevel() logrus.Level { 54 | switch config.LogLevel { 55 | case "trace": 56 | return logrus.TraceLevel 57 | case "debug": 58 | return logrus.DebugLevel 59 | case "info": 60 | return logrus.InfoLevel 61 | case "warn": 62 | return logrus.WarnLevel 63 | case "error": 64 | return logrus.ErrorLevel 65 | default: 66 | return logrus.TraceLevel 67 | } 68 | } 69 | 70 | func (config *Configuration) GetMaxStdLines() int { 71 | return config.StdLinesCount 72 | } 73 | 74 | func (config *Configuration) GetShutdownTimeout() int { 75 | return config.ShutdownTimeout 76 | } 77 | 78 | func (config *Configuration) GetListenPort() int { 79 | return config.ListenPort 80 | } 81 | 82 | func (config *Configuration) applyDefaults() { 83 | config.LogPath = DefaultLogPath 84 | config.LogLevel = DefaultLogLevel 85 | config.StdLinesCount = DefaultStdLinesCount 86 | config.ShutdownTimeout = DefaultShutdownTimeout 87 | config.ListenPort = DefaultListenPort 88 | config.DateLayout = DefaultStdDateLayout 89 | config.DatePrefix = DefaultStdDatePrefix 90 | config.DateSuffix = DefaultStdDateSuffix 91 | config.LogMaxSize = DefaultLogMaxSize 92 | config.LogMaxBackups = DefaultLogMaxBackups 93 | config.LogMaxAge = DefaultLogMaxAge 94 | config.LogLocalTime = DefaultLogLocalTime 95 | config.PidPath = DefaultPidPath 96 | config.PidFileName = DefaultPidFileName 97 | if _, err := os.Stat(DefaultConfigPath); os.IsNotExist(err) { 98 | err := os.Mkdir(DefaultConfigPath, 0755) 99 | if err != nil { 100 | fmt.Printf("Can not create default configuration directory: %s\n", err.Error()) 101 | return 102 | } 103 | } 104 | if _, err := os.Stat(DefaultLogPath); os.IsNotExist(err) { 105 | err := os.Mkdir(DefaultLogPath, 0755) 106 | if err != nil { 107 | fmt.Printf("Can not create default log directory: %s\n", err.Error()) 108 | return 109 | } 110 | } 111 | config.saveDefaults() 112 | } 113 | 114 | func (config *Configuration) saveDefaults() { 115 | prettyJson := "[]" 116 | jsonConfig, err := json.MarshalIndent(config, "", " ") 117 | prettyJson = string(jsonConfig) 118 | if err != nil { 119 | fmt.Printf("Can not marshal JSON: %s\n", err.Error()) 120 | return 121 | } 122 | err = ioutil.WriteFile(path.Join(path.Dir(DefaultConfigPath), DefaultConfigurationFileName), []byte(prettyJson), 0664) 123 | if err != nil { 124 | fmt.Printf("Can not save defaults: %s\n", err.Error()) 125 | } 126 | } 127 | 128 | func New(configPath string) *Configuration { 129 | config := Configuration{} 130 | buf, err := ioutil.ReadFile(path.Join(path.Dir(configPath), DefaultConfigurationFileName)) 131 | if err != nil { 132 | fmt.Printf("Configuration load error: %s\n", err.Error()) 133 | config.applyDefaults() 134 | return &config 135 | } 136 | configJson := string(buf) 137 | err = json.NewDecoder(strings.NewReader(configJson)).Decode(&config) 138 | if err != nil { 139 | fmt.Printf("Configuration decode error: %s\n", err.Error()) 140 | config.applyDefaults() 141 | return &config 142 | } 143 | return &config 144 | } 145 | -------------------------------------------------------------------------------- /application/application.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "github.com/sirupsen/logrus" 7 | "io/ioutil" 8 | "strings" 9 | "sync" 10 | ) 11 | 12 | type Environment struct { 13 | Name string `json:"name"` 14 | Value string `json:"value"` 15 | } 16 | 17 | type PreStart struct { 18 | Command string `json:"command"` 19 | Arguments string `json:"arguments"` 20 | WorkDir string `json:"workdir"` 21 | Timeout int `json:"timeout"` 22 | } 23 | 24 | type Application struct { 25 | Name string `json:"name"` 26 | Command string `json:"command"` 27 | Arguments string `json:"arguments"` 28 | WorkDir string `json:"workdir"` 29 | Timeout int `json:"timeout"` 30 | User string `json:"user"` 31 | Group string `json:"group"` 32 | Environment []Environment `json:"environment"` 33 | PreStart PreStart `json:"prestart"` 34 | } 35 | 36 | func (app *Application) Copy() (*Application, error) { 37 | jsonApp, err := json.Marshal(app) 38 | if err != nil { 39 | return nil, err 40 | } 41 | newApp := Application{} 42 | err = json.NewDecoder(strings.NewReader(string(jsonApp))).Decode(&newApp) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return &newApp, nil 47 | } 48 | 49 | type Storage struct { 50 | applications sync.Map 51 | path string 52 | lock sync.Mutex 53 | logger *logrus.Logger 54 | } 55 | 56 | func (store *Storage) Add(applicationConfiguration Application) error { 57 | _, ok := store.applications.Load(applicationConfiguration.Name) 58 | if ok { 59 | return errors.New("already exist") 60 | } 61 | store.applications.Store(applicationConfiguration.Name, applicationConfiguration) 62 | store.replace() 63 | return nil 64 | } 65 | 66 | func (store *Storage) Update(name string, applicationConfiguration Application) error { 67 | _, ok := store.applications.Load(name) 68 | if !ok { 69 | return errors.New("not found") 70 | } 71 | if name != applicationConfiguration.Name { 72 | store.applications.Delete(name) 73 | } 74 | store.applications.Store(applicationConfiguration.Name, applicationConfiguration) 75 | store.replace() 76 | return nil 77 | } 78 | 79 | func (store *Storage) Get(name string) (Application, bool) { 80 | rawApplication, ok := store.applications.Load(name) 81 | if ok { 82 | applicationConfiguration := rawApplication.(Application) 83 | return applicationConfiguration, true 84 | } 85 | return Application{}, false 86 | } 87 | 88 | func (store *Storage) Delete(name string) error { 89 | _, ok := store.applications.Load(name) 90 | if !ok { 91 | return errors.New("not found") 92 | } 93 | store.applications.Delete(name) 94 | store.replace() 95 | return nil 96 | } 97 | 98 | func (store *Storage) List() []Application { 99 | var applications []Application 100 | store.applications.Range(func(key, value interface{}) bool { 101 | applications = append(applications, value.(Application)) 102 | return true 103 | }) 104 | return applications 105 | } 106 | 107 | func (store *Storage) Load() { 108 | store.lock.Lock() 109 | defer store.lock.Unlock() 110 | buf, err := ioutil.ReadFile(store.path) 111 | if err != nil { 112 | store.logger. 113 | WithField("source", "storage"). 114 | WithField("path", store.path). 115 | WithField("error", err.Error()). 116 | Error("Can not load config file") 117 | return 118 | } 119 | applicationsJson := string(buf) 120 | var applications []Application 121 | err = json.NewDecoder(strings.NewReader(applicationsJson)).Decode(&applications) 122 | if err != nil { 123 | store.logger. 124 | WithField("source", "storage"). 125 | WithField("path", store.path). 126 | WithField("error", err.Error()). 127 | Error("Can not decode config file") 128 | return 129 | } 130 | for _, applicationConfiguration := range applications { 131 | store.applications.Store(applicationConfiguration.Name, applicationConfiguration) 132 | } 133 | } 134 | 135 | func (store *Storage) replace() { 136 | store.lock.Lock() 137 | defer store.lock.Unlock() 138 | var applications []Application 139 | store.applications.Range(func(key, value interface{}) bool { 140 | applications = append(applications, value.(Application)) 141 | return true 142 | }) 143 | prettyJson := "[]" 144 | if len(applications) != 0 { 145 | jsonApplications, err := json.MarshalIndent(applications, "", " ") 146 | prettyJson = string(jsonApplications) 147 | if err != nil { 148 | store.logger. 149 | WithField("source", "storage"). 150 | WithField("path", store.path). 151 | WithField("error", err.Error()). 152 | Error("Can not build JSON for applications") 153 | return 154 | } 155 | } 156 | err := ioutil.WriteFile(store.path, []byte(prettyJson), 0664) 157 | if err != nil { 158 | store.logger. 159 | WithField("source", "storage"). 160 | WithField("path", store.path). 161 | WithField("error", err.Error()). 162 | Error("Can not write JSON to file") 163 | } 164 | } 165 | 166 | func NewStorage(storePath string, logger *logrus.Logger) *Storage { 167 | store := Storage{logger: logger, path: storePath} 168 | store.Load() 169 | return &store 170 | } 171 | -------------------------------------------------------------------------------- /rest/rest.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/gorilla/mux" 8 | "github.com/sirupsen/logrus" 9 | "github.com/vvhq/exorsus/application" 10 | "github.com/vvhq/exorsus/configuration" 11 | "github.com/vvhq/exorsus/process" 12 | "github.com/vvhq/exorsus/status" 13 | "github.com/vvhq/exorsus/version" 14 | "net/http" 15 | "os" 16 | "sync" 17 | "time" 18 | ) 19 | 20 | type Service struct { 21 | port int 22 | store *application.Storage 23 | proc *process.Manager 24 | server *http.Server 25 | mainWaitGroup *sync.WaitGroup 26 | config *configuration.Configuration 27 | logger *logrus.Logger 28 | } 29 | 30 | func (service *Service) Start() { 31 | router := mux.NewRouter() 32 | router.HandleFunc("/applications/", service.listApplications).Methods("GET") 33 | router.HandleFunc("/applications/{name}", service.getApplication).Methods("GET") 34 | router.HandleFunc("/applications/", service.createApplication).Methods("POST") 35 | router.HandleFunc("/applications/{name}", service.updateApplication).Methods("PUT") 36 | router.HandleFunc("/applications/{name}", service.deleteApplication).Methods("DELETE") 37 | router.HandleFunc("/actions/start/", service.startAll).Methods("GET") 38 | router.HandleFunc("/actions/stop/", service.stopAll).Methods("GET") 39 | router.HandleFunc("/actions/restart/", service.restartAll).Methods("GET") 40 | router.HandleFunc("/actions/start/{name}", service.startApplication).Methods("GET") 41 | router.HandleFunc("/actions/stop/{name}", service.stopApplication).Methods("GET") 42 | router.HandleFunc("/actions/restart/{name}", service.restartApplication).Methods("GET") 43 | router.HandleFunc("/status/", service.statusAll).Methods("GET") 44 | router.HandleFunc("/status/{name}", service.status).Methods("GET") 45 | router.HandleFunc("/version/", service.getVersion).Methods("GET") 46 | 47 | service.server = &http.Server{Addr: fmt.Sprintf(":%d", service.port), Handler: router} 48 | service.logger. 49 | WithField("source", "rest"). 50 | Trace("Starting REST") 51 | service.mainWaitGroup.Add(1) 52 | go func() { 53 | err := service.server.ListenAndServe() 54 | if err != nil && err.Error() != "http: Server closed" { 55 | service.logger. 56 | WithField("source", "rest"). 57 | WithField("error", err.Error()). 58 | Error("Can not start REST") 59 | os.Exit(1) 60 | } 61 | service.logger. 62 | WithField("source", "rest"). 63 | Info("REST stopped") 64 | service.mainWaitGroup.Done() 65 | }() 66 | service.logger. 67 | WithField("source", "rest"). 68 | Info("REST started") 69 | } 70 | 71 | func (service *Service) Stop() { 72 | service.logger. 73 | WithField("source", "rest"). 74 | Trace("Stopping REST") 75 | ctx, _ := context.WithTimeout(context.Background(), 5*time.Second) 76 | err := service.server.Shutdown(ctx) 77 | if err != nil { 78 | service.logger. 79 | WithField("source", "rest"). 80 | WithField("error", err.Error()). 81 | Error("REST shutdown error") 82 | } 83 | } 84 | 85 | func (service *Service) httpError(responseWriter http.ResponseWriter, request *http.Request, httpStatus int, errorText string) { 86 | responseWriter.Header().Set("Content-Type", "application/json") 87 | jsonError := []byte(fmt.Sprintf("{\"error\":\"%s\"}", errorText)) 88 | responseWriter.WriteHeader(httpStatus) 89 | _, err := responseWriter.Write(jsonError) 90 | if err != nil { 91 | service.logger. 92 | WithField("source", "rest"). 93 | WithField("error", err.Error()). 94 | WithField("request", request.RequestURI). 95 | Error("Request error") 96 | } else { 97 | service.logger. 98 | WithField("source", "rest"). 99 | WithField("request", request.RequestURI). 100 | Trace("Request success") 101 | } 102 | } 103 | 104 | func (service *Service) httpSuccess(responseWriter http.ResponseWriter, request *http.Request, successText string) { 105 | responseWriter.Header().Set("Content-Type", "application/json") 106 | jsonSuccess := []byte(fmt.Sprintf("{\"success\":\"%s\"}", successText)) 107 | responseWriter.WriteHeader(http.StatusOK) 108 | _, err := responseWriter.Write(jsonSuccess) 109 | if err != nil { 110 | service.logger. 111 | WithField("source", "rest"). 112 | WithField("error", err.Error()). 113 | WithField("request", request.RequestURI). 114 | Error("Response error") 115 | } else { 116 | service.logger. 117 | WithField("source", "rest"). 118 | WithField("request", request.RequestURI). 119 | Trace("Response success") 120 | } 121 | } 122 | 123 | func (service *Service) getApplication(responseWriter http.ResponseWriter, request *http.Request) { 124 | responseWriter.Header().Set("Content-Type", "application/json") 125 | urlParameters := mux.Vars(request) 126 | applicationName, ok := urlParameters["name"] 127 | if !ok { 128 | service.httpError(responseWriter, request, http.StatusBadRequest, "Application name required") 129 | return 130 | } 131 | app, ok := service.store.Get(applicationName) 132 | if !ok { 133 | service.httpError(responseWriter, request, http.StatusNotFound, "application not found") 134 | return 135 | } 136 | jsonApp, err := json.Marshal(app) 137 | if err != nil { 138 | service.httpError(responseWriter, request, http.StatusBadRequest, err.Error()) 139 | return 140 | } 141 | _, err = responseWriter.Write(jsonApp) 142 | if err != nil { 143 | service.logger. 144 | WithField("source", "rest"). 145 | WithField("error", err.Error()). 146 | WithField("request", request.RequestURI). 147 | Error("Response error") 148 | } else { 149 | service.logger. 150 | WithField("source", "rest"). 151 | WithField("request", request.RequestURI). 152 | Trace("Response success") 153 | } 154 | } 155 | 156 | func (service *Service) listApplications(responseWriter http.ResponseWriter, request *http.Request) { 157 | responseWriter.Header().Set("Content-Type", "application/json") 158 | applications := service.store.List() 159 | jsonApplications, err := json.Marshal(applications) 160 | if err != nil { 161 | service.httpError(responseWriter, request, http.StatusBadRequest, err.Error()) 162 | return 163 | } 164 | if len(applications) == 0 { 165 | service.httpError(responseWriter, request, http.StatusNotFound, "no applications found") 166 | return 167 | } 168 | _, err = responseWriter.Write(jsonApplications) 169 | if err != nil { 170 | service.logger. 171 | WithField("source", "rest"). 172 | WithField("error", err.Error()). 173 | WithField("request", request.RequestURI). 174 | Error("Response error") 175 | } else { 176 | service.logger. 177 | WithField("source", "rest"). 178 | WithField("request", request.RequestURI). 179 | Trace("Response success") 180 | } 181 | } 182 | 183 | func (service *Service) createApplication(responseWriter http.ResponseWriter, request *http.Request) { 184 | responseWriter.Header().Set("Content-Type", "application/json") 185 | var app application.Application 186 | err := json.NewDecoder(request.Body).Decode(&app) 187 | if err != nil { 188 | service.httpError(responseWriter, request, http.StatusBadRequest, err.Error()) 189 | return 190 | } 191 | err = service.store.Add(app) 192 | if err != nil { 193 | service.httpError(responseWriter, request, 400, err.Error()) 194 | } else { 195 | service.proc.Append(process.New(&app, status.New(100), service.mainWaitGroup, service.config, service.logger)) 196 | service.httpSuccess(responseWriter, request, app.Name) 197 | } 198 | } 199 | 200 | func (service *Service) updateApplication(responseWriter http.ResponseWriter, request *http.Request) { 201 | responseWriter.Header().Set("Content-Type", "application/json") 202 | urlParameters := mux.Vars(request) 203 | applicationName, ok := urlParameters["name"] 204 | if !ok { 205 | service.httpError(responseWriter, request, http.StatusBadRequest, "Application name required") 206 | return 207 | } 208 | var app application.Application 209 | err := json.NewDecoder(request.Body).Decode(&app) 210 | if err != nil { 211 | service.httpError(responseWriter, request, http.StatusBadRequest, err.Error()) 212 | return 213 | } 214 | err = service.store.Update(applicationName, app) 215 | if err != nil { 216 | service.httpError(responseWriter, request, 404, err.Error()) 217 | } else { 218 | procStatus, _ := service.proc.Status(applicationName) 219 | updatedProc := process.New(&app, status.New(100), service.mainWaitGroup, service.config, service.logger) 220 | service.proc.Delete(applicationName) 221 | service.proc.Append(updatedProc) 222 | if procStatus.State == "Started" { 223 | updatedProc.Start() 224 | } 225 | service.httpSuccess(responseWriter, request, app.Name) 226 | } 227 | } 228 | 229 | func (service *Service) deleteApplication(responseWriter http.ResponseWriter, request *http.Request) { 230 | urlParameters := mux.Vars(request) 231 | applicationName, ok := urlParameters["name"] 232 | if !ok { 233 | service.httpError(responseWriter, request, http.StatusBadRequest, "application name required") 234 | return 235 | } 236 | err := service.store.Delete(applicationName) 237 | if err != nil { 238 | service.httpError(responseWriter, request, 404, err.Error()) 239 | } else { 240 | service.proc.Delete(applicationName) 241 | service.httpSuccess(responseWriter, request, applicationName) 242 | } 243 | } 244 | 245 | func (service *Service) startApplication(responseWriter http.ResponseWriter, request *http.Request) { 246 | responseWriter.Header().Set("Content-Type", "application/json") 247 | urlParameters := mux.Vars(request) 248 | applicationName, ok := urlParameters["name"] 249 | if !ok { 250 | service.httpError(responseWriter, request, http.StatusBadRequest, "application name required") 251 | return 252 | } 253 | app, ok := service.store.Get(applicationName) 254 | if !ok { 255 | service.httpError(responseWriter, request, http.StatusNotFound, "application not found") 256 | return 257 | } 258 | service.proc.Start(app.Name) 259 | service.httpSuccess(responseWriter, request, app.Name) 260 | } 261 | 262 | func (service *Service) stopApplication(responseWriter http.ResponseWriter, request *http.Request) { 263 | responseWriter.Header().Set("Content-Type", "application/json") 264 | urlParameters := mux.Vars(request) 265 | applicationName, ok := urlParameters["name"] 266 | if !ok { 267 | service.httpError(responseWriter, request, http.StatusBadRequest, "Application name required") 268 | return 269 | } 270 | app, ok := service.store.Get(applicationName) 271 | if !ok { 272 | service.httpError(responseWriter, request, http.StatusNotFound, "application not found") 273 | return 274 | } 275 | service.proc.Stop(app.Name) 276 | service.httpSuccess(responseWriter, request, app.Name) 277 | } 278 | 279 | func (service *Service) restartApplication(responseWriter http.ResponseWriter, request *http.Request) { 280 | responseWriter.Header().Set("Content-Type", "application/json") 281 | urlParameters := mux.Vars(request) 282 | applicationName, ok := urlParameters["name"] 283 | if !ok { 284 | service.httpError(responseWriter, request, http.StatusBadRequest, "Application name required") 285 | return 286 | } 287 | app, ok := service.store.Get(applicationName) 288 | if !ok { 289 | service.httpError(responseWriter, request, http.StatusNotFound, "application not found") 290 | return 291 | } 292 | service.proc.Restart(app.Name) 293 | service.httpSuccess(responseWriter, request, app.Name) 294 | } 295 | 296 | func (service *Service) startAll(responseWriter http.ResponseWriter, request *http.Request) { 297 | responseWriter.Header().Set("Content-Type", "application/json") 298 | applications := service.store.List() 299 | if len(applications) == 0 { 300 | service.httpError(responseWriter, request, http.StatusNotFound, "no applications found") 301 | return 302 | } 303 | service.proc.StartAll() 304 | service.httpSuccess(responseWriter, request, "all") 305 | } 306 | 307 | func (service *Service) stopAll(responseWriter http.ResponseWriter, request *http.Request) { 308 | responseWriter.Header().Set("Content-Type", "application/json") 309 | applications := service.store.List() 310 | if len(applications) == 0 { 311 | service.httpError(responseWriter, request, http.StatusNotFound, "no applications found") 312 | return 313 | } 314 | service.proc.StopAll() 315 | service.httpSuccess(responseWriter, request, "all") 316 | } 317 | 318 | func (service *Service) restartAll(responseWriter http.ResponseWriter, request *http.Request) { 319 | responseWriter.Header().Set("Content-Type", "application/json") 320 | applications := service.store.List() 321 | if len(applications) == 0 { 322 | service.httpError(responseWriter, request, http.StatusNotFound, "no applications found") 323 | return 324 | } 325 | service.proc.RestartAll() 326 | service.httpSuccess(responseWriter, request, "all") 327 | } 328 | 329 | func (service *Service) statusAll(responseWriter http.ResponseWriter, request *http.Request) { 330 | responseWriter.Header().Set("Content-Type", "application/json") 331 | allStatus := service.proc.StatusAll() 332 | jsonAllStatus, err := json.Marshal(allStatus) 333 | if err != nil { 334 | service.httpError(responseWriter, request, http.StatusBadRequest, err.Error()) 335 | return 336 | } 337 | if len(allStatus) == 0 { 338 | service.httpError(responseWriter, request, http.StatusNotFound, "status: no applications found") 339 | return 340 | } 341 | _, err = responseWriter.Write(jsonAllStatus) 342 | if err != nil { 343 | service.logger. 344 | WithField("source", "rest"). 345 | WithField("error", err.Error()). 346 | WithField("request", request.RequestURI). 347 | Error("Response error") 348 | } else { 349 | service.logger. 350 | WithField("source", "rest"). 351 | WithField("request", request.RequestURI). 352 | Trace("Response success") 353 | } 354 | } 355 | 356 | func (service *Service) status(responseWriter http.ResponseWriter, request *http.Request) { 357 | responseWriter.Header().Set("Content-Type", "application/json") 358 | urlParameters := mux.Vars(request) 359 | applicationName, ok := urlParameters["name"] 360 | if !ok { 361 | service.httpError(responseWriter, request, http.StatusBadRequest, "application name required") 362 | return 363 | } 364 | app, ok := service.store.Get(applicationName) 365 | if !ok { 366 | service.httpError(responseWriter, request, http.StatusNotFound, "application not found") 367 | return 368 | } 369 | appStatus, ok := service.proc.Status(app.Name) 370 | if !ok { 371 | service.httpError(responseWriter, request, http.StatusNotFound, "application not found") 372 | return 373 | } 374 | jsonAppStatus, err := json.Marshal(appStatus) 375 | if err != nil { 376 | service.httpError(responseWriter, request, http.StatusBadRequest, err.Error()) 377 | return 378 | } 379 | _, err = responseWriter.Write(jsonAppStatus) 380 | if err != nil { 381 | service.logger. 382 | WithField("source", "rest"). 383 | WithField("error", err.Error()). 384 | WithField("request", request.RequestURI). 385 | Error("Response error") 386 | } else { 387 | service.logger. 388 | WithField("source", "rest"). 389 | WithField("request", request.RequestURI). 390 | Trace("Response success") 391 | } 392 | } 393 | 394 | func (service *Service) getVersion(responseWriter http.ResponseWriter, request *http.Request) { 395 | jsonVersion := fmt.Sprintf("{\"version\": \"%s\"}", version.Version) 396 | responseWriter.Header().Set("Content-Type", "application/json") 397 | _, err := responseWriter.Write([]byte(jsonVersion)) 398 | if err != nil { 399 | service.logger. 400 | WithField("source", "rest"). 401 | WithField("error", err.Error()). 402 | WithField("request", request.RequestURI). 403 | Error("Response error") 404 | } else { 405 | service.logger. 406 | WithField("source", "rest"). 407 | WithField("request", request.RequestURI). 408 | Trace("Response success") 409 | } 410 | } 411 | 412 | func New(port int, store *application.Storage, proc *process.Manager, wg *sync.WaitGroup, config *configuration.Configuration, logger *logrus.Logger) *Service { 413 | return &Service{port: port, store: store, proc: proc, mainWaitGroup: wg, config: config, logger: logger} 414 | } 415 | -------------------------------------------------------------------------------- /process/process.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/sirupsen/logrus" 7 | "github.com/vvhq/exorsus/application" 8 | "github.com/vvhq/exorsus/configuration" 9 | "github.com/vvhq/exorsus/logging" 10 | "github.com/vvhq/exorsus/status" 11 | "io" 12 | "os" 13 | "os/exec" 14 | "os/user" 15 | "path" 16 | "path/filepath" 17 | "strconv" 18 | "strings" 19 | "sync" 20 | "syscall" 21 | "time" 22 | ) 23 | 24 | type Process struct { 25 | Name string 26 | app *application.Application 27 | status *status.Status 28 | command *exec.Cmd 29 | mainWaitGroup *sync.WaitGroup 30 | config *configuration.Configuration 31 | stdLogger *logrus.Logger 32 | logger *logrus.Logger 33 | } 34 | 35 | func (process *Process) Start() { 36 | process.mainWaitGroup.Add(1) 37 | go process.start() 38 | } 39 | 40 | func (process *Process) Stop() { 41 | process.mainWaitGroup.Add(1) 42 | go process.stop() 43 | } 44 | 45 | func (process *Process) Restart() { 46 | process.mainWaitGroup.Add(2) 47 | go func() { 48 | process.stop() 49 | process.start() 50 | }() 51 | } 52 | 53 | func (process *Process) GetPid() int { 54 | return process.status.GetPid() 55 | } 56 | 57 | func (process *Process) GetExitCode() int { 58 | return process.status.GetExitCode() 59 | } 60 | 61 | func (process *Process) GetError() error { 62 | return process.status.GetError() 63 | } 64 | 65 | func (process *Process) GetState() int { 66 | return process.status.GetState() 67 | } 68 | 69 | func (process *Process) GetStdOut() []string { 70 | return process.status.ListStdOutItems() 71 | } 72 | 73 | func (process *Process) GetStdErr() []string { 74 | return process.status.ListStdErrItems() 75 | } 76 | 77 | func (process *Process) Zombie() bool { 78 | _, err := os.Stat(fmt.Sprintf("/proc/%d", process.getCurrentPid())) 79 | return !os.IsNotExist(err) && process.status.GetState() == status.Failed 80 | } 81 | 82 | func (process *Process) getCurrentPid() int { 83 | currentPid := 0 84 | if process.command != nil && process.command.Process != nil { 85 | currentPid = process.command.Process.Pid 86 | } 87 | return currentPid 88 | } 89 | 90 | func (process *Process) pipe2Channel(pipe io.Reader, channel chan<- string) { 91 | process.mainWaitGroup.Add(1) 92 | go func() { 93 | defer process.mainWaitGroup.Done() 94 | buffer := make([]byte, 4096, 4096) 95 | for { 96 | numberBytesRead, err := pipe.Read(buffer) 97 | if numberBytesRead > 0 { 98 | bytesRead := buffer[:numberBytesRead] 99 | channel <- strings.TrimRight(string(bytesRead), "\n") 100 | } 101 | if err != nil { 102 | if err != io.EOF { 103 | process.logger. 104 | WithField("source", "process"). 105 | WithField("process", process.Name). 106 | WithField("state", process.GetState()). 107 | WithField("error", err.Error()). 108 | Error("Can not create pipe") 109 | } 110 | break 111 | } 112 | } 113 | }() 114 | } 115 | 116 | func (process *Process) stdOutChannelHandler(channel <-chan string) { 117 | process.mainWaitGroup.Add(1) 118 | go func() { 119 | defer process.mainWaitGroup.Done() 120 | for { 121 | item, out := <-channel 122 | if out { 123 | process.status.AddStdOutItem(item) 124 | process.stdLogger. 125 | WithField("source", "process"). 126 | WithField("process", process.Name). 127 | WithField("state", process.GetState()). 128 | WithField("item", item). 129 | Info("STDOUT message") 130 | } else { 131 | break 132 | } 133 | } 134 | }() 135 | } 136 | 137 | func (process *Process) stdErrChannelHandler(channel <-chan string) { 138 | process.mainWaitGroup.Add(1) 139 | go func() { 140 | defer process.mainWaitGroup.Done() 141 | for { 142 | item, out := <-channel 143 | if out { 144 | process.status.AddStdErrItem(item) 145 | process.stdLogger.WithField("SOURCE", "Process").WithField("NAME", process.Name).Error(item) 146 | } else { 147 | break 148 | } 149 | } 150 | }() 151 | } 152 | 153 | func (process *Process) preStart(stdOutChan chan<- string) { 154 | preTimeout := process.app.PreStart.Timeout 155 | if preTimeout == 0 { 156 | preTimeout = configuration.DefaultShutdownTimeout 157 | } 158 | preContext, preCancel := context.WithTimeout(context.Background(), time.Duration(preTimeout)*time.Second) 159 | defer preCancel() 160 | preArguments := strings.Fields(strings.TrimSpace(process.app.PreStart.Arguments)) 161 | preCommand := exec.CommandContext(preContext, process.app.PreStart.Command, preArguments...) 162 | preCommand.Dir = process.app.PreStart.WorkDir 163 | if process.findCredential() != nil { 164 | process.logger. 165 | WithField("source", "preprocess"). 166 | WithField("process", process.Name). 167 | WithField("state", process.GetState()). 168 | WithField("user", process.app.User). 169 | WithField("group", process.app.Group). 170 | Trace("Start pre process command as specific user/group") 171 | preCommand.SysProcAttr = &syscall.SysProcAttr{} 172 | preCommand.SysProcAttr.Credential = process.findCredential() 173 | } 174 | preCommand.Env = os.Environ() 175 | for _, env := range process.app.Environment { 176 | preCommand.Env = append(process.command.Env, fmt.Sprintf("%s=%s", env.Name, env.Value)) 177 | } 178 | process.logger. 179 | WithField("source", "preprocess"). 180 | WithField("path", preCommand.Path). 181 | WithField("dir", preCommand.Dir). 182 | WithField("args", preCommand.Args). 183 | Trace("About to start pre process command") 184 | preOut, preErr := preCommand.CombinedOutput() 185 | if preContext.Err() == context.DeadlineExceeded { 186 | process.logger. 187 | WithField("source", "preprocess"). 188 | WithField("process", process.Name). 189 | WithField("state", process.GetState()). 190 | WithField("error", preContext.Err().Error()). 191 | Error("Pre process command timed out") 192 | } 193 | if preErr != nil { 194 | process.logger. 195 | WithField("source", "preprocess"). 196 | WithField("process", process.Name). 197 | WithField("state", process.GetState()). 198 | WithField("error", preErr.Error()). 199 | Error("Pre process command exit with non zero exit code") 200 | } 201 | 202 | stdOutChan <- strings.Join(preCommand.Args, " ") 203 | stdOutChan <- strings.TrimRight(string(preOut), "\n") 204 | } 205 | 206 | func (process *Process) start() { 207 | defer process.mainWaitGroup.Done() 208 | if process.status.GetState() == status.Started { 209 | process.logger. 210 | WithField("source", "process"). 211 | WithField("process", process.Name). 212 | WithField("state", process.GetState()). 213 | WithField("operation", "start"). 214 | Warn("Process already started") 215 | return 216 | } 217 | if process.status.GetState() != status.Stopped { 218 | process.logger. 219 | WithField("source", "process"). 220 | WithField("process", process.Name). 221 | WithField("state", process.GetState()). 222 | WithField("operation", "start"). 223 | Warn("process busy") 224 | return 225 | } 226 | 227 | process.status.SetState(status.Starting) 228 | process.status.SetPid(process.getCurrentPid()) 229 | process.status.SetExitCode(0) 230 | process.status.SetError(nil) 231 | 232 | arguments := strings.Fields(strings.TrimSpace(process.app.Arguments)) 233 | 234 | process.command = exec.Command(process.app.Command, arguments...) 235 | process.command.Dir = process.app.WorkDir 236 | 237 | if process.findCredential() != nil { 238 | process.logger. 239 | WithField("source", "process"). 240 | WithField("process", process.Name). 241 | WithField("state", process.GetState()). 242 | WithField("user", process.app.User). 243 | WithField("group", process.app.Group). 244 | Trace("Start process as specific user/group") 245 | process.command.SysProcAttr = &syscall.SysProcAttr{} 246 | process.command.SysProcAttr.Credential = process.findCredential() 247 | } 248 | 249 | process.command.Env = os.Environ() 250 | for _, env := range process.app.Environment { 251 | process.command.Env = append(process.command.Env, fmt.Sprintf("%s=%s", env.Name, env.Value)) 252 | } 253 | 254 | stdOutChan := make(chan string, 4096) 255 | stdErrChan := make(chan string, 4096) 256 | stdOutPipe, err := process.command.StdoutPipe() 257 | if err != nil { 258 | process.logger. 259 | WithField("source", "process"). 260 | WithField("process", process.Name). 261 | WithField("state", process.GetState()). 262 | WithField("error", err.Error()). 263 | Error("Can not create stdout pipe") 264 | } else { 265 | process.pipe2Channel(stdOutPipe, stdOutChan) 266 | process.stdOutChannelHandler(stdOutChan) 267 | } 268 | stdErrPipe, err := process.command.StderrPipe() 269 | if err != nil { 270 | process.logger. 271 | WithField("source", "process"). 272 | WithField("process", process.Name). 273 | WithField("state", process.GetState()). 274 | WithField("error", err.Error()). 275 | Error("Can not create stderr pipe") 276 | } else { 277 | process.pipe2Channel(stdErrPipe, stdErrChan) 278 | process.stdErrChannelHandler(stdErrChan) 279 | } 280 | 281 | if process.app.PreStart.Command != "" { 282 | process.preStart(stdOutChan) 283 | } 284 | 285 | process.logger. 286 | WithField("source", "process"). 287 | WithField("path", process.command.Path). 288 | WithField("dir", process.command.Dir). 289 | WithField("args", process.command.Args). 290 | Trace("About to start command") 291 | 292 | err = process.command.Start() 293 | if err != nil { 294 | process.status.SetState(status.Stopped) 295 | process.status.SetPid(process.getCurrentPid()) 296 | process.status.SetError(err) 297 | process.status.SetExitCode(-1) 298 | close(stdOutChan) 299 | close(stdErrChan) 300 | process.logger. 301 | WithField("source", "process"). 302 | WithField("process", process.Name). 303 | WithField("state", process.GetState()). 304 | WithField("error", err.Error()). 305 | Error("Can not start process") 306 | return 307 | } 308 | process.status.SetState(status.Started) 309 | process.status.SetPid(process.getCurrentPid()) 310 | 311 | err = process.command.Wait() 312 | if err != nil { 313 | process.status.SetExitCode(-1) 314 | exitError, ok := err.(*exec.ExitError) 315 | if ok { 316 | exitStatus, ok := exitError.Sys().(syscall.WaitStatus) 317 | if ok { 318 | process.status.SetExitCode(exitStatus.ExitStatus()) 319 | } 320 | } 321 | process.status.SetError(err) 322 | process.logger. 323 | WithField("source", "process"). 324 | WithField("process", process.Name). 325 | WithField("state", process.GetState()). 326 | WithField("error", err.Error()). 327 | Error("Error waiting for process") 328 | } else { 329 | process.status.SetError(nil) 330 | process.status.SetExitCode(0) 331 | } 332 | close(stdOutChan) 333 | close(stdErrChan) 334 | process.status.SetState(status.Stopped) 335 | } 336 | 337 | func (process *Process) stop() { 338 | defer process.mainWaitGroup.Done() 339 | if process.status.GetState() == status.Stopped { 340 | process.logger. 341 | WithField("source", "process"). 342 | WithField("process", process.Name). 343 | WithField("state", process.GetState()). 344 | WithField("operation", "stop"). 345 | Trace("Process already stopped") 346 | return 347 | } 348 | if process.status.GetState() != status.Started { 349 | process.logger. 350 | WithField("source", "process"). 351 | WithField("process", process.Name). 352 | WithField("state", process.GetState()). 353 | WithField("operation", "stop"). 354 | Warn("Process busy") 355 | return 356 | } 357 | process.status.SetState(status.Stopping) 358 | process.status.SetError(nil) 359 | process.status.SetExitCode(-1) 360 | err := process.command.Process.Signal(syscall.SIGINT) 361 | if err != nil { 362 | process.status.SetExitCode(0) 363 | exitError, okErrCast := err.(*exec.ExitError) 364 | if okErrCast { 365 | exitStatus, okStatusCast := exitError.Sys().(syscall.WaitStatus) 366 | if okStatusCast { 367 | process.status.SetExitCode(exitStatus.ExitStatus()) 368 | } 369 | } 370 | process.status.SetPid(process.getCurrentPid()) 371 | process.status.SetError(err) 372 | process.logger. 373 | WithField("source", "process"). 374 | WithField("process", process.Name). 375 | WithField("state", process.GetState()). 376 | WithField("pid", fmt.Sprintf("%d", process.status.GetPid())). 377 | WithField("code", fmt.Sprintf("%d", process.status.GetExitCode())). 378 | WithField("error", err.Error()). 379 | Error("Can not gracefully stop process") 380 | } else { 381 | process.status.SetPid(process.getCurrentPid()) 382 | process.status.SetState(status.Stopped) 383 | process.status.SetError(nil) 384 | process.status.SetExitCode(0) 385 | process.logger. 386 | WithField("source", "process"). 387 | WithField("process", process.Name). 388 | WithField("state", process.GetState()). 389 | WithField("pid", fmt.Sprintf("%d", process.status.GetPid())). 390 | WithField("code", fmt.Sprintf("%d", process.status.GetExitCode())). 391 | Trace("Process stopped successfully") 392 | } 393 | time.Sleep(time.Second * time.Duration(process.app.Timeout+10)) 394 | if process.status.GetState() != status.Stopped { 395 | err := process.command.Process.Kill() 396 | if err != nil { 397 | process.status.SetExitCode(0) 398 | exitError, okErrCast := err.(*exec.ExitError) 399 | if okErrCast { 400 | exitStatus, okStatusCast := exitError.Sys().(syscall.WaitStatus) 401 | if okStatusCast { 402 | process.status.SetExitCode(exitStatus.ExitStatus()) 403 | } 404 | } 405 | process.status.SetPid(process.getCurrentPid()) 406 | process.status.SetError(err) 407 | process.logger. 408 | WithField("source", "process"). 409 | WithField("process", process.Name). 410 | WithField("state", process.GetState()). 411 | WithField("pid", fmt.Sprintf("%d", process.status.GetPid())). 412 | WithField("code", fmt.Sprintf("%d", process.status.GetExitCode())). 413 | WithField("error", err.Error()). 414 | Error("Can not KILL process") 415 | process.status.SetError(err) 416 | } else { 417 | process.status.SetPid(process.getCurrentPid()) 418 | process.status.SetState(status.Stopped) 419 | process.status.SetError(nil) 420 | process.status.SetExitCode(0) 421 | process.logger. 422 | WithField("source", "process"). 423 | WithField("process", process.Name). 424 | WithField("state", process.GetState()). 425 | WithField("pid", fmt.Sprintf("%d", process.status.GetPid())). 426 | WithField("code", fmt.Sprintf("%d", process.status.GetExitCode())). 427 | Trace("process stopped") 428 | } 429 | } 430 | if _, err := os.Stat(fmt.Sprintf("/proc/%d", process.getCurrentPid())); !os.IsNotExist(err) { 431 | process.status.SetState(status.Failed) 432 | process.status.SetExitCode(-1) 433 | process.logger. 434 | WithField("source", "process"). 435 | WithField("process", process.Name). 436 | WithField("state", process.GetState()). 437 | WithField("pid", fmt.Sprintf("%d", process.status.GetPid())). 438 | WithField("code", fmt.Sprintf("%d", process.status.GetExitCode())). 439 | WithField("error", err.Error()). 440 | Error("Process running detached") 441 | } else { 442 | process.logger. 443 | WithField("source", "process"). 444 | WithField("process", process.Name). 445 | WithField("state", process.GetState()). 446 | WithField("pid", fmt.Sprintf("%d", process.status.GetPid())). 447 | WithField("code", fmt.Sprintf("%d", process.status.GetExitCode())). 448 | Trace("Process does not exists") 449 | } 450 | } 451 | 452 | func (process *Process) findCredential() *syscall.Credential { 453 | uid := -1 454 | gid := -1 455 | if process.app.User != "" { 456 | lookupUser, err := user.Lookup(process.app.User) 457 | if err == nil { 458 | lookupUid, err := strconv.Atoi(lookupUser.Uid) 459 | if err == nil { 460 | uid = lookupUid 461 | } 462 | } 463 | } 464 | if process.app.Group != "" { 465 | lookupGroup, err := user.LookupGroup(process.app.Group) 466 | if err == nil { 467 | lookupGid, err := strconv.Atoi(lookupGroup.Gid) 468 | if err == nil { 469 | gid = lookupGid 470 | } 471 | } 472 | } 473 | if uid > 0 && gid > 0 { 474 | return &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)} 475 | } else { 476 | return nil 477 | } 478 | 479 | } 480 | 481 | func New(app *application.Application, status *status.Status, wg *sync.WaitGroup, config *configuration.Configuration, logger *logrus.Logger) *Process { 482 | logPath := path.Join(path.Dir(config.LogPath), fmt.Sprintf("app_%s.json", app.Name)) 483 | hostName, err := os.Hostname() 484 | if err == nil { 485 | logDirName, logFileName := filepath.Split(logPath) 486 | logPath = path.Join(logDirName, fmt.Sprintf("%s_%s", hostName, logFileName)) 487 | } 488 | logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) 489 | if err != nil { 490 | logFile = os.Stdout 491 | logger. 492 | WithField("source", "process"). 493 | WithField("process", app.Name). 494 | WithField("error", err.Error()). 495 | Error("Can not open log file") 496 | } 497 | stdLogger := logging.NewLogger(logFile, logrus.TraceLevel) 498 | stdLogger.SetFormatter(&logrus.JSONFormatter{}) 499 | return &Process{Name: app.Name, app: app, status: status, mainWaitGroup: wg, config: config, stdLogger: stdLogger, logger: logger} 500 | } 501 | 502 | type Status struct { 503 | Name string `json:"name"` 504 | Pid int `json:"pid"` 505 | Code int `json:"code"` 506 | StartupError string `json:"error"` 507 | State string `json:"state"` 508 | StdOut []string `json:"stdout"` 509 | StdErr []string `json:"stderr"` 510 | } 511 | 512 | func NewStatus(process *Process) Status { 513 | states := []string{"Stopped", "Started", "Stopping", "Starting", "Failed"} 514 | errorMessage := "" 515 | if process.GetError() != nil { 516 | errorMessage = process.GetError().Error() 517 | } 518 | procStatus := Status{ 519 | Name: process.Name, 520 | Pid: process.GetPid(), 521 | Code: process.GetExitCode(), 522 | StartupError: errorMessage, 523 | State: states[process.GetState()], 524 | StdOut: process.GetStdOut(), 525 | StdErr: process.GetStdErr()} 526 | return procStatus 527 | } 528 | 529 | type Manager struct { 530 | processes sync.Map 531 | mainWaitGroup *sync.WaitGroup 532 | logger *logrus.Logger 533 | } 534 | 535 | func (manager *Manager) Append(process *Process) { 536 | manager.processes.Store(process.Name, process) 537 | } 538 | 539 | func (manager *Manager) Delete(name string) { 540 | value, ok := manager.processes.Load(name) 541 | if ok { 542 | proc := value.(*Process) 543 | proc.Stop() 544 | manager.processes.Delete(name) 545 | } 546 | } 547 | 548 | func (manager *Manager) StartAll() { 549 | manager.processes.Range(func(key, value interface{}) bool { 550 | proc := value.(*Process) 551 | proc.Start() 552 | return true 553 | }) 554 | } 555 | 556 | func (manager *Manager) StopAll() { 557 | manager.processes.Range(func(key, value interface{}) bool { 558 | proc := value.(*Process) 559 | proc.Stop() 560 | return true 561 | }) 562 | } 563 | 564 | func (manager *Manager) RestartAll() { 565 | manager.processes.Range(func(key, value interface{}) bool { 566 | proc := value.(*Process) 567 | proc.Restart() 568 | return true 569 | }) 570 | } 571 | 572 | func (manager *Manager) Start(name string) { 573 | value, ok := manager.processes.Load(name) 574 | if ok { 575 | proc := value.(*Process) 576 | proc.Start() 577 | } 578 | } 579 | 580 | func (manager *Manager) Stop(name string) { 581 | value, ok := manager.processes.Load(name) 582 | if ok { 583 | proc := value.(*Process) 584 | proc.Stop() 585 | } 586 | } 587 | 588 | func (manager *Manager) Restart(name string) { 589 | value, ok := manager.processes.Load(name) 590 | if ok { 591 | proc := value.(*Process) 592 | proc.Restart() 593 | } 594 | } 595 | 596 | func (manager *Manager) List() []*Process { 597 | var processes []*Process 598 | manager.processes.Range(func(key, value interface{}) bool { 599 | processes = append(processes, value.(*Process)) 600 | return true 601 | }) 602 | return processes 603 | } 604 | 605 | func (manager *Manager) StatusAll() []Status { 606 | var allStatus []Status 607 | manager.processes.Range(func(key, value interface{}) bool { 608 | proc := value.(*Process) 609 | procStatus := NewStatus(proc) 610 | allStatus = append(allStatus, procStatus) 611 | return true 612 | }) 613 | return allStatus 614 | } 615 | 616 | func (manager *Manager) Status(name string) (Status, bool) { 617 | value, ok := manager.processes.Load(name) 618 | if ok { 619 | proc := value.(*Process) 620 | procStatus := NewStatus(proc) 621 | return procStatus, true 622 | } 623 | return Status{}, false 624 | } 625 | 626 | func NewManager(wg *sync.WaitGroup, logger *logrus.Logger) *Manager { 627 | return &Manager{mainWaitGroup: wg, logger: logger} 628 | } 629 | --------------------------------------------------------------------------------