├── .github ├── FUNDING.yml └── workflows │ └── release.yml ├── .gitignore ├── .npmignore ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── docs ├── assets │ ├── css │ │ ├── border-radius.htc │ │ ├── guide.css │ │ └── main.css │ └── images │ │ ├── finder.png │ │ ├── startstopstatus.jpg │ │ ├── tux.png │ │ └── windows8.png ├── guides.json └── guides │ ├── linuxtemplates │ └── README.md │ ├── nodeLinuxService │ └── README.md │ └── nodelinuxtroubleshooting │ └── README.md ├── example ├── helloworld.js ├── install.js └── uninstall.js ├── lib ├── daemon.js ├── node-linux.js ├── systemd.js ├── systemv.js ├── templates │ ├── systemd │ │ ├── service │ │ └── service-wrapper │ └── systemv │ │ ├── debian │ │ └── redhat └── wrapper.js └── package.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [coreybutler] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Tag, Release, & Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | deploy: 10 | name: Deploy 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Checkout updated source code 14 | - uses: actions/checkout@v2 15 | 16 | # If the version has changed, create a new git tag for it. 17 | - name: Tag 18 | id: autotagger 19 | uses: butlerlogic/action-autotag@master 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | # The remaining steps all depend on whether or not 24 | # a new tag was created. There is no need to release/publish 25 | # updates until the code base is in a releaseable state. 26 | 27 | # If the new version/tag is a pre-release (i.e. 1.0.0-beta.1), create 28 | # an environment variable indicating it is a prerelease. 29 | - name: Pre-release 30 | if: steps.autotagger.outputs.tagname != '' 31 | run: | 32 | if [[ "${{ steps.autotagger.output.version }}" == *"-"* ]]; then echo "::set-env IS_PRERELEASE=true";else echo "::set-env IS_PRERELEASE=''";fi 33 | 34 | # Create a github release 35 | # This will create a snapshot of the module, 36 | # available in the "Releases" section on Github. 37 | - name: Release 38 | id: create_release 39 | if: steps.autotagger.outputs.tagname != '' 40 | uses: actions/create-release@v1.0.0 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | with: 44 | tag_name: ${{ steps.autotagger.outputs.tagname }} 45 | release_name: ${{ steps.autotagger.outputs.tagname }} 46 | body: ${{ steps.autotagger.outputs.tagmessage }} 47 | draft: false 48 | prerelease: env.IS_PRERELEASE != '' 49 | 50 | # Use this action to publish a single module to npm. 51 | - name: Publish 52 | id: publish_npm 53 | if: steps.autotagger.outputs.tagname != '' 54 | uses: author/action-publish@master 55 | env: 56 | REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} 57 | 58 | - name: Rollback Release 59 | if: failure() && steps.create_release.outputs.id != '' 60 | uses: author/action-rollback@stable 61 | with: 62 | tag: ${{ steps.autotagger.outputs.tagname }} 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .* 4 | !.gitignore 5 | !.npmignore 6 | !.github 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | support 2 | test 3 | example 4 | docs 5 | Gruntfile.js 6 | *.sock 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please make sure you review the README, especially the updates, before submitting anything. 2 | 3 | **Contributions must be cross-platform compatible...** node-windows/mac/linux share a common API. 4 | 5 | It is also important to note that a new project, [Daemon Manager](http://github.com/coreybutler/daemon-manager), has been in the 6 | works for a while now. This module is not yet at a release point and I will not be accepting contributions until it is. The 7 | daemon manager is an abstraction of all the common wrapping functionality for Windows, OSX, and Linux. This is how the API 8 | will remain consistent across operating systems. 9 | 10 | --- 11 | 12 | ## NGN 13 | 14 | node-windows/mac/linux were built to support [NGN](http://github.com/nodengn). NGN is a Node.js-based microservice platform. 15 | In accordance to the Microservice/POA/SOA approach, it uses multiple independent but connected processes to support apps/systems. 16 | These processes are controlled with the node-* modules. 17 | 18 | It has been in the works for almost 2yrs and is slated for a pre-release when Node 0.12.x launches. The node-* series 19 | will progress in accordance to milestone accomplishments with NGN (which should be plentiful soon). 20 | 21 | --- 22 | 23 | # THANKS 24 | 25 | I sincerely appreciate your contributions and feedback. 26 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*global module:false*/ 2 | module.exports = function(grunt) { 3 | 4 | var cfg = {}; 5 | 6 | // Project configuration. 7 | grunt.initConfig({ 8 | pkg: '', 9 | meta: { 10 | banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - ' + 11 | '<%= grunt.template.today("yyyy-mm-dd") %>\n' + 12 | '<%= pkg.homepage ? "* " + pkg.homepage + "\n" : "" %>' + 13 | '* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' + 14 | ' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */' 15 | }, 16 | jsduck: { 17 | main: { 18 | // source paths with your code 19 | src: [ 20 | './lib' 21 | ], 22 | 23 | // docs output dir 24 | dest: '../node-linux-docs/manual', 25 | 26 | // extra options 27 | options: { 28 | 'title': 'node-linux', 29 | //'welcome': 'src/assets/html/welcome.html', 30 | 'head-html': '', 31 | //'categories': 'src/categories.json', 32 | 'guides': 'docs/guides.json', 33 | 'color': true, 34 | 'builtin-classes': true, 35 | 'warnings': ['-req_after_opt'], 36 | 'external': ['XMLHttpRequest'] 37 | } 38 | } 39 | }, 40 | clean: { 41 | docs: ["../node-linux-docs/manual"], 42 | }, 43 | copy: { 44 | jsduckassets: { 45 | files: [ 46 | {expand: true, cwd: './docs/assets/css/', src:['*.*'], dest: '../node-linux-docs/manual/resources/css/'}, 47 | {expand: true, cwd: './docs/assets/images/', src:['*.*'], dest: '../node-linux-docs/manual/resources/images/'}, 48 | {expand: true, cwd: './docs/assets/images/', src:['tabs.png'], dest: '../node-linux-docs/manual/resources/images/'} 49 | ] 50 | } 51 | } 52 | }); 53 | 54 | grunt.loadNpmTasks('grunt-contrib-copy'); 55 | grunt.loadNpmTasks('grunt-jsduck'); 56 | grunt.loadNpmTasks('grunt-contrib-clean'); 57 | 58 | // Default task. 59 | grunt.registerTask('default', ['clean:docs','jsduck','copy:jsduckassets']); 60 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2013 Corey Butler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-linux 2 | 3 | [![NPM version](https://badge.fury.io/js/node-linux.png)](http://badge.fury.io/js/node-linux) 4 | [![NGN Dependencies](https://david-dm.org/coreybutler/node-linux.png)](https://david-dm.org/coreybutler/node-linux) 5 | [![Build](https://api.travis-ci.org/coreybutler/node-linux.png)](https://travis-ci.org/coreybutler/node-linux) 6 | 7 | **Sponsors (as of 2020)** 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | Follow the author on [Twitter (@goldglovecb)](http://twitter.com/goldglovecb). 17 | 18 | **Contributions Requested** 19 | 20 | (see below) 21 | 22 | Documentation is available at the [node-linux portal](https://coreybutler.github.io/node-linux). 23 | 24 | This is a standalone module, originally designed for internal use in [NGN](http://github.com/thinkfirst/NGN). 25 | However; it is capable of providing the same features for Node.JS scripts 26 | independently of NGN. 27 | 28 | For alternative versions, see [node-windows](http://github.com/coreybutler/node-windows) 29 | and [node-mac](http://github.com/coreybutler/node-mac) 30 | 31 | This module makes it possible to daemonize Node.js scripts natively (using systemv init.d scripts). 32 | 33 | To start, install node-linux via: 34 | 35 | npm install node-linux 36 | 37 | node-linux has a utility to run Node.js scripts as Linux daemons. 38 | 39 | To create a service with node-linux, prepare a script like: 40 | 41 | ```js 42 | var Service = require('node-linux').Service; 43 | 44 | // Create a new service object 45 | var svc = new Service({ 46 | name:'Hello World', 47 | description: 'The nodejs.org example web server.', 48 | script: '/path/to/helloworld.js' 49 | }); 50 | 51 | // Listen for the "install" event, which indicates the 52 | // process is available as a service. 53 | svc.on('install',function(){ 54 | svc.start(); 55 | }); 56 | 57 | svc.install(); 58 | ``` 59 | 60 | The code above creates a new `Service` object, providing a pretty name and description. 61 | The `script` attribute identifies the Node.js script that should run as a service. Upon running 62 | this, the script will be available to the system. By default, node-linux produces systemv init 63 | scripts, meaning the services can be managed by typing `service myapp start` or `service myapp stop` 64 | (or `service myapp status` in some cases). 65 | 66 | ![Windows Mac](https://raw.github.com/coreybutler/node-linux/master/docs/assets/images/startstopstatus.jpg) 67 | 68 | The `Service` object emits the following events: 69 | 70 | - _install_ - Fired when the script is installed as a service. 71 | - _alreadyinstalled_ - Fired if the script is already known to be a service. 72 | - _invalidinstallation_ - Fired if an installation is detected but missing required files. 73 | - _uninstall_ - Fired when an uninstallation is complete. 74 | - _start_ - Fired when the new service is started. 75 | - _stop_ - Fired when the service is stopped. 76 | - _error_ - Fired in some instances when an error occurs. 77 | - _doesnotexist_ - Fired when an attempt to start a non-existent service is detected. 78 | 79 | In the example above, the script listens for the `install` event. Since this event 80 | is fired when a service installation is complete, it is safe to start the service. 81 | 82 | Services created by node-linux are like other services running on Linux. 83 | They can be started/stopped using `service myapp start` or `service myapp stop` and 84 | logs are available (default is in /var/log). 85 | 86 | ## Environment Variables 87 | 88 | Sometimes you may want to provide a service with static data, passed in on creation of the service. You can do this by setting environment variables in the service config, as shown below: 89 | 90 | ```js 91 | var svc = new Service({ 92 | name:'Hello World', 93 | description: 'The nodejs.org example web server.', 94 | script: '/path/to/helloworld.js', 95 | env: { 96 | name: "HOME", 97 | value: process.env["USERPROFILE"] // service is now able to access the user who created its' home directory 98 | } 99 | }); 100 | ``` 101 | 102 | You can also supply an array to set multiple environment variables: 103 | 104 | ```js 105 | var svc = new Service({ 106 | name:'Hello World', 107 | description: 'The nodejs.org example web server.', 108 | script: '/path/to/helloworld.js', 109 | env: [{ 110 | name: "HOME", 111 | value: process.env["USERPROFILE"] // service is now able to access the user who created its' home directory 112 | }, 113 | { 114 | name: "TEMP", 115 | value: path.join(process.env["USERPROFILE"],"/temp") // use a temp directory in user's home directory 116 | }] 117 | }); 118 | ``` 119 | 120 | ## Setting run as user/group 121 | 122 | By default your node service will run as root:root. You may not want that. 123 | Just pass the requested user/group values at startup 124 | 125 | ```js 126 | var svc = new Service({ 127 | name:'Hello World', 128 | description: 'The nodejs.org example web server.', 129 | script: '/path/to/helloworld.js', 130 | user: "vagrant", 131 | group: "vagrant" 132 | }); 133 | ``` 134 | 135 | ## Cleaning Up: Uninstall a Service 136 | 137 | Uninstalling a previously created service is syntactically similar to installation. 138 | 139 | ```js 140 | var Service = require('node-linux').Service; 141 | 142 | // Create a new service object 143 | var svc = new Service({ 144 | name:'Hello World', 145 | script: require('path').join(__dirname,'helloworld.js') 146 | }); 147 | 148 | // Listen for the "uninstall" event so we know when it's done. 149 | svc.on('uninstall',function(){ 150 | console.log('Uninstall complete.'); 151 | console.log('The service exists: ',svc.exists()); 152 | }); 153 | 154 | // Uninstall the service. 155 | svc.uninstall(); 156 | ``` 157 | 158 | The uninstall process only removes process-specific files. **It does NOT delete your Node.js script, but it will remove the logs!** 159 | 160 | ## What Makes node-linux Services Unique? 161 | 162 | Lots of things! 163 | 164 | **Long Running Processes & Monitoring:** 165 | 166 | There is no built-in service recovery in most Linux environments, and third party products can be fairly 167 | limited or not easily configured from code. Therefore, node-linux creates a wrapper around the Node.js script. 168 | This wrapper is responsible for restarting a failed service in an intelligent and configurable manner. For example, 169 | if your script crashes due to an unknown error, node-linux will attempt to restart it. By default, 170 | this occurs every second. However; if the script has a fatal flaw that makes it crash repeatedly, 171 | it adds unnecessary overhead to the system. node-linux handles this by increasing the time interval 172 | between restarts and capping the maximum number of restarts. 173 | 174 | **Smarter Restarts That Won't Pummel Your Server:** 175 | 176 | Using the default settings, node-linux adds 25% to the wait interval each time it needs to restart 177 | the script. With the default setting (1 second), the first restart attempt occurs after one second. 178 | The second occurs after 1.25 seconds. The third after 1.56 seconds (1.25 increased by 25%) and so on. 179 | Both the initial wait time and the growth rate are configuration options that can be passed to a new 180 | `Service`. For example: 181 | 182 | ```js 183 | var svc = new Service({ 184 | name:'Hello World', 185 | description: 'The nodejs.org example web server.', 186 | script: '/path/to/helloworld.js'), 187 | wait: 2, 188 | grow: .5 189 | }); 190 | ``` 191 | 192 | In this example, the wait period will start at 2 seconds and increase by 50%. So, the second attempt 193 | would be 3 seconds later while the fourth would be 4.5 seconds later. 194 | 195 | **Don't DOS Yourself!** 196 | 197 | Repetitive recycling could potentially go on forever with a bad script. To handle these situations, node-linux 198 | supports two kinds of caps. Using `maxRetries` will cap the maximum number of restart attempts. By 199 | default, this is unlimited. Setting it to 3 would tell the process to no longer restart a process 200 | after it has failed 3 times. Another option is `maxRestarts`, which caps the number of restarts attempted 201 | within 60 seconds. For example, if this is set to 3 (the default) and the process crashes/restarts repeatedly, 202 | node-linux will cease restart attempts after the 3rd cycle in a 60 second window. Both of these 203 | configuration options can be set, just like `wait` or `grow`. 204 | 205 | Finally, an attribute called `abortOnError` can be set to `true` if you want your script to **not** restart 206 | at all when it exits with an error. 207 | 208 | ## How Services Are Made 209 | 210 | node-linux uses the templates to generate init.d scripts for each Node.js script deployed as a 211 | service. This file is created in `/etc/init.d` by default. Additionally, a log file is 212 | generated in `/var/log/` for general output and error logging. 213 | 214 | _Event Logging_ 215 | 216 | A log source named `myappname.log` provides basic logging for the service. It can be used to see 217 | when the entire service starts/stops. 218 | 219 | By default, any `console.log`, `console.warn`, `console.error` or other output will be made available 220 | in one of these two files. 221 | 222 | # Contributions 223 | 224 | Due to some unforeseen life circumstances, I was not able to add all of the features I'd hoped to add 225 | before releasing this. I'll chip away at them over time, but I would be very interested in community contributions 226 | in the following areas: 227 | 228 | - systemd script generation 229 | - upstart script generation 230 | 231 | I have also added a tag in the issues called `feature request` to keep a running to-do list. 232 | 233 | If you are interested in working on one of these features, please get in touch with me before you start to discuss 234 | the feature. 235 | 236 | # License (MIT) 237 | 238 | Copyright (c) 2013 Corey Butler 239 | 240 | Permission is hereby granted, free of charge, to any person obtaining 241 | a copy of this software and associated documentation files (the 242 | 'Software'), to deal in the Software without restriction, including 243 | without limitation the rights to use, copy, modify, merge, publish, 244 | distribute, sublicense, and/or sell copies of the Software, and to 245 | permit persons to whom the Software is furnished to do so, subject to 246 | the following conditions: 247 | 248 | The above copyright notice and this permission notice shall be 249 | included in all copies or substantial portions of the Software. 250 | 251 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 252 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 253 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 254 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 255 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 256 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 257 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 258 | -------------------------------------------------------------------------------- /docs/assets/css/border-radius.htc: -------------------------------------------------------------------------------- 1 | --Do not remove this if you are using-- 2 | Original Author: Remiz Rahnas 3 | Original Author URL: http://www.htmlremix.com 4 | Published date: 2008/09/24 5 | 6 | Changes by Nick Fetchak: 7 | - IE8 standards mode compatibility 8 | - VML elements now positioned behind original box rather than inside of it - should be less prone to breakage 9 | Published date : 2009/11/18 10 | 11 | 12 | 13 | 143 | 144 | -------------------------------------------------------------------------------- /docs/assets/css/guide.css: -------------------------------------------------------------------------------- 1 | .console { 2 | -moz-border-radius: 20px; 3 | -webkit-border-radius: 20px; 4 | -khtml-border-radius: 20px; 5 | behavior: url(/assets/css/border-radius.htc); 6 | border-radius: 20px; 7 | position: relative; 8 | zoom: 1; 9 | background-color: #000000; 10 | color: #eeeeee; 11 | font-family: Monaco, Consolas, Arial, Helvetica, Sans-Serif; 12 | border: 1px solid #888888; 13 | border-top: 16px solid #888888; 14 | } 15 | -------------------------------------------------------------------------------- /docs/assets/css/main.css: -------------------------------------------------------------------------------- 1 | #header-content { 2 | background: url(../images/tux.png) 0 0 no-repeat !important; 3 | } 4 | #north-region { 5 | background-color: #313131 !important; 6 | background-image: none !important; 7 | } 8 | #content hr { 9 | height: 0; 10 | border: 0px; 11 | border-top: 1px solid #EFEFEF; 12 | margin: 15px 0 15px 0; 13 | } 14 | #content li { 15 | margin: 3px 0 0px 25px; 16 | list-style-type: square !important; 17 | } 18 | #content ol li { 19 | list-style-type:upper-roman; 20 | } 21 | #content h1 { 22 | margin: 0 0 8px 0; 23 | } 24 | -------------------------------------------------------------------------------- /docs/assets/images/finder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreybutler/node-linux/8c54ca888298d1a5ad3c891da7e867996b89dba8/docs/assets/images/finder.png -------------------------------------------------------------------------------- /docs/assets/images/startstopstatus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreybutler/node-linux/8c54ca888298d1a5ad3c891da7e867996b89dba8/docs/assets/images/startstopstatus.jpg -------------------------------------------------------------------------------- /docs/assets/images/tux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreybutler/node-linux/8c54ca888298d1a5ad3c891da7e867996b89dba8/docs/assets/images/tux.png -------------------------------------------------------------------------------- /docs/assets/images/windows8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreybutler/node-linux/8c54ca888298d1a5ad3c891da7e867996b89dba8/docs/assets/images/windows8.png -------------------------------------------------------------------------------- /docs/guides.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "title": "Node Mac", 3 | "items": [{ 4 | "name": "nodeLinuxService", 5 | "title": "Services", 6 | "description": "Create Linux daemons from your node.js scripts." 7 | },{ 8 | "name": "linuxtemplates", 9 | "title": "Custom Init.d Scripts", 10 | "description": "Use your own templates for generating init.d daemons." 11 | },{ 12 | "name": "nodelinuxtroubleshooting", 13 | "title": "Troubleshooting", 14 | "description": "Having problems? Check here for answers." 15 | }] 16 | }] -------------------------------------------------------------------------------- /docs/guides/linuxtemplates/README.md: -------------------------------------------------------------------------------- 1 | # SystemV Templates 2 | 3 | There are many flavors of Linux, and while many of them cn be served using the stock 4 | `initd` template scripts packaged with node-linux, there may be occasions where a 5 | custom template is a better fit for the server environment. This guide discusses how 6 | templates are built and how to create custom templates. 7 | 8 | ## How Node-Linux Creates init.d Scripts 9 | 10 | Node-Linux uses the [mustache](http://mustache.github.io/) template library along with 11 | the [mu](https://github.com/raycmorgan/Mu) module to create initd scripts. Templates are stored 12 | within the `node_modules/node-linux/lib/templates` directory. Debian and RedHat templates ship 13 | with node-linux, which should also work for Ubuntu and CentOS/Fedora/AMI respectively. 14 | 15 | ## Custom Templates 16 | 17 | Custom templates must implement a minimum of two functions to work properly: start and stop. 18 | Node-Linux uses these to start and stop scripts using syntax similar to what would be used on the 19 | command line to start/stop a service: `service myapp start` and `service myapp stop`. 20 | 21 | A custom init.d template is a mustache template, working with a simple 22 | find/replace approach. There are several variables passed to the template when the script is 23 | generated. 24 | 25 | ### Variables 26 | 27 | Use the following variables in your template file. For example, to generate `name` in a 28 | custom template, it should use the standard mustache syntax, i.e. `{{name}}`. 29 | 30 | The following variables are passed to every template. Custom templates can choose to use 31 | whichever ones are necessary. 32 | 33 | - `label` **(REQUIRED)**: The label/service/file name. This is all lowercase with no special characters or spaces. 34 | - `servicesummary`: A short summary of the service. Typically used in header documentation within the script. 35 | - `servicedescription`: A detailed summary of the service. Typically used in header documentation within the script. 36 | - `author`: The author of the script or daemon. Typically used for documentation only. 37 | - `script` **(REQUIRED)**: The Node.js script that should be run as a daemon. 38 | - `description`: A common description of the service. The is usually used instead of the servicedescription. 39 | - `user`: A user account under which the process should run. 40 | - `group`: A user group under which the process should run. 41 | - `pidroot`: The root directory where the PID file is stored. Defaults to '/var/run'. 42 | - `logroot`: The root directory where log files are written. Defaults to '/var/log'. 43 | - `wrappercode` **(REQUIRED)**: The dynamically generated parameters for the wrapper script. This script 44 | is responsible for controlling restarts. 45 | - `env`: Environment variables that should be passed to the running process. 46 | - `created`: The date when the service is created. Typically used for documentation only. 47 | - `execpath` **(REQUIRED)**: The full path & executable name of Node.JS. 48 | 49 | These variables are created by node-linux and passed to the template when it is rendered. 50 | Template authos should use the required variables at minimum. 51 | 52 | ## Using Custom Templates 53 | 54 | Once a custom template has been created, it can be used by specifying the template attribute of 55 | the service configuration. For example: 56 | 57 | var Service = require('./').Service, 58 | svc = new Service({ 59 | name: 'Hello World', 60 | descirption: 'A hello world web server.', 61 | template: '/path/to/custom/template' 62 | }); 63 | 64 | It's probably easiest to copy or study one of the existing templates when creating custom templates. -------------------------------------------------------------------------------- /docs/guides/nodeLinuxService/README.md: -------------------------------------------------------------------------------- 1 | # Mac Services 2 | 3 | To start, install node-mac via: 4 | 5 | npm install node-mac 6 | 7 | node-mac has a utility to run Node.js scripts as Mac daemons. Please note that like all 8 | Mac daemons, creating one requires sudo/root privileges. To create a service with 9 | node-mac, prepare a script like: 10 | 11 | var Service = require('node-mac').Service; 12 | 13 | // Create a new service object 14 | var svc = new Service({ 15 | name:'Hello World', 16 | description: 'The nodejs.org example web server.', 17 | script: 'C:\\path\\to\\helloworld.js') 18 | }); 19 | 20 | // Listen for the "install" event, which indicates the 21 | // process is available as a service. 22 | svc.on('install',function(){ 23 | svc.start(); 24 | }); 25 | 26 | svc.install(); 27 | 28 | The code above creates a new `Service` object, providing a pretty name and description. 29 | The `script` attribute identifies the Node.js script that should run as a service. Upon running 30 | this, the script will be visible from the Windows Services utility. 31 | 32 | ![Windows Mac](https://raw.github.com/coreybutler/node-mac/master/docs/helloworlddaemon.png) 33 | 34 | The `Service` object emits the following events: 35 | 36 | - _install_ - Fired when the script is installed as a service. 37 | - _alreadyinstalled_ - Fired if the script is already known to be a service. 38 | - _invalidinstallation_ - Fired if an installation is detected but missing required files. 39 | - _uninstall_ - Fired when an uninstallation is complete. 40 | - _start_ - Fired when the new service is started. 41 | - _stop_ - Fired when the service is stopped. 42 | - _error_ - Fired in some instances when an error occurs. 43 | 44 | In the example above, the script listens for the `install` event. Since this event 45 | is fired when a service installation is complete, it is safe to start the service. 46 | 47 | Services created by node-mac are similar to most other services running on OSX. 48 | They can be stopped from the Activity Monitor and make logs available in the Console app. 49 | 50 | ## Environment Variables 51 | 52 | Sometimes you may want to provide a service with static data, passed in on creation of the service. You can do this by setting environment variables in the service config, as shown below: 53 | 54 | var svc = new Service({ 55 | name:'Hello World', 56 | description: 'The nodejs.org example web server.', 57 | script: 'C:\\path\\to\\helloworld.js', 58 | env: { 59 | name: "HOME", 60 | value: process.env["USERPROFILE"] // service is now able to access the user who created its' home directory 61 | } 62 | }); 63 | 64 | You can also supply an array to set multiple environment variables: 65 | 66 | var svc = new Service({ 67 | name:'Hello World', 68 | description: 'The nodejs.org example web server.', 69 | script: 'C:\\path\\to\\helloworld.js', 70 | env: [{ 71 | name: "HOME", 72 | value: process.env["USERPROFILE"] // service is now able to access the user who created its' home directory 73 | }, 74 | { 75 | name: "TEMP", 76 | value: path.join(process.env["USERPROFILE"],"/temp") // use a temp directory in user's home directory 77 | }] 78 | }); 79 | 80 | ## Cleaning Up: Uninstall a Service 81 | 82 | Uninstalling a previously created service is syntactically similar to installation. 83 | 84 | var Service = require('node-mac').Service; 85 | 86 | // Create a new service object 87 | var svc = new Service({ 88 | name:'Hello World', 89 | script: require('path').join(__dirname,'helloworld.js') 90 | }); 91 | 92 | // Listen for the "uninstall" event so we know when it's done. 93 | svc.on('uninstall',function(){ 94 | console.log('Uninstall complete.'); 95 | console.log('The service exists: ',svc.exists); 96 | }); 97 | 98 | // Uninstall the service. 99 | svc.uninstall(); 100 | 101 | The uninstall process only removes process-specific files. **It does NOT delete your Node.js script, but it will remove the logs!** 102 | This process also removes the plist file for the service. 103 | 104 | ## What Makes node-mac Services Unique? 105 | 106 | Lots of things! 107 | 108 | **Long Running Processes & Monitoring:** 109 | 110 | The built-in service recovery for OSX services is fairly limited and cannot easily be configured 111 | from code. Therefore, node-mac creates a wrapper around the Node.js script. This wrapper 112 | is responsible for restarting a failed service in an intelligent and configurable manner. For example, 113 | if your script crashes due to an unknown error, node-mac will attempt to restart it. By default, 114 | this occurs every second. However; if the script has a fatal flaw that makes it crash repeatedly, 115 | it adds unnecessary overhead to the system. node-mac handles this by increasing the time interval 116 | between restarts and capping the maximum number of restarts. 117 | 118 | **Smarter Restarts That Won't Pummel Your Server:** 119 | 120 | Using the default settings, node-mac adds 25% to the wait interval each time it needs to restart 121 | the script. With the default setting (1 second), the first restart attempt occurs after one second. 122 | The second occurs after 1.25 seconds. The third after 1.56 seconds (1.25 increased by 25%) and so on. 123 | Both the initial wait time and the growth rate are configuration options that can be passed to a new 124 | `Service`. For example: 125 | 126 | var svc = new Service({ 127 | name:'Hello World', 128 | description: 'The nodejs.org example web server.', 129 | script: 'C:\\path\\to\\helloworld.js'), 130 | wait: 2, 131 | grow: .5 132 | }); 133 | 134 | In this example, the wait period will start at 2 seconds and increase by 50%. So, the second attempt 135 | would be 3 seconds later while the fourth would be 4.5 seconds later. 136 | 137 | **Don't DOS Yourself!** 138 | 139 | Repetitive recycling could potentially go on forever with a bad script. To handle these situations, node-mac 140 | supports two kinds of caps. Using `maxRetries` will cap the maximum number of restart attempts. By 141 | default, this is unlimited. Setting it to 3 would tell the process to no longer restart a process 142 | after it has failed 3 times. Another option is `maxRestarts`, which caps the number of restarts attempted 143 | within 60 seconds. For example, if this is set to 3 (the default) and the process crashes/restarts repeatedly, 144 | node-mac will cease restart attempts after the 3rd cycle in a 60 second window. Both of these 145 | configuration options can be set, just like `wait` or `grow`. 146 | 147 | Finally, an attribute called `abortOnError` can be set to `true` if you want your script to **not** restart 148 | at all when it exits with an error. 149 | 150 | ## How Services Are Made 151 | 152 | node-mac uses the `launchd` utility to create a unique process 153 | for each Node.js script deployed as a service. A plist file is created in `/Library/LaunchDaemons` 154 | by default. Additionally, two log files are generated in `/Library/Logs/` for general output 155 | and error logging. 156 | 157 | _Event Logging_ 158 | 159 | ![Mac log](https://raw.github.com/coreybutler/node-mac/master/docs/helloworldlog.png) 160 | 161 | Services created with node-mac have two event logs that can be viewed through the Console app. 162 | A log source named `myappname.log` provides basic logging for the service. It can be used to see 163 | when the entire service starts/stops. A second log, named `myappname_error.log` stores error output. 164 | 165 | By default, any `console.log`, `console.warn`, `console.error` or other output will be made available 166 | in one of these two files. 167 | 168 | # License (MIT) 169 | 170 | Copyright (c) 2013 Corey Butler 171 | 172 | Permission is hereby granted, free of charge, to any person obtaining 173 | a copy of this software and associated documentation files (the 174 | 'Software'), to deal in the Software without restriction, including 175 | without limitation the rights to use, copy, modify, merge, publish, 176 | distribute, sublicense, and/or sell copies of the Software, and to 177 | permit persons to whom the Software is furnished to do so, subject to 178 | the following conditions: 179 | 180 | The above copyright notice and this permission notice shall be 181 | included in all copies or substantial portions of the Software. 182 | 183 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 184 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 185 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 186 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 187 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 188 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 189 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 190 | -------------------------------------------------------------------------------- /docs/guides/nodelinuxtroubleshooting/README.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | If you're having problems with your service, please remember to check the logs in 4 | `/Library/Logs/`, also visible through the Console app on your Mac. 5 | 6 | Services on OSX are pretty straightforward. The only file (other than logs) generated 7 | is a standard `plist` file, found in `/Library/LaunchDaemons`. If your service won't startup, 8 | this file can be checked - but it is very unlikely to be the root of the problem. 9 | 10 | Most issues stem from changing the `wrapper.js` file (not a recommended practice). 11 | 12 | ## Other Issue? 13 | 14 | If you are having other issues, please post them in the [bug tracker](https://github.com/coreybutler/node-mac/issues). 15 | -------------------------------------------------------------------------------- /example/helloworld.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var fs = require('fs'); 3 | 4 | var server = http.createServer(function (req, res) { 5 | res.writeHead(200, {'Content-Type': 'text/plain'}); 6 | res.end(JSON.stringify(process.env)); 7 | }); 8 | 9 | server.listen(3000); 10 | console.log('Server running at http://127.0.0.1:3000/'); 11 | //fs.appendFileSync(log, 'Server running at http://127.0.0.1:3000/\n'); 12 | 13 | // Force the process to close after 15 seconds 14 | setTimeout(function(){ 15 | console.log('Timer hit limit'); 16 | //console.log(); 17 | //throw 'A test Error' 18 | //process.exit(); 19 | },25000); 20 | -------------------------------------------------------------------------------- /example/install.js: -------------------------------------------------------------------------------- 1 | var Service = require('../').Service; 2 | 3 | // Create a new service object 4 | var svc = new Service({ 5 | name:'Hello World', 6 | description: 'The nodejs.org example web server.', 7 | script: require('path').join(__dirname,'helloworld.js'), 8 | env:{ 9 | name: "NODE_ENV", 10 | value: "production" 11 | }, 12 | user:"vagrant", 13 | group:"vagrant" 14 | }); 15 | 16 | // Listen for the "install" event, which indicates the 17 | // process is available as a service. 18 | svc.on('install',function(){ 19 | console.log('\nInstallation Complete\n---------------------'); 20 | //svc.start(); 21 | }); 22 | 23 | // Just in case this file is run twice. 24 | svc.on('alreadyinstalled',function(){ 25 | console.log('This service is already installed.'); 26 | console.log('Attempting to start it.'); 27 | svc.start(); 28 | }); 29 | 30 | // Listen for the "start" event and let us know when the 31 | // process has actually started working. 32 | svc.on('start',function(){ 33 | console.log(svc.name+' started!\nVisit http://127.0.0.1:3000 to see it in action.\n'); 34 | }); 35 | 36 | // Install the script as a service. 37 | svc.install(); -------------------------------------------------------------------------------- /example/uninstall.js: -------------------------------------------------------------------------------- 1 | var Service = require('../').Service; 2 | 3 | // Create a new service object 4 | var svc = new Service({ 5 | name:'Hello World', 6 | script: require('path').join(__dirname,'helloworld.js') 7 | }); 8 | 9 | // Listen for the "uninstall" event so we know when it's done. 10 | svc.on('uninstall',function(){ 11 | console.log('Uninstall complete.'); 12 | console.log('The service exists: ',svc.exists()); 13 | }); 14 | 15 | svc.on('error',function(err){ 16 | console.log('ERROR:',err); 17 | }) 18 | 19 | // Uninstall the service. 20 | svc.uninstall(); -------------------------------------------------------------------------------- /lib/daemon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class nodelinux.Service 3 | * Manage node.js scripts as native Linux daemons. 4 | * var Service = require('node-linux').Service; 5 | * 6 | * // Create a new service object 7 | * var svc = new Service({ 8 | * name:'Hello World', 9 | * description: 'The nodejs.org example web server.', 10 | * script: '/path/to/helloworld.js' 11 | * }); 12 | * 13 | * // Listen for the "install" event, which indicates the 14 | * // process is available as a service. 15 | * svc.on('install',function(){ 16 | * svc.start(); 17 | * }); 18 | * 19 | * svc.install(); 20 | * @author Corey Butler 21 | * @singleton 22 | */ 23 | var fs = require('fs'), 24 | p = require('path'), 25 | exec = require('child_process').exec, 26 | wrapper = p.resolve(p.join(__dirname,'./wrapper.js')); 27 | 28 | var daemon = function(config) { 29 | 30 | Object.defineProperties(this,{ 31 | 32 | /** 33 | * @cfg {String} [mode] 34 | * The type of daemon to create. Defaults to the system's default daemonization utility. 35 | * Alternatively, `systemd` can be used. 36 | */ 37 | mode: { 38 | enumerable: true, 39 | writable: true, 40 | configurable: false, 41 | value: config.mode 42 | }, 43 | 44 | /** 45 | * @cfg {String} [defaultMode] 46 | * Scans the system in order to find out the default mode. 47 | * May return either 'systemv' or 'systemd'. 48 | */ 49 | defaultMode: { 50 | enumerable: false, 51 | get: function(){ 52 | 53 | // http://unix.stackexchange.com/questions/18209/detect-init-system-using-the-shell 54 | var hasSystemD = function() { 55 | return fs.existsSync("/usr/bin/systemctl") || fs.existsSync("/bin/systemctl"); 56 | } 57 | var hasUpstart = function() { 58 | return false; // TODO: Upstart support is not implemented, for now 59 | //return fs.existsSync("/sbin/initctl"); 60 | } 61 | var hasSystemV = function() { 62 | return fs.existsSync("/etc/init.d"); 63 | } 64 | 65 | if (hasSystemD()) { 66 | return 'systemd'; 67 | } else if (hasUpstart()) { 68 | return 'upstart'; 69 | } else if (hasSystemV()) { 70 | return 'systemv'; 71 | } else { 72 | throw new Error('Could not detect init system'); 73 | } 74 | 75 | } 76 | }, 77 | 78 | /** 79 | * @cfg {String} [user=root] 80 | * The user to run the service as. Defaults to 'root' 81 | */ 82 | user: { 83 | enumerable: true, 84 | writable: false, 85 | configurable: false, 86 | value: config.user || 'root' 87 | }, 88 | 89 | /** 90 | * @cfg {String} [group=root] 91 | * The group to run the service as. Defaults to 'root' 92 | */ 93 | group: { 94 | enumerable: true, 95 | writable: false, 96 | configurable: false, 97 | value: config.group || 'root' 98 | }, 99 | 100 | system: { 101 | enumerable: false, 102 | get: function(){ 103 | switch(this.mode){ 104 | case 'systemd': 105 | return require('./systemd'); 106 | default: 107 | return require('./systemv'); 108 | } 109 | } 110 | }, 111 | 112 | /** 113 | * @cfg {String} [author='Unknown'] 114 | * An optional descriptive header added to the top of the daemon file. Credits 115 | * the author of the script. 116 | */ 117 | author: { 118 | enumerable: true, 119 | writable: true, 120 | configurable: false, 121 | value: config.author || 'Unknown' 122 | }, 123 | 124 | /** 125 | * @cfg {String} [piddir=/var/run] 126 | * The root directory where the PID file is stored. 127 | */ 128 | piddir: { 129 | enumerable: true, 130 | writable: true, 131 | configurable: false, 132 | value: config.piddir || '/var/run' 133 | }, 134 | 135 | /** 136 | * @cfg {String} name 137 | * The descriptive name of the process, i.e. `My Process`. 138 | */ 139 | _name: { 140 | enumerable: false, 141 | writable: true, 142 | configurable: false, 143 | value: config.name || null 144 | }, 145 | 146 | /** 147 | * @property {String} name 148 | * The name of the process. 149 | */ 150 | name: { 151 | enumerable: true, 152 | get: function(){return this._name;}, 153 | set: function(value){this._name = value;} 154 | }, 155 | 156 | label: { 157 | enumerable: false, 158 | get: function(){ 159 | return this.name.replace(/[^a-zA-Z0-9\_]+/gi,'').toLowerCase() 160 | } 161 | }, 162 | 163 | outlog: { 164 | enumerable: false, 165 | get: function(){ 166 | return p.join(this.logpath,this.label+'.log'); 167 | } 168 | }, 169 | 170 | errlog: { 171 | enumerable: false, 172 | get: function(){ 173 | return p.join(this.logpath,this.label+'-error.log'); 174 | } 175 | }, 176 | 177 | /** 178 | * @property {Boolean} exists 179 | * Indicates that the service exists. 180 | * @readonly 181 | */ 182 | exists: { 183 | enumerable: true, 184 | value: function(){ 185 | return this.generator.exists; 186 | } 187 | }, 188 | 189 | /** 190 | * @cfg {String} [description=''] 191 | * Description of the service. 192 | */ 193 | description: { 194 | enumerable: true, 195 | writable: false, 196 | configurable: false, 197 | value: config.description || '' 198 | }, 199 | 200 | /** 201 | * @cfg {String} [cwd] 202 | * The absolute path of the current working directory. Defaults to the base directory of #script. 203 | */ 204 | cwd: { 205 | enumerable: false, 206 | writable: true, 207 | configurable: false, 208 | value: config.cwd || p.dirname(config.script) 209 | }, 210 | 211 | /** 212 | * @cfg {Array|Object} [env] 213 | * An optional array or object used to pass environment variables to the node.js script. 214 | * You can do this by setting environment variables in the service config, as shown below: 215 | * 216 | * var svc = new Service({ 217 | * name:'Hello World', 218 | * description: 'The nodejs.org example web server.', 219 | * script: '/path/to/helloworld.js', 220 | * env: { 221 | * name: "NODE_ENV", 222 | * value: "production" 223 | * } 224 | * }); 225 | * 226 | * You can also supply an array to set multiple environment variables: 227 | * 228 | * var svc = new Service({ 229 | * name:'Hello World', 230 | * description: 'The nodejs.org example web server.', 231 | * script: '/path/to/helloworld.js', 232 | * env: [{ 233 | * name: "HOME", 234 | * value: process.env["USERPROFILE"] // Access the user home directory 235 | * },{ 236 | * name: "NODE_ENV", 237 | * value: "production" 238 | * }] 239 | * }); 240 | */ 241 | _ev: { 242 | enumerable: false, 243 | writable: true, 244 | configurable: false, 245 | value: config.env || [] 246 | }, 247 | 248 | EnvironmentVariables: { 249 | enumerable: false, 250 | get: function(){ 251 | var ev = [], tmp = {}; 252 | if (Object.prototype.toString.call(this._ev) === '[object Array]'){ 253 | this._ev.forEach(function(item){ 254 | tmp = {}; 255 | tmp[item.name] = item.value; 256 | ev.push(tmp); 257 | }); 258 | } else { 259 | tmp[this._ev.name] = this._ev.value; 260 | ev.push(tmp); 261 | } 262 | return ev; 263 | } 264 | }, 265 | 266 | /** 267 | * @cfg {String} script required 268 | * The absolute path of the script to launch as a service. 269 | */ 270 | script: { 271 | enumerable: true, 272 | writable: true, 273 | configurable: false, 274 | value: config.script !== undefined ? require('path').resolve(config.script) : null 275 | }, 276 | 277 | /** 278 | * @cfg {String} [logpath=/Library/Logs/node-scripts] 279 | * The root directory where the log will be stored. 280 | */ 281 | logpath: { 282 | enumerable: true, 283 | writable: true, 284 | configurable: false, 285 | value: config.logpath || '/var/log' 286 | }, 287 | 288 | /** 289 | * @cfg {Number} [maxRetries=null] 290 | * The maximum number of restart attempts to make before the service is considered non-responsive/faulty. 291 | * Ignored by default. 292 | */ 293 | maxRetries: { 294 | enumerable: true, 295 | writable: false, 296 | configurable: false, 297 | value: config.maxRetries || null 298 | }, 299 | 300 | /** 301 | * @cfg {Number} [maxRestarts=3] 302 | * The maximum number of restarts within a 60 second period before haulting the process. 303 | * This cannot be _disabled_, but it can be rendered ineffective by setting a value of `0`. 304 | */ 305 | maxRestarts: { 306 | enumerable: true, 307 | writable: false, 308 | configurable: false, 309 | value: config.maxRestarts || 3 310 | }, 311 | 312 | /** 313 | * @cfg {Boolean} [abortOnError=false] 314 | * Setting this to `true` will force the process to exit if it encounters an error that stops the node.js script from running. 315 | * This does not mean the process will stop if the script throws an error. It will only abort if the 316 | * script throws an error causing the process to exit (i.e. `process.exit(1)`). 317 | */ 318 | abortOnError: { 319 | enumerable: true, 320 | writable: false, 321 | configurable: false, 322 | value: config.abortOnError instanceof Boolean ? config.abortOnError : false 323 | }, 324 | 325 | /** 326 | * @cfg {Number} [wait=1] 327 | * The initial number of seconds to wait before attempting a restart (after the script stops). 328 | */ 329 | wait: { 330 | enumerable: true, 331 | writable: false, 332 | configurable: false, 333 | value: config.wait || 1 334 | }, 335 | 336 | /** 337 | * @cfg {Number} [grow=.25] 338 | * A number between 0-1 representing the percentage growth rate for the #wait interval. 339 | * Setting this to anything other than `0` allows the process to increase it's wait period 340 | * on every restart attempt. If a process dies fatally, this will prevent the server from 341 | * restarting the process too rapidly (and too strenuously). 342 | */ 343 | grow: { 344 | enumerable: true, 345 | writable: false, 346 | configurable: false, 347 | value: config.grow || .25 348 | }, 349 | 350 | _suspendedEvents: { 351 | enumerable: false, 352 | writable: true, 353 | configurable: false, 354 | value: [] 355 | }, 356 | 357 | /** 358 | * @method isSuspended 359 | * Indicates the specified event is suspended. 360 | */ 361 | isSuspended: { 362 | enumerable: true, 363 | writable: false, 364 | configurable: false, 365 | value: function(eventname){ 366 | return this._suspendedEvents.indexOf(eventname) >= 0; 367 | } 368 | }, 369 | 370 | /** 371 | * @method suspendEvent 372 | * Stop firing the specified event. 373 | * @param {String} eventname 374 | * The event. 375 | */ 376 | suspendEvent: { 377 | enumerable: true, 378 | writable: false, 379 | configurable: false, 380 | value: function(eventname){ 381 | if (!this.isSuspended(eventname)){ 382 | this._suspendedEvents.push(eventname); 383 | } 384 | } 385 | }, 386 | 387 | /** 388 | * @method resumeEvent 389 | * Resume firing the specified event. 390 | * @param {String} eventname 391 | * The event. 392 | */ 393 | resumeEvent: { 394 | enumerable: true, 395 | writable: false, 396 | configurable: false, 397 | value: function(eventname){ 398 | if (this.isSuspended(eventname)){ 399 | this._suspendedEvents.splice(this._suspendedEvents.indexOf(eventname),1); 400 | } 401 | } 402 | }, 403 | 404 | _gen: { 405 | enumerable: false, 406 | writable: true, 407 | configurable: false, 408 | value: null 409 | }, 410 | 411 | generator: { 412 | enumerable: false, 413 | get: function(){ 414 | return this._gen; 415 | }, 416 | set: function(value) { 417 | var me = this; 418 | this._gen = value; 419 | 420 | // Handle generator events & bubble accordingly 421 | 422 | /** 423 | * @event install 424 | * Fired when the installation completes. 425 | */ 426 | this._gen.on('install',function(){ 427 | !me.isSuspended('install') && me.emit('install'); 428 | }); 429 | 430 | /** 431 | * @event uninstall 432 | * Fired when the uninstallation/removal completes. 433 | */ 434 | this._gen.on('uninstall',function(){ 435 | !me.isSuspended('uninstall') && me.emit('uninstall'); 436 | }); 437 | 438 | /** 439 | * @event enable 440 | * Fired when the enabling completes. 441 | */ 442 | this._gen.on('enable',function(){ 443 | !me.isSuspended('enable') && me.emit('enable'); 444 | }); 445 | 446 | /** 447 | * @event disable 448 | * Fired when the disabling completes. 449 | */ 450 | this._gen.on('disable',function(){ 451 | !me.isSuspended('disable') && me.emit('disable'); 452 | }); 453 | 454 | /** 455 | * @event alreadyinstalled 456 | * Fired when a duplicate #install is attempted. 457 | */ 458 | this._gen.on('alreadyinstalled',function(){ 459 | !me.isSuspended('alreadyinstalled') && me.emit('alreadyinstalled'); 460 | }); 461 | 462 | /** 463 | * @event invalidinstallation 464 | * Fired when an invalid installation is detected. 465 | */ 466 | this._gen.on('invalidinstallation',function(){ 467 | !me.isSuspended('invalidinstallation') && me.emit('invalidinstallation'); 468 | }); 469 | 470 | /** 471 | * @event start 472 | * Fired when the #start method finishes. 473 | */ 474 | this._gen.on('start',function(){ 475 | !me.isSuspended('start') && me.emit('start'); 476 | }); 477 | 478 | /** 479 | * @event stop 480 | * Fired when the #stop method finishes. 481 | */ 482 | this._gen.on('stop',function(){ 483 | !me.isSuspended('stop') && me.emit('stop'); 484 | }); 485 | 486 | /** 487 | * @event error 488 | * Fired when an error occurs. The error is passed as a callback to the listener. 489 | */ 490 | this._gen.on('error',function(err){ 491 | !me.isSuspended('error') && me.emit('error',err); 492 | }); 493 | 494 | /** 495 | * @event doesnotexist 496 | * Fired when an attempt to uninstall the service fails because it does not exist. 497 | */ 498 | this._gen.on('doesnotexist',function(err){ 499 | !me.isSuspended('doesnotexist') && me.emit('doesnotexist'); 500 | }); 501 | } 502 | }, 503 | 504 | /** 505 | * @method install 506 | * Install the script as a background process/daemon. 507 | * @param {Function} [callback] 508 | */ 509 | install: { 510 | enumerable: true, 511 | writable: true, 512 | configurable: false, 513 | value: function(callback){ 514 | 515 | // Generate the content 516 | this.generator.createProcess(callback||function(){}); 517 | } 518 | }, 519 | 520 | /** 521 | * @method uninstall 522 | * Uninstall an existing background process/daemon. 523 | * @param {Function} [callback] 524 | * Executed when the process is uninstalled. 525 | */ 526 | uninstall: { 527 | enumerable: true, 528 | writable: true, 529 | configurable: false, 530 | value: function(callback){ 531 | 532 | var me = this; 533 | this.suspendEvent('stop'); 534 | this.stop(function(){ 535 | me.resumeEvent('stop'); 536 | me.generator.removeProcess(function(success){ 537 | callback && callback(); 538 | }); 539 | }); 540 | } 541 | }, 542 | 543 | /** 544 | * @method enable 545 | * Enable the script as a background process/daemon. 546 | * @param {Function} [callback] 547 | */ 548 | enable: { 549 | enumerable: true, 550 | writable: true, 551 | configurable: false, 552 | value: function(callback){ 553 | 554 | // Generate the content 555 | this.generator.enable(callback||function(){}); 556 | } 557 | }, 558 | 559 | /** 560 | * @method disable 561 | * Enable the script as a background process/daemon. 562 | * @param {Function} [callback] 563 | */ 564 | disable: { 565 | enumerable: true, 566 | writable: true, 567 | configurable: false, 568 | value: function(callback){ 569 | 570 | // Generate the content 571 | this.generator.disable(callback||function(){}); 572 | } 573 | }, 574 | 575 | /** 576 | * @method start 577 | * Start and/or create a daemon. 578 | * @param {Function} [callback] 579 | */ 580 | start:{ 581 | enumerable: true, 582 | writable: false, 583 | configurable: false, 584 | value: function(callback){ 585 | this.generator.start(callback); 586 | } 587 | }, 588 | 589 | /** 590 | * @method stop 591 | * Stop the process if it is currently running. 592 | * @param {Function} [callback] 593 | */ 594 | stop: { 595 | enumerable: true, 596 | writable: false, 597 | configurable: false, 598 | value: function(callback){ 599 | this.generator.stop(callback); 600 | } 601 | }, 602 | 603 | /** 604 | * @method restart 605 | * @param {Function} [callback] 606 | */ 607 | restart: { 608 | enumerable: true, 609 | writable: true, 610 | configurable: false, 611 | value: function(callback){ 612 | var me = this; 613 | this.stop(function(){ 614 | me.start(callback); 615 | }); 616 | } 617 | } 618 | 619 | }); 620 | 621 | // Do not allow invalid daemonization type 622 | if (!this.mode) { 623 | this.mode = this.defaultMode; 624 | console.log("Using default mode:", this.mode); 625 | } else { 626 | console.log("Using mode:", this.mode); 627 | } 628 | if (['systemv','systemd'].indexOf(this.mode) < 0){ 629 | console.warn("Invalid mode %s, using systemv instead", this.mode); 630 | this.mode = 'systemv'; 631 | } 632 | 633 | // Require a script tag 634 | if (!this.script){ 635 | throw new Error('Script was not provided as a configuration attribute.'); 636 | } 637 | 638 | // Generate wrapper code arguments 639 | var args = [ 640 | '-f','"'+this.script.trim()+'"', 641 | '-l','"'+this.outlog.trim()+'"', 642 | '-e','"'+this.errlog.trim()+'"', 643 | '-t','"'+this.label.trim()+'"', 644 | '-g',this.grow.toString(), 645 | '-w',this.wait.toString(), 646 | '-r',this.maxRestarts.toString(), 647 | '-a',(this.abortOnError===true?'y':'n') 648 | ]; 649 | 650 | if (this.maxRetries!==null){ 651 | args.push('-m'); 652 | args.push(this.maxRetries.toString()); 653 | } 654 | 655 | // Add environment variables 656 | for (var i=0;i= 0; 13 | })[0]; 14 | 15 | switch(_os){ 16 | // Use RedHat for CentOS 17 | case 'centos': 18 | case 'fedora': 19 | case 'amzn': 20 | case 'redhat': 21 | _os = 'redhat'; 22 | break; 23 | 24 | // Use debian for Ubuntu & default 25 | case 'ubuntu': 26 | case 'debian': 27 | default: 28 | _os = 'debian'; 29 | break; 30 | } 31 | return _os; 32 | }; 33 | 34 | /** 35 | * @class nodelinux.systemv 36 | * A class used to create systemv init scripts to run a Node.js script as a background daemon/service. 37 | * @param {Object} config 38 | */ 39 | var init = function(config){ 40 | 41 | config = config || {}; 42 | 43 | Object.defineProperties(this,{ 44 | 45 | /** 46 | * @property {String} templateRoot 47 | * The root directory where initd templates are stored. 48 | */ 49 | templateRoot: { 50 | enumerable: true, 51 | writable: false, 52 | configurable: false, 53 | value: p.join(__dirname,'templates','systemv') 54 | }, 55 | 56 | tpl: { 57 | enumerable: false, 58 | writable: true, 59 | configurable: false, 60 | value: null 61 | }, 62 | 63 | _label: { 64 | enumerable: false, 65 | writable: true, 66 | configurable: false, 67 | value: null 68 | }, 69 | 70 | 71 | _configFilePath: { 72 | enumerable: false, 73 | writable: true, 74 | configurable: false, 75 | value: function() { return '/etc/init.d/'+this.label; } 76 | }, 77 | 78 | /** 79 | * @property {String} label 80 | * The label used for the daemon (file name). 81 | */ 82 | label: { 83 | enumerable: true, 84 | get: function(){ 85 | return this._label; 86 | } 87 | }, 88 | 89 | exists: { 90 | enumerable: false, 91 | get: function(){ 92 | return fs.existsSync(this._configFilePath()); 93 | } 94 | }, 95 | 96 | /** 97 | * @method generate 98 | * Generate a systemv init script for the current operating system. 99 | * @param {Function} callback 100 | * The callback is fired after the script is generated. 101 | * @param {Object} callback.script 102 | * The content of the initd script file. 103 | */ 104 | generate: { 105 | enumerable: true, 106 | writable: false, 107 | configurable: false, 108 | value: function(callback){ 109 | callback = callback || function(){}; 110 | 111 | var me = this; 112 | exec('cat /proc/version',function(error, stdout, stderr){ 113 | stdout = stdout.toLowerCase(); 114 | 115 | var _os = getLinuxFlavor(stdout); 116 | 117 | /** 118 | * @cfg {String} name required 119 | * The title of the process 120 | */ 121 | /** 122 | * @cfg {String} description 123 | * A description of what the service does. 124 | */ 125 | /** 126 | * @cfg {String} [author='Unknown'] 127 | * The author of the process. 128 | */ 129 | /** 130 | * @cfg {String} [user='root'] 131 | * The user account under which the process should run. 132 | */ 133 | /** 134 | * @cfg {String} [group='root'] 135 | * The user group under which the process should run. 136 | */ 137 | /** 138 | * @cfg {String} [pidroot='/var/run'] 139 | * The root directory where the PID file will be created (if applicable to the OS environment). 140 | */ 141 | /** 142 | * @cfg {String} [logroot='/var/log'] 143 | * The root directory where the log file will be created. 144 | */ 145 | /** 146 | * @cfg {Object} [env] 147 | * A key/value object containing environment variables that should be passed to the process. 148 | */ 149 | /** 150 | * @cfg {String} [template] 151 | * Use this template with the #generate method to create custom output for the initd script. 152 | * This should be an absolute filepath. 153 | */ 154 | opt = { 155 | label: me.label, 156 | servicesummary: config.name, 157 | servicedescription: config.description || config.name, 158 | author: config.author || 'Unknown', 159 | script: p.join(__dirname,'wrapper.js'), 160 | nodescript: config.script || '', 161 | wrappercode: (config.wrappercode || ''), 162 | description: config.description, 163 | user: config.user || 'root', 164 | group: config.group || 'root', 165 | pidroot: config.piddir || '/var/run', 166 | logroot: config.logpath || '/var/log', 167 | env: '', 168 | created: new Date(), 169 | execpath: process.execPath, 170 | }; 171 | 172 | // escape the double quotes in the wrappercode args here if we are on a redhat system 173 | // because we will put them inside an su -c "expandedcommand" 174 | // used by redhat systemv script to run service as specific user 175 | // DON'T escape the quotes on debian though, since its service script is implemented differently 176 | if (_os === "redhat") 177 | { 178 | // see http://stackoverflow.com/a/22837870 179 | opt.wrappercode = JSON.stringify(opt.wrappercode).slice(1, -1); 180 | } 181 | 182 | var _path = config.template == undefined ? p.join(me.templateRoot,_os) : p.resolve(config.template); 183 | mu.compile(_path,function(err,tpl){ 184 | var stream = mu.render(tpl,opt), 185 | chunk = ""; 186 | stream.on('data',function(data){ 187 | chunk += data; 188 | }); 189 | stream.on('end',function(){ 190 | callback(chunk); 191 | }); 192 | }); 193 | }); 194 | } 195 | }, 196 | 197 | /** 198 | * @event install 199 | * Fired when the #install completes. 200 | */ 201 | /** 202 | * @event alreadyinstalled 203 | * Fired when an #install is attempted but the process already exists. 204 | */ 205 | 206 | /** 207 | * @method createProcess 208 | * Generate the physical daemon/process file. 209 | * @param {Function} [callback] 210 | * An optional callback fired when the process file has been completed. 211 | * This constitutes an "installation" 212 | */ 213 | createProcess: { 214 | enumerable: true, 215 | writable: false, 216 | configurable: false, 217 | value: function(callback){ 218 | 219 | var filepath = p.join(this._configFilePath()), me = this; 220 | console.log("Installing service on", filepath); 221 | fs.exists(filepath,function(exists){ 222 | if(!exists){ 223 | me.generate(function(script){ 224 | fs.writeFile(filepath,script,function(err){ 225 | if (err) throw err; 226 | fs.chmod(filepath,'755',function(_err){ 227 | if (_err) throw _err; 228 | me.emit('install'); 229 | }) 230 | }); 231 | }); 232 | } else { 233 | me.emit('alreadyinstalled'); 234 | } 235 | }); 236 | } 237 | }, 238 | 239 | /** 240 | * @method removeProcess 241 | * Remove the process files, including the main init.d script and log files. 242 | * @param {Function} [callback] 243 | * An optional callback fired when the files have been removed. 244 | */ 245 | removeProcess: { 246 | enumerable: true, 247 | writable: false, 248 | configurable: false, 249 | value: function(callback){ 250 | 251 | if (!fs.existsSync(this._configFilePath())){ 252 | this.emit('doesnotexist'); 253 | return; 254 | } 255 | var me = this; 256 | 257 | // Remove the main process file first 258 | fs.unlink(this._configFilePath(),function(){ 259 | var lr = require('path').join(me.logroot || '/var/log',this.label+'.log'), 260 | er = require('path').join(me.logroot || '/var/log',this.label+'-error.log'), 261 | pr = require('path').join(me.pidroot || '/var/run',this.label+'.pid'); 262 | 263 | // Remove the PID 264 | fs.exists(pr,function(exists){ 265 | exists && fs.unlink(pr); 266 | }); 267 | 268 | // Remove any logs if they exist 269 | fs.exists(lr,function(exists){ 270 | if (exists){ 271 | fs.unlinkSync(lr); 272 | } 273 | fs.exists(er,function(exists){ 274 | if (exists){ 275 | fs.unlinkSync(er); 276 | } 277 | me.emit('uninstall'); 278 | callback && callback(); 279 | }); 280 | }); 281 | }); 282 | } 283 | }, 284 | 285 | /** 286 | * @method start 287 | * Start the process. 288 | */ 289 | start: { 290 | enumerable: true, 291 | writable: true, 292 | configurable: false, 293 | value: function(callback){ 294 | if (!this.exists){ 295 | this.emit('doesnotexist'); 296 | callback && callback(); 297 | return; 298 | } 299 | var me = this; 300 | var cmd = this._configFilePath()+' start'; 301 | console.log('Running %s...', cmd); 302 | exec(cmd,function(err){ 303 | if (err) throw err; 304 | /** 305 | * @event start 306 | * Fired when the #start method completes. 307 | */ 308 | me.emit('start'); 309 | callback && callback(); 310 | }); 311 | } 312 | }, 313 | 314 | /** 315 | * @method stop 316 | * Stop the process. 317 | */ 318 | stop: { 319 | enumerable: true, 320 | writable: true, 321 | configurable: false, 322 | value: function(callback){ 323 | if (!this.exists){ 324 | this.emit('doesnotexist'); 325 | callback && callback(); 326 | return; 327 | } 328 | var me = this; 329 | var cmd = this._configFilePath()+' stop'; 330 | exec(cmd,function(err){ 331 | if (!err) { 332 | /** 333 | * @event stop 334 | * Fired when the #stop method completes. 335 | */ 336 | me.emit('stop'); 337 | callback && callback(); 338 | } else { 339 | me.emit('error',err); 340 | } 341 | }); 342 | } 343 | }, 344 | 345 | /** 346 | * @method enable 347 | * Stop the process. 348 | */ 349 | enable: { 350 | enumerable: true, 351 | writable: true, 352 | configurable: false, 353 | value: function(callback){ 354 | if (!this.exists){ 355 | this.emit('doesnotexist'); 356 | callback && callback(); 357 | return; 358 | } 359 | var me = this; 360 | 361 | exec('cat /proc/version',function(error, stdout, stderr){ 362 | stdout = stdout.toLowerCase(); 363 | 364 | var _os = getLinuxFlavor(stdout); 365 | var cmd; 366 | if (_os === 'debian') { 367 | cmd = '/usr/sbin/update-rc.d '+me.label+' defaults'; 368 | } 369 | else { 370 | cmd = '/sbin/chkconfig '+me.label+' on'; 371 | } 372 | var me2 = me; 373 | exec(cmd,function(err){ 374 | if (!err) { 375 | /** 376 | * @event enable 377 | * Fired when the #enable method completes. 378 | */ 379 | me2.emit('enable'); 380 | callback && callback(); 381 | } else { 382 | me2.emit('error',err); 383 | } 384 | }); 385 | }); 386 | } 387 | }, 388 | 389 | /** 390 | * @method disable 391 | * Stop the process. 392 | */ 393 | disable: { 394 | enumerable: true, 395 | writable: true, 396 | configurable: false, 397 | value: function(callback){ 398 | if (!this.exists){ 399 | this.emit('doesnotexist'); 400 | callback && callback(); 401 | return; 402 | } 403 | var me = this; 404 | exec('cat /proc/version',function(error, stdout, stderr){ 405 | stdout = stdout.toLowerCase(); 406 | 407 | var _os = getLinuxFlavor(stdout); 408 | var cmd; 409 | if (_os === 'debian') { 410 | cmd = '/usr/sbin/update-rc.d '+this.label+' remove'; 411 | } 412 | else { 413 | cmd = '/sbin/chkconfig '+this.label+' off'; 414 | } 415 | var me2 = me; 416 | exec(cmd,function(err){ 417 | if (!err) { 418 | /** 419 | * @event enable 420 | * Fired when the #enable method completes. 421 | */ 422 | me2.emit('disable'); 423 | callback && callback(); 424 | } else { 425 | me2.emit('error',err); 426 | } 427 | }); 428 | }); 429 | } 430 | } 431 | 432 | }); 433 | 434 | this._label = (config.name||'').replace(/[^a-zA-Z0-9\-]/,'').toLowerCase(); 435 | 436 | }; 437 | 438 | var util = require('util'), 439 | EventEmitter = require('events').EventEmitter; 440 | 441 | // Inherit Events 442 | util.inherits(init,EventEmitter); 443 | 444 | module.exports = init; 445 | -------------------------------------------------------------------------------- /lib/templates/systemd/service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description={{description}} 3 | 4 | [Service] 5 | WorkingDirectory={{path}} 6 | ExecStart={{execpath}} {{nodescript}} 7 | Restart=always 8 | SyslogIdentifier={{label}} 9 | User={{user}} 10 | Group={{group}} 11 | Environment={{env}} 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /lib/templates/systemd/service-wrapper: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description={{description}} 3 | 4 | [Service] 5 | ExecStart={{execpath}} {{script}} {{{wrappercode}}} 6 | Restart=always 7 | SyslogIdentifier={{label}} 8 | 9 | [Install] 10 | WantedBy=multi-user.target 11 | -------------------------------------------------------------------------------- /lib/templates/systemv/debian: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Adapted from https://gist.github.com/peterhost/715255 4 | # If you wish the Daemon to be lauched at boot / stopped at shutdown : 5 | # INSTALL : update-rc.d scriptname defaults 6 | # (UNINSTALL : update-rc.d -f scriptname remove) 7 | # 8 | # Provides: {{label}} 9 | # Required-Start: $remote_fs $named $syslog 10 | # Required-Stop: $remote_fs $named $syslog 11 | # Default-Start: 2 3 4 5 12 | # Default-Stop: 0 1 6 13 | # Short-Description: {{servicesummary}} 14 | # Description: {{servicedescription}} 15 | # Author: {{author}} 16 | # Created: {{created}} 17 | 18 | # PATH should only include /usr/* if it runs after the mountnfs.sh script 19 | PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin # modify if needed 20 | 21 | DAEMON_ARGS="{{script}}" # path to your node.js server/app 22 | # NB: don't use ~/ in path 23 | 24 | DESC="{{description}}" # whatever fancy description you like 25 | 26 | NODEUSER={{user}}:{{group}} # USER who OWNS the daemon process (no matter whoever runs the init script) 27 | # user:group (if no group is specified, the primary GID for that user is used) 28 | 29 | LOCAL_VAR_RUN=/var/run # In case the init script is run by non-root user, you need to 30 | # indicate a directory writeable by $NODEUSER to store the PID file 31 | 32 | DAEMON={{execpath}} # this SHOULD POINT TO where your node executable is 33 | 34 | LABEL={{label}} 35 | LOGFILE={{logroot}}/$LABEL.log # Logfile path 36 | ERRFILE={{logroot}}/$LABEL-error.log # Error file path 37 | 38 | 39 | # ______________________________________________________________________________ 40 | 41 | # Do NOT "set -e" 42 | 43 | # Create the log file if it does not exist already 44 | if [ ! -a "{{logroot}}" ];then 45 | mkdir -p {{logroot}}; 46 | fi 47 | 48 | if [ ! -a $LOGFILE ];then 49 | touch $LOGFILE; 50 | fi 51 | 52 | # Create the error log file if it does not exist already 53 | if [ ! -a $ERRFILE ];then 54 | touch $ERRFILE; 55 | fi 56 | 57 | # Make sure the log and error files are both writable by the target node user 58 | chown $NODEUSER $LOGFILE; 59 | chown $NODEUSER $ERRFILE; 60 | 61 | 62 | [ $UID -eq "0" ] && LOCAL_VAR_RUN=/var/run # in case this script is run by root, override user setting 63 | THIS_ARG=$0 64 | INIT_SCRIPT_NAME=`basename $THIS_ARG` 65 | [ -h $THIS_ARG ] && INIT_SCRIPT_NAME=`basename $(readlink $THIS_ARG)` # in case of symlink 66 | INIT_SCRIPT_NAME_NOEXT=${INIT_SCRIPT_NAME%.*} 67 | PIDFILE="$LOCAL_VAR_RUN/$INIT_SCRIPT_NAME_NOEXT.pid" 68 | SCRIPTNAME=/etc/init.d/$INIT_SCRIPT_NAME 69 | 70 | # Exit if the package is not installed 71 | [ -x "$DAEMON" ] || { echo "can't find Node.js ($DAEMON)" >&2; exit 0; } 72 | 73 | # Exit if the 'run' folder is not present 74 | [ -d "$LOCAL_VAR_RUN" ] || { echo "Directory $LOCAL_VAR_RUN does not exist. Modify the '$INIT_SCRIPT_NAME_NOEXT' init.d script ($THIS_ARG) accordingly" >&2; exit 0; } 75 | 76 | # Read configuration variable file if it is present 77 | [ -r /etc/default/$INIT_SCRIPT_NAME ] && . /etc/default/$INIT_SCRIPT_NAME 78 | 79 | # Load the VERBOSE setting and other rcS variables 80 | [ -r /lib/init/vars.sh ] && . /lib/init/vars.sh 81 | 82 | # Define LSB log_* functions. 83 | 84 | # To be replaced by LSB functions 85 | # Defined here for distributions that don't define 86 | # log_daemon_msg 87 | 88 | log_daemon_msg () { 89 | echo $@ 90 | } 91 | 92 | # To be replaced by LSB functions 93 | # Defined here for distributions that don't define 94 | # log_end_msg 95 | log_end_msg () { 96 | retval=$1 97 | if [ $retval -eq 0 ]; then 98 | echo "." 99 | else 100 | echo " failed!" 101 | fi 102 | return $retval 103 | } 104 | 105 | # Define LSB log_* functions. 106 | # Depend on lsb-base (>= 3.0-6) to ensure that this file is present. 107 | . /lib/lsb/init-functions 108 | 109 | # uncomment to override system setting 110 | VERBOSE=yes 111 | 112 | # Constructed Variables 113 | USER=`id -n -u` 114 | PIDFILE={{pidroot}}/$INIT_SCRIPT_NAME_NOEXT.pid 115 | 116 | # 117 | # Function that starts the daemon/service 118 | # 119 | do_start() 120 | { 121 | # Return 1 if daemon was already running 122 | start-stop-daemon --start --quiet --pidfile $PIDFILE --chuid $NODEUSER --background --exec $DAEMON --name $LABEL --test > /dev/null \ 123 | || { [ "$VERBOSE" != no ] && log_daemon_msg " ---> Daemon already running $DESC" "$INIT_SCRIPT_NAME_NOEXT"; return 1; } 124 | 125 | # Return 2 if daemon could not be started 126 | { start-stop-daemon --start --quiet --chuid $NODEUSER --make-pidfile --pidfile $PIDFILE --background --name $LABEL --exec $DAEMON -- \ 127 | $DAEMON_ARGS {{{wrappercode}}} & } \ 128 | || { [ "$VERBOSE" != no ] && log_daemon_msg " ---> could not be start $DESC" "$INIT_SCRIPT_NAME_NOEXT"; return 2; } 129 | 130 | # Add code here, if necessary, that waits for the process to be ready 131 | # to handle requests from services started subsequently which depend 132 | # on this one. As a last resort, sleep for some time. 133 | 134 | #[ "$VERBOSE" != no ] && log_daemon_msg " ---> started $INIT_SCRIPT_NAME_NOEXT" 135 | # Return 0 when daemon has been started 136 | return 0; 137 | } 138 | 139 | # 140 | # Function that stops the daemon/service 141 | # 142 | do_stop() 143 | { 144 | # Return 145 | # 0 if daemon has been stopped 146 | # 1 if daemon was already stopped 147 | # 2 if daemon could not be stopped 148 | # other if a failure occurred 149 | 150 | start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --chuid $NODEUSER --name $LABEL 151 | RETVAL="$?" 152 | 153 | #[ "$VERBOSE" != no ] && [ "$RETVAL" = 1 ] && log_daemon_msg " ---> SIGKILL failed => hardkill $DESC" "$INIT_SCRIPT_NAME_NOEXT" 154 | [ "$RETVAL" = 2 ] && return 2 155 | 156 | # Wait for children to finish too if this is a daemon that forks 157 | # and if the daemon is only ever run from this initscript. 158 | # If the above conditions are not satisfied then add some other code 159 | # that waits for the process to drop all resources that could be 160 | # needed by services started subsequently. A last resort is to 161 | # sleep for some time. 162 | 163 | start-stop-daemon --stop --quiet --oknodo --retry=0/3/KILL/5 --pidfile $PIDFILE --chuid $NODEUSER --exec $DAEMON -- $DAEMON_ARGS {{{wrappercode}}} 164 | [ "$?" = 2 ] && return 2 165 | 166 | sleep 1 167 | 168 | # Kill the child process forcibly 169 | NODESCRIPT={{{nodescript}}} 170 | CPID=`pgrep -f $NODESCRIPT` 171 | kill -9 $CPID 172 | 173 | # Many daemons don't delete their pidfiles when they exit. 174 | rm -f $PIDFILE 175 | 176 | [ "$VERBOSE" != no ] && [ "$RETVAL" = 1 ] && log_daemon_msg " ---> $INIT_SCRIPT_NAME_NOEXT - $DESC not running" 177 | [ "$VERBOSE" != no -a "$RETVAL" = 0 ] && log_daemon_msg " ---> $INIT_SCRIPT_NAME_NOEXT - $DESC stopped" 178 | return "$RETVAL" 179 | } 180 | 181 | # 182 | # Function that sends a SIGHUP to the daemon/service 183 | # 184 | do_reload() { 185 | # 186 | # If the daemon can reload its configuration without 187 | # restarting (for example, when it is sent a SIGHUP), 188 | # then implement that here. 189 | # 190 | start-stop-daemon --stop --quiet --signal 1 --pidfile $PIDFILE --chuid $NODEUSER --name $LABEL 191 | return 0 192 | } 193 | 194 | # 195 | # Function that returns the daemon 196 | # 197 | do_status() { 198 | # 199 | # http://refspecs.freestandards.org/LSB_3.1.1/LSB-Core-generic/LSB-Core-generic/iniscrptact.html 200 | # 0 program is running or service is OK 201 | # 1 program is dead and /var/run pid file exists 202 | # (2 program is dead and /var/lock lock file exists) (not used here) 203 | # 3 program is not running 204 | # 4 program or service status is unknown 205 | RUNNING=$(running) 206 | 207 | ispidactive=$(pidof $NAME | grep `cat $PIDFILE 2>&1` >/dev/null 2>&1) 208 | ISPIDACTIVE=$? 209 | 210 | if [ -n "$RUNNING" ]; then 211 | if [ $ISPIDACTIVE ]; then 212 | log_success_msg "$INIT_SCRIPT_NAME_NOEXT running (launched by $USER, --chuid $NODEUSER)." 213 | exit 0 214 | fi 215 | else 216 | if [ -f $PIDFILE ]; then 217 | rm -f $PIDFILE 218 | log_success_msg "$INIT_SCRIPT_NAME_NOEXT is not running. Phantom pidfile, $PIDFILE, removed." 219 | exit 1 220 | else 221 | log_success_msg "$INIT_SCRIPT_NAME_NOEXT is not running." 222 | exit 3 223 | fi 224 | fi 225 | } 226 | 227 | running() { 228 | RUNSTAT=$(start-stop-daemon --start --quiet --pidfile $PIDFILE --chuid $NODEUSER --background --exec $DAEMON --test > /dev/null) 229 | if [ "$?" = 1 ]; then 230 | echo y 231 | fi 232 | } 233 | 234 | case "$1" in 235 | start) 236 | [ "$VERBOSE" != no ] && log_daemon_msg "Starting $INIT_SCRIPT_NAME_NOEXT" 237 | do_start 238 | case "$?" in 239 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 240 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 241 | esac 242 | ;; 243 | stop) 244 | [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $INIT_SCRIPT_NAME_NOEXT" 245 | do_stop 246 | case "$?" in 247 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 248 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 249 | esac 250 | ;; 251 | 252 | reload) 253 | # 254 | # If do_reload() is not implemented then leave this commented out 255 | # and leave 'force-reload' as an alias for 'restart'. 256 | # 257 | log_daemon_msg "Reloading $INIT_SCRIPT_NAME_NOEXT" 258 | do_reload 259 | log_end_msg $? 260 | ;; 261 | 262 | restart|force-reload) 263 | 264 | # 265 | # If the "reload" option is implemented then remove the 266 | # 'force-reload' alias 267 | # 268 | log_daemon_msg "Restarting $INIT_SCRIPT_NAME_NOEXT" 269 | do_stop 270 | case "$?" in 271 | 0|1) 272 | do_start 273 | case "$?" in 274 | 0) log_end_msg 0 ;; 275 | 1) log_end_msg 1 ;; # Old process is still running 276 | *) log_end_msg 1 ;; # Failed to start 277 | esac 278 | ;; 279 | *) 280 | # Failed to stop 281 | log_end_msg 1 282 | ;; 283 | esac 284 | ;; 285 | status) 286 | do_status 287 | ;; 288 | *) 289 | echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 290 | #echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2 291 | exit 3 292 | ;; 293 | esac 294 | 295 | exit 0 296 | -------------------------------------------------------------------------------- /lib/templates/systemv/redhat: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # chkconfig: 35 99 99 4 | # description: {{description}} 5 | # 6 | # Author: {{author}} 7 | # Created: {{created}} 8 | 9 | . /etc/rc.d/init.d/functions 10 | 11 | PIDFILE="{{pidroot}}/{{label}}.pid" 12 | #ps -fe | grep "{{label}}" | head -n1 | cut -d" " -f 6 > ${PIDFILE} 13 | 14 | USER={{user}} 15 | DAEMON={{execpath}} 16 | SCRIPT={{script}} 17 | LABEL={{label}} 18 | LOG_FILE={{logroot}}/{{label}}.log 19 | LOCK_FILE=/var/lock/subsys/node-daemon-$LABEL 20 | 21 | do_start() 22 | { 23 | if [ ! -f "${LOCK_FILE}" ] ; then 24 | su - ${USER} -s /bin/sh -c "$DAEMON $SCRIPT {{{wrappercode}}} >> $LOG_FILE 2>&1 ${PIDFILE} 25 | RETVAL=$? 26 | [ $RETVAL -eq 0 ] && touch ${LOCK_FILE} 27 | else 28 | echo "$LABEL is locked with ${LOCK_FILE}." 29 | RETVAL=1 30 | fi 31 | [ $RETVAL -eq 0 ] && echo_success || echo_failure 32 | } 33 | 34 | do_stop() 35 | { 36 | echo -n "Stopping $LABEL:" 37 | PID=`cat ${PIDFILE}` 38 | kill $PID > /dev/null 39 | if [ -f ${LOCK_FILE} ] ; then 40 | rm ${LOCK_FILE} 41 | fi 42 | if [ -f ${PIDFILE} ] ; then 43 | rm ${PIDFILE} 44 | fi 45 | RETVAL=$? 46 | echo 47 | [ $RETVAL -eq 0 ] && echo_success || echo_failure 48 | } 49 | 50 | case "$1" in 51 | start) 52 | do_start 53 | ;; 54 | stop) 55 | do_stop 56 | ;; 57 | restart) 58 | do_stop 59 | do_start 60 | ;; 61 | status) 62 | running="no" 63 | if [ -f ${PIDFILE} ]; then 64 | PID=$(cat ${PIDFILE}) 65 | if [ -x /proc/${PID} ]; then 66 | running="yes" 67 | fi 68 | fi 69 | 70 | if [ "${running}" == "yes" ]; then 71 | echo "${LABEL} is running ($PID)" 72 | else 73 | echo "${LABEL} is not running" 74 | fi 75 | ;; 76 | *) 77 | echo "Usage: $0 {start|stop|restart|status}" 78 | RETVAL=1 79 | esac 80 | 81 | exit $RETVAL 82 | -------------------------------------------------------------------------------- /lib/wrapper.js: -------------------------------------------------------------------------------- 1 | // Handle input parameters 2 | var optimist = require('optimist'), 3 | fs = require('fs'), 4 | max = 60, 5 | p = require('path'), 6 | argv = optimist 7 | .demand('file') 8 | .alias('f','file') 9 | .describe('file','The absolute path of the script to be run as a process.') 10 | .check(function(argv){ 11 | require('fs').existsSync(p.resolve(argv.f),function(exists){ 12 | return exists; 13 | }); 14 | }) 15 | .demand('log') 16 | .alias('l','log') 17 | .describe('log','The absolute path of the log file.') 18 | .demand('errorlog') 19 | .alias('e','errorlog') 20 | .describe('errorlog','The absolute path of the error log file.') 21 | .demand('title') 22 | .alias('t','title') 23 | .describe('title','The name/title of the process.') 24 | .default('maxretries',-1) 25 | .alias('m','maxretries') 26 | .describe('maxretries','The maximim number of times the process will be auto-restarted.') 27 | .default('maxrestarts',5) 28 | .alias('r','maxrestarts') 29 | .describe('maxrestarts','The maximim number of times the process should be restarted within a '+max+' second period shutting down.') 30 | .default('wait',1) 31 | .alias('w','wait') 32 | .describe('wait','The number of seconds between each restart attempt.') 33 | .check(function(argv){ 34 | return argv.w >= 0; 35 | }) 36 | .default('grow',0.25) 37 | .alias('g','grow') 38 | .describe('grow','A percentage growth rate at which the wait time is increased.') 39 | .check(function(argv){ 40 | return (argv.g >= 0 && argv.g <= 1); 41 | }) 42 | .default('abortonerror','no') 43 | .alias('a','abortonerror') 44 | .describe('abortonerror','Do not attempt to restart the process if it fails with an error,') 45 | .check(function(argv){ 46 | return ['y','n','yes','no'].indexOf(argv.a.trim().toLowerCase()) >= 0; 47 | }) 48 | .argv, 49 | //log = new Logger(argv.e == undefined ? argv.l : {source:argv.l,eventlog:argv.e}), 50 | fork = require('child_process').fork, 51 | script = p.resolve(argv.f), 52 | wait = argv.w*1000, 53 | grow = argv.g+1, 54 | attempts = 0, 55 | startTime = null, 56 | starts = 0, 57 | child = null, 58 | forcekill = false; 59 | 60 | process.title = argv.t || 'Node.JS Script'; 61 | 62 | // Log Formatting - Standard Output Hook 63 | process.stdout.write = (function(write) { 64 | return function(logLine, encoding, fd) { 65 | fs.appendFileSync(argv.log, new Date().toLocaleString()+' - SVCMGR - '+logLine); 66 | }; 67 | })(process.stdout.write); 68 | 69 | process.stderr.write = (function(write) { 70 | return function(logLine, encoding, fd) { 71 | fs.appendFileSync(argv.errorlog, new Date().toLocaleString()+' - SVCMGR - '+logLine); 72 | }; 73 | })(process.stderr.write); 74 | 75 | console.log(process.title + " start up"); 76 | 77 | if (argv.env){ 78 | if (Object.prototype.toString.call(argv.env) === '[object Array]'){ 79 | for(var i=0;i= argv.r){ 106 | if (new Date().getTime()-(max*1000) <= startTime.getTime()){ 107 | console.error('Too many restarts within the last '+max+' seconds. Please check the script.'); 108 | process.exit(); 109 | } 110 | } 111 | 112 | setTimeout(function(){ 113 | wait = wait * grow; 114 | attempts += 1; 115 | if (attempts > argv.m && argv.m >= 0){ 116 | console.error('Too many restarts. '+argv.f+' will not be restarted because the maximum number of total restarts has been exceeded.'); 117 | process.exit(); 118 | } else { 119 | launch(); 120 | } 121 | },wait); 122 | } else { 123 | attempts = 0; 124 | wait = argv.w * 1000; 125 | } 126 | }; 127 | 128 | 129 | /** 130 | * @method launch 131 | * A method to start a process. 132 | */ 133 | var launch = function(){ 134 | 135 | if (forcekill){ 136 | return; 137 | } 138 | 139 | console.log('Starting '+argv.f); 140 | 141 | // Set the start time if it's null 142 | if (startTime === null) { 143 | startTime = startTime || new Date(); 144 | setTimeout(function(){ 145 | startTime = null; 146 | starts = 0; 147 | },(max*1000)+1); 148 | } 149 | starts += 1; 150 | 151 | // Fork the child process piping stdin/out/err to the parent 152 | child = fork(script, {env:process.env, silent:true}); 153 | 154 | child.stdout.on('data', function (data) { 155 | fs.appendFileSync(argv.log, new Date().toLocaleString()+' - P.' + child.pid + ' - '+data); 156 | }); 157 | 158 | child.stderr.on('data', function (data) { 159 | fs.appendFileSync(argv.errorlog, new Date().toLocaleString()+' - P.' + child.pid + ' - '+ data); 160 | }); 161 | 162 | // When the child dies, attempt to restart based on configuration 163 | child.on('exit',function(code) { 164 | 165 | console.warn(argv.f+' stopped running.'); 166 | 167 | // If an error is thrown and the process is configured to exit, then kill the parent. 168 | if (code !== 0 && argv.a == "yes"){ 169 | console.error(argv.f+' exited with error code '+code); 170 | process.exit(); 171 | server.unref(); 172 | } 173 | 174 | delete child.pid; 175 | 176 | // Monitor the process 177 | monitor(); 178 | }); 179 | 180 | }; 181 | 182 | process.on('exit',function(){ 183 | console.log("Got exit signal, closing down"); 184 | forcekill = true; 185 | child.kill(); 186 | process.exit(); 187 | }); 188 | 189 | // Killing the wrapper does not kill the child node process without this handler 190 | process.on('SIGTERM',function(){ 191 | console.log("Got SIGTERM, closing down"); 192 | forcekill = true; 193 | child.kill(); 194 | process.exit(); 195 | }); 196 | 197 | process.on('uncaughtException', function(err) { 198 | console.error('Uncaught exception: ' + err.stack); 199 | server.unref(); 200 | process.exit(); 201 | }); 202 | 203 | process.on('SIGHUP', function() { 204 | console.log("SIGHUP received, restarting child"); 205 | child.kill(); 206 | }); 207 | 208 | // Launch the process 209 | launch(); 210 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-linux", 3 | "version": "0.1.12", 4 | "description": "Support daemon creation and management on Linux.", 5 | "keywords": [ 6 | "ngn", 7 | "linux", 8 | "daemon", 9 | "service", 10 | "centos", 11 | "redhat", 12 | "debian", 13 | "ubuntu" 14 | ], 15 | "author": "Corey Butler ", 16 | "devDependencies": { 17 | "mocha": "*" 18 | }, 19 | "main": "lib/node-linux.js", 20 | "os": [ 21 | "linux" 22 | ], 23 | "dependencies": { 24 | "optimist": "~0.6.1", 25 | "mu2": "~0.5.20" 26 | }, 27 | "readmeFilename": "README.md", 28 | "scripts": { 29 | "test": "mocha" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git://github.com/coreybutler/node-linux.git" 34 | }, 35 | "license": "MIT", 36 | "engine": "node >= 0.10.10" 37 | } 38 | --------------------------------------------------------------------------------