├── .gitignore ├── .gitmodules ├── .npmignore ├── Makefile ├── README.md ├── lib ├── client.js ├── error.js └── index.js ├── package.json ├── test ├── client.test.js ├── conn.test.js ├── connectionloss.test.js.dont ├── helper.js ├── test.js └── watch.test.js └── tools ├── jsl.node.conf ├── jsstyle.conf └── mk ├── Makefile.defs ├── Makefile.deps ├── Makefile.node_deps.defs ├── Makefile.node_deps.targ └── Makefile.targ /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /tmp 3 | build 4 | coverage 5 | docs/*.json 6 | docs/*.html 7 | cscope.in.out 8 | cscope.po.out 9 | cscope.out 10 | .coverage_data 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/jsstyle"] 2 | path = deps/jsstyle 3 | url = git://github.com/davepacheco/jsstyle.git 4 | [submodule "deps/javascriptlint"] 5 | path = deps/javascriptlint 6 | url = git://github.com/davepacheco/javascriptlint.git 7 | 8 | [submodule "deps/restdown"] 9 | path = deps/restdown 10 | url = git://github.com/trentm/restdown.git 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitmodules 2 | Makefile 3 | deps 4 | docs 5 | test 6 | tools -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2012, Joyent, Inc. All rights reserved. 3 | # 4 | # Makefile: basic Makefile for template API service 5 | # 6 | # This Makefile is a template for new repos. It contains only repo-specific 7 | # logic and uses included makefiles to supply common targets (javascriptlint, 8 | # jsstyle, restdown, etc.), which are used by other repos as well. You may well 9 | # need to rewrite most of this file, but you shouldn't need to touch the 10 | # included makefiles. 11 | # 12 | # If you find yourself adding support for new targets that could be useful for 13 | # other projects too, you should add these to the original versions of the 14 | # included Makefiles (in eng.git) so that other teams can use them too. 15 | # 16 | 17 | # 18 | # Tools 19 | # 20 | BUNYAN := ./node_modules/.bin/bunyan 21 | NPM := npm 22 | 23 | # 24 | # Files 25 | # 26 | DOC_FILES = index.restdown 27 | JS_FILES := $(shell find lib test -name '*.js') 28 | JSL_CONF_NODE = tools/jsl.node.conf 29 | JSL_FILES_NODE = $(JS_FILES) 30 | JSSTYLE_FILES = $(JS_FILES) 31 | JSSTYLE_FLAGS = -f tools/jsstyle.conf 32 | 33 | include ./tools/mk/Makefile.defs 34 | 35 | # 36 | # Repo-specific targets 37 | # 38 | .PHONY: all modules cover 39 | all: $(MODULES) $(REPO_DEPS) 40 | $(NPM) install 41 | 42 | $(MODULES): 43 | $(NPM) install 44 | 45 | CLEAN_FILES += ./node_modules 46 | 47 | .PHONY: cover 48 | cover: $(MODULES) 49 | @rm -fr ./.coverage_data 50 | $(NPM) test --coverage 51 | $(NPM) run report 52 | 53 | .PHONY: test 54 | test: $(MODULES) 55 | $(NPM) test | $(BUNYAN) 56 | 57 | include ./tools/mk/Makefile.deps 58 | include ./tools/mk/Makefile.targ 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-zkplus 2 | 3 | `zkplus` is the API you wish [ZooKeeper](http://zookeeper.apache.org/) 4 | had for [Node.js](http//nodejs.org). The `zkplus` API resembles 5 | Node's [fs](http://nodejs.org/api/fs.html) module quit a bit, with the 6 | caveat that data is always assumed to be JSON. That seems sensible 7 | and universal for most uses of ZK, and indeed makes this API quite a 8 | bit nicer. If you're doing something crazy like storing images and 9 | videos in ZooKeeper, you're doing it wrong, so move along. 10 | 11 | At a high-level, this API provides facilities for creating 12 | "directories", "files", and setting "watches" (ZK only provides the 13 | latter as an actual primitive; everything else is approximated here). 14 | 15 | # Installation 16 | 17 | ``` bash 18 | npm install zkplus 19 | ``` 20 | 21 | # Usage 22 | 23 | ```js 24 | var assert = require('assert'); 25 | var zkplus = require('zkplus'); 26 | 27 | var client = zkplus.createClient({ 28 | connectTimeout: 4000, 29 | servers: [{ 30 | host: '127.0.0.1', 31 | port: 2181 32 | }] 33 | }); 34 | 35 | client.connect(function (err) { 36 | assert.ifError(err); 37 | client.mkdirp('/foo/bar', function (err) { 38 | assert.ifError(err); 39 | client.rmr('/foo', function (err) { 40 | assert.ifError(err); 41 | client.close(); 42 | }); 43 | }); 44 | }); 45 | ``` 46 | 47 | # API 48 | 49 | ## zkplus.createClient(options) 50 | 51 | Creating a client is straightforward, as you simply invoke the 52 | `createClient` API, which takes an options object with the options 53 | below. Note that the `servers` parameter can be omitted if you only want to talk 54 | to a single ZooKeeper node; in that case, you can just use `host` as a top-level 55 | argument (useful for development). 56 | 57 | var zkplus = require('zkplus'); 58 | 59 | var client = zkplus.createClient({ 60 | host: 'localhost' 61 | }); 62 | 63 | 64 | | Parameter | Type | Description | 65 | | :-------- | :--- | :---------- | 66 | | connectTimeout | Number | number of milliseconds to wait on initial connect (or false for Infinity) | 67 | | log | [Bunyan](https://github.com/trentm/node-bunyan) | pre-created logger | 68 | | servers | Array | Array of objects with host and port | 69 | | retry | Object | `{max: 10, delay: 1000}` - an object with `max` and `delay` for attempts and sleep | 70 | | timeout | Number | Suggested timeout for sessions; negotiated value will be saved as `client.timeout` | 71 | 72 | The returned `client` object will be an instance of a `ZKClient`: 73 | 74 | ## Class: zkplus.ZKClient 75 | 76 | This is an [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter) 77 | with the following events and methods. 78 | 79 | ### Event: 'close' 80 | 81 | function onClose() { } 82 | 83 | Emitted when the client has been disconnected from the ZooKeeper server. 84 | 85 | ### Event: 'connect' 86 | 87 | function onConnect() { } 88 | 89 | Emitted when the client has connected (or reconnected) to the ZooKeeper server. 90 | 91 | ### Event: 'error' 92 | 93 | function onError(err) { } 94 | 95 | If the client driver has an unexpected error, it is sent here. 96 | 97 | ## ZKClient.connect(callback) 98 | 99 | Explicitly connects to the ZooKeeper server(s) passed in at instantiation time. 100 | The only argument to the callback is an optional `Error`. 101 | 102 | ## ZKClient.close([callback]) 103 | 104 | Shuts down the connection to the ZooKeeper server(s). Emits the `close` event 105 | when done. The only argument to the callback is an optional `Error`. 106 | 107 | ### ZKClient.create(path, object, [options], callback) 108 | 109 | Creates a `znode` in ZooKeeper. The `object` parameter must be a JSON object 110 | that will be saved as the raw data, and `options` may include `flags`. Flags 111 | allow you to specify the useful semantics that Zookeeper offers, namely 112 | sequences and ephemeral nodes (`sequence` and `ephemeral`, respectively). 113 | 114 | Additionally, zkplus has additional functionality that enables you to create 115 | an ephemeral node that is automatically recreated across connection drops. 116 | Recall that Zookeeper ephemeral nodes are deleted when a session closes (as 117 | per documentation); emperically the way connections/sessions work, it pretty 118 | much means you need to assume that the ephemeral node is dropped on connection 119 | loss. Thus, zkplus has an ability to automatically recreate any ephemeral 120 | nodes on reconnect. A flag of `ephemeral_plus` will enable this behavior. 121 | 122 | `callback` is of the form `function (err, path)`, where `path` is the newly 123 | created node (which you would need on using a `sequence`). 124 | 125 | var data = { 126 | foo: 'bar' 127 | }; 128 | var opts = { 129 | flags: ['sequence', 'ephemeral_plus'] 130 | }; 131 | client.create('/foo/bar', opts, function (err, path) { 132 | assert.ifError(err); 133 | console.log(path); // => /foo/bar/00000000 134 | }); 135 | 136 | ### ZKClient.get(path, callback) 137 | 138 | Returns the data associated with a znode, as a JS Object (remember, zkplus 139 | assumes all data is JSON). Callback is of the form `function (err, object)` 140 | 141 | client.get('/foo/bar/00000000', function (err, obj) { 142 | assert.ifError(err); 143 | console.log('%j', obj); // => { "hostname": "your_host_here" } 144 | }); 145 | 146 | ### ZKClient.getState() 147 | 148 | Returns the state of the underlying ZooKeeper driver. Possible states are: 149 | 150 | * connected 151 | * disconnected 152 | * expired 153 | * unknown 154 | 155 | ### ZKClient.mkdirp(path, callback) 156 | 157 | Does what you think it does. Recursively creates all znodes specified if they 158 | don't exist. Note this API is idempotent, as it will not error if the path 159 | already exists. Callback is of the form `function (err)`. 160 | 161 | client.mkdirp('/foo/bar/baz', function (err) { 162 | assert.ifError(err); 163 | }); 164 | 165 | ### ZKClient.put(path, object, [options], callback) 166 | 167 | Overwrites `path` with `object`. Callback is of the form `function (err)`. 168 | 169 | client.put('/foo/bar/hello', {value: 'world'}, function (err) { 170 | assert.ifError(err); 171 | }); 172 | 173 | ### ZKClient.readdir(path, callback) 174 | 175 | Lists all nodes under a given path, and returns you the keys as relative paths 176 | only. The keys returned will be sorted in ascending order. callback is of the 177 | form `function (err, nodes)`. 178 | 179 | client.readdir('/foo/bar', function (err, nodes) { 180 | assert.ifError(err); 181 | console.log(nodes.join()); // => ['00000000', 'baz'] 182 | }); 183 | 184 | ### ZKClient.rmr(path, callback) 185 | 186 | Recursively deletes everything under a given path. I.e., what you'd think 187 | `rm -r` would be. callback is of the form `function (err)`. 188 | 189 | client.rmr('/foo/bar', function (err) { 190 | assert.ifError(err); 191 | }); 192 | 193 | ### ZKClient.stat(path, callback) 194 | 195 | Returns a ZK stat object for a given path. ZK stats look like: 196 | 197 | { 198 | czxid, // created zxid (long) 199 | mzxid, // last modified zxid (long) 200 | ctime, // created (Date) 201 | mtime, // last modified (Date) 202 | version, // version (int) 203 | cversion, // child version (int) 204 | aversion, // acl version (int) 205 | ephemeralOwner, // owner session id if ephemeral, 0 otw (string) 206 | dataLength, //length of the data in the node (int) 207 | numChildren, //number of children of this node (int) 208 | pzxid // last modified children (long) 209 | } 210 | 211 | Reference the ZooKeeper documentation for more info. 212 | 213 | client.stat('/foo', function (err, stats) { 214 | assert.ifError(err); 215 | console.log('%j', stats); // => stuff like above :) 216 | }); 217 | 218 | ### ZKClient.unlink(path, [options], callback) 219 | 220 | Removes a znode from ZooKeeper. 221 | 222 | client.unlink('/foo', function (err) { 223 | assert.ifError(err); 224 | }); 225 | 226 | client.unlink('/foo', {version: 0}, function (err) { ... }); 227 | 228 | ### ZKlient.watch(path, [options], callback) 229 | 230 | The `watch` API makes usable the atrociousness that are ZooKeeper notifications 231 | (although, as unusable as they are, they're one of its most useful features). 232 | Using this API, you are able to set watches any time the content of a single 233 | node changes, or any time children are changed underneath that node. Unlink the 234 | raw ZooKeeper API, this will also automatically "rewatch" for you, such that 235 | future changes are still fired through the same listener. 236 | 237 | The defaults for this API are to listen only for data changes, and not to return 238 | you the initial data (i.e., assume you already know what you've got, and just 239 | want to get notifications about it). The `options` parameter drives the other 240 | behavior, and specifically allows you to set two flags currently: `method` and 241 | `initialData`. `method` defaults to `data`, and the semantics are such that 242 | only content changes to the znode you've passed in via `path` will be listened 243 | for. If you set `method` to `list`, then the semantics of the watch are to 244 | notify you when any children change (add/del) _under_ `path`. You cannot listen 245 | for both simultaneously; if you want both, you'll need to set two watches. On 246 | updates, the returned stream will fire `data` events. 247 | 248 | Additionally, the semantics are not to perform a `get`, but to only notify you 249 | on updates. Setting `initialData` to `true` will make the watch fire once 250 | "up front". 251 | 252 | `callback` is of the form `function (err, listener)`. 253 | 254 | client.watch('/foo', function (err, listener) { 255 | assert.ifError(err); 256 | listener.on('error', function (err) { 257 | console.error(err.stack); 258 | process.exit(1); 259 | }); 260 | 261 | listener.on('data', function (obj) { 262 | console.log('%j', obj); // => updated record 263 | }); 264 | 265 | listener.on('end', function () { 266 | // `end` has been called, and watch will no loner fire 267 | }); 268 | }); 269 | 270 | client.watch('/foo', { method: 'list' }, function (err, listener) { 271 | assert.ifError(err); 272 | listener.on('error', function (err) { 273 | console.error(err.stack); 274 | process.exit(1); 275 | }); 276 | 277 | listener.on(data, function (children) { 278 | console.log('%j', children); // => ['00000000', 'bar', ...] 279 | }); 280 | }); 281 | 282 | 283 | # Tests 284 | 285 | To launch tests you'll need a running zookeeper instance. 286 | 287 | ```bash 288 | export ZK_HOST=$your_zk_ip_here 289 | cd node-zkplus 290 | make prepush 291 | ``` 292 | 293 | ## License 294 | 295 | The MIT License (MIT) 296 | Copyright (c) 2012 Mark Cavage 297 | 298 | Permission is hereby granted, free of charge, to any person obtaining a copy of 299 | this software and associated documentation files (the "Software"), to deal in 300 | the Software without restriction, including without limitation the rights to 301 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 302 | the Software, and to permit persons to whom the Software is furnished to do so, 303 | subject to the following conditions: 304 | 305 | The above copyright notice and this permission notice shall be included in all 306 | copies or substantial portions of the Software. 307 | 308 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 309 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 310 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 311 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 312 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 313 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 314 | SOFTWARE. 315 | 316 | ## Bugs 317 | 318 | See . 319 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, Mark Cavage. All rights reserved. 2 | 3 | var EventEmitter = require('events').EventEmitter; 4 | var path = require('path'); 5 | var stream = require('stream'); 6 | var util = require('util'); 7 | 8 | var assert = require('assert-plus'); 9 | var vasync = require('vasync'); 10 | var once = require('once'); 11 | var uuid = require('node-uuid'); 12 | var zookeeper = require('node-zookeeper-client'); 13 | 14 | var errors = require('./error'); 15 | 16 | 17 | 18 | ///--- Globals 19 | 20 | var sprintf = util.format; 21 | 22 | var PROXY_EVENTS = { 23 | 'connected': 'connect', 24 | 'connectedReadOnly': 'connectedReadOnly', 25 | 'disconnected': 'close', 26 | 'expired': 'session_expired', 27 | 'authenticationFailed': 'authenticationFailed' 28 | }; 29 | 30 | 31 | 32 | ///--- Helpers 33 | 34 | function bufToLong(b) { 35 | var hi = b.readUInt32BE(0) >>> 0; 36 | hi = hi * 4294967296; 37 | var lo = b.readUInt32BE(4) >>> 0; 38 | 39 | return (hi + lo); 40 | } 41 | 42 | 43 | 44 | ///--- API 45 | 46 | function ZKClient(opts) { 47 | assert.object(opts, 'options'); 48 | assert.number(opts.connectTimeout, 'options.connectTimeout'); 49 | assert.object(opts.log, 'options.log'); 50 | assert.object(opts.retry, 'options.retry'); 51 | assert.number(opts.retry.delay, 'options.retry.delay'); 52 | assert.number(opts.retry.max, 'options.retry.max'); 53 | assert.arrayOfObject(opts.servers, 'options.servers'); 54 | assert.optionalNumber(opts.timeout, 'options.timeout'); 55 | 56 | EventEmitter.call(this); 57 | 58 | var self = this; 59 | 60 | this.connected = false; 61 | this.connectTimeout = opts.connectTimeout; 62 | this.log = opts.log.child({component: 'ZKPlus'}, true); 63 | this.ephemerals = {}; 64 | this.port = opts.port; 65 | this.servers = opts.servers.slice(0); 66 | this.watchers = []; 67 | 68 | this._connectString = this.servers.map(function (s) { 69 | assert.string(s.host, 'host'); 70 | assert.number(s.port, 'port'); 71 | return (sprintf('%s:%d', s.host, s.port)); 72 | }).join(','); 73 | 74 | this.zk = zookeeper.createClient(this._connectString, { 75 | sessionTimeout: opts.timeout, 76 | spinDelay: opts.retry.delay, 77 | retries: opts.retry.max 78 | }); 79 | 80 | Object.keys(PROXY_EVENTS).forEach(function (k) { 81 | var ev = PROXY_EVENTS[k]; 82 | var proxy = self.emit.bind(self, ev); 83 | self.zk.on(k, function proxyEvent() { 84 | self.log.trace('event: %s', ev); 85 | setImmediate(function () { 86 | proxy.apply(self, arguments); 87 | }); 88 | }); 89 | }); 90 | 91 | this.__defineGetter__('timeout', function () { 92 | return (self.zk.getSessionTimeout()); 93 | }); 94 | 95 | this.zk.on('connected', function () { 96 | self.connected = true; 97 | 98 | // rewatch 99 | self.watchers.forEach(function (w) { 100 | var l = self.log.child({ 101 | method: w.op, 102 | path: w.path 103 | }, true); 104 | l.trace('rewatch: entered'); 105 | self.zk[w.op](w.path, w.cb, function (err, data) { 106 | if (err) { 107 | l.trace(err, 'rewatch: failed'); 108 | w.stream.end(); 109 | } else { 110 | l.trace('rewatch: writing %j', data); 111 | w.stream.write(data); 112 | } 113 | }); 114 | }); 115 | 116 | // re-ephemeral 117 | Object.keys(self.ephemerals).forEach(function (k) { 118 | var e = self.ephemerals[k]; 119 | var l = self.log.child({ 120 | path: k, 121 | flags: e.flags 122 | }, true); 123 | l.trace('reephemeral: entered'); 124 | self.zk.create(k, e.data, e.flags, function (err, p) { 125 | if (err) { 126 | l.trace(err, 'reephemeral: failed'); 127 | self.emit('ephermeral_error', err); 128 | } else { 129 | l.trace('reephemeral: recreated %s', p); 130 | } 131 | }); 132 | }); 133 | }); 134 | 135 | this.zk.on('disconnected', function () { 136 | self.connected = false; 137 | }); 138 | 139 | this.zk.on('error', this.emit.bind(this, 'error')); 140 | } 141 | util.inherits(ZKClient, EventEmitter); 142 | 143 | 144 | ZKClient.prototype._connected = function _connected(cb) { 145 | assert.func(cb, 'callback'); 146 | 147 | if (!this.connected) { 148 | setImmediate(function notConnected() { 149 | cb(new errors.ZKError(zookeeper.Exception.CONNECTION_LOSS, 150 | 'not connected to Zookeeper')); 151 | }); 152 | } 153 | 154 | return (this.connected); 155 | }; 156 | 157 | 158 | ZKClient.prototype.connect = function connect(cb) { 159 | assert.optionalFunc(cb, 'callback'); 160 | 161 | cb = once(cb); 162 | 163 | var log = this.log; 164 | var self = this; 165 | var zk = this.zk; 166 | 167 | log.trace('connect: entered'); 168 | 169 | var _cb = once(function connectCallback(err) { 170 | if (err) { 171 | log.trace(err, 'connect: error'); 172 | zk.removeListener('connect', _cb); 173 | zk.once('connect', zk.close.bind(zk)); 174 | cb(err); 175 | } else { 176 | log.trace('connect: connected'); 177 | zk.removeListener('error', _cb); 178 | cb(); 179 | } 180 | }); 181 | 182 | 183 | zk.once('error', _cb); 184 | zk.once('connected', _cb); 185 | 186 | if (this.connectTimeout > 0) { 187 | setTimeout(function onTimeout() { 188 | _cb(new errors.ZKConnectTimeoutError(self._connectString)); 189 | }, this.connectTimeout); 190 | } 191 | zk.connect(); 192 | }; 193 | 194 | 195 | ZKClient.prototype.close = function close(cb) { 196 | assert.optionalFunc(cb, 'callback'); 197 | 198 | cb = once(cb); 199 | 200 | var log = this.log; 201 | var zk = this.zk; 202 | 203 | log.trace('close: entered'); 204 | 205 | var _cb = once(function (err) { 206 | if (err) { 207 | log.trace(err, 'close: error'); 208 | zk.removeListener('disconnected', _cb); 209 | if (cb) 210 | cb(err); 211 | } else { 212 | log.trace('close: done'); 213 | zk.removeListener('error', _cb); 214 | if (cb) 215 | cb(); 216 | } 217 | }); 218 | 219 | zk.once('disconnected', _cb); 220 | zk.once('error', _cb); 221 | 222 | zk.close(); 223 | }; 224 | 225 | 226 | ZKClient.prototype.create = function creat(p, obj, opts, cb) { 227 | assert.string(p, 'path'); 228 | assert.object(obj, 'object'); 229 | if (typeof (opts) === 'function') { 230 | cb = opts; 231 | opts = {}; 232 | } 233 | assert.object(opts, 'options'); 234 | assert.arrayOfString(opts.flags || [], 'options.flags'); 235 | assert.func(cb, 'callback'); 236 | 237 | p = path.normalize(p); 238 | cb = once(cb); 239 | 240 | if (!this._connected(cb)) 241 | return; 242 | 243 | var data = new Buffer(JSON.stringify(obj), 'utf8'); 244 | var f; 245 | var flags = opts.flags || []; 246 | var log = this.log.child({ 247 | flags: flags, 248 | path: p 249 | }, true); 250 | var self = this; 251 | var zk = this.zk; 252 | 253 | log.trace('create: entered'); 254 | 255 | if ((flags.indexOf('ephemeral') !== -1 || 256 | flags.indexOf('ephemeral_plus') !== -1) && 257 | flags.indexOf('sequence') !== -1) { 258 | f = zookeeper.CreateMode.EPHEMERAL_SEQUENTIAL; 259 | } else if (flags.indexOf('ephemeral') !== -1 || 260 | flags.indexOf('ephemeral_plus') !== -1) { 261 | f = zookeeper.CreateMode.EPHEMERAL; 262 | } else if (flags.indexOf('sequence') !== -1) { 263 | f = zookeeper.CreateMode.PERSISTENT_SEQUENTIAL; 264 | } else { 265 | f = zookeeper.CreateMode.PERSISTENT; 266 | } 267 | 268 | zk.create(p, data, f, function (err, _path) { 269 | if (err) { 270 | log.trace(err, 'create: error'); 271 | cb(err); 272 | } else { 273 | if (flags.indexOf('ephemeral_plus') !== -1) { 274 | self.ephemerals[p] = { 275 | data: data, 276 | flags: f, 277 | path: p 278 | }; 279 | } 280 | 281 | log.trace({path: _path}, 'create: complete'); 282 | cb(null, _path); 283 | } 284 | }); 285 | }; 286 | ZKClient.prototype.creat = ZKClient.prototype.create; 287 | 288 | 289 | ZKClient.prototype.get = function get(p, cb) { 290 | assert.string(p, 'path'); 291 | assert.func(cb, 'callback'); 292 | 293 | p = path.normalize(p); 294 | cb = once(cb); 295 | 296 | if (!this._connected(cb)) 297 | return; 298 | 299 | var log = this.log.child({path: p}, true); 300 | var zk = this.zk; 301 | 302 | log.trace('get: entered'); 303 | zk.getData(p, function (err, data) { 304 | if (err) { 305 | log.trace(err, 'get: failed'); 306 | cb(err); 307 | } else { 308 | var obj; 309 | try { 310 | obj = JSON.parse(data.toString('utf8')); 311 | } catch (e) { 312 | log.trace({ 313 | err: e, 314 | data: data 315 | }, 'get: failed (parsing data)'); 316 | cb(e); 317 | return; 318 | } 319 | 320 | log.trace({data: obj}, 'get: done'); 321 | cb(null, obj); 322 | } 323 | }); 324 | }; 325 | 326 | 327 | ZKClient.prototype.getState = function getState() { 328 | var state; 329 | switch (this.zk.getState()) { 330 | case zookeeper.State.SYNC_CONNECTED: 331 | state = 'connected'; 332 | break; 333 | case zookeeper.State.DISCONNECTED: 334 | state = 'disconnected'; 335 | break; 336 | case zookeeper.State.EXPIRED: 337 | state = 'expired'; 338 | break; 339 | default: 340 | state = 'unknown'; 341 | break; 342 | } 343 | 344 | return (state); 345 | }; 346 | 347 | 348 | ZKClient.prototype.mkdirp = function mkdirp(p, cb) { 349 | assert.string(p, 'path'); 350 | assert.func(cb, 'callback'); 351 | 352 | p = path.normalize(p); 353 | cb = once(cb); 354 | 355 | if (!this._connected(cb)) 356 | return; 357 | 358 | var log = this.log.child({path: p}, true); 359 | var zk = this.zk; 360 | 361 | log.trace('mkdirp: entered'); 362 | zk.mkdirp(p, function (err, _path) { 363 | if (err) { 364 | log.trace(err, 'mkdirp: error'); 365 | cb(err); 366 | } else { 367 | log.trace('mkdirp: done'); 368 | cb(); 369 | } 370 | }); 371 | }; 372 | 373 | 374 | ZKClient.prototype.put = function put(p, obj, opts, cb) { 375 | assert.string(p, 'path'); 376 | assert.object(obj, 'object'); 377 | if (typeof (opts) === 'function') { 378 | cb = opts; 379 | opts = {}; 380 | } 381 | assert.object(opts, 'options'); 382 | assert.func(cb, 'callback'); 383 | 384 | p = path.normalize(p); 385 | cb = once(cb); 386 | 387 | if (!this._connected(cb)) 388 | return; 389 | 390 | var data = new Buffer(JSON.stringify(obj), 'utf8'); 391 | var log = this.log.child({path: p}, true); 392 | var ver = opts.version !== undefined ? opts.version : -1; 393 | var zk = this.zk; 394 | 395 | log.trace({ 396 | object: obj, 397 | options: opts 398 | }, 'put: entered'); 399 | zk.setData(p, data, ver, function (err) { 400 | if (err) { 401 | log.trace(err, 'put: failed'); 402 | cb(err); 403 | } else { 404 | log.trace('put: done'); 405 | cb(); 406 | } 407 | }); 408 | }; 409 | 410 | 411 | ZKClient.prototype.readdir = function readdir(p, cb) { 412 | assert.string(p, 'path'); 413 | assert.func(cb, 'callback'); 414 | 415 | p = path.normalize(p); 416 | cb = once(cb); 417 | 418 | if (!this._connected(cb)) 419 | return; 420 | 421 | var log = this.log.child({path: p}, true); 422 | var zk = this.zk; 423 | 424 | log.trace('readdir: entered'); 425 | zk.getChildren(p, function (err, children) { 426 | if (err) { 427 | log.trace(err, 'readdir: error'); 428 | cb(err); 429 | } else { 430 | log.trace({children: children}, 'readdir: done'); 431 | cb(null, children); 432 | } 433 | }); 434 | }; 435 | 436 | 437 | ZKClient.prototype.rmr = function rmr(p, cb) { 438 | assert.string(p, 'path'); 439 | assert.func(cb, 'callback'); 440 | 441 | p = path.normalize(p); 442 | cb = once(cb); 443 | 444 | if (!this._connected(cb)) 445 | return; 446 | 447 | var inflight = 0; 448 | var log = this.log.child({path: p}, true); 449 | var nodes = []; 450 | var self = this; 451 | var zk = this.zk; 452 | 453 | function list(_p) { 454 | nodes.push(_p); 455 | inflight++; 456 | zk.getChildren(_p, function (err, children) { 457 | if (err) { 458 | cb(err); 459 | } else { 460 | children.forEach(function (n) { 461 | list(path.join(_p, n)); 462 | }); 463 | 464 | setImmediate(function () { 465 | if (--inflight === 0) 466 | remove(); 467 | }); 468 | } 469 | }); 470 | } 471 | 472 | function remove() { 473 | log.trace({ 474 | nodes: nodes 475 | }, 'rmr: all children listed; deleting'); 476 | 477 | vasync.forEachPipeline({ 478 | func: function (_p, _cb) { 479 | log.trace('rmr: removing "%s"', _p); 480 | if (self.ephemerals[_p]) 481 | delete self.ephemerals[_p]; 482 | zk.remove(_p, _cb); 483 | }, 484 | inputs: nodes.sort().reverse() 485 | }, function (err) { 486 | if (err) { 487 | log.trace(err, 'rmr: failed'); 488 | cb(err); 489 | } else { 490 | log.trace('rmr: done'); 491 | cb(); 492 | } 493 | }); 494 | } 495 | 496 | log.trace('rmr: entered'); 497 | list(p); 498 | }; 499 | 500 | 501 | ZKClient.prototype.stat = function stat(p, cb) { 502 | assert.string(p, 'path'); 503 | assert.func(cb, 'callback'); 504 | 505 | p = path.normalize(p); 506 | cb = once(cb); 507 | 508 | if (!this._connected(cb)) 509 | return; 510 | 511 | var log = this.log.child({path: p}, true); 512 | var zk = this.zk; 513 | 514 | log.trace('stat: entered'); 515 | zk.exists(p, false, function (err, _stat) { 516 | if (err) { 517 | log.trace(err, 'stat: failed'); 518 | cb(err); 519 | } else if (!_stat) { 520 | log.trace({stat: {}}, 'stat: done'); 521 | cb(null, {}); 522 | } else { 523 | if (_stat.specification) 524 | delete _stat.specification; 525 | _stat.ephemeralOwner = bufToLong(_stat.ephemeralOwner); 526 | _stat.czxid = bufToLong(_stat.mzxid); 527 | _stat.mzxid = bufToLong(_stat.mzxid); 528 | _stat.pzxid = bufToLong(_stat.pzxid); 529 | _stat.ctime = new Date(bufToLong(_stat.ctime)); 530 | _stat.mtime = new Date(bufToLong(_stat.mtime)); 531 | log.trace({stat: _stat}, 'stat: done'); 532 | cb(null, _stat); 533 | } 534 | }); 535 | }; 536 | 537 | 538 | ZKClient.prototype.toString = function toString() { 539 | var str = '[object ZKClient <'; 540 | str += sprintf('timeout=%d, servers=[%s]', 541 | this.timeout, 542 | this.servers.map(function (s) { 543 | return (sprintf('%s:%d', s.host, s.port)); 544 | }).join(', ')); 545 | str += '>]'; 546 | return (str); 547 | }; 548 | 549 | 550 | ZKClient.prototype.unlink = function unlink(p, opts, cb) { 551 | assert.string(p, 'path'); 552 | if (typeof (opts) === 'function') { 553 | cb = opts; 554 | opts = {}; 555 | } 556 | assert.object(opts, 'options'); 557 | assert.func(cb, 'callback'); 558 | 559 | p = path.normalize(p); 560 | cb = once(cb); 561 | 562 | if (!this._connected(cb)) 563 | return; 564 | 565 | var log = this.log.child({path: p}, true); 566 | var ver = opts.version !== undefined ? opts.version : -1; 567 | var zk = this.zk; 568 | 569 | log.trace('unlink: entered'); 570 | zk.remove(p, ver, function (err) { 571 | if (err) { 572 | log.trace(err, 'unlink: failed'); 573 | cb(err); 574 | } else { 575 | log.trace('unlink: done'); 576 | cb(); 577 | } 578 | }); 579 | }; 580 | 581 | 582 | ZKClient.prototype.watch = function zk_watch(p, opts, cb) { 583 | assert.string(p, 'path'); 584 | if (typeof (opts) === 'function') { 585 | cb = opts; 586 | opts = {}; 587 | } 588 | assert.object(opts, 'options'); 589 | assert.func(cb, 'callback'); 590 | 591 | p = path.normalize(p); 592 | cb = once(cb); 593 | 594 | if (!this._connected(cb)) 595 | return; 596 | 597 | var id = uuid.v4(); 598 | var log = this.log.child({path: p}, true); 599 | var op = opts.method === 'list' ? 'getChildren' : 'getData'; 600 | var self = this; 601 | var w = new stream.PassThrough({ 602 | objectMode: true 603 | }); 604 | var zk = this.zk; 605 | 606 | log.trace('watch: entered'); 607 | 608 | function _watch(event) { 609 | if (!w.readable) { 610 | self.watchers = self.watchers.filter(function (_w) { 611 | return (_w.id !== id); 612 | }); 613 | } 614 | 615 | if (!self.connected) 616 | return; 617 | 618 | zk[op](p, function (err, data) { 619 | if (err) { 620 | log.trace(err, 'watch: error relisting'); 621 | return; 622 | } 623 | 624 | var obj; 625 | try { 626 | if (op !== 'getChildren') 627 | obj = JSON.parse(data.toString()); 628 | } catch (e) { 629 | log.trace(e, 'watch: bad data'); 630 | return; 631 | } 632 | 633 | w.write(obj || data); 634 | }); 635 | } 636 | 637 | function _stat(arg, _cb) { 638 | self.stat(p, function (err, stats) { 639 | if (!err) 640 | arg.stats = stats; 641 | 642 | _cb(err); 643 | }); 644 | } 645 | 646 | function _get(arg, _cb) { 647 | zk[op](p, _watch, function (err, data) { 648 | if (err) { 649 | _cb(err); 650 | } else { 651 | if (op === 'getData') { 652 | try { 653 | if (data) 654 | data = JSON.parse(data); 655 | } catch (e) { 656 | log.trace(e, 'watch: bad data'); 657 | cb(e); 658 | return; 659 | } 660 | } 661 | arg.data = data; 662 | _cb(); 663 | } 664 | }); 665 | } 666 | 667 | var cookie = {}; 668 | vasync.pipeline({ 669 | funcs: [ 670 | _stat, 671 | _get 672 | ], 673 | arg: cookie 674 | }, function (err) { 675 | if (err) { 676 | log.trace(err, 'watch: failed (stat)'); 677 | w.end(); 678 | cb(err); 679 | } else { 680 | log.trace('watch: done'); 681 | if (opts.initialData) 682 | setImmediate(w.write.bind(w, cookie.data)); 683 | 684 | self.watchers.push({ 685 | cb: _watch, 686 | id: id, 687 | path: p, 688 | stream: w, 689 | op: op 690 | }); 691 | 692 | w.once('end', function () { 693 | self.watchers = self.watchers.filter(function (_w) { 694 | return (_w.id !== id); 695 | }); 696 | }); 697 | cb(null, w); 698 | } 699 | }); 700 | }; 701 | 702 | 703 | 704 | ///-- Exports 705 | 706 | module.exports = { 707 | ZKClient: ZKClient 708 | }; 709 | -------------------------------------------------------------------------------- /lib/error.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, Mark Cavage. All rights reserved. 2 | 3 | var util = require('util'); 4 | 5 | 6 | 7 | ///--- API 8 | 9 | function ZKError(code, message, constructorOpt) { 10 | if (Error.captureStackTrace) 11 | Error.captureStackTrace(this, constructorOpt || ZKError); 12 | 13 | this.code = code; 14 | this.message = message || ''; 15 | this.name = this.constructor.name; 16 | } 17 | util.inherits(ZKError, Error); 18 | 19 | 20 | function ZKConnectTimeoutError(host) { 21 | ZKError.call(this, 22 | 'ECONNREFUSED', 23 | util.format('connection timeout to "%s"', host || 'unknown'), 24 | ZKConnectTimeoutError); 25 | } 26 | util.inherits(ZKConnectTimeoutError, ZKError); 27 | 28 | 29 | 30 | ///--- Exports 31 | 32 | module.exports = { 33 | ZKError: ZKError, 34 | ZKConnectTimeoutError: ZKConnectTimeoutError 35 | }; 36 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, Mark Cavage. All rights reserved. 2 | 3 | var assert = require('assert-plus'); 4 | var bunyan = require('bunyan'); 5 | var zookeeper = require('node-zookeeper-client'); 6 | 7 | var ZKClient = require('./client').ZKClient; 8 | var errors = require('./error'); 9 | 10 | 11 | 12 | ///--- Helpers 13 | 14 | function createLogger() { 15 | var log = bunyan.createLogger({ 16 | name: 'zookeeper', 17 | serializers: {err: bunyan.stdSerializers.err}, 18 | stream: process.stderr, 19 | level: (process.env.LOG_LEVEL || 'info') 20 | }); 21 | 22 | return (log); 23 | } 24 | 25 | 26 | 27 | ///--- API 28 | 29 | function createClient(options) { 30 | assert.optionalObject(options, 'options'); 31 | 32 | options = options || {}; 33 | var opts = { 34 | clientId: options.clientId, 35 | clientPassword: options.clientPassword, 36 | servers: [], 37 | log: options.log || createLogger(), 38 | retry: options.retry || { 39 | delay: 1000, 40 | max: Number.MAX_VALUE 41 | }, 42 | timeout: options.timeout || 30000 43 | }; 44 | 45 | if (options.connectTimeout === undefined) { 46 | opts.connectTimeout = 4000; 47 | } else if (options.connectTimeout !== false) { 48 | opts.connectTimeout = options.connectTimeout; 49 | } else { 50 | opts.connectTimeout = 0; 51 | } 52 | 53 | if (options.servers) { 54 | options.servers.forEach(function (server) { 55 | if (!server.host) { 56 | // handle cases where we receive a 57 | // connection string: 58 | // "localhost:2181" 59 | server = server.split(':'); 60 | var port = 2181; 61 | if (server[1]) 62 | port = parseInt(server[1], 10); 63 | opts.servers.push({ 64 | host: server[0], 65 | port: port 66 | }); 67 | } else { 68 | opts.servers.push(server); 69 | } 70 | }); 71 | } else if (options.host) { 72 | opts.servers.push({ 73 | host: options.host, 74 | port: options.port || 2181 75 | }); 76 | } else { 77 | opts.servers.push({ 78 | host: '127.0.0.1', 79 | port: 2181 80 | }); 81 | } 82 | 83 | return (new ZKClient(opts)); 84 | } 85 | 86 | 87 | 88 | ///--- Exports 89 | 90 | module.exports = { 91 | Client: ZKClient, 92 | createClient: createClient 93 | }; 94 | 95 | Object.keys(errors).forEach(function (k) { 96 | module.exports[k] = errors[k]; 97 | }); 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zkplus", 3 | "description": "The ZooKeeper API you always wanted", 4 | "version": "0.4.0", 5 | "homepage": "http://mcavage.github.com/node-zkplus", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/mcavage/node-zkplus.git" 9 | }, 10 | "author": "Mark Cavage ", 11 | "contributors": [ 12 | "Anthony Barre", 13 | "Trent Mick", 14 | "Vincent Voyer", 15 | "Yunong Xiao" 16 | ], 17 | "main": "./lib/index.js", 18 | "engines": { 19 | "node": ">=0.10" 20 | }, 21 | "dependencies": { 22 | "assert-plus": "0.1.5", 23 | "bunyan": "1.8.12", 24 | "node-uuid": "1.4.1", 25 | "once": "1.3.0", 26 | "vasync": "1.5.0", 27 | "node-zookeeper-client": "0.2.1" 28 | }, 29 | "devDependencies": { 30 | "faucet": "0.0.1", 31 | "istanbul": "0.2.11", 32 | "tape": "2.13.3" 33 | }, 34 | "scripts": { 35 | "report": "./node_modules/.bin/istanbul report --html && open ./coverage/lcov-report/index.html", 36 | "test": "./node_modules/.bin/istanbul test test/test.js | ./node_modules/.bin/faucet" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/client.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Mark Cavage All rights reserved. 2 | 3 | var bunyan = require('bunyan'); 4 | var test = require('tape'); 5 | var uuid = require('node-uuid'); 6 | 7 | var helper = require('./helper'); 8 | var zkplus = require('../lib'); 9 | 10 | 11 | 12 | ///--- Globals 13 | 14 | var CLIENT; 15 | 16 | 17 | 18 | ///--- Tests 19 | 20 | test('constructor tests', function (t) { 21 | t.ok(zkplus.createClient()); 22 | t.ok(zkplus.createClient({ 23 | host: '::1', 24 | port: 2181 25 | })); 26 | t.ok(zkplus.createClient({ 27 | servers: ['127.0.0.1:2181'] 28 | })); 29 | 30 | t.end(); 31 | }); 32 | 33 | 34 | test('setup', function (t) { 35 | CLIENT = zkplus.createClient({ 36 | connectTimeout: false, 37 | log: helper.log, 38 | servers: [helper.zkServer], 39 | timeout: 1000 40 | }); 41 | 42 | t.ok(CLIENT); 43 | 44 | CLIENT.connect(function (err) { 45 | t.ifError(err); 46 | t.end(); 47 | }); 48 | }); 49 | 50 | 51 | test('connect timeout', function (t) { 52 | var client = zkplus.createClient({ 53 | connectTimeout: 250, 54 | log: helper.log, 55 | servers: [ 56 | { 57 | host: '169.254.0.1', 58 | port: 2181 59 | } 60 | ], 61 | timeout: 1000 62 | }); 63 | t.ok(client); 64 | client.connect(function (err) { 65 | t.ok(err); 66 | client.close(); 67 | t.end(); 68 | }); 69 | }); 70 | 71 | 72 | test('mkdirp', function (t) { 73 | CLIENT.mkdirp(helper.subdir, function (err) { 74 | t.ifError(err); 75 | t.end(); 76 | }); 77 | }); 78 | 79 | 80 | test('creat no options', function (t) { 81 | var data = { 82 | foo: 'bar' 83 | }; 84 | CLIENT.create(helper.file, data, function (err, path) { 85 | t.ifError(err); 86 | t.ok(path); 87 | t.equal(helper.file, path); 88 | CLIENT.get(path, function (err2, obj) { 89 | t.ifError(err2); 90 | t.ok(obj); 91 | t.deepEqual(data, obj); 92 | t.end(); 93 | }); 94 | }); 95 | }); 96 | 97 | 98 | test('create: ephemeral', function (t) { 99 | var opts = { 100 | flags: ['ephemeral'] 101 | }; 102 | var p = helper.dir + '/' + uuid.v4(); 103 | CLIENT.create(p, {}, opts, function (err, path) { 104 | t.ifError(err); 105 | t.ok(path); 106 | t.equal(p, path); 107 | t.end(); 108 | }); 109 | }); 110 | 111 | 112 | test('create: sequential', function (t) { 113 | var opts = { 114 | flags: ['sequence'] 115 | }; 116 | var p = helper.dir + '/' + uuid.v4(); 117 | CLIENT.create(p, {}, opts, function (err, path) { 118 | t.ifError(err); 119 | t.ok(path); 120 | t.notEqual(p, path); 121 | t.end(); 122 | }); 123 | }); 124 | 125 | 126 | test('create: ephemeral_sequential', function (t) { 127 | var opts = { 128 | flags: ['ephemeral', 'sequence'] 129 | }; 130 | CLIENT.create(helper.subdir, {}, opts, function (err, p) { 131 | t.ifError(err); 132 | t.ok(p); 133 | CLIENT.stat(p, function (err2, stat) { 134 | t.ifError(err2); 135 | t.ok(stat); 136 | t.ok((stat || {}).ephemeralOwner); 137 | t.end(); 138 | }); 139 | }); 140 | }); 141 | 142 | 143 | test('put not exists', function (t) { 144 | var obj = { 145 | hello: 'world' 146 | }; 147 | CLIENT.put(helper.file, obj, function (err) { 148 | t.ifError(err); 149 | CLIENT.get(helper.file, function (err2, obj2) { 150 | t.ifError(err2); 151 | t.deepEqual(obj, obj2); 152 | t.end(); 153 | }); 154 | }); 155 | }); 156 | 157 | 158 | test('put overwrite', function (t) { 159 | var obj = { 160 | hello: 'world' 161 | }; 162 | CLIENT.put(helper.file, {foo: 'bar'}, function (err) { 163 | t.ifError(err); 164 | CLIENT.put(helper.file, obj, function (err2) { 165 | t.ifError(err2); 166 | CLIENT.get(helper.file, function (err3, obj2) { 167 | t.ifError(err3); 168 | t.deepEqual(obj, obj2); 169 | t.end(); 170 | }); 171 | }); 172 | }); 173 | }); 174 | 175 | 176 | test('get', function (t) { 177 | var opts = { 178 | object: { 179 | hello: 'world' 180 | } 181 | }; 182 | CLIENT.put(helper.file, opts, function (err) { 183 | t.ifError(err); 184 | CLIENT.get(helper.file, function (err2, obj) { 185 | t.ifError(err2); 186 | t.deepEqual(opts, obj); 187 | t.end(); 188 | }); 189 | }); 190 | }); 191 | 192 | 193 | test('readdir', function (t) { 194 | CLIENT.readdir(helper.root, function (err, children) { 195 | t.ifError(err); 196 | t.ok(children); 197 | t.equal(children.length, 1); 198 | t.equal(helper.dir.split('/').pop(), children[0]); 199 | t.end(); 200 | }); 201 | }); 202 | 203 | 204 | test('update', function (t) { 205 | CLIENT.put(helper.file, {}, function (err) { 206 | t.ifError(err); 207 | var obj = { 208 | hello: 'world' 209 | }; 210 | CLIENT.put(helper.file, obj, function (err2) { 211 | t.ifError(err2); 212 | CLIENT.get(helper.file, function (err3, obj2) { 213 | t.ifError(err3); 214 | t.deepEqual(obj, obj2); 215 | t.end(); 216 | }); 217 | }); 218 | }); 219 | }); 220 | 221 | 222 | test('stat', function (t) { 223 | CLIENT.stat(helper.root, function (err, stat) { 224 | t.ifError(err); 225 | t.ok(stat); 226 | if (stat) { 227 | t.equal(typeof (stat.czxid), 'number'); 228 | t.equal(typeof (stat.mzxid), 'number'); 229 | t.equal(typeof (stat.pzxid), 'number'); 230 | t.ok(stat.ctime instanceof Date); 231 | t.ok(stat.mtime instanceof Date); 232 | } 233 | t.end(); 234 | }); 235 | }); 236 | 237 | 238 | test('unlink', function (t) { 239 | CLIENT.put(helper.file, {}, function (err) { 240 | t.ifError(err); 241 | CLIENT.unlink(helper.file, function (err2) { 242 | t.ifError(err2); 243 | CLIENT.get(helper.file, function (err3) { 244 | t.ok(err3); 245 | t.equal(err3.name, 'NO_NODE'); 246 | t.end(); 247 | }); 248 | }); 249 | }); 250 | }); 251 | 252 | 253 | test('getState', function (t) { 254 | t.equal(CLIENT.getState(), 'connected'); 255 | t.end(); 256 | }); 257 | 258 | 259 | test('toString', function (t) { 260 | t.ok(CLIENT.toString()); 261 | t.end(); 262 | }); 263 | 264 | 265 | test('teardown', function (t) { 266 | CLIENT.rmr(helper.root, function (err) { 267 | t.ifError(err); 268 | CLIENT.close(function (err2) { 269 | t.ifError(err2); 270 | t.end(); 271 | }); 272 | }); 273 | }); 274 | 275 | 276 | test('error when closed', function (t) { 277 | CLIENT.readdir(helper.root, function (err) { 278 | t.ok(err); 279 | t.end(); 280 | }); 281 | }); 282 | -------------------------------------------------------------------------------- /test/conn.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Mark Cavage All rights reserved. 2 | 3 | var net = require('net'); 4 | 5 | var bunyan = require('bunyan'); 6 | var test = require('tape'); 7 | var uuid = require('node-uuid'); 8 | 9 | var helper = require('./helper'); 10 | var zkplus = require('../lib'); 11 | 12 | 13 | 14 | ///--- Globals 15 | 16 | var CLIENT; 17 | var PROXY; 18 | 19 | 20 | 21 | ///--- Helpers 22 | 23 | function createProxy(cb) { 24 | var server = net.createServer(function (conn) { 25 | var zk = net.connect(helper.zkServer, function () { 26 | zk.pipe(conn); 27 | conn.pipe(zk); 28 | }); 29 | var id = conn.remoteAddress + ':' + conn.remotePort; 30 | 31 | zk.once('close', function () { 32 | if (!server._conns[id]) 33 | return; 34 | 35 | server._conns[id].client.destroy(); 36 | delete server._conns[id]; 37 | }); 38 | 39 | conn.once('close', function () { 40 | if (!server._conns[id]) 41 | return; 42 | 43 | server._conns[id].zk.destroy(); 44 | delete server._conns[id]; 45 | }); 46 | 47 | server._conns[id] = { 48 | client: conn, 49 | zk: zk 50 | }; 51 | }); 52 | 53 | server._conns = {}; 54 | 55 | var _close = server.close; 56 | server.close = function () { 57 | Object.keys(server._conns).forEach(function (k) { 58 | var obj = server._conns[k]; 59 | setImmediate(function () { 60 | obj.client.destroy(); 61 | obj.zk.destroy(); 62 | }); 63 | delete server._conns[k]; 64 | }); 65 | server._conns = {}; 66 | _close.apply(server, arguments); 67 | }; 68 | 69 | server.listen(2182, '127.0.0.1', function () { 70 | cb(null, server); 71 | }); 72 | } 73 | 74 | 75 | function heartbeat(t) { 76 | CLIENT.stat('/', function (err, stats) { 77 | t.ifError(err); 78 | t.ok(stats); 79 | t.end(); 80 | }); 81 | } 82 | 83 | 84 | 85 | ///--- Tests 86 | 87 | test('setup', function (t) { 88 | createProxy(function (err, proxy) { 89 | t.ifError(err); 90 | PROXY = proxy; 91 | 92 | CLIENT = zkplus.createClient({ 93 | connectTimeout: false, 94 | log: helper.log, 95 | servers: [ 96 | { 97 | host: 'localhost', 98 | port: 2182 99 | } 100 | ], 101 | timeout: 1000 102 | }); 103 | t.ok(CLIENT); 104 | CLIENT.connect(function (err2) { 105 | t.ifError(err2); 106 | CLIENT.mkdirp(helper.subdir, function (err3) { 107 | t.ifError(err3); 108 | t.end(); 109 | }); 110 | }); 111 | }); 112 | }); 113 | 114 | 115 | test('heartbeat', heartbeat); 116 | 117 | 118 | test('stop proxy', function (t) { 119 | CLIENT.once('close', function () { 120 | t.end(); 121 | }); 122 | PROXY.close(); 123 | }); 124 | 125 | 126 | test('start proxy', function (t) { 127 | CLIENT.once('connect', function () { 128 | heartbeat(t); 129 | }); 130 | PROXY.listen(2182, '127.0.0.1'); 131 | }); 132 | 133 | 134 | test('rewatch', function (t) { 135 | CLIENT.watch(helper.dir, {method: 'list'}, function (err, w) { 136 | t.ifError(err); 137 | if (err) { 138 | t.end(); 139 | return; 140 | } 141 | 142 | // Fires once on reconnect, and then again on new data 143 | w.once('data', function (update) { 144 | t.ok(update); 145 | 146 | w.once('data', function (update2) { 147 | t.ok(update2); 148 | w.end(); 149 | t.end(); 150 | }); 151 | 152 | CLIENT.create(helper.file, {hello: 'world'}, function (err2) { 153 | t.ifError(err2); 154 | }); 155 | }); 156 | 157 | PROXY.close(); 158 | 159 | process.nextTick(function () { 160 | PROXY.listen(2182, '127.0.0.1'); 161 | }); 162 | }); 163 | }); 164 | 165 | 166 | test('re-ephemeral', function (t) { 167 | var opts = { 168 | flags: ['ephemeral_plus', 'sequence'] 169 | }; 170 | CLIENT.create(helper.subdir, {}, opts, function (err, p) { 171 | t.ifError(err); 172 | 173 | CLIENT.once('close', function () { 174 | CLIENT.once('connect', function () { 175 | CLIENT.stat(p, function (err2, stat) { 176 | t.ifError(err2); 177 | t.ok(stat); 178 | t.ok(stat.ephemeralOwner); 179 | t.end(); 180 | }); 181 | }); 182 | 183 | PROXY.listen(2182, '127.0.0.1'); 184 | }); 185 | PROXY.close(); 186 | }); 187 | }); 188 | 189 | 190 | test('teardown', function (t) { 191 | CLIENT.rmr(helper.dir, function (err) { 192 | t.ifError(err); 193 | CLIENT.close(function (err2) { 194 | t.ifError(err2); 195 | PROXY.close(); 196 | t.end(); 197 | }); 198 | }); 199 | }); 200 | -------------------------------------------------------------------------------- /test/connectionloss.test.js.dont: -------------------------------------------------------------------------------- 1 | var zk = require('../lib'); 2 | 3 | if (require.cache[__dirname + '/helper.js']) 4 | delete require.cache[__dirname + '/helper.js']; 5 | var helper = require('./helper.js'); 6 | 7 | 8 | 9 | ///--- Globals 10 | 11 | 12 | var ZK; 13 | 14 | var HOST = process.env.ZK_HOST || 'localhost'; 15 | var PORT = parseInt(process.env.ZK_PORT, 10) || 2181; 16 | 17 | ZK = zk.createClient({ 18 | log: helper.createLogger('zk.client.test.js'), 19 | servers: [ { 20 | host: (process.env.ZK_HOST || 'localhost'), 21 | port: 2181 22 | }], 23 | timeout: 10000 24 | }); 25 | 26 | 27 | ZK.on('error', function(err) { 28 | //console.log('XXX: error', err); 29 | ZK.log.error({err: err}, 'got error'); 30 | setInterval(function(){}, 1000); 31 | }); 32 | 33 | ZK.on('connect', function() { 34 | console.log('here'); 35 | var voter = zk.createGenericElection({ 36 | client: ZK, 37 | path: '/yunong', 38 | log: ZK.log, 39 | object: {} 40 | }); 41 | voter.on('error', function (err) { 42 | log.error({err: err}, 'XXX got election err'); 43 | }); 44 | //ZK.watch('/yunong', function(err, emitter) { 45 | //console.log(emitter, 'XXX'); 46 | //console.log(err, 'XXX'); 47 | //emitter.on('error', function(err) { 48 | //ZK.log.debug({err: err}, 'got error'); 49 | //}); 50 | //}); 51 | }); 52 | ZK.connect(); 53 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Mark Cavage. All rights reserved. 2 | 3 | var bunyan = require('bunyan'); 4 | var uuid = require('node-uuid'); 5 | 6 | 7 | 8 | ///--- Globals 9 | 10 | var ROOT = '/' + uuid().substr(0, 7); 11 | var DIR = ROOT + '/' + uuid().substr(0, 7); 12 | var FILE = DIR + '/unit_test.json'; 13 | var SUBDIR = DIR + '/foo/bar/baz'; 14 | 15 | 16 | ///--- Exports 17 | 18 | module.exports = { 19 | 20 | root: ROOT, 21 | dir: DIR, 22 | subdir: SUBDIR, 23 | file: FILE, 24 | 25 | get log() { 26 | return (bunyan.createLogger({ 27 | level: process.env.LOG_LEVEL || 'info', 28 | name: process.argv[1], 29 | stream: process.stdout, 30 | src: true, 31 | serializers: bunyan.stdSerializers 32 | })); 33 | }, 34 | 35 | get zkServer() { 36 | return ({ 37 | host: process.env.ZK_HOST || '127.0.0.1', 38 | port: parseInt(process.env.ZK_PORT, 10) || 2181 39 | }); 40 | } 41 | 42 | }; 43 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Mark Cavage. All rights reserved. 2 | 3 | var assert = require('assert'); 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | 7 | 8 | 9 | ///--- Run All Tests 10 | 11 | (function main() { 12 | fs.readdir(__dirname, function (err, files) { 13 | assert.ifError(err); 14 | 15 | files.filter(function (f) { 16 | return (/\.test\.js$/.test(f)); 17 | }).map(function (f) { 18 | return (path.join(__dirname, f)); 19 | }).forEach(require); 20 | }); 21 | })(); 22 | -------------------------------------------------------------------------------- /test/watch.test.js: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Mark Cavage All rights reserved. 2 | 3 | var path = require('path'); 4 | 5 | var bunyan = require('bunyan'); 6 | var test = require('tape'); 7 | var uuid = require('node-uuid'); 8 | var vasync = require('vasync'); 9 | 10 | var helper = require('./helper'); 11 | var zkplus = require('../lib'); 12 | 13 | 14 | 15 | ///--- Globals 16 | 17 | var CLIENT; 18 | 19 | 20 | 21 | ///--- Tests 22 | 23 | test('setup', function (t) { 24 | CLIENT = zkplus.createClient({ 25 | connectTimeout: false, 26 | log: helper.log, 27 | servers: [helper.zkServer], 28 | timeout: 1000 29 | }); 30 | 31 | t.ok(CLIENT); 32 | 33 | CLIENT.connect(function (err) { 34 | t.ifError(err); 35 | if (err) { 36 | t.end(); 37 | return; 38 | } 39 | 40 | CLIENT.mkdirp(helper.subdir, function (err2) { 41 | t.ifError(err2); 42 | 43 | CLIENT.create(helper.file, {}, function (err3, p) { 44 | t.ifError(err3); 45 | t.ok(p); 46 | t.equal(helper.file, p); 47 | t.end(); 48 | }); 49 | }); 50 | }); 51 | }); 52 | 53 | 54 | test('watch (data)', function (t) { 55 | var f = helper.file; 56 | var obj = { 57 | foo: 'bar' 58 | }; 59 | var zk = CLIENT; 60 | 61 | vasync.pipeline({ 62 | funcs: [ 63 | function startWatch(_, cb) { 64 | zk.watch(f, function (err, w) { 65 | t.ifError(err); 66 | t.ok(w); 67 | if (err || !w) { 68 | cb(err || new Error('no watcher')); 69 | return; 70 | } 71 | 72 | w.on('data', function (update) { 73 | t.ok(update); 74 | t.deepEqual(update, obj); 75 | t.end(); 76 | }); 77 | 78 | setImmediate(cb); 79 | }); 80 | }, 81 | function _update(_, cb) { 82 | zk.put(helper.file, obj, function (err) { 83 | t.ifError(err); 84 | cb(err); 85 | }); 86 | } 87 | ] 88 | }, function (err) { 89 | t.ifError(err); 90 | if (err) 91 | t.end(); 92 | }); 93 | }); 94 | 95 | 96 | test('watch (directory)', function (t) { 97 | var d = helper.subdir; 98 | var name; 99 | var zk = CLIENT; 100 | 101 | vasync.pipeline({ 102 | funcs: [ 103 | function startWatch(_, cb) { 104 | zk.watch(d, {method: 'list'}, function (err, w) { 105 | t.ifError(err); 106 | t.ok(w); 107 | if (err || !w) { 108 | cb(err || new Error('no watcher')); 109 | return; 110 | } 111 | 112 | w.on('data', function (update) { 113 | t.ok(update); 114 | t.ok(update.indexOf(path.basename(name)) !== -1); 115 | t.end(); 116 | }); 117 | 118 | setImmediate(cb); 119 | }); 120 | }, 121 | function _update(_, cb) { 122 | zk.create(path.join(d, uuid.v4()), {}, function (err, p) { 123 | t.ifError(err); 124 | t.ok(p); 125 | cb(err); 126 | name = p; 127 | }); 128 | } 129 | ] 130 | }, function (err) { 131 | t.ifError(err); 132 | if (err) 133 | t.end(); 134 | }); 135 | }); 136 | 137 | 138 | test('watch (data+initialData)', function (t) { 139 | var f = helper.file; 140 | var obj = { 141 | slam: 'dunk' 142 | }; 143 | var zk = CLIENT; 144 | 145 | vasync.pipeline({ 146 | funcs: [ 147 | function setup(_, cb) { 148 | zk.put(f, {}, cb); 149 | }, 150 | function startWatch(_, cb) { 151 | zk.watch(f, {initialData: true}, function (err, w) { 152 | t.ifError(err); 153 | t.ok(w); 154 | if (err || !w) { 155 | cb(err || new Error('no watcher')); 156 | return; 157 | } 158 | 159 | var hits = 0; 160 | w.on('data', function (update) { 161 | t.ok(update); 162 | t.deepEqual(update, hits++ < 1 ? {} : obj); 163 | if (hits === 2) 164 | t.end(); 165 | }); 166 | 167 | setImmediate(cb); 168 | }); 169 | }, 170 | function _update(_, cb) { 171 | zk.put(helper.file, obj, function (err) { 172 | t.ifError(err); 173 | cb(err); 174 | }); 175 | } 176 | ] 177 | }, function (err) { 178 | t.ifError(err); 179 | if (err) 180 | t.end(); 181 | }); 182 | }); 183 | 184 | 185 | test('watch (directory + initial data)', function (t) { 186 | var d = helper.subdir; 187 | var name; 188 | var zk = CLIENT; 189 | 190 | vasync.pipeline({ 191 | funcs: [ 192 | function startWatch(_, cb) { 193 | var _opts = { 194 | initialData: true, 195 | method: 'list' 196 | }; 197 | zk.watch(d, _opts, function (err, w) { 198 | t.ifError(err); 199 | t.ok(w); 200 | if (err || !w) { 201 | cb(err || new Error('no watcher')); 202 | return; 203 | } 204 | 205 | var hits = 0; 206 | w.on('data', function (update) { 207 | t.ok(update); 208 | if (hits++ >= 1) { 209 | t.ok(update.indexOf(path.basename(name)) !== -1); 210 | t.end(); 211 | } 212 | }); 213 | 214 | setImmediate(cb); 215 | }); 216 | }, 217 | function _update(_, cb) { 218 | zk.create(path.join(d, uuid.v4()), {}, function (err, p) { 219 | t.ifError(err); 220 | t.ok(p); 221 | cb(err); 222 | name = p; 223 | }); 224 | } 225 | ] 226 | }, function (err) { 227 | t.ifError(err); 228 | if (err) 229 | t.end(); 230 | }); 231 | }); 232 | 233 | 234 | test('teardown', function (t) { 235 | CLIENT.rmr(helper.root, function (err) { 236 | t.ifError(err); 237 | CLIENT.close(function (err2) { 238 | t.ifError(err2); 239 | t.end(); 240 | }); 241 | }); 242 | }); 243 | -------------------------------------------------------------------------------- /tools/jsl.node.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Configuration File for JavaScript Lint 3 | # 4 | # This configuration file can be used to lint a collection of scripts, or to enable 5 | # or disable warnings for scripts that are linted via the command line. 6 | # 7 | 8 | ### Warnings 9 | # Enable or disable warnings based on requirements. 10 | # Use "+WarningName" to display or "-WarningName" to suppress. 11 | # 12 | +ambiguous_else_stmt # the else statement could be matched with one of multiple if statements (use curly braces to indicate intent 13 | +ambiguous_nested_stmt # block statements containing block statements should use curly braces to resolve ambiguity 14 | +ambiguous_newline # unexpected end of line; it is ambiguous whether these lines are part of the same statement 15 | +anon_no_return_value # anonymous function does not always return value 16 | +assign_to_function_call # assignment to a function call 17 | -block_without_braces # block statement without curly braces 18 | +comma_separated_stmts # multiple statements separated by commas (use semicolons?) 19 | +comparison_type_conv # comparisons against null, 0, true, false, or an empty string allowing implicit type conversion (use === or !==) 20 | +default_not_at_end # the default case is not at the end of the switch statement 21 | +dup_option_explicit # duplicate "option explicit" control comment 22 | +duplicate_case_in_switch # duplicate case in switch statement 23 | +duplicate_formal # duplicate formal argument {name} 24 | +empty_statement # empty statement or extra semicolon 25 | +identifier_hides_another # identifer {name} hides an identifier in a parent scope 26 | -inc_dec_within_stmt # increment (++) and decrement (--) operators used as part of greater statement 27 | +incorrect_version # Expected /*jsl:content-type*/ control comment. The script was parsed with the wrong version. 28 | +invalid_fallthru # unexpected "fallthru" control comment 29 | +invalid_pass # unexpected "pass" control comment 30 | +jsl_cc_not_understood # couldn't understand control comment using /*jsl:keyword*/ syntax 31 | +leading_decimal_point # leading decimal point may indicate a number or an object member 32 | +legacy_cc_not_understood # couldn't understand control comment using /*@keyword@*/ syntax 33 | +meaningless_block # meaningless block; curly braces have no impact 34 | +mismatch_ctrl_comments # mismatched control comment; "ignore" and "end" control comments must have a one-to-one correspondence 35 | +misplaced_regex # regular expressions should be preceded by a left parenthesis, assignment, colon, or comma 36 | +missing_break # missing break statement 37 | +missing_break_for_last_case # missing break statement for last case in switch 38 | +missing_default_case # missing default case in switch statement 39 | +missing_option_explicit # the "option explicit" control comment is missing 40 | +missing_semicolon # missing semicolon 41 | +missing_semicolon_for_lambda # missing semicolon for lambda assignment 42 | +multiple_plus_minus # unknown order of operations for successive plus (e.g. x+++y) or minus (e.g. x---y) signs 43 | +nested_comment # nested comment 44 | +no_return_value # function {name} does not always return a value 45 | +octal_number # leading zeros make an octal number 46 | +parseint_missing_radix # parseInt missing radix parameter 47 | +partial_option_explicit # the "option explicit" control comment, if used, must be in the first script tag 48 | +redeclared_var # redeclaration of {name} 49 | +trailing_comma_in_array # extra comma is not recommended in array initializers 50 | +trailing_decimal_point # trailing decimal point may indicate a number or an object member 51 | +undeclared_identifier # undeclared identifier: {name} 52 | +unreachable_code # unreachable code 53 | -unreferenced_argument # argument declared but never referenced: {name} 54 | -unreferenced_function # function is declared but never referenced: {name} 55 | +unreferenced_variable # variable is declared but never referenced: {name} 56 | +unsupported_version # JavaScript {version} is not supported 57 | +use_of_label # use of label 58 | +useless_assign # useless assignment 59 | +useless_comparison # useless comparison; comparing identical expressions 60 | -useless_quotes # the quotation marks are unnecessary 61 | +useless_void # use of the void type may be unnecessary (void is always undefined) 62 | +var_hides_arg # variable {name} hides argument 63 | +want_assign_or_call # expected an assignment or function call 64 | +with_statement # with statement hides undeclared variables; use temporary variable instead 65 | 66 | 67 | ### Output format 68 | # Customize the format of the error message. 69 | # __FILE__ indicates current file path 70 | # __FILENAME__ indicates current file name 71 | # __LINE__ indicates current line 72 | # __COL__ indicates current column 73 | # __ERROR__ indicates error message (__ERROR_PREFIX__: __ERROR_MSG__) 74 | # __ERROR_NAME__ indicates error name (used in configuration file) 75 | # __ERROR_PREFIX__ indicates error prefix 76 | # __ERROR_MSG__ indicates error message 77 | # 78 | # For machine-friendly output, the output format can be prefixed with 79 | # "encode:". If specified, all items will be encoded with C-slashes. 80 | # 81 | # Visual Studio syntax (default): 82 | +output-format __FILE__(__LINE__): __ERROR__ 83 | # Alternative syntax: 84 | #+output-format __FILE__:__LINE__: __ERROR__ 85 | 86 | 87 | ### Context 88 | # Show the in-line position of the error. 89 | # Use "+context" to display or "-context" to suppress. 90 | # 91 | +context 92 | 93 | 94 | ### Control Comments 95 | # Both JavaScript Lint and the JScript interpreter confuse each other with the syntax for 96 | # the /*@keyword@*/ control comments and JScript conditional comments. (The latter is 97 | # enabled in JScript with @cc_on@). The /*jsl:keyword*/ syntax is preferred for this reason, 98 | # although legacy control comments are enabled by default for backward compatibility. 99 | # 100 | -legacy_control_comments 101 | 102 | 103 | ### Defining identifiers 104 | # By default, "option explicit" is enabled on a per-file basis. 105 | # To enable this for all files, use "+always_use_option_explicit" 106 | -always_use_option_explicit 107 | 108 | # Define certain identifiers of which the lint is not aware. 109 | # (Use this in conjunction with the "undeclared identifier" warning.) 110 | # 111 | # Common uses for webpages might be: 112 | +define __dirname 113 | +define clearInterval 114 | +define clearTimeout 115 | +define console 116 | +define exports 117 | +define global 118 | +define module 119 | +define process 120 | +define require 121 | +define setImmediate 122 | +define setInterval 123 | +define setTimeout 124 | +define Buffer 125 | +define JSON 126 | +define Math 127 | 128 | ### JavaScript Version 129 | # To change the default JavaScript version: 130 | #+default-type text/javascript;version=1.5 131 | #+default-type text/javascript;e4x=1 132 | 133 | ### Files 134 | # Specify which files to lint 135 | # Use "+recurse" to enable recursion (disabled by default). 136 | # To add a set of files, use "+process FileName", "+process Folder\Path\*.js", 137 | # or "+process Folder\Path\*.htm". 138 | # 139 | 140 | -------------------------------------------------------------------------------- /tools/jsstyle.conf: -------------------------------------------------------------------------------- 1 | indent=4 2 | doxygen 3 | unparenthesized-return=1 4 | blank-after-start-comment=0 5 | -------------------------------------------------------------------------------- /tools/mk/Makefile.defs: -------------------------------------------------------------------------------- 1 | # -*- mode: makefile -*- 2 | # 3 | # Copyright (c) 2012, Joyent, Inc. All rights reserved. 4 | # 5 | # Makefile.defs: common defines. 6 | # 7 | # NOTE: This makefile comes from the "eng" repo. It's designed to be dropped 8 | # into other repos as-is without requiring any modifications. If you find 9 | # yourself changing this file, you should instead update the original copy in 10 | # eng.git and then update your repo to use the new version. 11 | # 12 | # This makefile defines some useful defines. Include it at the top of 13 | # your Makefile. 14 | # 15 | # Definitions in this Makefile: 16 | # 17 | # TOP The absolute path to the project directory. The top dir. 18 | # BRANCH The current git branch. 19 | # TIMESTAMP The timestamp for the build. This can be set via 20 | # the TIMESTAMP envvar (used by MG-based builds). 21 | # STAMP A build stamp to use in built package names. 22 | # 23 | 24 | TOP := $(shell pwd) 25 | 26 | # 27 | # Mountain Gorilla-spec'd versioning. 28 | # See "Package Versioning" in MG's README.md: 29 | # 30 | # 31 | # Need GNU awk for multi-char arg to "-F". 32 | _AWK := $(shell (which gawk >/dev/null && echo gawk) \ 33 | || (which nawk >/dev/null && echo nawk) \ 34 | || echo awk) 35 | BRANCH := $(shell git symbolic-ref HEAD | $(_AWK) -F/ '{print $$3}') 36 | ifeq ($(TIMESTAMP),) 37 | TIMESTAMP := $(shell date -u "+%Y%m%dT%H%M%SZ") 38 | endif 39 | _GITDESCRIBE := g$(shell git describe --all --long --dirty | $(_AWK) -F'-g' '{print $$NF}') 40 | STAMP := $(BRANCH)-$(TIMESTAMP)-$(_GITDESCRIBE) 41 | 42 | # node-gyp will print build info useful for debugging with V=1 43 | export V=1 44 | -------------------------------------------------------------------------------- /tools/mk/Makefile.deps: -------------------------------------------------------------------------------- 1 | # -*- mode: makefile -*- 2 | # 3 | # Copyright (c) 2012, Joyent, Inc. All rights reserved. 4 | # 5 | # Makefile.deps: Makefile for including common tools as dependencies 6 | # 7 | # NOTE: This makefile comes from the "eng" repo. It's designed to be dropped 8 | # into other repos as-is without requiring any modifications. If you find 9 | # yourself changing this file, you should instead update the original copy in 10 | # eng.git and then update your repo to use the new version. 11 | # 12 | # This file is separate from Makefile.targ so that teams can choose 13 | # independently whether to use the common targets in Makefile.targ and the 14 | # common tools here. 15 | # 16 | 17 | # 18 | # javascriptlint 19 | # 20 | JSL_EXEC ?= deps/javascriptlint/build/install/jsl 21 | JSL ?= $(JSL_EXEC) 22 | 23 | $(JSL_EXEC): | deps/javascriptlint/.git 24 | cd deps/javascriptlint && make install 25 | 26 | distclean:: 27 | if [[ -f deps/javascriptlint/Makefile ]]; then \ 28 | cd deps/javascriptlint && make clean; \ 29 | fi 30 | 31 | # 32 | # jsstyle 33 | # 34 | JSSTYLE_EXEC ?= deps/jsstyle/jsstyle 35 | JSSTYLE ?= $(JSSTYLE_EXEC) 36 | 37 | $(JSSTYLE_EXEC): | deps/jsstyle/.git 38 | 39 | # 40 | # restdown 41 | # 42 | RESTDOWN_EXEC ?= deps/restdown/bin/restdown 43 | RESTDOWN ?= python2.6 $(RESTDOWN_EXEC) 44 | $(RESTDOWN_EXEC): | deps/restdown/.git 45 | -------------------------------------------------------------------------------- /tools/mk/Makefile.node_deps.defs: -------------------------------------------------------------------------------- 1 | # -*- mode: makefile -*- 2 | # 3 | # Copyright (c) 2012, Joyent, Inc. All rights reserved. 4 | # 5 | # Makefile.node_deps.defs: Makefile for including npm modules whose sources 6 | # reside inside the repo. This should NOT be used for modules in the npm 7 | # public repo or modules that could be specified with git SHAs. 8 | # 9 | # NOTE: This makefile comes from the "eng" repo. It's designed to be dropped 10 | # into other repos as-is without requiring any modifications. If you find 11 | # yourself changing this file, you should instead update the original copy in 12 | # eng.git and then update your repo to use the new version. 13 | # 14 | 15 | # 16 | # This Makefile takes as input the following make variable: 17 | # 18 | # REPO_MODULES List of relative paths to node modules (i.e., npm 19 | # packages) inside this repo. For example: 20 | # src/node-canative, where there's a binary npm package 21 | # in src/node-canative. 22 | # 23 | # Based on the above, this Makefile defines the following new variables: 24 | # 25 | # REPO_DEPS List of relative paths to the installed modules. For 26 | # example: "node_modules/canative". 27 | # 28 | # The accompanying Makefile.node_deps.targ defines a target that will install 29 | # each of REPO_MODULES into REPO_DEPS and remove REPO_DEPS with "make clean". 30 | # The top-level Makefile is responsible for depending on REPO_DEPS where 31 | # appropriate (usually the "deps" or "all" target). 32 | # 33 | 34 | REPO_DEPS := $(REPO_MODULES:src/node-%=node_modules/%) 35 | CLEAN_FILES += $(REPO_DEPS) 36 | -------------------------------------------------------------------------------- /tools/mk/Makefile.node_deps.targ: -------------------------------------------------------------------------------- 1 | # -*- mode: makefile -*- 2 | # 3 | # Copyright (c) 2012, Joyent, Inc. All rights reserved. 4 | # 5 | # Makefile.node_deps.targ: targets for Makefile.node_deps.defs. 6 | # 7 | # NOTE: This makefile comes from the "eng" repo. It's designed to be dropped 8 | # into other repos as-is without requiring any modifications. If you find 9 | # yourself changing this file, you should instead update the original copy in 10 | # eng.git and then update your repo to use the new version. 11 | # 12 | 13 | NPM_EXEC ?= $(error NPM_EXEC must be defined for Makefile.node_deps.targ) 14 | 15 | node_modules/%: src/node-% | $(NPM_EXEC) 16 | $(NPM) install $< 17 | -------------------------------------------------------------------------------- /tools/mk/Makefile.targ: -------------------------------------------------------------------------------- 1 | # -*- mode: makefile -*- 2 | # 3 | # Copyright (c) 2012, Joyent, Inc. All rights reserved. 4 | # 5 | # Makefile.targ: common targets. 6 | # 7 | # NOTE: This makefile comes from the "eng" repo. It's designed to be dropped 8 | # into other repos as-is without requiring any modifications. If you find 9 | # yourself changing this file, you should instead update the original copy in 10 | # eng.git and then update your repo to use the new version. 11 | # 12 | # This Makefile defines several useful targets and rules. You can use it by 13 | # including it from a Makefile that specifies some of the variables below. 14 | # 15 | # Targets defined in this Makefile: 16 | # 17 | # check Checks JavaScript files for lint and style 18 | # Checks bash scripts for syntax 19 | # Checks SMF manifests for validity against the SMF DTD 20 | # 21 | # clean Removes built files 22 | # 23 | # docs Builds restdown documentation in docs/ 24 | # 25 | # prepush Depends on "check" and "test" 26 | # 27 | # test Does nothing (you should override this) 28 | # 29 | # xref Generates cscope (source cross-reference index) 30 | # 31 | # For details on what these targets are supposed to do, see the Joyent 32 | # Engineering Guide. 33 | # 34 | # To make use of these targets, you'll need to set some of these variables. Any 35 | # variables left unset will simply not be used. 36 | # 37 | # BASH_FILES Bash scripts to check for syntax 38 | # (paths relative to top-level Makefile) 39 | # 40 | # CLEAN_FILES Files to remove as part of the "clean" target. Note 41 | # that files generated by targets in this Makefile are 42 | # automatically included in CLEAN_FILES. These include 43 | # restdown-generated HTML and JSON files. 44 | # 45 | # DOC_FILES Restdown (documentation source) files. These are 46 | # assumed to be contained in "docs/", and must NOT 47 | # contain the "docs/" prefix. 48 | # 49 | # JSL_CONF_NODE Specify JavaScriptLint configuration files 50 | # JSL_CONF_WEB (paths relative to top-level Makefile) 51 | # 52 | # Node.js and Web configuration files are separate 53 | # because you'll usually want different global variable 54 | # configurations. If no file is specified, none is given 55 | # to jsl, which causes it to use a default configuration, 56 | # which probably isn't what you want. 57 | # 58 | # JSL_FILES_NODE JavaScript files to check with Node config file. 59 | # JSL_FILES_WEB JavaScript files to check with Web config file. 60 | # 61 | # You can also override these variables: 62 | # 63 | # BASH Path to bash (default: bash) 64 | # 65 | # CSCOPE_DIRS Directories to search for source files for the cscope 66 | # index. (default: ".") 67 | # 68 | # JSL Path to JavaScriptLint (default: "jsl") 69 | # 70 | # JSL_FLAGS_NODE Additional flags to pass through to JSL 71 | # JSL_FLAGS_WEB 72 | # JSL_FLAGS 73 | # 74 | # JSSTYLE Path to jsstyle (default: jsstyle) 75 | # 76 | # JSSTYLE_FLAGS Additional flags to pass through to jsstyle 77 | # 78 | 79 | # 80 | # Defaults for the various tools we use. 81 | # 82 | BASH ?= bash 83 | BASHSTYLE ?= tools/bashstyle 84 | CP ?= cp 85 | CSCOPE ?= cscope 86 | CSCOPE_DIRS ?= . 87 | JSL ?= jsl 88 | JSSTYLE ?= jsstyle 89 | MKDIR ?= mkdir -p 90 | MV ?= mv 91 | RESTDOWN_FLAGS ?= 92 | RMTREE ?= rm -rf 93 | JSL_FLAGS ?= --nologo --nosummary 94 | 95 | ifeq ($(shell uname -s),SunOS) 96 | TAR ?= gtar 97 | else 98 | TAR ?= tar 99 | endif 100 | 101 | 102 | # 103 | # Defaults for other fixed values. 104 | # 105 | BUILD = build 106 | DISTCLEAN_FILES += $(BUILD) 107 | DOC_BUILD = $(BUILD)/docs/public 108 | 109 | # 110 | # Configure JSL_FLAGS_{NODE,WEB} based on JSL_CONF_{NODE,WEB}. 111 | # 112 | ifneq ($(origin JSL_CONF_NODE), undefined) 113 | JSL_FLAGS_NODE += --conf=$(JSL_CONF_NODE) 114 | endif 115 | 116 | ifneq ($(origin JSL_CONF_WEB), undefined) 117 | JSL_FLAGS_WEB += --conf=$(JSL_CONF_WEB) 118 | endif 119 | 120 | # 121 | # Targets. For descriptions on what these are supposed to do, see the 122 | # Joyent Engineering Guide. 123 | # 124 | 125 | # 126 | # Instruct make to keep around temporary files. We have rules below that 127 | # automatically update git submodules as needed, but they employ a deps/*/.git 128 | # temporary file. Without this directive, make tries to remove these .git 129 | # directories after the build has completed. 130 | # 131 | .SECONDARY: $($(wildcard deps/*):%=%/.git) 132 | 133 | # 134 | # This rule enables other rules that use files from a git submodule to have 135 | # those files depend on deps/module/.git and have "make" automatically check 136 | # out the submodule as needed. 137 | # 138 | deps/%/.git: 139 | git submodule update --init deps/$* 140 | 141 | # 142 | # These recipes make heavy use of dynamically-created phony targets. The parent 143 | # Makefile defines a list of input files like BASH_FILES. We then say that each 144 | # of these files depends on a fake target called filename.bashchk, and then we 145 | # define a pattern rule for those targets that runs bash in check-syntax-only 146 | # mode. This mechanism has the nice properties that if you specify zero files, 147 | # the rule becomes a noop (unlike a single rule to check all bash files, which 148 | # would invoke bash with zero files), and you can check individual files from 149 | # the command line with "make filename.bashchk". 150 | # 151 | .PHONY: check-bash 152 | check-bash: $(BASH_FILES:%=%.bashchk) $(BASH_FILES:%=%.bashstyle) 153 | 154 | %.bashchk: % 155 | $(BASH) -n $^ 156 | 157 | %.bashstyle: % 158 | $(BASHSTYLE) $^ 159 | 160 | # 161 | # The above approach can be slow when there are many files to check because it 162 | # requires that "make" invoke the check tool once for each file, rather than 163 | # passing in several files at once. For the JavaScript check targets, we define 164 | # a variable for the target itself *only if* the list of input files is 165 | # non-empty. This avoids invoking the tool if there are no files to check. 166 | # 167 | JSL_NODE_TARGET = $(if $(JSL_FILES_NODE), check-jsl-node) 168 | .PHONY: check-jsl-node 169 | check-jsl-node: $(JSL_EXEC) 170 | $(JSL) $(JSL_FLAGS) $(JSL_FLAGS_NODE) $(JSL_FILES_NODE) 171 | 172 | JSL_WEB_TARGET = $(if $(JSL_FILES_WEB), check-jsl-web) 173 | .PHONY: check-jsl-web 174 | check-jsl-web: $(JSL_EXEC) 175 | $(JSL) $(JSL_FLAGS) $(JSL_FLAGS_WEB) $(JSL_FILES_WEB) 176 | 177 | .PHONY: check-jsl 178 | check-jsl: $(JSL_NODE_TARGET) $(JSL_WEB_TARGET) 179 | 180 | JSSTYLE_TARGET = $(if $(JSSTYLE_FILES), check-jsstyle) 181 | .PHONY: check-jsstyle 182 | check-jsstyle: $(JSSTYLE_EXEC) 183 | $(JSSTYLE) $(JSSTYLE_FLAGS) $(JSSTYLE_FILES) 184 | 185 | .PHONY: check 186 | check: check-jsl $(JSSTYLE_TARGET) check-bash 187 | @echo check ok 188 | 189 | .PHONY: clean 190 | clean:: 191 | -$(RMTREE) $(CLEAN_FILES) 192 | 193 | .PHONY: distclean 194 | distclean:: clean 195 | -$(RMTREE) $(DISTCLEAN_FILES) 196 | 197 | CSCOPE_FILES = cscope.in.out cscope.out cscope.po.out 198 | CLEAN_FILES += $(CSCOPE_FILES) 199 | 200 | .PHONY: xref 201 | xref: cscope.files 202 | $(CSCOPE) -bqR 203 | 204 | .PHONY: cscope.files 205 | cscope.files: 206 | find $(CSCOPE_DIRS) -name '*.c' -o -name '*.h' -o -name '*.cc' \ 207 | -o -name '*.js' -o -name '*.s' -o -name '*.cpp' > $@ 208 | 209 | # 210 | # The "docs" target is complicated because we do several things here: 211 | # 212 | # (1) Use restdown to build HTML and JSON files from each of DOC_FILES. 213 | # 214 | # (2) Copy these files into $(DOC_BUILD) (build/docs/public), which 215 | # functions as a complete copy of the documentation that could be 216 | # mirrored or served over HTTP. 217 | # 218 | # (3) Then copy any directories and media from docs/media into 219 | # $(DOC_BUILD)/media. This allows projects to include their own media, 220 | # including files that will override same-named files provided by 221 | # restdown. 222 | # 223 | # Step (3) is the surprisingly complex part: in order to do this, we need to 224 | # identify the subdirectories in docs/media, recreate them in 225 | # $(DOC_BUILD)/media, then do the same with the files. 226 | # 227 | DOC_MEDIA_DIRS := $(shell find docs/media -type d 2>/dev/null | grep -v "^docs/media$$") 228 | DOC_MEDIA_DIRS := $(DOC_MEDIA_DIRS:docs/media/%=%) 229 | DOC_MEDIA_DIRS_BUILD := $(DOC_MEDIA_DIRS:%=$(DOC_BUILD)/media/%) 230 | 231 | DOC_MEDIA_FILES := $(shell find docs/media -type f 2>/dev/null) 232 | DOC_MEDIA_FILES := $(DOC_MEDIA_FILES:docs/media/%=%) 233 | DOC_MEDIA_FILES_BUILD := $(DOC_MEDIA_FILES:%=$(DOC_BUILD)/media/%) 234 | 235 | # 236 | # Like the other targets, "docs" just depends on the final files we want to 237 | # create in $(DOC_BUILD), leveraging other targets and recipes to define how 238 | # to get there. 239 | # 240 | .PHONY: docs 241 | docs: \ 242 | $(DOC_FILES:%.restdown=$(DOC_BUILD)/%.html) \ 243 | $(DOC_FILES:%.restdown=$(DOC_BUILD)/%.json) \ 244 | $(DOC_MEDIA_FILES_BUILD) 245 | 246 | # 247 | # We keep the intermediate files so that the next build can see whether the 248 | # files in DOC_BUILD are up to date. 249 | # 250 | .PRECIOUS: \ 251 | $(DOC_FILES:%.restdown=docs/%.html) \ 252 | $(DOC_FILES:%.restdown=docs/%json) 253 | 254 | # 255 | # We do clean those intermediate files, as well as all of DOC_BUILD. 256 | # 257 | CLEAN_FILES += \ 258 | $(DOC_BUILD) \ 259 | $(DOC_FILES:%.restdown=docs/%.html) \ 260 | $(DOC_FILES:%.restdown=docs/%.json) 261 | 262 | # 263 | # Before installing the files, we must make sure the directories exist. The | 264 | # syntax tells make that the dependency need only exist, not be up to date. 265 | # Otherwise, it might try to rebuild spuriously because the directory itself 266 | # appears out of date. 267 | # 268 | $(DOC_MEDIA_FILES_BUILD): | $(DOC_MEDIA_DIRS_BUILD) 269 | 270 | $(DOC_BUILD)/%: docs/% | $(DOC_BUILD) 271 | $(CP) $< $@ 272 | 273 | docs/%.json docs/%.html: docs/%.restdown | $(DOC_BUILD) $(RESTDOWN_EXEC) 274 | $(RESTDOWN) $(RESTDOWN_FLAGS) -m $(DOC_BUILD) $< 275 | 276 | $(DOC_BUILD): 277 | $(MKDIR) $@ 278 | 279 | $(DOC_MEDIA_DIRS_BUILD): 280 | $(MKDIR) $@ 281 | 282 | # 283 | # The default "test" target does nothing. This should usually be overridden by 284 | # the parent Makefile. It's included here so we can define "prepush" without 285 | # requiring the repo to define "test". 286 | # 287 | .PHONY: test 288 | test: 289 | 290 | .PHONY: prepush 291 | prepush: check test 292 | --------------------------------------------------------------------------------