├── web ├── public │ ├── robots.txt │ ├── images │ │ ├── guvnor.png │ │ ├── favicon.png │ │ ├── guvnor@2x.png │ │ ├── tableflip.png │ │ ├── tableflip@2x.png │ │ ├── tableflip-colour.png │ │ └── tableflip@2x-colour.png │ ├── apple-touch-icon.png │ ├── css │ │ ├── app.styl │ │ ├── app │ │ │ ├── apps.styl │ │ │ ├── processes.styl │ │ │ ├── app.styl │ │ │ ├── host.styl │ │ │ ├── process.styl │ │ │ ├── exceptions.styl │ │ │ └── hosts.styl │ │ └── _variables.styl │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── font-mfizz.eot │ │ ├── font-mfizz.ttf │ │ ├── font-mfizz.woff │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.ttf │ │ └── fontawesome-webfont.woff │ └── javascript │ │ └── highcharts │ │ ├── README.md │ │ ├── modules │ │ └── no-data-to-display.js │ │ └── themes │ │ ├── grid-light.js │ │ └── skies.js ├── templates │ ├── includes │ │ ├── confirm.hbs │ │ ├── process │ │ │ ├── logs.hbs │ │ │ ├── exceptions.hbs │ │ │ ├── exceptionlist │ │ │ │ ├── empty.hbs │ │ │ │ └── entry.hbs │ │ │ ├── snapshotlist │ │ │ │ ├── empty.hbs │ │ │ │ └── entry.hbs │ │ │ ├── loglist │ │ │ │ └── entry.hbs │ │ │ ├── overview │ │ │ │ ├── cpu.hbs │ │ │ │ ├── memory.hbs │ │ │ │ ├── latency.hbs │ │ │ │ └── running.hbs │ │ │ ├── startOrRemove.hbs │ │ │ └── start.hbs │ │ ├── apps │ │ │ ├── console.hbs │ │ │ ├── line.hbs │ │ │ ├── setting.hbs │ │ │ ├── empty.hbs │ │ │ ├── refs.hbs │ │ │ ├── install.hbs │ │ │ └── app.hbs │ │ ├── processlist │ │ │ ├── empty.hbs │ │ │ └── process.hbs │ │ ├── host │ │ │ ├── resources.hbs │ │ │ └── system.hbs │ │ ├── hostlist │ │ │ ├── host.hbs │ │ │ └── process.hbs │ │ └── modal.hbs │ ├── pages │ │ ├── host │ │ │ ├── connecting.hbs │ │ │ ├── connectiontimedout.hbs │ │ │ ├── timeout.hbs │ │ │ ├── networkdown.hbs │ │ │ ├── error.hbs │ │ │ ├── errorconnecting.hbs │ │ │ ├── connectionrefused.hbs │ │ │ ├── connectionreset.hbs │ │ │ ├── overview.hbs │ │ │ ├── hostnotfound.hbs │ │ │ ├── incompatible.hbs │ │ │ ├── badsignature.hbs │ │ │ ├── install.hbs │ │ │ ├── processes.hbs │ │ │ └── apps.hbs │ │ ├── loadinghosts.hbs │ │ ├── nohosts.hbs │ │ └── process │ │ │ ├── overview.hbs │ │ │ ├── started.hbs │ │ │ ├── stopping.hbs │ │ │ ├── restarting.hbs │ │ │ ├── uninitialised.hbs │ │ │ ├── starting.hbs │ │ │ ├── stopped.hbs │ │ │ ├── unresponsive.hbs │ │ │ ├── exceptions.hbs │ │ │ ├── paused.hbs │ │ │ ├── errored.hbs │ │ │ ├── aborted.hbs │ │ │ ├── failed.hbs │ │ │ ├── logs.hbs │ │ │ └── snapshots.hbs │ ├── buttons │ │ ├── debug.hbs │ │ ├── stop.hbs │ │ ├── remove.hbs │ │ ├── start.hbs │ │ ├── gc.hbs │ │ ├── restart.hbs │ │ ├── workeradd.hbs │ │ ├── snapshot.hbs │ │ └── workerremove.hbs │ ├── forms │ │ └── controls │ │ │ ├── select.hbs │ │ │ ├── input.hbs │ │ │ ├── checkbox.hbs │ │ │ ├── tuple.hbs │ │ │ ├── element.hbs │ │ │ └── array.hbs │ ├── head.hbs │ └── body.hbs └── client │ ├── forms │ ├── envProps.js │ ├── envProp.js │ ├── refs.js │ ├── install.js │ ├── controls │ │ └── element.js │ └── app.js │ ├── pages │ ├── host │ │ ├── error.js │ │ ├── timeout.js │ │ ├── badsignature.js │ │ ├── connecting.js │ │ ├── hostnotfound.js │ │ ├── networkdown.js │ │ ├── connectionrefused.js │ │ ├── connectionreset.js │ │ ├── errorconnecting.js │ │ ├── connectiontimedout.js │ │ ├── incompatible.js │ │ ├── processes.js │ │ └── overview.js │ ├── process │ │ ├── started.js │ │ ├── stopping.js │ │ ├── restarting.js │ │ ├── uninitialised.js │ │ ├── starting.js │ │ ├── paused.js │ │ ├── aborted.js │ │ ├── errored.js │ │ ├── failed.js │ │ ├── stopped.js │ │ ├── exceptions.js │ │ ├── snapshots.js │ │ ├── unresponsive.js │ │ ├── overview.js │ │ └── logs.js │ ├── nohosts.js │ ├── loadinghosts.js │ ├── base.js │ ├── host.js │ └── process.js │ ├── views │ ├── apps │ │ ├── empty.js │ │ ├── line.js │ │ ├── refs.js │ │ ├── install.js │ │ └── console.js │ ├── processlist │ │ ├── empty.js │ │ └── process.js │ ├── process │ │ ├── exceptionlist │ │ │ ├── empty.js │ │ │ └── entry.js │ │ ├── snapshotlist │ │ │ ├── empty.js │ │ │ └── entry.js │ │ ├── loglist │ │ │ └── entry.js │ │ └── start.js │ ├── host │ │ └── system.js │ ├── confirm.js │ └── hostlist │ │ ├── host.js │ │ └── process.js │ ├── models │ ├── hosts.js │ ├── apps.js │ ├── user.js │ ├── installation.js │ ├── logs.js │ ├── snapshots.js │ ├── exceptions.js │ ├── users.js │ ├── start.js │ ├── snapshot.js │ ├── app.js │ ├── exception.js │ └── log.js │ ├── helpers │ └── notification.js │ └── buttons │ ├── debug.js │ ├── workeradd.js │ ├── workerremove.js │ ├── remove.js │ ├── stop.js │ ├── gc.js │ └── restart.js ├── test ├── integration │ └── fixtures │ │ ├── .gitignore │ │ ├── first-tick-crash.js │ │ ├── hello-world.js │ │ ├── 6-second-crash.js │ │ ├── hello-world.coffee │ │ ├── arguments.js │ │ ├── exec-arguments.js │ │ ├── remote-executor.js │ │ ├── crash-on-message.js │ │ ├── crashy.js │ │ ├── intermittently-crashy.js │ │ ├── exceptional.js │ │ ├── stdin.js │ │ ├── receive-event.js │ │ ├── http-server.js │ │ ├── colourful.js │ │ ├── talky.js │ │ ├── jibberjabber.js │ │ ├── exec.js │ │ ├── siglisten.js │ │ └── log-daemon-messages.js ├── suite.js ├── lib │ ├── daemon │ │ ├── domain │ │ │ └── RemoteUserTest.js │ │ ├── common │ │ │ ├── LatencyMonitorTest.js │ │ │ ├── UserInfoTest.js │ │ │ ├── ExceptionHandlerTest.js │ │ │ ├── ConfigLoaderTest.js │ │ │ └── LogRedirectorTest.js │ │ ├── cluster │ │ │ └── ClusterProcessWrapperTest.js │ │ ├── StartupNotifierTest.js │ │ └── rpc │ │ │ └── AdminRPCTest.js │ ├── common │ │ ├── CryptoTest.js │ │ └── ManagedAppTest.js │ ├── remote │ │ └── RemoteProcessTest.js │ └── cli │ │ └── TableTest.js └── web │ └── client │ └── models │ ├── snapshotsTest.js │ └── snapshotTest.js ├── bin ├── guv └── guv-web ├── guvnor-web-client ├── img ├── cli.png ├── host.png ├── logs.png ├── guvnor.png ├── guvnor.pxm ├── process.png └── exceptions.png ├── index.js ├── lib ├── daemon │ ├── domain │ │ ├── RemoteUser.js │ │ ├── PersistentStore.js │ │ └── PersistentProcessInfoStore.js │ ├── common │ │ ├── LatencyMonitor.js │ │ ├── ConsoleDebugLogger.js │ │ ├── LogRedirector.js │ │ ├── ExceptionHandler.js │ │ ├── RemoteProcessLogger.js │ │ ├── UserInfo.js │ │ └── ConfigLoader.js │ ├── rpc │ │ ├── AdminRPC.js │ │ ├── UserRPC.js │ │ └── tunnel.js │ ├── inspector │ │ └── index.js │ ├── DaemonLogger.js │ ├── util │ │ ├── LogAdder.js │ │ └── FileSystem.js │ ├── StartupNotifier.js │ ├── cluster │ │ └── ClusterProcessWrapper.js │ ├── service │ │ └── PortService.js │ ├── process │ │ └── index.js │ └── action │ │ └── UserProcess.js ├── common │ ├── HelpfulError.js │ ├── ExecSync.js │ ├── ManagedApp.js │ └── Crypto.js ├── web │ ├── resources │ │ ├── Host.js │ │ ├── HostProcessLog.js │ │ ├── HostProcessCPU.js │ │ ├── HostProcessLatency.js │ │ ├── HostProcessException.js │ │ ├── HostProcessHeapUsed.js │ │ ├── HostProcessHeapTotal.js │ │ ├── HostProcessResidentSize.js │ │ ├── HostProcess.js │ │ ├── HostApp.js │ │ ├── HostUser.js │ │ └── HostProcessSnapshot.js │ └── GuvnorWeb.js ├── cli │ ├── Cluster.js │ ├── Table.js │ └── commander.js └── remote │ └── RemoteProcess.js ├── .jshintrc ├── .gitignore ├── vagrant ├── bootstrap-node.sh └── bootstrap.sh ├── guvnor-web-users ├── guvnor-web-hosts ├── .travis.yml ├── docs ├── statuses.md ├── programmatic-access.md ├── web-users.md ├── clusters.md ├── remote.md └── daemon.md ├── UPGRADING.md └── LICENSE /web/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: -------------------------------------------------------------------------------- /test/integration/fixtures/.gitignore: -------------------------------------------------------------------------------- 1 | *.heapsnapshot 2 | -------------------------------------------------------------------------------- /test/suite.js: -------------------------------------------------------------------------------- 1 | module.exports = require('testsuite')(__dirname) 2 | -------------------------------------------------------------------------------- /web/templates/includes/confirm.hbs: -------------------------------------------------------------------------------- 1 |

