├── .appveyor.yml ├── .github ├── FUNDING.yml └── workflows │ ├── linux.yml │ └── windows.yml ├── .gitignore ├── AUTHORS ├── LICENSE ├── README.md ├── SECURITY.md ├── doc.go ├── errors.go ├── go.mod ├── go.sum ├── govisor ├── main.go ├── ui.go ├── ui │ ├── apanel.go │ ├── app.go │ ├── hpanel.go │ ├── ipanel.go │ ├── keybar.go │ ├── lpanel.go │ ├── mpanel.go │ ├── panel.go │ ├── statusbar.go │ └── titlebar.go └── util │ └── util.go ├── govisor_test.go ├── govisord ├── main.go └── samples │ ├── README.md │ ├── cert.pem │ ├── key.pem │ ├── passwd │ └── services │ ├── d1.json │ ├── d2.json │ ├── d3.json │ ├── s1.json │ ├── s2.json │ ├── s3.json │ ├── s4.json │ ├── s5.json │ └── s6.json ├── log.go ├── manager.go ├── multilog.go ├── process.go ├── process_test.go ├── process_test.sh ├── properties.go ├── provider.go ├── rest ├── client.go ├── common.go └── doc.go ├── server └── server.go └── service.go /.appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.0.{build} 2 | clone_folder: c:\gopath\src\github.com\gdamore\mangos 3 | environment: 4 | GOPATH: c:\gopath 5 | build_script: 6 | - go version 7 | - go env 8 | - SET PATH=%LOCALAPPDATA%\atom\bin;%GOPATH%\bin;%PATH% 9 | - go get -t ./... 10 | - go build ./... 11 | - go install ./... 12 | test_script: 13 | - go test ./... 14 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [gdamore] 4 | tidelift: go/github.com/gdamore/govisor 5 | -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: linux 2 | on: [push] 3 | jobs: 4 | 5 | build: 6 | name: build 7 | runs-on: [ ubuntu-latest ] 8 | steps: 9 | 10 | - name: Set up Go 11 | uses: actions/setup-go@v5 12 | with: 13 | go-version: 1.18 14 | id: go 15 | 16 | - name: Go version 17 | run: go version 18 | 19 | - name: Check out code into the Go module directory 20 | uses: actions/checkout@v4 21 | 22 | - name: Get dependencies 23 | run: go get -v -t -d ./... 24 | 25 | - name: Build 26 | run: go build -v . 27 | 28 | - name: Test 29 | run: go test ./... 30 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: windows 2 | on: [push] 3 | jobs: 4 | 5 | build: 6 | name: build 7 | runs-on: [ windows-latest ] 8 | steps: 9 | 10 | - name: Set up Go 11 | uses: actions/setup-go@v5 12 | with: 13 | go-version: 1.18 14 | id: go 15 | 16 | - name: Check out code into the Go module directory 17 | uses: actions/checkout@v4 18 | 19 | - name: Get dependencies 20 | run: go get -v -t -d ./... 21 | 22 | - name: Build 23 | run: go build -v . 24 | 25 | - name: Test 26 | run: go test ./... 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | govisord/govisord 2 | govisor/govisor 3 | 4 | .idea 5 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | List of govisor contributors and authors; these are the copyright holders 2 | for govisor, referred to as The Govisor Authors. 3 | 4 | Garrett D'Amore 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## govisor 2 | 3 | [![Linux](https://img.shields.io/github/actions/workflow/status/gdamore/govisor/linux.yml?branch=main&logoColor=grey&logo=linux&label=)](https://github.com/gdamore/govisor/actions/workflows/linux.yml) 4 | [![Windows](https://img.shields.io/github/actions/workflow/status/gdamore/govisor/windows.yml?branch=main&logoColor=grey&logo=windows&label=)](https://github.com/gdamore/govisor/actions/workflows/windows.yml) 5 | [![GitHub License](https://img.shields.io/github/license/gdamore/govisor.svg)](https://github.com/gdamore/govisor/blob/master/LICENSE) 6 | [![Issues](https://img.shields.io/github/issues/gdamore/govisor.svg)](https://github.com/gdamore/govisor/issues) 7 | [![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](https://godoc.org/github.com/gdamore/govisor) 8 | 9 | Govisor is a framework for managing services. It supports dependency 10 | graphs of services, and handles starting, stopping, and restarting services 11 | as necessary. It also deals with failures, and supports self-healing, and 12 | has some advanced logging capabilities. It also offers a REST API for 13 | managing your services, as well as a nicer client API, and a snazzy little 14 | terminal application to monitor the services. 15 | 16 | There is a daemon (govisord) that can be used to manage a tree of process in 17 | a manner similar to init or SMF or systemd. However, it is designed to be 18 | suitable for use by unprivileged users, and it is possible to run many copies 19 | of govisord on the same system (but you will have to choose different TCP 20 | management ports.) 21 | 22 | Govisord listens by default at http://localhost:8321/ 23 | 24 | See govisord/main.go for a list of options, etc. 25 | 26 | Govisor is designed for embedding as well. You can embed the manager into 27 | your own application. The REST API implementation provides a http.Handler, 28 | so you can also wrap or embed the API with other web services. 29 | 30 | The govisor client application, "govisor", is in the govisor/ directory. 31 | It has a number of options, try it with -h to see them. 32 | 33 | To install the daemon and client: `go get -v github.com/gdamore/govisor/...` 34 | 35 | ### Commercial Support 36 | 37 | Govisor is absolutely free, but support is available if needed: 38 | 39 | - [TideLift](https://tidelift.com/) subscriptions include support for _govisor_, as well as many other open source packages. 40 | - [Staysail Systems Inc.](mailto:info@staysail.tech) offers direct support, and custom development around _govisor_ on an hourly basis. 41 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security contact information 2 | 3 | To report a security vulnerability, please use the 4 | [Tidelift security contact](https://tidelift.com/security). 5 | Tidelift will coordinate the fix and disclosure. 6 | 7 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package govisor provides a pure Go process management framework. 16 | // This is similar in concept to supervisord, but the implementation 17 | // and the interfaces are wholly different. Some inspiration is taken 18 | // from Solaris' SMF facility. 19 | // 20 | // Unlike other frameworks, the intention is that this framework is not 21 | // a replacement for your system's master process management (i.e. init, 22 | // upstart, or similar), but rather a tool for user's (or administrators) 23 | // to manage their own groups of processes as part of application 24 | // deployment. 25 | // 26 | // Multiple instances of govisor may be deployed, and an instance may 27 | // be deployed using Go's HTTP handler framework, so that it is possible 28 | // to register the manager within an existing server instance. 29 | // 30 | package govisor 31 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package govisor 16 | 17 | import ( 18 | "errors" 19 | ) 20 | 21 | var ( 22 | ErrNoManager = errors.New("No manager for service") 23 | ErrConflict = errors.New("Conflicting service enabled") 24 | ErrIsEnabled = errors.New("Service is enabled") 25 | ErrNotRunning = errors.New("Service is not running") 26 | ErrBadPropType = errors.New("Bad property type") 27 | ErrBadPropName = errors.New("Bad property name") 28 | ErrBadPropValue = errors.New("Bad property value") 29 | ErrPropReadOnly = errors.New("Property not changeable") 30 | ErrRateLimited = errors.New("Restarting too quickly") 31 | ErrNameExists = errors.New("Service name already exists") 32 | ) 33 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gdamore/govisor 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/gdamore/tcell/v2 v2.7.1 7 | github.com/gorilla/mux v1.7.4 8 | github.com/smartystreets/goconvey v1.6.4 9 | golang.org/x/crypto v0.17.0 10 | golang.org/x/net v0.17.0 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= 2 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 3 | github.com/gdamore/tcell/v2 v2.7.1 h1:TiCcmpWHiAU7F0rA2I3S2Y4mmLmO9KHxJ7E1QhYzQbc= 4 | github.com/gdamore/tcell/v2 v2.7.1/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= 5 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 6 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 7 | github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= 8 | github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 9 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 10 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 11 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 12 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 13 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 14 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 15 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 16 | github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= 17 | github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 18 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 19 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 20 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 21 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 22 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 23 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 24 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 25 | golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= 26 | golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= 27 | golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 28 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 29 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 30 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 31 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 32 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 33 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 34 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 35 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 36 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 37 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 38 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 39 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 40 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 41 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 42 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 43 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 44 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 45 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 46 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 47 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 48 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 49 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 50 | golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= 51 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 52 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 53 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 54 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 55 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 56 | golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= 57 | golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= 58 | golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= 59 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 60 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 61 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 62 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 63 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 64 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 65 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 66 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 67 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 68 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 69 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 70 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 71 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 72 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 73 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 74 | -------------------------------------------------------------------------------- /govisor/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Command govisor implements a client application that communicate to 16 | // govisord. It uses subcommands. 17 | // 18 | // The flags are 19 | // 20 | // -a
- select the listen address, default is 21 | // http://localhost:8321 22 | // -u - user name & password for basic auth 23 | // 24 | // Subcommands are 25 | // 26 | // services - list all services 27 | // status [ ...] - show status for the named services (or all) 28 | // info - show more detailed service info 29 | // enable - enable the named service 30 | // disable - disable the named service 31 | // restart - restart the named service 32 | // clear - clear the named service 33 | // log - obtain the log for the named service 34 | // 35 | package main 36 | 37 | import ( 38 | "crypto/tls" 39 | "crypto/x509" 40 | "flag" 41 | "fmt" 42 | "io/ioutil" 43 | "log" 44 | "net/http" 45 | "net/url" 46 | "os" 47 | "path/filepath" 48 | "sort" 49 | "time" 50 | 51 | "github.com/gdamore/govisor/govisor/util" 52 | "github.com/gdamore/govisor/rest" 53 | ) 54 | 55 | var addr string = "http://127.0.0.1:8321" 56 | 57 | func usage() { 58 | fmt.Fprintf(os.Stderr, 59 | "Usage: %s [flags] [args]", os.Args[0]) 60 | os.Exit(1) 61 | } 62 | 63 | func showStatus(s *rest.ServiceInfo) { 64 | d := time.Since(s.TimeStamp) 65 | // for printing second resolution is sufficient 66 | d -= d % time.Second 67 | fmt.Printf("%-20s %-10s %10s %s\n", s.Name, 68 | util.Status(s), util.FormatDuration(d), s.Status) 69 | } 70 | 71 | func loadCertPath(roots *x509.CertPool, dirname string) error { 72 | return filepath.Walk(dirname, 73 | func(path string, info os.FileInfo, err error) error { 74 | if err != nil { 75 | return err 76 | } 77 | if !info.IsDir() { 78 | if err := loadCertFile(roots, path); err != nil { 79 | return err 80 | } 81 | } 82 | return nil 83 | }) 84 | } 85 | 86 | func loadCertFile(roots *x509.CertPool, fname string) error { 87 | data, err := ioutil.ReadFile(fname) 88 | if err == nil { 89 | roots.AppendCertsFromPEM(data) 90 | } 91 | return err 92 | } 93 | 94 | func fatal(f string, e error) { 95 | msg := e.Error() 96 | if len(msg) > 4 && msg[0] == '4' { 97 | fmt.Fprintf(os.Stderr, "%s: %s\n", f, msg[4:]) 98 | } else { 99 | fmt.Fprintf(os.Stderr, "%s: %s\n", f, msg) 100 | } 101 | os.Exit(1) 102 | } 103 | 104 | func main() { 105 | user := "" 106 | pass := "" 107 | logfile := "" 108 | cafile := "" 109 | capath := "" 110 | insecure := false 111 | 112 | flag.StringVar(&addr, "addr", addr, "govisor address") 113 | flag.StringVar(&user, "user", user, "user name for authentication") 114 | flag.StringVar(&pass, "pass", pass, "password for authentication") 115 | flag.StringVar(&cafile, "cacert", cafile, "CA certificate file") 116 | flag.StringVar(&capath, "capath", capath, "CA certificates directory") 117 | flag.StringVar(&logfile, "debuglog", logfile, "debug log file") 118 | flag.BoolVar(&insecure, "insecure", insecure, "allow insecure TLS connections") 119 | flag.Parse() 120 | 121 | var dlog *log.Logger 122 | if logfile != "" { 123 | f, e := os.Create(logfile) 124 | if e == nil { 125 | dlog = log.New(f, "DEBUG:", log.LstdFlags) 126 | log.SetOutput(f) 127 | } 128 | } 129 | 130 | roots := x509.NewCertPool() 131 | if cafile == "" && capath == "" { 132 | roots = nil 133 | } else { 134 | if cafile != "" { 135 | if e := loadCertFile(roots, cafile); e != nil { 136 | fatal("Unable to load cert file", e) 137 | } 138 | } 139 | if capath != "" { 140 | if e := loadCertPath(roots, capath); e != nil { 141 | fatal("Unable to load cert path", e) 142 | } 143 | } 144 | } 145 | 146 | u, e := url.Parse(addr) 147 | if e != nil { 148 | fatal("Bad address", e) 149 | } 150 | tcfg := &tls.Config{ 151 | RootCAs: roots, 152 | ServerName: u.Host, 153 | InsecureSkipVerify: insecure, 154 | } 155 | tran := &http.Transport{ 156 | TLSClientConfig: tcfg, 157 | } 158 | 159 | client := rest.NewClient(tran, addr) 160 | if user != "" || pass != "" { 161 | client.SetAuth(user, pass) 162 | } 163 | 164 | args := flag.Args() 165 | if len(args) == 0 { 166 | if e := doUI(client, addr, dlog); e != nil { 167 | // status by default 168 | args = []string{"status"} 169 | } else { 170 | return 171 | } 172 | } 173 | 174 | switch args[0] { 175 | case "services": 176 | if len(args) != 1 { 177 | usage() 178 | } 179 | s, e := client.Services() 180 | if e != nil { 181 | fatal("Error", e) 182 | } 183 | sort.Strings(s) 184 | for _, name := range s { 185 | fmt.Println(name) 186 | } 187 | case "enable": 188 | if len(args) != 2 { 189 | usage() 190 | } 191 | e := client.EnableService(args[1]) 192 | if e != nil { 193 | fatal("Error", e) 194 | } 195 | case "disable": 196 | if len(args) != 2 { 197 | usage() 198 | } 199 | e := client.DisableService(args[1]) 200 | if e != nil { 201 | fatal("Error", e) 202 | } 203 | 204 | case "restart": 205 | if len(args) != 2 { 206 | usage() 207 | } 208 | e := client.RestartService(args[1]) 209 | if e != nil { 210 | fatal("Error", e) 211 | } 212 | 213 | case "clear": 214 | if len(args) != 2 { 215 | usage() 216 | } 217 | e := client.ClearService(args[1]) 218 | if e != nil { 219 | fatal("Error", e) 220 | } 221 | 222 | case "log": 223 | loginfo := &rest.LogInfo{} 224 | switch len(args) { 225 | case 0: 226 | usage() 227 | case 1: 228 | loginfo, e = client.GetLog("") 229 | case 2: 230 | loginfo, e = client.GetLog(args[1]) 231 | } 232 | if e != nil { 233 | fatal("Error", e) 234 | } 235 | for _, line := range loginfo.Records { 236 | fmt.Printf("%s %s\n", 237 | line.Time.Format(time.StampMilli), line.Text) 238 | } 239 | case "info": 240 | if len(args) != 2 { 241 | usage() 242 | } 243 | s, e := client.GetService(args[1]) 244 | if e != nil { 245 | fatal("Failed", e) 246 | } 247 | fmt.Printf("Name: %s\n", s.Name) 248 | fmt.Printf("Desc: %s\n", s.Description) 249 | fmt.Printf("Status: %s\n", util.Status(s)) 250 | fmt.Printf("Since: %v\n", time.Now().Sub(s.TimeStamp)) 251 | fmt.Printf("Detail: %s\n", s.Status) 252 | fmt.Printf("Provides: ") 253 | for _, p := range s.Provides { 254 | fmt.Printf(" %s", p) 255 | } 256 | fmt.Printf("\n") 257 | fmt.Printf("Depends: ") 258 | for _, p := range s.Depends { 259 | fmt.Printf(" %s", p) 260 | } 261 | fmt.Printf("\n") 262 | fmt.Printf("Conflicts: ") 263 | for _, p := range s.Conflicts { 264 | fmt.Printf(" %s", p) 265 | } 266 | fmt.Printf("\n") 267 | case "status": 268 | names := args[1:] 269 | var e error 270 | if len(names) == 0 { 271 | names, e = client.Services() 272 | if e != nil { 273 | fatal("Error", e) 274 | } 275 | } 276 | if len(names) == 0 { 277 | // No services? 278 | return 279 | } 280 | infos := []*rest.ServiceInfo{} 281 | for _, n := range names { 282 | info, e := client.GetService(n) 283 | if e == nil { 284 | infos = append(infos, info) 285 | } else { 286 | fatal("Error", e) 287 | } 288 | } 289 | util.SortServices(infos) 290 | for _, info := range infos { 291 | showStatus(info) 292 | } 293 | case "ui": 294 | if e := doUI(client, addr, dlog); e != nil { 295 | fatal("Error", e) 296 | } 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /govisor/ui.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "log" 19 | 20 | "github.com/gdamore/govisor/govisor/ui" 21 | "github.com/gdamore/govisor/rest" 22 | ) 23 | 24 | func doUI(client *rest.Client, url string, logger *log.Logger) error { 25 | app := ui.NewApp(client, url) 26 | app.SetLogger(logger) 27 | 28 | app.Run() 29 | return nil 30 | } 31 | 32 | /* 33 | Our screen has the following appearance: 34 | 35 | Server: http://localhost:8321/ 36 | xxx Services xxx Running yyy Faulted zzz Standby Govisor v1.0 37 | ____________________________________________________________________________ 38 | ... 39 | testservice:name faulted 4d10m32s Failed: Terminated 40 | ... 41 | testservice:ok running 5s Service started 42 | ... 43 | dontrunme:ever disabled 132d10m5s Service disabled 44 | ... 45 | ____________________________________________________________________________ 46 | [Q]uit [I]Info [E]nable [D]isable [R]estart [C]lear [L]og [H]elp 47 | */ 48 | -------------------------------------------------------------------------------- /govisor/ui/apanel.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package ui 16 | 17 | import ( 18 | "github.com/gdamore/tcell/v2" 19 | "github.com/gdamore/tcell/v2/views" 20 | 21 | "github.com/gdamore/govisor/rest" 22 | ) 23 | 24 | type AuthPanel struct { 25 | hlayout *views.BoxLayout 26 | left *views.BoxLayout 27 | right *views.BoxLayout 28 | uprompt *views.Text 29 | pprompt *views.Text 30 | ufield *views.Text 31 | pfield *views.Text 32 | passactive bool 33 | username []rune 34 | password []rune 35 | info *rest.ServiceInfo 36 | err error // last error retrieving state 37 | 38 | Panel 39 | } 40 | 41 | func NewAuthPanel(app *App, server string) *AuthPanel { 42 | apanel := &AuthPanel{} 43 | apanel.Panel.Init(app) 44 | 45 | st := tcell.StyleDefault. 46 | Foreground(tcell.ColorSilver). 47 | Background(tcell.ColorBlack) 48 | 49 | apanel.username = make([]rune, 0, 128) 50 | apanel.password = make([]rune, 0, 128) 51 | 52 | apanel.hlayout = views.NewBoxLayout(views.Horizontal) 53 | apanel.left = views.NewBoxLayout(views.Vertical) 54 | apanel.right = views.NewBoxLayout(views.Vertical) 55 | apanel.uprompt = views.NewText() 56 | apanel.pprompt = views.NewText() 57 | apanel.ufield = views.NewText() 58 | apanel.pfield = views.NewText() 59 | apanel.ufield.SetText(" ") 60 | apanel.pfield.SetText(" ") 61 | apanel.uprompt.SetText("Username: ") 62 | apanel.pprompt.SetText("Password: ") 63 | 64 | apanel.uprompt.SetStyle(st) 65 | apanel.pprompt.SetStyle(st) 66 | 67 | apanel.ufield.SetStyle(st) 68 | apanel.pfield.SetStyle(st) 69 | 70 | apanel.hlayout.SetStyle(st) 71 | apanel.left.SetStyle(st) 72 | apanel.right.SetStyle(st) 73 | 74 | apanel.left.AddWidget(views.NewSpacer(), 1.0) 75 | apanel.left.AddWidget(apanel.uprompt, 0.0) 76 | apanel.left.AddWidget(apanel.pprompt, 0.0) 77 | apanel.left.AddWidget(views.NewSpacer(), 1.0) 78 | 79 | apanel.right.AddWidget(views.NewSpacer(), 1.0) 80 | apanel.right.AddWidget(apanel.ufield, 0.0) 81 | apanel.right.AddWidget(apanel.pfield, 0.0) 82 | apanel.right.AddWidget(views.NewSpacer(), 1.0) 83 | 84 | apanel.hlayout.AddWidget(views.NewSpacer(), 1.0) 85 | apanel.hlayout.AddWidget(apanel.left, 0.0) 86 | apanel.hlayout.AddWidget(apanel.right, 0.0) 87 | apanel.hlayout.AddWidget(views.NewSpacer(), 1.0) 88 | 89 | apanel.SetTitle(server) 90 | apanel.SetStatus("Authentication Required") 91 | apanel.SetKeys([]string{"[ESC] Quit"}) 92 | apanel.SetContent(apanel.hlayout) 93 | 94 | return apanel 95 | } 96 | 97 | func (a *AuthPanel) ResetFields() { 98 | a.passactive = false 99 | a.username = a.username[0:0] 100 | a.password = a.password[0:0] 101 | } 102 | 103 | func (a *AuthPanel) Draw() { 104 | a.update() 105 | a.Panel.Draw() 106 | } 107 | 108 | func (a *AuthPanel) HandleEvent(ev tcell.Event) bool { 109 | switch ev := ev.(type) { 110 | case *tcell.EventKey: 111 | switch ev.Key() { 112 | case tcell.KeyEsc: 113 | a.App().Quit() 114 | return true 115 | case tcell.KeyTab, tcell.KeyEnter: 116 | if a.passactive { 117 | a.App().SetUserPassword(string(a.username), 118 | string(a.password)) 119 | a.App().ShowMain() 120 | } else { 121 | a.passactive = true 122 | } 123 | case tcell.KeyBacktab: 124 | if a.passactive { 125 | a.passactive = false 126 | } 127 | case tcell.KeyCtrlU, tcell.KeyCtrlW: 128 | if a.passactive { 129 | a.password = a.password[:0] 130 | } else { 131 | a.username = a.username[:0] 132 | } 133 | case tcell.KeyBackspace, tcell.KeyBackspace2: 134 | if a.passactive { 135 | if len(a.password) > 0 { 136 | a.password = 137 | a.password[:len(a.password)-1] 138 | } 139 | } else { 140 | if len(a.username) > 0 { 141 | a.username = 142 | a.username[:len(a.username)-1] 143 | } 144 | } 145 | case tcell.KeyRune: 146 | r := ev.Rune() 147 | if a.passactive { 148 | if len(a.password) < 256 { 149 | a.password = append(a.password, r) 150 | } 151 | } else { 152 | if len(a.username) < 256 { 153 | a.username = append(a.username, r) 154 | } 155 | } 156 | default: 157 | return false 158 | } 159 | return true 160 | } 161 | return a.Panel.HandleEvent(ev) 162 | } 163 | 164 | // update must be called with AppLock held. 165 | func (a *AuthPanel) update() { 166 | 167 | maxlen := 16 168 | 169 | a.Panel.SetError() 170 | 171 | var passprompt []rune 172 | userprompt := append([]rune{}, a.username...) 173 | 174 | for range a.password { 175 | passprompt = append(passprompt, '*') 176 | } 177 | if !a.passactive { 178 | userprompt = append(userprompt, '_') 179 | } else { 180 | passprompt = append(passprompt, '_') 181 | } 182 | 183 | if len(userprompt) > maxlen { 184 | userprompt = userprompt[len(userprompt)-maxlen:] 185 | userprompt[0] = '<' 186 | } 187 | for len(userprompt) < maxlen { 188 | userprompt = append(userprompt, ' ') 189 | } 190 | if len(passprompt) > maxlen { 191 | passprompt = passprompt[len(passprompt)-maxlen:] 192 | passprompt[0] = '<' 193 | } 194 | for len(passprompt) < maxlen { 195 | passprompt = append(passprompt, ' ') 196 | } 197 | a.ufield.SetText(string(userprompt)) 198 | a.pfield.SetText(string(passprompt)) 199 | 200 | focus := tcell.StyleDefault. 201 | Foreground(tcell.ColorWhite).Background(tcell.ColorNavy) 202 | idle := tcell.StyleDefault. 203 | Foreground(tcell.ColorSilver).Background(tcell.ColorBlack) 204 | 205 | if a.passactive { 206 | a.pfield.SetStyle(focus) 207 | a.ufield.SetStyle(idle) 208 | } else { 209 | a.ufield.SetStyle(focus) 210 | a.pfield.SetStyle(idle) 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /govisor/ui/app.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package ui 16 | 17 | import ( 18 | "errors" 19 | "log" 20 | "time" 21 | 22 | "golang.org/x/net/context" 23 | 24 | "github.com/gdamore/tcell/v2" 25 | "github.com/gdamore/tcell/v2/views" 26 | 27 | "github.com/gdamore/govisor/govisor/util" 28 | "github.com/gdamore/govisor/rest" 29 | ) 30 | 31 | type App struct { 32 | app *views.Application 33 | view views.View 34 | panel views.Widget 35 | info *InfoPanel 36 | help *HelpPanel 37 | auth *AuthPanel 38 | log *LogPanel 39 | main *MainPanel 40 | client *rest.Client 41 | logger *log.Logger 42 | err error 43 | items []*rest.ServiceInfo 44 | selected *rest.ServiceInfo 45 | logName string 46 | logInfo *rest.LogInfo 47 | logErr error 48 | logCtx context.Context 49 | logCancel context.CancelFunc 50 | wake chan struct{} 51 | 52 | views.WidgetWatchers 53 | } 54 | 55 | func (a *App) show(w views.Widget) { 56 | a.app.PostFunc(func() { 57 | if w != a.panel { 58 | a.panel.SetView(nil) 59 | a.panel = w 60 | } 61 | 62 | a.panel.SetView(a.view) 63 | a.Resize() 64 | a.app.Refresh() 65 | }) 66 | } 67 | 68 | func (a *App) ShowHelp() { 69 | a.show(a.help) 70 | } 71 | 72 | func (a *App) ShowInfo(name string) { 73 | a.info.SetName(name) 74 | a.show(a.info) 75 | } 76 | 77 | func (a *App) ShowLog(name string) { 78 | if a.logCancel != nil { 79 | a.logCancel() 80 | } 81 | ctx, cancel := context.WithTimeout(context.Background(), time.Hour) 82 | a.logInfo = nil 83 | a.logName = name 84 | a.logCtx = ctx 85 | a.logCancel = cancel 86 | a.log.SetName(name) 87 | go a.refreshLog(ctx, name) 88 | 89 | a.show(a.log) 90 | } 91 | 92 | func (a *App) ShowMain() { 93 | a.show(a.main) 94 | } 95 | 96 | func (a *App) ShowAuth() { 97 | a.auth.ResetFields() 98 | a.show(a.auth) 99 | } 100 | 101 | func (a *App) SetUserPassword(user, pass string) { 102 | a.client.SetAuth(user, pass) 103 | a.items = nil 104 | a.err = nil 105 | var s struct{} 106 | select { 107 | case a.wake <- s: 108 | default: 109 | } 110 | } 111 | 112 | func (a *App) DisableService(name string) { 113 | a.client.DisableService(name) 114 | } 115 | 116 | func (a *App) EnableService(name string) { 117 | a.client.EnableService(name) 118 | } 119 | 120 | func (a *App) ClearService(name string) { 121 | a.client.ClearService(name) 122 | } 123 | 124 | func (a *App) RestartService(name string) { 125 | a.client.RestartService(name) 126 | } 127 | 128 | func (a *App) Quit() { 129 | /* This just posts the quit event. */ 130 | a.app.Quit() 131 | } 132 | 133 | func (a *App) SetLogger(logger *log.Logger) { 134 | a.logger = logger 135 | if logger != nil { 136 | logger.Printf("Start logger") 137 | } 138 | } 139 | 140 | func (a *App) Logf(fmt string, v ...interface{}) { 141 | if a.logger != nil { 142 | a.logger.Printf(fmt, v...) 143 | } 144 | } 145 | 146 | func (a *App) HandleEvent(ev tcell.Event) bool { 147 | switch ev := ev.(type) { 148 | case *tcell.EventKey: 149 | switch ev.Key() { 150 | // Intercept a few control keys up front, for global handling. 151 | case tcell.KeyCtrlC: 152 | a.Quit() 153 | return true 154 | case tcell.KeyCtrlL: 155 | a.app.Refresh() 156 | return true 157 | } 158 | } 159 | 160 | if a.panel != nil { 161 | return a.panel.HandleEvent(ev) 162 | } 163 | return false 164 | } 165 | 166 | func (a *App) Draw() { 167 | if a.panel != nil { 168 | a.panel.Draw() 169 | } 170 | } 171 | 172 | func (a *App) Resize() { 173 | if a.panel != nil { 174 | a.panel.Resize() 175 | } 176 | } 177 | 178 | func (a *App) SetView(view views.View) { 179 | a.view = view 180 | if a.panel != nil { 181 | a.panel.SetView(view) 182 | } 183 | } 184 | 185 | func (a *App) Size() (int, int) { 186 | if a.panel != nil { 187 | return a.panel.Size() 188 | } 189 | return 0, 0 190 | } 191 | 192 | func (a *App) GetClient() *rest.Client { 193 | return a.client 194 | } 195 | 196 | func (a *App) GetAppName() string { 197 | return "Govisor v1.3" 198 | } 199 | 200 | func NewApp(client *rest.Client, url string) *App { 201 | 202 | app := &App{} 203 | app.app = &views.Application{} 204 | app.client = client 205 | app.info = NewInfoPanel(app) 206 | app.help = NewHelpPanel(app) 207 | app.auth = NewAuthPanel(app, url) 208 | app.log = NewLogPanel(app) 209 | app.main = NewMainPanel(app, url) 210 | app.panel = app.main 211 | app.wake = make(chan struct{}) 212 | 213 | app.app.SetStyle(tcell.StyleDefault. 214 | Foreground(tcell.ColorSilver). 215 | Background(tcell.ColorBlack)) 216 | 217 | go app.refresh() 218 | return app 219 | } 220 | 221 | // refresh keeps the app items current 222 | 223 | func (a *App) getItems() ([]*rest.ServiceInfo, error) { 224 | names, e := a.client.Services() 225 | if e != nil { 226 | return nil, e 227 | } 228 | items := make([]*rest.ServiceInfo, 0, len(names)) 229 | for _, n := range names { 230 | item, e := a.client.GetService(n) 231 | if e == nil { 232 | items = append(items, item) 233 | } 234 | } 235 | util.SortServices(items) 236 | return items, nil 237 | } 238 | 239 | func (a *App) refresh() { 240 | client := a.client 241 | etag := "" 242 | for { 243 | items, e := a.getItems() 244 | 245 | a.app.PostFunc(func() { 246 | a.items = items 247 | a.err = e 248 | a.app.Update() 249 | }) 250 | ctx, cancel := context.WithTimeout(context.Background(), 251 | time.Hour) 252 | etag, e = client.Watch(ctx, etag) 253 | cancel() 254 | if e != nil { 255 | select { 256 | case <-a.wake: 257 | case <-time.After(2 * time.Second): 258 | } 259 | } 260 | } 261 | } 262 | 263 | func (a *App) refreshLog(ctx context.Context, name string) { 264 | info, e := a.client.GetLog(name) 265 | 266 | for { 267 | a.app.PostFunc(func() { 268 | if a.logName == name { 269 | a.logInfo = info 270 | a.logErr = e 271 | a.app.Update() 272 | } 273 | }) 274 | select { 275 | case <-ctx.Done(): 276 | return 277 | default: 278 | } 279 | info, e = a.client.WatchLog(ctx, name, info) 280 | if (e != nil) { 281 | select { 282 | case <- ctx.Done(): 283 | return 284 | case <- a.wake: 285 | case <- time.After(2 * time.Second): 286 | } 287 | } 288 | } 289 | } 290 | 291 | func (a *App) GetItems() ([]*rest.ServiceInfo, error) { 292 | return a.items, a.err 293 | } 294 | 295 | func (a *App) GetItem(name string) (*rest.ServiceInfo, error) { 296 | if a.err != nil { 297 | return nil, a.err 298 | } 299 | for _, i := range a.items { 300 | if i.Name == name { 301 | return i, nil 302 | } 303 | } 304 | return nil, errors.New("Service not found") 305 | } 306 | 307 | func (a *App) GetLog(name string) (*rest.LogInfo, error) { 308 | if a.logName == name { 309 | return a.logInfo, a.logErr 310 | } 311 | return nil, nil 312 | } 313 | 314 | func (a *App) Run() { 315 | a.Logf("Starting up user interface") 316 | a.app.SetRootWidget(a) 317 | a.ShowMain() 318 | go func() { 319 | // Give us periodic updates 320 | for { 321 | a.app.Update() 322 | time.Sleep(time.Second) 323 | } 324 | }() 325 | a.Logf("Starting app loop") 326 | a.app.Run() 327 | } 328 | -------------------------------------------------------------------------------- /govisor/ui/hpanel.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package ui 16 | 17 | import ( 18 | "github.com/gdamore/tcell/v2" 19 | "github.com/gdamore/tcell/v2/views" 20 | ) 21 | 22 | type HelpPanel struct { 23 | text *views.TextArea 24 | Panel 25 | } 26 | 27 | func (h *HelpPanel) HandleEvent(ev tcell.Event) bool { 28 | switch ev := ev.(type) { 29 | case *tcell.EventKey: 30 | switch ev.Key() { 31 | case tcell.KeyEsc: 32 | h.App().ShowMain() 33 | return true 34 | case tcell.KeyRune: 35 | switch ev.Rune() { 36 | case 'Q', 'q': 37 | h.app.ShowMain() 38 | return true 39 | } 40 | } 41 | } 42 | return h.Panel.HandleEvent(ev) 43 | } 44 | 45 | func (h *HelpPanel) Draw() { 46 | h.SetKeys([]string{"[ESC] Main"}) 47 | h.SetTitle("Help") 48 | h.Panel.Draw() 49 | } 50 | 51 | func (h *HelpPanel) Init(app *App) { 52 | 53 | h.Panel.Init(app) 54 | 55 | // No, we don't have context-sensitive help. 56 | h.text = views.NewTextArea() 57 | h.text.SetLines([]string{ 58 | "Supported keys (not all keys available in all contexts)", 59 | "", 60 | " : return to main screen", 61 | " : quit", 62 | " : refresh the screeen", 63 | " : show this help", 64 | " , : navigation", 65 | " , ", 66 | " , ", 67 | " : enable selected service", 68 | " : disable selected service", 69 | " : view detailed information for service", 70 | " : restart selected service", 71 | " : clear faults on selected service", 72 | " : view log for selected service", 73 | "", 74 | "This program is distributed under the Apache 2.0 License", 75 | "Copyright 2016 The Govisor Authors", 76 | }) 77 | h.SetContent(h.text) 78 | } 79 | 80 | func NewHelpPanel(app *App) *HelpPanel { 81 | 82 | h := &HelpPanel{} 83 | 84 | h.Init(app) 85 | return h 86 | } 87 | -------------------------------------------------------------------------------- /govisor/ui/ipanel.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package ui 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/gdamore/tcell/v2" 21 | "github.com/gdamore/tcell/v2/views" 22 | 23 | "github.com/gdamore/govisor/govisor/util" 24 | "github.com/gdamore/govisor/rest" 25 | ) 26 | 27 | type InfoPanel struct { 28 | text *views.TextArea 29 | info *rest.ServiceInfo 30 | name string // service name 31 | err error // last error retrieving state 32 | 33 | Panel 34 | } 35 | 36 | func NewInfoPanel(app *App) *InfoPanel { 37 | ipanel := &InfoPanel{} 38 | ipanel.Panel.Init(app) 39 | 40 | ipanel.text = views.NewTextArea() 41 | ipanel.text.EnableCursor(false) 42 | ipanel.SetContent(ipanel.text) 43 | ipanel.text.SetStyle(tcell.StyleDefault. 44 | Foreground(tcell.ColorSilver).Background(tcell.ColorBlack)) 45 | 46 | // We don't change the keybar, so set it once 47 | ipanel.SetKeys([]string{"[Q] Quit", "[H] Help"}) 48 | 49 | return ipanel 50 | } 51 | 52 | func (i *InfoPanel) Draw() { 53 | i.update() 54 | i.Panel.Draw() 55 | } 56 | 57 | func (i *InfoPanel) HandleEvent(ev tcell.Event) bool { 58 | info := i.info 59 | switch ev := ev.(type) { 60 | case *tcell.EventKey: 61 | switch ev.Key() { 62 | case tcell.KeyEsc: 63 | i.App().ShowMain() 64 | return true 65 | case tcell.KeyF1: 66 | i.App().ShowHelp() 67 | return true 68 | case tcell.KeyRune: 69 | switch ev.Rune() { 70 | case 'Q', 'q': 71 | i.App().ShowMain() 72 | return true 73 | case 'H', 'h': 74 | i.App().ShowHelp() 75 | return true 76 | case 'L', 'l': 77 | if info != nil { 78 | i.App().ShowLog(info.Name) 79 | return true 80 | } 81 | case 'R', 'r': 82 | if info != nil { 83 | i.App().RestartService(info.Name) 84 | return true 85 | } 86 | case 'E', 'e': 87 | if info != nil && !info.Enabled { 88 | i.App().EnableService(info.Name) 89 | return true 90 | } 91 | case 'D', 'd': 92 | if info != nil && info.Enabled { 93 | i.App().DisableService(info.Name) 94 | return true 95 | } 96 | case 'C', 'c': 97 | if info != nil && info.Failed { 98 | i.App().ClearService(info.Name) 99 | return true 100 | } 101 | } 102 | } 103 | } 104 | return i.Panel.HandleEvent(ev) 105 | } 106 | 107 | func (i *InfoPanel) SetName(name string) { 108 | i.name = name 109 | } 110 | 111 | // update must be called with AppLock held. 112 | func (i *InfoPanel) update() { 113 | 114 | s, e := i.App().GetItem(i.name) 115 | 116 | if i.info == s && i.err == e { 117 | return 118 | } 119 | i.info = s 120 | i.err = e 121 | words := []string{"[ESC] Main", "[H] Help"} 122 | 123 | i.SetTitle("Details for " + i.name) 124 | 125 | if s == nil { 126 | if i.err != nil { 127 | i.SetStatus(fmt.Sprintf("No data: %v", i.err)) 128 | i.SetError() 129 | } else { 130 | i.SetStatus("Loading...") 131 | i.SetNormal() 132 | } 133 | i.text.SetLines(nil) 134 | i.SetKeys(words) 135 | return 136 | } 137 | 138 | i.SetStatus("") 139 | if !s.Enabled { 140 | i.SetNormal() 141 | } else if s.Failed { 142 | i.SetError() 143 | } else if s.Running { 144 | i.SetGood() 145 | } else { 146 | i.SetWarn() 147 | } 148 | 149 | lines := make([]string, 0, 8) 150 | lines = append(lines, fmt.Sprintf("%13s %s", "Name:", s.Name)) 151 | lines = append(lines, fmt.Sprintf("%13s %s", "Description:", 152 | s.Description)) 153 | lines = append(lines, fmt.Sprintf("%13s %s", "Status:", util.Status(s))) 154 | lines = append(lines, fmt.Sprintf("%13s %v", "Since:", s.TimeStamp)) 155 | lines = append(lines, fmt.Sprintf("%13s %s", "Detail:", s.Status)) 156 | 157 | l := fmt.Sprintf("%13s", "Provides:") 158 | for _, p := range s.Provides { 159 | l = l + fmt.Sprintf(" %s", p) 160 | } 161 | lines = append(lines, l) 162 | 163 | l = fmt.Sprintf("%13s", "Depends:") 164 | for _, p := range s.Depends { 165 | l = l + fmt.Sprintf(" %s", p) 166 | } 167 | lines = append(lines, l) 168 | 169 | l = fmt.Sprintf("%13s", "Conflicts:") 170 | for _, p := range s.Conflicts { 171 | l = l + fmt.Sprintf(" %s", p) 172 | } 173 | lines = append(lines, l) 174 | 175 | i.text.SetLines(lines) 176 | 177 | words = append(words, "[L] Log") 178 | if !s.Enabled { 179 | words = append(words, "[E] Enable") 180 | } else { 181 | words = append(words, "[D] Disable") 182 | if s.Failed { 183 | words = append(words, "[C] Clear") 184 | } 185 | words = append(words, "[R] Restart") 186 | } 187 | i.SetKeys(words) 188 | } 189 | -------------------------------------------------------------------------------- /govisor/ui/keybar.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package ui 16 | 17 | import ( 18 | "sync" 19 | 20 | "github.com/gdamore/tcell/v2" 21 | "github.com/gdamore/tcell/v2/views" 22 | ) 23 | 24 | type KeyBar struct { 25 | once sync.Once 26 | views.SimpleStyledTextBar 27 | } 28 | 29 | func (k *KeyBar) Init() { 30 | k.once.Do(func() { 31 | normal := tcell.StyleDefault. 32 | Foreground(tcell.ColorBlack). 33 | Background(tcell.ColorSilver) 34 | alternate := tcell.StyleDefault. 35 | Foreground(tcell.ColorBlue). 36 | Background(tcell.ColorSilver).Bold(true) 37 | 38 | k.SimpleStyledTextBar.Init() 39 | k.SimpleStyledTextBar.SetStyle(normal) 40 | k.RegisterLeftStyle('N', normal) 41 | k.RegisterLeftStyle('A', alternate) 42 | }) 43 | } 44 | 45 | func (k *KeyBar) SetKeys(words []string) { 46 | b := make([]rune, 0, 80) 47 | for i, w := range words { 48 | esc := false 49 | if i != 0 && len(w) != 0 { 50 | b = append(b, ' ') 51 | } 52 | for _, r := range w { 53 | if esc { 54 | if r == ']' { 55 | b = append(b, '%', 'N') 56 | esc = false 57 | } else if r == '%' { 58 | b = append(b, '%') 59 | } 60 | b = append(b, r) 61 | 62 | } else { 63 | b = append(b, r) 64 | if r == '[' { 65 | esc = true 66 | b = append(b, '%', 'A') 67 | } else if r == '%' { 68 | b = append(b, '%') 69 | } 70 | } 71 | } 72 | } 73 | k.SetLeft(string(b)) 74 | } 75 | 76 | func NewKeyBar() *KeyBar { 77 | kb := &KeyBar{} 78 | kb.Init() 79 | return kb 80 | } 81 | -------------------------------------------------------------------------------- /govisor/ui/lpanel.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package ui 16 | 17 | import ( 18 | "fmt" 19 | "time" 20 | 21 | "github.com/gdamore/tcell/v2" 22 | "github.com/gdamore/tcell/v2/views" 23 | 24 | "github.com/gdamore/govisor/rest" 25 | ) 26 | 27 | type LogPanel struct { 28 | text *views.TextArea 29 | info *rest.ServiceInfo 30 | name string // service name 31 | err error // last error retrieving state 32 | 33 | Panel 34 | } 35 | 36 | func NewLogPanel(app *App) *LogPanel { 37 | p := &LogPanel{} 38 | 39 | p.Panel.Init(app) 40 | 41 | // We don't change the keybar, so set it once 42 | p.SetKeys([]string{"[Q] Quit", "[H] Help"}) 43 | 44 | p.text = views.NewTextArea() 45 | p.text.EnableCursor(false) 46 | p.text.SetStyle(tcell.StyleDefault. 47 | Foreground(tcell.ColorSilver).Background(tcell.ColorBlack)) 48 | p.SetContent(p.text) 49 | p.update() 50 | 51 | return p 52 | } 53 | 54 | func (p *LogPanel) Draw() { 55 | p.update() 56 | p.Panel.Draw() 57 | } 58 | 59 | func (p *LogPanel) HandleEvent(ev tcell.Event) bool { 60 | info := p.info 61 | app := p.app 62 | switch ev := ev.(type) { 63 | case *tcell.EventKey: 64 | switch ev.Key() { 65 | case tcell.KeyEsc: 66 | app.ShowMain() 67 | return true 68 | case tcell.KeyF1: 69 | app.ShowHelp() 70 | return true 71 | case tcell.KeyRune: 72 | switch ev.Rune() { 73 | case 'Q', 'q': 74 | app.ShowMain() 75 | return true 76 | case 'H', 'h': 77 | app.ShowHelp() 78 | return true 79 | case 'I', 'i': 80 | if info != nil { 81 | app.ShowInfo(info.Name) 82 | return true 83 | } 84 | case 'R', 'r': 85 | if info != nil { 86 | app.RestartService(info.Name) 87 | return true 88 | } 89 | case 'E', 'e': 90 | if info != nil && !info.Enabled { 91 | app.EnableService(info.Name) 92 | return true 93 | } 94 | case 'D', 'd': 95 | if info != nil && info.Enabled { 96 | app.DisableService(info.Name) 97 | return true 98 | } 99 | case 'C', 'c': 100 | if info != nil && info.Failed { 101 | app.ClearService(info.Name) 102 | return true 103 | } 104 | } 105 | } 106 | } 107 | return p.Panel.HandleEvent(ev) 108 | } 109 | 110 | func (p *LogPanel) SetName(name string) { 111 | p.SetTitle("Loading") 112 | p.text.SetLines(nil) 113 | p.name = name 114 | } 115 | 116 | // update must be called with AppLock held. 117 | func (p *LogPanel) update() { 118 | 119 | svcinfo, e1 := p.app.GetItem(p.name) 120 | loginfo, e2 := p.app.GetLog(p.name) 121 | p.info = svcinfo 122 | 123 | words := []string{"[ESC] Main", "[H] Help"} 124 | 125 | if p.name == "" { 126 | p.SetTitle("Consolidated Log") 127 | } else { 128 | p.SetTitle("Log for " + p.name) 129 | } 130 | 131 | if (svcinfo == nil && p.name != "") || loginfo == nil { 132 | e := e2 133 | if e == nil { 134 | e = e1 135 | } 136 | if p.err != nil { 137 | p.SetStatus(fmt.Sprintf("No data: %v", e)) 138 | p.SetError() 139 | } else { 140 | p.SetStatus("Loading ...") 141 | p.SetNormal() 142 | } 143 | p.text.SetLines([]string{""}) 144 | p.SetKeys(words) 145 | return 146 | } 147 | 148 | p.SetStatus("") 149 | if svcinfo != nil { 150 | if !svcinfo.Enabled { 151 | p.SetNormal() 152 | } else if svcinfo.Failed { 153 | p.SetError() 154 | } else if svcinfo.Running { 155 | p.SetGood() 156 | } else { 157 | p.SetWarn() 158 | } 159 | } 160 | 161 | lines := make([]string, 0, len(loginfo.Records)) 162 | for _, r := range loginfo.Records { 163 | line := fmt.Sprintf("%s %s", 164 | r.Time.Format(time.StampMilli), r.Text) 165 | lines = append(lines, line) 166 | } 167 | p.text.SetLines(lines) 168 | 169 | if svcinfo != nil { 170 | words = append(words, "[I] Info") 171 | if !svcinfo.Enabled { 172 | words = append(words, "[E] Enable") 173 | } else { 174 | words = append(words, "[D] Disable") 175 | if svcinfo.Failed { 176 | words = append(words, "[C] Clear") 177 | } 178 | words = append(words, "[R] Restart") 179 | } 180 | } 181 | p.SetKeys(words) 182 | } 183 | -------------------------------------------------------------------------------- /govisor/ui/mpanel.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package ui 16 | 17 | import ( 18 | "fmt" 19 | "time" 20 | 21 | "github.com/gdamore/tcell/v2" 22 | "github.com/gdamore/tcell/v2/views" 23 | 24 | "github.com/gdamore/govisor/govisor/util" 25 | "github.com/gdamore/govisor/rest" 26 | ) 27 | 28 | type sorted []*rest.ServiceInfo 29 | 30 | var ( 31 | StyleNormal = tcell.StyleDefault. 32 | Foreground(tcell.ColorSilver). 33 | Background(tcell.ColorBlack) 34 | StyleGood = tcell.StyleDefault. 35 | Foreground(tcell.ColorGreen). 36 | Background(tcell.ColorBlack) 37 | StyleWarn = tcell.StyleDefault. 38 | Foreground(tcell.ColorYellow). 39 | Background(tcell.ColorBlack) 40 | StyleError = tcell.StyleDefault. 41 | Foreground(tcell.ColorMaroon). 42 | Background(tcell.ColorBlack) 43 | ) 44 | 45 | func (s sorted) Swap(i, j int) { 46 | s[i], s[j] = s[j], s[i] 47 | } 48 | 49 | func (s sorted) Len() int { 50 | return len(s) 51 | } 52 | 53 | func (s sorted) Less(i, j int) bool { 54 | a := s[i] 55 | b := s[j] 56 | 57 | if a.Failed != b.Failed { 58 | // put failed items at front 59 | return a.Failed 60 | } 61 | if a.Enabled != b.Enabled { 62 | // enabled in front of non-enabled items 63 | return a.Enabled 64 | } 65 | // We don't worry about suspended items vs. running -- no clear order 66 | // there. We just sort based on name 67 | return a.Name < b.Name 68 | } 69 | 70 | // MainPanel implements a Widget as a Panel, but provides the data 71 | // model and handling for the content area, using data loaded from a Govisor 72 | // REST API service. 73 | type MainPanel struct { 74 | info *rest.ServiceInfo 75 | name string // service name 76 | err error // last error retrieving state 77 | content *views.CellView 78 | selected *rest.ServiceInfo 79 | ndisabled int 80 | nfailed int 81 | nrunning int 82 | nstopped int 83 | width int 84 | height int 85 | curx int 86 | cury int 87 | lines []string 88 | styles []tcell.Style 89 | items []*rest.ServiceInfo 90 | 91 | Panel 92 | } 93 | 94 | // mainModel provides the model for a CellArea. 95 | type mainModel struct { 96 | m *MainPanel 97 | } 98 | 99 | func NewMainPanel(app *App, server string) *MainPanel { 100 | m := &MainPanel{} 101 | 102 | m.Panel.Init(app) 103 | m.content = views.NewCellView() 104 | m.SetContent(m.content) 105 | 106 | m.content.SetModel(&mainModel{m}) 107 | m.content.SetStyle(StyleNormal) 108 | 109 | m.SetTitle(server) 110 | m.SetKeys([]string{"[Q] Quit"}) 111 | 112 | return m 113 | } 114 | 115 | func (m *MainPanel) Draw() { 116 | m.update() 117 | m.Panel.Draw() 118 | } 119 | 120 | func (m *MainPanel) HandleEvent(ev tcell.Event) bool { 121 | switch ev := ev.(type) { 122 | case *tcell.EventKey: 123 | switch ev.Key() { 124 | case tcell.KeyEsc: 125 | m.unselect() 126 | return true 127 | case tcell.KeyF1: 128 | m.App().ShowHelp() 129 | return true 130 | case tcell.KeyEnter: 131 | if m.selected != nil { 132 | m.App().ShowInfo(m.selected.Name) 133 | return true 134 | } 135 | case tcell.KeyRune: 136 | switch ev.Rune() { 137 | case 'Q', 'q': 138 | m.App().Quit() 139 | return true 140 | case 'H', 'h': 141 | m.App().ShowHelp() 142 | return true 143 | case 'I', 'i': 144 | if m.selected != nil { 145 | m.App().ShowInfo(m.selected.Name) 146 | return true 147 | } 148 | case 'L', 'l': 149 | if m.selected != nil { 150 | m.App().ShowLog(m.selected.Name) 151 | return true 152 | } else { 153 | m.App().ShowLog("") 154 | return true 155 | } 156 | case 'E', 'e': 157 | if m.selected != nil && !m.selected.Enabled { 158 | m.App().EnableService(m.selected.Name) 159 | return true 160 | } 161 | case 'D', 'd': 162 | if m.selected != nil && m.selected.Enabled { 163 | m.App().DisableService(m.selected.Name) 164 | return true 165 | } 166 | case 'C', 'c': 167 | if m.selected != nil && m.selected.Failed { 168 | m.App().ClearService(m.selected.Name) 169 | return true 170 | } 171 | case 'R', 'r': 172 | if m.selected != nil { 173 | m.App().RestartService(m.selected.Name) 174 | return true 175 | } 176 | } 177 | } 178 | } 179 | return m.Panel.HandleEvent(ev) 180 | } 181 | 182 | // Model items 183 | func (model *mainModel) GetCell(x, y int) (rune, tcell.Style, []rune, int) { 184 | var ch rune 185 | var style tcell.Style 186 | 187 | m := model.m 188 | 189 | if y < 0 || y >= len(m.lines) { 190 | return ch, StyleNormal, nil, 1 191 | } 192 | 193 | if x >= 0 && x < len(m.lines[y]) { 194 | ch = rune(m.lines[y][x]) 195 | } else { 196 | ch = ' ' 197 | } 198 | style = m.styles[y] 199 | if m.items[y] == m.selected { 200 | style = style.Reverse(true) 201 | } 202 | return ch, style, nil, 1 203 | } 204 | 205 | func (model *mainModel) GetBounds() (int, int) { 206 | // This assumes that all content is displayable runes of width 1. 207 | m := model.m 208 | y := len(m.lines) 209 | x := 0 210 | for _, l := range m.lines { 211 | if x < len(l) { 212 | x = len(l) 213 | } 214 | } 215 | return x, y 216 | } 217 | 218 | func (model *mainModel) GetCursor() (int, int, bool, bool) { 219 | m := model.m 220 | return m.curx, m.cury, true, false 221 | } 222 | 223 | func (model *mainModel) MoveCursor(offx, offy int) { 224 | 225 | m := model.m 226 | m.curx += offx 227 | m.cury += offy 228 | m.updateCursor(true) 229 | } 230 | 231 | func (model *mainModel) SetCursor(x, y int) { 232 | m := model.m 233 | m.curx = x 234 | m.cury = y 235 | m.updateCursor(true) 236 | } 237 | 238 | func (m *MainPanel) unselect() { 239 | m.cury = 0 240 | m.curx = 0 241 | m.updateCursor(false) 242 | } 243 | 244 | func (m *MainPanel) updateCursor(selected bool) { 245 | if m.curx > m.width-1 { 246 | m.curx = m.width - 1 247 | } 248 | if m.cury > m.height-1 { 249 | m.cury = m.height - 1 250 | } 251 | if m.curx < 0 { 252 | m.curx = 0 253 | } 254 | if m.cury < 0 { 255 | m.cury = 0 256 | } 257 | if selected && m.height > 0 { 258 | if m.selected == nil { 259 | m.curx = 0 260 | m.cury = 0 261 | } 262 | m.selected = m.items[m.cury] 263 | } else { 264 | m.selected = nil 265 | } 266 | } 267 | 268 | // update is called to update content, e.g. in response to Draw() or 269 | // as part of another update. It is called with the AppLock held. 270 | func (m *MainPanel) update() { 271 | 272 | items, err := m.App().GetItems() 273 | m.items = items 274 | 275 | // preserve selected item 276 | if sel := m.selected; sel != nil { 277 | m.selected = nil 278 | cury := 0 279 | for _, item := range m.items { 280 | if item.Name == sel.Name { 281 | m.selected = item 282 | m.cury = cury 283 | } 284 | cury++ 285 | } 286 | } 287 | if err != nil { 288 | switch e := err.(type) { 289 | case *rest.Error: 290 | if e.Code == 401 { 291 | m.App().ShowAuth() 292 | return 293 | } 294 | } 295 | m.SetError() 296 | m.SetStatus(fmt.Sprintf("Cannot load items: %v", err)) 297 | m.lines = []string{} 298 | m.styles = []tcell.Style{} 299 | return 300 | } 301 | 302 | lines := make([]string, 0, len(m.items)) 303 | styles := make([]tcell.Style, 0, len(m.items)) 304 | 305 | m.ndisabled = 0 306 | m.nfailed = 0 307 | m.nstopped = 0 308 | m.nrunning = 0 309 | 310 | m.height = 0 311 | m.width = 0 312 | 313 | for _, info := range items { 314 | d := time.Since(info.TimeStamp) 315 | d -= d % time.Second 316 | line := fmt.Sprintf("%-20s %-10s %10s %-10s", 317 | info.Name, util.Status(info), util.FormatDuration(d), 318 | info.Status) 319 | 320 | if len(line) > m.width { 321 | m.width = len(line) 322 | } 323 | m.height++ 324 | 325 | lines = append(lines, line) 326 | var style tcell.Style 327 | if !info.Enabled { 328 | style = StyleNormal 329 | m.ndisabled++ 330 | } else if info.Failed { 331 | style = StyleError 332 | m.nfailed++ 333 | } else if !info.Running { 334 | style = StyleWarn 335 | m.nstopped++ 336 | } else { 337 | style = StyleGood 338 | m.nrunning++ 339 | } 340 | styles = append(styles, style) 341 | } 342 | 343 | m.lines = lines 344 | m.styles = styles 345 | 346 | m.SetStatus(fmt.Sprintf( 347 | "%6d Services %6d Faulted %6d Running %6d Standby %6d Disabled", 348 | len(m.items), 349 | m.nfailed, m.nrunning, m.nstopped, m.ndisabled)) 350 | 351 | if m.nfailed > 0 { 352 | m.SetError() 353 | } else if m.nstopped > 0 { 354 | m.SetWarn() 355 | } else if m.nrunning > 0 { 356 | m.SetGood() 357 | } else { 358 | m.SetNormal() 359 | } 360 | 361 | words := []string{"[Q] Quit", "[H] Help"} 362 | 363 | if item := m.selected; item != nil { 364 | words = append(words, "[I] Info") 365 | words = append(words, "[L] Log") 366 | if !item.Enabled { 367 | words = append(words, "[E] Enable") 368 | } else { 369 | words = append(words, "[D] Disable") 370 | if item.Failed { 371 | words = append(words, "[C] Clear") 372 | } 373 | words = append(words, "[R] Restart") 374 | } 375 | } else { 376 | words = append(words, "[L] Log") 377 | } 378 | m.SetKeys(words) 379 | } 380 | -------------------------------------------------------------------------------- /govisor/ui/panel.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package ui 16 | 17 | import ( 18 | "sync" 19 | 20 | "github.com/gdamore/tcell/v2/views" 21 | ) 22 | 23 | // Panel is just a wrapper around the views.Panel, but it changes 24 | // the names of elements to match our usage, making it easier (hopefully) 25 | // to grok what is going on. 26 | 27 | type Panel struct { 28 | tb *TitleBar 29 | sb *StatusBar 30 | kb *KeyBar 31 | once sync.Once 32 | app *App 33 | 34 | views.Panel 35 | } 36 | 37 | func (p *Panel) SetTitle(title string) { 38 | p.tb.SetCenter(title) 39 | } 40 | 41 | func (p *Panel) SetKeys(words []string) { 42 | p.kb.SetKeys(words) 43 | } 44 | 45 | func (p *Panel) SetStatus(status string) { 46 | p.sb.SetText(status) 47 | } 48 | 49 | func (p *Panel) SetGood() { 50 | p.sb.SetGood() 51 | } 52 | 53 | func (p *Panel) SetNormal() { 54 | p.sb.SetNormal() 55 | } 56 | 57 | func (p *Panel) SetWarn() { 58 | p.sb.SetWarn() 59 | } 60 | 61 | func (p *Panel) SetError() { 62 | p.sb.SetError() 63 | } 64 | 65 | func (p *Panel) Init(app *App) { 66 | p.once.Do(func() { 67 | p.app = app 68 | 69 | p.tb = NewTitleBar() 70 | p.tb.SetRight(app.GetAppName()) 71 | p.tb.SetCenter(" ") 72 | 73 | p.kb = NewKeyBar() 74 | 75 | p.sb = NewStatusBar() 76 | 77 | p.Panel.SetTitle(p.tb) 78 | p.Panel.SetMenu(p.sb) 79 | p.Panel.SetStatus(p.kb) 80 | }) 81 | } 82 | 83 | func (p *Panel) App() *App { 84 | return p.app 85 | } 86 | 87 | func NewPanel(app *App) *Panel { 88 | p := &Panel{} 89 | p.Init(app) 90 | return p 91 | } 92 | -------------------------------------------------------------------------------- /govisor/ui/statusbar.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package ui 16 | 17 | import ( 18 | "sync" 19 | 20 | "github.com/gdamore/tcell/v2" 21 | "github.com/gdamore/tcell/v2/views" 22 | ) 23 | 24 | // StatusBar is like a titlebar, but it changes color based on the 25 | // status of a screen -- e.g. red background to indicate a fault condition. 26 | type StatusBar struct { 27 | once sync.Once 28 | status string 29 | views.SimpleStyledTextBar 30 | } 31 | 32 | var ( 33 | StatusBarStyleNormal = tcell.StyleDefault. 34 | Foreground(tcell.ColorBlack). 35 | Background(tcell.ColorSilver) 36 | StatusBarStyleGood = tcell.StyleDefault. 37 | Foreground(tcell.ColorWhite). 38 | Background(tcell.ColorGreen). 39 | Bold(true) 40 | StatusBarStyleWarn = tcell.StyleDefault. 41 | Foreground(tcell.ColorBlack). 42 | Background(tcell.ColorYellow) 43 | StatusBarStyleError = tcell.StyleDefault. 44 | Foreground(tcell.ColorWhite). 45 | Background(tcell.ColorMaroon). 46 | Bold(true) 47 | ) 48 | 49 | func (sb *StatusBar) Init() { 50 | sb.once.Do(func() { 51 | sb.SimpleStyledTextBar.Init() 52 | sb.SetNormal() 53 | }) 54 | } 55 | 56 | func (sb *StatusBar) SetStyle(style tcell.Style) { 57 | sb.SimpleStyledTextBar.SetStyle(style) 58 | sb.SimpleStyledTextBar.RegisterLeftStyle('N', style) 59 | sb.SimpleStyledTextBar.SetLeft(sb.status) 60 | } 61 | 62 | func (sb *StatusBar) SetGood() { 63 | sb.SetStyle(StatusBarStyleGood) 64 | } 65 | 66 | func (sb *StatusBar) SetNormal() { 67 | sb.SetStyle(StatusBarStyleNormal) 68 | } 69 | 70 | func (sb *StatusBar) SetWarn() { 71 | sb.SetStyle(StatusBarStyleWarn) 72 | } 73 | 74 | func (sb *StatusBar) SetError() { 75 | sb.SetStyle(StatusBarStyleError) 76 | } 77 | 78 | func (sb *StatusBar) SetText(status string) { 79 | sb.status = status 80 | sb.SetLeft(status) 81 | } 82 | 83 | func NewStatusBar() *StatusBar { 84 | sb := &StatusBar{} 85 | sb.Init() 86 | return sb 87 | } 88 | -------------------------------------------------------------------------------- /govisor/ui/titlebar.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package ui 16 | 17 | import ( 18 | "sync" 19 | 20 | "github.com/gdamore/tcell/v2" 21 | "github.com/gdamore/tcell/v2/views" 22 | ) 23 | 24 | type TitleBar struct { 25 | once sync.Once 26 | views.SimpleStyledTextBar 27 | } 28 | 29 | func (tb *TitleBar) Init() { 30 | tb.once.Do(func() { 31 | normal := tcell.StyleDefault. 32 | Foreground(tcell.ColorBlack). 33 | Background(tcell.ColorSilver) 34 | alternate := tcell.StyleDefault. 35 | Foreground(tcell.ColorBlue). 36 | Background(tcell.ColorSilver) 37 | 38 | tb.SimpleStyledTextBar.Init() 39 | tb.SimpleStyledTextBar.SetStyle(normal) 40 | tb.RegisterLeftStyle('N', normal) 41 | tb.RegisterLeftStyle('A', alternate) 42 | tb.RegisterCenterStyle('N', normal) 43 | tb.RegisterCenterStyle('A', alternate) 44 | tb.RegisterRightStyle('N', normal) 45 | tb.RegisterRightStyle('A', alternate) 46 | }) 47 | } 48 | 49 | func NewTitleBar() *TitleBar { 50 | tb := &TitleBar{} 51 | tb.Init() 52 | return tb 53 | } 54 | -------------------------------------------------------------------------------- /govisor/util/util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package util is used for internal implementation bits in the CLI/UI. 16 | package util 17 | 18 | import ( 19 | "fmt" 20 | "sort" 21 | "time" 22 | 23 | "github.com/gdamore/govisor/rest" 24 | ) 25 | 26 | func Status(s *rest.ServiceInfo) string { 27 | if !s.Enabled { 28 | return "disabled" 29 | } 30 | if s.Failed { 31 | return "failed" 32 | } 33 | if s.Running { 34 | return "running" 35 | } 36 | return "standby" 37 | } 38 | 39 | func FormatDuration(d time.Duration) string { 40 | 41 | sec := int((d % time.Minute) / time.Second) 42 | min := int((d % time.Hour) / time.Minute) 43 | hour := int(d / time.Hour) 44 | 45 | return fmt.Sprintf("%d:%02d:%02d", hour, min, sec) 46 | } 47 | 48 | type sorted []*rest.ServiceInfo 49 | 50 | func (s sorted) Swap(i, j int) { 51 | s[i], s[j] = s[j], s[i] 52 | } 53 | 54 | func (s sorted) Len() int { 55 | return len(s) 56 | } 57 | 58 | func (s sorted) Less(i, j int) bool { 59 | a := s[i] 60 | b := s[j] 61 | 62 | if a.Failed != b.Failed { 63 | // put failed items at front 64 | return a.Failed 65 | } 66 | if a.Enabled != b.Enabled { 67 | // enabled in front of non-enabled items 68 | return a.Enabled 69 | } 70 | // We don't worry about suspended items vs. running -- no clear order 71 | // there. We just sort based on name 72 | return a.Name < b.Name 73 | } 74 | 75 | func SortServices(items []*rest.ServiceInfo) { 76 | sort.Sort(sorted(items)) 77 | } 78 | -------------------------------------------------------------------------------- /govisor_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package govisor 16 | 17 | import ( 18 | "errors" 19 | "log" 20 | "strings" 21 | "sync" 22 | "testing" 23 | "time" 24 | 25 | . "github.com/smartystreets/goconvey/convey" 26 | ) 27 | 28 | type testLog struct { 29 | t *testing.T 30 | } 31 | 32 | func (tl *testLog) Write(p []byte) (n int, err error) { 33 | s := string(p) 34 | s = strings.Trim(s, "\n") 35 | tl.t.Log(s) 36 | return len(p), nil 37 | } 38 | 39 | type testS struct { 40 | name string 41 | failed bool 42 | started bool 43 | provides []string 44 | depends []string 45 | conflicts []string 46 | logger *log.Logger 47 | notify func() 48 | sync.Mutex 49 | } 50 | 51 | func (s *testS) Name() string { 52 | return s.name 53 | } 54 | 55 | func (s *testS) Description() string { 56 | return "Test Service" 57 | } 58 | 59 | func (s *testS) Start() error { 60 | s.Lock() 61 | defer s.Unlock() 62 | if s.failed { 63 | return errors.New("Injected failure") 64 | } 65 | s.started = true 66 | return nil 67 | } 68 | 69 | func (s *testS) Stop() { 70 | s.Lock() 71 | s.started = false 72 | s.Unlock() 73 | } 74 | 75 | func (s *testS) Check() error { 76 | s.Lock() 77 | defer s.Unlock() 78 | if s.failed { 79 | return errors.New("Test service failure") 80 | } 81 | return nil 82 | } 83 | 84 | func (s *testS) Provides() []string { 85 | return s.provides 86 | } 87 | 88 | func (s *testS) Depends() []string { 89 | return s.depends 90 | } 91 | 92 | func (s *testS) Conflicts() []string { 93 | return s.conflicts 94 | } 95 | 96 | func (s *testS) SetProperty(n PropertyName, v interface{}) error { 97 | switch n { 98 | case PropLogger: 99 | if v, ok := v.(*log.Logger); ok { 100 | s.logger = v 101 | return nil 102 | } 103 | return ErrBadPropType 104 | case PropNotify: 105 | if v, ok := v.(func()); ok { 106 | s.notify = v 107 | return nil 108 | } 109 | return ErrBadPropType 110 | default: 111 | return ErrBadPropName 112 | } 113 | } 114 | 115 | func (s *testS) Property(n PropertyName) (interface{}, error) { 116 | switch n { 117 | case PropLogger: 118 | return s.logger, nil 119 | default: 120 | return nil, ErrBadPropName 121 | } 122 | } 123 | 124 | func (s *testS) inject() { 125 | s.Lock() 126 | s.logger.Printf("Injecting failure on %s", s.name) 127 | s.failed = true 128 | if s.notify != nil { 129 | s.logger.Printf("Sending fail notify") 130 | s.notify() 131 | } 132 | s.Unlock() 133 | } 134 | 135 | func (s *testS) clear() { 136 | s.Lock() 137 | s.logger.Printf("Clearing failure on %s", s.name) 138 | s.failed = false 139 | if s.notify != nil { 140 | s.logger.Printf("Sending clear notify") 141 | s.notify() 142 | } 143 | s.Unlock() 144 | } 145 | 146 | var testS1 = &testS{ 147 | name: "test:S1", 148 | provides: []string{"alias:S1", "dep:S2"}, 149 | conflicts: []string{"conflict:S1"}, 150 | depends: []string{}, 151 | } 152 | 153 | var testS2 = &testS{ 154 | name: "test:S2", 155 | provides: []string{}, 156 | conflicts: []string{}, 157 | depends: []string{"dep:S2"}, 158 | } 159 | 160 | func SetTestLogger(t *testing.T, m *Manager) { 161 | m.SetLogger(log.New(&testLog{t: t}, "", log.LstdFlags)) 162 | } 163 | 164 | func WithManager(t *testing.T, name string, fn func(m *Manager)) func() { 165 | return func() { 166 | m := NewManager(name) 167 | So(m, ShouldNotBeNil) 168 | SetTestLogger(t, m) 169 | Reset(func() { 170 | m.Shutdown() 171 | }) 172 | fn(m) 173 | } 174 | } 175 | 176 | func TestBadPropertyName(t *testing.T) { 177 | Convey("Bogus property name", t, 178 | WithManager(t, "BadPropName", func(m *Manager) { 179 | s1 := NewService(&testS{name: "test:BadName"}) 180 | So(s1, ShouldNotBeNil) 181 | m.AddService(s1) 182 | e := s1.SetProperty(PropertyName("Nosuch"), true) 183 | So(e, ShouldNotBeNil) 184 | })) 185 | } 186 | 187 | func TestBadPropertyType(t *testing.T) { 188 | Convey("Bad property type", t, 189 | WithManager(t, "BadPropType", func(m *Manager) { 190 | s1 := NewService(&testS{name: "test:BadType"}) 191 | So(s1, ShouldNotBeNil) 192 | m.AddService(s1) 193 | e := s1.SetProperty(PropName, 42) 194 | So(e, ShouldNotBeNil) 195 | })) 196 | } 197 | 198 | func TestSetPropOK(t *testing.T) { 199 | Convey("Set Properties", t, 200 | WithManager(t, "SetProp", func(m *Manager) { 201 | s1 := NewService(&testS{name: "test:Name"}) 202 | So(s1, ShouldNotBeNil) 203 | e := s1.SetProperty(PropName, "test:NewName") 204 | So(e, ShouldBeNil) 205 | n, e := s1.GetProperty(PropName) 206 | So(e, ShouldBeNil) 207 | ns, ok := n.(string) 208 | So(ok, ShouldBeTrue) 209 | So(ns, ShouldEqual, "test:NewName") 210 | 211 | e = s1.SetProperty(PropDepends, []string{"s1:dep"}) 212 | So(e, ShouldBeNil) 213 | 214 | e = s1.SetProperty(PropConflicts, []string{"conf"}) 215 | So(e, ShouldBeNil) 216 | 217 | e = s1.SetProperty(PropProvides, []string{"abc:123"}) 218 | So(e, ShouldBeNil) 219 | })) 220 | } 221 | 222 | func TestReadOnlyProps(t *testing.T) { 223 | Convey("Read only properties", t, 224 | WithManager(t, "ReadOnly", func(m *Manager) { 225 | s1 := NewService(&testS{name: "test:ro"}) 226 | m.AddService(s1) 227 | e := s1.SetProperty(PropName, "test:shouldfail") 228 | So(e, ShouldNotBeNil) 229 | })) 230 | } 231 | 232 | func TestDependencies(t *testing.T) { 233 | Convey("Dependencies", t, 234 | WithManager(t, "Deps", func(m *Manager) { 235 | s1 := NewService(&testS{name: "test:s1"}) 236 | So(s1, ShouldNotBeNil) 237 | s2 := NewService(&testS{name: "test:s2"}) 238 | So(s2, ShouldNotBeNil) 239 | e := s2.SetProperty(PropDepends, []string{"test:s1"}) 240 | So(e, ShouldBeNil) 241 | 242 | Convey("Both start disabled", func() { 243 | So(s1.Enabled(), ShouldBeFalse) 244 | So(s2.Enabled(), ShouldBeFalse) 245 | }) 246 | 247 | Convey("Enabling S2 works", func() { 248 | m.AddService(s1) 249 | m.AddService(s2) 250 | So(s1.Enabled(), ShouldBeFalse) 251 | So(s2.Enabled(), ShouldBeFalse) 252 | e = s2.Enable() 253 | So(e, ShouldBeNil) 254 | 255 | Convey("But S2 isn't running yet", func() { 256 | So(s2.Running(), ShouldBeFalse) 257 | }) 258 | 259 | Convey("Enabling S1 starts S2", func() { 260 | e = s1.Enable() 261 | So(e, ShouldBeNil) 262 | So(s1.Enabled(), ShouldBeTrue) 263 | So(s2.Enabled(), ShouldBeTrue) 264 | So(s1.Running(), ShouldBeTrue) 265 | So(s2.Running(), ShouldBeTrue) 266 | 267 | Convey("Disabling S1 stops both", func() { 268 | e = s1.Disable() 269 | So(e, ShouldBeNil) 270 | So(s1.Enabled(), ShouldBeFalse) 271 | So(s2.Enabled(), ShouldBeTrue) 272 | So(s1.Running(), ShouldBeFalse) 273 | So(s2.Running(), ShouldBeFalse) 274 | }) 275 | }) 276 | }) 277 | })) 278 | } 279 | 280 | func checkSerial(m *Manager, sn int64) int64 { 281 | newsn := m.Serial() 282 | So(newsn, ShouldBeGreaterThan, sn) 283 | return newsn 284 | } 285 | 286 | func TestSerial(t *testing.T) { 287 | Convey("Serial numbers", t, WithManager(t, "Serial", func(m *Manager) { 288 | sn := m.Serial() 289 | So(sn, ShouldNotEqual, 0) 290 | s1 := NewService(&testS{name: "test:s1"}) 291 | So(s1, ShouldNotBeNil) 292 | m.AddService(s1) 293 | sn = checkSerial(m, sn) 294 | s2 := NewService(&testS{name: "test:s2"}) 295 | So(s2, ShouldNotBeNil) 296 | e := s2.SetProperty(PropDepends, []string{"test:s1"}) 297 | m.AddService(s2) 298 | So(e, ShouldBeNil) 299 | sn = checkSerial(m, sn) 300 | 301 | start := time.Now() 302 | end := start 303 | endsn := sn 304 | go func() { 305 | endsn = m.WatchSerial(sn, time.Second*5) 306 | end = time.Now() 307 | }() 308 | time.Sleep(time.Millisecond * 20) 309 | e = s1.Enable() 310 | So(e, ShouldBeNil) 311 | So(m.Serial(), ShouldBeGreaterThan, sn) 312 | time.Sleep(time.Millisecond * 10) 313 | So(endsn, ShouldBeGreaterThan, sn) 314 | So(end.Sub(start), ShouldBeLessThan, time.Second) 315 | So(end.Sub(start), ShouldBeGreaterThan, time.Millisecond*10) 316 | t.Logf("took %v", end.Sub(start)) 317 | So(endsn, ShouldEqual, m.Serial()) 318 | So(s2.Serial(), ShouldEqual, sn) 319 | So(s1.Serial(), ShouldEqual, endsn) 320 | })) 321 | } 322 | 323 | // XXX: This test function needs to be refactored 324 | func TestGovisor(t *testing.T) { 325 | t1 := *testS1 326 | t2 := *testS2 327 | 328 | Convey("Given a new govisor", t, func() { 329 | m := NewManager("TestGoVisor") 330 | So(m, ShouldNotBeNil) 331 | SetTestLogger(t, m) 332 | Convey("And new services S1 and S2", func() { 333 | s1 := NewService(&t1) 334 | So(s1, ShouldNotBeNil) 335 | m.AddService(s1) 336 | So(s1.Enabled(), ShouldBeFalse) 337 | So(s1.Running(), ShouldBeFalse) 338 | So(s1.Failed(), ShouldBeFalse) 339 | 340 | s2 := NewService(&t2) 341 | So(s2, ShouldNotBeNil) 342 | m.AddService(s2) 343 | So(s2.Enabled(), ShouldBeFalse) 344 | So(s2.Running(), ShouldBeFalse) 345 | So(s2.Failed(), ShouldBeFalse) 346 | 347 | Convey("We can enable S2 (depends on S1)", func() { 348 | e := s2.Enable() 349 | So(e, ShouldBeNil) 350 | So(s2.Enabled(), ShouldBeTrue) 351 | Convey("But it isn't running yet", func() { 352 | So(s2.Running(), ShouldBeFalse) 353 | }) 354 | Convey("We can enable S1", func() { 355 | e = s1.Enable() 356 | So(e, ShouldBeNil) 357 | So(s1.Enabled(), ShouldBeTrue) 358 | So(s1.Running(), ShouldBeTrue) 359 | So(s2.Running(), ShouldBeTrue) 360 | Convey("We can restart all of them", func() { 361 | e := s2.Restart() 362 | So(e, ShouldBeNil) 363 | So(s1.Failed(), ShouldBeFalse) 364 | So(s2.Failed(), ShouldBeFalse) 365 | So(s1.Running(), ShouldBeTrue) 366 | So(s2.Running(), ShouldBeTrue) 367 | }) 368 | Convey("Failure injection", func() { 369 | m.StopMonitoring() 370 | t1.inject() 371 | e := s1.Check() 372 | So(e, ShouldNotBeNil) 373 | So(s1.Failed(), ShouldBeTrue) 374 | So(s1.Running(), ShouldBeFalse) 375 | So(s2.Running(), ShouldBeFalse) 376 | t1.clear() 377 | s1.Clear() 378 | m.StartMonitoring() 379 | 380 | t1.inject() 381 | // wait for callbacks 382 | time.Sleep(time.Millisecond) 383 | So(s1.Failed(), ShouldBeTrue) 384 | So(s1.Running(), ShouldBeFalse) 385 | So(s2.Running(), ShouldBeFalse) 386 | t1.clear() 387 | s1.Clear() 388 | 389 | t.Logf("Test without healing") 390 | t1.inject() 391 | time.Sleep(time.Millisecond) 392 | So(s1.Failed(), ShouldBeTrue) 393 | t1.clear() 394 | time.Sleep(time.Millisecond) 395 | So(s1.Failed(), ShouldBeTrue) 396 | So(s1.Running(), ShouldBeFalse) 397 | s1.Clear() 398 | So(s1.Failed(), ShouldBeFalse) 399 | So(s1.Running(), ShouldBeTrue) 400 | 401 | t.Logf("Check self healing") 402 | e = s1.SetProperty(PropRestart, true) 403 | So(e, ShouldBeNil) 404 | t1.inject() 405 | time.Sleep(time.Millisecond) 406 | So(s1.Failed(), ShouldBeTrue) 407 | So(s1.Running(), ShouldBeFalse) 408 | t1.clear() 409 | time.Sleep(time.Millisecond) 410 | So(s1.Failed(), ShouldBeFalse) 411 | So(s1.Running(), ShouldBeTrue) 412 | }) 413 | }) 414 | }) 415 | }) 416 | }) 417 | } 418 | -------------------------------------------------------------------------------- /govisord/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Command govisord implements a daemon that can manage proceses from 16 | // manifest files using Govisor. 17 | // 18 | // The flags are 19 | // 20 | // -a
- select the listen address, default is 21 | // http://localhost:8321 22 | // -d - select the directory. manifests live in 23 | // the directory "services" underneath this 24 | // -p - use Basic Auth with a password of user:bcrypt 25 | // pairs. Bcrypt is an encrypted password. 26 | // -g - generate & use encrypted password & user 27 | // -e - enable/disable (true/false) all services (true) 28 | // -n - name this instance, e.g. for Realm, etc. 29 | // 30 | package main 31 | 32 | import ( 33 | "encoding/csv" 34 | "flag" 35 | "fmt" 36 | "io" 37 | "log" 38 | "net/http" 39 | "os" 40 | "os/signal" 41 | "path" 42 | "strings" 43 | "syscall" 44 | "time" 45 | 46 | "golang.org/x/crypto/bcrypt" 47 | 48 | "github.com/gdamore/govisor" 49 | "github.com/gdamore/govisor/server" 50 | ) 51 | 52 | var addr string = "http://127.0.0.1:8321" 53 | 54 | type MyHandler struct { 55 | h *server.Handler 56 | auth bool 57 | passwd map[string]string 58 | name string 59 | } 60 | 61 | func (h *MyHandler) needAuth(w http.ResponseWriter, r *http.Request) { 62 | w.Header().Set("WWW-Authenticate", 63 | fmt.Sprintf("Basic realm=%q", h.name)) 64 | http.Error(w, http.StatusText(http.StatusUnauthorized), 65 | http.StatusUnauthorized) 66 | } 67 | 68 | func (h *MyHandler) loadPasswdFile(name string) error { 69 | file, e := os.Open(name) 70 | if e != nil { 71 | return e 72 | } 73 | rd := csv.NewReader(file) 74 | rd.Comment = '#' 75 | rd.Comma = ':' 76 | rd.FieldsPerRecord = 2 77 | for { 78 | rec, e := rd.Read() 79 | if e == io.EOF { 80 | break 81 | } else if e != nil { 82 | return e 83 | } 84 | h.passwd[rec[0]] = rec[1] 85 | } 86 | h.auth = true 87 | file.Close() 88 | return nil 89 | } 90 | 91 | func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 92 | // Consider adding logging, and timeouts, to mitigate 93 | if h.auth { 94 | user, pass, ok := r.BasicAuth() 95 | if !ok { 96 | h.needAuth(w, r) 97 | return 98 | } 99 | enc, ok := h.passwd[user] 100 | if !ok { 101 | h.needAuth(w, r) 102 | return 103 | } 104 | if e := bcrypt.CompareHashAndPassword([]byte(enc), []byte(pass)); e != nil { 105 | h.needAuth(w, r) 106 | return 107 | } 108 | } 109 | h.h.ServeHTTP(w, r) 110 | } 111 | 112 | var logFile = "" 113 | 114 | func die(format string, v ...interface{}) { 115 | if logFile != "" { 116 | log.Printf(format, v...) 117 | } 118 | fmt.Printf(format+"\n", v...) 119 | os.Exit(1) 120 | } 121 | 122 | func main() { 123 | dir := "." 124 | name := "govisord" 125 | enable := true 126 | passFile := "" 127 | genpass := "" 128 | certFile := "" 129 | keyFile := "" 130 | m := govisor.NewManager(name) 131 | 132 | flag.StringVar(&certFile, "certfile", certFile, "certificate file (for TLS)") 133 | flag.StringVar(&keyFile, "keyfile", keyFile, "key file (for TLS)") 134 | flag.StringVar(&addr, "addr", addr, "listen address") 135 | flag.StringVar(&dir, "dir", dir, "configuration directory") 136 | flag.StringVar(&name, "name", name, "govisor name") 137 | flag.BoolVar(&enable, "enable", enable, "enable all services") 138 | flag.StringVar(&passFile, "passfile", passFile, "password file") 139 | flag.StringVar(&genpass, "passwd", genpass, "generate password") 140 | flag.StringVar(&logFile, "logfile", logFile, "log file") 141 | flag.Parse() 142 | 143 | var lf *os.File 144 | var e error 145 | if logFile != "" { 146 | lf, e = os.OpenFile(logFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) 147 | if e != nil { 148 | die("Failed to open log file: %v", e) 149 | } 150 | log.SetOutput(lf) 151 | m.SetLogger(log.New(lf, "", log.LstdFlags)) 152 | } 153 | 154 | sigs := make(chan os.Signal, 1) 155 | done := make(chan bool, 1) 156 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) 157 | 158 | h := &MyHandler{ 159 | h: server.NewHandler(m), 160 | name: name, 161 | auth: false, 162 | passwd: make(map[string]string), 163 | } 164 | if genpass != "" { 165 | h.auth = true 166 | rec := strings.SplitN(genpass, ":", 2) 167 | if len(rec) != 2 { 168 | die("Missing user:password") 169 | } 170 | enc, e := bcrypt.GenerateFromPassword([]byte(rec[1]), 0) 171 | if e != nil { 172 | die("bcrypt: %v", e) 173 | } 174 | h.passwd[rec[0]] = string(enc) 175 | log.Printf("Encrypted password is '%s'", string(enc)) 176 | } 177 | if passFile != "" { 178 | if e := h.loadPasswdFile(passFile); e != nil { 179 | die("Unable to load passwd file: %v", e) 180 | } 181 | } else if _, err := os.Stat(path.Join(dir, "passwd")); err == nil { 182 | if e := h.loadPasswdFile(path.Join(dir, "passwd")); e != nil { 183 | die("Unable to load passwd file: %v", e) 184 | } 185 | } 186 | 187 | if certFile == "" { 188 | certFile = path.Join(dir, "cert.pem") 189 | } 190 | if keyFile == "" { 191 | keyFile = path.Join(dir, "key.pem") 192 | } 193 | 194 | go func() { 195 | var e error 196 | if strings.HasPrefix(addr, "https://") { 197 | e = http.ListenAndServeTLS(addr[len("https://"):], 198 | certFile, keyFile, h) 199 | } else if strings.HasPrefix(addr, "http://") { 200 | e = http.ListenAndServe(addr[len("http://"):], h) 201 | } else { 202 | e = http.ListenAndServe(addr, h) 203 | } 204 | if e != nil { 205 | die("HTTP/HTTPS failed: %v", e) 206 | } 207 | }() 208 | 209 | /* This sleep is long enough to verify that our HTTP service started */ 210 | time.Sleep(time.Millisecond * 100) 211 | 212 | svcDir := path.Join(dir, "services") 213 | if d, e := os.Open(svcDir); e != nil { 214 | die("Failed to open services directory %s: %v", svcDir, e) 215 | } else if files, e := d.Readdirnames(-1); e != nil { 216 | die("Failed to scan scan services: %v", e) 217 | } else { 218 | for _, f := range files { 219 | fname := path.Join(svcDir, f) 220 | if mf, e := os.Open(fname); e != nil { 221 | log.Printf("Failed to open manifest %s: %v", 222 | fname, e) 223 | } else if p, e := govisor.NewProcessFromJson(mf); e != nil { 224 | log.Printf("Failed to load manifest %s: %v", 225 | fname, e) 226 | mf.Close() 227 | } else if e := m.AddService(p); e != nil { 228 | /* Failure logged by m already */ 229 | mf.Close() 230 | } 231 | } 232 | } 233 | 234 | m.StartMonitoring() 235 | if enable { 236 | svcs, _, _ := m.Services() 237 | for _, s := range svcs { 238 | s.Enable() 239 | } 240 | } 241 | 242 | // Set up a handler, so that we shutdown cleanly if possible. 243 | go func() { 244 | <-sigs 245 | done <- true 246 | }() 247 | 248 | // Wait for a termination signal, and shutdown cleanly if we get it. 249 | <-done 250 | m.Shutdown() 251 | os.Exit(1) 252 | } 253 | -------------------------------------------------------------------------------- /govisord/samples/README.md: -------------------------------------------------------------------------------- 1 | # govisord samples 2 | 3 | This directory is a sample configuration tree for govisord. It only really 4 | works on POSIX systems, as the manifests herein depend on programs like 5 | echo and true that may not exist on Windows or other systems. 6 | 7 | The individual manifests for each service are located in the "services" 8 | subdirectory. File names are not important, but each service must have a 9 | unique name. govisord will assume every service with a manifest in the 10 | directory should be added. By default it will also attempt to enable them. 11 | 12 | To try it out, point govisor at this directory using the -dir command line 13 | switch. For example: 14 | 15 | % ../govisord -dir . 16 | 17 | The passwd file here provides a username of "demo" with password "demo". 18 | 19 | Note that a cert.pem and key.pem file are provided for use with TLS (mostly 20 | for testing); the certificate is self-signed and bound to localhost, 21 | and expires sometime in September 2025. You should *NOT* use these files 22 | in production but generate your own instead. 23 | 24 | You can then use the govisor command in the ../../govisor directory to 25 | try it out. 26 | 27 | Enjoy. 28 | -------------------------------------------------------------------------------- /govisord/samples/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC+DCCAeKgAwIBAgIQFEZzCQzp3iXPKFHC2DfZtTALBgkqhkiG9w0BAQswEjEQ 3 | MA4GA1UEChMHQWNtZSBDbzAeFw0xNTA5MTkxNTMwMTdaFw0yNTA5MTYxNTMwMTda 4 | MBIxEDAOBgNVBAoTB0FjbWUgQ28wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK 5 | AoIBAQCpQotfN0DSvTe4DeHsu00S+p3t+13m/MgXdEkj8TVNTNnHQOprgfD+CK/Q 6 | pnED6PD6xVr3DKy23I65P4UH0V32Fw8iKNTMqc5IeWU5NEsJZ1h5y5VN7Q/aQoV8 7 | hWTCS4mL5FW/55XQt4YyIUmWHaXEGTqq5dTeGY7o1w15/goafNpyUjbabCmhOvyD 8 | Axxv1O/6ZhbcmJKpjGZOEAY0CbYun0GOb3T6GEBzer9WmrhxnFMeNsiOQU0tLtEY 9 | 4fJwdl4a+LdVD7WC49+uQ4iqGqTuTc5m90cFu3L7ulOiepUUBXeuZlwrW8CiEwth 10 | kr+81WpM4iWSsVcnWbOgIPYkcFKRAgMBAAGjTjBMMA4GA1UdDwEB/wQEAwIApDAT 11 | BgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MBQGA1UdEQQNMAuC 12 | CWxvY2FsaG9zdDALBgkqhkiG9w0BAQsDggEBABPMYitMIFUpULGC6NTnUb6Q4eGK 13 | 7g1hgmh6rsZMqP6Zk/cd7uj2gvU4VaiYBXxF+VaYDzgoyz/FHe95pdm6wrrNq3Pk 14 | xwh85WzL4U8aG4ffHAT/XQ3fMkOKKHbQV8bWDFWitCgYJ4p8NxUULJTNQv9wVPbU 15 | cBM4BLDHsajEzB4mHJw9fLMJszF8yJ3pYgcuJrcjoOrsu+qejDEKrxZTikEjhHuu 16 | w49y2aAdYskqN3UVDqtIGdbA6VM0q4EiTnoEewkTpmx3SeLBJobeY0a0TE51knKb 17 | dAXSksbzsFgkAkHtdb1xrkru7A31zPv6+JHyHhnZs+2n2dQC491YT4j0RSY= 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /govisord/samples/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAqUKLXzdA0r03uA3h7LtNEvqd7ftd5vzIF3RJI/E1TUzZx0Dq 3 | a4Hw/giv0KZxA+jw+sVa9wysttyOuT+FB9Fd9hcPIijUzKnOSHllOTRLCWdYecuV 4 | Te0P2kKFfIVkwkuJi+RVv+eV0LeGMiFJlh2lxBk6quXU3hmO6NcNef4KGnzaclI2 5 | 2mwpoTr8gwMcb9Tv+mYW3JiSqYxmThAGNAm2Lp9Bjm90+hhAc3q/Vpq4cZxTHjbI 6 | jkFNLS7RGOHycHZeGvi3VQ+1guPfrkOIqhqk7k3OZvdHBbty+7pTonqVFAV3rmZc 7 | K1vAohMLYZK/vNVqTOIlkrFXJ1mzoCD2JHBSkQIDAQABAoIBAQCdC0ATZUnA/o1f 8 | OYpAwvxdOqkj8RGMS0g/8sYWEix5f2+k34dhnpwCp/6w4m2DyjMlCc4/4MOzmurV 9 | 2KjOuySqO3TwJeBj0L20IuaXb/ybSgceYpUyS11lDkmaYo49dCa35HDncLFhiZZ6 10 | lsYXAYUXcK0tebfLJv2g7j/zy5CGR4Sb23fc9Frf0HGNZHs6aRTumC77bbRkSVRQ 11 | X63El9qpbOvm5Qtdjr38mrIMmP5M1lm80UtK4NUBxPimHNGoDqTgL/BNWV3sKdi2 12 | ZpUzC+W/+3ahrGImMO/ABIwVsR/izXfi/+2IWTc+R6hrQJHk6/b8LD+RXQRqL320 13 | spi72P/tAoGBAMJfL5JRtWedkvqu3M9Y09myzb4iDQ738uGlfYjIs7Xwzcn2V3bs 14 | hgF0tkBETj7WHq0Gyn1wNhdTXswLySm4D31kiBFPafTPAxwaPkzqXo3dqSh+nUoz 15 | 5lk/Yi8Gh0UZqA5YiuZboLyUCXZvcRRk3cnE/CguDeqzYIVLmN5gW6ubAoGBAN7t 16 | Em9rGo/Ucp9NQn5AP6wMjqTsdNK0IY8Xm57zfG7vVzGMV/4KxFW5RygOnnUlPIaD 17 | 9kOy032uFlE29I2szUu2Kbc28+Gte+EYAQZox7XucsPRw7tgWJ9arbL0MjzLCuM1 18 | vEUc3AubBhJo3x/xX5YFom/DxkEWt5Wz6Kk0REtDAoGAM6NcHRBOFFf9i1HXYdKk 19 | faqOFFwxge+HfEEeB6/iZoyx71zdwb92nn5mSM6cxv7VqXwrYYXlKO9COQ1MhSPc 20 | IaEKsSaa7KnulpG2SXLeaewgm5x/pw4AvWcka3/dghMd3anSRP7ExjTCCs/yh8uA 21 | TLFjmD0b/2VmWCNMWo/Y6s8CgYEAmGDachh2zou8UdGguuW14CexNWB/tir0qXfM 22 | owjyLG4jXrX8y6SWcbY0wlfDznVGevgm1D2fPNBLZvY2kcTli3QX7al9aOyJueIL 23 | iVMD+ALmpZ1zulfwG/UCuEkdMdrkNUzcxS0l6DTuIJVYtt/po4V0dAsRMBqUIJsa 24 | VNZqPDsCgYAbKtaOTPzzmD4hbGBbxQTEp14fYX2hptkWI37Azu5YmTBER8iBXhe1 25 | xeAKPvwQ3P6BNbVZlDBjoQPy+JqT87ooJRZMZgRq9VnoL8SY2XGJdoUDVXE/hjom 26 | gdAOOORc08fbNw1g4MeiOoexb+njwZpM62tkPx/E1uWUW3JCfjDb5Q== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /govisord/samples/passwd: -------------------------------------------------------------------------------- 1 | # 2 | # The "demo" account below has a password of "demo" 3 | # 4 | demo:$2a$10$7t362pwOtgVoXUrpKM2HwuF1TM0MmusiM/oVDx9HvTvzDDBLcap2m 5 | -------------------------------------------------------------------------------- /govisord/samples/services/d1.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d1:ok", 3 | "description": "d1 depends on s1", 4 | "command": [ "echo", "hello, world" ], 5 | "depends": [ "s1" ] 6 | } 7 | 8 | -------------------------------------------------------------------------------- /govisord/samples/services/d2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d2:ok", 3 | "description": "d2 depends on s2", 4 | "depends": [ "s2:ok" ], 5 | "command": [ "sleep", "3600" ] 6 | } 7 | 8 | -------------------------------------------------------------------------------- /govisord/samples/services/d3.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d3:s3", 3 | "description": "d3 depends on s3", 4 | "depends": [ "s3" ], 5 | "command": [ "echo", "this shouldn't run" ] 6 | } 7 | 8 | -------------------------------------------------------------------------------- /govisord/samples/services/s1.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "s1:ok", 3 | "description": "first s1", 4 | "command": [ "echo", "hello, world" ] 5 | } 6 | 7 | -------------------------------------------------------------------------------- /govisord/samples/services/s2.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "s2:ok", 3 | "description": "first s2", 4 | "command": [ "sleep", "3600" ] 5 | } 6 | 7 | -------------------------------------------------------------------------------- /govisord/samples/services/s3.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "s3:fail", 3 | "description": "this one fails", 4 | "command": [ "false" ] 5 | } 6 | 7 | -------------------------------------------------------------------------------- /govisord/samples/services/s4.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "s4:noisy", 3 | "description": "this stamps at 1 second intervals", 4 | "command": [ "sh", "-c", "i=0; while true ; do date; sleep 1; done" ] 5 | } 6 | 7 | -------------------------------------------------------------------------------- /govisord/samples/services/s5.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "s5:incompat", 3 | "description": "s5 conflicts with s4", 4 | "conflicts": [ "s4" ], 5 | "command": [ "sleep", "3600" ] 6 | } 7 | 8 | -------------------------------------------------------------------------------- /govisord/samples/services/s6.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "s6:exit", 3 | "description": "exits, and is fail on exit", 4 | "failOnExit": true, 5 | "command": [ "echo", "bailing cleanly" ] 6 | } 7 | 8 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package govisor 16 | 17 | import ( 18 | "strings" 19 | "sync" 20 | "time" 21 | ) 22 | 23 | const ( 24 | MaxLogRecords = 1000 25 | ) 26 | 27 | type LogRecord struct { 28 | Id int64 `json:"id,string"` 29 | Time time.Time `json:"time"` 30 | Text string `json:"text"` 31 | } 32 | 33 | type Log struct { 34 | records []LogRecord 35 | numRecords int 36 | maxRecords int 37 | id int64 38 | cvs map[*sync.Cond]bool 39 | mx sync.Mutex 40 | } 41 | 42 | func (log *Log) lock() { 43 | log.mx.Lock() 44 | } 45 | 46 | func (log *Log) unlock() { 47 | log.mx.Unlock() 48 | } 49 | 50 | // Write implements the Writer interface consumed by Logger. 51 | func (log *Log) Write(b []byte) (int, error) { 52 | if log.maxRecords == 0 { 53 | log.maxRecords = MaxLogRecords 54 | } 55 | if log.records == nil { 56 | log.records = make([]LogRecord, log.maxRecords) 57 | log.numRecords = 0 58 | } 59 | str := strings.Trim(string(b), "\n") 60 | log.lock() 61 | for _, line := range strings.Split(str, "\n") { 62 | idx := log.numRecords % log.maxRecords 63 | log.id++ 64 | log.records[idx].Text = line 65 | log.records[idx].Id = log.id 66 | log.records[idx].Time = time.Now() 67 | // NB: numRecords may actually be more than maxRecords. 68 | // In that case, we've looped, but we use this really to 69 | // track the next index. 70 | log.numRecords++ 71 | } 72 | for cv := range log.cvs { 73 | cv.Broadcast() 74 | } 75 | log.unlock() 76 | return len(b), nil 77 | } 78 | 79 | func (log *Log) Clear() { 80 | log.lock() 81 | log.numRecords = 0 82 | // We presume that we cannot add new records more quickly than 83 | // once every nanosecond. 84 | log.id = time.Now().UnixNano() 85 | log.unlock() 86 | } 87 | 88 | // GetRecords returns the records that are stored, as well as an ID 89 | // suitable for use as an Etag. The last parameter can be the last ID 90 | // that was checked, in which case this function will return nil immediately 91 | // if the log has not changed since that ID was returned, without duplicating 92 | // any records. These IDs are suitable for use as an Etag in REST APIs. 93 | // Note that IDs are not unique across different Log instances. 94 | func (log *Log) GetRecords(last int64) ([]LogRecord, int64) { 95 | log.lock() 96 | if log.id == last { 97 | log.unlock() 98 | return nil, last 99 | } 100 | var recs []LogRecord 101 | cnt := log.numRecords 102 | cur := log.numRecords 103 | if log.numRecords > log.maxRecords { 104 | recs = make([]LogRecord, 0, log.maxRecords) 105 | cnt = log.maxRecords 106 | } else { 107 | recs = make([]LogRecord, 0, log.numRecords) 108 | } 109 | if cnt > cur { 110 | cnt = cur 111 | } 112 | index := cur - cnt 113 | for j := 0; j < cnt; j++ { 114 | recs = append(recs, log.records[index%log.maxRecords]) 115 | index++ 116 | } 117 | id := log.id 118 | log.unlock() 119 | return recs, id 120 | } 121 | 122 | func (log *Log) Watch(last int64, expire time.Duration) int64 { 123 | expired := false 124 | var timer *time.Timer 125 | cv := sync.NewCond(&log.mx) 126 | if expire > 0 { 127 | timer = time.AfterFunc(expire, func() { 128 | log.lock() 129 | expired = true 130 | cv.Broadcast() 131 | log.unlock() 132 | }) 133 | } else { 134 | expired = true 135 | } 136 | 137 | log.lock() 138 | log.cvs[cv] = true 139 | for { 140 | if log.id != last || expired { 141 | break 142 | } 143 | cv.Wait() 144 | } 145 | delete(log.cvs, cv) 146 | if log.id != last { 147 | last = log.id 148 | } 149 | log.unlock() 150 | if timer != nil { 151 | timer.Stop() 152 | } 153 | return last 154 | } 155 | 156 | // NewLog returns a Log instance. 157 | func NewLog() *Log { 158 | log := &Log{ 159 | maxRecords: MaxLogRecords, 160 | id: time.Now().UnixNano(), 161 | cvs: make(map[*sync.Cond]bool), 162 | } 163 | return log 164 | } 165 | -------------------------------------------------------------------------------- /manager.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package govisor 16 | 17 | import ( 18 | "io" 19 | "log" 20 | "os" 21 | "runtime" 22 | "sync" 23 | "time" 24 | ) 25 | 26 | type Manager struct { 27 | services map[*Service]bool 28 | name string 29 | baseDir string 30 | logger *log.Logger 31 | mylog *log.Logger 32 | log *Log 33 | mlog *MultiLogger 34 | writer io.Writer 35 | cleanup bool 36 | monitoring bool 37 | serial int64 38 | listSerial int64 39 | listStamp time.Time 40 | createTime time.Time 41 | updateTime time.Time 42 | mx sync.Mutex 43 | cvs map[*sync.Cond]bool 44 | } 45 | 46 | type ManagerInfo struct { 47 | Name string 48 | Serial int64 49 | UpdateTime time.Time 50 | CreateTime time.Time 51 | } 52 | 53 | func (m *Manager) lock() { 54 | m.mx.Lock() 55 | } 56 | 57 | func (m *Manager) unlock() { 58 | m.mx.Unlock() 59 | } 60 | 61 | func (m *Manager) wakeUp() { 62 | // NB: If the lock is not held here, then there is a risk 63 | // that the woken goroutines won't get see the updated 64 | // serial number!! 65 | for cv := range m.cvs { 66 | cv.Broadcast() 67 | } 68 | } 69 | 70 | // bumpSerial increments the serial and notifies watchers. It returns 71 | // the new serial number, so that it can be stored in services. 72 | // Call with lock held. 73 | func (m *Manager) bumpSerial() int64 { 74 | m.updateTime = time.Now() 75 | m.serial++ 76 | rv := m.serial 77 | m.wakeUp() 78 | return rv 79 | } 80 | 81 | // watchSerial monitors for a change in a specific serial number. It returns 82 | // the new serial number when it changes. If the serial number has not 83 | // changed in the given duration then the old value is returned. A poll 84 | // can be done by supplying 0 for the expiration. 85 | func (m *Manager) watchSerial(old int64, src *int64, expire time.Duration) int64 { 86 | expired := false 87 | cv := sync.NewCond(&m.mx) 88 | var timer *time.Timer 89 | var rv int64 90 | 91 | // Schedule timeout 92 | if expire > 0 { 93 | timer = time.AfterFunc(expire, func() { 94 | m.lock() 95 | expired = true 96 | cv.Broadcast() 97 | m.unlock() 98 | }) 99 | } else { 100 | expired = true 101 | } 102 | 103 | m.lock() 104 | m.cvs[cv] = true 105 | for { 106 | rv = *src 107 | if rv != old || expired { 108 | break 109 | } 110 | cv.Wait() 111 | } 112 | delete(m.cvs, cv) 113 | m.unlock() 114 | if timer != nil { 115 | timer.Stop() 116 | } 117 | return rv 118 | } 119 | 120 | // WatchSerial monitors for a change in the global serial number. 121 | func (m *Manager) WatchSerial(old int64, expire time.Duration) int64 { 122 | return m.watchSerial(old, &m.serial, expire) 123 | } 124 | 125 | // WatchServices monitors for a change in the list of services. 126 | func (m *Manager) WatchServices(old int64, expire time.Duration) int64 { 127 | return m.watchSerial(old, &m.listSerial, expire) 128 | } 129 | 130 | // Serial returns the global serial number. This is incremented 131 | // anytime a service has a state change. 132 | func (m *Manager) Serial() int64 { 133 | m.lock() 134 | rv := m.serial 135 | m.unlock() 136 | return rv 137 | } 138 | 139 | // Name returns the name the manager was allocated with. This makes it 140 | // possible to distinguish between separate manager instances. This name 141 | // can influence logged messages and file/directory names. 142 | func (m *Manager) Name() string { 143 | return m.name 144 | } 145 | 146 | // GetInfo returns top-level information about the Manager. This is done 147 | // in a manner that ensures that the info is consistent. 148 | func (m *Manager) GetInfo() *ManagerInfo { 149 | m.lock() 150 | i := &ManagerInfo{ 151 | Name: m.name, 152 | Serial: m.serial, 153 | CreateTime: m.createTime, 154 | UpdateTime: m.updateTime, 155 | } 156 | m.unlock() 157 | return i 158 | } 159 | 160 | // AddService adds a service, registering it, to the manager. 161 | func (m *Manager) AddService(s *Service) error { 162 | m.lock() 163 | for s2 := range m.services { 164 | if s.Name() == s2.Name() { 165 | m.unlock() 166 | m.logf("[%s] Failed to add service [%s]: %v", 167 | m.Name(), s.Name(), ErrNameExists) 168 | return ErrNameExists 169 | } 170 | } 171 | s.setManager(m) 172 | m.listSerial = m.bumpSerial() 173 | s.serial = m.bumpSerial() 174 | m.listStamp = time.Now() 175 | m.logf("[%s] Added service [%s]: %s", m.Name(), s.Name(), 176 | s.Description()) 177 | m.unlock() 178 | return nil 179 | } 180 | 181 | // DeleteService deletes a service from the manager. 182 | func (m *Manager) DeleteService(s *Service) error { 183 | m.lock() 184 | if s.enabled { 185 | m.unlock() 186 | return ErrIsEnabled 187 | } 188 | s.delManager() 189 | m.logf("[%s] Deleted service [%s]", m.Name(), s.Name()) 190 | m.listSerial = m.bumpSerial() 191 | s.serial = m.bumpSerial() 192 | m.listStamp = time.Now() 193 | m.unlock() 194 | return nil 195 | } 196 | 197 | // Services returns all of our services. Note that the order is 198 | // arbitrary. (At present it happens to be done based on order of 199 | // addition.) 200 | func (m *Manager) Services() ([]*Service, int64, time.Time) { 201 | m.lock() 202 | rv := make([]*Service, 0, len(m.services)) 203 | for s := range m.services { 204 | rv = append(rv, s) 205 | } 206 | ts := m.listStamp 207 | sn := m.listSerial 208 | m.unlock() 209 | return rv, sn, ts 210 | } 211 | 212 | // FindServices finds the list of services that have either a matching 213 | // Name, or Provides. That is, they find all of our services, where the 214 | // service.Match() would return true for the string match. 215 | func (m *Manager) FindServices(match string) []*Service { 216 | rv := []*Service{} 217 | m.lock() 218 | for s := range m.services { 219 | if s.Matches(match) { 220 | rv = append(rv, s) 221 | } 222 | } 223 | m.unlock() 224 | return rv 225 | } 226 | 227 | func (m *Manager) setBaseDir() { 228 | m.baseDir = os.Getenv("GOVISORDIR") 229 | switch runtime.GOOS { 230 | case "nacl", "plan9": 231 | m.baseDir = "" 232 | case "windows": 233 | if len(m.baseDir) == 0 { 234 | m.baseDir = os.Getenv("HOME") 235 | } 236 | if len(m.baseDir) == 0 { 237 | m.baseDir = "C:\\" 238 | } 239 | default: 240 | if len(m.baseDir) == 0 { 241 | if os.Geteuid() == 0 { 242 | m.baseDir = "/var" 243 | } else { 244 | m.baseDir = os.Getenv("HOME") 245 | } 246 | } 247 | if len(m.baseDir) == 0 { 248 | m.baseDir = "." 249 | } 250 | } 251 | } 252 | 253 | // SetLogger is used to establish a logger. It overrides the default, so it 254 | // shouldn't be used unless you want to control all logging. 255 | func (m *Manager) SetLogger(l *log.Logger) { 256 | if m.logger != nil { 257 | m.mlog.DelLogger(m.logger) 258 | } 259 | m.logger = l 260 | m.mlog.AddLogger(l) 261 | } 262 | 263 | func (m *Manager) getLogger(s *Service) *log.Logger { 264 | 265 | return log.New(m.mlog, "", 0) 266 | } 267 | 268 | func (m *Manager) monitor() { 269 | finish := false 270 | for !finish { 271 | m.lock() 272 | if m.monitoring { 273 | for s := range m.services { 274 | if s.enabled { 275 | if e := s.checkService(); e != nil { 276 | s.selfHeal() 277 | } 278 | } 279 | } 280 | } 281 | if m.cleanup { 282 | m.monitoring = false 283 | finish = true 284 | } 285 | m.unlock() 286 | 287 | // a "prime" number of milliseconds, to ensure a more 288 | // or less even distribution of clock events 289 | time.Sleep(time.Millisecond * 587) 290 | } 291 | } 292 | 293 | // notify is called asynchronously by services, when they detect a failure. 294 | // It MUST NOT be called by the service as part of a synchronous call to 295 | // the check routine. We do add a check to prevent infinite recursion, but 296 | // again, the caller should be careful not to do this. Notification should 297 | // only be done when a service transitions from succeeding to failing, or vice 298 | // versa. 299 | func (m *Manager) notify(s *Service) { 300 | if s.checking { 301 | // No need for notification, and lets not recurse! 302 | return 303 | } 304 | if s.enabled { 305 | if e := s.checkService(); e != nil { 306 | s.selfHeal() 307 | } 308 | } 309 | } 310 | 311 | func (m *Manager) logf(format string, v ...interface{}) { 312 | if m.mylog != nil { 313 | m.mylog.Printf(format, v...) 314 | } else { 315 | log.Printf(format, v...) 316 | } 317 | } 318 | 319 | func (m *Manager) StopMonitoring() { 320 | m.lock() 321 | m.monitoring = false 322 | m.unlock() 323 | m.logf("*** Govisor stopping monitoring: %s ***", m.name) 324 | } 325 | 326 | func (m *Manager) StartMonitoring() { 327 | m.logf("*** Govisor starting monitoring: %s ***", m.name) 328 | m.lock() 329 | m.monitoring = true 330 | m.unlock() 331 | } 332 | 333 | // Shutdown stops all services, and stops monitoring too. Finally, it removes 334 | // them all from the manager. Think of this as effectively tearing down the 335 | // entire thing. 336 | func (m *Manager) Shutdown() { 337 | m.lock() 338 | m.monitoring = false 339 | for s := range m.services { 340 | s.enabled = false 341 | s.stopRecurse("Shutting down") 342 | s.delManager() 343 | } 344 | m.unlock() 345 | m.logf("*** Govisor shut down: %s ***", m.name) 346 | } 347 | 348 | func (m *Manager) GetLog(lastid int64) ([]LogRecord, int64) { 349 | m.lock() 350 | defer m.unlock() 351 | return m.log.GetRecords(lastid) 352 | } 353 | 354 | func (m *Manager) WatchLog(old int64, expire time.Duration) int64 { 355 | return m.log.Watch(old, expire) 356 | } 357 | 358 | func NewManager(name string) *Manager { 359 | if name == "" { 360 | name = "govisor" 361 | } 362 | // We set the origin serial number to the current timestamp in nsec. 363 | // The assumption here is that we won't have changes to serial number 364 | // occur at frequency > 1GHz. Hence, it should be safe for us to use 365 | // these as unique values, and this may help clients that cache force 366 | // an invalidation if the server for some reason restarts. 367 | m := &Manager{name: name, serial: time.Now().UnixNano()} 368 | m.services = make(map[*Service]bool) 369 | m.cvs = make(map[*sync.Cond]bool) 370 | m.createTime = time.Now() 371 | m.updateTime = m.createTime 372 | m.mlog = NewMultiLogger() 373 | m.log = NewLog() 374 | m.mlog.AddLogger(log.New(m.log, "", 0)) 375 | m.logger = log.New(os.Stderr, "", 0) 376 | m.mylog = m.getLogger(nil) 377 | m.setBaseDir() 378 | go m.monitor() 379 | return m 380 | } 381 | -------------------------------------------------------------------------------- /multilog.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package govisor 16 | 17 | import ( 18 | "log" 19 | "strings" 20 | "sync" 21 | ) 22 | 23 | // MultiLogger implements a wrapper around log.Logger, that permits a single 24 | // logger interface to be used to fan out multiple logs. The idea is that it 25 | // implements an io.Writer, which breaks up the lines and delivers them 26 | // each to the various contained loggers. The contained loggers may have 27 | // their own Prefix and Flags, and those shall not interfere with the parent. 28 | type MultiLogger struct { 29 | log *log.Logger 30 | loggers []*log.Logger 31 | lock sync.Mutex 32 | } 33 | 34 | // Write implements the io.Writer, suitable for use with Logger. It is 35 | // expected that the input is text, delimited by newlines, and delivered 36 | // an entire line at a time. This isn't exactly io.Writer, but it is the 37 | // semantic to which the log.Logger interface conforms. 38 | func (l *MultiLogger) Write(b []byte) (int, error) { 39 | lines := strings.Split(strings.Trim(string(b), "\n"), "\n") 40 | l.lock.Lock() 41 | for _, line := range lines { 42 | for _, logger := range l.loggers { 43 | logger.Println(line) 44 | } 45 | } 46 | l.lock.Unlock() 47 | return len(b), nil 48 | } 49 | 50 | // AddLogger adds a logger to the MultiLogger. Once called, all new log entries 51 | // will be fanned out to this logger, as well as any others that may have been 52 | // registered earlier. A logger can only be added once. 53 | func (l *MultiLogger) AddLogger(logger *log.Logger) { 54 | l.lock.Lock() 55 | defer l.lock.Unlock() 56 | for _, x := range l.loggers { 57 | if x == logger { 58 | return 59 | } 60 | } 61 | l.loggers = append(l.loggers, logger) 62 | } 63 | 64 | // DeleteLogger is removes a logger from the list of destinations that logged 65 | // events are fanned out to. 66 | func (l *MultiLogger) DelLogger(logger *log.Logger) { 67 | l.lock.Lock() 68 | defer l.lock.Unlock() 69 | 70 | for i, x := range l.loggers { 71 | if x == logger { 72 | l.loggers = append(l.loggers[:i], l.loggers[i+1:]...) 73 | break 74 | } 75 | } 76 | } 77 | 78 | // SetPrefix applies the prefix to every registered logger. 79 | func (l *MultiLogger) SetPrefix(prefix string) { 80 | l.lock.Lock() 81 | for _, x := range l.loggers { 82 | x.SetPrefix(prefix) 83 | } 84 | l.lock.Unlock() 85 | } 86 | 87 | // SetFlags applies the flags to every registered logger. 88 | func (l *MultiLogger) SetFlags(flags int) { 89 | l.lock.Lock() 90 | for _, x := range l.loggers { 91 | x.SetFlags(flags) 92 | } 93 | l.lock.Unlock() 94 | } 95 | 96 | func (l *MultiLogger) Logger() *log.Logger { 97 | return l.log 98 | } 99 | 100 | func NewMultiLogger() *MultiLogger { 101 | m := &MultiLogger{} 102 | m.log = log.New(m, "", 0) 103 | return m 104 | } 105 | -------------------------------------------------------------------------------- /process.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package govisor 16 | 17 | import ( 18 | "bufio" 19 | "encoding/json" 20 | "errors" 21 | "fmt" 22 | "io" 23 | "log" 24 | "os" 25 | "os/exec" 26 | "strings" 27 | "sync" 28 | "syscall" 29 | "time" 30 | ) 31 | 32 | const ( 33 | PropProcessFailOnExit PropertyName = "_ProcFailOnExit" 34 | PropProcessStopCmd = "_ProcStopCmd" 35 | PropProcessStopTime = "_ProcStopTime" 36 | PropProcessCheckCmd = "_ProcCheckCmd" 37 | PropProcessDirectory = "_ProcDirectory" 38 | ) 39 | 40 | // 41 | // Process represents an actual operating system level process. This implements 42 | // the Provider interface, and hence Process objects can be used as such. 43 | // 44 | // XXX: is there any reason for this to be public? 45 | // XXX: Should we support Setsid and other SysProcAttr settings? 46 | // 47 | type Process struct { 48 | name string // This is the Govisor name, must be set 49 | desc string // Description 50 | provides []string // Usually empty, but a service can offer more 51 | depends []string // Govisor services we depend upon 52 | conflicts []string // Govisor services that conflict with us 53 | logger *log.Logger // Log for messages, stdout, and stderr. 54 | reason error // Why we failed 55 | failed bool // True if we are in failure state 56 | stopped bool // True if we were stopped 57 | 58 | stopTime time.Duration // Time to wait for clean shutdown, 0 = forever 59 | failOnExit bool // If true, mark failed if the process exits. 60 | stopCmd *exec.Cmd 61 | checkCmd *exec.Cmd 62 | startCmd *exec.Cmd 63 | process *os.Process 64 | directory string 65 | 66 | lock sync.Mutex 67 | waiter sync.WaitGroup 68 | } 69 | 70 | func (p *Process) doLog(r io.ReadCloser, prefix string) { 71 | // Gather stdin/stdout in chunks of lines 72 | reader := bufio.NewReader(r) 73 | for { 74 | line, err := reader.ReadString('\n') 75 | if len(line) != 0 { 76 | p.logger.Print(prefix, strings.Trim(line, "\n")) 77 | } 78 | if err != nil { 79 | return 80 | } 81 | } 82 | } 83 | 84 | func (p *Process) Name() string { 85 | return p.name 86 | } 87 | 88 | func (p *Process) Description() string { 89 | return p.desc 90 | } 91 | 92 | func copyArray(src []string) []string { 93 | rv := make([]string, 0, len(src)) 94 | rv = append(rv, src...) 95 | return rv 96 | } 97 | 98 | func (p *Process) Provides() []string { 99 | return copyArray(p.provides) 100 | } 101 | 102 | func (p *Process) Conflicts() []string { 103 | return copyArray(p.conflicts) 104 | } 105 | 106 | func (p *Process) Depends() []string { 107 | return copyArray(p.depends) 108 | } 109 | 110 | func (p *Process) doWait(cmd *exec.Cmd) { 111 | 112 | e := cmd.Wait() 113 | p.lock.Lock() 114 | p.process = nil 115 | if !p.stopped { 116 | if e != nil { 117 | p.failed = true 118 | p.reason = e 119 | p.logger.Printf("Failed: %v", e) 120 | } else if p.failOnExit { 121 | e = errors.New("Unexpected termination") 122 | p.reason = e 123 | p.failed = true 124 | p.logger.Printf("Failed: %v", e) 125 | } 126 | } 127 | p.lock.Unlock() 128 | p.waiter.Done() 129 | } 130 | 131 | func (p *Process) Start() error { 132 | 133 | p.lock.Lock() 134 | defer p.lock.Unlock() 135 | 136 | p.stopped = false 137 | p.failed = false 138 | p.reason = nil 139 | 140 | cmd := &exec.Cmd{} 141 | *cmd = *p.startCmd 142 | 143 | // XXX: search path 144 | 145 | if cmd.Stdout == nil { 146 | stdout, e := cmd.StdoutPipe() 147 | if e != nil { 148 | p.logger.Printf("Failed to capture stdout: %v", e) 149 | } else { 150 | go p.doLog(stdout, "stdout> ") 151 | } 152 | } 153 | if cmd.Stderr == nil { 154 | stderr, e := cmd.StderrPipe() 155 | if e != nil { 156 | p.logger.Printf("Failed to capture stderr: %v", e) 157 | } else { 158 | go p.doLog(stderr, "stderr> ") 159 | } 160 | } 161 | 162 | if e := cmd.Start(); e != nil { 163 | p.failed = true 164 | p.reason = e 165 | return e 166 | } 167 | p.logger.Printf("Process id %d", cmd.Process.Pid) 168 | p.process = cmd.Process 169 | p.waiter.Add(1) 170 | 171 | go p.doWait(cmd) 172 | 173 | return nil 174 | } 175 | 176 | func (p *Process) runCmdWithTimeout(pfx string, c *exec.Cmd, d time.Duration) error { 177 | newc := &exec.Cmd{} 178 | *newc = *c 179 | if proc := p.process; proc != nil { 180 | if c.Env == nil { 181 | newc.Env = os.Environ() 182 | } 183 | newc.Env = append(make([]string, 0, len(newc.Env)+1), newc.Env...) 184 | newc.Env = append(newc.Env, fmt.Sprintf("PID=%d", proc.Pid)) 185 | } 186 | 187 | // XXX: search path 188 | // XXX: expand $PID in args 189 | 190 | if d == 0 { 191 | d = time.Second * 10 192 | } 193 | if stderr, e := newc.StderrPipe(); e != nil { 194 | p.logger.Printf("Failed to capture stderr: %v", e) 195 | } else { 196 | go p.doLog(stderr, pfx+"stderr> ") 197 | } 198 | if stdout, e := newc.StdoutPipe(); e != nil { 199 | p.logger.Printf("Failed to capture stdout: %v", e) 200 | } else { 201 | go p.doLog(stdout, pfx+"stdout> ") 202 | } 203 | 204 | if e := newc.Start(); e != nil { 205 | return e 206 | } 207 | proc := newc.Process 208 | timer := time.AfterFunc(d, func() { 209 | p.logger.Printf("Timeout waiting for %s command", pfx) 210 | proc.Kill() 211 | }) 212 | e := newc.Wait() 213 | timer.Stop() 214 | return e 215 | } 216 | 217 | func (p *Process) shutdown() { 218 | if proc := p.process; proc != nil && proc.Pid != -1 { 219 | if p.stopCmd == nil { 220 | e := proc.Signal(syscall.SIGTERM) 221 | if e != nil { 222 | p.logger.Printf("Failed sending SIGTERM: %v", e) 223 | } 224 | } else { 225 | // Put the Pid into the environment as $PID 226 | e := p.runCmdWithTimeout("stop", p.stopCmd, p.stopTime) 227 | if e != nil { 228 | p.logger.Printf("Failed stop cmd: %v", e) 229 | } 230 | } 231 | } 232 | } 233 | 234 | func (p *Process) kill() { 235 | if proc := p.process; proc != nil { 236 | e := proc.Kill() 237 | if e != nil { 238 | p.logger.Printf("Failed killing: %v", e) 239 | } 240 | } 241 | } 242 | 243 | func (p *Process) Stop() { 244 | 245 | p.lock.Lock() 246 | p.stopped = true 247 | if proc := p.process; proc != nil { 248 | var timer *time.Timer 249 | p.shutdown() 250 | if p.stopTime > 0 { 251 | timer = time.AfterFunc(p.stopTime, func() { 252 | p.logger.Printf("Graceful shutdown timed out") 253 | p.lock.Lock() 254 | p.kill() 255 | p.lock.Unlock() 256 | }) 257 | } 258 | p.lock.Unlock() 259 | p.waiter.Wait() 260 | p.lock.Lock() 261 | if timer != nil { 262 | timer.Stop() 263 | } 264 | } 265 | p.process = nil 266 | p.lock.Unlock() 267 | } 268 | 269 | func (p *Process) Check() error { 270 | p.lock.Lock() 271 | defer p.lock.Unlock() 272 | 273 | if p.failed { 274 | return p.reason 275 | } 276 | return nil 277 | } 278 | 279 | func (p *Process) SetProperty(n PropertyName, v interface{}) error { 280 | switch n { 281 | case PropLogger: 282 | if v, ok := v.(*log.Logger); ok { 283 | p.logger = v 284 | return nil 285 | } 286 | return ErrBadPropType 287 | case PropProcessFailOnExit: 288 | if v, ok := v.(bool); ok { 289 | p.failOnExit = v 290 | return nil 291 | } 292 | return ErrBadPropType 293 | case PropProcessStopTime: 294 | if v, ok := v.(time.Duration); ok { 295 | p.stopTime = v 296 | return nil 297 | } 298 | return ErrBadPropType 299 | case PropProcessStopCmd: 300 | if v, ok := v.(*exec.Cmd); ok { 301 | p.stopCmd = new(exec.Cmd) 302 | *p.stopCmd = *v 303 | return nil 304 | } 305 | return ErrBadPropType 306 | case PropProcessDirectory: 307 | if v, ok := v.(string); ok { 308 | p.directory = v 309 | return nil 310 | } 311 | return ErrBadPropType 312 | } 313 | return ErrBadPropName 314 | } 315 | 316 | func (p *Process) Property(n PropertyName) (interface{}, error) { 317 | switch n { 318 | case PropLogger: 319 | return p.logger, nil 320 | case PropProcessFailOnExit: 321 | return p.failOnExit, nil 322 | case PropProcessStopTime: 323 | return p.stopTime, nil 324 | case PropProcessStopCmd: 325 | return p.stopCmd, nil 326 | case PropProcessDirectory: 327 | return p.directory, nil 328 | } 329 | return nil, ErrBadPropName 330 | } 331 | 332 | type ProcessManifest struct { 333 | Name string `json:"name"` 334 | Description string `json:"description"` 335 | Command []string `json:"command"` 336 | Env []string `json:"env"` 337 | StopCmd []string `json:"stopCommand"` 338 | StopTime time.Duration `json:"stopTime"` 339 | FailOnExit bool `json:"failOnExit"` 340 | CheckCmd []string `json:"check"` 341 | Restart bool `json:"restart"` 342 | Provides []string `json:"provides"` 343 | Depends []string `json:"depends"` 344 | Conflicts []string `json:"conflicts"` 345 | Directory string `json:"directory"` 346 | } 347 | 348 | func NewProcessFromManifest(m ProcessManifest) *Service { 349 | p := &Process{} 350 | p.name = m.Name 351 | p.desc = m.Description 352 | p.directory = m.Directory 353 | if len(m.Command) != 0 { 354 | p.startCmd = exec.Command(m.Command[0], m.Command[1:]...) 355 | p.startCmd.Dir = p.directory 356 | } 357 | if len(m.StopCmd) != 0 { 358 | p.stopCmd = exec.Command(m.StopCmd[0], m.StopCmd[1:]...) 359 | p.stopCmd.Dir = p.directory 360 | } 361 | if len(m.CheckCmd) != 0 { 362 | p.checkCmd = exec.Command(m.CheckCmd[0], m.CheckCmd[1:]...) 363 | p.checkCmd.Dir = p.directory 364 | } 365 | p.stopTime = m.StopTime 366 | p.depends = m.Depends 367 | p.conflicts = m.Conflicts 368 | p.provides = m.Provides 369 | p.failOnExit = m.FailOnExit 370 | 371 | s := NewService(p) 372 | s.SetProperty(PropRestart, m.Restart) 373 | return s 374 | } 375 | 376 | func NewProcessFromJson(r io.Reader) (*Service, error) { 377 | dec := json.NewDecoder(r) 378 | var m ProcessManifest 379 | if e := dec.Decode(&m); e != nil { 380 | return nil, e 381 | } 382 | return NewProcessFromManifest(m), nil 383 | } 384 | 385 | func NewProcess(name string, cmd *exec.Cmd) *Service { 386 | p := &Process{} 387 | p.logger = log.New(os.Stderr, "", log.LstdFlags) 388 | p.stopTime = time.Second * 10 389 | p.startCmd = &exec.Cmd{} 390 | *p.startCmd = *cmd 391 | p.name = name 392 | p.desc = name + " process: " + cmd.Path 393 | p.directory = cmd.Dir 394 | return NewService(p) 395 | } 396 | -------------------------------------------------------------------------------- /process_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // +build darwin dragonfly freebsd linux netbsd openbsd solaris 16 | 17 | // The test suite relies pretty heavily on a process_test.sh script that is 18 | // bundled, but is pretty specific to POSIX systems. Implementing a suitable 19 | // test script for other systems is left as an exercise for the reader. 20 | 21 | package govisor 22 | 23 | import ( 24 | "os" 25 | "os/exec" 26 | "testing" 27 | "time" 28 | 29 | . "github.com/smartystreets/goconvey/convey" 30 | ) 31 | 32 | func TestProcessStartStop(t *testing.T) { 33 | Convey("Test start/stop of a new process", t, func() { 34 | m := NewManager("TestProcessStartStop") 35 | SetTestLogger(t, m) 36 | s1 := NewProcess("ProcessStartStop:S1", &exec.Cmd{ 37 | Path: "process_test.sh", 38 | Args: []string{"process_test.sh", "3600"}, 39 | }) 40 | So(s1, ShouldNotBeNil) 41 | 42 | m.AddService(s1) 43 | So(s1.Enabled(), ShouldBeFalse) 44 | So(s1.Running(), ShouldBeFalse) 45 | e := s1.Enable() 46 | So(e, ShouldBeNil) 47 | So(s1.Enabled(), ShouldBeTrue) 48 | So(s1.Running(), ShouldBeTrue) 49 | 50 | time.Sleep(time.Millisecond * 10) 51 | 52 | e = s1.Disable() 53 | So(e, ShouldBeNil) 54 | So(s1.Enabled(), ShouldBeFalse) 55 | So(s1.Running(), ShouldBeFalse) 56 | 57 | time.Sleep(time.Millisecond * 10) 58 | 59 | m.Shutdown() 60 | }) 61 | } 62 | 63 | func TestProcessFail(t *testing.T) { 64 | Convey("Test a failing process", t, func() { 65 | m := NewManager("TestProcessFail") 66 | SetTestLogger(t, m) 67 | s1 := NewProcess("ProcessFail:S1", &exec.Cmd{ 68 | Path: "process_test.sh", 69 | Args: []string{"process_test.sh", "fail"}, 70 | }) 71 | So(s1, ShouldNotBeNil) 72 | m.AddService(s1) 73 | m.StopMonitoring() 74 | e := s1.Enable() 75 | So(e, ShouldBeNil) 76 | So(s1.Enabled(), ShouldBeTrue) 77 | time.Sleep(time.Millisecond * 10) 78 | e = s1.Check() 79 | So(e, ShouldNotBeNil) 80 | So(s1.Enabled(), ShouldBeTrue) 81 | So(s1.Failed(), ShouldBeTrue) 82 | So(s1.Running(), ShouldBeFalse) 83 | }) 84 | } 85 | 86 | func TestProcessFromManifest(t *testing.T) { 87 | Convey("Test process from a manifest", t, func() { 88 | mydir, _ := os.Getwd() 89 | exname := mydir + "/" + "process_test.sh" 90 | manifest := ProcessManifest{ 91 | Name: "SampleProcessManifest", 92 | Description: "A sample description", 93 | Directory: "/usr", 94 | Command: []string{exname, "checkwd", "/usr"}, 95 | FailOnExit: false, 96 | Provides: []string{"testmanifest"}, 97 | } 98 | 99 | m := NewManager("TestProcessFromManifest") 100 | SetTestLogger(t, m) 101 | s1 := NewProcessFromManifest(manifest) 102 | So(s1, ShouldNotBeNil) 103 | 104 | m.AddService(s1) 105 | So(s1.Enabled(), ShouldBeFalse) 106 | So(s1.Running(), ShouldBeFalse) 107 | e := s1.Enable() 108 | So(e, ShouldBeNil) 109 | So(s1.Enabled(), ShouldBeTrue) 110 | 111 | time.Sleep(time.Millisecond * 100) 112 | So(s1.Failed(), ShouldBeFalse) 113 | 114 | e = s1.Disable() 115 | So(e, ShouldBeNil) 116 | So(s1.Enabled(), ShouldBeFalse) 117 | So(s1.Running(), ShouldBeFalse) 118 | 119 | time.Sleep(time.Millisecond * 10) 120 | 121 | m.Shutdown() 122 | }) 123 | } 124 | -------------------------------------------------------------------------------- /process_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright 2016 The Govisor Authors 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use file except in compliance with the License. 7 | # You may obtain a copy of the license at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # This test program exists to support testing the process module. 18 | # Its unlikely to be useful for anything else. See process_test.go 19 | # for usage. Note that it requires a POSIX shell. Bash is known to 20 | # work. 21 | 22 | error() { 23 | echo "$*" 1>&2 24 | } 25 | 26 | case "$1" in 27 | [0-9]*) 28 | echo "Sleeping $1 secs (stdout)" 29 | error "Sleep $1 secs (stderr)" 30 | sleep $1 31 | ;; 32 | "") 33 | echo "Sleeping an hour (stdout)" 34 | error "Sleep an hour (stderr)" 35 | sleep 3600 36 | ;; 37 | 38 | checkwd) 39 | pwd=`pwd` 40 | exp="$2" 41 | if [ "$pwd" != "$exp" ] 42 | then 43 | error "CWD of $pwd incorrect, expected $exp" 44 | exit 1 45 | fi 46 | echo "CWD is $pwd" 47 | exit 0 48 | ;; 49 | 50 | fail) 51 | error "Injected failure" 52 | exit 2 53 | ;; 54 | 55 | exit) 56 | echo "Clean failure" 57 | exit 0 58 | ;; 59 | *) 60 | echo "Unknown argument" 61 | exit 1 62 | ;; 63 | esac 64 | -------------------------------------------------------------------------------- /properties.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package govisor 16 | 17 | // Property names. Internal names will all start with an underscore. 18 | // Other, provider specific names, may be supplied. Note that there is 19 | // no provision for property discovery. Consumers wishing to use a property 20 | // must know the property name and type. 21 | type PropertyName string 22 | 23 | const ( 24 | PropLogger PropertyName = "_Logger" // Where logs get sent 25 | PropRestart = "_Restart" // Auto-restart on failure 26 | PropRateLimit = "_RateLimit" // Max starts per period 27 | PropRatePeriod = "_RatePeriod" // Period for RateLimit 28 | PropName = "_Name" // Service name 29 | PropDescription = "_Description" // Service description 30 | PropDepends = "_Depends" // Dependencies list 31 | PropConflicts = "_Conflicts" // Conflicts list 32 | PropProvides = "_Provides" // Provides list 33 | PropNotify = "_Notify" // Notification callback 34 | ) 35 | -------------------------------------------------------------------------------- /provider.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package govisor 16 | 17 | // Provider is what service providers must implement. Note that except for 18 | // the Name and Dependencies elements, the service manager promises not to 19 | // call these methods concurrently. That is, implementers need not worry 20 | // about locking. Applications should not use this interface. 21 | type Provider interface { 22 | // Name returns the name of the provider. For example, a 23 | // provider for SMTP could return return "smtp" here. 24 | // For services that have variants, an optioanl variant can be 25 | // returned by appending a colon. For example, "smtp:sendmail" 26 | // indicates a variant using Berkeley sendmail, whereas "smtp:qmail" 27 | // indicates a variant using qmail. Both of these would satisfy 28 | // a dependency of "smtp". Names may include any alpha numeric, 29 | // or the underscore. No punctuation (modulo the colon separating 30 | // primary name and variant) may be used. 31 | Name() string 32 | 33 | // Description returns what you think. Should be only 32 characters 34 | // to avoid UI truncation. 35 | Description() string 36 | 37 | // Provides returns a list of service names that this provides. 38 | // The Name is implicitly added, so only additional values need to 39 | // be listed. 40 | Provides() []string 41 | 42 | // Depends returns a list of dependencies, that must be satisfied 43 | // in order for this service to run. These are names, that can be 44 | // either fully qualified (such as "smtp:postfix" or just the 45 | // base name (such as "smtp"). 46 | Depends() []string 47 | 48 | // Conflicts returns a list of incompatible values. Note that 49 | // the service itself is excluded when checking, so that one could 50 | // have a service list conflicts of "smtp", even though it provides 51 | // "smtp:postfix". This would ensure that it does not run with any 52 | // any other service. 53 | Conflicts() []string 54 | 55 | // Start attempts to start the service. It blocks until the service 56 | // is either started successfuly, or has definitively failed. 57 | Start() error 58 | 59 | // Stop attempts to stop the service. As with Start, it blocks until 60 | // the operation is complete. This is never allowed to fail. 61 | Stop() 62 | 63 | // Check performs a health check on the service. This can be just 64 | // a check that a process is running, or it can include verification 65 | // by using test protocol packets or queries, or whatever is 66 | // appropriate. It runs synchronously as well. If all is well it 67 | // returns nil, otherwise it returns an error. The error message, 68 | // provided by Error(), should give some clue as to the reason for the 69 | // failed check. 70 | Check() error 71 | 72 | // Property returns the value of a property. 73 | Property(PropertyName) (interface{}, error) 74 | 75 | // SetProperty sets the value of a property. 76 | SetProperty(PropertyName, interface{}) error 77 | } 78 | -------------------------------------------------------------------------------- /rest/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rest 16 | 17 | import ( 18 | "encoding/json" 19 | "io/ioutil" 20 | "net/http" 21 | "net/url" 22 | "strconv" 23 | "strings" 24 | "sync" 25 | "time" 26 | 27 | "golang.org/x/net/context" 28 | ) 29 | 30 | type LogInfo struct { 31 | name string 32 | etag string 33 | Records []LogRecord 34 | } 35 | 36 | type Client struct { 37 | user string // HTTP Basic-Auth 38 | pass string 39 | base string // URI to root of tree on server 40 | auth bool 41 | client *http.Client 42 | transport *http.Transport 43 | 44 | // Cached data 45 | manager *ManagerInfo 46 | services map[string]*ServiceInfo // service entries 47 | names []string // service names 48 | etag string // etag for list of services 49 | logs map[string]*LogInfo 50 | cv *sync.Cond 51 | lock sync.Mutex 52 | } 53 | 54 | func (c *Client) SetAuth(user string, pass string) { 55 | c.user = user 56 | c.pass = pass 57 | c.auth = true 58 | } 59 | 60 | func (c *Client) url(name string) string { 61 | if name == "" { 62 | return c.base + "/services" 63 | } 64 | return c.base + "/services/" + url.QueryEscape(name) 65 | } 66 | 67 | func (c *Client) Watch(ctx context.Context, etag string) (string, error) { 68 | var e error 69 | c.lock.Lock() 70 | if c.manager != nil && etag == "" { 71 | etag = c.manager.etag 72 | c.lock.Unlock() 73 | return etag, nil 74 | } 75 | c.lock.Unlock() 76 | 77 | minfo := &ManagerInfo{} 78 | if minfo.etag, e = c.poll(ctx, c.base, etag, 300, minfo); e != nil { 79 | return "", e 80 | } 81 | if minfo.etag != "" { 82 | c.lock.Lock() 83 | if c.manager == nil || c.manager.etag != minfo.etag { 84 | c.manager = minfo 85 | } 86 | c.lock.Unlock() 87 | etag = minfo.etag 88 | } 89 | return etag, nil 90 | } 91 | 92 | func (c *Client) pollServices(ctx context.Context, secs int) ([]string, error) { 93 | 94 | var e error 95 | v := []string{} 96 | 97 | c.lock.Lock() 98 | otag := c.etag 99 | etag := "" 100 | onames := c.names 101 | c.lock.Unlock() 102 | 103 | if etag, e = c.poll(ctx, c.url(""), otag, secs, &v); e != nil { 104 | return nil, e 105 | } 106 | if etag == "" || etag == otag { 107 | return onames, nil 108 | } 109 | services := make(map[string]*ServiceInfo) 110 | 111 | c.lock.Lock() 112 | c.etag = etag 113 | c.names = v 114 | // save the services we found 115 | for _, n := range v { 116 | if svc, ok := c.services[n]; ok { 117 | services[n] = svc 118 | delete(c.services, n) 119 | } 120 | } 121 | c.services = services 122 | // we let GC clean up the old hash 123 | c.lock.Unlock() 124 | 125 | return v, nil 126 | } 127 | 128 | // Services returns a list of service names known to the implementation 129 | func (c *Client) Services() ([]string, error) { 130 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 131 | defer cancel() 132 | return c.pollServices(ctx, 0) 133 | } 134 | 135 | func (c *Client) pollService(ctx context.Context, name string, secs int, last *ServiceInfo) (*ServiceInfo, error) { 136 | 137 | v := &ServiceInfo{} 138 | c.lock.Lock() 139 | osvc, ok := c.services[name] 140 | c.lock.Unlock() 141 | 142 | otag := "" 143 | if last == nil { 144 | secs = 0 145 | } else if ok && last.etag != osvc.etag { 146 | // If we asked for a check against a value, and the cached 147 | // value is not the same, then we can return the cached value. 148 | return osvc, nil 149 | } else { 150 | // Either we didn't have a value cached, or they are the same. 151 | otag = last.etag 152 | } 153 | 154 | etag, e := c.poll(ctx, c.url(name), otag, secs, v) 155 | if e != nil { 156 | c.lock.Lock() 157 | delete(c.services, name) 158 | c.lock.Unlock() 159 | return nil, e 160 | } 161 | if etag == "" { 162 | return osvc, nil 163 | } 164 | v.etag = etag 165 | c.lock.Lock() 166 | c.services[name] = v 167 | c.lock.Unlock() 168 | return v, nil 169 | } 170 | 171 | func (c *Client) GetService(name string) (*ServiceInfo, error) { 172 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 173 | defer cancel() 174 | return c.pollService(ctx, name, 0, nil) 175 | } 176 | 177 | func (c *Client) WatchService(ctx context.Context, name string, last *ServiceInfo) (*ServiceInfo, error) { 178 | return c.pollService(ctx, name, 300, last) 179 | } 180 | 181 | // poll issues an HTTP GET against the URL, optionally checking for a cache, 182 | // including optionally issuing a long poll that tries to wait until the 183 | // value changes. The return values are the new Etag and any error. If the 184 | // value did not change, then the returned etag will be "", but the error will 185 | // be nil. 186 | type chanResp struct { 187 | r *http.Response 188 | e error 189 | } 190 | 191 | func (c *Client) poll(ctx context.Context, url string, etag string, wait int, v interface{}) (string, error) { 192 | 193 | req, e := http.NewRequest("GET", url, nil) 194 | if e != nil { 195 | return "", e 196 | } 197 | if c.auth { 198 | req.SetBasicAuth(c.user, c.pass) 199 | } 200 | client := c.client 201 | if etag != "" { 202 | req.Header.Set("If-None-Match", etag) 203 | if wait > 0 { 204 | req.Header.Set(PollEtagHeader, etag) 205 | req.Header.Set(PollTimeHeader, strconv.Itoa(wait)) 206 | } 207 | } 208 | 209 | ch := make(chan chanResp) 210 | go func() { 211 | res, e := client.Do(req) 212 | ch <- chanResp{r: res, e: e} 213 | }() 214 | 215 | var res *http.Response 216 | select { 217 | case <-ctx.Done(): 218 | c.transport.CancelRequest(req) 219 | <-ch // wait for the Do to finish (or be canceled) 220 | e = ctx.Err() 221 | case cr := <-ch: 222 | res = cr.r 223 | e = cr.e 224 | } 225 | switch e { 226 | case nil: 227 | case context.DeadlineExceeded: 228 | return "", &Error{ 229 | Code: http.StatusRequestTimeout, 230 | Message: "Request timed out", 231 | } 232 | default: 233 | return "", e 234 | } 235 | 236 | defer res.Body.Close() 237 | if res.StatusCode == http.StatusNotModified { 238 | return "", nil 239 | } 240 | if res.StatusCode != http.StatusOK { 241 | err := &Error{Code: res.StatusCode, Message: res.Status} 242 | 243 | if ebody, e := ioutil.ReadAll(res.Body); e == nil { 244 | if e := json.Unmarshal(ebody, err); e == nil { 245 | return "", err 246 | } 247 | } 248 | 249 | return "", &Error{Code: res.StatusCode, Message: res.Status} 250 | } 251 | body, e := ioutil.ReadAll(res.Body) 252 | if e != nil { 253 | return "", e 254 | } 255 | if e := json.Unmarshal(body, v); e != nil { 256 | return "", e 257 | } 258 | return res.Header.Get("Etag"), nil 259 | } 260 | 261 | func (c *Client) post(url string) error { 262 | req, e := http.NewRequest("POST", url, strings.NewReader("")) 263 | if e != nil { 264 | return e 265 | } 266 | req.Header.Set("Content-Type", "text/plain") // we don't really care 267 | if c.auth { 268 | req.SetBasicAuth(c.user, c.pass) 269 | } 270 | res, e := c.client.Do(req) 271 | if e != nil { 272 | return e 273 | } 274 | defer res.Body.Close() 275 | if res.StatusCode != http.StatusOK { 276 | err := &Error{Code: res.StatusCode, Message: res.Status} 277 | 278 | if ebody, e := ioutil.ReadAll(res.Body); e == nil { 279 | if e := json.Unmarshal(ebody, err); e == nil { 280 | return err 281 | } 282 | } 283 | 284 | return &Error{Code: res.StatusCode, Message: res.Status} 285 | } 286 | return nil 287 | } 288 | 289 | func (c *Client) postService(name string, action string) error { 290 | return c.post(c.url(name) + "/" + action) 291 | } 292 | 293 | func (c *Client) EnableService(name string) error { 294 | return c.postService(name, "enable") 295 | } 296 | 297 | func (c *Client) DisableService(name string) error { 298 | return c.postService(name, "disable") 299 | } 300 | 301 | func (c *Client) ClearService(name string) error { 302 | return c.postService(name, "clear") 303 | } 304 | 305 | func (c *Client) RestartService(name string) error { 306 | return c.postService(name, "restart") 307 | } 308 | 309 | func (c *Client) pollLog(ctx context.Context, name string, secs int, last *LogInfo) (*LogInfo, error) { 310 | 311 | v := &LogInfo{} 312 | 313 | c.lock.Lock() 314 | cached, ok := c.logs[name] 315 | c.lock.Unlock() 316 | 317 | otag := "" 318 | 319 | if last == nil { 320 | secs = 0 321 | } else if ok && last.etag != cached.etag { 322 | // TODO: We should modify this to validate the cached etag 323 | // VERIFY THIS 324 | // 325 | secs = 0 326 | otag = cached.etag 327 | // 328 | // If we asked for a check against a value, and the cached 329 | // value is not the same, then we can return the cached value. 330 | //return cached, nil 331 | } else { 332 | // Either we didn't have a value cached, or they are the same. 333 | otag = last.etag 334 | } 335 | 336 | url := c.url(name) + "/log" 337 | if name == "" { 338 | url = c.base + "/log" 339 | } 340 | 341 | etag, e := c.poll(ctx, url, otag, secs, &v.Records) 342 | if e != nil { 343 | c.lock.Lock() 344 | delete(c.logs, name) 345 | c.lock.Unlock() 346 | return nil, e 347 | } 348 | if etag == "" { 349 | return cached, nil 350 | } 351 | v.etag = etag 352 | c.lock.Lock() 353 | c.logs[name] = v 354 | c.lock.Unlock() 355 | 356 | return v, nil 357 | } 358 | 359 | func (c *Client) WatchLog(ctx context.Context, name string, last *LogInfo) (*LogInfo, error) { 360 | 361 | // Let the poll wait for up to 300 secs (5 minutes). 362 | return c.pollLog(ctx, name, 300, last) 363 | } 364 | 365 | func (c *Client) GetLog(name string) (*LogInfo, error) { 366 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 367 | defer cancel() 368 | return c.pollLog(ctx, name, 0, nil) 369 | } 370 | 371 | // GetServiceLog returns the log, utilizing caching checks. It does not 372 | // wait for changes to the log. 373 | //func (c *Client) GetServiceLog(name string) ([]LogRecord, error) { 374 | // return c.GetLog(name) 375 | //} 376 | 377 | // NewClient returns a Client handle. The transport maybe nil to use 378 | // a default transport, but it may also be adjusted to support additional 379 | // options such as TLS. baseURI is the base URL to use. 380 | func NewClient(t *http.Transport, baseURI string) *Client { 381 | if t == nil { 382 | t = &http.Transport{} 383 | } 384 | c := &Client{ 385 | transport: t, 386 | base: baseURI, 387 | client: &http.Client{Transport: t}, 388 | logs: make(map[string]*LogInfo), 389 | services: make(map[string]*ServiceInfo), 390 | } 391 | return c 392 | } 393 | -------------------------------------------------------------------------------- /rest/common.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rest 16 | 17 | import ( 18 | "time" 19 | ) 20 | 21 | const ( 22 | MimeJson = "application/json; charset=UTF-8" 23 | ) 24 | 25 | const ( 26 | // PollHeader should be set to the last Etag on an incoming request. 27 | // If set we will wait until the resource has an ETag that is different 28 | // from the supplied value, yielding a form of long polling. 29 | // This is only valid with GET requests. Note that the service may 30 | // return early without actually waiting, even if the ETag has not 31 | // changed. Typically there is a default timeout of around a minute 32 | // to make sure that the client is alive and well. 33 | PollEtagHeader = "X-Govisor-Poll-Etag" 34 | PollTimeHeader = "X-Govisor-Poll-Time" 35 | ) 36 | 37 | var ok struct{} 38 | 39 | type ManagerInfo struct { 40 | Name string `json:"name"` 41 | Serial string `json:"serial"` 42 | CreateTime time.Time `json:"created"` 43 | UpdateTime time.Time `json:"updated"` 44 | etag string 45 | } 46 | 47 | type ServiceInfo struct { 48 | Name string `json:"name"` 49 | Description string `json:"description"` 50 | Enabled bool `json:"enabled"` 51 | Running bool `json:"running"` 52 | Failed bool `json:"failed"` 53 | Provides []string `json:"provides"` 54 | Depends []string `json:"depends"` 55 | Conflicts []string `json:"conflicts"` 56 | Status string `json:"status"` 57 | TimeStamp time.Time `json:"tstamp"` 58 | Serial string `json:"serial"` 59 | etag string 60 | } 61 | 62 | type LogRecord struct { 63 | Id string `json:"id"` 64 | Time time.Time `json:"time"` 65 | Text string `json:"text"` 66 | } 67 | 68 | type Error struct { 69 | Code int `json:"code"` 70 | Message string `json:"message"` 71 | } 72 | 73 | func (e *Error) Error() string { 74 | return e.Message 75 | } 76 | -------------------------------------------------------------------------------- /rest/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package rest implements REST API methods for remotely managing govisor. 16 | package rest 17 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package server 16 | 17 | import ( 18 | "encoding/json" 19 | "net/http" 20 | "strconv" 21 | "time" 22 | 23 | "github.com/gorilla/mux" 24 | 25 | "github.com/gdamore/govisor" 26 | "github.com/gdamore/govisor/rest" 27 | ) 28 | 29 | // Handler wraps a Manager, adding http.Handler functionality. 30 | type Handler struct { 31 | m *govisor.Manager 32 | r *mux.Router 33 | } 34 | 35 | var ok = struct{}{} 36 | 37 | func (h *Handler) internalError(w http.ResponseWriter, e error) { 38 | http.Error(w, e.Error(), http.StatusInternalServerError) 39 | } 40 | 41 | func (h *Handler) writeJson(w http.ResponseWriter, v interface{}) { 42 | if b, e := json.Marshal(v); e != nil { 43 | h.internalError(w, e) 44 | } else { 45 | w.Header().Set("Content-Type", rest.MimeJson) 46 | w.Write(b) 47 | } 48 | } 49 | 50 | func (h *Handler) writeError(w http.ResponseWriter, e *rest.Error) { 51 | if b, err := json.Marshal(e); err != nil { 52 | h.internalError(w, err) 53 | } else { 54 | w.Header().Set("Content-Type", rest.MimeJson) 55 | w.WriteHeader(e.Code) 56 | w.Write(b) 57 | } 58 | } 59 | 60 | func (h *Handler) checkPoll(r *http.Request, 61 | watchFn func(old int64, expire time.Duration) int64) { 62 | if ptag := r.Header.Get(rest.PollEtagHeader); len(ptag) < 2 { 63 | return 64 | } else if ptag[0] != '"' || ptag[len(ptag)-1] != '"' { 65 | return 66 | } else if v, e := strconv.ParseInt(ptag[1:len(ptag)-1], 16, 64); e != nil { 67 | return 68 | } else { 69 | ptime, _ := strconv.Atoi(r.Header.Get(rest.PollTimeHeader)) 70 | watchFn(v, time.Duration(ptime)*time.Second) 71 | } 72 | } 73 | 74 | func (h *Handler) condCheckGet(w http.ResponseWriter, r *http.Request, 75 | etag string, ts time.Time) bool { 76 | 77 | if chkTag := r.Header.Get("If-None-Match"); chkTag == etag { 78 | w.WriteHeader(http.StatusNotModified) 79 | return false 80 | } 81 | if chkTime := r.Header.Get("If-Modified-Since"); chkTime == "" { 82 | return true 83 | } else { 84 | when, e := time.Parse(http.TimeFormat, chkTime) 85 | if e != nil { 86 | return true 87 | } 88 | // Round up to 1 nearest second. Note that the carefully 89 | // chosen use of ts.Before means that this will match 90 | // if the same timestamp is used. 91 | if ts.Before(when.Add(time.Second)) { 92 | w.WriteHeader(http.StatusNotModified) 93 | return false 94 | } 95 | } 96 | return true 97 | } 98 | 99 | func (h *Handler) listServices(w http.ResponseWriter, r *http.Request) { 100 | 101 | h.checkPoll(r, h.m.WatchServices) 102 | svcs, sn, ts := h.m.Services() 103 | l := make([]string, 0, len(svcs)) 104 | 105 | for _, svc := range svcs { 106 | l = append(l, svc.Name()) 107 | } 108 | etag := "\"" + strconv.FormatInt(sn, 16) + "\"" 109 | if !h.condCheckGet(w, r, etag, ts) { 110 | return 111 | } 112 | w.Header().Set("Etag", etag) 113 | w.Header().Set("Last-Modified", ts.UTC().Format(http.TimeFormat)) 114 | h.writeJson(w, l) 115 | } 116 | 117 | // TODO consider conditionalizing this 118 | func (h *Handler) findService(name string) (*govisor.Service, *rest.Error) { 119 | svcs, _, _ := h.m.Services() 120 | for _, svc := range svcs { 121 | if svc.Name() == name { 122 | return svc, nil 123 | } 124 | } 125 | return nil, &rest.Error{http.StatusNotFound, "Service not found"} 126 | } 127 | 128 | func (h *Handler) getService(w http.ResponseWriter, r *http.Request) { 129 | 130 | vars := mux.Vars(r) 131 | name := vars["service"] 132 | svc, e := h.findService(name) 133 | if e != nil { 134 | h.writeError(w, e) 135 | return 136 | } 137 | h.checkPoll(r, svc.WatchService) 138 | var info *rest.ServiceInfo 139 | // This loop ensures we provide a consistent view of 140 | // the service. We assume (hope!) that the service isn't 141 | // changing so quickly that we can't complete all these in 142 | // the time it takes for a single loop iteration. 143 | for { 144 | sn := svc.Serial() // must be at start 145 | info = &rest.ServiceInfo{ 146 | Name: svc.Name(), 147 | Description: svc.Description(), 148 | Enabled: svc.Enabled(), 149 | Running: svc.Running(), 150 | Failed: svc.Failed(), 151 | Provides: svc.Provides(), 152 | Depends: svc.Depends(), 153 | Conflicts: svc.Conflicts(), 154 | Serial: strconv.FormatInt(sn, 16), 155 | } 156 | info.Status, info.TimeStamp = svc.Status() 157 | // check must be last 158 | if newsn := svc.Serial(); sn == newsn { 159 | break 160 | } else { 161 | sn = newsn 162 | } 163 | } 164 | 165 | etag := "\"" + info.Serial + "\"" 166 | if !h.condCheckGet(w, r, etag, info.TimeStamp) { 167 | return 168 | } 169 | 170 | w.Header().Set("Etag", etag) 171 | w.Header().Set("Last-Modified", 172 | info.TimeStamp.UTC().Format(http.TimeFormat)) 173 | h.writeJson(w, info) 174 | 175 | } 176 | 177 | func (h *Handler) enableService(w http.ResponseWriter, r *http.Request) { 178 | vars := mux.Vars(r) 179 | name := vars["service"] 180 | if svc, e := h.findService(name); e != nil { 181 | h.writeError(w, e) 182 | } else if err := svc.Enable(); err != nil { 183 | if e == govisor.ErrConflict { 184 | e = &rest.Error{http.StatusConflict, err.Error()} 185 | } else { 186 | e = &rest.Error{http.StatusBadRequest, err.Error()} 187 | } 188 | h.writeError(w, e) 189 | } else { 190 | h.writeJson(w, ok) 191 | } 192 | } 193 | 194 | func (h *Handler) disableService(w http.ResponseWriter, r *http.Request) { 195 | vars := mux.Vars(r) 196 | name := vars["service"] 197 | if svc, e := h.findService(name); e != nil { 198 | h.writeError(w, e) 199 | } else if err := svc.Disable(); err != nil { 200 | e = &rest.Error{http.StatusBadRequest, err.Error()} 201 | h.writeError(w, e) 202 | } else { 203 | h.writeJson(w, ok) 204 | } 205 | } 206 | 207 | func (h *Handler) restartService(w http.ResponseWriter, r *http.Request) { 208 | vars := mux.Vars(r) 209 | name := vars["service"] 210 | if svc, e := h.findService(name); e != nil { 211 | h.writeError(w, e) 212 | } else if err := svc.Restart(); err != nil { 213 | e = &rest.Error{http.StatusBadRequest, err.Error()} 214 | h.writeError(w, e) 215 | } else { 216 | h.writeJson(w, ok) 217 | } 218 | } 219 | 220 | func (h *Handler) clearService(w http.ResponseWriter, r *http.Request) { 221 | vars := mux.Vars(r) 222 | name := vars["service"] 223 | if svc, e := h.findService(name); e != nil { 224 | h.writeError(w, e) 225 | } else { 226 | svc.Clear() 227 | h.writeJson(w, ok) 228 | } 229 | } 230 | 231 | func (h *Handler) getLog(w http.ResponseWriter, r *http.Request) { 232 | vars := mux.Vars(r) 233 | name := vars["service"] 234 | if svc, e := h.findService(name); e != nil { 235 | h.writeError(w, e) 236 | } else { 237 | h.checkPoll(r, svc.WatchLog) 238 | recs, sn := svc.GetLog(0) 239 | jrecs := make([]rest.LogRecord, len(recs)) 240 | when := time.Now() 241 | for i := range recs { 242 | jrecs[i].Id = strconv.FormatInt(recs[i].Id, 16) 243 | jrecs[i].Time = recs[i].Time 244 | jrecs[i].Text = recs[i].Text 245 | when = jrecs[i].Time 246 | } 247 | etag := "\"" + strconv.FormatInt(sn, 16) + "\"" 248 | if !h.condCheckGet(w, r, etag, when) { 249 | return 250 | } 251 | w.Header().Set("Etag", etag) 252 | w.Header().Set("Last-Modified", when.Format(http.TimeFormat)) 253 | h.writeJson(w, jrecs) 254 | } 255 | } 256 | 257 | func (h *Handler) getManagerLog(w http.ResponseWriter, r *http.Request) { 258 | m := h.m 259 | h.checkPoll(r, m.WatchLog) 260 | recs, sn := m.GetLog(0) 261 | jrecs := make([]rest.LogRecord, len(recs)) 262 | when := time.Now() 263 | for i := range recs { 264 | jrecs[i].Id = strconv.FormatInt(recs[i].Id, 16) 265 | jrecs[i].Time = recs[i].Time 266 | jrecs[i].Text = recs[i].Text 267 | when = jrecs[i].Time 268 | } 269 | etag := "\"" + strconv.FormatInt(sn, 16) + "\"" 270 | if !h.condCheckGet(w, r, etag, when) { 271 | return 272 | } 273 | w.Header().Set("Etag", etag) 274 | w.Header().Set("Last-Modified", when.Format(http.TimeFormat)) 275 | h.writeJson(w, jrecs) 276 | } 277 | 278 | func (h *Handler) getManager(w http.ResponseWriter, r *http.Request) { 279 | h.checkPoll(r, h.m.WatchSerial) 280 | info := h.m.GetInfo() 281 | sstr := strconv.FormatInt(info.Serial, 16) 282 | etag := "\"" + sstr + "\"" 283 | i := &rest.ManagerInfo{ 284 | Name: info.Name, 285 | Serial: sstr, 286 | CreateTime: info.CreateTime, 287 | UpdateTime: info.UpdateTime, 288 | } 289 | if !h.condCheckGet(w, r, etag, info.UpdateTime) { 290 | return 291 | } 292 | w.Header().Set("Etag", etag) 293 | w.Header().Set("Last-Modified", 294 | i.UpdateTime.UTC().Format(http.TimeFormat)) 295 | h.writeJson(w, i) 296 | } 297 | 298 | func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 299 | h.r.ServeHTTP(w, req) 300 | } 301 | 302 | func NewHandler(m *govisor.Manager) *Handler { 303 | r := mux.NewRouter() 304 | h := &Handler{m: m, r: r} 305 | r.HandleFunc("/", h.getManager).Methods("GET") 306 | r.HandleFunc("/log", h.getManagerLog).Methods("GET") 307 | r.HandleFunc("/services", h.listServices).Methods("GET") 308 | r.HandleFunc("/services/{service}", h.getService).Methods("GET") 309 | r.HandleFunc("/services/{service}/enable", h.enableService).Methods("POST") 310 | r.HandleFunc("/services/{service}/disable", h.disableService).Methods("POST") 311 | r.HandleFunc("/services/{service}/clear", h.clearService).Methods("POST") 312 | r.HandleFunc("/services/{service}/restart", h.restartService).Methods("POST") 313 | r.HandleFunc("/services/{service}/log", h.getLog).Methods("GET") 314 | return h 315 | } 316 | -------------------------------------------------------------------------------- /service.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Govisor Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use file except in compliance with the License. 5 | // You may obtain a copy of the license at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package govisor 16 | 17 | import ( 18 | "log" 19 | "strings" 20 | "time" 21 | ) 22 | 23 | // Service describes a generic system service -- such as a process, or 24 | // group of processes. Applications are expected to use the Service 25 | // structure to interact with all managed services. 26 | // 27 | // Implementors can provide custom services (which may be any kind of entity) 28 | // by implementing the Provider interface. 29 | // 30 | // Service methods are not thread safe, until the service is added to a 31 | // Manager. Once the service is added to a Manager, the Manager's lock 32 | // will protect concurrent accesses. 33 | // 34 | // Services go through a number of possible states as illustrated in the 35 | // following state diagram. Note that these states are logical, as there is 36 | // no formal state machine in the code. This diagram is for illustration 37 | // purposes only. 38 | // 39 | // +------------+ 40 | // | | 41 | // +---------> Disabled <-------+ 42 | // | | | | 43 | // | +----+--A----+ | 44 | // | | | | 45 | // +-----+----+ +----V--+---+ | 46 | // | | | | | 47 | // | Failed +----> DepWait <----+ | 48 | // | | | | | | 49 | // +-----A--A-+ +----+------+ | | 50 | // | | | | | 51 | // | | +----v-------+ | | 52 | // | | | | | | 53 | // | +------+ Starting | | | 54 | // | | | | | 55 | // | +----+-------+ | | 56 | // | | | | 57 | // | +---V---+ | | 58 | // | | | | | 59 | // +----------+ Run +-------+---+ 60 | // | | 61 | // +-------+ 62 | // 63 | type Service struct { 64 | prov Provider 65 | mgr *Manager 66 | name string 67 | desc string 68 | depends []string 69 | conflicts []string 70 | provides []string 71 | enabled bool 72 | running bool 73 | stopping bool 74 | failed bool 75 | restart bool 76 | checking bool 77 | healthy bool 78 | err error 79 | parents map[string]map[*Service]bool 80 | children map[*Service]bool 81 | incompat map[*Service]bool 82 | logger *log.Logger 83 | stamp time.Time 84 | reason string 85 | starts int 86 | rateLog bool 87 | rateLimit int 88 | ratePeriod time.Duration 89 | startTimes []time.Time 90 | notify func() 91 | slog *Log 92 | mlog *MultiLogger 93 | serial int64 94 | } 95 | 96 | // The service name. This takes either the form or :. 97 | // Except for the colon used to separate the from , no 98 | // punctuation characters other than underscores are permitted. When 99 | // attempting to resolve dependencies, a dependency can list either the 100 | // full : name or just . In the former case, the 101 | // full service name must match. In the latter case, any service with 102 | // the same component matches. 103 | func (s *Service) Name() string { 104 | return s.name 105 | } 106 | 107 | // Description returns a descriptive name for the service. If possible, 108 | // user interfaces should try to allocate at least 32 characters of horizontal 109 | // space when displaying descriptions. 110 | func (s *Service) Description() string { 111 | return s.desc 112 | } 113 | 114 | // Provides is a way to indicate other service names that this service 115 | // offers. This permits a service instance to support multiple real 116 | // capabilities, or to provide multiple aliases. For example, a daemon 117 | // might offer "http" and "ftp" both. 118 | func (s *Service) Provides() []string { 119 | return s.provides 120 | } 121 | 122 | // Depends returns a list of service names. See the Name description 123 | // for how these are used. 124 | func (s *Service) Depends() []string { 125 | return s.depends 126 | } 127 | 128 | func (s *Service) Serial() int64 { 129 | var rv int64 130 | if m := s.mgr; m != nil { 131 | m.lock() 132 | rv = s.serial 133 | m.unlock() 134 | } 135 | return rv 136 | } 137 | 138 | // Status returns the most reason status message, and the time when the 139 | // status was recorded. 140 | func (s *Service) Status() (string, time.Time) { 141 | if m := s.mgr; m != nil { 142 | m.lock() 143 | defer m.unlock() 144 | } 145 | return s.reason, s.stamp 146 | } 147 | 148 | func (s *Service) WatchService(old int64, expire time.Duration) int64 { 149 | if m := s.mgr; m != nil { 150 | return m.watchSerial(old, &s.serial, expire) 151 | } 152 | // This isn't perfect, as it won't wake up when if a manager is 153 | // added later. But really, nobody ought to be calling this unless 154 | // the service is added to a manager. 155 | if old == s.serial { 156 | time.Sleep(expire) 157 | } 158 | return s.serial 159 | } 160 | 161 | func (s *Service) WatchLog(old int64, expire time.Duration) int64 { 162 | return s.slog.Watch(old, expire) 163 | } 164 | 165 | // Conflicts returns a list of strings or service names that 166 | // cannot be enabled with this one. The system will make sure that 167 | // attempts to enable the service are rejected. Note that the scope 168 | // of conflict is limited to a single Manager; that is the check will 169 | // not prevent two conflicting services running under the control of 170 | // different Managers. 171 | func (s *Service) Conflicts() []string { 172 | return s.conflicts 173 | } 174 | 175 | // Enabled checks if a service is enabled. 176 | func (s *Service) Enabled() bool { 177 | if m := s.mgr; m == nil { 178 | return false 179 | } else { 180 | m.lock() 181 | rv := s.enabled 182 | m.unlock() 183 | return rv 184 | } 185 | } 186 | 187 | // Running checks if a service is running. This will be false if the 188 | // service has failed for any reason, or is unable to run due to a missing 189 | // dependency. 190 | func (s *Service) Running() bool { 191 | if m := s.mgr; m == nil { 192 | return false 193 | } else { 194 | m.lock() 195 | rv := s.running && !s.stopping 196 | m.unlock() 197 | return rv 198 | } 199 | } 200 | 201 | // Failed returns true if the service is in a failure state. 202 | func (s *Service) Failed() bool { 203 | if m := s.mgr; m == nil { 204 | return false 205 | } else { 206 | m.lock() 207 | rv := s.failed 208 | m.unlock() 209 | return rv 210 | } 211 | } 212 | 213 | // Enable enables the service. This will also start any services that may 214 | // have not been running due to unsatisfied dependencies, but which now 215 | // are able to (and were otherwise enabled.) 216 | func (s *Service) Enable() error { 217 | if s.mgr == nil { 218 | return ErrNoManager 219 | } 220 | s.mgr.lock() 221 | defer s.mgr.unlock() 222 | 223 | if s.enabled { 224 | return nil 225 | } 226 | 227 | for c := range s.incompat { 228 | if c.enabled { 229 | s.logf("Cannot enable %s: conflicts with %s", 230 | s.Name(), c.Name()) 231 | s.reason = "Disabled due to conflict" 232 | s.serial = s.mgr.bumpSerial() 233 | s.stamp = time.Now() 234 | return ErrConflict 235 | } 236 | } 237 | s.serial = s.mgr.bumpSerial() 238 | s.reason = "Enabled" 239 | s.stamp = time.Now() 240 | s.logf("Enabling service %s", s.Name()) 241 | s.enabled = true 242 | s.starts = 0 243 | s.startRecurse("Enabled service") 244 | return nil 245 | } 246 | 247 | // Disable disables the service, stopping it. It also will stop any services 248 | // which will no longer have satisfied dependencies as a result of this 249 | // service being disabled. It also clears the error state. 250 | func (s *Service) Disable() error { 251 | if s.mgr == nil { 252 | return ErrNoManager 253 | } 254 | s.mgr.lock() 255 | defer s.mgr.unlock() 256 | 257 | if !s.enabled && s.reason == "Disabled" { 258 | return nil 259 | } 260 | 261 | s.serial = s.mgr.bumpSerial() 262 | s.logf("Disabling service %s", s.Name()) 263 | s.stamp = time.Now() 264 | s.reason = "Disabled" 265 | s.enabled = false 266 | s.failed = false 267 | s.err = nil 268 | s.stopRecurse("Disabled") 269 | return nil 270 | } 271 | 272 | // Restart restarts a service. It also clears any failure condition 273 | // that may have occurred. 274 | func (s *Service) Restart() error { 275 | if s.mgr == nil { 276 | return ErrNoManager 277 | } 278 | 279 | s.mgr.lock() 280 | defer s.mgr.unlock() 281 | 282 | if !s.enabled { 283 | return nil 284 | } 285 | 286 | s.serial = s.mgr.bumpSerial() 287 | s.logf("Restarting service %s", s.Name()) 288 | s.enabled = false 289 | s.stopRecurse("Stopping for restart") 290 | 291 | s.stamp = time.Now() 292 | s.reason = "Restarting" 293 | s.starts = 0 294 | s.failed = false 295 | s.err = nil 296 | s.enabled = true 297 | s.startRecurse("Restarting") 298 | return nil 299 | } 300 | 301 | // Clear clears any error condition in the service, without actually 302 | // enabling it. It will attempt to start the service if it isn't 303 | // already running, and is enabled. 304 | func (s *Service) Clear() { 305 | if s.mgr == nil { 306 | return 307 | } 308 | s.mgr.lock() 309 | defer s.mgr.unlock() 310 | 311 | s.serial = s.mgr.bumpSerial() 312 | if s.failed { 313 | s.reason = "Cleared fault" 314 | s.stamp = time.Now() 315 | s.logf("Clearing fault on %s", s.Name()) 316 | } 317 | s.starts = 0 318 | s.failed = false 319 | s.err = nil 320 | s.startRecurse("Cleared fault") 321 | } 322 | 323 | // Check checks if a service is running, and performs any appropriate health 324 | // checks. It returns nil if the service is running and healthy, or false 325 | // otherwise. If it returns false, it will stop the service, as well as 326 | // dependent services, and put the service into failed state. 327 | func (s *Service) Check() error { 328 | if s.mgr == nil { 329 | return ErrNoManager 330 | } 331 | return s.checkService() 332 | } 333 | 334 | // matchServiceNames matches if the first (concrete) name matches 335 | // the second. This is true if either the variant of s1 is empty, 336 | // or the two variants collide. 337 | func serviceMatches(s1, s2 string) bool { 338 | a1 := strings.SplitN(s1, ":", 2) 339 | a2 := strings.SplitN(s2, ":", 2) 340 | 341 | if a1[0] != a2[0] { 342 | return false 343 | } 344 | if len(a1) == 1 { 345 | return true 346 | } 347 | if len(a2) == 1 { 348 | return false 349 | } 350 | return a1[1] == a2[1] 351 | } 352 | 353 | // Matches returns true if the service name matches the check. This is 354 | // true if either the check is a complete match, or if the first part of 355 | // our name (or Provides) is identical to the check. For example, if our 356 | // name is "x:y", then this would return true for a check of "x", or "x:y", 357 | // but not for "x:z", nor "z:y". 358 | func (s *Service) Matches(check string) bool { 359 | if serviceMatches(check, s.Name()) { 360 | return true 361 | } 362 | for _, p := range s.Provides() { 363 | if serviceMatches(check, p) { 364 | return true 365 | } 366 | } 367 | return false 368 | } 369 | 370 | // SetProperty sets a property on the service. 371 | func (s *Service) SetProperty(n PropertyName, v interface{}) error { 372 | if m := s.mgr; m != nil { 373 | m.lock() 374 | defer m.unlock() 375 | } 376 | if e := s.setProp(n, v); e != nil { 377 | s.logf("Failed to set property %s: %v", s.Name(), e) 378 | return e 379 | } 380 | return nil 381 | } 382 | 383 | func (s *Service) setProp(n PropertyName, v interface{}) error { 384 | // Lock it if we are already added. Some properties cannot be 385 | // set once a the service is added. 386 | if m := s.mgr; m != nil { 387 | switch n { 388 | case PropName, 389 | PropDescription, 390 | PropConflicts, 391 | PropDepends, 392 | PropProvides: 393 | // These properties cannot be altered once they are 394 | // added to a service. 395 | return ErrPropReadOnly 396 | } 397 | // We might fail, but better to bump serial number than to 398 | // not bump it when we should have 399 | s.serial = m.bumpSerial() 400 | } 401 | switch n { 402 | case PropLogger: 403 | if v, ok := v.(*log.Logger); ok { 404 | if s.enabled { 405 | // Cannot change logger while service enabled. 406 | return ErrPropReadOnly 407 | } 408 | if s.logger != nil { 409 | s.mlog.DelLogger(s.logger) 410 | } 411 | s.logger = v 412 | s.mlog.AddLogger(s.logger) 413 | } else { 414 | return ErrBadPropType 415 | } 416 | case PropRestart: 417 | if v, ok := v.(bool); ok { 418 | s.restart = v 419 | } else { 420 | return ErrBadPropType 421 | } 422 | case PropRateLimit: 423 | if v, ok := v.(int); ok { 424 | s.starts = 0 425 | if v > 0 { 426 | s.startTimes = make([]time.Time, v) 427 | } else { 428 | s.startTimes = nil 429 | } 430 | s.rateLimit = v 431 | } else { 432 | return ErrBadPropType 433 | } 434 | case PropRatePeriod: 435 | if v, ok := v.(time.Duration); ok { 436 | s.starts = 0 437 | s.ratePeriod = v 438 | } else { 439 | return ErrBadPropType 440 | } 441 | case PropName: 442 | if v, ok := v.(string); ok { 443 | s.name = v 444 | } else { 445 | return ErrBadPropType 446 | } 447 | case PropDescription: 448 | if v, ok := v.(string); ok { 449 | s.desc = v 450 | } else { 451 | return ErrBadPropType 452 | } 453 | case PropConflicts: 454 | if v, ok := v.([]string); ok { 455 | s.conflicts = append([]string{}, v...) 456 | } else { 457 | return ErrBadPropType 458 | } 459 | case PropDepends: 460 | if v, ok := v.([]string); ok { 461 | s.depends = append([]string{}, v...) 462 | } else { 463 | return ErrBadPropType 464 | } 465 | case PropProvides: 466 | if v, ok := v.([]string); ok { 467 | s.provides = append([]string{}, v...) 468 | } else { 469 | return ErrBadPropType 470 | } 471 | case PropNotify: 472 | if v, ok := v.(func()); ok { 473 | s.notify = v 474 | // We don't want to pass this one down, as we've 475 | // registered ourselves there. 476 | return nil 477 | } else { 478 | return ErrBadPropType 479 | } 480 | default: 481 | return s.prov.SetProperty(n, v) 482 | } 483 | 484 | // Pass the new property to the provider. The provider doesn't get a 485 | // a chance to veto properties we've already dealt with though. 486 | s.prov.SetProperty(n, v) 487 | return nil 488 | } 489 | 490 | func (s *Service) GetProperty(n PropertyName) (interface{}, error) { 491 | if m := s.mgr; m != nil { 492 | m.lock() 493 | defer m.unlock() 494 | } 495 | 496 | switch n { 497 | case PropLogger: 498 | return s.logger, nil 499 | case PropRestart: 500 | return s.restart, nil 501 | case PropRateLimit: 502 | return s.rateLimit, nil 503 | case PropRatePeriod: 504 | return s.ratePeriod, nil 505 | case PropName: 506 | return s.name, nil 507 | case PropDescription: 508 | return s.desc, nil 509 | case PropConflicts: 510 | return append([]string{}, s.conflicts...), nil 511 | case PropDepends: 512 | return append([]string{}, s.depends...), nil 513 | case PropProvides: 514 | return append([]string{}, s.provides...), nil 515 | case PropNotify: 516 | return s.notify, nil 517 | } 518 | return s.prov.Property(n) 519 | } 520 | 521 | func (s *Service) GetLog(lastid int64) ([]LogRecord, int64) { 522 | if m := s.mgr; m != nil { 523 | m.lock() 524 | defer m.unlock() 525 | } 526 | return s.slog.GetRecords(lastid) 527 | } 528 | 529 | // setManager is called by the framework when the service is added to 530 | // the manager. This calculates the various dependency graphs, updating 531 | // links to other services in the manager. 532 | func (s *Service) setManager(mgr *Manager) { 533 | if s.mgr != nil { 534 | // This is a serious programmer mistake 535 | panic("Already added to a manager") 536 | } 537 | s.mlog.AddLogger(mgr.getLogger(s)) 538 | s.mgr = mgr 539 | 540 | s.incompat = make(map[*Service]bool) 541 | s.children = make(map[*Service]bool) 542 | s.parents = make(map[string]map[*Service]bool) 543 | for _, d := range s.Depends() { 544 | s.parents[d] = make(map[*Service]bool) 545 | } 546 | for t := range mgr.services { 547 | 548 | // do we satisfy a dependency of t? 549 | for _, d := range t.Depends() { 550 | if s.Matches(d) { 551 | t.parents[d][s] = true 552 | s.children[t] = true 553 | break 554 | } 555 | } 556 | 557 | // does t satisfy a dependency of s? 558 | for _, d := range s.Depends() { 559 | if t.Matches(d) { 560 | s.parents[d][t] = true 561 | t.children[s] = true 562 | break 563 | } 564 | } 565 | 566 | // do we conflict with t? 567 | for _, c := range t.Conflicts() { 568 | if s.Matches(c) { 569 | s.incompat[t] = true 570 | t.incompat[s] = true 571 | } 572 | } 573 | for _, c := range s.Conflicts() { 574 | if t.Matches(c) { 575 | s.incompat[t] = true 576 | t.incompat[s] = true 577 | } 578 | } 579 | } 580 | s.stamp = time.Now() 581 | s.reason = "Added service" 582 | s.logf("Added service %s to %s: %s", s.Name(), mgr.Name(), 583 | s.Description()) 584 | mgr.services[s] = true 585 | } 586 | 587 | func (s *Service) delManager() { 588 | if s.mgr == nil { 589 | return 590 | } 591 | 592 | // remove the item 593 | delete(s.mgr.services, s) 594 | 595 | // remove from each of our conflicts 596 | for c := range s.incompat { 597 | delete(c.incompat, s) 598 | delete(s.incompat, c) 599 | } 600 | 601 | // our children (things that may depend upon us) 602 | for c := range s.children { 603 | for p := range c.parents { 604 | delete(c.parents[p], s) 605 | } 606 | delete(s.children, c) 607 | } 608 | 609 | // our parents (this we depend upon) 610 | for d, p := range s.parents { 611 | for t := range p { 612 | delete(p, t) 613 | delete(t.children, s) 614 | } 615 | delete(s.parents, d) 616 | } 617 | 618 | s.reason = "Removed service" 619 | s.stamp = time.Now() 620 | s.mgr = nil 621 | } 622 | 623 | func (s *Service) logf(fmt string, v ...interface{}) { 624 | s.mlog.Logger().Printf(fmt, v...) 625 | } 626 | 627 | func (s *Service) startRecurse(detail string) { 628 | if s.running { 629 | return 630 | } 631 | if !s.canRun() { 632 | return 633 | } 634 | if e := s.tooQuickly(); e != nil { 635 | return 636 | } 637 | if s.rateLimit > 0 { 638 | s.startTimes[s.starts%s.rateLimit] = time.Now() 639 | } 640 | s.starts++ 641 | s.serial = s.mgr.bumpSerial() 642 | if e := s.prov.Start(); e != nil { 643 | s.logf("Failed to start %s: %v", s.Name(), e) 644 | s.reason = "Failed start:" + e.Error() 645 | s.stamp = time.Now() 646 | s.err = e 647 | s.failed = true 648 | return 649 | } 650 | s.reason = "Started" 651 | s.stamp = time.Now() 652 | s.logf("Started %s: %s", s.Name(), detail) 653 | s.running = true 654 | s.failed = false 655 | for child := range s.children { 656 | child.startRecurse("Dependency running") 657 | } 658 | } 659 | 660 | func (s *Service) stopRecurse(detail string) { 661 | if !s.running || s.stopping { 662 | return 663 | } 664 | s.stopping = true 665 | for child := range s.children { 666 | if child.canRun() { 667 | continue 668 | } 669 | child.stopRecurse("Unmet dependency") 670 | } 671 | s.serial = s.mgr.bumpSerial() 672 | s.prov.Stop() 673 | s.reason = detail 674 | s.stamp = time.Now() 675 | s.logf("Stopped %s: %s", s.Name(), detail) 676 | 677 | s.running = false 678 | s.stopping = false 679 | } 680 | 681 | func (s *Service) canRun() bool { 682 | if s.stopping || !s.enabled { 683 | return false 684 | } 685 | for _, deps := range s.parents { 686 | sat := false 687 | for d := range deps { 688 | if d.enabled && d.running && !d.stopping && !d.failed { 689 | sat = true 690 | break 691 | } 692 | } 693 | if !sat { 694 | if s.reason != "Unmet dependency" { 695 | s.reason = "Unmet dependency" 696 | s.serial = s.mgr.bumpSerial() 697 | s.stamp = time.Now() 698 | } 699 | return false 700 | } 701 | } 702 | 703 | for c := range s.incompat { 704 | if c.enabled { 705 | return false 706 | } 707 | } 708 | return true 709 | } 710 | 711 | func (s *Service) checkService() error { 712 | if s.failed { 713 | return s.err 714 | } 715 | if !s.running { 716 | return ErrNotRunning 717 | } 718 | s.checking = true 719 | if e := s.prov.Check(); e != nil { 720 | s.serial = s.mgr.bumpSerial() 721 | s.logf("Service %s faulted: %v", s.Name(), e) 722 | s.failed = true 723 | s.stopRecurse("Faulted: " + e.Error()) 724 | s.err = e 725 | s.checking = false 726 | return e 727 | } 728 | if s.reason != "Healthy" { 729 | s.serial = s.mgr.bumpSerial() 730 | s.reason = "Healthy" 731 | s.logf("Service healthy") 732 | s.stamp = time.Now() 733 | } 734 | s.checking = false 735 | return nil 736 | } 737 | 738 | // A service is restarting too quickly if it restarts more than a specified 739 | // number of times in an interval. Once we hit that threshold, we wait for 740 | // a full interval count before we will restart. Effectively, this means 741 | // that if we hit the threshold, we actually won't restart for *another* 742 | // interval, reducing our rate to 1/2 the configured rate, punishing us for 743 | // bad behavior. 744 | func (s *Service) tooQuickly() error { 745 | if s.rateLimit == 0 { 746 | return nil 747 | } 748 | if s.starts < s.rateLimit { 749 | return nil 750 | } 751 | 752 | // If we've restarted more than n times in the last period, 753 | // then rate limit us. 754 | idx := (s.starts - 1) % s.rateLimit 755 | end := s.startTimes[idx] 756 | if time.Now().Before(end.Add(s.ratePeriod)) { 757 | 758 | // Log it if not already done. 759 | if !s.rateLog { 760 | s.logf("Service %s restarting too quickly", s.Name()) 761 | } 762 | // And we uncoditionally mark this to note cool down. 763 | s.rateLog = true 764 | return ErrRateLimited 765 | } 766 | 767 | // If we haven't restarted recently too quickly, we're done. 768 | if !s.rateLog { 769 | // Not in cool down mode. 770 | return nil 771 | } 772 | 773 | // Check to see if cool down from prior rate limit is expired. 774 | idx = (s.starts - 2) % s.rateLimit 775 | end = s.startTimes[idx] 776 | if time.Now().Before(end.Add(s.ratePeriod)) { 777 | return ErrRateLimited 778 | } 779 | 780 | // All cool downs expired. 781 | s.rateLog = false 782 | return nil 783 | } 784 | 785 | func (s *Service) selfHeal() { 786 | if s.failed && s.restart { 787 | s.logf("Attempting self-healing") 788 | s.startRecurse("Self-healing attempt") 789 | } 790 | } 791 | 792 | func (s *Service) doNotify() { 793 | go func() { 794 | var cb func() 795 | if m := s.mgr; m != nil { 796 | m.lock() 797 | m.notify(s) 798 | cb = s.notify 799 | m.unlock() 800 | } else { 801 | cb = s.notify 802 | } 803 | if cb != nil { 804 | go cb() 805 | } 806 | }() 807 | } 808 | 809 | // NewService allocates a service instance from a Provider. The intention 810 | // is that Providers use this in their own constructors to present only a 811 | // Service interface to applications. 812 | func NewService(p Provider) *Service { 813 | s := &Service{prov: p} 814 | s.ratePeriod = time.Minute 815 | s.rateLimit = 10 816 | s.startTimes = make([]time.Time, s.rateLimit) 817 | 818 | s.name = p.Name() 819 | s.desc = p.Description() 820 | s.conflicts = append([]string{}, p.Conflicts()...) 821 | s.depends = append([]string{}, p.Depends()...) 822 | s.provides = append([]string{}, p.Provides()...) 823 | s.mlog = NewMultiLogger() 824 | s.mlog.Logger().SetPrefix("[" + s.Name() + "] ") 825 | s.prov.SetProperty(PropLogger, s.mlog.Logger()) 826 | s.slog = NewLog() 827 | s.mlog.AddLogger(log.New(s.slog, "", 0)) 828 | p.SetProperty(PropNotify, s.doNotify) 829 | return s 830 | } 831 | --------------------------------------------------------------------------------