├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── config └── disconf.properties ├── index.js ├── lib ├── ConfNode.js ├── HttpClient.js └── utils │ ├── log.js │ └── write.js ├── package.json └── test ├── lib ├── request.js └── zookeeper.js └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 4 11 | end_of_line = lf 12 | insert_final_newline = true 13 | 14 | # Matches the exact files either package.json or .travis.yml 15 | [{package.json,.travis.yml}] 16 | indent_style = space 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "parserOptions": { 3 | "ecmaVersion": 5, 4 | "sourceType": "module" 5 | }, 6 | 7 | "env": { 8 | "browser": false, 9 | "commonjs": true 10 | }, 11 | 12 | "rules": { 13 | "indent": ["error", 4, { "SwitchCase": 1 }], 14 | "linebreak-style": ["error", "unix"], 15 | "quotes": ["error", "single"], 16 | "semi": ["error", "always"], 17 | "object-curly-spacing": ["warn", "never", {"objectsInObjects": false}], 18 | "no-debugger": "error", 19 | "no-alert": "error", 20 | "no-console": "warn" 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Webstorm 40 | .idea 41 | 42 | # Test 43 | test/config 44 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | 2 | language: node_js 3 | sudo: true 4 | node_js: 5 | - '0.12' 6 | - '4' 7 | cache: 8 | directories: 9 | - node_modules 10 | script: 11 | - npm test 12 | after_script: 13 | - npm run coverage 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Corey Fei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-disconf-client 2 | 3 | [![Build Status](https://api.travis-ci.org/Corey600/node-disconf-client.svg)](http://travis-ci.org/Corey600/node-disconf-client) 4 | [![Coverage Status](https://coveralls.io/repos/github/Corey600/node-disconf-client/badge.svg)](https://coveralls.io/github/Corey600/node-disconf-client) 5 | [![NPM Downloads](https://img.shields.io/npm/dm/node-disconf-client.svg?style=flat)](https://www.npmjs.org/package/node-disconf-client) 6 | [![NPM Version](http://img.shields.io/npm/v/node-disconf-client.svg?style=flat)](https://www.npmjs.org/package/node-disconf-client) 7 | [![License](https://img.shields.io/npm/l/node-disconf-client.svg?style=flat)](https://www.npmjs.org/package/node-disconf-client) 8 | 9 | A Javascript module for 10 | [Node.js](http://nodejs.org) 11 | to connect 12 | [Disconf](https://github.com/knightliao/disconf) 13 | service. 14 | 15 | ## Installation 16 | 17 | You can install it using npm: 18 | 19 | ```bash 20 | $ npm install node-disconf-client 21 | ``` 22 | 23 | ## Example 24 | 25 | ```javascript 26 | var os = require('os'); 27 | var path = require('path'); 28 | var disconf = require('node-disconf-client'); 29 | // 可以配合config模块使用 30 | var config = require('config'); 31 | 32 | var configDir = config.util.getEnv('NODE_CONFIG_DIR'); 33 | disconf.init({ 34 | // 配置disconf的本地配置文件路径 35 | path: configDir, 36 | filename: 'disconf.properties' 37 | }, { 38 | // 配置远程下载的配置保存哪个文件(所有配置聚合后的文件) 39 | // 这里以 hostname 命名,使能被 config 模块读取 40 | dist_file: path.join(configDir, os.hostname() + '.properties'), 41 | // 配置远程下载的配置保存哪个目录(配置源文件) 42 | user_define_download_dir: path.join(configDir, 'download') 43 | }); 44 | 45 | // 错误事件 46 | disconf.on('error', function (err) { 47 | console.log('error:', err.stack); 48 | }); 49 | 50 | // 准备事件,此时重新加载config模块,使配置生效 51 | disconf.on('ready', function (data) { 52 | console.log('ready:', data); 53 | var conf = disconf.util.reloadConfig(); 54 | console.log('conf:', conf); 55 | }); 56 | 57 | // 配置在远程被修改,此时重新加载config模块,使配置生效 58 | disconf.on('change', function (event, data) { 59 | console.log('change:', event.name, data); 60 | var conf = disconf.util.reloadConfig(); 61 | console.log('conf:', conf); 62 | }); 63 | ``` 64 | 65 | ## Documentation 66 | 67 | ### init(file[, option[, callback]]) 68 | 69 | This is the only external interface that executes instance initialization. The argument `file` can set the directory and file name of the _option_ file, that can configure the client itself. The argument `option` can set _option_ manually. Its priority is higher than the file. Part of _option_ with the same name is same as the Java client. You can refer Java [`Disconf-Client`](https://github.com/knightliao/disconf/wiki/%E9%85%8D%E7%BD%AE%E8%AF%B4%E6%98%8E). 70 | 71 | *Arguments* 72 | 73 | * file {*Object*} - The file of the _option_. Currently available options are: 74 | 75 | * `path` {*String*} - The directory path of _option_ file. 76 | * `filename` {*String*} - The name of _option_ file, defaults to `disconf.properties`. 77 | 78 | 79 | * option {*Object*} - The client's own configuration file. Currently available options are: 80 | 81 | * `dist_file`: {*String*} - 获取的配置写入的目标文件路径. 82 | * `conf_file_name`: {*String*} - 需要远程获取的配置文件,用逗号分隔,例如: demo.properties,system.properties 83 | * `conf_item_name`: {*String*} - 需要远程获取的配置项,用逗号分隔,例如: test,demo 84 | 85 | _以下配置同 java 客户端一致:_ 86 | * `enable: {remote: {conf: true}}`: {*Boolean*} - 是否使用远程配置文件,true(默认)会从远程获取配置 false则直接获取本地配置 87 | * `conf_server_host`: {*String*} - 配置服务器的 HOST,用逗号分隔 127.0.0.1:8000,127.0.0.1:8000 88 | * `app`: {*String*} - APP,请采用 产品线_服务名 格式 89 | * `version`: {*String*} - 版本,请采用 X_X_X_X 格式 90 | * `env`: {*String*} - 部署环境 91 | * `debug`: {*Boolean*} - 调试 92 | * `ignore`: {*String*} - 忽略哪些分布式配置,用逗号分隔 93 | * `conf_server_url_retry_times`: {*Number*} - 获取远程配置 重试次数,默认是3次 94 | * `conf_server_url_retry_sleep_seconds`: {*Number*} - 获取远程配置 重试时休眠时间,默认是5秒 95 | * `user_define_download_dir`: {*String*} - 用户定义的下载文件夹, 远程文件下载后会放在这里。注意,此文件夹必须有有权限,否则无法下载到这里 96 | 97 | 98 | * callback(err, zk) {*Function*} - The callback function. 99 | 100 | * `err` {*Error*} - Error during initialization. 101 | * `zk` {*{}*} - The [node-zookeeper-client](https://github.com/alexguan/node-zookeeper-client) instance. 102 | 103 | *Example* 104 | 105 | ```javascript 106 | var disconf = require('node-disconf-client'); 107 | disconf.init({ 108 | path: './config', 109 | filename: 'disconf.properties' 110 | }, { 111 | dist_file: './config/remote.properties', 112 | user_define_download_dir: './config/download'), 113 | conf_item_name: 'demo', 114 | conf_server_host: '127.0.0.1:8000', 115 | app: 'DEFAULT_APP', 116 | version: 'DEFAULT_VERSION', 117 | env: 'DEFAULT_ENV', 118 | }); 119 | ``` 120 | 121 | ---- 122 | 123 | ### Event 124 | 125 | Optionally, you can register watcher functions after calling 126 | [`init`](#initfile-option-callback) methods. 127 | 128 | #### `ready` 129 | 130 | After the first time to downloaded all remote configuration, this event will be emit. It will return the data that contains all the configuration. 131 | 132 | *Callback* 133 | 134 | * data {*Object*} - All the configuration. 135 | 136 | *Example* 137 | 138 | ```javascript 139 | disconf.on('ready', function (data) { 140 | console.log('ready:', data); 141 | }); 142 | ``` 143 | 144 | #### `change` 145 | 146 | At any time when remote configuration changes, this event will be emit. It will return the event and the data that contains all the configuration. 147 | 148 | *Callback* 149 | 150 | * event {*Event*} - It is a event instance form [node-zookeeper-client](https://github.com/alexguan/node-zookeeper-client#event). 151 | * data {*Object*} - All the configuration after the update. 152 | 153 | *Example* 154 | 155 | ```javascript 156 | disconf.on('change', function (event, data) { 157 | console.log('change:', event.name, data); 158 | }); 159 | ``` 160 | 161 | #### `error` 162 | 163 | At any time when a error appears, this event will be emit. 164 | 165 | *Callback* 166 | 167 | * err {*Error*} - Any possible error. 168 | 169 | *Example* 170 | 171 | ```javascript 172 | disconf.on('error', function (err) { 173 | console.log('error:', err.stack); 174 | }); 175 | ``` 176 | 177 | ---- 178 | 179 | ### Utility 180 | 181 | It provides utility tools include in `util`. 182 | 183 | #### reloadConfig(void) 184 | 185 | This function can reload the [`config`](https://github.com/lorenwest/node-config) module, make the changes of the configuration files to take effect. The function returns the [`config`](https://github.com/lorenwest/node-config) instance. But you can still perform again `require` to get the [`config`](https://github.com/lorenwest/node-config) instance. 186 | 187 | Before using this function you must have installed the [`config`](https://github.com/lorenwest/node-config) module. Such as like the following: 188 | 189 | ``` 190 | $ npm install config 191 | ``` 192 | 193 | *Example* 194 | 195 | ```javascript 196 | var disconf = require('node-disconf-client'); 197 | var conf = disconf.util.reloadConfig(); 198 | ``` 199 | 200 | ---- 201 | 202 | ### Todo list 203 | 204 | - [x] 启动时从disconf服务远程获取配置并写入配置文件,建议以config模块要求的规则命名{full_hostname}.EXT 205 | - [x] 监控zookeeper节点,实时将远程配置文件修改同步到本地磁盘文件,提供远程配置修改事件供外部监听 206 | - [x] 建立临时zookeeper节点,上传配置至远程服务器进行校验 207 | - [x] 只在磁盘上更新配置文件,不修改config模块加载的内容,需要另外重新加载config模块以更新磁盘上配置文件的修改 208 | - [x] 可在pm2启动时监听配置文件夹,配置文件被修改时重启服务。以实现远程修改配置文件,服务自动重启 209 | 210 | ### Change List 211 | 212 | - disconf 配置文件增加`.js`和`json`后缀类型文件支持,值类型为列表的配置项同时支持`json`数组和以`,`逗号分隔的字符串 213 | - 修改 disconf 配置文件的配置优先级大于初始化时指定的配置 214 | 215 | ## License 216 | 217 | Licensed under the 218 | [MIT](http://opensource.org/licenses/MIT) 219 | license. 220 | -------------------------------------------------------------------------------- /config/disconf.properties: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # Disconf 本地配置 4 | # 5 | 6 | ##### 区别java客户端特有的配置 ##### 7 | 8 | # 获取的配置写入的目标文件路径 9 | # 10 | # dist_file=./config/default.properties 11 | 12 | # 需要远程获取的配置文件,用逗号分隔 13 | # 14 | conf_file_name=demo.properties,system.properties 15 | 16 | # 需要远程获取的配置项,用逗号分隔 17 | # 18 | conf_item_name=test 19 | 20 | ##### 以下配置同java客户端一致 ##### 21 | 22 | # 是否使用远程配置文件 23 | # true(默认)会从远程获取配置 false则直接获取本地配置 24 | # 25 | enable.remote.conf=true 26 | 27 | # 配置服务器的 HOST,用逗号分隔 127.0.0.1:8000,127.0.0.1:8000 28 | # 29 | conf_server_host=127.0.0.1:2181 30 | 31 | # APP 请采用 产品线_服务名 格式 32 | # 33 | app=demo 34 | 35 | # 版本, 请采用 X_X_X_X 格式 36 | # 37 | version=1_0_0 38 | 39 | # 环境 40 | # 41 | env=test 42 | 43 | # debug 44 | # 45 | debug=false 46 | 47 | # 忽略哪些分布式配置,用逗号分隔 48 | # 49 | ignore=test,system.properties 50 | 51 | # 获取远程配置 重试次数,默认是3次 52 | # 53 | conf_server_url_retry_times=3 54 | 55 | # 获取远程配置 重试时休眠时间,默认是5秒 56 | # 57 | conf_server_url_retry_sleep_seconds=1 58 | 59 | # 用户定义的下载文件夹, 远程文件下载后会放在这里。注意,此文件夹必须有有权限,否则无法下载到这里 60 | # 61 | # user_define_download_dir=./config/download 62 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Corey600 on 2016/8/1. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var fs = require('fs'); 8 | var sysPath = require('path'); 9 | var util = require('util'); 10 | var EventEmitter = require('events').EventEmitter; 11 | 12 | var _ = require('lodash'); 13 | var zookeeper = require('node-zookeeper-client'); 14 | var properties = require('properties'); 15 | 16 | var HttpClient = require('./lib/HttpClient'); 17 | var ConfNode = require('./lib/ConfNode'); 18 | var write = require('./lib/utils/write'); 19 | var log = require('./lib/utils/log')('index'); 20 | 21 | /** 22 | * Session timeout in milliseconds, 23 | * defaults to 10 seconds. 24 | * 25 | * @type {number} 26 | */ 27 | var ZK_SESSION_TIMEOUT = 10000; 28 | 29 | /** 30 | * The delay (in milliseconds) between each connection attempts, 31 | * defaults to 20 seconds. 32 | * 33 | * @type {number} 34 | */ 35 | var ZK_SPIN_DELAY = 2000; 36 | 37 | /** 38 | * The number of retry attempts for connection loss exception, 39 | * defaults to 3. 40 | * 41 | * @type {number} 42 | */ 43 | var ZK_RETRIES = 3; 44 | 45 | /** 46 | * @constructor 47 | */ 48 | function DisconfClient() { 49 | var defaultPath = sysPath.join(process.cwd(), 'config'); 50 | 51 | /** 52 | * 客户端配置文件路径,文件内容为 this._option 的配置内容 53 | * @type {{path, name: string}} 54 | * @private 55 | */ 56 | this._file = { 57 | path: defaultPath, 58 | name: 'disconf.properties' 59 | }; 60 | 61 | /** 62 | * 客户端配置默认值 63 | * @type {*} 64 | * @private 65 | */ 66 | this._option = { 67 | 68 | /*----- 区别java客户端特有的配置 -----*/ 69 | 70 | // 获取的配置写入的目标文件路径 71 | dist_file: sysPath.join(defaultPath, 'remote.properties'), 72 | 73 | // 需要远程获取的配置文件,用逗号分隔 test,demo.properties,system.properties 74 | conf_file_name: '', 75 | 76 | // 需要远程获取的配置项,用逗号分隔 test,demo.properties,system.properties 77 | conf_item_name: '', 78 | 79 | /*----- 以下配置同java客户端一致 -----*/ 80 | 81 | // 是否使用远程配置文件 82 | // true(默认)会从远程获取配置 false则直接获取本地配置 83 | enable: {remote: {conf: true}}, 84 | 85 | // 配置服务器的 HOST,用逗号分隔 127.0.0.1:8000,127.0.0.1:8000 86 | conf_server_host: '', 87 | 88 | // APP 请采用 产品线_服务名 格式 89 | app: 'DEFAULT_APP', 90 | 91 | // 版本, 请采用 X_X_X_X 格式 92 | version: 'DEFAULT_VERSION', 93 | 94 | // 部署环境 95 | env: 'DEFAULT_ENV', 96 | 97 | // debug 98 | debug: false, 99 | 100 | // 忽略哪些分布式配置,用逗号分隔 101 | ignore: '', 102 | 103 | // 获取远程配置 重试次数,默认是3次 104 | conf_server_url_retry_times: 3, 105 | 106 | // 获取远程配置 重试时休眠时间,默认是5秒 107 | conf_server_url_retry_sleep_seconds: 5, 108 | 109 | // 用户定义的下载文件夹, 远程文件下载后会放在这里。注意,此文件夹必须有有权限,否则无法下载到这里 110 | user_define_download_dir: sysPath.join(defaultPath, 'download') 111 | }; 112 | 113 | /** 114 | * http 客户端实例 115 | * @type {HttpClient|null} 116 | * @private 117 | */ 118 | this._http = null; 119 | 120 | /** 121 | * zookeeper 客户端实例 122 | * @type {*|null} 123 | * @private 124 | */ 125 | this._zk = null; 126 | } 127 | 128 | util.inherits(DisconfClient, EventEmitter); 129 | 130 | /** 131 | * Initialize the instance. 132 | * 133 | * @param {{path: string, name: string}} file 134 | * @param {*} option 135 | * @param {Function} callback 136 | * @returns {DisconfClient} 137 | * @public 138 | */ 139 | DisconfClient.prototype.init = function (file, option, callback) { 140 | var self = this; 141 | if (!callback || !_.isFunction(callback)) callback = function () { 142 | }; 143 | file = file || {}; 144 | this._file = _.defaults(file, this._file); 145 | 146 | var _path = sysPath.join(file.path, file.name); 147 | log.info('the DisconfClient\'s option file path: ' + _path); 148 | 149 | option = option || {}; 150 | // If the DisconfClient\'s option file can be required. 151 | var extname = sysPath.extname(_path); 152 | if('.js' === extname || '.json' === extname){ 153 | var data = {}; 154 | try { 155 | data = require(_path); 156 | }catch (e){ 157 | log.warn('the DisconfClient\'s option file is not exist. err: ', e.stack); 158 | } 159 | // file has the highest priority 160 | self._option = _.defaults(data, option, self._option); 161 | log.info('the DisconfClient\'s option content: ' + JSON.stringify(self._option)); 162 | self._getZkInfo(callback); 163 | return this; 164 | } 165 | // If the DisconfClient\'s option file is exist. 166 | fs.access(_path, fs.F_OK, function (err) { 167 | if (!err) { 168 | // Read DisconfClient\'s option file. 169 | fs.readFile(_path, function (err, data) { 170 | if (err) return callback(err); 171 | try { 172 | data = properties.parse(data.toString(), {namespaces: true, variables: true, sections: true}); 173 | } catch (e) { 174 | return callback(e); 175 | } 176 | // file has the highest priority 177 | self._option = _.defaults(data, option, self._option); 178 | log.info('the DisconfClient\'s option content: ' + JSON.stringify(self._option)); 179 | self._getZkInfo(callback); 180 | }); 181 | } else { 182 | log.warn('the DisconfClient\'s option file is not exist. err: ', err.stack); 183 | self._option = _.defaults(option, self._option); 184 | log.info('the DisconfClient\'s option content: ' + JSON.stringify(self._option)); 185 | self._getZkInfo(callback); 186 | } 187 | }); 188 | return this; 189 | }; 190 | 191 | /** 192 | * Get zookeeper server info. 193 | * 194 | * @param {Function} callback 195 | * @returns {DisconfClient} 196 | * @private 197 | */ 198 | DisconfClient.prototype._getZkInfo = function (callback) { 199 | var self = this; 200 | var http = self._http = new HttpClient(self._option); 201 | http.getZkInfo(function (err, host, prefix) { 202 | if (err) return callback(err); 203 | try { 204 | log.info('zk host: %s, prefix: %s', host, prefix); 205 | var zk = self._zk = zookeeper.createClient(host, { 206 | sessionTimeout: ZK_SESSION_TIMEOUT, 207 | spinDelay: ZK_SPIN_DELAY, 208 | retries: ZK_RETRIES 209 | }); 210 | zk.connect(); 211 | if (self._option.enable.remote.conf) { 212 | // 远程配置开启 213 | self._initConfigNodes(prefix); 214 | } else { 215 | // 远程配置关闭,直接准备事件 216 | self.emit('ready', null); 217 | } 218 | callback(null, zk); 219 | } catch (e) { 220 | callback(e); 221 | } 222 | }); 223 | return this; 224 | }; 225 | 226 | /** 227 | * Initialize the config file/item nodes. 228 | * 229 | * @param {string} prefix 230 | * @returns {DisconfClient} 231 | * @private 232 | */ 233 | DisconfClient.prototype._initConfigNodes = function (prefix) { 234 | var self = this; 235 | 236 | // get the ignore config list 237 | var ignores = self._option.ignore; 238 | if (!_.isNil(ignores) && _.isString(ignores)) { 239 | ignores = ignores.split(','); 240 | } else { 241 | ignores = []; 242 | } 243 | 244 | // get ConfNode list 245 | var nodeList = []; 246 | var types = ['file', 'item']; 247 | types.forEach(function (type) { 248 | var names = self._option['conf_' + type + '_name']; 249 | if (_.isString(names)) { 250 | names = names.split(','); 251 | } 252 | if (!_.isArray(names) || names.length <= 0) { 253 | return; 254 | } 255 | names.forEach(function (name) { 256 | if (name == '' || ignores.indexOf(name) >= 0) return; 257 | nodeList.push(new ConfNode(type, name, prefix, self)); 258 | }); 259 | }); 260 | 261 | // watch every node 262 | var readyNum = 0; 263 | var allConfig = {}; 264 | nodeList.forEach(function (node) { 265 | node.nodeWatch(function (err, event, data) { 266 | // 监听 267 | if (err) { 268 | log.error('zookeeper watch event error. event: %s, error: %', event.name, err.stack); 269 | self.emit('error', err); 270 | return; 271 | } 272 | allConfig = _.defaultsDeep(data, allConfig); 273 | write(self._option.dist_file, properties.stringify(allConfig), function (err) { 274 | if (err) { 275 | log.error('write all config error. event: %s, error: %', event.name, err.stack); 276 | self.emit('error', err); 277 | } else { 278 | self.emit('change', event, {key: node.getKey(), data: data}); 279 | } 280 | }); 281 | }, function (err, data) { 282 | // 回调 283 | readyNum++; 284 | if (err) { 285 | self.emit('error', err); 286 | return; 287 | } 288 | allConfig = _.defaultsDeep(data, allConfig); 289 | if (readyNum >= nodeList.length) { 290 | write(self._option.dist_file, properties.stringify(allConfig), function (err) { 291 | if (err) { 292 | self.emit('error', err); 293 | } else { 294 | self.emit('ready', allConfig); 295 | } 296 | }); 297 | } 298 | }); 299 | }); 300 | return this; 301 | }; 302 | 303 | 304 | /** 305 | * Utilities are under the util namespace. 306 | * 307 | * @type {*} 308 | * @public 309 | */ 310 | var _util = DisconfClient.prototype.util = {}; 311 | 312 | /** 313 | * Reload 'config' module. 314 | * 315 | * @returns {Config} 316 | * @public 317 | */ 318 | _util.reloadConfig = function () { 319 | var config = require('config'); 320 | //noinspection JSCheckFunctionSignatures 321 | var dir = config.util.getEnv('NODE_CONFIG_DIR'); 322 | for (var key in require.cache) { 323 | //noinspection JSUnfilteredForInLoop 324 | var fileName = key; 325 | //noinspection JSCheckFunctionSignatures 326 | if (-1 === fileName.indexOf(dir)) { 327 | continue; 328 | } 329 | delete require.cache[fileName]; 330 | } 331 | delete require.cache[require.resolve('config')]; 332 | return require('config'); 333 | }; 334 | 335 | /** 336 | * @type {DisconfClient} 337 | */ 338 | module.exports = new DisconfClient(); 339 | -------------------------------------------------------------------------------- /lib/ConfNode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Corey600 on 2016/8/9. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var os = require('os'); 8 | var sysPath = require('path'); 9 | var uuid = require('uuid'); 10 | var zookeeper = require('node-zookeeper-client'); 11 | var properties = require('properties'); 12 | var write = require('./utils/write'); 13 | var log = require('./utils/log')('ConfigNode'); 14 | 15 | /** 16 | * 主机名称 17 | * 18 | * @type {string} 19 | */ 20 | var HOSTNAME = os.hostname(); 21 | 22 | /** 23 | * @param {string} type 24 | * @param {string} key 25 | * @param {string} prefix 26 | * @param {DisconfClient} client 27 | * @constructor 28 | */ 29 | function ConfigNode(type, key, prefix, client) { 30 | /** 31 | * 配置类型 'item' or 'file'. 32 | * @type {string} 33 | * @private 34 | */ 35 | this._type = type; 36 | 37 | /** 38 | * 配置节点名称 39 | * @type {string} 40 | * @private 41 | */ 42 | this._key = key; 43 | 44 | /** 45 | * http客户端 46 | * @type {HttpClient} 47 | * @private 48 | */ 49 | this._http = client._http; 50 | 51 | /** 52 | * node-zookeeper-client 的实例 53 | * @type {*} 54 | * @private 55 | */ 56 | this._zk = client._zk; 57 | 58 | /** 59 | * 选项 60 | * @type {*} 61 | * @private 62 | */ 63 | this._option = client._option; 64 | 65 | /** 66 | * 配置(持久)节点的路径 67 | * @type {string} 68 | * @private 69 | */ 70 | this._path = this._buildPath(prefix); 71 | 72 | /** 73 | * 创建临时节点传入的名称 74 | * @type {string} 75 | * @private 76 | */ 77 | this._tempPath = this._path + '/' + HOSTNAME + '_' + process.pid + '_' + uuid.v4().replace(/-/g, ''); 78 | 79 | /** 80 | * 临时节点创建之后真实的名称 81 | * @type {string|null} 82 | * @private 83 | */ 84 | this._realPath = null; 85 | } 86 | 87 | /** 88 | * Build the path string. 89 | * 90 | * @param {string} prefix 91 | * @returns {string} 92 | * @private 93 | */ 94 | ConfigNode.prototype._buildPath = function (prefix) { 95 | var option = this._option; 96 | return (prefix 97 | + '/' + option.app + '_' + option.version + '_' + option.env 98 | + '/' + this._type + '/' + this._key); 99 | }; 100 | 101 | /** 102 | * Get the key. 103 | * 104 | * @returns {string} 105 | * @public 106 | */ 107 | ConfigNode.prototype.getKey = function () { 108 | return this._key; 109 | }; 110 | 111 | /** 112 | * Watch the event of configuration node. 113 | * 114 | * @param {Function} watcher 115 | * @param {Function} callback 116 | * @returns {ConfigNode} 117 | * @public 118 | */ 119 | ConfigNode.prototype.nodeWatch = function (watcher, callback) { 120 | var self = this; 121 | self._createPersistentNode(self._path, HOSTNAME, function (err, path) { 122 | if (err) return callback(err); 123 | self._doWatch(path, null, watcher, callback); 124 | }); 125 | return this; 126 | }; 127 | 128 | /** 129 | * Do watch the event of configuration node. 130 | * 131 | * @param {string} path 132 | * @param {Event} event 133 | * @param {Function} watcher 134 | * @param {Function} callback 135 | * @private 136 | */ 137 | ConfigNode.prototype._doWatch = function (path, event, watcher, callback) { 138 | var self = this; 139 | self._zk.getData(path, 140 | function (event) { 141 | self._doWatch(path, event, watcher, callback); 142 | }, 143 | function (err/*, data, stat*/) { 144 | if (err) return callback(err); 145 | self._getData(function (err, data) { 146 | if(event){ 147 | watcher(err, event, data); 148 | }else{ 149 | callback(err, data); 150 | } 151 | }); 152 | } 153 | ); 154 | }; 155 | 156 | /** 157 | * Get the configuration data from server. 158 | * 159 | * @param {Function} callback 160 | * @returns {ConfigNode} 161 | * @private 162 | */ 163 | ConfigNode.prototype._getData = function (callback) { 164 | var self = this; 165 | self._http.getConfig(self._type, self._key, function (err, data) { 166 | if (err) return callback(err); 167 | try { 168 | // to deal with the configuration item 169 | if (self._type === 'item') { 170 | data = data.value; 171 | } 172 | 173 | // save the configuration data to a file 174 | write( 175 | sysPath.join(self._option.user_define_download_dir, self._key), 176 | data, 177 | function (err) { 178 | if (err) { 179 | log.warn('save the configuration data to a file error. key: %s, error: %s', self._key, err.stack); 180 | } else { 181 | log.info('save the configuration data to a file success. key: %s', self._key); 182 | } 183 | } 184 | ); 185 | 186 | // parse the configuration data 187 | if (self._type === 'file') { 188 | // file 189 | data = properties.parse(data); 190 | for(var item in data){ 191 | if(data.hasOwnProperty(item) && data[item] === null){ 192 | log.warn('item [%s] of config file [%s] is null, now transform to empty string.', item, self._key); 193 | data[item] = ''; 194 | } 195 | } 196 | self._pushData(JSON.stringify(data)); 197 | } else { 198 | // item 199 | self._pushData(data); 200 | data = properties.parse(self._key + '=' + data); 201 | } 202 | } catch (e) { 203 | return callback(e); 204 | } 205 | return callback(null, data); 206 | }); 207 | return this; 208 | }; 209 | 210 | /** 211 | * Returns the configuration data for the server check. 212 | * 213 | * @param {*} data 214 | * @returns {ConfigNode} 215 | * @private 216 | */ 217 | ConfigNode.prototype._pushData = function (data) { 218 | var self = this; 219 | var ephemeralHandle = function (err, path) { 220 | if (err) { 221 | log.warn('create ephemeral node error. path: %s, error: %s', path, err.stack); 222 | } else { 223 | log.info('push config data success. path: %s', path); 224 | } 225 | }; 226 | var _path = self._realPath; 227 | if (_path === null) { 228 | // 临时节点还未创建则创建 229 | self._createEphemeralNode(self._tempPath, data, ephemeralHandle); 230 | return this; 231 | } 232 | self._zk.exists(_path, function (err, stat) { 233 | if (err) { 234 | log.warn('push config data error. path: %s, error: %s', _path, err.stack); 235 | return; 236 | } 237 | if (stat) { 238 | self._zk.setData( 239 | _path, 240 | new Buffer(data), 241 | function (err, stat) { 242 | if (err) { 243 | log.warn('push config data error. path: %s, error: %s', _path, err.stack); 244 | } else { 245 | log.info('push config data success. stat: %s', JSON.stringify(stat)); 246 | } 247 | } 248 | ); 249 | } else { 250 | // 临时节点还未创建则创建 251 | self._createEphemeralNode(self._tempPath, data, ephemeralHandle); 252 | } 253 | }); 254 | return this; 255 | }; 256 | 257 | /** 258 | * 创建持久节点,父路径不存在则会主动创建 259 | * 如果原本就存在则回调传入的路径,如果是新创建的则回调新创建的真实路径 260 | * 261 | * @param {string} path 262 | * @param {*} data 263 | * @param {Function} callback 264 | * @returns {ConfigNode} 265 | * @private 266 | */ 267 | ConfigNode.prototype._createPersistentNode = function (path, data, callback) { 268 | var self = this; 269 | self._zk.exists(path, function (err, stat) { 270 | if (!err && !stat) { 271 | log.info('persistent node does need to create. path: %s', path); 272 | self._zk.mkdirp( 273 | path, 274 | new Buffer(data), 275 | zookeeper.CreateMode.PERSISTENT, 276 | callback 277 | ); 278 | } else { 279 | callback(err, path); 280 | } 281 | }); 282 | return this; 283 | }; 284 | 285 | /** 286 | * 创建临时节点,父路径不存在不会主动创建,而会抛错 287 | * 如果原本就存在则回调传入的路径,如果是新创建的则回调新创建的真实路径 288 | * 289 | * @param {string} path 290 | * @param {*} data 291 | * @param {Function} callback 292 | * @returns {ConfigNode} 293 | * @private 294 | */ 295 | ConfigNode.prototype._createEphemeralNode = function (path, data, callback) { 296 | var self = this; 297 | self._zk.exists(path, function (err, stat) { 298 | if (!err && !stat) { 299 | log.info('ephemeral node does need to create. path: %s', path); 300 | self._zk.create( 301 | path, 302 | new Buffer(data), 303 | zookeeper.CreateMode.EPHEMERAL_SEQUENTIAL, 304 | function (errr, path) { 305 | // 更新临时节点真实名称 306 | self._realPath = path; 307 | callback(err, path); 308 | } 309 | ); 310 | } else { 311 | callback(err, path); 312 | } 313 | }); 314 | return this; 315 | }; 316 | 317 | /** 318 | * @type {Function} 319 | */ 320 | module.exports = ConfigNode; 321 | -------------------------------------------------------------------------------- /lib/HttpClient.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Corey600 on 2016/8/2. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var _ = require('lodash'); 8 | var request = require('request'); 9 | var log = require('./utils/log')('HttpClient'); 10 | 11 | /** 12 | * 请求协议 13 | * 14 | * @type {string} 15 | */ 16 | var PROTOCOL = 'http://'; 17 | 18 | /** 19 | * 获取zookeeper信息接口 20 | * 21 | * @type {{HOSTS: string, PREFIX: string}} 22 | */ 23 | var ZOO_PATH = { 24 | HOSTS: '/api/zoo/hosts', 25 | PREFIX: '/api/zoo/prefix' 26 | }; 27 | 28 | /** 29 | * 获取配置信息接口 30 | * 31 | * @type {{ITEM: string, FILE: string}} 32 | */ 33 | var CONFIG_PATH = { 34 | ITEM: '/api/config/item', 35 | FILE: '/api/config/file' 36 | }; 37 | 38 | /** 39 | * 构造函数 40 | * 41 | * @param {*} option 42 | * @constructor 43 | */ 44 | function HttpClient(option) { 45 | /** 46 | * APP 47 | * @type {string} 48 | * @private 49 | */ 50 | this._app = option.app; 51 | 52 | /** 53 | * 版本 54 | * @type {string} 55 | * @private 56 | */ 57 | this._version = option.version; 58 | 59 | /** 60 | * 部署环境 61 | * @type {string} 62 | * @private 63 | */ 64 | this._env = option.env; 65 | 66 | /** 67 | * 重试次数 68 | * @type {number} 69 | * @private 70 | */ 71 | this._retryTimes = option.conf_server_url_retry_times; 72 | 73 | /** 74 | * 重试间隔时间,单位 秒/s 75 | * @type {number} 76 | * @private 77 | */ 78 | this._retrySleepSeconds = option.conf_server_url_retry_sleep_seconds; 79 | 80 | var hosts = option.conf_server_host || ''; 81 | var list = hosts.split(','); // 传进来的字符串用逗号分隔多个地址 82 | /** 83 | * 配置服务器的 HOST 列表 84 | * @type {Array.} 85 | * @private 86 | */ 87 | this._host = list 88 | .filter(function (item) { 89 | return item != ''; 90 | }) 91 | .map(function (item) { 92 | if (item.indexOf(PROTOCOL) >= 0) { 93 | return item; 94 | } 95 | return PROTOCOL + item; 96 | }); 97 | } 98 | 99 | /** 100 | * 发起 http 请求,如果失败则重试或尝试下一个地址 101 | * 102 | * @param {number} hostIndex 103 | * @param {number} retryTimes 104 | * @param {string} path 105 | * @param {Function} cb 106 | * @returns {HttpClient} 107 | * @private 108 | */ 109 | HttpClient.prototype._doRequest = function (hostIndex, retryTimes, path, cb) { 110 | var self = this; 111 | if (!self._host || hostIndex >= self._host.length) { 112 | // 全部地址都尝试请求后才提示失败,需保证异步化 113 | process.nextTick(function () { 114 | cb(new Error('Http request failed! path: ' + path)); 115 | }); 116 | return this; 117 | } 118 | 119 | var url = self._host[hostIndex] + path; 120 | request(url, function (err, response, body) { 121 | if (err || response.statusCode != 200) { 122 | log.warn('One request failed! url: %s, err/res: %s', url, err ? err.stack : response.statusCode); 123 | setTimeout(function () { 124 | if (retryTimes < self._retryTimes) { 125 | self._doRequest(hostIndex, retryTimes + 1, path, cb); 126 | } else { 127 | self._doRequest(hostIndex + 1, 0, path, cb); 128 | } 129 | }, self._retrySleepSeconds * 1000); 130 | } else { 131 | // 成功则解析并回调数据 132 | try { 133 | if (_.isString(body)) { 134 | body = JSON.parse(body); 135 | } 136 | } catch (e) { 137 | // 解析出错就直接返回原始数据,这里不抛出错误,只打印提示 138 | log.info('parse request body is skipped. path: %s', path); 139 | } 140 | cb(null, body); 141 | } 142 | }); 143 | return this; 144 | }; 145 | 146 | /** 147 | * 执行 http 请求 148 | * 149 | * @param {string} path 150 | * @param {Function} cb 151 | * @returns {HttpClient} 152 | * @private 153 | */ 154 | HttpClient.prototype._request = function (path, cb) { 155 | return this._doRequest(0, 0, path, cb); 156 | }; 157 | 158 | /** 159 | * 获取 zookeeper 信息 160 | * 161 | * @param {Function} cb 162 | * @returns {HttpClient} 163 | * @public 164 | */ 165 | HttpClient.prototype.getZkInfo = function (cb) { 166 | var self = this; 167 | self._request(ZOO_PATH.HOSTS, function (hostsErr, body) { 168 | if (hostsErr) { 169 | return cb(hostsErr); 170 | } 171 | var zooHosts = body.value; 172 | self._request(ZOO_PATH.PREFIX, function (prefixErr, body) { 173 | if (prefixErr) { 174 | return cb(prefixErr); 175 | } 176 | var zooPrefix = body.value; 177 | cb(null, zooHosts, zooPrefix); 178 | }); 179 | }); 180 | return this; 181 | }; 182 | 183 | /** 184 | * 获取配置 185 | * 186 | * @param {string} type 187 | * @param {string} name 188 | * @param {Function} cb 189 | * @returns {HttpClient} 190 | * @public 191 | */ 192 | HttpClient.prototype.getConfig = function (type, name, cb) { 193 | var _path = (type === 'item' ? CONFIG_PATH.ITEM : CONFIG_PATH.FILE); 194 | _path += ('?app=' + this._app + '&'); 195 | _path += ('version=' + this._version + '&'); 196 | _path += ('env=' + this._env + '&'); 197 | _path += ('key=' + name); 198 | this._request(_path, cb); 199 | return this; 200 | }; 201 | 202 | /** 203 | * 导出 204 | * 205 | * @type {Function} 206 | */ 207 | module.exports = HttpClient; 208 | -------------------------------------------------------------------------------- /lib/utils/log.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Corey600 on 2016/8/10. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var debug = require('debug'); 8 | 9 | /** 10 | * Prefix string. 11 | * 12 | * @type {string} 13 | */ 14 | var PREFIX = 'Disconf'; 15 | 16 | /** 17 | * Get log instance. 18 | * 19 | * @param {string} tag 20 | * @returns {{info: Function, warn: Function, error: Function}} 21 | */ 22 | module.exports = function (tag) { 23 | return { 24 | info: debug(PREFIX + ':info [@' + tag + ']'), 25 | warn: debug(PREFIX + ':warn [@' + tag + ']'), 26 | error: debug(PREFIX + ':error [@' + tag + ']') 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /lib/utils/write.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Corey600 on 2016/8/11. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var fs = require('fs'); 8 | var path = require('path'); 9 | 10 | /** 11 | * 递归创建目录 12 | * 13 | * @param {string} dirpath 14 | * @param {number|null} mode 15 | * @param {Function} callback 16 | * @private 17 | */ 18 | var _mkdir = function (dirpath, mode, callback) { 19 | fs.access(dirpath, fs.F_OK, function (err) { 20 | if (err) { 21 | var parent = path.dirname(dirpath); 22 | _mkdir(parent, mode, function (/*err*/) { 23 | // 忽略错误,如果错误真的影响文件的创建,会在文件写时抛出错误 24 | // if (err) return callback(err); 25 | fs.mkdir(dirpath, mode, callback); 26 | }); 27 | } else { 28 | callback(null); 29 | } 30 | }); 31 | }; 32 | 33 | /** 34 | * 写入并覆盖到文件,如果目录不存在则创建 35 | * 36 | * @param {string} filepath 37 | * @param {*} data 38 | * @param {Function} callback 39 | * @public 40 | */ 41 | module.exports = function (filepath, data, callback) { 42 | //noinspection JSUnusedLocalSymbols 43 | _mkdir(path.dirname(filepath), null, function (err) { 44 | // 忽略错误,如果错误真的影响文件的创建,会在文件写时抛出错误 45 | // if (err) return callback(err); 46 | fs.writeFile(filepath, data, function (err) { 47 | if (err) return callback(err); 48 | callback(null, filepath); 49 | }); 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-disconf-client", 3 | "version": "0.1.5", 4 | "description": "A Javascript module for Node.js to connect Disconf service.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha --colors test/*.js", 8 | "coverage": "cat ./coverage/lcov.info | coveralls" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/Corey600/node-disconf-client.git" 13 | }, 14 | "keywords": [ 15 | "nodejs", 16 | "config", 17 | "disconf", 18 | "zookeeper" 19 | ], 20 | "author": "Corey600", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/Corey600/node-disconf-client/issues" 24 | }, 25 | "homepage": "https://github.com/Corey600/node-disconf-client#readme", 26 | "dependencies": { 27 | "debug": "^2.2.0", 28 | "lodash": "^4.14.1", 29 | "node-zookeeper-client": "^0.2.2", 30 | "properties": "^1.2.1", 31 | "request": "^2.74.0", 32 | "uuid": "^3.0.0" 33 | }, 34 | "devDependencies": { 35 | "config": "^1.21.0", 36 | "coveralls": "^2.11.9", 37 | "expect.js": "^0.3.1", 38 | "istanbul": "^0.4.3", 39 | "mm": "1.5.0", 40 | "mocha": "^2.5.3" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/lib/request.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by feichenxi on 2016/11/29. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var url = require('url'); 8 | var _ = require('lodash'); 9 | 10 | function Request(options) { 11 | options = options || {}; 12 | function callback() { 13 | var args = Array.prototype.slice.call(arguments); 14 | if(_.isFunction(options.callback)) 15 | setTimeout(function(){ 16 | options.callback.apply(this, args); 17 | }, 0); 18 | } 19 | function returnConf(key) { 20 | switch(key){ 21 | case 'demo.properties': 22 | callback(false, { 23 | statusCode: 200 24 | }, 'name=demo\nversion=v1.0.0\n'); 25 | break; 26 | case 'test': 27 | callback(false, { 28 | statusCode: 200 29 | }, 'hello'); 30 | break; 31 | default: 32 | callback(new Error('config key is error!')); 33 | break; 34 | } 35 | } 36 | var data = url.parse(options.uri, true); 37 | switch (data.pathname){ 38 | case '/api/zoo/hosts': 39 | callback(false, { 40 | statusCode: 200 41 | }, { 42 | status: 1, 43 | message: '', 44 | value: '127.0.0.1:4180,127.0.0.1:4181,127.0.0.1:4182' 45 | }); 46 | break; 47 | case '/api/zoo/prefix': 48 | callback(false, { 49 | statusCode: 200 50 | }, { 51 | status: 1, 52 | message: '', 53 | value: '/disconf' 54 | }); 55 | break; 56 | case '/api/config/file': 57 | returnConf(data.query.key); 58 | break; 59 | case '/api/config/item': 60 | returnConf(data.key); 61 | break; 62 | default: 63 | callback(new Error('uri is error!')); 64 | break; 65 | } 66 | } 67 | 68 | module.exports = Request; 69 | -------------------------------------------------------------------------------- /test/lib/zookeeper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Corey600 on 2016/6/18. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | function MyZookeeper() { 8 | } 9 | 10 | MyZookeeper.prototype.connect = function(){ 11 | }; 12 | 13 | MyZookeeper.prototype.exists = function(path, cb){ 14 | setTimeout(function(){ 15 | cb && cb(false, true); 16 | }, 10); 17 | }; 18 | 19 | MyZookeeper.prototype.create = function(path, data, mode, cb){ 20 | setTimeout(function(){ 21 | cb && cb(false, path); 22 | }, 10); 23 | }; 24 | 25 | MyZookeeper.prototype.mkdirp = function(path, data, mode, cb){ 26 | setTimeout(function(){ 27 | cb && cb(false, path); 28 | }, 10); 29 | }; 30 | 31 | MyZookeeper.prototype.setData = function(path, data, cb){ 32 | setTimeout(function(){ 33 | cb && cb(false, true); 34 | }, 10); 35 | }; 36 | 37 | MyZookeeper.prototype.getData = function(path, watch, cb){ 38 | setTimeout(function(){ 39 | //watch && watch('test change'); 40 | cb && cb(false); 41 | }, 10); 42 | }; 43 | 44 | module.exports = MyZookeeper; 45 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Corey600 on 2016/8/1. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | // modules to test 8 | var disconf = require('../index'); 9 | 10 | // require core modules 11 | var os = require('os'); 12 | var sysPath = require('path'); 13 | 14 | // require thirdpart modules 15 | var zookeeper = require('node-zookeeper-client'); 16 | var request = require('request'); 17 | var expect = require('expect.js'); 18 | var mm = require('mm'); 19 | 20 | // require custom modules 21 | var MyZookeeper = require('./lib/zookeeper'); 22 | var MyRequest = require('./lib/request'); 23 | 24 | // root dir 25 | var rootDir = sysPath.dirname(sysPath.dirname(__filename)); 26 | 27 | // set env 28 | var NODE_CONFIG_DIR = process.env.NODE_CONFIG_DIR = sysPath.join(rootDir, 'test/config'); 29 | 30 | // config dir of disconf 31 | var configDir = sysPath.join(rootDir, 'config'); 32 | 33 | describe('test', function () { 34 | beforeEach(function (done) { 35 | mm(request, 'Request', MyRequest); 36 | mm(zookeeper, 'createClient', function(conn, opt){ 37 | return new MyZookeeper(conn, opt); 38 | }); 39 | done(); 40 | }); 41 | 42 | afterEach(function (done) { 43 | mm.restore(); 44 | done(); 45 | }); 46 | 47 | describe('main', function () { 48 | 49 | before(function (done) { 50 | done(); 51 | }); 52 | 53 | after(function (done) { 54 | done(); 55 | }); 56 | 57 | it('excute success', function (done) { 58 | disconf.init({ 59 | path: configDir, 60 | name: 'disconf.properties' 61 | }, { 62 | dist_file: sysPath.join(NODE_CONFIG_DIR, os.hostname() + '.properties'), 63 | user_define_download_dir: sysPath.join(NODE_CONFIG_DIR, 'download') 64 | }, function (err, zk) { 65 | expect(Boolean(err)).to.be(false); 66 | expect(zk).not.to.be(undefined); 67 | }); 68 | 69 | disconf.on('error', function (err) { 70 | expect(Boolean(err)).not.to.be(false); 71 | done(); 72 | }); 73 | 74 | disconf.on('ready', function (data) { 75 | var conf = disconf.util.reloadConfig(); 76 | expect(data).not.to.be(null); 77 | expect(conf).not.to.be(null); 78 | done(); 79 | }); 80 | }); 81 | }); 82 | }); 83 | --------------------------------------------------------------------------------