├── LICENSE ├── README.md ├── apm.go ├── config.toml └── lib ├── cli └── cli.go ├── install └── install.go ├── master ├── master.go └── remote_master.go ├── preparable └── process.go ├── process ├── proc_container.go └── proc_status.go ├── utils ├── file_util.go └── filemutex.go └── watcher └── watcher.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Top Free Games 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
9 | # APM - Aguia Process Manager 10 | APM is a lightweight process manager written in Golang for Golang applications. It helps you keep your applications alive forever, reload and start them from the source code. 11 | 12 | [](http://goreportcard.com/badge/topfreegames/apm) 13 | [](https://godoc.org/github.com/topfreegames/apm) 14 | 15 | Starting an application is easy: 16 | ```bash 17 | $ ./apm bin app-name --source="github.com/topfreegames/apm" 18 | ``` 19 | 20 | This will basically compile your project source code and start it as a 21 | daemon in the background. The application will have already be 22 | downloaded into `GOPATH` issuing something like 23 | 24 | go get github.com/topfreegames/apm 25 | 26 | You will probably be able to run anything in any directory, as long as 27 | it is under `GOPATH` 28 | 29 | ## Install APM 30 | 31 | ```bash 32 | $ go get github.com/topfreegames/apm 33 | ``` 34 | 35 | ## Start APM 36 | 37 | In order to properly use APM, you always need to start a server. This will be changed in the next version, but in the meantime you need to run the command bellow to start using APM. 38 | ```bash 39 | $ apm serve 40 | ``` 41 | If no config file is provided, it will default to a folder '.apmenv' where `apm` is first started. 42 | 43 | ## Stop APM 44 | 45 | ```bash 46 | $ apm serve-stop 47 | ``` 48 | 49 | ## Starting a new application 50 | If it's the first time you are starting a new golang application, you need to tell APM to first build its binary. Then you need to first run: 51 | ```bash 52 | $ apm bin app-name --source="github.com/yourproject/project" 53 | ``` 54 | 55 | This will automatically compile, start and daemonize your application. If you need to later on, stop, restart or delete your app from APM, you can just run normal commands using the app-name you specified. Example: 56 | ```bash 57 | $ apm stop app-name 58 | $ apm restart app-name 59 | $ apm delete app-name 60 | ``` 61 | 62 | ## Main features 63 | 64 | ### Commands overview 65 | 66 | ```bash 67 | $ apm serve --config-file="config/file/path.toml" 68 | $ apm serve-stop --config-file="config/file/path.toml" 69 | 70 | $ apm bin app-name --source="github.com/topfreegames/apm" # Compile, start, daemonize and auto restart application. 71 | $ apm start app-name # Start, daemonize and auto restart application. 72 | $ apm restart app-name # Restart a previously saved process 73 | $ apm stop app-name # Stop application. 74 | $ apm delete app-name # Delete application forever. 75 | 76 | $ apm save # Save current process list 77 | $ apm resurrect # Restore previously saved processes 78 | 79 | $ apm status # Display status for each app. 80 | ``` 81 | 82 | ### Managing process via HTTP 83 | 84 | You can also use all of the above commands via HTTP requests. Just set the flag ```--dns``` together with ```./apm serve``` and then you can use a remote client to start, stop, delete and query status for each app. 85 | -------------------------------------------------------------------------------- /apm.go: -------------------------------------------------------------------------------- 1 | /* 2 | APM is a lightweight process manager written in Golang for Golang applications. It helps you keep all of your applications alive forever, if you want to. You can also reload, start, stop, delete and query status on the fly. 3 | 4 | APM also provide a way to start a process by compiling a Golang project source code. 5 | 6 | The main APM module is the Master module, it's the glue that keep everything running as it should be. 7 | 8 | If you need to use the remote version of APM, take a look at RemoteMaster on Master package. 9 | 10 | To use the remote version of APM, use: 11 | 12 | - remoteServer := master.StartRemoteMasterServer(dsn, configFile) 13 | 14 | It will start a remote master and return the instance. 15 | 16 | To make remote requests, use the Remote Client by instantiating using: 17 | 18 | - remoteClient, err := master.StartRemoteClient(dsn, timeout) 19 | 20 | It will start the remote client and return the instance so you can use to initiate requests, such as: 21 | 22 | - remoteClient.StartGoBin(sourcePath, name, keepAlive, args) 23 | */ 24 | package main 25 | 26 | import "github.com/kardianos/osext" 27 | import "gopkg.in/alecthomas/kingpin.v2" 28 | import "github.com/topfreegames/apm/lib/cli" 29 | import "github.com/topfreegames/apm/lib/master" 30 | 31 | import "github.com/sevlyar/go-daemon" 32 | 33 | import "path" 34 | import "path/filepath" 35 | import "syscall" 36 | import "os" 37 | import "os/signal" 38 | 39 | import log "github.com/Sirupsen/logrus" 40 | 41 | var ( 42 | app = kingpin.New("apm", "Aguia Process Manager.") 43 | dns = app.Flag("dns", "TCP Dns host.").Default(":9876").String() 44 | timeout = app.Flag("timeout", "Timeout to connect to client").Default("30s").Duration() 45 | 46 | serveStop = app.Command("serve-stop", "Stop APM server instance.") 47 | serveStopConfigFile = serveStop.Flag("config-file", "Config file location").String() 48 | 49 | serve = app.Command("serve", "Create APM server instance.") 50 | serveConfigFile = serve.Flag("config-file", "Config file location").String() 51 | 52 | resurrect = app.Command("resurrect", "Resurrect all previously save processes.") 53 | 54 | bin = app.Command("bin", "Create bin process.") 55 | binSourcePath = bin.Flag("source", "Go project source path. (Ex: github.com/topfreegames/apm)").Required().String() 56 | binName = bin.Arg("name", "Process name.").Required().String() 57 | binKeepAlive = bin.Flag("keep-alive", "Keep process alive forever.").Required().Bool() 58 | binArgs = bin.Flag("args", "External args.").Strings() 59 | 60 | restart = app.Command("restart", "Restart a process.") 61 | restartName = restart.Arg("name", "Process name.").Required().String() 62 | 63 | start = app.Command("start", "Start a process.") 64 | startName = start.Arg("name", "Process name.").Required().String() 65 | 66 | stop = app.Command("stop", "Stop a process.") 67 | stopName = stop.Arg("name", "Process name.").Required().String() 68 | 69 | delete = app.Command("delete", "Delete a process.") 70 | deleteName = delete.Arg("name", "Process name.").Required().String() 71 | 72 | save = app.Command("save", "Save a list of processes onto a file.") 73 | 74 | status = app.Command("status", "Get APM status.") 75 | ) 76 | 77 | func main() { 78 | switch kingpin.MustParse(app.Parse(os.Args[1:])) { 79 | case serveStop.FullCommand(): 80 | stopRemoteMasterServer() 81 | case serve.FullCommand(): 82 | startRemoteMasterServer() 83 | case resurrect.FullCommand(): 84 | cli := cli.InitCli(*dns, *timeout) 85 | cli.Resurrect() 86 | case bin.FullCommand(): 87 | cli := cli.InitCli(*dns, *timeout) 88 | cli.StartGoBin(*binSourcePath, *binName, *binKeepAlive, *binArgs) 89 | case restart.FullCommand(): 90 | cli := cli.InitCli(*dns, *timeout) 91 | cli.RestartProcess(*restartName) 92 | case start.FullCommand(): 93 | cli := cli.InitCli(*dns, *timeout) 94 | cli.StartProcess(*startName) 95 | case stop.FullCommand(): 96 | cli := cli.InitCli(*dns, *timeout) 97 | cli.StopProcess(*stopName) 98 | case delete.FullCommand(): 99 | cli := cli.InitCli(*dns, *timeout) 100 | cli.DeleteProcess(*deleteName) 101 | case save.FullCommand(): 102 | cli := cli.InitCli(*dns, *timeout) 103 | cli.Save() 104 | case status.FullCommand(): 105 | cli := cli.InitCli(*dns, *timeout) 106 | cli.Status() 107 | } 108 | } 109 | 110 | func isDaemonRunning(ctx *daemon.Context) (bool, *os.Process, error) { 111 | d, err := ctx.Search() 112 | 113 | if err != nil { 114 | return false, d, err 115 | } 116 | 117 | if err := d.Signal(syscall.Signal(0)); err != nil { 118 | return false, d, err 119 | } 120 | 121 | return true, d, nil 122 | } 123 | 124 | func startRemoteMasterServer() { 125 | if *serveConfigFile == "" { 126 | folderPath, err := osext.ExecutableFolder() 127 | if err != nil { 128 | log.Fatal(err) 129 | } 130 | *serveConfigFile = folderPath + "/.apmenv/config.toml" 131 | os.MkdirAll(path.Dir(*serveConfigFile), 0777) 132 | } 133 | ctx := &daemon.Context{ 134 | PidFileName: path.Join(filepath.Dir(*serveConfigFile), "main.pid"), 135 | PidFilePerm: 0644, 136 | LogFileName: path.Join(filepath.Dir(*serveConfigFile), "main.log"), 137 | LogFilePerm: 0640, 138 | WorkDir: "./", 139 | Umask: 027, 140 | } 141 | if ok, _, _ := isDaemonRunning(ctx); ok { 142 | log.Info("Server is already running.") 143 | return 144 | } 145 | 146 | log.Info("Starting daemon...") 147 | d, err := ctx.Reborn() 148 | if err != nil { 149 | log.Fatalf("Failed to reborn daemon due to %+v.", err) 150 | } 151 | 152 | if d != nil { 153 | return 154 | } 155 | 156 | defer ctx.Release() 157 | 158 | log.Info("Starting remote master server...") 159 | remoteMaster := master.StartRemoteMasterServer(*dns, *serveConfigFile) 160 | 161 | sigsKill := make(chan os.Signal, 1) 162 | signal.Notify(sigsKill, 163 | syscall.SIGINT, 164 | syscall.SIGTERM, 165 | syscall.SIGQUIT) 166 | 167 | <-sigsKill 168 | log.Info("Received signal to stop...") 169 | err = remoteMaster.Stop() 170 | if err != nil { 171 | log.Fatal(err) 172 | } 173 | os.Exit(0) 174 | } 175 | 176 | func stopRemoteMasterServer() { 177 | if *serveStopConfigFile == "" { 178 | folderPath, err := osext.ExecutableFolder() 179 | if err != nil { 180 | log.Fatal(err) 181 | } 182 | *serveStopConfigFile = folderPath + "/.apmenv/config.toml" 183 | os.MkdirAll(path.Dir(*serveStopConfigFile), 0777) 184 | } 185 | ctx := &daemon.Context{ 186 | PidFileName: path.Join(filepath.Dir(*serveStopConfigFile), "main.pid"), 187 | PidFilePerm: 0644, 188 | LogFileName: path.Join(filepath.Dir(*serveStopConfigFile), "main.log"), 189 | LogFilePerm: 0640, 190 | WorkDir: "./", 191 | Umask: 027, 192 | } 193 | 194 | if ok, p, _ := isDaemonRunning(ctx); ok { 195 | if err := p.Signal(syscall.Signal(syscall.SIGQUIT)); err != nil { 196 | log.Fatalf("Failed to kill daemon %v", err) 197 | } 198 | } else { 199 | ctx.Release() 200 | log.Info("instance is not running.") 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | SysFolder = "" 2 | PidFile = "" 3 | OutFile = "" 4 | ErrFile = "" 5 | 6 | [Watcher] 7 | 8 | [Procs] 9 | -------------------------------------------------------------------------------- /lib/cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import "github.com/topfreegames/apm/lib/master" 4 | 5 | import "math" 6 | import "log" 7 | import "time" 8 | import "fmt" 9 | 10 | // Cli is the command line client. 11 | type Cli struct { 12 | remoteClient *master.RemoteClient 13 | } 14 | 15 | // InitCli initiates a remote client connecting to dsn. 16 | // Returns a Cli instance. 17 | func InitCli(dsn string, timeout time.Duration) *Cli { 18 | client, err := master.StartRemoteClient(dsn, timeout) 19 | if err != nil { 20 | log.Fatalf("Failed to start remote client due to: %+v\n", err) 21 | } 22 | return &Cli{ 23 | remoteClient: client, 24 | } 25 | } 26 | 27 | // Save will save all previously saved processes onto a list. 28 | // Display an error in case there's any. 29 | func (cli *Cli) Save() { 30 | err := cli.remoteClient.Save() 31 | if err != nil { 32 | log.Fatalf("Failed to save list of processes due to: %+v\n", err) 33 | } 34 | } 35 | 36 | // Resurrect will restore all previously save processes. 37 | // Display an error in case there's any. 38 | func (cli *Cli) Resurrect() { 39 | err := cli.remoteClient.Resurrect() 40 | if err != nil { 41 | log.Fatalf("Failed to resurrect all previously save processes due to: %+v\n", err) 42 | } 43 | } 44 | // StartGoBin will try to start a go binary process. 45 | // Returns a fatal error in case there's any. 46 | func (cli *Cli) StartGoBin(sourcePath string, name string, keepAlive bool, args []string) { 47 | err := cli.remoteClient.StartGoBin(sourcePath, name, keepAlive, args) 48 | if err != nil { 49 | log.Fatalf("Failed to start go bin due to: %+v\n", err) 50 | } 51 | } 52 | 53 | // RestartProcess will try to restart a process with procName. Note that this process 54 | // must have been already started through StartGoBin. 55 | func (cli *Cli) RestartProcess(procName string) { 56 | err := cli.remoteClient.RestartProcess(procName) 57 | if err != nil { 58 | log.Fatalf("Failed to restart process due to: %+v\n", err) 59 | } 60 | } 61 | 62 | // StartProcess will try to start a process with procName. Note that this process 63 | // must have been already started through StartGoBin. 64 | func (cli *Cli) StartProcess(procName string) { 65 | err := cli.remoteClient.StartProcess(procName) 66 | if err != nil { 67 | log.Fatalf("Failed to start process due to: %+v\n", err) 68 | } 69 | } 70 | 71 | // StopProcess will try to stop a process named procName. 72 | func (cli *Cli) StopProcess(procName string) { 73 | err := cli.remoteClient.StopProcess(procName) 74 | if err != nil { 75 | log.Fatalf("Failed to stop process due to: %+v\n", err) 76 | } 77 | } 78 | 79 | // DeleteProcess will stop and delete all dependencies from process procName forever. 80 | func (cli *Cli) DeleteProcess(procName string) { 81 | err := cli.remoteClient.DeleteProcess(procName) 82 | if err != nil { 83 | log.Fatalf("Failed to delete process due to: %+v\n", err) 84 | } 85 | } 86 | 87 | // Status will display the status of all procs started through StartGoBin. 88 | func (cli *Cli) Status() { 89 | procResponse, err := cli.remoteClient.MonitStatus() 90 | if err != nil { 91 | log.Fatalf("Failed to get status due to: %+v\n", err) 92 | } 93 | maxName := 0 94 | for id := range procResponse.Procs { 95 | proc := procResponse.Procs[id] 96 | maxName = int(math.Max(float64(maxName), float64(len(proc.Name)))) 97 | } 98 | totalSize := maxName + 51; 99 | topBar := "" 100 | for i := 1; i <= totalSize; i += 1 { 101 | topBar += "-" 102 | } 103 | infoBar := fmt.Sprintf("|%s|%s|%s|%s|", 104 | PadString("pid", 13), 105 | PadString("name", maxName + 2), 106 | PadString("status", 16), 107 | PadString("keep-alive", 15)) 108 | fmt.Println(topBar) 109 | fmt.Println(infoBar) 110 | for id := range procResponse.Procs { 111 | proc := procResponse.Procs[id] 112 | kp := "True" 113 | if !proc.KeepAlive { 114 | kp = "False" 115 | } 116 | fmt.Printf("|%s|%s|%s|%s|\n", 117 | PadString(fmt.Sprintf("%d", proc.Pid), 13), 118 | PadString(proc.Name, maxName + 2), 119 | PadString(proc.Status.Status, 16), 120 | PadString(kp, 15)) 121 | } 122 | fmt.Println(topBar) 123 | } 124 | 125 | // PadString will add totalSize spaces evenly to the right and left side of str. 126 | // Returns str after applying the pad. 127 | func PadString(str string, totalSize int) string { 128 | turn := 0 129 | for { 130 | if len(str) >= totalSize { 131 | break 132 | } 133 | if turn == 0 { 134 | str = " " + str 135 | turn ^= 1 136 | } else { 137 | str = str + " " 138 | turn ^= 1 139 | } 140 | } 141 | return str 142 | } 143 | -------------------------------------------------------------------------------- /lib/install/install.go: -------------------------------------------------------------------------------- 1 | package install 2 | -------------------------------------------------------------------------------- /lib/master/master.go: -------------------------------------------------------------------------------- 1 | /* 2 | Master package is the main package that keeps everything running as it should be. It's responsible for starting, stopping and deleting processes. It also will keep an eye on the Watcher in case a process dies so it can restart it again. 3 | 4 | RemoteMaster is responsible for exporting the main APM operations as HTTP requests. If you want to start a Remote Server, run: 5 | 6 | - remoteServer := master.StartRemoteMasterServer(dsn, configFile) 7 | 8 | It will start a remote master and return the instance. 9 | 10 | To make remote requests, use the Remote Client by instantiating using: 11 | 12 | - remoteClient, err := master.StartRemoteClient(dsn, timeout) 13 | 14 | It will start the remote client and return the instance so you can use to initiate requests, such as: 15 | 16 | - remoteClient.StartGoBin(sourcePath, name, keepAlive, args) 17 | */ 18 | 19 | package master 20 | 21 | import "os" 22 | import "path" 23 | import "errors" 24 | import "fmt" 25 | import "sync" 26 | 27 | import "time" 28 | 29 | import "github.com/topfreegames/apm/lib/preparable" 30 | import "github.com/topfreegames/apm/lib/process" 31 | import "github.com/topfreegames/apm/lib/utils" 32 | import "github.com/topfreegames/apm/lib/watcher" 33 | 34 | import log "github.com/Sirupsen/logrus" 35 | 36 | // Master is the main module that keeps everything in place and execute 37 | // the necessary actions to keep the process running as they should be. 38 | type Master struct { 39 | sync.Mutex 40 | 41 | SysFolder string // SysFolder is the main APM folder where the necessary config files will be stored. 42 | PidFile string // PidFille is the APM pid file path. 43 | OutFile string // OutFile is the APM output log file path. 44 | ErrFile string // ErrFile is the APM err log file path. 45 | Watcher *watcher.Watcher // Watcher is a watcher instance. 46 | 47 | Procs map[string]process.ProcContainer // Procs is a map containing all procs started on APM. 48 | } 49 | 50 | // DecodableMaster is a struct that the config toml file will decode to. 51 | // It is needed because toml decoder doesn't decode to interfaces, so the 52 | // Procs map can't be decoded as long as we use the ProcContainer interface 53 | type DecodableMaster struct { 54 | SysFolder string 55 | PidFile string 56 | OutFile string 57 | ErrFile string 58 | 59 | Watcher *watcher.Watcher 60 | 61 | Procs map[string]*process.Proc 62 | } 63 | 64 | // InitMaster will start a master instance with configFile. 65 | // It returns a Master instance. 66 | func InitMaster(configFile string) *Master { 67 | watcher := watcher.InitWatcher() 68 | decodableMaster := &DecodableMaster{} 69 | decodableMaster.Procs = make(map[string]*process.Proc) 70 | 71 | err := utils.SafeReadTomlFile(configFile, decodableMaster) 72 | if err != nil { 73 | panic(err) 74 | } 75 | 76 | procs := make(map[string]process.ProcContainer) 77 | for k, v := range decodableMaster.Procs { 78 | procs[k] = v; 79 | } 80 | // We need this hack because toml decoder doesn't decode to interfaces 81 | master := &Master { 82 | SysFolder: decodableMaster.SysFolder, 83 | PidFile: decodableMaster.PidFile, 84 | OutFile: decodableMaster.OutFile, 85 | ErrFile: decodableMaster.ErrFile, 86 | Watcher: decodableMaster.Watcher, 87 | Procs: procs, 88 | } 89 | 90 | if master.SysFolder == "" { 91 | os.MkdirAll(path.Dir(configFile), 0777) 92 | master.SysFolder = path.Dir(configFile) + "/" 93 | } 94 | master.Watcher = watcher 95 | master.Revive() 96 | log.Infof("All procs revived...") 97 | go master.WatchProcs() 98 | go master.SaveProcsLoop() 99 | go master.UpdateStatus() 100 | return master 101 | } 102 | 103 | // WatchProcs will keep the procs running forever. 104 | func (master *Master) WatchProcs() { 105 | for proc := range master.Watcher.RestartProc() { 106 | if !proc.ShouldKeepAlive() { 107 | master.Lock() 108 | master.updateStatus(proc) 109 | master.Unlock() 110 | log.Infof("Proc %s does not have keep alive set. Will not be restarted.", proc.Identifier()) 111 | continue 112 | } 113 | log.Infof("Restarting proc %s.", proc.Identifier()) 114 | if proc.IsAlive() { 115 | log.Warnf("Proc %s was supposed to be dead, but it is alive.", proc.Identifier()) 116 | } 117 | master.Lock() 118 | proc.AddRestart() 119 | err := master.restart(proc) 120 | master.Unlock() 121 | if err != nil { 122 | log.Warnf("Could not restart process %s due to %s.", proc.Identifier(), err) 123 | } 124 | } 125 | } 126 | 127 | // Prepare will compile the source code into a binary and return a preparable 128 | // ready to be executed. 129 | func (master *Master) Prepare(sourcePath string, name string, language string, keepAlive bool, args []string) (preparable.ProcPreparable, []byte, error) { 130 | procPreparable := &preparable.Preparable{ 131 | Name: name, 132 | SourcePath: sourcePath, 133 | SysFolder: master.SysFolder, 134 | Language: language, 135 | KeepAlive: keepAlive, 136 | Args: args, 137 | } 138 | output, err := procPreparable.PrepareBin() 139 | return procPreparable, output, err 140 | } 141 | 142 | // RunPreparable will run procPreparable and add it to the watch list in case everything goes well. 143 | func (master *Master) RunPreparable(procPreparable preparable.ProcPreparable) error { 144 | master.Lock() 145 | defer master.Unlock() 146 | if _, ok := master.Procs[procPreparable.Identifier()]; ok { 147 | log.Warnf("Proc %s already exist.", procPreparable.Identifier()) 148 | return errors.New("Trying to start a process that already exist.") 149 | } 150 | proc, err := procPreparable.Start() 151 | if err != nil { 152 | return err 153 | } 154 | master.Procs[proc.Identifier()] = proc 155 | master.saveProcsWrapper() 156 | master.Watcher.AddProcWatcher(proc) 157 | proc.SetStatus("running") 158 | return nil 159 | } 160 | 161 | // ListProcs will return a list of all procs. 162 | func (master *Master) ListProcs() []process.ProcContainer { 163 | procsList := []process.ProcContainer{} 164 | for _, v := range master.Procs { 165 | procsList = append(procsList, v) 166 | } 167 | return procsList 168 | } 169 | 170 | // RestartProcess will restart a process. 171 | func (master *Master) RestartProcess(name string) error { 172 | err := master.StopProcess(name) 173 | if err != nil { 174 | return err 175 | } 176 | return master.StartProcess(name) 177 | } 178 | 179 | // StartProcess will a start a process. 180 | func (master *Master) StartProcess(name string) error { 181 | master.Lock() 182 | defer master.Unlock() 183 | if proc, ok := master.Procs[name]; ok { 184 | return master.start(proc) 185 | } 186 | return errors.New("Unknown process.") 187 | } 188 | 189 | // StopProcess will stop a process with the given name. 190 | func (master *Master) StopProcess(name string) error { 191 | master.Lock() 192 | defer master.Unlock() 193 | if proc, ok := master.Procs[name]; ok { 194 | return master.stop(proc) 195 | } 196 | return errors.New("Unknown process.") 197 | } 198 | 199 | // DeleteProcess will delete a process and all its files and childs forever. 200 | func (master *Master) DeleteProcess(name string) error { 201 | master.Lock() 202 | defer master.Unlock() 203 | log.Infof("Trying to delete proc %s", name) 204 | if proc, ok := master.Procs[name]; ok { 205 | err := master.stop(proc) 206 | if err != nil { 207 | return err 208 | } 209 | delete(master.Procs, name) 210 | err = master.delete(proc) 211 | if err != nil { 212 | return err 213 | } 214 | log.Infof("Successfully deleted proc %s", name) 215 | } 216 | return nil 217 | } 218 | 219 | // Revive will revive all procs listed on ListProcs. This should ONLY be called 220 | // during Master startup. 221 | func (master *Master) Revive() error { 222 | master.Lock() 223 | defer master.Unlock() 224 | procs := master.ListProcs() 225 | log.Info("Reviving all processes") 226 | for id := range procs { 227 | proc := procs[id] 228 | if !proc.ShouldKeepAlive() { 229 | log.Infof("Proc %s does not have KeepAlive set. Will not revive it.", proc.Identifier()) 230 | continue 231 | } 232 | log.Infof("Reviving proc %s", proc.Identifier()) 233 | err := master.start(proc) 234 | if err != nil { 235 | return fmt.Errorf("Failed to revive proc %s due to %s", proc.Identifier(), err) 236 | } 237 | } 238 | return nil 239 | } 240 | 241 | // NOT thread safe method. Lock should be acquire before calling it. 242 | func (master *Master) start(proc process.ProcContainer) error { 243 | if !proc.IsAlive() { 244 | err := proc.Start() 245 | if err != nil { 246 | return err 247 | } 248 | master.Watcher.AddProcWatcher(proc) 249 | proc.SetStatus("running") 250 | } 251 | return nil 252 | } 253 | 254 | func (master *Master) delete(proc process.ProcContainer) error { 255 | return proc.Delete() 256 | } 257 | 258 | // NOT thread safe method. Lock should be acquire before calling it. 259 | func (master *Master) stop(proc process.ProcContainer) error { 260 | if proc.IsAlive() { 261 | waitStop := master.Watcher.StopWatcher(proc.Identifier()) 262 | err := proc.GracefullyStop() 263 | if err != nil { 264 | return err 265 | } 266 | if waitStop != nil { 267 | <-waitStop 268 | proc.NotifyStopped() 269 | proc.SetStatus("stopped") 270 | } 271 | log.Infof("Proc %s successfully stopped.", proc.Identifier()) 272 | } 273 | return nil 274 | } 275 | 276 | // UpdateStatus will update a process status every 30s. 277 | func (master *Master) UpdateStatus() { 278 | for { 279 | master.Lock() 280 | procs := master.ListProcs() 281 | for id := range procs { 282 | proc := procs[id] 283 | master.updateStatus(proc) 284 | } 285 | master.Unlock() 286 | time.Sleep(30 * time.Second) 287 | } 288 | } 289 | 290 | func (master *Master) updateStatus(proc process.ProcContainer) { 291 | if proc.IsAlive() { 292 | proc.SetStatus("running") 293 | } else { 294 | proc.NotifyStopped() 295 | proc.SetStatus("stopped") 296 | } 297 | } 298 | 299 | // NOT thread safe method. Lock should be acquire before calling it. 300 | func (master *Master) restart(proc process.ProcContainer) error { 301 | err := master.stop(proc) 302 | if err != nil { 303 | return err 304 | } 305 | return master.start(proc) 306 | } 307 | 308 | // SaveProcsLoop will loop forever to save the list of procs onto the proc file. 309 | func (master *Master) SaveProcsLoop() { 310 | for { 311 | log.Infof("Saving list of procs.") 312 | master.Lock() 313 | master.saveProcsWrapper() 314 | master.Unlock() 315 | time.Sleep(5 * time.Minute) 316 | } 317 | } 318 | 319 | // Stop will stop APM and all of its running procs. 320 | func (master *Master) Stop() error { 321 | log.Info("Stopping APM...") 322 | procs := master.ListProcs() 323 | for id := range procs { 324 | proc := procs[id] 325 | log.Info("Stopping proc %s", proc.Identifier()) 326 | master.stop(proc) 327 | } 328 | log.Info("Saving and returning list of procs.") 329 | return master.saveProcsWrapper() 330 | } 331 | 332 | // SaveProcs will save a list of procs onto a file inside configPath. 333 | // Returns an error in case there's any. 334 | func (master *Master) SaveProcs() error { 335 | master.Lock() 336 | defer master.Unlock() 337 | return master.saveProcsWrapper() 338 | } 339 | 340 | // NOT Thread Safe. Lock should be acquired before calling it. 341 | func (master *Master) saveProcsWrapper() error { 342 | configPath := master.getConfigPath() 343 | return utils.SafeWriteTomlFile(master, configPath) 344 | } 345 | 346 | func (master *Master) getConfigPath() string { 347 | return path.Join(master.SysFolder, "config.toml") 348 | } 349 | -------------------------------------------------------------------------------- /lib/master/remote_master.go: -------------------------------------------------------------------------------- 1 | package master 2 | 3 | import "net" 4 | import "net/rpc" 5 | import "log" 6 | import "time" 7 | import "fmt" 8 | 9 | import "github.com/topfreegames/apm/lib/process" 10 | 11 | // RemoteMaster is a struct that holds the master instance. 12 | type RemoteMaster struct { 13 | master *Master // Master instance 14 | } 15 | 16 | // RemoteClient is a struct that holds the remote client instance. 17 | type RemoteClient struct { 18 | conn *rpc.Client // RpcConnection for the remote client. 19 | } 20 | 21 | // GoBin is a struct that represents the necessary arguments for a go binary to be built. 22 | type GoBin struct { 23 | SourcePath string // SourcePath is the package path. (Ex: github.com/topfreegames/apm) 24 | Name string // Name is the process name that will be given to the process. 25 | KeepAlive bool // KeepAlive will determine whether APM should keep the proc live or not. 26 | Args []string // Args is an array containing all the extra args that will be passed to the binary after compilation. 27 | } 28 | 29 | type ProcDataResponse struct { 30 | Name string 31 | Pid int 32 | Status *process.ProcStatus 33 | KeepAlive bool 34 | } 35 | 36 | type ProcResponse struct { 37 | Procs []*ProcDataResponse 38 | } 39 | // Save will save the current running and stopped processes onto a file. 40 | // Returns an error in case there's any. 41 | func (remote_master *RemoteMaster) Save(req string, ack *bool) error { 42 | req = "" 43 | *ack = true 44 | return remote_master.master.SaveProcs() 45 | } 46 | 47 | // Resurrect will restore all previously save processes. 48 | // Returns an error in case there's any. 49 | func (remote_master *RemoteMaster) Resurrect(req string, ack *bool) error { 50 | req = "" 51 | *ack = true 52 | return remote_master.master.Revive() 53 | } 54 | 55 | // StartGoBin will build a binary based on the arguments passed on goBin, then it will start the process 56 | // and keep it alive if KeepAlive is set to true. 57 | // It returns an error and binds true to ack pointer. 58 | func (remote_master *RemoteMaster) StartGoBin(goBin *GoBin, ack *bool) error { 59 | preparable, output, err := remote_master.master.Prepare(goBin.SourcePath, goBin.Name, "go", goBin.KeepAlive, goBin.Args) 60 | *ack = true 61 | if err != nil { 62 | return fmt.Errorf("ERROR: %s OUTPUT: %s", err, string(output)) 63 | } 64 | return remote_master.master.RunPreparable(preparable) 65 | } 66 | 67 | // RestartProcess will restart a process that was previously built using GoBin. 68 | // It returns an error in case there's any. 69 | func (remote_master *RemoteMaster) RestartProcess(procName string, ack *bool) error { 70 | *ack = true 71 | return remote_master.master.RestartProcess(procName) 72 | } 73 | 74 | // StartProcess will start a process that was previously built using GoBin. 75 | // It returns an error in case there's any. 76 | func (remote_master *RemoteMaster) StartProcess(procName string, ack *bool) error { 77 | *ack = true 78 | return remote_master.master.StartProcess(procName) 79 | } 80 | 81 | // StopProcess will stop a process that is currently running. 82 | // It returns an error in case there's any. 83 | func (remote_master *RemoteMaster) StopProcess(procName string, ack *bool) error { 84 | *ack = true 85 | return remote_master.master.StopProcess(procName) 86 | } 87 | 88 | // MonitStatus will query for the status of each process and bind it to procs pointer list. 89 | // It returns an error in case there's any. 90 | func (remote_master *RemoteMaster) MonitStatus(req string, response *ProcResponse) error { 91 | req = "" 92 | procs := remote_master.master.ListProcs() 93 | procsResponse := []*ProcDataResponse{} 94 | for id := range procs { 95 | proc := procs[id] 96 | procData := &ProcDataResponse { 97 | Name: proc.Identifier(), 98 | Pid: proc.GetPid(), 99 | Status: proc.GetStatus(), 100 | KeepAlive: proc.ShouldKeepAlive(), 101 | } 102 | procsResponse = append(procsResponse, procData) 103 | } 104 | *response = ProcResponse { 105 | Procs: procsResponse, 106 | } 107 | return nil 108 | } 109 | 110 | // DeleteProcess will delete a process with name procName. 111 | // It returns an error in case there's any. 112 | func (remote_master *RemoteMaster) DeleteProcess(procName string, ack *bool) error { 113 | *ack = true 114 | return remote_master.master.DeleteProcess(procName) 115 | } 116 | 117 | // Stop will stop APM remote server. 118 | // It returns an error in case there's any. 119 | func (remote_master *RemoteMaster) Stop() error { 120 | return remote_master.master.Stop() 121 | } 122 | 123 | // StartRemoteMasterServer starts a remote APM server listening on dsn address and binding to 124 | // configFile. 125 | // It returns a RemoteMaster instance. 126 | func StartRemoteMasterServer(dsn string, configFile string) *RemoteMaster { 127 | remoteMaster := &RemoteMaster{ 128 | master: InitMaster(configFile), 129 | } 130 | rpc.Register(remoteMaster) 131 | l, e := net.Listen("tcp", dsn) 132 | if e != nil { 133 | log.Fatal("listen error: ", e) 134 | } 135 | go rpc.Accept(l) 136 | return remoteMaster 137 | } 138 | 139 | // StartRemoteClient will start a remote client that can talk to a remote server that 140 | // is already running on dsn address. 141 | // It returns an error in case there's any or it could not connect within the timeout. 142 | func StartRemoteClient(dsn string, timeout time.Duration) (*RemoteClient, error) { 143 | conn, err := net.DialTimeout("tcp", dsn, timeout) 144 | if err != nil { 145 | return nil, err 146 | } 147 | return &RemoteClient{conn: rpc.NewClient(conn)}, nil 148 | } 149 | 150 | // Save will save a list of procs onto a file. 151 | // Returns an error in case there's any. 152 | func (client *RemoteClient) Save() error { 153 | var started bool 154 | return client.conn.Call("RemoteMaster.Save", "", &started) 155 | } 156 | 157 | // Resurrect will restore all previously save processes. 158 | // Returns an error in case there's any. 159 | func (client *RemoteClient) Resurrect() error { 160 | var started bool 161 | return client.conn.Call("RemoteMaster.Resurrect", "", &started) 162 | } 163 | 164 | // StartGoBin is a wrapper that calls the remote StartsGoBin. 165 | // It returns an error in case there's any. 166 | func (client *RemoteClient) StartGoBin(sourcePath string, name string, keepAlive bool, args []string) error { 167 | goBin := &GoBin{ 168 | SourcePath: sourcePath, 169 | Name: name, 170 | KeepAlive: keepAlive, 171 | Args: args, 172 | } 173 | var started bool 174 | return client.conn.Call("RemoteMaster.StartGoBin", goBin, &started) 175 | } 176 | 177 | // RestartProcess is a wrapper that calls the remote RestartProcess. 178 | // It returns an error in case there's any. 179 | func (client *RemoteClient) RestartProcess(procName string) error { 180 | var started bool 181 | return client.conn.Call("RemoteMaster.RestartProcess", procName, &started) 182 | } 183 | 184 | // StartProcess is a wrapper that calls the remote StartProcess. 185 | // It returns an error in case there's any. 186 | func (client *RemoteClient) StartProcess(procName string) error { 187 | var started bool 188 | return client.conn.Call("RemoteMaster.StartProcess", procName, &started) 189 | } 190 | 191 | // StopProcess is a wrapper that calls the remote StopProcess. 192 | // It returns an error in case there's any. 193 | func (client *RemoteClient) StopProcess(procName string) error { 194 | var stopped bool 195 | return client.conn.Call("RemoteMaster.StopProcess", procName, &stopped) 196 | } 197 | 198 | // DeleteProcess is a wrapper that calls the remote DeleteProcess. 199 | // It returns an error in case there's any. 200 | func (client *RemoteClient) DeleteProcess(procName string) error { 201 | var deleted bool 202 | return client.conn.Call("RemoteMaster.DeleteProcess", procName, &deleted) 203 | } 204 | 205 | // MonitStatus is a wrapper that calls the remote MonitStatus. 206 | // It returns a tuple with a list of process and an error in case there's any. 207 | func (client *RemoteClient) MonitStatus() (ProcResponse, error) { 208 | var response *ProcResponse 209 | err := client.conn.Call("RemoteMaster.MonitStatus", "", &response) 210 | return *response, err 211 | } 212 | -------------------------------------------------------------------------------- /lib/preparable/process.go: -------------------------------------------------------------------------------- 1 | package preparable 2 | 3 | import "os/exec" 4 | import "strings" 5 | 6 | import "github.com/topfreegames/apm/lib/process" 7 | 8 | type ProcPreparable interface { 9 | PrepareBin() ([]byte, error) 10 | Start() (process.ProcContainer, error) 11 | getPath() string 12 | Identifier() string 13 | getBinPath() string 14 | getPidPath() string 15 | getOutPath() string 16 | getErrPath() string 17 | } 18 | // ProcPreparable is a preparable with all the necessary informations to run 19 | // a process. To actually run a process, call the Start() method. 20 | type Preparable struct { 21 | Name string 22 | SourcePath string 23 | Cmd string 24 | SysFolder string 25 | Language string 26 | KeepAlive bool 27 | Args []string 28 | } 29 | 30 | // PrepareBin will compile the Golang project from SourcePath and populate Cmd with the proper 31 | // command for the process to be executed. 32 | // Returns the compile command output. 33 | func (preparable *Preparable) PrepareBin() ([]byte, error) { 34 | // Remove the last character '/' if present 35 | if preparable.SourcePath[len(preparable.SourcePath)-1] == '/' { 36 | preparable.SourcePath = strings.TrimSuffix(preparable.SourcePath, "/") 37 | } 38 | cmd := "" 39 | cmdArgs := []string{} 40 | binPath := preparable.getBinPath() 41 | if preparable.Language == "go" { 42 | cmd = "go" 43 | cmdArgs = []string{"build", "-o", binPath, preparable.SourcePath + "/."} 44 | } 45 | 46 | preparable.Cmd = preparable.getBinPath() 47 | return exec.Command(cmd, cmdArgs...).Output() 48 | } 49 | 50 | // Start will execute the process based on the information presented on the preparable. 51 | // This function should be called from inside the master to make sure 52 | // all the watchers and process handling are done correctly. 53 | // Returns a tuple with the process and an error in case there's any. 54 | func (preparable *Preparable) Start() (process.ProcContainer, error) { 55 | proc := &process.Proc{ 56 | Name: preparable.Name, 57 | Cmd: preparable.Cmd, 58 | Args: preparable.Args, 59 | Path: preparable.getPath(), 60 | Pidfile: preparable.getPidPath(), 61 | Outfile: preparable.getOutPath(), 62 | Errfile: preparable.getErrPath(), 63 | KeepAlive: preparable.KeepAlive, 64 | Status: &process.ProcStatus{}, 65 | } 66 | 67 | err := proc.Start() 68 | return proc, err 69 | } 70 | 71 | func (preparable *Preparable) Identifier() string { 72 | return preparable.Name; 73 | } 74 | 75 | func (preparable *Preparable) getPath() string { 76 | if preparable.SysFolder[len(preparable.SysFolder)-1] == '/' { 77 | preparable.SysFolder = strings.TrimSuffix(preparable.SysFolder, "/") 78 | } 79 | return preparable.SysFolder + "/" + preparable.Name 80 | } 81 | 82 | func (preparable *Preparable) getBinPath() string { 83 | return preparable.getPath() + "/" + preparable.Name 84 | } 85 | 86 | func (preparable *Preparable) getPidPath() string { 87 | return preparable.getBinPath() + ".pid" 88 | } 89 | 90 | func (preparable *Preparable) getOutPath() string { 91 | return preparable.getBinPath() + ".out" 92 | } 93 | 94 | func (preparable *Preparable) getErrPath() string { 95 | return preparable.getBinPath() + ".err" 96 | } 97 | -------------------------------------------------------------------------------- /lib/process/proc_container.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import "os" 4 | import "syscall" 5 | import "errors" 6 | import "strconv" 7 | 8 | import "github.com/topfreegames/apm/lib/utils" 9 | 10 | type ProcContainer interface { 11 | Start() error 12 | ForceStop() error 13 | GracefullyStop() error 14 | Restart() error 15 | Delete() error 16 | IsAlive() bool 17 | Identifier() string 18 | ShouldKeepAlive() bool 19 | AddRestart() 20 | NotifyStopped() 21 | SetStatus(status string) 22 | GetPid() int 23 | GetStatus() *ProcStatus 24 | Watch() (*os.ProcessState, error) 25 | release() 26 | } 27 | 28 | // Proc is a os.Process wrapper with Status and more info that will be used on Master to maintain 29 | // the process health. 30 | type Proc struct { 31 | Name string 32 | Cmd string 33 | Args []string 34 | Path string 35 | Pidfile string 36 | Outfile string 37 | Errfile string 38 | KeepAlive bool 39 | Pid int 40 | Status *ProcStatus 41 | process *os.Process 42 | } 43 | 44 | // Start will execute the command Cmd that should run the process. It will also create an out, err and pidfile 45 | // in case they do not exist yet. 46 | // Returns an error in case there's any. 47 | func (proc *Proc) Start() error { 48 | outFile, err := utils.GetFile(proc.Outfile) 49 | if err != nil { 50 | return err 51 | } 52 | errFile, err := utils.GetFile(proc.Errfile) 53 | if err != nil { 54 | return err 55 | } 56 | wd, _ := os.Getwd() 57 | procAtr := &os.ProcAttr{ 58 | Dir: wd, 59 | Env: os.Environ(), 60 | Files: []*os.File{ 61 | os.Stdin, 62 | outFile, 63 | errFile, 64 | }, 65 | } 66 | args := append([]string{proc.Name}, proc.Args...) 67 | process, err := os.StartProcess(proc.Cmd, args, procAtr) 68 | if err != nil { 69 | return err 70 | } 71 | proc.process = process 72 | proc.Pid = proc.process.Pid 73 | err = utils.WriteFile(proc.Pidfile, []byte(strconv.Itoa(proc.process.Pid))) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | proc.Status.SetStatus("started") 79 | return nil 80 | } 81 | 82 | // ForceStop will forcefully send a SIGKILL signal to process killing it instantly. 83 | // Returns an error in case there's any. 84 | func (proc *Proc) ForceStop() error { 85 | if proc.process != nil { 86 | err := proc.process.Signal(syscall.SIGKILL) 87 | proc.Status.SetStatus("stopped") 88 | proc.release() 89 | return err 90 | } 91 | return errors.New("Process does not exist.") 92 | } 93 | 94 | // GracefullyStop will send a SIGTERM signal asking the process to terminate. 95 | // The process may choose to die gracefully or ignore this signal completely. In that case 96 | // the process will keep running unless you call ForceStop() 97 | // Returns an error in case there's any. 98 | func (proc *Proc) GracefullyStop() error { 99 | if proc.process != nil { 100 | err := proc.process.Signal(syscall.SIGTERM) 101 | proc.Status.SetStatus("asked to stop") 102 | return err 103 | } 104 | return errors.New("Process does not exist.") 105 | } 106 | 107 | // Restart will try to gracefully stop the process and then Start it again. 108 | // Returns an error in case there's any. 109 | func (proc *Proc) Restart() error { 110 | if proc.IsAlive() { 111 | err := proc.GracefullyStop() 112 | if err != nil { 113 | return err 114 | } 115 | } 116 | return proc.Start() 117 | } 118 | 119 | // Delete will delete everything created by this process, including the out, err and pid file. 120 | // Returns an error in case there's any. 121 | func (proc *Proc) Delete() error { 122 | proc.release() 123 | err := utils.DeleteFile(proc.Outfile) 124 | if err != nil { 125 | return err 126 | } 127 | err = utils.DeleteFile(proc.Errfile) 128 | if err != nil { 129 | return err 130 | } 131 | return os.RemoveAll(proc.Path) 132 | } 133 | 134 | // IsAlive will check if the process is alive or not. 135 | // Returns true if the process is alive or false otherwise. 136 | func (proc *Proc) IsAlive() bool { 137 | p, err := os.FindProcess(proc.Pid) 138 | if err != nil { 139 | return false 140 | } 141 | return p.Signal(syscall.Signal(0)) == nil 142 | } 143 | 144 | // Watch will stop execution and wait until the process change its state. Usually changing state, means that the process died. 145 | // Returns a tuple with the new process state and an error in case there's any. 146 | func (proc *Proc) Watch() (*os.ProcessState, error) { 147 | return proc.process.Wait() 148 | } 149 | 150 | // Will release the process and remove its PID file 151 | func (proc *Proc) release() { 152 | if proc.process != nil { 153 | proc.process.Release() 154 | } 155 | utils.DeleteFile(proc.Pidfile) 156 | } 157 | 158 | // Notify that process was stopped so we can set its PID to -1 159 | func (proc *Proc) NotifyStopped() { 160 | proc.Pid = -1; 161 | } 162 | 163 | // Add one restart to proc status 164 | func (proc *Proc) AddRestart() { 165 | proc.Status.AddRestart() 166 | } 167 | 168 | // Return proc current PID 169 | func (proc *Proc) GetPid() int { 170 | return proc.Pid; 171 | } 172 | 173 | // Return proc current status 174 | func (proc *Proc) GetStatus() *ProcStatus { 175 | return proc.Status; 176 | } 177 | 178 | // Set proc status 179 | func (proc *Proc) SetStatus(status string) { 180 | proc.Status.SetStatus(status); 181 | } 182 | 183 | // Proc identifier that will be used by watcher to keep track of its processes 184 | func (proc *Proc) Identifier() string { 185 | return proc.Name; 186 | } 187 | 188 | // Returns true if the process should be kept alive or not 189 | func (proc *Proc) ShouldKeepAlive() bool { 190 | return proc.KeepAlive; 191 | } 192 | -------------------------------------------------------------------------------- /lib/process/proc_status.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | // ProcStatus is a wrapper with the process current status. 4 | type ProcStatus struct { 5 | Status string 6 | Restarts int 7 | } 8 | 9 | // SetStatus will set the process string status. 10 | func (proc_status *ProcStatus) SetStatus(status string) { 11 | proc_status.Status = status 12 | } 13 | 14 | // AddRestart will add one restart to the process status. 15 | func (proc_status *ProcStatus) AddRestart() { 16 | proc_status.Restarts++ 17 | } 18 | -------------------------------------------------------------------------------- /lib/utils/file_util.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "io/ioutil" 4 | import "os" 5 | 6 | import "github.com/BurntSushi/toml" 7 | 8 | // WriteFile will write the info on array of bytes b to filepath. It will set the file 9 | // permission mode to 0660 10 | // Returns an error in case there's any. 11 | func WriteFile(filepath string, b []byte) error { 12 | return ioutil.WriteFile(filepath, b, 0660) 13 | } 14 | 15 | // GetFile will open filepath. 16 | // Returns a tuple with a file and an error in case there's any. 17 | func GetFile(filepath string) (*os.File, error) { 18 | return os.OpenFile(filepath, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0777) 19 | } 20 | 21 | // SafeReadTomlFile will try to acquire a lock on the file and then read its content afterwards. 22 | // Returns an error in case there's any. 23 | func SafeReadTomlFile(filename string, v interface{}) error { 24 | fileLock := MakeFileMutex(filename) 25 | fileLock.Lock() 26 | defer fileLock.Unlock() 27 | _, err := toml.DecodeFile(filename, v) 28 | 29 | return err 30 | } 31 | 32 | // SafeWriteTomlFile will try to acquire a lock on the file and then write to it. 33 | // Returns an error in case there's any. 34 | func SafeWriteTomlFile(v interface{}, filename string) error { 35 | fileLock := MakeFileMutex(filename) 36 | fileLock.Lock() 37 | defer fileLock.Unlock() 38 | f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0777) 39 | defer f.Close() 40 | if err != nil { 41 | return err 42 | } 43 | encoder := toml.NewEncoder(f) 44 | return encoder.Encode(v) 45 | } 46 | 47 | // DeleteFile will delete filepath permanently. 48 | // Returns an error in case there's any. 49 | func DeleteFile(filepath string) error { 50 | _, err := os.Stat(filepath) 51 | if err != nil { 52 | return err 53 | } 54 | err = os.Remove(filepath) 55 | return err 56 | } 57 | -------------------------------------------------------------------------------- /lib/utils/filemutex.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "sync" 4 | import "os" 5 | import "syscall" 6 | 7 | // FileMutex is a wrapper used to create lock on files. 8 | type FileMutex struct { 9 | mutex *sync.Mutex 10 | file *os.File 11 | } 12 | 13 | // MakeFileMutex will create a FileMutex intance. 14 | // Returns a FileMutex instance. 15 | func MakeFileMutex(filename string) *FileMutex { 16 | file, err := os.OpenFile(filename, os.O_RDONLY|os.O_CREATE, 0777) 17 | if err != nil { 18 | return &FileMutex{file: nil} 19 | } 20 | mutex := &sync.Mutex{} 21 | return &FileMutex{file: file, mutex: mutex} 22 | } 23 | 24 | // Lock will try to acquire a lock on the file. 25 | func (fMutex *FileMutex) Lock() { 26 | fMutex.mutex.Lock() 27 | if fMutex.file != nil { 28 | if err := syscall.Flock(int(fMutex.file.Fd()), syscall.LOCK_EX); err != nil { 29 | panic(err) 30 | } 31 | } 32 | } 33 | 34 | // Unlock will try to release a lock on a file. 35 | func (fMutex *FileMutex) Unlock() { 36 | fMutex.mutex.Unlock() 37 | if fMutex.file != nil { 38 | if err := syscall.Flock(int(fMutex.file.Fd()), syscall.LOCK_UN); err != nil { 39 | panic(err) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/watcher/watcher.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import "os" 4 | import "sync" 5 | 6 | import "github.com/topfreegames/apm/lib/process" 7 | import log "github.com/Sirupsen/logrus" 8 | 9 | // ProcStatus is a wrapper with the process state and an error in case there's any. 10 | type ProcStatus struct { 11 | state *os.ProcessState 12 | err error 13 | } 14 | 15 | // ProcWatcher is a wrapper that act as a object that watches a process. 16 | type ProcWatcher struct { 17 | procStatus chan *ProcStatus 18 | proc process.ProcContainer 19 | stopWatcher chan bool 20 | } 21 | 22 | // Watcher is responsible for watching a list of processes and report to Master in 23 | // case the process dies at some point. 24 | type Watcher struct { 25 | sync.Mutex 26 | restartProc chan process.ProcContainer 27 | watchProcs map[string]*ProcWatcher 28 | } 29 | 30 | // InitWatcher will create a Watcher instance. 31 | // Returns a Watcher instance. 32 | func InitWatcher() *Watcher { 33 | watcher := &Watcher{ 34 | restartProc: make(chan process.ProcContainer), 35 | watchProcs: make(map[string]*ProcWatcher), 36 | } 37 | return watcher 38 | } 39 | 40 | // RestartProc is a wrapper to export the channel restartProc. It basically keeps track of 41 | // all the processes that died and need to be restarted. 42 | // Returns a channel with the dead processes that need to be restarted. 43 | func (watcher *Watcher) RestartProc() chan process.ProcContainer { 44 | return watcher.restartProc 45 | } 46 | 47 | // AddProcWatcher will add a watcher on proc. 48 | func (watcher *Watcher) AddProcWatcher(proc process.ProcContainer) { 49 | watcher.Lock() 50 | defer watcher.Unlock() 51 | if _, ok := watcher.watchProcs[proc.Identifier()]; ok { 52 | log.Warnf("A watcher for this process already exists.") 53 | return 54 | } 55 | procWatcher := &ProcWatcher{ 56 | procStatus: make(chan *ProcStatus, 1), 57 | proc: proc, 58 | stopWatcher: make(chan bool, 1), 59 | } 60 | watcher.watchProcs[proc.Identifier()] = procWatcher 61 | go func() { 62 | log.Infof("Starting watcher on proc %s", proc.Identifier()) 63 | state, err := proc.Watch() 64 | procWatcher.procStatus <- &ProcStatus{ 65 | state: state, 66 | err: err, 67 | } 68 | }() 69 | go func() { 70 | defer delete(watcher.watchProcs, procWatcher.proc.Identifier()) 71 | select { 72 | case procStatus := <-procWatcher.procStatus: 73 | log.Infof("Proc %s is dead, advising master...", procWatcher.proc.Identifier()) 74 | log.Infof("State is %s", procStatus.state.String()) 75 | watcher.restartProc <- procWatcher.proc 76 | break 77 | case <-procWatcher.stopWatcher: 78 | break 79 | } 80 | }() 81 | } 82 | 83 | // StopWatcher will stop a running watcher on a process with identifier 'identifier' 84 | // Returns a channel that will be populated when the watcher is finally done. 85 | func (watcher *Watcher) StopWatcher(identifier string) chan bool { 86 | if watcher, ok := watcher.watchProcs[identifier]; ok { 87 | log.Infof("Stopping watcher on proc %s", identifier) 88 | watcher.stopWatcher <- true 89 | waitStop := make(chan bool, 1) 90 | go func() { 91 | <-watcher.procStatus 92 | waitStop <- true 93 | }() 94 | return waitStop 95 | } 96 | return nil 97 | } 98 | --------------------------------------------------------------------------------