├── .github └── FUNDING.yml ├── .gitignore ├── .gitmodules ├── .goreleaser.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cmd └── monexec │ └── main.go ├── docs ├── _config.yml ├── index.md ├── logo.svg └── service │ └── index.md ├── go.mod ├── go.sum ├── monexec └── config.go ├── plugins ├── README.md ├── adp_consul.go ├── adp_critical.go ├── adp_email.go ├── adp_http.go ├── adp_rest.go ├── adp_telegram.go ├── bindata.go ├── plugin_interface.go └── utils.go ├── pool ├── executable.go ├── logger_stream.go ├── pool.go ├── set_attrs.go ├── set_attrs_linux.go └── utils.go ├── release.sh ├── sample ├── dev.env ├── email.html ├── sample.yaml ├── sample2.yaml ├── sample3.yaml ├── sample4_logfile.yaml ├── sample5_critical.yaml └── sample6_rest.yaml ├── snapcraft.yaml └── swagger.yaml /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://reddec.net/about/#donate'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/** 2 | build 3 | .goxc.local.json 4 | vendor/ 5 | dist 6 | snap/** 7 | stage/** 8 | prime/** 9 | parts/** 10 | *.snap -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ui"] 2 | path = ui 3 | url = git@github.com:reddec/monexec-ui.git 4 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example goreleaser.yaml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | builds: 4 | - binary: monexec 5 | main: cmd/monexec/main.go 6 | goos: 7 | - windows 8 | - darwin 9 | - linux 10 | goarch: 11 | - amd64 12 | - 386 13 | - arm64 14 | archive: 15 | format: tar.gz 16 | nfpm: 17 | homepage: https://github.com/reddec/monexec 18 | description: Tool for controlling processes like a supervisord but with some features 19 | maintainer: RedDec 20 | license: MIT 21 | formats: 22 | - deb 23 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the project 2 | 3 | First and most important: thanks a lot for choosing this project as a place to which you are planning to invest your time and knowledge! 4 | 5 | This guide contains as much information as possible that should help you to join to the development. 6 | 7 | Please feel free to enrich this document through PR requests. 8 | 9 | ## Pull requests 10 | 11 | I am highly recommend to discuss a new feature through issues section before development to be sure that feature is not yet already done or someone already doing it. 12 | 13 | The project trying to follow simple 'git flow': 14 | * `master` branch should be always available for 'testing' use (i.e. available for build, run and not to fail) 15 | * **tagged** commit means stable version 16 | * (recommended, but not critical) new features should come from `feature/some-name` branches, bug-fixes from `bug/bug-name` and so on. Don't stuck on it too much: if you can't really decide which branch name required - use `feature/` prefix 17 | 18 | Simple way to understand project is to look at plugins implementation. They are relatively simple and standardized 19 | 20 | Useful resources: 21 | * Good tutorial [how to create PR](https://help.github.com/en/articles/creating-a-pull-request) from GitHub. 22 | 23 | ## Environment 24 | 25 | - **Go**: at this time project aims to the latest golang toolchain (1.12 now), however if you will use newer version please note it in the PR. 26 | - [Go SDK](https://golang.org/doc/install) 27 | - **Modules**: the project is using go1.11+ modules without `vendor` directory. 28 | - [How to use modules](https://blog.golang.org/using-go-modules) 29 | - **IDE**: Me (reddec) personally using [Jetbrains Goland](https://www.jetbrains.com/go/) as a professional user, however you may use free combination of IDEA community edition + Golang plugin. Or whatever you want - just please **don't commit IDE** files to the repository 30 | 31 | ## Prepare and build 32 | 33 | 1. Install go and setup [GOPATH](https://github.com/golang/go/wiki/GOPATH) 34 | 2. Clone project through git 35 | 3. In the project directory download all dependency for the command: `GO111MODULE=on go get -v ./...` 36 | 4. Build it: `GO111MODULE=on go build ./cmd/...` 37 | 5. Run it: `./monexec --help` 38 | 39 | ## Project structure 40 | 41 | * `docs` - site and project documentation 42 | * `monexec` - root configuration 43 | * `plugins` - plugin implementation 44 | * `pool` - core (need refactor) supervisor logic 45 | * `sample` - examples and configuration files 46 | * `ui` - sub-module for UI 47 | * `swagger.yaml` - swagger definition of HTTP plugin endpoints -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Baryshnikov Alexander 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Monexec 2 | 3 | ![Mx](docs/logo.svg) 4 | 5 | ***MON**itoring **EXE**cutables* 6 | 7 | [![GitHub release](https://img.shields.io/github/release/reddec/monexec.svg)](https://github.com/reddec/monexec/releases) 8 | [![license](https://img.shields.io/github/license/reddec/monexec.svg)](https://github.com/reddec/monexec) 9 | [![](https://godoc.org/github.com/reddec/monexec/monexec?status.svg)](http://godoc.org/github.com/reddec/monexec/monexec) 10 | [![Snap Status](https://build.snapcraft.io/badge/reddec/monexec.svg)](https://build.snapcraft.io/user/reddec/monexec) 11 | [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=4UKBSN5HVB3Y8&source=url) 12 | 13 | It’s tool for controlling processes like a supervisord but with some important features: 14 | 15 | * Easy to use - no dependencies. Just a single binary file pre-compilled for most major platforms 16 | * Easy to hack - monexec can be used as a Golang library with clean and simple architecture 17 | * Integrated with Consul - optionally, monexec can register all running processes as services and deregister on fail 18 | * Optional notification to Telegram 19 | * Supports gracefull and fast shutdown by signals 20 | * Developed for used inside Docker containers 21 | * Different strategies for processes 22 | * Support additional environment files 23 | * Support template-based email notification 24 | * Support HTTP notification 25 | * REST API (see swagger.yaml) 26 | * Web UI (if REST API enabled) 27 | 28 | [Buy me a coffe on Patreon](https://www.patreon.com/bePatron?u=15369842) 29 | 30 | ![screen1](https://reddec.net/images/project/monexec/screen1.png) 31 | ![screen2](https://reddec.net/images/project/monexec/screen2.gif) 32 | ## Installing 33 | 34 | [![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/monexec) 35 | 36 | * [snapcraft: monexec](https://snapcraft.io/monexec) 37 | 38 | * Precompilled binaries: [release page](https://github.com/reddec/monexec/releases) 39 | 40 | * From source (required Go toolchain): 41 | 42 | ``` 43 | go get -v -u github.com/reddec/monexec/... 44 | ``` 45 | 46 | recommended way is snap 47 | 48 | ## Documentation 49 | 50 | Usage: [https://reddec.github.io/monexec/](https://reddec.github.io/monexec/) 51 | 52 | API: [Godoc](http://godoc.org/github.com/reddec/monexec/monexec) 53 | 54 | 55 | ## Examples 56 | 57 | See documentation for details [https://reddec.github.io/monexec/](https://reddec.github.io/monexec/) 58 | 59 | ### Run from cmd 60 | 61 | ```bash 62 | monexec run -l srv1 --consul -- nc -l 9000 63 | ``` 64 | 65 | ### Run from config 66 | 67 | ```bash 68 | monexec start ./myservice.yaml 69 | ``` 70 | 71 | ### Notifications 72 | 73 | Add notification to Telegram 74 | 75 | ```yaml 76 | telegram: 77 | # BOT token 78 | token: "123456789:AAAAAAAAAAAAAAAAAAAAAA_BBBBBBBBBBBB" 79 | services: 80 | # services that will be monitored 81 | - "listener2" 82 | recipients: 83 | # List of telegrams chat id 84 | - 123456789 85 | template: | 86 | *{{.label}}* 87 | Service {{.label}} {{.action}} 88 | {{if .error}}⚠️ *Error:* {{.error}}{{end}} 89 | _time: {{.time}}_ 90 | _host: {{.hostname}}_ 91 | ``` 92 | 93 | #### Email 94 | 95 | Add email notification 96 | 97 | ```yaml 98 | email: 99 | services: 100 | - myservice 101 | smtp: "smtp.gmail.com:587" 102 | from: "example-monitor@gmail.com" 103 | password: "xyzzzyyyzyyzyz" 104 | to: 105 | - "admin1@example.com" 106 | template: | 107 | Subject: {{.label}} 108 | 109 | Service {{.label}} {{.action}} 110 | ``` 111 | 112 | #### HTTP 113 | 114 | Add HTTP request as notification 115 | 116 | ```yaml 117 | http: 118 | services: 119 | - myservice 120 | url: "http://example.com/{{.label}}/{{.action}}" 121 | templateFile: "./body.txt" 122 | ``` 123 | -------------------------------------------------------------------------------- /cmd/monexec/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "github.com/reddec/monexec/monexec" 6 | "github.com/reddec/monexec/plugins" 7 | "github.com/reddec/monexec/pool" 8 | "gopkg.in/alecthomas/kingpin.v2" 9 | "gopkg.in/yaml.v2" 10 | "log" 11 | "os" 12 | "os/signal" 13 | "syscall" 14 | ) 15 | 16 | var ( 17 | version = "dev" 18 | ) 19 | var ( 20 | runCommand = kingpin.Command("run", "Run single executable") 21 | runGenerate = runCommand.Flag("generate", "Generate instead of run YAML configuration based on args").Bool() 22 | runBin = runCommand.Arg("command", "Path to executable").Required().String() 23 | runArgs = runCommand.Arg("args", "Arguments to executable").Strings() 24 | runRestartCount = runCommand.Flag("restart-count", "Restart count (negative means infinity)").Short('r').Default("-1").Int() 25 | runRestartDelay = runCommand.Flag("restart-delay", "Delay before restart").Short('d').Default("5s").Duration() 26 | runStopTimeout = runCommand.Flag("graceful-timeout", "Timeout for graceful shutdown").Short('g').Default("5s").Duration() 27 | runLabel = runCommand.Flag("label", "Label name for executable. Default - autogenerated").Short('l').String() 28 | runWorkDir = runCommand.Flag("workdir", "Workdir for executable").Short('w').String() 29 | runEnv = runCommand.Flag("env", "Environment addition variables").Short('e').StringMap() 30 | runEnvFiles = runCommand.Flag("env-file", "Files with additional environment variables").Short('E').Strings() 31 | runRawOutput = runCommand.Flag("raw", "Raw stdout without prefixes").Short('R').Bool() 32 | 33 | runConsulEnable = runCommand.Flag("consul", "Enable consul integration").Bool() 34 | runConsulAddress = runCommand.Flag("consul-address", "Consul address").Default("http://localhost:8500").String() 35 | runConsulPermanent = runCommand.Flag("consul-permanent", "Keep service in consul auto timeout").Bool() 36 | runConsulTTL = runCommand.Flag("consul-ttl", "Keep-alive TTL for services").Default("3s").Duration() 37 | runConsulDeRegTTL = runCommand.Flag("consul-unreg", "Timeout after for auto de-registration").Default("1m").Duration() 38 | ) 39 | 40 | var ( 41 | startCommand = kingpin.Command("start", "Start supervisor from configuration files") 42 | startSources = startCommand.Arg("source", "Source files and/or directories with YAML files (.yml or .yaml)").Required().Strings() 43 | ) 44 | 45 | func run() { 46 | config := monexec.DefaultConfig() 47 | 48 | config.Services = append(config.Services, pool.Executable{ 49 | Name: *runLabel, 50 | Command: *runBin, 51 | Args: *runArgs, 52 | RestartTimeout: *runRestartDelay, 53 | Restart: *runRestartCount, 54 | StopTimeout: *runStopTimeout, 55 | WorkDir: *runWorkDir, 56 | Environment: *runEnv, 57 | EnvFiles: *runEnvFiles, 58 | RawOutput: *runRawOutput, 59 | }) 60 | monexec.FillDefaultExecutable(&config.Services[0]) 61 | 62 | if *runConsulEnable { 63 | plg := plugins.DefaultConsul() 64 | plg.URL = *runConsulAddress 65 | plg.TTL = *runConsulTTL 66 | plg.AutoDeregistrationTimeout = *runConsulDeRegTTL 67 | if *runConsulPermanent { 68 | plg.Permanent = append(plg.Permanent, config.Services[0].Name) 69 | } else { 70 | plg.Dynamic = append(plg.Dynamic, config.Services[0].Name) 71 | } 72 | config.Plugins = map[string]interface{}{ 73 | "consul": &plg, 74 | } 75 | } 76 | 77 | if *runGenerate { 78 | data, err := yaml.Marshal(config) 79 | if err != nil { 80 | panic(err) 81 | } 82 | os.Stdout.Write(data) 83 | } else { 84 | 85 | runConfigInSupervisor(&config, &pool.Pool{}) 86 | } 87 | } 88 | 89 | func start() { 90 | config, err := monexec.LoadConfig(*startSources...) 91 | if err != nil { 92 | log.Fatal(err) 93 | } 94 | 95 | runConfigInSupervisor(config, &pool.Pool{}) 96 | } 97 | 98 | func runConfigInSupervisor(config *monexec.Config, sv *pool.Pool) { 99 | ctx, stop := context.WithCancel(context.Background()) 100 | 101 | c := make(chan os.Signal, 2) 102 | signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL) 103 | go func() { 104 | for range c { 105 | stop() 106 | break 107 | } 108 | }() 109 | 110 | err := config.Run(sv, ctx) 111 | 112 | if err != nil { 113 | log.Fatal(err) 114 | } 115 | done := sv.Done() 116 | select { 117 | case <-ctx.Done(): 118 | sv.Terminate() 119 | <-done 120 | case <-done: 121 | 122 | } 123 | stop() 124 | config.ClosePlugins() 125 | } 126 | 127 | func main() { 128 | kingpin.Version(version).DefaultEnvars() 129 | switch kingpin.Parse() { 130 | case "run": 131 | run() 132 | case "start": 133 | start() 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # MONEXEC 5 | 6 | [![GitHub release](https://img.shields.io/github/release/reddec/monexec.svg)](https://github.com/reddec/monexec/releases) 7 | [![license](https://img.shields.io/github/license/reddec/monexec.svg)](https://github.com/reddec/monexec) 8 | [![](https://godoc.org/github.com/reddec/monexec/monexec?status.svg)](http://godoc.org/github.com/reddec/monexec/monexec) 9 | [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=4UKBSN5HVB3Y8&source=url) 10 | 11 | It's tool for controlling processes like a **supervisord** but with some important features: 12 | * Easy to use - no dependencies. Just a single binary file pre-compilled for most major platforms 13 | * Easy to hack - monexec can be used as a Golang library with clean and simple architecture 14 | * Integrated with Consul - optionally, monexec can register all running processes as services and deregister on fail 15 | * Supports gracefull and fast shutdown by signals 16 | * Developed for used inside Docker containers 17 | * Different strategies for processes 18 | * Support template-based email notification 19 | 20 | [download for most major platform](https://github.com/reddec/monexec/releases) 21 | 22 | # Installing 23 | 24 | Precompilled binaries: 25 | [release page](https://github.com/reddec/monexec/releases) 26 | 27 | From source (required Go toolchain): 28 | 29 | ``` 30 | go get -v -u github.com/reddec/monexec/... 31 | ``` 32 | 33 | # How to integrate with Consul 34 | 35 | Consul is a service registry and service discover system. MONEXEC can automatically register application in Consul as a service. 36 | 37 | Auto(de)registration available for `run` or `start` commands. 38 | 39 | Use general flag `--consul` (or env var `MONEXEC_CONSUL=true`) for enable Consul integration. Monexec will try register and update status of service in Consul local agent. 40 | 41 | Monexec will continue work even if Consul becomes unavailable. 42 | 43 | Consul address by default located to localhost, but can be overrided by `--consul-address` or `MONEXEC_CONSUL_ADDRESS` environment variable. 44 | 45 | Additional Consul configuration is available only by [Go Consul API environment variables](https://godoc.org/github.com/hashicorp/consul/api#pkg-constants) (improvments for this are in roadmap). 46 | 47 | ## Examples: 48 | 49 | **Register in local agent:** 50 | 51 | Temporary (will auto de-registrate service in a critical state or after gracefull shutdown) 52 | 53 | ```bash 54 | monexec run -l srv1 --consul -- nc -l 9000 55 | ``` 56 | 57 | Permanent 58 | 59 | ```bash 60 | monexec run -l srv1 --consul --consul-permanent -- nc -l 9000 61 | ``` 62 | 63 | **Register in remote agent:** 64 | 65 | Suppose Consul agent is running in host `registry` 66 | 67 | ```bash 68 | monexec run --consul --consul-address "http://registry:8500" -l srv1 -- nc -l 9000 69 | ``` 70 | 71 | # Documentation 72 | 73 | Still under the construction. PR very welcome! 74 | 75 | Look at cookbook also 76 | 77 | 1. [Services and service configuration](service) 78 | 79 | # Cookbook 80 | 81 | # How to integrate with Telegram 82 | 83 | Since `0.1.1` you can receive notifications over Telegram. 84 | 85 | You have to know: 86 | 87 | * BOT token : can be obtained here http://t.me/botfather 88 | * Receipients ChatID's : can be obtained here http://t.me/MyTelegramID_bot 89 | 90 | Message template (based on Golang templates) also required. We recommend use this: 91 | 92 | ``` 93 | *{{.label}}* 94 | Service {{.label}} {{.action}} 95 | {{if .error}}⚠️ *Error:* {{.error}}{{end}}_time: {{.time}}_ 96 | _host: {{.hostname}}_ 97 | ``` 98 | 99 | Available params: 100 | 101 | * `.label` - name of service 102 | * `.action` - servce action. Can be `spawned` or `stopped` 103 | * `.time` - current time in UTC format with timezone 104 | * `.error` - error message available only on `stopped` action 105 | * `.hostname` - current hostname 106 | 107 | Configuration avaiable only from .yaml files: 108 | 109 | ```yaml 110 | telegram: 111 | # BOT token token: "123456789:AAAAAAAAAAAAAAAAAAAAAA_BBBBBBBBBBBB" 112 | services: # services that will be monitored 113 | - "listener2" 114 | recipients: # List of telegrams chat id 115 | - 123456789 116 | template: | *{{.label}}* Service {{.label}} {{.action}} {{if .error}}⚠️ *Error:* {{.error}}{{end}} _time: {{.time}}_ _host: {{.hostname}}_ 117 | ``` 118 | 119 | Since `0.1.4` you also can specify `templateFile` instead of `template` 120 | 121 | # How to integrate with email 122 | 123 | Since `0.1.3` you can receive notifications over email. 124 | 125 | If you are using Google emails (tested): 126 | 127 | * Obtain application password https://myaccount.google.com/apppasswords 128 | * SMTP server will be: `smtp.gmail.com:587` 129 | 130 | Message template (based on Golang templates) also required. We recommend use this: 131 | 132 | ``` 133 | Content-Type: text/html 134 | Subject: {{.label}} {{.action}} 135 | 136 |

{{.label}}