2 | -------------------------------------------------------------------------------- /bin/guv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require(__dirname + '/../lib/cli')() 4 | -------------------------------------------------------------------------------- /guvnor-web-client: -------------------------------------------------------------------------------- 1 | 2 | ; how often to update the UI 3 | frequency = 5000 4 | -------------------------------------------------------------------------------- /img/cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableflip/guvnor/HEAD/img/cli.png -------------------------------------------------------------------------------- /img/host.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableflip/guvnor/HEAD/img/host.png -------------------------------------------------------------------------------- /img/logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableflip/guvnor/HEAD/img/logs.png -------------------------------------------------------------------------------- /web/templates/includes/process/logs.hbs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /bin/guv-web: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require(__dirname + '/../lib/web') 4 | -------------------------------------------------------------------------------- /img/guvnor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableflip/guvnor/HEAD/img/guvnor.png -------------------------------------------------------------------------------- /img/guvnor.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableflip/guvnor/HEAD/img/guvnor.pxm -------------------------------------------------------------------------------- /img/process.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableflip/guvnor/HEAD/img/process.png -------------------------------------------------------------------------------- /img/exceptions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableflip/guvnor/HEAD/img/exceptions.png -------------------------------------------------------------------------------- /web/templates/includes/process/exceptions.hbs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/templates/includes/apps/console.hbs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/templates/includes/apps/line.hbs: -------------------------------------------------------------------------------- 1 |
  • {{model.message}}
  • 2 | -------------------------------------------------------------------------------- /web/templates/includes/apps/setting.hbs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/templates/includes/process/exceptionlist/empty.hbs: -------------------------------------------------------------------------------- 1 |

    No exceptions have been thrown

    2 | -------------------------------------------------------------------------------- /test/integration/fixtures/first-tick-crash.js: -------------------------------------------------------------------------------- 1 | throw new Error('A sprocket got stuck in the flange') 2 | -------------------------------------------------------------------------------- /web/templates/includes/apps/empty.hbs: -------------------------------------------------------------------------------- 1 | 2 | No apps are installed 3 | -------------------------------------------------------------------------------- /web/public/images/guvnor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableflip/guvnor/HEAD/web/public/images/guvnor.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Remote: require('./lib/remote'), 3 | Local: require('./lib/local') 4 | } 5 | -------------------------------------------------------------------------------- /web/public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableflip/guvnor/HEAD/web/public/images/favicon.png -------------------------------------------------------------------------------- /test/integration/fixtures/hello-world.js: -------------------------------------------------------------------------------- 1 | setInterval(function () { 2 | console.info('hello world') 3 | }, 1000) 4 | -------------------------------------------------------------------------------- /web/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableflip/guvnor/HEAD/web/public/apple-touch-icon.png -------------------------------------------------------------------------------- /web/public/css/app.styl: -------------------------------------------------------------------------------- 1 | @import '_variables' 2 | @import '_mixins' 3 | @import 'animate' 4 | @import 'app/main' 5 | -------------------------------------------------------------------------------- /web/public/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableflip/guvnor/HEAD/web/public/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /web/public/fonts/font-mfizz.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableflip/guvnor/HEAD/web/public/fonts/font-mfizz.eot -------------------------------------------------------------------------------- /web/public/fonts/font-mfizz.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableflip/guvnor/HEAD/web/public/fonts/font-mfizz.ttf -------------------------------------------------------------------------------- /web/public/fonts/font-mfizz.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableflip/guvnor/HEAD/web/public/fonts/font-mfizz.woff -------------------------------------------------------------------------------- /web/public/images/guvnor@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableflip/guvnor/HEAD/web/public/images/guvnor@2x.png -------------------------------------------------------------------------------- /web/public/images/tableflip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableflip/guvnor/HEAD/web/public/images/tableflip.png -------------------------------------------------------------------------------- /test/integration/fixtures/6-second-crash.js: -------------------------------------------------------------------------------- 1 | setTimeout(function () { 2 | throw new Error('Oh Noes!') 3 | }, 6000) 4 | -------------------------------------------------------------------------------- /test/integration/fixtures/hello-world.coffee: -------------------------------------------------------------------------------- 1 | hello = () -> console.info 'hello world' 2 | 3 | setInterval hello, 1000 4 | -------------------------------------------------------------------------------- /web/public/images/tableflip@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableflip/guvnor/HEAD/web/public/images/tableflip@2x.png -------------------------------------------------------------------------------- /web/templates/includes/process/snapshotlist/empty.hbs: -------------------------------------------------------------------------------- 1 | 2 | No snapshots have been taken 3 | -------------------------------------------------------------------------------- /web/templates/includes/processlist/empty.hbs: -------------------------------------------------------------------------------- 1 | 2 | There are no processes running 3 | 4 | -------------------------------------------------------------------------------- /web/public/images/tableflip-colour.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableflip/guvnor/HEAD/web/public/images/tableflip-colour.png -------------------------------------------------------------------------------- /web/templates/pages/host/connecting.hbs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/arguments.js: -------------------------------------------------------------------------------- 1 | process.send({ 2 | event: 'arguments:received', 3 | args: process.argv.slice(2) 4 | }) 5 | -------------------------------------------------------------------------------- /test/integration/fixtures/exec-arguments.js: -------------------------------------------------------------------------------- 1 | process.send({ 2 | event: 'arguments:received', 3 | args: process.execArgv 4 | }) 5 | -------------------------------------------------------------------------------- /web/public/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableflip/guvnor/HEAD/web/public/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /web/public/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableflip/guvnor/HEAD/web/public/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /test/integration/fixtures/remote-executor.js: -------------------------------------------------------------------------------- 1 | process.on('custom:hello', function (callback) { 2 | callback('hello world') 3 | }) 4 | -------------------------------------------------------------------------------- /web/public/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableflip/guvnor/HEAD/web/public/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /web/public/images/tableflip@2x-colour.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tableflip/guvnor/HEAD/web/public/images/tableflip@2x-colour.png -------------------------------------------------------------------------------- /web/templates/buttons/debug.hbs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/templates/buttons/stop.hbs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/templates/buttons/remove.hbs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/templates/buttons/start.hbs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/templates/buttons/gc.hbs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/templates/buttons/restart.hbs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/templates/buttons/workeradd.hbs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/templates/pages/loadinghosts.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 | -------------------------------------------------------------------------------- /web/templates/buttons/snapshot.hbs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/templates/includes/process/loglist/entry.hbs: -------------------------------------------------------------------------------- 1 |
  • {{model.dateFormatted}}{{{model.messageFormatted}}}
  • 2 | -------------------------------------------------------------------------------- /lib/daemon/domain/RemoteUser.js: -------------------------------------------------------------------------------- 1 | function RemoteUser (options) { 2 | this.name = options.name 3 | this.secret = options.secret 4 | } 5 | 6 | module.exports = RemoteUser 7 | -------------------------------------------------------------------------------- /web/templates/buttons/workerremove.hbs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/client/forms/envProps.js: -------------------------------------------------------------------------------- 1 | var FormView = require('ampersand-form-view') 2 | 3 | module.exports = FormView.extend({ 4 | fields: function () { 5 | return [] 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /test/integration/fixtures/crash-on-message.js: -------------------------------------------------------------------------------- 1 | process.on('custom:euthanise', function () { 2 | 3 | process.nextTick(function () { 4 | throw new Error('goodbye cruel world') 5 | }) 6 | }) 7 | -------------------------------------------------------------------------------- /test/integration/fixtures/crashy.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | 3 | http.createServer(function (req, res) { 4 | }).listen(9000) 5 | 6 | http.createServer(function (req, res) { 7 | }).listen(9000) 8 | -------------------------------------------------------------------------------- /web/client/pages/host/error.js: -------------------------------------------------------------------------------- 1 | var HostPage = require('../host') 2 | var templates = require('../../templates') 3 | 4 | module.exports = HostPage.extend({ 5 | template: templates.pages.host.error 6 | }) 7 | -------------------------------------------------------------------------------- /web/client/pages/host/timeout.js: -------------------------------------------------------------------------------- 1 | var HostPage = require('../host') 2 | var templates = require('../../templates') 3 | 4 | module.exports = HostPage.extend({ 5 | template: templates.pages.host.timeout 6 | }) 7 | -------------------------------------------------------------------------------- /web/client/views/apps/empty.js: -------------------------------------------------------------------------------- 1 | var View = require('ampersand-view') 2 | var templates = require('../../templates') 3 | 4 | module.exports = View.extend({ 5 | template: templates.includes.apps.empty 6 | }) 7 | -------------------------------------------------------------------------------- /web/client/views/apps/line.js: -------------------------------------------------------------------------------- 1 | var View = require('ampersand-view') 2 | var templates = require('../../templates') 3 | 4 | module.exports = View.extend({ 5 | template: templates.includes.apps.line 6 | }) 7 | -------------------------------------------------------------------------------- /web/client/pages/host/badsignature.js: -------------------------------------------------------------------------------- 1 | var HostPage = require('../host') 2 | var templates = require('../../templates') 3 | 4 | module.exports = HostPage.extend({ 5 | template: templates.pages.host.badsignature 6 | }) 7 | -------------------------------------------------------------------------------- /web/client/pages/host/connecting.js: -------------------------------------------------------------------------------- 1 | var HostPage = require('../host') 2 | var templates = require('../../templates') 3 | 4 | module.exports = HostPage.extend({ 5 | template: templates.pages.host.connecting 6 | }) 7 | -------------------------------------------------------------------------------- /web/client/pages/host/hostnotfound.js: -------------------------------------------------------------------------------- 1 | var HostPage = require('../host') 2 | var templates = require('../../templates') 3 | 4 | module.exports = HostPage.extend({ 5 | template: templates.pages.host.hostnotfound 6 | }) 7 | -------------------------------------------------------------------------------- /web/client/pages/host/networkdown.js: -------------------------------------------------------------------------------- 1 | var HostPage = require('../host') 2 | var templates = require('../../templates') 3 | 4 | module.exports = HostPage.extend({ 5 | template: templates.pages.host.networkdown 6 | }) 7 | -------------------------------------------------------------------------------- /web/client/pages/process/started.js: -------------------------------------------------------------------------------- 1 | var ProcessPage = require('../process') 2 | var templates = require('../../templates') 3 | 4 | module.exports = ProcessPage.extend({ 5 | template: templates.pages.process.started 6 | }) 7 | -------------------------------------------------------------------------------- /web/client/views/processlist/empty.js: -------------------------------------------------------------------------------- 1 | var View = require('ampersand-view') 2 | var templates = require('../../templates') 3 | 4 | module.exports = View.extend({ 5 | template: templates.includes.processlist.empty 6 | }) 7 | -------------------------------------------------------------------------------- /web/client/pages/host/connectionrefused.js: -------------------------------------------------------------------------------- 1 | var HostPage = require('../host') 2 | var templates = require('../../templates') 3 | 4 | module.exports = HostPage.extend({ 5 | template: templates.pages.host.connectionrefused 6 | }) 7 | -------------------------------------------------------------------------------- /web/client/pages/host/connectionreset.js: -------------------------------------------------------------------------------- 1 | var HostPage = require('../host') 2 | var templates = require('../../templates') 3 | 4 | module.exports = HostPage.extend({ 5 | template: templates.pages.host.connectionreset 6 | }) 7 | -------------------------------------------------------------------------------- /web/client/pages/host/errorconnecting.js: -------------------------------------------------------------------------------- 1 | var HostPage = require('../host') 2 | var templates = require('../../templates') 3 | 4 | module.exports = HostPage.extend({ 5 | template: templates.pages.host.errorconnecting 6 | }) 7 | -------------------------------------------------------------------------------- /web/client/pages/process/stopping.js: -------------------------------------------------------------------------------- 1 | var ProcessPage = require('../process') 2 | var templates = require('../../templates') 3 | 4 | module.exports = ProcessPage.extend({ 5 | template: templates.pages.process.stopping 6 | }) 7 | -------------------------------------------------------------------------------- /web/templates/pages/host/connectiontimedout.hbs: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /web/client/models/hosts.js: -------------------------------------------------------------------------------- 1 | var Collection = require('ampersand-collection') 2 | var Host = require('./host') 3 | 4 | module.exports = Collection.extend({ 5 | mainIndex: 'name', 6 | model: Host, 7 | comparator: 'name' 8 | }) 9 | -------------------------------------------------------------------------------- /web/client/pages/nohosts.js: -------------------------------------------------------------------------------- 1 | var PageView = require('./base') 2 | var templates = require('../templates') 3 | 4 | module.exports = PageView.extend({ 5 | pageTitle: 'Guvnor - no hosts', 6 | template: templates.pages.nohosts 7 | }) 8 | -------------------------------------------------------------------------------- /web/client/pages/process/restarting.js: -------------------------------------------------------------------------------- 1 | var ProcessPage = require('../process') 2 | var templates = require('../../templates') 3 | 4 | module.exports = ProcessPage.extend({ 5 | template: templates.pages.process.restarting 6 | }) 7 | -------------------------------------------------------------------------------- /web/client/pages/host/connectiontimedout.js: -------------------------------------------------------------------------------- 1 | var HostPage = require('../host') 2 | var templates = require('../../templates') 3 | 4 | module.exports = HostPage.extend({ 5 | template: templates.includes.pages.connectiontimedout 6 | }) 7 | -------------------------------------------------------------------------------- /web/client/pages/process/uninitialised.js: -------------------------------------------------------------------------------- 1 | var ProcessPage = require('../process') 2 | var templates = require('../../templates') 3 | 4 | module.exports = ProcessPage.extend({ 5 | template: templates.pages.process.uninitialised 6 | }) 7 | -------------------------------------------------------------------------------- /web/templates/pages/host/timeout.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 6 |
    7 | -------------------------------------------------------------------------------- /test/integration/fixtures/intermittently-crashy.js: -------------------------------------------------------------------------------- 1 | // crash every 10-20 seconds - e.g. not often enough to cause the process to be aborted 2 | setTimeout(function () { 3 | throw new Error('I die!') 4 | }, ~~(Math.random() * 10000) + 10000) 5 | -------------------------------------------------------------------------------- /web/client/views/process/exceptionlist/empty.js: -------------------------------------------------------------------------------- 1 | var View = require('ampersand-view') 2 | var templates = require('../../../templates') 3 | 4 | module.exports = View.extend({ 5 | template: templates.includes.process.exceptionlist.empty 6 | }) 7 | -------------------------------------------------------------------------------- /web/client/views/process/snapshotlist/empty.js: -------------------------------------------------------------------------------- 1 | var View = require('ampersand-view') 2 | var templates = require('../../../templates') 3 | 4 | module.exports = View.extend({ 5 | template: templates.includes.process.snapshotlist.empty 6 | }) 7 | -------------------------------------------------------------------------------- /test/integration/fixtures/exceptional.js: -------------------------------------------------------------------------------- 1 | /* 2 | process.on('uncaughtException', function uncaughtListener (err) { 3 | console.info('i caught an exception') 4 | }) 5 | */ 6 | setTimeout(function () { 7 | throw new Error('panic!') 8 | }, 1000) 9 | -------------------------------------------------------------------------------- /web/client/pages/loadinghosts.js: -------------------------------------------------------------------------------- 1 | var PageView = require('./base') 2 | var templates = require('../templates') 3 | 4 | module.exports = PageView.extend({ 5 | pageTitle: 'Guvnor - loading hosts', 6 | template: templates.pages.loadinghosts 7 | }) 8 | -------------------------------------------------------------------------------- /web/templates/pages/nohosts.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 5 |
    6 | -------------------------------------------------------------------------------- /web/public/css/app/apps.styl: -------------------------------------------------------------------------------- 1 | .panel-apps p 2 | margin: 0 3 | 4 | .apps-list .remove 5 | white-space: normal 6 | 7 | button 8 | margin-bottom: 3px 9 | 10 | /* 11 | .apps .install-button 12 | float: right 13 | margin-top: -19px*/ 14 | -------------------------------------------------------------------------------- /web/templates/pages/host/networkdown.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 6 |
    7 | -------------------------------------------------------------------------------- /web/templates/pages/host/error.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 6 |
    7 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "asi": true, 3 | "browser": false, 4 | "browserify": true, 5 | "curly": false, 6 | "expr": true, 7 | "indent": 2, 8 | "loopfunc": true, 9 | "node": true, 10 | "trailing": true, 11 | "undef": true, 12 | "white": true 13 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | coverage 14 | 15 | npm-debug.log 16 | node_modules 17 | 18 | .idea 19 | *.iml 20 | 21 | .DS_Store 22 | Thumbs.db 23 | .vagrant -------------------------------------------------------------------------------- /web/templates/pages/host/errorconnecting.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 6 |
    7 | -------------------------------------------------------------------------------- /web/client/models/apps.js: -------------------------------------------------------------------------------- 1 | var Collection = require('ampersand-rest-collection') 2 | var App = require('./app') 3 | 4 | module.exports = Collection.extend({ 5 | url: function () { 6 | return '/hosts/' + this.parent.name + '/apps' 7 | }, 8 | model: App 9 | }) 10 | -------------------------------------------------------------------------------- /test/integration/fixtures/stdin.js: -------------------------------------------------------------------------------- 1 | process.stdin.resume() 2 | 3 | process.stdin.on('data', function(buffer) { 4 | 5 | // got some input, inform guvnor 6 | process.send({ 7 | event: 'stdin:received', 8 | args: [buffer.toString('utf8').trim()] 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /web/templates/forms/controls/select.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 |
    5 |
    6 |
    7 |
    -------------------------------------------------------------------------------- /web/templates/head.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /web/templates/forms/controls/input.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 |
    5 |
    6 |
    7 |
    8 | -------------------------------------------------------------------------------- /web/templates/pages/host/connectionrefused.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 6 |
    7 | -------------------------------------------------------------------------------- /web/templates/forms/controls/checkbox.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 |
    5 |
    6 |
    7 |
    8 | -------------------------------------------------------------------------------- /web/templates/includes/process/exceptionlist/entry.hbs: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /web/templates/pages/host/connectionreset.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 6 |
    7 | -------------------------------------------------------------------------------- /web/templates/pages/host/overview.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    {{model.name}}

    5 | 6 | 7 |
    8 |
    9 |
    10 | -------------------------------------------------------------------------------- /test/integration/fixtures/receive-event.js: -------------------------------------------------------------------------------- 1 | console.info('hello') 2 | 3 | process.on('custom:event:sent', function () { 4 | console.info('received event') 5 | 6 | process.send({ 7 | event: 'custom:event:received', 8 | args: Array.prototype.slice.call(arguments) 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /web/templates/pages/host/hostnotfound.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 6 |
    7 | -------------------------------------------------------------------------------- /web/client/models/user.js: -------------------------------------------------------------------------------- 1 | var AmpersandModel = require('ampersand-model') 2 | 3 | module.exports = AmpersandModel.extend({ 4 | idAttribute: 'uid', 5 | props: { 6 | name: 'string', 7 | group: 'string', 8 | groups: ['array', true, function () { 9 | return [] 10 | }] 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /web/client/models/installation.js: -------------------------------------------------------------------------------- 1 | var AmpersandModel = require('ampersand-model') 2 | var Logs = require('./logs') 3 | 4 | module.exports = AmpersandModel.extend({ 5 | props: { 6 | id: 'string', 7 | name: 'string', 8 | url: 'string' 9 | }, 10 | collections: { 11 | logs: Logs 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /web/client/models/logs.js: -------------------------------------------------------------------------------- 1 | var Collection = require('ampersand-rest-collection') 2 | var Log = require('./log') 3 | 4 | module.exports = Collection.extend({ 5 | url: function () { 6 | return '/hosts/' + this.parent.collection.parent.name + '/processes/' + this.parent.id + '/logs' 7 | }, 8 | model: Log 9 | }) 10 | -------------------------------------------------------------------------------- /web/public/javascript/highcharts/README.md: -------------------------------------------------------------------------------- 1 | This is the official shim repo for Highcharts JS releases including minified files. It is optimized for inclusion by Bower. 2 | 3 | For the Highcharts source code, issue tracker and support utilities, see [the highcharts.com repo](https://github.com/highslide-software/highcharts.com). 4 | -------------------------------------------------------------------------------- /web/templates/forms/controls/tuple.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 | remove 5 |
    6 |
    7 |
    8 |
    9 | -------------------------------------------------------------------------------- /web/templates/includes/process/overview/cpu.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    CPU usage

    4 |
    5 |
    6 |
    7 |
    8 |
    9 | -------------------------------------------------------------------------------- /web/templates/includes/process/overview/memory.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    Memory usage

    4 |
    5 |
    6 |
    7 |
    8 |
    9 | -------------------------------------------------------------------------------- /test/integration/fixtures/http-server.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | var port = process.env.PORT || 9000 3 | 4 | http.createServer(function (req, res) { 5 | res.writeHead(200, {'Content-Type': 'text/plain'}) 6 | res.end('DERP DERP') 7 | }).listen(port) 8 | 9 | console.log('Server listening on %d - pid %d', port, process.pid) 10 | -------------------------------------------------------------------------------- /vagrant/bootstrap-node.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Setup nvm 4 | git clone https://github.com/creationix/nvm.git ~/.nvm 5 | cd ~/.nvm 6 | git checkout `git describe --abbrev=0 --tags` 7 | 8 | echo source ~/.nvm/nvm.sh >> ~/.profile 9 | 10 | source ~/.profile 11 | 12 | nvm install 0.12 13 | 14 | echo nvm use 0.12 >> ~/.profile 15 | -------------------------------------------------------------------------------- /web/templates/includes/process/overview/latency.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    Event loop latency

    4 |
    5 |
    6 |
    7 |
    8 |
    9 | -------------------------------------------------------------------------------- /web/templates/includes/process/startOrRemove.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 |
    5 | -------------------------------------------------------------------------------- /web/client/models/snapshots.js: -------------------------------------------------------------------------------- 1 | var Collection = require('ampersand-rest-collection') 2 | var Snapshot = require('./snapshot') 3 | 4 | module.exports = Collection.extend({ 5 | url: function () { 6 | return '/hosts/' + this.parent.collection.parent.name + '/processes/' + this.parent.id + '/snapshots' 7 | }, 8 | model: Snapshot 9 | }) 10 | -------------------------------------------------------------------------------- /vagrant/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Update system and install git and a compiler 4 | apt-get update 5 | apt-get install -y build-essential git 6 | 7 | # Install some nodes and some io.jss for root and vagrant 8 | chmod +x /vagrant/bootstrap-node.sh 9 | /vagrant/bootstrap-node.sh 10 | sudo -u vagrant -i /vagrant/bootstrap-node.sh 11 | -------------------------------------------------------------------------------- /web/client/models/exceptions.js: -------------------------------------------------------------------------------- 1 | var Collection = require('ampersand-rest-collection') 2 | var Exception = require('./exception') 3 | 4 | module.exports = Collection.extend({ 5 | url: function () { 6 | return '/hosts/' + this.parent.collection.parent.name + '/processes/' + this.parent.id + '/exceptions' 7 | }, 8 | model: Exception 9 | }) 10 | -------------------------------------------------------------------------------- /web/client/models/users.js: -------------------------------------------------------------------------------- 1 | var Collection = require('ampersand-rest-collection') 2 | var User = require('./user') 3 | 4 | module.exports = Collection.extend({ 5 | url: function () { 6 | return '/hosts/' + this.parent.name + '/users' 7 | }, 8 | model: User, 9 | mainIndex: 'name', 10 | comparator: 'name', 11 | textAttribute: 'name' 12 | }) 13 | -------------------------------------------------------------------------------- /web/templates/pages/host/incompatible.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    {{name}} is running a version of guvnor incompatible with this version of guvnor-web

    4 |

    Please run a version that satisfies {{requiredVersion}}.

    5 |
    6 |
    7 | -------------------------------------------------------------------------------- /web/public/css/app/processes.styl: -------------------------------------------------------------------------------- 1 | .panel-processes p 2 | margin: 0 3 | 4 | .process-list 5 | .pid, .uptime, .restarts, .cpu, .memory 6 | width: 80px 7 | 8 | @media (max-width: 512px) 9 | .process-list 10 | .pid, .uptime, .restarts 11 | display: none 12 | 13 | @media (max-width: 640px) 14 | .process-list 15 | .pid 16 | display: none 17 | -------------------------------------------------------------------------------- /web/templates/forms/controls/element.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 |
    5 |
    6 | 7 |
    8 |
    9 | remove 10 |
    11 |
    12 | -------------------------------------------------------------------------------- /web/public/css/app/app.styl: -------------------------------------------------------------------------------- 1 | .install-log 2 | list-style: none 3 | padding: 5px 0 4 | margin: 20px 5 | background-color: #000 6 | color: #FFF 7 | font-family: 'Lucida Console', Monaco, monospace 8 | height: 350px 9 | overflow: auto 10 | 11 | li 12 | padding: 0 5px 13 | white-space: nowrap 14 | 15 | li.error 16 | background-color: #330100 17 | -------------------------------------------------------------------------------- /web/templates/includes/host/resources.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    Resource usage

    4 |
    5 |
    6 |
    7 |
    8 |
    9 |
    -------------------------------------------------------------------------------- /guvnor-web-users: -------------------------------------------------------------------------------- 1 | ; # User configuration 2 | ; 3 | ; Users should follow this format: 4 | ; 5 | ; [username] 6 | ; password = a password 7 | ; 8 | ; Then set up one or more per-host mappings for keys/secrets (where 'hostname' 9 | ; is a hostname previously declared in bossweb-hostsrc) 10 | ; 11 | ; [username.hostname] 12 | ; key = user key 13 | ; secret = longish string 14 | -------------------------------------------------------------------------------- /web/templates/forms/controls/array.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 | add 5 |
    6 |
    7 |
    8 |
    9 | -------------------------------------------------------------------------------- /web/templates/includes/process/start.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | 5 | 6 |
    7 |
    8 | -------------------------------------------------------------------------------- /web/client/views/process/loglist/entry.js: -------------------------------------------------------------------------------- 1 | var View = require('ampersand-view') 2 | var templates = require('../../../templates') 3 | 4 | module.exports = View.extend({ 5 | template: templates.includes.process.loglist.entry, 6 | bindings: { 7 | 'model.visible': { 8 | type: 'booleanClass', 9 | selector: 'li', 10 | name: 'visible' 11 | } 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /web/templates/pages/process/overview.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    {{model.name}}

    5 |
    6 |
    7 |
    8 |
    9 |
    10 |
    11 |
    12 | -------------------------------------------------------------------------------- /guvnor-web-hosts: -------------------------------------------------------------------------------- 1 | ; # Host configuration 2 | ; 3 | ; Hosts should follow this format: 4 | ; 5 | ; [hostname] 6 | ; host = my.host.com 7 | ; port = port number 8 | ; user = user the daemon runs as on my.host.com 9 | ; key = user key 10 | ; secret = longish string 11 | ; 12 | ; You may omit the host and port options - if so ${remote.advertise} should be true in /etc/boss/bossrc on that host 13 | -------------------------------------------------------------------------------- /web/templates/includes/apps/refs.hbs: -------------------------------------------------------------------------------- 1 |
    2 |

    Choose a ref from the list below:

    3 |
    4 |
    5 |
    6 |
    7 | 8 | 9 |
    10 |
    11 | -------------------------------------------------------------------------------- /web/client/views/host/system.js: -------------------------------------------------------------------------------- 1 | var View = require('ampersand-view') 2 | var templates = require('../../templates') 3 | 4 | module.exports = View.extend({ 5 | template: templates.includes.host.system, 6 | bindings: { 7 | 'model.uptimeFormatted': { 8 | type: 'text', 9 | hook: 'uptime' 10 | }, 11 | 'model.cpuSpeed': { 12 | type: 'text', 13 | hook: 'cpuSpeed' 14 | } 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /lib/daemon/common/LatencyMonitor.js: -------------------------------------------------------------------------------- 1 | var Autowire = require('wantsit').Autowire 2 | 3 | var LatencyMonitor = function () { 4 | this._lag = Autowire 5 | } 6 | 7 | LatencyMonitor.prototype.afterPropertiesSet = function () { 8 | var lag = this._lag(1000) 9 | 10 | Object.defineProperty(this, 'latency', { 11 | get: function () { 12 | return Math.max(0, lag()) 13 | } 14 | }) 15 | } 16 | 17 | module.exports = LatencyMonitor 18 | -------------------------------------------------------------------------------- /web/templates/includes/hostlist/host.hbs: -------------------------------------------------------------------------------- 1 |
  • 2 | 8 |
  • 9 | -------------------------------------------------------------------------------- /web/client/helpers/notification.js: -------------------------------------------------------------------------------- 1 | require('bootstrap-notify') 2 | var jQuery = require('jquery') 3 | 4 | var sprintf = require('sprintf-js').sprintf 5 | 6 | module.exports = function (options) { 7 | if (!Array.isArray(options.message)) { 8 | options.message = [options.message] 9 | } 10 | 11 | jQuery.notify('

    ' + options.header + '

    ' + sprintf.apply(null, options.message), { 12 | type: options.type ? options.type : 'info', 13 | offset: 15 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /web/templates/includes/processlist/process.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{model.name}}
    {{model.script}} 3 | {{model.pid}} 4 | {{model.uptimeFormatted}} 5 | {{model.restarts}} 6 | {{model.memoryFormatted}} 7 | {{model.cpuFormatted}} 8 | 9 | -------------------------------------------------------------------------------- /web/templates/pages/host/badsignature.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 7 |
    8 | -------------------------------------------------------------------------------- /test/integration/fixtures/colourful.js: -------------------------------------------------------------------------------- 1 | var colors = require('colors') 2 | 3 | var colours = ['red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', 'grey'] 4 | var text = [ 5 | 'Wow', 6 | 'Such colour', 7 | 'So bright', 8 | 'Joy', 9 | 'Too much!', 10 | 'Many shades', 11 | 'Rainbo' 12 | ] 13 | 14 | function rand(arr) { 15 | return arr[Math.floor(Math.random() * arr.length)] 16 | } 17 | 18 | setInterval(function () { 19 | console.log(rand(text)[rand(colours)]) 20 | }, 1000) 21 | -------------------------------------------------------------------------------- /web/client/pages/host/incompatible.js: -------------------------------------------------------------------------------- 1 | var HostPage = require('../host') 2 | var templates = require('../../templates') 3 | var config = require('clientconfig') 4 | 5 | module.exports = HostPage.extend({ 6 | template: templates.pages.host.incompatible, 7 | render: function () { 8 | this.renderWithTemplate({ 9 | name: this.model.name, 10 | version: this.model.version, 11 | requiredVersion: config.minVersion 12 | }, templates.includes.host.incompatible) 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /web/public/css/_variables.styl: -------------------------------------------------------------------------------- 1 | // colors 2 | 3 | $header = #404040 4 | $bodybg = #202020 5 | 6 | $red = #c80000 7 | $text = #333 8 | $subtext = #777 9 | 10 | $pink = #EB008B 11 | $ltgray = #EBECEC 12 | $dkgray = #202020 13 | $blue = #00aeef 14 | $dkblue = #006991 15 | 16 | // font sizes 17 | 18 | $fontLG = 20px 19 | $fontMD = 16px 20 | $fontSM = 13px 21 | 22 | // dimensions 23 | 24 | $base = 10px 25 | $seven = 7 * $base 26 | $five = 5 * $base 27 | $three = 3 * $base 28 | $page = 38 * $base 29 | -------------------------------------------------------------------------------- /lib/common/HelpfulError.js: -------------------------------------------------------------------------------- 1 | // monkey patch the Error type to serialise properties to JSON - otherwise we end up with empty objects in the browser. 2 | Object.defineProperty(Error.prototype, 'toJSON', { 3 | value: function () { 4 | var alt = {} 5 | 6 | Object.getOwnPropertyNames(this).forEach(function (key) { 7 | alt[key] = this[key] 8 | }, this) 9 | 10 | if (!alt.message && alt.code) { 11 | alt.message = alt.code 12 | } 13 | 14 | return alt 15 | }, 16 | configurable: true 17 | }) 18 | -------------------------------------------------------------------------------- /test/lib/daemon/domain/RemoteUserTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | RemoteUser = require('../../../../lib/daemon/domain/RemoteUser') 3 | 4 | describe('RemoteUser', function () { 5 | it('should set name and secret from options', function () { 6 | var options = { 7 | name: 'foo', 8 | secret: 'bar' 9 | } 10 | var remoteUser = new RemoteUser(options) 11 | 12 | expect(remoteUser.name).to.equal(options.name) 13 | expect(remoteUser.secret).to.equal(options.secret) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /web/templates/pages/process/started.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    {{model.name}}

    5 |
    6 |
    7 |

    Started

    8 |
    9 |
    10 |

    {{model.name}} started...

    11 |
    12 |
    13 |
    14 |
    15 |
    16 | -------------------------------------------------------------------------------- /web/templates/pages/process/stopping.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    {{model.name}}

    5 |
    6 |
    7 |

    Starting

    8 |
    9 |
    10 |

    {{model.name}} stopping...

    11 |
    12 |
    13 |
    14 |
    15 |
    16 | -------------------------------------------------------------------------------- /web/templates/includes/process/snapshotlist/entry.hbs: -------------------------------------------------------------------------------- 1 | 2 | Date 3 | Size 4 | Path 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /web/templates/pages/process/restarting.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    {{model.name}}

    5 |
    6 |
    7 |

    Starting

    8 |
    9 |
    10 |

    {{model.name}} restarting...

    11 |
    12 |
    13 |
    14 |
    15 |
    16 | -------------------------------------------------------------------------------- /web/templates/pages/process/uninitialised.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    {{model.name}}

    5 |
    6 |
    7 |

    Starting

    8 |
    9 |
    10 |

    {{model.name}} uninitialised...

    11 |
    12 |
    13 |
    14 |
    15 |
    16 | -------------------------------------------------------------------------------- /web/client/pages/process/starting.js: -------------------------------------------------------------------------------- 1 | var ProcessPage = require('../process') 2 | var templates = require('../../templates') 3 | var StopButton = require('../../buttons/stop') 4 | 5 | module.exports = ProcessPage.extend({ 6 | template: templates.pages.process.starting, 7 | subviews: { 8 | stopButton: { 9 | container: '[data-hook=stopbutton]', 10 | prepareView: function (el) { 11 | return new StopButton({ 12 | el: el, 13 | model: this.model 14 | }) 15 | } 16 | } 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /test/web/client/models/snapshotsTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | Snapshots = require('../../../../web/client/models/snapshots') 3 | 4 | describe('snapshots', function () { 5 | it('should format url correctly', function () { 6 | var snapshots = new Snapshots() 7 | snapshots.parent = { 8 | id: 'bar', 9 | collection: { 10 | parent: { 11 | name: 'foo' 12 | } 13 | } 14 | } 15 | 16 | expect(snapshots.url()).to.equal('/hosts/foo/processes/bar/snapshots') 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /web/templates/includes/apps/install.hbs: -------------------------------------------------------------------------------- 1 |
    2 |

    To install an app, specify a git url that contains a package.json file at it's root.

    3 |

    If you omit the app name, it will be taken from the package.json file.

    4 |
    5 |
    6 |
    7 |
    8 | 9 | 10 |
    11 |
    12 | -------------------------------------------------------------------------------- /lib/common/ExecSync.js: -------------------------------------------------------------------------------- 1 | var child_process = require('child_process') 2 | 3 | if (child_process.execSync) { 4 | // node 0.12+, io.js 5 | module.exports = child_process.execSync.bind(child_process) 6 | } else { 7 | // node 0.10 8 | var execSync = require('execSync') 9 | 10 | module.exports = function () { 11 | var result = execSync.exec.apply(execSync, arguments) 12 | 13 | if (result.code !== 0) { 14 | throw new Error('Command failed with code ' + result.code) 15 | } 16 | 17 | return result.stdout.trim() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/integration/fixtures/talky.js: -------------------------------------------------------------------------------- 1 | var util = require('util') 2 | 3 | setInterval(function () { 4 | console.log('This is a console.log') 5 | console.info('This is a console.info') 6 | console.error('This is an console.error') 7 | console.warn('This is an console.warn') 8 | console.time('This is a console.time') 9 | console.dir('This is a console.dir') 10 | console.timeEnd('This is a console.time') 11 | console.trace() 12 | 13 | util.debug('This is a util.debug') 14 | util.log('This is a util.log with html') 15 | }, 2000) 16 | -------------------------------------------------------------------------------- /test/integration/fixtures/jibberjabber.js: -------------------------------------------------------------------------------- 1 | var util = require('util') 2 | 3 | setInterval(function () { 4 | console.log('This is a console.log') 5 | console.info('This is a console.info') 6 | console.error('This is an console.error') 7 | console.warn('This is an console.warn') 8 | console.time('This is a console.time') 9 | console.dir('This is a console.dir') 10 | console.timeEnd('This is a console.time') 11 | console.trace() 12 | 13 | util.debug('This is a util.debug') 14 | util.log('This is a util.log with html') 15 | }, 2000) 16 | -------------------------------------------------------------------------------- /lib/web/resources/Host.js: -------------------------------------------------------------------------------- 1 | var Autowire = require('wantsit').Autowire 2 | 3 | var Host = function () { 4 | this._hostList = Autowire 5 | } 6 | 7 | Host.prototype.retrieve = function (request, reply) { 8 | var host = this._hostList.getHostByName(request.params.hostId) 9 | 10 | if (host) { 11 | reply(host) 12 | } else { 13 | reply('No host found for name ' + request.params.hostId).code(404) 14 | } 15 | } 16 | 17 | Host.prototype.retrieveAll = function (request, reply) { 18 | reply(this._hostList.getHosts()) 19 | } 20 | 21 | module.exports = Host 22 | -------------------------------------------------------------------------------- /web/client/buttons/debug.js: -------------------------------------------------------------------------------- 1 | var View = require('ampersand-view') 2 | var templates = require('../templates') 3 | 4 | module.exports = View.extend({ 5 | template: templates.buttons.debug, 6 | events: { 7 | 'click [data-hook=debugbutton]': 'debugProcess' 8 | }, 9 | debugProcess: function (event) { 10 | event.target.blur() 11 | 12 | window.open('http://' + 13 | this.model.collection.parent.host + 14 | ':' + 15 | this.model.collection.parent.debuggerPort + 16 | '/debug?port=' + 17 | this.model.debugPort 18 | ) 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /web/client/models/start.js: -------------------------------------------------------------------------------- 1 | var AmpersandModel = require('ampersand-model') 2 | 3 | module.exports = AmpersandModel.extend({ 4 | props: { 5 | id: 'string', 6 | name: 'string', 7 | user: 'string', 8 | group: 'string', 9 | script: 'string', 10 | cwd: 'string', 11 | env: ['object', true, function () { 12 | return {} 13 | }], 14 | argv: ['array', true, function () { 15 | return [] 16 | }], 17 | execArgv: ['array', true, function () { 18 | return [] 19 | }], 20 | instances: ['number', true, 1] 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /web/templates/pages/process/starting.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    {{model.name}}

    5 |
    6 |
    7 |

    Starting

    8 |
    9 |
    10 |

    {{model.name}} starting...

    11 |
    12 |
    13 |
    14 |
    15 |
    16 |
    17 | -------------------------------------------------------------------------------- /web/client/pages/base.js: -------------------------------------------------------------------------------- 1 | // base view for pages 2 | var View = require('ampersand-view') 3 | 4 | module.exports = View.extend({ 5 | // register keyboard handlers 6 | registerKeyboardShortcuts: function () { 7 | /* 8 | var self = this 9 | _.each(this.keyboardShortcuts, function (value, k) { 10 | // register key handler scoped to this page 11 | key(k, self.cid, _.bind(self[value], self)) 12 | }) 13 | key.setScope(this.cid) 14 | */ 15 | }, 16 | unregisterKeyboardShortcuts: function () { 17 | // key.deleteScope(this.cid) 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /web/templates/pages/process/stopped.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    {{model.name}}

    5 |
    6 |
    7 |

    Stopped

    8 |
    9 |
    10 |

    {{model.name}} is not running.

    11 |
    12 |
    13 |
    14 |
    15 |
    16 |
    17 |
    18 | -------------------------------------------------------------------------------- /web/client/pages/process/paused.js: -------------------------------------------------------------------------------- 1 | var ProcessPage = require('../process') 2 | var templates = require('../../templates') 3 | 4 | module.exports = ProcessPage.extend({ 5 | template: templates.pages.process.paused, 6 | events: { 7 | 'click button.process-debug': 'debugProcess' 8 | }, 9 | debugProcess: function (event) { 10 | event.target.blur() 11 | 12 | window.open('http://' + 13 | this.model.collection.parent.host + 14 | ':' + 15 | this.model.collection.parent.debuggerPort + 16 | '/debug?port=' + 17 | this.model.debugPort 18 | ) 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /test/web/client/models/snapshotTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | Snapshot = require('../../../../web/client/models/snapshot') 3 | 4 | describe('snapshot', function () { 5 | it('should return formatted size', function () { 6 | var snapshot = new Snapshot({ 7 | size: 5 8 | }) 9 | 10 | expect(snapshot.sizeFormatted).to.equal('5 Bytes') 11 | }) 12 | 13 | it('should return formatted date', function () { 14 | var snapshot = new Snapshot({ 15 | date: 10 16 | }) 17 | 18 | // ignore timezone offset.. 19 | expect(snapshot.dateFormatted).to.contain('1970-01-01') 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "4" 5 | - "5" 6 | # node_js 4 requires gcc 4.8 7 | env: 8 | - NODE_ENV=travis CXX="g++-4.8" 9 | # gcc 4.8 requires ubuntu-toolchain-r-test 10 | addons: 11 | apt: 12 | sources: 13 | - ubuntu-toolchain-r-test 14 | packages: 15 | - g++-4.8 16 | - gcc-4.8 17 | after_script: npm run coveralls 18 | notifications: 19 | webhooks: 20 | urls: 21 | - https://webhooks.gitter.im/e/0ab204e5cf80192468dd 22 | on_success: change # options: [always|never|change] default: always 23 | on_failure: always # options: [always|never|change] default: always 24 | on_start: false # default: false 25 | -------------------------------------------------------------------------------- /lib/web/resources/HostProcessLog.js: -------------------------------------------------------------------------------- 1 | var Autowire = require('wantsit').Autowire 2 | 3 | var HostProcessLogs = function () { 4 | this._hostList = Autowire 5 | } 6 | 7 | HostProcessLogs.prototype.retrieveAll = function (request, reply) { 8 | var host = this._hostList.getHostByName(request.params.hostId) 9 | 10 | if (!host) { 11 | return reply('No host found for name ' + request.params.hostId).code(404) 12 | } 13 | 14 | var proc = host.findProcessById(request.params.processId) 15 | 16 | if (!proc) { 17 | return reply('No process found for id ' + request.params.processId).code(404) 18 | } 19 | 20 | reply(proc.logs) 21 | } 22 | 23 | module.exports = HostProcessLogs 24 | -------------------------------------------------------------------------------- /lib/web/resources/HostProcessCPU.js: -------------------------------------------------------------------------------- 1 | var Autowire = require('wantsit').Autowire 2 | 3 | var HostProcessCPU = function () { 4 | this._hostList = Autowire 5 | } 6 | 7 | HostProcessCPU.prototype.retrieveOne = function (request, reply) { 8 | var host = this._hostList.getHostByName(request.params.hostId) 9 | 10 | if (!host) { 11 | return reply('No host found for name ' + request.params.hostId).code(404) 12 | } 13 | 14 | var proc = host.findProcessById(request.params.processId) 15 | 16 | if (!proc) { 17 | return reply('No process found for id ' + request.params.processId).code(404) 18 | } 19 | 20 | reply(proc.usage.cpu) 21 | } 22 | 23 | module.exports = HostProcessCPU 24 | -------------------------------------------------------------------------------- /web/client/forms/envProp.js: -------------------------------------------------------------------------------- 1 | var FormView = require('ampersand-form-view') 2 | var InputView = require('ampersand-input-view') 3 | 4 | module.exports = FormView.extend({ 5 | fields: function () { 6 | return [ 7 | new InputView({ 8 | label: 'Name', 9 | name: 'name', 10 | value: this.model.name || '', 11 | required: false, 12 | placeholder: 'Name', 13 | parent: this 14 | }), 15 | new InputView({ 16 | label: 'Value', 17 | name: 'value', 18 | value: this.model.value || '', 19 | required: false, 20 | placeholder: 'Value', 21 | parent: this 22 | }) 23 | ] 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /web/templates/pages/process/unresponsive.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    {{model.name}}

    5 |
    6 |
    7 |

    Unresponsive

    8 |
    9 |
    10 |

    {{model.name}} is unresponsive. You may try to debug, restart or stop the process.

    11 |
    12 |
    13 |
    14 |
    15 |
    16 |
    17 |
    18 |
    19 | -------------------------------------------------------------------------------- /lib/cli/Cluster.js: -------------------------------------------------------------------------------- 1 | var util = require('util') 2 | var Actions = require('./Actions') 3 | 4 | var Cluster = function () { 5 | Actions.call(this) 6 | } 7 | util.inherits(Cluster, Actions) 8 | 9 | Cluster.prototype.setClusterWorkers = function (pidOrNames, workers, options) { 10 | workers = parseInt(workers, 10) 11 | 12 | if (isNaN(workers)) { 13 | return this._logger.error('Please pass a number for cluster workers') 14 | } 15 | 16 | this._withEach(pidOrNames, options, function (managedProcess, guvnor, done) { 17 | this._logger.debug('Setting cluster workers to', workers) 18 | managedProcess.setClusterWorkers(workers, done) 19 | }.bind(this)) 20 | } 21 | 22 | module.exports = Cluster 23 | -------------------------------------------------------------------------------- /web/client/forms/refs.js: -------------------------------------------------------------------------------- 1 | var FormView = require('ampersand-form-view') 2 | var SelectView = require('ampersand-select-view') 3 | var templates = require('../templates') 4 | 5 | module.exports = FormView.extend({ 6 | template: templates.includes.apps.refs, 7 | fields: function () { 8 | return [ 9 | new SelectView({ 10 | label: 'Ref', 11 | name: 'ref', 12 | value: this.model.ref, 13 | options: this.model.refs.map(function (ref) { 14 | return ref.name 15 | }), 16 | parent: this, 17 | required: true, 18 | eagerValidate: true, 19 | template: templates.forms.controls.select() 20 | }) 21 | ] 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /lib/web/resources/HostProcessLatency.js: -------------------------------------------------------------------------------- 1 | var Autowire = require('wantsit').Autowire 2 | 3 | var HostProcessLatency = function () { 4 | this._hostList = Autowire 5 | } 6 | 7 | HostProcessLatency.prototype.retrieveOne = function (request, reply) { 8 | var host = this._hostList.getHostByName(request.params.hostId) 9 | 10 | if (!host) { 11 | return reply('No host found for name ' + request.params.hostId).code(404) 12 | } 13 | 14 | var proc = host.findProcessById(request.params.processId) 15 | 16 | if (!proc) { 17 | return reply('No process found for id ' + request.params.processId).code(404) 18 | } 19 | 20 | reply(proc.usage.latency) 21 | } 22 | 23 | module.exports = HostProcessLatency 24 | -------------------------------------------------------------------------------- /lib/web/resources/HostProcessException.js: -------------------------------------------------------------------------------- 1 | var Autowire = require('wantsit').Autowire 2 | 3 | var HostProcessExceptions = function () { 4 | this._hostList = Autowire 5 | } 6 | 7 | HostProcessExceptions.prototype.retrieveAll = function (request, reply) { 8 | var host = this._hostList.getHostByName(request.params.hostId) 9 | 10 | if (!host) { 11 | return reply('No host found for name ' + request.params.hostId).code(404) 12 | } 13 | 14 | var proc = host.findProcessById(request.params.processId) 15 | 16 | if (!proc) { 17 | return reply('No process found for id ' + request.params.processId).code(404) 18 | } 19 | 20 | reply(proc.exceptions) 21 | } 22 | 23 | module.exports = HostProcessExceptions 24 | -------------------------------------------------------------------------------- /lib/web/resources/HostProcessHeapUsed.js: -------------------------------------------------------------------------------- 1 | var Autowire = require('wantsit').Autowire 2 | 3 | var HostProcessHeapUsed = function () { 4 | this._hostList = Autowire 5 | } 6 | 7 | HostProcessHeapUsed.prototype.retrieveOne = function (request, reply) { 8 | var host = this._hostList.getHostByName(request.params.hostId) 9 | 10 | if (!host) { 11 | return reply('No host found for name ' + request.params.hostId).code(404) 12 | } 13 | 14 | var proc = host.findProcessById(request.params.processId) 15 | 16 | if (!proc) { 17 | return reply('No process found for id ' + request.params.processId).code(404) 18 | } 19 | 20 | reply(proc.usage.heapUsed) 21 | } 22 | 23 | module.exports = HostProcessHeapUsed 24 | -------------------------------------------------------------------------------- /lib/web/resources/HostProcessHeapTotal.js: -------------------------------------------------------------------------------- 1 | var Autowire = require('wantsit').Autowire 2 | 3 | var HostProcessHeapTotal = function () { 4 | this._hostList = Autowire 5 | } 6 | 7 | HostProcessHeapTotal.prototype.retrieveOne = function (request, reply) { 8 | var host = this._hostList.getHostByName(request.params.hostId) 9 | 10 | if (!host) { 11 | return reply('No host found for name ' + request.params.hostId).code(404) 12 | } 13 | 14 | var proc = host.findProcessById(request.params.processId) 15 | 16 | if (!proc) { 17 | return reply('No process found for id ' + request.params.processId).code(404) 18 | } 19 | 20 | reply(proc.usage.heapTotal) 21 | } 22 | 23 | module.exports = HostProcessHeapTotal 24 | -------------------------------------------------------------------------------- /web/client/models/snapshot.js: -------------------------------------------------------------------------------- 1 | var AmpersandModel = require('ampersand-model') 2 | var prettysize = require('prettysize') 3 | var moment = require('moment') 4 | 5 | module.exports = AmpersandModel.extend({ 6 | props: { 7 | id: 'string', 8 | date: 'number', 9 | path: 'string', 10 | size: 'number' 11 | }, 12 | derived: { 13 | sizeFormatted: { 14 | deps: ['string'], 15 | fn: function () { 16 | return prettysize(this.size) 17 | } 18 | }, 19 | dateFormatted: { 20 | deps: ['date'], 21 | fn: function () { 22 | var date = new Date(this.date) 23 | 24 | return moment(date).format('YYYY-MM-DD HH:mm:ss Z') 25 | } 26 | } 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /lib/web/resources/HostProcessResidentSize.js: -------------------------------------------------------------------------------- 1 | var Autowire = require('wantsit').Autowire 2 | 3 | var HostProcessResidentSize = function () { 4 | this._hostList = Autowire 5 | } 6 | 7 | HostProcessResidentSize.prototype.retrieveOne = function (request, reply) { 8 | var host = this._hostList.getHostByName(request.params.hostId) 9 | 10 | if (!host) { 11 | return reply('No host found for name ' + request.params.hostId).code(404) 12 | } 13 | 14 | var proc = host.findProcessById(request.params.processId) 15 | 16 | if (!proc) { 17 | return reply('No process found for id ' + request.params.processId).code(404) 18 | } 19 | 20 | reply(proc.usage.residentSize) 21 | } 22 | 23 | module.exports = HostProcessResidentSize 24 | -------------------------------------------------------------------------------- /lib/daemon/rpc/AdminRPC.js: -------------------------------------------------------------------------------- 1 | var RPCEndpoint = require('./RPCEndpoint') 2 | var util = require('util') 3 | 4 | var AdminRPC = function () { 5 | RPCEndpoint.call(this) 6 | } 7 | util.inherits(AdminRPC, RPCEndpoint) 8 | 9 | AdminRPC.prototype._getSocketName = function () { 10 | return 'admin.socket' 11 | } 12 | 13 | AdminRPC.prototype._getApi = function () { 14 | return [ 15 | 'kill', 'remoteHostConfig', 'addRemoteUser', 'removeRemoteUser', 'listRemoteUsers', 16 | 'rotateRemoteUserKeys', 'generateRemoteRpcCertificates', 'startProcessAsUser', 17 | 'dumpProcesses', 'restoreProcesses' 18 | ] 19 | } 20 | 21 | AdminRPC.prototype._getUmask = function () { 22 | return parseInt('077', 8) 23 | } 24 | 25 | module.exports = AdminRPC 26 | -------------------------------------------------------------------------------- /web/templates/includes/hostlist/process.hbs: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /web/templates/pages/host/install.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    {{model.name}}

    5 |
    6 |
    7 |

    Install app

    8 |
    9 |
    10 |
    11 |
    12 |
    13 | 14 |
    15 |
    16 |
    17 |
    18 |
    19 |
    20 |
    21 | -------------------------------------------------------------------------------- /web/client/models/app.js: -------------------------------------------------------------------------------- 1 | var AmpersandModel = require('ampersand-model') 2 | 3 | module.exports = AmpersandModel.extend({ 4 | props: { 5 | id: 'string', 6 | name: 'string', 7 | user: 'string', 8 | url: 'string', 9 | ref: 'string', 10 | 11 | refs: ['array', true, function () { 12 | return [] 13 | }], 14 | execArgv: ['array', true, function () { 15 | return [] 16 | }], 17 | argv: ['array', true, function () { 18 | return [] 19 | }], 20 | env: ['object', true, function () { 21 | return {} 22 | }], 23 | group: 'string', 24 | debug: 'boolean', 25 | instances: 'number' 26 | }, 27 | session: { 28 | isRemoving: 'boolean', 29 | isStarting: 'boolean' 30 | } 31 | }) 32 | -------------------------------------------------------------------------------- /web/client/pages/host/processes.js: -------------------------------------------------------------------------------- 1 | var HostPage = require('../host') 2 | var templates = require('../../templates') 3 | var CollectionView = require('ampersand-collection-view') 4 | var ProcessView = require('../../views/processlist/process') 5 | var NoProcessesView = require('../../views/processlist/empty') 6 | 7 | module.exports = HostPage.extend({ 8 | template: templates.pages.host.processes, 9 | subviews: { 10 | processes: { 11 | container: '[data-hook=processes]', 12 | prepareView: function (el) { 13 | return new CollectionView({ 14 | el: el, 15 | collection: this.model.processes, 16 | view: ProcessView, 17 | emptyView: NoProcessesView 18 | }) 19 | } 20 | } 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /web/client/views/apps/refs.js: -------------------------------------------------------------------------------- 1 | var View = require('ampersand-view') 2 | var templates = require('../../templates') 3 | var RefsForm = require('../../forms/refs') 4 | 5 | module.exports = View.extend({ 6 | template: templates.includes.apps.refs, 7 | events: { 8 | 'click [data-hook=cancel-button]': 'onCancel' 9 | }, 10 | subviews: { 11 | form: { 12 | container: 'form', 13 | prepareView: function (el) { 14 | return new RefsForm({ 15 | model: this.model, 16 | el: el, 17 | submitCallback: function (data) { 18 | this.onSubmit(data) 19 | }.bind(this) 20 | }) 21 | } 22 | } 23 | }, 24 | onCancel: function () { 25 | }, 26 | onSubmit: function (data) { 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /lib/daemon/common/ConsoleDebugLogger.js: -------------------------------------------------------------------------------- 1 | var util = require('util') 2 | var Console = require('winston').transports.Console 3 | 4 | /** 5 | * A logger intended for use with child processes - if process.send 6 | * is not defined then something has gone wrong so this logger will 7 | * print the log message to the console instead of just swallowing it. 8 | */ 9 | var ConsoleDebugLogger = function (options) { 10 | Console.call(this, options) 11 | } 12 | util.inherits(ConsoleDebugLogger, Console) 13 | 14 | ConsoleDebugLogger.prototype.log = function (level, msg, meta, callback) { 15 | if (this.silent || process.send) { 16 | return callback(null, true) 17 | } 18 | 19 | Console.prototype.log.apply(this, arguments) 20 | } 21 | 22 | module.exports = ConsoleDebugLogger 23 | -------------------------------------------------------------------------------- /lib/daemon/common/LogRedirector.js: -------------------------------------------------------------------------------- 1 | var Autowire = require('wantsit').Autowire 2 | 3 | var LogRedirector = function () { 4 | this._logger = Autowire 5 | } 6 | 7 | LogRedirector.prototype.afterPropertiesSet = function () { 8 | var stderr = process.stderr.write 9 | var stdout = process.stdout.write 10 | 11 | process.stderr.write = function (string, encoding, fd) { 12 | if (!process.send) { 13 | return stderr.apply(process.stderr, arguments) 14 | } 15 | 16 | this._logger.error(string) 17 | }.bind(this) 18 | process.stdout.write = function (string, encoding, fd) { 19 | if (!process.send) { 20 | return stdout.apply(process.stdout, arguments) 21 | } 22 | 23 | this._logger.info(string) 24 | }.bind(this) 25 | } 26 | 27 | module.exports = LogRedirector 28 | -------------------------------------------------------------------------------- /web/public/css/app/host.styl: -------------------------------------------------------------------------------- 1 | .resource-data-graph 2 | width: 400px 3 | height: 160px 4 | margin: 0 auto 20px auto 5 | display: inline-block 6 | 7 | .system-details 8 | .engine, .platform, .arch, .release, .guvnor, .uptime 9 | width: 80px 10 | 11 | @media (max-width: 320px) 12 | .system-details 13 | .engine, .platform, .arch, .release 14 | display: none 15 | 16 | .resource-data-graph 17 | width: 260px 18 | 19 | @media (min-width: 321px) and (max-width: 512px) 20 | .system-details 21 | .engine, .platform, .arch, .release 22 | display: none 23 | 24 | .resource-data-graph 25 | width: 300px 26 | 27 | @media (max-width: 640px) 28 | .system-details 29 | .engine, .arch 30 | display: none 31 | 32 | .resource-data 33 | text-align: center 34 | -------------------------------------------------------------------------------- /web/templates/pages/process/exceptions.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    {{model.name}}

    5 |
    6 |
    7 |

    Exceptions

    8 |
    9 |
    10 |
      11 |
    • Date
    • 12 |
    • Code
    • 13 |
    • Message
    • 14 |
    • 15 |
    16 |
    17 |
    18 |
    19 |
    20 |
    21 | -------------------------------------------------------------------------------- /web/client/views/confirm.js: -------------------------------------------------------------------------------- 1 | var View = require('ampersand-view') 2 | var templates = require('../templates') 3 | var AmpersandModel = require('ampersand-model') 4 | 5 | var Model = AmpersandModel.extend({ 6 | props: { 7 | title: 'string', 8 | message: 'string' 9 | } 10 | }) 11 | 12 | module.exports = View.extend({ 13 | template: templates.includes.confirm, 14 | initialize: function () { 15 | this.model = new Model() 16 | }, 17 | bindings: { 18 | 'model.title': '[data-hook=title]', 19 | 'model.message': '[data-hook=message]' 20 | }, 21 | setTitle: function (title) { 22 | this.model.title = title 23 | }, 24 | setMessage: function (message) { 25 | this.model.message = message 26 | }, 27 | onYes: function () { 28 | }, 29 | onNo: function () { 30 | } 31 | }) 32 | -------------------------------------------------------------------------------- /web/templates/pages/process/paused.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    {{model.name}}

    5 |
    6 |
    7 |

    Paused

    8 |
    9 |
    10 |
    11 | {{model.name}} is running in debug mode, is paused and is waiting for a debugger to attach to port {{model.debugPort}}. Please click the debug button below. 12 |
    13 | 14 |
    15 |
    16 |
    17 |
    18 |
    19 | -------------------------------------------------------------------------------- /web/client/views/apps/install.js: -------------------------------------------------------------------------------- 1 | var View = require('ampersand-view') 2 | var templates = require('../../templates') 3 | var InstallForm = require('../../forms/install') 4 | var App = require('../../models/app') 5 | 6 | module.exports = View.extend({ 7 | template: templates.includes.apps.install, 8 | events: { 9 | 'click [data-hook=cancel-button]': 'onCancel' 10 | }, 11 | subviews: { 12 | form: { 13 | container: 'form', 14 | prepareView: function (el) { 15 | return new InstallForm({ 16 | model: new App(), 17 | el: el, 18 | submitCallback: function (data) { 19 | this.onSubmit(data) 20 | }.bind(this) 21 | }) 22 | } 23 | } 24 | }, 25 | onCancel: function () { 26 | }, 27 | onSubmit: function (data) { 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /web/templates/pages/process/errored.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    {{model.name}}

    5 |
    6 |
    7 |

    Errored

    8 |
    9 |
    10 |
    11 |

    {{model.name}} errored.

    12 |

    This means an error was thrown by your module.

    13 |

    Please check the exception list or logs for more information.

    14 |
    15 |
    16 |
    17 |
    18 |
    19 |
    20 |
    21 |
    22 | -------------------------------------------------------------------------------- /lib/daemon/inspector/index.js: -------------------------------------------------------------------------------- 1 | var DebugServer = require('../../../node_modules/node-inspector/lib/debug-server').DebugServer 2 | 3 | process.title = 'guvnor - node-inspector' 4 | 5 | var debugServer = new DebugServer() 6 | debugServer.start({ 7 | webPort: parseInt(process.env.GUVNOR_NODE_INSPECTOR_PORT, 10), 8 | webHost: process.env.GUVNOR_NODE_INSPECTOR_HOST 9 | }) 10 | debugServer.once('listening', function (error) { 11 | if (error) { 12 | process.send({ 13 | event: 'node-inspector:failed', 14 | args: [{ 15 | message: error.message, 16 | code: error.code, 17 | stack: error.stack 18 | }] 19 | }) 20 | } else { 21 | process.send({ 22 | event: 'node-inspector:ready', 23 | args: [ 24 | debugServer.address().port 25 | ] 26 | }) 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /web/client/views/processlist/process.js: -------------------------------------------------------------------------------- 1 | var View = require('ampersand-view') 2 | var templates = require('../../templates') 3 | 4 | module.exports = View.extend({ 5 | template: templates.includes.processlist.process, 6 | bindings: { 7 | 'model.pid': '[data-hook=pid]', 8 | 'model.name': '[data-hook=name]', 9 | 'model.user': '[data-hook=user]', 10 | 'model.group': '[data-hook=group]', 11 | 'model.uptimeFormatted': '[data-hook=uptime]', 12 | 'model.restarts': '[data-hook=restarts]', 13 | 'model.memoryFormatted': '[data-hook=memory]', 14 | 'model.cpuFormatted': '[data-hook=cpu]' 15 | }, 16 | events: { 17 | 'click td': 'showProcess' 18 | }, 19 | showProcess: function () { 20 | window.app.navigate('/host/' + this.model.collection.parent.name + '/process/' + this.model.id) 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /web/templates/pages/process/aborted.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    {{model.name}}

    5 |
    6 |
    7 |

    Aborted

    8 |
    9 |
    10 |
    11 |

    {{model.name}} was aborted because it failed to start too many times.

    12 |

    Please use the logs and exception tabs to diagnose the problem and the start button to try again.

    13 |
    14 |
    15 |
    16 |
    17 |
    18 |
    19 |
    20 |
    21 | -------------------------------------------------------------------------------- /lib/daemon/DaemonLogger.js: -------------------------------------------------------------------------------- 1 | var Transport = require('winston').Transport 2 | var util = require('util') 3 | var Autowire = require('wantsit').Autowire 4 | 5 | var DaemonLogger = function () { 6 | Transport.call(this, { level: 'debug' }) 7 | 8 | this._userRpc = Autowire 9 | this._adminRpc = Autowire 10 | this._logger = Autowire 11 | } 12 | util.inherits(DaemonLogger, Transport) 13 | 14 | DaemonLogger.prototype.name = 'daemon' 15 | 16 | DaemonLogger.prototype.log = function (level, msg, meta, callback) { 17 | if (this.silent || !process.send) { 18 | return callback(null, true) 19 | } 20 | 21 | this._userRpc.broadcast('daemon:log:' + level, { 22 | date: Date.now(), 23 | message: ('' + msg).trim() 24 | }) 25 | 26 | this.emit('logged') 27 | callback(null, true) 28 | } 29 | 30 | module.exports = DaemonLogger 31 | -------------------------------------------------------------------------------- /lib/daemon/util/LogAdder.js: -------------------------------------------------------------------------------- 1 | var Autowire = require('wantsit').Autowire 2 | var winston = require('winston') 3 | 4 | var LogAdder = function () { 5 | this._fileSystem = Autowire 6 | this._logger = Autowire 7 | this._daemonLogger = Autowire 8 | } 9 | 10 | LogAdder.prototype.afterPropertiesSet = function () { 11 | // now we have a log directory so add the logger 12 | this._logger.add(new winston.transports.DailyRotateFile({ 13 | filename: this._fileSystem.getLogDir() + '/guvnor.log', 14 | level: 'debug' 15 | }), null, true) 16 | this._logger.add(new winston.transports.File({ 17 | filename: this._fileSystem.getLogDir() + '/guvnor.error.log', 18 | level: 'error', 19 | handleExceptions: true 20 | }), null, true) 21 | this._logger.add(this._daemonLogger, null, true) 22 | } 23 | 24 | module.exports = LogAdder 25 | -------------------------------------------------------------------------------- /web/client/pages/host/overview.js: -------------------------------------------------------------------------------- 1 | var HostPage = require('../host') 2 | var templates = require('../../templates') 3 | var SystemDataView = require('../../views/host/system') 4 | var ResourceDataView = require('../../views/host/resources') 5 | 6 | module.exports = HostPage.extend({ 7 | template: templates.pages.host.overview, 8 | subviews: { 9 | system: { 10 | container: '[data-hook=system]', 11 | prepareView: function (el) { 12 | return new SystemDataView({ 13 | model: this.model, 14 | el: el 15 | }) 16 | } 17 | }, 18 | resources: { 19 | container: '[data-hook=resources]', 20 | prepareView: function (el) { 21 | return new ResourceDataView({ 22 | model: this.model, 23 | el: el 24 | }) 25 | } 26 | } 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /web/client/pages/process/aborted.js: -------------------------------------------------------------------------------- 1 | var ProcessPage = require('../process') 2 | var templates = require('../../templates') 3 | var StartButton = require('../../buttons/start') 4 | var RemoveButton = require('../../buttons/remove') 5 | 6 | module.exports = ProcessPage.extend({ 7 | template: templates.pages.process.aborted, 8 | subviews: { 9 | startButton: { 10 | container: '[data-hook=startbutton]', 11 | prepareView: function (el) { 12 | return new StartButton({ 13 | el: el, 14 | model: this.model 15 | }) 16 | } 17 | }, 18 | removeButton: { 19 | container: '[data-hook=removebutton]', 20 | prepareView: function (el) { 21 | return new RemoveButton({ 22 | el: el, 23 | model: this.model 24 | }) 25 | } 26 | } 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /web/client/pages/process/errored.js: -------------------------------------------------------------------------------- 1 | var ProcessPage = require('../process') 2 | var templates = require('../../templates') 3 | var StartButton = require('../../buttons/start') 4 | var RemoveButton = require('../../buttons/remove') 5 | 6 | module.exports = ProcessPage.extend({ 7 | template: templates.pages.process.errored, 8 | subviews: { 9 | startButton: { 10 | container: '[data-hook=startbutton]', 11 | prepareView: function (el) { 12 | return new StartButton({ 13 | el: el, 14 | model: this.model 15 | }) 16 | } 17 | }, 18 | removeButton: { 19 | container: '[data-hook=removebutton]', 20 | prepareView: function (el) { 21 | return new RemoveButton({ 22 | el: el, 23 | model: this.model 24 | }) 25 | } 26 | } 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /web/client/pages/process/failed.js: -------------------------------------------------------------------------------- 1 | var ProcessPage = require('../process') 2 | var templates = require('../../templates') 3 | var StartButton = require('../../buttons/start') 4 | var RemoveButton = require('../../buttons/remove') 5 | 6 | module.exports = ProcessPage.extend({ 7 | template: templates.pages.process.failed, 8 | subviews: { 9 | startButton: { 10 | container: '[data-hook=startbutton]', 11 | prepareView: function (el) { 12 | return new StartButton({ 13 | el: el, 14 | model: this.model 15 | }) 16 | } 17 | }, 18 | removeButton: { 19 | container: '[data-hook=removebutton]', 20 | prepareView: function (el) { 21 | return new RemoveButton({ 22 | el: el, 23 | model: this.model 24 | }) 25 | } 26 | } 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /web/client/pages/process/stopped.js: -------------------------------------------------------------------------------- 1 | var ProcessPage = require('../process') 2 | var templates = require('../../templates') 3 | var StartButton = require('../../buttons/start') 4 | var RemoveButton = require('../../buttons/remove') 5 | 6 | module.exports = ProcessPage.extend({ 7 | template: templates.pages.process.stopped, 8 | subviews: { 9 | startButton: { 10 | container: '[data-hook=startbutton]', 11 | prepareView: function (el) { 12 | return new StartButton({ 13 | el: el, 14 | model: this.model 15 | }) 16 | } 17 | }, 18 | removeButton: { 19 | container: '[data-hook=removebutton]', 20 | prepareView: function (el) { 21 | return new RemoveButton({ 22 | el: el, 23 | model: this.model 24 | }) 25 | } 26 | } 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /lib/daemon/common/ExceptionHandler.js: -------------------------------------------------------------------------------- 1 | var Autowire = require('wantsit').Autowire 2 | 3 | var ExceptionHandler = function () { 4 | this._logger = Autowire 5 | this._parentProcess = Autowire 6 | } 7 | 8 | ExceptionHandler.prototype.afterPropertiesSet = function () { 9 | process.on('uncaughtException', this._onUncaughtException.bind(this)) 10 | } 11 | 12 | ExceptionHandler.prototype._onUncaughtException = function (error) { 13 | this._parentProcess.send('process:uncaughtexception', { 14 | message: error.message, 15 | code: error.code, 16 | stack: error.stack, 17 | date: Date.now() 18 | }) 19 | 20 | if (process.listeners('uncaughtException').length === 1) { 21 | this._parentProcess.send('process:fatality') 22 | 23 | process.nextTick(process.exit.bind(process, 1)) 24 | } 25 | } 26 | 27 | module.exports = ExceptionHandler 28 | -------------------------------------------------------------------------------- /lib/daemon/StartupNotifier.js: -------------------------------------------------------------------------------- 1 | var Autowire = require('wantsit').Autowire 2 | 3 | var StartupNotifier = function () { 4 | this._userRpc = Autowire 5 | this._adminRpc = Autowire 6 | this._remoteRpc = Autowire 7 | this._nodeInspectorWrapper = Autowire 8 | this._parentProcess = Autowire 9 | this._commandLine = Autowire 10 | this._fileSystem = Autowire 11 | } 12 | 13 | StartupNotifier.prototype.afterPropertiesSet = function () { 14 | // change directory to the rundir 15 | process.chdir(this._fileSystem.getRunDir()) 16 | 17 | // all done, send our parent process a message 18 | this._parentProcess.send('daemon:ready', { 19 | user: this._userRpc.socket, 20 | admin: this._adminRpc.socket, 21 | remote: this._remoteRpc.port, 22 | debug: this._nodeInspectorWrapper.debuggerPort 23 | }) 24 | } 25 | 26 | module.exports = StartupNotifier 27 | -------------------------------------------------------------------------------- /lib/daemon/common/RemoteProcessLogger.js: -------------------------------------------------------------------------------- 1 | var Transport = require('winston').Transport 2 | var util = require('util') 3 | var Autowire = require('wantsit').Autowire 4 | 5 | var RemoteProcessLogger = function (options) { 6 | Transport.call(this, options) 7 | 8 | this._parentProcess = Autowire 9 | } 10 | util.inherits(RemoteProcessLogger, Transport) 11 | 12 | RemoteProcessLogger.prototype.name = 'remote' 13 | 14 | RemoteProcessLogger.prototype.log = function (level, msg, meta, callback) { 15 | if (this.silent || !msg) { 16 | return callback(null, true) 17 | } 18 | 19 | if (msg) { 20 | this._parentProcess.send('process:log:' + level, { 21 | date: Date.now(), 22 | message: msg.toString().trim() 23 | }) 24 | 25 | this.emit('logged') 26 | } 27 | 28 | callback(null, true) 29 | } 30 | 31 | module.exports = RemoteProcessLogger 32 | -------------------------------------------------------------------------------- /docs/statuses.md: -------------------------------------------------------------------------------- 1 | # Statuses 2 | 3 | ## Process 4 | 5 | These are the meanings of the possible values for the ProcessInfo object's `status` property. 6 | 7 | 1. `uninitialised` The process has not yet started 8 | 2. `starting` The process has been forked 9 | 3. `started` The process has started 10 | 4. `running` The users' module code has been loaded and is running 11 | 5. `restarting` The processes is restarting 12 | 6. `stopping` The process is stopping (n.b. it will not restart) 13 | 7. `stopped` The process has stopped 14 | 8. `errored` User code in the processes threw an error 15 | 9. `failed` The process wrapper failed to start 16 | 10. `aborted` The process `errored` too many times and will not be restarted 17 | 11. `paused` The process waiting for a debugger to attach 18 | 12. `unresponsive` The process did not respond to a status request in a timely fashion 19 | -------------------------------------------------------------------------------- /web/templates/includes/apps/app.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{model.name}} 3 | {{model.user}} 4 | {{model.ref}} 5 | {{model.url}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /web/templates/pages/process/failed.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    {{model.name}}

    5 |
    6 |
    7 |

    Failed

    8 |
    9 |
    10 |
    11 |

    {{model.name}} failed to initialise.

    12 |

    This usually means something was wrong with the process configuration.

    13 |

    Please double check the script path, current working directory, user/group, etc.

    14 |
    15 |
    16 |
    17 |
    18 |
    19 |
    20 |
    21 |
    22 | -------------------------------------------------------------------------------- /test/lib/daemon/common/LatencyMonitorTest.js: -------------------------------------------------------------------------------- 1 | var sinon = require('sinon'), 2 | expect = require('chai').expect, 3 | LatencyMonitor = require('../../../../lib/daemon/common/LatencyMonitor') 4 | 5 | describe('LatencyMonitor', function () { 6 | var monitor, lag 7 | 8 | beforeEach(function () { 9 | lag = sinon.stub() 10 | 11 | monitor = new LatencyMonitor() 12 | monitor._lag = sinon.stub() 13 | 14 | monitor._lag.withArgs(1000).returns(lag) 15 | 16 | monitor.afterPropertiesSet() 17 | }) 18 | 19 | it('should report latency', function () { 20 | var latency = 5 21 | 22 | lag.returns(latency) 23 | 24 | expect(monitor.latency).to.equal(latency) 25 | }) 26 | 27 | it('should correct spurious latency', function () { 28 | var latency = -10 29 | 30 | lag.returns(latency) 31 | 32 | expect(monitor.latency).to.equal(0) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /web/client/buttons/workeradd.js: -------------------------------------------------------------------------------- 1 | var View = require('ampersand-view') 2 | var templates = require('../templates') 3 | var notify = require('../helpers/notification') 4 | 5 | module.exports = View.extend({ 6 | template: templates.buttons.workeradd, 7 | events: { 8 | 'click [data-hook=addworkerbutton]': 'addWorkerToCluster' 9 | }, 10 | addWorkerToCluster: function (event) { 11 | event.target.blur() 12 | 13 | window.app.socket.emit('cluster:addworker', { 14 | host: this.model.collection.parent.name, 15 | process: this.model.id 16 | }, function (error) { 17 | if (error) { 18 | notify({ 19 | header: 'Add worker error', 20 | message: ['Could not add a worker to %s on %s - %s', this.model.name, this.model.collection.parent.name, error.message], 21 | type: 'danger' 22 | }) 23 | } 24 | }.bind(this)) 25 | } 26 | }) 27 | -------------------------------------------------------------------------------- /web/client/forms/install.js: -------------------------------------------------------------------------------- 1 | var FormView = require('ampersand-form-view') 2 | var InputView = require('ampersand-input-view') 3 | var templates = require('../templates') 4 | 5 | module.exports = FormView.extend({ 6 | template: templates.includes.apps.install, 7 | fields: function () { 8 | return [ 9 | new InputView({ 10 | label: 'URL (required)', 11 | name: 'url', 12 | value: this.model.url, 13 | placeholder: 'https://github.com/you/your-project.git', 14 | parent: this, 15 | template: templates.forms.controls.input() 16 | }), 17 | new InputView({ 18 | label: 'Name', 19 | name: 'name', 20 | value: this.model.name, 21 | placeholder: 'Your Project', 22 | parent: this, 23 | required: false, 24 | template: templates.forms.controls.input() 25 | }) 26 | ] 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /web/templates/includes/modal.hbs: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /lib/daemon/common/UserInfo.js: -------------------------------------------------------------------------------- 1 | var Autowire = require('wantsit').Autowire 2 | 3 | var UserInfo = function () { 4 | this._posix = Autowire 5 | } 6 | 7 | UserInfo.prototype.afterPropertiesSet = function () { 8 | var group = this._posix.getgrnam(process.env.GUVNOR_RUN_AS_GROUP || process.getgid()) 9 | 10 | this._gid = group.gid 11 | this._groupname = group.name 12 | 13 | var user = this._posix.getpwnam(process.env.GUVNOR_RUN_AS_USER || process.getuid()) 14 | 15 | this._uid = user.uid 16 | this._username = user.name 17 | } 18 | 19 | UserInfo.prototype.getGid = function () { 20 | return this._gid 21 | } 22 | 23 | UserInfo.prototype.getGroupName = function () { 24 | return this._groupname 25 | } 26 | 27 | UserInfo.prototype.getUid = function () { 28 | return this._uid 29 | } 30 | 31 | UserInfo.prototype.getUserName = function () { 32 | return this._username 33 | } 34 | 35 | module.exports = UserInfo 36 | -------------------------------------------------------------------------------- /web/templates/pages/process/logs.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    {{model.name}}

    5 |
    6 |
    7 |

    Logs

    8 | 9 | 10 | 11 |
    12 |
    13 |
      14 |
      15 |
      16 |
      17 |
      18 |
      19 | -------------------------------------------------------------------------------- /web/client/views/apps/console.js: -------------------------------------------------------------------------------- 1 | var View = require('ampersand-view') 2 | var templates = require('../../templates') 3 | var CollectionView = require('ampersand-collection-view') 4 | var LineView = require('./line') 5 | 6 | module.exports = View.extend({ 7 | template: templates.includes.apps.console, 8 | initialize: function () { 9 | this.listenTo(this.model.logs, 'add', this.scrollLogs.bind(this)) 10 | }, 11 | scrollLogs: function () { 12 | setTimeout(function () { 13 | var list = this.query('[data-hook=log]') 14 | 15 | list.scrollTop = list.scrollHeight 16 | }.bind(this), 100) 17 | }, 18 | subviews: { 19 | lines: { 20 | container: '[data-hook=log]', 21 | prepareView: function (el) { 22 | return new CollectionView({ 23 | el: el, 24 | collection: this.model.logs, 25 | view: LineView 26 | }) 27 | } 28 | } 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /web/client/buttons/workerremove.js: -------------------------------------------------------------------------------- 1 | var View = require('ampersand-view') 2 | var templates = require('../templates') 3 | var notify = require('../helpers/notification') 4 | 5 | module.exports = View.extend({ 6 | template: templates.buttons.workerremove, 7 | events: { 8 | 'click [data-hook=removeworkerbutton]': 'removeWorkerFromCluster' 9 | }, 10 | removeWorkerFromCluster: function (event) { 11 | event.target.blur() 12 | 13 | window.app.socket.emit('cluster:removeworker', { 14 | host: this.model.collection.parent.name, 15 | process: this.model.id 16 | }, function (error) { 17 | if (error) { 18 | notify({ 19 | header: 'Remove worker error', 20 | message: ['Could not remove a worker from %s on %s - %s', this.model.name, this.model.collection.parent.name, error.message], 21 | type: 'danger' 22 | }) 23 | } 24 | }.bind(this)) 25 | } 26 | }) 27 | -------------------------------------------------------------------------------- /web/client/models/exception.js: -------------------------------------------------------------------------------- 1 | var AmpersandModel = require('ampersand-model') 2 | var moment = require('moment') 3 | 4 | module.exports = AmpersandModel.extend({ 5 | props: { 6 | id: 'string', 7 | date: 'number', 8 | message: ['string', false, '-'], 9 | code: ['string', false, '-'], 10 | stack: 'string' 11 | }, 12 | session: { 13 | visible: ['boolean', true, true] 14 | }, 15 | derived: { 16 | dateFormatted: { 17 | deps: ['date'], 18 | fn: function (value) { 19 | return moment(value).format('YYYY-MM-DD HH:mm:ss Z') 20 | } 21 | }, 22 | messageOrStackSummary: { 23 | deps: ['message', 'stack'], 24 | fn: function () { 25 | if (this.message) { 26 | return this.message 27 | } 28 | 29 | // return first line of stacktrace 30 | return this.stack.substring(0, this.stack.indexOf('\n')) 31 | } 32 | } 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /lib/daemon/common/ConfigLoader.js: -------------------------------------------------------------------------------- 1 | var Autowire = require('wantsit').Autowire 2 | 3 | // Loads config from the parent process 4 | var ConfigLoader = function (prefix) { 5 | this._prefix = prefix || 'process' 6 | this._parentProcess = Autowire 7 | this._coercer = Autowire 8 | } 9 | 10 | ConfigLoader.prototype.afterPropertiesSet = function (done) { 11 | // notify once config has been loaded from parent process 12 | this._parentProcess.once('daemon:config:response', function (config) { 13 | Object.keys(config).forEach(function (key) { 14 | if (key.substring(0, 1) === '_') { 15 | return 16 | } 17 | 18 | this[key] = this._coercer(config[key]) 19 | }.bind(this)) 20 | 21 | done() 22 | }.bind(this)) 23 | 24 | // request config from parent process 25 | process.nextTick(this._parentProcess.send.bind(this._parentProcess, this._prefix + ':config:request')) 26 | } 27 | 28 | module.exports = ConfigLoader 29 | -------------------------------------------------------------------------------- /web/client/views/hostlist/host.js: -------------------------------------------------------------------------------- 1 | var View = require('ampersand-view') 2 | var templates = require('../../templates') 3 | var HostListProcessView = require('./process') 4 | 5 | module.exports = View.extend({ 6 | template: templates.includes.hostlist.host, 7 | bindings: { 8 | 'model.os': { 9 | type: function (el, value) { 10 | if (!value || value === 'unknown') { 11 | el.className = 'fa fa-desktop' 12 | } else if (value === 'darwin') { 13 | el.className = 'fa fa-apple' 14 | } else if (value === 'linux') { 15 | el.className = 'fa fa-linux' 16 | } else { 17 | el.className = 'icon-' + value 18 | } 19 | }, 20 | selector: '[data-hook=host-icon]' 21 | } 22 | }, 23 | render: function () { 24 | this.renderWithTemplate() 25 | 26 | this.renderCollection(this.model.processes, HostListProcessView, '[data-hook=process-list]') 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /web/client/pages/process/exceptions.js: -------------------------------------------------------------------------------- 1 | var ProcessPage = require('../process') 2 | var templates = require('../../templates') 3 | var CollectionView = require('ampersand-collection-view') 4 | var ExceptionView = require('../../views/process/exceptionlist/entry') 5 | var NoExceptionsView = require('../../views/process/exceptionlist/empty') 6 | 7 | module.exports = ProcessPage.extend({ 8 | template: templates.pages.process.exceptions, 9 | subviews: { 10 | exceptions: { 11 | container: '[data-hook=exceptions]', 12 | prepareView: function (el) { 13 | return new CollectionView({ 14 | el: el, 15 | collection: this.model.exceptions, 16 | view: ExceptionView, 17 | emptyView: NoExceptionsView 18 | }) 19 | } 20 | } 21 | }, 22 | bindings: { 23 | 'model.areExceptionsPinned': { 24 | type: 'booleanClass', 25 | name: 'active', 26 | selector: '.exceptions-pin' 27 | } 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /web/templates/pages/host/processes.hbs: -------------------------------------------------------------------------------- 1 |
      2 |
      3 |
      4 |

      {{model.name}}

      5 |
      6 |
      7 |

      Processes

      8 |
      9 |
      10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
      TitlePidUptimeRestartsMemoryCPU
      23 |
      24 |
      25 |
      26 |
      27 |
      28 | -------------------------------------------------------------------------------- /lib/web/resources/HostProcess.js: -------------------------------------------------------------------------------- 1 | var Autowire = require('wantsit').Autowire 2 | 3 | var HostProcess = function () { 4 | this._hostList = Autowire 5 | } 6 | 7 | HostProcess.prototype.retrieve = function (request, reply) { 8 | var host = this._hostList.getHostByName(request.params.hostId) 9 | 10 | if (host) { 11 | var process = host.findProcessById(request.params.processId) 12 | 13 | if (process) { 14 | reply(process) 15 | } else { 16 | reply('No process found for id ' + request.params.processId).code(404) 17 | } 18 | } else { 19 | reply('No host found for name ' + request.params.hostId).code(404) 20 | } 21 | } 22 | 23 | HostProcess.prototype.retrieveAll = function (request, reply) { 24 | var host = this._hostList.getHostByName(request.params.hostId) 25 | 26 | if (host) { 27 | reply(host.processes) 28 | } else { 29 | reply('No host found for name ' + request.params.hostId).code(404) 30 | } 31 | } 32 | 33 | module.exports = HostProcess 34 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrading 2 | 3 | ## 2.x.x to 3.x.x 4 | 5 | 1. Stop boss and uninstall 6 | 7 | $ sudo bs kill 8 | $ sudo npm remove -g process-boss 9 | 10 | 2. Rename your configuration files and directories (all .key .json etc files should come too): 11 | * `/etc/boss` becomes `/etc/guvnor` 12 | * `/etc/boss/bossrc` becomes `/etc/guvnor/guvnor` 13 | * `~/.config/boss/bossweb` becomes `~/.config/guvnor/guvnor-web` 14 | * `~/.config/boss/bossweb-hosts` becomes `~/.config/guvnor/guvnor-web-hosts` 15 | * `~/.config/boss/bossweb-users` becomes `~/.config/guvnor/guvnor-web-users` 16 | 3. If you have a `[boss]` section in `/etc/guvnor/guvnor`, rename it `[guvnor]` 17 | 4. Rename run/log/app directories 18 | * `/var/log/boss` becomes `/var/log/guvnor` 19 | * `/var/run/boss` becomes `/var/run/guvnor` 20 | * `/usr/local/boss` becomes `/usr/local/guvnor` 21 | 5. Install guvnor and start 22 | 23 | $ sudo npm install -g guvnor 24 | $ sudo guv 25 | -------------------------------------------------------------------------------- /web/templates/pages/process/snapshots.hbs: -------------------------------------------------------------------------------- 1 |
      2 |
      3 |
      4 |

      {{model.name}}

      5 |
      6 |
      7 |

      Heap snapshots

      8 |
      9 |
      10 |
      11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
      DateSizePath 
      22 |
      23 |
      24 |
      25 |
      26 |
      27 | -------------------------------------------------------------------------------- /docs/programmatic-access.md: -------------------------------------------------------------------------------- 1 | # Help 2 | 3 | 1. [Starting and stopping processes](processes.md) 4 | 1. [Controlling the Daemon](daemon.md) 5 | 1. [Managing clusters](clusters.md) 6 | 1. [Installing and running apps](apps.md) 7 | 1. [Remote access and monitoring (e.g. guv-web)](remote.md) 8 | 1. [Web interface](web.md) 9 | 1. [Web interface - configuration](web-config.md) 10 | 1. [Web interface - user management](web-users.md) 11 | 1. Programmatic access 12 | 1. [Programmatic access - local](programmatic-access-local.md) 13 | 1. [Programmatic access - remote](programmatic-access-remote.md) 14 | 1. [Programmatic access - events](programmatic-access-events.md) 15 | 16 | ## Programmatic access 17 | 18 | It is possible to connect to guvnor from your own program. 19 | 20 | Access is divded into two categories - [remote](programmatic-access-remote.md) and [local](programmatic-access-local.md). 21 | 22 | If guvnor is running on the same machine as your program, that's local access. If it's on a remote server, it's, well, remote access. 23 | -------------------------------------------------------------------------------- /web/client/forms/controls/element.js: -------------------------------------------------------------------------------- 1 | var View = require('ampersand-view') 2 | var _ = require('underscore') 3 | 4 | module.exports = View.extend({ 5 | render: function () { 6 | this.renderWithTemplate() 7 | 8 | this.keyInput.el = this.queryByHook('key') 9 | this.valueInput.el = this.queryByHook('value') 10 | 11 | this.keyInput.render() 12 | this.valueInput.render() 13 | }, 14 | bindings: _.extend({ 15 | 'removable': { 16 | type: 'toggle', 17 | hook: 'remove-field' 18 | } 19 | }), 20 | derived: { 21 | valid: { 22 | fn: function () { 23 | return this.keyInput.valid && this.valueInput.valid 24 | }, 25 | cache: false 26 | } 27 | }, 28 | props: { 29 | removable: 'boolean', 30 | template: ['string'], 31 | keyInput: 'any', 32 | valueInput: 'any' 33 | }, 34 | events: { 35 | 'click [data-hook~=remove-field]': 'handleRemoveClick' 36 | }, 37 | handleRemoveClick: function () { 38 | this.parent.removeField(this) 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /web/client/views/process/start.js: -------------------------------------------------------------------------------- 1 | var View = require('ampersand-view') 2 | var templates = require('../../templates') 3 | var StartForm = require('../../forms/start') 4 | var splitargs = require('splitargs') 5 | 6 | module.exports = View.extend({ 7 | template: templates.includes.process.start, 8 | events: { 9 | 'click [data-hook=cancel-button]': 'onCancel' 10 | }, 11 | subviews: { 12 | form: { 13 | container: '[data-hook=start-form]', 14 | prepareView: function (el) { 15 | return new StartForm({ 16 | model: this.model, 17 | el: el, 18 | submitCallback: function (data) { 19 | data.execArgv = splitargs(data.execArgv) 20 | data.argv = splitargs(data.argv) 21 | data.instances = parseInt(data.instances, 10) 22 | data.user = data.user.name 23 | 24 | this.onSubmit(data) 25 | }.bind(this) 26 | }) 27 | } 28 | } 29 | }, 30 | onCancel: function () { 31 | }, 32 | onSubmit: function (data) { 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /web/templates/pages/host/apps.hbs: -------------------------------------------------------------------------------- 1 |
      2 |
      3 |
      4 |

      {{model.name}}

      5 |
      6 |
      7 |

      Apps

      8 | 9 |
      10 |
      11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
      NameUserRefURL 
      23 |
      24 |
      25 |
      26 |
      27 |
      28 | -------------------------------------------------------------------------------- /lib/common/ManagedApp.js: -------------------------------------------------------------------------------- 1 | var util = require('util') 2 | var EventEmitter = require('wildemitter') 3 | 4 | var ManagedApp = function (info, daemon) { 5 | EventEmitter.call(this, { 6 | wildcard: true, 7 | delimiter: ':' 8 | }) 9 | 10 | // we're going to make the callbacks object non-enumerable 11 | delete this.callbacks 12 | 13 | Object.defineProperties(this, { 14 | callbacks: { 15 | value: {}, 16 | writable: false 17 | } 18 | }) 19 | 20 | this.update(info) 21 | 22 | this.switchRef = daemon.switchApplicationRef.bind(daemon, this.name) 23 | this.listRefs = daemon.listApplicationRefs.bind(daemon, this.name) 24 | this.updateRefs = daemon.updateApplicationRefs.bind(daemon, this.name) 25 | this.currentRef = daemon.currentRef.bind(daemon, this.name) 26 | } 27 | util.inherits(ManagedApp, EventEmitter) 28 | 29 | ManagedApp.prototype.update = function (info) { 30 | if (!info) { 31 | return 32 | } 33 | 34 | for (var key in info) { 35 | this[key] = info[key] 36 | } 37 | } 38 | 39 | module.exports = ManagedApp 40 | -------------------------------------------------------------------------------- /web/client/models/log.js: -------------------------------------------------------------------------------- 1 | var AmpersandModel = require('ampersand-model') 2 | var ansiHtml = require('ansi-html') 3 | var moment = require('moment') 4 | 5 | module.exports = AmpersandModel.extend({ 6 | idAttribute: 'date', 7 | props: { 8 | type: { 9 | type: 'string', 10 | values: ['info', 'warn', 'error', 'debug'] 11 | }, 12 | date: 'number', 13 | message: 'string' 14 | }, 15 | session: { 16 | visible: ['boolean', true, true] 17 | }, 18 | derived: { 19 | messageFormatted: { 20 | deps: ['message'], 21 | fn: function () { 22 | if (!this.message) { 23 | return '' 24 | } 25 | 26 | var message = this.message.replace(//g, '>') 28 | 29 | return ansiHtml(message) 30 | } 31 | }, 32 | dateFormatted: { 33 | deps: ['date'], 34 | fn: function () { 35 | var date = new Date(this.date) 36 | 37 | return moment(date).format('YYYY-MM-DD HH:mm:ss Z') 38 | } 39 | } 40 | } 41 | }) 42 | -------------------------------------------------------------------------------- /lib/remote/RemoteProcess.js: -------------------------------------------------------------------------------- 1 | var util = require('util') 2 | var ManagedProcess = require('../common/ManagedProcess') 3 | 4 | var RemoteProcess = function (info, daemon) { 5 | ManagedProcess.call(this) 6 | 7 | Object.defineProperty(this, '_daemon', { 8 | value: daemon 9 | }) 10 | } 11 | util.inherits(RemoteProcess, ManagedProcess) 12 | 13 | RemoteProcess.prototype.connect = function (callback) { 14 | this.once('_connected', callback) 15 | 16 | this._daemon._connectToProcess(this.id, function (error, remote) { 17 | if (!error) { 18 | this._bindRemoteMethods(remote) 19 | } 20 | 21 | this._connected = true 22 | 23 | this.emit('_connected', error, this) 24 | }.bind(this)) 25 | } 26 | 27 | RemoteProcess.prototype.disconnect = function (callback) { 28 | if (this._rpc.disconnect) { 29 | this._rpc.disconnect(ManagedProcess.prototype.disconnect.bind(this, callback)) 30 | this._connected = false 31 | } else { 32 | ManagedProcess.prototype.disconnect.call(this, callback) 33 | } 34 | } 35 | 36 | module.exports = RemoteProcess 37 | -------------------------------------------------------------------------------- /web/client/views/hostlist/process.js: -------------------------------------------------------------------------------- 1 | var View = require('ampersand-view') 2 | var templates = require('../../templates') 3 | var dom = require('ampersand-dom') 4 | var semver = require('semver') 5 | 6 | module.exports = View.extend({ 7 | template: templates.includes.hostlist.process, 8 | bindings: { 9 | 'model.name': '[data-hook=process-name]', 10 | 'model.language': { 11 | type: function (el, value) { 12 | var nodeVersion = this.model.collection.parent.versions.node 13 | var className = semver.satisfies(nodeVersion, '>=1.0.0') ? 'iojsIcon' : 'nodeIcon' 14 | 15 | if (value === 'coffee') { 16 | className += ' fa fa-coffee' 17 | } else { 18 | className += ' icon-nodejs' 19 | } 20 | 21 | el.className = className 22 | }, 23 | selector: '[data-hook=process-icon]' 24 | } 25 | }, 26 | events: { 27 | 'click a[href]': 'updateActiveNav' 28 | }, 29 | updateActiveNav: function () { 30 | var el = this.query('.process') 31 | dom.addClass(el, 'active') 32 | } 33 | }) 34 | -------------------------------------------------------------------------------- /test/integration/fixtures/exec.js: -------------------------------------------------------------------------------- 1 | var child_process = require('child_process'), 2 | posix = require('posix') 3 | 4 | module.exports = function (command, args, path, callback) { 5 | var userDetails = posix.getpwnam(process.getuid()) 6 | 7 | if (arguments.length == 3) { 8 | callback = path 9 | path = process.cwd() 10 | } 11 | 12 | var stdout = '' 13 | var stderr = '' 14 | 15 | var proc = child_process.spawn( 16 | command, args, { 17 | cwd: path, 18 | uid: userDetails.uid, 19 | gid: userDetails.gid, 20 | env: { 21 | HOME: userDetails.homedir, 22 | PATH: '/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin' 23 | } 24 | } 25 | ) 26 | proc.stdout.on('data', function (buff) { 27 | stdout += buff.toString('utf8') 28 | }) 29 | proc.stderr.on('data', function (buff) { 30 | stderr += buff.toString('utf8') 31 | }) 32 | proc.once('close', function (code) { 33 | proc.removeAllListeners('data') 34 | 35 | callback(code != 0 ? new Error('Process failed') : undefined, stdout, stderr) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /web/client/pages/process/snapshots.js: -------------------------------------------------------------------------------- 1 | var ProcessPage = require('../process') 2 | var templates = require('../../templates') 3 | var CollectionView = require('ampersand-collection-view') 4 | var SnapshotsView = require('../../views/process/snapshotlist/entry') 5 | var NoSnapshotsView = require('../../views/process/snapshotlist/empty') 6 | var SnapshotButton = require('../../buttons/snapshot') 7 | 8 | module.exports = ProcessPage.extend({ 9 | template: templates.pages.process.snapshots, 10 | subviews: { 11 | snapshots: { 12 | container: '[data-hook=snapshots]', 13 | prepareView: function (el) { 14 | return new CollectionView({ 15 | el: el, 16 | collection: this.model.snapshots, 17 | view: SnapshotsView, 18 | emptyView: NoSnapshotsView 19 | }) 20 | } 21 | }, 22 | snapshotButton: { 23 | container: '[data-hook=snapshotbutton]', 24 | prepareView: function (el) { 25 | return new SnapshotButton({ 26 | el: el, 27 | model: this.model 28 | }) 29 | } 30 | } 31 | } 32 | }) 33 | -------------------------------------------------------------------------------- /lib/web/resources/HostApp.js: -------------------------------------------------------------------------------- 1 | var Autowire = require('wantsit').Autowire 2 | 3 | var HostApp = function () { 4 | this._hostList = Autowire 5 | } 6 | 7 | HostApp.prototype.retrieve = function (request, reply) { 8 | var host = this._hostList.getHostByName(request.params.hostId) 9 | 10 | if (host) { 11 | var process = host.findAppByName(request.params.processId) 12 | 13 | if (process) { 14 | reply(process) 15 | } else { 16 | reply('No app found for name ' + request.params.appId).code(404) 17 | } 18 | } else { 19 | reply('No host found for name ' + request.params.hostId).code(404) 20 | } 21 | } 22 | 23 | HostApp.prototype.retrieveAll = function (request, reply) { 24 | var host = this._hostList.getHostByName(request.params.hostId) 25 | 26 | if (host) { 27 | host.findApps(function (error, apps) { 28 | if (error) { 29 | reply(error) 30 | 31 | return 32 | } 33 | 34 | reply(apps) 35 | }) 36 | } else { 37 | reply('No host found for name ' + request.params.hostId).code(404) 38 | } 39 | } 40 | 41 | module.exports = HostApp 42 | -------------------------------------------------------------------------------- /lib/web/resources/HostUser.js: -------------------------------------------------------------------------------- 1 | var Autowire = require('wantsit').Autowire 2 | 3 | var HostApp = function () { 4 | this._hostList = Autowire 5 | } 6 | 7 | HostApp.prototype.retrieve = function (request, reply) { 8 | var host = this._hostList.getHostByName(request.params.hostId) 9 | 10 | if (host) { 11 | var process = host.findUserByName(request.params.processId) 12 | 13 | if (process) { 14 | reply(process) 15 | } else { 16 | reply('No app found for name ' + request.params.appId).code(404) 17 | } 18 | } else { 19 | reply('No host found for name ' + request.params.hostId).code(404) 20 | } 21 | } 22 | 23 | HostApp.prototype.retrieveAll = function (request, reply) { 24 | var host = this._hostList.getHostByName(request.params.hostId) 25 | 26 | if (host) { 27 | host.findUsers(function (error, users) { 28 | if (error) { 29 | reply(error) 30 | 31 | return 32 | } 33 | 34 | reply(users) 35 | }) 36 | } else { 37 | reply('No host found for name ' + request.params.hostId).code(404) 38 | } 39 | } 40 | 41 | module.exports = HostApp 42 | -------------------------------------------------------------------------------- /web/client/views/process/exceptionlist/entry.js: -------------------------------------------------------------------------------- 1 | var View = require('ampersand-view') 2 | var templates = require('../../../templates') 3 | var dom = require('ampersand-dom') 4 | 5 | module.exports = View.extend({ 6 | template: templates.includes.process.exceptionlist.entry, 7 | bindings: { 8 | 'model.visible': { 9 | type: 'booleanClass', 10 | selector: 'tr', 11 | name: 'visible' 12 | } 13 | }, 14 | events: { 15 | 'click ul': 'showDetails' 16 | }, 17 | showDetails: function (event) { 18 | // the stack trace is contained in a code/pre element - if the user clicks that 19 | // they might be trying to copy the trace so only hide it if they've clicked 20 | // the surrounding list element(s) 21 | if ( 22 | event.target.nodeName.toUpperCase() !== 'LI' 23 | && 24 | event.target.nodeName.toUpperCase() !== 'UL') { 25 | return 26 | } 27 | 28 | var stack = this.query('.stack') 29 | 30 | if (dom.hasClass(stack, 'visible')) { 31 | dom.removeClass(stack, 'visible') 32 | } else { 33 | dom.addClass(stack, 'visible') 34 | } 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /lib/common/Crypto.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto') 2 | 3 | var Crypto = function () {} 4 | 5 | Crypto.prototype.sign = function (principal, secret, callback) { 6 | crypto.randomBytes(32, function (error, bytes) { 7 | if (error) return callback(error) 8 | 9 | var nonce = bytes.toString('base64') 10 | var date = Date.now() 11 | 12 | callback(undefined, { 13 | principal: principal, 14 | date: date, 15 | nonce: nonce, 16 | hash: this._hash(date + secret + nonce) 17 | }) 18 | }.bind(this)) 19 | } 20 | 21 | Crypto.prototype.verify = function (signature, secret) { 22 | return signature.hash === this._hash(signature.date + secret + signature.nonce) 23 | } 24 | 25 | Crypto.prototype._hash = function (date, nonce, secret) { 26 | var shasum = crypto.createHash('sha1') 27 | shasum.update(date + secret + nonce) 28 | return shasum.digest('base64') 29 | } 30 | 31 | Crypto.prototype.generateSecret = function (callback) { 32 | crypto.randomBytes(32, function (error, bytes) { 33 | callback(error, bytes ? bytes.toString('base64') : undefined) 34 | }) 35 | } 36 | 37 | module.exports = Crypto 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Tableflip 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. -------------------------------------------------------------------------------- /web/client/pages/process/unresponsive.js: -------------------------------------------------------------------------------- 1 | var ProcessPage = require('../process') 2 | var templates = require('../../templates') 3 | var StopButton = require('../../buttons/stop') 4 | var RestartButton = require('../../buttons/restart') 5 | var DebugButton = require('../../buttons/debug') 6 | 7 | module.exports = ProcessPage.extend({ 8 | template: templates.pages.process.unresponsive, 9 | subviews: { 10 | stopButton: { 11 | container: '[data-hook=stopbutton]', 12 | prepareView: function (el) { 13 | return new StopButton({ 14 | el: el, 15 | model: this.model 16 | }) 17 | } 18 | }, 19 | restartButton: { 20 | container: '[data-hook=restartbutton]', 21 | prepareView: function (el) { 22 | return new RestartButton({ 23 | el: el, 24 | model: this.model 25 | }) 26 | } 27 | }, 28 | debugButton: { 29 | container: '[data-hook=debugbutton]', 30 | prepareView: function (el) { 31 | return new DebugButton({ 32 | el: el, 33 | model: this.model 34 | }) 35 | } 36 | } 37 | } 38 | }) 39 | -------------------------------------------------------------------------------- /test/lib/common/CryptoTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | Crypto = require('../../../lib/common/Crypto') 3 | 4 | describe('Crypto', function () { 5 | 6 | it('should produce a signature', function (done) { 7 | var crypto = new Crypto() 8 | 9 | crypto.generateSecret(function (error, secret) { 10 | var principal = 'foo' 11 | 12 | crypto.sign(principal, secret, function (error, signature) { 13 | expect(error).to.not.exist 14 | expect(signature.principal).to.equal(principal) 15 | expect(signature.nonce).to.be.ok 16 | expect(signature.date).to.be.ok 17 | expect(signature.hash).to.be.ok 18 | 19 | done() 20 | }) 21 | }) 22 | }) 23 | 24 | it('should verify a signature', function (done) { 25 | var crypto = new Crypto() 26 | 27 | crypto.generateSecret(function (error, secret) { 28 | var principal = 'foo' 29 | 30 | crypto.sign(principal, secret, function (error, signature) { 31 | expect(error).to.not.exist 32 | 33 | var result = crypto.verify(signature, secret) 34 | 35 | expect(result).to.be.true 36 | 37 | done() 38 | }) 39 | }) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /web/templates/includes/host/system.hbs: -------------------------------------------------------------------------------- 1 |
      2 |
      3 |

      Vital statistics

      4 |
      5 |
      6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
      HostnamePlatformArchReleaseGuvnorEngineUptime
      {{model.hostname}}{{model.platform}}{{model.arch}}{{model.release}}{{model.guvnor}}{{model.engine}}{{model.uptimeFormatted}}
      30 |
      31 |
      -------------------------------------------------------------------------------- /test/lib/daemon/common/UserInfoTest.js: -------------------------------------------------------------------------------- 1 | var sinon = require('sinon'), 2 | expect = require('chai').expect, 3 | path = require('path'), 4 | UserInfo = require('../../../../lib/daemon/common/UserInfo') 5 | 6 | describe('UserInfo', function () { 7 | var userInfo 8 | 9 | beforeEach(function () { 10 | userInfo = new UserInfo() 11 | userInfo._posix = { 12 | getgrnam: sinon.stub(), 13 | getpwnam: sinon.stub() 14 | } 15 | }) 16 | 17 | it('find uid/gid', function () { 18 | var groupName = 'foo' 19 | var gid = 10 20 | 21 | var userName = 'bar' 22 | var uid = 11 23 | 24 | userInfo._posix.getgrnam.withArgs(groupName).returns({ 25 | gid: gid, 26 | name: groupName 27 | }) 28 | userInfo._posix.getpwnam.withArgs(userName).returns({ 29 | uid: uid, 30 | name: userName 31 | }) 32 | 33 | process.env.GUVNOR_RUN_AS_GROUP = groupName 34 | process.env.GUVNOR_RUN_AS_USER = userName 35 | 36 | userInfo.afterPropertiesSet() 37 | 38 | expect(userInfo.getGid()).to.equal(gid) 39 | expect(userInfo.getGroupName()).to.equal(groupName) 40 | expect(userInfo.getUid()).to.equal(uid) 41 | expect(userInfo.getUserName()).to.equal(userName) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /lib/daemon/cluster/ClusterProcessWrapper.js: -------------------------------------------------------------------------------- 1 | var ProcessWrapper = require('../process/ProcessWrapper') 2 | var util = require('util') 3 | var async = require('async') 4 | 5 | var ClusterProcessWrapper = function () { 6 | ProcessWrapper.call(this) 7 | } 8 | util.inherits(ClusterProcessWrapper, ProcessWrapper) 9 | 10 | ClusterProcessWrapper.prototype.afterPropertiesSet = function () { 11 | async.series([ 12 | this._setProcessName.bind(this), 13 | this._processRpc.startDnodeServer.bind(this._processRpc) 14 | ], this._done.bind(this)) 15 | } 16 | 17 | ClusterProcessWrapper.prototype._setProcessName = function (callback) { 18 | ProcessWrapper.prototype._setProcessName.call(this, function () { 19 | process.title = 'Cluster: ' + process.title 20 | 21 | callback() 22 | }) 23 | } 24 | 25 | ClusterProcessWrapper.prototype._done = function (error, results) { 26 | if (error) { 27 | return this._parentProcess.send('cluster:failed', { 28 | date: Date.now(), 29 | message: error.message, 30 | code: error.code, 31 | stack: error.stack 32 | }) 33 | } 34 | 35 | // pass rpc socket path to parent process 36 | this._parentProcess.send('cluster:started', results[1]) 37 | } 38 | 39 | module.exports = ClusterProcessWrapper 40 | -------------------------------------------------------------------------------- /web/templates/body.hbs: -------------------------------------------------------------------------------- 1 | 2 |
      3 |
      4 | 20 | 21 |
      22 | 26 |
      27 | 28 | -------------------------------------------------------------------------------- /web/templates/includes/process/overview/running.hbs: -------------------------------------------------------------------------------- 1 |
      2 |
      3 |

      Vital statistics

      4 |
      5 |
      6 |
      7 | {{model.name}} is running in debug mode, is paused and is waiting for a debugger to attach to port {{model.debugPort}}. Please click the debug button below. 8 |
      9 |
      10 |
      11 |

      {{model.name}} has been running for {{model.uptimeFormatted}} with {{model.restarts}} restart(s).

      12 |

      The current pid is {{model.pid}} and it's running as {{model.user}}:{{model.group}}.

      13 | 14 |
      15 |
      16 |
      17 |
      18 | 19 | {{#if model.cluster}} 20 |
      21 |
      22 | {{/if}} 23 |
      24 |
      25 | -------------------------------------------------------------------------------- /web/public/css/app/process.styl: -------------------------------------------------------------------------------- 1 | .panel-logs 2 | padding: 0 3 | 4 | ul.logs 5 | background: #000 6 | color: #FFF 7 | font-family: 'Lucida Console', Monaco, monospace 8 | height: 600px 9 | margin: 0 10 | overflow: auto 11 | padding: 5px 0 12 | 13 | li 14 | font-size: 12px 15 | background-color: transparent 16 | color: #FFF 17 | line-height: 1.4 18 | white-space: pre 19 | padding: 1px 5px 20 | display: none 21 | 22 | li.error 23 | background-color: #330100 24 | 25 | li.visible 26 | display: block 27 | 28 | li span.date 29 | color: #999 30 | margin-right: 8px 31 | 32 | ul.logs 33 | li span.date 34 | display: none 35 | 36 | ul.logs.showTimes 37 | li span.date 38 | display: inline-block 39 | 40 | ul.logs:after 41 | content: "\2588" 42 | margin-left: 5px 43 | -webkit-animation: blinker 1s linear 0s infinite 44 | -moz-animation: blinker 1s linear 0s infinite 45 | -ms-animation: blinker 1s linear 0s infinite 46 | -animation: blinker 1s linear 0s infinite 47 | 48 | .exceptions p 49 | padding: 5px 0 50 | margin: 0 51 | 52 | .process .details button 53 | margin-bottom: 5px 54 | 55 | .start-form .add-field 56 | color: #77b300 57 | 58 | .start-form .remove-field 59 | color: #cc0000 60 | margin-top: 10px 61 | display: block -------------------------------------------------------------------------------- /test/lib/daemon/cluster/ClusterProcessWrapperTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | sinon = require('sinon'), 3 | ClusterProcessWrapper = require('../../../../lib/daemon/cluster/ClusterProcessWrapper'), 4 | EventEmitter = require('events').EventEmitter 5 | 6 | describe('ClusterProcessWrapper', function () { 7 | var wrapper, name 8 | 9 | beforeEach(function () { 10 | name = process.title 11 | 12 | wrapper = new ClusterProcessWrapper() 13 | wrapper._logger = { 14 | info: sinon.stub(), 15 | warn: sinon.stub(), 16 | error: sinon.stub(), 17 | debug: sinon.stub() 18 | } 19 | wrapper._processRpc = { 20 | startDnodeServer: sinon.stub() 21 | } 22 | wrapper._parentProcess = { 23 | send: sinon.stub() 24 | } 25 | }) 26 | 27 | afterEach(function () { 28 | process.title = name 29 | }) 30 | 31 | it('should start up', function (done) { 32 | process.env.GUVNOR_PROCESS_NAME = 'ClusterProcessWrapperTest-startup' 33 | 34 | wrapper._processRpc.startDnodeServer.callsArgWith(0, undefined, '/foo/bar') 35 | 36 | wrapper._parentProcess.send = function (type, socket) { 37 | expect(type).to.equal('cluster:started') 38 | expect(socket).to.equal('/foo/bar') 39 | 40 | done() 41 | } 42 | 43 | wrapper.afterPropertiesSet() 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /lib/daemon/rpc/UserRPC.js: -------------------------------------------------------------------------------- 1 | var Autowire = require('wantsit').Autowire 2 | var RPCEndpoint = require('./RPCEndpoint') 3 | var util = require('util') 4 | 5 | var UserRPC = function () { 6 | RPCEndpoint.call(this) 7 | 8 | this._processService = Autowire 9 | this._appService = Autowire 10 | } 11 | util.inherits(UserRPC, RPCEndpoint) 12 | 13 | UserRPC.prototype.afterPropertiesSet = function (done) { 14 | RPCEndpoint.prototype.afterPropertiesSet.call(this, done) 15 | 16 | // broadcast events to all dnode clients 17 | this._guvnor.on('*', this.broadcast.bind(this)) 18 | this._processService.on('*', this.broadcast.bind(this)) 19 | this._appService.on('*', this.broadcast.bind(this)) 20 | } 21 | 22 | UserRPC.prototype._getApi = function () { 23 | return [ 24 | 'startProcess', 'listProcesses', 'findProcessInfoById', 25 | 'findProcessInfoByPid', 'findProcessInfoByName', 'deployApplication', 26 | 'removeApplication', 'listApplications', 'switchApplicationRef', 27 | 'listApplicationRefs', 'updateApplicationRefs', 'removeProcess', 28 | 'listUsers', 'currentRef', 'stopProcess', 'findAppByName', 'findAppById' 29 | ] 30 | } 31 | 32 | UserRPC.prototype._getSocketName = function () { 33 | return 'user.socket' 34 | } 35 | 36 | UserRPC.prototype._getUmask = function () { 37 | return parseInt('007', 8) 38 | } 39 | 40 | module.exports = UserRPC 41 | -------------------------------------------------------------------------------- /test/lib/daemon/common/ExceptionHandlerTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | sinon = require('sinon'), 3 | path = require('path'), 4 | ExceptionHandler = require('../../../../lib/daemon/common/ExceptionHandler') 5 | 6 | var loggerStub = { 7 | info: sinon.stub(), 8 | warn: sinon.stub(), 9 | error: sinon.stub(), 10 | debug: sinon.stub() 11 | } 12 | var parentProcessStub = { 13 | send: sinon.stub() 14 | } 15 | 16 | describe('ExceptionHandler', function () { 17 | 18 | it('should notify of uncaught exceptions', function () { 19 | process.listeners = sinon.stub() 20 | process.listeners.withArgs('uncaughtException').returns([{}, {}]) 21 | 22 | var exceptionHandler = new ExceptionHandler() 23 | exceptionHandler._logger = loggerStub 24 | exceptionHandler._parentProcess = parentProcessStub 25 | exceptionHandler.afterPropertiesSet() 26 | 27 | // the method under test 28 | exceptionHandler._onUncaughtException({}) 29 | 30 | var foundUncaughtExceptionEvent = false 31 | 32 | for (var i = 0; i < parentProcessStub.send.callCount; i++) { 33 | var event = parentProcessStub.send.getCall(i).args[0] 34 | 35 | if (event == 'process:uncaughtexception') { 36 | foundUncaughtExceptionEvent = true 37 | } 38 | } 39 | 40 | expect(foundUncaughtExceptionEvent).to.equal(true) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /web/public/css/app/exceptions.styl: -------------------------------------------------------------------------------- 1 | ul.exceptions 2 | background-color: #181818 3 | display: flex 4 | flex-flow: row wrap 5 | 6 | ul.exceptions, ul.exceptions ul 7 | list-style: none 8 | padding: 0 9 | margin: 0 10 | 11 | ul.exceptions li 12 | padding: 8px 15px 5px 15px 13 | display: inline-block 14 | overflow: hidden 15 | text-overflow: ellipsis 16 | 17 | ul.exceptions > li 18 | padding: 8px 15px 19 | font-weight: bold 20 | color: #FFF 21 | border-bottom: 2px solid #282828 22 | 23 | ul.exceptions li.list 24 | padding: 0 25 | border-bottom: none 26 | font-weight: normal 27 | width: 100% 28 | 29 | ul.exceptions li.list tr:nth-child(odd) 30 | background-color: #000 31 | 32 | ul.exceptions li.list p 33 | background-color: #000 34 | padding: 8px 15px 35 | 36 | ul.exceptions li.list ul 37 | border-bottom: 1px solid #282828 38 | 39 | ul.exceptions li.list ul:hover 40 | cursor: pointer 41 | 42 | ul.exceptions li.list ul:nth-child(odd) 43 | background-color: #080808 44 | 45 | ul.exceptions li.list ul:hover 46 | background-color: #282828 47 | 48 | ul.exceptions li.message 49 | -webkit-flex-grow: 2 50 | flex-grow: 2 51 | 52 | ul.exceptions li.date 53 | width: 25% 54 | 55 | ul.exceptions li.code 56 | width: 25% 57 | 58 | ul.exceptions li.stack 59 | display: none 60 | 61 | ul.exceptions li.stack.visible 62 | display: block 63 | -------------------------------------------------------------------------------- /test/integration/fixtures/siglisten.js: -------------------------------------------------------------------------------- 1 | // Start reading from stdin so we don't exit. 2 | process.stdin.resume() 3 | 4 | process.on('SIGUSR1', function () { 5 | console.log('Got SIGUSR1') 6 | 7 | process.send({ 8 | event: 'signal:received', 9 | args: ['SIGUSR1'] 10 | }) 11 | }) 12 | 13 | process.on('SIGTERM', function () { 14 | console.log('Got SIGTERM') 15 | 16 | process.send({ 17 | event: 'signal:received', 18 | args: ['SIGTERM'] 19 | }) 20 | }) 21 | 22 | process.on('SIGPIPE', function () { 23 | console.log('Got SIGPIPE') 24 | 25 | process.send({ 26 | event: 'signal:received', 27 | args: ['SIGPIPE'] 28 | }) 29 | }) 30 | 31 | process.on('SIGHUP', function () { 32 | console.log('Got SIGHUP') 33 | 34 | process.send({ 35 | event: 'signal:received', 36 | args: ['SIGHUP'] 37 | }) 38 | }) 39 | 40 | process.on('SIGINT', function () { 41 | console.log('Got SIGINT') 42 | 43 | process.send({ 44 | event: 'signal:received', 45 | args: ['SIGINT'] 46 | }) 47 | }) 48 | 49 | process.on('SIGBREAK', function () { 50 | console.log('Got SIGBREAK') 51 | 52 | process.send({ 53 | event: 'signal:received', 54 | args: ['SIGBREAK'] 55 | }) 56 | }) 57 | 58 | process.on('SIGWINCH', function () { 59 | console.log('Got SIGWINCH') 60 | 61 | process.send({ 62 | event: 'signal:received', 63 | args: ['SIGWINCH'] 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /lib/daemon/service/PortService.js: -------------------------------------------------------------------------------- 1 | var Autowire = require('wantsit').Autowire 2 | var async = require('async') 3 | 4 | var MAX_PORT_ATTEMPTS = 20 5 | 6 | var PortService = function () { 7 | this._net = Autowire 8 | this._config = Autowire 9 | } 10 | 11 | PortService.prototype.afterPropertiesSet = function () { 12 | if (this._config.ports && this._config.ports.start && this._config.ports.end) { 13 | this._nextPort = this._config.ports.start 14 | } 15 | } 16 | 17 | PortService.prototype.freePort = function (callback) { 18 | if (this._nextPort) { 19 | async.retry(MAX_PORT_ATTEMPTS, function (callback) { 20 | this._findFreePort(this._nextPort, function (error, port) { 21 | this._nextPort++ 22 | 23 | if (this._nextPort > this._config.ports.end) { 24 | this._nextPort = this._config.ports.start 25 | } 26 | 27 | callback(error, port) 28 | }.bind(this)) 29 | }.bind(this), callback) 30 | } else { 31 | this._findFreePort(0, callback) 32 | } 33 | } 34 | 35 | PortService.prototype._findFreePort = function (port, callback) { 36 | var server = this._net.createServer() 37 | server.on('listening', function () { 38 | port = server.address().port 39 | server.close() 40 | }) 41 | server.on('close', function () { 42 | callback(undefined, port) 43 | }) 44 | server.listen(port, '127.0.0.1') 45 | } 46 | 47 | module.exports = PortService 48 | -------------------------------------------------------------------------------- /test/lib/daemon/StartupNotifierTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | sinon = require('sinon'), 3 | StartupNotifier = require('../../../lib/daemon/StartupNotifier') 4 | 5 | describe('StartupNotifier', function () { 6 | var notifier 7 | 8 | beforeEach(function () { 9 | notifier = new StartupNotifier() 10 | notifier._parentProcess = {} 11 | notifier._userRpc = {} 12 | notifier._adminRpc = {} 13 | notifier._remoteRpc = {} 14 | notifier._nodeInspectorWrapper = {} 15 | notifier._commandLine = {} 16 | notifier._fileSystem = { 17 | getRunDir: sinon.stub().returns(process.cwd()) 18 | } 19 | }) 20 | 21 | it('should notify of startup', function (done) { 22 | notifier._userRpc.socket = 'usersocket' 23 | notifier._adminRpc.socket = 'adminsocket' 24 | notifier._remoteRpc.port = 'rpcport' 25 | notifier._nodeInspectorWrapper.debuggerPort = 'debuggerport' 26 | 27 | notifier._parentProcess.send = function (type, sockets) { 28 | expect(type).to.equal('daemon:ready') 29 | 30 | // should have been sent the non-privileged socket 31 | expect(sockets.user).to.equal('usersocket') 32 | expect(sockets.admin).to.equal('adminsocket') 33 | expect(sockets.remote).to.equal('rpcport') 34 | expect(sockets.debug).to.equal('debuggerport') 35 | 36 | done() 37 | } 38 | 39 | notifier.afterPropertiesSet() 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /docs/web-users.md: -------------------------------------------------------------------------------- 1 | # Help 2 | 3 | 1. [Starting and stopping processes](processes.md) 4 | 1. [Controlling the Daemon](daemon.md) 5 | 1. [Managing clusters](clusters.md) 6 | 1. [Installing and running apps](apps.md) 7 | 1. [Remote access and monitoring (e.g. guv-web)](remote.md) 8 | 1. [Web interface](web.md) 9 | 1. [Web interface - configuration](web-config.md) 10 | 1. Web interface - user management 11 | 1. [Programmatic access](programmatic-access.md) 12 | 1. [Programmatic access - local](programmatic-access-local.md) 13 | 1. [Programmatic access - remote](programmatic-access-remote.md) 14 | 1. [Programmatic access - events](programmatic-access-events.md) 15 | 16 | ## User administration 17 | 18 | These commands administer users on a `guvnor-web` instance and will update your `guvnor-web` and `guvnor-web-users` config files. 19 | 20 | For the time being `guvnor-web` must not be running while you do this. 21 | 22 | ### Adding users 23 | 24 | ```sh 25 | guv-web useradd alex 26 | ``` 27 | 28 | ### Removing users 29 | 30 | ```sh 31 | guv-web rmuser alex 32 | ``` 33 | 34 | ### Resetting passwords 35 | 36 | ```sh 37 | guv-web passwd alex 38 | ``` 39 | 40 | ### Listing users 41 | 42 | ```sh 43 | guv-web lsusers 44 | ``` 45 | 46 | ### Changing the password salt 47 | 48 | N.b. this will invalidate all user passwords, so don't forget to reset them otherwise no-one will be able to log in! 49 | 50 | ```sh 51 | guv-web gensalt 52 | ``` 53 | -------------------------------------------------------------------------------- /test/integration/fixtures/log-daemon-messages.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function (guvnor) { 3 | if (process.env.npm_package_version) { 4 | return 5 | } 6 | 7 | // log all received events 8 | guvnor.on('*', function (type) { 9 | if (type.substring(0, 'daemon:log'.length) == 'daemon:log' || 10 | type.substring(0, 'process:uncaughtexception'.length) == 'process:uncaughtexception' || 11 | type.substring(0, 'daemon:fatality'.length) == 'daemon:fatality' || 12 | type.substring(0, 'process:log'.length) == 'process:log' || 13 | type.substring(0, 'worker:log'.length) == 'worker:log') { 14 | // already handled 15 | return 16 | } 17 | 18 | console.log(type) 19 | }) 20 | guvnor.on('daemon:log:*', function (type, event) { 21 | console.log(type, event.message) 22 | }) 23 | guvnor.on('process:log:*', function (type, processInfo, event) { 24 | console.log(type, processInfo.id, event) 25 | }) 26 | guvnor.on('cluster:log:*', function (type, processInfo, event) { 27 | console.log(type, processInfo.id, event) 28 | }) 29 | guvnor.on('worker:log:*', function (type, clusterInfo, workerInfo, event) { 30 | console.log(type, workerInfo.id, event) 31 | }) 32 | guvnor.on('process:uncaughtexception:*', function (type, error) { 33 | console.log(error.stack) 34 | }) 35 | guvnor.on('daemon:fatality', function (error) { 36 | console.log(error.stack) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /web/public/javascript/highcharts/modules/no-data-to-display.js: -------------------------------------------------------------------------------- 1 | /* 2 | Highcharts JS v4.0.4 (2014-09-02) 3 | Plugin for displaying a message when there is no data visible in chart. 4 | 5 | (c) 2010-2014 Highsoft AS 6 | Author: Oystein Moseng 7 | 8 | License: www.highcharts.com/license 9 | */ 10 | (function(c){function f(){return!!this.points.length}function g(){this.hasData()?this.hideNoData():this.showNoData()}var d=c.seriesTypes,e=c.Chart.prototype,h=c.getOptions(),i=c.extend;i(h.lang,{noData:"No data to display"});h.noData={position:{x:0,y:0,align:"center",verticalAlign:"middle"},attr:{},style:{fontWeight:"bold",fontSize:"12px",color:"#60606a"}};if(d.pie)d.pie.prototype.hasData=f;if(d.gauge)d.gauge.prototype.hasData=f;if(d.waterfall)d.waterfall.prototype.hasData=f;c.Series.prototype.hasData= 11 | function(){return this.dataMax!==void 0&&this.dataMin!==void 0};e.showNoData=function(a){var b=this.options,a=a||b.lang.noData,b=b.noData;if(!this.noDataLabel)this.noDataLabel=this.renderer.label(a,0,0,null,null,null,null,null,"no-data").attr(b.attr).css(b.style).add(),this.noDataLabel.align(i(this.noDataLabel.getBBox(),b.position),!1,"plotBox")};e.hideNoData=function(){if(this.noDataLabel)this.noDataLabel=this.noDataLabel.destroy()};e.hasData=function(){for(var a=this.series,b=a.length;b--;)if(a[b].hasData()&& 12 | !a[b].options.isInternal)return!0;return!1};e.callbacks.push(function(a){c.addEvent(a,"load",g);c.addEvent(a,"redraw",g)})})(Highcharts); 13 | -------------------------------------------------------------------------------- /docs/clusters.md: -------------------------------------------------------------------------------- 1 | # Help 2 | 3 | 1. [Starting and stopping processes](processes.md) 4 | 1. [Controlling the Daemon](daemon.md) 5 | 1. Managing clusters 6 | 1. [Installing and running apps](apps.md) 7 | 1. [Remote access and monitoring (e.g. guv-web)](remote.md) 8 | 1. [Web interface](web.md) 9 | 1. [Web interface - configuration](web-config.md) 10 | 1. [Web interface - user management](web-users.md) 11 | 1. [Programmatic access](programmatic-access.md) 12 | 1. [Programmatic access - local](programmatic-access-local.md) 13 | 1. [Programmatic access - remote](programmatic-access-remote.md) 14 | 1. [Programmatic access - events](programmatic-access-events.md) 15 | 16 | ## Managing clusters 17 | 18 | To start an app as a cluster, add the `-i $n` option to the `guv start` command where `$n` is the number of workers you want. 19 | 20 | The number of workers is limited by the `cluster` module to `$cores - 1` where `$cores` is the number of cores on your machine. 21 | 22 | 1. [workers](#workers) 23 | 24 | ## workers 25 | 26 | After starting a process with `-i $num` (e.g. start ``$num` instances of a script), use the `workers` subcommand to adjust the number of cluster workers. 27 | 28 | ```sh 29 | guv workers 30 | ``` 31 | 32 | ### e.g. 33 | 34 | Make process 49308 (previously started with `-i 2`) run with 4 workers: 35 | 36 | ```sh 37 | $ guv workers 49308 4 38 | ``` 39 | 40 | The maximum workers you can set is dependent on your system as `num_cpus - 1` 41 | -------------------------------------------------------------------------------- /web/client/pages/host.js: -------------------------------------------------------------------------------- 1 | var PageView = require('./base') 2 | 3 | function endsWith (haystack, needle) { 4 | return haystack.substring(haystack.length - needle.length) === needle 5 | } 6 | 7 | module.exports = PageView.extend({ 8 | initialize: function () { 9 | // if this host is removed from the collection while we are looking at it, redirect the user to the overview 10 | this.listenTo(window.app.hosts, 'remove', function (host) { 11 | if (host.name === this.model.name) { 12 | window.app.navigate('/') 13 | } 14 | }) 15 | }, 16 | bindings: { 17 | 'model.name': '[data-hook=host-name]', 18 | 'model.status': { 19 | type: function (el, value) { 20 | if (value === 'connected' && (endsWith(window.location.href, 'apps') || endsWith(window.location.href, 'processes'))) { 21 | return 22 | } 23 | 24 | // if the status of a process changes while we are watching it, redirect the 25 | // user to a page with an appropriate message 26 | if (endsWith(window.location.href, value)) { 27 | return 28 | } 29 | 30 | // dirty looking setTimeout because the first time this code gets run, we 31 | // are inside the router.trigger callback for the default page and probably 32 | // haven't finished displaying it yet.. 33 | setTimeout(window.app.navigate.bind(window.app, '/host/' + this.model.name + '/' + value), 1) 34 | } 35 | } 36 | } 37 | }) 38 | -------------------------------------------------------------------------------- /test/lib/common/ManagedAppTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | sinon = require('sinon'), 3 | ManagedApp = require('../../../lib/common/ManagedApp'), 4 | EventEmitter = require('events').EventEmitter 5 | 6 | describe('ManagedApp', function () { 7 | var app, daemon 8 | 9 | beforeEach(function () { 10 | daemon = { 11 | switchApplicationRef: sinon.stub(), 12 | listApplicationRefs: sinon.stub(), 13 | updateApplicationRefs: sinon.stub(), 14 | currentRef: sinon.stub() 15 | } 16 | 17 | app = new ManagedApp({}, daemon) 18 | }) 19 | 20 | it('should proxy daemon methos', function () { 21 | expect(daemon.switchApplicationRef.called).to.be.false 22 | app.switchRef() 23 | expect(daemon.switchApplicationRef.called).to.be.true 24 | 25 | expect(daemon.listApplicationRefs.called).to.be.false 26 | app.listRefs() 27 | expect(daemon.listApplicationRefs.called).to.be.true 28 | 29 | expect(daemon.updateApplicationRefs.called).to.be.false 30 | app.updateRefs() 31 | expect(daemon.updateApplicationRefs.called).to.be.true 32 | 33 | expect(daemon.currentRef.called).to.be.false 34 | app.currentRef() 35 | expect(daemon.currentRef.called).to.be.true 36 | }) 37 | 38 | it('should update details', function () { 39 | expect(app.foo).to.not.exist 40 | 41 | app.update({ 42 | foo: 'bar' 43 | }) 44 | 45 | expect(app.foo).to.equal('bar') 46 | }) 47 | 48 | it('should survive updating details with null', function () { 49 | app.update(null) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /web/client/pages/process/overview.js: -------------------------------------------------------------------------------- 1 | var ProcessPage = require('../process') 2 | var templates = require('../../templates') 3 | var DetailsView = require('../../views/process/overview/running') 4 | var MemoryGraphView = require('../../views/process/overview/memory') 5 | var CpuGraphView = require('../../views/process/overview/cpu') 6 | var LatencyGraphView = require('../../views/process/overview/latency') 7 | 8 | module.exports = ProcessPage.extend({ 9 | pageTitle: function () { 10 | return 'Guvnor - ' + this.model.name + ' - ' + this.model.status 11 | }, 12 | template: templates.pages.process.overview, 13 | subviews: { 14 | details: { 15 | container: '[data-hook=details]', 16 | prepareView: function (el) { 17 | return new DetailsView({ 18 | model: this.model, 19 | el: el 20 | }) 21 | } 22 | }, 23 | memory: { 24 | container: '[data-hook=memory]', 25 | prepareView: function (el) { 26 | return new MemoryGraphView({ 27 | model: this.model, 28 | el: el 29 | }) 30 | } 31 | }, 32 | cpu: { 33 | container: '[data-hook=cpu]', 34 | prepareView: function (el) { 35 | return new CpuGraphView({ 36 | model: this.model, 37 | el: el 38 | }) 39 | } 40 | }, 41 | latency: { 42 | container: '[data-hook=latency]', 43 | prepareView: function (el) { 44 | return new LatencyGraphView({ 45 | model: this.model, 46 | el: el 47 | }) 48 | } 49 | } 50 | } 51 | }) 52 | -------------------------------------------------------------------------------- /web/public/javascript/highcharts/themes/grid-light.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Grid-light theme for Highcharts JS 3 | * @author Torstein Honsi 4 | */ 5 | 6 | // Load the fonts 7 | Highcharts.createElement('link', { 8 | href: 'http://fonts.googleapis.com/css?family=Dosis:400,600', 9 | rel: 'stylesheet', 10 | type: 'text/css' 11 | }, null, document.getElementsByTagName('head')[0]); 12 | 13 | Highcharts.theme = { 14 | colors: ["#7cb5ec", "#f7a35c", "#90ee7e", "#7798BF", "#aaeeee", "#ff0066", "#eeaaee", 15 | "#55BF3B", "#DF5353", "#7798BF", "#aaeeee"], 16 | chart: { 17 | backgroundColor: null, 18 | style: { 19 | fontFamily: "Dosis, sans-serif" 20 | } 21 | }, 22 | title: { 23 | style: { 24 | fontSize: '16px', 25 | fontWeight: 'bold', 26 | textTransform: 'uppercase' 27 | } 28 | }, 29 | tooltip: { 30 | borderWidth: 0, 31 | backgroundColor: 'rgba(219,219,216,0.8)', 32 | shadow: false 33 | }, 34 | legend: { 35 | itemStyle: { 36 | fontWeight: 'bold', 37 | fontSize: '13px' 38 | } 39 | }, 40 | xAxis: { 41 | gridLineWidth: 1, 42 | labels: { 43 | style: { 44 | fontSize: '12px' 45 | } 46 | } 47 | }, 48 | yAxis: { 49 | minorTickInterval: 'auto', 50 | title: { 51 | style: { 52 | textTransform: 'uppercase' 53 | } 54 | }, 55 | labels: { 56 | style: { 57 | fontSize: '12px' 58 | } 59 | } 60 | }, 61 | plotOptions: { 62 | candlestick: { 63 | lineColor: '#404048' 64 | } 65 | }, 66 | 67 | 68 | // General 69 | background2: '#F0F0EA' 70 | 71 | }; 72 | 73 | // Apply the theme 74 | Highcharts.setOptions(Highcharts.theme); 75 | -------------------------------------------------------------------------------- /docs/remote.md: -------------------------------------------------------------------------------- 1 | # Help 2 | 3 | 1. [Starting and stopping processes](processes.md) 4 | 1. [Controlling the Daemon](daemon.md) 5 | 1. [Managing clusters](clusters.md) 6 | 1. [Installing and running apps](apps.md) 7 | 1. Remote access and monitoring (e.g. guv-web) 8 | 1. [Web interface](web.md) 9 | 1. [Web interface - configuration](web-config.md) 10 | 1. [Web interface - user management](web-users.md) 11 | 1. [Programmatic access](programmatic-access.md) 12 | 1. [Programmatic access - local](programmatic-access-local.md) 13 | 1. [Programmatic access - remote](programmatic-access-remote.md) 14 | 1. [Programmatic access - events](programmatic-access-events.md) 15 | 16 | ## Remote access and monitoring 17 | 18 | These commands are for use with [guvnor-web](web.md). 19 | 20 | 1. [remoteconfig](#remoteconfig) 21 | 1. [useradd](#useradd) 22 | 1. [rmuser](#rmuser) 23 | 1. [lsusers](#lsusers) 24 | 1. [reset](#reset) 25 | 26 | ## remoteconfig 27 | 28 | For use with [guvnor-web](web.md). 29 | 30 | ```sh 31 | guv remoteconfig 32 | ``` 33 | 34 | ## useradd 35 | 36 | To add a user for [guvnor-web](web.md). If you specify `[hostname]` the configuration output is more likely to be correct. 37 | 38 | ```sh 39 | guv useradd [options] [hostname] 40 | ``` 41 | 42 | ## rmuser 43 | 44 | To remove a user for [guvnor-web](web.md). 45 | 46 | ```sh 47 | guv rmuser 48 | ``` 49 | 50 | ## lsusers 51 | 52 | To list users for [guvnor-web](web.md). 53 | 54 | ```sh 55 | guv lsusers 56 | ``` 57 | 58 | ## reset 59 | 60 | To reset the secret for a [guvnor-web](web.md) user. 61 | 62 | ```sh 63 | guv reset 64 | ``` 65 | -------------------------------------------------------------------------------- /web/client/views/process/snapshotlist/entry.js: -------------------------------------------------------------------------------- 1 | var View = require('ampersand-view') 2 | var templates = require('../../../templates') 3 | var ConfirmView = require('../../confirm') 4 | 5 | module.exports = View.extend({ 6 | template: templates.includes.process.snapshotlist.entry, 7 | bindings: { 8 | 'model.dateFormatted': '[data-hook=date]', 9 | 'model.path': '[data-hook=path]', 10 | 'model.sizeFormatted': '[data-hook=size]' 11 | }, 12 | events: { 13 | 'click [data-hook=downloadbutton]': 'downloadSnapshot', 14 | 'click [data-hook=removebutton]': 'removeSnapshot' 15 | }, 16 | downloadSnapshot: function (event) { 17 | event.target.blur() 18 | 19 | window.location = this.collection.url() + '/' + this.model.id 20 | }, 21 | removeSnapshot: function (event) { 22 | event.target.blur() 23 | 24 | var confirmView = new ConfirmView() 25 | confirmView.setMessage('Are you sure you want to remove this snapshot? This action cannot be undone.') 26 | 27 | window.app.modal.reset() 28 | window.app.modal.setTitle('Danger zone!') 29 | window.app.modal.setContent(confirmView) 30 | window.app.modal.setIsDanger(true) 31 | window.app.modal.setOkText('Remove') 32 | window.app.modal.setCallback(function () { 33 | this.model.isRemoving = true 34 | 35 | window.app.socket.emit('process:snapshot:remove', { 36 | host: this.parent.collection.parent.collection.parent.name, 37 | process: this.parent.collection.parent.id, 38 | snapshot: this.model.id 39 | }, function () {}) 40 | }.bind(this)) 41 | window.app.modal.show() 42 | } 43 | }) 44 | -------------------------------------------------------------------------------- /test/lib/daemon/common/ConfigLoaderTest.js: -------------------------------------------------------------------------------- 1 | var sinon = require('sinon'), 2 | expect = require('chai').expect, 3 | ConfigLoader = require('../../../../lib/daemon/common/ConfigLoader') 4 | 5 | describe('ConfigLoader', function () { 6 | it('should invoke afterPropertiesSet callback when config has been sent', function (done) { 7 | var configLoader = new ConfigLoader() 8 | configLoader._parentProcess = { 9 | once: sinon.stub(), 10 | send: sinon.stub() 11 | } 12 | configLoader._coercer = sinon.stub() 13 | 14 | configLoader._coercer.returnsArg(0) 15 | 16 | configLoader.afterPropertiesSet(function () { 17 | // should have applied config options 18 | expect(configLoader.foo).to.equal('bar') 19 | 20 | // but not ones prefixed with underscores 21 | expect(configLoader._foo).to.not.exist 22 | 23 | done() 24 | }) 25 | 26 | process.nextTick(function () { 27 | // should have asked parent process for config 28 | expect(configLoader._parentProcess.send.callCount).to.equal(1) 29 | expect(configLoader._parentProcess.send.getCall(0).args[0]).to.equal('process:config:request') 30 | 31 | // should have set up listener for config response 32 | expect(configLoader._parentProcess.once.callCount).to.equal(1) 33 | expect(configLoader._parentProcess.once.getCall(0).args[0]).to.equal('daemon:config:response') 34 | 35 | // invoke config response listener 36 | var callback = configLoader._parentProcess.once.getCall(0).args[1] 37 | 38 | callback({ 39 | foo: 'bar', 40 | _foo: 'bar' 41 | }) 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /web/client/buttons/remove.js: -------------------------------------------------------------------------------- 1 | var View = require('ampersand-view') 2 | var templates = require('../templates') 3 | var ConfirmView = require('../views/confirm') 4 | 5 | module.exports = View.extend({ 6 | template: templates.buttons.remove, 7 | bindings: { 8 | 'model.isRemoving': [{ 9 | type: 'booleanClass', 10 | no: 'fa-remove', 11 | selector: '[data-hook=removebutton] i' 12 | }, { 13 | type: 'booleanClass', 14 | name: 'fa-circle-o-notch', 15 | selector: '[data-hook=removebutton] i' 16 | }, { 17 | type: 'booleanClass', 18 | name: 'fa-spin', 19 | selector: '[data-hook=removebutton] i' 20 | }, { 21 | type: 'booleanAttribute', 22 | name: 'disabled', 23 | selector: '[data-hook=removebutton]' 24 | }] 25 | }, 26 | events: { 27 | 'click [data-hook=removebutton]': 'removeProcess' 28 | }, 29 | removeProcess: function (event) { 30 | event.target.blur() 31 | 32 | var confirmView = new ConfirmView() 33 | confirmView.setMessage('Are you sure you want to remove this process? This action cannot be undone.') 34 | 35 | window.app.modal.reset() 36 | window.app.modal.setTitle('Danger zone!') 37 | window.app.modal.setContent(confirmView) 38 | window.app.modal.setIsDanger(true) 39 | window.app.modal.setOkText('Remove') 40 | window.app.modal.setCallback(function () { 41 | this.model.isRemoving = true 42 | 43 | window.app.socket.emit('process:remove', { 44 | host: this.model.collection.parent.name, 45 | process: this.model.id 46 | }, function () { 47 | }) 48 | }.bind(this)) 49 | window.app.modal.show() 50 | } 51 | }) 52 | -------------------------------------------------------------------------------- /docs/daemon.md: -------------------------------------------------------------------------------- 1 | # Help 2 | 3 | 1. [Starting and stopping processes](processes.md) 4 | 1. Controlling the Daemon 5 | 1. [Managing clusters](clusters.md) 6 | 1. [Installing and running apps](apps.md) 7 | 1. [Remote access and monitoring (e.g. guv-web)](remote.md) 8 | 1. [Web interface](web.md) 9 | 1. [Web interface - configuration](web-config.md) 10 | 1. [Web interface - user management](web-users.md) 11 | 1. [Programmatic access](programmatic-access.md) 12 | 1. [Programmatic access - local](programmatic-access-local.md) 13 | 1. [Programmatic access - remote](programmatic-access-remote.md) 14 | 1. [Programmatic access - events](programmatic-access-events.md) 15 | 16 | ## Controlling the Daemon 17 | 18 | 1. [config](#config) 19 | 1. [kill](#kill) 20 | 1. [logs](#logs) 21 | 1. [dump](#dump) 22 | 1. [restore](#restore) 23 | 24 | ## config 25 | 26 | Prints out a configuration option 27 | 28 | ```sh 29 | guv config 30 | ``` 31 | 32 | ### e.g. 33 | 34 | ```sh 35 | $ guv config remote.inspector.enabled 36 | // prints 'true' 37 | ``` 38 | 39 | ## kill 40 | 41 | Stop all processes and kill the daemon. By default all currently running processes will be saved and restarted when guvnor restarts. 42 | 43 | ```sh 44 | guv kill 45 | ``` 46 | 47 | ## logs 48 | 49 | Show live logs for a process (or all processes if `` is omitted) in the console 50 | 51 | ```sh 52 | guv logs [pid] 53 | ``` 54 | 55 | ## dump 56 | 57 | Create `/etc/guvnor/processes.json` with a list of running processes to for use with `guv restore` 58 | 59 | ```sh 60 | guv dump 61 | ``` 62 | 63 | ## restore 64 | 65 | Restore running processes from `/etc/guvnor/processes.json` 66 | 67 | ```sh 68 | guv restore 69 | ``` 70 | -------------------------------------------------------------------------------- /lib/daemon/domain/PersistentStore.js: -------------------------------------------------------------------------------- 1 | var Autowire = require('wantsit').Autowire 2 | var async = require('async') 3 | var util = require('util') 4 | var Store = require('../../common/Store') 5 | 6 | var PersistentStore = function (factoryName, fileName) { 7 | Store.call(this, factoryName) 8 | 9 | this._file = fileName 10 | 11 | this._fs = Autowire 12 | this._jsonfile = Autowire 13 | this._logger = Autowire 14 | this._config = Autowire 15 | this._fileSystem = Autowire 16 | } 17 | util.inherits(PersistentStore, Store) 18 | 19 | PersistentStore.prototype.afterPropertiesSet = function (done) { 20 | this._file = this._fileSystem.getConfDir() + '/' + this._file 21 | 22 | this._jsonfile.readFile(this._file, function (error, array) { 23 | if ( 24 | error && 25 | // means someone did cat /dev/null > users.json 26 | error.type !== 'unexpected_eos' && 27 | // the file did not exist 28 | error.code !== 'ENOENT' 29 | ) { 30 | return done(error) 31 | } 32 | 33 | if (!array || !Array.isArray(array)) { 34 | return done() 35 | } 36 | 37 | async.parallel(array.map(function (details) { 38 | return function (callback) { 39 | this.create([details], callback) 40 | }.bind(this) 41 | }.bind(this)), done) 42 | }.bind(this)) 43 | } 44 | 45 | PersistentStore.prototype.save = function (callback) { 46 | this._jsonfile.writeFile(this._file, this._store, { 47 | mode: parseInt('0600', 8) 48 | }, callback) 49 | } 50 | 51 | PersistentStore.prototype.saveSync = function () { 52 | this._jsonfile.writeFileSync(this._file, this._store, { 53 | mode: parseInt('0600', 8) 54 | }) 55 | } 56 | 57 | module.exports = PersistentStore 58 | -------------------------------------------------------------------------------- /web/client/buttons/stop.js: -------------------------------------------------------------------------------- 1 | var View = require('ampersand-view') 2 | var templates = require('../templates') 3 | var notify = require('../helpers/notification') 4 | 5 | module.exports = View.extend({ 6 | template: templates.buttons.stop, 7 | bindings: { 8 | 'model.isStopping': [{ 9 | type: 'booleanClass', 10 | no: 'fa-stop', 11 | selector: '[data-hook=stopbutton] i' 12 | }, { 13 | type: 'booleanClass', 14 | name: 'fa-circle-o-notch', 15 | selector: '[data-hook=stopbutton] i' 16 | }, { 17 | type: 'booleanClass', 18 | name: 'fa-spin', 19 | selector: '[data-hook=stopbutton] i' 20 | }, { 21 | type: 'booleanAttribute', 22 | name: 'disabled', 23 | selector: '[data-hook=stopbutton]' 24 | }] 25 | }, 26 | events: { 27 | 'click [data-hook=stopbutton]': 'stopProcess' 28 | }, 29 | stopProcess: function (event) { 30 | event.target.blur() 31 | 32 | this.model.isStopping = true 33 | 34 | window.app.socket.emit('process:stop', { 35 | host: this.model.collection.parent.name, 36 | process: this.model.id 37 | }, function (error) { 38 | this.model.isStopping = false 39 | 40 | if (error) { 41 | notify({ 42 | header: 'Stop error', 43 | message: ['%s on %s has failed to stop - %s', this.model.name, this.model.collection.parent.name, error.message || error.code], 44 | type: 'danger' 45 | }) 46 | } else { 47 | notify({ 48 | header: 'Process stopped', 49 | message: ['%s on %s stopped', this.model.name, this.model.collection.parent.name], 50 | type: 'success' 51 | }) 52 | } 53 | }.bind(this)) 54 | } 55 | }) 56 | -------------------------------------------------------------------------------- /web/client/buttons/gc.js: -------------------------------------------------------------------------------- 1 | var View = require('ampersand-view') 2 | var templates = require('../templates') 3 | var notify = require('../helpers/notification') 4 | 5 | module.exports = View.extend({ 6 | template: templates.buttons.gc, 7 | bindings: { 8 | 'model.isGc': [{ 9 | type: 'booleanClass', 10 | no: 'fa-trash', 11 | selector: '[data-hook=gcbutton] i' 12 | }, { 13 | type: 'booleanClass', 14 | name: 'fa-circle-o-notch', 15 | selector: '[data-hook=gcbutton] i' 16 | }, { 17 | type: 'booleanClass', 18 | name: 'fa-spin', 19 | selector: '[data-hook=gcbutton] i' 20 | }, { 21 | type: 'booleanAttribute', 22 | name: 'disabled', 23 | selector: '[data-hook=gcbutton]' 24 | }] 25 | }, 26 | events: { 27 | 'click [data-hook=gcbutton]': 'garbageCollectProcess' 28 | }, 29 | garbageCollectProcess: function (event) { 30 | event.target.blur() 31 | 32 | this.model.isGc = true 33 | 34 | window.app.socket.emit('process:gc', { 35 | host: this.model.collection.parent.name, 36 | process: this.model.id 37 | }, function (error) { 38 | this.model.isGc = false 39 | 40 | if (error) { 41 | notify({ 42 | header: 'Garbage collection error', 43 | message: ['%s on %s has failed to collect garbage - %s', this.model.name, this.model.collection.parent.name, error.message], 44 | type: 'danger' 45 | }) 46 | } else { 47 | notify({ 48 | header: 'Garbage collection complete', 49 | message: ['%s on %s has collected garbage', this.model.name, this.model.collection.parent.name], 50 | type: 'success' 51 | }) 52 | } 53 | }.bind(this)) 54 | } 55 | }) 56 | -------------------------------------------------------------------------------- /web/client/buttons/restart.js: -------------------------------------------------------------------------------- 1 | var View = require('ampersand-view') 2 | var templates = require('../templates') 3 | var notify = require('../helpers/notification') 4 | 5 | module.exports = View.extend({ 6 | template: templates.buttons.restart, 7 | bindings: { 8 | 'model.isRestarting': [{ 9 | type: 'booleanClass', 10 | no: 'fa-refresh', 11 | selector: '[data-hook=restartbutton] i' 12 | }, { 13 | type: 'booleanClass', 14 | name: 'fa-circle-o-notch', 15 | selector: '[data-hook=restartbutton] i' 16 | }, { 17 | type: 'booleanClass', 18 | name: 'fa-spin', 19 | selector: '[data-hook=restartbutton] i' 20 | }, { 21 | type: 'booleanAttribute', 22 | name: 'disabled', 23 | selector: '[data-hook=restartbutton]' 24 | }] 25 | }, 26 | events: { 27 | 'click [data-hook=restartbutton]': 'restartProcess' 28 | }, 29 | restartProcess: function (event) { 30 | event.target.blur() 31 | 32 | this.model.isRestarting = true 33 | 34 | window.app.socket.emit('process:restart', { 35 | host: this.model.collection.parent.name, 36 | process: this.model.id 37 | }, function (error) { 38 | this.model.isRestarting = false 39 | 40 | if (error) { 41 | notify({ 42 | header: 'Restart error', 43 | message: ['%s on %s failed to restart - %s', this.model.name, this.model.collection.parent.name, error.message], 44 | type: 'danger' 45 | }) 46 | } else { 47 | notify({ 48 | header: 'Restart complete', 49 | message: ['%s on %s restarted', this.model.name, this.model.collection.parent.name], 50 | type: 'success' 51 | }) 52 | } 53 | }.bind(this)) 54 | } 55 | }) 56 | -------------------------------------------------------------------------------- /lib/daemon/process/index.js: -------------------------------------------------------------------------------- 1 | var Container = require('wantsit').Container 2 | var winston = require('winston') 3 | 4 | var container = new Container({ 5 | timeout: 0 6 | }) 7 | container.register('dnode', require('boss-dnode')) 8 | container.register('usage', require('usage')) 9 | container.register('posix', require('posix')) 10 | container.register('fs', require('fs')) 11 | container.register('heapdump', require('heapdump')) 12 | container.register('coercer', require('coercer')) 13 | container.register('lag', require('event-loop-lag')) 14 | container.register('logger', new winston.Logger()) 15 | container.register('process', process) 16 | container.createAndRegister('remoteProcessLogger', require('../common/RemoteProcessLogger'), [{ 17 | name: 'remote' 18 | }], function (error, logger) { 19 | if (!error) { 20 | container.find('logger').add(logger, null, true) 21 | } 22 | }) 23 | container.createAndRegister('consoleDebugLogger', require('../common/ConsoleDebugLogger'), [{ 24 | name: 'console', 25 | colorize: true 26 | }], function (error, logger) { 27 | if (!error) { 28 | container.find('logger').add(logger, null, true) 29 | } 30 | }) 31 | container.createAndRegister('logRedirector', require('../common/LogRedirector')) 32 | container.createAndRegister('parentProcess', require('../common/ParentProcess')) 33 | container.createAndRegister('exceptionHandler', require('../common/ExceptionHandler')) 34 | container.createAndRegister('userInfo', require('../common/UserInfo')) 35 | container.createAndRegister('config', require('../common/ConfigLoader')) 36 | container.createAndRegister('processRpc', require('./ProcessRPC')) 37 | container.createAndRegister('processWrapper', require('./ProcessWrapper')) 38 | container.createAndRegister('latencyMonitor', require('../common/LatencyMonitor')) 39 | -------------------------------------------------------------------------------- /test/lib/remote/RemoteProcessTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | sinon = require('sinon'), 3 | RemoteProcess = require('../../../lib/remote/RemoteProcess') 4 | 5 | describe('RemoteProcess', function () { 6 | var remoteProcess, daemon 7 | 8 | beforeEach(function () { 9 | daemon = {} 10 | 11 | remoteProcess = new RemoteProcess({}, daemon) 12 | remoteProcess._logger = { 13 | debug: sinon.stub() 14 | } 15 | }) 16 | 17 | it('should have the daemon property', function () { 18 | expect(remoteProcess._daemon).to.equal(daemon) 19 | }) 20 | 21 | it('should connect to a remote process via the daemon', function (done) { 22 | var remote = { 23 | foo: sinon.stub() 24 | } 25 | 26 | daemon._connectToProcess = sinon.stub().callsArgWith(1, undefined, remote) 27 | 28 | remoteProcess.connect(function (error) { 29 | expect(error).to.not.exist 30 | expect(remoteProcess._rpc.foo).to.be.a('function') 31 | 32 | done() 33 | }) 34 | }) 35 | 36 | it('should propagate error when connecting to a remote process', function (done) { 37 | var error = new Error('Urk!') 38 | 39 | daemon._connectToProcess = sinon.stub().callsArgWith(1, error) 40 | 41 | remoteProcess.connect(function (er) { 42 | expect(er).to.equal(error) 43 | 44 | done() 45 | }) 46 | }) 47 | 48 | it('should disconnect from remote process', function (done) { 49 | remoteProcess.disconnect(done) 50 | }) 51 | 52 | it('should disconnect from remote process via rpc', function (done) { 53 | remoteProcess._rpc.disconnect = sinon.stub().callsArg(0) 54 | 55 | remoteProcess.disconnect(function () { 56 | expect(remoteProcess._rpc.disconnect.called).to.be.true 57 | 58 | done() 59 | }) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /test/lib/daemon/common/LogRedirectorTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | sinon = require('sinon'), 3 | path = require('path'), 4 | LogRedirector = require('../../../../lib/daemon/common/LogRedirector') 5 | 6 | describe('LogRedirector', function () { 7 | var stderr, stdout 8 | 9 | before(function () { 10 | stderr = process.stderr.write 11 | stdout = process.stdout.write 12 | }) 13 | 14 | // ProcessWrapper has the side effects of overriding stderr and stdout so restore them after every test 15 | afterEach(function () { 16 | process.stderr.write = stderr 17 | process.stdout.write = stdout 18 | 19 | delete process.send 20 | }) 21 | 22 | var logRedirector 23 | 24 | beforeEach(function () { 25 | logRedirector = new LogRedirector() 26 | logRedirector._logger = { 27 | info: sinon.stub(), 28 | warn: sinon.stub(), 29 | error: sinon.stub(), 30 | debug: sinon.stub() 31 | } 32 | 33 | // LogRediretor will use the console if a parent process is unavailable 34 | // so we stub out the parent processes' ipc channel 35 | process.send = sinon.stub() 36 | }) 37 | 38 | it('should redirect logs', function () { 39 | var message = 'hello world' 40 | 41 | expect(logRedirector._logger.info.callCount).to.equal(0) 42 | expect(logRedirector._logger.error.callCount).to.equal(0) 43 | 44 | logRedirector.afterPropertiesSet() 45 | 46 | console.info(message) 47 | expect(logRedirector._logger.info.callCount).to.equal(1) 48 | expect(logRedirector._logger.info.getCall(0).args[0]).to.equal(message + '\n') 49 | 50 | console.error(message) 51 | expect(logRedirector._logger.error.callCount).to.equal(1) 52 | expect(logRedirector._logger.error.getCall(0).args[0]).to.equal(message + '\n') 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /lib/cli/Table.js: -------------------------------------------------------------------------------- 1 | require('colors') 2 | 3 | var stripAnsi = require('strip-ansi') 4 | 5 | var Table = function (emptyMessage) { 6 | this._rows = [] 7 | 8 | this._emptyMessage = emptyMessage 9 | this._columnLengths = [] 10 | } 11 | 12 | Table.prototype._calculateLengths = function (data) { 13 | data.forEach(function (datum, index) { 14 | var length = stripAnsi('' + datum).length 15 | 16 | if (!this._columnLengths[index] || length > this._columnLengths[index]) { 17 | this._columnLengths[index] = length 18 | } 19 | }.bind(this)) 20 | } 21 | 22 | Table.prototype.addHeader = function (data) { 23 | this._calculateLengths(data) 24 | 25 | this._header = data 26 | } 27 | 28 | Table.prototype.addRow = function (data) { 29 | this._calculateLengths(data) 30 | 31 | this._rows.push(data) 32 | } 33 | 34 | Table.prototype.print = function (func) { 35 | if (this._rows.length === 0) { 36 | return func(this._emptyMessage.bold) 37 | } 38 | 39 | if (this._header) { 40 | var output = '' 41 | 42 | this._header.forEach(function (item, index) { 43 | output += this._rpad(item, this._columnLengths[index]) + ' ' 44 | }.bind(this)) 45 | 46 | func(output.bold) 47 | } 48 | 49 | this._rows.forEach(function (row) { 50 | var output = '' 51 | 52 | row.forEach(function (item, index) { 53 | output += this._rpad(item, this._columnLengths[index]) + ' ' 54 | }.bind(this)) 55 | 56 | output += '' 57 | 58 | func(output) 59 | }.bind(this)) 60 | } 61 | 62 | Table.prototype._rpad = function (thing, len) { 63 | if (thing === undefined || thing === null) { 64 | thing = '' 65 | } else { 66 | thing = thing + '' 67 | } 68 | 69 | while (stripAnsi(thing).length < len) { 70 | thing = thing + ' ' 71 | } 72 | 73 | return thing 74 | } 75 | 76 | module.exports = Table 77 | -------------------------------------------------------------------------------- /lib/daemon/rpc/tunnel.js: -------------------------------------------------------------------------------- 1 | require('../../common/HelpfulError') 2 | 3 | var posix = require('posix') 4 | var dnode = require('boss-dnode') 5 | 6 | function sendErrorAndExit (message, error) { 7 | if (arguments.length === 1) { 8 | error = message 9 | message = error.message 10 | } 11 | 12 | process.send({ 13 | type: 'remote:error', 14 | args: [{ 15 | message: message, 16 | stack: error.stack, 17 | code: error.code 18 | }] 19 | }) 20 | 21 | process.exit(1) 22 | } 23 | 24 | function switchUser (userName) { 25 | try { 26 | // drop privileges 27 | var user = posix.getpwnam(userName) 28 | 29 | if (user.gid !== process.getgid()) { 30 | process.setgid(user.gid) 31 | process.setgroups([]) // Remove old groups 32 | process.initgroups(user.uid, user.gid) // Add user groups 33 | } 34 | 35 | if (user.uid !== process.getuid()) { 36 | process.setuid(user.uid) // Switch to requested user 37 | } 38 | } catch (e) { 39 | sendErrorAndExit('Could not switch to ' + userName, e) 40 | } 41 | } 42 | 43 | // e.g. we are root but really alex wants to run a process as alan. 44 | // root can switch to alan but perhaps alex can't, so first switch to alex 45 | switchUser(process.env.GUVNOR_RUNNING_USER) 46 | // then switch to alan 47 | switchUser(process.env.GUVNOR_TARGET_USER) 48 | 49 | // create dnode connection 50 | var client 51 | 52 | try { 53 | client = dnode.connect(process.env.GUVNOR_SOCKET) 54 | } catch (e) { 55 | sendErrorAndExit(e) 56 | } 57 | 58 | client.on('remote', function (remote) { 59 | var d = dnode(remote) 60 | process.stdin.pipe(d).pipe(process.stdout) 61 | process.send({ 62 | type: 'remote:ready' 63 | }) 64 | }) 65 | client.on('error', sendErrorAndExit) 66 | 67 | // make sure we don't exit immediately 68 | process.stdin.resume() 69 | -------------------------------------------------------------------------------- /test/lib/cli/TableTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | sinon = require('sinon'), 3 | Table = require('../../../lib/cli/Table') 4 | 5 | describe('Table', function () { 6 | var table 7 | 8 | beforeEach(function () { 9 | table = new Table('empty') 10 | }) 11 | 12 | it('should return a message when the table is empty', function (done) { 13 | table.print(function (output) { 14 | expect(output).to.contain('empty') 15 | 16 | done() 17 | }) 18 | }) 19 | 20 | it('should update lengths when adding a header', function () { 21 | table.addHeader(['foo', 'barbar', 'bazbazbaz']) 22 | 23 | expect(table._columnLengths[0]).to.equal(3) 24 | expect(table._columnLengths[1]).to.equal(6) 25 | expect(table._columnLengths[2]).to.equal(9) 26 | }) 27 | 28 | it('should update lengths when adding a row', function () { 29 | table.addRow(['foo', 'barbar', 'bazbazbaz']) 30 | 31 | expect(table._columnLengths[0]).to.equal(3) 32 | expect(table._columnLengths[1]).to.equal(6) 33 | expect(table._columnLengths[2]).to.equal(9) 34 | }) 35 | 36 | it('should choose the longer of two column entries', function () { 37 | table.addRow(['foofoofoo']) 38 | table.addRow(['bar']) 39 | 40 | expect(table._columnLengths[0]).to.equal(9) 41 | }) 42 | 43 | it('should print a table', function (done) { 44 | table.addHeader(['foo']) 45 | table.addRow(['barbar']) 46 | table.addRow(['bazbaz']) 47 | 48 | expect(table._columnLengths[0]).to.equal(6) 49 | 50 | var row = 0 51 | 52 | table.print(function (output) { 53 | if (row == 0) { 54 | expect(output).to.contain('foo') 55 | } else if (row == 1) { 56 | expect(output).to.contain('barbar') 57 | } else if (row == 2) { 58 | expect(output).to.contain('bazbaz') 59 | 60 | done() 61 | } 62 | 63 | row++ 64 | }) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /web/client/pages/process.js: -------------------------------------------------------------------------------- 1 | var PageView = require('./base') 2 | 3 | function endsWith (haystack, needle) { 4 | return haystack.substring(haystack.length - needle.length) === needle 5 | } 6 | 7 | module.exports = PageView.extend({ 8 | pageTitle: function () { 9 | return 'Guvnor - ' + this.model.name + ' - ' + this.model.status 10 | }, 11 | initialize: function () { 12 | // if this process is removed from the collection while we are looking at it, redirect the user to the host overview 13 | this.listenTo(window.app.hosts.get(this.model.collection.parent.name).processes, 'remove', function (process) { 14 | if (process.id === this.model.id) { 15 | window.app.navigate('/host/' + this.model.collection.parent.name) 16 | } 17 | }) 18 | }, 19 | bindings: { 20 | 'model.name': '[data-hook=process-name]', 21 | 'model.status': { 22 | type: function (el, value) { 23 | if (value === 'running' && (endsWith(window.location.href, 'logs') || endsWith(window.location.href, 'execeptions') || endsWith(window.location.href, 'snapshots'))) { 24 | return 25 | } 26 | 27 | // if the status of a process changes while we are watching it, redirect the 28 | // user to a page with an appropriate message 29 | if (window.location.href.substring(window.location.href.length - value.length) === value) { 30 | return 31 | } 32 | 33 | // dirty looking setTimeout because the first time this code gets run, we 34 | // are inside the router.trigger callback for the default page and probably 35 | // haven't finished displaying it yet.. 36 | setTimeout(window.app.navigate.bind(window.app, '/host/' + this.model.collection.parent.name + '/process/' + this.model.id + '/' + value)) 37 | } 38 | }, 39 | 'model.collection.parent.status': { 40 | type: function (el, value) { 41 | } 42 | } 43 | } 44 | }) 45 | -------------------------------------------------------------------------------- /lib/daemon/action/UserProcess.js: -------------------------------------------------------------------------------- 1 | var Autowire = require('wantsit').Autowire 2 | 3 | var UserProcess = function () { 4 | this._posix = Autowire 5 | } 6 | 7 | UserProcess.prototype.afterPropertiesSet = function () { 8 | if (!this._dropPrivileges()) { 9 | return 10 | } 11 | 12 | this._run(function () { 13 | process.send({ event: 'remote:ready' }) 14 | }, function (error) { 15 | if (error) { 16 | // there was an error invoking the remote method 17 | process.send({ 18 | event: 'remote:error', 19 | args: [error] 20 | }) 21 | } else { 22 | // invoking the remote method succeeded - n.b. the actual method 23 | // may still have errored 24 | process.send({ 25 | event: 'remote:success', 26 | args: Array.prototype.slice.call(arguments, 1) 27 | }) 28 | } 29 | }) 30 | } 31 | 32 | UserProcess.prototype._dropPrivileges = function () { 33 | try { 34 | var user 35 | var num = parseInt(process.env.GUVNOR_USER, 10) 36 | 37 | if (isNaN(num)) { 38 | user = this._posix.getpwnam(process.env.GUVNOR_USER) 39 | } else { 40 | user = this._posix.getpwnam(num) 41 | } 42 | 43 | if (user.gid !== process.getgid()) { 44 | process.setgid(user.gid) 45 | process.setgroups([]) // Remove old groups 46 | process.initgroups(user.uid, user.gid) // Add user groups 47 | } 48 | 49 | if (user.uid !== process.getuid()) { 50 | process.setuid(user.uid) // Switch to requested user 51 | } 52 | } catch (error) { 53 | this._sendError(error) 54 | 55 | return false 56 | } 57 | 58 | return true 59 | } 60 | 61 | UserProcess.prototype._sendError = function (error) { 62 | process.send({ 63 | event: 'remote:error', 64 | args: [{ 65 | message: error.message, 66 | stack: error.stack 67 | }] 68 | }) 69 | } 70 | 71 | UserProcess.prototype._run = function () { 72 | // subclasses should implement this method 73 | } 74 | 75 | module.exports = UserProcess 76 | -------------------------------------------------------------------------------- /lib/daemon/domain/PersistentProcessInfoStore.js: -------------------------------------------------------------------------------- 1 | var util = require('util') 2 | var PersistentStore = require('./PersistentStore') 3 | var path = require('path') 4 | 5 | var PersistentProcessInfoStore = function (factoryName, fileName) { 6 | PersistentStore.call(this, factoryName, fileName) 7 | } 8 | util.inherits(PersistentProcessInfoStore, PersistentStore) 9 | 10 | PersistentProcessInfoStore.prototype.save = function (callback) { 11 | this._jsonfile.writeFile(this._file, this._store.map(this._removeRuntimeProperties.bind(this)), { 12 | mode: parseInt('0600', 8) 13 | }, callback) 14 | } 15 | 16 | PersistentProcessInfoStore.prototype.saveSync = function () { 17 | this._jsonfile.writeFileSync(this._file, this._store.map(this._removeRuntimeProperties.bind(this)), { 18 | mode: parseInt('0600', 8) 19 | }) 20 | } 21 | 22 | PersistentProcessInfoStore.prototype._removeRuntimeProperties = function (processInfo) { 23 | var output = JSON.parse(JSON.stringify(processInfo)) 24 | 25 | delete output.id 26 | delete output.pid 27 | delete output.debugPort 28 | delete output.restarts 29 | delete output.totalRestarts 30 | delete output.status 31 | delete output.socket 32 | delete output.manager 33 | 34 | if (!output.debug) { 35 | delete output.debug 36 | } 37 | 38 | if (!processInfo.cluster) { 39 | delete output.instances 40 | } 41 | 42 | delete output.cluster 43 | 44 | if (path.dirname(output.script) === output.cwd) { 45 | delete output.cwd 46 | } 47 | 48 | if (output.argv.length === 0) { 49 | delete output.argv 50 | } 51 | 52 | if (output.execArgv.length === 0) { 53 | delete output.execArgv 54 | } 55 | 56 | if (output.restartOnError) { 57 | delete output.restartOnError 58 | } 59 | 60 | if (output.restartRetries === 5) { 61 | delete output.restartRetries 62 | } 63 | 64 | if (output.name === path.basename(output.script)) { 65 | delete output.name 66 | } 67 | 68 | return output 69 | } 70 | 71 | module.exports = PersistentProcessInfoStore 72 | -------------------------------------------------------------------------------- /web/client/forms/app.js: -------------------------------------------------------------------------------- 1 | var FormView = require('ampersand-form-view') 2 | var InputView = require('ampersand-input-view') 3 | var CheckboxView = require('ampersand-checkbox-view') 4 | 5 | module.exports = FormView.extend({ 6 | fields: function () { 7 | return [ 8 | new InputView({ 9 | label: 'Name', 10 | name: 'name', 11 | value: this.model.name || '', 12 | required: false, 13 | placeholder: 'Name', 14 | parent: this 15 | }), 16 | new InputView({ 17 | label: 'User', 18 | name: 'user', 19 | value: this.model.user || '', 20 | required: false, 21 | placeholder: 'User', 22 | parent: this 23 | }), 24 | new InputView({ 25 | label: 'Url', 26 | name: 'url', 27 | value: this.model.url || '', 28 | required: false, 29 | placeholder: 'Url', 30 | parent: this 31 | }), 32 | new InputView({ 33 | label: 'Cwd', 34 | name: 'cwd', 35 | value: this.model.cwd || '', 36 | required: false, 37 | placeholder: 'Cwd', 38 | parent: this 39 | }), 40 | new InputView({ 41 | label: 'Exec Argv', 42 | name: 'execArgv', 43 | value: this.model.execArgv || '', 44 | required: false, 45 | placeholder: 'Exec Argv', 46 | parent: this 47 | }), 48 | new InputView({ 49 | label: 'Group', 50 | name: 'group', 51 | value: this.model.group || '', 52 | required: false, 53 | placeholder: 'Group', 54 | parent: this 55 | }), 56 | new CheckboxView({ 57 | label: 'Debug', 58 | name: 'debug', 59 | value: this.model.debug, 60 | parent: this 61 | }), 62 | new InputView({ 63 | label: 'Instances', 64 | name: 'instances', 65 | value: this.model.instances || '', 66 | required: false, 67 | placeholder: 'Instances', 68 | parent: this 69 | }) 70 | ] 71 | } 72 | }) 73 | -------------------------------------------------------------------------------- /web/client/pages/process/logs.js: -------------------------------------------------------------------------------- 1 | var ProcessPage = require('../process') 2 | var templates = require('../../templates') 3 | var CollectionView = require('ampersand-collection-view') 4 | var LogListView = require('../../views/process/loglist/entry') 5 | 6 | module.exports = ProcessPage.extend({ 7 | template: templates.pages.process.logs, 8 | initialize: function () { 9 | ProcessPage.prototype.initialize.call(this) 10 | 11 | this.listenTo(this.model.logs, 'add', this.scrollLogs.bind(this)) 12 | }, 13 | subviews: { 14 | logs: { 15 | container: '[data-hook=logs]', 16 | prepareView: function (el) { 17 | return new CollectionView({ 18 | el: el, 19 | collection: this.model.logs, 20 | view: LogListView 21 | }) 22 | } 23 | } 24 | }, 25 | events: { 26 | 'click button.logs-time': 'toggleTimes', 27 | 'click button.logs-pin': 'pinLogs', 28 | 'click button.logs-clear': 'clearLogs' 29 | }, 30 | toggleTimes: function (event) { 31 | this.model.shouldShowTimes = !this.model.shouldShowTimes 32 | }, 33 | pinLogs: function (event) { 34 | this.model.areLogsPinned = !this.model.areLogsPinned 35 | }, 36 | clearLogs: function (event) { 37 | this.model.logs.forEach(function (log) { 38 | log.visible = false 39 | }) 40 | }, 41 | scrollLogs: function () { 42 | setTimeout(function () { 43 | if (!this.model.areLogsPinned) { 44 | var list = this.query('[data-hook=logs]') 45 | 46 | if (!list) { 47 | return 48 | } 49 | 50 | list.scrollTop = list.scrollHeight 51 | } 52 | }.bind(this), 100) 53 | }, 54 | bindings: { 55 | 'model.areLogsPinned': { 56 | type: 'booleanClass', 57 | name: 'active', 58 | selector: '.logs-pin' 59 | }, 60 | 'model.shouldShowTimes': [{ 61 | type: 'booleanClass', 62 | name: 'showTimes', 63 | selector: 'ul.logs' 64 | }, { 65 | type: 'booleanClass', 66 | name: 'active', 67 | selector: '.logs-time' 68 | }] 69 | } 70 | }) 71 | -------------------------------------------------------------------------------- /test/lib/daemon/rpc/AdminRPCTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | sinon = require('sinon'), 3 | inherits = require('util').inherits, 4 | AdminRPC = require('../../../../lib/daemon/rpc/AdminRPC'), 5 | EventEmitter = require('events').EventEmitter 6 | 7 | describe('AdminRPC', function () { 8 | 9 | it('should expose admin methods', function () { 10 | var rpc = new AdminRPC() 11 | rpc._config = { 12 | guvnor: {} 13 | } 14 | rpc._processFactory = { 15 | connect: sinon.stub() 16 | } 17 | rpc._guvnor = { 18 | kill: sinon.stub(), 19 | remoteHostConfig: sinon.stub(), 20 | addRemoteUser: sinon.stub(), 21 | removeRemoteUser: sinon.stub(), 22 | listRemoteUsers: sinon.stub(), 23 | rotateRemoteUserKeys: sinon.stub(), 24 | generateRemoteRpcCertificates: sinon.stub(), 25 | startProcessAsUser: sinon.stub(), 26 | dumpProcesses: sinon.stub(), 27 | restoreProcesses: sinon.stub() 28 | } 29 | rpc._dnode = sinon.stub() 30 | rpc._fileSystem = { 31 | getRunDir: sinon.stub() 32 | } 33 | rpc._fs = { 34 | existsSync: sinon.stub(), 35 | unlinkSync: sinon.stub(), 36 | chown: sinon.stub(), 37 | exists: sinon.stub() 38 | } 39 | rpc._logger = { 40 | info: sinon.stub(), 41 | warn: sinon.stub(), 42 | error: sinon.stub(), 43 | debug: sinon.stub() 44 | } 45 | 46 | var socket = new EventEmitter() 47 | var dnode = new EventEmitter() 48 | dnode.listen = sinon.stub() 49 | dnode.listen.returns(socket) 50 | rpc._dnode.returns(dnode) 51 | 52 | rpc.afterPropertiesSet() 53 | 54 | expect(rpc.kill).to.be.a('function') 55 | expect(rpc.remoteHostConfig).to.be.a('function') 56 | expect(rpc.addRemoteUser).to.be.a('function') 57 | expect(rpc.removeRemoteUser).to.be.a('function') 58 | expect(rpc.listRemoteUsers).to.be.a('function') 59 | expect(rpc.rotateRemoteUserKeys).to.be.a('function') 60 | expect(rpc.dumpProcesses).to.be.a('function') 61 | expect(rpc.restoreProcesses).to.be.a('function') 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /lib/web/GuvnorWeb.js: -------------------------------------------------------------------------------- 1 | var Container = require('wantsit').Container 2 | var ObjectFactory = require('wantsit').ObjectFactory 3 | var logger = require('andlog') 4 | 5 | var GuvnorWeb = function () { 6 | 7 | } 8 | 9 | GuvnorWeb.prototype.start = function () { 10 | process.title = 'guvnor-web' 11 | 12 | // make errors a little more descriptive 13 | process.on('uncaughtException', function (error) { 14 | console.error('Uncaught error', error.message) 15 | console.error(error.stack ? error.stack : 'No stack trace available') 16 | 17 | process.exit(1) 18 | }) 19 | 20 | process.on('SIGABRT', function () { 21 | console.error('Received SIGABRT') 22 | }) 23 | 24 | // create container 25 | var container = new Container({ 26 | timeout: 0 27 | }) 28 | container.on('error', function (error) { 29 | console.warn('Container error:', error.message || error.message.stack) 30 | }) 31 | 32 | // parse configuration 33 | container.createAndRegister('config', require('./components/Configuration')) 34 | container.register('logger', logger) 35 | container.register('posix', require('posix')) 36 | container.register('remote', require('../remote')) 37 | container.register('webSocketResponder', { 38 | broadcast: function () {} 39 | }) 40 | container.register('moonbootsConfig', { 41 | 'isDev': process.env.NODE_ENV === 'development' 42 | }) 43 | container.createAndRegister('hostDataFactory', ObjectFactory, [require('./domain/HostData')]) 44 | container.createAndRegister('processDataFactory', ObjectFactory, [require('./domain/ProcessData')]) 45 | container.createAndRegister('hostList', require('./components/HostList')) 46 | container.createAndRegister('server', require('./Server')) 47 | 48 | // optional dependency, don't care if it fails 49 | try { 50 | container.register('mdns', require('mdns')) 51 | } catch (e) { 52 | logger.info('Failed to register mdns component. If running Linux, please run `$ sudo apt-get install libavahi-compat-libdnssd-dev` before installing guvnor.') 53 | logger.info(e.stack) 54 | } 55 | } 56 | 57 | module.exports = GuvnorWeb 58 | -------------------------------------------------------------------------------- /lib/daemon/util/FileSystem.js: -------------------------------------------------------------------------------- 1 | var Autowire = require('wantsit').Autowire 2 | 3 | var FileSystem = function () { 4 | this._config = Autowire 5 | this._logger = Autowire 6 | this._posix = Autowire 7 | this._fs = Autowire 8 | this._mkdirp = Autowire 9 | } 10 | 11 | FileSystem.prototype.afterPropertiesSet = function () { 12 | this._createDirectorySync(this._config.guvnor.logdir, parseInt('0770', 8)) 13 | this._createDirectorySync(this._config.guvnor.rundir, parseInt('0770', 8)) 14 | this._createDirectorySync(this._config.guvnor.rundir + '/processes', parseInt('0770', 8)) 15 | this._createDirectorySync(this._config.guvnor.confdir, parseInt('0770', 8)) 16 | this._createDirectorySync(this._config.guvnor.appdir, parseInt('0770', 8)) 17 | } 18 | 19 | FileSystem.prototype.getRunDir = function () { 20 | return this._config.guvnor.rundir 21 | } 22 | 23 | FileSystem.prototype.getLogDir = function () { 24 | return this._config.guvnor.logdir 25 | } 26 | 27 | FileSystem.prototype.getConfDir = function () { 28 | return this._config.guvnor.confdir 29 | } 30 | 31 | FileSystem.prototype.getAppDir = function () { 32 | return this._config.guvnor.appdir 33 | } 34 | 35 | FileSystem.prototype._createDirectorySync = function (directory, mode) { 36 | var gid = this._posix.getgrnam(this._config.guvnor.group).gid 37 | var oldmask = process.umask() 38 | 39 | try { 40 | if (this._fs.existsSync(directory)) { 41 | return 42 | } 43 | 44 | this._logger.debug('Creating', directory, 'with mode', mode.toString(8)) 45 | 46 | oldmask = process.umask(0) 47 | this._mkdirp.sync(directory, { 48 | mode: mode 49 | }) 50 | process.umask(oldmask) 51 | 52 | this._fs.chownSync(directory, process.getuid(), gid) 53 | } catch (error) { 54 | process.umask(oldmask) 55 | 56 | // we've been run as a non-root user 57 | if (error.code === 'EACCES') { 58 | this._logger.error('I do not have permission to create', directory, '- please run me as a privileged user.') 59 | process.exit(-1) 60 | } 61 | 62 | throw error 63 | } 64 | } 65 | 66 | module.exports = FileSystem 67 | -------------------------------------------------------------------------------- /web/public/css/app/hosts.styl: -------------------------------------------------------------------------------- 1 | 2 | .navbar 3 | border-color: background 4 | 5 | .navbar-brand 6 | display: block 7 | padding-left: 55px 8 | background-repeat: no-repeat 9 | background-image: url('/images/guvnor.png') 10 | background-size: 50px 50px 11 | color: #777 12 | 13 | .navbar-brand:hover 14 | color: #777 15 | 16 | .navbar-header small 17 | display: inline-block 18 | float: right 19 | font-size: 100% 20 | padding: 15px 21 | 22 | .nav > li.host-name 23 | padding: 0 24 | 25 | #nav-shadow 26 | position: absolute 27 | top: 0 28 | right: 0 29 | bottom: 0 30 | left: 0 31 | background-color: rgba(0, 0, 0, 0.8) 32 | z-index: 100 33 | display: none 34 | 35 | #nav-shadow.shadow 36 | display: block 37 | 38 | @media (min-width: 768px) 39 | #nav-shadow.shadow 40 | display: none 41 | 42 | #wrapper .navbar li, #wrapper .navbar a 43 | width: 100% 44 | 45 | #wrapper .navbar a 46 | white-space: nowrap 47 | overflow: hidden 48 | text-overflow: ellipsis 49 | 50 | .host-list 51 | height: calc(unquote('100% - 50px')) 52 | 53 | .host-name 54 | padding: 15px 55 | background-color: #181818 56 | border-bottom: 10px solid #222 57 | 58 | ul.process.active li 59 | background-color: #0A0A0A 60 | display: block 61 | 62 | li 63 | border-left: 10px solid #333 64 | border-bottom: 2px solid #222 65 | 66 | li.processes 67 | border-left-width: 20px 68 | 69 | li.processName 70 | border-left-width: 20px 71 | 72 | li.processName .nodeIcon 73 | color: #7fbf00 74 | 75 | li.processName .iojsIcon 76 | color: #f7df1e 77 | 78 | li.processLogs, li.processExceptions, li.processSnapshots 79 | border-left-width: 30px 80 | 81 | @media (min-width: 768px) 82 | li.processLogs, li.processExceptions, li.processSnapshots 83 | display: none 84 | 85 | li.active 86 | border-color: highlight 87 | background-color: #050505 88 | 89 | @media (max-width: 768px) 90 | .host-list 91 | position: fixed 92 | overflow-y: auto 93 | top: 50px 94 | left: 0 95 | right: 0 96 | margin: 0 97 | -------------------------------------------------------------------------------- /lib/web/resources/HostProcessSnapshot.js: -------------------------------------------------------------------------------- 1 | var Autowire = require('wantsit').Autowire 2 | var Stream = require('stream') 3 | var path = require('path') 4 | 5 | var HostProcessHeapDump = function () { 6 | this._hostList = Autowire 7 | this._logger = Autowire 8 | } 9 | 10 | HostProcessHeapDump.prototype.retrieveAll = function (request, reply) { 11 | var host = this._hostList.getHostByName(request.params.hostId) 12 | 13 | if (!host) { 14 | return reply('No host found for name ' + request.params.hostId).code(404) 15 | } 16 | 17 | var proc = host.findProcessById(request.params.processId) 18 | 19 | if (!proc) { 20 | return reply('No process found for id ' + request.params.processId).code(404) 21 | } 22 | 23 | reply(proc.snapshots) 24 | } 25 | 26 | HostProcessHeapDump.prototype.retrieve = function (request, reply) { 27 | var host = this._hostList.getHostByName(request.params.hostId) 28 | 29 | if (!host) { 30 | return reply('No host found for name ' + request.params.hostId).code(404) 31 | } 32 | 33 | host.findProcessInfoById(request.params.processId, function (error, managedProcess) { 34 | if (error || !managedProcess) { 35 | return reply('No process found for id ' + request.params.processId).code(404) 36 | } 37 | 38 | var stream = new Stream.Readable() 39 | stream.on('error', this._logger.warn.bind(this._logger)) 40 | 41 | managedProcess.fetchHeapSnapshot( 42 | request.params.snapshotId, 43 | stream.emit.bind(stream, 'readable'), 44 | function (data) { 45 | stream.push(data, 'base64') 46 | }, function () { 47 | managedProcess.disconnect() 48 | stream.push(null) 49 | }, function (error, heapSnapshot, read) { 50 | if (error) { 51 | return reply('No snapshot found for id ' + request.params.snapshotId).code(404) 52 | } 53 | 54 | stream._read = read 55 | 56 | reply(stream) 57 | .type('application/octet-stream') 58 | .header('Content-Disposition', 'attachment; filename="' + path.basename(heapSnapshot.path) + '"') 59 | .bytes(heapSnapshot.size) 60 | }) 61 | }.bind(this)) 62 | } 63 | 64 | module.exports = HostProcessHeapDump 65 | -------------------------------------------------------------------------------- /lib/cli/commander.js: -------------------------------------------------------------------------------- 1 | var commander = require('commander') 2 | 3 | // monkey patch until https://github.com/tj/commander.js/issues/289 is resolved 4 | commander.normalize = function (args) { 5 | var ret = [] 6 | var arg 7 | var lastOpt 8 | var index 9 | var subcommand 10 | 11 | // find subcommand - horrifically naive - just look for the first argument 12 | // not prefixed with a dash 13 | for (var i = 0, len = args.length; i < len; ++i) { 14 | if (args[i].substring(0, 1) !== '-') { 15 | subcommand = this.findCommand(args[i]) 16 | 17 | break 18 | } 19 | } 20 | 21 | for (i = 0, len = args.length; i < len; ++i) { 22 | arg = args[i] 23 | if (i > 0) { 24 | lastOpt = this.optionFor(subcommand, args[i - 1]) 25 | } 26 | 27 | if (arg === '--') { 28 | // Honor option terminator 29 | ret = ret.concat(args.slice(i)) 30 | break 31 | } else if (lastOpt && lastOpt.required) { 32 | ret.push(arg) 33 | } else if (arg.length > 1 && arg[0] === '-' && arg[1] !== '-') { 34 | arg.slice(1).split('').forEach(function (c) { 35 | ret.push('-' + c) 36 | }) 37 | } else if (/^--/.test(arg) && ~(index = arg.indexOf('='))) { 38 | ret.push(arg.slice(0, index), arg.slice(index + 1)) 39 | } else { 40 | ret.push(arg) 41 | } 42 | } 43 | 44 | return ret 45 | } 46 | 47 | commander.findCommand = function (subcommand) { 48 | if (!subcommand) { 49 | return 50 | } 51 | 52 | for (var i = 0; i < this.commands.length; i++) { 53 | if (this.commands[i]._name === subcommand) { 54 | return this.commands[i] 55 | } 56 | } 57 | } 58 | 59 | commander.optionFor = function (subcommand, arg) { 60 | if (!subcommand) { 61 | return null 62 | } 63 | 64 | var options = this.options 65 | 66 | if (arguments.length === 1) { 67 | // invoked as 'bs --foo' 68 | arg = subcommand 69 | } else { 70 | // invoked as 'bs foo --bar' 71 | options = subcommand.options 72 | } 73 | 74 | for (var i = 0, len = options.length; i < len; ++i) { 75 | if (options[i].is(arg)) { 76 | return options[i] 77 | } 78 | } 79 | } 80 | 81 | module.exports = commander 82 | -------------------------------------------------------------------------------- /web/public/javascript/highcharts/themes/skies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Skies theme for Highcharts JS 3 | * @author Torstein Honsi 4 | */ 5 | 6 | Highcharts.theme = { 7 | colors: ["#514F78", "#42A07B", "#9B5E4A", "#72727F", "#1F949A", "#82914E", "#86777F", "#42A07B"], 8 | chart: { 9 | className: 'skies', 10 | borderWidth: 0, 11 | plotShadow: true, 12 | plotBackgroundImage: 'http://www.highcharts.com/demo/gfx/skies.jpg', 13 | plotBackgroundColor: { 14 | linearGradient: [0, 0, 250, 500], 15 | stops: [ 16 | [0, 'rgba(255, 255, 255, 1)'], 17 | [1, 'rgba(255, 255, 255, 0)'] 18 | ] 19 | }, 20 | plotBorderWidth: 1 21 | }, 22 | title: { 23 | style: { 24 | color: '#3E576F', 25 | font: '16px Lucida Grande, Lucida Sans Unicode, Verdana, Arial, Helvetica, sans-serif' 26 | } 27 | }, 28 | subtitle: { 29 | style: { 30 | color: '#6D869F', 31 | font: '12px Lucida Grande, Lucida Sans Unicode, Verdana, Arial, Helvetica, sans-serif' 32 | } 33 | }, 34 | xAxis: { 35 | gridLineWidth: 0, 36 | lineColor: '#C0D0E0', 37 | tickColor: '#C0D0E0', 38 | labels: { 39 | style: { 40 | color: '#666', 41 | fontWeight: 'bold' 42 | } 43 | }, 44 | title: { 45 | style: { 46 | color: '#666', 47 | font: '12px Lucida Grande, Lucida Sans Unicode, Verdana, Arial, Helvetica, sans-serif' 48 | } 49 | } 50 | }, 51 | yAxis: { 52 | alternateGridColor: 'rgba(255, 255, 255, .5)', 53 | lineColor: '#C0D0E0', 54 | tickColor: '#C0D0E0', 55 | tickWidth: 1, 56 | labels: { 57 | style: { 58 | color: '#666', 59 | fontWeight: 'bold' 60 | } 61 | }, 62 | title: { 63 | style: { 64 | color: '#666', 65 | font: '12px Lucida Grande, Lucida Sans Unicode, Verdana, Arial, Helvetica, sans-serif' 66 | } 67 | } 68 | }, 69 | legend: { 70 | itemStyle: { 71 | font: '9pt Trebuchet MS, Verdana, sans-serif', 72 | color: '#3E576F' 73 | }, 74 | itemHoverStyle: { 75 | color: 'black' 76 | }, 77 | itemHiddenStyle: { 78 | color: 'silver' 79 | } 80 | }, 81 | labels: { 82 | style: { 83 | color: '#3E576F' 84 | } 85 | } 86 | }; 87 | 88 | // Apply the theme 89 | var highchartsOptions = Highcharts.setOptions(Highcharts.theme); 90 | --------------------------------------------------------------------------------