├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── appveyor.yml ├── package.json ├── src └── monogamous.js └── tests ├── app.js ├── injectable.js └── monogamous-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | .zuulrc 5 | /dist/ 6 | 7 | ###### Mac OS generated files 8 | .DS_Store 9 | .DS_Store? 10 | ._* 11 | .Spotlight-V100 12 | .Trashes 13 | ehthumbs.db 14 | Thumbs.db 15 | 16 | doc/ 17 | build/ 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | 30 | # Compiled binary addons (http://nodejs.org/api/addons.html) 31 | build/Release 32 | 33 | # Dependency directory 34 | # Deployed apps should consider commenting this line out: 35 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 36 | node_modules 37 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .editorconfig 2 | .idea/ 3 | /*.tgz 4 | /.travis.yml 5 | /.jshintrc 6 | /buildconfig.env.example 7 | /test/ 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "iojs" 5 | install: 6 | - npm install 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mike Nichols 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Monogamous 2 | 3 | > Only one instance of an app at a time. 4 | 5 | [![Build Status: Linux](https://travis-ci.org/mnichols/monogamous.svg?branch=master)](https://travis-ci.org/mnichols/monogamous) 6 | [![Build Status: Windows](https://ci.appveyor.com/api/projects/status/14ud4t906nm8earn/branch/master?svg=true)](https://ci.appveyor.com/project/MikeNichols/monogamous/branch/master) 7 | 8 | ## Install 9 | 10 | `npm install monogamous` 11 | 12 | **NOTE**: If you are using this for Electron, you should know that this feature 13 | was added as an option. See [here](https://github.com/atom/electron/blob/v0.34.1/docs/api/app.md#appmakesingleinstancecallback) 14 | and [here](https://github.com/atom/electron/releases/tag/v0.34.1). 15 | 16 | ## Usage (using Electron as an example) 17 | 18 | #### Decorating main process entrypoint 19 | ```js 20 | 21 | //index.js 22 | import monogamous from 'monogamous' 23 | import main from './main' //main process app stuff 24 | import app from 'app' 25 | 26 | booter = monogamous({ sock: 'myapp'}, { other: 'args'}) 27 | /** 28 | * this presumes your `app.on('ready')` is inside your boot method 29 | */ 30 | booter.on('boot', main.boot.bind(main)) 31 | booter.on('reboot', main.reboot.bind(main)) 32 | booter.on('error', function(err) { console.error('ops', err) }) 33 | 34 | booter.boot({ more: 'args'}) 35 | ``` 36 | 37 | #### Inside main process entrypoint 38 | ```js 39 | 40 | //index.js 41 | import monogamous from 'monogamous' 42 | import main from './main' //main process app stuff 43 | import app from 'app' 44 | 45 | booter = monogamous({ sock: 'myapp'}, { other: 'args'}) 46 | 47 | booter.on('boot', main.boot.bind(main)) 48 | booter.on('reboot', main.reboot.bind(main)) 49 | booter.on('error', function(err) { console.error('ops', err) }) 50 | 51 | //electron's ready event gets it going 52 | app.on('ready', booter.boot.bind(booter)) 53 | ``` 54 | 55 | ## Events 56 | 57 | - `boot` : raised if an instance is not running. Your app may start up pristine here 58 | - `reboot` : another instance was attempted. 59 | - `end` : a call to `end()` shutdown the instance server 60 | 61 | `boot` and `reboot` events receive an merged arguments object merging the following inputs, 62 | in order of precedence: 63 | 64 | - args passed to monogamous creation; eg `monogamous({ sock: 'foo'}, {these:'arepassedthru'})` 65 | - process argv , hashed (using [minimist](https://www.npmjs.com/package/minimist)) 66 | - args passed to `boot`; eg `mono.boot({ these:'arealsopassedthru'})` 67 | 68 | 69 | ## API 70 | 71 | **Monogamous Factory** 72 | 73 | ```js 74 | //only the 'sock' property is required to name your socket 75 | let booter = monogamous({ sock: 'keepitsimple' }, [other args...]) 76 | 77 | ``` 78 | 79 | **Instance Methods** 80 | 81 | - `boot([args])` : {Function} tries to connect to `sock`;failure to connect means an instance is running 82 | - `end()` : {Function} closes socket server 83 | 84 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Test against this version of Node.js 2 | environment: 3 | matrix: 4 | # io.js 5 | - nodejs_version: "3.2.0" 6 | # node.js 7 | - nodejs_version: "0.12.7" 8 | 9 | # Install scripts. (runs after repo cloning) 10 | install: 11 | # Get the latest stable version of Node.js or io.js 12 | - ps: Install-Product node $env:nodejs_version 13 | # install modules 14 | - npm install 15 | 16 | # Post-install test scripts. 17 | test_script: 18 | # Output useful info for debugging. 19 | - node --version 20 | - npm --version 21 | # run tests 22 | - npm test 23 | 24 | # Don't actually build (off). 25 | # Otherwise MSBUILD gets involved and that is always bad. 26 | build: off 27 | 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monogamous", 3 | "version": "1.0.3", 4 | "description": "boot single-instance application", 5 | "main": "./dist/monogamous.js", 6 | "scripts": { 7 | "clean": "rimraf dist/* && mkdir dist || true", 8 | "test": "babel-tape-runner ./tests/**/*-test.js | faucet", 9 | "build": "npm run clean && babel src --out-dir dist", 10 | "prepublish": "npm run build && npm test" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/mnichols/monogamous" 15 | }, 16 | "keywords": [ 17 | "single", 18 | "instance", 19 | "electron", 20 | "flatulence" 21 | ], 22 | "author": "Mike Nichols ", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/mnichols/monogamous/issues" 26 | }, 27 | "homepage": "https://github.com/mnichols/monogamous", 28 | "devDependencies": { 29 | "babel": "^5.8.23", 30 | "babel-tape-runner": "^1.2.0", 31 | "faucet": "0.0.1", 32 | "rimraf": "^2.4.3", 33 | "tape": "^4.2.0" 34 | }, 35 | "dependencies": { 36 | "minimist": "^1.2.0", 37 | "stampit": "^2.1.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/monogamous.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import os from 'os' 4 | import {EventEmitter} from 'events' 5 | import path from 'path' 6 | import fs from 'fs' 7 | import net from 'net' 8 | import minimist from 'minimist' 9 | import stampit from 'stampit' 10 | 11 | /** 12 | * handles duplicate instance attempts 13 | * */ 14 | const server = stampit() 15 | .props({ 16 | force: false 17 | , emitter: undefined 18 | }) 19 | .init(function(){ 20 | let serve 21 | const handleReboot = (data) => { 22 | let parsed 23 | try { 24 | parsed = JSON.parse(data.toString('utf-8')) 25 | } catch(err) { 26 | if(!(err instanceof SyntaxError )) { 27 | throw err 28 | } 29 | } 30 | this.emitter.emit('reboot', parsed) 31 | } 32 | const handleConnection = (conn) => { 33 | conn.on('data', handleReboot) 34 | } 35 | const handleError = (err) => { 36 | console.error('boot server failed', err) 37 | } 38 | const handleListening = () => { 39 | this.emitter.emit('boot', this.args) 40 | } 41 | const handleClose = () => { 42 | this.emitter.emit('end') 43 | } 44 | 45 | const start = () => { 46 | if(this.force) { 47 | this.emitter.emit('boot', this.args) 48 | return 49 | } 50 | this.removeSocket() 51 | serve = net.createServer(handleConnection) 52 | serve.listen(this.socketPath()) 53 | serve.on('listening', handleListening) 54 | serve.on('error', handleError) 55 | serve.on('close', handleClose) 56 | } 57 | this.startServer = start 58 | this.end = function(){ 59 | return serve.close() 60 | } 61 | }) 62 | 63 | const client = stampit() 64 | .methods({ 65 | //try to connect to socket and forward args to running instance 66 | //on error, it must need to boot 67 | connect: function(){ 68 | let spath = this.socketPath() 69 | let _client 70 | const handleConnection = () => { 71 | //send data to previous instance and then shutdown 72 | _client.write(JSON.stringify(this.args), function(){ 73 | _client.end() 74 | //not an error... 75 | process.exit(0) 76 | }) 77 | } 78 | _client = net.connect({path:spath},handleConnection) 79 | //unable to connect, so boot the app once our server starts 80 | _client.on('error', this.startServer) 81 | } 82 | 83 | }) 84 | 85 | 86 | //boot strategy for windows 87 | const win32 = stampit() 88 | .props({ 89 | force: false 90 | }) 91 | .compose(client, server) 92 | .methods({ 93 | socketPath (){ 94 | return `\\\\.\\pipe\\${this.sock}-sock}` 95 | } 96 | , removeSocket () { 97 | //nothing to do here 98 | return false 99 | } 100 | , boot () { 101 | this.connect() 102 | } 103 | }) 104 | 105 | //boot strategy for everyone else 106 | const defaultPlatform = stampit() 107 | .props({ 108 | force: false 109 | }) 110 | .compose(client, server) 111 | .methods({ 112 | socketPath () { 113 | return path.join(os.tmpdir(),`${this.sock}-${process.env.USER}.sock`) 114 | } 115 | , boot() { 116 | //fail fast 117 | if(this.force || !fs.existsSync(this.socketPath())) { 118 | this.startServer() 119 | } else { 120 | this.connect() 121 | } 122 | } 123 | , removeSocket () { 124 | let spath = this.socketPath() 125 | if(fs.existsSync(spath)) { 126 | try { 127 | fs.unlinkSync(spath) 128 | } catch( err ) { 129 | /* 130 | * 131 | * Ignore ENOENT errors in case the file was deleted between the exists 132 | * check and the call to unlink sync. This occurred occasionally on CI 133 | * which is why this check is here. 134 | * */ 135 | if( err.code !== 'ENOENT') { 136 | throw err 137 | } 138 | } 139 | } 140 | } 141 | }) 142 | 143 | export default stampit() 144 | .props({ 145 | //the name of the app to name the socket/pipe 146 | sock: undefined 147 | //force instance to start 148 | , force: false 149 | }) 150 | .init(function({ args}){ 151 | if(!this.sock) { 152 | throw new Error('`sock` is required') 153 | } 154 | //compose event emitter to workaround stampit undefined return 155 | let emitter = this.emitter = new EventEmitter 156 | let platform 157 | this.on = emitter.on.bind(emitter) 158 | this.once = emitter.once.bind(emitter) 159 | this.removeListener = emitter.removeListener.bind(emitter) 160 | this.removeAllListeners = emitter.removeAllListeners.bind(emitter) 161 | this.emit = emitter.emit.bind(emitter) 162 | 163 | /** 164 | * call this to emit proper events 165 | * based on state of app (running/not) 166 | * */ 167 | this.boot = function(bootArgs) { 168 | args.push(minimist(process.argv.slice(2))) 169 | args.push(bootArgs) 170 | let argv = Object.assign({}, ...args) 171 | if(process.platform === 'win32') { 172 | platform = win32({ 173 | sock: this.sock 174 | , emitter: emitter 175 | , force: this.force 176 | , args: argv 177 | }) 178 | } else { 179 | platform = defaultPlatform({ 180 | sock: this.sock 181 | , emitter: emitter 182 | , force: this.force 183 | , args: argv 184 | }) 185 | } 186 | 187 | // boot it! 188 | platform.boot() 189 | }.bind(this) 190 | 191 | this.end = function() { 192 | if(platform) { 193 | return platform.end() 194 | } 195 | this.emit('end') 196 | } 197 | }) 198 | 199 | -------------------------------------------------------------------------------- /tests/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('babel/register') 4 | var monogamous = require('../src/monogamous') 5 | var name = process.argv[2] 6 | var booter = monogamous({ sock: 'test'},{ hee: 'haw'}) 7 | booter.on('boot',function(args) { 8 | process.send({ name: name,event: 'boot', args: args}) 9 | }) 10 | booter.on('reboot', function(args) { 11 | process.send({ name: name, event: 'reboot', args: args}) 12 | }) 13 | booter.boot({ madefer: 'walkin'}) 14 | -------------------------------------------------------------------------------- /tests/injectable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('babel/register') 4 | var monogamous = require('../src/monogamous') 5 | var name = process.argv[2] 6 | var booter = monogamous({ sock: 'test'},{ hee: 'haw'}) 7 | booter.on('end',function(e){ 8 | process.send({ event: 'end', args: e}) 9 | }) 10 | booter.on('boot',function(e){ 11 | process.send({ event: 'boot', args: e}) 12 | }) 13 | process.on('message', function(m){ 14 | if(m === 'boot') { 15 | booter.boot() 16 | } 17 | if(m === 'end') { 18 | booter.end() 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /tests/monogamous-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import test from 'tape' 4 | import cp from 'child_process' 5 | import monogamous from '../src/monogamous' 6 | 7 | test('booting more than once instance', (assert) => { 8 | const appPath= __dirname + '/app.js' 9 | let app = cp.fork(appPath,['main']) 10 | let dupe 11 | 12 | let messages = [] 13 | app.on('message',function(m) { 14 | messages.push(m) 15 | if(m.event === 'boot') { 16 | dupe = cp.fork(appPath,['sub1']) 17 | } 18 | if(m.event === 'reboot') { 19 | setTimeout(function(){ 20 | assert.equal(messages.length, 2) 21 | assert.equal(messages[0].name,'main') 22 | assert.equal(messages[0].event, 'boot') 23 | assert.deepEqual(messages[0].args, { 24 | '_': ['main'] 25 | , 'hee': 'haw' 26 | , 'madefer': 'walkin' 27 | }) 28 | assert.equal(messages[1].name,'main') 29 | assert.equal(messages[1].event,'reboot') 30 | assert.deepEqual(messages[1].args, { 31 | '_': ['sub1'] 32 | , 'hee': 'haw' 33 | , 'madefer': 'walkin' 34 | }) 35 | assert.false(dupe.connected) 36 | assert.true(app.connected) 37 | app.kill() 38 | dupe.kill() 39 | assert.end() 40 | },100) 41 | } 42 | }) 43 | }) 44 | 45 | test('ending the booter', (assert) => { 46 | const appPath= __dirname + '/injectable.js' 47 | let app = cp.fork(appPath,['main']) 48 | app.on('message',function(m){ 49 | if(m.event === 'boot') { 50 | return app.send('end') 51 | } 52 | if(m.event === 'end') { 53 | assert.true(app.connected) 54 | assert.pass('ended') 55 | app.kill() 56 | assert.end() 57 | } 58 | 59 | }) 60 | app.send('boot') 61 | }) 62 | --------------------------------------------------------------------------------