137 | 138 | 139 |
Label {{.label}}
ID ({{.id}})
Action {{.action}}
Hostname {{.hostname}}
Local time {{.time}}
User {{env "USER"}}
140 | ``` 141 | 142 | Available params: 143 | 144 | * `.label` - name of service 145 | * `.action` - servce action. Can be `spawned` or `stopped` 146 | * `.time` - current time in UTC format with timezone 147 | * `.error` - error message available only on `stopped` action 148 | * `.hostname` - current hostname 149 | 150 | Plus all operations from http://masterminds.github.io/sprig/ (like `env` or `upper`) 151 | 152 | Configuration avaiable only from .yaml files: 153 | 154 | 155 | ```yaml 156 | 157 | email: 158 | services: 159 | - myservice 160 | smtp: "smtp.gmail.com:587" 161 | from: "example-monitor@gmail.com" 162 | password: "xyzzzyyyzyyzyz" 163 | to: 164 | - "admin1@example.com" 165 | - "admin2@example.com" 166 | template: | Subject: {{.label}} 167 | Service {{.label}} {{.action}} templateFile: "./email.html" 168 | ``` 169 | 170 | `template` will be used as fallback for `templateFile`. If template file location is not absolute, it will be calculated 171 | from configuration directory. 172 | 173 | # How to integrate with HTTP 174 | 175 | Since `0.1.4` you can send notifications over HTTP 176 | 177 | * Supports any kind of methods (by default - `POST` but can be changed in `http.method`) 178 | * **Body** - template-based text same as in `Telegram` or `Email` plugin 179 | * **URL** - also template-based text (yes, with same rules as in `body` ;-) ) 180 | * **Headers** - you can also provide any headers (no, no templates here) 181 | * **Timeout** - limit time for request. By default - `20s` 182 | 183 | Configuration avaiable only from .yaml files: 184 | 185 | ```yaml 186 | http: 187 | services: 188 | - myservice 189 | url: "http://example.com/{{.label}}/{{.action}}" 190 | templateFile: "./body.txt" 191 | ``` 192 | 193 | `template` will be used as fallback for `templateFile`. If template file location is not absolute, it will be calculated from configuration directory. 194 | 195 | 196 | |Parameter | Type | Required | Default | Description | 197 | |--------------|----------|----------|---------|-------------| 198 | |`url` |`string` | yes | | Target URL 199 | |`method` |`string` | no | POST | HTTP method 200 | |`services` |`list` | yes | | List of services that will trigger plugin 201 | |`headers` |`map` | no | {} | Map (string -> string) of additional headers per request 202 | |`timeout` |`duration`| no | 20s | Request timeout 203 | |`template` |`string` | no | '' | Template string 204 | |`templateFile`|`string` | no | '' | Path to file of template (more priority then `template`, but `template` will be used as fallback) 205 | 206 | # Usage 207 | 208 | `monexec [command-flags...] [args,...]` 209 | 210 | All flags can be set by environment variables with prefix `MONEXEC_`. For example flag `--label sample` can be set as `export MONEXEC_LABEL="sample"` 211 | 212 | # How to enable REST API 213 | 214 | Since `0.1.6` you can enable simple REST API by adding `rest` plugin. 215 | 216 | Full version 217 | 218 | ```yaml 219 | rest: 220 | listen: "localhost:9900" 221 | cors: false 222 | ``` 223 | 224 | _cors option added in `0.1.9`_ 225 | 226 | or minimal (default is `localhost:9900`) 227 | 228 | ```yaml 229 | rest: 230 | ``` 231 | 232 | API documentation see in swagger.yaml file in repository 233 | 234 | **WEB UI** enable on `/ui` path 235 | 236 | ![screencapture-127-0-0-1-9000-2018-06-28-20_46_16](https://user-images.githubusercontent.com/6597086/42038135-c961b11a-7b1c-11e8-9437-44de6b36510c.png) 237 | 238 | ## Commands 239 | 240 | ### run 241 | Run single executable 242 | 243 | **Usage:** 244 | `monexec run [flags...] [args...]` 245 | 246 | **Example:** 247 | `monexec run -- nc -l 9000` - will run command `nc -l 9000` and restart it forever if needed with default timeout 248 | 249 | **Flags:** 250 | 251 | * `--generate` Generate to stdout YAML configuration based on args instead of run 252 | * `-r, --restart-count=-1` Restart count (negative means infinity) 253 | * `-d, --restart-delay=5s` Delay before restart 254 | * `-g, --graceful-timeout=5s` Timeout for graceful shutdown. Application first got signal `SIGTERM` and after this timeout `SIGKILL` 255 | * `-l, --label=LABEL` Label name for executable. Default - autogenerated 256 | * `-w, --workdir=WORKDIR` Workdir for executable 257 | * `-e, --env=ENV ...` Environment addition variables 258 | * `--consul` Enable consul integration 259 | * `--consul-address="http://localhost:8500"` Consul address 260 | * `--consul-permanent` Keep service in consul auto timeout 261 | * `--consul-ttl=3s` Keep-alive TTL for services 262 | * `--consul-unreg=1m` Timeout after for auto de-registration 263 | 264 | ## start 265 | Start processes based on YAML configuration files 266 | 267 | **Usage:** 268 | `monexec start ` 269 | 270 | **Example:** 271 | 272 | `monexec start ./*.yaml` 273 | 274 | Configuration sources can be multiple directories and/or files. Files must contain valid YAML content and have `.yaml` or `.yml` extension. 275 | 276 | Minimal configuration file: 277 | 278 | ```yaml 279 | command: path/to/executable 280 | ``` 281 | 282 | Full sample of configuration file: 283 | 284 | ```yaml 285 | services: 286 | - label: Netcat Sample Service 287 | command: nc 288 | args: 289 | - -l 290 | - "9000" 291 | stop_timeout: 5s 292 | restart_delay: 5s 293 | restart: -1 294 | consul: 295 | url: http://localhost:8500 296 | ttl: 3s 297 | timeout: 1m0s 298 | ``` 299 | 300 | # How to generate sample config 301 | 302 | Generate configuration file based on `run` like arguments: just add `--generate` 303 | 304 | **Usage:** 305 | 306 | Same as `run` 307 | 308 | For example, during development we are using 309 | 310 | ```bash 311 | monexec run -l srvExt1 --consul --restart-count 10 restart -- java -jar srvExt1.jar 312 | ``` 313 | 314 | We want to save this settings into configuration file. Just add `--generate` 315 | 316 | ```bash 317 | monexec run --generate -l srvExt1 --consul --restart-count 10 restart -- java -jar srvExt1.jar 318 | ``` 319 | 320 | and get 321 | 322 | ```yaml 323 | services: 324 | - label: srvExt1 325 | command: restart 326 | args: 327 | - java 328 | - -jar - 329 | srvExt1.jar 330 | stop_timeout: 5s 331 | restart_delay: 5s 332 | restart: 10 333 | consul: 334 | url: http://localhost:8500 335 | ttl: 3s 336 | timeout: 1m0s 337 | register: 338 | - srvExt1 339 | ``` 340 | 341 | # How to log to file a service 342 | 343 | Since `0.1.5` you can copy content of STDERR/STDOUT service output to specific file: option `logFile` in service section. If file path not absolute log file is putted relative to working directory. 344 | 345 | ```yaml 346 | services: 347 | - label: listener4 348 | command: nc 349 | logFile: /var/log/listener4.log 350 | args: 351 | - -v 352 | - -l 353 | - 9001 354 | stop_timeout: 5s 355 | restart_delay: 5s 356 | restart: -1 357 | ``` 358 | 359 | # Critical services 360 | 361 | 362 | When critical services stopped, all other processes have to be stopped also 363 | 364 | 365 | Add section `critical` to configuration: 366 | 367 | 368 | ```yaml 369 | services: 370 | - label: srvExt1 371 | command: restart 372 | args: 373 | - java 374 | - -jar 375 | - srvExt1.jar 376 | stop_timeout: 5s 377 | restart_delay: 5s 378 | restart: 10 379 | - label: consul 380 | command: restart 381 | args: 382 | - consul 383 | - agen 384 | - -dev 385 | - -bootstrap 386 | - -uiconsul: 387 | url: http://localhost:8500 388 | ttl: 3s 389 | timeout: 1m0s 390 | register: 391 | - srvExt1critical: 392 | - consul 393 | ``` 394 | 395 | # Raw stdout 396 | 397 | For several reasons (i.e. use in a bash tools) raw stdout is required from application. 398 | 399 | Since `0.1.12` to disable all prefixes in STDOUT (in STDERR they will still persists) use flag `--raw, -R`. 400 | 401 | **Example:** 402 | 403 | ```bash 404 | monexec -R echo 123 > sample.txt # run echo command. Use CTRL+C to interrupt when needed 405 | ``` 406 | 407 | The file `sample.txt` will now contains ONLY result of echo command (i.e. `123`) 408 | 409 | # Environment variables from file 410 | 411 | General environment variables processing: 412 | 413 | 1. system environments 414 | 2. environments defined by `--env, -e` (CLI) or in `environment` (config) 415 | 3. (since `0.1.14`), environments files in order as they defined by `--env-file, -E` (CLI) or in `envFile` (config) 416 | 417 | If environment file couldn't be read, the application **can still be launched** - only warning in log presented. 418 | 419 | Environment file format: 420 | 421 | * pair: KEY=VALUE 422 | * comment: line started with # 423 | * empty lines or invalid (without = symbol) ignored 424 | * there is no way to escape new line symbol 425 | 426 | Example of file: 427 | 428 | ```env 429 | # some comment 430 | 431 | 432 | it was empty lines and this is a broken record 433 | MAIL=will be replaced 434 | MAIL=owner@reddec.net 435 | SUBJECT=multiple = are supported 436 | ``` 437 | -------------------------------------------------------------------------------- /docs/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 25 | 30 | 36 | 41 | 46 | 52 | 53 | 54 | 77 | 79 | 80 | 82 | image/svg+xml 83 | 85 | 86 | 87 | 88 | 89 | 94 | 99 | monexec 110 | 119 | 128 | CLI 139 | go 150 | light supervisor 161 | Mx 172 | 173 | 174 | -------------------------------------------------------------------------------- /docs/service/index.md: -------------------------------------------------------------------------------- 1 | ## Service configuration 2 | 3 | Describes strategy, runtime parameters and other options for one executable. 4 | 5 | ```yaml 6 | services: 7 | # list of services definition 8 | # plugins definition 9 | ``` 10 | 11 | Example of service config 12 | 13 | ```yaml 14 | label: "service name" 15 | command: "path to executable" 16 | args: 17 | - arguments 18 | - to 19 | - executable 20 | environment: 21 | SOME_PARAM: some value 22 | ANOTHER_PARAM: another value 23 | envFiles: 24 | - source environment file1 25 | - source environment file2 26 | workdir: "working directory" 27 | stop_timeout: 3s 28 | restart_delay: 5s 29 | restart: -1 30 | logFile: "path to log file" 31 | raw: false 32 | ``` 33 | 34 | ### label 35 | 36 | > string, not required, default is random name 37 | 38 | Readable name of service. Will be presented in all logs. 39 | By default human-readable random name will be generated. 40 | 41 | *example*: `service-1` 42 | 43 | ### command 44 | 45 | > string, required 46 | 47 | Path to executable binary. For non-absolute path binary has to be available through `PATH` environment variable. 48 | 49 | *example*: `cat` 50 | 51 | ### args 52 | 53 | > array of string, not required, default is empty 54 | 55 | Arguments that will be passed to the `command`. 56 | 57 | 58 | Hi @thim81 ! Thanks for you question. Let me answer in a reverse order: 59 | 60 | ### args 61 | 62 | > array of string, not required, 63 | 64 | Each argument should be place as separated array element. There is no way how to start single command joined with arguments in one line due to security issue: when all arguments are passing separately, it's impossible to make an insecure call. 65 | 66 | A some theoretical example: assume that `command` accepts a single-line command that should be invoked by the system. In some cases (not only by hackers, but just by mistake) someone may write command like: `echo aa bb cc; ls /`. That will interpreted by the shell as a two commands: `echo aa bb cc` and `ls /` that probably not what you really want to do. 67 | 68 | In case of separated arguments, all arguments passed not to shell but to the command it self. So it doesn't matter what symbols, escaping characters or anything else someone will put. 69 | 70 | Anyway there is still a hack: to set `command: /usr/bin/bash` and `args: ['-c', 'any complex command']. 71 | 72 | However, writing manually all parameters can make a hassle. To help with it, monexec has a special CLI argument: `--generate` that will make most parameters automatically. For example: 73 | `monexec run --generate -- nc -l 9001` creates: 74 | ```yaml 75 | services: 76 | - label: crystal-spangle 77 | command: nc 78 | args: 79 | - -l 80 | - "9001" 81 | stop_timeout: 5s 82 | restart_delay: 5s 83 | restart: -1 84 | ``` 85 | 86 | *example*: 87 | ```yaml 88 | args: 89 | - "-l" 90 | - "9001" 91 | ``` 92 | 93 | ### environment 94 | 95 | > map of string=>string, not required, default is empty 96 | 97 | Additional environment variables that will be appended over system for the service. 98 | 99 | *example*: 100 | 101 | ```yaml 102 | environment: 103 | API_TOKEN: "xx-yy-zz" 104 | ENDPOINT: "api.example.com" 105 | ``` 106 | 107 | ### envFiles 108 | 109 | > list of string, not required, default is empty 110 | 111 | Load environment variables from file. 112 | 113 | General environment variables processing: 114 | 115 | 1. system environments 116 | 2. environments defined by `--env, -e` (CLI) or in `environment` (config) 117 | 3. (since `0.1.14`), environments files in order as they defined by `--env-file, -E` (CLI) or in `envFile` (config) 118 | 119 | File path are absolute or relative to the 120 | If environment file couldn't be read, the application **can still be launched** - only warning in log presented. 121 | 122 | Environment file format: 123 | 124 | * pair: KEY=VALUE 125 | * comment: line started with # 126 | * empty lines or invalid (without = symbol) ignored 127 | * there is no way to escape new line symbol 128 | 129 | Example of file: 130 | 131 | ```env 132 | # some comment 133 | 134 | it was empty lines and this is a broken record MAIL=will be replaced MAIL=owner@reddec.net SUBJECT=multiple = are supported 135 | ``` -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/reddec/monexec 2 | 3 | require ( 4 | github.com/Masterminds/semver v1.2.2 5 | github.com/Masterminds/sprig v2.15.0+incompatible 6 | github.com/Pallinder/go-randomdata v0.0.0-20180505152823-b073033ef5a7 7 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc 8 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf 9 | github.com/aokoli/goutils v0.0.0-20140502001128-9c37978a95bd 10 | github.com/armon/go-metrics v0.0.0-20160717043458-3df31a1ada83 11 | github.com/elazarl/go-bindata-assetfs v1.0.0 12 | github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 13 | github.com/gin-gonic/gin v0.0.0-20170702092826-d459835d2b07 14 | github.com/golang/protobuf v1.1.0 15 | github.com/google/pprof v0.0.0-20190109223431-e84dfd68c163 16 | github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c 17 | github.com/hashicorp/consul v1.1.0 18 | github.com/hashicorp/go-cleanhttp v0.0.0-20171218145408-d5fe4b57a186 19 | github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90 20 | github.com/hashicorp/serf v0.0.0-20180504200640-4b67f2c2b2bb 21 | github.com/huandu/xstrings v0.0.0-20151130125119-3959339b3335 22 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 23 | github.com/imdario/mergo v0.0.0-20171009183408-7fe0c75c13ab 24 | github.com/mattn/go-isatty v0.0.3 25 | github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747 26 | github.com/mitchellh/mapstructure v0.0.0-20180511142126-bb74f1db0675 27 | github.com/pkg/errors v0.8.0 28 | github.com/technoweenie/multipartstreamer v1.0.1 29 | github.com/ugorji/go v1.1.1 30 | golang.org/x/arch v0.0.0-20181203225421-5a4828bb7045 31 | golang.org/x/crypto v0.0.0-20161006174701-d172538b2cfc 32 | golang.org/x/sys v0.0.0-20180622082034-63fc586f45fe 33 | golang.org/x/text v0.3.0 34 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 35 | gopkg.in/go-playground/validator.v8 v8.18.2 36 | gopkg.in/telegram-bot-api.v4 v4.6.2 37 | gopkg.in/yaml.v2 v2.2.1 38 | ) 39 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Masterminds/semver v1.2.2 h1:ptelpryog9A0pR4TGFvIAvw2c8SaNrYkFtfrxhSviss= 2 | github.com/Masterminds/semver v1.2.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 3 | github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= 4 | github.com/Pallinder/go-randomdata v0.0.0-20180505152823-b073033ef5a7 h1:OhT9Tzlt/RjAJlMz8dAu10pRsBcVb5ETAQY6ZHgwY0w= 5 | github.com/Pallinder/go-randomdata v0.0.0-20180505152823-b073033ef5a7/go.mod h1:yHmJgulpD2Nfrm0cR9tI/+oAgRqCQQixsA8HyRZfV9Y= 6 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= 7 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 8 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= 9 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 10 | github.com/aokoli/goutils v0.0.0-20140502001128-9c37978a95bd h1:gE1k0mCB0xXeVlUd54YnVzrNA2odhHUdY/qYL5jf3HY= 11 | github.com/aokoli/goutils v0.0.0-20140502001128-9c37978a95bd/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= 12 | github.com/armon/go-metrics v0.0.0-20160717043458-3df31a1ada83 h1:WRzocQR0FF7Hlsw5697z1p0NRN6SjEdY+nUftdaY7ZA= 13 | github.com/armon/go-metrics v0.0.0-20160717043458-3df31a1ada83/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 14 | github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= 15 | github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= 16 | github.com/gin-gonic/gin v0.0.0-20170702092826-d459835d2b07 h1:cZPJWzd2oNeoS0oJM2TlN9rl0OnCgUr10gC8Q4mH+6M= 17 | github.com/gin-gonic/gin v0.0.0-20170702092826-d459835d2b07/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= 18 | github.com/golang/protobuf v1.1.0 h1:0iH4Ffd/meGoXqF2lSAhZHt8X+cPgkfn/cb6Cce5Vpc= 19 | github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 20 | github.com/google/pprof v0.0.0-20190109223431-e84dfd68c163/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 21 | github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c h1:jWtZjFEUE/Bz0IeIhqCnyZ3HG6KRXSntXe4SjtuTH7c= 22 | github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 23 | github.com/hashicorp/consul v1.1.0 h1:xKwxj1mDPRmQVbiZxJlD7R5PiS7XsttGP7vNx16IcLY= 24 | github.com/hashicorp/consul v1.1.0/go.mod h1:mFrjN1mfidgJfYP1xrJCF+AfRhr6Eaqhb2+sfyn/OOI= 25 | github.com/hashicorp/go-cleanhttp v0.0.0-20171218145408-d5fe4b57a186 h1:URgjUo+bs1KwatoNbwG0uCO4dHN4r1jsp4a5AGgHRjo= 26 | github.com/hashicorp/go-cleanhttp v0.0.0-20171218145408-d5fe4b57a186/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 27 | github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90 h1:VBj0QYQ0u2MCJzBfeYXGexnAl17GsH1yidnoxCqqD9E= 28 | github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90/go.mod h1:o4zcYY1e0GEZI6eSEr+43QDYmuGglw1qSO6qdHUHCgg= 29 | github.com/hashicorp/serf v0.0.0-20180504200640-4b67f2c2b2bb h1:P6RWBQHagByyc35BhW8kX6JlLKWG0ZOND/eAIvAx4Zo= 30 | github.com/hashicorp/serf v0.0.0-20180504200640-4b67f2c2b2bb/go.mod h1:h/Ru6tmZazX7WO/GDmwdpS975F019L4t5ng5IgwbNrE= 31 | github.com/huandu/xstrings v0.0.0-20151130125119-3959339b3335 h1:KZOP9q7J/P4eMBibPuVwuloXgd2dTbLAHRPqxw7NXOw= 32 | github.com/huandu/xstrings v0.0.0-20151130125119-3959339b3335/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= 33 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 34 | github.com/imdario/mergo v0.0.0-20171009183408-7fe0c75c13ab h1:k/Biv+LJL35wkk0Hveko1nj7as4tSHkHdZaNlzn/gcQ= 35 | github.com/imdario/mergo v0.0.0-20171009183408-7fe0c75c13ab/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 36 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 37 | github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747 h1:eQox4Rh4ewJF+mqYPxCkmBAirRnPaHEB26UkNuPyjlk= 38 | github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 39 | github.com/mitchellh/mapstructure v0.0.0-20180511142126-bb74f1db0675 h1:/rdJjIiKG5rRdwG5yxHmSE/7ZREjpyC0kL7GxGT/qJw= 40 | github.com/mitchellh/mapstructure v0.0.0-20180511142126-bb74f1db0675/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 41 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 42 | github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= 43 | github.com/ugorji/go v1.1.1/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= 44 | golang.org/x/arch v0.0.0-20181203225421-5a4828bb7045/go.mod h1:cYlCBUl1MsqxdiKgmc4uh7TxZfWSFLOGSRR090WDxt8= 45 | golang.org/x/crypto v0.0.0-20161006174701-d172538b2cfc h1:DfGUWE6VaxsoTkTHNGspjXfDCOroOPdvTIuiYeYpC4o= 46 | golang.org/x/crypto v0.0.0-20161006174701-d172538b2cfc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 47 | golang.org/x/sys v0.0.0-20180622082034-63fc586f45fe/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 48 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 49 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= 50 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 51 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 52 | gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= 53 | gopkg.in/telegram-bot-api.v4 v4.6.2 h1:oUu8dyT/KzUWusD3OiPWTLoFM6n9uskQlW1GLoxjQcE= 54 | gopkg.in/telegram-bot-api.v4 v4.6.2/go.mod h1:5DpGO5dbumb40px+dXcwCpcjmeHNYLpk0bp3XRNvWDM= 55 | gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= 56 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 57 | -------------------------------------------------------------------------------- /monexec/config.go: -------------------------------------------------------------------------------- 1 | package monexec 2 | 3 | import ( 4 | "context" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | "strings" 11 | "time" 12 | 13 | "github.com/Pallinder/go-randomdata" 14 | "gopkg.in/yaml.v2" 15 | "github.com/reddec/monexec/pool" 16 | "github.com/mitchellh/mapstructure" 17 | "errors" 18 | "github.com/reddec/monexec/plugins" 19 | "reflect" 20 | ) 21 | 22 | type Config struct { 23 | Services []pool.Executable `yaml:"services"` 24 | Plugins map[string]interface{} `yaml:",inline"` // all unparsed means plugins 25 | loadedPlugins map[string]plugins.PluginConfigNG `yaml:"-"` 26 | } 27 | 28 | func (c *Config) MergeFrom(other *Config) error { 29 | c.Services = append(c.Services, other.Services...) 30 | // -- merge plugins 31 | for otherPluginName, otherPluginInstance := range other.loadedPlugins { 32 | if ownPlugin, needMerge := c.loadedPlugins[otherPluginName]; needMerge { 33 | err := ownPlugin.MergeFrom(otherPluginInstance) 34 | if err != nil { 35 | return errors.New("merge " + otherPluginName + ": " + err.Error()) 36 | } 37 | } else { // new one - just copy 38 | c.loadedPlugins[otherPluginName] = otherPluginInstance 39 | } 40 | } 41 | return nil 42 | } 43 | 44 | func (c *Config) ClosePlugins() { 45 | for _, plugin := range c.loadedPlugins { 46 | plugin.Close() 47 | } 48 | } 49 | 50 | func DefaultConfig() Config { 51 | config := Config{} 52 | 53 | config.loadedPlugins = make(map[string]plugins.PluginConfigNG) 54 | return config 55 | } 56 | 57 | func FillDefaultExecutable(exec *pool.Executable) { 58 | if exec.RestartTimeout == 0 { 59 | exec.RestartTimeout = 6 * time.Second 60 | } 61 | if exec.Restart == 0 { 62 | exec.Restart = -1 63 | } 64 | if exec.StopTimeout == 0 { 65 | exec.StopTimeout = 3 * time.Second 66 | } 67 | if exec.Name == "" { 68 | exec.Name = randomdata.Noun() + "-" + randomdata.Adjective() 69 | } 70 | } 71 | 72 | func (config *Config) Run(sv *pool.Pool, ctx context.Context) error { 73 | // Initialize plugins 74 | //-- prepare and add all plugins 75 | for pluginName, pluginInstance := range config.loadedPlugins { 76 | err := pluginInstance.Prepare(ctx, sv) 77 | if err != nil { 78 | log.Println("failed prepare plugin", pluginName, "-", err) 79 | } else { 80 | log.Println("plugin", pluginName, "ready") 81 | sv.Watch(pluginInstance) 82 | } 83 | } 84 | 85 | // Run 86 | for i := range config.Services { 87 | exec := config.Services[i] 88 | FillDefaultExecutable(&exec) 89 | sv.Add(&exec) 90 | } 91 | 92 | sv.StartAll(ctx) 93 | return nil 94 | } 95 | 96 | func LoadConfig(locations ...string) (*Config, error) { 97 | c := DefaultConfig() 98 | ans := &c 99 | var files []os.FileInfo 100 | for _, location := range locations { 101 | if stat, err := os.Stat(location); err != nil { 102 | return nil, err 103 | } else if stat.IsDir() { 104 | fs, err := ioutil.ReadDir(location) 105 | if err != nil { 106 | return nil, err 107 | } 108 | files = fs 109 | } else { 110 | location = filepath.Dir(location) 111 | files = []os.FileInfo{stat} 112 | } 113 | for _, info := range files { 114 | if strings.HasSuffix(info.Name(), ".yml") || strings.HasSuffix(info.Name(), ".yaml") { 115 | fileName := path.Join(location, info.Name()) 116 | data, err := ioutil.ReadFile(fileName) 117 | if err != nil { 118 | return nil, err 119 | } 120 | var conf = DefaultConfig() 121 | err = yaml.Unmarshal(data, &conf) 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | // -- load all plugins for current config here 127 | for pluginName, description := range conf.Plugins { 128 | pluginInstance, found := plugins.BuildPlugin(pluginName, fileName) 129 | if !found { 130 | log.Println("plugin", pluginName, "not found") 131 | continue 132 | } 133 | 134 | var wrap = description 135 | refVal := reflect.ValueOf(wrap) 136 | if wrap != nil && refVal.Type().Kind() == reflect.Slice { 137 | wrap = map[string]interface{}{ 138 | "": description, 139 | } 140 | } 141 | 142 | config := &mapstructure.DecoderConfig{ 143 | Metadata: nil, 144 | Result: pluginInstance, 145 | DecodeHook: mapstructure.StringToTimeDurationHookFunc(), 146 | } 147 | 148 | decoder, err := mapstructure.NewDecoder(config) 149 | if err != nil { 150 | panic(err) // failed to initialize decoder - something really wrong 151 | } 152 | 153 | err = decoder.Decode(wrap) 154 | if err != nil { 155 | log.Println("failed load plugin", pluginName, "-", err) 156 | continue 157 | } 158 | conf.loadedPlugins[pluginName] = pluginInstance 159 | } 160 | 161 | err = ans.MergeFrom(&conf) 162 | if err != nil { 163 | return nil, err 164 | } 165 | } 166 | } 167 | } 168 | return ans, nil 169 | } 170 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # Sample plugin 2 | 3 | ```go 4 | package plugins 5 | 6 | import ( 7 | "context" 8 | "github.com/reddec/monexec/pool" 9 | ) 10 | 11 | 12 | type MyPlugin struct {} 13 | 14 | func (p *MyPlugin) Prepare(ctx context.Context, pl *pool.Pool) error { return nil } 15 | 16 | func (p *MyPlugin) OnSpawned(ctx context.Context, sv pool.Instance) {} 17 | 18 | func (p *MyPlugin) OnStarted(ctx context.Context, sv pool.Instance) {} 19 | 20 | func (p *MyPlugin) OnStopped(ctx context.Context, sv pool.Instance, err error) {} 21 | 22 | func (p *MyPlugin) OnFinished(ctx context.Context, sv pool.Instance) {} 23 | 24 | func (p *MyPlugin) MergeFrom(other interface{}) error { return nil} 25 | 26 | func (a *MyPlugin) Close() error { return nil } 27 | 28 | func init() { 29 | registerPlugin("myPlugin", func(file string) PluginConfigNG { 30 | return &MyPlugin{} 31 | }) 32 | } 33 | ``` -------------------------------------------------------------------------------- /plugins/adp_consul.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/hashicorp/consul/api" 7 | "log" 8 | "sync" 9 | "context" 10 | "fmt" 11 | "os" 12 | "errors" 13 | "github.com/reddec/monexec/pool" 14 | ) 15 | 16 | type ConsulPlugin struct { 17 | URL string `yaml:"url"` 18 | TTL time.Duration `yaml:"ttl"` 19 | AutoDeregistrationTimeout time.Duration `yaml:"timeout"` 20 | Dynamic []string `yaml:"register,omitempty"` 21 | Permanent []string `yaml:"permanent,omitempty"` 22 | 23 | registerLabels map[string]consulRegistration `yaml:"-"` 24 | Log *log.Logger `yaml:"-"` 25 | Client *api.Client `yaml:"-"` 26 | matched map[string]struct{} `yaml:"-"` 27 | lock sync.Mutex `yaml:"-"` 28 | stop chan struct{} `yaml:"-"` 29 | done chan struct{} `yaml:"-"` 30 | } 31 | 32 | func DefaultConsul() ConsulPlugin { 33 | return ConsulPlugin{ 34 | URL: "http://localhost:8500", 35 | AutoDeregistrationTimeout: 5 * time.Minute, 36 | TTL: 2 * time.Minute, 37 | } 38 | } 39 | 40 | func (p *ConsulPlugin) Prepare(ctx context.Context, pl *pool.Pool) error { 41 | consulConfig := api.DefaultConfig() 42 | consulConfig.Address = p.URL 43 | consul, err := api.NewClient(consulConfig) 44 | if err != nil { 45 | return err 46 | } 47 | var consulRegs []consulRegistration 48 | for _, label := range p.Dynamic { 49 | consulRegs = append(consulRegs, consulRegistration{Permanent: false, Label: label}) 50 | } 51 | for _, label := range p.Permanent { 52 | consulRegs = append(consulRegs, consulRegistration{Permanent: true, Label: label}) 53 | } 54 | 55 | lbs := make(map[string]consulRegistration) 56 | for _, v := range consulRegs { 57 | lbs[v.Label] = v 58 | } 59 | 60 | p.Log = log.New(os.Stderr, "[consul] ", log.LstdFlags) 61 | p.stop = make(chan struct{}, 1) 62 | p.done = make(chan struct{}, 1) 63 | p.matched = make(map[string]struct{}) 64 | p.registerLabels = lbs 65 | p.Client = consul 66 | 67 | go p.checkLoop() 68 | return nil 69 | } 70 | 71 | func (p *ConsulPlugin) OnSpawned(ctx context.Context, sv pool.Instance) {} 72 | 73 | func (c *ConsulPlugin) OnStarted(ctx context.Context, sv pool.Instance) { 74 | label := sv.Config().Name 75 | info, exists := c.registerLabels[label] 76 | if !exists { 77 | return 78 | } 79 | dereg := c.AutoDeregistrationTimeout 80 | if dereg < c.TTL { 81 | dereg = 2 * c.TTL 82 | } 83 | if dereg < 1*time.Minute { 84 | dereg = 1 * time.Minute 85 | } 86 | err := c.Client.Agent().ServiceRegister(&api.AgentServiceRegistration{ 87 | Name: label, 88 | Tags: []string{fmt.Sprintf("%v", os.Getpid())}, 89 | Check: &api.AgentServiceCheck{ 90 | TTL: c.TTL.String(), 91 | DeregisterCriticalServiceAfter: dereg.String(), 92 | }, 93 | }) 94 | if err != nil { 95 | c.Log.Println("Can't register service", label, "in Consul:", err) 96 | } else { 97 | checkID := label + ":ttl" 98 | reg := api.AgentCheckRegistration{} 99 | reg.Name = checkID 100 | reg.TTL = c.TTL.String() 101 | reg.ServiceID = label 102 | 103 | if !info.Permanent { 104 | reg.DeregisterCriticalServiceAfter = dereg.String() 105 | } 106 | 107 | err = c.Client.Agent().CheckRegister(®) 108 | if err != nil { 109 | c.Log.Println("Can't register service TTL check", label, "in Consul:", err) 110 | } else { 111 | c.Log.Println("Service", label, "registered in Consul") 112 | c.lock.Lock() 113 | c.matched[checkID] = struct{}{} 114 | c.lock.Unlock() 115 | } 116 | } 117 | 118 | } 119 | 120 | func (c *ConsulPlugin) OnStopped(ctx context.Context, sv pool.Instance, err error) { 121 | label := sv.Config().Name 122 | info, exists := c.registerLabels[label] 123 | if !exists { 124 | return 125 | } 126 | c.lock.Lock() 127 | delete(c.matched, label+":ttl") 128 | c.lock.Unlock() 129 | 130 | if !info.Permanent { 131 | err = c.Client.Agent().ServiceDeregister(label) 132 | if err != nil { 133 | c.Log.Println("Can't deregister service", label, "in Consul:", err) 134 | } else { 135 | c.Log.Println("Service", label, "deregistered in Consul") 136 | } 137 | } 138 | } 139 | 140 | func (p *ConsulPlugin) OnFinished(ctx context.Context, sv pool.Instance) {} 141 | 142 | func (p *ConsulPlugin) MergeFrom(a interface{}) error { 143 | other := a.(*ConsulPlugin) 144 | def := DefaultConsul() 145 | 146 | if p.URL == def.URL { 147 | p.URL = other.URL 148 | } else if p.URL != def.URL && other.URL != def.URL && other.URL != p.URL { 149 | return errors.New("Different CONSUL definition (different URL) - specify same or only once") 150 | } 151 | 152 | if p.TTL == def.TTL { 153 | p.TTL = other.TTL 154 | } else if p.TTL != def.TTL && other.TTL != def.TTL && other.TTL != p.TTL { 155 | return errors.New("Different CONSUL definition (different TTL) - specify same or only once") 156 | } 157 | 158 | if p.AutoDeregistrationTimeout == def.AutoDeregistrationTimeout { 159 | p.AutoDeregistrationTimeout = other.AutoDeregistrationTimeout 160 | } else if p.AutoDeregistrationTimeout != def.AutoDeregistrationTimeout && 161 | other.AutoDeregistrationTimeout != def.AutoDeregistrationTimeout && 162 | other.AutoDeregistrationTimeout != p.AutoDeregistrationTimeout { 163 | return errors.New("Different CONSUL definition (different AutoDeregistrationTimeout) - specify same or only once") 164 | } 165 | 166 | p.Permanent = append(p.Permanent, other.Permanent...) 167 | p.Dynamic = append(p.Dynamic, other.Dynamic...) 168 | return nil 169 | 170 | } 171 | 172 | func (c *ConsulPlugin) checkLoop() { 173 | defer close(c.done) 174 | LOOP: 175 | for { 176 | select { 177 | case <-time.After(c.TTL / 2): 178 | c.updateChecks() 179 | case <-c.stop: 180 | break LOOP 181 | } 182 | } 183 | } 184 | 185 | func (c *ConsulPlugin) updateChecks() { 186 | c.lock.Lock() 187 | wg := sync.WaitGroup{} 188 | wg.Add(len(c.matched)) 189 | for id, _ := range c.matched { 190 | go func(id string) { 191 | defer wg.Done() 192 | err := c.Client.Agent().UpdateTTL(id, "application running", "pass") 193 | if err != nil { 194 | c.Log.Println("Can't update TTL for service", id, "in Consul:", err) 195 | } 196 | }(id) 197 | } 198 | c.lock.Unlock() 199 | wg.Wait() 200 | } 201 | 202 | func (c *ConsulPlugin) Close() error { 203 | close(c.stop) 204 | <-c.done 205 | return nil 206 | } 207 | 208 | // Define options for consul registration 209 | type consulRegistration struct { 210 | // Keep service registered even if stopped 211 | Permanent bool `json:"permanent,omitempty" yaml:"permanent,omitempty" ini:"permanent,omitempty"` 212 | // Name of service 213 | Label string `json:"label" yaml:"label" ini:"label"` 214 | } 215 | 216 | func init() { 217 | registerPlugin("consul", func(file string) PluginConfigNG { 218 | x := DefaultConsul() 219 | return &x 220 | }) 221 | } 222 | -------------------------------------------------------------------------------- /plugins/adp_critical.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "context" 5 | "github.com/reddec/monexec/pool" 6 | ) 7 | 8 | type Critical struct { 9 | Labels []string `mapstructure:""` 10 | } 11 | 12 | func (p *Critical) OnSpawned(ctx context.Context, sv pool.Instance) {} 13 | 14 | func (p *Critical) OnStarted(ctx context.Context, sv pool.Instance) {} 15 | 16 | func (p *Critical) OnStopped(ctx context.Context, sv pool.Instance, err error) {} 17 | 18 | func (p *Critical) OnFinished(ctx context.Context, sv pool.Instance) { 19 | terminate := false 20 | for _, l := range p.Labels { 21 | if l == sv.Supervisor().Config().Name { 22 | terminate = true 23 | break 24 | } 25 | } 26 | if terminate { 27 | go sv.Pool().Terminate() 28 | } 29 | } 30 | 31 | func (a *Critical) MergeFrom(other interface{}) (error) { 32 | b := other.(*Critical) 33 | a.Labels = append(a.Labels, b.Labels...) 34 | return nil 35 | } 36 | 37 | func (a *Critical) Prepare(ctx context.Context, pl *pool.Pool) error { 38 | return nil 39 | } 40 | func (a *Critical) Close() error { return nil } 41 | func init() { 42 | registerPlugin("critical", func(file string) PluginConfigNG { 43 | return &Critical{} 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /plugins/adp_email.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "net/smtp" 7 | "net" 8 | "errors" 9 | "path/filepath" 10 | "github.com/reddec/monexec/pool" 11 | "context" 12 | ) 13 | 14 | type Email struct { 15 | Smtp string `yaml:"smtp"` 16 | From string `yaml:"from"` 17 | Password string `yaml:"password"` 18 | To []string `yaml:"to"` 19 | Services []string `yaml:"services"` 20 | withTemplate `mapstructure:",squash" yaml:",inline"` 21 | 22 | log *log.Logger 23 | hostname string 24 | servicesSet map[string]bool 25 | workDir string 26 | } 27 | 28 | func (c *Email) renderAndSend(message string) { 29 | c.log.Println(message) 30 | host, _, _ := net.SplitHostPort(c.Smtp) 31 | auth := smtp.PlainAuth("", c.From, c.Password, host) 32 | err := smtp.SendMail(c.Smtp, auth, c.From, c.To, []byte(message)) 33 | if err != nil { 34 | c.log.Println("failed send mail:", err) 35 | } else { 36 | c.log.Println("sent") 37 | } 38 | } 39 | 40 | func (c *Email) OnSpawned(ctx context.Context, sv pool.Instance) {} 41 | 42 | func (c *Email) OnStarted(ctx context.Context, sv pool.Instance) { 43 | label := sv.Config().Name 44 | if c.servicesSet[label] { 45 | content, renderErr := c.renderDefault("spawned", label, label, nil, c.log) 46 | if renderErr != nil { 47 | c.log.Println("failed render:", renderErr) 48 | } else { 49 | c.renderAndSend(content) 50 | } 51 | } 52 | } 53 | 54 | func (c *Email) OnStopped(ctx context.Context, sv pool.Instance, err error) { 55 | label := sv.Config().Name 56 | if c.servicesSet[label] { 57 | content, renderErr := c.renderDefault("stopped", label, label, err, c.log) 58 | if renderErr != nil { 59 | c.log.Println("failed render:", renderErr) 60 | } else { 61 | c.renderAndSend(content) 62 | } 63 | } 64 | } 65 | 66 | func (p *Email) OnFinished(ctx context.Context, sv pool.Instance) {} 67 | 68 | func (c *Email) Prepare(ctx context.Context, pl *pool.Pool) error { 69 | c.servicesSet = makeSet(c.Services) 70 | c.log = log.New(os.Stderr, "[email] ", log.LstdFlags) 71 | c.hostname, _ = os.Hostname() 72 | return nil 73 | } 74 | 75 | func (a *Email) MergeFrom(other interface{}) (error) { 76 | b := other.(*Email) 77 | if a.From == "" { 78 | a.From = b.From 79 | } 80 | if a.From != b.From { 81 | return errors.New("different from address") 82 | } 83 | if a.Smtp == "" { 84 | a.Smtp = b.Smtp 85 | } 86 | if a.Smtp != b.Smtp { 87 | return errors.New("different smtp servers") 88 | } 89 | if a.Template == "" { 90 | a.Template = b.Template 91 | } 92 | if a.Template != b.Template { 93 | return errors.New("different templates") 94 | } 95 | a.TemplateFile = realPath(a.TemplateFile, a.workDir) 96 | b.TemplateFile = realPath(b.TemplateFile, b.workDir) 97 | if a.TemplateFile == "" { 98 | a.TemplateFile = b.TemplateFile 99 | } 100 | if a.TemplateFile != b.TemplateFile { 101 | return errors.New("different template files") 102 | } 103 | if a.Password == "" { 104 | a.Password = b.Password 105 | } 106 | if a.Password != b.Password { 107 | return errors.New("different password") 108 | } 109 | a.To = unique(append(a.To, b.To...)) 110 | a.Services = append(a.Services, b.Services...) 111 | return nil 112 | } 113 | func (a *Email) Close() error { return nil } 114 | func init() { 115 | registerPlugin("email", func(file string) PluginConfigNG { 116 | return &Email{workDir: filepath.Dir(file)} 117 | }) 118 | } 119 | -------------------------------------------------------------------------------- /plugins/adp_http.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "github.com/pkg/errors" 7 | "path/filepath" 8 | "net/http" 9 | "bytes" 10 | "github.com/Masterminds/sprig" 11 | "html/template" 12 | "io" 13 | "time" 14 | "context" 15 | "github.com/reddec/monexec/pool" 16 | ) 17 | 18 | type Http struct { 19 | URL string `yaml:"url" mapstructure:"url"` // template URL string 20 | Method string `yaml:"method"` // default POST 21 | Headers map[string]string `yaml:"headers" mapstructure:"headers"` // additional header (non-template) 22 | Services []string `yaml:"services"` 23 | Timeout time.Duration `yaml:"timeout"` 24 | withTemplate `mapstructure:",squash" yaml:",inline"` 25 | log *log.Logger `yaml:"-"` 26 | servicesSet map[string]bool 27 | workDir string 28 | } 29 | 30 | func (c *Http) renderAndSend(message string, params map[string]interface{}) { 31 | c.log.Println(message) 32 | 33 | tpl, err := template.New("").Funcs(sprig.FuncMap()).Parse(string(c.URL)) 34 | if err != nil { 35 | c.log.Println("failed parse URL as template:", err) 36 | return 37 | } 38 | urlM := &bytes.Buffer{} 39 | err = tpl.Execute(urlM, params) 40 | if err != nil { 41 | c.log.Println("failed execute URL as template:", err) 42 | return 43 | } 44 | 45 | req, err := http.NewRequest(c.Method, urlM.String(), bytes.NewBufferString(message)) 46 | if err != nil { 47 | c.log.Println("failed prepare request:", err) 48 | return 49 | } 50 | 51 | for k, v := range c.Headers { 52 | req.Header.Set(k,v) 53 | } 54 | 55 | ctx, closer := context.WithTimeout(context.Background(), c.Timeout) 56 | defer closer() 57 | 58 | res, err := http.DefaultClient.Do(req.WithContext(ctx)) 59 | if err != nil { 60 | c.log.Println("failed make request:", err) 61 | return 62 | } 63 | io.Copy(os.Stdout, res.Body) // allow keep-alive 64 | res.Body.Close() 65 | } 66 | 67 | func (p *Http) OnSpawned(ctx context.Context, sv pool.Instance) {} 68 | 69 | func (c *Http) OnStarted(ctx context.Context, sv pool.Instance) { 70 | label := sv.Config().Name 71 | if c.servicesSet[label] { 72 | content, params, renderErr := c.renderDefaultParams("spawned", label, label, nil, c.log) 73 | if renderErr != nil { 74 | c.log.Println("failed render:", renderErr) 75 | } else { 76 | c.renderAndSend(content, params) 77 | } 78 | } 79 | } 80 | 81 | func (c *Http) OnStopped(ctx context.Context, sv pool.Instance, err error) { 82 | label := sv.Config().Name 83 | if c.servicesSet[label] { 84 | content, params, renderErr := c.renderDefaultParams("stopped", label, label, err, c.log) 85 | if renderErr != nil { 86 | c.log.Println("failed render:", renderErr) 87 | } else { 88 | c.renderAndSend(content, params) 89 | } 90 | } 91 | } 92 | 93 | func (p *Http) OnFinished(ctx context.Context, sv pool.Instance) {} 94 | func (a *Http) Close() error { return nil } 95 | func (c *Http) Prepare(ctx context.Context, pl *pool.Pool) error { 96 | c.servicesSet = makeSet(c.Services) 97 | c.log = log.New(os.Stderr, "[http] ", log.LstdFlags) 98 | if c.Method == "" { 99 | c.Method = "POST" 100 | } 101 | if c.Timeout == 0 { 102 | c.Timeout = 20 * time.Second 103 | } 104 | return nil 105 | } 106 | 107 | func (a *Http) MergeFrom(other interface{}) (error) { 108 | b := other.(*Http) 109 | if a.URL == "" { 110 | a.URL = b.URL 111 | } 112 | if a.URL != b.URL { 113 | return errors.New("different urls") 114 | } 115 | if a.Method == "" { 116 | a.Method = b.Method 117 | } 118 | if a.Method != b.Method { 119 | return errors.New("different methods") 120 | } 121 | if a.Timeout == 0 { 122 | a.Timeout = b.Timeout 123 | } 124 | if a.Timeout != b.Timeout { 125 | return errors.New("different timeout") 126 | } 127 | a.withTemplate.resolvePath(a.workDir) 128 | b.withTemplate.resolvePath(b.workDir) 129 | if err := a.withTemplate.MergeFrom(&b.withTemplate); err != nil { 130 | return err 131 | } 132 | if a.Headers == nil { 133 | a.Headers = make(map[string]string) 134 | } 135 | for k, v := range b.Headers { 136 | a.Headers[k] = v 137 | } 138 | a.Services = append(a.Services, b.Services...) 139 | return nil 140 | } 141 | 142 | func init() { 143 | registerPlugin("http", func(file string) PluginConfigNG { 144 | return &Http{workDir: filepath.Dir(file)} 145 | }) 146 | } 147 | -------------------------------------------------------------------------------- /plugins/adp_rest.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/elazarl/go-bindata-assetfs" 7 | "github.com/gin-gonic/gin" 8 | "github.com/pkg/errors" 9 | "github.com/reddec/monexec/pool" 10 | "io" 11 | "net/http" 12 | "os" 13 | "time" 14 | ) 15 | 16 | const restApiStartupCheck = 1 * time.Second 17 | 18 | //go:generate go-bindata -pkg plugins -prefix ../ui/dist/ ../ui/dist/ 19 | type RestPlugin struct { 20 | Listen string `yaml:"listen"` 21 | CORS bool `yaml:"cors"` 22 | server *http.Server 23 | } 24 | 25 | func (p *RestPlugin) Prepare(ctx context.Context, pl *pool.Pool) error { 26 | 27 | router := gin.Default() 28 | if p.CORS { 29 | router.Use(CORSMiddleware()) 30 | } 31 | router.StaticFS("/ui/", &assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: AssetInfo, Prefix: ""}) 32 | router.GET("/", func(gctx *gin.Context) { 33 | gctx.Redirect(http.StatusTemporaryRedirect, "ui") 34 | }) 35 | router.GET("/supervisors", func(gctx *gin.Context) { 36 | var names = make([]string, 0) 37 | for _, sv := range pl.Supervisors() { 38 | names = append(names, sv.Config().Name) 39 | } 40 | gctx.JSON(http.StatusOK, names) 41 | }) 42 | router.GET("/supervisor/:name", func(gctx *gin.Context) { 43 | name := gctx.Param("name") 44 | for _, sv := range pl.Supervisors() { 45 | if sv.Config().Name == name { 46 | gctx.JSON(http.StatusOK, sv.Config()) 47 | return 48 | } 49 | } 50 | gctx.AbortWithStatus(http.StatusNotFound) 51 | }) 52 | router.GET("/supervisor/:name/log", func(gctx *gin.Context) { 53 | name := gctx.Param("name") 54 | for _, sv := range pl.Supervisors() { 55 | if sv.Config().Name == name { 56 | if sv.Config().LogFile == "" { 57 | break 58 | } 59 | f, err := os.Open(sv.Config().LogFile) 60 | if err != nil { 61 | gctx.AbortWithError(http.StatusBadGateway, err) 62 | return 63 | } 64 | defer f.Close() 65 | gctx.Header("Content-Type", "text/plain") 66 | gctx.Header("Content-Disposition", "attachment; filename=\""+sv.Config().Name+".log\"") 67 | gctx.AbortWithStatus(http.StatusOK) 68 | io.Copy(gctx.Writer, f) 69 | return 70 | } 71 | } 72 | gctx.AbortWithStatus(http.StatusNotFound) 73 | }) 74 | router.POST("/supervisor/:name", func(gctx *gin.Context) { 75 | name := gctx.Param("name") 76 | for _, sv := range pl.Supervisors() { 77 | if sv.Config().Name == name { 78 | in := pl.Start(ctx, sv) 79 | gctx.JSON(http.StatusOK, in) 80 | return 81 | } 82 | } 83 | gctx.AbortWithStatus(http.StatusNotFound) 84 | }) 85 | router.GET("/instances", func(gctx *gin.Context) { 86 | var names = make([]string, 0) 87 | for _, sv := range pl.Instances() { 88 | names = append(names, sv.Config().Name) 89 | } 90 | gctx.JSON(http.StatusOK, names) 91 | }) 92 | 93 | router.GET("/instance/:name", func(gctx *gin.Context) { 94 | name := gctx.Param("name") 95 | for _, sv := range pl.Instances() { 96 | if sv.Config().Name == name { 97 | gctx.JSON(http.StatusOK, sv) 98 | return 99 | } 100 | } 101 | gctx.AbortWithStatus(http.StatusNotFound) 102 | }) 103 | 104 | router.POST("/instance/:name", func(gctx *gin.Context) { 105 | name := gctx.Param("name") 106 | for _, sv := range pl.Instances() { 107 | if sv.Config().Name == name { 108 | pl.Stop(sv) 109 | gctx.AbortWithStatus(http.StatusCreated) 110 | return 111 | } 112 | } 113 | gctx.AbortWithStatus(http.StatusNotFound) 114 | }) 115 | 116 | p.server = &http.Server{Addr: p.Listen, Handler: router} 117 | fmt.Println("rest interface will be available on", p.Listen) 118 | start := make(chan error, 1) 119 | go func() { 120 | start <- p.server.ListenAndServe() 121 | }() 122 | select { 123 | case err := <-start: 124 | return err 125 | case <-time.After(restApiStartupCheck): 126 | return nil 127 | } 128 | } 129 | 130 | func (p *RestPlugin) OnSpawned(ctx context.Context, sv pool.Instance) {} 131 | 132 | func (p *RestPlugin) OnStarted(ctx context.Context, sv pool.Instance) {} 133 | 134 | func (p *RestPlugin) OnStopped(ctx context.Context, sv pool.Instance, err error) {} 135 | 136 | func (p *RestPlugin) OnFinished(ctx context.Context, sv pool.Instance) {} 137 | 138 | func (p *RestPlugin) MergeFrom(o interface{}) error { 139 | def := defaultRestPlugin() 140 | other := o.(*RestPlugin) 141 | if p.Listen == def.Listen { 142 | p.Listen = other.Listen 143 | } else if other.Listen != def.Listen && other.Listen != p.Listen { 144 | return errors.Errorf("unmatched Rest listen address %v != %v", p.Listen, other.Listen) 145 | } 146 | return nil 147 | } 148 | 149 | func (a *RestPlugin) Close() error { 150 | ctx, closer := context.WithTimeout(context.Background(), 1*time.Second) 151 | defer closer() 152 | return a.server.Shutdown(ctx) 153 | } 154 | 155 | func defaultRestPlugin() *RestPlugin { 156 | return &RestPlugin{ 157 | Listen: "localhost:9900", 158 | } 159 | } 160 | 161 | func CORSMiddleware() gin.HandlerFunc { 162 | return func(c *gin.Context) { 163 | c.Writer.Header().Set("Access-Control-Allow-Origin", "*") 164 | c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") 165 | c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") 166 | c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT") 167 | 168 | if c.Request.Method == "OPTIONS" { 169 | c.AbortWithStatus(204) 170 | return 171 | } 172 | 173 | c.Next() 174 | } 175 | } 176 | func init() { 177 | registerPlugin("rest", func(file string) PluginConfigNG { 178 | return defaultRestPlugin() 179 | }) 180 | } 181 | -------------------------------------------------------------------------------- /plugins/adp_telegram.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "gopkg.in/telegram-bot-api.v4" 7 | "errors" 8 | "path/filepath" 9 | "github.com/reddec/monexec/pool" 10 | "context" 11 | ) 12 | 13 | type Telegram struct { 14 | Token string `yaml:"token"` 15 | Recipients []int64 `yaml:"recipients"` 16 | Services []string `yaml:"services"` 17 | withTemplate `mapstructure:",squash" yaml:",inline"` 18 | 19 | servicesSet map[string]bool `yaml:"-"` 20 | logger *log.Logger `yaml:"-"` 21 | bot *tgbotapi.BotAPI `yaml:"-"` 22 | workDir string 23 | hostname string 24 | } 25 | 26 | func (c *Telegram) Prepare(ctx context.Context, pl *pool.Pool) error { 27 | c.servicesSet = make(map[string]bool) 28 | for _, srv := range c.Services { 29 | c.servicesSet[srv] = true 30 | } 31 | c.logger = log.New(os.Stderr, "[telegram] ", log.LstdFlags) 32 | bot, err := tgbotapi.NewBotAPI(c.Token) 33 | if err != nil { 34 | return err 35 | } 36 | c.bot = bot 37 | c.hostname, _ = os.Hostname() 38 | return nil 39 | } 40 | 41 | func (p *Telegram) OnSpawned(ctx context.Context, sv pool.Instance) {} 42 | 43 | func (c *Telegram) OnStarted(ctx context.Context, sv pool.Instance) { 44 | if c.servicesSet[sv.Config().Name] { 45 | content, renderErr := c.renderDefault("spawned", string(sv.Config().Name), sv.Config().Name, nil, c.logger) 46 | if renderErr != nil { 47 | c.logger.Println("failed render:", renderErr) 48 | } else { 49 | c.renderAndSend(content) 50 | } 51 | } 52 | } 53 | 54 | func (c *Telegram) OnStopped(ctx context.Context, sv pool.Instance, err error) { 55 | if c.servicesSet[sv.Config().Name] { 56 | content, renderErr := c.renderDefault("stopped", string(sv.Config().Name), sv.Config().Name, err, c.logger) 57 | if renderErr != nil { 58 | c.logger.Println("failed render:", renderErr) 59 | } else { 60 | c.renderAndSend(content) 61 | } 62 | } 63 | } 64 | 65 | func (p *Telegram) OnFinished(ctx context.Context, sv pool.Instance) {} 66 | 67 | func (c *Telegram) renderAndSend(message string) { 68 | msg := tgbotapi.NewMessage(0, message) 69 | msg.ParseMode = "markdown" 70 | for _, r := range c.Recipients { 71 | msg.ChatID = r 72 | _, err := c.bot.Send(msg) 73 | if err != nil { 74 | c.logger.Println("failed send message to", r, "due to", err) 75 | } 76 | } 77 | } 78 | 79 | func (a *Telegram) MergeFrom(other interface{}) (error) { 80 | b := other.(*Telegram) 81 | if a.Token == "" { 82 | a.Token = b.Token 83 | } 84 | if a.Token != b.Token { 85 | return errors.New("token are different") 86 | } 87 | a.withTemplate.resolvePath(a.workDir) 88 | b.withTemplate.resolvePath(b.workDir) 89 | if err := a.withTemplate.MergeFrom(&b.withTemplate); err != nil { 90 | return err 91 | } 92 | a.Recipients = append(a.Recipients, b.Recipients...) 93 | a.Services = append(a.Services, b.Services...) 94 | return nil 95 | } 96 | func (a *Telegram) Close() error { return nil } 97 | func init() { 98 | registerPlugin("telegram", func(file string) PluginConfigNG { 99 | return &Telegram{workDir: filepath.Dir(file)} 100 | }) 101 | } 102 | -------------------------------------------------------------------------------- /plugins/bindata.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bindata. 2 | // sources: 3 | // ../ui/dist/index.html 4 | // ../ui/dist/main.js 5 | // DO NOT EDIT! 6 | 7 | package plugins 8 | 9 | import ( 10 | "bytes" 11 | "compress/gzip" 12 | "fmt" 13 | "io" 14 | "io/ioutil" 15 | "os" 16 | "path/filepath" 17 | "strings" 18 | "time" 19 | ) 20 | 21 | func bindataRead(data []byte, name string) ([]byte, error) { 22 | gz, err := gzip.NewReader(bytes.NewBuffer(data)) 23 | if err != nil { 24 | return nil, fmt.Errorf("Read %q: %v", name, err) 25 | } 26 | 27 | var buf bytes.Buffer 28 | _, err = io.Copy(&buf, gz) 29 | clErr := gz.Close() 30 | 31 | if err != nil { 32 | return nil, fmt.Errorf("Read %q: %v", name, err) 33 | } 34 | if clErr != nil { 35 | return nil, err 36 | } 37 | 38 | return buf.Bytes(), nil 39 | } 40 | 41 | type asset struct { 42 | bytes []byte 43 | info os.FileInfo 44 | } 45 | 46 | type bindataFileInfo struct { 47 | name string 48 | size int64 49 | mode os.FileMode 50 | modTime time.Time 51 | } 52 | 53 | func (fi bindataFileInfo) Name() string { 54 | return fi.name 55 | } 56 | func (fi bindataFileInfo) Size() int64 { 57 | return fi.size 58 | } 59 | func (fi bindataFileInfo) Mode() os.FileMode { 60 | return fi.mode 61 | } 62 | func (fi bindataFileInfo) ModTime() time.Time { 63 | return fi.modTime 64 | } 65 | func (fi bindataFileInfo) IsDir() bool { 66 | return false 67 | } 68 | func (fi bindataFileInfo) Sys() interface{} { 69 | return nil 70 | } 71 | 72 | var _indexHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x3c\x8f\xb1\x8e\xc2\x30\x0c\x86\x5f\xc5\x97\xfd\x2e\xeb\x0d\x8e\x97\x3b\xd8\x10\x0c\x65\x60\x0c\x89\x45\x53\xb5\x69\x15\x9b\xa8\x7d\x7b\x54\x5a\x31\xd9\xdf\x27\xd9\xbf\x7e\xfc\xfa\x3f\xff\x35\xb7\xcb\x01\x5a\x1d\x7a\x02\x5c\x07\xf4\x3e\x3f\x1c\xe7\x15\xd9\x47\x02\x1c\x58\x3d\x84\xd6\x17\x61\x75\xd7\xe6\xf8\xfd\x4b\x80\x9a\xb4\x67\x3a\x8d\x99\x67\x0e\x68\x37\x04\xb4\xfb\xcd\x7d\x8c\x0b\x01\xc6\x54\x21\x45\x27\xcf\x89\x4b\x4d\x32\x16\x21\xb4\x31\x55\x02\x94\x50\xd2\xa4\xa0\xcb\xc4\xce\x28\xcf\x6a\x3b\x5f\xfd\x66\x0d\x48\x09\xce\x0c\x3e\xe5\x9f\x4e\x0c\xa1\xdd\x3c\xa1\xdd\x1f\x7f\x78\x5f\xd6\xe4\x77\x87\x57\x00\x00\x00\xff\xff\x65\x5a\x0d\x38\xd4\x00\x00\x00") 73 | 74 | func indexHtmlBytes() ([]byte, error) { 75 | return bindataRead( 76 | _indexHtml, 77 | "index.html", 78 | ) 79 | } 80 | 81 | func indexHtml() (*asset, error) { 82 | bytes, err := indexHtmlBytes() 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | info := bindataFileInfo{name: "index.html", size: 212, mode: os.FileMode(420), modTime: time.Unix(1530190485, 0)} 88 | a := &asset{bytes: bytes, info: info} 89 | return a, nil 90 | } 91 | 92 | var _mainJs = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\x3b\xed\x72\xdb\x38\x92\xaf\x22\xf3\x66\x39\x40\x05\xe6\xc8\xd9\xd9\xab\x39\x6a\x10\x5f\x26\xe3\x4c\x9c\xf5\x24\xb3\xf9\xd8\xdd\x2a\x9f\xcb\x43\x91\x2d\x09\x36\x05\x28\x20\x24\xcb\x23\xf3\x7e\xdf\x53\x5c\xdd\xb3\xdc\xa3\xdc\x93\x5c\x35\x40\x90\xa0\x3e\xf2\x71\x53\xb5\x75\xa9\x94\x49\x02\x8d\x06\xfa\x13\xdd\x40\xeb\x68\xb2\x94\xb9\x11\x4a\x12\xa0\x9b\x55\xa6\x07\x86\x6f\xea\x91\x6f\x1c\x48\x22\xe8\x46\x4c\x88\xb9\x14\x57\x54\x83\x59\x6a\x39\xc0\xf7\x04\xd6\x0b\xa5\x4d\x35\xc2\x21\x9a\x63\x13\xdf\x88\x54\xb0\x32\x3d\x3a\x61\x4d\x67\xba\xa9\xeb\x51\x33\x08\x70\x50\x9e\x95\x25\xd1\x7e\x2c\xd3\xac\x7b\x97\x94\xe9\xa4\xe4\x47\xc3\xae\xad\x96\xc9\x9c\x03\x93\x49\xce\x0d\x93\x49\xc1\xbb\xa5\x32\xc3\x04\xdd\xc8\x44\xe1\x2b\x7d\x78\x78\x3d\xbe\x81\xdc\x24\x05\x4c\x84\x84\x5f\xb4\x5a\x80\x36\xf7\x16\x6c\x03\x72\x39\x07\x9d\x8d\x4b\x48\x8f\x86\x6c\x0a\x26\x15\x35\xad\x99\x4c\x34\x0f\x49\x8f\x96\xd2\x8d\x2e\xa2\x23\x6e\xee\x17\xa0\x26\x83\xb7\xf7\xf3\xb1\x2a\xe3\xd8\x3d\x13\xa3\xde\x1a\x2d\xe4\xf4\x5d\x36\x8d\xe3\x43\x33\xee\xc2\xb2\xcd\x2a\x2b\x97\x90\x46\x3f\xab\x62\x59\x42\x54\x53\x76\x68\x70\x74\x7d\x0d\x55\x03\xe6\x87\x1d\x0d\xdd\x72\x4d\x8f\x7c\x2b\x94\x93\xd8\xc4\x31\x01\x8e\x04\x50\xf6\x5d\x6c\xbc\x84\x60\x24\x26\xe4\x5b\xec\x8d\x94\x9d\x2a\xe2\x9e\x26\x88\x63\xfc\x9f\x74\x33\x75\x83\x50\x96\x82\x37\x8b\xcb\x35\x64\x06\x88\x5c\x96\x25\x45\x74\x32\xd1\x44\x1c\x5a\xba\x60\x51\x01\x93\x6c\x59\x9a\x68\x9b\xe3\x8e\x0a\xa8\x29\x7b\x6c\x17\x54\x59\xbe\x74\x4c\x06\x3a\x51\x9a\x58\x35\x1a\x08\x39\x00\x2a\x93\x82\x08\xa6\x59\x4b\xae\xa1\x9b\x56\x89\xcc\x55\x9d\x8c\x85\x2c\xec\xba\x98\xa6\xd4\xeb\x97\x40\x1e\x49\xbe\xab\xcd\x5b\xd4\x9e\xb6\x10\x1d\xd6\xa4\x59\x7b\x9d\xee\xe9\x6c\x35\x18\xd7\x65\x58\x94\x45\xcc\x50\x66\x70\x3a\xb5\x25\x92\x06\xb0\x61\xd1\x42\x2b\xa3\x90\xc8\x64\x96\x55\xaf\xef\xa4\x67\x96\xb3\x02\x1c\x80\x38\x16\x3c\x8a\x98\x24\x32\xa9\xf8\x90\xd6\xe4\xb2\xa7\xe3\x12\xf5\xb2\x82\x01\xf2\x2c\x37\x51\x67\x96\x82\xd0\x4d\xdd\x7e\x69\x37\xbd\xe7\xa3\x44\x3e\x1a\x0a\x97\xf2\x8a\x9b\x4b\x79\xd5\x9a\x60\x37\x42\x1d\x1e\x71\xb2\x07\xbc\x72\xe0\x26\xc9\x16\x0b\x90\xc5\xb3\x99\x28\x0b\x02\xb4\x03\xc8\xfc\x72\x4d\x22\x64\x05\xda\xfc\x00\x13\xa5\x81\x00\x93\x01\x54\x8e\x52\x81\x64\x91\x69\x90\xe6\x95\x2a\x20\xd1\x30\x57\x2b\xd8\xc5\x57\x6e\xad\x8f\x0f\x47\xf2\x7b\x48\x4a\x90\x53\x33\x1b\xc9\x47\xfc\xc4\x2e\x36\x8e\xf1\x2f\xca\x25\x18\x3b\xc1\x59\x1a\x1a\x0a\x95\x2f\xe7\x20\xbd\x36\x9f\x95\x80\x5f\xbd\xa9\x96\x87\xc1\xdf\xc1\xda\x2e\xb3\x07\x5f\x90\x43\xe0\xcf\xd4\xdc\x62\x8f\xa2\x00\x7c\xe6\x39\x03\x49\x56\x14\x67\x2b\x90\xe6\x42\x54\x06\x24\x68\x62\x98\x64\x47\x27\x01\xf0\xb4\x03\x76\x9c\xf9\x04\xfc\xbc\x83\xaf\xcc\x7d\x09\x49\x05\xa6\xb5\x49\xec\xa8\xd1\x6a\x0d\xb5\x96\xbd\xe0\x1b\xbd\x94\x52\xc8\x29\xba\x68\xa3\x33\x59\x09\xc4\x52\xa5\x97\x57\x6c\xac\x96\xb2\x48\xad\x51\x59\x4c\xd5\x0c\xc0\xb8\xef\x2c\x37\x62\x05\x6f\x96\x25\xa0\x43\x67\x0b\xad\xe6\xa2\x82\xa6\xaf\x40\xb9\x6d\xcc\x4c\x54\x49\x80\x31\x59\x2c\xab\x19\x01\xca\x6c\x47\x33\xeb\xc3\x03\x09\x3f\xad\xab\x87\x0f\x4b\xa8\xcc\x53\x29\xe6\x19\x0e\x7c\xae\xb3\x39\x38\x28\xbb\x20\x3f\xc4\x7e\x70\xfb\x2a\x61\x6d\x9c\x07\xc0\x4f\x4a\x29\xad\x71\x15\xb8\xbc\xd6\x2f\x1e\x59\xc8\x8e\x0e\xba\xc9\x95\xac\xcc\x00\xf8\x84\x44\xb6\x39\xa2\xa3\x56\x78\x33\xc8\x8a\x2d\xc5\x66\x8b\x60\x34\x87\xc4\x3e\x6b\x8b\x35\xe0\xc6\xa5\xb9\xf2\x0b\xec\xb7\x22\x69\x5b\x4b\x68\xcc\xc2\x2e\xf3\xd7\x7f\xbd\x85\xfb\x09\x92\x5a\x0d\xbe\xda\x98\x7a\xf0\xd5\x06\xea\x5f\x77\x46\xe4\x55\x65\x11\x36\x5a\x8f\x84\x22\xf1\xa4\x61\x77\xcb\xc6\x93\x91\xa7\xee\x4e\xc8\x42\xdd\x25\x0b\xd0\x13\xa5\xe7\x99\xcc\x21\x91\xea\x8e\xd0\x51\x09\x66\x60\xf8\x8e\x94\x1a\x73\x42\x2b\x1b\x99\xe3\xe3\x91\xe7\x93\xdc\x01\xbd\x34\x57\x23\x89\x1e\x6d\xaa\xb3\x79\x1c\xc3\x13\xde\x7e\x25\x20\x8b\x38\x96\x49\xa1\x24\x10\x8a\x1e\x0d\x64\x21\xe4\xd4\x43\xb9\xaf\xa4\x32\x99\x36\x08\x67\x5f\x48\xdb\x81\x23\x1a\x52\x4e\x89\x4c\x96\x8b\x02\x77\x9d\x2d\xd5\xe1\x47\x43\x9a\xb6\x43\x1e\x1e\x76\x28\xa9\x16\xa5\xc8\x81\x18\x76\x42\x6b\x8c\x56\x82\xb1\xf4\x53\x4a\x46\x47\x50\x56\x30\xf0\xc3\x42\xb5\x41\xbe\x01\xff\x84\x60\x1c\xff\x00\xf9\xb7\x0d\x59\x40\x09\x06\x9c\x6e\xd2\xd1\xb6\xa6\xf0\x4d\x5d\xd7\x2c\x84\x41\xfd\xf5\xa6\x9c\xf9\xf5\xf2\x9d\x16\x4b\xae\x21\x11\x1b\x44\x34\x99\x88\xd2\x80\x26\xc0\x9f\x40\x1c\x1f\x9f\x70\xce\x21\x11\xb2\x80\xf5\xeb\x09\x31\x94\x26\x37\x4a\x48\x07\x5a\xb3\xa9\x56\xcb\xc5\xeb\xa5\xd1\xaa\xf2\x6a\xa4\xec\x17\xdf\x68\x98\x67\xc2\x3a\x87\x21\xc3\x0d\x6a\x9c\xe5\xb7\xe8\x19\xea\x9a\xdd\x65\xc2\xa4\x84\xf2\x27\x64\x91\x34\xd6\xff\xf0\xd0\xbd\xf3\x5f\xdc\x33\xd1\x50\xa9\x72\x85\x4a\xd0\xf6\x25\x66\x06\x92\xe0\xd8\x4d\x07\x8f\x9e\xa3\xa6\x01\x14\x0d\x02\xce\x95\xe3\x82\xf3\xfa\x26\x31\xea\x16\x64\x2f\x22\x15\x04\x98\x60\x8a\x55\x2e\x2e\x75\x10\x47\x9c\xcb\x26\x8e\x19\x19\xbf\x90\x82\xab\x38\xde\x5c\xaa\xab\xb4\xaa\x1b\x1b\xc9\xb8\x26\x9a\x6c\x6a\x66\x92\xdc\xac\x29\xeb\x60\x29\xcb\x31\x54\x20\x26\xc9\x97\x1a\xf7\x27\x0e\x14\x3f\xd4\x7c\xa1\x24\x48\xc3\x32\x3a\x32\xc9\xb8\x54\xf9\xad\x85\xb2\x6f\xd5\xa9\x7f\x49\x26\x4a\x9f\x65\xf9\x8c\xd8\x2d\x8f\x3f\xd9\xe0\x92\x84\x8d\xb5\xc8\x22\xe9\xf1\x9d\x41\xa2\x1c\x4b\x20\x29\xc8\x09\x2e\xc2\xe1\xc0\xdd\xb7\xe1\x4d\x4d\xd3\xa6\xd5\x81\xe4\x49\x4e\x28\xcb\x2f\xf3\x44\x9c\x46\x22\x4a\xa3\x79\x74\x45\x4c\x32\x57\x4b\x69\x08\x62\xc8\x64\x3e\x53\x1a\xdf\xda\x15\x27\x5a\x29\x83\x3b\x02\xd9\x20\xb7\x1b\x7c\x3c\x6f\xe7\x0b\xe8\xc0\xf0\x3d\xb7\xa6\x13\x06\x50\x3e\x10\x88\xe3\xc8\x37\x07\x81\xa4\x95\x6d\x8d\xa1\x27\x0a\xa2\x11\x35\xf0\x27\x1b\x81\x52\x99\x81\x64\x27\xcc\x24\x36\xfc\x63\x40\x6b\xe6\xbb\xf2\xcc\xe4\x33\xf6\x98\x99\x04\xb4\x56\x1a\xfb\xec\xba\x1d\xdb\x8f\x38\x37\xad\x6f\xf0\xb1\x1d\x69\x9b\xd8\x90\xb2\xa3\x61\x8d\x16\xeb\xc4\x1f\x0e\xc3\x49\xc3\x31\x3b\x8b\x60\x47\xc3\x50\x3d\x36\x97\x4d\xd7\x55\x0a\x75\xdd\x6d\xab\xe3\x76\x4f\x2b\xa0\x32\x5a\xdd\x73\xe1\xdc\xd1\x44\x68\x20\x51\xd3\x18\x35\x3e\xaa\x02\xe3\xfb\xaf\x27\x3a\x9b\xda\x4d\xa5\x20\x47\x27\x47\x9c\x7b\x3f\xd6\x76\x58\x01\x37\x6d\x95\xc9\x0c\xa0\x13\x68\x27\xbe\xee\xc5\x91\x70\xc4\xe1\xd4\x70\x6e\x52\x40\xf2\x1e\x1e\x60\x5f\x3c\xff\xf0\xb0\x4f\x38\x1d\xce\x9b\xd0\xa0\xc0\x06\x7a\x76\xf6\x59\x26\x8b\x12\x74\x15\xc7\xfd\xef\x4b\xb8\x4a\x2a\xeb\x4d\x5d\xd8\xdf\x86\xe7\x82\x0f\x47\xe2\x7b\xe9\xbd\x9e\xc0\x20\x6c\xe3\xd2\x3f\x79\x29\xae\x46\x3a\xb9\xbe\x46\xcf\xe1\xb6\xfa\xe0\xcb\x25\x75\x36\xea\xc5\xa9\x30\x7c\xee\xf5\x9e\xd0\x80\x05\xf7\x5d\x78\x15\x70\xa9\xeb\xbf\xf5\x5e\xb2\x5d\xf0\xbe\x84\x85\x41\x72\x8d\x41\x02\x37\xee\x89\x36\xb7\xb0\x5b\x05\x37\x0c\xac\x65\x70\x67\x20\x0f\x0f\xc0\xd0\xc1\x2a\x0d\xdc\xb8\xe7\xc3\x03\x34\xb6\x83\x5f\xdd\xd4\xeb\x9e\x6b\xda\x66\x9a\x0f\x05\xc2\x36\x7e\x79\x45\xbb\xf4\xc1\x06\x45\x86\xb2\x4d\x8e\x5b\x73\x19\x26\x1b\x88\x13\xb8\x0c\x1c\xf7\xe8\xdf\xc1\xee\x96\x6e\x63\x03\xdc\xd8\x02\x2e\xbd\x6a\x35\xf4\x1a\x0d\xdc\xba\x34\xcc\x01\xdd\xc6\x87\x6b\xbf\x46\xbb\x6e\x43\xae\xb6\x05\x65\xf1\x3a\x6c\x1c\xdb\x40\xdd\xf1\x8e\xf6\xbb\x94\xdc\xdb\x9c\x4d\x0c\x68\xdf\xb3\x83\x3e\x0c\x4f\xef\xba\x2c\x2c\x90\x25\x43\x7f\xce\x04\x06\x2e\x5e\xb5\x94\xcb\xfc\x1c\x50\x21\x26\x13\xd0\x15\x81\x4b\x75\xc5\xcc\xa5\xba\xa2\x71\x4c\xe4\xa5\xba\xe2\x02\xe3\x80\x91\x40\xbf\x15\x18\x90\xf7\xe8\x94\xb5\xd6\xa6\x01\xbd\xe0\x12\xb5\x21\xb4\x35\xdf\x8d\x1a\xe1\xd5\xde\x25\x93\xfb\xa0\xbc\xc9\xfa\xe9\x9c\xf1\x5b\x90\x88\x6d\xf2\x59\x26\xa7\x50\xa4\x92\x35\x1e\x28\x0d\x49\x5c\x68\x58\x09\xb5\xac\x52\x53\x6f\xa3\x4b\x16\x7b\xa7\x73\xd8\x5d\xf8\xf3\x45\xe8\x69\xc0\xf0\xd7\xc8\x70\x17\x8e\x60\xd6\xdb\x58\x2a\xc5\x00\x56\x4c\x0c\xa1\x24\x80\x7d\xd3\xe4\x74\xbd\xc5\x5d\x6e\xad\x35\xd8\x6d\x80\x99\x87\x07\x6b\x5c\x35\x8a\xec\x2d\xdf\x34\x7e\x30\x1d\xdb\x53\x95\x7b\x86\x24\xa4\x37\x4c\xc9\x74\xcd\x2a\x30\xe9\x2b\xd6\x09\x22\x15\x0c\x35\x35\xbd\x63\xd7\x76\xd3\x4a\xdf\x30\x2f\xe8\xf4\xba\xb6\xf9\xc9\x33\xbe\x99\x80\xc9\x67\x6f\x91\x44\xd2\xc4\xa2\xf8\x77\x22\xa6\x29\xb0\x71\x56\x41\x6a\x6a\xa7\x49\x53\x30\xa4\xb5\x2c\x3b\x8a\xfc\x8a\xa1\xf4\x37\x42\x56\x06\xad\xeb\x9b\x5f\x1f\x81\xcc\x55\x01\xef\xdf\x9c\x3f\xf3\x3b\x22\x81\xe4\x55\x36\x07\xca\x36\x73\x30\x33\x55\xa4\xd1\x14\x4c\x54\xd3\x76\xeb\xfa\x76\xf8\xad\x8d\x9f\x90\xcb\xcb\xea\x34\xc8\x97\xea\x14\x92\x9b\x0a\x6d\xb5\x83\xde\x78\xf7\x4f\x5a\x40\xf0\x51\x67\x4d\x6b\xea\x76\x3a\x62\xf8\x13\x4b\x8b\x2a\xc1\xed\x78\xcd\x32\x98\xa1\x35\xad\x99\x8b\x87\x3d\xb9\x2d\x22\xd6\x10\x6e\x1c\xe1\xb2\x47\x38\xe6\x38\xed\xb9\xcd\xee\x2a\x5c\x10\xc1\x5a\xbe\xc8\xfa\x9b\x6a\xb9\x00\xbd\x12\x95\xd2\xfb\x39\x63\xb6\x39\xb3\x50\x55\x8f\x35\xb8\xdb\x3e\x1e\x0e\x03\xf6\xf4\xe6\x0f\x25\x37\xf2\xd4\xde\x65\x5a\x12\x0f\xce\xfc\x0b\x66\xd5\x1d\x77\x60\x87\x3b\x6e\x29\x36\x2c\x40\xee\xa8\xc5\x97\x33\xe7\xcb\x78\xf3\x71\x9d\xf9\x4c\xce\x7c\xfb\x8f\xe6\x8c\x51\x6a\x5a\x82\x0f\xe3\x9d\xfb\xc8\x4b\x91\xdf\x42\x11\xd1\x3a\x08\x98\xdf\x21\xff\x4a\xc8\xf4\x3b\x31\x07\xb5\x34\xce\x9b\x19\xf7\x11\x38\x84\xb3\x60\x7b\x6b\x4c\x6b\x93\x13\xba\x21\x12\xd3\xe6\x42\xac\x22\x4a\x93\xbc\xcc\xaa\x0a\x57\xc1\xa3\x5c\xe8\xbc\x84\xc1\x78\x7a\x3c\xd5\x70\x3f\x18\x97\x42\xde\x0a\x39\x1d\x54\x2b\x28\x0d\x1c\x9f\x3c\x3e\xc9\x26\x37\xb3\xa8\x66\x73\x87\x38\x23\x92\xb9\x53\x2f\x7b\x66\x00\x71\x9c\x13\xd9\xdb\xd5\x9e\xff\x9e\x15\x80\xfc\x1d\x33\x9f\xff\x9f\x67\xd6\x50\xfc\x8e\x79\x2f\x0e\xcc\x2b\xf9\x92\x44\xf9\x0c\x72\xe4\x68\x92\x24\x98\xbc\x7d\x36\xd2\x5f\x42\xa4\x41\xde\xd4\x24\x97\x68\x50\xf5\x0e\x8d\x19\x52\x28\xa4\x04\xfd\xe2\xdd\xcf\x17\xfc\xeb\xef\x0b\xb1\x1a\x58\x8a\x79\x84\x63\xb6\x89\x7c\xf2\xfd\x37\x85\x58\x3d\xf9\x37\x39\xd8\xf3\x0f\x07\x7c\xcd\x66\x44\x32\xa7\x92\x11\x13\x98\xe7\x07\x0c\x94\x22\x87\xb1\xf9\x52\x91\xb1\x69\x0f\x65\x48\xf4\x57\x1f\x27\xda\xfa\xd8\x2f\xa3\x7a\x51\x66\xf7\x5f\x48\x75\xa6\xcd\x3f\x98\xec\x9f\x03\xb2\x99\x60\x9a\x29\x8e\x29\x20\x7a\xc7\xe4\x6f\x4a\xdf\xfe\x28\x74\x5f\xaf\xbc\x3a\x33\x81\x2a\xf6\xdf\xff\x95\x99\x74\x10\x51\xa6\xf9\x92\x28\xca\xe6\x38\xd1\x44\x49\x73\xec\x8e\xc9\x58\x24\x4c\x56\x8a\x3c\x0a\xbb\x26\xd9\x5c\x94\xf7\x11\x8b\xe6\x4a\xaa\x6a\x91\xe5\xe0\xbb\x73\x55\x2a\x1d\xb1\x68\xaa\xb3\x7b\xdf\x36\x56\xba\x00\x7d\x6c\xd4\x22\x62\x51\x29\xa6\x33\x83\xbd\x83\x42\x19\x03\xc5\xe0\x64\xb1\xf6\x80\x8b\xac\xc0\x0c\xaf\x81\x1c\x26\x7f\x82\xf9\x76\x57\x09\x13\x13\xb1\xe8\xbb\xdd\x41\x1a\x11\xf7\xbb\xe6\x99\x9e\x0a\xb9\x0f\x5d\xd3\xd3\x60\x3b\xde\x1d\xe3\xb1\xb9\xae\x1d\xe9\xb0\x8a\x08\x26\xf1\xa1\x99\xa4\x35\x5b\xf8\x9c\xc4\x31\x3e\x8e\xd5\x11\xe7\x64\x57\x12\x18\xc8\xea\xa4\xc8\x4c\xc6\xd5\x47\xec\xf7\xfd\x8e\x4c\x59\xc9\x0a\x36\xe3\x26\x91\xe8\xff\xa7\xbc\x49\x5b\x77\x24\x6b\xb4\x15\x2c\xbe\xcd\x1a\xa1\xce\x28\x53\x28\xe9\xfd\x2a\x1b\x51\x56\x5a\xf0\x22\xa2\xac\xe0\x4b\x32\x45\x46\x94\x2c\xba\x53\xba\x38\x1e\x6b\xc8\x6e\x23\x16\xd9\xe7\x71\x56\x96\x9f\xe0\x85\xc0\x87\x72\x5f\xa5\x7b\x14\xac\xec\x18\x44\x20\x71\xb9\x1a\x66\x58\x8e\x35\x34\x8e\x67\xc8\x2c\x4f\x5c\xc0\xa2\x19\x65\xfb\x07\x4c\x71\x40\xcb\x04\x1c\x51\xb8\x11\xd3\x8f\x30\xf5\xe9\xd6\x8d\x02\x13\xdc\x34\xc8\x13\x90\x46\x0b\xa8\x48\x2b\xaf\x33\xb9\x12\x5a\x49\x0c\x81\x91\x8b\x97\x57\x4c\xf1\xe1\x48\x7d\x2f\x7c\x48\xad\x30\xf9\xd5\x98\x93\xbc\x27\x43\xf6\x03\x31\x4c\x30\xd5\x5e\x46\x05\x02\xc9\xc6\xf6\xb0\xd9\x4f\x0b\x7c\x38\x82\xef\xb5\x47\x03\x0e\x0d\x5c\x25\x39\xa1\xa3\x9e\xa7\x00\xb9\xfa\xa4\x97\x18\xf5\x53\x73\xdd\x4b\xcd\xf5\xa5\xb8\x4a\x50\xa9\x5d\xcc\xee\x65\x60\x8f\x6a\x76\x98\xba\xf9\x5c\x6e\x74\x29\xdb\x1e\x86\x34\xc7\xc7\x15\xf7\x0c\x19\x21\x8b\x4e\xf1\x4f\x82\xd3\x57\x34\x25\x2d\xd3\x2a\xca\x6c\x47\x4e\x9a\x97\x76\xad\xb4\xb6\x29\x8c\xea\x08\x6a\xd9\x6d\x4f\xc3\x46\xbe\x9d\xfb\xe9\xeb\x2d\x7f\x59\x12\x7b\xac\x14\x88\xff\xc7\x2d\x9b\xda\x91\x54\xb5\xc8\x64\x44\x19\x11\xed\xb6\x60\x60\x6d\x9e\x29\x69\x40\x1a\x1e\x15\xea\x4e\x96\x2a\x2b\x06\xa5\x9a\x0e\x26\x02\x5d\xa3\x48\x66\x1a\x26\x5c\x73\x93\x60\x14\xfa\x28\x0a\x83\xec\xe8\x51\xcb\x3e\x94\xe7\xa3\xe8\x9b\x52\x4d\x77\x25\xd8\x58\x4f\x68\x21\x88\xab\xa7\xee\x1a\xd5\xfd\x4b\x66\x41\x8b\xf0\x8b\xfb\x88\x45\xfc\x6d\xbf\x9b\x99\xb3\x05\x5b\xb1\x31\xbb\x66\x37\xec\x9e\xdd\xb2\x35\x7b\xc5\xee\xd8\x6b\xf6\x86\xbd\xe5\xf6\x78\xb9\x9d\xf2\x0d\xd8\x1d\xef\x34\xfa\x9f\xff\xf8\xcf\x28\xdd\x6e\x66\xcf\x38\xd9\x6e\x6b\x22\xd0\x6f\x4e\xe0\x5f\x68\x62\xd4\x73\xb1\x86\x82\x3c\xa6\xec\x5d\x00\xfa\xd6\xa8\xc5\x21\xb8\xb3\x1d\x97\x1a\xc7\x3f\x93\xa1\x35\x85\x2e\x88\xb4\xae\xd8\x87\xc7\x36\x59\x3d\xe7\xfb\x94\x39\x8e\x9f\xda\xb1\xec\xa2\xeb\xbe\x50\xd3\xe7\xa2\x84\x38\xfe\xd1\xa1\x3d\xb4\x77\xb6\xaf\x67\x71\x7c\xe6\x74\x78\xd7\xcb\x46\x94\x9d\xc7\xf1\xb9\xed\xde\xe3\x84\xbd\xf3\x75\x88\x8a\x40\x0b\xe7\x08\xac\x1d\xcf\xec\xf6\xbc\xe0\x4b\xf2\x96\xb2\xd5\x7e\x4f\x1e\x51\x36\x0e\x46\x5f\x07\xa3\x07\x4d\x9c\x6f\xb1\xdc\xf0\x25\x79\x46\xd9\x3d\xf6\x57\x11\x65\xb7\x07\xd1\xad\x03\x74\xaf\x2c\x38\x86\x80\x21\xae\x3b\xbe\x24\xef\x28\x7b\xed\x71\xbd\x39\x88\xeb\x22\x8e\x2f\x2c\x0b\x44\xe8\xdf\xf4\x72\x27\x0a\xea\x45\x4e\xcf\xb7\x22\xa7\x79\x26\xa4\x3d\xab\xde\x19\x55\x84\x60\x2e\x11\xd0\x2e\xe8\xd8\x01\x1d\x87\xa0\xe3\x72\x09\x07\x21\xd7\x21\xa4\xd2\x99\x9c\x1e\x86\x2d\x43\xd8\x89\x52\x66\x17\xe6\xe0\xb6\x69\xd5\x67\x8e\xef\xf6\xd8\x32\x43\xc7\x85\xdd\x56\x6f\xdc\x18\x96\x11\xc5\x9a\x97\xd2\x0f\xc6\x7d\x95\x55\x64\xce\x0a\x7c\x2c\xdc\x63\xe5\x1a\xc7\xee\x71\xcd\xc6\xf8\xb8\x71\x8f\x7b\xf7\xb8\x75\x7d\x6b\xf7\x78\xc5\xd6\xf8\xb8\x73\x8f\xd7\xee\xf1\x06\xfb\xac\xd0\x30\x12\xe8\xef\x1e\xdb\xe6\x77\x7a\x76\x7a\x96\xb8\xbe\x94\x90\x33\xee\x8c\x91\x5a\x71\x07\x74\xd1\xf4\x2c\x8e\xc9\x99\xbb\xcb\x38\xe3\x8e\xd6\x7d\x26\x79\x7a\x7e\x7a\xde\xe1\x3b\xe7\x4f\x03\x7c\xc8\x0f\x15\x14\x0f\xe0\x86\x9b\x9e\xc7\x31\x39\x77\x78\xcf\xb9\x3f\xfa\xf5\x61\xd8\x5b\xf4\x9c\x5f\xe6\xb5\xa8\xbd\xaf\xb1\xd1\xc4\xdb\x10\xd5\x33\x44\xf5\xf9\x1e\x0d\xd1\xdc\x38\x34\xcf\x42\x34\xef\x10\xcd\x67\x7a\x3b\xc4\x71\xe7\x70\xbc\x0b\xb8\xd5\x78\xa8\xd3\x8b\xd3\x8b\x8e\x53\x17\xfc\xc7\x80\x53\x81\xe4\x68\x7a\x11\xc7\xe4\xc2\x71\xe8\x82\x37\xd2\xec\x6d\x96\x56\x05\x0b\xd2\xcf\x32\x9e\x53\x66\xfb\x75\xa3\x8a\xf6\x56\x1c\xe2\x98\xe4\x98\x26\xe4\xa4\xa4\x8d\x8e\x14\xa4\xb7\xa9\xfc\xe0\x2b\x12\x5c\x28\xb0\x5d\x4d\x04\x5d\xbd\x8e\x0d\xf7\x6c\x7d\xca\xe5\xf0\x8a\x09\x17\xcc\xb9\xef\x13\xfc\x86\x2c\x9f\x5d\x37\x8d\xfe\xd3\x1e\x8f\x73\xc9\x44\x37\xe1\x07\x24\xe5\xd6\x5d\x2c\xb4\x47\xb7\xc1\xc1\xb0\x9b\x3f\x75\x8f\x9a\xf5\xce\x81\xdc\xe9\x51\x94\x24\x11\x2b\xc0\x64\xa2\x84\x22\x3d\x3a\xa9\x51\x60\xc8\x76\x8f\x4d\x48\xa3\x55\x7b\xa7\xdf\x9e\xef\xb7\x97\x43\x97\xef\xae\x58\x5b\x4d\x30\x05\xd3\x54\x99\xfc\x70\x7f\x5e\x90\xa8\xef\x08\x9a\xb4\x8a\x3e\x3c\xec\x1c\xfe\x07\xa5\x09\x90\x88\x82\xef\x1f\xc9\xa0\x17\x97\x7c\x9d\x58\x87\x97\xf4\x61\x37\x36\x0f\x4b\x6d\x57\x9d\x38\xef\xb5\x1f\x44\x43\x51\x27\xe8\x08\xf7\x77\x63\x4f\x9d\xf8\xf3\x95\x6d\x98\x71\x96\xdf\x4e\xb5\x5a\xca\xe2\xd8\x81\x97\x62\x0e\xcd\xa4\xee\x64\xe4\x93\x23\xdc\xfc\xee\x04\xe9\x33\xd0\x37\xc9\x63\x9d\xb8\xf3\x97\xed\x11\x85\xa8\x30\x69\x4f\x85\x2c\x85\x84\x63\xbb\x5d\x8c\xee\x44\x61\x66\xe9\x09\xcc\x47\x33\x40\x04\xf6\xb5\x49\x4a\x75\x56\x88\x65\x95\xda\xe4\x70\xd4\xe4\x7e\x63\x65\x8c\x9a\xa7\x8f\x17\xeb\x3a\xd1\xcb\x1d\xa2\x1b\x74\xc3\xe1\x1f\x46\x4d\xee\x99\x7e\xb7\x58\x6f\x21\xfc\x13\x0e\x6e\x72\xfd\x43\x8b\x9c\x94\xb0\x1e\xe1\x9f\xe3\x42\x68\xb0\xba\x90\x6a\x75\x37\xca\x4a\x31\x95\xc7\xc2\xc0\xbc\x4a\x73\x90\x06\xf4\x21\x54\xe9\x4c\xad\x40\x07\x7c\x4a\xef\x66\xc2\x40\x35\x57\xb7\x50\x27\xed\x86\xf9\x85\x0b\xa8\x93\x66\x9f\xdb\x91\x47\x43\xa2\x65\x62\x91\xe9\x5b\x97\xc7\x67\xd5\xcc\xe5\xf1\xa3\x5e\x2e\x8e\x6c\xae\x13\x90\xab\x8f\x30\x10\x35\xf9\xd8\xd2\x9b\x62\x2e\x3e\x0a\x4e\x17\xd2\xf6\x6c\x01\xf5\xd3\x9d\x2c\x6e\x63\x6a\xeb\x21\xd2\x2d\x53\x69\x8f\x22\x1f\x57\x03\xd4\x04\xcc\x90\xe4\x44\x48\x61\xa0\x4e\xf2\x4c\xef\xea\xa5\x5a\x1f\x57\xb3\xac\x50\x77\xe9\x70\xf0\xed\x62\x3d\xf8\x6e\xb1\x1e\x0c\x07\x7a\x3a\xce\xc8\x90\x0d\x9a\xff\xc9\x63\x3a\xea\x8a\x4e\xd2\x61\xf2\xc7\x6a\x57\xee\x3d\x22\xa2\x8b\x65\x2e\x8a\x6c\xf0\x36\x93\xd5\xe0\xbd\x14\xb9\x2a\x20\x62\x03\xdf\xfc\x93\xce\xa4\x6d\xa8\x32\x59\x1d\x57\xa0\xc5\xa4\xd1\x42\x8b\x69\xaf\x3a\xcf\xb3\xf5\xb1\x63\xe1\x1f\x87\x43\x54\x33\xef\xb9\xb6\x69\xea\x00\x91\xd7\x83\x23\x31\x5f\x28\x6d\x32\x69\xf6\xb2\xc0\xeb\x52\xc8\x08\x64\xc2\xc9\x3f\x1f\xe0\x44\x8d\xfb\x91\xc9\x84\xdc\x55\x94\xc0\x2e\xec\xf8\xda\x96\x58\xed\xc2\x7d\xb9\x25\x8c\x6e\x96\x95\x11\x93\xfb\xe3\xdc\xf9\xbf\xce\x40\xb2\xf9\x8e\x37\xf0\xcb\x78\xec\x85\x52\x89\xdf\xc0\x5a\xbf\xfd\xba\x73\xde\x60\xac\xca\xc2\x29\xa2\x15\xed\x44\xe9\x79\xba\x5c\x2c\x40\xe7\x59\x05\x75\x82\x0b\xdc\xaf\xc1\x43\xef\x4f\x86\x5e\x09\xac\x87\x4e\x2b\x55\x8a\xc2\x37\xb5\xce\x67\x30\x1c\xe0\xdf\xc7\x9d\xef\x71\x3e\xcd\x4e\xea\x62\x9a\xc1\xa1\xf7\x7f\x1a\xda\x7f\x87\xf4\xc1\xea\x8b\x75\x57\x18\xa4\xef\x5f\xec\xe3\xce\xfd\xe1\xeb\x5e\x4c\x6e\x5d\xe9\xb8\xcc\x30\xc8\x46\x2a\x06\xc8\xba\x60\x82\xa0\x6e\xed\x80\xc5\x6d\xfe\x34\xfc\xc3\x46\x2d\xb2\x5c\x98\xfb\x74\x58\xd7\x5f\xb3\x8a\x00\xeb\x95\xda\xd1\xda\x96\xc0\xb8\xbb\xf7\xe6\x22\xd7\xdf\x34\xf3\xcb\xab\x9d\x82\x89\x7e\x89\xed\x76\xb6\x7a\x38\x57\xed\x65\xc6\xec\x59\xf7\xfd\x4c\xcd\xe7\x99\x2c\xd8\xbb\xae\xe9\xa9\x9e\x56\x4d\xa1\xd4\x20\x0a\x72\xc9\x9f\x83\x2a\x18\x0c\x18\xec\x1d\x8f\xaf\x56\x3b\xeb\x6e\xfb\x4e\x9f\xa7\xe7\x36\xd1\x7c\xcf\x7f\x26\x86\xb2\xa7\xdc\x9d\xe1\x8d\x82\xf3\x87\x7e\x56\x1a\x06\x4c\x87\xa7\xb8\x08\xa6\xf8\x25\xfd\xca\x4e\xf1\x81\xff\x80\x53\xbc\xe0\x1f\xb6\xa6\xf8\xed\xd0\x14\x3f\xed\xc9\x88\x5f\x72\xd3\x7a\x8e\x38\x76\x47\x01\x9f\x4e\x76\x75\xf7\xaa\x82\x0c\xb1\x74\xd9\x69\xb1\x37\xc5\x7d\x6a\x43\xd2\x45\xbf\x2f\xc2\x5c\xb6\xc5\x35\xee\x5e\x5f\x58\xe8\xeb\xbd\x98\x6e\x10\x6c\x11\x35\x09\xec\x4e\xf7\xad\xed\xd6\x60\xd3\x57\x9b\xe8\xda\xcc\x75\x3b\x59\xdd\x19\xf7\x32\x8e\x5f\xba\x14\xbd\x77\x52\x9f\xcd\x61\x4f\x82\xaa\xdb\x08\xf9\x47\xca\x74\x38\xc0\xf9\xb8\x3d\x43\x6e\xda\x21\xbf\x51\x36\x23\xb7\xed\xe7\x4f\x94\xcd\xf1\xd3\x59\x58\xc4\xa2\x21\xb2\xa5\x97\xa2\xaa\x62\xe7\x0a\xa2\x9f\x46\xb7\x4e\x78\x07\x2c\xcc\x9e\xdf\xf0\x08\x7d\xfe\x20\x7a\x44\x3a\xb1\x9f\x46\xfe\x2d\x4a\xa3\x88\x3e\x8a\x3e\x3b\x69\x0d\xce\x7a\xb5\x3b\xeb\x55\x2e\x27\xd5\x28\xee\x39\xd1\x4d\x36\x8b\x89\xa9\x70\x89\xa9\x70\x89\xe9\x0a\x25\x3c\x27\xe3\x16\xe0\x1a\x9b\x30\x49\x5d\xb9\x24\x75\xe5\x92\xd4\x95\x4b\x52\x6f\x5d\x92\x7a\xeb\x92\xd4\x5b\x97\xa4\xae\x1a\x91\xcd\xc9\xaa\xcb\x4e\xed\xef\x41\xcc\x76\xe6\x27\x42\x1f\x80\xf9\x54\xd9\xa6\x76\xef\x11\x02\xcd\x55\xd8\x44\xeb\xa9\xcb\x90\x88\xb3\x5c\xd1\x24\x52\x01\x31\x94\x7d\xc0\x11\x68\x7d\x6e\xc4\x8b\x66\x84\x33\x44\x3f\x22\xa0\xae\xcb\xd9\xda\xf4\x51\x6c\xf9\x20\xc4\xb3\x6e\xd3\x44\xb3\x9d\x26\x8a\xfd\xfe\xa9\x9f\x18\x8a\x4e\xa0\x2f\x4f\x5f\x26\x8e\x15\x29\x21\x2f\xf9\xdf\x82\x75\x05\xec\xa2\xe9\xcb\x38\x26\x2f\xdd\xf2\x5f\xb6\xc9\x78\xe7\x0d\xde\xe0\xec\x81\xca\x88\xcf\x57\x19\x5b\xaa\x13\x2a\xde\x76\xb2\xf9\xb4\xc9\x34\x7b\x76\xf4\xa2\x69\xec\x59\xca\xb4\x6f\x29\x56\xe4\x2e\xd9\x6c\xaa\xc8\x76\x4a\x68\xfa\x55\x4b\xae\xdc\xca\xd6\x59\xf6\x8a\x1a\x5d\x2d\xb4\x6d\x0f\x2f\xc2\x79\x05\x3e\x99\x27\x5d\x67\x78\x79\x9f\x4c\x84\xcc\xca\xf2\xbe\xe9\x26\xf6\x16\xfe\x31\xfc\x91\xd6\xa3\x5d\xd8\xb6\xee\xd5\x10\xea\xae\xf4\x9b\xea\xb7\x8f\x97\xfb\x28\x5b\xc9\x14\x52\xb6\xa7\xfc\xa7\xa6\x36\x45\x35\x99\x9e\x42\x5b\x9d\xd4\xd5\xec\xa0\xb8\x5d\x93\x2b\x0f\xf5\x90\x0c\xda\x2a\xd1\xd7\x5b\xfb\x2e\xa5\xb5\x26\x1f\xba\x5f\xac\xb0\xb7\x94\xf5\x1b\x9e\xb9\xdf\x10\xbc\xe0\x1f\xec\xf3\x37\xbe\x51\xf2\xdc\xc0\xfc\x2d\x94\x90\x1b\xe8\x7e\x05\x60\x8b\x2d\xaa\xa6\x35\xc5\xb5\x06\xa5\x08\x3f\x7d\xf4\x9a\x7d\xb1\x73\xea\x5e\xaa\xac\x68\x2e\xc0\xf7\x5c\x91\x2e\x52\x71\xf0\x70\xfb\x65\x58\xab\xe7\x8b\x88\xbc\x81\x35\xb5\x44\xf6\x40\xbd\xcb\xfc\x4d\xe2\x97\xdd\x9d\x16\xd5\x4c\x70\x09\x77\x83\x17\x64\x83\xda\x95\xba\xb8\x85\xd9\xc2\xc0\xb4\x29\x1b\x64\x68\x89\xa9\xac\x83\xa3\x0d\x25\xbb\x5a\x8c\xee\x77\x4c\xf6\x77\x1a\x5b\x6c\xf3\x13\xa1\x48\x2d\x2b\x44\x5f\x92\x2d\xd9\xa2\x15\xa7\xa3\xdd\xd6\x19\x6f\x0c\x97\xcd\x2f\xf0\x36\xf5\xc8\x1f\x33\x55\xf6\x0e\xcd\xbd\xb7\x94\xa0\xc6\x20\xbd\xb6\x0f\x5f\x9a\x0b\x05\x7b\xc9\xe6\x09\xef\xee\x1d\xaa\xe6\x22\xae\x61\x0e\xdf\xc7\x1c\xf4\x3d\xae\xfc\xd0\x5b\xb9\xf0\x67\x22\xa4\x7f\xfb\xf2\xe7\x7d\x97\x6f\xcd\x44\x9f\xb8\x5f\x43\x41\xfe\x75\xdf\xfd\xda\xe7\x5d\xa9\xf1\x22\xe0\xe1\x67\x5c\x97\xd9\x38\x28\x50\x31\x62\x98\x6a\x0a\xcd\x9b\xf5\x3e\x3c\x98\xe6\x8e\xa6\xe3\x09\xdd\x08\xae\x3c\x40\x7b\x45\x56\xf1\xe1\xa8\xea\x68\xaa\x82\x2b\xb2\x8c\xff\x95\x28\x26\x58\x45\x47\xfa\xb2\xba\x3a\xc5\x3f\xd6\x75\x67\xf6\x8a\xac\x72\x74\x67\x94\xd9\x0e\x77\x45\x56\xd9\x2b\xb2\xf0\x00\x54\xfa\xab\xb2\xaa\x23\xa6\x72\xc4\x54\x9f\xba\x2a\x73\x57\x64\x6c\x8f\xe9\xfc\x25\x2c\x42\xde\xe4\xa9\x60\xf3\x54\x38\x63\x4b\x45\x00\xf7\xd7\xcf\x3f\xea\xf3\xba\x78\x29\xf7\x9c\xec\xb9\xce\x7d\x67\x7b\x7f\xff\xd8\xd9\x5e\x63\xd4\xee\xd7\x4b\xed\x39\x5e\xeb\x79\x6c\x9d\xd7\xa1\x83\xbc\xc3\xe9\x87\xaf\x10\x0d\xaa\x62\xbb\xd6\xa0\xf2\xf5\xb3\x33\x15\xfb\xdb\x1b\x85\x2e\xa8\x29\x28\x4b\x81\xe5\x66\x9d\x9a\xd6\xb3\xdb\x93\xc8\xa6\xbc\x3d\xfd\x89\xe1\xce\x91\xfe\x99\xd9\x1a\xb0\xf4\x2f\xcd\xcf\x27\xa3\x86\xd8\x88\xd9\x4a\xb0\x34\xc2\x41\x51\xfb\xd3\xc4\x15\x09\x0d\x4a\x35\xae\xc4\x2a\x3f\x53\xcd\xef\x08\x42\x67\xd2\x06\x74\xbe\x13\xdb\x55\xb3\x3d\x70\xdb\x6e\x9d\x0d\xb7\x5b\x58\xa8\x73\x3d\xd7\x83\x11\xb3\x59\x73\xc3\xda\xe5\x09\xfb\x6b\x01\x81\x01\x44\xb0\x22\x1a\xc7\x2b\x82\x06\xfc\xf0\xe0\x27\x44\x24\x5d\xe1\xae\xea\x7e\x86\xb1\x1d\x30\xa8\xf6\x67\x10\x80\x49\x87\x15\xeb\xef\x0a\x01\x7c\x31\xa1\xd5\x19\xe8\x55\x0e\xb6\x45\x81\x10\x16\x4c\x56\xbf\x1e\xac\x17\xdd\xaa\x09\x35\xfc\x89\xff\x2d\x8c\xdd\xee\x93\x79\xb6\xc0\xc6\xfd\x78\x0f\x94\x1b\x1e\xae\x4e\x6d\x67\xa3\xfb\x8b\x50\xbd\x41\x40\x50\x7c\x1a\x46\x3e\x5b\x45\xa8\xb4\xfe\xff\x17\x98\xb8\xee\xad\x42\xf5\xfd\x35\xea\xbb\xe5\xe9\x7b\x2a\xd3\xbb\xa2\x74\x4d\xfe\xbe\x1d\xdf\x84\x0d\xbf\xd1\x11\xee\xf3\x7f\x27\x1b\xb7\xa6\xf4\xf0\x81\x7e\xa7\x18\x11\x75\x7b\xff\xa6\xae\x69\x7d\x45\x47\xff\x1b\x00\x00\xff\xff\x9a\x7a\x62\x6b\x4c\x3f\x00\x00") 93 | 94 | func mainJsBytes() ([]byte, error) { 95 | return bindataRead( 96 | _mainJs, 97 | "main.js", 98 | ) 99 | } 100 | 101 | func mainJs() (*asset, error) { 102 | bytes, err := mainJsBytes() 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | info := bindataFileInfo{name: "main.js", size: 16204, mode: os.FileMode(420), modTime: time.Unix(1550941822, 0)} 108 | a := &asset{bytes: bytes, info: info} 109 | return a, nil 110 | } 111 | 112 | // Asset loads and returns the asset for the given name. 113 | // It returns an error if the asset could not be found or 114 | // could not be loaded. 115 | func Asset(name string) ([]byte, error) { 116 | cannonicalName := strings.Replace(name, "\\", "/", -1) 117 | if f, ok := _bindata[cannonicalName]; ok { 118 | a, err := f() 119 | if err != nil { 120 | return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) 121 | } 122 | return a.bytes, nil 123 | } 124 | return nil, fmt.Errorf("Asset %s not found", name) 125 | } 126 | 127 | // MustAsset is like Asset but panics when Asset would return an error. 128 | // It simplifies safe initialization of global variables. 129 | func MustAsset(name string) []byte { 130 | a, err := Asset(name) 131 | if err != nil { 132 | panic("asset: Asset(" + name + "): " + err.Error()) 133 | } 134 | 135 | return a 136 | } 137 | 138 | // AssetInfo loads and returns the asset info for the given name. 139 | // It returns an error if the asset could not be found or 140 | // could not be loaded. 141 | func AssetInfo(name string) (os.FileInfo, error) { 142 | cannonicalName := strings.Replace(name, "\\", "/", -1) 143 | if f, ok := _bindata[cannonicalName]; ok { 144 | a, err := f() 145 | if err != nil { 146 | return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) 147 | } 148 | return a.info, nil 149 | } 150 | return nil, fmt.Errorf("AssetInfo %s not found", name) 151 | } 152 | 153 | // AssetNames returns the names of the assets. 154 | func AssetNames() []string { 155 | names := make([]string, 0, len(_bindata)) 156 | for name := range _bindata { 157 | names = append(names, name) 158 | } 159 | return names 160 | } 161 | 162 | // _bindata is a table, holding each asset generator, mapped to its name. 163 | var _bindata = map[string]func() (*asset, error){ 164 | "index.html": indexHtml, 165 | "main.js": mainJs, 166 | } 167 | 168 | // AssetDir returns the file names below a certain 169 | // directory embedded in the file by go-bindata. 170 | // For example if you run go-bindata on data/... and data contains the 171 | // following hierarchy: 172 | // data/ 173 | // foo.txt 174 | // img/ 175 | // a.png 176 | // b.png 177 | // then AssetDir("data") would return []string{"foo.txt", "img"} 178 | // AssetDir("data/img") would return []string{"a.png", "b.png"} 179 | // AssetDir("foo.txt") and AssetDir("notexist") would return an error 180 | // AssetDir("") will return []string{"data"}. 181 | func AssetDir(name string) ([]string, error) { 182 | node := _bintree 183 | if len(name) != 0 { 184 | cannonicalName := strings.Replace(name, "\\", "/", -1) 185 | pathList := strings.Split(cannonicalName, "/") 186 | for _, p := range pathList { 187 | node = node.Children[p] 188 | if node == nil { 189 | return nil, fmt.Errorf("Asset %s not found", name) 190 | } 191 | } 192 | } 193 | if node.Func != nil { 194 | return nil, fmt.Errorf("Asset %s not found", name) 195 | } 196 | rv := make([]string, 0, len(node.Children)) 197 | for childName := range node.Children { 198 | rv = append(rv, childName) 199 | } 200 | return rv, nil 201 | } 202 | 203 | type bintree struct { 204 | Func func() (*asset, error) 205 | Children map[string]*bintree 206 | } 207 | var _bintree = &bintree{nil, map[string]*bintree{ 208 | "index.html": &bintree{indexHtml, map[string]*bintree{}}, 209 | "main.js": &bintree{mainJs, map[string]*bintree{}}, 210 | }} 211 | 212 | // RestoreAsset restores an asset under the given directory 213 | func RestoreAsset(dir, name string) error { 214 | data, err := Asset(name) 215 | if err != nil { 216 | return err 217 | } 218 | info, err := AssetInfo(name) 219 | if err != nil { 220 | return err 221 | } 222 | err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) 223 | if err != nil { 224 | return err 225 | } 226 | err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) 227 | if err != nil { 228 | return err 229 | } 230 | err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) 231 | if err != nil { 232 | return err 233 | } 234 | return nil 235 | } 236 | 237 | // RestoreAssets restores an asset under the given directory recursively 238 | func RestoreAssets(dir, name string) error { 239 | children, err := AssetDir(name) 240 | // File 241 | if err != nil { 242 | return RestoreAsset(dir, name) 243 | } 244 | // Dir 245 | for _, child := range children { 246 | err = RestoreAssets(dir, filepath.Join(name, child)) 247 | if err != nil { 248 | return err 249 | } 250 | } 251 | return nil 252 | } 253 | 254 | func _filePath(dir, name string) string { 255 | cannonicalName := strings.Replace(name, "\\", "/", -1) 256 | return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) 257 | } 258 | 259 | -------------------------------------------------------------------------------- /plugins/plugin_interface.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "github.com/reddec/monexec/pool" 5 | "io" 6 | "context" 7 | ) 8 | 9 | // factories of plugins 10 | var plugins = make(map[string]func(fileName string) PluginConfigNG) 11 | 12 | // Register one plugin factory. File name not for parsing! 13 | func registerPlugin(name string, factory func(fileName string) PluginConfigNG) { 14 | plugins[name] = factory 15 | } 16 | 17 | // Build but not fill one config 18 | func BuildPlugin(name string, file string) (PluginConfigNG, bool) { 19 | if plugin, ok := plugins[name]; ok { 20 | return plugin(file), true 21 | } 22 | return nil, false 23 | } 24 | 25 | // Base interface for any future plugins 26 | type PluginConfigNG interface { 27 | // Must handle events 28 | pool.EventHandler 29 | // Closable 30 | io.Closer 31 | // Merge change from other instance. Other is always has same type as original 32 | MergeFrom(other interface{}) error 33 | // Prepare internal state 34 | Prepare(ctx context.Context, pl *pool.Pool) error 35 | } 36 | -------------------------------------------------------------------------------- /plugins/utils.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "html/template" 5 | "log" 6 | "github.com/Masterminds/sprig" 7 | "io/ioutil" 8 | "bytes" 9 | "path/filepath" 10 | "github.com/pkg/errors" 11 | "time" 12 | "os" 13 | ) 14 | 15 | type withTemplate struct { 16 | Template string `yaml:"template"` 17 | TemplateFile string `yaml:"templateFile"` // template file (relative to config dir) has priority. Template supports basic utils 18 | } 19 | 20 | func (wt *withTemplate) renderDefault(action, id, label string, err error, logger *log.Logger) (string, error) { 21 | s, _, err := wt.renderDefaultParams(action, id, label, err, logger) 22 | return s, err 23 | } 24 | 25 | func (wt *withTemplate) renderDefaultParams(action, id, label string, err error, logger *log.Logger) (string, map[string]interface{}, error) { 26 | hostname, _ := os.Hostname() 27 | params := map[string]interface{}{ 28 | "id": id, 29 | "label": label, 30 | "error": err, 31 | "action": action, 32 | "hostname": hostname, 33 | "time": time.Now().String(), 34 | } 35 | s, err := wt.render(params, logger) 36 | return s, params, err 37 | } 38 | 39 | func (wt *withTemplate) render(params map[string]interface{}, logger *log.Logger) (string, error) { 40 | parser, err := parseFileOrTemplate(wt.TemplateFile, wt.Template, logger) 41 | if err != nil { 42 | return "", errors.Wrap(err, "parse template") 43 | } 44 | message := &bytes.Buffer{} 45 | 46 | renderErr := parser.Execute(message, params) 47 | if renderErr != nil { 48 | logger.Println("failed render:", renderErr, "; params:", params) 49 | return "", err 50 | } 51 | return message.String(), nil 52 | } 53 | 54 | func (wt *withTemplate) resolvePath(workDir string) { 55 | wt.TemplateFile = realPath(wt.TemplateFile, workDir) 56 | } 57 | 58 | func (wt *withTemplate) MergeFrom(other *withTemplate) error { 59 | if wt.TemplateFile == "" { 60 | wt.TemplateFile = other.TemplateFile 61 | } 62 | if wt.TemplateFile != other.TemplateFile { 63 | return errors.New("template files are different") 64 | } 65 | if wt.Template == "" { 66 | wt.Template = other.Template 67 | } 68 | if wt.Template != other.Template { 69 | return errors.New("different templates") 70 | } 71 | return nil 72 | } 73 | 74 | func unique(names []string) []string { 75 | var hash = make(map[string]struct{}) 76 | for _, name := range names { 77 | hash[name] = struct{}{} 78 | } 79 | var ans = make([]string, 0, len(hash)) 80 | for name := range hash { 81 | ans = append(ans, name) 82 | } 83 | return ans 84 | } 85 | 86 | func makeSet(names []string) map[string]bool { 87 | var hash = make(map[string]bool) 88 | for _, name := range names { 89 | hash[name] = true 90 | } 91 | return hash 92 | } 93 | 94 | func parseFileOrTemplate(file string, fallbackContent string, logger *log.Logger) (*template.Template, error) { 95 | templateContent, err := ioutil.ReadFile(file) 96 | if err != nil { 97 | logger.Println("read template:", err) 98 | templateContent = []byte(fallbackContent) 99 | } 100 | return template.New("").Funcs(sprig.FuncMap()).Parse(string(templateContent)) 101 | } 102 | 103 | func renderOrFallback(templateText string, params map[string]interface{}, fallback string, logger *log.Logger) string { 104 | if templateText == "" { 105 | return fallback 106 | } 107 | t, err := template.New("").Funcs(sprig.FuncMap()).Parse(string(templateText)) 108 | if err != nil { 109 | logger.Println("failed parse:", err) 110 | return fallback 111 | } 112 | message := &bytes.Buffer{} 113 | err = t.Execute(message, params) 114 | if err != nil { 115 | logger.Println("failed render:", err) 116 | return fallback 117 | } 118 | return message.String() 119 | } 120 | 121 | func realPath(path string, workDir string) string { 122 | if path == "" { 123 | return "" 124 | } 125 | if filepath.IsAbs(path) { 126 | return path 127 | } 128 | p, _ := filepath.Abs(filepath.Join(workDir, path)) 129 | return p 130 | } 131 | -------------------------------------------------------------------------------- /pool/executable.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "strings" 11 | "sync" 12 | "time" 13 | ) 14 | 15 | // Executable - basic information about process. 16 | type Executable struct { 17 | Name string `yaml:"label,omitempty"` // Human-readable label for process. If not set - command used 18 | Command string `yaml:"command"` // Executable 19 | Args []string `yaml:"args,omitempty"` // Arguments to command 20 | Environment map[string]string `yaml:"environment,omitempty"` // Additional environment variables 21 | EnvFiles []string `yaml:"envFiles"` // Additional environment variables from files (not found files ignored). Format key=value 22 | WorkDir string `yaml:"workdir,omitempty"` // Working directory. If not set - current dir used 23 | StopTimeout time.Duration `yaml:"stop_timeout,omitempty"` // Timeout before terminate process 24 | RestartTimeout time.Duration `yaml:"restart_delay,omitempty"` // Restart delay 25 | Restart int `yaml:"restart,omitempty"` // How much restart allowed. -1 infinite 26 | LogFile string `yaml:"logFile,omitempty"` // if empty - only to log. If not absolute - relative to workdir 27 | RawOutput bool `yaml:"raw,omitempty"` // print stdout as-is without prefixes 28 | 29 | log *log.Logger 30 | loggerInit sync.Once 31 | } 32 | 33 | func (b *Executable) WithName(name string) *Executable { 34 | cp := *b 35 | cp.loggerInit = sync.Once{} 36 | cp.Name = name 37 | return &cp 38 | } 39 | 40 | // Arg adds additional positional argument 41 | func (b *Executable) Arg(arg string) *Executable { 42 | b.Args = append(b.Args, arg) 43 | return b 44 | } 45 | 46 | // Env adds additional environment key-value pair 47 | func (b *Executable) Env(arg, value string) *Executable { 48 | if b.Environment == nil { 49 | b.Environment = make(map[string]string) 50 | } 51 | b.Environment[arg] = value 52 | return b 53 | } 54 | 55 | func (e *Executable) logger() *log.Logger { 56 | e.loggerInit.Do(func() { 57 | e.log = log.New(os.Stderr, "["+e.Name+"] ", log.LstdFlags) 58 | }) 59 | return e.log 60 | } 61 | 62 | // try to do graceful process termination by sending SIGKILL. If no response after StopTimeout 63 | // SIGTERM is used 64 | func (exe *Executable) stopOrKill(cmd *exec.Cmd, res <-chan error) error { 65 | exe.logger().Println("Sending SIGINT") 66 | err := cmd.Process.Signal(os.Interrupt) 67 | if err != nil { 68 | exe.logger().Println("Failed send SIGINT:", err) 69 | } 70 | 71 | select { 72 | case err = <-res: 73 | exe.logger().Println("Process gracefull stopped") 74 | case <-time.After(exe.StopTimeout): 75 | exe.logger().Println("Process gracefull shutdown waiting timeout") 76 | err = kill(cmd, exe.logger()) 77 | } 78 | return err 79 | } 80 | 81 | // run once executable, wrap output and wait for finish 82 | func (exe *Executable) run(ctx context.Context) error { 83 | cmd := exec.Command(exe.Command, exe.Args...) 84 | for _, param := range os.Environ() { 85 | cmd.Env = append(cmd.Env, param) 86 | } 87 | if exe.Environment != nil { 88 | for k, v := range exe.Environment { 89 | cmd.Env = append(cmd.Env, k+"="+v) 90 | } 91 | } 92 | for _, fileName := range exe.EnvFiles { 93 | params, err := ParseEnvironmentFile(fileName) 94 | if err != nil { 95 | exe.logger().Println("failed parse environment file", fileName, ":", err) 96 | continue 97 | } 98 | for k, v := range params { 99 | cmd.Env = append(cmd.Env, k+"="+v) 100 | } 101 | } 102 | if exe.WorkDir != "" { 103 | cmd.Dir = exe.WorkDir 104 | } 105 | 106 | setAttrs(cmd) 107 | 108 | var outputs []io.Writer 109 | var stderr []io.Writer 110 | var stdout []io.Writer 111 | 112 | output := NewLoggerStream(exe.logger(), "out:") 113 | outputs = append(outputs, output) 114 | defer output.Close() 115 | stderr = outputs 116 | stdout = outputs 117 | 118 | if exe.RawOutput { 119 | stdout = append(stdout, os.Stdout) 120 | } 121 | 122 | res := make(chan error, 1) 123 | 124 | if exe.LogFile != "" { 125 | pth, _ := filepath.Abs(exe.LogFile) 126 | if pth != exe.LogFile { 127 | // relative 128 | wd, _ := filepath.Abs(exe.WorkDir) 129 | exe.LogFile = filepath.Join(wd, exe.LogFile) 130 | } 131 | logFile, err := os.OpenFile(exe.LogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) 132 | if err != nil { 133 | exe.logger().Println("Failed open log file:", err) 134 | } else { 135 | defer logFile.Close() 136 | outputs = append(outputs, logFile) 137 | } 138 | } 139 | 140 | logStderrStream := io.MultiWriter(stderr...) 141 | logStdoutStream := io.MultiWriter(stdout...) 142 | 143 | cmd.Stderr = logStderrStream 144 | cmd.Stdout = logStdoutStream 145 | 146 | err := cmd.Start() 147 | if err == nil { 148 | exe.logger().Println("Started with PID", cmd.Process.Pid) 149 | } else { 150 | exe.logger().Println("Failed start `", exe.Command, strings.Join(exe.Args, " "), "` :", err) 151 | } 152 | 153 | go func() { res <- cmd.Wait() }() 154 | select { 155 | case <-ctx.Done(): 156 | err = exe.stopOrKill(cmd, res) 157 | case err = <-res: 158 | } 159 | return err 160 | } 161 | 162 | type runnable struct { 163 | Executable *Executable `json:"config"` 164 | Running bool `json:"running"` 165 | pool *Pool 166 | closer func() 167 | done chan struct{} 168 | } 169 | 170 | func (exe *Executable) Start(ctx context.Context, pool *Pool) Instance { 171 | chCtx, closer := context.WithCancel(ctx) 172 | run := &runnable{ 173 | Executable: exe, 174 | closer: closer, 175 | done: make(chan struct{}), 176 | pool: pool, 177 | } 178 | go run.run(chCtx) 179 | return run 180 | } 181 | 182 | func (exe *Executable) Config() *Executable { return exe } 183 | 184 | func (rn *runnable) run(ctx context.Context) { 185 | defer rn.closer() 186 | defer close(rn.done) 187 | restarts := rn.Executable.Restart 188 | rn.pool.OnSpawned(ctx, rn) 189 | LOOP: 190 | for { 191 | rn.Running = true 192 | rn.pool.OnStarted(ctx, rn) 193 | err := rn.Executable.run(ctx) 194 | if err != nil { 195 | rn.Executable.logger().Println("stopped with error:", err) 196 | } else { 197 | rn.Executable.logger().Println("stopped") 198 | } 199 | rn.Running = false 200 | rn.pool.OnStopped(ctx, rn, err) 201 | if restarts != -1 { 202 | if restarts <= 0 { 203 | rn.Executable.logger().Println("max restarts attempts reached") 204 | break 205 | } else { 206 | restarts-- 207 | } 208 | } 209 | rn.Executable.logger().Println("waiting", rn.Executable.RestartTimeout) 210 | select { 211 | case <-time.After(rn.Executable.RestartTimeout): 212 | case <-ctx.Done(): 213 | rn.Executable.logger().Println("instance done:", ctx.Err()) 214 | break LOOP 215 | } 216 | } 217 | rn.Executable.logger().Println("instance restart loop done") 218 | rn.pool.OnFinished(ctx, rn) 219 | } 220 | 221 | func (rn *runnable) Supervisor() Supervisor { return rn.Executable } 222 | 223 | func (rn *runnable) Config() *Executable { return rn.Executable } 224 | 225 | func (rn *runnable) Pool() *Pool { return rn.pool } 226 | 227 | func (rn *runnable) Stop() { 228 | rn.closer() 229 | <-rn.done 230 | } 231 | -------------------------------------------------------------------------------- /pool/logger_stream.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "io" 5 | "bufio" 6 | ) 7 | 8 | type LogInterface interface { 9 | Println(v ...interface{}) 10 | } 11 | 12 | func NewLoggerStream(logger LogInterface, prefix string) io.WriteCloser { 13 | reader, writer := io.Pipe() 14 | go func() { 15 | scanner := bufio.NewReader(reader) 16 | for { 17 | line, _, err := scanner.ReadLine() 18 | if err != nil { 19 | break 20 | } 21 | logger.Println(prefix, string(line)) 22 | } 23 | }() 24 | return writer 25 | } 26 | -------------------------------------------------------------------------------- /pool/pool.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | ) 7 | 8 | type Instance interface { 9 | Stop() 10 | Config() *Executable 11 | Supervisor() Supervisor 12 | Pool() *Pool 13 | } 14 | 15 | type Supervisor interface { 16 | Start(ctx context.Context, pool *Pool) Instance 17 | Config() *Executable 18 | } 19 | 20 | type EventHandler interface { 21 | OnSpawned(ctx context.Context, in Instance) 22 | OnStarted(ctx context.Context, in Instance) 23 | OnStopped(ctx context.Context, in Instance, err error) 24 | OnFinished(ctx context.Context, in Instance) 25 | } 26 | 27 | type Pool struct { 28 | handlers []EventHandler 29 | handlersLock sync.RWMutex 30 | 31 | supervisors []Supervisor 32 | svLock sync.RWMutex 33 | 34 | instances []Instance 35 | inLock sync.RWMutex 36 | 37 | doneInit sync.Once 38 | done chan struct{} 39 | 40 | terminating bool 41 | } 42 | 43 | func (p *Pool) StopAll() { 44 | wg := sync.WaitGroup{} 45 | for _, sv := range p.grabInstances() { 46 | wg.Add(1) 47 | go func(sv Instance) { 48 | defer wg.Done() 49 | p.Stop(sv) 50 | }(sv) 51 | } 52 | wg.Wait() 53 | } 54 | 55 | func (p *Pool) StartAll(ctx context.Context) { 56 | if p.terminating { 57 | return 58 | } 59 | wg := sync.WaitGroup{} 60 | for _, sv := range p.Supervisors() { 61 | wg.Add(1) 62 | go func(sv Supervisor) { 63 | defer wg.Done() 64 | p.Start(ctx, sv) 65 | }(sv) 66 | } 67 | wg.Wait() 68 | } 69 | 70 | func (p *Pool) Start(ctx context.Context, sv Supervisor) Instance { 71 | if p.terminating { 72 | return nil 73 | } 74 | 75 | in := sv.Start(ctx, p) 76 | p.inLock.Lock() 77 | p.instances = append(p.instances, in) 78 | p.inLock.Unlock() 79 | return in 80 | } 81 | 82 | func (p *Pool) Stop(in Instance) { 83 | in.Stop() 84 | p.inLock.Lock() 85 | for i, v := range p.instances { 86 | if v == in { 87 | p.instances = append(p.instances[:i], p.instances[i+1:]...) 88 | break 89 | } 90 | } 91 | p.inLock.Unlock() 92 | } 93 | 94 | func (p *Pool) Add(sv Supervisor) { 95 | if p.terminating { 96 | return 97 | } 98 | p.svLock.Lock() 99 | defer p.svLock.Unlock() 100 | p.supervisors = append(p.supervisors, sv) 101 | } 102 | 103 | func (p *Pool) Watch(handler EventHandler) { 104 | p.handlersLock.Lock() 105 | defer p.handlersLock.Unlock() 106 | p.handlers = append(p.handlers, handler) 107 | } 108 | 109 | func (p *Pool) cloneHandlers() []EventHandler { 110 | p.handlersLock.RLock() 111 | var dest = make([]EventHandler, len(p.handlers)) 112 | copy(dest, p.handlers) 113 | p.handlersLock.RUnlock() 114 | return dest 115 | } 116 | 117 | func (p *Pool) Supervisors() []Supervisor { 118 | p.svLock.RLock() 119 | var dest = make([]Supervisor, len(p.supervisors)) 120 | copy(dest, p.supervisors) 121 | p.svLock.RUnlock() 122 | return dest 123 | } 124 | 125 | func (p *Pool) Instances() []Instance { 126 | p.inLock.RLock() 127 | var dest = make([]Instance, len(p.instances)) 128 | copy(dest, p.instances) 129 | p.inLock.RUnlock() 130 | return dest 131 | } 132 | 133 | func (p *Pool) grabInstances() []Instance { 134 | p.inLock.Lock() 135 | var dest = p.instances 136 | p.instances = nil 137 | p.inLock.Unlock() 138 | return dest 139 | } 140 | 141 | func (p *Pool) OnSpawned(ctx context.Context, sv Instance) { 142 | for _, handler := range p.cloneHandlers() { 143 | handler.OnSpawned(ctx, sv) 144 | } 145 | } 146 | 147 | func (p *Pool) OnStarted(ctx context.Context, sv Instance) { 148 | for _, handler := range p.cloneHandlers() { 149 | handler.OnStarted(ctx, sv) 150 | } 151 | } 152 | 153 | func (p *Pool) OnStopped(ctx context.Context, sv Instance, err error) { 154 | for _, handler := range p.cloneHandlers() { 155 | handler.OnStopped(ctx, sv, err) 156 | } 157 | } 158 | 159 | func (p *Pool) OnFinished(ctx context.Context, sv Instance) { 160 | for _, handler := range p.cloneHandlers() { 161 | handler.OnFinished(ctx, sv) 162 | } 163 | } 164 | 165 | func (p *Pool) doneChan() chan struct{} { 166 | p.doneInit.Do(func() { 167 | p.done = make(chan struct{}, 1) 168 | }) 169 | return p.done 170 | } 171 | 172 | func (p *Pool) notifyDone() { 173 | close(p.doneChan()) 174 | } 175 | 176 | func (p *Pool) Done() <-chan struct{} { 177 | return p.doneChan() 178 | } 179 | 180 | func (p *Pool) Terminate() { 181 | if p.terminating { 182 | return 183 | } 184 | p.terminating = true 185 | p.StopAll() 186 | p.notifyDone() 187 | } 188 | -------------------------------------------------------------------------------- /pool/set_attrs.go: -------------------------------------------------------------------------------- 1 | // 2 | 3 | // +build !linux 4 | 5 | package pool 6 | 7 | import ( 8 | "os/exec" 9 | "log" 10 | ) 11 | 12 | func setAttrs(cmd *exec.Cmd) { 13 | } 14 | 15 | func kill(cmd *exec.Cmd, logger *log.Logger) error { 16 | return cmd.Process.Kill() 17 | } 18 | -------------------------------------------------------------------------------- /pool/set_attrs_linux.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "syscall" 5 | "os/exec" 6 | "log" 7 | ) 8 | 9 | func setAttrs(cmd *exec.Cmd) { 10 | cmd.SysProcAttr = &syscall.SysProcAttr{ 11 | Pdeathsig: syscall.SIGKILL, 12 | Setpgid: true, 13 | } 14 | } 15 | 16 | func kill(cmd *exec.Cmd, logger *log.Logger) error { 17 | pgid, err := syscall.Getpgid(cmd.Process.Pid) 18 | if err == nil { 19 | if err := syscall.Kill(-pgid, syscall.SIGKILL); err != nil { 20 | logger.Println("Failed kill by process group:", err) 21 | err = cmd.Process.Kill() // fallback 22 | } 23 | } else { 24 | err = cmd.Process.Kill() // fallback 25 | } 26 | 27 | if err != nil { 28 | logger.Println("Failed kill:", err) 29 | } 30 | return err 31 | } 32 | -------------------------------------------------------------------------------- /pool/utils.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | // Environment variables file format: 11 | // pair: KEY=VALUE 12 | // comment: line started with # 13 | // empty lines or invalid (without = symbol) ignored 14 | // there is no way to escape new line symbol 15 | func ParseEnvironmentStream(stream io.Reader) map[string]string { 16 | ans := map[string]string{} 17 | reader := bufio.NewScanner(stream) 18 | for reader.Scan() { 19 | line := strings.TrimSpace(reader.Text()) 20 | if len(line) == 0 || strings.HasPrefix(line, "#") { 21 | continue 22 | } 23 | kv := strings.SplitN(line, "=", 2) 24 | if len(kv) != 2 { 25 | // broken line 26 | continue 27 | } 28 | ans[kv[0]] = kv[1] 29 | } 30 | return ans 31 | } 32 | 33 | func ParseEnvironmentFile(fileName string) (map[string]string, error) { 34 | file, err := os.Open(fileName) 35 | if err != nil { 36 | return nil, err 37 | } 38 | defer file.Close() 39 | return ParseEnvironmentStream(file), nil 40 | } 41 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | version=$(git describe --tags | cut -c1-7) 3 | snapcraft list-revisions monexec | grep $version | awk '{print $1}' | xargs -n 1 -i snapcraft release monexec '{}' beta,candidate,stable 4 | goreleaser --rm-dist -------------------------------------------------------------------------------- /sample/dev.env: -------------------------------------------------------------------------------- 1 | # some comment 2 | 3 | 4 | it was empty lines and now broken record 5 | MAIL=will be replaced 6 | MAIL=owner@reddec.net 7 | SUBJECT=multiple = are supported 8 | -------------------------------------------------------------------------------- /sample/email.html: -------------------------------------------------------------------------------- 1 | Content-Type: text/html 2 | Subject: {{.label}} {{.action}} 3 | 4 |

{{.label}}

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
Label{{.label}}
ID({{.id}})
Action{{.action}}
Hostname{{.hostname}}
Local time{{.time}}
User{{env "USER"}}
32 | -------------------------------------------------------------------------------- /sample/sample.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | - label: listener 3 | command: /bin/bash 4 | args: 5 | - -c 6 | - nc -l 9000 7 | stop_timeout: 5s 8 | restart_delay: 5s 9 | restart: -1 10 | consul: 11 | url: http://localhost:8500 12 | ttl: 3s 13 | timeout: 1m0s 14 | permanent: 15 | - listener 16 | -------------------------------------------------------------------------------- /sample/sample2.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | - label: listener2 3 | command: /bin/bash 4 | args: 5 | - -c 6 | - nc -l 9001 7 | stop_timeout: 5s 8 | restart_delay: 5s 9 | restart: -1 10 | consul: 11 | permanent: 12 | - listener2 13 | 14 | telegram: 15 | token: "123456789:AAAAAAAAAAAAAAAAAAAAAA_BBBBBBBBBBBB" 16 | services: 17 | - "listener2" 18 | recipients: 19 | - 123456789 20 | template: | 21 | *{{.label}}* 22 | Service {{.label}} {{.action}} 23 | {{if .error}}⚠️ *Error:* {{.error}}{{end}} 24 | _time: {{.time}}_ 25 | _host: {{.hostname}}_ 26 | 27 | email: 28 | services: 29 | - listener2 30 | smtp: "smtp.gmail.com:587" 31 | from: "sample@gmail.com" 32 | password: "xxxxxxxxxxxxxxxxxxxx" 33 | to: 34 | - "admin1@example.com" 35 | templateFile: "./email.html" -------------------------------------------------------------------------------- /sample/sample3.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | - label: listener3 3 | command: /bin/bash 4 | args: 5 | - -c 6 | - nc -l 9001 7 | stop_timeout: 5s 8 | restart_delay: 5s 9 | restart: -1 10 | 11 | http: 12 | services: 13 | - listener3 14 | url: "http://127.0.0.1:9000/{{.label}}/{{.action}}" 15 | templateFile: "./email.html" -------------------------------------------------------------------------------- /sample/sample4_logfile.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | - label: listener4 3 | command: nc 4 | logFile: listener4.log 5 | args: 6 | - -v 7 | - -l 8 | - 9001 9 | stop_timeout: 5s 10 | restart_delay: 5s 11 | restart: -1 -------------------------------------------------------------------------------- /sample/sample5_critical.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | - label: listener2 3 | command: /bin/bash 4 | args: 5 | - -c 6 | - nc -l 9001 7 | stop_timeout: 5s 8 | restart_delay: 1s 9 | restart: 1 10 | critical: 11 | - listener2 -------------------------------------------------------------------------------- /sample/sample6_rest.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | - label: listener2 3 | command: /bin/bash 4 | args: 5 | - -c 6 | - nc -l 9001 7 | stop_timeout: 5s 8 | restart_delay: 1s 9 | restart: 1 10 | - label: listener3 11 | command: /bin/bash 12 | logFile: listener3.log 13 | args: 14 | - -c 15 | - nc -l 9002 16 | stop_timeout: 5s 17 | restart_delay: 1s 18 | rest: 19 | cors: true -------------------------------------------------------------------------------- /snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: monexec 2 | version: git 3 | summary: Light supervisor with optional Consul autoregistration 4 | icon: docs/logo.svg 5 | description: | 6 | It’s tool for controlling processes like a supervisord but with some important features: 7 | 8 | - Easy to use - no dependencies. Just a single binary file pre-compilled for most major platforms 9 | - Easy to hack - monexec can be used as a Golang library with clean and simple architecture 10 | - Integrated with Consul - optionally, monexec can register all running processes as services and deregister on fail 11 | - Optional notification to Telegram 12 | - Supports gracefull and fast shutdown by signals 13 | - Developed for used inside Docker containers 14 | - Different strategies for processes 15 | - Support template-based email notification 16 | - Support HTTP notification 17 | - REST API (see swagger.yaml) 18 | - Web UI (if REST API enabled) 19 | 20 | Source code licensed under MIT and can be obtained via https://github.com/reddec/monexec 21 | 22 | grade: stable 23 | confinement: strict 24 | 25 | parts: 26 | go: 27 | source-tag: go1.11.2 28 | cli: 29 | after: [go] 30 | plugin: go 31 | go-importpath: github.com/reddec/monexec 32 | 33 | apps: 34 | monexec: 35 | command: bin/monexec 36 | plugs: 37 | - network -------------------------------------------------------------------------------- /swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | info: 3 | description: > 4 | API description for MonExec - light supervisor 5 | version: 0.1.6 6 | title: MonExec 7 | contact: 8 | email: owner@reddec.net 9 | license: 10 | name: MIT 11 | host: localhost:9900 12 | basePath: / 13 | schemes: 14 | - http 15 | paths: 16 | /supervisors: 17 | get: 18 | summary: Get all names of all loaded executables configuration 19 | description: '' 20 | operationId: ListSupervisors 21 | produces: 22 | - application/json 23 | responses: 24 | '200': 25 | description: Success 26 | schema: 27 | type: array 28 | items: 29 | type: string 30 | /supervisors/{name}: 31 | get: 32 | summary: Get full config of service 33 | description: '' 34 | operationId: GetSupervisor 35 | produces: 36 | - application/json 37 | parameters: 38 | - in: path 39 | required: true 40 | type: string 41 | name: name 42 | description: Supervisor label 43 | responses: 44 | '200': 45 | description: Success 46 | schema: 47 | $ref: '#/definitions/Executable' 48 | post: 49 | summary: Create instance of supervisor config and run it 50 | description: '' 51 | operationId: StartInstace 52 | produces: 53 | - application/json 54 | parameters: 55 | - in: path 56 | required: true 57 | type: string 58 | name: name 59 | description: Supervisor label 60 | responses: 61 | '200': 62 | description: Success 63 | schema: 64 | $ref: '#/definitions/Instance' 65 | /instances: 66 | get: 67 | summary: Get all names of all spawned services 68 | description: '' 69 | operationId: ListInstances 70 | produces: 71 | - application/json 72 | responses: 73 | '200': 74 | description: Success 75 | schema: 76 | type: array 77 | items: 78 | type: string 79 | /instance/{name}: 80 | get: 81 | summary: Get instance config and status 82 | description: '' 83 | operationId: GetInstance 84 | produces: 85 | - application/json 86 | parameters: 87 | - in: path 88 | required: true 89 | type: string 90 | name: name 91 | description: Instance label 92 | responses: 93 | '200': 94 | description: Success 95 | schema: 96 | $ref: '#/definitions/Executable' 97 | post: 98 | summary: Stop instance by label 99 | description: '' 100 | operationId: StopInstace 101 | produces: 102 | - application/json 103 | parameters: 104 | - in: path 105 | required: true 106 | type: string 107 | name: name 108 | description: Instance label 109 | responses: 110 | '201': 111 | description: Success 112 | definitions: 113 | Executable: 114 | type: object 115 | properties: 116 | Name: 117 | type: string 118 | Command: 119 | type: string 120 | Args: 121 | type: array 122 | items: 123 | type: string 124 | Environment: 125 | type: object 126 | WorkDir: 127 | type: string 128 | StopTimeout: 129 | type: string 130 | RestartTimeout: 131 | type: string 132 | Restart: 133 | type: integer 134 | LogFiles: 135 | type: string 136 | Instance: 137 | type: object 138 | properties: 139 | running: 140 | type: boolean 141 | config: 142 | $ref: '#/definitions/Executable' 143 | 144 | 145 | externalDocs: 146 | url: 'https://github.com/reddec/monexec' 147 | --------------------------------------------------------------------------------