commit 3a9804f020d894ff0de2425d898c7125ba8e1b21 Author: zhongjin Date: Mon Sep 17 20:32:19 2018 +0800 Initial commit diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1c7f54d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,874 @@ +# 1.4.2 (2018-04-12) +Main changes: add instance with desired number, Force using of socket.io 2.1.0, Bugfixes + +* (bluefox) fix error with letsencrypt debug output +* (bluefox) fix delObject +* (bluefox) fix restore of backup +* (bluefox) allow to add instance with desired number +* (apollon77) fix auto multihost +* (bluefox) fix for adapter update +* (bluefox) Force using of socket.io 2.1.0 + +# 1.4.0 (2018-04-03) +Main changes: uninstall via npm, downgrade winston-syslog, refactored deleteAdapter, object.json auto-backups + +* (AlCalzone) fix npm version pre-install check +* (bluefox) remove winston-syslog 2.0 and replace it with 1.2.6 +* (AlCalzone) refactored and promisified `deleteAdapter` and `deleteInstance` +* (bluefox) remove controller from the adapters list in admin +* (bluefox) also uninstall adapters using npm +* (bluefox) backup object.json files every 2 hours for last 48 hours (warning! Disk usage) +* (bluefox) added cli command to update/add the vis/knx license + +# 1.3.0 (2018-03-11) +Main changes: socket.io Version downgraded because of bug. Better npm5 support + +* (AlCalzone) Remove the outdated npm package and disable package-lock before installing (#175) +* (AlCalzone) reworked npm adapter version check without `npm` package +* (AlCalzone) ignore local NPM for version check +* (AlCalzone) don't modify the parent's process PATH variable +* (AlCalzone) check npm version before installing and potentially disable package-lock +* (AlCalzone) don't cancel installation, or we're breaking yunkong2 anyways +* (AlCalzone) ignore local npm version for preinstall check +* (bluefox) move socket.io from 2.0.4 to 1.5.1 because of bug +* (bluefox) add intro to default tabs + +# 1.2.7 (2018-03-06) +Main changes: support of npm5, Multihost fixed, added promises to adapter.js + +* (bluefox) fix multihost connect +* (bluefox) add "multihost status" command +* (bluefox) make statistics interval adjustable +* (bluefox) better scan of installed adapters +* (bluefox) better deletion of adapters +* (bluefox) fix requests like getStates('*.info.connection') +* (bluefox) create instance's objects by start. +* (AlCalzone) Add promisified methods to the adapter class +* (AlCalzone) enable basic type-checking and fix found error +* (Apollon77) fix potential error and check if that.log exists +* (bluefox) updates npm packets +* (Apollon77) log an error when npmInstallWithCheck throws +* (AlCalzone) [npm5] Disable package-lock.json before installing anything +* (Apollon77) use stable tag from admin in dependencies +* (bluefox) fix empty ID error + +# 1.2.5 (2018-01-27) +* (bluefox) move buildRepository.js to yunkong2.repositories +* (bluefox) fix adapter download +* (Apollon77) also include npm5 fix into reinstall.sh +* (bluefox) add functions to standard enums +* (bluefox) check if pattern is valid +* (bluefox) catch error by deleting of adapter. +* (bluefox) better adapter directory search +* (Apollon77) another fix for reinstall.sh, add --unsafe-perm +* (Apollon77) exit mocha tests explicitely when completed, needed pot. with most current version of mocha +* (Apollon77) add npm5 check and "Block" to "yunkong2 install" commands + +# 1.2.4 (2017-12-15) +* (bluefox) The fix for npm5 + +# 1.2.3 (2017-11-24) +* (bluefox) fix windows problem and storing of error messages +* (bluefox) fix logging level: silly +* (bluefox) fix dependency check +* (bluefox) fix small errors +* (bluefox) add repo commands to cli +* (bluefox) setTimeout(0) => setImmediate +* (bluefox) add timestamp and "from" information object +* (bluefox) allow to enable redis by setup +* (bluefox) catch backup errors +* (bluefox) ignore errors by setup first +* (bluefox) fix generate repository +* (bluefox) calculate number of datapoints in vis and deliver it in statistics +* (bluefox) fix restoring of backup +* (AlCalzone) fix install urls ending with ".git" + +# 1.2.0 (2017-09-24) +* (bluefox) fixed upgrade command +* (bluefox) allow install from custom repositories +* (bluefox) remove online and sources repositories +* (bluefox) fix multihosts command +* (bluefox) catch the error outputs of instances if they die +* (bluefox) no more support for node.js 0.10/0.12 +* (bluefox) add new logging level: silly + +# 1.1.3 (2017-08-13) +* (bluefox) Extend statistics (node.js versions and some HW parameters will be reported) +* (bluefox) Update npm packets +* (bluefox) catch semver error +* (bluefox) change interface of getDevices functions +* (bluefox) change interface of createChannel functions +* (bluefox) working on multihost service +* (bluefox) fix users cli +* (bluefox) implement defaultNewAcl +* (bluefox) remove 0.10 and add 8 by tests +* (bluefox) fix restart of adapters + +# 1.1.2 (2017-07-13) +* (bluefox) Close sockets by default for external connects + +# 1.1.1 (2017-06-29) +* (jens-maus) allow redis connections via unix sockets by specifying host as e.g. '/var/run/redis/redis.sock' and setting port to 0. This should slightly improve performance on busy installations. +* (Apollon77) optimizations for permission handling + +# 1.1.0 (2017-06-08) +* (bluefox) BREAKING Changes: For multihost systems the user MUST explicit allow connections from other IPs in /opt/yunkong2/yunkong2-data/yunkong2.json + +``` +"host": "127.0.0.1", +=> +"host": "0.0.0.0", +``` + +for objects (line 11) and for states (line 21). + +# 1.0.3 (2017-06-01) +* (bluefox) getHostInfo for new admin +* (bluefox) allow using of files for certificates +* (bluefox) always install zwave with unsafe-perm option +* (apollon77) add handling for undefined +* (apollon77) uptime is a number only and no String/List/Array beside the fact that also with a number a "toString" should exist, or it's undefined because an old host is listed with no value ... + +# 1.0.1 (2017-05-03) +* (bluefox) Change repository generation (not relevant for users) +* (bluefox) small change for install porcess (not relevant for users) + +# 1.0.0 (2017-04-23) +* (bluefox) No big changes, just version +* (bluefox) remove warning about iDs +* (bluefox) fix tests +* (bluefox) try to ignore npm error 1 +* (bluefox) fix possible error. +* (bluefox) create states with ack=true by default + +# 0.17.2 (2017-03-25) +* (bluefox) do not store logs and messages and just publish it +* (bluefox) remove mochawsome + +# 0.17.1 (2017-03-15) +* (bluefox) add stable repository +* (bluefox) fix bug with user=>users (required for user rights) +* (bluefox) install discovery adapter too if exists at first start +* (bluefox) extend configuration with city, country and GPS coordinates +* (bluefox) send city and country in statistics and allow to disable it + +# 0.16.2 (2017-03-08) +* (bluefox) fix "yunkong2 passwd username" + +# 0.16.1 (2017-02-27) +* (bluefox) use SHA256 instead of SHA1 (All passwords must be reset via "yunkong2 passwd username") +* (bluefox) change cli commands for user: user add, user del, user set, user enable... +* (bluefox) replace letsencrypt with greenlock +* (bluefox) try to fix sha256 issue with node.js 0.10.x +* (bluefox) add uniti +* (Apollon77) change reinstall.sh script +* (bluefox) add setStateChanged +* (bluefox) implement event counters +* (bluefox) read values from cache if possible in adapter +* (bluefox) add parser adapter, smartmeter, fakeroku, wetty, fronius, Worx Landroid Rasenmäher +* (bluefox) fix log sources +* (bluefox) fix callbacks calls by setObjectNotExists +* (bluefox) fix getForeignObjects +* (bluefox) write tests +* (Patrick) adapter.namespace now always int (was int or string mixed) +* (Patrick) _fixId better results for empty obj and string (return namespace + '.') +* (bluefox) small fixes for multihost +* (bluefox) add getInstalledAdapter for autodiscovery + +# 0.15.3 (2017-01-20) +* (bluefox) fix of autoSubscribe +* (bluefox) no extra uplaod if install of adapters from github + +# 0.15.1 (2017-01-14) +* (bluefox) documentation of adapter +* (bluefox) support of autoSubscribe +* (bluefox) add innogy-smarthome, vis-players +* (bluefox) fix error with "preserve" settings +* (bluefox) restart adapter immediately if desired +* (bluefox) support of web extensions + +# 0.14.0 (2016-12-17) +* (bluefox) add debug information for logging +* (bluefox) remove occ +* (bluefox) add mpd, icons-fatcow-hosting +* (bluefox) update node-schedule +* (bluefox) add cli command "show uuid" +* (bluefox) try to fix issue with npm3 +* (bluefox) add console command "repo [name]" +* (bluefox) try to extract the information from local sources-dist.json it online not reachable +* (bluefox) add host remove +* (bluefox) install non enabled adapters +* (bluefox) install icons on backup too +* (bluefox) fix some sporadic errors + +# 0.13.3 (2016-11-26) +* (bluefox) support of getLocationOnDisk message for admin +* (bluefox) allow upgrade to specific version, like adapter@0.1.0 +* (bluefox) you can change any native parameter of instance with set +* (bluefox) add support of syslog (see yunkong2.json to enable) +* (bluefox) add radar +* (bluefox) do not change title of instance by upload and update + +# 0.13.2 (2016-11-04) +* (bluefox) fix new installation + +# 0.13.1 (2016-10-31) +* (bluefox) fix renaming of host by start +* (bluefox) implement auto-objects and auto-states for adapter +* (bluefox) recursive deletion of folders with objects.unlink +* (bluefox) support of tarballs as install path +* (bluefox) rename rpi to rpi2, homekit to homekit2 +* (bluefox) add upnp +* (bluefox) fix fileName of log file +* (bluefox) remove peerDependencies + +# 0.13.0 (2016-10-21) +* (bluefox) fix letsencrypt to use fullchain.pem +* (bluefox) fix error with emitter +* (bluefox) fix formatDate for russian month +* (bluefox) backup letsencrypt files too +* (bluefox) install missing adapters one after other and not parallel. +* (bluefox) let remove UUID to prepare images with yunkong2 +* (bluefox) using peerDependencies +* (bluefox) rename host automatically if singlehost +* (bluefox) add write/read properties to alive variable +* (bluefox) fix creation of package.json for npm 3 +* (bluefox) try catch for parse of states +* (appolon77) add possibility to send messages with json +* (bluefox) updates some packages +* (bluefox) print node.js version at start +* (bluefox) force logger to use local time +* (appolon77) fix multi instance messaging +* (bluefox) using peerDependencies +* (bluefox) rename host automatically if singlehost +* (bluefox) update artnet +* (bluefox) add vis-canvas-gauges +* (bluefox) add rflink +* (bluefox) add foobar2000 +* (bluefox) add mqtt client +* (bluefox) add lgtv +* (bluefox) add pushsafer + +# 0.12.2 (2016-09-04) +* (bluefox) fix read versions by multihost +* (bluefox) add owntracks +* (bluefox) add amazon-dash +* (bluefox) control enabled for instance over system.adapter.NAME.INSTANCE.alive variable + +# 0.12.1 (2016-09-02) +* (bluefox) fixed letsencrypt file + +# 0.12.0 (2016-08-27) +* (bluefox) working on objects in redis +* (bluefox) add botvac adapter +* (bluefox) better multihost +* (bluefox) fix formatDate + +# 0.11.3 (2016-08-24) +* (PArns) fix upgrade of adapters +* (bluefox) update "_design/xyz" by upgrade + +# 0.11.2 (2016-08-13) +* (bluefox) fix upgrade of adapters + +# 0.11.1 (2016-07-30) +* (bluefox) fix dependency if depend on js-controller + +# 0.11.0 (2016-07-27) +* (bluefox) implement auto-restart of adapters (expert mode) +* (bluefox) add rights check for getBinaryState/setBinaryState +* (bluefox) support of default ttl for sessions +* (bluefox) fix custom setup +* (bluefox) fix upload binary files +* (bluefox) fix list of files in subdirectories + +# 0.10.1 (2016-07-06) +* (bluefox) support of chained certificates +* (bluefox) add nut +* (bluefox) add vis-map + +# 0.10.0 (2016-07-01) +* (bluefox) suport of exportand import +* (bluefox) activate redis for states +* (bluefox) fix install of adapter with singletonHost +* (bluefox) issue event if state deleted (redis) +* (bluefox) fix error with administrator users +* (bluefox) do not store repository if with errors +* (bluefox) fix checkPassword and setPassword +* (bluefox) update wrong SSL certificates +* (bluefox) add freemem state to host +* (bluefox) add milliseconds to formatDate +* (bluefox) update tar.gz + +* (bluefox) add fhem +* (bluefox) add netatmo +* (bluefox) add tankerkoenig +* (bluefox) add vis-history +* (bluefox) add homepilot +* (bluefox) add cloud + + +# 0.9.0 (2016-05-23) +* (bluefox) make from seconds the ms +* (bluefox) add console command "isrun" +* (bluefox) add "--timeout 5000" +* (bluefox) fix small errors +* (bluefox) change function formatValue +* (bluefox) fix stop of scheduled adapters +* (bluefox) add "--logs" flag for adapter start (required by adapter debugging) +* (bluefox) make hostname configurable +* (bluefox) fix update of adapters and settings + +# 0.8.10 (2016-04-25) +* (bluefox) fix restart script +* (bluefox) update default certificates + +# 0.8.9 (2016-04-23) +* (bluefox) do not handle exceptions in logger +* (bluefox) change logger +* (bluefox) set valid mimeType for *.manifest +* (bluefox) add noolite adapter +* (bluefox) change download script +* (bluefox) change rename script +* (bluefox) add starline +* (bluefox) change repository building +* (bluefox) add 'delete' objects +* (bluefox) change behavior by exceptions +* (bluefox) workaround for DHCP delay +* (bluefox) fix passwd command +* (bluefox) do not write error under windows: "cannot delete log file" + +# 0.8.8 (2016-02-29) +* (bluefox) replace winston with latest module +* (bluefox) add syslog support +* (bluefox) fix some LINT warnings +* (bluefox) add "host self" command (identical to "host this") +* (bluefox) fix error with npm 3 if no node_modules directory found +* (bluefox) support of noCache flag +* (bluefox) fix error if _data.json file broken +* (bluefox) support of file uploading: yunkong2 fileName /adapter/fileName + +# 0.8.7 (2016-02-24) +* (bluefox) fix getForeignObjects +* (bluefox) add telegram +* (bluefox) enable OEM naming +* (bluefox) fix small error if multihost not available +* (bluefox) add reinstall script +* (bluefox) add vis-justgage adapter +* (bluefox) add mysensors + +# 0.8.6 (2016-02-04) +* (bluefox) add text2command adapter +* (bluefox) fix upload problem +* (bluefox) use node-schedule 1.0.0 +* (bluefox) extend node node_modules/yunkong2.js-controller/lib/buildRepository.js command + +# 0.8.5 (2016-02-01) +* (bluefox) update version of node-schedule to fix problem with Februar.2016 +* (bluefox) update socket.io version +* (bluefox) add logo image +* (bluefox) add buildRepository.js +* (bluefox) If desired, that adapter must be terminated +* (bluefox) use isFloatComma in formatValue +* (soef) formatDate extended and formatValue added +* (soef) formatDate extended to use seconds as duratiorn +* (soef) formatValue added to convert a value to a string with thousand separator.... +* (bluefox) add homekit and miele +* (bluefox) fix upload of files + +# 0.8.4 (2016-01-22) +* (bluefox) fix version +* (bluefox) fix log outputs + +# 0.8.3 (2016-01-21) +* (bluefox) add commands like "npm start" +* (bluefox) check singletonHost one on host +* (bluefox) add memoryLimitMB for controller and adapters +* (bluefox) make install from NON-git sources possible again. +* (bluefox) add rpi, weatherunderground, chromecast, geofency, samsung, squeezebox, vcard, yamaha +* (husky-koglhof) occ und rpi Adapter +* (angelnu) visdebug - check for different adapter directories +* (bluefox) enable install of icons-open-icon-library-png + + +# 0.8.2 (2015-12-14) +* (bluefox) fix upgrade. + +# 0.8.1 (2015-12-14) +* (bluefox) fix permissions for administrator group, but not admin user. +* (bluefox) support of getHistory command and defaultHistory +* (bluefox) implement "yunkong2 restart adapter" +* (bluefox) enable write dependencies as an object +* (bluefox) remove directory adapter and move example to yunkong2.template +* (bluefox) prepare support of syslog +* (bluefox) add yunkong2.sql +* (bluefox) add yunkong2.influxdb +* (bluefox) remove example adapter (it is replaced with yunkong2.template) +* (bluefox) start of renaming of js-controller to enable branding + +# 0.7.15 (2015-11-10) +* (bluefox) add command visdebug +* (bluefox) add flag preserveSettings +* (bluefox) add vis-keyboard +* (bluefox) fix error with host rename +* (bluefox) fix sendTo and sendToHost with callback. +* (bluefox) update objects by upload of adapter (important for community adapters) +* (bluefox) add vis-google-fonts +* (bluefox) support of quality in setState +* (bluefox) add adapter mobile + +# 0.7.14 (2015-10-13) +* (bluefox) fix restart under linux +* (bluefox) add wolf adapter to repository +* (smilingJack) increase timeout by update of repository +* (bluefox) fix set --ssl +* (bluefox) add "connectTimeout" parameter to yunkong2.json. Try to fix EADDRINUSE error under raspi. + +# 0.7.13 (2015-09-30) +* (bluefox) add vis-jqui-mfd +* (bluefox) allow install direct from github in admin +* (bluefox) add vis-fancyswitch, vis-rgraph +* (bluefox) fill state by createState even if no default value set +* (bluefox) add modbus + +# 0.7.12 (2015-09-15) +* (bluefox) add terminal adapter +* (bluefox) inplement "yunkong2 url xxx" +* (bluefox) fix restore +* (bluefox) fix _failCounter entry in Adapters +* (bluefox) fix log outputs + +# 0.7.11 (2015-08-23) +* (bluefox) fix installation of adapter + +# 0.7.10 (2015-08-22) +* (bluefox) fix first installation + +# 0.7.9 (2015-08-20) +* (bluefox) fix broker upgrade +* (bluefox) improve vis upload +* (bluefox) fix adapter.deleteChannel +* (bluefox) use regex by deleting of channel or instance +* (bluefox) fix delete instance errors +* (bluefox) add new console commands: set, host. To set settings of instance from console and change host name. + +# 0.7.8 (2015-08-12) +* (bluefox) fix error with node-red +* (bluefox) move setup files into directories + +# 0.7.7 (2015-08-11) +* (bluefox) add harmony to repository + +# 0.7.6 (2015-08-06) +* (bluefox) change log file extension to .log +* (bluefox) enable destroyDB, but check before if yunkong2 is running +* (bluefox) update packages +* (bluefox) add bars, plumbs, scenes + +# 0.7.5 (2015-07-27) +* (bluefox) add "yunkong2 upload all" and "yunkong2 start all" commands +* (bluefox) fix "yunkong2 package" +* (bluefox) make it possible to allow OBJECTS and STATES only on localhost +* (bluefox) add new adapter pushbullet +* (bluefox) fix restart by installing. + +# 0.7.4 (2015-07-19) +* (bluefox) fix restartAdapters flag.(for vis-metro and co) +* (bluefox) add vis-hqWidgets +* (bluefox) add vis-colorpicker +* (bluefox) normalize paths in backup +* (bluefox) add better backup + + +# 0.7.3 (2015-07-12) +* (bluefox) add flot to repository +* (bluefox) add chmodFile for adapter +* (bluefox) implement rm +* (bluefox) fix permissions problem +* (bluefox) set permission by creation of file +* (bluefox) make possible upload of subtree +* (bluefox) fix user name +* (bluefox) fix update function + +# 0.7.2 (2015-06-29) +* (bluefox) remove _failCounter from adapter list +* (bluefox) update license + +# 0.7.1 (2015-06-28) +* (bluefox) support of permissions +* (bluefox) fix backup +* (bluefox) fix error with noFileCache +* (bluefox) add unsubscribeForeignObjects to adapters +* (bluefox) add icon sets to repository +* (bluefox) implement list: objets, states, instances, adapters +* (bluefox) support of "list hosts" +* (bluefox) verify version by install and start +* (bluefox) fix error with dependencies +* (bluefox) fix delete of logs +* (bluefox) fix getPort function in adapter. + + +# 0.7.0 (2015-05-07) +* (bluefox) support of permissions + +# 0.6.6 (2015-05-07) +* (bluefox) implement daily rolling files. +* (bluefox) fix addChannelToEnum, deleteChannelFromEnum, deleteChannel if no device name +* (bluefox) fix "state set", "message" console commands +* (bluefox) fix errors in adapter.js + +# 0.6.5 (2015-04-27) +* (bluefox) add default certificates + +# 0.6.4 (2015-04-17) +* (bluefox) optimize install call +* (bluefox) backup/restore + +# 0.6.3 (2015-04-16) +* (bluefox) use system npm for update and install +* (bluefox) generate uuid as hash of MAC +* (bluefox) delete empty adaptors from repository + +# 0.6.0 (2015-03-22) +* (bluefox) try to implement backup/restore +* (bluefox) support of "--install" flag for sayIt +* (bluefox) add megad to repository +* (bluefox) enable subscribeStates() same as subscribeStates('*') +* (bluefox) replace "slient" mode with "install" mode + +# 0.5.14 (2015-03-11) +* (bluefox) enable silent mode + +# 0.5.14 (2015-03-08) +* (bluefox) update utils.js (silent mode) +* (bluefox) fix error by setup.js + +# 0.5.12 (2015-03-07) +* (bluefox) fix error with sendTo('email') +* (bluefox) increase timeout for npm to 5000 ms + +# 0.5.11 (2015-02-26) +* (bluefox) fix function deleteDevice in adapter.js + +# 0.5.10 (2015-02-26) +* (bluefox) do not start more times the scheduled task after a long sleep + +# 0.5.9 (2015-02-21) +* (bluefox) fix error with trimFifo (used for history adapter) +* (bluefox) use system "npm" by updating of js-controller + +# 0.5.8 (2015-02-18) +* (bluefox) add start/stop/restart adapter from console. +* (bluefox) better wakeup of adapters. + +# 0.5.7 (2015-01-14) +* (bluefox) add sayit adapter +* (bluefox) fix clear of log file + +# 0.5.4 (2015-01-27) +* (bluefox) fix restart under windows + +# 0.5.5 (2015-01-30) +* (bluefox) add yr as npm +* (bluefox) extend adapter.js with formatDate + +# 0.5.6 (2015-02-06) +* (bluefox) add simple-api + +# 0.5.3 (2015-01-27) +* (bluefox) fix log for restart + +# 0.5.2 (2015-01-27) +* (bluefox) remove node-windows from dependencies + +# 0.5.1 (2015-01-26) +* (bluefox) fix log +* (bluefox) show npm version and not git version +* (bluefox) use npm packet to install and not the exec npm + +# 0.5.0 (2015-01-23) +* (bluefox) make it possible to install yunkong2 with "npm install yunkong2" + +# 0.4.6 (2015-01-21) +* (bluefox) add developing flag "noFileCache" to do not cache web files. +* (bluefox) improve "adapter.getPort" on windows. +* (bluefox) create yunkong2.sh with 0777 by install + +# 0.4.5 (2015-01-20) +* (bluefox) fix problem with no objects after "setup" started + +# 0.4.4 (2015-01-20) +* (bluefox) move "data" directory by "npm install" to "../../yunkong2-data" + +# 0.4.3 (2015-01-18) +* (bluefox) restart objects socket if some exception occurs + +# 0.4.2 (2015-01-14) +* (bluefox) fix error in objectsInMemClient and objectsInMemServer + +# 0.4.1 (2015-01-10) +* (bluefox) fix first setup + +# 0.4.0 (2015-01-10) +* (bluefox) support of multiple hosts + +# 0.3.17 (2015-01-10) +* (bluefox) fix problem with "hosts are not shown in admin" + +# 0.3.16 (2015-01-09) +* (bluefox) support of multiple hosts + +# 0.3.15 (2015-01-09) +* (bluefox) "chmod 777 * -R /opt/yunkong2" => "chmod 777 -R /opt/yunkong2" + +# 0.3.14 (2015-01-09) +* (bluefox) fix error update of js-controller under linux/osx + +# 0.3.13 (2015-01-08) +* (bluefox) fix error with publish/subscribe + +# 0.3.12 (2015-01-07) +* (bluefox) support of "onlyWWW" flag + +# 0.3.11 (2015-01-06) +* (bluefox) fix error if state is null or undefined +* (bluefox) store fifos from states in file + +# 0.3.10 (2015-01-06) +* (bluefox) support of file manager in vis + +# 0.3.9 (2015-01-04) +* (bluefox) try to fix update of controller + +# 0.3.8 (2015-01-04) +* (bluefox) fix error with subscribes +* (bluefox) fix error with extendObject +* (bluefox) fix error with delete adapter +* (bluefox) fix error in deleteChannelFromEnum + +# 0.3.7 (2015-01-03) +* (bluefox) fix upload problem + +# 0.3.6 (2015-01-03) +* (bluefox) fix package.json + +# 0.3.1 (2015-01-02) +* (bluefox) enable npm install + +# 0.3.0 (2014-12-28) +* (bluefox) no redis any more + +# 0.2.9 (2014-12-20) +* (bluefox) fix problem with restart controller +* (bluefox) check flag supportStopInstance before send signal to adapter + +# 0.2.8 (2014-12-20) +* (bluefox) fix problem with upgrade adapter + +# 0.2.7 (2014-12-19) +* (bluefox) fix problem with upload adapter + +# 0.2.6 (2014-12-19) +* (bluefox) implement getConfigKeys in redis. +* (bluefox) new running mode: "once" + +# 0.2.5 (2014-12-14) +* (bluefox) enable start of "no-daemon" adapters like "rickshaw" or "vis". + +# 0.2.4 (2014-12-10) +* (bluefox) fix delObject function +* (bluefox) remove unused log message + +# 0.2.3 (2014-12-08) +* (bluefox) optimize start/stop/restart. + +# 0.2.2 (2014-12-06) +* (bluefox) fix error in redis. + +# 0.2.1 (2014-12-06) +* (bluefox) fix error in redis. + +# 0.2.0 (2014-12-04) +* (bluefox) remove couchDB and store everything in redis. + +# 0.1.6 (2014-11-29) +* (bluefox) use npm to install some adapters. + +# 0.1.5 (2014-11-26) +* (bluefox) fix log in controller.js one more time + +# 0.1.4 (2014-11-26) +* (bluefox) fix log in controller.js + +# 0.1.3 (2014-11-24) +* (bluefox) fix some errors and add restart.js + +# 0.1.2 (2014-11-24) +* (bluefox) fix messageboxes + +# 0.1.1 (2014-11-23) +* (bluefox) fix log output in admin. + +# 0.1.0 (2014-11-22) +* (bluefox) new naming concept. No children and parents set extra. + +# 0.0.37 (2014-11-16) +* (bluefox) fix adapter.js +* (bluefox) call "chmod +x yunkong2" after updgrade of controller + +# 0.0.36 (2014-11-15) +* (bluefox) fix adapter.js + +# 0.0.35 (2014-11-09) +* (bluefox) add logging to controller + +# 0.0.34 (2014-11-08) +* (bluefox) create restore/backup (from console) + +# 0.0.33 (2014-11-04) +* (bluefox) support of node-red as adapter and defined exit codes for errors + +# 0.0.32 (2014-11-04) +* (bluefox) support of node-red as adapter + +# 0.0.31 (2014-11-02) +* (bluefox) fix error with binary states + +# 0.0.30 (2014-11-01) +* (bluefox) fix error in "support of listDevices for configuration" + +# 0.0.29 (2014-10-30) +* (bluefox) (bluefox) fix creatChannel for adapter + +# 0.0.28 (2014-10-30) +* (bluefox) support of listDevices for configuration + +# 0.0.27 (2014-10-30) +* (bluefox) check common.os (e.g. to install adapter only on linux) +* (bluefox) support of common.install adapter settings. + +# 0.0.26 (2014-10-25) +* (bluefox) change state names to 'io.*' + +# 0.0.25 (2014-10-24) +* (bluefox) show version in log + +# 0.0.24 (2014-10-22) +* (bluefox) fix dependencies of packets + +# 0.0.22 (2014-10-20) +* (bluefox) fix error in adapter.js + +# 0.0.21 (2014-10-19) +* (bluefox) store repository in the DB + +# 0.0.20 (2014-10-19) +* (bluefox) change example adapter for emitEvent +* (bluefox) support of certificates +* (bluefox) fix names for states + +# 0.0.19 (2014-10-02) +* (bluefox) fix add/delete adapter +* (bluefox) fill source-dist.json with grunt +* (bluefox) call "npm install" after adapter updated + +# 0.0.18 (2014-09-27) +* (bluefox) new concept of updates and repositories + +# 0.0.17 (2014-09-04) +* (hobbyquaker) trimFifo calls callback with trimmed data +* (hobbyquaker) fix instance restart + +# 0.0.16 (2014-08-22) +* (hobbyquaker) admin-ui: enums +* (hobbyquaker) admin-ui: ... +* (hobbyquaker) fixes + +# 0.0.15 (2014-08-17) +* (hobbyquaker) admin-ui: adapter-settings +* (hobbyquaker) admin-ui: add instance +* (hobbyquaker) admin-ui: cmd execution + +# 0.0.14 (2014-08-11) +* (bluefox) adapter admin: https +* (bluefox) adapter admin: auth +* (bluefox) admin-ui: user and group management +* (hobbyquaker) fixes +* (hobbyquaker) added adapter cul to sources-dist.json + +# 0.0.13 (2014-07-31) +* (hobbyquaker) new object types user and group +* (hobbyquaker) yunkong2 setup: create user and group admin. Default password: yunkong2 + +# 0.0.12 +* (hobbyquaker) setup.js fixes +* (hobbyquaker) setup.js create multiple system objects + + +# 0.0.11 +* (hobbyquaker) admin ui: instances + +# 0.0.10 + +* (hobbyquaker) refactoring controller.js and setup.js +* (hobbyquaker) yunkong2.js command line options +* (hobbyquaker) yunkong2 with shebang (needs chmod +x) +* (hobbyquaker) added dbdump.js +* (hobbyquaker) fixes and other stuff... + +# 0.0.9 + +* (hobbyquaker) Javascript Script Engine +* (bluefox) Gruntfile.js +* (bluefox) SCHEMA.md + + +# 0.0.8 + +* (hobbyquaker) ctrl: instance mode schedule +* (hobbyquaker) yunkong2.js add: set instanceObjects (new attribute in io-package.json) +* (hobbyquaker) added meta attribute to sources.json +* (hobbyquaker) added adapter yr to sources.json + +# 0.0.7 + +* (hobbyquaker) fix Admin UI - handle IDs with spaces + + +# 0.0.6 + +* (hobbyquaker) download adapters via ```yunkong2.js add ``` (has to be defined in conf/sources.json) +* (hobbyquaker) automatically install node dependencies on ```yunkong2.js add``` +* (hobbyquaker) restructuring +* (hobbyquaker) history adapter +* (hobbyquaker) renamed adapter web to admin (this adapters purpose is to do only the admin-ui) +* (hobbyquaker) renamed adapter legacy to web (this adapter should provide a ccu.io-like webserver for easy porting of dashui, scriptgui, yahui, ...) +* (hobbyquaker) renamed adapter dummy to example + + + +# 0.0.5 + +* (hobbyquaker) hm-rpc Adapter checks Datapoint-Type and warns if readonly +* (hobbyquaker) Admin-UI - gridStates update on stateChange + +# 0.0.4 + +* (hobbyquaker) hm-rega Adapter +* (hobbyquaker) ctrl restarts crashed adapters automatically + +# 0.0.3 + +* (hobbyquaker) Adapter web +* (hobbyquaker) Admin UI + +# 0.0.2 + +* (hobbyquaker) Installation/instancing of adapters via ```yunkong2.js add``` +* (hobbyquaker) Adapter command line param instead of IPC +* (hobbyquaker) Config-file yunkong2.json + +# 0.0.1 + +* (hobbyquaker) first release + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..afe387b --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014-2018 bluefox, +Copyright (c) 2014 hobbyquaker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ea116ef --- /dev/null +++ b/README.md @@ -0,0 +1,135 @@ +![Logo](lib/img/yunkong2.png) +# yunkong2.js-controller +================== + +[![NPM version](http://img.shields.io/npm/v/yunkong2.js-controller.svg)](https://www.npmjs.com/package/yunkong2.js-controller) +[![Downloads](https://img.shields.io/npm/dm/yunkong2.js-controller.svg)](https://www.npmjs.com/package/yunkong2.js-controller) +[![Tests](http://img.shields.io/travis/yunkong2/yunkong2.js-controller/master.svg)](https://travis-ci.org/yunkong2/yunkong2.js-controller) + +[![NPM](https://nodei.co/npm/yunkong2.js-controller.png?downloads=true)](https://nodei.co/npm/yunkong2.js-controller/) + +Here you can find change [log](CHANGELOG.md). + +This is a main controller, that starts all other yunkong2 adapters. + +Official Web-Site: http://yunkong2.net + +Forum: http://forum.yunkong2.net + +Trello: https://trello.com/b/q0SZKdfW/yunkong2-whiteboard + +yunkong2 wiki: https://github.com/yunkong2/yunkong2/wiki/Home-(English) + +Explanation of the concept: https://github.com/yunkong2/yunkong2 + +---------------------------------------------------------------------- + +This is a Javascript/Node.js implementation of an yunkong2 controller. + +## Manual installation of yunkong2.js-controller on Debian based Linux (Raspbian, Ubuntu, ...) + +### Install Node.js + +``` +sudo apt-get update +sudo apt-get upgrade +sudo apt-get install curl build-essential +sudo curl -sL https://deb.nodesource.com/setup_6.x | sudo bash - +sudo apt-get install -y nodejs +``` + +### Install yunkong2 on linux + +``` +sudo mkdir /opt/yunkong2 +sudo cd /opt/yunkong2 +sudo chmod 777 /opt/yunkong2 +sudo npm install yunkong2 --unsafe-perm +``` + +After that the yunkong2 should run and be available in browser under ```http://:8081/``` + +### Start yunkong2 controller on linux + +* run ```./yunkong2 start``` in the yunkong2 directory to start the yunkong2 controller in the background +* watch the logfile ```tail -f log/yunkong2.log``` + +or + +* run ```node node_modules/yunkong2.js-controller/controller.js``` to start the yunkong2 controller in foreground and watch the log on console + +### Install js-controller on windows + +* Create and change to the directory under which you want to install yunkong2. + + ```mkdir C:/yunkong2``` + ```cd C:/yunkong2``` + +* install npm packet from created directory + + ```npm install yunkong2``` + +### Start yunkong2 controller on windows + +* run ```yunkong2 start``` in the yunkong2 directory to start the yunkong2 controller in the background console +* check the logfile ```node_modules/yunkong2.js-controller/log/yunkong2.log``` + +or + +* run ```node node_modules/yunkong2.js-controller/controller.js``` to start the yunkong2 controller in foreground and watch the log on console + +### Admin UI + +The admin adapter starts a web-server that hosts the Admin UI. Default port is 8081, so just open http://<yunkong2>:8081/ +If port 8081 is occupied, you can install second Admin UI on alternate port and change port for first admin UI: + +* run ```./yunkong2 add admin --enabled --port 8090``` and go to the http://<yunkong2>:8090/. Of course you can change port 8090 to other one. + +## Using REDIS as States-DB +There is a possibility to use REDIS as states database. It is reasonable to do that for big installations or for systems with performance problems. +It is possible to switch anytime between REDIS and In-Memory Javascript DB. The only problem, that all states must be updated by adapters again (values will be lost). +Objects and configuration are not affected. + +To install REDIS on linux/debuan just write: ```apt-get install redis-server``` . + +If you plan to use mulithost installation you must allow connections to redis from any address (default only 127.0.0.1). +To do that edit file */etc/redis/redis.conf* (```sudo nano /etc/redis/redis.conf```) and replace ```bind 127.0.0.1``` with ```bind 0.0.0.0``` . +Don't forget to restart redis after that. (```sudo /etc/init.d/redis-server restart```) + +To install on windows download latest release here [https://github.com/MSOpenTech/redis/releases](https://github.com/MSOpenTech/redis/releases). + +To switch to REDIS write in the console following: + +``` +>yunkong2 stop +>yunkong2 setup custom +``` + +And then: + +``` +Type of objects DB [file, couch, redis], default [file]: +Host of objects DB(file), default[127.0.0.1]: +Port of objects DB(file), default[9001]: +Type of states DB [file, redis], default [file]: redis +Host of states DB (redis), default[127.0.0.1]: +Port of states DB (redis), default[6379]: +Data directory (file), default[../../../yunkong2-data/]: +Host name of this machine [FastPC]: +creating conf/yunkong2.json +``` + +Note that in fourth line it was entered **redis**. + +Now yunkong2 can be started. +Of course redis must be first installed and firewall rules must be checked. + +To switch back to JS States write the same commands again, just instead of **redis** in fourth line write nothing and press ENTER. + +## License + +The MIT License (MIT) + +Copyright (c) 2014-2018 bluefox , + +Copyright (c) 2014 hobbyquaker diff --git a/_service_iobroker.bat b/_service_iobroker.bat new file mode 100644 index 0000000..d85edb4 --- /dev/null +++ b/_service_iobroker.bat @@ -0,0 +1,3 @@ +echo Used for restart from js-controller itself. Not for user. +%WINDIR%\system32\net.exe stop yunkong2 +%WINDIR%\system32\net.exe start yunkong2 diff --git a/conf/cert.crt b/conf/cert.crt new file mode 100644 index 0000000..1dd31fd --- /dev/null +++ b/conf/cert.crt @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICSTCCAbICCQDwWQ5sMoq7ETANBgkqhkiG9w0BAQUFADBpMQswCQYDVQQGEwJE +RTETMBEGA1UECAwKU29tZS1TdGF0ZTERMA8GA1UECgwIaW9Ccm9rZXIxEDAOBgNV +BAMMB0JsdWVmb3gxIDAeBgkqhkiG9w0BCQEWEWRvZ2Fmb3hAZ21haWwuY29tMB4X +DTE2MDQyNTIxMjQwMVoXDTE3MDQyNTIxMjQwMVowaTELMAkGA1UEBhMCREUxEzAR +BgNVBAgMClNvbWUtU3RhdGUxETAPBgNVBAoMCGlvQnJva2VyMRAwDgYDVQQDDAdC +bHVlZm94MSAwHgYJKoZIhvcNAQkBFhFkb2dhZm94QGdtYWlsLmNvbTCBnzANBgkq +hkiG9w0BAQEFAAOBjQAwgYkCgYEAxTR0JnD7ucvZ4CA92FAq9jVGP3gXHhCnRXBM +IGQY6dQzOdnf/SWBUq0Bh0DLy6KRubokF1YqdJkfea2cWhdJqOcHUwITNaXMU4Rn +goIWVZvT6C262pL5ffoEh6GGzVcX7/X4tHD2HOwm3opboVlktIZtVaVjzPD1+q5H +c+a/TiMCAwEAATANBgkqhkiG9w0BAQUFAAOBgQBVhdIg59lHKtdpv5O0icvqD4f0 +tbqMvhWJ/7fhzr1fdjb5OK74g2G90KMhYnzOk0aZu4pgEoXHugpBLb+ndxJnG41p +IYe2qg4tp6AjR/uFswdrBLRUhW63yls3FiTEJjKCrGNEdjZoqsTEfwhXab3EoT7t +Wu+st1V0yiHlsvRGTg== +-----END CERTIFICATE----- diff --git a/conf/cert.key b/conf/cert.key new file mode 100644 index 0000000..718d5ba --- /dev/null +++ b/conf/cert.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDFNHQmcPu5y9ngID3YUCr2NUY/eBceEKdFcEwgZBjp1DM52d/9 +JYFSrQGHQMvLopG5uiQXVip0mR95rZxaF0mo5wdTAhM1pcxThGeCghZVm9PoLbra +kvl9+gSHoYbNVxfv9fi0cPYc7CbeiluhWWS0hm1VpWPM8PX6rkdz5r9OIwIDAQAB +AoGBAJVSWoChHHpa+ObUgv+/9Efpnv+AF0EUqxPRLFN6d8LWgtNTPl+YfovzpCyd +y7KtrlpLr/hbrloLd+HSq4ksCQEfJ7Le/4fjc2lt3Ib/K9qSr3bnmIWAK00VU+fF +mN1NTFJTV0O2+ctCOY9ZRwue5ehTp9eqPjsGwdeldii1WbSBAkEA6Z0YjMg+04z1 +M8FEUWSdPf6AHWB45hDJ+qPuIDNZxvVOcEsTyRsfkb1PKZm2NDx6mBN16po13Vka +QPy35ApoOwJBANgaMdbig76A1tvyhtklJPTU0g0N7CzXy+PNu8B3YghY8dYF/gSv +cBr0d8xGaZEczGQ35C0Tb9gTadHL64kxuzkCQHYaQYsKwRhaLqxXjJ5Ja2UoAMTZ +PMWyvynDLmOBEmYPJfSHQB1vZOpc9mRlnUOTP7caP4a3J3wby7YHDUBwMnkCQHGx +1mbn5chkoKY3gxrboAXvslOL76XoIy1HIHCyXrFlmlav8GUmqCSGWkDvCrt+G0re +3P2aLE3SaOooD1OvBoECQQDXMxPNYVGIErO7hxp9T9BXKcbnQV/mNhJYdl9VUoVB +gcVGatR1dBZX31Yt+HY4/ym9YdQ8MGCg2Kfmm0haLakP +-----END RSA PRIVATE KEY----- diff --git a/conf/iobroker-dist.json b/conf/iobroker-dist.json new file mode 100644 index 0000000..cb9171f --- /dev/null +++ b/conf/iobroker-dist.json @@ -0,0 +1,96 @@ +{ + "system": { + "memoryLimitMB": 0, + "hostname": "", + "statisticsInterval": 15000, + "statisticsIntervalComment": "Interval how often the counters for input/output in adapters and controller will be updated" + }, + "multihostService": { + "enabled": false, + "secure": true + }, + "network": { + "IPv4": true, + "IPv6": true, + "bindAddress": null + }, + "objects" : { + "type": "file", + "typeComment": "Possible values: 'file' - [port 9001], redis - [port 6379], couch - [port 5984].", + "host": "127.0.0.1", + "port": 9001, + "user": "", + "pass": "", + "noFileCache": false, + "connectTimeout": 2000, + "backup": { + "disabled": false, + "files": 24, + "filesComment": "Minimal number of backup files, after the deletion will be executed according to backupTime settings", + "hours": 48, + "hoursComment": "All backups older than 48 hours will be deleted. But only if the number of files is greater than of backupNumber", + "period": 120, + "periodComment": "by default backup every 2 hours. Time is in minutes. To disable backup set the value to 0", + "path": "", + "pathComment": "Absolute path to backup directory or empty to backup in data directory" + } + }, + "states" : { + "type": "file", + "typeComment": "Possible values: 'file' - [port 9000], 'redis' - [port 6379].", + "host": "127.0.0.1", + "port": 9000, + "maxQueue": 1000, + "options": { + "auth_pass" : null, + "retry_max_delay" : 15000 + }, + "backup": { + "disabled": false, + "files": 24, + "filesComment": "Minimal number of backup files, after the deletion will be executed according to backupTime settings", + "hours": 48, + "hoursComment": "All backups older than 48 hours will be deleted. But only if the number of files is greater than of backupNumber", + "period": 120, + "periodComment": "by default backup every 2 hours. Time is in minutes. To disable backup set the value to 0", + "path": "", + "pathComment": "Absolute path to backup directory or empty to backup in data directory" + } + }, + "log": { + "level": "info", + "maxDays": 7, + "noStdout": true, + "transport": { + "file1": { + "type": "file", + "enabled": true, + "filename": "log/yunkong2", + "fileext": ".log", + "maxsize": null, + "maxFiles": null + }, + "syslog1": { + "type": "syslog", + "enabled": false, + + "host": "localhost", + "host_comment": "The host running syslogd, defaults to localhost.", + + "port_comment": "The port on the host that syslog is running on, defaults to syslogd's default port(514/UDP).", + + "protocol": "udp4", + "protocol_comment": "The network protocol to log over (e.g. tcp4, udp4, unix, unix-connect, etc).", + + "path_comment": "The path to the syslog dgram socket (i.e. /dev/log or /var/run/syslog for OS X).", + "facility_comment": "Syslog facility to use (Default: local0).", + "localhost": "yunkong2", + "localhost_comment": "Host to indicate that log messages are coming from (Default: localhost).", + "sysLogType_comment": "The type of the syslog protocol to use (Default: BSD).", + "app_name_comment": "The name of the application (Default: process.title).", + "eol_comment": "The end of line character to be added to the end of the message (Default: Message without modifications)." + } + } + }, + "dataDirComment": "Always relative to yunkong2.js-controller/" +} \ No newline at end of file diff --git a/conf/sources-dist.json b/conf/sources-dist.json new file mode 100644 index 0000000..8e1ef58 --- /dev/null +++ b/conf/sources-dist.json @@ -0,0 +1,1213 @@ +{ + "admin": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.admin/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.admin/master/admin/admin.png", + "version": "2.0.9", + "type": "general", + "published": "2014-12-04T18:45:44.907Z" + }, + "amazon-dash": { + "meta": "https://raw.githubusercontent.com/PArns/yunkong2.amazon-dash/master/io-package.json", + "icon": "https://raw.githubusercontent.com/PArns/yunkong2.amazon-dash/master/admin/amazon-dash.png", + "version": "0.2.7", + "type": "hardware", + "published": "2016-09-05T19:03:16.756Z" + }, + "artnet": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.artnet/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.artnet/master/admin/artnet.png", + "version": "1.0.0", + "type": "lighting", + "published": "2016-10-13T18:11:08.868Z" + }, + "b-control-em": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.b-control-em/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.b-control-em/master/admin/bcontrol.png", + "version": "0.2.1", + "type": "energy", + "published": "2015-01-02T17:10:47.222Z" + }, + "benq": { + "meta": "https://raw.githubusercontent.com/instalator/yunkong2.benq/master/io-package.json", + "icon": "https://raw.githubusercontent.com/instalator/yunkong2.benq/master/admin/benq.png", + "version": "0.2.0", + "type": "multimedia", + "published": "2017-07-20T16:42:10.650Z" + }, + "ble": { + "meta": "https://raw.githubusercontent.com/AlCalzone/yunkong2.ble/master/io-package.json", + "icon": "https://raw.githubusercontent.com/AlCalzone/yunkong2.ble/master/admin/ble.png", + "version": "0.3.4", + "type": "hardware", + "published": "2017-09-05T15:57:13.123Z" + }, + "bmw": { + "meta": "https://raw.githubusercontent.com/frankjoke/yunkong2.bmw/master/io-package.json", + "icon": "https://raw.githubusercontent.com/frankjoke/yunkong2.bmw/master/admin/bmw.png", + "version": "1.3.1", + "type": "hardware", + "published": "2017-09-02T11:56:25.197Z" + }, + "botvac": { + "meta": "https://raw.githubusercontent.com/Pmant/yunkong2.botvac/master/io-package.json", + "icon": "https://raw.githubusercontent.com/Pmant/yunkong2.botvac/master/admin/botvac.png", + "version": "1.0.0", + "type": "household", + "published": "2016-07-24T22:08:47.215Z" + }, + "vr200": { + "meta": "https://raw.githubusercontent.com/Eisbaeeer/yunkong2.vr200/master/io-package.json", + "icon": "https://raw.githubusercontent.com/Eisbaeeer/yunkong2.vr200/master/admin/VR200.png", + "version": "0.2.0", + "type": "household", + "published": "2018-02-23T20:41:35.213Z" + }, + "broadlink2": { + "meta": "https://raw.githubusercontent.com/frankjoke/yunkong2.broadlink2/master/io-package.json", + "icon": "https://raw.githubusercontent.com/frankjoke/yunkong2.broadlink2/master/admin/broadlink.png", + "version": "1.8.0", + "type": "iot-systems", + "published": "2017-07-27T12:44:47.864Z" + }, + "chromecast": { + "meta": "https://raw.githubusercontent.com/angelnu/yunkong2.chromecast/master/io-package.json", + "icon": "https://raw.githubusercontent.com/angelnu/yunkong2.chromecast/master/admin/chromecast.png", + "version": "1.3.5", + "type": "multimedia", + "published": "2016-01-18T22:15:11.609Z" + }, + "cloud": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.cloud/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.cloud/master/admin/cloud.png", + "version": "2.5.0", + "type": "communication", + "published": "2016-06-24T18:36:32.658Z" + }, + "cul": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.cul/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.cul/master/admin/busware.jpg", + "version": "0.4.0", + "type": "iot-systems", + "published": "2015-04-16T19:14:41.319Z" + }, + "daikin": { + "meta": "https://raw.githubusercontent.com/Apollon77/yunkong2.daikin/master/io-package.json", + "icon": "https://raw.githubusercontent.com/Apollon77/yunkong2.daikin/master/admin/daikin.jpg", + "version": "0.2.3", + "type": "climate-control", + "published": "2017-03-29T22:00:25.803Z" + }, + "daswetter": { + "meta": "https://raw.githubusercontent.com/rg-engineering/yunkong2.daswetter/master/io-package.json", + "icon": "https://raw.githubusercontent.com/rg-engineering/yunkong2.daswetter/master/admin/daswettercom.png", + "version": "1.0.3", + "type": "weather", + "published": "2017-05-14T10:42:31.173Z" + }, + "deconz": { + "meta": "https://raw.githubusercontent.com/jey-cee/yunkong2.deconz/master/io-package.json", + "icon": "https://raw.githubusercontent.com/jey-cee/yunkong2.deconz/master/admin/deconz.png", + "version": "0.1.1", + "type": "hardware", + "published": "2018-01-02T20:06:31.173Z" + }, + "discovery": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.discovery/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.discovery/master/admin/discovery.png", + "published": "2017-03-22T06:30:11.864Z", + "version": "1.1.0", + "type": "general" + }, + "dwd": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.dwd/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.dwd/master/admin/dwd.png", + "version": "2.2.2", + "type": "weather", + "published": "2015-01-02T17:23:09.173Z" + }, + "egigeozone": { + "meta": "https://raw.githubusercontent.com/BasGo/yunkong2.egigeozone/master/io-package.json", + "icon": "https://raw.githubusercontent.com/BasGo/yunkong2.egigeozone/master/admin/egigeozone.png", + "version": "0.1.2", + "type": "geoposition", + "published": "2017-09-28T10:21:33.986Z" + }, + "ebus": { + "meta": "https://raw.githubusercontent.com/rg-engineering/yunkong2.ebus/master/io-package.json", + "icon": "https://raw.githubusercontent.com/rg-engineering/yunkong2.ebus/master/admin/myhomecontrol.png", + "version": "0.5.1", + "type": "hardware", + "published": "2018-03-03T13:10:00.000Z" + }, + "email": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.email/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.email/master/admin/email.png", + "version": "1.0.2", + "type": "messaging", + "published": "2015-01-02T23:50:13.927Z" + }, + "epson_stylus_px830": { + "meta": "https://raw.githubusercontent.com/Pix---/yunkong2.epson_stylus_px830/master/io-package.json", + "icon": "https://raw.githubusercontent.com/Pix---/yunkong2.epson_stylus_px830/master/admin/epson_stylus_px830.png", + "version": "0.0.2", + "type": "infrastructure", + "published": "2016-06-27T10:18:05.711Z" + }, + "fakeroku": { + "meta": "https://raw.githubusercontent.com/Pmant/yunkong2.fakeroku/master/io-package.json", + "icon": "https://raw.githubusercontent.com/Pmant/yunkong2.fakeroku/master/admin/fakeroku.png", + "version": "0.2.1", + "type": "multimedia", + "published": "2017-02-05T13:08:56.966Z" + }, + "feiertage": { + "meta": "https://raw.githubusercontent.com/Pix---/yunkong2.feiertage/master/io-package.json", + "icon": "https://raw.githubusercontent.com/Pix---/yunkong2.feiertage/master/admin/feiertage.png", + "version": "1.0.0", + "type": "date-and-time", + "published": "2016-04-30T13:42:40.309Z" + }, + "fhem": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.fhem/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.fhem/master/admin/fhem.png", + "version": "0.4.1", + "type": "iot-systems", + "published": "2016-05-31T20:33:16.704Z" + }, + "find-my-iphone": { + "meta": "https://raw.githubusercontent.com/soef/yunkong2.find-my-iphone/master/io-package.json", + "icon": "https://raw.githubusercontent.com/soef/yunkong2.find-my-iphone/master/admin/find-my-iphone.png", + "version": "0.2.15", + "type": "geoposition", + "published": "2016-10-10T18:31:04.566Z" + }, + "firetv": { + "meta": "https://raw.githubusercontent.com/soef/yunkong2.firetv/master/io-package.json", + "icon": "https://raw.githubusercontent.com/soef/yunkong2.firetv/master/admin/firetv.png", + "type": "multimedia", + "published": "2017-05-18T20:30:53.871Z", + "version": "0.0.27" + }, + "flot": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.flot/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.flot/master/admin/flot.png", + "version": "1.7.7", + "type": "visualization", + "published": "2015-06-10T19:35:14.599Z" + }, + "foobar2000": { + "meta": "https://raw.githubusercontent.com/instalator/yunkong2.foobar2000/master/io-package.json", + "icon": "https://raw.githubusercontent.com/instalator/yunkong2.foobar2000/master/admin/foobar2000.png", + "version": "1.0.0", + "type": "multimedia", + "published": "2016-10-20T10:58:40.127Z" + }, + "fritzbox": { + "meta": "https://raw.githubusercontent.com/ruhr70/yunkong2.fritzbox/master/io-package.json", + "icon": "https://raw.githubusercontent.com/ruhr70/yunkong2.fritzbox/master/admin/fritzbox.png", + "version": "0.2.1", + "type": "infrastructure", + "published": "2015-07-04T18:44:53.023Z" + }, + "fritzdect": { + "meta": "https://raw.githubusercontent.com/foxthefox/yunkong2.fritzdect/master/io-package.json", + "icon": "https://raw.githubusercontent.com/foxthefox/yunkong2.fritzdect/master/admin/fritzdect_logo.png", + "version": "0.1.1", + "type": "hardware", + "published": "2018-02-11T14:19:40.633Z" + }, + "fronius": { + "meta": "https://raw.githubusercontent.com/ldittmar81/yunkong2.fronius/master/io-package.json", + "icon": "https://raw.githubusercontent.com/ldittmar81/yunkong2.fronius/master/admin/fronius.png", + "version": "1.0.2", + "type": "energy", + "published": "2017-02-23T23:25:42.603Z" + }, + "fullcalendar": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.fullcalendar/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.fullcalendar/master/admin/fullcalendar.png", + "type": "date-and-time", + "version": "0.2.4", + "published": "2017-06-12T22:10:16.740Z" + }, + "g-homa": { + "meta": "https://raw.githubusercontent.com/AlCalzone/yunkong2.g-homa/master/io-package.json", + "icon": "https://raw.githubusercontent.com/AlCalzone/yunkong2.g-homa/master/admin/g-homa.png", + "version": "0.2.0", + "type": "iot-systems", + "published": "2017-08-23T11:28:42.347Z" + }, + "geofency": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.geofency/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.geofency/master/admin/geofency.png", + "version": "0.3.2", + "type": "geoposition", + "published": "2016-01-15T20:18:56.071Z" + }, + "haier": { + "meta": "https://raw.githubusercontent.com/instalator/yunkong2.haier/master/io-package.json", + "icon": "https://raw.githubusercontent.com/instalator/yunkong2.haier/master/admin/haier.png", + "version": "1.0.0", + "type": "climate-control", + "published": "2017-08-26T18:38:38.677Z" + }, + "harmony": { + "meta": "https://raw.githubusercontent.com/Pmant/yunkong2.harmony/master/io-package.json", + "icon": "https://raw.githubusercontent.com/Pmant/yunkong2.harmony/master/admin/harmony.png", + "version": "0.9.0", + "type": "multimedia", + "published": "2015-08-18T08:32:32.461Z" + }, + "hid": { + "meta": "https://raw.githubusercontent.com/soef/yunkong2.hid/master/io-package.json", + "icon": "https://raw.githubusercontent.com/soef/yunkong2.hid/master/admin/hid.png", + "version": "0.1.16", + "type": "utility", + "published": "2016-01-26T00:32:41.036Z" + }, + "history": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.history/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.history/master/admin/history.png", + "version": "1.7.2", + "type": "storage", + "published": "2015-01-02T21:07:06.894Z" + }, + "hm-rega": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.hm-rega/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.hm-rega/master/admin/homematic.png", + "version": "1.7.0", + "type": "iot-systems", + "published": "2015-01-02T23:31:13.087Z" + }, + "hm-rpc": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.hm-rpc/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.hm-rpc/master/admin/homematic.png", + "version": "1.6.0", + "type": "iot-systems", + "published": "2015-01-02T23:35:36.140Z" + }, + "hmm": { + "meta": "https://raw.githubusercontent.com/husky-koglhof/yunkong2.hmm/master/io-package.json", + "icon": "https://raw.githubusercontent.com/husky-koglhof/yunkong2.hmm/master/admin/homematic.png", + "version": "1.0.1", + "type": "iot-systems", + "published": "2015-02-26T17:41:33.345Z" + }, + "homepilot": { + "meta": "https://raw.githubusercontent.com/Pix---/yunkong2.homepilot/master/io-package.json", + "icon": "https://raw.githubusercontent.com/Pix---/yunkong2.homepilot/master/admin/homepilot.png", + "version": "0.3.1", + "type": "iot-systems", + "published": "2016-07-09T11:17:48.404Z" + }, + "hue": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.hue/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.hue/master/admin/hue.jpeg", + "version": "0.6.9", + "type": "lighting", + "published": "2015-03-04T22:35:03.350Z" + }, + "ical": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.ical/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.ical/master/admin/ical.png", + "version": "1.5.2", + "type": "date-and-time", + "published": "2015-02-22T11:33:05.718Z" + }, + "icons-addictive-flavour-png": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.icons-addictive-flavour-png/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.icons-addictive-flavour-png/master/admin/icons-addictive-flavour-png.png", + "version": "0.1.0", + "type": "visualization-icons", + "published": "2015-05-20T18:40:16.922Z" + }, + "icons-fatcow-hosting": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.icons-fatcow-hosting/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.icons-fatcow-hosting/master/admin/icons-fatcow-hosting.png", + "version": "0.1.0", + "type": "visualization-icons", + "published": "2016-11-28T20:43:40.799Z" + }, + "icons-icons8": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.icons-icons8/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.icons-icons8/master/admin/icons8.png", + "version": "0.0.1", + "type": "visualization-icons", + "published": "2016-04-30T08:36:47.965Z" + }, + "icons-material-png": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.icons-material-png/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.icons-material-png/master/admin/icons-material-png.png", + "version": "0.1.0", + "type": "visualization-icons", + "published": "2015-05-20T18:40:46.087Z" + }, + "icons-material-svg": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.icons-material-svg/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.icons-material-svg/master/admin/icons-material-svg.png", + "version": "0.1.0", + "type": "visualization-icons", + "published": "2015-05-20T18:41:10.070Z" + }, + "icons-mfd-png": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.icons-mfd-png/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.icons-mfd-png/master/admin/icons-mfd-png.png", + "version": "1.0.2", + "type": "visualization-icons", + "published": "2015-05-20T18:42:58.330Z" + }, + "icons-mfd-svg": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.icons-mfd-svg/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.icons-mfd-svg/master/admin/icons-mfd-svg.png", + "version": "1.0.2", + "type": "visualization-icons", + "published": "2015-05-20T18:39:41.938Z" + }, + "icons-open-icon-library-png": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.icons-open-icon-library-png/master/io-package.json", + "url": "https://github.com/yunkong2/yunkong2.icons-open-icon-library-png/tarball/master", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.icons-open-icon-library-png/master/admin/icons-open-icon-library-png.png", + "version": "0.1.2", + "type": "visualization-icons", + "published": "2015-05-31T17:49:40.646Z" + }, + "icons-ultimate-png": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.icons-ultimate-png/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.icons-ultimate-png/master/admin/icons-ultimate-png.png", + "version": "1.0.1", + "type": "visualization-icons", + "published": "2015-05-20T18:45:26.242Z" + }, + "influxdb": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.influxdb/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.influxdb/master/admin/influxdb.png", + "version": "1.5.2", + "type": "storage", + "published": "2015-12-14T22:28:26.453Z" + }, + "innogy-smarthome": { + "meta": "https://raw.githubusercontent.com/PArns/yunkong2.innogy-smarthome/master/io-package.json", + "icon": "https://raw.githubusercontent.com/PArns/yunkong2.innogy-smarthome/master/admin/innogy-smarthome.png", + "version": "0.1.17", + "type": "iot-systems", + "published": "2017-01-07T12:19:30.574Z" + }, + "javascript": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.javascript/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.javascript/master/admin/js.jpeg", + "version": "3.6.4", + "type": "logic", + "published": "2015-01-02T23:37:49.644Z" + }, + "jeelink": { + "meta": "https://raw.githubusercontent.com/foxthefox/yunkong2.jeelink/master/io-package.json", + "icon": "https://raw.githubusercontent.com/foxthefox/yunkong2.jeelink/master/admin/jeelab_logo.png", + "version": "0.0.4", + "type": "hardware", + "published": "2018-02-11T16:19:40.633Z" + }, + "js-controller": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.js-controller/master/io-package.json", + "url": "https://github.com/yunkong2/yunkong2.js-controller/tarball/master", + "version": "1.2.3", + "type": "general", + "published": "2015-01-03T14:47:20.911Z" + }, + "js2fs": { + "meta": "https://raw.githubusercontent.com/soef/yunkong2.js2fs/master/io-package.json", + "icon": "https://raw.githubusercontent.com/soef/yunkong2.js2fs/master/admin/js2fs.png", + "version": "0.1.3", + "type": "utility", + "published": "2017-07-10T13:01:38.945Z" + }, + "km200": { + "meta": "https://raw.githubusercontent.com/frankjoke/yunkong2.km200/master/io-package.json", + "icon": "https://raw.githubusercontent.com/frankjoke/yunkong2.km200/master/admin/km200.png", + "version": "1.1.6", + "type": "climate-control", + "published": "2016-11-18T21:35:17.155Z" + }, + "knx": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.knx/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.knx/master/admin/knx.png", + "version": "0.8.6", + "type": "iot-systems", + "published": "2015-09-27T07:32:43.557Z" + }, + "kodi": { + "meta": "https://raw.githubusercontent.com/instalator/yunkong2.kodi/master/io-package.json", + "icon": "https://raw.githubusercontent.com/instalator/yunkong2.kodi/master/admin/kodi.png", + "version": "1.0.0", + "type": "multimedia", + "published": "2016-05-22T15:47:37.487Z" + }, + "landroid": { + "meta": "https://raw.githubusercontent.com/ldittmar81/yunkong2.landroid/master/io-package.json", + "icon": "https://raw.githubusercontent.com/ldittmar81/yunkong2.landroid/master/admin/landroid.png", + "version": "1.0.0", + "type": "garden", + "published": "2017-02-08T23:54:56.311Z" + }, + "landroid-s": { + "meta": "https://raw.githubusercontent.com/MeisterTR/yunkong2.landroid-s/master/io-package.json", + "icon": "https://raw.githubusercontent.com/MeisterTR/yunkong2.landroid-s/master/admin/landroid-s.png", + "version": "2.0.0", + "type": "garden", + "published": "2017-07-22T11:45:31.427Z" + }, + "lgtv": { + "meta": "https://raw.githubusercontent.com/SMundt/yunkong2.lgtv/master/io-package.json", + "icon": "https://raw.githubusercontent.com/SMundt/yunkong2.lgtv/master/admin/lgtv.png", + "version": "1.0.2", + "type": "multimedia", + "published": "2016-09-05T17:34:06.772Z" + }, + "lgtv-rs": { + "meta": "https://raw.githubusercontent.com/instalator/yunkong2.lgtv-rs/master/io-package.json", + "icon": "https://raw.githubusercontent.com/instalator/yunkong2.lgtv-rs/master/admin/lg.png", + "version": "0.0.4", + "type": "multimedia", + "published": "2017-09-07T14:22:57.452Z" + }, + "lifx": { + "meta": "https://raw.githubusercontent.com/foxthefox/yunkong2.lifx/master/io-package.json", + "icon": "https://raw.githubusercontent.com/foxthefox/yunkong2.lifx/master/admin/lifx_logo.png", + "version": "0.0.5", + "type": "lighting", + "published": "2018-02-11T13:26:57.226Z" + }, + "lightify": { + "meta": "https://raw.githubusercontent.com/soef/yunkong2.lightify/master/io-package.json", + "icon": "https://raw.githubusercontent.com/soef/yunkong2.lightify/master/admin/lightify.png", + "version": "0.2.16", + "type": "lighting", + "published": "2016-03-16T00:01:08.739Z" + }, + "loxone": { + "meta": "https://raw.githubusercontent.com/UncleSamSwiss/yunkong2.loxone/master/io-package.json", + "icon": "https://raw.githubusercontent.com/UncleSamSwiss/yunkong2.loxone/master/admin/loxone.png", + "version": "0.4.0", + "type": "iot-systems", + "published": "2017-04-24T19:18:46.399Z" + }, + "maxcube": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.maxcube/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.maxcube/master/admin/maxcube.png", + "version": "0.1.2", + "type": "climate-control", + "published": "2017-06-05T20:26:24.569Z" + }, + "maxcul": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.maxcul/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.maxcul/master/admin/maxcul.png", + "type": "iot-systems", + "published": "2017-04-29T06:43:12.134Z", + "version": "0.3.0" + }, + "megad": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.megad/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.megad/master/admin/megad.png", + "version": "1.2.1", + "type": "hardware", + "published": "2015-03-20T23:02:25.119Z" + }, + "megadd": { + "meta": "https://raw.githubusercontent.com/ausHaus/yunkong2.megadd/master/io-package.json", + "icon": "https://raw.githubusercontent.com/ausHaus/yunkong2.megadd/master/admin/megad.png", + "published": "2017-03-16T15:53:22.103Z", + "version": "0.2.0", + "type": "hardware" + }, + "megaesp": { + "meta": "https://raw.githubusercontent.com/ausHaus/yunkong2.megaesp/master/io-package.json", + "icon": "https://raw.githubusercontent.com/ausHaus/yunkong2.megaesp/master/admin/megad.png", + "published": "2017-03-16T15:53:22.103Z", + "version": "0.1.0", + "type": "hardware" + }, + "miele": { + "meta": "https://raw.githubusercontent.com/soef/yunkong2.miele/master/io-package.json", + "icon": "https://raw.githubusercontent.com/soef/yunkong2.miele/master/admin/xmiele.png", + "version": "0.1.5", + "type": "household", + "published": "2016-01-23T09:52:24.818Z" + }, + "mihome": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.mihome/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.mihome/master/admin/mihome.png", + "type": "iot-systems", + "version": "1.0.5", + "published": "2017-06-05T17:40:26.665Z" + }, + "mihome-plug": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.mihome-plug/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.mihome-plug/master/admin/mihome-plug.png", + "type": "hardware", + "version": "0.1.1", + "published": "2017-08-04T16:03:47.676Z" + }, + "mihome-vacuum": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.mihome-vacuum/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.mihome-vacuum/master/admin/mihome-vacuum.png", + "published": "2017-04-10T17:10:21.690Z", + "version": "0.6.0", + "type": "household" + }, + "mikrotik": { + "meta": "https://raw.githubusercontent.com/instalator/yunkong2.mikrotik/master/io-package.json", + "icon": "https://raw.githubusercontent.com/instalator/yunkong2.mikrotik/master/admin/mikrotik.png", + "version": "1.0.0", + "type": "hardware", + "published": "2017-07-27T16:46:54.455Z" + }, + "milight": { + "meta": "https://raw.githubusercontent.com/foxthefox/yunkong2.milight/master/io-package.json", + "icon": "https://raw.githubusercontent.com/foxthefox/yunkong2.milight/master/admin/easybulb_logo.png", + "version": "0.3.4", + "type": "lighting", + "published": "2018-02-11T14:02:47.304Z" + }, + "milight-smart-light": { + "meta": "https://raw.githubusercontent.com/Steiger04/yunkong2.milight-smart-light/master/io-package.json", + "icon": "https://raw.githubusercontent.com/Steiger04/yunkong2.milight-smart-light/master/admin/lib/images/milight-smart-light-md.png", + "version": "0.0.5", + "type": "lighting", + "published": "2017-08-29T11:37:57.432Z" + }, + "mobile": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.mobile/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.mobile/master/admin/mobile.png", + "version": "0.4.11", + "type": "visualization", + "published": "2015-11-08T12:28:29.742Z" + }, + "modbus": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.modbus/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.modbus/master/admin/modbus.png", + "version": "1.1.0", + "type": "protocols", + "published": "2015-10-14T20:14:18.945Z" + }, + "mpd": { + "meta": "https://raw.githubusercontent.com/instalator/yunkong2.mpd/master/io-package.json", + "icon": "https://raw.githubusercontent.com/instalator/yunkong2.mpd/master/admin/mpd.png", + "version": "1.0.0", + "type": "multimedia", + "published": "2016-12-15T15:55:12.928Z" + }, + "mqtt": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.mqtt/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.mqtt/master/admin/mqtt.png", + "version": "1.4.1", + "type": "protocols", + "published": "2014-11-28T14:42:57.910Z" + }, + "mqtt-client": { + "meta": "https://raw.githubusercontent.com/Pmant/yunkong2.mqtt-client/master/io-package.json", + "icon": "https://raw.githubusercontent.com/Pmant/yunkong2.mqtt-client/master/admin/mqtt-client.png", + "version": "1.0.1", + "type": "protocols", + "published": "2016-06-19T20:44:36.935Z" + }, + "musiccast": { + "meta": "https://raw.githubusercontent.com/foxthefox/yunkong2.musiccast/master/io-package.json", + "icon": "https://raw.githubusercontent.com/foxthefox/yunkong2.musiccast/master/admin/musiccast.png", + "version": "0.0.7", + "type": "multimedia", + "published": "2018-02-11T16:19:40.633Z" + }, + "mysensors": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.mysensors/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.mysensors/master/admin/mysensors.png", + "version": "1.2.0", + "type": "iot-systems", + "published": "2016-02-24T21:13:56.700Z" + }, + "netatmo": { + "meta": "https://raw.githubusercontent.com/PArns/yunkong2.netatmo/master/io-package.json", + "icon": "https://raw.githubusercontent.com/PArns/yunkong2.netatmo/master/admin/netatmo.png", + "version": "1.3.0", + "type": "weather", + "published": "2016-06-01T20:14:22.572Z" + }, + "node-red": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.node-red/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.node-red/master/admin/node-red.png", + "version": "1.5.1", + "type": "logic", + "published": "2015-01-02T21:28:03.378Z" + }, + "noolite": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.noolite/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.noolite/master/admin/noolite.png", + "version": "0.0.1", + "type": "iot-systems", + "published": "2016-03-12T21:57:35.479Z" + }, + "nut": { + "meta": "https://raw.githubusercontent.com/Apollon77/yunkong2.nut/master/io-package.json", + "icon": "https://raw.githubusercontent.com/Apollon77/yunkong2.nut/master/admin/nut.png", + "version": "1.1.1", + "type": "hardware", + "published": "2016-07-06T10:12:46.812Z" + }, + "onkyo": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.onkyo/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.onkyo/master/admin/onkyo.png", + "version": "0.2.1", + "type": "multimedia", + "published": "2015-03-22T15:08:19.799Z" + }, + "openhab": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.openhab/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.openhab/master/admin/openhab.png", + "type": "iot-systems", + "published": "2017-05-18T20:30:53.876Z", + "version": "0.2.1" + }, + "opi": { + "meta": "https://raw.githubusercontent.com/PrinzEisenherz1/yunkong2.opi/master/io-package.json", + "icon": "https://raw.githubusercontent.com/PrinzEisenherz1/yunkong2.opi/master/admin/opi.png", + "type": "hardware", + "published": "2018-01-24T14:38:00.000Z", + "version": "0.1.1" + }, + "owfs": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.owfs/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.owfs/master/admin/owfs.png", + "version": "0.5.0", + "type": "hardware", + "published": "2015-04-16T21:20:18.623Z" + }, + "owntracks": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.owntracks/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.owntracks/master/admin/owntracks.png", + "version": "0.2.0", + "type": "geoposition", + "published": "2016-09-04T17:18:10.022Z" + }, + "panasonic-viera": { + "meta": "https://raw.githubusercontent.com/ldittmar81/yunkong2.panasonic-viera/master/io-package.json", + "icon": "https://raw.githubusercontent.com/ldittmar81/yunkong2.panasonic-viera/master/admin/panasonic-viera.png", + "version": "0.1.0", + "published": "2017-10-02T02:11:12.134Z", + "type": "multimedia" + }, + "parser": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.parser/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.parser/master/admin/parser.png", + "version": "1.0.1", + "type": "logic", + "published": "2017-01-21T17:30:41.954Z" + }, + "paw": { + "meta": "https://raw.githubusercontent.com/bondrogeen/yunkong2.paw/master/io-package.json", + "icon": "https://raw.githubusercontent.com/bondrogeen/yunkong2.paw/master/admin/paw.png", + "type": "hardware", + "published": "2017-04-29T06:43:12.134Z", + "version": "0.0.7" + }, + "phantomjs": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.phantomjs/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.phantomjs/master/admin/phantomjs.png", + "version": "1.0.0", + "type": "utility", + "published": "2016-04-29T06:04:14.612Z" + }, + "piface": { + "meta": "https://raw.githubusercontent.com/eisbaeeer/yunkong2.piface/master/io-package.json", + "icon": "https://raw.githubusercontent.com/eisbaeeer/yunkong2.piface/master/admin/piface.png", + "version": "1.0.0", + "type": "hardware", + "published": "2016-04-29T12:31:59.913Z" + }, + "pimatic": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.pimatic/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.pimatic/master/admin/pimatic.png", + "version": "0.1.5", + "type": "iot-systems", + "published": "2017-03-15T21:26:19.592Z" + }, + "ping": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.ping/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.ping/master/admin/ping.png", + "version": "1.3.2", + "type": "network", + "published": "2015-01-02T23:47:36.408Z" + }, + "places": { + "meta": "https://raw.githubusercontent.com/BasGo/yunkong2.places/master/io-package.json", + "icon": "https://raw.githubusercontent.com/BasGo/yunkong2.places/master/admin/places.png", + "version": "0.5.1", + "type": "geoposition", + "published": "2018-03-21T19:25:00.000Z" + }, + "plexconnect": { + "meta": "https://raw.githubusercontent.com/eisbaeeer/yunkong2.plexconnect/master/io-package.json", + "icon": "https://raw.githubusercontent.com/eisbaeeer/yunkong2.plexconnect/master/admin/plex-logo.png", + "type": "multimedia", + "published": "2018-02-23T20:39:21.007Z" + }, + "proxy": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.proxy/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.proxy/master/admin/proxy.png", + "version": "1.0.1", + "type": "network", + "published": "2017-03-11T23:57:45.008Z" + }, + "pushbullet": { + "meta": "https://raw.githubusercontent.com/Jens1809/yunkong2.pushbullet/master/io-package.json", + "icon": "https://raw.githubusercontent.com/Jens1809/yunkong2.pushbullet/master/admin/pushbullet.png", + "version": "0.0.11", + "type": "messaging", + "published": "2015-07-25T20:26:50.201Z" + }, + "pushover": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.pushover/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.pushover/master/admin/pushover.png", + "version": "1.0.4", + "type": "messaging", + "published": "2015-01-02T23:54:28.708Z" + }, + "pushsafer": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.pushsafer/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.pushsafer/master/admin/pushsafer.png", + "version": "0.1.8", + "type": "messaging", + "published": "2016-09-19T21:09:18.382Z" + }, + "radar": { + "meta": "https://raw.githubusercontent.com/frankjoke/yunkong2.radar/master/io-package.json", + "icon": "https://raw.githubusercontent.com/frankjoke/yunkong2.radar/master/admin/radar.png", + "version": "1.1.3", + "type": "network", + "published": "2016-11-18T21:34:57.045Z" + }, + "rflink": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.rflink/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.rflink/master/admin/rflink.png", + "version": "1.2.0", + "type": "iot-systems", + "published": "2016-10-16T10:42:10.989Z" + }, + "rickshaw": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.rickshaw/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.rickshaw/master/admin/rickshaw.png", + "version": "0.4.5", + "type": "visualization", + "published": "2015-01-02T20:46:10.368Z" + }, + "rpi2": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.rpi2/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.rpi2/master/admin/rpi.png", + "version": "0.3.2", + "type": "hardware", + "published": "2016-10-23T14:16:37.202Z" + }, + "rwe-smarthome": { + "meta": "https://raw.githubusercontent.com/PArns/yunkong2.rwe-smarthome/master/io-package.json", + "icon": "https://raw.githubusercontent.com/PArns/yunkong2.rwe-smarthome/master/admin/rwe-smarthome.png", + "version": "0.1.11", + "type": "iot-systems", + "published": "2016-04-03T19:31:17.154Z" + }, + "s7": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.s7/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.s7/master/admin/S7.png", + "version": "1.1.3", + "type": "iot-systems", + "published": "2015-04-20T18:35:15.020Z" + }, + "samsung": { + "meta": "https://raw.githubusercontent.com/soef/yunkong2.samsung/master/io-package.json", + "icon": "https://raw.githubusercontent.com/soef/yunkong2.samsung/master/admin/samsung.png", + "version": "0.2.9", + "type": "multimedia", + "published": "2016-01-16T17:36:01.791Z" + }, + "sayit": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.sayit/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.sayit/master/admin/sayit.png", + "version": "1.6.7", + "type": "multimedia", + "published": "2015-02-14T20:00:09.375Z" + }, + "sbfspot": { + "meta": "https://raw.githubusercontent.com/rg-engineering/yunkong2.sbfspot/master/io-package.json", + "icon": "https://raw.githubusercontent.com/rg-engineering/yunkong2.sbfspot/master/admin/myhomecontrol.png", + "version": "2.2.1", + "type": "hardware", + "published": "2017-06-03T14:49:48.110Z" + }, + "scenes": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.scenes/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.scenes/master/admin/scenes.png", + "version": "1.0.2", + "type": "logic", + "published": "2015-08-09T09:01:54.033Z" + }, + "simple-api": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.simple-api/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.simple-api/master/admin/simple-api.png", + "version": "1.6.2", + "type": "communication", + "published": "2015-02-06T06:54:32.754Z" + }, + "smartmeter": { + "meta": "https://raw.githubusercontent.com/Apollon77/yunkong2.smartmeter/master/io-package.json", + "icon": "https://raw.githubusercontent.com/Apollon77/yunkong2.smartmeter/master/admin/smartmeter.png", + "version": "1.1.0", + "type": "energy", + "published": "2017-01-30T20:48:39.862Z" + }, + "socketio": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.socketio/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.socketio/master/admin/socketio.png", + "version": "1.8.6", + "type": "communication", + "published": "2015-01-02T20:43:54.368Z" + }, + "solarwetter": { + "meta": "https://raw.githubusercontent.com/Pix---/yunkong2.solarwetter/master/io-package.json", + "icon": "https://raw.githubusercontent.com/Pix---/yunkong2.solarwetter/master/admin/solarwetter.png", + "version": "1.0.0", + "type": "weather", + "published": "2016-06-01T10:34:25.177Z" + }, + "sonoff": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.sonoff/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.sonoff/master/admin/sonoff.png", + "type": "lighting", + "version": "1.0.0", + "published": "2017-10-05T18:49:38.731Z" + }, + "sonos": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.sonos/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.sonos/master/admin/sonos.png", + "version": "1.6.2", + "type": "multimedia", + "published": "2015-01-02T21:25:03.373Z" + }, + "sony-bravia": { + "meta": "https://raw.githubusercontent.com/ldittmar81/yunkong2.sony-bravia/master/io-package.json", + "icon": "https://raw.githubusercontent.com/ldittmar81/yunkong2.sony-bravia/master/admin/sony-bravia.png", + "version": "0.1.0", + "published": "2017-10-02T23:46:12.134Z", + "type": "multimedia" + }, + "sql": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.sql/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.sql/master/admin/sql.png", + "version": "1.6.9", + "type": "storage", + "published": "2015-12-06T16:07:51.458Z" + }, + "squeezebox": { + "meta": "https://raw.githubusercontent.com/UncleSamSwiss/yunkong2.squeezebox/master/io-package.json", + "icon": "https://raw.githubusercontent.com/UncleSamSwiss/yunkong2.squeezebox/master/admin/squeezebox.png", + "version": "0.2.1", + "type": "multimedia", + "published": "2016-01-16T12:56:24.243Z" + }, + "starline": { + "meta": "https://raw.githubusercontent.com/instalator/yunkong2.starline/master/io-package.json", + "icon": "https://raw.githubusercontent.com/instalator/yunkong2.starline/master/admin/starline.png", + "version": "1.0.0", + "type": "alarm", + "published": "2016-04-20T13:50:38.550Z" + }, + "tankerkoenig": { + "meta": "https://raw.githubusercontent.com/Pix---/yunkong2.tankerkoenig/master/io-package.json", + "icon": "https://raw.githubusercontent.com/Pix---/yunkong2.tankerkoenig/master/admin/tankerkoenig.png", + "version": "1.0.4", + "type": "misc-data", + "published": "2016-06-12T14:59:04.116Z" + }, + "telegram": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.telegram/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.telegram/master/admin/telegram.png", + "version": "1.1.4", + "type": "messaging", + "published": "2016-02-14T13:00:28.242Z" + }, + "terminal": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.terminal/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.terminal/master/admin/terminal.png", + "version": "0.1.2", + "type": "utility", + "published": "2015-08-25T19:09:39.972Z" + }, + "text2command": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.text2command/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.text2command/master/admin/text2command.png", + "version": "1.1.6", + "type": "logic", + "published": "2016-02-09T22:46:16.344Z" + }, + "tr-064": { + "meta": "https://raw.githubusercontent.com/soef/yunkong2.tr-064/master/io-package.json", + "icon": "https://raw.githubusercontent.com/soef/yunkong2.tr-064/master/admin/tr-064.png", + "version": "0.4.14", + "type": "infrastructure", + "published": "2016-01-16T19:27:11.122Z" + }, + "tradfri": { + "meta": "https://raw.githubusercontent.com/AlCalzone/yunkong2.tradfri/master/io-package.json", + "icon": "https://raw.githubusercontent.com/AlCalzone/yunkong2.tradfri/master/admin/tradfri.png", + "version": "1.1.10", + "type": "lighting", + "published": "2017-08-23T11:33:34.827Z" + }, + "tvspielfilm": { + "meta": "https://raw.githubusercontent.com/Pix---/yunkong2.tvspielfilm/master/io-package.json", + "icon": "https://raw.githubusercontent.com/Pix---/yunkong2.tvspielfilm/master/admin/tvspielfilm.png", + "version": "1.0.4", + "type": "misc-data", + "published": "2016-05-12T09:49:00.541Z" + }, + "unifi": { + "meta": "https://raw.githubusercontent.com/jens-maus/yunkong2.unifi/master/io-package.json", + "icon": "https://raw.githubusercontent.com/jens-maus/yunkong2.unifi/master/admin/unifi.png", + "version": "0.3.1", + "type": "network", + "published": "2017-01-18T08:20:08.834Z" + }, + "upnp": { + "meta": "https://raw.githubusercontent.com/Jey-Cee/yunkong2.upnp/master/io-package.json", + "icon": "https://raw.githubusercontent.com/Jey-Cee/yunkong2.upnp/master/admin/upnp-discovery.png", + "version": "0.3.6", + "type": "network", + "published": "2016-10-31T18:40:22.374Z" + }, + "vcard": { + "meta": "https://raw.githubusercontent.com/hometm/yunkong2.vcard/master/io-package.json", + "icon": "https://raw.githubusercontent.com/hometm/yunkong2.vcard/master/admin/vcard.png", + "version": "0.0.9", + "type": "misc-data", + "published": "2015-10-02T08:45:00.272Z" + }, + "viessmann": { + "meta": "https://raw.githubusercontent.com/misanorot/yunkong2.viessmann/master/io-package.json", + "icon": "https://raw.githubusercontent.com/misanorot/yunkong2.viessmann/master/admin/viessmann.png", + "version": "0.4.0", + "type": "climate-control", + "published": "2017-10-16T19:37:29.283Z" + }, + "vis": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis/master/admin/vis.png", + "version": "1.1.1", + "type": "visualization", + "published": "2015-01-03T16:36:01.398Z" + }, + "vis-bars": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-bars/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-bars/master/admin/bars.png", + "version": "0.1.4", + "type": "visualization-widgets", + "published": "2015-08-06T17:13:20.115Z" + }, + "vis-canvas-gauges": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-canvas-gauges/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-canvas-gauges/master/admin/vis-canvas-gauges.png", + "version": "0.1.5", + "type": "visualization-widgets", + "published": "2016-09-29T20:28:59.797Z" + }, + "vis-colorpicker": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-colorpicker/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-colorpicker/master/admin/colorpicker.png", + "version": "1.1.1", + "type": "visualization-widgets", + "published": "2015-07-14T20:44:24.530Z" + }, + "vis-fancyswitch": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-fancyswitch/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-fancyswitch/master/admin/fancyswitch.png", + "version": "1.1.0", + "type": "visualization-widgets", + "published": "2015-10-04T13:27:00.250Z" + }, + "vis-google-fonts": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-google-fonts/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-google-fonts/master/admin/vis-google-fonts.png", + "version": "0.1.0", + "type": "visualization-widgets", + "published": "2015-11-09T23:04:11.937Z" + }, + "vis-history": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-history/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-history/master/admin/vis-history.png", + "version": "0.2.7", + "type": "visualization-widgets", + "published": "2016-06-13T20:51:31.454Z" + }, + "vis-hqwidgets": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-hqwidgets/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-hqwidgets/master/admin/hqwidgets.png", + "version": "1.1.1", + "type": "visualization-widgets", + "published": "2015-07-19T16:00:19.063Z" + }, + "vis-jqui-mfd": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-jqui-mfd/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-jqui-mfd/master/admin/jqui-mfd.png", + "version": "1.0.9", + "type": "visualization-widgets", + "published": "2015-09-30T20:11:35.214Z" + }, + "vis-justgage": { + "meta": "https://raw.githubusercontent.com/Pmant/yunkong2.vis-justgage/master/io-package.json", + "icon": "https://raw.githubusercontent.com/Pmant/yunkong2.vis-justgage/master/admin/justgage.png", + "version": "1.0.0", + "type": "visualization-widgets", + "published": "2016-02-17T00:56:07.344Z" + }, + "vis-keyboard": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-keyboard/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-keyboard/master/admin/keyboard.png", + "version": "0.0.2", + "type": "visualization-widgets", + "published": "2015-10-28T20:37:47.053Z" + }, + "vis-lcars": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-lcars/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-lcars/master/admin/lcars.png", + "version": "1.0.4", + "type": "visualization-widgets", + "published": "2015-07-09T22:03:51.410Z" + }, + "vis-map": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-map/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-map/master/admin/vis-map.png", + "version": "1.0.1", + "type": "visualization-widgets", + "published": "2016-07-09T06:35:25.570Z" + }, + "vis-metro": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-metro/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-metro/master/admin/metro.png", + "version": "1.1.2", + "type": "visualization-widgets", + "published": "2015-06-28T21:47:39.618Z" + }, + "vis-players": { + "meta": "https://raw.githubusercontent.com/instalator/yunkong2.vis-players/master/io-package.json", + "icon": "https://raw.githubusercontent.com/instalator/yunkong2.vis-players/master/admin/players.png", + "version": "0.1.5", + "type": "visualization-widgets", + "published": "2016-12-29T14:56:46.555Z" + }, + "vis-plumb": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-plumb/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-plumb/master/admin/plumb.png", + "version": "1.0.1", + "type": "visualization-widgets", + "published": "2015-08-06T17:13:01.225Z" + }, + "vis-rgraph": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-rgraph/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-rgraph/master/admin/rgraph.png", + "version": "0.0.2", + "type": "visualization-widgets", + "published": "2015-10-04T15:09:12.293Z" + }, + "vis-timeandweather": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-timeandweather/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.vis-timeandweather/master/admin/timeandweather.png", + "version": "1.1.7", + "type": "visualization-widgets", + "published": "2015-10-04T15:09:43.962Z" + }, + "vis-weather": { + "meta": "https://raw.githubusercontent.com/rg-engineering/yunkong2.vis-weather/master/io-package.json", + "icon": "https://raw.githubusercontent.com/rg-engineering/yunkong2.vis-weather/master/admin/vis-weather.png", + "version": "1.2.0", + "type": "visualization-widgets", + "published": "2017-05-14T10:52:23.840Z" + }, + "weatherunderground": { + "meta": "https://raw.githubusercontent.com/dschaedl/yunkong2.weatherunderground/master/io-package.json", + "icon": "https://raw.githubusercontent.com/dschaedl/yunkong2.weatherunderground/master/admin/wu.png", + "version": "1.1.2", + "type": "weather", + "published": "2015-12-27T09:53:12.280Z" + }, + "web": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.web/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.web/master/admin/web.png", + "version": "2.1.9", + "type": "general", + "published": "2015-01-02T20:45:26.654Z" + }, + "wetty": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.wetty/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.wetty/master/admin/wetty.png", + "version": "0.1.1", + "type": "utility", + "published": "2017-02-08T23:39:19.698Z" + }, + "wifilight": { + "meta": "https://raw.githubusercontent.com/soef/yunkong2.wifilight/master/io-package.json", + "icon": "https://raw.githubusercontent.com/soef/yunkong2.wifilight/master/admin/wifilight.png", + "version": "0.2.0", + "type": "lighting", + "published": "2016-09-28T09:50:23.697Z" + }, + "wm-bus": { + "meta": "https://raw.githubusercontent.com/soef/yunkong2.wm-bus/master/io-package.json", + "icon": "https://raw.githubusercontent.com/soef/yunkong2.wm-bus/master/admin/wm-bus.png", + "version": "0.1.12", + "type": "protocols", + "published": "2016-02-13T14:06:42.570Z" + }, + "wolf": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.wolf/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.wolf/master/admin/wolf_logo.png", + "version": "0.9.2", + "type": "climate-control", + "published": "2015-10-08T23:20:16.800Z" + }, + "xs1": { + "meta": "https://raw.githubusercontent.com/frankjoke/yunkong2.xs1/master/io-package.json", + "icon": "https://raw.githubusercontent.com/frankjoke/yunkong2.xs1/master/admin/xs1.png", + "version": "1.0.2", + "type": "iot-systems", + "published": "2016-11-18T21:34:23.442Z" + }, + "yahka": { + "meta": "https://raw.githubusercontent.com/jensweigele/yunkong2.yahka/master/io-package.json", + "icon": "https://raw.githubusercontent.com/jensweigele/yunkong2.yahka/master/admin/yahka.png", + "version": "0.5.5", + "type": "iot-systems", + "published": "2016-10-05T20:29:55.035Z" + }, + "yamaha": { + "meta": "https://raw.githubusercontent.com/soef/yunkong2.yamaha/master/io-package.json", + "icon": "https://raw.githubusercontent.com/soef/yunkong2.yamaha/master/admin/yamaha.png", + "version": "0.3.18", + "type": "multimedia", + "published": "2016-01-16T17:39:17.385Z" + }, + "yr": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.yr/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.yr/master/admin/yr.png", + "version": "1.0.6", + "type": "weather", + "published": "2015-01-30T22:05:03.364Z" + }, + "zont": { + "meta": "https://raw.githubusercontent.com/kirovilya/yunkong2.zont/master/io-package.json", + "icon": "https://raw.githubusercontent.com/kirovilya/yunkong2.zont/master/admin/zont.png", + "version": "0.5.2", + "type": "communication", + "published": "2018-02-07T20:48:00.364Z" + }, + "zwave": { + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.zwave/master/io-package.json", + "icon": "https://raw.githubusercontent.com/yunkong2/yunkong2.zwave/master/admin/zwave.png", + "version": "1.0.0", + "type": "hardware", + "published": "2015-01-03T00:02:56.370Z" + } +} diff --git a/controller.js b/controller.js new file mode 100644 index 0000000..404a9e8 --- /dev/null +++ b/controller.js @@ -0,0 +1,2624 @@ +/** + * application.controller + * + * Controls Adapter-Processes + * + * Copyright 2013-2018 bluefox , + * 2014 hobbyquaker + * + */ +'use strict'; + +const schedule = require('node-schedule'); +const os = require('os'); +const fs = require('fs'); +const cp = require('child_process'); +const ioPackage = require(__dirname + '/io-package.json'); +const tools = require(__dirname + '/lib/tools'); +const version = ioPackage.common.version; +let adapterDir = __dirname.replace(/\\/g, '/'); +let zipFiles; + +// Change version in io-package.json and start grunt task to modify the version +const title = tools.appName + '.js-controller'; +process.title = title; + +let Objects; +let States; + +let semver; +let logger; +let isDaemon = false; +let callbackId = 1; +let callbacks = {}; +let hostname = tools.getHostName(); +let logList = []; +let detectIpsCount = 0; +let disconnectTimeout = null; +let connected = null; // not false, because want to detect first connection +let ipArr = []; +let lastCalculationOfIps = null; +const errorCodes = [ + 'OK', // 0 + '', // 1 + 'Adapter has invalid config or no config found', // 2 + 'Adapter disabled or invalid config', // 3 + 'invalid config: no _id found', // 4 + 'invalid config', // 5 + 'uncaught exception', // 6 + 'Adapter already running', // 7 + 'node.js: Cannot find module', // 8 + '', // 9 + 'Cannot find start file of adapter' // 10 +]; +let procs = {}; +let subscribe = {}; +let states = null; +let objects = null; +let storeTimer = null; +let isStopping = null; +let allInstancesStopped = true; +let stopTimeout = 10000; +let uncaughtExceptionCount = 0; +let installQueue = []; +let started = false; +let inputCount = 0; +let outputCount = 0; +let mhService = null; // multihost service +let uptimeStart = new Date().getTime(); + +const config = getConfig(); + +function getConfig() { + if (!fs.existsSync(tools.getConfigFileName())) { + if (process.argv.indexOf('start') !== -1) { + isDaemon = true; + logger = require(__dirname + '/lib/logger')('info', [tools.appName], true); + } else { + logger = require(__dirname + '/lib/logger')('info', [tools.appName]); + } + logger.error('host.' + hostname + ' conf/' + tools.appName + '.json missing - call node ' + tools.appName + '.js setup'); + process.exit(1); + return null; + } else { + let _config = JSON.parse(fs.readFileSync(tools.getConfigFileName())); + if (!_config.states) _config.states = {type: 'file'}; + if (!_config.objects) _config.objects = {type: 'file'}; + if (!_config.system) _config.system = {}; + return _config; + } +} + +function _startMultihost(_config, secret) { + const MHService = require(__dirname + '/lib/multihostServer.js'); + const cpus = os.cpus(); + mhService = new MHService(hostname, logger, _config, { + node: process.version, + arch: os.arch(), + model: cpus && cpus[0] && cpus[0].model ? cpus[0].model : 'unknown', + cpus: cpus ? cpus.length : 1, + mem: os.totalmem(), + ostype: os.type() + }, getIPs(), secret); +} + +function startMultihost(__config) { + let _config = __config || getConfig(); + if (_config.multihostService && _config.multihostService.enabled) { + if (mhService) { + try { + mhService.close(function () { + mhService = null; + setImmediate(function () { + startMultihost(_config); + }); + }); + return; + } catch (e) { + logger.warn('Cannot stop multihost: ' + e); + } + } + + if ((!_config.objects.host || _config.objects.host === '127.0.0.1' || _config.objects.host === 'localhost') && _config.objects.type === 'file') { + logger.warn('Host on this system is not possible, because IP address is for objects is ' + _config.objects.host); + } else + if ((_config.states.host || _config.states.host === '127.0.0.1' || _config.states.host === 'localhost') && _config.states.type === 'file') { + logger.warn('Host on this system is not possible, because IP address is for states is ' + _config.states.host); + } + + if (_config.multihostService.secure) { + objects.getObject('system.config', function (err, obj) { + if (obj && obj.native && obj.native.secret) { + tools.decryptPhrase(obj.native.secret, _config.multihostService.password, function (secret) { + _startMultihost(_config, secret); + }); + } else { + logger.error('Cannot start multihost: no system.config found') + } + }); + } else { + _startMultihost(_config, false); + } + + return true; + } else if (mhService) { + try { + mhService.close(); + mhService = null; + } catch (e) { + logger.warn('Cannot stop multihost: ' + e); + } + return false; + } +} + +// get the list of IP addresses of this host +function getIPs() { + if (!lastCalculationOfIps || new Date().getTime() - lastCalculationOfIps > 10000) { + const ifaces = os.networkInterfaces(); + lastCalculationOfIps = new Date().getTime(); + ipArr = []; + for (let dev in ifaces) { + if (!ifaces.hasOwnProperty(dev)) continue; + + /*jshint loopfunc:true */ + ifaces[dev].forEach(function (details) { + //noinspection JSUnresolvedVariable + if (!details.internal) ipArr.push(details.address); + }); + } + } + + return ipArr; +} + +// subscribe or unsubscribe loggers +function logRedirect(isActive, id) { + if (isActive) { + if (logList.indexOf(id) === -1) logList.push(id); + } else { + const pos = logList.indexOf(id); + if (pos !== -1) logList.splice(pos, 1); + } +} + +function createStates() { + return new States({ + namespace: 'host.' + hostname, + connection: config.states, + logger: logger, + hostname: hostname, + change: function (id, state) { + inputCount++; + if (!id) { + logger.error('host.' + hostname + ' change event with no ID: ' + JSON.stringify(state)); + return; + } + // If some log transporter activated or deactivated + if (id.match(/.logging$/)) { + logRedirect(state ? state.val : false, id.substring(0, id.length - '.logging'.length)); + } else + // If this is messagebox + if (id === 'messagebox.system.host.' + hostname) { + // Read it from fifo list + states.delMessage('system.host.' + hostname, state._id); + let obj = state; + if (obj) { + // If callback stored for this request + if (obj.callback && + obj.callback.ack && + obj.callback.id && + callbacks && + callbacks['_' + obj.callback.id]) { + // Call callback function + if (callbacks['_' + obj.callback.id].cb) { + callbacks['_' + obj.callback.id].cb(obj.message); + delete callbacks['_' + obj.callback.id]; + } + + // delete too old callbacks IDs + let now = (new Date()).getTime(); + for (let _id in callbacks) { + if (!callbacks.hasOwnProperty(_id)) continue; + if (now - callbacks[_id].time > 3600000) delete callbacks[_id]; + } + } else { + processMessage(obj); + } + } + } else + // If this system.adapter.NAME.0.alive + if (id.match(/^system.adapter.[^.]+\.\d+\.alive$/)) { + if (state && !state.ack) { + let enabled = state.val; + setImmediate(function () { + objects.getObject(id.substring(0, id.length - '.alive'.length), function (err, obj) { + if (err) logger.error('Cannot read object: ' + err); + if (obj && obj.common) { + // IF adapter enabled => disable it + if ((obj.common.enabled && !enabled) || (!obj.common.enabled && enabled)) { + obj.common.enabled = !!enabled; + logger.warn('host.' + hostname + ' instance "' + obj._id + '" ' + (obj.common.enabled ? 'enabled' : 'disabled')); + setImmediate(function () { + obj.from = 'system.host.' + hostname; + obj.ts = new Date().getTime(); + objects.setObject(obj._id, obj); + }); + } + } + }); + }); + } + } else + if (subscribe[id]) { + for (let i = 0; i < subscribe[id].length; i++) { + // wake up adapter + if (procs[subscribe[id][i]]) { + console.log('Wake up ' + id + ' ' + JSON.stringify(state)); + startInstance(subscribe[id][i], true); + } else { + logger.warn('host.' + hostname + ' controller Adapter subscribed on ' + id + ' does not exist!'); + } + } + } else + // Monitor activity of the adapter and restart it if stopped + if (!isStopping && id.substring(id.length - '.alive'.length) === '.alive') { + let adapter = id.substring(0, id.length - '.alive'.length); + if (procs[adapter] && + !procs[adapter].stopping && + !procs[adapter].process && + procs[adapter].config && + procs[adapter].config.common.enabled && + procs[adapter].config.common.mode === 'daemon') { + startInstance(adapter, false); + } + } + }, + connected: function () { + if (states.clearAllLogs) states.clearAllLogs(); + if (states.clearAllMessages) states.clearAllMessages(); + } + }); +} + +// create "objects" object +function createObjects() { + return new Objects({ + namespace: 'host.' + hostname, + connection: config.objects, + logger: logger, + hostname: hostname, + connected: function (type) { + // stop disconnect timeout + if (disconnectTimeout) { + clearTimeout(disconnectTimeout); + disconnectTimeout = null; + } + + if (!connected) { + logger.info('host.' + hostname + ' ' + type + ' connected'); + + if (connected === null) { + connected = true; + if (!isStopping) { + // Do not start if we still stopping the instances + checkHost(type, () => { + startMultihost(config); + setMeta(); + started = true; + getInstances(); + startAliveInterval(); + initMessageQueue(); + }); + } + } else { + connected = true; + started = true; + + // Do not start if we still stopping the instances + if (!isStopping) { + getInstances(); + startAliveInterval(); + initMessageQueue(); + } + } + } + }, + disconnected: function (/*error*/) { + if (disconnectTimeout) clearTimeout(disconnectTimeout); + disconnectTimeout = setTimeout(function () { + connected = false; + disconnectTimeout = null; + logger.warn('host.' + hostname + ' Slave controller detected disconnection. Stop all instances.'); + stopInstances(true, function () { + // if during stopping the DB has connection again + if (connected && !isStopping) { + getInstances(); + startAliveInterval(); + initMessageQueue(); + } + }); + }, config.objects.connectTimeout || 2000); + + }, + change: function (id, obj) { + if (!started || !id.match(/^system\.adapter\.[a-zA-Z0-9-_]+\.[0-9]+$/)) return; + logger.info('host.' + hostname + ' object change ' + id); + try{ + if (procs[id]) { + // known adapter + if (!obj) { + procs[id].config.common.enabled = false; + procs[id].config.common.host = null; + procs[id].config.deleted = true; + logger.info('host.' + hostname + ' object deleted ' + id); + } else { + if (procs[id].config.common.enabled && !obj.common.enabled) logger.info('host.' + hostname + ' "' + id + '" disabled'); + if (!procs[id].config.common.enabled && obj.common.enabled) logger.info('host.' + hostname + ' "' + id + '" enabled'); + procs[id].config = obj; + } + if (procs[id].process || procs[id].config.common.mode === 'schedule' || procs[id].config.common.mode === 'subscribe') { + stopInstance(id, function () { + let _ipArr = getIPs(); + + if (_ipArr.indexOf(procs[id].config.common.host) !== -1 || procs[id].config.common.host === hostname) { + if (procs[id].config.common.enabled && (!procs[id].config.common.webExtension || !procs[id].config.native.webInstance)) { + if (procs[id].restartTimer) clearTimeout(procs[id].restartTimer); + procs[id].restartTimer = setTimeout(function (_id) { + startInstance(_id); + }, 2500, id); + } + } else { + delete procs[id]; + } + }); + } else { + let __ipArr = getIPs(); + if (procs[id].config && (__ipArr.indexOf(procs[id].config.common.host) !== -1 || procs[id].config.common.host === hostname)) { + if (procs[id].config.common.enabled && (!procs[id].config.common.webExtension || !procs[id].config.native.webInstance)) { + startInstance(id); + } + } else { + delete procs[id]; + } + } + + } else if (obj && obj.common) { + let _ipArr = getIPs(); + // new adapter + if (_ipArr.indexOf(obj.common.host) !== -1 || obj.common.host === hostname) { + procs[id] = {config: obj}; + if (procs[id].config.common.enabled && (!procs[id].config.common.webExtension || !procs[id].config.native.webInstance)) { + startInstance(id); + } + } + } + } catch (err) { + logger.error('cannot process: ' + id); + } + } + }); +} + +function startAliveInterval() { + config.system = config.system || {}; + config.system.statisticsInterval = parseInt(config.system.statisticsInterval, 10) || 15000; + reportStatus(); + setInterval(reportStatus, config.system.statisticsInterval); +} + +function reportStatus() { + let id = 'system.host.' + hostname; + outputCount += 10; + states.setState(id + '.alive', {val: true, ack: true, expire: Math.floor(config.system.statisticsInterval / 1000) + 10, from: id}); + states.setState(id + '.load', {val: parseFloat(os.loadavg()[0].toFixed(2)), ack: true, from: id}); + states.setState(id + '.mem', {val: Math.round(100 * os.freemem() / os.totalmem()), ack: true, from: id}); + let mem = process.memoryUsage(); + //noinspection JSUnresolvedVariable + states.setState(id + '.memRss', {val: parseFloat((mem.rss / 1048576/* 1MB */).toFixed(2)), ack: true, from: id}); + //noinspection JSUnresolvedVariable + states.setState(id + '.memHeapTotal', {val: parseFloat((mem.heapTotal / 1048576/* 1MB */).toFixed(2)), ack: true, from: id}); + //noinspection JSUnresolvedVariable + states.setState(id + '.memHeapUsed', {val: parseFloat((mem.heapUsed / 1048576/* 1MB */).toFixed(2)), ack: true, from: id}); + // Under windows toFixed returns string ? + states.setState(id + '.uptime', {val: parseInt(process.uptime().toFixed(), 10), ack: true, from: id}); + states.setState(id + '.freemem', {val: Math.round(os.freemem() / 1048576/* 1MB */), ack: true, from: id}); + states.setState(id + '.inputCount', {val: inputCount, ack: true, from: id}); + states.setState(id + '.outputCount', {val: outputCount, ack: true, from: id}); + inputCount = 0; + outputCount = 0; +} + +function changeHost(objs, oldHostname, newHostname, callback) { + if (!objs || !objs.length) { + if (callback) callback(); + } else { + let row = objs.shift(); + if (row && row.value && row.value.common && row.value.common.host === oldHostname) { + let obj = row.value; + obj.common.host = newHostname; + logger.info('Reassign instance ' + obj._id.substring('system.adapter.'.length) + ' from ' + oldHostname + ' to ' + newHostname); + obj.from = 'system.host.' + tools.getHostName(); + obj.ts = new Date().getTime(); + objects.setObject(obj._id, obj, function (/* err */) { + setImmediate(function () { + changeHost(objs, oldHostname, newHostname, callback); + }); + }); + } else { + setImmediate(function () { + changeHost(objs, oldHostname, newHostname, callback); + }); + } + } +} + +function cleanAutoSubscribe(instance, autoInstance, callback) { + states.getState(autoInstance + '.subscribes', function (err, state) { + if (!state || !state.val) { + if (typeof callback === 'function') { + setImmediate(function () { + callback(); + }); + } + return; + } + let subs; + try { + subs = JSON.parse(state.val) + } catch (e) { + logger.error('Cannot parse subscribes: ' + state.val); + if (typeof callback === 'function') { + setImmediate(function () { + callback(); + }); + } + return; + } + let modified = false; + // look for all subscribes from this instance + for (let pattern in subs) { + if (!subs.hasOwnProperty(pattern)) continue; + for (let id in subs[pattern]) { + if (subs[pattern].hasOwnProperty(id) && id === instance) { + modified = true; + delete subs[pattern][id]; + } + } + let found = false; + for (let f in subs[pattern]) { + if (subs[pattern].hasOwnProperty(f)) { + found = true; + break; + } + } + if (!found) { + modified = true; + delete subs[pattern]; + } + } + + if (modified) { + outputCount++; + states.setState(autoInstance + '.subscribes', subs, function () { + if (typeof callback === 'function') { + callback(); + } + }); + } else if (typeof callback === 'function') { + setImmediate(function () { + callback(); + }); + } + }); +} + +function cleanAutoSubscribes(instance, callback) { + // instance = 'system.adapter.name.0' + instance = instance.substring(15); // get name.0 + + // read all instances + objects.getObjectView('system', 'instance', {startkey: 'system.adapter.', endkey: 'system.adapter.\u9999'}, function (err, res) { + let count = 0; + if (res && res.rows) { + for (let c = res.rows.length - 1; c >= 0; c--) { + // remove this instance from autoSubscribe + if (res.rows[c].value.common.subscribable) { + count++; + cleanAutoSubscribe(instance, res.rows[c].id, function () { + if (!--count && callback) callback(); + }); + } + } + } + if (!count && callback) callback(); + }); +} + +function delObjects(objs, callback) { + if (!objs || !objs.length) { + if (callback) callback(); + } else { + let row = objs.shift(); + if (row && row.id) { + logger.info('Delete state "' + row.id + '"'); + if (row.value.type === 'state') { + states.delState(row.id, function (/* err */) { + objects.delObject(row.id, function (/* err */) { + setImmediate(function () { + delObjects(objs, callback); + }); + }); + }); + } else { + objects.delObject(row.id, function (/* err */) { + setImmediate(function () { + delObjects(objs, callback); + }); + }); + } + } else { + setImmediate(function () { + delObjects(objs, callback); + }); + } + } +} +/** + * try to check host in objects + *

+ * This function tries to find all hosts in the objects and if + * only one host found and it is not actual host, change the + * host name to new one. + *

+ * + * @return none + */ +function checkHost(type, callback) { + if (type === 'InMemoryDB') { + objects.getObjectView('system', 'host', {}, function (_err, doc) { + if (!_err && doc && doc.rows && + doc.rows.length === 1 && + doc.rows[0].value.common.name !== hostname) + { + let oldHostname = doc.rows[0].value.common.name; + let oldId = doc.rows[0].value._id; + + // find out all instances and rewrite it to actual hostname + objects.getObjectView('system', 'instance', {}, function (err, doc) { + if (err && err.status_code === 404) { + if (callback) callback(); + } else if (doc.rows.length === 0) { + logger.info('host.' + hostname + ' no instances found'); + // no instances found + if (callback) callback(); + } else { + // reassign all instances + changeHost(doc.rows, oldHostname, hostname, function () { + logger.info('Delete host ' + oldId); + + // delete host object + objects.delObject(oldId, function () { + + // delete all hosts states + objects.getObjectView('system', 'state', {startkey: 'system.host.' + oldHostname + '.', endkey: 'system.host.' + oldHostname + '.\u9999', include_docs: true}, function (_err, doc) { + delObjects(doc.rows, function () { + if (callback) callback(); + }); + }); + }); + }); + } + }); + } else if (callback) { + callback(); + } + }); + } else { + if (callback) callback(); + } +} + +// collect short diag information +function collectDiagInfo(type, callback) { + if (type !== 'extended' && type !== 'normal' && type !== 'no-city') { + callback && callback(null); + } else { + objects.getObject('system.config', function (err, systemConfig) { + objects.getObject('system.meta.uuid', function (err, obj) { + // create uuid + if (err || !obj) { + obj = {native: {uuid: 'not found'}}; + } + objects.getObjectView('system', 'host', {}, function (_err, doc) { + // we need to show city and country at the beginning, so include it now and delete it later if not allowed. + let diag = { + uuid: obj.native.uuid, + language: systemConfig.common.language, + country: '', + city: '', + hosts: [], + node: process.version, + arch: os.arch(), + adapters: {} + }; + if (type === 'extended' || type === 'no-city') { + diag.country = systemConfig.common.country; + let cpus = os.cpus(); + diag.model = cpus && cpus[0] && cpus[0].model ? cpus[0].model : 'unknown'; + diag.cpus = cpus ? cpus.length : 1; + diag.mem = os.totalmem(); + diag.ostype = os.type(); + delete diag.city; + } + if (type === 'extended') { + diag.city = systemConfig.common.city; + } else if (type === 'normal') { + delete diag.city; + delete diag.country; + } + if (!_err && doc) { + if (doc && doc.rows.length) { + if (!semver) semver = require('semver'); + + doc.rows.sort(function (a, b) { + try { + return semver.lt((a && a.value && a.value.common) ? a.value.common.installedVersion : '0.0.0', (b && b.value && b.value.common) ? b.value.common.installedVersion : '0.0.0'); + } catch (e) { + logger.error('host.' + hostname + ' Invalid versions: ' + ((a && a.value && a.value.common) ? a.value.common.installedVersion : '0.0.0') + '[' + ((a && a.value && a.value.common) ? a.value.common.name : 'unknown') + '] or ' + ((b && b.value && b.value.common) ? b.value.common.installedVersion : '0.0.0') + '[' + ((b && b.value && b.value.common) ? b.value.common.name : 'unknown') + ']'); + return 0; + } + }); + + // Read installed versions of all hosts + for (let i = 0; i < doc.rows.length; i++) { + diag.hosts.push({ + version: doc.rows[i].value.common.installedVersion, + platform: doc.rows[i].value.common.platform, + type: doc.rows[i].value.native.os.platform + }); + } + } + } + objects.getObjectView('system', 'adapter', {}, function (__err, doc) { + let visFound = false; + if (!_err && doc) { + if (doc && doc.rows.length) { + // Read installed versions of all adapters + for (let i = 0; i < doc.rows.length; i++) { + diag.adapters[doc.rows[i].value.common.name] = { + version: doc.rows[i].value.common.version, + platform: doc.rows[i].value.common.platform + }; + if (doc.rows[i].value.common.name === 'vis') { + visFound = true; + } + } + } + } + // read number of vis datapoints + if (visFound) { + let visUtils = require(__dirname + '/lib/vis/states'); + try { + visUtils(objects, null, 0, null, function (err, points) { + let total = null; + let tasks = []; + if (points && points.length) { + for (let i = 0; i < points.length; i++) { + if (points[i].id === 'vis.0.datapoints.total') { + total = points[i].val; + } + tasks.push({ + _id: points[i].id, + type: 'state', + native: {}, + common: { + name: 'Datapoints count', + role: 'state', + type: 'number', + read: true, + write: false + }, + state: { + val: points[i].val, + ack: true + } + }); + } + } + if (total !== null) { + diag.vis = total; + } + extendObjects(tasks, function () { + if (callback) callback(diag); + }); + }); + } catch (e) { + logger.error('cannot call visUtils: ' + e); + if (callback) callback(diag); + } + } else { + if (callback) callback(diag); + } + }); + }); + }); + }); + } +} + +// check if some IPv4 address found. If not try in 30 seconds one more time (max 10 times) +function setIPs(ipList) { + let _ipList = ipList || getIPs(); + + // check if IPs detected (because of DHCP delay) + let found = false; + for (let a = 0; a < _ipList.length; a++) { + if (_ipList[a] === '127.0.0.1' || _ipList[a] === '::1/128' || !_ipList[a].match(/^\d+\.\d+\.\d+\.\d+$/)) continue; + found = true; + break; + } + // IPv4 address still not found, try again in 30 seconds + if (!found && detectIpsCount < 10) { + detectIpsCount++; + setTimeout(function () { + setIPs(); + }, 30000); + } else if (found) { + // IPv4 found => write to object + objects.getObject('system.host.' + hostname, function (err, oldObj) { + let networkInterfaces = os.networkInterfaces(); + if (JSON.stringify(oldObj.native.hardware.networkInterfaces) !== JSON.stringify(networkInterfaces) || + JSON.stringify(oldObj.common.address) !== JSON.stringify(ipList)) { + oldObj.common.address = ipList; + oldObj.native.hardware.networkInterfaces = networkInterfaces; + oldObj.from = 'system.host.' + tools.getHostName(); + oldObj.ts = new Date().getTime(); + objects.setObject(oldObj._id, oldObj, function (err) { + if (err) logger.error('Cannot write host object:' + err); + }); + } + }); + } else { + logger.info('No IPv4 address found after 5 minutes.'); + } +} + +// write 10 objects each after other +function extendObjects(tasks, callback) { + if (!tasks || !tasks.length) { + if (typeof callback === 'function') callback(); + return; + } + let task = tasks.shift(); + let state = task.state; + if (state !== undefined) { + delete task.state; + } + objects.extendObject(task._id, task, function () { + if (state) { + states.setState(task._id, state, function () { + setImmediate(extendObjects, tasks, callback); + }); + } else { + setImmediate(extendObjects, tasks, callback); + } + }); +} + +function setMeta() { + let id = 'system.host.' + hostname; + + objects.getObject(id, function (err, oldObj) { + let newObj = { + _id: id, + type: 'host', + common: { + name: hostname, + title: oldObj && oldObj.common && oldObj.common.title ? oldObj.common.title : ioPackage.common.title, + installedVersion: version, + platform: ioPackage.common.platform, + cmd: process.argv[0] + ' ' + (process.execArgv.join(' ') + ' ').replace(/--inspect-brk=\d+ /, '') + process.argv.slice(1).join(' '), + hostname: hostname, + address: getIPs(), + type: ioPackage.common.name + }, + native: { + process: { + title: process.title, + versions: process.versions, + env: process.env + }, + os: { + hostname: hostname, + type: os.type(), + platform: os.platform(), + arch: os.arch(), + release: os.release(), + endianness: os.endianness(), + tmpdir: os.tmpdir() + }, + hardware: { + cpus: os.cpus(), + totalmem: os.totalmem() + } + } + }; + + if (oldObj && oldObj.common && oldObj.common.icon) { + newObj.common.icon = oldObj.common.icon; + } + if (oldObj && oldObj.common && oldObj.common.color) { + newObj.common.color = oldObj.common.color; + } + // remove dynamic information + if (newObj.native && newObj.native.hardware && newObj.native.hardware.cpus) { + for (let c = 0; c < newObj.native.hardware.cpus.length; c++) { + if (newObj.native.hardware.cpus[c].times) delete newObj.native.hardware.cpus[c].times; + } + } + if (oldObj && oldObj.native.hardware && oldObj.native.hardware.networkInterfaces) { + newObj.native.hardware.networkInterfaces = oldObj.native.hardware.networkInterfaces; + } + + if (oldObj) { + delete oldObj.cmd; + delete oldObj.from; + delete oldObj.ts; + delete oldObj.acl; + } + + if (!oldObj || JSON.stringify(newObj) !== JSON.stringify(oldObj)) { + newObj.from = 'system.host.' + tools.getHostName(); + newObj.ts = new Date().getTime(); + objects.setObject(id, newObj, function (err) { + if (err) logger.error('Cannot write host object:' + err); + }); + } + setIPs(newObj.common.address); + }); + + let tasks = []; + + let obj = { + _id: id + '.mem', + type: 'state', + common: { + type: 'number', + name: 'Memory usage', + unit: '%', + read: true, + write: false, + min: 0, + max: 100 + }, + native: {} + }; + tasks.push(obj); + + obj = { + _id: id + '.memHeapUsed', + type: 'state', + common: { + type: 'number', + name: 'Memory from heap used in MB', + read: true, + write: false, + min: 0, + unit: 'MB' + }, + native: {} + }; + tasks.push(obj); + + obj = { + _id: id + '.memHeapTotal', + type: 'state', + common: { + type: 'number', + name: 'Memory heap reserved in MB', + read: true, + write: false, + min: 0, + unit: 'MB' + }, + native: {} + }; + tasks.push(obj); + + obj = { + _id: id + '.memRss', + type: 'state', + common: { + type: 'number', + name: 'Resident set size in MB', + desc: 'RSS is the resident set size, the portion of the process\'s memory held in RAM', + read: true, + write: false, + min: 0, + unit: 'MB' + }, + native: {} + }; + tasks.push(obj); + + obj = { + _id: id + '.uptime', + type: 'state', + common: { + type: 'number', + name: 'Uptime in seconds', + read: true, + write: false, + min: 0, + unit: 'seconds' + }, + native: {} + }; + tasks.push(obj); + + obj = { + _id: id + '.load', + type: 'state', + common: { + unit: '', + type: 'number', + read: true, + write: false, + name: 'Load Average 1min' + }, + native: {} + }; + tasks.push(obj); + + obj = { + _id: id + '.alive', + type: 'state', + common: { + name: 'Host alive', + read: true, + write: false, + type: 'boolean' + }, + native: {} + }; + tasks.push(obj); + + obj = { + _id: id + '.freemem', + type: 'state', + common: { + name: 'Available RAM in MB', + unit: 'MB', + read: true, + write: false, + type: 'number' + }, + native: {} + }; + tasks.push(obj); + + obj = { + _id: id + '.inputCount', + type: 'state', + common: { + name: hostname + ' - inputs level', + desc: 'State\'s inputs in 15 seconds', + type: 'number', + read: true, + write: false, + role: 'state', + unit: 'events/15 seconds' + }, + native: {} + }; + tasks.push(obj); + + obj = { + _id: id + '.outputCount', + type: 'state', + common: { + name: hostname + ' outputs level', + desc: 'State\'s outputs in 15 seconds', + type: 'number', + read: true, + write: false, + role: 'state', + unit: 'events/15 seconds' + }, + native: {} + }; + tasks.push(obj); + + extendObjects(tasks, function () { + // create UUID if not exist + tools.createUuid(objects, function (uuid) { + if (uuid && logger) logger.info('Created UUID: ' + uuid); + }); + }); +} + +// Subscribe on message queue +function initMessageQueue() { + states.subscribeMessage('system.host.' + hostname); +} + +// Send message to other adapter instance +function sendTo(objName, command, message, callback) { + if (typeof message === 'undefined') { + message = command; + command = 'send'; + } + let obj = {command: command, message: message, from: 'system.host.' + hostname}; + if (objName.substring(0, 'system.adapter.'.length) !== 'system.adapter.' && + objName.substring(0, 'system.host.'.length) !== 'system.host.') objName = 'system.adapter.' + objName; + + if (callback) { + if (typeof callback === 'function') { + obj.callback = { + message: message, + id: callbackId++, + ack: false, + time: (new Date()).getTime() + }; + if (callbackId > 0xFFFFFFFF) callbackId = 1; + if (!callbacks) callbacks = {}; + callbacks['_' + obj.callback.id] = {cb: callback}; + } else { + obj.callback = callback; + obj.callback.ack = true; + } + } + + states.pushMessage(objName, obj); +} + +function getVersionFromHost(hostId, callback) { + states.getState(hostId + '.alive', function (err, state) { + if (state && state.val) { + sendTo(hostId, 'getVersion', null, function (ioPack) { + if (callback) setImmediate(callback, ioPack); + }); + } else { + logger.warn('host.' + hostname + ' "' + hostId + '" is offline'); + if (callback) setImmediate(callback, null, hostId); + } + }); +} + +// Process message to controller, like execute some script +function processMessage(msg) { + let ioPack; + // important: Do not forget to update the list of protected commands in yunkong2.admin/lib/socket.js for "socket.on('sendToHost'" + // and yunkong2.socketio/lib/socket.js + + switch (msg.command) { + case 'cmdExec': + let spawn = require('child_process').spawn; + let args = [__dirname + '/' + tools.appName + '.js']; + let cmd = msg.message.data.split(' '); + for (let i = 0; i < cmd.length; i++) { + args.push(cmd[i]); + } + logger.info(tools.appName + ' ' + args.slice(1).join(' ')); + + let child = spawn('node', args); + if (child.stdout) { + child.stdout.on('data', function (data) { + data = data.toString().replace('\n', ''); + logger.info(tools.appName + ' ' + data); + if (msg.from) sendTo(msg.from, 'cmdStdout', {id: msg.message.id, data: data}); + }); + } + + if (child.stderr) { + child.stderr.on('data', function (data) { + data = data.toString().replace('\n', ''); + logger.error(tools.appName + ' ' + data); + if (msg.from) sendTo(msg.from, 'cmdStderr', {id: msg.message.id, data: data}); + }); + } + + child.on('exit', function (exitCode) { + logger.info(tools.appName + ' exit ' + exitCode); + if (msg.from) { + sendTo(msg.from, 'cmdExit', {id: msg.message.id, data: exitCode}); + // Sometimes finished command is lost, recent it + setTimeout(function () { + sendTo(msg.from, 'cmdExit', {id: msg.message.id, data: exitCode}); + }, 1000); + } + }); + break; + + case 'getRepository': + if (msg.callback && msg.from) { + objects.getObject('system.config', function (err, systemConfig) { + // Collect statistics + if (systemConfig && systemConfig.common && systemConfig.common.diag) { + collectDiagInfo(systemConfig.common.diag, function (obj) { + if (obj) tools.sendDiagInfo(obj); + }); + } + + objects.getObject('system.repositories', function (err, repos) { + // Check if repositories exists + if (!err && repos && repos.native && repos.native.repositories) { + let updateRepo = false; + if (typeof msg.message === 'object') { + updateRepo = msg.message.update; + msg.message = msg.message.repo; + } + + let active = msg.message || systemConfig.common.activeRepo; + + if (repos.native.repositories[active]) { + + if (typeof repos.native.repositories[active] === 'string') { + repos.native.repositories[active] = { + link: repos.native.repositories[active], + json: null + }; + } + + // If repo is not yet loaded + if (!repos.native.repositories[active].json || updateRepo) { + logger.info('host.' + hostname + ' Update repository "' + active + '" under "' + repos.native.repositories[active].link + '"'); + // Load it + tools.getRepositoryFile(repos.native.repositories[active].link, function (err, sources) { + if (err) logger.warn('host.' + hostname + ' warning: ' + err); + repos.native.repositories[active].json = sources; + sendTo(msg.from, msg.command, repos.native.repositories[active].json, msg.callback); + repos.from = 'system.host.' + tools.getHostName(); + repos.ts = new Date().getTime(); + // Store uploaded repo + objects.setObject('system.repositories', repos); + }); + } else { + // We have already repo, give it back + sendTo(msg.from, msg.command, repos.native.repositories[active].json, msg.callback); + } + } else { + logger.warn('host.' + hostname + ' Requested repository "' + active + '" does not exist in config.'); + sendTo(msg.from, msg.command, null, msg.callback); + } + } + }); + }); + } else { + logger.error('host.' + hostname + ' Invalid request ' + msg.command + '. "callback" or "from" is null'); + } + break; + + case 'getInstalled': + if (msg.callback && msg.from) { + // Get list of all hosts + objects.getObjectView('system', 'host', {}, function (err, doc) { + let result = tools.getInstalledInfo(version); + result.hosts = {}; + let infoCount = 0; + let timeout = null; + + if (doc && doc.rows.length) { + // Read installed versions of all hosts + for (let i = 0; i < doc.rows.length; i++) { + // If desired local version, do not ask it, just answer + if (doc.rows[i].id === 'system.host.' + hostname) { + let _ioPack; + try { + _ioPack = JSON.parse(fs.readFileSync(__dirname + '/io-package.json')); + } catch (e) { + logger.error('host.' + hostname + ' cannot read and parse "' + __dirname + '/io-package.json"'); + } + if (_ioPack) { + _ioPack.common.host = hostname; + _ioPack.common.runningVersion = version; + result.hosts[hostname] = _ioPack.common; + } else { + result.hosts[hostname] = {}; + } + } else { + infoCount++; + getVersionFromHost(doc.rows[i].id, function (ioPack, id) { + if (ioPack) { + result.hosts[ioPack.host] = ioPack; + result.hosts[ioPack.host].controller = true; + } + + if (!--infoCount) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + sendTo(msg.from, msg.command, result, msg.callback); + } else { + logger.warn('host.' + hostname + ' too delayed answer for ' + (ioPack ? ioPack.host : id)); + } + } + }); + } + } + } + if (!infoCount) { + sendTo(msg.from, msg.command, result, msg.callback); + } else { + // Start timeout and send answer in 5 seconds if some hosts are offline + timeout = setTimeout(function () { + logger.warn('host.' + hostname + ' some hosts are offline'); + timeout = null; + sendTo(msg.from, msg.command, result, msg.callback); + }, 5000); + } + }); + } else { + logger.error('host.' + hostname + ' Invalid request ' + msg.command + '. "callback" or "from" is null'); + } + break; + + case 'getInstalledAdapter': + if (msg.callback && msg.from && msg.message) { + // read adapter file + let dir = tools.getAdapterDir(msg.message); + let _result = null; + if (fs.existsSync(dir + '/io-package.json')) { + try { + _result = JSON.parse(fs.readFileSync(dir + '/io-package.json')); + } catch (e) { + logger.error('host.' + hostname + ' cannot read and parse "' + dir + '/io-package.json"'); + } + } + sendTo(msg.from, msg.command, _result, msg.callback); + } else { + logger.error('host.' + hostname + ' Invalid request ' + msg.command + '. "callback" or "from" is null'); + } + break; + + case 'getVersion': + if (msg.callback && msg.from) { + ioPack = null; + try { + ioPack = JSON.parse(fs.readFileSync(__dirname + '/io-package.json')); + } catch (e) { + logger.error('host.' + hostname + ' cannot read and parse "' + __dirname + '/io-package.json"'); + } + if (ioPack) { + ioPack.common.host = hostname; + ioPack.common.runningVersion = version; + sendTo(msg.from, msg.command, ioPack.common, msg.callback); + } else { + sendTo(msg.from, msg.command, null, msg.callback); + } + } else { + logger.error('host.' + hostname + ' Invalid request ' + msg.command + '. "callback" or "from" is null'); + } + break; + + case 'getDiagData': + if (msg.callback && msg.from) { + if (msg.message) { + collectDiagInfo(msg.message, function (obj) { + sendTo(msg.from, msg.command, obj, msg.callback); + }); + } else { + sendTo(msg.from, msg.command, null, msg.callback); + } + } else { + logger.error('host.' + hostname + ' Invalid request ' + msg.command + '. "callback" or "from" is null'); + } + break; + + case 'getLocationOnDisk': + if (msg.callback && msg.from) { + sendTo(msg.from, msg.command, {path: __dirname, platform: require('os').platform()}, msg.callback); + } else { + logger.error('host.' + hostname + ' Invalid request ' + msg.command + '. "callback" or "from" is null'); + } + break; + + case 'getDevList': + if (msg.callback && msg.from) { + ioPack = null; + + if (require('os').platform() === 'linux') { + let _spawn = require('child_process').spawn; + let _args = ['/dev']; + logger.info('host.' + hostname + ' ls /dev'); + let _child = _spawn('ls', _args); + let result = ''; + if (_child.stdout) { + _child.stdout.on('data', function (data) { + result += data.toString(); + }); + } + if (_child.stderr) { + _child.stderr.on('data', function (data) { + logger.error('host.' + hostname + ' ls ' + data); + }); + } + + _child.on('exit', function (/*exitCode*/) { + result = result.replace(/(\r\n|\n|\r|\t)/gm, ' '); + let parts = result.split(' '); + let resList = []; + for (let t = 0; t < parts.length; t++) { + parts[t] = parts[t].trim(); + if (parts[t]) resList.push(parts[t]); + } + + sendTo(msg.from, msg.command, resList, msg.callback); + }); + break; + + } else { + sendTo(msg.from, msg.command, null, msg.callback); + } + } else { + logger.error('host.' + hostname + ' Invalid request ' + msg.command + '. "callback" or "from" is null'); + } + break; + + case 'getLogs': + if (msg.callback && msg.from) { + ioPack = null; + + let lines = msg.message || 200; + let text = ''; + let logFile_ = logger.getFileName(); //__dirname + '/log/' + tools.appName + '.log'; + if (!fs.existsSync(logFile_)) logFile_ = __dirname + '/../../log/' + tools.appName + '.log'; + + if (fs.existsSync(logFile_)) { + let stats = fs.statSync(logFile_); + + fs.createReadStream(logFile_, { + start: (stats.size > 150 * lines) ? stats.size - 150 * lines : 0, + end: stats.size + }).on('data', function (chunk) { + text += chunk.toString(); + }) + .on('end', function () { // done + let lines = text.split('\n'); + lines.shift(); + lines.push(stats.size); + sendTo(msg.from, msg.command, lines, msg.callback); + }).on('error', function () { // done + sendTo(msg.from, msg.command, [stats.size], msg.callback); + }); + } else { + sendTo(msg.from, msg.command, [0], msg.callback); + } + } else { + logger.error('host.' + hostname + ' Invalid request ' + msg.command + '. "callback" or "from" is null'); + } + break; + + case 'getHostInfo': + if (msg.callback && msg.from) { + // installed adapters + // available adapters + // node.js --version + // npm --version + // uptime + tools.getHostInfo(objects, function (err, data) { + if (err) { + logger.error('host.' + hostname + ' cannot get getHostInfo: ' + err); + } + data = data || {}; + data.Uptime = Math.round((new Date().getTime() - uptimeStart) / 1000); + sendTo(msg.from, msg.command, data, msg.callback); + }); + } else { + logger.error('host.' + hostname + ' Invalid request ' + msg.command + '. "callback" or "from" is null'); + } + break; + + case 'delLogs': + let logFile = logger.getFileName(); //__dirname + '/log/' + tools.appName + '.log'; + if (fs.existsSync(__dirname + '/log/' + tools.appName + '.log')) fs.writeFile(__dirname + '/log/' + tools.appName + '.log', ''); + if (fs.existsSync(__dirname + '/../../log/' + tools.appName + '.log')) fs.writeFile(__dirname + '/../../log/' + tools.appName + '.log', ''); + if (fs.existsSync(logFile)) fs.writeFile(logFile); + + if (msg.callback && msg.from) sendTo(msg.from, msg.command, null, msg.callback); + break; + + case 'readDirAsZip': + if (msg.callback && msg.from) { + zipFiles = zipFiles || require(__dirname + '/lib/zipFiles'); + zipFiles.readDirAsZip(objects, msg.message.id, msg.message.name, msg.message.options, function (err, base64) { + if (base64) { + sendTo(msg.from, msg.command, {error: err, data: base64}, msg.callback); + } else { + sendTo(msg.from, msg.command, {error: err}, msg.callback); + } + }); + } + break; + + case 'writeDirAsZip': + zipFiles = zipFiles || require(__dirname + '/lib/zipFiles'); + zipFiles.writeDirAsZip(objects, msg.message.id, msg.message.name, new Buffer(msg.message.data, 'base64'), msg.message.options, function (err) { + if (msg.callback && msg.from) sendTo(msg.from, msg.command, {error: err}, msg.callback); + }); + break; + + case 'readObjectsAsZip': + if (msg.callback && msg.from) { + zipFiles = zipFiles || require(__dirname + '/lib/zipFiles'); + zipFiles.readObjectsAsZip(objects, msg.message.id, msg.message.adapter, msg.message.options, function (err, base64) { + if (base64) { + sendTo(msg.from, msg.command, {error: err, data: base64}, msg.callback); + } else { + sendTo(msg.from, msg.command, {error: err}, msg.callback); + } + }); + } + break; + + case 'writeObjectsAsZip': + zipFiles = zipFiles || require(__dirname + '/lib/zipFiles'); + zipFiles.writeObjectsAsZip(objects, msg.message.id, msg.message.adapter, new Buffer(msg.message.data, 'base64'), msg.message.options, function (err) { + if (msg.callback && msg.from) sendTo(msg.from, msg.command, {error: err}, msg.callback); + }); + break; + + case 'checkLogging': + (function () { + // this is temporary function to check the logging functionality + // Print all information into log + let logs = []; + let count = 0; + function printLog(id, callback) { + states.lenLog(id, function (err, len) { + logs.push('Subscriber - ' + id + ' (queued ' + len + ') ' + (err || '')); + if (len) { + states.getLog(id, function (err, obj) { + if (obj) { + logs.push(id + ' (' + JSON.stringify(obj) + ')'); + } + + printLog(id, callback); + }); + } else { + if (callback) callback(); + } + }); + } + // LogList + logs.push('Actual Loglist - ' + JSON.stringify(logList)); + + // Read current state of all log subscribers + states.getKeys('*.logging', function (err, keys) { + if (keys && keys.length) { + states.getStates(keys, function (err, obj) { + if (obj) { + for (let i = 0; i < keys.length; i++) { + // We can JSON.parse, but index is 16x faster + if (obj[i]) { + let id = keys[i].substring(0, keys[i].length - '.logging'.length).replace(/^io\./, ''); + + if ((typeof obj[i] === 'string' && (obj[i].indexOf('"val":true') !== -1 || obj[i].indexOf('"val":"true"') !== -1)) || + (typeof obj[i] === 'object' && (obj[i].val === true || obj[i].val === 'true'))) { + count++; + printLog(id, function () { + if (!--count) { + for (let m = 0; m < logs.length; m++) { + logger.error('host.' + hostname + ' LOGINFO: ' + logs[m]); + } + logs = []; + } + }); + } else { + if (logs) logs.push('Subscriber - ' + id + ' (disabled)'); + } + } + } + } + setTimeout(function () { + for (let m = 0; m < logs.length; m++) { + logger.error('host.' + hostname + ' LOGINFO: ' + logs[m]); + } + logs = []; + }, 3000); + }); + } + }); + + // Get list of all active adapters and send them message with command checkLogging + for (let _id in procs) { + if (procs.hasOwnProperty(_id) && procs[_id].process) { + outputCount++; + states.setState(_id + '.checkLogging', {val: true, ack: false, from: 'system.host.' + hostname}); + } + } + })(); + break; + + case 'updateMultihost': + (function () { + let result = startMultihost(); + if (msg.callback) { + sendTo(msg.from, msg.command, {result: result}, msg.callback); + } + })(); + break; + } +} + +function getInstances() { + objects.getObjectView('system', 'instance', {}, function (err, doc) { + if (err && err.status_code === 404) { + logger.error('host.' + hostname + ' _design/system missing - call node ' + tools.appName + '.js setup'); + //if (objects.destroy) objects.destroy(); + //if (states && states.destroy) states.destroy(); + //process.exit(1); + return; + } else if (doc.rows.length === 0) { + logger.info('host.' + hostname + ' no instances found'); + } else { + let _ipArr = getIPs(); + logger.info('host.' + hostname + ' ' + doc.rows.length + ' instance' + (doc.rows.length === 1 ? '' : 's') + ' found'); + let count = 0; + + // first mark all instances as disabled to detect disabled once + for (let id in procs) { + if (procs.hasOwnProperty(id) && procs[id].config && procs[id].config.common && procs[id].config.common.enabled) { + procs[id].config.common.enabled = false; + } + } + + for (let i = 0; i < doc.rows.length; i++) { + let instance = doc.rows[i].value; + + // register all common fields, that may not be deleted, like "mobile" or "history" + //noinspection JSUnresolvedVariable + if (objects.addPreserveSettings && instance.common.preserveSettings) { + //noinspection JSUnresolvedVariable + objects.addPreserveSettings(instance.common.preserveSettings); + } + + if (instance.common.mode === 'web' || instance.common.mode === 'none') { + if (instance.common.host === hostname) { + let name = instance._id.split('.')[2]; + const adapterDir_ = tools.getAdapterDir(name); + if (!fs.existsSync(adapterDir_)) { + procs[instance._id] = {downloadRetry: 0, config: {common: {enabled: false}}}; + installQueue.push({id: instance._id, disabled: true}); + // start install queue if not started + if (installQueue.length === 1) installAdapters(); + } + } + continue; + } + + logger.debug('host.' + hostname + ' check instance "' + doc.rows[i].id + '" for host "' + instance.common.host + '"'); + console.log('host.' + hostname + ' check instance "' + doc.rows[i].id + '" for host "' + instance.common.host + '"'); + + if (_ipArr.indexOf(instance.common.host) !== -1 || instance.common.host === hostname) { + procs[instance._id] = procs[instance._id] || {}; + procs[instance._id].config = JSON.parse(JSON.stringify(instance)); + if (instance.common.enabled && (!instance.common.webExtension || !instance.native.webInstance)) count++; + } + } + + if (count > 0) { + logger.info('host.' + hostname + ' starting ' + count + ' instance' + (count > 1 ? 's' : '')); + } else { + logger.warn('host.' + hostname + ' does not start any instances on this host'); + } + } + + initInstances(); + }); +} + +function initInstances() { + let seconds = 0; + let interval = 2000; + let id; + + // Start first admin + for (id in procs) { + if (!procs.hasOwnProperty(id)) continue; + + if (procs[id].config.common.enabled && (!procs[id].config.common.webExtension || !procs[id].config.native.webInstance)) { + if (id.indexOf('system.adapter.admin') !== -1) { + // do not process if still running. It will be started when old one will be finished + if (procs[id].process) { + logger.info('host.' + hostname + ' instance "' + id + '" was not started, because running.'); + continue; + } + if (installQueue.indexOf(id) === -1) { + if (procs[id].restartTimer) { + clearTimeout(procs[id].restartTimer); + } + procs[id].restartTimer = setTimeout(function (_id) { + startInstance(_id); + }, interval * seconds, id); + + seconds += 2; // 4 seconds pause between starts + } + } + } else if (procs[id].process) { + // stop instance if disabled + stopInstance(id); + } + } + + for (id in procs) { + if (!procs.hasOwnProperty(id)) continue; + + if (procs[id].config.common.enabled && (!procs[id].config.common.webExtension || !procs[id].config.native.webInstance)) { + if (id.indexOf('system.adapter.admin') === -1) { + // do not process if still running. It will be started when old one will be finished + if (procs[id].process) { + logger.info('host.' + hostname + ' instance "' + id + '" was not started, because running.'); + continue; + } + + if (installQueue.indexOf(id) === -1) { + if (procs[id].restartTimer) { + clearTimeout(procs[id].restartTimer); + } + procs[id].restartTimer = setTimeout(function (_id) { + startInstance(_id); + }, interval * seconds, id); + + seconds += 2; // 4 seconds pause between starts + } + } + } else { + let name = id.split('.')[2]; + let adapterDir = tools.getAdapterDir(name); + if (!fs.existsSync(adapterDir)) { + procs[id].downloadRetry = procs[id].downloadRetry || 0; + installQueue.push({id: id, disabled: true}); + // start install queue if not started + if (installQueue.length === 1) installAdapters(); + } + } + } +} + +function checkVersion(id, name, version) { + let isFound = false; + + if (name === 'js-controller') { + // Check only version + if (version !== null) { + if (!semver) semver = require('semver'); + if (!semver.satisfies(ioPackage.common.version, version)) { + logger.error('host.' + hostname + ' startInstance ' + id + 'Invalid version of "' + name + '". Installed "' + ioPackage.common.version + '", required "' + version); + return false; + } else { + isFound = true; + } + } else { + isFound = true; + } + } + if (!isFound) { + for (let p in procs) { + if (!procs.hasOwnProperty(p)) continue; + if (procs[p] && procs[p].config && procs[p].config.common && procs[p].config.common.name === name) { + if (version && !semver.satisfies(procs[p].config.common.version, version)) { + logger.error('host.' + hostname + ' startInstance ' + id + ': required adapter "' + name + '" has wrong version. Installed "' + procs[p].config.common.version + '", required "' + version + '"!'); + return false; + } + isFound = true; + } + } + } + + if (!isFound) { + logger.error('host.' + hostname + ' startInstance ' + id + ': required adapter "' + name + '" not found!'); + return false; + } else { + return true; + } +} + +function checkVersions(id, deps) { + try { + if (deps instanceof Array) { + for (let d = 0; d < deps.length; deps++) { + let version = null; + let name = null; + if (typeof deps[d] === 'object') { + if (!semver) semver = require('semver'); + + for (let n in deps[d]) { + if (!deps[d].hasOwnProperty(n)) continue; + name = n; + version = deps[d][n]; + break; + } + } else { + name = deps[d]; + } + if (!checkVersion(id, name, version)) { + return false; + } + } + } else if (typeof deps === 'object') { + if (deps.length !== undefined || deps[0]) { + for (let i in deps) { + if (!deps.hasOwnProperty(i)) continue; + for (let __name in deps[i]) { + if (!deps[i].hasOwnProperty(__name)) continue; + if (!checkVersion(id, __name, deps[__name][i])) { + return false; + } + } } + } else { + for (let _name in deps) { + if (!deps.hasOwnProperty(_name)) continue; + if (!checkVersion(id, _name, deps[_name])) { + return false; + } + } + } + } + } + catch (e) { + logger.error('host.' + hostname + ' startInstance ' + id + ' [checkVersions]: ' + e); + logger.error('host.' + hostname + ' startInstance ' + id + ' [checkVersions]: ' + JSON.stringify(deps)); + return false; + } + return true; +} + +// Store process IDS to make possible kill them all by restart +function storePids() { + if (!storeTimer) { + storeTimer = setTimeout(function () { + storeTimer = null; + let pids = []; + for (let id in procs) { + if (!procs.hasOwnProperty(id)) continue; + + if (procs[id].process) { + pids.push(procs[id].process.pid); + } + pids.push(process.pid); + } + fs.writeFileSync(__dirname + '/pids.txt', JSON.stringify(pids)); + }, 1000); + } +} + +function installAdapters() { + if (!installQueue.length) return; + + let task = installQueue[0]; + let name = task.id.split('.')[2]; + + if (procs[task.id].downloadRetry < 3) { + procs[task.id].downloadRetry++; + logger.warn('host.' + hostname + ' startInstance cannot find adapter "' + name + '". Try to install it... ' + procs[task.id].downloadRetry + ' attempt'); + logger.info(tools.appName + ' install ' + name); + + try { + let child = require('child_process').spawn('node', [__dirname + '/' + tools.appName + '.js', 'install', name]); + if (child.stdout) { + child.stdout.on('data', function (data) { + data = data.toString().replace('\n', ''); + logger.info(tools.appName + ' ' + data); + }); + } + if (child.stderr) { + child.stderr.on('data', function (data) { + data = data.toString().replace('\n', ''); + logger.error(tools.appName + ' ' + data); + }); + } + + child.on('exit', function (exitCode) { + logger.info(tools.appName + ' exit ' + exitCode); + if (!task.disabled) { + startInstance(task.id, task.wakeUp); + } + + setTimeout(function () { + installQueue.shift(); + installAdapters(); + }, 1000); + }); + child.on('error', function (err) { + logger.error('Cannot execute "' + __dirname + '/' + tools.appName + '.js install ' + name + ': ' + err); + setTimeout(function () { + installQueue.shift(); + installAdapters(); + }, 1000); + }) + } catch (err) { + logger.error('Cannot execute "' + __dirname + '/' + tools.appName + '.js install ' + name + ': ' + err); + setTimeout(function () { + installQueue.shift(); + installAdapters(); + }, 1000); + } + } else { + logger.error('host.' + hostname + ' Cannot download adapter "' + name + '". To restart it disable/enable it or restart host.'); + setTimeout(function () { + installQueue.shift(); + installAdapters(); + }, 500); + } +} + +function cleanErrors(id, now, doOutput) { + if (!procs[id] || !procs[id].errors || !procs[id].errors.length) return; + + now = now || new Date().getTime(); + + if (!doOutput && procs[id].lastCleanErrors && now - procs[id].lastCleanErrors < 1000) return; + + procs[id].lastCleanErrors = now; + + // output of errors into log + if (doOutput) { + for (let i = 0; i < procs[id].errors.length; i++) { + if (procs[id].errors[i] && now - procs[id].errors[i].ts < 30000 && procs[id].errors[i].text) { + let lines = procs[id].errors[i].text.replace('\x1B[31merror\x1B[39m:', '').replace('\x1B[34mdebug\x1B[39m:', 'debug:').split('\n'); + for (let k = 0; k < lines.length; k++) { + if (lines[k]) { + logger.error('Caught by controller[' + i + ']: ' + lines[k]); + } + } + } + } + procs[id].errors = []; + } else { + // delete to old errors + for (let e = procs[id].errors.length - 1; e >= 0; e--) { + if (now - procs[id].errors[e].ts > 30000) { + procs[id].errors.splice(0, e); + break; + } + } + } +} + +function startInstance(id, wakeUp) { + if (isStopping || !connected) return; + + if (!procs[id]) { + logger.error('host.' + hostname + ' startInstance ' + id + ': object not found!'); + return; + } + + let instance = procs[id].config; + let name = id.split('.')[2]; + let mode = instance.common.mode; + + if (procs[id].restartTimer) { + clearTimeout(procs[id].restartTimer); + delete procs[id].restartTimer; + } + + if (wakeUp) { + mode = 'daemon'; + } + + //noinspection JSUnresolvedVariable + if (instance.common.wakeup) { + // TODO + } + + // Check if all required adapters installed and have valid version + if (instance.common.dependencies) { + if (checkVersions(id, instance.common.dependencies)) { + delete instance.common.dependencies; + } else { + return; + } + } + + let fileName = instance.common.main || 'main.js'; + const adapterDir_ = tools.getAdapterDir(name); + if (!fs.existsSync(adapterDir_)) { + procs[id].downloadRetry = procs[id].downloadRetry || 0; + installQueue.push({id: id, wakeUp: wakeUp}); + // start install queue if not started + if (installQueue.length === 1) installAdapters(); + return; + } + + let args = (instance && instance._id && instance.common) ? [instance._id.split('.').pop(), instance.common.loglevel || 'info'] : [0, 'info']; + + // define memory limit for adapter + //noinspection JSUnresolvedVariable + if (instance.common.memoryLimitMB && parseInt(instance.common.memoryLimitMB, 10)) { + //noinspection JSUnresolvedVariable + args.push('--max-old-space-size=' + parseInt(instance.common.memoryLimitMB, 10)); + } + + let fileNameFull = adapterDir_ + '/' + fileName; + + // workaround for old vis. + if (instance.common.onlyWWW && name === 'vis') instance.common.onlyWWW = false; + + if (instance.common.mode !== 'extension' && (instance.common.onlyWWW || !fs.existsSync(fileNameFull))) { + fileName = name + '.js'; + fileNameFull = adapterDir_ + '/' + fileName; + if (instance.common.onlyWWW || !fs.existsSync(fileNameFull)) { + // If not just www files + if (instance.common.onlyWWW || fs.existsSync(adapterDir_ + '/www')) { + logger.debug('host.' + hostname + ' startInstance ' + name + '.' + args[0] + ' only WWW files. Nothing to start'); + } else { + logger.error('host.' + hostname + ' startInstance ' + name + '.' + args[0] + ': cannot find start file!'); + } + return; + } + } + procs[id].downloadRetry = 0; + + + //noinspection JSUnresolvedVariable + if (instance.common.subscribe || instance.common.wakeup) { + procs[id].subscribe = instance.common.subscribe || (instance._id + '.wakeup'); + let parts = instance._id.split('.'); + let instanceId = parts[parts.length - 1]; + procs[id].subscribe = procs[id].subscribe.replace('', instanceId); + + if (subscribe[procs[id].subscribe]) { + if (subscribe[procs[id].subscribe].indexOf(id) === -1) { + subscribe[procs[id].subscribe].push(id); + } + } else { + subscribe[procs[id].subscribe] = [id]; + + // Subscribe on changes + if (procs[id].subscribe.match(/$messagebox/)) { + states.subscribeMessage(procs[id].subscribe.substring('messagebox'.length)); + } else { + states.subscribe(procs[id].subscribe); + } + } + } + + switch (mode) { + case 'once': + case 'daemon': + if (procs[id] && !procs[id].process) { + allInstancesStopped = false; + logger.debug('host.' + hostname + ' startInstance ' + name + '.' + args[0] + ' loglevel=' + args[1]); + procs[id].process = cp.fork(fileNameFull, args, {stdio: ['ignore', 'ignore', 'pipe', 'ipc']}); + + // catch error output + if (procs[id].process.stderr) { + procs[id].process.stderr.on('data', function (data) { + if (!data || !procs[id] || typeof procs[id] !== 'object') return; + let text = data.toString(); + // show for debug + console.error(text); + procs[id].errors = procs[id].errors || []; + let now = new Date().getTime(); + procs[id].errors.push({ts: now, text: text}); + // limit output to 300 messages + if (procs[id].errors > 300) { + procs[id].errors.splice(procs[id].errors.length - 300); + } + cleanErrors(id, now); + }); + } + + storePids(); // Store all pids to make possible kill them all + + procs[id].process.on('exit', function (code, signal) { + outputCount += 2; + states.setState(id + '.alive', {val: false, ack: true, from: 'system.host.' + hostname}); + states.setState(id + '.connected', {val: false, ack: true, from: 'system.host.' + hostname}); + + cleanAutoSubscribes(id); + + if (procs[id] && procs[id].config && procs[id].config.common.logTransporter) { + outputCount++; + states.setState(id + '.logging', {val: false, ack: true, from: 'system.host.' + hostname}); + } + + // show stored errors + cleanErrors(id, null, code !== 4294967196); + + if (mode !== 'once') { + if (signal) { + logger.warn('host.' + hostname + ' instance ' + id + ' terminated due to ' + signal); + } else if (code === null) { + logger.error('host.' + hostname + ' instance ' + id + ' terminated abnormally'); + } + + if ((procs[id] && procs[id].stopping) || isStopping || wakeUp) { + logger.info('host.' + hostname + ' instance ' + id + ' terminated with code ' + code + ' (' + (errorCodes[code] || '') + ')'); + if (procs[id].stopping !== undefined) { + delete procs[id].stopping; + } + + if (procs[id].process) { + delete procs[id].process; + } + + if (isStopping) { + for (let i in procs) { + if (!procs.hasOwnProperty(i)) continue; + if (procs[i].process) { + //console.log(procs[i].config.common.name + ' still running'); + return; + } + } + logger.info('host.' + hostname + ' All instances are stopped.'); + allInstancesStopped = true; + } + storePids(); // Store all pids to make possible kill them all + return; + } else { + //noinspection JSUnresolvedVariable + if (code === 4294967196 /* -100 */ && procs[id].config.common.restartSchedule) { + logger.info('host.' + hostname + ' instance ' + id + ' scheduled normal terminated and will be started anew.'); + } else { + logger.error('host.' + hostname + ' instance ' + id + ' terminated with code ' + code + ' (' + (errorCodes[code] || '') + ')'); + } + } + } + + if (procs[id] && procs[id].process) { + delete procs[id].process; + } + if (!wakeUp && connected && !isStopping && procs[id] && procs[id].config && procs[id].config.common && procs[id].config.common.enabled && (!procs[id].config.common.webExtension || !procs[id].config.native.webInstance) && mode !== 'once') { + + logger.info('host.' + hostname + ' Restart adapter ' + id + ' because enabled'); + + //noinspection JSUnresolvedVariable + if (procs[id].restartTimer) { + clearTimeout(procs[id].restartTimer); + } + procs[id].restartTimer = setTimeout(function (_id) { + startInstance(_id); + }, code === 4294967196 ? 1000 : (procs[id].config.common.restartSchedule ? 1000 : 30000), id); + // 4294967196 (-100) is special code that adapter wants itself to be restarted immediately + } else { + if (mode !== 'once') { + logger.info('host.' + hostname + ' Do not restart adapter ' + id + ' because disabled or deleted'); + } else { + logger.info('host.' + hostname + ' instance ' + id + ' terminated while should be started once'); + } + } + storePids(); // Store all pids to make possible kill them all + }); + if (!wakeUp && procs[id] && procs[id].config.common && procs[id].config.common.enabled && (!procs[id].config.common.webExtension || !procs[id].config.native.webInstance) && mode !== 'once') { + logger.info('host.' + hostname + ' instance ' + instance._id + ' started with pid ' + procs[id].process.pid); + } + } else { + if (!wakeUp && procs[id]) logger.warn('host.' + hostname + ' instance ' + instance._id + ' already running with pid ' + procs[id].process.pid); + } + break; + + case 'schedule': + if (!instance.common.schedule) { + logger.error(instance._id + ' schedule attribute missing'); + break; + } + if (procs[id].schedule) { + procs[id].schedule.cancel(); + logger.info('host.' + hostname + ' instance canceled schedule ' + instance._id); + } + + procs[id].schedule = schedule.scheduleJob(instance.common.schedule, function () { + if (!procs[id]) { + logger.error('host.' + hostname + ' scheduleJob: Task deleted (' + id + ')'); + return; + } + // After sleep of PC all scheduled runs come together. There is no need to run it X times in one second. Just the last. + if (procs[id].lastStart && (new Date()).getTime() - procs[id].lastStart < 2000) { + logger.warn('host.' + hostname + ' instance ' + instance._id + ' does not started, because just executed'); + return; + } + + // Remember the last run + procs[id].lastStart = new Date().getTime(); + if (!procs[id].process) { + let args = [instance._id.split('.').pop(), instance.common.loglevel || 'info']; + procs[id].process = cp.fork(fileNameFull, args); + storePids(); // Store all pids to make possible kill them all + logger.info('host.' + hostname + ' instance ' + instance._id + ' started with pid ' + procs[instance._id].process.pid); + + procs[id].process.on('exit', function (code, signal) { + outputCount++; + states.setState(id + '.alive', {val: false, ack: true, from: 'system.host.' + hostname}); + if (signal) { + logger.warn('host.' + hostname + ' instance ' + id + ' terminated due to ' + signal); + } else if (code === null) { + logger.error('host.' + hostname + ' instance ' + id + ' terminated abnormally'); + } else { + if (code === 0 || code === '0') { + logger.info('host.' + hostname + ' instance ' + id + ' terminated with code ' + code + ' (' + (errorCodes[code] || '') + ')'); + } else { + logger.error('host.' + hostname + ' instance ' + id + ' terminated with code ' + code + ' (' + (errorCodes[code] || '') + ')'); + } + } + if (procs[id] && procs[id].process) delete procs[id].process; + storePids(); // Store all pids to make possible kill them all + }); + } else { + if (!wakeUp) logger.warn('host.' + hostname + ' instance ' + instance._id + ' already running with pid ' + procs[id].process.pid); + } + }); + logger.info('host.' + hostname + ' instance scheduled ' + instance._id + ' ' + instance.common.schedule); + // Start one time adapter by start or if configuration changed + //noinspection JSUnresolvedVariable + if (instance.common.allowInit) { + procs[id].process = cp.fork(fileNameFull, args); + storePids(); // Store all pids to make possible kill them all + logger.info('host.' + hostname + ' instance ' + instance._id + ' started with pid ' + procs[instance._id].process.pid); + + procs[id].process.on('exit', function (code, signal) { + cleanAutoSubscribes(id); + + outputCount++; + states.setState(id + '.alive', {val: false, ack: true, from: 'system.host.' + hostname}); + if (signal) { + logger.warn('host.' + hostname + ' instance ' + id + ' terminated due to ' + signal); + } else if (code === null) { + logger.error('host.' + hostname + ' instance ' + id + ' terminated abnormally'); + } else { + if (code === 0 || code === '0') { + logger.info('host.' + hostname + ' instance ' + id + ' terminated with code ' + code + ' (' + (errorCodes[code] || '') + ')'); + } else { + logger.error('host.' + hostname + ' instance ' + id + ' terminated with code ' + code + ' (' + (errorCodes[code] || '') + ')'); + } + } + delete procs[id].process; + storePids(); // Store all pids to make possible kill them all + }); + } + + break; + + case 'extension': + case 'subscribe': + break; + + default: + logger.error(instance._id + ' invalid mode'); + + } +} + +function stopInstance(id, callback) { + logger.info('host.' + hostname + ' stopInstance ' + id); + if (!procs[id]) { + logger.warn('host.' + hostname + ' unknown instance ' + id); + if (typeof callback === 'function') callback(); + return; + } + + let instance = procs[id].config; + if (!instance || !instance.common || !instance.common.mode) { + if (procs[id].process) { + procs[id].stopping = true; + procs[id].process.kill(); + delete procs[id].process; + } + if (procs[id].schedule) { + procs[id].schedule.cancel(); + delete procs[id].schedule; + } + + if (procs[id].subscribe) { + // Remove this id from subsribed on this message + if (subscribe[procs[id].subscribe] && subscribe[procs[id].subscribe].indexOf(id) !== -1) { + subscribe[procs[id].subscribe].splice(subscribe[procs[id].subscribe].indexOf(id), 1); + + // If no one subscribed + if (!subscribe[procs[id].subscribe].length) { + // Delete item + delete subscribe[procs[id].subscribe]; + + // Unsubscribe + if (procs[id].subscribe.match(/$messagebox/)) { + states.unsubscribeMessage(procs[id].subscribe.substring('messagebox'.length)); + } else { + states.unsubscribe(procs[id].subscribe); + } + } + } + } + if (typeof callback === 'function') callback(); + return; + } + + switch (instance.common.mode) { + case 'daemon': + if (!procs[id].process) { + logger.warn('host.' + hostname + ' stopInstance ' + instance._id + ' not running'); + if (typeof callback === 'function') callback(); + } else { + //noinspection JSUnresolvedVariable + if (instance.common.messagebox && instance.common.supportStopInstance) { + let timeout; + // Send to adapter signal "stopInstance" because on some systems SIGTERM does not work + sendTo(instance._id, 'stopInstance', null, function (result) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + logger.info('host.' + hostname + ' stopInstance self ' + instance._id + ' killing pid ' + procs[id].process.pid + (result ? ': ' + result : '')); + if (procs[id].process) { + procs[id].stopping = true; + procs[id].process.kill(); + delete procs[id].process; + } + + if (typeof callback === 'function') { + callback(); + callback = null; + } + }); + + let timeoutDuration = (instance.common.supportStopInstance === true) ? 1000 : (instance.common.supportStopInstance || 1000); + // If no response from adapter, kill it in 1 second + timeout = setTimeout(function () { + timeout = null; + if (procs[id].process) { + logger.info('host.' + hostname + ' stopInstance timeout "' + timeoutDuration + ' ' + instance._id + ' killing pid ' + procs[id].process.pid); + procs[id].stopping = true; + procs[id].process.kill(); + delete procs[id].process; + } + if (typeof callback === 'function') { + callback(); + callback = null; + } + }, timeoutDuration); + } else { + logger.info('host.' + hostname + ' stopInstance ' + instance._id + ' killing pid ' + procs[id].process.pid); + procs[id].stopping = true; + procs[id].process.kill(); + delete procs[id].process; + if (typeof callback === 'function') { + callback(); + callback = null; + } + } + } + break; + + case 'schedule': + if (!procs[id].schedule) { + logger.warn('host.' + hostname + ' stopInstance ' + instance._id + ' not scheduled'); + } else { + procs[id].schedule.cancel(); + delete procs[id].schedule; + logger.info('host.' + hostname + ' stopInstance canceled schedule ' + instance._id); + } + if (typeof callback === 'function') { + callback(); + callback = null; + } + break; + + case 'subscribe': + // Remove this id from subscribed on this message + if (subscribe[procs[id].subscribe] && subscribe[procs[id].subscribe].indexOf(id) !== -1) { + subscribe[procs[id].subscribe].splice(subscribe[procs[id].subscribe].indexOf(id), 1); + + // If no one subscribed + if (!subscribe[procs[id].subscribe].length) { + // Delete item + delete subscribe[procs[id].subscribe]; + + // Unsubscribe + if (procs[id].subscribe.match(/$messagebox/)) { + states.unsubscribeMessage(procs[id].subscribe.substring('messagebox'.length)); + } else { + states.unsubscribe(procs[id].subscribe); + } + } + } + + if (!procs[id].process) { + if (typeof callback === 'function') callback(); + } else { + logger.info('host.' + hostname + ' stopInstance ' + instance._id + ' killing pid ' + procs[id].process.pid); + procs[id].stopping = true; + procs[id].process.kill(); + delete procs[id].process; + if (typeof callback === 'function') { + callback(); + callback = null; + } + } + break; + + default: + } +} +/* + //test disconnect + setTimeout(function () { + if (disconnectTimeout) clearTimeout(disconnectTimeout); + disconnectTimeout = setTimeout(function () { + console.log('TEST !!!!! STOP!!!! ==============================================='); + connected = false; + disconnectTimeout = null; + logger.warn('host.' + hostname + ' Slave controller detected disconnection. Stop all instances.'); + stopInstances(true, function () { + // if during stopping the DB has connection again + if (connected && !isStopping) { + getInstances(); + startAliveInterval(); + initMessageQueue(); + } + }); + }, config.objects.connectTimeout || 2000); + + }, 60000); + + setTimeout(function () { + console.log('TEST !!!!! START AGAIN!!!! ==============================================='); + // stop disconnect timeout + if (disconnectTimeout) { + clearTimeout(disconnectTimeout); + disconnectTimeout = null; + } + + if (!connected) { + if (connected === null) setMeta(); + + connected = true; + logger.info('host.' + hostname + ' ' + ' connected'); + + // Do not start if we still stopping the instances + if (!isStopping) { + getInstances(); + startAliveInterval(); + initMessageQueue(); + } + } + }, 63000); + */ + +function stopInstances(forceStop, callback) { + let timeout; + function waitForInstances() { + if (!allInstancesStopped) { + setTimeout(waitForInstances, 200); + } else { + if (timeout) clearTimeout(timeout); + isStopping = null; + if (typeof callback === 'function') callback(); + callback = null; + } + } + + try { + let elapsed = (isStopping ? ((new Date()).getTime() - isStopping) : 0); + logger.debug('host.' + hostname + ' stop isStopping=' + elapsed + ' isDaemon=' + isDaemon + ' allInstancesStopped=' + allInstancesStopped); + if (elapsed >= stopTimeout) { + isStopping = null; + if (timeout) clearTimeout(timeout); + if (typeof callback === 'function') callback(true); + callback = null; + } else { + // Sometimes process receives SIGTERM twice + isStopping = isStopping || new Date().getTime(); + } + + if (forceStop || isDaemon) { + // send instances SIGTERM, only needed if running in background (isDaemon) + // or slave lost connection to master + for (let id in procs) { + if (!procs.hasOwnProperty(id)) continue; + stopInstance(id); + } + } + + waitForInstances(); + } catch (e) { + logger.error(e.message); + isStopping = null; + if (timeout) clearTimeout(timeout); + if (typeof callback === 'function') callback(); + callback = null; + } + + // force after Xs + timeout = setTimeout(function () { + timeout = null; + isStopping = null; + if (typeof callback === 'function') callback(true); + callback = null; + }, stopTimeout); +} + +function stop() { + if (mhService) { + mhService.close(); + mhService = null; + } + + stopInstances(false, function (wasForced) { + if (objects && objects.destroy) objects.destroy(); + + outputCount++; + states.setState('system.host.' + hostname + '.alive', {val: false, ack: true, from: 'system.host.' + hostname}, function () { + logger.info('host.' + hostname + ' ' + (wasForced ? 'force terminating' : 'terminated')); + if (wasForced) { + for (let i in procs) { + if (!procs.hasOwnProperty(i)) continue; + if (procs[i].process) { + if (procs[i].config && procs[i].config.common && procs[i].config.common.name) { + logger.info('Adapter ' + procs[i].config.common.name + ' still running'); + } + } + } + } + if (states && states.destroy) states.destroy(); + setTimeout(function () { + process.exit(1); + }, 1000); + }); + }); +} + +// bootstrap +function init() { + // Get "objects" object + // If "file" and on the local machine + if (config.objects.type === 'file' && (!config.objects.host || config.objects.host === 'localhost' || config.objects.host === '127.0.0.1' || config.objects.host === '0.0.0.0')) { + Objects = require(__dirname + '/lib/objects/objectsInMemServer'); + } else { + Objects = require(__dirname + '/lib/objects'); + } + + // Get "states" object + if (config.states.type === 'file' && (!config.states.host || config.states.host === 'localhost' || config.states.host === '127.0.0.1' || config.states.host === '0.0.0.0')) { + States = require(__dirname + '/lib/states/statesInMemServer'); + } else { + States = require(__dirname + '/lib/states'); + } + + // Detect if outputs to console are forced. By default they are disabled and redirected to log file + if (config.log.noStdout && process.argv && (process.argv.indexOf('--console') !== -1 || process.argv.indexOf('--logs') !== -1)) { + config.log.noStdout = false; + } + + // Detect if controller runs as a linux-daemon + if (process.argv.indexOf('start') !== -1) { + isDaemon = true; + config.log.noStdout = true; + logger = require(__dirname + '/lib/logger.js')(config.log); + } else { + logger = require(__dirname + '/lib/logger.js')(config.log); + } + + // Delete all log files older than x das + logger.activateDateChecker(true, config.log.maxDays); + + // If installed as npm module + adapterDir = adapterDir.split('/'); + if (adapterDir.pop() === 'node_modules') { + adapterDir = adapterDir.join('/'); + } else { + adapterDir = __dirname.replace(/\\/g, '/') + '/node_modules'; + } + + // If some message from logger + logger.on('logging', function (transport, level, msg/*, meta*/) { + if (transport.name !== tools.appName) return; + // Send to all adapter, that required logs + for (let i = 0; i < logList.length; i++) { + states.pushLog(logList[i], {message: msg, severity: level, from: 'host.' + hostname, ts: (new Date()).getTime()}); + } + }); + + logger.info('host.' + hostname + ' ' + tools.appName + '.js-controller version ' + version + ' ' + ioPackage.common.name + ' starting'); + logger.info('host.' + hostname + ' Copyright (c) 2014-2018 bluefox, 2014 hobbyquaker'); + logger.info('host.' + hostname + ' hostname: ' + hostname + ', node: ' + process.version); + logger.info('host.' + hostname + ' ip addresses: ' + getIPs().join(' ')); + + // create package.json for npm >= 3.x if not exists + if (__dirname.replace(/\\/g, '/').toLowerCase().indexOf('/node_modules/' + title.toLowerCase()) !== -1) { + try { + if (!fs.existsSync(__dirname + '/../../package.json')) { + fs.writeFileSync(__dirname + '/../../package.json', JSON.stringify({ + name: 'yunkong2.core', + version: '0.1.0', + private: true + }, null, 2)); + } else { + // npm3 requires version attribute + let p = JSON.parse(fs.readFileSync(__dirname + '/../../package.json').toString()); + if (!p.version) { + fs.writeFileSync(__dirname + '/../../package.json', JSON.stringify({ + name: 'yunkong2.core', + version: '1.0.0', + private: true + }, null, 2)); + } + } + } catch (e) { + console.error('Cannot create "' + __dirname + '/../../package.json": ' + e); + } + } + + // create states object + states = createStates(); + + // Subscribe for all logging objects + states.subscribe('*.logging'); + + // Subscribe for all logging objects + states.subscribe('system.adapter.*.alive'); + + // Read current state of all log subscribers + states.getKeys('*.logging', function (err, keys) { + if (keys && keys.length) { + states.getStates(keys, function (err, obj) { + if (obj) { + for (let i = 0; i < keys.length; i++) { + // We can JSON.parse, but index is 16x faster + if (obj[i]) { + if (typeof obj[i] === 'string' && (obj[i].indexOf('"val":true') !== -1 || obj[i].indexOf('"val":"true"') !== -1)) { + logRedirect(true, keys[i].substring(0, keys[i].length - '.logging'.length).replace(/^io\./, '')); + } else if (typeof obj[i] === 'object' && (obj[i].val === true || obj[i].val === 'true')) { + logRedirect(true, keys[i].substring(0, keys[i].length - '.logging'.length).replace(/^io\./, '')); + } + } + } + } + }); + } + }); + + objects = createObjects(); + + objects.subscribe('system.adapter.*'); + + process.on('SIGINT', function () { + logger.info('host.' + hostname + ' received SIGINT'); + stop(); + }); + + process.on('SIGTERM', function () { + logger.info('host.' + hostname + ' received SIGTERM'); + stop(); + }); + + process.on('uncaughtException', function (err) { + if (err.arguments && err.arguments[0] === 'fragmentedOperation') { + logger.error('fragmentedOperation: restart objects'); + // restart objects + objects.destroy(); + objects = null; + // Give time to close the objects + setTimeout(function () { + objects = createObjects(); + }, 3000); + return; + } + + // If by terminating one more exception => stop immediately to break the circle + if (uncaughtExceptionCount) { + console.error(err.message || err); + if (err.stack) console.error(err.stack); + process.exit(2); + return; + } + uncaughtExceptionCount++; + if (typeof err === 'object') { + if (err.errno === 'EADDRINUSE') { + logger.error('Another instance is running or some application uses port!'); + logger.error('uncaught exception: ' + err.message); + } else { + logger.error('uncaught exception: ' + err.message); + logger.error(err.stack); + } + } else { + logger.error('uncaught exception: ' + err); + } + stop(); + // Restart itself + processMessage({command: 'cmdExec', message: {data: '_restart'}}); + }); + +} + +init(); \ No newline at end of file diff --git a/io-package.json b/io-package.json new file mode 100644 index 0000000..2a2a133 --- /dev/null +++ b/io-package.json @@ -0,0 +1,495 @@ +{ + "common": { + "name": "js-controller", + "version": "1.4.2", + "platform": "Javascript/Node.js", + "controller": true, + "title": "JS controller", + "titleLang": { + "en": "JS controller", + "de": "JS-Controller", + "ru": "Контроллер JS", + "pt": "Controlador JS", + "nl": "JS-controller", + "fr": "Contrôleur JS", + "it": "Controller JS", + "es": "Controlador JS", + "pl": "Kontroler JS" + }, + "news": { + "1.4.2": { + "en": "see CHANGELOG.md", + "de": "Sehe CHANGELOG.md", + "ru": "см. CHANGELOG.md", + "pt": "veja CHANGELOG.md", + "nl": "zie CHANGELOG.md", + "fr": "voir CHANGELOG.md", + "it": "vedi CHANGELOG.md", + "es": "ver CHANGELOG.md", + "pl": "zobacz CHANGELOG.md" + }, + "1.4.0": { + "en": "see CHANGELOG.md", + "de": "Sehe CHANGELOG.md", + "ru": "см. CHANGELOG.md", + "pt": "veja CHANGELOG.md", + "nl": "zie CHANGELOG.md", + "fr": "voir CHANGELOG.md", + "it": "vedi CHANGELOG.md", + "es": "ver CHANGELOG.md", + "pl": "zobacz CHANGELOG.md" + }, + "1.3.0": { + "en": "socket.io Version was downgraded because of bug\nBetter npm5 support", + "de": "socket.io Die Version wurde aufgrund eines Fehlers heruntergestuft\nBessere npm5-Unterstützung", + "ru": "Версия socket.io была понижена из-за ошибки\nЛучшая поддержка npm5", + "pt": "A versão socket.io foi rebaixada por causa do erro\nMelhor suporte npm5", + "nl": "socket.io De versie is gedowngraded vanwege een bug\nBetere npm5-ondersteuning", + "fr": "La version de socket.io a été déclassée à cause d'un bug\nMeilleur support npm5", + "it": "socket.io La versione è stata declassata a causa di un bug\nMigliore supporto per npm5", + "es": "La versión de socket.io se ha degradado debido a errores\nMejor soporte npm5", + "pl": "Wersja socket.io została obniżona z powodu błędu\nLepsza obsługa npm5" + }, + "1.2.7": { + "en": "see CHANGELOG.md", + "de": "Sehe CHANGELOG.md", + "ru": "см. CHANGELOG.md", + "pt": "veja CHANGELOG.md", + "nl": "zie CHANGELOG.md", + "fr": "voir CHANGELOG.md", + "it": "vedi CHANGELOG.md", + "es": "ver CHANGELOG.md", + "pl": "zobacz CHANGELOG.md" + }, + "1.2.4": { + "en": "The fix for npm5", + "de": "Der Fix für npm5", + "ru": "Исправление для npm5", + "pt": "A correção para npm5", + "fr": "Le correctif pour npm5", + "nl": "De oplossing voor npm5" + }, + "1.2.3": { + "en": "fix logging level: silly\nfix dependency check\nfix small errors\nsee changelog", + "de": "Korrigiere Log-Silly\nKorrigiere Abhängigkeitsprüfung\nKorrigiere kleine Fehler\nSchaue Changelog", + "ru": "Поправлен лог silly\nИсправлена проверка зависимостей пакетов\nИсправлены маленькие ошибки\nСм. список изменений" + }, + "1.2.0": { + "en": "fixed upgrade command\nallow install from custom repositories\nremove online and sources repositories\nfix multihosts command\ncatch the error outputs of instances if they die\nno more support for node.js 0.10/0.12\nadd new logging level: silly", + "de": "fixed upgrade command\nallow install from custom repositories\nremove online and sources repositories\nfix multihosts command\ncatch the error outputs of instances if they die\nno more support for node.js 0.10/0.12\nadd new logging level: silly", + "ru": "fixed upgrade command\nallow install from custom repositories\nremove online and sources repositories\nfix multihosts command\ncatch the error outputs of instances if they die\nno more support for node.js 0.10/0.12\nadd new logging level: silly" + }, + "1.1.3": { + "en": "Extend statistics\nUpdate npm packets", + "de": "Statistik erweitern\nnpm Pakete aktualisieren", + "ru": "Расширение статистики\nОбновление пакетов npm" + }, + "1.1.2": { + "en": "Close sockets by default for external connects", + "de": "Erlaube die Verbindung auf Objeke/States-Sockets nicht", + "ru": "По умолчанию закрыто соединение с сокетами для внешних подключений" + }, + "1.1.1": { + "en": "Allow redis connections via unix sockets", + "de": "Erlaube Redis verbindungen via Unix Sockets", + "ru": "Allow redis unix socket connections" + }, + "1.1.0": { + "en": "Close sockets by default for external connects", + "de": "Erlaube die Verbindung auf Objeke/States-Sockets nicht", + "ru": "По умолчанию закрыто соединение с сокетами для внешних подключений" + }, + "1.0.3": { + "en": "see CHANGELOG.md", + "de": "Sehe CHANGELOG.md", + "ru": "Смотри CHANGELOG.md" + }, + "1.0.0": { + "en": "No big changes, just version", + "de": "Keine große Änderungen, nur Versionsänderung", + "ru": "Никаких больших изменений. Только номер версии." + } + }, + "desc": { + "en": "Javascript/Node.js implementation of yunkong2 controller", + "de": "Javascript/Node.js Implementierung des yunkong2-Controllers", + "ru": "Javascript/Node.js реализация контроллера yunkong2", + "pt": "Implementação do Javascript/Node.js do controlador yunkong2", + "nl": "Javascript/Node.js implementatie van yunkong2-controller", + "fr": "Implémentation Javascript/Node.js du contrôleur yunkong2", + "it": "Implementazione Javascript/Node.js del controller yunkong2", + "es": "Implementación de JavaScript/Node.js del controlador yunkong2", + "pl": "Implementacja JavaScript/Node.js kontrolera yunkong2" + }, + "messagebox": true, + "readme": "https://github.com/yunkong2/yunkong2.js-controller/blob/master/README.md", + "authors": [ + "bluefox ", + "hobbyquaker " + ], + "license": "MIT", + "type": "general", + "unsafePerm": true + }, + "objects": [ + { + "_id": "_design/system", + "language": "javascript", + "common": { + "dontDelete": true + }, + "views": { + "host": { + "map": "function(doc) { if (doc.type === 'host') emit(doc._id, doc) }" + }, + "adapter": { + "map": "function(doc) { if (doc.type === 'adapter') emit(doc._id, doc) }" + }, + "instance": { + "map": "function(doc) { if (doc.type === 'instance') emit(doc._id, doc) }" + }, + "instanceStats": { + "map": "function(doc) { if (doc.type === 'instance') emit(doc._id, parseInt(doc._id.split('.').pop(), 10)) }", + "reduce": "_stats" + }, + "meta": { + "map": "function(doc) { if (doc.type === 'meta') emit(doc._id, doc) }" + }, + "device": { + "map": "function(doc) { if (doc.type === 'device') emit(doc._id, doc) }" + }, + "channel": { + "map": "function(doc) { if (doc.type === 'channel') emit(doc._id, doc) }" + }, + "state": { + "map": "function(doc) { if (doc.type === 'state') emit(doc._id, doc) }" + }, + "enum": { + "map": "function(doc) { if (doc.type === 'enum') emit(doc._id, doc) }" + }, + "script": { + "map": "function(doc) { if (doc.type === 'script') emit(doc._id, doc) }" + }, + "group": { + "map": "function(doc) { if (doc.type === 'group') emit(doc.common.name, doc) }" + }, + "user": { + "map": "function(doc) { if (doc.type === 'user') emit(doc.common.name, doc) }" + }, + "config": { + "map": "function(doc) { if (doc.type === 'config') emit(doc.common.name, doc) }" + } + }, + "acl": { + "owner": "system.user.admin", + "ownerGroup": "system.group.administrator", + "object": 1092 + } + }, + { + "_id": "system.group.administrator", + "type": "group", + "common": { + "name": { + "en": "Administrator", + "de": "Administrator", + "ru": "Администратор", + "pt": "Administrador", + "nl": "Beheerder", + "fr": "Administrateur", + "it": "Amministratore", + "es": "Administrador", + "pl": "Administrator" + }, + "description": { + "en": "Can do everything with System", + "de": "Kann alles mit System machen", + "ru": "Может делать все с помощью системы", + "pt": "Pode fazer tudo com o sistema", + "nl": "Kan alles doen met System", + "fr": "Peut tout faire avec le système", + "it": "Può fare tutto con il sistema", + "es": "Puede hacer todo con System", + "pl": "Potrafi zrobić wszystko dzięki Systemowi" + }, + "members": [ + "system.user.admin" + ], + "dontDelete": true, + "acl": { + "object": { + "list": true, + "read": true, + "write": true, + "delete": true + }, + "state": { + "list": true, + "read": true, + "write": true, + "create": true, + "delete": true + }, + "users": { + "list": true, + "read": true, + "write": true, + "create": true, + "delete": true + }, + "other": { + "execute": true, + "http": true, + "sendto": true + }, + "file": { + "list": true, + "read": true, + "write": true, + "create": true, + "delete": true + } + } + }, + "acl": { + "owner": "system.user.admin", + "ownerGroup": "system.group.administrator", + "object": 1604 + } + }, + { + "_id": "system.group.user", + "type": "group", + "common": { + "name": { + "en": "User", + "de": "Benutzer", + "ru": "Пользователь", + "pt": "Do utilizador", + "nl": "Gebruiker", + "fr": "Utilisateur", + "it": "Utente", + "es": "Usuario", + "pl": "Użytkownik" + }, + "description": { + "en": "Cannot modify everything", + "de": "Kann nicht alles ändern", + "ru": "Не может изменять все", + "pt": "Não é possível modificar tudo", + "nl": "Kan niet alles wijzigen", + "fr": "Impossible de tout modifier", + "it": "Non è possibile modificare tutto", + "es": "No se puede modificar todo", + "pl": "Nie można modyfikować wszystkiego" + }, + "members": [], + "dontDelete": true, + "url": "https://github.com/yunkong2/yunkong2.js-controller/archive/master.zip", + "meta": "https://raw.githubusercontent.com/yunkong2/yunkong2.js-controller/master/io-package.json", + "acl": { + "object": { + "list": true, + "read": true, + "write": false, + "delete": false + }, + "state": { + "list": true, + "read": true, + "write": true, + "create": true, + "delete": false + }, + "users": { + "list": true, + "read": true, + "write": false, + "create": false, + "delete": false + }, + "other": { + "execute": false, + "http": true, + "sendto": false + }, + "file": { + "list": true, + "read": true, + "write": false, + "create": false, + "delete": false + } + } + }, + "acl": { + "owner": "system.user.admin", + "ownerGroup": "system.group.administrator", + "object": 1604 + } + }, + { + "_id": "enum.rooms", + "common": { + "icon": "home", + "name": { + "en": "Rooms", + "de": "Räume", + "ru": "Комнаты", + "pt": "Quartos", + "nl": "Kamers", + "fr": "Pièces", + "it": "Camere", + "es": "Habitaciones", + "pl": "Pokoje" + }, + "desc": { + "en": "List of the rooms", + "de": "Liste der Räumen", + "ru": "Список комнат", + "pt": "Lista dos quartos", + "nl": "Lijst met kamers", + "fr": "Liste des chambres", + "it": "Elenco delle stanze", + "es": "Lista de las habitaciones", + "pl": "Lista pokoi" + }, + "members": [], + "dontDelete": true + }, + "type": "enum", + "acl": { + "owner": "system.user.admin", + "ownerGroup": "system.group.administrator", + "permissions": 1911 + } + }, + { + "_id": "enum.functions", + "common": { + "icon": "lightbulb_outline", + "name": { + "en": "Functions", + "de": "Funktionen", + "ru": "функции", + "pt": "Funções", + "nl": "functies", + "fr": "Les fonctions", + "it": "funzioni", + "es": "Funciones", + "pl": "Funkcje" + }, + "desc": { + "en": "List of the functions", + "de": "Liste der Funktionen", + "ru": "Список функций", + "pt": "Lista das funções", + "nl": "Lijst met functies", + "fr": "Liste des fonctions", + "it": "Elenco delle funzioni", + "es": "Lista de las funciones", + "pl": "Lista funkcji" + }, + "members": [], + "dontDelete": true + }, + "type": "enum", + "acl": { + "owner": "system.user.admin", + "ownerGroup": "system.group.administrator", + "permissions": 1911 + } + }, + { + "_id": "system.config", + "type": "config", + "common": { + "name": "System configuration", + "city": "", + "country": "", + "longitude": "", + "latitude": "", + "language": "", + "tempUnit": "°C", + "currency": "", + "dontDelete": true, + "dateFormat": "DD.MM.YYYY", + "isFloatComma": true, + "licenseConfirmed": false, + "defaultHistory": "", + "activeRepo": "default", + "diag": "extended", + "tabs": [ + "tab-intro", + "tab-adapters", + "tab-instances", + "tab-objects", + "tab-log", + "tab-scenes", + "tab-javascript", + "tab-text2command-0", + "tab-node-red-0" + ] + }, + "native": {}, + "acl": { + "owner": "system.user.admin", + "ownerGroup": "system.group.administrator", + "permissions": 1604 + } + }, + { + "_id": "system.repositories", + "type": "config", + "common": { + "name": "System repositories", + "dontDelete": true + }, + "native": { + "repositories": { + "default": { + "link": "http://download.yunkong2.net/sources-dist.json", + "json": null + }, + "latest": { + "link": "http://download.yunkong2.net/sources-dist-latest.json", + "json": null + } + }, + "oldRepositories": { + "sources": { + "link": "conf/sources-dist.json", + "json": null + }, + "online": { + "link": "https://raw.githubusercontent.com/yunkong2/yunkong2.repositories/master/sources-dist.json", + "json": null + } + } + }, + "acl": { + "owner": "system.user.admin", + "ownerGroup": "system.group.administrator", + "permissions": 1604 + } + }, + { + "_id": "system.certificates", + "type": "config", + "common": { + "name": "System certificates" + }, + "native": { + "certificates": { + "defaultPrivate": "-----BEGIN RSA PRIVATE KEY-----\r\nMIICXQIBAAKBgQDFNHQmcPu5y9ngID3YUCr2NUY/eBceEKdFcEwgZBjp1DM52d/9JYFSrQGHQMvLopG5uiQXVip0mR95rZxaF0mo5wdTAhM1pcxThGeCghZVm9PoLbrakvl9+gSHoYbNVxfv9fi0cPYc7CbeiluhWWS0hm1VpWPM8PX6rkdz5r9OIwIDAQABAoGBAJVSWoChHHpa+ObUgv+/9Efpnv+AF0EUqxPRLFN6d8LWgtNTPl+YfovzpCydy7KtrlpLr/hbrloLd+HSq4ksCQEfJ7Le/4fjc2lt3Ib/K9qSr3bnmIWAK00VU+fFmN1NTFJTV0O2+ctCOY9ZRwue5ehTp9eqPjsGwdeldii1WbSBAkEA6Z0YjMg+04z1M8FEUWSdPf6AHWB45hDJ+qPuIDNZxvVOcEsTyRsfkb1PKZm2NDx6mBN16po13VkaQPy35ApoOwJBANgaMdbig76A1tvyhtklJPTU0g0N7CzXy+PNu8B3YghY8dYF/gSvcBr0d8xGaZEczGQ35C0Tb9gTadHL64kxuzkCQHYaQYsKwRhaLqxXjJ5Ja2UoAMTZPMWyvynDLmOBEmYPJfSHQB1vZOpc9mRlnUOTP7caP4a3J3wby7YHDUBwMnkCQHGx1mbn5chkoKY3gxrboAXvslOL76XoIy1HIHCyXrFlmlav8GUmqCSGWkDvCrt+G0re3P2aLE3SaOooD1OvBoECQQDXMxPNYVGIErO7hxp9T9BXKcbnQV/mNhJYdl9VUoVBgcVGatR1dBZX31Yt+HY4/ym9YdQ8MGCg2Kfmm0haLakP\r\n-----END RSA PRIVATE KEY-----\r\n", + "defaultPublic": "-----BEGIN CERTIFICATE-----\r\nMIICSTCCAbICCQDwWQ5sMoq7ETANBgkqhkiG9w0BAQUFADBpMQswCQYDVQQGEwJERTETMBEGA1UECAwKU29tZS1TdGF0ZTERMA8GA1UECgwIaW9Ccm9rZXIxEDAOBgNVBAMMB0JsdWVmb3gxIDAeBgkqhkiG9w0BCQEWEWRvZ2Fmb3hAZ21haWwuY29tMB4XDTE2MDQyNTIxMjQwMVoXDTE3MDQyNTIxMjQwMVowaTELMAkGA1UEBhMCREUxEzARBgNVBAgMClNvbWUtU3RhdGUxETAPBgNVBAoMCGlvQnJva2VyMRAwDgYDVQQDDAdCbHVlZm94MSAwHgYJKoZIhvcNAQkBFhFkb2dhZm94QGdtYWlsLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAxTR0JnD7ucvZ4CA92FAq9jVGP3gXHhCnRXBMIGQY6dQzOdnf/SWBUq0Bh0DLy6KRubokF1YqdJkfea2cWhdJqOcHUwITNaXMU4RngoIWVZvT6C262pL5ffoEh6GGzVcX7/X4tHD2HOwm3opboVlktIZtVaVjzPD1+q5Hc+a/TiMCAwEAATANBgkqhkiG9w0BAQUFAAOBgQBVhdIg59lHKtdpv5O0icvqD4f0tbqMvhWJ/7fhzr1fdjb5OK74g2G90KMhYnzOk0aZu4pgEoXHugpBLb+ndxJnG41pIYe2qg4tp6AjR/uFswdrBLRUhW63yls3FiTEJjKCrGNEdjZoqsTEfwhXab3EoT7tWu+st1V0yiHlsvRGTg==\r\n-----END CERTIFICATE-----\r\n" + } + }, + "acl": { + "owner": "system.user.admin", + "ownerGroup": "system.group.administrator", + "permissions": 1536 + } + } + ] +} diff --git a/iobroker b/iobroker new file mode 100644 index 0000000..49f3c4d --- /dev/null +++ b/iobroker @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require(__dirname + '/lib/setup.js'); \ No newline at end of file diff --git a/iobroker.bat b/iobroker.bat new file mode 100644 index 0000000..5a35198 --- /dev/null +++ b/iobroker.bat @@ -0,0 +1 @@ +node yunkong2.js %1 %2 %3 %4 %5 %6 %7 %8 \ No newline at end of file diff --git a/killall.sh b/killall.sh new file mode 100644 index 0000000..0d55c57 --- /dev/null +++ b/killall.sh @@ -0,0 +1,2 @@ +sudo pgrep -f '^io.*' | sudo xargs kill -9 +sudo pgrep -f '^node-red*' | sudo xargs kill -9 \ No newline at end of file diff --git a/lib/adapter.js b/lib/adapter.js new file mode 100644 index 0000000..ad0c1d6 --- /dev/null +++ b/lib/adapter.js @@ -0,0 +1,5102 @@ +/* jshint -W097 */ +/* jshint strict: false */ +/* jslint node: true */ +'use strict'; + +// This is file, that makes all communication with controller. All options are optional except name. +// following options are available: +// name: name of the adapter. Must be exactly the same as directory name. +// dirname: adapter directory name +// instance: instance number of adapter +// objects: true or false, if desired to have oObjects. This is a list with all states, channels and devices of this adapter and it will be updated automatically. +// states: true or false, if desired to have oStates. This is a list with all states values and it will be updated automatically. +// systemConfig: if required system configuration. Store it in systemConfig attribute +// objectChange: callback function (id, obj) that will be called if object changed +// stateChange: callback function (id, obj) that will be called if state changed +// message: callback to inform about new message the adapter +// unload: callback to stop the adapter +// config: configuration of the connection to controller +// noNamespace: return short names of objects and states in objectChange and in stateChange + +var net = require('net'); +var fs = require('fs'); +var extend = require('node.extend'); +var util = require('util'); +var EventEmitter = require('events').EventEmitter; +var tools = require(__dirname + '/tools'); +var getConfigFileName = tools.getConfigFileName; +var schedule; + +var password = require(__dirname + '/password'); +var config = null; +var that; +var defaultObjs; + +if (fs.existsSync(getConfigFileName())) { + config = JSON.parse(fs.readFileSync(getConfigFileName(), 'utf8')); + if (!config.states) config.states = {type: 'file'}; + if (!config.objects) config.objects = {type: 'file'}; +} else { + throw 'Cannot find ' + getConfigFileName(); +} + +/** + * Adapter class + * + * @class + * @param {string|object} options object like {name: "adapterName", systemConfig: true} or just "adapterName" + * @return {object} object instance + */ +function Adapter(options) { + if (!(this instanceof Adapter)) return new Adapter(options); + + if (!options || (!config && !options.config)) throw 'Configuration not set!'; + + if (options.config && !options.config.log) options.config.log = config.log; + + config = options.config || config; + var regUser = /^system\.user\./; + var regGroup = /^system\.group\./; + + that = this; + that.logList = []; + + // possible arguments + // 0,1,.. - instance + // info, debug, warn, error - log level + // --force + // --logs + // --silent + // --install + if (process.argv) { + for (var a = 1; a < process.argv.length; a++) { + if (process.argv[a] === 'info' || process.argv[a] === 'debug' || process.argv[a] === 'error' || process.argv[a] === 'warn' || process.argv[a] === 'silly') { + config.log.level = process.argv[a]; + } else if (process.argv[a] === '--silent') { + config.isInstall = true; + process.argv[a] = '--install'; + } else if (process.argv[a] === '--install') { + config.isInstall = true; + } else if (process.argv[a] === '--logs') { + config.consoleOutput = true; + } else if (process.argv[a] === '--force') { + config.forceIfDisabled = true; + } else if (parseInt(process.argv[a], 10).toString() === process.argv[a]) { + config.instance = parseInt(process.argv[a], 10); + } + } + } + + config.log.level = config.log.level || 'info'; + if (config.log.noStdout && process.argv && process.argv.indexOf('--console') !== -1) { + config.log.noStdout = false; + } + + var logger = require(__dirname + '/logger.js')(config.log); + + // compatibility + if (!logger.silly) { + logger.silly = logger.debug; + } + + // enable "var adapter = require(__dirname + '/../../lib/adapter.js')('adapterName');" call + if (typeof options === 'string') options = {name: options}; + + if (!options) throw 'Empty options!'; + if (!options.name) throw 'No name of adapter!'; + + // If installed as npm module + if (options.dirname) { + this.adapterDir = options.dirname.replace(/\\/g, '/'); + } else { + this.adapterDir = __dirname.replace(/\\/g, '/').split('/'); + // it can be .../node_modules/appName.js-controller/node_modules/appName.adapter + // .../appName.js-controller/node_modules/appName.adapter + // .../appName.js-controller/adapter/adapter + // remove "lib" + this.adapterDir.pop(); + var jsc = this.adapterDir.pop(); + if ((jsc === tools.appName + '.js-controller' || jsc === tools.appName.toLowerCase() + '.js-controller') && this.adapterDir.pop() === 'node_modules') { + // js-controller is installed as npm + var appName = tools.appName.toLowerCase(); + this.adapterDir = this.adapterDir.join('/'); + if (fs.existsSync(this.adapterDir + '/node_modules/' + appName + '.' + options.name)) { + this.adapterDir += '/node_modules/' + appName + '.' + options.name; + } else if (fs.existsSync(this.adapterDir + '/node_modules/' + appName + '.js-controller/node_modules/' + appName + '.' + options.name)) { + this.adapterDir += '/node_modules/' + appName + '.js-controller/node_modules/' + appName + '.' + options.name; + } else if (fs.existsSync(this.adapterDir + '/node_modules/' + appName + '.js-controller/adapter/' + options.name)) { + this.adapterDir += '/node_modules/' + appName + '.js-controller/adapter/' + options.name; + } else if (fs.existsSync(this.adapterDir + '/node_modules/' + tools.appName + '.js-controller/node_modules/' + appName + '.' + options.name)) { + this.adapterDir += '/node_modules/' + tools.appName + '.js-controller/node_modules/' + appName + '.' + options.name; + } else { + logger.error('Cannot find directory of adapter ' + options.name); + process.exit(10); + } + } else { + this.adapterDir = __dirname.replace(/\\/g, '/'); + // remove "/lib" + this.adapterDir = this.adapterDir.substring(0, this.adapterDir.length - 4); + if (fs.existsSync(this.adapterDir + '/node_modules/' + tools.appName + '.' + options.name)) { + this.adapterDir += '/node_modules/' + tools.appName + '.' + options.name; + } else if (fs.existsSync(this.adapterDir + '/../node_modules/' + tools.appName + '.' + options.name)) { + var parts = this.adapterDir.split('/'); + parts.pop(); + this.adapterDir = parts.join('/') + '/node_modules/' + tools.appName + '.' + options.name; + } else { + logger.error('Cannot find directory of adapter ' + options.name); + process.exit(10); + } + } + } + + if (fs.existsSync(this.adapterDir + '/package.json')) { + this.pack = JSON.parse(fs.readFileSync(this.adapterDir + '/package.json', 'utf8')); + } else { + logger.info('Non npm module. No package.json'); + } + + if (!this.pack || !this.pack.io) { + if (fs.existsSync(this.adapterDir + '/io-package.json')) { + this.ioPack = JSON.parse(fs.readFileSync(this.adapterDir + '/io-package.json', 'utf8')); + } else { + logger.error('Cannot find: ' + this.adapterDir + '/io-package.json'); + process.exit(10); + } + } else { + this.ioPack = this.pack.io; + } + + // If required system configuration. Store it in systemConfig attribute + if (options.systemConfig) that.systemConfig = config; + + var States; + if (config.states && config.states.type) { + if (config.states.type === 'file') { + States = require(__dirname + '/states/statesInMemClient'); + } else if (config.states.type === 'redis') { + States = require(__dirname + '/states/statesInRedis'); + } else { + throw 'Unknown objects type: ' + config.states.type; + } + } else { + States = require(__dirname + '/states'); + } + + var Objects; + if (config.objects && config.objects.type) { + if (config.objects.type === 'file') { + Objects = require(__dirname + '/objects/objectsInMemClient'); + } else if (config.objects.type === 'redis') { + Objects = require(__dirname + '/objects/objectsInRedis'); + } else if (config.objects.type === 'couch') { + Objects= require(__dirname + '/objects/objectsInCouch'); + } else { + throw 'Unknown objects type: ' + config.objects.type; + } + } else { + Objects = require(__dirname + '/objects'); + } + + var os = require('os'); + var ifaces = os.networkInterfaces(); + var ipArr = []; + for (var dev in ifaces) { + if (!ifaces.hasOwnProperty(dev)) continue; + /*jshint loopfunc:true */ + ifaces[dev].forEach(function (details) { + if (!details.internal) ipArr.push(details.address); + }); + } + + var instance = (options.instance !== undefined) ? options.instance : (config.instance || 0); + + that.name = options.name; + that.namespace = options.name + '.' + instance; + that.users = []; // cache of user groups + that.defaultHistory = null; + that.autoSubscribe = null; // array of instances, that support auto subscribe + that.inputCount = 0; + that.outputCount = 0; + + var reportInterval; + + var callbackId = 1; + that.getPortRunning = null; + + /** + * Helper function to find next free port + * + * Looks for first free TCP port starting with given one: + *


+     *     adapter.getPort(8081, function (port) {
+     *         adapter.log.debug('Followinf port is free: ' + port);
+     *     });
+     * 
+ * + * @alias getPort + * @memberof Adapter + * @param {number} port port number to start the search for free port + * @param {function} callback return result + *
function (port) {}
+ */ + that.getPort = function adapterGetPort(port, callback) { + if (!port) throw 'adapterGetPort: no port'; + + port = parseInt(port, 10); + that.getPortRunning = {port: port, callback: callback}; + var server = net.createServer(); + try { + + server.listen(port, function (/* err */) { + server.once('close', function () { + if (typeof callback === 'function') { + //that.getPortRunning = null; + callback(port); + } + }); + server.close(); + }); + server.on('error', function (/* err */) { + setTimeout(function () { + that.getPort(port + 1, callback); + }, 100); + }); + } catch (e) { + setImmediate(function () { + that.getPort(port + 1, callback); + }); + } + }; + + /** + * Promise-version of Adapter.getPort + */ + that.getPortAsync = tools.promisifyNoError(that.getPort, that); + + /** + * validates user and password + * + * + * @alias checkPassword + * @memberof Adapter + * @param {string} user user name as text + * @param {string} pw password as text + * @param {object} options optional user context + * @param {function} callback return result + *

+     *            function (result) {
+     *              adapter.log.debug('User is valid');
+     *            }
+     *        
+ */ + that.checkPassword = function checkPassword(user, pw, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!callback) throw 'checkPassword: no callback'; + + if (user && !regUser.test(user)) { + user = 'system.user.' + user; + } + + that.getForeignObject(user, options, function (err, obj) { + if (err || !obj || !obj.common || (!obj.common.enabled && user !== 'system.user.admin')) { + callback(false); + return; + } + password(pw).check(obj.common.password, function (err, res) { + callback(res); + }); + }); + }; + /** + * Promise-version of Adapter.checkPassword + */ + that.checkPasswordAsync = tools.promisifyNoError(that.checkPassword, that); + + /** + * sets the user's password + * + * @alias setPassword + * @memberof Adapter + * @param {string} user user name as text + * @param {string} pw password as text + * @param {object} options optional user context + * @param {function} callback return result + *

+     *            function (err) {
+     *              if (err) adapter.log.error('Cannot set password: ' + err);
+     *            }
+     *        
+ */ + that.setPassword = function setPassword(user, pw, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (user && !regUser.test(user)) { + user = 'system.user.' + user; + } + + that.getForeignObject(user, options, function (err, obj) { + if (err || !obj) { + if (typeof callback === 'function') callback('User does not exist'); + return; + } + password(pw).hash(null, null, function (err, res) { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + that.extendForeignObject(user, { + common: { + password: res + } + }, options, function () { + if (typeof callback === 'function') callback(null); + }); + }); + }); + + }; + /** + * Promise-version of Adapter.setPassword + */ + that.setPasswordAsync = tools.promisify(that.setPassword, that); + + /** + * returns if user exists and is in the group + * + * This function used mostly internally and the adapter developer do not require it. + * + * @alias checkGroup + * @memberof Adapter + * @param {string} user user name as text + * @param {string} group group name + * @param {object} options optional user context + * @param {function} callback return result + *

+     *            function (result) {
+     *              if (result) adapter.log.debug('User exists and in the group');
+     *            }
+     *        
+ */ + that.checkGroup = function checkGroup(user, group, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (user && !regUser.test(user)) { + user = 'system.user.' + user; + } + if (group && !regGroup.test(group)) { + group = 'system.group.' + group; + } + that.getForeignObject(user, options, function (err, obj) { + if (err || !obj) { + callback(false); + return; + } + that.getForeignObject(group, options, function (err, obj) { + if (err || !obj) { + callback(false); + return; + } + if (obj.common.members.indexOf(user) !== -1) { + callback(true); + } else { + callback(false); + } + }); + }); + }; + /** + * Promise-version of Adapter.checkGroup + */ + that.checkGroupAsync = tools.promisifyNoError(that.checkGroup, that); + + /** @typedef {{[permission: string]: {type: 'object' | 'state' | '' | 'other' | 'file', operation: string}}} CommandsPermissions */ + + /** + * get the user permissions + * + * This function used mostly internally and the adapter developer do not require it. + * The function reads permissions of user's groups (it can be more than one) and merge permissions together + * + * @alias calculatePermissions + * @memberof Adapter + * @param {string} user user name as text + * @param {object} commandsPermissions object that describes the access rights like + *

+     *         // static information
+     *         var commandsPermissions = {
+     *            getObject:          {type: 'object',    operation: 'read'},
+     *            getObjects:         {type: 'object',    operation: 'list'},
+     *            getObjectView:      {type: 'object',    operation: 'list'},
+     *            setObject:          {type: 'object',    operation: 'write'},
+     *            subscribeObjects:   {type: 'object',    operation: 'read'},
+     *            unsubscribeObjects: {type: 'object',    operation: 'read'},
+     *
+     *            getStates:          {type: 'state',     operation: 'list'},
+     *            getState:           {type: 'state',     operation: 'read'},
+     *            setState:           {type: 'state',     operation: 'write'},
+     *            getStateHistory:    {type: 'state',     operation: 'read'},
+     *            subscribe:          {type: 'state',     operation: 'read'},
+     *            unsubscribe:        {type: 'state',     operation: 'read'},
+     *            getVersion:         {type: '',          operation: ''},
+     *
+     *            httpGet:            {type: 'other',     operation: 'http'},
+     *            sendTo:             {type: 'other',     operation: 'sendto'},
+     *            sendToHost:         {type: 'other',     operation: 'sendto'},
+     *
+     *            readFile:           {type: 'file',      operation: 'read'},
+     *            readFile64:         {type: 'file',      operation: 'read'},
+     *            writeFile:          {type: 'file',      operation: 'write'},
+     *            writeFile64:        {type: 'file',      operation: 'write'},
+     *            unlink:             {type: 'file',      operation: 'delete'},
+     *            rename:             {type: 'file',      operation: 'write'},
+     *            mkdir:              {type: 'file',      operation: 'write'},
+     *            readDir:            {type: 'file',      operation: 'list'},
+     *            chmodFile:          {type: 'file',      operation: 'write'},
+     *
+     *            authEnabled:        {type: '',          operation: ''},
+     *            disconnect:         {type: '',          operation: ''},
+     *            listPermissions:    {type: '',          operation: ''},
+     *            getUserPermissions: {type: 'object',    operation: 'read'}
+     *         };
+     *        
+ * @param {object} options optional user context + * @param {function} callback return result + *

+     *            function (acl) {
+     *              // Access control object for admin looks like:
+     *              // {
+     *              //    file: {
+     *              //         read:       true,
+     *              //         write:      true,
+     *              //         'delete':   true,
+     *              //         create:     true,
+     *              //         list:       true
+     *              //     },
+     *              //     object: {
+     *              //         read:       true,
+     *              //         write:      true,
+     *              //         'delete':   true,
+     *              //         list:       true
+     *              //     },
+     *              //     state: {
+     *              //         read:       true,
+     *              //         write:      true,
+     *              //         'delete':   true,
+     *              //         create:     true,
+     *              //         list:       true
+     *              //     },
+     *              //     user: 'admin',
+     *              //     users:  {
+     *              //         read:       true,
+     *              //         write:      true,
+     *              //         create:     true,
+     *              //         'delete':   true,
+     *              //         list:       true
+     *              //     },
+     *              //     other: {
+     *              //         execute:    true,
+     *              //         http:       true,
+     *              //         sendto:     true
+     *              //     },
+     *              //     groups: ['administrator'] // can be more than one
+     *              // }
+     *            }
+     *        
+ */ + that.calculatePermissions = function (user, commandsPermissions, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!regUser.test(user)) { + user = 'system.user.' + user; + } + // read all groups + var acl = {user: user}; + if (user === 'system.user.admin') { + acl.groups = ['system.group.administrator']; + for (var c in commandsPermissions) { + if (!commandsPermissions.hasOwnProperty(c) || !commandsPermissions[c].type) continue; + acl[commandsPermissions[c].type] = acl[commandsPermissions[c].type] || {}; + acl[commandsPermissions[c].type][commandsPermissions[c].operation] = true; + } + + if (callback) callback(acl); + return; + } + acl.groups = []; + that.getForeignObjects('*', 'group', options, function (err, groups) { + // aggregate all groups permissions, where this user is + if (groups) { + for (var g in groups) { + if (!groups.hasOwnProperty(g)) continue; + if (groups[g] && + groups[g].common && + groups[g].common.members && + groups[g].common.members.indexOf(user) !== -1) { + acl.groups.push(groups[g]._id); + if (groups[g]._id === 'system.group.administrator') { + acl = { + file: { + read: true, + write: true, + 'delete': true, + create: true, + list: true + }, + object: { + read: true, + write: true, + 'delete': true, + list: true + }, + state: { + read: true, + write: true, + 'delete': true, + create: true, + list: true + }, + user: user, + users: { + read: true, + write: true, + create: true, + 'delete': true, + list: true + }, + other: { + execute: true, + http: true, + sendto: true + }, + groups: acl.groups + }; + break; + } + + var gAcl = groups[g].common.acl; + try { + for (var type in gAcl) { + if (!gAcl.hasOwnProperty(type)) continue; + + // fix bug. Some version have user instead of users. + if (type === 'user') { + acl.users = acl.users || {}; + } else { + acl[type] = acl[type] || {}; + } + for (var op in gAcl[type]) { + if (gAcl[type].hasOwnProperty(op)) { + // fix error + if (type === 'user') { + acl.users[op] = acl.users[op] || gAcl.user[op]; + } else { + acl[type][op] = acl[type][op] || gAcl[type][op]; + } + } + } + } + } catch (e) { + that.log.error('Cannot set acl: ' + e); + that.log.error('Cannot set acl: ' + JSON.stringify(gAcl)); + that.log.error('Cannot set acl: ' + JSON.stringify(acl)); + } + } + } + } + + if (callback) callback(acl); + }); + }; + /** + * Promise-version of Adapter.calculatePermissions + */ + that.calculatePermissionsAsync = tools.promisifyNoError(that.calculatePermissions, that); + + function readFileCertificate(cert) { + if (typeof cert === 'string') { + try { + if (cert.length < 1024 && fs.existsSync(cert)) { + cert = fs.readFileSync(cert).toString(); + // start watcher of this file + fs.watch(cert, function (eventType, filename) { + that.log.warn('New certificate "' + filename + '" detected. Restart adapter'); + setTimeout(stop, 2000, false, true); + }); + } + } catch (e) { + // ignore + } + } + return cert; + } + /** + * returns SSL certificates by name + * + * This function returns SSL certificates (private key, public cert and chained certificate). + * Names are defined in the system's configuration in admin, e.g. "defaultPrivate", "defaultPublic". + * The result can be directly used for creation of https server. + * + * @alias getCertificates + * @memberof Adapter + * @param {string} publicName public certificate name + * @param {string} privateName private certificate name + * @param {string} chainedName optional chained certificate name + * @param {function} callback return result + *

+     *            function (err, certs) {
+     *              adapter.log.debug('private key: ' + certs.key);
+     *              adapter.log.debug('public cert: ' + certs.cert);
+     *              adapter.log.debug('chained cert: ' + certs.ca);
+     *            }
+     *        
+ */ + that.getCertificates = function (publicName, privateName, chainedName, callback) { + if (typeof publicName === 'function') { + callback = publicName; + publicName = null; + } + if (typeof privateName === 'function') { + callback = privateName; + privateName = null; + } + if (typeof chainedName === 'function') { + callback = chainedName; + chainedName = null; + } + publicName = publicName || that.config.certPublic; + privateName = privateName || that.config.certPrivate; + chainedName = chainedName || that.config.certChained; + + // Load certificates + that.getForeignObject('system.certificates', function (err, obj) { + if (err || !obj || + !obj.native.certificates || + !publicName || + !privateName || + !obj.native.certificates[publicName] || + !obj.native.certificates[privateName] || + (chainedName && !obj.native.certificates[chainedName]) + ) { + that.log.error('Cannot enable secure web server, because no certificates found: ' + publicName + ', ' + privateName + ', ' + chainedName); + if (callback) callback('Not found'); + } else { + var ca; + if (chainedName) { + var chained = readFileCertificate(obj.native.certificates[chainedName]).split('-----END CERTIFICATE-----\r\n'); + ca = []; + for (var c = 0; c < chained.length; c++) { + if (chained[c].replace(/[\r\n|\r|\n]+/, '').trim()) { + ca.push(chained[c] + '-----END CERTIFICATE-----\r\n'); + } + } + } + if (callback) { + callback(null, { + key: readFileCertificate(obj.native.certificates[privateName]), + cert: readFileCertificate(obj.native.certificates[publicName]), + ca: ca + }, obj.native.letsEncrypt); + } + } + }); + }; + /** + * Promise-version of Adapter.getCertificates + */ + that.getCertificatesAsync = tools.promisify(that.getCertificates, that); + + // Can be later deleted if no more appears + that.inited = false; + + initObjects(function () { + if (that.inited) { + if (that.log) that.log.warn('Reconnection to DB.'); + return; + } + + that.inited = true; + + // auto oObjects + if (options.objects) { + that.getAdapterObjects(function (objs) { + that.oObjects = objs; + that.subscribeObjects('*'); + initStates(prepareInitAdapter); + }); + } else { + initStates(prepareInitAdapter); + } + }); + + function createInstancesObjects(callback, objs) { + if (!objs) { + objs = that.ioPack.instanceObjects; + } + + if (!objs || !objs.length) { + callback(); + } else { + var obj = objs.shift(); + that.getObject(obj._id, function (err, _obj) { + if (!_obj) { + if (obj.common) { + if (obj.common.name) { + obj.common.name = obj.common.name.replace('%INSTANCE%', instance); + } + if (obj.common.desc) { + obj.common.desc = obj.common.desc.replace('%INSTANCE%', instance); + } + } + + that.setObject(obj._id, obj, function (err) { + if (err && that.log) that.log.error('Cannot setObject: ' + err); + setImmediate(createInstancesObjects, callback, objs); + }); + } else { + setImmediate(createInstancesObjects, callback, objs); + } + }); + } + } + + function prepareInitAdapter() { + that.getForeignState('system.adapter.' + that.namespace + '.alive', function (err, res) { + if (options.instance !== undefined) { + initAdapter(options); + } else + if (!config.isInstall && res && res.val === true) { + logger.error(options.name + '.' + instance + ' already running'); + process.exit(7); + } else { + that.getForeignObject('system.adapter.' + that.namespace, function (err, res) { + if ((err || !res) && !config.isInstall) { + logger.error(options.name + '.' + instance + ' invalid config'); + process.exit(2); + } else { + createInstancesObjects(function () { + initAdapter(res); + }); + } + }); + } + }); + } + + function autoSubscribeOn(cb) { + if (!that.autoSubscribe) { + // collect all + that.objects.getObjectView('system', 'instance', {startkey: 'system.adapter.', endkey: 'system.adapter.\u9999'}, options, function (err, res) { + if (res && res.rows) { + that.autoSubscribe = []; + for (var c = res.rows.length - 1; c >= 0; c--) { + + if (res.rows[c].value.common.subscribable) { + var _id = res.rows[c].id.substring(15); + if (that.autoSubscribe.indexOf(_id) === -1) that.autoSubscribe.push(_id); + } + } + } + + if (typeof cb === 'function') cb(); + }); + // because of autoSubscribe + that.objects.subscribe('system.adapter.*'); + } else if (typeof cb === 'function') { + cb(); + } + } + + function initObjects(cb) { + that.objects = new Objects({ + namespace: that.namespace, + connection: config.objects, + logger: logger, + connected: function () { + that.connected = true; + // Read dateformat if using of formatDate is announced + if (options.useFormatDate) { + that.getForeignObject('system.config', function (err, data) { + if (data && data.common) { + that.dateFormat = data.common.dateFormat; + that.isFloatComma = data.common.isFloatComma; + } + if (typeof cb === 'function') cb(); + }); + } else if (typeof cb === 'function') { + cb(); + } + }, + disconnected: function () { + that.connected = false; + }, + change: function (id, obj) { + if (obj === 'null') obj = null; + if (!id) { + logger.error(that.namespace + ' change ID is empty: ' + JSON.stringify(obj)); + return; + } + + // If desired, that adapter must be terminated + if (id === 'system.adapter.' + that.namespace && obj && obj.common && obj.common.enabled === false) { + that.log.info('Adapter is disabled => stop'); + if (!obj.common.enabled) { + stop(); + setTimeout(function () { + process.exit(); + }, 4000); + } + } + + // update oObjects structure if desired + if (that.oObjects) { + if (obj) { + that.oObjects[id] = obj; + } else { + delete that.oObjects[id]; + } + } + + // process autosubscribe adapters + if (id.match(/^system\.adapter\./)) { + if (obj && obj.common.subscribable) { + var _id = id.substring(15); // 'system.adapter.'.length + if (obj.common.enabled) { + if (that.autoSubscribe.indexOf(_id) === -1) that.autoSubscribe.push(_id); + } else { + var pos = that.autoSubscribe.indexOf(_id); + if (pos !== -1) that.autoSubscribe.splice(pos, 1); + } + } + } + + // It was an error in the calculation + if ((options.noNamespace || config.noNamespace) && that._namespaceRegExp.test(id)) { + // emit 'objectChange' event instantly + setImmediate(function () { + if (typeof options.objectChange === 'function') options.objectChange(id.substring(that.namespace.length + 1), obj); + that.emit('objectChange', id.substring(that.namespace.length + 1), obj); + }); + } else { + setImmediate(function () { + if (typeof options.objectChange === 'function') options.objectChange(id, obj); + // emit 'objectChange' event instantly + that.emit('objectChange', id, obj); + }); + } + }, + connectTimeout: function (/* err */) { + if (config.isInstall) { + if (logger) logger.warn(that.namespace + ' no connection to objects DB'); + process.exit(0); + } else { + if (logger) logger.error(that.namespace + ' no connection to objects DB'); + } + } + }); + + that._namespaceRegExp = new RegExp('^' + that.namespace); // chache the regex object 'adapter.0' + + that._fixId = function _fixId(id, isPattern/* , type */) { + var result = ''; + // If id is an object + if (typeof id === 'object') { + // Add namespace + device + channel + result = that.namespace + '.' + (id.device ? id.device + '.' : '') + (id.channel ? id.channel + '.' : '') + (id.state ? id.state : ''); + } else { + result = id; + + if (!that._namespaceRegExp.test(id)) { + if (!isPattern) { + result = that.namespace + (id ? '.' + id : ''); + } else { + result = that.namespace + '.' + (id ? id : ''); + } + } + } + return result; + }; + + /** + * Creates or overwrites object in objectDB. + * + * This function can create or overwrite objects in objectDB for this adapter. + * Only Ids that belong to this adapter can be modified. So the function automatically adds "adapter.X." to ID. + * common, native and type attributes are mandatory and it will be checked. + * Additionally type "state" requires role, type and name, e.g.: + *
{
+         *     common: {
+         *          name: 'object name',
+         *          type: 'number', // string, boolean, object, mixed, array
+         *          role: 'value'   // see https://github.com/yunkong2/yunkong2/blob/master/doc/SCHEMA.md#state-commonrole
+         *     },
+         *     native: {},
+         *     type: 'state' // channel, device
+         * }
+ * + * @alias setObject + * @memberof Adapter + * @param {string} id object ID, that must be overwritten or created. + * @param {object} obj new object + * @param {object} options optional user context + * @param {function} callback return result + *

+         *            function (err, obj) {
+         *              // obj is {id: id}
+         *              if (err) adapter.log.error('Cannot write object: ' + err);
+         *            }
+         *        
+ */ + that.setObject = function setObject(id, obj, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!defaultObjs) { + defaultObjs = require(__dirname + '/defaultObjs.js')('de', '°C', 'EUR'); + } + + if (!id) { + logger.error(that.namespace + ' setObject id missing!!'); + if (typeof callback === 'function') callback('id missing!'); + return; + } + + if (!obj) { + logger.error(that.namespace + ' setObject ' + id + ' object missing!'); + if (typeof callback === 'function') callback('object missing!'); + return; + } + + if (obj.hasOwnProperty('type')) { + if (!obj.hasOwnProperty('native')) { + logger.warn(that.namespace + ' setObject ' + id + ' (type=' + obj.type + ') property native missing!'); + obj.native = {}; + } + // Check property 'common' + if (!obj.hasOwnProperty('common')) { + logger.warn(that.namespace + ' setObject ' + id + ' (type=' + obj.type + ') property common missing!'); + obj.common = {}; + } else if (obj.type === 'state') { + // Try to extend the model for type='state' + // Check property 'role' by 'state' + if (obj.common.hasOwnProperty('role') && defaultObjs[obj.common.role]) { + obj.common = extend(true, defaultObjs[obj.common.role], obj.common); + } else if (!obj.common.hasOwnProperty('role')) { + logger.warn(that.namespace + ' setObject ' + id + ' (type=' + obj.type + ') property common.role missing!'); + } + if (!obj.common.hasOwnProperty('type')) { + logger.warn(that.namespace + ' setObject ' + id + ' (type=' + obj.type + ') property common.type missing!'); + } + } + + if (!obj.common.hasOwnProperty('name')) { + obj.common.name = id; + logger.debug(that.namespace + ' setObject ' + id + ' (type=' + obj.type + ') property common.name missing, using id as name'); + } + + id = that._fixId(id, false, obj.type); + + if (obj.children || obj.parent) { + logger.warn(that.namespace + ' Do not use parent or children for ' + id); + } + if (!obj.from) obj.from = 'system.adapter.' + that.namespace; + if (!obj.ts) obj.ts = new Date().getTime(); + that.objects.setObject(id, obj, options, callback); + } else { + logger.error(that.namespace + ' setObject ' + id + ' mandatory property type missing!'); + if (typeof callback === 'function') callback('mandatory property type missing!'); + } + }; + /** + * Promise-version of Adapter.setObject + */ + that.setObjectAsync = tools.promisify(that.setObject, that); + + /** + * Get all states, channels and devices of this adapter. + * + * @alias getAdapterObjects + * @memberof Adapter + * @param {function} callback return result + *

+         *            function (objects) {
+         *                for (var id in objects) {
+         *                    adapter.log.debug(id);
+         *                }
+         *            }
+         *        
+ */ + that.getAdapterObjects = function (callback) { + var objects = {}; + + that.objects.getObjectView('system', 'state', {startkey: that.namespace + '.', endkey: that.namespace + '.\u9999', include_docs: true}, function (err, _states) { + that.objects.getObjectView('system', 'channel', {startkey: that.namespace + '.', endkey: that.namespace + '.\u9999', include_docs: true}, function (err, _channels) { + that.objects.getObjectView('system', 'device', {startkey: that.namespace + '.', endkey: that.namespace + '.\u9999', include_docs: true}, function (err, _devices) { + if (_channels) { + for (var c = _channels.rows.length - 1; c >= 0; c--) { + objects[_channels.rows[c].id] = _channels.rows[c].value; + } + } + if (_devices) { + for (var d = _devices.rows.length - 1; d >= 0; d--) { + objects[_devices.rows[d].id] = _devices.rows[d].value; + } + } + if (_states) { + if (options.states) that.oStates = {}; + for (var s = _states.rows.length - 1; s >= 0; s--) { + objects[_states.rows[s].id] = _states.rows[s].value; + if (that.oStates) { + that.oStates[_states.rows[s].id] = null; + } + } + } + if (typeof callback === 'function') callback(objects); + }); + }); + }); + }; + /** + * Promise-version of Adapter.getAdapterObjects + */ + that.getAdapterObjectsAsync = tools.promisifyNoError(that.getAdapterObjects, that); + + /** + * Extend some object and create it if it does not exist + * + * You can change or extend some object. E.g existing object is: + *

+         *     {
+         *          common: {
+         *              name: 'Adapter name',
+         *              desc: 'Description'
+         *          },
+         *          type: 'state',
+         *          native: {
+         *              unused: 'text'
+         *          }
+         *     }
+         * 
+ * + * If following object will be passed as argument + * + *

+         *     {
+         *          common: {
+         *              desc: 'New description',
+         *              min: 0,
+         *              max: 100
+         *          },
+         *          native: {
+         *              unused: null
+         *          }
+         *     }
+         * 
+ * + * We will get as output: + *

+         *     {
+         *          common: {
+         *              desc: 'New description',
+         *              min: 0,
+         *              max: 100
+         *          },
+         *          type: 'state',
+         *          native: {
+         *          }
+         *     }
+         * 
+ * + * + * @alias extendObject + * @memberof Adapter + * @param {string} id object ID, that must be extended + * @param {object} obj part that must be extended + * @param {object} options optional user context + * @param {function} callback return result + *

+         *            function (err, obj) {
+         *                if (err) adapter.log.error(err);
+         *                // obj is {"id": id}
+         *            }
+         *        
+ */ + that.extendObject = function extendObject(id, obj, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + id = that._fixId(id, false, obj.type); + + if (obj.children || obj.parent) { + logger.warn(that.namespace + ' Do not use parent or children for ' + id); + } + // delete arrays if they should be changed + if (obj && ( + (obj.common && obj.common.members) || + (obj.native && obj.native.repositories) || + (obj.native && obj.native.certificates) || + (obj.native && obj.native.devices)) + ) { + // Read whole object + that.objects.getObject(id, options, function (err, oldObj) { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + if (!oldObj) { + logger.error(that.namespace + ' Object ' + id + ' not exist!'); + oldObj = {}; + } + if (obj.native && obj.native.repositories && oldObj.native && oldObj.native.repositories) { + oldObj.native.repositories = []; + } + if (obj.common && obj.common.members && oldObj.common && oldObj.common.members) { + oldObj.common.members = []; + } + if (obj.native && obj.native.certificates && oldObj.native && oldObj.native.certificates) { + oldObj.native.certificates = []; + } + if (obj.native && obj.native.devices && oldObj.native && oldObj.native.devices) { + oldObj.native.devices = []; + } + obj = extend(true, oldObj, obj); + + if (!obj.from) obj.from = 'system.adapter.' + that.namespace; + if (!obj.ts) obj.ts = new Date().getTime(); + + that.objects.setObject(id, obj, options, callback); + }); + } else { + if (!obj.from) obj.from = 'system.adapter.' + that.namespace; + if (!obj.ts) obj.ts = new Date().getTime(); + that.objects.extendObject(id, obj, options, callback); + } + }; + /** + * Promise-version of Adapter.extendObject + */ + that.extendObjectAsync = tools.promisify(that.extendObject, that); + + /** + * Same as {@link Adapter.setObject}, but for any object. + * + * ID must be specified as a full name with adapter namespace. E.g "hm-rpc.0.ABC98989.1.STATE" + * + * @alias setForeignObject + * @memberof Adapter + * @param {string} id object ID, that must be overwritten or created. + * @param {object} obj new object + * @param {object} options optional user context + * @param {function} callback return result + *

+         *            function (err, obj) {
+         *              // obj is {id: id}
+         *              if (err) adapter.log.error('Cannot write object: ' + err);
+         *            }
+         *        
+ */ + that.setForeignObject = function setForeignObject(id, obj, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!obj.from) obj.from = 'system.adapter.' + that.namespace; + if (!obj.ts) obj.ts = new Date().getTime(); + + that.objects.setObject(id, obj, options, callback); + }; + /** + * Promise-version of Adapter.setForeignObject + */ + that.setForeignObjectAsync = tools.promisify(that.setForeignObject, that); + + /** + * Same as {@link Adapter.extendObject}, but for any object. + * + * ID must be specified as a full name with adapter namespace. E.g "hm-rpc.0.ABC98989.1.STATE" + * + * @alias extendForeignObject + * @memberof Adapter + * @param {string} id object ID, that must be extended + * @param {object} obj part that must be extended + * @param {object} options optional user context + * @param {function} callback return result + *

+         *            function (err, obj) {
+         *                // obj is {"id": id}
+         *                if (err) adapter.log.error(err);
+         *            }
+         *        
+ */ + that.extendForeignObject = function extendForeignObject(id, obj, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + // delete arrays if they should be changed + if (obj && ((obj.native && (obj.native.repositories || obj.native.certificates || obj.native.devices)) || + (obj.common && obj.common.members))) { + // Read whole object + that.objects.getObject(id, options, function (err, oldObj) { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + if (!oldObj) { + logger.error(that.namespace + ' Object ' + id + ' not exist!'); + oldObj = {}; + } + if (obj.native && obj.native.repositories && oldObj.native && oldObj.native.repositories) { + oldObj.native.repositories = []; + } + if (obj.common && obj.common.members && oldObj.common && oldObj.common.members) { + oldObj.common.members = []; + } + if (obj.native && obj.native.certificates && oldObj.native && oldObj.native.certificates) { + oldObj.native.certificates = []; + } + if (obj.native && obj.native.devices && oldObj.native && oldObj.native.devices) { + oldObj.native.devices = []; + } + obj = extend(true, oldObj, obj); + + if (!obj.from) obj.from = 'system.adapter.' + that.namespace; + if (!obj.ts) obj.ts = new Date().getTime(); + + that.objects.setObject(id, obj, callback); + }); + } else { + if (!obj.from) obj.from = 'system.adapter.' + that.namespace; + if (!obj.ts) obj.ts = new Date().getTime(); + + that.objects.extendObject(id, obj, options, callback); + } + }; + /** + * Promise-version of Adapter.extendForeignObject + */ + that.extendForeignObjectAsync = tools.promisify(that.extendForeignObject, that); + + /** + * Get object of this instance. + * + * It is not required, that ID consists namespace. E.g. to get object of "adapterName.X.myObject", only "myObject" is required as ID. + * + * @alias getObject + * @memberof Adapter + * @param {string} id exactly object ID (without namespace) + * @param {object} options optional user context + * @param {function} callback return result + *

+         *            function (err, obj) {
+         *              if (err) adapter.log.error('Cannot get object: ' + err);
+         *            }
+         *        
+ */ + that.getObject = function getObject(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + that.objects.getObject(that._fixId(id), options, callback); + }; + /** + * Promise-version of Adapter.getObject + */ + that.getObjectAsync = tools.promisify(that.getObject, that); + + /** + * Get the enum tree. + * + * Get enums of specified tree or all enums if nothing specified as object with values. + * If getEnum called with no enum specified, all enums will be returned: + *

+         *      adapter.getEnums(function (err, enums, requestEnum) {
+         *        // All enums
+         *        if (err) adapter.log.error('Cannot get object: ' + err);
+         *        for (var e in enums) {
+         *           adapter.log.debug('Enum "' + e + '" has following members: ' + enums[e].common.members.join(', '));
+         *        }
+         *      });
+         * 
+ * + * @alias getEnum + * @memberof Adapter + * @param {string} _enum enum name, e.g. 'rooms', 'function' or '' (all enums) + * @param {object} options optional user context + * @param {function} callback return result + *

+         *            function (err, enums, requestEnum) {
+         *              // requestEnum is _enum
+         *              if (err) adapter.log.error('Cannot get object: ' + err);
+         *              for (var e in enums) {
+         *                 adapter.log.debug('Enum "' + e + '" has following members: ' + enums[e].common.members.join(', '));
+         *              }
+         *            }
+         *        
+ */ + that.getEnum = function getEnum(_enum, options, callback) { + if (typeof _enum === 'function') { + callback = _enum; + options = null; + _enum = ''; + } + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!_enum.match('^enum.')) _enum = 'enum.' + _enum; + var result = {}; + + that.objects.getObjectView('system', 'enum', {startkey: _enum + '.', endkey: _enum + '.\u9999'}, options, function (err, res) { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + for (var t = 0; t < res.rows.length; t++) { + result[res.rows[t].id] = res.rows[t].value; + } + if (typeof callback === 'function') callback(err, result, _enum); + }); + }; + /** + * Promise-version of Adapter.getEnum + */ + that.getEnumAsync = tools.promisify(that.getEnum, that, ["result", "requestEnum"]); + + /** + * Read the members of given enums. + * + * Get enums of specified tree or all enums if nothing specified as object with values. + * + * @alias getEnums + * @memberof Adapter + * @param {string|array} _enumList enum name or names, e.g. ['rooms', 'function'] + * @param {object} options optional user context + * @param {function} callback return result + *

+         *            function (err, enums) {
+         *              // requestEnum is _enum
+         *              if (err) adapter.log.error('Cannot get object: ' + err);
+         *              // Result is like
+         *              // {
+         *              //    "enum.rooms": {
+         *              //       "enum.rooms.livingroom": {
+         *              //           common: {
+         *              //              members: ['ID1', 'ID2']
+         *              //           }
+         *              //       },
+         *              //       "enum.rooms.sleepingroom": {
+         *              //           common: {
+         *              //              members: ['ID3', 'ID4']
+         *              //           }
+         *              //       }
+         *              //    },
+         *              //    "enum.functions": {
+         *              //       "enum.rooms.light": {
+         *              //           common: {
+         *              //              members: ['ID1', 'ID6']
+         *              //           }
+         *              //       },
+         *              //       "enum.rooms.weather": {
+         *              //           common: {
+         *              //              members: ['ID4', 'ID7']
+         *              //           }
+         *              //       }
+         *              //    }
+         *              // }
+         *            }
+         *        
+ */ + that.getEnums = function getEnums(_enumList, options, callback) { + if (typeof _enumList === 'function') { + callback = _enumList; + _enumList = null; + } + if (typeof options === 'function') { + callback = options; + options = null; + } + var _enums = {}; + if (_enumList) { + if (typeof _enumList === 'string') _enumList = [_enumList]; + var count = 0; + for (var t = 0; t < _enumList.length; t++) { + count++; + that.getEnum(_enumList[t], options, function (err, list, _enum) { + if (list) _enums[_enum] = list; + if (!--count && callback) callback(err, _enums); + }); + } + } else { + // Read all enums + that.objects.getObjectView('system', 'enum', {startkey: 'enum.', endkey: 'enum.\u9999'}, options, function (err, res) { + if (err) { + callback(err); + return; + } + var result = {}; + if (res && res.rows) { + for (var i = 0; i < res.rows.length; i++) { + var parts = res.rows[i].id.split('.', 3); + if (!parts[2]) continue; + if (!result[parts[0] + '.' + parts[1]]) result[parts[0] + '.' + parts[1]] = {}; + result[parts[0] + '.' + parts[1]][res.rows[i].id] = res.rows[i].value; + } + } + + if (callback) callback(err, result); + }); + } + }; + /** + * Promise-version of Adapter.getEnums + */ + that.getEnumsAsync = tools.promisify(that.getEnums, that); + + /** + * Get objects by pattern, by specific type and resolve their enums. + * + * Get all objects in the system of specified type. E.g.: + * + *

+         *            adapter.getForeignObjects('hm-rega.0.*', 'state', ['rooms', 'functions'], function (err, objs) {
+         *              if (err) adapter.log.error('Cannot get object: ' + err);
+         *              // objs look like:
+         *              // {
+         *              //    "hm-rega.0.ABC0000.1.STATE": {
+         *              //        common: {...},
+         *              //        native: {},
+         *              //        type: 'state',
+         *              //        enums: {
+         *              //           'enums.rooms.livingroom': 'Living room',
+         *              //           'enums.functions.light': 'Light'
+         *              //       }
+         *              //    },
+         *              //    "hm-rega.0.ABC0000.2.STATE": {
+         *              //        common: {...},
+         *              //        native: {},
+         *              //        type: 'state',
+         *              //        enums: {
+         *              //           'enums.rooms.sleepingroom': 'Sleeping room',
+         *              //           'enums.functions.window': 'Windows'
+         *              //       }
+         *              //    }
+         *            }
+         *        
+ * + * @alias getForeignObjects + * @memberof Adapter + * @param {string} pattern object ID/wildchars + * @param {string} type type of object: 'state', 'channel' or 'device'. Default - 'state' + * @param {string|string[]} enums object ID, that must be overwritten or created. + * @param {object} options optional user context + * @param {function} callback return result + *

+         *            function (err, obj) {
+         *              if (err) adapter.log.error('Cannot get object: ' + err);
+         *            }
+         *        
+ */ + that.getForeignObjects = function getForeignObjects(pattern, type, enums, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + var params = {}; + if (pattern && pattern !== '*') { + params = { + startkey: pattern.replace('*', ''), + endkey: pattern.replace('*', '\u9999') + }; + } + if (typeof enums === 'function') { + callback = enums; + enums = null; + } + if (typeof type === 'function') { + callback = type; + type = null; + } + if (typeof type === 'object') { + options = type; + type = null; + } + if (typeof enums === 'object' && !(enums instanceof Array)) { + options = enums; + enums = null; + } + that.objects.getObjectView('system', type || 'state', params, options, function (err, res) { + if (err) { + callback(err); + return; + } + + that.getEnums(enums, function (err, _enums) { + var list = {}; + for (var i = 0; i < res.rows.length; i++) { + var id = res.rows[i].id; + if (typeof id !== 'string') { + that.log.debug('Invalid id returned from getEnums: ' + JSON.stringify(id)); + continue; + } + list[id] = res.rows[i].value; + if (_enums && id) { + // get device or channel of this state and check it too + var parts = id.split('.'); + parts.splice(parts.length - 1, 1); + var channel = parts.join('.'); + parts.splice(parts.length - 1, 1); + var device = parts.join('.'); + + list[id].enums = {}; + for (var es in _enums) { + if (!_enums.hasOwnProperty(es)) continue; + for (var e in _enums[es]) { + if (!_enums[es].hasOwnProperty(e)) continue; + if (!_enums[es][e] || !_enums[es][e].common || !_enums[es][e].common.members) + continue; + if (_enums[es][e].common.members.indexOf(id) !== -1 || + _enums[es][e].common.members.indexOf(channel) !== -1 || + _enums[es][e].common.members.indexOf(device) !== -1) { + list[id].enums[e] = _enums[es][e].common.name; + } + } + } + } + } + callback(null, list); + }); + }); + }; + /** + * Promise-version of Adapter.getForeignObjects + */ + that.getForeignObjectsAsync = tools.promisify(that.getForeignObjects, that); + + /** + * Find any object by name or ID. + * + * Find object by the exact name or ID. + * + * @alias findForeignObject + * @memberof Adapter + * @param {string} id exactly object ID (without namespace) + * @param {string} type optional common.type of state: 'number', 'string', 'boolean', 'file', ... + * @param {object} options optional user context + * @param {function} callback return result + *

+         *            adapter.findForeignObject('Some name', function (err, id, name) {
+         *              if (err) adapter.log.error('Cannot get object: ' + err);
+         *              adapter.log.debug('ID of object with name "' + name + '" is "' + id + '"');
+         *            }
+         *        
+ */ + that.findForeignObject = function findForeignObject(id, type, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + that.objects.findObject(id, type, options, callback); + }; + /** + * Promise-version of Adapter.findForeignObject + */ + that.findForeignObjectAsync = tools.promisify(that.findForeignObject, that, ["id", "name"]); + + /** + * Get any object. + * + * ID must be specified with namespace. + * + * @alias getForeignObject + * @memberof Adapter + * @param {string} id exactly object ID (with namespace) + * @param {object} options optional user context + * @param {function} callback return result + *

+         *            function (err, obj) {
+         *              if (err) adapter.log.error('Cannot get object: ' + err);
+         *            }
+         *        
+ */ + that.getForeignObject = function getForeignObject(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + that.objects.getObject(id, options, callback); + }; + /** + * Promise-version of Adapter.getForeignObject + */ + that.getForeignObjectAsync = tools.promisify(that.getForeignObject, that); + + /** + * Delete object of this instance. + * + * It is not required, that ID consists namespace. E.g. to get object of "adapterName.X.myObject", only "myObject" is required as ID. + * + * @alias delObject + * @memberof Adapter + * @param {string} id exactly object ID (without namespace) + * @param {object} options optional user context + * @param {function} callback return result + *

+         *            function (err) {
+         *              if (err) adapter.log.error('Cannot delete object: ' + err);
+         *            }
+         *        
+ */ + that.delObject = function delObject(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + that.objects.delObject(that._fixId(id), options, callback); + }; + /** + * Promise-version of Adapter.delObject + */ + that.delObjectAsync = tools.promisify(that.delObject, that); + + /** + * Delete any object. + * + * ID must be specified with namespace. + * + * @alias delForeignObject + * @memberof Adapter + * @param {string} id exactly object ID (with namespace) + * @param {object} options optional user context + * @param {function} callback return result + *

+         *            function (err) {
+         *              if (err) adapter.log.error('Cannot delete object: ' + err);
+         *            }
+         *        
+ */ + that.delForeignObject = function delForeignObject(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + that.objects.delObject(id, options, callback); + }; + /** + * Promise-version of Adapter.delForeignObject + */ + that.delForeignObjectAsync = tools.promisify(that.delForeignObject, that); + + /** + * Subscribe for the changes of objects in this instance. + ** + * @alias subscribeObjects + * @memberof Adapter + * @param {string} pattern pattern like 'channel.*' or '*' (all objects of this adapter) - without namespaces + * @param {object} options optional user context + */ + that.subscribeObjects = function subscribeObjects(pattern, options) { + if (pattern === '*') { + that.objects.subscribe(that.namespace + '.*'); + } else { + pattern = that._fixId(pattern, true); + that.objects.subscribe(pattern, options); + } + }; + + /** + * Unsubscribe on the changes of objects in this instance. + * + * @alias unsubscribeObjects + * @memberof Adapter + * @param {string} pattern pattern like 'channel.*' or '*' (all objects) - without namespaces + * @param {object} options optional user context + */ + that.unsubscribeObjects = function unsubscribeObjects(pattern, options) { + if (pattern === '*') { + that.objects.unsubscribe(that.namespace + '.*', options); + } else { + pattern = that._fixId(pattern, true); + that.objects.unsubscribe(pattern); + } + }; + + /** + * Subscribe for the changes of objects in any instance. + * + * @alias subscribeForeignObjects + * @memberof Adapter + * @param {string} pattern pattern like 'channel.*' or '*' (all objects) - without namespaces + * @param {object} options optional user context + */ + that.subscribeForeignObjects = function subscribeObjects(pattern, options) { + that.objects.subscribe(pattern, options); + }; + + /** + * Unsubscribe for the patterns on all objects. + * + * @alias unsubscribeForeignObjects + * @memberof Adapter + * @param {string} pattern pattern like 'channel.*' or '*' (all objects) - without namespaces + * @param {object} options optional user context + */ + that.unsubscribeForeignObjects = function unsubscribeForeignObjects(pattern, options) { + if (!pattern) pattern = '*'; + that.objects.unsubscribe(pattern, options); + }; + + that.setObjectNotExists = function setObjectNotExists(id, object, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + id = that._fixId(id); + + if (object.children || object.parent) { + logger.warn(that.namespace + ' Do not use parent or children for ' + id); + } + + that.objects.getObject(id, options, function (err, obj) { + if (!obj) { + if (!object.from) object.from = 'system.adapter.' + that.namespace; + if (!object.ts) object.ts = new Date().getTime(); + + that.objects.setObject(id, object, callback); + } else { + if (typeof callback === 'function') callback(null); + } + }); + }; + /** + * Promise-version of Adapter.setObjectNotExists + */ + that.setObjectNotExistsAsync = tools.promisify(that.setObjectNotExists, that); + + that.setForeignObjectNotExists = function setForeignObjectNotExists(id, obj, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + that.objects.getObject(id, options, function (err, _obj) { + if (!_obj) { + if (!obj.from) obj.from = 'system.adapter.' + that.namespace; + if (!obj.ts) obj.ts = new Date().getTime(); + + that.objects.setObject(id, obj, callback); + } else { + if (typeof callback === 'function') callback(null); + } + }); + }; + /** + * Promise-version of Adapter.setForeignObjectNotExists + */ + that.setForeignObjectNotExistsAsync = tools.promisify(that.setForeignObjectNotExists, that); + + that._DCS2ID = function (device, channel, stateOrPoint) { + var id = ''; + if (device) id += device; + if (channel) id += ((id) ? '.' : '') + channel; + + if (stateOrPoint !== true && stateOrPoint !== false) { + if (stateOrPoint) id += ((id) ? '.' : '') + stateOrPoint; + } else if (stateOrPoint === true) { + if (id) id += '.'; + } + return id; + }; + + that.createDevice = function createDevice(deviceName, common, _native, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!deviceName) { + that.log.error('Try to create device with empty name!'); + return; + } + if (typeof _native === 'function') { + callback = _native; + _native = {}; + } + if (typeof common === 'function') { + callback = common; + common = {}; + } + common = common || {}; + common.name = common.name || deviceName; + + deviceName = deviceName.replace(/[.\s]+/g, '_'); + _native = _native || {}; + + that.setObjectNotExists(deviceName, { + type: 'device', + common: common, + native: _native + }, options, callback); + }; + /** + * Promise-version of Adapter.createDevice + */ + that.createDeviceAsync = tools.promisify(that.createDevice, that); + + // name of channel must be in format "channel" + that.createChannel = function createChannel(parentDevice, channelName, roleOrCommon, _native, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!channelName) throw 'Try to create channel without name!'; + + if (typeof _native === 'function') { + callback = _native; + _native = {}; + } + + if (typeof roleOrCommon === 'function') { + callback = roleOrCommon; + roleOrCommon = undefined; + } + + var common = {}; + if (typeof roleOrCommon === 'string') { + common = { + role: roleOrCommon + }; + } else if (typeof roleOrCommon === 'object') { + common = roleOrCommon; + } + common.name = common.name || channelName; + + if (parentDevice) parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + channelName = channelName.replace(/[.\s]+/g, '_'); + channelName = that._DCS2ID(parentDevice, channelName); + + _native = _native || {}; + + var obj = { + type: 'channel', + common: common, + native: _native + }; + + that.setObjectNotExists(channelName, obj, options, callback); + }; + /** + * Promise-version of Adapter.createChannel + */ + that.createChannelAsync = tools.promisify(that.createChannel, that); + + that.createState = function createState(parentDevice, parentChannel, stateName, roleOrCommon, _native, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!stateName) throw 'Empty name is not allowed!'; + + if (typeof _native === 'function') { + callback = _native; + _native = {}; + } + + if (typeof roleOrCommon === 'function') { + callback = roleOrCommon; + roleOrCommon = undefined; + } + + var common = {}; + if (typeof roleOrCommon === 'string') { + common = { + role: roleOrCommon + }; + } else if (typeof roleOrCommon === 'object') { + common = roleOrCommon; + } + + common.name = common.name || stateName; + _native = _native || {}; + + common.read = (common.read === undefined) ? true : common.read; + common.write = (common.write === undefined) ? false : common.write; + + if (!common.role) { + logger.error(that.namespace + ' Try to create state ' + (parentDevice ? (parentDevice + '.') : '') + parentChannel + '.' + stateName + ' without role'); + return; + } + + if (parentDevice) parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + if (parentChannel) parentChannel = parentChannel.replace(/[.\s]+/g, '_'); + stateName = stateName.replace(/[.\s]+/g, '_'); + var id = that._fixId({device: parentDevice, channel: parentChannel, state: stateName}); + + // Check min, max and def values for number + if (common.type !== undefined && common.type === 'number') { + var min = 0; + var max = 0; + var def = 0; + var err; + if (common.min !== undefined) { + min = common.min; + if (typeof min !== 'number') { + min = parseFloat(min); + if (isNaN(min)) { + err = 'Wrong type of ' + id + '.common.min'; + logger.error(that.namespace + ' ' + err); + if (callback) callback(err); + return; + } else { + common.min = min; + } + } + } + if (common.max !== undefined) { + max = common.max; + if (typeof max !== 'number') { + max = parseFloat(max); + if (isNaN(max)) { + err = 'Wrong type of ' + id + '.common.max'; + logger.error(that.namespace + ' ' + err); + if (callback) callback(err); + return; + } else { + common.max = max; + } + } + } + if (common.def !== undefined) { + def = common.def; + if (typeof def !== 'number') { + def = parseFloat(def); + if (isNaN(def)) { + err = 'Wrong type of ' + id + '.common.def'; + logger.error(that.namespace + ' ' + err); + if (callback) callback(err); + return; + } else { + common.def = def; + } + } + } + if (common.min !== undefined && common.max !== undefined && min > max) { + common.max = min; + common.min = max; + } + if (common.def !== undefined && common.min !== undefined && def < min) common.def = min; + if (common.def !== undefined && common.max !== undefined && def > max) common.def = max; + } + + that.setObjectNotExists(id, { + type: 'state', + common: common, + native: _native + }, options, callback); + + if (common.def !== undefined) { + that.setState(id, common.def, options); + } else { + that.setState(id, null, true, options); + } + }; + /** + * Promise-version of Adapter.createState + */ + that.createStateAsync = tools.promisify(that.createState, that); + + that.deleteDevice = function deleteDevice(deviceName, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + deviceName = deviceName.replace(/[.\s]+/g, '_'); + if (!that._namespaceRegExp.test(deviceName)) deviceName = that.namespace + '.' + deviceName; + + that.objects.getObjectView('system', 'device', {startkey: deviceName, endkey: deviceName}, options, function (err, res) { + if (err || !res || !res.rows) { + if (typeof callback === 'function') callback(err); + callback = null; + return; + } + var cnt = 0; + if (res.rows.length > 1) that.log.warn('Found more than one device ' + deviceName); + + for (var t = 0; t < res.rows.length; t++) { + cnt++; + that.delObject(res.rows[t].id, options, function (err) { + if (err) { + if (typeof callback === 'function') callback(err); + callback = null; + return; + } + + if (!--cnt) { + that.objects.getObjectView('system', 'channel', {startkey: deviceName + '.', endkey: deviceName + '.\u9999'}, options, function (err, res) { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + var _cnt = 0; + for (var k = 0; k < res.rows.length; k++) { + _cnt++; + that.deleteChannel(deviceName, res.rows[k].id, options, function (err) { + if (!(--_cnt) && callback) { + callback(err); + } else { + if (err) { + if (typeof callback === 'function') callback(err); + callback = null; + } + } + }); + } + if (!_cnt && callback) callback(); + }); + } + }); + } + if (!cnt && callback) callback(); + }); + }; + /** + * Promise-version of Adapter.deleteDevice + */ + that.deleteDeviceAsync = tools.promisify(that.deleteDevice, that); + + that.addChannelToEnum = function addChannelToEnum(enumName, addTo, parentDevice, channelName, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (parentDevice) { + if (that._namespaceRegExp.test(parentDevice)) { + parentDevice = parentDevice.substring(that.namespace.length + 1); + } + parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + } + + if (that._namespaceRegExp.test(channelName)) { + channelName = channelName.substring(that.namespace.length + 1); + } + if (parentDevice && channelName.substring(0, parentDevice.length) === parentDevice) { + channelName = channelName.substring(parentDevice.length + 1); + } + channelName = channelName.replace(/[.\s]+/g, '_'); + + var objId = that.namespace + '.' + that._DCS2ID(parentDevice, channelName); + + if (addTo.match(/^enum\./)) { + that.objects.getObject(addTo, options, function (err, obj) { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + if (!err && obj) { + var pos = obj.common.members.indexOf(objId); + if (pos === -1) { + obj.common.members.push(objId); + obj.from = 'system.adapter.' + that.namespace; + obj.ts = new Date().getTime(); + + that.objects.setObject(obj._id, obj, options, function (err) { + if (callback) callback(err); + }); + } + } + }); + } else { + if (enumName.match(/^enum\./)) enumName = enumName.substring(5); + + that.objects.getObject('enum.' + enumName + '.' + addTo, options, function (err, obj) { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + + if (obj) { + var pos = obj.common.members.indexOf(objId); + if (pos === -1) { + obj.common.members.push(objId); + + obj.from = 'system.adapter.' + that.namespace; + obj.ts = new Date().getTime(); + + that.objects.setObject(obj._id, obj, options, callback); + } else { + if (callback) callback(); + } + } else { + // Create enum + that.objects.setObject('enum.' + enumName + '.' + addTo, { + common: { + name: addTo, + members: [objId] + }, + from: 'system.adapter.' + that.namespace, + ts: new Date().getTime(), + type: 'enum' + }, options, callback); + } + }); + } + }; + /** + * Promise-version of Adapter.addChannelToEnum + */ + that.addChannelToEnumAsync = tools.promisify(that.addChannelToEnum, that); + + that.deleteChannelFromEnum = function deleteChannelFromEnum(enumName, parentDevice, channelName, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (parentDevice) { + if (parentDevice.substring(0, that.namespace.length) === that.namespace) { + parentDevice = parentDevice.substring(that.namespace.length + 1); + } + parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + } + + if (channelName && channelName.substring(0, that.namespace.length) === that.namespace) { + channelName = channelName.substring(that.namespace.length + 1); + } + if (parentDevice && channelName && channelName.substring(0, parentDevice.length) === parentDevice) { + channelName = channelName.substring(parentDevice.length + 1); + } + channelName = channelName || ''; + channelName = channelName.replace(/[.\s]+/g, '_'); + + var objId = that.namespace + '.' + that._DCS2ID(parentDevice, channelName); + + if (enumName) { + enumName = 'enum.' + enumName + '.'; + } else { + enumName = 'enum.'; + } + + that.objects.getObjectView('system', 'enum', {startkey: enumName, endkey: enumName + '\u9999'}, options, function (err, res) { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + if (res) { + var count = 0; + for (var i = 0; i < res.rows.length; i++) { + count++; + that.objects.getObject(res.rows[i].id, options, function (err, obj) { + if (err) { + if (typeof callback === 'function') callback(err); + callback = null; + return; + } + if (!err && obj && obj.common && obj.common.members) { + var pos = obj.common.members.indexOf(objId); + if (pos !== -1) { + obj.common.members.splice(pos, 1); + count++; + obj.from = 'system.adapter.' + that.namespace; + obj.ts = new Date().getTime(); + + that.objects.setObject(obj._id, obj, options, function (err) { + if (!--count && callback) { + callback(err); + } else { + if (err) { + if (typeof callback === 'function') callback(err); + callback = null; + } + } + }); + } + } + if (!--count && callback) callback(err); + }); + } + } else if (callback) { + callback (err); + } + }); + }; + /** + * Promise-version of Adapter.deleteChannelFromEnum + */ + that.deleteChannelFromEnumAsync = tools.promisify(that.deleteChannelFromEnum, that); + + that.deleteChannel = function deleteChannel(parentDevice, channelName, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (typeof channelName === 'function') { + callback = channelName; + channelName = parentDevice; + parentDevice = ''; + } + if (parentDevice && !channelName) { + channelName = parentDevice; + parentDevice = ''; + } else if (parentDevice && typeof channelName === 'function') { + callback = channelName; + channelName = parentDevice; + parentDevice = ''; + } + if (!parentDevice) parentDevice = ''; + that.deleteChannelFromEnum('', parentDevice, channelName); + var _parentDevice = parentDevice; + var _channelName = channelName; + + if (parentDevice) { + if (that._namespaceRegExp.test(parentDevice)) { + parentDevice = parentDevice.substring(that.namespace.length + 1); + } + parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + } + + if (channelName && that._namespaceRegExp.test(channelName)) { + channelName = channelName.substring(that.namespace.length + 1); + } + if (parentDevice && channelName && channelName.substring(0, parentDevice.length) === parentDevice) { + channelName = channelName.substring(parentDevice.length + 1); + } + channelName = channelName || ''; + channelName = channelName.replace(/[.\s]+/g, '_'); + + channelName = that.namespace + '.' + that._DCS2ID(parentDevice, channelName); + + logger.info(that.namespace + ' Delete channel ' + channelName); + + that.objects.getObjectView('system', 'channel', {startkey: channelName, endkey: channelName}, options, function (err, res) { + if (err || !res || !res.rows) { + if (typeof callback === 'function') callback(err); + callback = null; + return; + } + var cnt = 0; + if (res.rows.length > 1) that.log.warn('Found more than one channel ' + channelName); + + for (var t = 0; t < res.rows.length; t++) { + cnt++; + that.delObject(res.rows[t].id, options, function (err) { + if (err) { + if (typeof callback === 'function') callback(err); + callback = null; + return; + } + if (!--cnt) { + that.objects.getObjectView('system', 'state', {startkey: channelName + '.', endkey: channelName + '.\u9999'}, options, function (err, res) { + if (err || !res || !res.rows) { + if (typeof callback === 'function') callback(err); + callback = null; + return; + } + var _cnt = 0; + for (var k = 0; k < res.rows.length; k++) { + _cnt++; + that.deleteState(_parentDevice, _channelName, res.rows[k].id, options, function (err) { + if (!--_cnt && callback) { + callback(err); + } else { + if (err) { + if (typeof callback === 'function') callback(err); + callback = null; + } + } + }); + } + if (!_cnt && callback) callback(); + }); + } + }); + } + if (!cnt && callback) callback(); + }); + }; + /** + * Promise-version of Adapter.deleteChannel + */ + that.deleteChannelAsync = tools.promisify(that.deleteChannel, that); + + that.deleteState = function deleteState(parentDevice, parentChannel, stateName, options, callback) { + if (typeof parentChannel === 'function' && stateName === undefined) { + stateName = parentDevice; + callback = parentChannel; + parentChannel = ''; + parentDevice = ''; + } else + if (parentChannel === undefined && stateName === undefined) { + stateName = parentDevice; + parentDevice = ''; + parentChannel = ''; + } else { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (typeof stateName === 'function') { + callback = stateName; + stateName = parentChannel; + parentChannel = parentDevice; + parentDevice = ''; + } + if (typeof parentChannel === 'function') { + callback = parentChannel; + stateName = parentDevice; + parentChannel = ''; + parentDevice = ''; + } + if (typeof parentChannel === 'function') { + callback = parentChannel; + stateName = parentDevice; + parentChannel = ''; + parentDevice = ''; + } + } + + that.deleteStateFromEnum('', parentDevice, parentChannel, stateName, options); + + if (parentDevice) { + if (that._namespaceRegExp.test(parentDevice)) { + parentDevice = parentDevice.substring(that.namespace.length + 1); + } + + parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + } + + if (parentChannel) { + if (that._namespaceRegExp.test(parentChannel)) { + parentChannel = parentChannel.substring(that.namespace.length + 1); + } + if (parentDevice && parentChannel.substring(0, parentDevice.length) === parentDevice) { + parentChannel = parentChannel.substring(parentDevice.length + 1); + } + + parentChannel = parentChannel.replace(/[.\s]+/g, '_'); + } + + if (that._namespaceRegExp.test(stateName)) { + stateName = stateName.substring(that.namespace.length + 1); + } + if (parentDevice && stateName.substring(0, parentDevice.length) === parentDevice) { + stateName = stateName.substring(parentDevice.length + 1); + } + if (parentChannel && stateName.substring(0, parentChannel.length) === parentChannel) { + stateName = stateName.substring(parentChannel.length + 1); + } + stateName = stateName || ''; + stateName = stateName.replace(/[.\s]+/g, '_'); + + var _name = that._DCS2ID(parentDevice, parentChannel, stateName); + that.delState(_name, options, function () { + that.delObject(_name, options, callback); + }); + }; + /** + * Promise-version of Adapter.deleteState + */ + that.deleteStateAsync = tools.promisify(that.deleteState, that); + + that.getDevices = function getDevices(options, callback) { + if (typeof options === 'function' && typeof callback === 'object') { + var tmp = callback; + callback = options; + options = tmp; + } + if (typeof options === 'function') { + callback = options; + options = null; + } + + that.objects.getObjectView('system', 'device', {startkey: that.namespace + '.', endkey: that.namespace + '.\u9999'}, options, function (err, obj) { + if (callback) { + if (obj.rows.length) { + var res = []; + for (var i = 0; i < obj.rows.length; i++) { + res.push(obj.rows[i].value); + } + callback(null, res); + } else { + callback(err, []); + } + } + }); + }; + /** + * Promise-version of Adapter.getDevices + */ + that.getDevicesAsync = tools.promisify(that.getDevices, that); + + that.getChannelsOf = function getChannelsOf(parentDevice, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (typeof parentDevice === 'function') { + callback = parentDevice; + parentDevice = null; + } + if (!parentDevice) parentDevice = ''; + + if (parentDevice && that._namespaceRegExp.test(parentDevice)) { + parentDevice = parentDevice.substring(that.namespace.length + 1); + } + + parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + parentDevice = that.namespace + (parentDevice ? ('.' + parentDevice) : ''); + that.objects.getObjectView('system', 'channel', {startkey: parentDevice + '.', endkey: parentDevice + '.\u9999'}, options, function (err, obj) { + if (callback) { + if (obj.rows.length) { + var res = []; + for (var i = 0; i < obj.rows.length; i++) { + res.push(obj.rows[i].value); + } + callback(null, res); + } else { + callback(err, []); + } + } + }); + }; + /** + * Promise-version of Adapter.getChannelsOf + */ + that.getChannelsOfAsync = tools.promisify(that.getChannelsOf, that); + + that.getChannels = that.getChannelsOf; + + that.getStatesOf = function getStatesOf(parentDevice, parentChannel, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (typeof parentDevice === 'function') { + callback = parentDevice; + parentDevice = null; + parentChannel = null; + } + if (typeof parentChannel === 'function') { + callback = parentChannel; + parentChannel = null; + } + + if (!parentDevice) { + parentDevice = ''; + } else { + if (that._namespaceRegExp.test(parentDevice)) { + parentDevice = parentDevice.substring(that.namespace.length + 1); + } + + parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + } + + if (!parentChannel) { + parentChannel = ''; + } else if (that._namespaceRegExp.test(parentChannel)) { + parentChannel = parentChannel.substring(that.namespace.length + 1); + } + + if (parentDevice && parentChannel && parentChannel.substring(0, parentDevice.length) === parentDevice) { + parentChannel = parentChannel.substring(parentDevice.length + 1); + } + + parentChannel = parentChannel.replace(/[.\s]+/g, '_'); + + var id = that.namespace + '.' + that._DCS2ID(parentDevice, parentChannel, true); + + that.objects.getObjectView('system', 'state', {startkey: id, endkey: id + '\u9999'}, options, function (err, obj) { + if (callback) { + var res = []; + if (obj.rows.length) { + var read = 0; + for (var i = 0; i < obj.rows.length; i++) { + read++; + that.objects.getObject(obj.rows[i].id, function (err, subObj) { + if (subObj) res.push(subObj); + + if (!--read) callback(null, res); + }); + } + } else { + callback(null, res); + } + } + }); + }; + /** + * Promise-version of Adapter.getStatesOf + */ + that.getStatesOfAsync = tools.promisify(that.getStatesOf, that); + + that.addStateToEnum = function addStateToEnum(enumName, addTo, parentDevice, parentChannel, stateName, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (parentDevice) { + if (that._namespaceRegExp.test(parentDevice)) { + parentDevice = parentDevice.substring(that.namespace.length + 1); + } + + parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + } + + if (parentChannel) { + if (that._namespaceRegExp.test(parentChannel)) { + parentChannel = parentChannel.substring(that.namespace.length + 1); + } + if (parentDevice && parentChannel.substring(0, parentDevice.length) === parentDevice) { + parentChannel = parentChannel.substring(parentDevice.length + 1); + } + + parentChannel = parentChannel.replace(/[.\s]+/g, '_'); + } + + if (that._namespaceRegExp.test(stateName)) { + stateName = stateName.substring(that.namespace.length + 1); + } + if (parentDevice && stateName.substring(0, parentDevice.length) === parentDevice) { + stateName = stateName.substring(parentDevice.length + 1); + } + if (parentChannel && stateName.substring(0, parentChannel.length) === parentChannel) { + stateName = stateName.substring(parentChannel.length + 1); + } + stateName = stateName.replace(/[.\s]+/g, '_'); + + var objId = that._fixId({device: parentDevice, channel: parentChannel, state: stateName}); + + if (addTo.match(/^enum\./)) { + that.objects.getObject(addTo, options, function (err, obj) { + if (!err && obj) { + var pos = obj.common.members.indexOf(objId); + if (pos === -1) { + obj.common.members.push(objId); + obj.from = 'system.adapter.' + that.namespace; + obj.ts = new Date().getTime(); + that.objects.setObject(obj._id, obj, options, callback); + } else if (callback) { + callback(); + } + } else { + if (callback) callback(err || 'object not found'); + } + }); + } else { + if (enumName.match(/^enum\./)) enumName = enumName.substring(5); + + that.objects.getObject('enum.' + enumName + '.' + addTo, options, function (err, obj) { + if (!err && obj) { + var pos = obj.common.members.indexOf(objId); + if (pos === -1) { + obj.common.members.push(objId); + obj.from = 'system.adapter.' + that.namespace; + obj.ts = new Date().getTime(); + that.objects.setObject(obj._id, obj, callback); + } else if (callback) { + callback(); + } + } else { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + + // Create enum + that.objects.setObject('enum.' + enumName + '.' + addTo, { + common: { + name: addTo, + members: [objId] + }, + from: 'system.adapter.' + that.namespace, + ts: new Date().getTime(), + type: 'enum' + }, options, callback); + } + }); + } + }; + /** + * Promise-version of Adapter.addStateToEnum + */ + that.addStateToEnumAsync = tools.promisify(that.addStateToEnum, that); + + that.deleteStateFromEnum = function deleteStateFromEnum(enumName, parentDevice, parentChannel, stateName, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (parentDevice) { + if (that._namespaceRegExp.test(parentDevice)) { + parentDevice = parentDevice.substring(that.namespace.length + 1); + } + + parentDevice = parentDevice.replace(/[.\s]+/g, '_'); + } + + if (parentChannel) { + if (that._namespaceRegExp.test(parentChannel)) { + parentChannel = parentChannel.substring(that.namespace.length + 1); + } + if (parentDevice && parentChannel.substring(0, parentDevice.length) === parentDevice) { + parentChannel = parentChannel.substring(parentDevice.length + 1); + } + + parentChannel = parentChannel.replace(/[.\s]+/g, '_'); + } + + if (that._namespaceRegExp.test(stateName)) { + stateName = stateName.substring(that.namespace.length + 1); + } + if (parentDevice && stateName.substring(0, parentDevice.length) === parentDevice) { + stateName = stateName.substring(parentDevice.length + 1); + } + if (parentChannel && stateName.substring(0, parentChannel.length) === parentChannel) { + stateName = stateName.substring(parentChannel.length + 1); + } + stateName = stateName.replace(/[.\s]+/g, '_'); + + var objId = that._fixId({device: parentDevice, channel: parentChannel, state: stateName}, false, 'state'); + + if (enumName) { + enumName = 'enum.' + enumName + '.'; + } else { + enumName = 'enum.'; + } + + that.objects.getObjectView('system', 'enum', {startkey: enumName, endkey: enumName + '\u9999'}, options, function (err, res) { + if (!err && res) { + var count = 0; + for (var i = 0; i < res.rows.length; i++) { + count++; + that.objects.getObject(res.rows[i].id, options, function (err, obj) { + if (err) { + if (callback) { + callback(err); + callback = null; + } + return; + } + + if (!err && obj && obj.common && obj.common.members) { + var pos = obj.common.members.indexOf(objId); + if (pos !== -1) { + obj.common.members.splice(pos, 1); + count++; + obj.from = 'system.adapter.' + that.namespace; + obj.ts = new Date().getTime(); + that.objects.setObject(obj._id, obj, function (err) { + if (!--count && callback) { + callback(err); + callback = null; + } + }); + } + } + if (!--count && callback) { + callback(err); + callback = null; + } + }); + } + if (!count && callback) { + callback(); + callback = null; + } + } else if (callback) { + callback(err); + callback = null; + } + }); + }; + /** + * Promise-version of Adapter.deleteStateFromEnum + */ + that.deleteStateFromEnumAsync = tools.promisify(that.deleteStateFromEnum, that); + + that.chmodFile = function chmodFile(_adapter, path, options, callback) { + if (_adapter === null) _adapter = that.name; + + if (typeof options === 'function') { + callback = options; + options = null; + } + + that.objects.chmodFile(_adapter, path, options, callback); + }; + /** + * Promise-version of Adapter.chmodFile + */ + that.chmodFileAsync = tools.promisify(that.chmodFile, that, ["entries", "id"]); + + /** + * Read directory from DB. + * + * This function reads the content of directory from DB for given adapter and path. + * If getEnum called with no enum specified, all enums will be returned: + *

+         *      adapter.readDir('vis.0', '/main/', function (err, filesOrDirs) {
+         *        // All enums
+         *        if (err) adapter.log.error('Cannot read directory: ' + err);
+         *        if (filesOrDirs) {
+         *           for (var f = 0; f < filesOrDirs.length; f++) {
+         *              adapter.log.debug('Directory main has following files and dirs: ' + filesOrDirs[f].file + '[dir - ' + filesOrDirs[f].isDir + ']');
+         *           }
+          *       }
+         *      });
+         * 
+ * + * @alias readDir + * @memberof Adapter + * @param {string} _adapter adapter name. If adapter name is null, so the name (not instance) of current adapter will be taken. + * @param {string} path path to direcory without adapter name. E.g. If you want to read "/vis.0/main/views.json", here must be "/main/views.json" and _adapter must be equal to "vis.0". + * @param {object} options optional user context + * @param {function} callback return result + *

+         *            function (err, filesOrDirs) {
+         *                // filesOrDirs is array with elements like
+         *                // {
+         *                //      file:       'views.json,
+         *                //      stats:      node.js stats object like https://nodejs.org/api/fs.html#fs_class_fs_stats ,
+         *                //      isDir:      true/false,
+         *                //      acl:        access control list object,
+         *                //      modifiedAt: time when modified,
+         *                //      createdAt:  time when created
+         *                // }
+         *            }
+         *        
+ */ + that.readDir = function readDir(_adapter, path, options, callback) { + if (_adapter === null) _adapter = that.name; + + if (typeof options === 'function') { + callback = options; + options = null; + } + + that.objects.readDir(_adapter, path, options, callback); + }; + /** + * Promise-version of Adapter.readDir + */ + that.readDirAsync = tools.promisify(that.readDir, that); + + that.unlink = function unlink(_adapter, name, options, callback) { + if (_adapter === null) _adapter = that.name; + + if (typeof options === 'function') { + callback = options; + options = null; + } + + that.objects.unlink(_adapter, name, options, callback); + }; + /** + * Promise-version of Adapter.unlink + */ + that.unlinkAsync = tools.promisify(that.unlink, that); + + that.delFile = that.unlink; + that.delFileAsync = that.unlinkAsync; + + that.rename = function rename(_adapter, oldName, newName, options, callback) { + if (_adapter === null) _adapter = that.name; + if (typeof options === 'function') { + callback = options; + options = null; + } + that.objects.rename(_adapter, oldName, newName, options, callback); + }; + /** + * Promise-version of Adapter.rename + */ + that.renameAsync = tools.promisify(that.rename, that); + + that.mkdir = function mkdir(_adapter, dirname, options, callback) { + if (_adapter === null) _adapter = that.name; + if (typeof options === 'function') { + callback = options; + options = null; + } + + that.objects.mkdir(_adapter, dirname, options, callback); + }; + /** + * Promise-version of Adapter.mkdir + */ + that.mkdirAsync = tools.promisify(that.mkdir, that); + + /** + * Read file from DB. + * + * This function reads the content of one file from DB for given adapter and file name. + *

+         *      adapter.readFile('vis.0', '/main/vis-views.json', function (err, data) {
+         *        // All enums
+         *        if (err) adapter.log.error('Cannot read file: ' + err);
+         *        console.log('Content of file is: ' + data);
+         *      });
+         * 
+ * + * @alias readFile + * @memberof Adapter + * @param {string} _adapter adapter name. If adapter name is null, so the name (not instance) of current adapter will be taken. + * @param {string} filename path to file without adapter name. E.g. If you want to read "/vis.0/main/views.json", here must be "/main/views.json" and _adapter must be equal to "vis.0". + * @param {object} options optional user context + * @param {function} callback return result + *

+         *            function (err, data) {
+         *                // data is utf8 or binary Buffer depends on the file extension.
+         *            }
+         *        
+ */ + that.readFile = function readFile(_adapter, filename, options, callback) { + if (_adapter === null) _adapter = that.name; + + if (typeof options === 'function') { + callback = options; + options = null; + } + + that.objects.readFile(_adapter, filename, options, callback); + }; + /** + * Promise-version of Adapter.readFile + */ + that.readFileAsync = tools.promisify(that.readFile, that, ["file", "mimeType"]); + + /** + * Write file to DB. + * + * This function writes the content of one file into DB for given adapter and file name. + *

+         *      adapter.writeFile('vis.0', '/main/vis-views.json', function (err, data) {
+         *        // All enums
+         *        if (err) adapter.log.error('Cannot read file: ' + err);
+         *        console.log('Content of file is: ' + data);
+         *      });
+         * 
+ * + * @alias readFile + * @memberof Adapter + * @param {string} _adapter adapter name. If adapter name is null, so the name (not instance) of current adapter will be taken. + * @param {string} filename path to file without adapter name. E.g. If you want to read "/vis.0/main/views.json", here must be "/main/views.json" and _adapter must be equal to "vis.0". + * @param {object} data data as UTF8 string or buffer depends on the file extension. + * @param {object} options optional user context + * @param {function} callback return result + *

+         *            function (err) {
+         *
+         *            }
+         *        
+ */ + that.writeFile = function writeFile(_adapter, filename, data, options, callback) { + if (_adapter === null) _adapter = that.name; + + if (typeof options === 'function') { + callback = options; + options = null; + } + + that.objects.writeFile(_adapter, filename, data, options, callback); + }; + /** + * Promise-version of Adapter.writeFile + */ + that.writeFileAsync = tools.promisify(that.writeFile, that); + + that.formatValue = function (value, decimals, _format) { + if (typeof decimals !== 'number') { + _format = decimals; + decimals = 2; + } + + var format = (!_format || _format.length !== 2) ? ((that.isFloatComma === undefined) ? '.,' : ((that.isFloatComma) ? '.,' : ',.')) : _format; + + if (typeof value !== 'number') value = parseFloat(value); + return isNaN(value) ? '' : value.toFixed(decimals).replace(format[0], format[1]).replace(/\B(?=(\d{3})+(?!\d))/g, format[0]); + }; + + that.formatDate = function formatDate(dateObj, isDuration, _format) { + + if ((typeof isDuration === 'string' && isDuration.toLowerCase() === 'duration') || isDuration === true) { + isDuration = true; + } + if (typeof isDuration !== 'boolean') { + _format = isDuration; + isDuration = false; + } + + if (!dateObj) return ''; + var type = typeof dateObj; + if (type === 'string') dateObj = new Date(dateObj); + + if (type !== 'object') { + var j = parseInt(dateObj, 10); + if (j == dateObj) { + // may this is interval + if (j < 946681200) { + isDuration = true; + dateObj = new Date(dateObj); + } else { + // if less 2000.01.01 00:00:00 + dateObj = (j < 946681200000) ? new Date(j * 1000) : new Date(j); + } + } else { + dateObj = new Date(dateObj); + } + } + var format = _format || that.dateFormat || 'DD.MM.YYYY'; + + if (isDuration) dateObj.setMilliseconds(dateObj.getMilliseconds() + dateObj.getTimezoneOffset() * 60 * 1000); + + var validFormatChars = 'YJГMМDTДhSчmмsс'; + var s = ''; + var result = ''; + + function put(s) { + /** @type {number | string} */ + var v = ''; + switch (s) { + case 'YYYY': + case 'JJJJ': + case 'ГГГГ': + case 'YY': + case 'JJ': + case 'ГГ': + v = /** @type {Date} */(dateObj).getFullYear(); + if (s.length === 2) v %= 100; + if (v <= 9) v = '0' + v; + break; + case 'MM': + case 'M': + case 'ММ': + case 'М': + v = dateObj.getMonth() + 1; + if ((v < 10) && (s.length === 2)) v = '0' + v; + break; + case 'DD': + case 'TT': + case 'D': + case 'T': + case 'ДД': + case 'Д': + v = dateObj.getDate(); + if ((v < 10) && (s.length === 2)) v = '0' + v; + break; + case 'hh': + case 'SS': + case 'h': + case 'S': + case 'чч': + case 'ч': + v = dateObj.getHours(); + if ((v < 10) && (s.length === 2)) v = '0' + v; + break; + case 'mm': + case 'm': + case 'мм': + case 'м': + v = dateObj.getMinutes(); + if ((v < 10) && (s.length === 2)) v = '0' + v; + break; + case 'ss': + case 's': + case 'cc': + case 'c': + v = dateObj.getSeconds(); + if ((v < 10) && (s.length === 2)) v = '0' + v; + v = v.toString(); + break; + case 'sss': + case 'ссс': + v = dateObj.getMilliseconds(); + if (v < 10) { + v = '00' + v; + } else if (v < 100) { + v = '0' + v; + } + v = v.toString(); + } + return result += v; + } + + for (var i = 0; i < format.length; i++) { + if (validFormatChars.indexOf(format[i]) >= 0) + s += format[i]; + else { + put(s); + s = ''; + result += format[i]; + } + } + put(s); + return result; + }; + } + + // TODO: clear somehow the cache by changing of user permissions + function getUserGroups(options, callback) { + if (that.users[options.user]) { + options.groups = that.users[options.user]; + return callback(options); + } + options.groups = []; + that.getForeignObjects('*', 'group', function (err, groups) { + // aggregate all groups permissions, where this user is + if (groups) { + for (var g in groups) { + if (groups[g] && + groups[g].common && + groups[g].common.members && + groups[g].common.members.indexOf(options.user) !== -1) { + options.groups.push(groups[g]._id); + } + } + } + + that.users[options.user] = options.groups; + callback(options); + }); + + } + + function checkStates(ids, options, command, callback) { + if (!options.groups) { + return getUserGroups(options, function () { + checkStates(ids, options, command, callback); + }); + } + + if (ids instanceof Array) { + var errors = []; + var count = ids.length; + if (count === 0) { + callback(null, ids); + return; + } + for (var i = 0; i < ids.length; i++) { + checkStates(ids[i], options, command, function (err, obj) { + if (err && obj) { + errors.push(obj._id); + } + + if (!--count) { + if (errors.length) { + for (var j = ids.length - 1; j >= 0; j--) { + if (errors.indexOf(ids[j]) !== -1) { + ids.splice(j, 1); + } + } + } + + callback(null, ids); + } + }); + } + } else { + var originalChecked = undefined; + if (options.checked !== undefined) originalChecked = options.checked; + options.checked = true; + that.objects.getObject(ids, options, function (err, obj) { + if (originalChecked !== undefined) { + options.checked = originalChecked; + } else { + options.checked = undefined; + } + if (err) { + callback(err, {_id: ids}); + return; + } else { + var limitToOwnerRights = options.limitToOwnerRights === true; + if (obj && obj.acl) { + if (obj.acl.state === undefined) obj.acl.state = obj.acl.object; + if (obj.acl.state !== undefined) { + // If user is owner + if (options.user === obj.acl.owner) { + if (command === 'setState' || command === 'delState') { + if (!(obj.acl.state & (2 << 8))/*write*/) { + that.log.warn('Permission error for user "' + options.user + '": ' + command); + callback('permissionError', {_id: ids}); + return; + } + } else if (command === 'getState') { + if (!(obj.acl.state & (4 << 8))/*read*/) { + that.log.warn('Permission error for user "' + options.user + '": ' + command); + callback('permissionError', {_id: ids}); + return; + } + } else { + that.log.warn('Called unknown command:' + command); + } + } else if (options.groups.indexOf(obj.acl.ownerGroup) !== -1 && !limitToOwnerRights) { + if (command === 'setState' || command === 'delState') { + if (!(obj.acl.state & (2 << 4))/*write*/) { + that.log.warn('Permission error for user "' + options.user + '": ' + command); + callback('permissionError', {_id: ids}); + return; + } + } else if (command === 'getState') { + if (!(obj.acl.state & (4 << 4))/*read*/) { + that.log.warn('Permission error for user "' + options.user + '": ' + command); + callback('permissionError', {_id: ids}); + return; + } + } else { + that.log.warn('Called unknown command:' + command); + } + } else if (!limitToOwnerRights) { + if (command === 'setState' || command === 'delState') { + if (!(obj.acl.state & 2)/*write*/) { + that.log.warn('Permission error for user "' + options.user + '": ' + command); + callback('permissionError', {_id: ids}); + return; + } + } else if (command === 'getState') { + if (!(obj.acl.state & 4)/*read*/) { + that.log.warn('Permission error for user "' + options.user + '": ' + command); + callback('permissionError', {_id: ids}); + return; + } + } else { + that.log.warn('Called unknown command:' + command); + callback('permissionError', {_id: ids}); + return; + } + } + else { + that.log.warn('Permissions limited to Owner rights'); + callback('permissionError', {_id: ids}); + return; + } + } + else if (limitToOwnerRights) { + that.log.warn('Permissions limited to Owner rights'); + callback('permissionError', {_id: ids}); + return; + } + } + else if (limitToOwnerRights){ + that.log.warn('Permissions limited to Owner rights'); + callback('permissionError', {_id: ids}); + return; + } + } + callback(); + }); + } + } + + // find out default history instance + function getDefaultHistory(callback) { + if (!that.defaultHistory) { + // read default history instance from system.config + return that.getForeignObject('system.config', function (err, data) { + if (data && data.common) that.defaultHistory = data.common.defaultHistory; + + // if no default history set + if (!that.defaultHistory) { + // read all adapters + that.objects.getObjectView('system', 'instance', {startkey: '', endkey: '\u9999'}, function (err, _obj) { + if (_obj) { + for (var i = 0; i < _obj.rows.length; i++) { + if (_obj.rows[i].value.common && _obj.rows[i].value.common.type === 'storage') { + that.defaultHistory = _obj.rows[i].id.substring('system.adapter.'.length); + break; + } + } + } + if (!that.defaultHistory) that.defaultHistory = 'history.0'; + if (callback) callback(); + }); + } else { + if (callback) callback(); + } + }); + } else { + if (callback) callback(); + + } + } + + function pattern2RegEx(pattern) { + if (pattern !== '*') { + if (pattern[0] === '*' && pattern[pattern.length - 1] !== '*') pattern += '$'; + if (pattern[0] !== '*' && pattern[pattern.length - 1] === '*') pattern = '^' + pattern; + } + pattern = (pattern || '').toString().replace(/\./g, '\\.'); + pattern = pattern.replace(/\*/g, '.*'); + return pattern; + } + + function _setStateChangedHelper (id, state, callback) { + that.getForeignState(id, function (err, oldState) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + var differ = false; + if (!oldState) { + differ = true; + } else + if (state.val !== oldState.val) { + differ = true; + } else + if (state.ack !== undefined && state.ack !== oldState.ack) { + differ = true; + } else + if (state.q !== undefined && state.q !== oldState.q) { + differ = true; + } else + if (state.ts !== undefined && state.ts !== oldState.ts) { + differ = true; + } else + if (state.c !== undefined && state.c !== oldState.c) { + differ = true; + } else + if (state.expire !== undefined && state.expire !== oldState.expire) { + differ = true; + } else + if (state.from !== undefined && state.from !== oldState.from) { + differ = true; + } + if (differ) { + that.outputCount++; + that.states.setState(id, state, function (/* err */) { + if (typeof callback === 'function') callback(null, id, false); + }); + } else { + if (typeof callback === 'function') callback(null, id, true); + } + } + }); + } + + // initStates is called from initAdapter + function initStates(cb) { + logger.debug(that.namespace + ' objectDB connected'); + + config.states.maxQueue = config.states.maxQueue || 1000; + + // Internal object, but some special adapters want to access it anyway. + that.states = new States({ + namespace: that.namespace, + connection: config.states, + connected: function () { + logger.debug(that.namespace + ' statesDB connected'); + + if (options.subscribable) { + that.states.subscribe('system.adapter.' + that.namespace + '.subscribes'); + that.states.getState('system.adapter.' + that.namespace + '.subscribes', function (err, state) { + if (!state || !state.val) { + that.patterns = {}; + } else { + try { + that.patterns = JSON.parse(state.val); + for (var p in that.patterns) { + that.patterns[p].regex = pattern2RegEx(p); + } + } catch (e) { + that.patterns = {}; + } + } + if (typeof cb === 'function') cb(); + }); + } else if (typeof cb === 'function') { + cb(); + } + }, + logger: logger, + change: function (id, state) { + that.inputCount++; + if (state === 'null') state = null; + + if (!id || typeof id !== 'string') { + console.log('Something is wrong! ' + JSON.stringify(id)); + return; + } + + // todo remove it as an error with log will be found + if (id === 'system.adapter.' + that.namespace + '.checkLogging') { + checkLogging(); + return; + } + + // someone subscribes or unsubscribes from adapter + if (options.subscribable && id === 'system.adapter.' + that.namespace + '.subscribes') { + var subs; + try { + subs = JSON.parse(state.val || '{}'); + } catch (e) { + subs = {}; + } + for (var p in subs) { + subs[p].regex = pattern2RegEx(p); + } + + that.patterns = subs; + if (typeof options.subscribesChange === 'function') { + options.subscribesChange(state); + } else { + that.emit('subscribesChange', state); + } + return; + } + + // Clear cache if accidentally got the message about change (Will work for admin and javascript) + if (id.match(/^system\.user\./) || id.match(/^system\.group\./)) { + that.users = []; + } + + // If someone want to have log messages + if (that.logList && id.match(/\.logging$/)) { + var instance = id.substring(0, id.length - '.logging'.length); + if (logger) logger.debug(that.namespace + ' ' + instance + ': logging ' + (state ? state.val : false)); + that.logRedirect(state ? state.val : false, instance); + } else + if (id === 'log.system.adapter.' + that.namespace) { + that.processLog(state); + } else + // If this is messagebox + if (id === 'messagebox.system.adapter.' + that.namespace && state) { + // Read it from fifo list + that.states.delMessage('system.adapter.' + that.namespace, state._id); + var obj = state; + if (obj) { + // If callback stored for this request + if (obj.callback && + obj.callback.ack && + obj.callback.id && + that.callbacks && + that.callbacks['_' + obj.callback.id]) { + // Call callback function + if (that.callbacks['_' + obj.callback.id].cb) { + that.callbacks['_' + obj.callback.id].cb(obj.message); + delete that.callbacks['_' + obj.callback.id]; + } + // delete too old callbacks IDs, like garbage collector + var now = (new Date()).getTime(); + for (var _id in that.callbacks) { + if (now - that.callbacks[_id].time > 3600000) delete that.callbacks[_id]; + } + + } else { + if (options.message) { + // Else inform about new message the adapter + options.message(obj); + } + that.emit('message', obj); + } + } + } else { + if (that.oStates) { + if (!state) { + delete that.oStates[id]; + } else { + that.oStates[id] = state; + } + } + + // It was an error in the calculation + if ((options.noNamespace || config.noNamespace) && that._namespaceRegExp.test(id)) { + if (typeof options.stateChange === 'function') { + options.stateChange(id.substring(that.namespace.length + 1), state); + } else { + // emit 'stateChange' event instantly + setImmediate(function () { + that.emit('stateChange', id.slice(that.namespace.length + 1), state); + }); + } + + } else { + if (typeof options.stateChange === 'function') { + options.stateChange(id, state); + } else { + // emit 'stateChange' event instantly + setImmediate(function () { + that.emit('stateChange', id, state); + }); + } + } + } + }, + connectTimeout: function (error) { + if (config.isInstall) { + if (logger) logger.warn(that.namespace + ' no connection to states DB'); + process.exit(0); + } else { + if (logger) logger.error(that.namespace + ' no connection to states DB: ' + (error || '')); + } + } + }); + + /** + * Send message to other adapter instance or all instances of adapter. + * + * This function sends a message to specific instance or all instances of some specific adapter. + * If no instance given (e.g. "pushover"), the callback argument will be ignored. Because normally many responses will come. + * + * @alias sendTo + * @memberof Adapter + * @param {string} instanceName name of the instance where the message must be send to. E.g. "pushover.0" or "system.adapter.pushover.0". + * @param {string} command command name, like "send", "browse", "list". Command is depend on target adapter implementation. + * @param {object} message object that will be given as argument for request + * @param {function} callback optional return result + *

+         *            function (result) {
+         *              // result is target adapter specific and can vary from adapter to adapter
+         *              if (!result) adapter.log.error('No response received');
+         *            }
+         *        
+ */ + that.sendTo = function sendTo(instanceName, command, message, callback) { + if ((typeof message === 'function') && (typeof callback === 'undefined')) { + callback = message; + message = undefined; + } + if (typeof message === 'undefined') { + message = command; + command = 'send'; + } + var obj = {command: command, message: message, from: 'system.adapter.' + that.namespace}; + + if (!instanceName.match(/^system\.adapter\./)) instanceName = 'system.adapter.' + instanceName; + + if (typeof message !== 'object') { + that.log.debug('sendTo "' + command + '" to ' + instanceName + ' from system.adapter.' + that.namespace + ': ' + message); + } else { + that.log.debug('sendTo "' + command + '" to ' + instanceName + ' from system.adapter.' + that.namespace); + } + + // If not specific instance + if (!instanceName.match(/\.[0-9]+$/)) { + // Send to all instances of adapter + that.objects.getObjectView('system', 'instance', {startkey: instanceName + '.', endkey: instanceName + '.\u9999'}, function (err, _obj) { + if (_obj) { + for (var i = 0; i < _obj.rows.length; i++) { + that.states.pushMessage(_obj.rows[i].id, obj); + } + } + }); + } else { + if (callback) { + if (typeof callback === 'function') { + // force subscribe even no messagebox enabled + if (!that.common.messagebox && !that.mboxSubscribed) { + that.mboxSubscribed = true; + that.states.subscribeMessage('system.adapter.' + that.namespace); + } + + obj.callback = { + message: message, + id: callbackId++, + ack: false, + time: (new Date()).getTime() + }; + if (callbackId >= 0xFFFFFFFF) callbackId = 1; + if (!that.callbacks) that.callbacks = {}; + that.callbacks['_' + obj.callback.id] = {cb: callback}; + + // delete too old callbacks IDs + var now = (new Date()).getTime(); + for (var _id in that.callbacks) { + if (now - that.callbacks[_id].time > 3600000) delete that.callbacks[_id]; + } + } else { + obj.callback = callback; + obj.callback.ack = true; + } + } + + that.states.pushMessage(instanceName, obj); + } + }; + /** + * Promise-version of Adapter.sendTo + */ + that.sendToAsync = tools.promisifyNoError(that.sendTo, that); + + /** + * Send message to specific host or to all hosts. + * + * This function sends a message to specific host or all hosts. + * If no host name given (e.g. null), the callback argument will be ignored. Because normally many responses will come. + * + * @alias sendToHost + * @memberof Adapter + * @param {string} hostName name of the host where the message must be send to. E.g. "myPC" or "system.host.myPC". If argument is empty, the message will be sent to all hosts. + * @param {string} command command name. One of: "cmdExec", "getRepository", "getInstalled", "getVersion", "getDiagData", "getLocationOnDisk", "getDevList", "getLogs", "delLogs", "readDirAsZip", "writeDirAsZip", "readObjectsAsZip", "writeObjectsAsZip", "checkLogging". Commands can be checked in controller.js (function processMessage) + * @param {object} message object that will be given as argument for request + * @param {function} callback optional return result + *

+         *            function (result) {
+         *              // result is target adapter specific and can vary from command to command
+         *              if (!result) adapter.log.error('No response received');
+         *            }
+         *        
+ */ + that.sendToHost = function sendToHost(hostName, command, message, callback) { + if (typeof message === 'undefined') { + message = command; + command = 'send'; + } + var obj = {command: command, message: message, from: 'system.adapter.' + that.namespace}; + + if (hostName && !hostName.match(/^system\.host\./)) hostName = 'system.host.' + hostName; + + if (!hostName) { + // Send to all hosts + that.objects.getObjectList({startkey: 'system.host.', endkey: 'system.host.' + '\u9999'}, null, function (err, res) { + if (!err && res.rows.length) { + for (var i = 0; i < res.rows.length; i++) { + var parts = res.rows[i].id.split('.'); + // ignore system.host.name.alive and so on + if (parts.length === 3) { + that.states.pushMessage(res.rows[i].id, obj); + } + } + } + }); + } else { + if (callback) { + if (typeof callback === 'function') { + // force subscribe even no messagebox enabled + if (!that.common.messagebox && !that.mboxSubscribed) { + that.mboxSubscribed = true; + that.states.subscribeMessage('system.adapter.' + that.namespace); + } + + obj.callback = { + message: message, + id: callbackId++, + ack: false, + time: (new Date()).getTime() + }; + if (callbackId >= 0xFFFFFFFF) callbackId = 1; + if (!that.callbacks) that.callbacks = {}; + that.callbacks['_' + obj.callback.id] = {cb: callback}; + } else { + obj.callback = callback; + obj.callback.ack = true; + } + } + + that.states.pushMessage(hostName, obj); + } + }; + /** + * Promise-version of Adapter.sendToHost + */ + that.sendToHostAsync = tools.promisifyNoError(that.sendToHost, that); + + /** + * Writes value into states DB. + * + * This function can write values into states DB for this adapter. + * Only Ids that belong to this adapter can be modified. So the function automatically adds "adapter.X." to ID. + * ack, options and callback are optional + * + * @alias setState + * @memberof Adapter + * @param {string} id object ID of the state. + * @param {object|string|number|boolean} state simple value or object with attribues. + * If state is object and ack exists too as function argument, function argument has priority. + *

+         *      {
+         *          val:    value,
+         *          ack:    true|false,       // default - false; is command(false) or status(true)
+         *          ts:     timestampMS,      // default - now
+         *          q:      qualityAsNumber,  // default - 0 (ok)
+         *          from:   origin,           // default - this adapter
+         *          c:      comment,          // default - empty
+         *          expire: expireInSeconds   // default - 0
+         *      }
+         *  
+ * @param {boolean} ack optional is command(false) or status(true) + * @param {object} options optional user context + * @param {function} callback optional return error and id + *

+         *            function (err, id) {
+         *              if (err) adapter.log.error('Cannot set value for "' + id + '": ' + err);
+         *            }
+         *        
+ */ + that.setState = function setState(id, state, ack, options, callback) { + if (typeof state === 'object' && typeof ack !== 'boolean') { + callback = options; + options = ack; + ack = undefined; + } + if (typeof options === 'function') { + callback = options; + options = {}; + } + + id = that._fixId(id, false, 'state'); + + if (typeof ack === 'function') { + callback = ack; + ack = undefined; + } + + if (typeof state !== 'object' || state === null || state === undefined) state = {val: state}; + + if (ack !== undefined) { + state.ack = ack; + } + + state.from = 'system.adapter.' + that.namespace; + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(id, options, 'setState', function (err) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + that.outputCount++; + that.states.setState(id, state, callback); + } + }); + } else { + that.outputCount++; + that.states.setState(id, state, callback); + } + }; + /** + * Promise-version of Adapter.setState + */ + that.setStateAsync = tools.promisify(that.setState, that); + + /** + * Writes value into states DB only if the value really changed. + * + * This function can write values into states DB for this adapter. + * Only Ids that belong to this adapter can be modified. So the function automatically adds "adapter.X." to ID. + * ack, options and callback are optional + * + * @alias setStateChanged + * @memberof Adapter + * @param {string} id object ID of the state. + * @param {object|string|number|boolean} state simple value or object with attribues. + * @param {boolean} ack optional is command(false) or status(true) + * @param {object} options optional user context + * @param {function} callback optional return error and id + *

+         *            function (err, id, notChanged) {
+         *              if (err) adapter.log.error('Cannot set value for "' + id + '": ' + err);
+         *              if (!notChanged) adapter.log.debug('Value was chnaged');
+         *            }
+         *        
+ */ + that.setStateChanged = function setStateChanged(id, state, ack, options, callback) { + if (typeof state === 'object' && typeof ack !== 'boolean') { + callback = options; + options = ack; + ack = undefined; + } + if (typeof options === 'function') { + callback = options; + options = {}; + } + + id = that._fixId(id, false, 'state'); + + if (typeof ack === 'function') { + callback = ack; + ack = undefined; + } + + if (typeof state !== 'object' || state === null || state === undefined) state = {val: state}; + + if (ack !== undefined) { + state.ack = ack; + } + + state.from = 'system.adapter.' + that.namespace; + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(id, options, 'setState', function (err) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + _setStateChangedHelper(id, state, callback); + } + }); + } else { + _setStateChangedHelper(id, state, callback); + } + }; + /** + * Promise-version of Adapter.setStateChanged + */ + that.setStateChangedAsync = tools.promisify(that.setStateChanged, that, ["id", "notChanged"]); + + /** + * Writes value into states DB for any instance. + * + * This function can write values into states DB for all instances and system states too. + * ack, options and callback are optional + * + * @alias setForeignState + * @memberof Adapter + * @param {string} id object ID of the state. + * @param {object|string|number|boolean} state simple value or object with attribues. + * If state is object, so the ack will be ignored and must be included into object. + *

+         *      {
+         *          val:    value,
+         *          ack:    true|false,       // default - false; is command(false) or status(true)
+         *          ts:     timestampMS,      // default - now
+         *          q:      qualityAsNumber,  // default - 0 (ok)
+         *          from:   origin,           // default - this adapter
+         *          c:      comment,          // default - empty
+         *          expire: expireInSeconds   // default - 0
+         *      }
+         *  
+ * @param {boolean} ack optional is command(false) or status(true) + * @param {object} options optional user context + * @param {function} callback optional return error and id + *

+         *            function (err, id) {
+         *              if (err) adapter.log.error('Cannot set value for "' + id + '": ' + err);
+         *            }
+         *        
+ */ + that.setForeignState = function setForeignState(id, state, ack, options, callback) { + if (typeof state === 'object' && typeof ack !== 'boolean') { + callback = options; + options = ack; + ack = undefined; + } + + if (typeof options === 'function') { + callback = options; + options = {}; + } + + if (typeof ack === 'function') { + callback = ack; + ack = undefined; + } + + if (typeof state !== 'object' || state === null || state === undefined) state = {val: state}; + + if (ack !== undefined) { + state.ack = ack; + } + + state.from = 'system.adapter.' + that.namespace; + + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(id, options, 'setState', function (err) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + that.outputCount++; + that.states.setState(id, state, callback); + } + }); + } else { + that.outputCount++; + that.states.setState(id, state, callback); + } + }; + /** + * Promise-version of Adapter.setForeignState + */ + that.setForeignStateAsync = tools.promisify(that.setForeignState, that); + + /** + * Writes value into states DB for any instance, but only if state changed. + * + * This function can write values into states DB for all instances and system states too. + * ack, options and callback are optional + * + * @alias setForeignStateChanged + * @memberof Adapter + * @param {string} id object ID of the state. + * @param {object|string|number|boolean} state simple value or object with attribues. + * If state is object and ack exists too as function argument, function argument has priority. + *

+         *      {
+         *          val:    value,
+         *          ack:    true|false,       // default - false; is command(false) or status(true)
+         *          ts:     timestampMS,      // default - now
+         *          q:      qualityAsNumber,  // default - 0 (ok)
+         *          from:   origin,           // default - this adapter
+         *          c:      comment,          // default - empty
+         *          expire: expireInSeconds   // default - 0
+         *      }
+         *  
+ * @param {boolean} ack optional is command(false) or status(true) + * @param {object} options optional user context + * @param {function} callback optional return error and id + *

+         *            function (err, id) {
+         *              if (err) adapter.log.error('Cannot set value for "' + id + '": ' + err);
+         *            }
+         *        
+ */ + that.setForeignStateChanged = function setForeignStateChanged(id, state, ack, options, callback) { + if (typeof state === 'object' && typeof ack !== 'boolean') { + callback = options; + options = ack; + ack = undefined; + } + + if (typeof options === 'function') { + callback = options; + options = {}; + } + + if (typeof ack === 'function') { + callback = ack; + ack = undefined; + } + + if (typeof state !== 'object' || state === null || state === undefined) state = {val: state}; + + if (ack !== undefined) { + state.ack = ack; + } + + state.from = 'system.adapter.' + that.namespace; + + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(id, options, 'setState', function (err) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + _setStateChangedHelper(id, state, callback); + } + }); + } else { + _setStateChangedHelper(id, state, callback); + } + }; + /** + * Promise-version of Adapter.setForeignStateChanged + */ + that.setForeignStateChangedAsync = tools.promisify(that.setForeignStateChanged, that); + + /** + * Read value from states DB. + * + * This function can read values from states DB for this adapter. + * Only Ids that belong to this adapter can be read. So the function automatically adds "adapter.X." to ID. + * + * @alias getState + * @memberof Adapter + * @param {string} id object ID of the state. + * @param {object} options optional user context + * @param {function} callback return result + *

+         *            function (err, state) {
+         *              if (err) adapter.log.error('Cannot read value: ' + err);
+         *            }
+         *        
+ * + * See possible attributes of the state in @setState explanation + */ + that.getState = function getState(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + + id = that._fixId(id, false, 'state'); + + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(id, options, 'getState', function (err) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + if (that.oStates && that.oStates[id]) { + if (typeof callback === 'function') callback(null, that.oStates[id]); + } else { + that.states.getState(id, callback); + } + } + }); + } else { + if (that.oStates && that.oStates[id]) { + if (typeof callback === 'function') callback(null, that.oStates[id]); + } else { + that.states.getState(id, callback); + } + } + }; + /** + * Promise-version of Adapter.getState + */ + that.getStateAsync = tools.promisify(that.getState, that); + + /** + * Read value from states DB for any instance and system state. + * + * This function can read values from states DB for all instances and adapters. It expects the full path of object ID. + * + * @alias getForeignState + * @memberof Adapter + * @param {string} id object ID of the state. + * @param {object} options optional user context + * @param {function} callback return result + *

+         *            function (err, state) {
+         *              if (err) adapter.log.error('Cannot read value: ' + err);
+         *            }
+         *        
+ * + * See possible attributes of the state in @setState explanation + */ + that.getForeignState = function getForeignState(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(id, options, 'getState', function (err) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + if (that.oStates && that.oStates[id]) { + if (typeof callback === 'function') callback(null, that.oStates[id]); + } else { + that.states.getState(id, callback); + } + } + }); + } else { + if (that.oStates && that.oStates[id]) { + if (typeof callback === 'function') callback(null, that.oStates[id]); + } else { + that.states.getState(id, callback); + } + } + }; + /** + * Promise-version of Adapter.getForeignState + */ + that.getForeignStateAsync = tools.promisify(that.getForeignState, that); + + /** + * Read historian data for states of any instance or system state. + * + * This function can read values from history adapters like: history, sql, influxdb. It expects the full path of object ID. + * Normally only foreign history has interest, so there is no getHistory and getForeignHistory + * + * Possible options: + * + * - instance - (optional) name of instance, where to read the historian data, e.g. 'history.0', 'sql.1'. By default will be taken from system settings. + * - start - (optional) time in ms - new Date().getTime()', by default is (now - 1 week) + * - end - (optional) time in ms - new Date().getTime()', by default is (now + 5000 seconds) + * - step - (optional) used in aggregate (m4, max, min, average, total) step in ms of intervals + * - count - number of values if aggregate is 'onchange' or number of intervals if other aggregate method. Count will be ignored if step is set. + * - from - if from field should be included in answer + * - ack - if ack field should be included in answer + * - q - if q field should be included in answer + * - addId - if id field should be included in answer + * - limit - do not return more entries than limit + * - ignoreNull - if null values should be include (false), replaced by last not null value (true) or replaced with 0 (0) + * - sessionId - (optional) identifier of request, will be returned back in the answer + * - aggregate - aggregate method: + * - minmax - used special algorithm. Splice the whole time range in small intervals and find for every interval max, min, start and end values. + * - max - Splice the whole time range in small intervals and find for every interval max value and use it for this interval (nulls will be ignored). + * - min - Same as max, but take minimal value. + * - average - Same as max, but take average value. + * - total - Same as max, but calculate total value. + * - count - Same as max, but calculate number of values (nulls will be calculated). + * - none - No aggregation at all. Only raw values in given period. + * + * @alias getHistory + * @memberof Adapter + * @param {string} id object ID of the state. + * @param {object} options see function description + * @param {function} callback return result + *

+         *            function (error, result, step, sessionId) {
+         *              if (error) adapter.log.error('Cannot read value: ' + err);
+         *            }
+         *        
+ * + * See possible attributes of the state in @setState explanation + */ + that.getHistory = function getHistory(id, options, callback) { + options = options || {}; + options.end = options.end || (new Date()).getTime() + 5000000; + if (!options.count && !options.start) { + options.start = options.start || (new Date()).getTime() - 604800000; // - 1 week + } + + if (!options.instance) { + if (!that.defaultHistory) { + // read default history instance from system.config + return getDefaultHistory(function () { + that.getHistory(id, options, callback); + }); + } else { + options.instance = that.defaultHistory; + } + } + + that.sendTo(options.instance || 'history.0', 'getHistory', {id: id, options: options}, function (res) { + setImmediate(function () { + callback(res.error, res.result, res.step, res.sessionId); + }); + }); + }; + /** + * Promise-version of Adapter.getHistory + */ + that.getHistoryAsync = tools.promisify(that.getHistory, that, ["result", "step", "sessionId"]); + + /** + * Convert ID into object with device's, channel's and state's name. + * + * Convert "adapter.instance.D.C.S" in object {device: D, channel: C, state: S} + * Convert ID to {device: D, channel: C, state: S} + * + * @alias idToDCS + * @memberof Adapter + * @param {string} id short or long string of ID like "stateID" or "adapterName.0.stateID". + * @return {object} parsed ID as an object + */ + that.idToDCS = function idToDCS(id) { + if (!id) return null; + var parts = id.split('.'); + if (parts[0] + '.' + parts[1] !== that.namespace) { + that.log.warn('Try to decode id not from this adapter'); + return null; + } + return {device: parts[2], channel: parts[3], state: parts[4]}; + }; + + /** + * Delete one state of this adapter. + * + * Deletes the state. If State does not exist, no error will be returned. + * Do not forget do delete the object for the state too (with delObject) + *

+         *     adapter.delState('stateID', function (err) {
+         *         console.log('adapterName.0.stateID is deleted');
+         *     });
+         * 
+ * + * @alias delState + * @memberof Adapter + * @param {string} id short or long string of ID like "stateID" or "adapterName.0.stateID". + * @param {object} options optional argument to describe the user context + * @param {function} callback return result function (err) {} + */ + that.delState = function delState(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + + id = that._fixId(id, false, 'state'); + + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(id, options, 'delState', function (err) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + that.states.delState(id, callback); + } + }); + } else { + that.states.delState(id, callback); + } + }; + /** + * Promise-version of Adapter.delState + */ + that.delStateAsync = tools.promisify(that.delState, that); + + /** + * Delete one state of any adapter. + * + * Deletes the state. If State does not exist, no error will be returned. + * Do not forget do delete the object for the state too (with delObject) + *

+         *     adapter.delState('adapterName.0.stateID', function (err) {
+         *         console.log('adapterName.0.stateID is deleted');
+         *     });
+         * 
+ * + * @alias delForeignState + * @memberof Adapter + * @param {string} id long string for ID like "adapterName.0.stateID". + * @param {object} options optional argument to describe the user context + * @param {function} callback return result function (err) {} + */ + that.delForeignState = function delForeignState(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(id, options, 'delState', function (err) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + that.states.delState(id, callback); + } + }); + } else { + that.states.delState(id, callback); + } + + }; + /** + * Promise-version of Adapter.delForeignState + */ + that.delForeignStateAsync = tools.promisify(that.delForeignState, that); + + /** + * Read all states of this adapter, that pass the pattern + * + * Allows to read all states of current adapter according to pattern. To read all states of current adapter use: + *

+         *     adapter.getStates('*', function (err, states) {
+         *         for (var id in states) {
+         *              adapter.log.debug('"' + id + '" = "' + states[id].val);
+         *         }
+         *     });
+         * 
+ * + * @alias getStates + * @memberof Adapter + * @param {string} pattern string in form 'adapter.0.*' or like this. It can be array of IDs too. + * @param {object} options optional argument to describe the user context + * @param {function} callback return result function (err, states) {}, where states is an object like {"ID1": {"val": 1, "ack": true}, "ID2": {"val": 2, "ack": false}, ...} + */ + that.getStates = function getStates(pattern, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + pattern = that._fixId(pattern, true, 'state'); + that.getForeignStates(pattern, options, callback); + }; + /** + * Promise-version of Adapter.getStates + */ + that.getStatesAsync = tools.promisify(that.getStates, that); + + /** + * Read all states of all adapters (and system states), that pass the pattern + * + * Allows to read all states of current adapter according to pattern. To read all states of current adapter use: + *

+         *     adapter.getStates('*', function (err, states) {
+         *         for (var id in states) {
+         *              adapter.log.debug('"' + id + '" = "' + states[id].val);
+         *         }
+         *     });
+         * 
+ * + * @alias getForeignStates + * @memberof Adapter + * @param {string} pattern string in form 'adapter.0.*' or like this. It can be array of IDs too. + * @param {object} options optional argument to describe the user context + * @param {function} callback return result function (err, states) {}, where states is an object like {"ID1": {"val": 1, "ack": true}, "ID2": {"val": 2, "ack": false}, ...} + */ + that.getForeignStates = function getForeignStates(pattern, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + var list = {}; + if (typeof pattern === 'function') { + callback = pattern; + pattern = '*'; + } + + if (typeof callback !== 'function') { + logger.error(that.namespace + ' getForeignStates invalid callback for ' + pattern); + return; + } + + if (typeof pattern === 'object') { + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(pattern, options, 'getState', function (err, keys) { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + that.states.getStates(keys, function (err, arr) { + if (err) { + callback(err); + return; + } + for (var i = 0; i < keys.length; i++) { + if (typeof arr[i] === 'string') arr[i] = JSON.parse(arr[i]); + list[keys[i]] = arr[i] || {}; + } + callback(null, list); + }); + }); + } else { + that.states.getStates(pattern, function (err, arr) { + if (err) { + callback(err); + return; + } + for (var i = 0; i < pattern.length; i++) { + if (typeof arr[i] === 'string') arr[i] = JSON.parse(arr[i]); + list[pattern[i]] = arr[i] || {}; + } + callback(null, list); + }); + } + return; + } + var keys = []; + var params = {}; + if (pattern && pattern !== '*') { + params = { + startkey: pattern.replace('*', ''), + endkey: pattern.replace('*', '\u9999') + }; + } + var originalChecked = undefined; + if (options.checked !== undefined) originalChecked = options.checked; + options.checked = true; + that.objects.getObjectView('system', 'state', params, options, function (err, res) { + if (originalChecked !== undefined) { + options.checked = originalChecked; + } else { + options.checked = undefined; + } + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + // filter out + var regEx; + // process patterns like "*.someValue". The patterns "someValue.*" will be processed by getObjectView + if (pattern && pattern !== '*' && pattern[pattern.length - 1] !== '*') { + regEx = new RegExp(pattern2RegEx(pattern)); + } + for (var i = 0; i < res.rows.length; i++) { + if (!regEx || regEx.test(res.rows[i].id)) { + keys.push(res.rows[i].id); + } + } + + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(keys, options, 'getState', function (err, keys) { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + that.states.getStates(keys, function (err, arr) { + if (err) { + callback(err); + return; + } + for (var i = 0; i < res.rows.length; i++) { + if (typeof arr[i] === 'string') arr[i] = JSON.parse(arr[i]); + list[keys[i]] = arr[i] || null; + } + if (typeof callback === 'function') callback(null, list); + }); + }); + } else { + that.states.getStates(keys, function (err, arr) { + if (err) { + callback(err); + return; + } + for (var i = 0; i < res.rows.length; i++) { + if (typeof arr[i] === 'string') { + try { + arr[i] = JSON.parse(arr[i]); + } catch (e) { + logger.error(that.namespace + ' Cannot parse state"' + keys[i] + ': ' + arr[i]); + } + } + list[keys[i]] = arr[i] || null; + } + if (typeof callback === 'function') callback(null, list); + }); + } + }); + }; + /** + * Promise-version of Adapter.getForeignStates + */ + that.getForeignStatesAsync = tools.promisify(that.getForeignStates, that); + + /** + * Subscribe for changes on all states of all adapters (and system states), that pass the pattern + * + * Allows to Subscribe on changes all states of all instances according to pattern. E.g. to read all states of 'adapterName.X' instance use: + *

+         *     adapter.subscribeForeignStates('adapterName.X.*');
+         * 
+ * + * @alias subscribeForeignStates + * @memberof Adapter + * @param {string} pattern string in form 'adapter.0.*' or like this. It can be array of IDs too. + * @param {object} options optional argument to describe the user context + * @param {function} callback return result function (err) {} + */ + that.subscribeForeignStates = function subscribeForeignStates(pattern, options, callback) { + if (!pattern) pattern = '*'; + if (typeof options === 'function') { + callback = options; + options = null; + } + + // Todo check rights for options + + autoSubscribeOn(function () { + // compare if this pattern for one of autosubscribe adapters + for (var s = 0; s < that.autoSubscribe.length; s++) { + if (pattern === '*' || pattern.substring(0, that.autoSubscribe[s].length + 1) === that.autoSubscribe[s] + '.') { + // put this pattern into adapter list + that.states.getState('system.adapter.' + that.autoSubscribe[s] + '.subscribes', function (err, state) { + state = {}; + state.val = state.val || '{}'; + var subs; + try { + subs = JSON.parse(state.val); + } catch (e) { + that.log.error('Cannot parse subscribes for "' + that.autoSubscribe[s] + '.subscribes"'); + } + subs[pattern] = subs[pattern] || {}; + subs[pattern][that.namespace] = subs[pattern][that.namespace] || 0; + subs[pattern][that.namespace]++; + that.outputCount++; + that.states.setState('system.adapter.' + that.autoSubscribe[s] + '.subscribes', subs); + }); + } + } + + that.states.subscribe(pattern, callback); + }); + }; + /** + * Promise-version of Adapter.subscribeForeignStates + */ + that.subscribeForeignStatesAsync = tools.promisify(that.subscribeForeignStates, that); + + /** + * Unsubscribe for changes for given pattern + * + * This function allows to unsubsrcibe from changes. The pattern must be equal to requested one. + * + *

+         *     adapter.subscribeForeignStates('adapterName.X.*');
+         *     adapter.unsubscribeForeignStates('adapterName.X.abc*'); // This will not work
+         *     adapter.unsubscribeForeignStates('adapterName.X.*'); // Valid unsubscribe
+         * 
+ * + * @alias unsubscribeForeignStates + * @memberof Adapter + * @param {string} pattern string in form 'adapter.0.*'. Must be the same as subscribe. + * @param {object} options optional argument to describe the user context + * @param {function} callback return result function (err) {} + */ + that.unsubscribeForeignStates = function unsubscribeForeignStates(pattern, options, callback) { + if (!pattern) pattern = '*'; + + // Todo check rights for options + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (that.autoSubscribe) { + for (var s = 0; s < that.autoSubscribe.length; s++) { + if (pattern === '*' || pattern.substring(0, that.autoSubscribe[s].length + 1) === that.autoSubscribe[s] + '.') { + // remove this pattern from adapter list + that.states.getState('system.adapter.' + that.autoSubscribe[s] + '.subscribes', function (err, state) { + if (!state || !state.val) return; + var subs; + try { + subs = JSON.parse(state.val); + } catch (e) { + that.log.error('Cannot parse subscribes for "' + that.autoSubscribe[s] + '.subscribes"'); + return; + } + if (!subs[pattern]) return; + if (subs[pattern][that.namespace] === undefined) return; + subs[pattern][that.namespace]--; + if (subs[pattern][that.namespace] <= 0) delete subs[pattern][that.namespace]; + var found = false; + // if any other subs are there + for (var id in subs[pattern]) { + if (subs[pattern].hasOwnProperty(id)) { + found = true; + break; + } + } + if (!found) delete subs[pattern]; + that.outputCount++; + that.states.setState('system.adapter.' + that.autoSubscribe[s] + '.subscribes', subs); + }); + } + } + } + + that.states.unsubscribe(pattern, callback); + }; + /** + * Promise-version of Adapter.unsubscribeForeignStates + */ + that.unsubscribeForeignStatesAsync = tools.promisify(that.unsubscribeForeignStates, that); + + /** + * Subscribe for changes on all states of this instance, that pass the pattern + * + * Allows to Subscribe on changes all states of current adapter according to pattern. To read all states of current adapter use: + *

+         *     adapter.subscribeStates('*'); // subscribe for all states of this adapter
+         * 
+ * + * @alias subscribeStates + * @memberof Adapter + * @param {string} pattern string in form 'adapter.0.*' or like this. It can be array of IDs too. + * @param {object} options optional argument to describe the user context + * @param {function} callback return result function (err) {} + */ + that.subscribeStates = function subscribeStates(pattern, options, callback) { + // Todo check rights for options + if (typeof options === 'function') { + callback = options; + options = null; + } + + // Exception. Threat the '*' case automatically + if (!pattern || pattern === '*') { + that.states.subscribe(that.namespace + '.*', callback); + } else { + pattern = that._fixId(pattern, true, 'state'); + that.states.subscribe(pattern, callback); + } + }; + /** + * Promise-version of Adapter.subscribeStates + */ + that.subscribeStatesAsync = tools.promisify(that.subscribeStates, that); + + /** + * Unsubscribe for changes for given pattern for own states. + * + * This function allows to unsubsrcibe from changes. The pattern must be equal to requested one. + * + *

+         *     adapter.subscribeForeignStates('*');
+         *     adapter.unsubscribeForeignStates('abc*'); // This will not work
+         *     adapter.unsubscribeForeignStates('*');    // Valid unsubscribe
+         * 
+ * + * @alias unsubscribeStates + * @memberof Adapter + * @param {string} pattern string in form 'adapter.0.*'. Must be the same as subscribe. + * @param {object} options optional argument to describe the user context + * @param {function} callback return result function (err) {} + */ + that.unsubscribeStates = function unsubscribeStates(pattern, options, callback) { + // Todo check rights for options + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!pattern || pattern === '*') { + that.states.unsubscribe(that.namespace + '.*', callback); + } else { + pattern = that._fixId(pattern, true, 'state'); + that.states.unsubscribe(pattern, callback); + } + }; + /** + * Promise-version of Adapter.unsubscribeStates + */ + that.unsubscribeStatesAsync = tools.promisify(that.unsubscribeStates, that); + + that.pushFifo = function pushFifo(id, state, callback) { + that.states.pushFifo(id, state, callback); + }; + + that.trimFifo = function trimFifo(id, start, end, callback) { + that.states.trimFifo(id, start, end, callback); + }; + + that.getFifoRange = function getFifoRange(id, start, end, callback) { + that.states.getFifoRange(id, start, end, callback); + }; + + that.getFifo = function getFifo(id, callback) { + that.states.getFifo(id, callback); + }; + + that.lenFifo = function lenFifo(id, callback) { + that.states.lenFifo(id, callback); + }; + + that.subscribeFifo = function subscribeFifo(pattern) { + that.states.subscribeFifo(pattern); + }; + + that.getSession = function getSession(id, callback) { + that.states.getSession(id, callback); + }; + + that.setSession = function setSession(id, ttl, data, callback) { + that.states.setSession(id, ttl, data, callback); + }; + + that.destroySession = function destroySession(id, callback) { + that.states.destroySession(id, callback); + }; + + that.getMessage = function getMessage(callback) { + that.states.getMessage('system.adapter.' + that.namespace, callback); + }; + + that.lenMessage = function lenMessage(callback) { + that.states.lenMessage('system.adapter.' + that.namespace, callback); + }; + + // Write binary block into redis, e.g image + that.setBinaryState = function setBinaryState(id, binary, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(id, options, 'setState', function (err) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + that.states.setBinaryState(id, binary, callback); + } + }); + } else { + that.states.setBinaryState(id, binary, callback); + } + }; + /** + * Promise-version of Adapter.setBinaryState + */ + that.setBinaryStateAsync = tools.promisify(that.setBinaryState, that); + + // Read binary block from redis, e.g. image + that.getBinaryState = function getBinaryState(id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = {}; + } + if (options && options.user && options.user !== 'system.user.admin') { + checkStates(id, options, 'getState', function (err) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + that.states.getBinaryState(id, callback); + } + }); + } else { + that.states.getBinaryState(id, callback); + } + }; + /** + * Promise-version of Adapter.getBinaryState + */ + that.getBinaryStateAsync = tools.promisify(that.getBinaryState, that); + + } + + // read all logs prepared for this adapter at start + function readLogs(callback) { + // read all stored messages + that.states.getLog('system.adapter.' + that.namespace, function (err, msg) { + if (msg) { + if (msg) that.emit('log', msg); + setImmediate(function () { + readLogs(callback); + }); + } else if (callback) { + callback(); + } + }); + } + + function printLog(logs, id, callback) { + that.states.lenLog(id, function (err, len) { + logs.push('Subscriber - ' + id + ' (queued ' + len + ') ' + (err || '')); + if (callback) callback(); + }); + } + + // debug function to find error with stop logging + function checkLogging() { + var logs = []; + var count = 0; + // LogList + logs.push('Actual Loglist - ' + JSON.stringify(that.logList)); + + // Read current state of all log subscribers + that.states.getKeys('*.logging', function (err, keys) { + if (keys && keys.length) { + that.states.getStates(keys, function (err, obj) { + if (obj) { + for (var i = 0; i < keys.length; i++) { + // We can JSON.parse, but index is 16x faster + if (obj[i]) { + var id = keys[i].substring(0, keys[i].length - '.logging'.length).replace(/^io\./, ''); + if ((typeof obj[i] === 'string' && (obj[i].indexOf('"val":true') !== -1 || obj[i].indexOf('"val":"true"') !== -1)) || + (typeof obj[i] === 'object' && (obj[i].val === true || obj[i].val === 'true'))) { + count++; + printLog(logs, id, function () { + if (!--count) { + for (var m = 0; m < logs.length; m++) { + that.log.error('LOGINFO: ' + logs[m]); + } + logs = null; + } + }); + } else { + if (logs) { + logs.push('Subscriber - ' + id + ' (disabled)'); + } else { + that.log.error('LOGINFO: Subscriber - ' + id + ' (disabled)'); + } + } + } + } + } + if (!count && logs) { + for (var m = 0; m < logs.length; m++) { + that.log.error('LOGINFO: ' + logs[m]); + } + logs = null; + } + }); + } + }); + } + + function initLogging() { + // temporary log buffer + var messages = []; + // Read current state of all log subscriber + that.states.getKeys('*.logging', function (err, keys) { + if (keys && keys.length) { + that.states.getStates(keys, function (err, obj) { + if (obj) { + for (var i = 0; i < keys.length; i++) { + // We can JSON.parse, but index is 16x faster + if (!obj[i]) continue; + var id = keys[i].substring(0, keys[i].length - '.logging'.length); + if (typeof obj[i] === 'string' && (obj[i].indexOf('"val":true') !== -1 || obj[i].indexOf('"val":"true"') !== -1)) { + that.logRedirect(true, id); + } else if (typeof obj[i] === 'object' && (obj[i].val === true || obj[i].val === 'true')) { + that.logRedirect(true, id); + } + } + if (that.logList.length && messages && messages.length) { + for (var m = 0; m < messages.length; m++) { + for (var k = 0; k < that.logList.length; k++) { + that.states.pushLog(that.logList[k], messages[m]); + } + } + } + } + // clear log buffer + messages = null; + }); + } else { + // disable log buffer + messages = null; + } + }); + + that.logRedirect = function (isActive, id) { + // ignore itself + if (id === 'system.adapter.' + that.namespace) return; + + if (isActive) { + if (that.logList.indexOf(id) === -1) that.logList.push(id); + } else { + var pos = that.logList.indexOf(id); + if (pos !== -1) that.logList.splice(pos, 1); + } + }; + + // If some message from logger + logger.on('logging', function (transport, level, msg /* , meta */) { + if (transport.name !== tools.appName) return; + + var message = {message: msg, severity: level, from: that.namespace, ts: (new Date()).getTime()}; + if (options.logTransporter) { + that.emit('log', message); + } + + if (!that.logList.length) { + // if log buffer still active + if (messages && !options.logTransporter) { + messages.push(message); + + // do not let messages to grow without limit + if (messages.length > config.states.maxQueue) { + messages.splice(0, messages.length - config.states.maxQueue); + } + } + } else { + // Send to all adapter, that required logs + for (var i = 0; i < that.logList.length; i++) { + that.states.pushLog(that.logList[i], message); + } + } + }); + + options.logTransporter = options.logTransporter || that.ioPack.common.logTransporter; + + if (options.logTransporter) { + that.requireLog = function (isActive) { + if (that.states) { + if (that.logRequired !== isActive) { + that.logRequired = isActive; // remember state + if (!isActive) { + if (that.logOffTimer) { + clearTimeout(that.logOffTimer); + } + // disable log receiving after 10 seconds + that.logOffTimer = setTimeout(function () { + that.logOffTimer = null; + that.log.debug('Change log subscriber state: FALSE'); + that.outputCount++; + that.states.setState('system.adapter.' + that.namespace + '.logging', {val: false, ack: true, from: 'system.adapter.' + that.namespace}); + }, 10000); + } else { + if (that.logOffTimer) { + clearTimeout(that.logOffTimer); + that.logOffTimer = null; + } else { + that.log.debug('Change log subscriber state: true'); + that.outputCount++; + that.states.setState('system.adapter.' + that.namespace + '.logging', {val: true, ack: true, from: 'system.adapter.' + that.namespace}); + } + } + } + } + }; + + that.processLog = function (msg) { + if (msg) that.emit('log', msg); + that.states.delLog('system.adapter.' + that.namespace, msg._id); + }; + + readLogs(); + + that.states.subscribeLog('system.adapter.' + that.namespace); + } + } + + function initAdapter(adapterConfig) { + initLogging(); + + if (options.instance === undefined) { + if (!adapterConfig || !adapterConfig.common || !adapterConfig.common.enabled) { + if (adapterConfig && adapterConfig.common && adapterConfig.common.enabled !== undefined) { + if (!config.isInstall) logger.error(that.namespace + ' adapter disabled'); + } else { + if (!config.isInstall) logger.error(that.namespace + ' no config found for adapter'); + } + + if (!config.isInstall && (!process.argv || !config.forceIfDisabled)) { + var id = 'system.adapter.' + that.namespace; + that.outputCount += 2; + that.states.setState(id + '.alive', {val: true, ack: true, expire: 30, from: id}); + that.states.setState(id + '.connected', {val: true, ack: true, expire: 30, from: id}, function () { + process.exit(3); + }); + setTimeout(function () { + process.exit(3); + }, 1000); + return; + } + } + + if (!config.isInstall && !adapterConfig._id) { + logger.error(that.namespace + ' invalid config: no _id found'); + process.exit(4); + return; + } + + var name; + var instance; + + if (!config.isInstall) { + var tmp = adapterConfig._id.match(/^system\.adapter\.([a-zA-Z0-9-_]+)\.([0-9]+)$/); + if (!tmp) { + logger.error(that.namespace + ' invalid config'); + process.exit(5); + return; + } + name = tmp[1]; + instance = parseInt(tmp[2]) || 0; + } else { + name = options.name; + instance = 0; + adapterConfig = adapterConfig || {common: {mode: 'once', name: name}, native: {}}; + } + + for (var tp in logger.transports) { + if (logger.transports.hasOwnProperty(tp)) { + logger.transports[tp].level = adapterConfig.common.logLevel || 'info'; + } + } + + that.name = adapterConfig.common.name; + that.instance = instance; + that.namespace = name + '.' + instance; + process.title = 'io.' + that.namespace; + + that.config = adapterConfig.native; + that.host = adapterConfig.common.host; + that.common = adapterConfig.common; + + if (adapterConfig.common.mode === 'subscribe' || + adapterConfig.common.mode === 'schedule' || + adapterConfig.common.mode === 'once') { + that.stop = function () { + stop(true); + }; + } + + // Monitor logging state + that.states.subscribe('*.logging'); + + if (typeof options.message === 'function' && !adapterConfig.common.messagebox) { + logger.error(that.namespace + ' : message handler implemented, but messagebox not enabled. Define common.messagebox in io-package.json for adapter or delete message handler.'); + } else if (/*typeof options.message === 'function' && */adapterConfig.common.messagebox) { + that.mboxSubscribed = true; + that.states.subscribeMessage('system.adapter.' + that.namespace); + } + + // set configured in DB log level + if (adapterConfig.common.loglevel) { + for (var trans in logger.transports) { + if (logger.transports.hasOwnProperty(trans)) { + logger.transports[trans].level = adapterConfig.common.loglevel; + } + } + } + } else { + that.name = adapterConfig.name || options.name; + that.instance = adapterConfig.instance || 0; + that.namespace = that.name + '.' + that.instance; + + that.config = adapterConfig.native || {}; + that.common = adapterConfig.common || {}; + that.host = that.common.host || tools.getHostName() || require('os').hostname(); + } + + var Log = function () {}; + + if (config.consoleOutput) { + Log.prototype.silly = function (msg) { + console.log(msg); + logger.silly(that.namespace + ' ' + msg); + }; + Log.prototype.debug = function (msg) { + console.log(msg); + logger.debug(that.namespace + ' ' + msg); + }; + Log.prototype.info = function (msg) { + console.log(msg); + logger.info(that.namespace + ' ' + msg); + }; + Log.prototype.error = function (msg) { + console.error(msg); + logger.error(that.namespace + ' ' + msg); + }; + Log.prototype.warn = function (msg) { + console.warn(msg); + logger.warn(that.namespace + ' ' + msg); + }; + } else { + Log.prototype.silly = function (msg) { + logger.silly(that.namespace + ' ' + msg); + }; + Log.prototype.debug = function (msg) { + logger.debug(that.namespace + ' ' + msg); + }; + Log.prototype.info = function (msg) { + logger.info(that.namespace + ' ' + msg); + }; + Log.prototype.error = function (msg) { + logger.error(that.namespace + ' ' + msg); + }; + Log.prototype.warn = function (msg) { + logger.warn(that.namespace + ' ' + msg); + }; + } + + that.log = new Log(); + that.log.level = config.log.level; + + if (options.instance === undefined) { + that.version = (that.pack && that.pack.version) ? that.pack.version : ((that.ioPack && that.ioPack.common) ? that.ioPack.common.version : 'unknown'); + + that.log.info('starting. Version ' + that.version + ' in ' + that.adapterDir + ', node: ' + process.version); + config.system = config.system || {}; + config.system.statisticsInterval = parseInt(config.system.statisticsInterval, 10) || 15000; + reportInterval = setInterval(reportStatus, config.system.statisticsInterval); + reportStatus(); + } + + if (adapterConfig && adapterConfig.common && adapterConfig.common.restartSchedule) { + try { + schedule = require('node-schedule'); + } catch (e) { + that.log.error('Cannot load node-schedule. Scheduled restart is disabled'); + } + if (schedule) { + that.log.debug('Schedule restart: ' + adapterConfig.common.restartSchedule); + schedule.scheduleJob(adapterConfig.common.restartSchedule, function () { + that.log.info('Scheduled restart.'); + stop(false, true); + }); + } + } + + // auto oStates + if (options.states) { + that.getStates('*', function (err, _states) { + that.oStates = _states; + that.subscribeStates('*'); + if (typeof options.ready === 'function') options.ready(); + that.emit('ready'); + }); + } else { + if (typeof options.ready === 'function') options.ready(); + that.emit('ready'); + + // todo remove it later, when the error is fixed + that.subscribeStates('checkLogging'); + } + } + + function reportStatus() { + var id = 'system.adapter.' + that.namespace; + that.outputCount += 7; + that.states.setState(id + '.alive', {val: true, ack: true, expire: Math.floor(config.system.statisticsInterval / 1000) + 10, from: id}); + if (that.connected) { + that.states.setState(id + '.connected', {val: true, ack: true, expire: 30, from: id}); + that.outputCount++; + } + //RSS is the resident set size, the portion of the process's memory held in RAM (as opposed to the swap space or the part held in the filesystem). + var mem = process.memoryUsage(); + that.states.setState(id + '.memRss', {val: parseFloat((mem.rss / 1048576/* 1MB */).toFixed(2)), ack: true, from: id}); + that.states.setState(id + '.memHeapTotal', {val: parseFloat((mem.heapTotal / 1048576/* 1MB */).toFixed(2)), ack: true, from: id}); + that.states.setState(id + '.memHeapUsed', {val: parseFloat((mem.heapUsed / 1048576/* 1MB */).toFixed(2)), ack: true, from: id}); + // Under windows toFixed returns string ? + that.states.setState(id + '.uptime', {val: parseInt(process.uptime().toFixed(), 10), ack: true, from: id}); + that.states.setState(id + '.inputCount', {val: that.inputCount, ack: true, from: id}); + that.states.setState(id + '.outputCount', {val: that.outputCount, ack: true, from: id}); + that.inputCount = 0; + that.outputCount = 0; + } + + function stop(isPause, isScheduled) { + clearInterval(reportInterval); + var id = 'system.adapter.' + that.namespace; + + if (typeof options.unload === 'function') { + options.unload(function () { + if (that.states) { + that.outputCount++; + that.states.setState(id + '.alive', {val: false, ack: true, from: id}, function () { + if (!isPause && that.log) that.log.info('terminating'); + process.exit(isScheduled ? -100: 0); + }); + } + }); + } else { + that.emit('unload', function () { + if (that.states) { + that.outputCount++; + that.states.setState(id + '.alive', {val: false, ack: true, from: id}, function () { + if (!isPause && that.log) that.log.info('terminating'); + process.exit(isScheduled ? -100: 0); + }); + } + }); + + // Make delay to let event 'unload' to be processed + setTimeout(function () { + if (that.states) { + that.outputCount++; + that.states.setState(id + '.alive', {val: false, ack: true, from: id}, function () { + if (!isPause && that.log) that.log.info('terminating'); + process.exit(isScheduled ? -100: 0); + }); + + // Give 2 seconds to write the value + setTimeout(function () { + if (!isPause && that.log) that.log.info('terminating with timeout'); + process.exit(isScheduled ? -100: 0); + }, 1000); + } else { + if (!isPause && that.log) that.log.info('terminating'); + process.exit(isScheduled ? -100: 0); + } + }, that.common ? that.common.stopTimeout || 500 : 500); + } + } + + process.once('SIGINT', stop); + process.once('SIGTERM', stop); + // And the exit event shuts down the child. + process.once('exit', stop); + + process.on('uncaughtException', function (err) { + console.error(err); + + // catch it on windows + if (that.getPortRunning && err.message === 'listen EADDRINUSE') { + logger.warn(that.namespace + ' Port ' + that.getPortRunning.port + ' is in use. Get next'); + + setImmediate(function () { + that.getPort(that.getPortRunning.port + 1, that.getPortRunning.callback); + }); + return; + } + + logger.error(that.namespace + ' uncaught exception: ' + (err.message || err)); + if (err.stack) logger.error(that.namespace + ' ' + err.stack); + + try { + stop(); + setTimeout(function () { + process.exit(6); + }, 1000); + } catch (err) { + logger.error(that.namespace + ' exception by stop: ' + (err.message || err)); + } + }); + + return this; +} + +// extend the EventEmitter class using our class +util.inherits(Adapter, EventEmitter); + +module.exports = Adapter; diff --git a/lib/dbdump.js b/lib/dbdump.js new file mode 100644 index 0000000..abc4b1f --- /dev/null +++ b/lib/dbdump.js @@ -0,0 +1,78 @@ +/** + * + * dpdump.js + * + * Utility to get JSON Database Dumps + * + * 7'2014 hobbyquaker + * + */ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +'use strict'; + +var yargs = require('yargs') + .usage('$0 [-d design] [-s search] [-p pattern]') + .example('$0 -s state', 'get all objects with type=state') + .example('$0 -s state -p hue.*', 'get all objects from the adapter hue with type=state') + // Todo .example('$0 -p system.*', 'get all objects with _id=system.*') + .alias('s', 'search') + .alias('d', 'design') + .default('design', 'system') + ; + +if (!yargs.argv.s && !yargs.argv.p) { + yargs.showHelp(); + process.exit(0); +} + +var Objects = require(__dirname + '/objects.js'); + + +var db = new Objects({ + logger: { + silly: function (msg) { }, + debug: function (msg) { }, + info: function (msg) { }, + warn: function (msg) { + console.log(msg); + }, + error: function (msg) { + console.log(msg); + } + }, + connected: function () { + + if (yargs.argv.search) { + db.getObjectView(yargs.argv.design, yargs.argv.search, {}, function (err, res) { + if (err) { + console.log(err); + process.exit(1); + } + showResult(res); + + }); + } else { + db.getObjectList({include_docs: true}, function (err, res) { + if (err) { + console.log(err); + process.exit(1); + } + showResult(res); + }); + } + + } +}); + +function showResult(res) { + var outArr = []; + for (var i = 0; i < res.total_rows; i++) { + var obj = res.rows[i].value; + delete obj._rev; + outArr.push(obj); + } + console.log(JSON.stringify(outArr, null, ' ')); + process.exit(0); +} + diff --git a/lib/defaultObjs.js b/lib/defaultObjs.js new file mode 100644 index 0000000..0aa38d1 --- /dev/null +++ b/lib/defaultObjs.js @@ -0,0 +1,60 @@ +'use strict'; + +function createDefaults(lang, temperature, currency) { + var defaults = { + "level.dimmer": { + "def": 0, + "type": "number", + "read": true, + "write": true, + "min": 0, + "max": 100, + "unit": "%" + }, + "indicator.working": { + "def": false, + "type": "boolean", + "read": true, + "write": false, + "min": false, + "max": true + }, + "indicator.maintenance": { + "def": false, + "type": "boolean", + "read": true, + "write": false, + "min": false, + "max": true + }, + "indicator.maintenance.lowbat": { + "def": false, + "type": "boolean", + "read": true, + "write": false, + "min": false, + "max": true, + "desc": "Low battery" + }, + "indicator.maintenance.unreach": { + "def": false, + "type": "boolean", + "read": true, + "write": false, + "min": false, + "max": true, + "desc": "Device unreachable" + }, + "switch": { + "def": false, + "type": "boolean", + "read": true, + "write": true + } + }; + + return defaults; +} + + +module.exports = createDefaults; diff --git a/lib/img/iobroker.png b/lib/img/iobroker.png new file mode 100644 index 0000000..ed57a3d Binary files /dev/null and b/lib/img/iobroker.png differ diff --git a/lib/letsencrypt.js b/lib/letsencrypt.js new file mode 100644 index 0000000..3bdfc91 --- /dev/null +++ b/lib/letsencrypt.js @@ -0,0 +1,182 @@ +'use strict'; + +function createServer(app, settings, certificates, leSettings, log) { + let server; + + const leLog = function (debug, arg1, arg2, arg3) { + if (debug) { + const args = Array.prototype.slice.call(arguments); + args.shift(); // remove debug argument + // skip "no match" + if (args[0][0] === 'n' && args[0][1] === 'o') return; + log.info(arg1 + (arg2 || '') + (arg3 || '')); + } + }; + + if (settings.secure) { + if (leSettings && (!leSettings.email || !leSettings.email) && settings.leEnabled) { + log.error('Please specify the email address and domains to use Let\'s Encrypt certificates!'); + } + + if (leSettings && leSettings.email && leSettings.domains && settings.leEnabled) { + const tools = require(__dirname + '/tools'); + const tls = require('tls'); + const fs = require('fs'); + const LE = require('greenlock'); + let leDir; + + let configPath = tools.getConfigFileName().replace(/\\/g, '/'); + let parts = configPath.split('/'); + parts.pop(); + configPath = parts.join('/'); + + if (leSettings.path && (leSettings.path[0] === '/' || leSettings.path.match(/^[A-Za-z]:/))) { + leDir = leSettings.path; + } else { + leDir = configPath + '/' + (leSettings.path || 'letsencrypt'); + } + + // for lex outputs + if (!console.debug) console.debug = console.log; + + if (!fs.existsSync(leDir)) fs.mkdirSync(leDir); + + // prepare domains + if (typeof leSettings.domains === 'string') { + leSettings.domains = leSettings.domains.split(','); + for (let d = leSettings.domains.length - 1; d >= 0; d--) { + leSettings.domains[d] = leSettings.domains[d].trim(); + if (!leSettings.domains[d]) leSettings.domainss.splice(d, 1); + } + } + + + + let lex = LE.create({ + debug: true, + configDir: leDir, + agreeTos: true, + store: require(__dirname + '/letsencryptStore.js').create({debug: true, log: leLog}), + server: 'https://acme-v01.api.letsencrypt.org/directory', //'staging', + email: leSettings.email, + approvedDomains: leSettings.domains, + log: leLog + }); + + if (settings.leUpdate) { + settings.lePort = parseInt(settings.lePort, 10) || 80; + // handles acme-challenge and redirects to https + // used for validation of requests like http://example.com/.well-known/acme-challenge/BLABALBAL + require('http').createServer(lex.middleware(function redirectHttps(req, res) { + res.setHeader('Location', 'https://' + req.headers.host + req.url); + res.statusCode = 302; + res.end(''); + })).listen(settings.lePort, function () { + log.info('LetsEncrypt challenge server is started on ' + settings.lePort); + }); + } + + let options = JSON.parse(JSON.stringify(certificates)); + let defaultTls = tls.createSecureContext(certificates); + let hostTls; + let running; + + options.SNICallback = function (hostname, cb) { + if (leSettings.domains.indexOf(hostname) !== -1) { + if (settings.leUpdate) { + if (running === true) { + cb(null, hostTls || defaultTls); + } else + if (running) { + running.push(cb); + } else { + running = [cb]; + return lex.httpsOptions.SNICallback(hostname, function (err, tls) { + if (tls) log.debug('Got valid certificates from letsencrypt'); + if (err) log.error('Cannot get certificates: ' + err); + lex.debug = false; + hostTls = tls; + for (let r = 0; r < running.length; r++) { + running[r](err, tls || defaultTls); + } + running = true; + }); + } + } else { + if (!hostTls) { + // validate certificates + lex.check({domains: leSettings.domains}).then(function (certInfo) { + if (certInfo) { + hostTls = tls.createSecureContext({ + key: certInfo.privkey || certInfo.key, // privkey.pem + cert: certInfo.fullchain || certInfo.cert, // fullchain.pem (cert.pem + '\n' + chain.pem) + ca: certInfo.ca + }); + cb(null, hostTls); + } else { + log.error('No letsencrypt certificates found in "' + leDir + '"'); + cb(null, defaultTls); + // do not register domain + /* + if (!running) { + running = [cb]; + lex.letsencrypt.register({ + domains: leSettings.domains, + email: leSettings.email, + agreeTos: true + }, function (err, certInfo) { + //log.debug("[LEX] '" + hostname + "' register completed", err && err.stack || null, certInfo); + if ((!err || !err.stack) && !certInfo) { + log.error((new Error('[LEX] SANITY FAIL: no error and yet no certs either')).stack); + } + if (certInfo) { + hostTls = tls.createSecureContext({ + key: certInfo.privkey || certInfo.key, // privkey.pem + cert: certInfo.fullchain || certInfo.cert, // fullchain.pem (cert.pem + '\n' + chain.pem) + ca: certInfo.ca + }); + for (var r = 0; r < running.length; r++) { + running[r](null, hostTls); + } + } else { + for (var r = 0; r < running.length; r++) { + running[r](null, defaultTls); + } + } + running = true; + }); + } else { + if (running === true) { + cb(null, defaultTls); + } else { + running.push(cb); + } + } + */ + } + }, + function (err) { + if (err) log.error(err); + cb(null, defaultTls); + }); + } else { + cb(null, hostTls); + } + } + } else { + cb(null, defaultTls); + } + }; + + server = require('https').createServer(options, lex.middleware(app)); + } else { + server = require('https').createServer(certificates, app); + } + } else { + server = require('http').createServer(app); + } + + return server; +} + +exports.createServer = createServer; \ No newline at end of file diff --git a/lib/letsencryptStore.js b/lib/letsencryptStore.js new file mode 100644 index 0000000..04752e3 --- /dev/null +++ b/lib/letsencryptStore.js @@ -0,0 +1,628 @@ +// Initially copied from https://github.com/Daplie/le-store-certbot +// but must be completely rewritten to support DB + +'use strict'; + +var PromiseA = require('bluebird'); +var mkdirpAsync = PromiseA.promisify(require('mkdirp')); +var path = require('path'); +var fs = PromiseA.promisifyAll(require('fs')); +var sfs = require('safe-replace'); + +var log = function (debug) { + if (debug) { + var args = Array.prototype.slice.call(arguments); + args.shift(); + args.unshift('[le-store-certbot]'); + console.log.apply(console, args); + } +}; + +function writeRenewalConfig(args) { + var pyobj = args.pyobj; + pyobj.checkpoints = parseInt(pyobj.checkpoints, 10) || 0; + + var pyconf = PromiseA.promisifyAll(require('pyconf')); + + var liveDir = args.liveDir || path.join(args.configDir, 'live', args.domains[0]); + + var certPath = args.certPath || pyobj.cert || path.join(liveDir, 'cert.pem'); + var fullchainPath = args.fullchainPath || pyobj.fullchain || path.join(liveDir, 'fullchain.pem'); + var chainPath = args.chainPath || pyobj.chain || path.join(liveDir, 'chain.pem'); + var privkeyPath = args.privkeyPath || pyobj.privkey + //|| args.domainPrivateKeyPath || args.domainKeyPath || pyobj.keyPath + || path.join(liveDir, 'privkey.pem'); + + log(args.debug, 'writeRenewalConfig privkeyPath', privkeyPath); + + var updates = { + account: args.account.id + , configDir: args.configDir + , domains: args.domains + + , email: args.email + , tos: args.agreeTos && true + // yes, it's an array. weird, right? + , webrootPath: args.webrootPath && [args.webrootPath] || [] + , server: args.server || args.acmeDiscoveryUrl + + , privkey: privkeyPath + , fullchain: fullchainPath + , cert: certPath + , chain: chainPath + + , http01Port: args.http01Port + , keyPath: args.domainPrivateKeyPath || args.privkeyPath + , rsaKeySize: args.rsaKeySize + , checkpoints: pyobj.checkpoints + /* // TODO XXX what's the deal with these? they don't make sense + // are they just old junk? or do they have a meaning that I don't know about? + , fullchainPath: path.join(args.configDir, 'chain.pem') + , certPath: path.join(args.configDir, 'cert.pem') + , chainPath: path.join(args.configDir, 'chain.pem') + */ // TODO XXX end + , workDir: args.workDir + , logsDir: args.logsDir + }; + + // final section is completely dynamic + // :hostname = :webroot_path + args.domains.forEach(function (hostname) { + updates[hostname] = args.webrootPath; + }); + + // must write back to the original pyobject or + // annotations will be lost + Object.keys(updates).forEach(function (key) { + pyobj[key] = updates[key]; + }); + + return mkdirpAsync(path.dirname(args.renewalPath)).then(function () { + return pyconf.writeFileAsync(args.renewalPath, pyobj); + }).then(function () { + // NOTE + // writing twice seems to causes a bug, + // so instead we re-read the file from the disk + return pyconf.readFileAsync(args.renewalPath); + }); +} + +function pyToJson(pyobj) { + if (!pyobj) { + return null; + } + + var jsobj = {}; + Object.keys(pyobj).forEach(function (key) { + jsobj[key] = pyobj[key]; + }); + jsobj.__lines = undefined; + jsobj.__keys = undefined; + + return jsobj; +} + +var defaults = { + configDir: [ '~', 'letsencrypt', 'etc' ].join(path.sep) // /etc/letsencrypt/ + , logsDir: [ '~', 'letsencrypt', 'var', 'log' ].join(path.sep) // /var/log/letsencrypt/ + , workDir: [ '~', 'letsencrypt', 'var', 'lib' ].join(path.sep) // /var/lib/letsencrypt/ + + , accountsDir: [ ':configDir', 'accounts', ':serverDir' ].join(path.sep) + , renewalPath: [ ':configDir', 'renewal', ':hostname.conf' ].join(path.sep) + , renewalDir: [ ':configDir', 'renewal', '' ].join(path.sep) + , serverDirGet: function (copy) { + return (copy.server || '').replace('https://', '').replace(/(\/)$/, '').replace(/\//g, path.sep); + } + + , privkeyPath: ':configDir/live/:hostname/privkey.pem'.split(/\//).join(path.sep) + , fullchainPath: [ ':configDir', 'live', ':hostname', 'fullchain.pem' ].join(path.sep) + , certPath: [ ':configDir', 'live', ':hostname', 'cert.pem' ].join(path.sep) + , chainPath: [ ':configDir', 'live', ':hostname', 'chain.pem' ].join(path.sep) + + , rsaKeySize: 2048 + , webrootPath: [ ':workDir', 'acme-challenge' ].join(path.sep) +}; + +module.exports.create = function (configs) { + var mergedConfigs; + + if (configs && typeof configs.log === 'function') log = configs.log; + + var store = { + getOptions: function () { + if (mergedConfigs) { + return configs; + } + + if (!configs.domainKeyPath) { + configs.domainKeyPath = configs.privkeyPath || defaults.privkeyPath; + } + + Object.keys(defaults).forEach(function (key) { + if (!configs[key]) { + configs[key] = defaults[key]; + } + }); + + mergedConfigs = configs; + return configs; + } + + , keypairs: { + checkAsync: function (keypath, format) { + if (!keypath) { + return null; + } + return fs.readFileAsync(keypath, 'ascii').then(function (key) { + if ('jwk' === format) { + return { privateKeyJwk: JSON.parse(key) }; + } + else { + return { privateKeyPem: key }; + } + }, function (err) { + if ('ENOENT' !== err.code) { + throw err; + } + + return null; + }); + } + , setAsync: function (keypath, keypair, format) { + return mkdirpAsync(path.dirname(keypath)).then(function () { + var key; + + if ('jwk' === format) { + key = JSON.stringify(keypair.privateKeyJwk, null, ' '); + } + else { + key = keypair.privateKeyPem; + } + + return fs.writeFileAsync(keypath, key, 'ascii').then(function () { + return keypair; + }); + }); + } + } + + // + // Certificates + // + , certificates: { + // Certificates + checkKeypairAsync: function (args) { + if (!args.domainKeyPath) { + return PromiseA.reject(new Error("missing options.domainKeyPath")); + } + + return store.keypairs.checkAsync(args.domainKeyPath, 'pem'); + } + // Certificates + , setKeypairAsync: function (args, keypair) { + return store.keypairs.setAsync(args.domainKeyPath, keypair, 'pem'); + } + // Certificates + , checkAsync: function (args) { + if (!args.fullchainPath || !args.privkeyPath || !args.certPath || !args.chainPath) { + return PromiseA.reject(new Error("missing one or more of privkeyPath, fullchainPath, certPath, chainPath from options")); + } + + //, fs.readFileAsync(fullchainPath, 'ascii') + // note: if this ^^ gets added back in, the arrays below must change + return PromiseA.all([ + fs.readFileAsync(args.privkeyPath, 'ascii') // 0 + , fs.readFileAsync(args.certPath, 'ascii') // 1 + , fs.readFileAsync(args.chainPath, 'ascii') // 2 + , fs.readFileAsync(args.fullchainPath, 'ascii') // 3 + + // stat the file, not the link + , fs.statAsync(args.certPath) // 4 + ]).then(function (arr) { + return { + privkey: arr[0] // privkey.pem + , cert: arr[1] // cert.pem + , chain: arr[2] // chain.pem + , fullchain: arr[3] // fullchain.pem + /* + // TODO populate these values only if they are known + , issuedAt: arr[4].mtime.valueOf() + , expiresAt: arr[4].mtime.valueOf() + (90 * 24 * 60 * 60 * 100) + */ + }; + }, function (err) { + if (args.debug) { + console.error("[le-store-certbot] certificates.check"); + console.error(err.stack); + } + return null; + }); + } + // Certificates + , setAsync: function (args) { + return store.configs.getAsync(args).then(function (pyobj) { + var pems = args.pems; + + pyobj.checkpoints = parseInt(pyobj.checkpoints, 10) || 0; + + var liveDir = args.liveDir || path.join(args.configDir, 'live', args.domains[0]); + + var certPath = args.certPath || pyobj.cert || path.join(liveDir, 'cert.pem'); + var fullchainPath = args.fullchainPath || pyobj.fullchain || path.join(liveDir, 'fullchain.pem'); + var chainPath = args.chainPath || pyobj.chain || path.join(liveDir, 'chain.pem'); + var privkeyPath = args.privkeyPath || pyobj.privkey + || args.domainKeyPath + || path.join(liveDir, 'privkey.pem'); + + var archiveDir = args.archiveDir || path.join(args.configDir, 'archive', args.domains[0]); + + var checkpoints = pyobj.checkpoints.toString(); + var certArchive = path.join(archiveDir, 'cert' + checkpoints + '.pem'); + var fullchainArchive = path.join(archiveDir, 'fullchain' + checkpoints + '.pem'); + var chainArchive = path.join(archiveDir, 'chain'+ checkpoints + '.pem'); + var privkeyArchive = path.join(archiveDir, 'privkey' + checkpoints + '.pem'); + + return mkdirpAsync(archiveDir).then(function () { + return PromiseA.all([ + sfs.writeFileAsync(certArchive, pems.cert, 'ascii') + , sfs.writeFileAsync(chainArchive, pems.chain, 'ascii') + , sfs.writeFileAsync(fullchainArchive, pems.cert + pems.chain, 'ascii') + , sfs.writeFileAsync(privkeyArchive, pems.privkey, 'ascii') + ]); + }).then(function () { + return mkdirpAsync(liveDir); + }).then(function () { + return PromiseA.all([ + sfs.writeFileAsync(certPath, pems.cert, 'ascii') + , sfs.writeFileAsync(chainPath, pems.chain, 'ascii') + , sfs.writeFileAsync(fullchainPath, pems.cert + pems.chain, 'ascii') + , sfs.writeFileAsync(privkeyPath, pems.privkey, 'ascii') + ]); + }).then(function () { + pyobj.checkpoints += 1; + args.checkpoints += 1; + + // TODO other than for compatibility this is optional, right? + // or is it actually needful for renewal? (i.e. list of domains) + return writeRenewalConfig(args); + }).then(function () { + return { + privkey: pems.privkey + , cert: pems.cert + , chain: pems.chain + + /* + // TODO populate these only if they are actually known + , issuedAt: Date.now() + , expiresAt: Date.now() + (90 * 24 * 60 * 60 * 100) + */ + }; + }); + }); + } + + } + + // + // Accounts + // + , accounts: { + // Accounts + _getAccountKeyPath: function (args) { + var promise = PromiseA.resolve(args.accountId); + + if (args.email && !args.accountKeyPath && !args.accountId) { + promise = store.accounts._getAccountIdByEmail(args); + } + + return promise.then(function (accountId) { + if (!accountId) { + return null; + } + return args.accountKeyPath || path.join(args.accountsDir, accountId, 'private_key.json'); + }); + } + // Accounts + , _getAccountIdByEmail: function (args) { + // If we read 10,000 account directories looking for + // just one email address, that could get crazy. + // We should have a folder per email and list + // each account as a file in the folder + // TODO + var email = args.email; + if ('string' !== typeof email) { + log(args.debug, "No email given"); + return PromiseA.resolve(null); + } + return fs.readdirAsync(args.accountsDir).then(function (nodes) { + log(args.debug, "success reading arg.accountsDir"); + + return PromiseA.all(nodes.map(function (node) { + return fs.readFileAsync(path.join(args.accountsDir, node, 'regr.json'), 'utf8').then(function (text) { + var regr = JSON.parse(text); + regr.__accountId = node; + + return regr; + }); + })).then(function (regrs) { + var accountId; + + log(args.debug, "regrs.length", regrs.length); + + regrs.some(function (regr) { + return regr.body.contact.some(function (contact) { + var match = contact.toLowerCase() === 'mailto:' + email.toLowerCase(); + if (match) { + accountId = regr.__accountId; + return true; + } + }); + }); + + if (!accountId) { + return null; + } + + return accountId; + }); + }).then(function (accountId) { + return accountId; + }, function (err) { + if ('ENOENT' === err.code) { + // ignore error + return null; + } + + return PromiseA.reject(err); + }); + } + // Accounts + , _getAccountIdByPublicKey: function (keypair) { + // we use insecure md5 - even though we know it's bad - because that's how the python client did + return require('crypto').createHash('md5').update(keypair.publicKeyPem).digest('hex'); + } + // Accounts + , checkKeypairAsync: function (args) { + if (!(args.accountKeyPath || args.accountsDir)) { + return PromiseA.reject(new Error("must provide one of options.accountKeyPath or options.accountsDir")); + } + + return store.accounts._getAccountKeyPath(args).then(function (keypath) { + return store.keypairs.checkAsync(keypath, 'jwk'); + }); + } + // Accounts + , setKeypairAsync: function (args, keypair) { + var accountId; + + if (args.email) { + accountId = store.accounts._getAccountIdByPublicKey(keypair); + } + + return store.accounts._getAccountKeyPath({ + accountsDir: args.accountsDir + , email: args.email + , accountId: args.accountId || accountId + }).then(function (keypath) { + return store.keypairs.setAsync(keypath, keypair, 'jwk'); + }); + } + // Accounts + , checkAsync: function (args) { + var promise; + var files = {}; + var accountId; + + if (args.accountId) { + promise = PromiseA.resolve(args.accountId); + } + else if (args.email) { + promise = store.accounts._getAccountIdByEmail(args); + } + else { + promise = PromiseA.reject(new Error("must provide accountId or email")); + } + + return promise.then(function (_accountId) { + log(args.debug, 'accountId:', _accountId); + if (!_accountId) { + return false; + } + accountId = _accountId; + var accountDir = path.join(args.accountsDir, accountId); + var configs = [ 'meta.json', 'private_key.json', 'regr.json' ]; + + return PromiseA.all(configs.map(function (filename) { + var keyname = filename.slice(0, -5); + + return fs.readFileAsync(path.join(accountDir, filename), 'utf8').then(function (text) { + var data; + + try { + data = JSON.parse(text); + } catch(e) { + files[keyname] = { error: e }; + return; + } + + files[keyname] = data; + + return true; + }, function (err) { + log(args.debug, 'Error reading account files:', err); + files[keyname] = { error: err }; + }); + })); + }).then(function (hasAccount) { + if (!hasAccount) { + return null; + } + var err; + + if (!Object.keys(files).every(function (key) { + return !files[key].error; + }) || !files.private_key || !files.private_key.n) { + err = new Error("Account '" + accountId + "' was corrupt (had id, but was missing files)."); + err.code = 'E_ACCOUNT_CORRUPT'; + err.data = files; + return PromiseA.reject(err); + } + + //files.private_key; + //files.regr; + //files.meta; + files.accountId = accountId; // preserve current account id + files.id = accountId; + files.keypair = { privateKeyJwk: files.private_key }; + + return files; + }); + } + // Accounts + , setAsync: function (args, reg) { + var os = require("os"); + var accountId = store.accounts._getAccountIdByPublicKey(reg.keypair); + var accountDir = path.join(args.accountsDir, accountId); + var accountMeta = { + creation_host: os.hostname() + , creation_dt: new Date().toISOString() + }; + + return mkdirpAsync(accountDir).then(function () { + + // TODO abstract file writing + return PromiseA.all([ + // meta.json {"creation_host": "ns1.redirect-www.org", "creation_dt": "2015-12-11T04:14:38Z"} + fs.writeFileAsync(path.join(accountDir, 'meta.json'), JSON.stringify(accountMeta), 'utf8') + // private_key.json { "e", "d", "n", "q", "p", "kty", "qi", "dp", "dq" } + , fs.writeFileAsync(path.join(accountDir, 'private_key.json'), JSON.stringify(reg.keypair.privateKeyJwk), 'utf8') + // regr.json: + /* + { body: { contact: [ 'mailto:coolaj86@gmail.com' ], + agreement: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf', + key: { e: 'AQAB', kty: 'RSA', n: '...' } }, + uri: 'https://acme-v01.api.letsencrypt.org/acme/reg/71272', + new_authzr_uri: 'https://acme-v01.api.letsencrypt.org/acme/new-authz', + terms_of_service: 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf' } + */ + , fs.writeFileAsync(path.join(accountDir, 'regr.json'), JSON.stringify({ body: reg.receipt }), 'utf8') + ]); + }).then(function () { + return { + id: accountId + , accountId: accountId + , email: args.email + , keypair: reg.keypair + , receipt: reg.receipt + }; + }); + } + // Accounts + , getAccountIdAsync: function (args) { + var pyconf = PromiseA.promisifyAll(require('pyconf')); + + return pyconf.readFileAsync(args.renewalPath).then(function (renewal) { + var accountId = renewal.account; + renewal = renewal.account; + + return accountId; + }, function (err) { + if ("ENOENT" === err.code) { + return store.accounts._getAccountIdByEmail(args); + } + + return PromiseA.reject(err); + }); + } + } + + // + // Configs + // + , configs: { + // Configs + checkAsync: function (copy) { + copy.domains = []; + + return store.configs._checkHelperAsync(copy).then(function (pyobj) { + var exists = pyobj.checkpoints >= 0; + if (!exists) { + return null; + } + + return pyToJson(pyobj); + }); + } + // Configs + , _checkHelperAsync: function (args) { + var pyconf = PromiseA.promisifyAll(require('pyconf')); + + return pyconf.readFileAsync(args.renewalPath).then(function (pyobj) { + return pyobj; + }, function () { + return pyconf.readFileAsync(path.join(__dirname, 'renewal.conf.tpl')).then(function (pyobj) { + return pyobj; + }); + }); + } + // Configs + , getAsync: function (args) { + return store.configs._checkHelperAsync(args).then(function (pyobj) { + var minver = pyobj.checkpoints >= 0; + + args.pyobj = pyobj; + + if (!minver) { + args.checkpoints = 0; + pyobj.checkpoints = 0; + return writeRenewalConfig(args); + } + + // args.account.id = pyobj.account + // args.configDir = args.configDir || pyobj.configDir; + + args.checkpoints = pyobj.checkpoints; + + args.agreeTos = (args.agreeTos || pyobj.tos) && true; + args.email = args.email || pyobj.email; + args.domains = args.domains || pyobj.domains; + + // yes, it's an array. weird, right? + args.webrootPath = args.webrootPath || pyobj.webrootPath[0]; + args.server = args.server || args.acmeDiscoveryUrl || pyobj.server; + + args.certPath = args.certPath || pyobj.cert; + args.privkeyPath = args.privkeyPath || pyobj.privkey; + args.chainPath = args.chainPath || pyobj.chain; + args.fullchainPath = args.fullchainPath || pyobj.fullchain; + + //, workDir: args.workDir + //, logsDir: args.logsDir + args.rsaKeySize = args.rsaKeySize || pyobj.rsaKeySize; + args.http01Port = args.http01Port || pyobj.http01Port; + args.domainKeyPath = args.domainKeyPath || args.keyPath || pyobj.keyPath; + + return writeRenewalConfig(args); + }); + } + // Configs + , allAsync: function (copy) { + copy.domains = []; + + return fs.readdirAsync(copy.renewalDir).then(function (nodes) { + nodes = nodes.filter(function (node) { + return /^[a-z0-9]+.*\.conf$/.test(node); + }); + + return PromiseA.all(nodes.map(function (node) { + copy.domains = [node.replace(/\.conf$/, '')]; + return store.configs.getAsync(copy); + })); + }); + } + } + + }; + + return store; +}; diff --git a/lib/logger.js b/lib/logger.js new file mode 100644 index 0000000..8b8818a --- /dev/null +++ b/lib/logger.js @@ -0,0 +1,251 @@ +/* jshint -W097 */ +/* jshint strict: false */ +/*jslint node: true */ +'use strict'; + +var winston = require('winston'); +var DailyRotateFile = require('winston-daily-rotate-file'); +var fs = require('fs'); +var path = require('path'); +var os = require('os'); +var tools = require(__dirname + '/tools.js'); +var SysLog; +var hostname = tools.getHostName(); + +try { + SysLog = require('winston-syslog').Syslog; +} catch (ex) { + //console.log('No syslog support'); +} + +var logger = function (level, files, noStdout, prefix) { + var userOptions = {}; + var options = { + transports: [] + }; + + //var defaultMaxSize;// = 10 * 1024 * 1024; + + if (typeof files === 'string') { + files = [files]; + } + + files = files || []; + + var isNpm = (__dirname.replace(/\\/g, '/').toLowerCase().indexOf('node_modules/' + tools.appName.toLowerCase() + '.js-controller') !== -1); + + if (typeof level === 'object') { + userOptions = Object.assign({}, level); + + level = userOptions.level; + prefix = userOptions.prefix; + noStdout = userOptions.noStdout; + + if (userOptions.prefix !== undefined) delete userOptions.prefix; + + if (userOptions.transport) { + var fName = 0; + for (var f in userOptions.transport) { + if (!userOptions.transport.hasOwnProperty(f)) continue; + if (userOptions.transport[f].type === 'file' && userOptions.transport[f].enabled !== false) { + + userOptions.transport[f].filename = userOptions.transport[f].filename || 'log/' + tools.appName; + + if (!userOptions.transport[f].fileext && userOptions.transport[f].filename.indexOf('.log') === -1) { + userOptions.transport[f].fileext = '.log'; + } + + if (!fName) userOptions.transport[f].systemLog = true; + + userOptions.transport[f].handleExceptions = false; + userOptions.transport[f].name = !fName ? tools.appName : 'dailyRotateFile' + fName; + fName++; + userOptions.transport[f].filename = userOptions.transport[f].filename.replace(/\\/g, '/'); + if (userOptions.transport[f].filename.match(/^\w:\/|^\//)) { + userOptions.transport[f].filename = path.normalize(userOptions.transport[f].filename); + } else { + userOptions.transport[f].filename = path.normalize(__dirname + (isNpm ? '/../../../' : '/../') + userOptions.transport[f].filename); + } + + userOptions.transport[f].label = prefix || ''; + userOptions.transport[f].level = userOptions.transport[f].level || level; + userOptions.transport[f].json = (userOptions.transport[f].json !== undefined) ? userOptions.transport[f].json : false; + userOptions.transport[f].silent = (userOptions.transport[f].silent !== undefined) ? userOptions.transport[f].silent : false; + userOptions.transport[f].colorize = (userOptions.transport[f].colorize !== undefined) ? userOptions.transport[f].colorize : ((userOptions.colorize === undefined) ? true : userOptions.colorize); + userOptions.transport[f].localTime = (userOptions.transport[f].localTime !== undefined) ? userOptions.transport[f].localTime : ((userOptions.localTime === undefined) ? true : userOptions.localTime); +// userOptions.transport[f].maxsize = (userOptions.transport[f].maxsize !== undefined) ? userOptions.transport[f].maxsize : defaultMaxSize; + userOptions.transport[f].timestamp = timestamp; + userOptions.transport[f].datePattern = '.yyyy-MM-dd' + (userOptions.transport[f].fileext || ''); + /*userOptions.transport[f].logException = function (message, info, next, err) { + console.error(message); + };*/ + + var _log = new DailyRotateFile(userOptions.transport[f]); + options.transports.push(_log); + } else if (userOptions.transport[f].type === 'syslog' && userOptions.transport[f].enabled !== false) { + if (!SysLog) { + console.error('Syslog configured, but not installed!'); + continue; + } + // host: The host running syslogd, defaults to localhost. + // port: The port on the host that syslog is running on, defaults to syslogd's default port. + // protocol: The network protocol to log over (e.g. tcp4, udp4, unix, unix-connect, etc). + // path: The path to the syslog dgram socket (i.e. /dev/log or /var/run/syslog for OS X). + // pid: PID of the process that log messages are coming from (Default process.pid). + // facility: Syslog facility to use (Default: local0). + // localhost: Host to indicate that log messages are coming from (Default: localhost). + // sysLogType: The type of the syslog protocol to use (Default: BSD). + // app_name: The name of the application (Default: process.title). + // eol: The end of line character to be added to the end of the message (Default: Message without modifications). + // replace the used by syslog attribute "type" with own "sysLogType" + + // If no name defined, use hostname as name + userOptions.transport[f].localhost = userOptions.transport[f].localhost || hostname; + + if (userOptions.transport[f].sysLogType) { + userOptions.transport[f].type = userOptions.transport[f].sysLogType; + delete userOptions.transport[f].sysLogType; + } else { + delete userOptions.transport[f].type; + } + try { + options.transports.push(new SysLog(userOptions.transport[f])); + } catch (err) { + console.log('Cannot activate syslog: ' + err); + } + } + } + } + } else { + for (var i = 0; i < files.length; i++) { + var opt = { + name: !i ? tools.appName : 'dailyRotateFile' + i, + filename: path.normalize(isNpm ? __dirname + '/../../../log/' + files[i] : __dirname + '/../log/' + files[i]), + datePattern: '.yyyy-MM-dd.log', + json: false, // If true, messages will be logged as JSON (default true). + level: level, + silent: false, + localTime: true, + colorize: (userOptions.colorize === undefined) ? true : userOptions.colorize, + timestamp: timestamp, + label: prefix || '', + handleExceptions: false + //maxsize: defaultMaxSize + }; + + options.transports.push(new DailyRotateFile(opt)); + } + } + + if (!noStdout) { + options.transports.push(new winston.transports.Console({ + level: level, + silent: false, + colorize: (userOptions.colorize === undefined) ? true : userOptions.colorize, + timestamp: timestamp, + label: prefix || '' + })); + } + + var log = new winston.Logger(options); + + log.getFileName = function () { + if (this.transports && this.transports[tools.appName]) { + if (this.transports[tools.appName].filename) { + return this.transports[tools.appName].dirname + '/' + this.transports[tools.appName].filename; + } else if (this.transports[tools.appName]._getFilename) { + return this.transports[tools.appName].dirname + '/' + this.transports[tools.appName]._getFilename(); + } else { + return ''; + } + } else { + return ''; + } + }; + + log.activateDateChecker = function (isEnabled, daysCount) { + if (!isEnabled && this._fileChecker) { + clearInterval(this._fileChecker); + } else if (isEnabled && !this._fileChecker) { + if (!daysCount) daysCount = 3; + + // Check every hour + this._fileChecker = setInterval(function () { + if (this.transports[tools.appName] && fs.existsSync(this.transports[tools.appName].dirname)) { + var files = fs.readdirSync(this.transports[tools.appName].dirname); + var for3days = new Date(); + for3days.setDate(for3days.getDate() - daysCount); + + for (var i = 0; i < files.length; i++) { + var match = files[i].match(/.+\.(\d+-\d+-\d+)/); + if (match) { + var date = new Date(match[1]); + if (date < for3days) { + // delete file + try { + this.transports[tools.appName].log('info', 'host.' + hostname + ' Delete log file ' + files[i]); + fs.unlinkSync(this.transports[tools.appName].dirname + '/' + files[i]); + } catch (e) { + // there is a bug under windows, that file stays opened and cannot be deleted + this.log(os.platform().match(/^win/) ? 'info' : 'error', 'host.' + hostname + ' Cannot delete file "' + path.normalize(this.transports[tools.appName].dirname + '/' + files[i]) + '": ' + e); + } + } + } + } + } + }.bind(this), 3600000); // every hour + } + }; + + winston.unhandleExceptions(); + + return log; +}; + +function timestamp() { + var ts = new Date(); + var result = ts.getFullYear() + '-'; + + /** @type {number | string} */ + var value = ts.getMonth() + 1; + if (value < 10) value = '0' + value; + result += value + '-'; + + value = ts.getDate(); + if (value < 10) value = '0' + value; + result += value + ' '; + + value = ts.getHours(); + if (value < 10) value = '0' + value; + result += value + ':'; + + value = ts.getMinutes(); + if (value < 10) value = '0' + value; + result += value + ':'; + + value = ts.getSeconds(); + if (value < 10) value = '0' + value; + result += value + '.'; + + + value = ts.getMilliseconds(); + if (value < 10) { + value = '00' + value; + } else + if (value < 100) { + value = '0' + value; + } + + result += value + ' '; + + return result; +} + +module.exports = logger; + + + + + + + diff --git a/lib/multihostClient.js b/lib/multihostClient.js new file mode 100644 index 0000000..f6bd6b8 --- /dev/null +++ b/lib/multihostClient.js @@ -0,0 +1,215 @@ +'use strict'; + +var dgram = require('dgram'); +var crypto = null; +var port = 50005; +var MULTICAST_ADDR = '239.255.255.250'; + +function MHClient(hostname, logger, config, info) { + var id = 1; + var server; + var timer; + + function getIPs() { + var ifaces = require('os').networkInterfaces(); + var ipArr = []; + for (var dev in ifaces) { + if (!ifaces.hasOwnProperty(dev)) continue; + + /*jshint loopfunc:true */ + ifaces[dev].forEach(function (details) { + //noinspection JSUnresolvedVariable + if (!details.internal) ipArr.push(details.address); + }); + } + + return ipArr; + } + + function stopServer() { + if (server) { + try { + server.close(); + } catch (e) { + + } + server = null; + } + + if (timer) { + clearTimeout(timer); + timer = null; + } + } + + function sha(secret, salt, callback) { + // calculate sha256 + crypto = crypto || require('crypto'); + var hash = crypto.createHash('sha256'); + + hash.on('readable', function () { + var data = hash.read(); + if (data) { + callback(data.toString('hex')); + } + }); + + hash.write(secret + salt); + hash.end(); + } + + function startServer(isBroadcast, timeout, onReady, onMessage, onFinished) { + if (server) { + onFinished('Some operation still active'); + return; + } + server = dgram.createSocket('udp4'); + timeout = parseInt(timeout, 10) || 2000; + + timer = setTimeout(function () { + stopServer(); + + if (onFinished) { + onFinished(null); + onFinished = null; + } + + }, timeout); + + server.on('error', function (err) { + stopServer(); + + if (onFinished) { + onFinished(err); + onFinished = null; + } + }); + + server.on('message', function (msg, rinfo) { + msg = msg.toString(); + try { + msg = JSON.parse(msg); + if (onMessage) { + if (onMessage(server, msg, rinfo)) { + stopServer(); + onFinished = null; + } + } + } catch (e) { + console.error('Invalid answer: ' + msg); + } + }); + + server.on('listening', function (msg, rinfo) { + // var address = server.address(); + if (isBroadcast) server.setBroadcast(true); + onReady && onReady(server); + }); + + server.bind(); + } + + this.browse = function (timeout, isDebug, callback) { + var result = []; + var ownIps = getIPs(); + + startServer(true, timeout, + function onReady(srv) { + var text = JSON.stringify({ + cmd: 'browse', + id: ++id + }); + server.send(text, 0, text.length, port, MULTICAST_ADDR); + }, + function onMessage (srv, msg, rinfo) { + // ignore own answers + if (isDebug || rinfo.address !== '127.0.0.1' && ownIps.indexOf(rinfo.address) === -1) { + if (msg.result === 'not authenticated') { + result.push({ip: rinfo.address, hostname: rinfo.address, info: 'authentication required', auth: msg.auth}); + } else if (msg.result === 'ok') { + result.push(msg); + } else { + console.log('Unknown answer: ' + JSON.stringify(msg)); + } + } + if (isDebug) { + console.log(JSON.stringify(msg)); + } + }, + function onFinished(err) { + callback(err, result); + } + ); + }; + + this.connect = function (ip, password, callback) { + startServer(false, 2000, + function onReady(srv) { + var text = JSON.stringify({ + cmd: 'browse', + id: ++id + }); + server.send(text, 0, text.length, port, ip); + }, + function onMessage (srv, msg, rinfo) { + if (msg.cmd === 'browse' && msg.id === id) { + if (msg.result === 'ok') { + if (callback) { + if (!msg.objects) { + if (callback) { + callback('Invalid configuration received: ' + JSON.stringify(msg)); + callback = null; + } + } else if (!msg.states) { + if (callback) { + callback('Invalid configuration received: ' + JSON.stringify(msg)); + callback = null; + } + } else { + callback && callback(null, msg.objects, msg.states, rinfo.address); + } + } + } else if (msg.result === 'not authenticated') { + if (!password) { + if (callback) { + callback('not authenticated' + msg); + callback = null; + } + } else { + sha(password, msg.salt, function (shaText) { + // send password + var text = JSON.stringify({ + cmd: 'browse', + id: ++id, + password: shaText + }); + server.send(text, 0, text.length, port, ip); + }); + return false; + } + } else if (msg.result === 'invalid password') { + if (callback) { + callback('invalid password'); + callback = null; + } + } else { + console.log(msg.result); + } + return true; + } else { + console.warn('Unexpected message: ' + JSON.stringify(msg)); + } + }, + function onFinished(err) { + if (callback) { + callback(err); + callback = null; + } + } + ); + }; + + return this; +} + +module.exports = MHClient; \ No newline at end of file diff --git a/lib/multihostServer.js b/lib/multihostServer.js new file mode 100644 index 0000000..1688b1a --- /dev/null +++ b/lib/multihostServer.js @@ -0,0 +1,275 @@ +'use strict'; +var dgram = require('dgram'); +var port = 50005; +var MULTICAST_ADDR = '239.255.255.250'; + +function MHServer(hostname, logger, config, info, ips, secret) { + var server = null; + var count = 0; + var initTimer = null; + var buffer = {}; + var lastFrame = {}; + var crypto; + + var that = this; + var authList = {}; + config = Object.assign({}, config); // make a copy + + if (config.objects) { + config.objects = { + type: config.objects.type, + host: config.objects.host, + port: config.objects.port, + user: config.objects.user, + pass: config.objects.pass + }; + } + if (config.states) { + config.states = { + type: config.states.type, + host: config.states.host, + port: config.states.port, + user: config.states.user, + pass: config.states.pass, + options: config.states.options, + maxQueue: config.states.maxQueue + }; + } + + function send(msg, rinfo) { + if (server) { + setImmediate(function () { + var text = JSON.stringify(msg); + try { + server.send(text, 0, text.length, rinfo.port, rinfo.address); + } catch (e) { + logger.warn('host.' + hostname + ' cannot send answer to ' + rinfo.address + ':' + rinfo.port + ': ' + e); + } + }); + } + } + + // delete all old connections + function checkAuthList(ts) { + ts = ts || new Date().getTime(); + for (var id in authList) { + if (authList.hasOwnProperty(id)) { + if (!authList[id]) { + delete authList[id]; + } else if (ts - authList[id].ts > 31000) { + delete authList[id]; + } + } + } + } + + function isSlave(oHost, sHots, ownIps) { + return !(oHost === 'localhost' || oHost === '127.0.0.1' || ownIps.indexOf(oHost) !== -1); + } + + function sha(secret, salt, callback) { + // calculate sha256 + crypto = crypto || require('crypto'); + var hash = crypto.createHash('sha256'); + + hash.on('readable', function () { + var data = hash.read(); + if (data) { + callback(data.toString('hex')); + } + }); + + hash.write(secret + salt); + hash.end(); + } + + // hello => auth => browse + function process(msg, rinfo) { + if (!msg) return; + + var ts = new Date().getTime(); + checkAuthList(ts); + + var id = rinfo.address + ':' + rinfo.port; + + switch (msg.cmd) { + case 'browse': + if (secret && msg.password && authList[id]) { + sha(secret, authList[id].salt, function (shaText) { + if (shaText !== msg.password) { + send({ + auth: config.multihostService.secure, + cmd: msg.cmd, + id: msg.id, + result: 'invalid password' + }, rinfo); + } else { + authList[id].auth = true; + send({ + auth: config.multihostService.secure, + cmd: msg.cmd, + id: msg.id, + objects: config.objects, + states: config.states, + info: info, + hostname: hostname, + slave: isSlave(config.objects.host, config.states.host, ips), + result: 'ok' + }, rinfo); + } + }); + return; + } + + if (!config.multihostService.secure || (authList[id] && authList[id].auth)) { + send({ + auth: config.multihostService.secure, + cmd: msg.cmd, + id: msg.id, + objects: config.objects, + states: config.states, + info: info, + hostname: hostname, + slave: isSlave(config.objects.host, config.states.host, ips), + result: 'ok' + }, rinfo); + } else { + authList[id] = { + time: ts, + salt: (Math.random() * 1000000 + ts).toString().substring(0, 16), + auth: false + }; + // padding + if (authList[id].salt.length < 16) { + authList[id].salt += new Array(16 - authList[id].salt.length).join('_'); + } + send({ + auth: config.multihostService.secure, + cmd: msg.cmd, + id: msg.id, + result: 'not authenticated', + salt: authList[id].salt + }, rinfo); + } + break; + + default: + send({ + cmd: msg.cmd, + id: msg.id, + result: 'unknown command' + }, rinfo); + break; + } + } + + this.init = function () { + if (initTimer) { + clearTimeout(initTimer); + initTimer = null; + } + + if (count > 10) { + logger.warn('host.' + hostname + ' Port ' + port + ' is occupied. Service stopped.'); + return; + } + + server = dgram.createSocket('udp4'); + + server.on('error', function (err) { + logger.error('host.' + hostname + ' multihost service error: ' + err.stack); + server.close(); + server = null; + + if (!initTimer) { + initTimer = setTimeout(function () { + initTimer = null; + that.init(); + }, 5000); + } + }); + + server.on('close', function (err) { + server = null; + + if (!initTimer) { + initTimer = setTimeout(function () { + initTimer = null; + that.init(); + }, 5000); + } + }); + + server.on('message', function (msg, rinfo) { + // following messages are allowed + var text = msg.toString(); + var now = new Date().getTime(); + var id = rinfo.address + ':' + rinfo.port; + + for (var ids in buffer) { + if (!lastFrame[ids]) { + delete buffer[ids]; + } else if (now - lastFrame[ids] > 1000) { + delete buffer[ids]; + delete lastFrame[ids]; + } + } + + if (lastFrame[id] && now - lastFrame[id] > 1000) { + buffer[id] = ''; + } + + lastFrame[id] = now; + + if (!buffer[id] && text[0] !== '{') { + // ignore message + logger.warn('host.' + hostname + ' Message ignored: ' + text); + } else { + buffer[id] = (buffer[id] || '') + msg.toString(); + if (buffer[id] && buffer[id][buffer[id].length - 1] === '}') { + try { + var data = JSON.parse(buffer[id]); + buffer[id] = ''; + if (data) { + process(data, rinfo); + } + } catch (e) { + // may be not yet complete. + } + } + } + }); + + server.on('listening', function () { + server.addMembership(MULTICAST_ADDR); + var address = server.address(); + logger.warn('host.' + hostname + ' multihost service started on ' + address.address + ':' + address.port); + }); + + server.bind(50005); + }; + + this.close = function (callback) { + if (initTimer) { + clearTimeout(initTimer); + initTimer = null; + } + if (server) { + try { + server.close(callback); + server = null; + } catch (e) { + server = null; + callback && callback() + } + } else if (callback) { + callback(); + } + }; + + this.init(); + + return this; +} + +module.exports = MHServer; \ No newline at end of file diff --git a/lib/objects.js b/lib/objects.js new file mode 100644 index 0000000..8e23fc1 --- /dev/null +++ b/lib/objects.js @@ -0,0 +1,13 @@ +'use strict'; + +var getConfigFileName = require(__dirname + '/tools').getConfigFileName; +var config = JSON.parse(require('fs').readFileSync(getConfigFileName(), 'utf8')); +if (!config.objects) config.objects = {type: 'file'}; + +if (config.objects.type === 'file') { + module.exports = require(__dirname + '/objects/objectsInMemClient'); +} else if (config.objects.type === 'redis') { + module.exports = require(__dirname + '/objects/objectsInRedis'); +} else { + throw 'Unknown objects type: ' + config.objects.type; +} \ No newline at end of file diff --git a/lib/objects/objectsInMemClient.js b/lib/objects/objectsInMemClient.js new file mode 100644 index 0000000..624b7f3 --- /dev/null +++ b/lib/objects/objectsInMemClient.js @@ -0,0 +1,501 @@ +/** + * Object DB in memory - Client + * + * Copyright 2013-2018 bluefox + * + * MIT License + * + */ + +/* jshint -W097 */ +/* jshint strict: false */ +/* jslint node: true */ +/* jshint -W061 */ +'use strict'; + +const io = require('socket.io-client'); +const util = require('util'); +const stream = require('stream'); +const Writable = stream.Writable; +let memStore = {}; + +/* Writable memory stream */ +function WMStrm(key, options) { + // allow use without new operator + if (!(this instanceof WMStrm)) { + return new WMStrm(key, options); + } + + Writable.call(this, options); // init super + this.key = key; // save key + memStore[key] = new Buffer(''); // empty +} +util.inherits(WMStrm, Writable); + +WMStrm.prototype._write = function (chunk, enc, cb) { + if (chunk) { + // our memory store stores things in buffers + let buffer = (Buffer.isBuffer(chunk)) ? + chunk : // already is Buffer use it + new Buffer(chunk, enc); // string, convert + + // concatenate to the buffer already there + if (!memStore[this.key]) { + memStore[this.key] = new Buffer(''); + console.log('memstore for ' + this.key + ' is null'); + } + memStore[this.key] = Buffer.concat([memStore[this.key], buffer]); + } + if (!cb) throw 'No callback for WMStrm.prototype._write'; + cb(); +}; + +function ObjectsInMemClient(settings) { + let client; + let that = this; + let subscribes = []; + let connectionTimeout; + + let log = settings.logger; + if (!log) { + log = { + silly: function (msg) {/*console.log(msg);*/}, + debug: function (msg) {/*console.log(msg);*/}, + info: function (msg) {/*console.log(msg);*/}, + warn: function (msg) { + console.log(msg); + }, + error: function (msg) { + console.log(msg); + } + }; + } else if (!log.silly) { + log.silly = log.debug; + } + + let __construct = (function () { + if (!settings.connection.secure) { + client = io.connect('http://' + (settings.connection.host !=='0.0.0.0' ? settings.connection.host || '127.0.0.1' : '127.0.0.1') + ':' + (settings.connection.port || 9001)); + } else { + client = io.connect('https://' + (settings.connection.host !=='0.0.0.0' ? settings.connection.host || '127.0.0.1' : '127.0.0.1') + ':' + (settings.connection.port || 9001)); + } + + if (typeof settings.change === 'function') { + client.on('message', function (pattern, channel, message) { + log.silly(settings.namespace + ' inMem message ', pattern, channel, message); + try { + settings.change(channel, message); + } catch (e) { + log.error(settings.namespace + ' message ' + channel + ' ' + message + ' ' + e.message); + log.error(settings.namespace + ' ' + e.stack); + } + }); + } + client.on('disconnect', function (error) { + if (connectionTimeout) { + clearTimeout(connectionTimeout); + connectionTimeout = null; + } + if (typeof settings.disconnected === 'function') { + settings.disconnected(error); + } + }); + client.on('error', function (error) { + if (connectionTimeout) { + clearTimeout(connectionTimeout); + connectionTimeout = null; + } + if (typeof settings.disconnected === 'function') { + settings.disconnected(error); + } else { + log.error(settings.namespace + ' ' + error.message); + log.error(settings.namespace + ' ' + error.stack); + } + }); + client.on('connect', function (error) { + if (connectionTimeout) { + clearTimeout(connectionTimeout); + connectionTimeout = null; + } + if (typeof settings.connected === 'function') settings.connected('InMemoryDB ' + settings.connection.host + ':' + settings.connection.port); + }); + client.on('reconnect', function (error) { + // Re-initialise subscribes + for (let i = 0; i < subscribes.length; i++) { + client.emit('subscribe', subscribes[i]); + } + if (typeof settings.connected === 'function') settings.connected('InMemoryDB ' + settings.connection.host + ':' + settings.connection.port); + }); + connectionTimeout = setTimeout(function () { + if (typeof settings.connectTimeout === 'function') settings.connectTimeout('Connection timeout'); + connectionTimeout = null; + }, 5000); + })(); + + settings = settings || {}; + + this.subscribe = function (pattern, options) { + if (subscribes.indexOf(pattern) === -1) subscribes.push(pattern); + if (!client) return; + client.emit('subscribe', pattern, options); + }; + + this.unsubscribe = function (pattern) { + let pos = subscribes.indexOf(pattern); + if (pos !== -1) subscribes.splice(pos, 1); + + if (!client) return; + client.emit('unsubscribe', pattern); + }; + + this.destroy = function (callback) { + if (!client) return; + // Client may not close the DB + if (callback) callback(); + //client.emit('destroy', callback); + }; + + this.enableFileCache = function (enabled, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!client) return; + client.emit('enableFileCache', enabled, options, callback); + }; + + this.insert = function (id, attName, ignore, options, obj, callback) { + //return pipe for write into redis + let strm = new WMStrm(id + '/' + attName); + strm.on('finish', function () { + if (!memStore[id + '/' + attName]) log.error(settings.namespace + ' File ' + id + ' / ' + attName + ' is empty'); + that.writeFile(id, attName, memStore[id + '/' + attName], options, function () { + if (memStore[id + '/' + attName] !== undefined) delete memStore[id + '/' + attName]; + if (callback) callback(null, null); + }); + }); + return strm; + }; + + this.writeFile = function (id, name, data, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!client) return; + client.emit('writeFile', id, name, data, options, callback); + }; + + this.readFile = function (id, name, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!client) return; + client.emit('readFile', id, name, options, callback); + }; + + this.readDir = function (id, name, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!client) return; + client.emit('readDir', id, name, options, callback); + }; + + this.readDirAsZip = function (id, name, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!client) return; + client.emit('readDirAsZip', id, name, options, callback); + }; + + this.writeDirAsZip = function (id, name, options, data, callback) { + if (typeof data === 'function') { + callback = data; + data = options; + options = null; + } + if (!client) return; + client.emit('writeDirAsZip', id, name, options, data, callback); + }; + + this.unlink = function (id, name, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!client) return; + client.emit('unlink', id, name, options, callback); + }; + + this.delFile = this.unlink; + + this.rename = function (id, oldName, newName, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!client) return; + client.emit('rename', id, oldName, newName, options, callback); + }; + + this.mkdir = function (id, dirname, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!client) return; + client.emit('mkdir', id, dirname, options, callback); + }; + + this.chmodFile = function (id, name, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!client) return; + client.emit('chmodFile', id, name, options, callback); + }; + + this.chownFile = function (id, name, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!client) return; + client.emit('chownFile', id, name, options, callback); + }; + + this.touch = function (id, name, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!client) return; + client.emit('touch', id, name, options, callback); + }; + + this.rm = function (id, name, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!client) return; + client.emit('rm', id, name, options, callback); + }; + + this.chmodObject = function (pattern, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!client) return; + client.emit('chmodObject', pattern, options, callback); + }; + + this.chownObject = function (pattern, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!client) return; + client.emit('chownObject', pattern, options, callback); + }; + + this.getObjectView = function (design, search, params, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!client) return; + client.emit('getObjectView', design, search, params, options, callback); + }; + + this.getObjectList = function (params, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!client) return; + client.emit('getObjectList', params, options, callback); + }; + + this.getObjectListAsync = (params, options) => { + if (!client) return Promise.reject('No client'); + return new Promise((resolve, reject) => { + this.getObjectList(params, options, (err, arr) => { + if (err) { + reject(err); + } else { + resolve(arr); + } + }) + }); + }; + + this.extendObject = function (id, obj, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!client) return; + client.emit('extendObject', id, obj, options, callback); + }; + + this.extendObjectAsync = function (id, obj, options) { + if (!client) return Promise.reject('No client'); + return new Promise((resolve, reject) => { + this.extendObject(id, obj, options, err => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + }; + + this.getKeys = function (pattern, options, callback, dontModify) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!client) return; + client.emit('getKeys', pattern, options, callback, dontModify); + }; + this.getConfigKeys = this.getKeys; + + this.getObjects = function (keys, options, callback, dontModify) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!client) return; + client.emit('getObjects', keys, options, callback, dontModify); + }; + + this.getObjectsByPattern = (pattern, options, callback) => { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!client) return; + client.emit('getObjectsByPattern', pattern, options, callback); + }; + + this.getObjectsByPatternAsync = (pattern, options) => { + if (!client) return Promise.reject('No client'); + return new Promise((resolve, reject) => { + this.getObjectsByPattern(pattern, options, (err, objs) => { + if (err) { + reject(err); + } else { + resolve(objs); + } + }); + }); + }; + + this.setObject = function (id, obj, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!client) return; + client.emit('setObject', id, obj, options, callback); + }; + + this.setObjectAsync = function (id, obj, options) { + if (!client) return Promise.reject('No client'); + return new Promise((resolve, reject) => { + this.setObject(id, obj, options, err => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + }; + + this.delObject = function (id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!client) return; + client.emit('delObject', id, options, callback); + }; + + this.delObjectAsync = function (id, options) { + if (!client) return Promise.reject('No client'); + return new Promise((resolve, reject) => { + this.delObject(id, options, err => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + }; + + this.getObject = function (id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!client) return; + client.emit('getObject', id, options, callback); + }; + + this.getObjectAsync = function (id, options) { + if (!client) return Promise.reject('No client'); + return new Promise((resolve, reject) => { + this.getObject(id, options, (err, obj) => { + if (err) { + reject(err); + } else { + resolve(obj); + } + }); + }); + }; + + this.findObject = function (idOrName, type, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!client) return; + client.emit('findObject', idOrName, type, options, callback); + }; + + this.findObjectAsync = function (idOrName, type, options) { + if (!client) return Promise.reject('No client'); + return new Promise((resolve, reject) => { + this.findObject(idOrName, type, options, (err, obj) => { + if (err) { + reject(err); + } else { + resolve(obj); + } + }); + }); + }; + + this.destroyDB = function (options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!client) return; + client.emit('destroyDB', options, callback); + }; +} + +module.exports = ObjectsInMemClient; \ No newline at end of file diff --git a/lib/objects/objectsInMemServer.js b/lib/objects/objectsInMemServer.js new file mode 100644 index 0000000..5eb99a3 --- /dev/null +++ b/lib/objects/objectsInMemServer.js @@ -0,0 +1,3169 @@ +/** + * Object DB in memory - Server + * + * Copyright 2013-2018 bluefox + * + * MIT License + * + */ + +/* jshint -W097 */ +/* jshint strict: false */ +/* jslint node: true */ +/* jshint -W061 */ +'use strict'; + +const extend = require('node.extend'); +const fs = require('fs'); +const socketio = require('socket.io'); +const tools = require(__dirname + '/../tools'); +const getDefaultDataDir = tools.getDefaultDataDir; + +const stream = require('stream'); +const util = require('util'); +const Writable = stream.Writable; +let memStore = {}; + +/* Writable memory stream */ +function WMStrm(key, options) { + // allow use without new operator + if (!(this instanceof WMStrm)) return new WMStrm(key, options); + + Writable.call(this, options); // init super + this.key = key; // save key + memStore[key] = new Buffer(''); // empty +} +util.inherits(WMStrm, Writable); + +WMStrm.prototype._write = function (chunk, enc, cb) { + if (chunk) { + // our memory store stores things in buffers + let buffer = (Buffer.isBuffer(chunk)) ? + chunk : // already is Buffer use it + new Buffer(chunk, enc); // string, convert + + // concatenate to the buffer already there + if (!memStore[this.key]) { + memStore[this.key] = new Buffer(''); + console.log('memstore for ' + this.key + ' is null'); + } + memStore[this.key] = Buffer.concat([memStore[this.key], buffer]); + } + if (!cb) throw 'Callback is empty'; + cb(); +}; + +function ObjectsInMemServer(settings) { + if (!(this instanceof ObjectsInMemServer)) return new ObjectsInMemServer(settings); + settings = settings || {}; + + let change; + let zlib; + let that = this; + let objects = {}; + let fileOptions = {}; + let files = {}; + let configTimer = null; + let writeTimer = null; + let writeIds = []; + let users = {}; + let groups = {}; + let preserveSettings = []; + let regUser = /^system\.user/; + let regGroup = /^system\.group/; + let defaultAcl = { + groups: [], + acl: { + file: { + list: false, + read: false, + write: false, + create: false, + 'delete': false + }, + object: { + list: false, + read: false, + write: false, + 'delete': false + } + } + }; + let defaultNewAcl = settings.defaultNewAcl || null; + let namespace = settings.namespace || settings.hostname || ''; + let lastSave = null; + + let dataDir = (settings.connection.dataDir || getDefaultDataDir()); + if (dataDir) { + if (dataDir[0] === '.' && dataDir[1] === '.') { + dataDir = __dirname + '/../../' + dataDir; + } else if (dataDir[0] === '.' && dataDir[1] === '/') { + dataDir = __dirname + '/../../' + dataDir.substring(2); + } + } + dataDir = dataDir.replace(/\\/g, '/'); + if (dataDir[dataDir.length - 1] !== '/') dataDir += '/'; + + // Create data directory + if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir); + } + + let objectsName = dataDir + 'objects.json'; + const objectsDir = dataDir + 'files/'; + + settings.backup = settings.backup || { + disabled: false, // deactivates + files: 24, // minimum number of files + hours: 48, // hours + period: 120, // minutes + path: '' // absolute path + }; + const backupDir = settings.backup.path || (dataDir + 'backup-objects/'); + + if (!settings.backup.disabled) { + zlib = zlib || require('zlib'); + // Interval in minutes => to milliseconds + settings.backup.period = settings.backup.period === undefined ? 120 : parseInt(settings.backup.period); + if (isNaN(settings.backup.period)) { + settings.backup.period = 120; + } + settings.backup.period *= 60000; + + settings.backup.files = settings.backup.files === undefined ? 24 : parseInt(settings.backup.files); + if (isNaN(settings.backup.files)) { + settings.backup.files = 24; + } + + settings.backup.hours = settings.backup.hours === undefined ? 48 : parseInt(settings.backup.hours); + if (isNaN(settings.backup.hours)) { + settings.backup.hours = 48; + } + // Create backup directory + if (!fs.existsSync(backupDir)) { + fs.mkdirSync(backupDir); + } + } + + let log = settings.logger; + if (!log) { + log = { + silly: function (msg) {/*console.log(msg);*/}, + debug: function (msg) {/*console.log(msg);*/}, + info: function (msg) {/*console.log(msg);*/}, + warn: function (msg) { + console.log(msg); + }, + error: function (msg) { + console.log(msg); + } + }; + } else if (!log.silly) { + log.silly = log.debug; + } + + let server = { + app: null, + server: null, + io: null, + settings: settings + }; + + /*function prepareRights(options) { + let fOptions = {}; + options = options || {}; + if (!options.user) { + options = { + user: 'system.user.admin', + params: options + }; + } + + // acl.owner = user that creates or owns the file + // acl.group = group, that assigned to file + // acl.permissions = '0777' - default 1 (execute, 2 write, 4 read + if (!options.user) { + fOptions.acl = { + owner: 'system.user.admin', + ownerGroup: 'system.group.administrator', + permissions: 0x644 // '0777' + }; + } else { + fOptions.acl = { + owner: options.user + }; + fOptions.acl.ownerGroup = options.group; + fOptions.acl.permissions = 0x644; + } + fOptions.acl.ownerGroup = fOptions.acl.ownerGroup || 'system.group.administrator'; + + return fOptions; + }*/ + + // -------------- FILE FUNCTIONS ------------------------------------------- + // memServer specific function + function mkpathSync(rootpath, dirpath) { + // Remove filename + dirpath = dirpath.split('/'); + dirpath.pop(); + if (!dirpath.length) return; + + for (let i = 0; i < dirpath.length; i++) { + rootpath += dirpath[i] + '/'; + if (!fs.existsSync(rootpath)) { + fs.mkdirSync(rootpath); + } + } + } + + function saveFileSettings(id, force) { + if (typeof id === 'boolean') { + force = id; + id = undefined; + } + + if (id !== undefined && writeIds.indexOf(id) === -1) writeIds.push(id); + + if (writeTimer) clearTimeout(writeTimer); + + // if store immediately + if (force) { + writeTimer = null; + // Store dirs description + for (let _id = 0; _id < writeIds.length; _id++) { + try { + fs.writeFileSync(objectsDir + writeIds[_id] + '/_data.json', JSON.stringify(fileOptions[writeIds[_id]])); + } catch (e) { + log.error(namespace + ' Cannot write files: ' + objectsDir + writeIds[_id] + '/_data.json: ' + e.message); + } + } + writeIds = []; + } else { + writeTimer = setTimeout(function () { + // Store dirs description + for (let id = 0; id < writeIds.length; id++) { + try { + fs.writeFileSync(objectsDir + writeIds[id] + '/_data.json', JSON.stringify(fileOptions[writeIds[id]])); + } catch (e) { + log.error(namespace + ' Cannot write files: ' + objectsDir + writeIds[id] + '/_data.json: ' + e.message); + } + } + writeIds = []; + }, 1000); + } + } + + function checkFile(id, name, options, flag) { + if (typeof fileOptions[id][name].acl !== 'object') { + fileOptions[id][name] = { + mimeType: fileOptions[id][name], + acl: { + owner: (defaultNewAcl && defaultNewAcl.owner) || 'system.user.admin', + ownerGroup: (defaultNewAcl && defaultNewAcl.ownerGroup) || 'system.group.administrator', + permissions: (defaultNewAcl && defaultNewAcl.file) || 0x644 // '0644' + } + }; + } + + // Set default owner group + fileOptions[id][name].acl.ownerGroup = fileOptions[id][name].acl.ownerGroup || (defaultNewAcl && defaultNewAcl.ownerGroup) || 'system.group.administrator'; + fileOptions[id][name].acl.owner = fileOptions[id][name].acl.owner || (defaultNewAcl && defaultNewAcl.owner) || 'system.user.admin'; + fileOptions[id][name].acl.permissions = fileOptions[id][name].acl.permissions || (defaultNewAcl && defaultNewAcl.file) || 0x644; // '0644' + + if (options.user !== 'system.user.admin' && + options.groups.indexOf('system.group.administrator') === -1 && + fileOptions[id][name].acl) { + if (fileOptions[id][name].acl.owner !== options.user) { + // Check if the user is in the group + if (options.groups.indexOf(fileOptions[id][name].acl.ownerGroup) !== -1) { + // Check group rights + if (!(fileOptions[id][name].acl.permissions & (flag << 4))) { + return false; + } + } else { + // everybody + if (!(fileOptions[id][name].acl.permissions & flag)) { + return false; + } + } + } else { + // Check user rights + if (!(fileOptions[id][name].acl.permissions & (flag << 8))) { + return false; + } + } + } + return true; + } + + function checkFileRights(id, name, options, flag, callback) { + options = options || {}; + if (!options.user) { + // Before files converted, lets think: if no options it is admin + options = { + user: 'system.user.admin', + params: options, + group: 'system.group.administrator' + }; + } + + if (options.checked) { + return callback(null, options); + } + + if (!options.acl) { + that.getUserGroup(options.user, function (user, groups, acl) { + options.acl = acl || {}; + options.groups = groups; + options.group = groups ? groups[0] : null; + checkFileRights(id, name, options, flag, callback); + }); + return; + } + // If user may write + if (flag === 2 && !options.acl.file.write) {// write + return callback('permissionError', options); + } + // If user may read + if (flag === 4 && !options.acl.file.read) {// read + return callback('permissionError', options); + } + + // read rights of file + if (!fileOptions[id]) { + if (fs.existsSync(objectsDir + id + '/_data.json')) { + try { + fileOptions[id] = JSON.parse(fs.readFileSync(objectsDir + id + '/_data.json', 'utf8')); + } catch (e) { + log.error(namespace + ' Cannot parse ' + objectsDir + id + '/_data.json: ' + e); + } + } else { + fileOptions[id] = {}; + } + } + + options.checked = true; + if (!name || !fileOptions[id] || !fileOptions[id][name]) { + return callback(null, options); + } + if (checkFile(id, name, options,flag)) { + return callback(null, options); + } else { + return callback('permissionError', options); + } + + /*if (typeof fileOptions[id][name].acl !== 'object') { + fileOptions[id][name] = { + mimeType: fileOptions[id][name], + acl: { + owner: 'system.user.admin', + permissions: 0x644, + ownerGroup: 'system.group.administrator' + } + }; + } + // Set default onwer group + fileOptions[id][name].acl.ownerGroup = fileOptions[id][name].acl.ownerGroup || 'system.group.administrator'; + + if (options.user !== 'system.user.admin' && + options.groups.indexOf('system.group.administrator') === -1 && + fileOptions[id][name].acl) { + if (fileOptions[id][name].acl.owner !== options.user) { + // Check if the user is in the group + if (options.groups.indexOf(fileOptions[id][name].acl.ownerGroup) !== -1) { + // Check group rights + if (!(fileOptions[id][name].acl.permissions & (flag << 4))) { + return callback('permissionError', options); + } + } else { + // everybody + if (!(fileOptions[id][name].acl.permissions & flag)) { + return callback('permissionError', options); + } + } + } else { + // Check user rights + if (!(fileOptions[id][name].acl.permissions & (flag << 8))) { + return callback('permissionError', options); + } + } + } + return callback(null, options);*/ + } + + function setDefaultAcl(callback) { + try { + defaultNewAcl = Object.assign({}, objects['system.config'].common.defaultNewAcl); + } catch (e) { + defaultNewAcl = { + owner: 'system.user.admin', + ownerGroup: 'system.group.administrator', + object: 0x664, + state: 0x664, + file: 0x664 + }; + objects['system.config'].common.defaultNewAcl = Object.assign({}, defaultNewAcl); + } + + let count = 0; + // Set all objects without ACL to this one + for (let id in objects) { + if (objects.hasOwnProperty(id) && objects[id] && !objects[id].acl) { + objects[id].acl = Object.assign({}, defaultNewAcl); + delete objects[id].acl.file; + if (objects[id].type !== 'state') { + delete objects[id].acl.state; + } + + count++; + } + } + if (typeof callback === 'function') callback(null, count); + } + + this.getUserGroup = function (user, callback) { + if (!user || typeof user !== 'string' || !user.match(/^system\.user\./)) { + console.log('invalid user name: ' + user); + user = JSON.stringify(user); + return callback.call(that, user, [], Object.assign({}, defaultAcl.acl)); + } + if (users[user]) { + return callback.call(that, user, users[user].groups, users[user].acl); + } + + // Read all groups + this.getObjectList({startkey: 'system.group.', endkey: 'system.group.\u9999'}, {checked: true}, function (err, arr) { + if (err) log.error(namespace + ' ' + err); + groups = []; + if (arr) { + // Read all groups + for (let g = 0; g < arr.rows.length; g++) { + groups[g] = arr.rows[g].value; + if (groups[g]._id === 'system.group.administrator') { + groups[g].common.acl = { + file: { + list: true, + read: true, + write: true, + create: true, + 'delete': true + }, + object: { + list: true, + read: true, + write: true, + create: true, + 'delete': true + }, + users: { + list: true, + read: true, + write: true, + create: true, + 'delete': true + } + }; + } + } + } + + that.getObjectList({startkey: 'system.user.', endkey: 'system.user.\u9999'}, {checked: true}, function (err, arr) { + if (err) log.error(namespace + ' ' + err); + users = {}; + + if (arr) { + for (let i = 0; i < arr.rows.length; i++) { + users[arr.rows[i].value._id] = Object.assign({}, defaultAcl); + if (arr.rows[i].value._id === 'system.user.admin') { + users['system.user.admin'].acl.file = { + list: true, + read: true, + write: true, + create: true, + 'delete': true + }; + users['system.user.admin'].acl.object = { + create: true, + list: true, + read: true, + write: true, + 'delete': true + }; + users['system.user.admin'].acl.users = { + create: true, + list: true, + read: true, + write: true, + 'delete': true + }; + } + } + } + + for (let g = 0; g < groups.length; g++) { + if (!groups[g].common.members) continue; + for (let m = 0; m < groups[g].common.members.length; m++) { + let u = groups[g].common.members[m]; + if (!users[u]) { + log.warn('Unknown user in group "' + g + '": ' + u); + continue; + } + users[u].groups.push(groups[g]._id); + + if (groups[g].common.acl && groups[g].common.acl.file) { + if (!users[u].acl || !users[u].acl.file) { + users[u].acl = users[u].acl || {}; + users[u].acl.file = users[u].acl.file || {}; + + users[u].acl.file.create = groups[g].common.acl.file.create; + users[u].acl.file.read = groups[g].common.acl.file.read; + users[u].acl.file.write = groups[g].common.acl.file.write; + users[u].acl.file['delete'] = groups[g].common.acl.file['delete']; + users[u].acl.file.list = groups[g].common.acl.file.list; + } else { + users[u].acl.file.create = users[u].acl.file.create || groups[g].common.acl.file.create; + users[u].acl.file.read = users[u].acl.file.read || groups[g].common.acl.file.read; + users[u].acl.file.write = users[u].acl.file.write || groups[g].common.acl.file.write; + users[u].acl.file['delete'] = users[u].acl.file['delete'] || groups[g].common.acl.file['delete']; + users[u].acl.file.list = users[u].acl.file.list || groups[g].common.acl.file.list; + } + } + + if (groups[g].common.acl && groups[g].common.acl.object) { + if (!users[u].acl || !users[u].acl.object) { + users[u].acl = users[u].acl || {}; + users[u].acl.object = users[u].acl.object || {}; + + users[u].acl.object.create = groups[g].common.acl.object.create; + users[u].acl.object.read = groups[g].common.acl.object.read; + users[u].acl.object.write = groups[g].common.acl.object.write; + users[u].acl.object['delete'] = groups[g].common.acl.object['delete']; + users[u].acl.object.list = groups[g].common.acl.object.list; + } else { + users[u].acl.object.create = users[u].acl.object.create || groups[g].common.acl.object.create; + users[u].acl.object.read = users[u].acl.object.read || groups[g].common.acl.object.read; + users[u].acl.object.write = users[u].acl.object.write || groups[g].common.acl.object.write; + users[u].acl.object['delete'] = users[u].acl.object['delete'] || groups[g].common.acl.object['delete']; + users[u].acl.object.list = users[u].acl.object.list || groups[g].common.acl.object.list; + } + } + + if (groups[g].common.acl && groups[g].common.acl.users) { + if (!users[u].acl || !users[u].acl.users) { + users[u].acl = users[u].acl || {}; + users[u].acl.users = users[u].acl.users || {}; + + users[u].acl.users.create = groups[g].common.acl.users.create; + users[u].acl.users.read = groups[g].common.acl.users.read; + users[u].acl.users.write = groups[g].common.acl.users.write; + users[u].acl.users['delete'] = groups[g].common.acl.users['delete']; + users[u].acl.users.list = groups[g].common.acl.users.list; + + } else { + users[u].acl.users.create = users[u].acl.users.create || groups[g].common.acl.users.create; + users[u].acl.users.read = users[u].acl.users.read || groups[g].common.acl.users.read; + users[u].acl.users.write = users[u].acl.users.write || groups[g].common.acl.users.write; + users[u].acl.users['delete'] = users[u].acl.users['delete'] || groups[g].common.acl.users['delete']; + users[u].acl.users.list = users[u].acl.users.list || groups[g].common.acl.users.list; + } + } + } + } + + callback.call(that, user, users[user] ? users[user].groups : [], users[user] ? users[user].acl : Object.assign({}, defaultAcl.acl)); + }); + }); + }; + + this.getMimeType = function (ext) { + if (ext instanceof Array) ext = ext[0]; + let _mimeType = 'text/javascript'; + let isBinary = false; + + if (ext === '.css') { + _mimeType = 'text/css'; + } else if (ext === '.ico') { + _mimeType = 'image/x-icon'; + isBinary = true; + } else if (ext === '.bmp') { + _mimeType = 'image/bmp'; + isBinary = true; + } else if (ext === '.png') { + isBinary = true; + _mimeType = 'image/png'; + } else if (ext === '.jpg') { + isBinary = true; + _mimeType = 'image/jpeg'; + } else if (ext === '.jpeg') { + isBinary = true; + _mimeType = 'image/jpeg'; + } else if (ext === '.gif') { + isBinary = true; + _mimeType = 'image/gif'; + } else if (ext === '.tif') { + isBinary = true; + _mimeType = 'image/tiff'; + } else if (ext === '.js') { + _mimeType = 'application/javascript'; + } else if (ext === '.html') { + _mimeType = 'text/html'; + } else if (ext === '.htm') { + _mimeType = 'text/html'; + } else if (ext === '.json') { + _mimeType = 'application/json'; + } else if (ext === '.xml') { + _mimeType = 'text/xml'; + } else if (ext === '.svg') { + _mimeType = 'image/svg+xml'; + } else if (ext === '.eot') { + isBinary = true; + _mimeType = 'application/vnd.ms-fontobject'; + } else if (ext === '.ttf') { + isBinary = true; + _mimeType = 'application/font-sfnt'; + } else if (ext === '.cur') { + isBinary = true; + _mimeType = 'application/x-win-bitmap'; + } else if (ext === '.woff') { + isBinary = true; + _mimeType = 'application/font-woff'; + } else if (ext === '.wav') { + isBinary = true; + _mimeType = 'audio/wav'; + } else if (ext === '.mp3') { + isBinary = true; + _mimeType = 'audio/mpeg3'; + } else if (ext === '.avi') { + isBinary = true; + _mimeType = 'video/avi'; + } else if (ext === '.mp4') { + isBinary = true; + _mimeType = 'video/mp4'; + } else if (ext === '.mkv') { + isBinary = true; + _mimeType = 'video/mkv'; + } else if (ext === '.zip') { + isBinary = true; + _mimeType = 'application/zip'; + } else if (ext === '.ogg') { + isBinary = true; + _mimeType = 'audio/ogg'; + } else if (ext === '.manifest') { + _mimeType = 'text/cache-manifest'; + } else { + _mimeType = 'text/javascript'; + } + + return {mimeType: _mimeType, isBinary: isBinary}; + }; + + this.insert = function (id, attName, ignore, options, obj, callback) { + if (typeof options === 'string') { + options = {mimeType: options}; + } + + //return pipe for write into redis + let strm = new WMStrm(id + '/' + attName); + strm.on('finish', function () { + if (!memStore[id + '/' + attName]) log.error(namespace + ' File ' + id + ' / ' + attName + ' is empty'); + that.writeFile(id, attName, memStore[id + '/' + attName] || '', options, function () { + if (memStore[id + '/' + attName] !== undefined) delete memStore[id + '/' + attName]; + if (callback) setImmediate(callback, null, null); + }); + }); + return strm; + }; + + this.writeFile = function (id, name, data, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (typeof options === 'string') { + options = {mimeType: options}; + } + + if (name[0] === '/') name = name.substring(1); + + try { + if (!fileOptions[id]) { + if (fs.existsSync(objectsDir + id + '/_data.json')) { + try { + fileOptions[id] = JSON.parse(fs.readFileSync(objectsDir + id + '/_data.json', 'utf8')); + } catch (e) { + log.error(namespace + ' Cannot parse ' + objectsDir + id + '/_data.json: ' + e); + } + } else { + fileOptions[id] = {}; + } + } + + files[id] = files[id] || {}; + + // If file yet exists => check the permissions + if (!options || !options.checked) { + return checkFileRights(id, name, options, 0x2/*write*/, function (err, options) { + if (err) { + if (typeof callback === 'function') { + callback(err); + } + } else { + return that.writeFile(id, name, data, options, callback); + } + }); + } + + try { + if (!fs.existsSync(objectsDir)) fs.mkdirSync(objectsDir); + if (!fs.existsSync(objectsDir + id)) fs.mkdirSync(objectsDir + id); + } catch (e) { + log.error(namespace + ' Cannot create directories: ' + objectsDir + id + ': ' + e.message); + log.error(namespace + ' Check the permissions! Or call "sudo chmod -R 777 *" in ' + tools.appName +' dir'); + if (typeof callback === 'function') callback(e.message); + return; + } + + let isBinary; + let ext = name.match(/\.[^.]+$/); + let mime = that.getMimeType(ext); + let _mimeType = mime.mimeType; + isBinary = mime.isBinary; + + if (!fileOptions[id][name]) { + fileOptions[id][name] = {createdAt: (new Date()).getTime()}; + } + if (!fileOptions[id][name].acl) { + fileOptions[id][name].acl = { + owner: options.user || (defaultNewAcl && defaultNewAcl.owner) || 'system.user.admin', + ownerGroup: options.group || (defaultNewAcl && defaultNewAcl.ownerGroup) || 'system.group.administrator', + permissions: options.mode || (defaultNewAcl && defaultNewAcl.file) || 0x644 + }; + } + + fileOptions[id][name].mimeType = options.mimeType || _mimeType; + fileOptions[id][name].binary = isBinary; + fileOptions[id][name].acl.ownerGroup = fileOptions[id][name].acl.ownerGroup || (defaultNewAcl && defaultNewAcl.ownerGroup) || 'system.group.administrator'; + fileOptions[id][name].modifiedAt = (new Date()).getTime(); + + if (isBinary) { + // Reload by read + delete files[id][name]; + } else { + files[id][name] = data; + } + + try { + // Create directories if complex structure + mkpathSync(objectsDir + id + '/', name); + // Store file + fs.writeFileSync(objectsDir + id + '/' + name, data, {'flag': 'w', 'encoding': isBinary ? 'binary' : 'utf8'}); + // Store dir description + saveFileSettings(id); + } catch (e) { + log.error(namespace + ' Cannot write files: ' + objectsDir + id + '/' + name + ': ' + e.message); + if (typeof callback === 'function') callback(e.message); + return; + } + if (typeof callback === 'function') callback(); + } catch (e) { + if (typeof callback === 'function') callback(e.message); + } + }; + + this.readFile = function (id, name, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (name[0] === '/') name = name.substring(1); + + if (!options || !options.checked) { + checkFileRights(id, name, options, 0x4/*read*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return that.readFile(id, name, options, callback); + } + }); + return; + } + + try { + if (!fileOptions[id]) { + if (fs.existsSync(objectsDir + id + '/_data.json')) { + try { + fileOptions[id] = JSON.parse(fs.readFileSync(objectsDir + id + '/_data.json', 'binary')); + } catch (e) { + log.error(namespace + ' Cannot parse ' + objectsDir + id + '/_data.json: ' + e); + fileOptions[id] = {}; + } + } else { + fileOptions[id] = {}; + } + } + + if (!files[id]) files[id] = {}; + + if (!files[id][name] || settings.connection.noFileCache || options.noFileCache) { + if (fs.existsSync(objectsDir + id + '/' + name)) { + // Create description object if not exists + if (!fileOptions[id][name]) { + fileOptions[id][name] = { + acl: { + owner: (defaultNewAcl && defaultNewAcl.owner) || 'system.user.admin', + ownerGroup: (defaultNewAcl && defaultNewAcl.ownerGroup) || 'system.group.administrator', + permissions: (defaultNewAcl && defaultNewAcl.file.permissions) || 0x777 + + } + }; + } + if (typeof fileOptions[id][name] !== 'object') { + fileOptions[id][name] = { + mimeType: fileOptions[id][name], + acl: { + owner: (defaultNewAcl && defaultNewAcl.owner) || 'system.user.admin', + ownerGroup: (defaultNewAcl && defaultNewAcl.ownerGroup) || 'system.group.administrator', + permissions: (defaultNewAcl && defaultNewAcl.file.permissions) || 0x777 + } + }; + } + + files[id][name] = fs.readFileSync(objectsDir + id + '/' + name); + if (fileOptions[id][name].binary === undefined) { + let pos = name.lastIndexOf('.'); + let ext = ''; + if (pos !== -1) ext = name.substring(pos); + let mimeType = that.getMimeType(ext); + fileOptions[id][name].binary = mimeType.isBinary; + fileOptions[id][name].mimeType = mimeType.mimeType; + } + + if (!fileOptions[id][name].binary) { + if (files[id][name]) files[id][name] = files[id][name].toString(); + } + } else { + if (fileOptions[id][name] !== undefined) delete fileOptions[id][name]; + if (files[id][name] !== undefined) delete files[id][name]; + } + } + + if (fileOptions[id][name] && !fileOptions[id][name].acl) { + // all files belongs to admin by default, but everyone can edit it + fileOptions[id][name].acl = { + owner: (defaultNewAcl && defaultNewAcl.owner) || 'system.user.admin', + ownerGroup: (defaultNewAcl && defaultNewAcl.ownerGroup) || 'system.group.administrator', + permissions: (defaultNewAcl && defaultNewAcl.file.permissions) || 0x677 + }; + } + + if (typeof callback === 'function') { + if (fileOptions[id][name] !== null && fileOptions[id][name] !== undefined) { + if (!fileOptions[id][name].mimeType) { + let _pos = name.lastIndexOf('.'); + let _ext = ''; + if (_pos !== -1) _ext = name.substring(_pos); + let _mimeType = that.getMimeType(_ext); + fileOptions[id][name].mimeType = _mimeType.mimeType; + } + callback(null, files[id][name], fileOptions[id][name].mimeType); + } else { + callback('Not exists'); + } + } + } catch (e) { + if (typeof callback === 'function') { + callback(e.message); + } + } + }; + + this.unlink = function (id, name, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (name[0] === '/') name = name.substring(1); + + if (!options || !options.checked) { + checkFileRights(id, name, options, 0x2/*write*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + if (!options.acl.file['delete']) { + if (typeof callback === 'function') callback('permissionError'); + } else { + return that.unlink(id, name, options, callback); + } + } + }); + return; + } + + try { + let changed = false; + if (!fileOptions[id]) { + if (fs.existsSync(objectsDir + id + '/_data.json')) { + fileOptions[id] = JSON.parse(fs.readFileSync(objectsDir + id + '/_data.json', 'utf8')); + } else { + fileOptions[id] = {}; + } + } + if (fileOptions[id][name]) { + changed = true; + delete fileOptions[id][name]; + } + if (files[id] && files[id][name]) { + delete files[id][name]; + } + if (fs.existsSync(objectsDir + id + '/' + name)) { + let stat = fs.statSync(objectsDir + id + '/' + name); + + if (stat.isDirectory()) { + // read all entries and delete every one + let fdir = fs.readdirSync(objectsDir + id + '/' + name); + let cnt = 0; + for (let f = 0; f < fdir.length; f++) { + cnt++; + that.unlink(id, name + '/' + fdir[f], options, function (err) { + if (!--cnt) { + log.debug('Delete directory ' + id + '/' + name); + try { + fs.rmdirSync(objectsDir + id + '/' + name); + } catch (e) { + log.error('Cannot delete directory "' + id + '/' + name + '": ' + e); + } + if (typeof callback === 'function') { + setImmediate(function () { + callback(err); + }); + } + } + }); + } + if (!cnt) { + log.debug('Delete directory ' + id + '/' + name); + try { + fs.rmdirSync(objectsDir + id + '/' + name); + } catch (e) { + log.error('Cannot delete directory "' + id + '/' + name + '": ' + e); + } + if (typeof callback === 'function') { + setImmediate(function () { + callback(); + }); + } + } + } else { + log.debug('Delete file ' + id + '/' + name); + try { + fs.unlinkSync(objectsDir + id + '/' + name); + } catch (e) { + log.error('Cannot delete file "' + id + '/' + name + '": ' + e); + } + if (typeof callback === 'function') { + setImmediate(function () { + callback(); + }); + } + } + } else { + if (typeof callback === 'function') { + setImmediate(function () { + callback('Not exists'); + }); + } + } + // Store dir description + if (changed) saveFileSettings(id); + } catch (e) { + if (typeof callback === 'function') { + setImmediate(function () { + callback(e.message); + }); + } + } + }; + + this.delFile = this.unlink; + + this.readDir = function (id, name, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!options || !options.checked) { + checkFileRights(id, name, options, 0x4/*read*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + if (!options.acl.file.list) { + if (typeof callback === 'function') callback('permissionError'); + } else { + return that.readDir(id, name, options, callback); + } + } + }); + return; + } + if (!fileOptions[id]) { + if (fs.existsSync(objectsDir + id + '/_data.json')) { + try { + fileOptions[id] = JSON.parse(fs.readFileSync(objectsDir + id + '/_data.json', 'binary')); + } catch (e) { + log.error(namespace + ' Cannot parse ' + objectsDir + id + '/_data.json: ' + e); + } + } else { + fileOptions[id] = {}; + } + } + // Find all files and directories starts with name + let _files = []; + if (name[0] === '/') name = name.substring(1); + + if (name && name[name.length - 1] !== '/') name += '/'; + let len = (name) ? name.length : 0; + for (let f in fileOptions[id]) { + if (fileOptions[id].hasOwnProperty(f) && (!name || f.substring(0, len) === name)) { + /** @type {string|string[]} */ + let rest = f.substring(len); + rest = rest.split('/', 2); + if (rest[0] && _files.indexOf(rest[0]) === -1) { + _files.push(rest[0]); + } + } + } + + if (fs.existsSync(objectsDir + id + '/' + name)) { + try { + let dirFiles = fs.readdirSync(objectsDir + id + '/' + name); + for (let i = 0; i < dirFiles.length; i++) { + if (dirFiles[i] === '..' || dirFiles[i] === '.') continue; + if (dirFiles[i] !== '_data.json' && _files.indexOf(dirFiles[i]) === -1) { + _files.push(dirFiles[i]); + } + } + } catch (e) { + if (typeof callback === 'function') { + setImmediate(function () { + callback(e, []); + }); + } + return; + } + } else { + if (typeof callback === 'function') { + setImmediate(function () { + callback('Not exists', []); + }); + } + return; + } + + _files.sort(); + let res = []; + for (let j = 0; j < _files.length; j++) { + if (_files[j] === '..' || _files[j] === '.') continue; + if (fs.existsSync(objectsDir + id + '/' + name + _files[j])) { + let stats = fs.statSync(objectsDir + id + '/' + name + _files[j]); + let acl = (fileOptions[id][name + _files[j]] && fileOptions[id][name + _files[j]].acl) ? + Object.assign({}, fileOptions[id][name + _files[j]].acl) : // copy settings + { + read: true, + write : true, + owner: (defaultNewAcl && defaultNewAcl.owner) || 'system.user.admin', + ownerGroup: (defaultNewAcl && defaultNewAcl.ownerGroup) || 'system.group.administrator', + permissions: (defaultNewAcl && defaultNewAcl.file.permissions) || 0x644 + }; + + try { + // if filter for user + if (options.filter && acl) { + // If user may not write + if (!options.acl.file.write) {// write + acl.permissions &= ~0x222; + } + // If user may not read + if (!options.acl.file.read) {// read + acl.permissions &= ~0x444; + } + + if (options.user !== 'system.user.admin' && options.groups.indexOf('system.group.administrator') === -1) { + if (acl.owner !== options.user) { + // Check if the user is in the group + if (options.groups.indexOf(acl.ownerGroup) !== -1) { + // Check group rights + if (!(acl.permissions & (0x6 << 4))) { + continue; + } + acl.read = !!(acl.permissions & 0x40); + acl.write = !!(acl.permissions & 0x20); + } else { + // everybody + if (!(acl.permissions & 0x6)) { + continue; + } + acl.read = !!(acl.permissions & 0x4); + acl.write = !!(acl.permissions & 0x2); + } + } else { + // Check user rights + if (!(acl.permissions & (0x6 << 8))) { + continue; + } + acl.read = !!(acl.permissions & 0x400); + acl.write = !!(acl.permissions & 0x200); + } + } else { + acl.read = true; + acl.write = true; + } + } + } catch (e) { + log.error(namespace + ' Cannot read permssions of ' + objectsDir + id + '/' + name + _files[j] + ': ' + e); + } + + res.push({ + file: _files[j], + stats: stats, + isDir: stats.isDirectory(), + acl: acl, + modifiedAt: fileOptions[id][name + _files[j]] ? fileOptions[id][name + _files[j]].modifiedAt : undefined, + createdAt: fileOptions[id][name + _files[j]] ? fileOptions[id][name + _files[j]].createdAt : undefined + }); + } + } + + if (typeof callback === 'function') { + setImmediate(function () { + callback(null, res); + }); + } + }; + + this.rename = function (id, oldName, newName, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (oldName[0] === '/') oldName = oldName.substring(1); + if (newName[0] === '/') newName = newName.substring(1); + + if (!options || !options.checked) { + checkFileRights(id, oldName, options, 0x2/*write*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + if (!options.acl.file.write) { + if (typeof callback === 'function') callback('permissionError'); + } else { + return that.rename(id, oldName, newName, options, callback); + } + } + }); + return; + } + try { + if (!fileOptions[id]) { + if (fs.existsSync(objectsDir + id + '/_data.json')) { + fileOptions[id] = JSON.parse(fs.readFileSync(objectsDir + id + '/_data.json', 'utf8')); + } else { + fileOptions[id] = {}; + } + } + if (fileOptions[id][oldName]) { + let type = fileOptions[id][oldName]; + delete fileOptions[id][oldName]; + fileOptions[id][newName] = type; + fs.writeFileSync(objectsDir + id + '/_data.json', JSON.stringify(fileOptions[id])); + } + if (files[id] && files[id][oldName]) { + let data = files[id][oldName]; + delete files[id][oldName]; + files[id][newName] = data; + } + if (fs.existsSync(objectsDir + id + '/' + oldName)) { + fs.renameSync(objectsDir + id + '/' + oldName, objectsDir + id + '/' + newName); + if (typeof callback === 'function') callback(); + } else { + if (typeof callback === 'function') callback('Not exists'); + } + } catch (e) { + if (typeof callback === 'function') callback(e.message); + } + }; + + this.touch = function (id, name, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!options || !options.checked) { + checkFileRights(id, null, options, 0x2/*write*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return that.touch(id, name, options, callback); + } + }); + return; + } + try { + if (!fileOptions[id]) { + if (fs.existsSync(objectsDir + id + '/_data.json')) { + fileOptions[id] = JSON.parse(fs.readFileSync(objectsDir + id + '/_data.json', 'utf8')); + } else { + fileOptions[id] = {}; + } + } + + let regEx = new RegExp(pattern2RegEx(name)); + let processed = []; + let now = (new Date()).getTime(); + let changed = false; + for (let f in fileOptions[id]) { + if (!fileOptions[id].hasOwnProperty(f)) continue; + if (regEx.test(f) && checkFile(id, f, options, 2/*write*/)) { + changed = true; + // Check if file exists + if (fs.existsSync(objectsDir + id + '/' + f)) { + if (!fileOptions[id][f]) { + fileOptions[id][f] = {}; + fileOptions[id][f].createdAt = now; + } + + if (typeof fileOptions[id][f] !== 'object') { + fileOptions[id][f] = { + mimeType: fileOptions[id][f] + }; + } + + if (!fileOptions[id][f].mimeType) { + let pos = f.lastIndexOf('.'); + let ext = ''; + if (pos !== -1) ext = f.substring(pos); + let mimeType = that.getMimeType(ext); + fileOptions[id][f].binary = mimeType.isBinary; + fileOptions[id][f].mimeType = mimeType.mimeType; + } + + if (!fileOptions[id][f].acl) { + fileOptions[id][f].acl = { + owner: (defaultNewAcl && defaultNewAcl.owner) || 'system.user.admin', + ownerGroup: (defaultNewAcl && defaultNewAcl.ownerGroup) || 'system.group.administrator', + permissions: (defaultNewAcl && defaultNewAcl.file) || 0x644 // '0644' + }; + } + let fOp = fileOptions[id][f]; + fOp.modifiedAt = now; + + let stats = fs.statSync(objectsDir + id + '/' + f); + let parts = f.split('/'); + let fileName = parts.pop(); + processed.push({ + path: parts.join('/'), + file: fileName, + stats: stats, + isDir: stats.isDirectory(), + acl: fOp.acl || {}, + modifiedAt: fOp.modifiedAt, + createdAt: fOp.createdAt + }); + } else { + delete fileOptions[id][f]; + } + } + } + + // Store dir description + if (changed) fs.writeFileSync(objectsDir + id + '/_data.json', JSON.stringify(fileOptions[id])); + + if (typeof callback === 'function') callback(null, processed); + } catch (e) { + if (typeof callback === 'function') callback(e.message); + } + }; + + this.rm = function (id, name, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!options || !options.checked) { + checkFileRights(id, null, options, 0x2/*write*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + if (!options.acl.file['delete']) { + if (typeof callback === 'function') callback('permissionError'); + } else { + return that.rm(id, name, options, callback); + } + } + }); + return; + } + try { + if (!fileOptions[id]) { + if (fs.existsSync(objectsDir + id + '/_data.json')) { + fileOptions[id] = JSON.parse(fs.readFileSync(objectsDir + id + '/_data.json', 'utf8')); + } else { + fileOptions[id] = {}; + } + } + + let regEx = new RegExp(pattern2RegEx(name)); + let processed = []; + let changed = false; + let dirs = []; + for (let f in fileOptions[id]) { + if (!fileOptions[id].hasOwnProperty(f)) continue; + if (regEx.test(f) && checkFile(id, f, options, 2/*write*/)) { + let stat; + if (fileOptions[id][f]) { + changed = true; + delete fileOptions[id][f]; + } + if (files && files[id] && files[id][f]) { + delete files[id][f]; + } + if (fs.existsSync(objectsDir + id + '/' + f)) { + stat = fs.statSync(objectsDir + id + '/' + f); + + if (stat.isDirectory()) { + if (dirs.indexOf(f) === -1) dirs.push(f); + } else { + fs.unlinkSync(objectsDir + id + '/' + f); + } + } + let parts = f.split('/'); + let fileName = parts.pop(); + let path = parts.join('/'); + if (dirs.indexOf(path) === -1) dirs.push(path); + processed.push({ + path: path, + file: fileName, + isDir: stat && stat.isDirectory() + }); + } + } + + // try to delete directories + for (let d = 0; d < dirs.length; d++) { + try { + let _files = fs.readdirSync(objectsDir + id + '/' + dirs[d]); + + if (_files.length) { + console.log('Directory ' + id + '/' + dirs[d] + ' is not empty'); + } else { + fs.rmdirSync(objectsDir + id + '/' + dirs[d]); + } + } catch (e) { + console.error('Cannot delete ' + id + '/' + dirs[d] + ': ' + e); + } + } + + // Store dir description + if (changed) fs.writeFileSync(objectsDir + id + '/_data.json', JSON.stringify(fileOptions[id])); + + if (typeof callback === 'function') callback(null, processed); + } catch (e) { + if (typeof callback === 'function') callback(e.message); + } + }; + + this.mkdir = function (id, dirname, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (dirname[0] === '/') dirname = dirname.substring(1); + + if (!options || !options.checked) { + checkFileRights(id, dirname, options, 0x2/*write*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + if (!options.acl.file.write) { + if (typeof callback === 'function') callback('permissionError'); + } else { + return that.mkdir(id, dirname, options, callback); + } + } + }); + return; + } + try { + if (!fileOptions[id]) { + if (fs.existsSync(objectsDir + id + '/_data.json')) { + fileOptions[id] = JSON.parse(fs.readFileSync(objectsDir + id + '/_data.json', 'utf8')); + } else { + fileOptions[id] = {}; + } + } + if (!fs.existsSync(objectsDir + id + '/' + dirname)) { + fs.mkdirSync(objectsDir + id + '/' + dirname); + if (typeof callback === 'function') callback(); + } else { + if (typeof callback === 'function') callback('Yet exists'); + } + } catch (e) { + if (typeof callback === 'function') callback(e.message); + } + }; + + this.chownFile = function (id, name, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + options = options || {}; + if (typeof options !== 'object') { + options = {owner: options}; + } + + if (name[0] === '/') name = name.substring(1); + + if (!options.ownerGroup && options.group) options.ownerGroup = options.group; + if (!options.owner && options.user) options.owner = options.user; + + if (!options.owner) { + log.error(namespace + ' user is not defined'); + if (typeof callback === 'function') callback('invalid parameter'); + return; + } + + if (!options.ownerGroup) { + // get user group + this.getUserGroup(options.owner, function (user, groups /* , permissions */) { + if (!groups || !groups[0]) { + if (typeof callback === 'function') callback('user "' + options.owner + '" belongs to no group'); + return; + } else { + options.ownerGroup = groups[0]; + } + that.chownFile(id, name, options, callback); + }); + return; + } + + if (!options.checked) { + checkFileRights(id, null, options, 0x2/* write */, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + if (!options.acl.file.write) { + if (typeof callback === 'function') callback('permissionError'); + } else { + return that.chownFile(id, name, options, callback); + } + } + }); + return; + } + + try { + if (!fileOptions[id]) { + if (fs.existsSync(objectsDir + id + '/_data.json')) { + try { + fileOptions[id] = JSON.parse(fs.readFileSync(objectsDir + id + '/_data.json', 'binary')); + } catch (e) { + log.error(namespace + ' Cannot parse ' + objectsDir + id + '/_data.json: ' + e); + } + } else { + fileOptions[id] = {}; + } + } + + let regEx = new RegExp(pattern2RegEx(name)); + let processed = []; + let changed = false; + for (let f in fileOptions[id]) { + if (!fileOptions[id].hasOwnProperty(f)) continue; + if (regEx.test(f) && checkFile(id, f, options, 2/*write*/)) { + changed = true; + if (typeof fileOptions[id][f] !== 'object') { + fileOptions[id][f] = { + mimeType: fileOptions[id][f] + }; + } + + if (!fileOptions[id][f].acl) { + fileOptions[id][f].acl = { + owner: (defaultNewAcl && defaultNewAcl.owner) || 'system.user.admin', + ownerGroup: (defaultNewAcl && defaultNewAcl.ownerGroup) || 'system.group.administrator', + permissions: (defaultNewAcl && defaultNewAcl.file) || 0x644 // '0644' + }; + } + + fileOptions[id][f].acl.owner = options.owner; + fileOptions[id][f].acl.ownerGroup = options.ownerGroup; + + if (fs.existsSync(objectsDir + id + '/' + f)) { + let stats = fs.statSync(objectsDir + id + '/' + f); + let acl = fileOptions[id][f]; + let parts = f.split('/'); + let fileName = parts.pop(); + processed.push({ + path: parts.join('/'), + file: fileName, + stats: stats, + isDir: stats.isDirectory(), + acl: acl.acl || {}, + modifiedAt: fileOptions[id][f].modifiedAt, + createdAt: fileOptions[id][f].createdAt + }); + } + } + } + + // Store dir description + if (changed) fs.writeFileSync(objectsDir + id + '/_data.json', JSON.stringify(fileOptions[id])); + if (typeof callback === 'function') { + setImmediate(function () { + callback(null, processed, id); + }); + } + } catch (e) { + if (typeof callback === 'function') { + setImmediate(function () { + callback(e.message); + }); + } + } + }; + + this.chmodFile = function (id, name, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + options = options || {}; + + if (name[0] === '/') name = name.substring(1); + + if (typeof options !== 'object') { + options = {mode: options}; + } + + if (options.mode === undefined) { + log.error(namespace + ' mode is not defined'); + if (typeof callback === 'function') callback('invalid parameter'); + return; + } else if (typeof options.mode === 'string') { + options.mode = parseInt(options.mode, 16); + } + + if (!options.checked) { + checkFileRights(id, null, options, 0x2/*write*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + if (!options.acl.file.write) { + if (typeof callback === 'function') callback('permissionError'); + } else { + return that.chmodFile(id, name, options, callback); + } + } + }); + return; + } + + try { + if (!fileOptions[id]) { + if (fs.existsSync(objectsDir + id + '/_data.json')) { + try { + fileOptions[id] = JSON.parse(fs.readFileSync(objectsDir + id + '/_data.json', 'binary')); + } catch (e) { + log.error(namespace + ' Cannot parse ' + objectsDir + id + '/_data.json: ' + e); + } + } else { + fileOptions[id] = {}; + } + } + + let regEx = new RegExp(pattern2RegEx(name)); + let processed = []; + let changed = false; + for (let f in fileOptions[id]) { + if (!fileOptions[id].hasOwnProperty(f)) continue; + if (regEx.test(f) && checkFile(id, f, options, 2/*write*/)) { + changed = true; + if (typeof fileOptions[id][f] !== 'object') { + fileOptions[id][f] = { + mimeType: fileOptions[id][f] + }; + } + + if (!fileOptions[id][f].acl) { + fileOptions[id][f].acl = { + owner: (defaultNewAcl && defaultNewAcl.owner) || 'system.user.admin', + ownerGroup: (defaultNewAcl && defaultNewAcl.ownerGroup) || 'system.group.administrator', + permissions: (defaultNewAcl && defaultNewAcl.file) || 0x644 // '0644' + }; + } + + fileOptions[id][f].acl.permissions = options.mode; + if (fs.existsSync(objectsDir + id + '/' + f)) { + let stats = fs.statSync(objectsDir + id + '/' + f); + let acl = fileOptions[id][f]; + let parts = f.split('/'); + let fileName = parts.pop(); + processed.push({ + path: parts.join('/'), + file: fileName, + stats: stats, + isDir: stats.isDirectory(), + acl: acl.acl || {}, + modifiedAt: acl.modifiedAt, + createdAt: acl.createdAt + }); + } + } + } + + // Store dir description + if (changed) fs.writeFileSync(objectsDir + id + '/_data.json', JSON.stringify(fileOptions[id])); + if (typeof callback === 'function') { + setImmediate(function () { + callback(null, processed, id); + }); + } + } catch (e) { + if (typeof callback === 'function') { + setImmediate(function () { + callback(e.message); + }); + } + } + }; + + this.enableFileCache = function (enabled, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!options || !options.checked) { + checkObjectRights(null, options, 0x2/*write*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return this.enableFileCache(enabled, options, callback); + } + }.bind(this)); + return; + } + if (settings.connection.noFileCache !== enabled) { + settings.connection.noFileCache = !!enabled; + if (!settings.connection.noFileCache) { + // clear cache + files = {}; + } + } + if (typeof callback === 'function') { + setImmediate(function () { + callback(null, settings.connection.noFileCache); + }); + } + }; + + // -------------- OBJECT FUNCTIONS ------------------------------------------- + function checkObject(id, options, flag) { + // read rights of object + if (!objects[id] || !objects[id].common || !objects[id].acl || flag === 'list') { + return true; + } + + if (options.user !== 'system.user.admin' && + options.groups && options.groups.indexOf('system.group.administrator') === -1) { + if (objects[id].acl.owner !== options.user) { + // Check if the user is in the group + if (options.groups.indexOf(objects[id].acl.ownerGroup) !== -1) { + // Check group rights + if (!(objects[id].acl.object & (flag << 4))) { + return false; + } + } else { + // everybody + if (!(objects[id].acl.object & flag)) { + return false; + } + } + } else { + // Check group rights + if (!(objects[id].acl.object & (flag << 8))) { + return false; + } + } + } + return true; + } + + function checkObjectRights(id, options, flag, callback) { + options = options || {}; + if (!options.user) { + // Before files converted, lets think: if no options it is admin + options = { + user: 'system.user.admin', + params: options, + group: 'system.group.administrator', + acl: { + object: { + read: true, + write: true, + 'delete': true, + create: true, + list: true + }, + file: { + read: true, + write: true, + 'delete': true, + create: true, + list: true + }, + /* state: { + read: true, + write: true, + 'delete': true, + create: true, + list: true + },*/ + users: { + read: true, + write: true, + create: true, + 'delete': true, + list: true + } + } + }; + } + + if (options.checked) { + return callback(null, options); + } + + if (!options.acl) { + that.getUserGroup(options.user, function (user, groups, acl) { + options.acl = acl || {}; + options.groups = groups; + options.group = groups ? groups[0] : null; + checkObjectRights(id, options, flag, callback); + }); + return; + } + + // if user or group objects + if (regUser.test(id) || regGroup.test(id)) { + // If user may write + if (flag === 2 && !options.acl.users.write) {// write + return callback('permissionError', options); + } + + // If user may read + if (flag === 4 && !options.acl.users.read) {// read + return callback('permissionError', options); + } + + // If user may delete + if (flag === 'delete' && !options.acl.users.delete) {// delete + return callback('permissionError', options); + } + + // If user may list + if (flag === 'list' && !options.acl.users.list) {// list + return callback('permissionError', options); + } + + // If user may create + if (flag === 'create' && !options.acl.users.create) {// create + return callback('permissionError', options); + } + + if (flag === 'delete') flag = 2; // write + } + + // If user may write + if (flag === 2 && !options.acl.object.write) {// write + return callback('permissionError', options); + } + + // If user may read + if (flag === 4 && !options.acl.object.read) {// read + return callback('permissionError', options); + } + + // If user may delete + if (flag === 'delete' && !options.acl.object.delete) {// delete + return callback('permissionError', options); + } + + // If user may list + if (flag === 'list' && !options.acl.object.list) {// list + return callback('permissionError', options); + } + + if (flag === 'delete') flag = 2; // write + + options.checked = true; + + if (id && !checkObject(id, options, flag)) { + return callback('permissionError', options); + } + + return callback(null, options); + } + + function clone(obj) { + if (obj === null || obj === undefined || typeof obj !== 'object') { + return obj; + } + + let temp = obj.constructor(); // changed + + for (let key in obj) { + if (obj.hasOwnProperty(key)) { + temp[key] = clone(obj[key]); + } + } + return temp; + } + + function pattern2RegEx(pattern) { + if (pattern !== '*') { + if (pattern[0] === '*' && pattern[pattern.length - 1] !== '*') pattern += '$'; + if (pattern[0] !== '*' && pattern[pattern.length - 1] === '*') pattern = '^' + pattern; + } + pattern = pattern.replace(/\./g, '\\.'); + pattern = pattern.replace(/\*/g, '.*'); + return pattern; + } + + function deleteOldBackupFiles() { + // delete files only if settings.backupNumber is not 0 + let files = fs.readdirSync(backupDir); + files.sort(); + const limit = Date.now() - settings.backup.hours * 3600000; + + for (let f = files.length - 1; f >= 0; f--) { + if (!files[f].match(/_objects.json.gz$/)) { + files.splice(f, 1); + } + } + + while (files.length > settings.backup.files) { + let file = files.shift(); + // extract time + const ms = new Date(file.substring(0, 10) + ' ' + file.substring(11, 16).replace('-', ':') + ':00').getTime(); + if (limit > ms) { + try { + fs.unlink(backupDir + file); + } catch (e) { + log.error(`Cannot delete file "${backupDir + file}: ${JSON.stringify(e)}`); + } + } + } + } + + function getTimeStr(date) { + let dateObj = new Date(date); + + let text = dateObj.getFullYear().toString() + '-'; + let v = dateObj.getMonth() + 1; + if (v < 10) text += '0'; + text += v.toString() + '-'; + + v = dateObj.getDate(); + if (v < 10) text += '0'; + text += v.toString() + '_'; + + v = dateObj.getHours(); + if (v < 10) text += '0'; + text += v.toString() + '-'; + + v = dateObj.getMinutes(); + if (v < 10) text += '0'; + text += v.toString(); + + return text; + } + + function saveConfig() { + if (fs.existsSync(objectsName)) { + let old = fs.readFileSync(objectsName); + fs.writeFileSync(objectsName + '.bak', old); + } + try { + const actual = JSON.stringify(objects); + fs.writeFileSync(objectsName, actual); + + if (!settings.backup.disabled) { + // save files for the last x hours + const now = Date.now(); + + // makes backups only if settings.backupInterval is not 0 + if (settings.backup.period && (!lastSave || now - lastSave > settings.backup.period)) { + lastSave = now; + let backFileName = backupDir + getTimeStr(now) + '_objects.json.gz'; + + if (!fs.existsSync(backFileName)) { + zlib = zlib || require('zlib'); + let output = fs.createWriteStream(backFileName); + let compress = zlib.createGzip(); + /* The following line will pipe everything written into compress to the file stream */ + compress.pipe(output); + /* Since we're piped through the file stream, the following line will do: + 'Hello World!'->gzip compression->file which is the desired effect */ + compress.write(actual); + compress.end(); + + // analyse older files + deleteOldBackupFiles(); + } + } + } + } catch (e) { + log.error(namespace + ' Cannot save file ' + objectsName + ': ' + e); + } + if (configTimer) { + clearTimeout(configTimer); + configTimer = null; + } + } + + function subscribe(socket, type, pattern, options) { + socket._subscribe = socket._subscribe || {}; + let s = socket._subscribe[type] = socket._subscribe[type] || []; + for (let i = 0; i < s.length; i++) { + if (s[i].pattern === pattern) return; + } + + s.push({pattern: pattern, regex: new RegExp(pattern2RegEx(pattern)), options: options}); + } + + function unsubscribe(socket, type, pattern /*, options */) { + if (!socket._subscribe || !socket._subscribe[type]) return; + let s = socket._subscribe[type]; + for (let i = 0; i < s.length; i++) { + if (s[i].pattern === pattern) { + s.splice(i, 1); + return; + } + } + } + + function publish(socket, type, id, obj) { + if (!socket._subscribe || !socket._subscribe[type]) return; + let s = socket._subscribe[type]; + for (let i = 0; i < s.length; i++) { + if (s[i].regex.test(id)) { + socket.emit('message', s[i].pattern, id, obj); + return; + } + } + } + + function publishAll(type, id, obj) { + if (id === undefined) { + console.log('Problem'); + } + + let clients = server.io.sockets.connected; + + for (let i in clients) { + if (clients.hasOwnProperty(i)) { + publish(clients[i], type, id, obj); + } + } + + if (change && that._subscribe && that._subscribe[type]) { + for (let j = 0; j < that._subscribe[type].length; j++) { + if (that._subscribe[type][j].regex.test(id)) { + setImmediate(change, id, obj); + break; + } + } + } + } + + /* + function storeHistory(id, obj) { + let parts = id.split('.'); + let date = parts.pop(); + let file = historyName + date + '/' + parts.join('.') + '.json'; + + if (!fs.existsSync(historyName + date)) fs.mkdirSync(historyName + date); + + fs.writeFileSync(file, JSON.stringify(obj.common.data, null, 2)); + + delete obj.common.data; + } + + function today() { + let dateObj = new Date(); + + let text = dateObj.getFullYear().toString(); + let v = dateObj.getMonth() + 1; + if (v < 10) text += '0'; + text += v.toString(); + + v = dateObj.getDate(); + if (v < 10) text += '0'; + text += v.toString(); + + return text; + } + + function loadHistory(id, obj) { + let parts = id.split('.'); + let date = parts.pop(); + let file = historyName + date + '/' + parts.join('.') + '.json'; + + if (fs.existsSync(file)) { + if (!obj || !obj.common) { + obj = { + type: 'history', + common: { + source: id, + day: date, + data: [] + }, + native: {} + }; + } + + try { + obj.common.data = JSON.parse(fs.readFileSync(file)); + } catch (e) { + log.error(namespace + ' Cannot parse file ' + file + ': ' + e.message); + obj.common.data = []; + } + } + }*/ + + this.subscribeConfig = function (pattern, options, callback) { + if (!options || !options.checked) { + let socket = this; + checkObjectRights(null, options, 'list', function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return that.subscribeConfig.call(socket, pattern, options, callback); + } + }.bind(this)); + return; + } + + subscribe(this, 'objects', pattern, options); + if (typeof callback === 'function') { + setImmediate(function () { + callback(); + }) + } + }; + this.subscribe = this.subscribeConfig; + + this.unsubscribeConfig = function (pattern, options, callback) { + if (!options || !options.checked) { + let socket = this; + checkObjectRights(null, options, 'list', function (err, options) { + if (err) { + if (typeof callback === 'function')callback(err); + } else { + return that.unsubscribeConfig.call(socket, pattern, options, callback); + } + }.bind(this)); + return; + } + unsubscribe(this, 'objects', pattern); + if (typeof callback === 'function') { + setImmediate(function () { + callback(); + }); + } + }; + this.unsubscribe = this.unsubscribeConfig; + + this.chownObject = function (pattern, options, callback) { + options = options || {}; + if (typeof options !== 'object') { + options = {owner: options}; + } + + if (!options.ownerGroup && options.group) options.ownerGroup = options.group; + if (!options.owner && options.user) options.owner = options.user; + + if (!options.owner) { + log.error(namespace + ' user is not defined'); + if (typeof callback === 'function') callback('invalid parameter'); + return; + } + + if (!options.ownerGroup) { + // get user group + this.getUserGroup(options.owner, function (user, groups /* , permissions*/) { + if (!groups || !groups[0]) { + if (typeof callback === 'function') callback('user "' + options.owner + '" belongs to no group'); + return; + } else { + options.ownerGroup = groups[0]; + } + that.chownObject(pattern, options, callback); + }); + return; + } + + if (!options.checked) { + checkObjectRights(null, options, 0x2/*write*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + if (!options.acl.object || !options.acl.object.write) { + if (typeof callback === 'function') callback('permissionError'); + } else { + return that.chownObject(pattern, options, callback); + } + } + }); + return; + } + + this.getConfigKeys(pattern, options, function (err, keys) { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + let list = []; + for (let k = 0; k < keys.length; k++) { + if (!checkObject(keys[k], options, 2/*write*/)) continue; + if (!objects[keys[k]].acl) { + objects[keys[k]].acl = { + owner: (defaultNewAcl && defaultNewAcl.owner) || 'system.user.admin', + ownerGroup: (defaultNewAcl && defaultNewAcl.ownerGroup) || 'system.group.administrator', + object: (defaultNewAcl && defaultNewAcl.object) || 0x644 // '0644' + }; + if (objects[keys[k]].type === 'state') { + objects[keys[k]].acl.state = (defaultNewAcl && defaultNewAcl.state) || 0x644; // '0644' + } + } + objects[keys[k]].acl.owner = options.owner; + objects[keys[k]].acl.ownerGroup = options.ownerGroup; + list.push(Object.assign({}, objects[keys[k]])); + } + if (typeof callback === 'function') callback(null, list); + if (!configTimer) configTimer = setTimeout(saveConfig, 5000); + }); + }; + + this.chmodObject = function (pattern, options, callback) { + options = options || {}; + + if (typeof options !== 'object') { + options = {object: options}; + } + + if (options.mode && !options.object) options.object = options.mode; + + if (options.object === undefined) { + log.error(namespace + ' mode is not defined'); + if (typeof callback === 'function') callback('invalid parameter'); + return; + } else if (typeof options.mode === 'string') { + options.mode = parseInt(options.mode, 16); + } + + if (!options.checked) { + checkObjectRights(null, options, 0x2/*write*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + if (!options.acl.file.write) { + if (typeof callback === 'function') callback('permissionError'); + } else { + return that.chmodObject(pattern, options, callback); + } + } + }); + return; + } + this.getConfigKeys(pattern, options, function (err, keys) { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + let list = []; + for (let k = 0; k < keys.length; k++) { + if (!checkObject(keys[k], options, 2/*write*/)) continue; + if (!objects[keys[k]].acl) { + objects[keys[k]].acl = { + owner: (defaultNewAcl && defaultNewAcl.owner) || 'system.user.admin', + ownerGroup: (defaultNewAcl && defaultNewAcl.ownerGroup) || 'system.group.administrator', + object: (defaultNewAcl && defaultNewAcl.object) || 0x644 // '0644' + }; + if (objects[keys[k]].type === 'state') { + objects[keys[k]].acl.state = (defaultNewAcl && defaultNewAcl.state) || 0x644; // '0644' + } + } + if (options.object !== undefined) objects[keys[k]].acl.object = options.object; + if (options.state !== undefined) objects[keys[k]].acl.state = options.state; + list.push(Object.assign({}, objects[keys[k]])); + } + if (typeof callback === 'function') callback(null, list); + if (!configTimer) configTimer = setTimeout(saveConfig, 5000); + }); + }; + + this.getObject = function (id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!options || !options.checked) { + checkObjectRights(id, options, 0x4/*read*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return this.getObject(id, options, callback); + } + }.bind(this)); + return; + } + + if (typeof callback === 'function') { + let obj = clone(objects[id]); + // Read history from file + /*if (regHistory.test(id)) { + if (!obj) obj = {}; + if (!obj.common || !obj.common.data) loadHistory(id, obj); + + if (obj.common && (!objects[id] || !objects[id].common)) objects[id] = obj; + + // store the history for today in cache + if (obj.common && obj.common.day) { + if (today() === obj.common.day) { + objects[id].common.data = obj.common.data; + } else if (objects[id].common.data) { + delete objects[id].common.data; + } + } + }*/ + + if (typeof callback === 'function') { + setImmediate(callback, null, obj); + } + } + }; + + this.getObjectAsync = function (id, options) { + return new Promise((resolve, reject) => { + this.getObject(id, options, (err, obj) => { + if (err) { + reject(err); + } else { + resolve(obj); + } + }); + }); + }; + + this.getKeys = function (pattern, options, callback, dontModify) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!options || !options.checked) { + checkObjectRights(null, options, 'list', function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return this.getConfigKeys(pattern, options, callback, dontModify); + } + }.bind(this)); + return; + } + + let r = new RegExp(pattern2RegEx(pattern)); + let result = []; + for (let id in objects) { + if (!objects.hasOwnProperty(id)) continue; + if (r.test(id) && checkObject(id, options, 'list')) { + result.push(id); + } + } + result.sort(); + if (typeof callback === 'function') { + setImmediate(function () { + callback(null, result); + }); + } + }; + this.getConfigKeys = this.getKeys; + + this.getObjects = function (keys, options, callback, dontModify) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!options || !options.checked) { + checkObjectRights(null, options, 0x4/*read*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return this.getObjects(keys, options, callback, dontModify); + } + }.bind(this)); + return; + } + + if (!keys) { + if (typeof callback === 'function') callback('no keys', null); + return; + } + if (!keys.length) { + if (typeof callback === 'function') callback(null, []); + return; + } + let result = []; + for (let i = 0; i < keys.length; i++) { + if (checkObject(keys[i], options, 4/*read*/)) { + result.push(clone(objects[keys[i]])); + } else { + result.push({error: 'permissionError'}); + } + } + if (typeof callback === 'function') { + setImmediate(function () { + callback(null, result); + }); + } + }; + + this.getObjectsByPattern = (pattern, options, callback) => { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!options || !options.checked) { + checkObjectRights(null, options, 0x4/*read*/, (err, options) => { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return this.getObjectsByPattern(pattern, options, callback); + } + }); + return; + } + + let r = new RegExp(pattern2RegEx(pattern)); + let keys = []; + for (let id in objects) { + if (!objects.hasOwnProperty(id)) continue; + if (r.test(id) && checkObject(id, options, 0x4/*read*/)) { + keys.push(id); + } + } + keys.sort(); + let result = []; + for (let i = 0; i < keys.length; i++) { + result.push(JSON.parse(JSON.stringify(objects[keys[i]]))); + } + if (typeof callback === 'function') { + setImmediate(function () { + callback(null, result); + }); + } + }; + + this.getObjectsByPatternAsync = (pattern, options) => { + return new Promise((resolve, reject) => { + this.getObjectsByPattern(pattern, options, (err, objs) => { + if (err) { + reject(err); + } else { + resolve(objs); + } + }) + }); + }; + + /** + * set anew or update object + * + * This function writes the object into DB + * + * @alias setObject + * @memberof objectsInMemServer + * @param {string} id ID of the object + * @param {object} obj + * @param {object} options options for access control are optional + * @param {function} callback return function + */ + this.setObject = function (id, obj, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!options || !options.checked) { + checkObjectRights(id, options, 0x2/*write*/, function (err, options) { + if (err) { + if (typeof callback === 'function') { + callback(err); + } + } else { + return this.setObject(id, obj, options, callback); + } + }.bind(this)); + return; + } + + if (!id || id.indexOf('*') !== -1) { + if (typeof callback === 'function') { + callback(`Invalid ID: ${id}`); + } + return; + } + + if (!obj) { + log.error(namespace + ' setObject: Argument object is null'); + if (typeof callback === 'function') { + callback('obj is null'); + } + return; + } + + obj._id = id; + + if (id === 'system.config' && obj && obj.common && objects[id] && objects[id].common && JSON.stringify(obj.common.defaultNewAcl) !== JSON.stringify(objects[id].common.defaultNewAcl)) { + objects[id] = obj; + return setDefaultAcl(function () { + that.setObject(id, obj, options, callback); + }); + } + + if (!tools.checkNonEditable(objects[id], obj)) { + if (typeof callback === 'function') { + callback('Invalid password for update of vendor information'); + } + return; + } + + // do not delete common settings, like "history" or "mobile". It can be erased only with "null" + if (objects[id] && objects[id].common) { + for (let i = 0; i < preserveSettings.length; i++) { + // remove settings if desired + if (obj.common && obj.common[preserveSettings[i]] === null) { + delete obj.common[preserveSettings[i]]; + continue; + } + + if (objects[id].common[preserveSettings[i]] !== undefined && (!obj.common || obj.common[preserveSettings[i]] === undefined)) { + if (!obj.common) obj.common = {}; + obj.common[preserveSettings[i]] = objects[id].common[preserveSettings[i]]; + } + } + } + + if (objects[id] && objects[id].acl && !obj.acl) { + obj.acl = objects[id].acl; + } + + // add user default rights + if (defaultNewAcl && !obj.acl) { + obj.acl = Object.assign({}, defaultNewAcl); + delete obj.acl.file; + if (obj.type !== 'state') { + delete obj.acl.state; + } + if (options.owner) { + obj.acl.owner = options.owner; + + if (!options.ownerGroup) { + obj.acl.ownerGroup = null; + this.getUserGroup(options.owner, function (user, groups /* , permissions */) { + if (!groups || !groups[0]) { + options.ownerGroup = (defaultNewAcl && defaultNewAcl.ownerGroup) || 'system.group.administrator'; + } else { + options.ownerGroup = groups[0]; + } + that.setObject(id, obj, options, callback); + }); + return; + } + } + } + if (defaultNewAcl && obj.acl && !obj.acl.ownerGroup && options.ownerGroup) { + obj.acl.ownerGroup = options.ownerGroup; + } + + objects[id] = JSON.parse(JSON.stringify(obj)); + publishAll('objects', id, obj); + if (typeof callback === 'function') { + setImmediate(function () { + callback(null, {id: id}); + }); + } + if (!configTimer) { + configTimer = setTimeout(saveConfig, 5000); + } + }; + + this.setObjectAsync = (id, obj, options) => { + return new Promise((resolve, reject) => { + this.setObject(id, obj, options, err => { + if (err) { + reject(err); + } else { + resolve(); + } + }) + }); + }; + + this.delObject = function (id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!options || !options.checked) { + checkObjectRights(id, options, 'delete', function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return this.delObject(id, options, callback); + } + }.bind(this)); + return; + } + + if (objects[id]) { + if (objects[id].common && objects[id].common.dontDelete) { + if (typeof callback === 'function') { + setImmediate(function () { + callback('Object is marked as non deletable'); + }); + } + return; + } + + delete objects[id]; + publishAll('objects', id, null); + if (typeof callback === 'function') { + setImmediate(function () { + callback(null); + }); + } + if (!configTimer) { + configTimer = setTimeout(saveConfig, 5000); + } + } else { + if (typeof callback === 'function') { + setImmediate(function () { + callback('Not exists'); + }); + } + } + }; + + this.delObjectAsync = function (id, options) { + return new Promise((resolve, reject) => { + this.delObject(id, options, err => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + }; + + this._applyView = function (func, params, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!options || !options.checked) { + checkObjectRights(null, options, 'list', function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return this._applyView(func, params, options, callback); + } + }.bind(this)); + return; + } + + let result = { + rows: [] + }; + + function _emit_(id, obj) { + result.rows.push({id: id, value: obj}); + } + + let f = eval('(' + func.map.replace(/emit/g, '_emit_') + ')'); + + for (let id in objects) { + if (!objects.hasOwnProperty(id)) continue; + if (params) { + if (params.startkey && id < params.startkey) continue; + if (params.endkey && id > params.endkey) continue; + } + if (objects[id]) { + try { + f(objects[id]); + } catch (e) { + console.log('Cannot execute map: ' + e.message); + + } + } + } + // Calculate max + if (func.reduce === '_stats') { + let max = null; + for (let i = 0; i < result.rows.length; i++) { + if (max === null || result.rows[i].value > max) { + max = result.rows[i].value; + } + } + if (max !== null) { + result.rows = [{id: '_stats', value: {max: max}}]; + } else { + result.rows = []; + } + } + + if (typeof callback === 'function') callback(null, result); + }; + + this.getObjectView = function (design, search, params, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!options || !options.checked) { + checkObjectRights(null, options, 'list', function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return this.getObjectView(design, search, params, options, callback); + } + }.bind(this)); + return; + } + + if (objects['_design/' + design]) { + if (objects['_design/' + design].views && objects['_design/' + design].views[search]) { + that._applyView(objects['_design/' + design].views[search], params, options, callback); + } else { + console.log('Cannot find search "' + search + '" in "' + design + '"'); + if (typeof callback === 'function') callback({status_code: 404, status_text: 'Cannot find search "' + search + '" in "' + design + '"'}); + } + } else { + console.log('Cannot find view "' + design + '"'); + if (typeof callback === 'function') callback({status_code: 404, status_text: 'Cannot find view "' + design + '"'}); + } + }; + + this.getObjectList = function (params, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!options || !options.checked) { + checkObjectRights(null, options, 'list', function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return this.getObjectList(params, options, callback); + } + }.bind(this)); + return; + } + // return rows with id and doc + let result = { + rows: [] + }; + + for (let id in objects) { + if (!checkObject(id, options, 'read')) continue; + if (params) { + if (params.startkey && id < params.startkey) continue; + if (params.endkey && id > params.endkey) continue; + if (!params.include_docs && id[0] === '_') continue; + } + let obj = {id: id, value: clone(objects[id])}; + obj.doc = obj.value; + + if (options.sorted) { + // insert sorted + if (!result.rows.length) { + result.rows.push(obj); + } else if (obj.id <= result.rows[0].id) { + result.rows.unshift(obj); + } else if (obj.id >= result.rows[result.rows.length - 1].id) { + result.rows.push(obj); + } else { + for (let t = 1; t < result.rows.length; t++) { + if (obj.id > result.rows[t - 1].id && obj.id <= result.rows[t].id) { + result.rows.splice(t, 0, obj); + break; + } + } + } + } else { + result.rows.push(obj); + } + } + if (typeof callback === 'function') callback(null, result); + }; + + this.getObjectListAsync = (params, options) => { + return new Promise((resolve, reject) => { + this.getObjectList(params, options, (err, arr) => { + if (err) { + reject(err); + } else { + resolve(arr); + } + }) + }); + }; + + this.extendObject = function (id, obj, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!options || !options.checked) { + checkObjectRights(id, options, 2/*write*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return this.extendObject(id, obj, options, callback); + } + }.bind(this)); + return; + } + + if (!id || id.indexOf('*') !== -1) { + if (typeof callback === 'function') { + callback(`Invalid ID: ${id}`); + } + return; + } + + if (id === 'system.config' && obj && obj.common && objects[id] && objects[id].common && JSON.stringify(obj.common.defaultNewAcl) !== JSON.stringify(objects[id].common.defaultNewAcl)) { + objects[id] = obj; + return setDefaultAcl(function () { + that.extendObject(id, obj, options, callback); + }); + } + + let oldObj; + if (objects[id] && objects[id].nonEdit) { + oldObj = Object.assign({}, objects[id]) + } + + objects[id] = objects[id] || {}; + objects[id] = extend(true, objects[id], obj); + objects[id]._id = id; + + // add user default rights + if (defaultNewAcl && !objects[id].acl) { + objects[id].acl = Object.assign({}, defaultNewAcl); + delete objects[id].acl.file; + if (objects[id].type !== 'state') { + delete objects[id].acl.state; + } + + if (options.owner) { + objects[id].acl.owner = options.owner; + + if (!options.ownerGroup) { + objects[id].acl.ownerGroup = null; + this.getUserGroup(options.owner, function (user, groups /*, permissions */) { + if (!groups || !groups[0]) { + options.ownerGroup = (defaultNewAcl && defaultNewAcl.ownerGroup) || 'system.group.administrator'; + } else { + options.ownerGroup = groups[0]; + } + that.extendObject(id, obj, options, callback); + }); + return; + } + } + } + if (defaultNewAcl && options.ownerGroup && objects[id].acl && !objects[id].acl.ownerGroup) { + objects[id].acl.ownerGroup = options.ownerGroup; + } + + if (oldObj && !tools.checkNonEditable(oldObj, objects[id])) { + if (typeof callback === 'function') { + callback('Invalid password for update of vendor information'); + } + return; + } + + publishAll('objects', id, objects[id]); + + if (typeof callback === 'function') { + setImmediate(function () { + callback(null, {id: id, value: objects[id]}, id); + }); + } + + if (!configTimer) configTimer = setTimeout(saveConfig, 5000); + }; + + this.extendObjectAsync = function (id, obj, options) { + return new Promise((resolve, reject) => { + this.extendObject(id, obj, options, err => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + }; + + this.setConfig = this.setObject; + + this.delConfig = this.delObject; + + this.getConfig = this.getObject; + + this.getConfigs = this.getObjects; + + this.findObject = function (idOrName, type, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!options || !options.checked) { + checkObjectRights(null, options, 'list', function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return this.findObject(idOrName, type, options, callback); + } + }.bind(this)); + return; + } + + if (!objects) { + if (typeof callback === 'function') callback('Not implemented'); + return; + } + + // Assume it is ID + if (objects[idOrName] && (!type || (objects[idOrName].common && objects[idOrName].common.type === type))) { + if (typeof callback === 'function') callback(null, idOrName, objects[idOrName].common.name); + } else { + // Assume it is name + for (let id in objects) { + if (!checkObject(id, options, 4/*read*/)) continue; + if (objects[id].common && + objects[id].common.name === idOrName && + (!type || (objects[id].common && objects[id].common.type === type))) { + if (typeof callback === 'function') callback(null, id, idOrName); + return; + } + } + if (typeof callback === 'function') callback(null, null, idOrName); + } + }; + + // can be called only from js-controller + this.addPreserveSettings = function (settings) { + if (typeof settings !== 'object') settings = [settings]; + + for (let s = 0; s < settings.length; s++) { + if (preserveSettings.indexOf(settings[s]) === -1) preserveSettings.push(settings[s]); + } + }; + + this.destroyDB = function (options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + options = options || {}; + + if (!options.checked) { + checkObjectRights(null, options, 0x2/*write*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + if (!options.acl.file.write) { + if (typeof callback === 'function') callback('permissionError'); + } else { + return that.destroyDB(options, callback); + } + } + }); + return; + } + + if (fs.existsSync(objectsName)) { + fs.unlinkSync(objectsName); + } + if (typeof callback === 'function') callback(); + }; + + function socketEvents(socket /*, user*/) { + socket.on('writeFile', function (id, name, data, options, callback) { + that.writeFile.apply(that, arguments); + }); + + socket.on('destroy', function (callback) { + // client may not close DB + if (typeof callback === 'function') callback(); + //that.destroy.apply(that, arguments); + }); + + socket.on('enableFileCache', function (enabled, options, callback) { + that.enableFileCache.apply(that, arguments); + }); + + socket.on('readFile', function (id, name, params, options, callback) { + that.readFile.apply(that, arguments); + }); + + socket.on('readDir', function (id, path, options, callback) { + that.readDir.apply(that, arguments); + }); + + socket.on('unlink', function (id, name, options, callback) { + that.unlink.apply(that, arguments); + }); + + socket.on('rename', function (id, oldName, newName, options, callback) { + that.rename.apply(that, arguments); + }); + + socket.on('mkdir', function (id, dirname, callback) { + that.mkdir.apply(that, arguments); + }); + + socket.on('chownFile', function (id, path, options, callback) { + that.chownFile.apply(that, arguments); + }); + + socket.on('chmodFile', function (id, path, options, callback) { + that.chmodFile.apply(that, arguments); + }); + + socket.on('rm', function (id, path, options, callback) { + that.rm.apply(that, arguments); + }); + + socket.on('touch', function (id, path, options, callback) { + that.touch.apply(that, arguments); + }); + + socket.on('subscribe', function (pattern, options) { + // it must be "this" and not "that" + that.subscribe.apply(this, arguments); + }); + + socket.on('unsubscribe', function (pattern, options) { + // it must be "this" and not "that" + that.unsubscribe.apply(this, arguments); + }); + + socket.on('getObjectView', function (design, search, params, options, callback) { + that.getObjectView.apply(that, arguments); + }); + + socket.on('getObjectList', function (params, options, callback) { + that.getObjectList.apply(that, arguments); + }); + + socket.on('extendObject', function (id, obj, options, callback) { + that.extendObject.apply(that, arguments); + }); + + socket.on('setObject', function (id, obj, options, callback) { + that.setObject.apply(that, arguments); + }); + + socket.on('delObject', function (id, options, callback) { + that.delObject.apply(that, arguments); + }); + + socket.on('findObject', function (idOrName, type, options, callback) { + that.findObject.apply(that, arguments); + }); + + socket.on('destroyDB', function (options, callback) { + that.destroyDB.apply(that, arguments); + }); + + socket.on('getObject', function (id, options, callback) { + that.getObject.apply(that, arguments); + }); + + socket.on('chownObject', function (pattern, options, callback) { + that.chownObject.apply(that, arguments); + }); + + socket.on('chmodObject', function (pattern, options, callback) { + that.chmodObject.apply(that, arguments); + }); + + socket.on('error', function (err) { + log.error(namespace + ' ' + err); + }); + } + + function initSocket(socket) { + if (settings.auth) { + let user = null; + socketEvents(socket /*, user*/); + } else { + socketEvents(socket); + } + } + + function _initWebServer(settings, server) { + + try { + if (settings.secure) { + if (!settings.certificates) return; + server.server = require('https').createServer(settings.certificates, function (req, res) { + res.writeHead(501); + res.end('Not Implemented'); + }); + } else { + server.server = require('http').createServer(function (req, res) { + res.writeHead(501); + res.end('Not Implemented'); + }); + } + server.server.listen(settings.port || 9001, (settings.host && settings.host !== 'localhost') ? settings.host : ((settings.host === 'localhost') ? '127.0.0.1' : undefined)); + } catch (e) { + log.error(namespace + ' Cannot start inMem-objects on port ' + (settings.port || 9001) + ': ' + e.message); + console.log('Cannot start inMem-objects on port ' + (settings.port || 9001) + ': ' + e.message); + process.exit(24); + } + + server.io = socketio.listen(server.server); + + if (settings.auth) { + server.io.use(function (socket, next) { + if (!socket.request._query.user || !socket.request._query.pass) { + console.log('No password or username!'); + next(new Error('Authentication error')); + } else { + next(new Error('Authentication error')); + // TODO + /*adapter.checkPassword(socket.request._query.user, socket.request._query.pass, function (res) { + if (res) { + console.log("Logged in: " + socket.request._query.user + ', ' + socket.request._query.pass); + return next(); + } else { + console.log("Invalid password or user name: " + socket.request._query.user + ', ' + socket.request._query.pass); + next(new Error('Invalid password or user name')); + } + });*/ + } + }); + } + server.io.set('origins', '*:*'); + server.io.on('connection', initSocket); + + log.info(namespace + ' ' + (settings.secure ? 'Secure ' : '') + ' inMem-objects listening on port ' + (settings.port || 9001)); + } + + // Destructor of the class. Called by shutting down. + this.destroy = function () { + if (configTimer) saveConfig(); + + saveFileSettings(true); + + if (server.io) { + if (server.io.sockets && server.io.sockets.connected) { + for (let s in server.io.sockets.connected) { + if (server.io.sockets.connected.hasOwnProperty(s)) { + delete server.io.sockets.connected[s]; + } + } + } + try { + server.io.close(); + } catch (e) { + console.log(e.message); + } + } + }; + + let __construct = (function () { + if (fs.existsSync(objectsName)) { + try { + objects = JSON.parse(fs.readFileSync(objectsName).toString()); + } catch (e) { + log.error(namespace + ' Cannot parse ' + objectsName + ': ' + e); + if (fs.existsSync(objectsName + '.bak')) { + try { + objects = JSON.parse(fs.readFileSync(objectsName + '.bak').toString()); + } catch (e) { + log.error(namespace + ' Cannot parse ' + objectsName + '.bak: ' + e); + objects = {}; + } + } else { + objects = {}; + } + } + } else if (fs.existsSync(objectsName + '.bak')) { + try { + objects = JSON.parse(fs.readFileSync(objectsName + '.bak').toString()); + } catch (e) { + log.error(namespace + ' Cannot parse ' + objectsName + '.bak: ' + e); + objects = {}; + } + } else { + objects = {}; + } + + // init default new acl + if (objects['system.config'] && objects['system.config'].common && objects['system.config'].common.defaultNewAcl) { + defaultNewAcl = Object.assign({}, objects['system.config'].common.defaultNewAcl); + } + + // Create history directory + // (!fs.existsSync(historyName)) fs.mkdirSync(historyName); + + change = settings.change || function (id /*, obj */) { + log.silly(namespace + ' objects change: ' + id + ' ' + JSON.stringify(change)); + }; + + // Check if directory exists + objectsName = objectsName.replace(/\\/g, '/'); + /** @type {string|string[]} */ + let parts = objectsName.split('/'); + parts.pop(); + parts = parts.join('/'); + if (!fs.existsSync(parts)) fs.mkdirSync(parts); + + _initWebServer(settings.connection, server); + + if (settings.connected) { + setImmediate(function () { + settings.connected('InMemoryDB'); + }); + } + })(); +} + +module.exports = ObjectsInMemServer; diff --git a/lib/objects/objectsInRedis.js b/lib/objects/objectsInRedis.js new file mode 100644 index 0000000..4f15a7f --- /dev/null +++ b/lib/objects/objectsInRedis.js @@ -0,0 +1,1928 @@ +/** + * Object DB in REDIS - Client + * + * Copyright 2014-2018 bluefox + * + * MIT License + * + */ +/* jshint -W097 */ +/* jshint strict:false */ +/* jslint node: true */ +/* jshint -W061 */ +'use strict'; + +const extend = require('node.extend'); +const tools = require(__dirname + '/../tools'); +const redis = require('redis'); +const stream = require('stream'); +const util = require('util'); +const Writable = stream.Writable; +let memStore = {}; + +/* Writable memory stream */ +function WMStrm(key, options) { + // allow use without new operator + if (!(this instanceof WMStrm)) return new WMStrm(key, options); + + Writable.call(this, options); // init super + this.key = key; // save key + memStore[key] = new Buffer(''); // empty +} +util.inherits(WMStrm, Writable); + +WMStrm.prototype._write = function (chunk, enc, cb) { + if (chunk) { + // our memory store stores things in buffers + let buffer = (Buffer.isBuffer(chunk)) ? + chunk : // already is Buffer use it + new Buffer(chunk, enc); // string, convert + + // concatenate to the buffer already there + if (!memStore[this.key]) { + memStore[this.key] = new Buffer(''); + console.log('memstore for ' + this.key + ' is null'); + } + memStore[this.key] = Buffer.concat([memStore[this.key], buffer]); + } + if (!cb) throw 'Callback is empty'; + cb(); +}; + +function ObjectsInRedis(settings) { + settings = settings || {}; + const redisNamespace = (settings.redisNamespace || 'config') + '.'; + const ioRegExp = new RegExp('^' + redisNamespace); + const onChange = settings.change; // on change handler + const redisNamespaceL = redisNamespace.length; + + let client; + let sub; + let that = this; + let users = {}; + let groups = {}; + let fileOptions = {}; + let preserveSettings = []; + let regUser = /^system\.user/; + let regGroup = /^system\.group/; + let defaultAcl = { + groups: [], + acl: { + file: { + list: false, + read: false, + write: false, + create: false, + 'delete': false + }, + object: { + list: false, + read: false, + write: false, + 'delete': false + } + } + }; + let defaultNewAcl = settings.defaultNewAcl || null; + + let log = settings.logger; + if (!log) { + log = { + silly: function (msg) {/*console.log(msg);*/}, + debug: function (msg) {/*console.log(msg);*/}, + info: function (msg) {/*console.log(msg);*/}, + warn: function (msg) { + console.log(msg); + }, + error: function (msg) { + console.log(msg); + } + }; + } else if (!log.silly) { + log.silly = log.debug; + } + + // -------------- FILE FUNCTIONS ------------------------------------------- + function checkFile(id, name, options, flag) { + // read file settings from redis + + if (typeof fileOptions[id][name].acl !== 'object') { + fileOptions[id][name] = { + mimeType: fileOptions[id][name], + acl: { + owner: (defaultNewAcl && defaultNewAcl.owner) || 'system.user.admin', + ownerGroup: (defaultNewAcl && defaultNewAcl.ownerGroup) || 'system.group.administrator', + permissions: (defaultNewAcl && defaultNewAcl.file) || 0x644 // '0644' + } + }; + } + + // Set default owner group + fileOptions[id][name].acl.ownerGroup = fileOptions[id][name].acl.ownerGroup || (defaultNewAcl && defaultNewAcl.ownerGroup) || 'system.group.administrator'; + fileOptions[id][name].acl.owner = fileOptions[id][name].acl.owner || (defaultNewAcl && defaultNewAcl.owner) || 'system.user.admin'; + fileOptions[id][name].acl.permissions = fileOptions[id][name].acl.permissions || (defaultNewAcl && defaultNewAcl.file) || 0x644; // '0644' + + if (options.user !== 'system.user.admin' && + options.groups.indexOf('system.group.administrator') === -1 && + fileOptions[id][name].acl) { + if (fileOptions[id][name].acl.owner !== options.user) { + // Check if the user is in the group + if (options.groups.indexOf(fileOptions[id][name].acl.ownerGroup) !== -1) { + // Check group rights + if (!(fileOptions[id][name].acl.permissions & (flag << 4))) { + return false; + } + } else { + // everybody + if (!(fileOptions[id][name].acl.permissions & flag)) { + return false; + } + } + } else { + // Check user rights + if (!(fileOptions[id][name].acl.permissions & (flag << 8))) { + return false; + } + } + } + return true; + } + + function checkFileRights(id, name, options, flag, callback) { + options = options || {}; + if (!options.user) { + // Before files converted, lets think: if no options it is admin + options = { + user: 'system.user.admin', + params: options, + group: 'system.group.administrator' + }; + } + + if (options.checked) { + return callback(null, options); + } + + if (!options.acl) { + that.getUserGroup(options.user, function (user, groups, acl) { + options.acl = acl || {}; + options.groups = groups; + options.group = groups ? groups[0] : null; + checkFileRights(id, name, options, flag, callback); + }); + return; + } + // If user may write + if (flag === 2 && !options.acl.file.write) {// write + return callback('permissionError', options); + } + // If user may read + if (flag === 4 && !options.acl.file.read) {// read + return callback('permissionError', options); + } + + // read rights of file + if (!fileOptions[id]) { + // Redis actually does not support the file rights for users + /*if (fs.existsSync(objectsDir + id + '/_data.json')) { + try { + fileOptions[id] = JSON.parse(fs.readFileSync(objectsDir + id + '/_data.json', 'utf8')); + } catch (e) { + log.error('Cannot parse ' + objectsDir + id + '/_data.json: ' + e); + } + } else*/ { + fileOptions[id] = {}; + } + } + + options.checked = true; + if (!name || !fileOptions[id] || !fileOptions[id][name]) { + return callback(null, options); + } + if (checkFile(id, name, options,flag)) { + return callback(null, options); + } else { + return callback('permissionError', options); + } + + /*if (typeof fileOptions[id][name].acl != 'object') { + fileOptions[id][name] = { + mimeType: fileOptions[id][name], + acl: { + owner: 'system.user.admin', + permissions: 0x644, + ownerGroup: 'system.group.administrator' + } + }; + } + // Set default onwer group + fileOptions[id][name].acl.ownerGroup = fileOptions[id][name].acl.ownerGroup || 'system.group.administrator'; + + if (options.user != 'system.user.admin' && + options.groups.indexOf('system.group.administrator') == -1 && + fileOptions[id][name].acl) { + if (fileOptions[id][name].acl.owner != options.user) { + // Check if the user is in the group + if (options.groups.indexOf(fileOptions[id][name].acl.ownerGroup) != -1) { + // Check group rights + if (!(fileOptions[id][name].acl.permissions & (flag << 4))) { + return callback('permissionError', options); + } + } else { + // everybody + if (!(fileOptions[id][name].acl.permissions & flag)) { + return callback('permissionError', options); + } + } + } else { + // Check user rights + if (!(fileOptions[id][name].acl.permissions & (flag << 8))) { + return callback('permissionError', options); + } + } + } + return callback(null, options);*/ + } + + function setDefaultAcl(callback) { + try { + defaultNewAcl = Object.assign({}, objects['system.config'].common.defaultNewAcl); + } catch (e) { + defaultNewAcl = { + owner: 'system.user.admin', + ownerGroup: 'system.group.administrator', + object: 0x664, + state: 0x664, + file: 0x664 + }; + objects['system.config'].common.defaultNewAcl = Object.assign({}, defaultNewAcl); + } + + let count = 0; + // Set all objects without ACL to this one + // todo + /* + for (let id in objects) { + if (objects.hasOwnProperty(id) && objects[id] && !objects[id].acl) { + objects[id].acl = Object.assign({}, defaultNewAcl); + delete objects[id].acl.file; + if (objects[id].type !== 'state') { + delete objects[id].acl.state; + } + + count++; + } + } + */ + if (typeof callback === 'function') callback(null, count); + } + + this.getUserGroup = function (user, callback) { + if (!user || typeof user !== 'string' || !user.match(/^system\.user\./)) { + console.log('invalid user name: ' + user); + user = JSON.stringify(user); + return callback.call(that, user, [], Object.assign({}, defaultAcl.acl)); + } + if (users[user]) { + return callback.call(that, user, users[user].groups, users[user].acl); + } + + // Read all groups + this.getObjectList({startkey: 'system.group.', endkey: 'system.group.\u9999'}, {checked: true}, function (err, arr) { + if (err) log.error(err); + groups = []; + if (arr) { + // Read all groups + for (let g = 0; g < arr.rows.length; g++) { + groups[g] = arr.rows[g].value; + if (groups[g]._id === 'system.group.administrator') { + groups[g].common.acl = { + file: { + list: true, + read: true, + write: true, + create: true, + 'delete': true + }, + object: { + list: true, + read: true, + write: true, + create: true, + 'delete': true + }, + users: { + list: true, + read: true, + write: true, + create: true, + 'delete': true + } + }; + } + } + } + + that.getObjectList({startkey: 'system.user.', endkey: 'system.user.\u9999'}, {checked: true}, function (err, arr) { + if (err) log.error(err); + users = {}; + + if (arr) { + for (let i = 0; i < arr.rows.length; i++) { + users[arr.rows[i].value._id] = Object.assign({}, defaultAcl); + if (arr.rows[i].value._id === 'system.user.admin') { + users['system.user.admin'].acl.file = { + list: true, + read: true, + write: true, + create: true, + 'delete': true + }; + users['system.user.admin'].acl.object = { + create: true, + list: true, + read: true, + write: true, + 'delete': true + }; + users['system.user.admin'].acl.users = { + create: true, + list: true, + read: true, + write: true, + 'delete': true + }; + } + } + } + + for (let g = 0; g < groups.length; g++) { + if (!groups[g].common.members) continue; + for (let m = 0; m < groups[g].common.members.length; m++) { + let u = groups[g].common.members[m]; + users[u].groups.push(groups[g]._id); + + if (groups[g].common.acl && groups[g].common.acl.file) { + if (!users[u].acl || !users[u].acl.file) { + users[u].acl = users[u].acl || {}; + users[u].acl.file = users[u].acl.file || {}; + + users[u].acl.file.create = groups[g].common.acl.file.create; + users[u].acl.file.read = groups[g].common.acl.file.read; + users[u].acl.file.write = groups[g].common.acl.file.write; + users[u].acl.file['delete'] = groups[g].common.acl.file['delete']; + users[u].acl.file.list = groups[g].common.acl.file.list; + } else { + users[u].acl.file.create = users[u].acl.file.create || groups[g].common.acl.file.create; + users[u].acl.file.read = users[u].acl.file.read || groups[g].common.acl.file.read; + users[u].acl.file.write = users[u].acl.file.write || groups[g].common.acl.file.write; + users[u].acl.file['delete'] = users[u].acl.file['delete'] || groups[g].common.acl.file['delete']; + users[u].acl.file.list = users[u].acl.file.list || groups[g].common.acl.file.list; + } + } + + if (groups[g].common.acl && groups[g].common.acl.object) { + if (!users[u].acl || !users[u].acl.object) { + users[u].acl = users[u].acl || {}; + users[u].acl.object = users[u].acl.object || {}; + + users[u].acl.object.create = groups[g].common.acl.object.create; + users[u].acl.object.read = groups[g].common.acl.object.read; + users[u].acl.object.write = groups[g].common.acl.object.write; + users[u].acl.object['delete'] = groups[g].common.acl.object['delete']; + users[u].acl.object.list = groups[g].common.acl.object.list; + } else { + users[u].acl.object.create = users[u].acl.object.create || groups[g].common.acl.object.create; + users[u].acl.object.read = users[u].acl.object.read || groups[g].common.acl.object.read; + users[u].acl.object.write = users[u].acl.object.write || groups[g].common.acl.object.write; + users[u].acl.object['delete'] = users[u].acl.object['delete'] || groups[g].common.acl.object['delete']; + users[u].acl.object.list = users[u].acl.object.list || groups[g].common.acl.object.list; + } + } + + if (groups[g].common.acl && groups[g].common.acl.users) { + if (!users[u].acl || !users[u].acl.users) { + users[u].acl = users[u].acl || {}; + users[u].acl.users = users[u].acl.users || {}; + + users[u].acl.users.create = groups[g].common.acl.users.create; + users[u].acl.users.read = groups[g].common.acl.users.read; + users[u].acl.users.write = groups[g].common.acl.users.write; + users[u].acl.users['delete'] = groups[g].common.acl.users['delete']; + users[u].acl.users.list = groups[g].common.acl.users.list; + + } else { + users[u].acl.users.create = users[u].acl.users.create || groups[g].common.acl.users.create; + users[u].acl.users.read = users[u].acl.users.read || groups[g].common.acl.users.read; + users[u].acl.users.write = users[u].acl.users.write || groups[g].common.acl.users.write; + users[u].acl.users['delete'] = users[u].acl.users['delete'] || groups[g].common.acl.users['delete']; + users[u].acl.users.list = users[u].acl.users.list || groups[g].common.acl.users.list; + } + } + } + } + + callback.call(that, user, users[user] ? users[user].groups : [], users[user] ? users[user].acl : Object.assign({}, defaultAcl.acl)); + }); + }); + }; + + this.getMimeType = function (ext) { + if (ext instanceof Array) ext = ext[0]; + let _mimeType = 'text/javascript'; + let isBinary = false; + + if (ext === '.css') { + _mimeType = 'text/css'; + } else if (ext === '.bmp') { + _mimeType = 'image/bmp'; + isBinary = true; + } else if (ext === '.png') { + isBinary = true; + _mimeType = 'image/png'; + } else if (ext === '.jpg') { + isBinary = true; + _mimeType = 'image/jpeg'; + } else if (ext === '.jpeg') { + isBinary = true; + _mimeType = 'image/jpeg'; + } else if (ext === '.gif') { + isBinary = true; + _mimeType = 'image/gif'; + } else if (ext === '.tif') { + isBinary = true; + _mimeType = 'image/tiff'; + } else if (ext === '.js') { + _mimeType = 'application/javascript'; + } else if (ext === '.html') { + _mimeType = 'text/html'; + } else if (ext === '.htm') { + _mimeType = 'text/html'; + } else if (ext === '.json') { + _mimeType = 'application/json'; + } else if (ext === '.xml') { + _mimeType = 'text/xml'; + } else if (ext === '.svg') { + _mimeType = 'image/svg+xml'; + } else if (ext === '.eot') { + isBinary = true; + _mimeType = 'application/vnd.ms-fontobject'; + } else if (ext === '.ttf') { + isBinary = true; + _mimeType = 'application/font-sfnt'; + } else if (ext === '.cur') { + isBinary = true; + _mimeType = 'application/x-win-bitmap'; + } else if (ext === '.woff') { + isBinary = true; + _mimeType = 'application/font-woff'; + } else if (ext === '.wav') { + isBinary = true; + _mimeType = 'audio/wav'; + } else if (ext === '.mp3') { + isBinary = true; + _mimeType = 'audio/mpeg3'; + } else if (ext === '.avi') { + isBinary = true; + _mimeType = 'video/avi'; + } else if (ext === '.mp4') { + isBinary = true; + _mimeType = 'video/mp4'; + } else if (ext === '.mkv') { + isBinary = true; + _mimeType = 'video/mkv'; + } else if (ext === '.zip') { + isBinary = true; + _mimeType = 'application/zip'; + } else if (ext === '.ogg') { + isBinary = true; + _mimeType = 'audio/ogg'; + } else if (ext === '.manifest') { + _mimeType = 'text/cache-manifest'; + } else { + _mimeType = 'text/javascript'; + } + + return {mimeType: _mimeType, isBinary: isBinary}; + }; + + this.insert = function (id, attName, ignore, options, obj, callback) { + if (typeof options === 'string') { + options = {mimeType: options}; + } + + //return pipe for write into redis + let strm = new WMStrm(id + '/' + attName); + strm.on('finish', function () { + if (!memStore[id + '/' + attName]) log.error('File ' + id + ' / ' + attName + ' is empty'); + that.writeFile(id, attName, memStore[id + '/' + attName] || '', options, function () { + if (memStore[id + '/' + attName] !== undefined) delete memStore[id + '/' + attName]; + if (callback) setImmediate(callback, null, null); + }); + }); + return strm; + }; + + this.writeFile = function (id, name, data, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (typeof options === 'string') { + options = {mimeType: options}; + } + + if (name[0] === '/') name = name.substring(1); + + // If file yet exists => check the permissions + if (!options || !options.checked) { + return checkFileRights(id, name, options, 0x2/*write*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return that.writeFile(id, name, data, options, callback); + } + }); + } + + let isBinary; + let ext = name.match(/\.[^.]+$/); + let mime = that.getMimeType(ext); + let _mimeType = mime.mimeType; + isBinary = mime.isBinary; + + if (!fileOptions[id][name]) { + fileOptions[id][name] = {createdAt: (new Date()).getTime()}; + } + if (!fileOptions[id][name].acl) { + fileOptions[id][name].acl = { + owner: options.user || (defaultNewAcl && defaultNewAcl.owner) || 'system.user.admin', + ownerGroup: options.group || (defaultNewAcl && defaultNewAcl.ownerGroup) || 'system.group.administrator', + permissions: options.mode || (defaultNewAcl && defaultNewAcl.file) || 0x644 + }; + } + + fileOptions[id][name].mimeType = options.mimeType || _mimeType; + fileOptions[id][name].binary = isBinary; + fileOptions[id][name].acl.ownerGroup = fileOptions[id][name].acl.ownerGroup || (defaultNewAcl && defaultNewAcl.ownerGroup) || 'system.group.administrator'; + fileOptions[id][name].modifiedAt = (new Date()).getTime(); + + states.getConfig(id, function (err, _obj) { + if (_obj) { + _obj._attachments = _obj._attachments || {}; + if (!_obj._attachments[name]) { + _obj._attachments[name] = fileOptions[id][name]; + } + states.setConfig(id, _obj, function () { + states.setBinaryState('_files/' + id + '/' + name, data, callback); + }); + } else { + states.setBinaryState('_files/' + id + '/' + name, data, callback); + } + }); + }; + + this.readFile = function (id, name, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (name[0] === '/') name = name.substring(1); + + if (!options || !options.checked) { + checkFileRights(id, name, options, 0x4/*read*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return that.readFile(id, name, options, callback); + } + }); + return; + } + + states.getConfig(id, function (err, _obj) { + states.getBinaryState('_files/' + id + '/' + name, function (err, buffer) { + if (typeof callback === 'function') { + let mimeType; + if (_obj && _obj._attachments && _obj._attachments.mimeType) { + mimeType = _obj._attachments.mimeType; + if (!_obj._attachments.binary) { + buffer = buffer.toString(); + } + } else { + let ext = name.match(/\.[^.]+$/); + let mime = that.getMimeType(ext); + mimeType = mime.mimeType; + } + + callback(err, buffer, mimeType); + } + }); + }); + }; + + this.unlink = function (id, name, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (name[0] === '/') name = name.substring(1); + + if (!options || !options.checked) { + checkFileRights(id, name, options, 0x2/*write*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + if (!options.acl.file['delete']) { + if (typeof callback === 'function') callback('permissionError'); + } else { + return that.unlink(id, name, options, callback); + } + } + }); + return; + } + + states.getConfig(id, function (err, _obj) { + if (_obj && _obj._attachments) { + if (_obj._attachments[name]) { + delete _obj._attachments[name]; + } + states.setConfig(id, _obj); + } + states.delBinaryState('_files/' + id + '/' + name, callback); + }); + }; + + this.readDir = function (id, name, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!options || !options.checked) { + checkFileRights(id, name, options, 0x4/*read*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + if (!options.acl.file.list) { + if (typeof callback === 'function') callback('permissionError'); + } else { + return that.readDir(id, name, options, callback); + } + } + }); + return; + } + + // TODO + + if (typeof callback === 'function') { + setImmediate(function () { + callback('not implemented'); + }); + } + }; + + this.rename = function (id, oldName, newName, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (oldName[0] === '/') oldName = oldName.substring(1); + if (newName[0] === '/') newName = newName.substring(1); + + if (!options || !options.checked) { + checkFileRights(id, oldName, options, 0x2/*write*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + if (!options.acl.file.write) { + if (typeof callback === 'function') callback('permissionError'); + } else { + return that.rename(id, oldName, newName, options, callback); + } + } + }); + return; + } + + // TODO + if (typeof callback === 'function') callback('not implemented'); + }; + + this.touch = function (id, name, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!options || !options.checked) { + checkFileRights(id, null, options, 0x2/*write*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return that.touch(id, name, options, callback); + } + }); + return; + } + + // TODO + if (typeof callback === 'function') callback('not implemented'); + }; + + this.rm = function (id, name, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!options || !options.checked) { + checkFileRights(id, null, options, 0x2/*write*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + if (!options.acl.file['delete']) { + if (typeof callback === 'function') callback('permissionError'); + } else { + return that.rm(id, name, options, callback); + } + } + }); + return; + } + // TODO + if (typeof callback === 'function') callback('not implemented'); + }; + + this.mkdir = function (id, dirname, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (dirname[0] === '/') dirname = dirname.substring(1); + + if (!options || !options.checked) { + checkFileRights(id, dirname, options, 0x2/*write*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + if (!options.acl.file.write) { + if (typeof callback === 'function') callback('permissionError'); + } else { + return that.mkdir(id, dirname, options, callback); + } + } + }); + return; + } + // TODO + if (typeof callback === 'function') callback('not implemented'); + }; + + this.chownFile = function (id, name, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + options = options || {}; + if (typeof options !== 'object') { + options = {owner: options}; + } + + if (name[0] === '/') name = name.substring(1); + + if (!options.ownerGroup && options.group) options.ownerGroup = options.group; + if (!options.owner && options.user) options.owner = options.user; + + if (!options.owner) { + log.error('user is not defined'); + if (typeof callback === 'function') callback('invalid parameter'); + return; + } + + if (!options.ownerGroup) { + // get user group + this.getUserGroup(options.owner, function (user, groups /* , permissions */) { + if (!groups || !groups[0]) { + if (typeof callback === 'function') callback('user "' + options.owner + '" belongs to no group'); + return; + } else { + options.ownerGroup = groups[0]; + } + that.chownFile(id, name, options, callback); + }); + return; + } + + if (!options.checked) { + checkFileRights(id, null, options, 0x2/*write*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + if (!options.acl.file.write) { + if (typeof callback === 'function') callback('permissionError'); + } else { + return that.chownFile(id, name, options, callback); + } + } + }); + return; + } + + // TODO + if (typeof callback === 'function') callback('not implemented'); + }; + + this.chmodFile = function (id, name, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + options = options || {}; + + if (name[0] === '/') name = name.substring(1); + + if (typeof options !== 'object') { + options = {mode: options}; + } + + if (options.mode === undefined) { + log.error('mode is not defined'); + if (typeof callback === 'function') callback('invalid parameter'); + return; + } else if (typeof options.mode === 'string') { + options.mode = parseInt(options.mode, 16); + } + + if (!options.checked) { + checkFileRights(id, null, options, 0x2/*write*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + if (!options.acl.file.write) { + if (typeof callback === 'function') callback('permissionError'); + } else { + return that.chmodFile(id, name, options, callback); + } + } + }); + return; + } + + // TODO + if (typeof callback === 'function') callback('not implemented'); + }; + + this.enableFileCache = function (enabled, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!options || !options.checked) { + checkObjectRights(options, 0x2/*write*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return this.enableFileCache(enabled, options, callback); + } + }.bind(this)); + return; + } + + if (typeof callback === 'function') { + // cache cannot be enabled + setImmediate(function () { + callback(null, false); + }); + } + }; + + // -------------- OBJECT FUNCTIONS ------------------------------------------- + function checkObject(obj, options, flag) { + // read rights of object + if (!obj || !obj.common || !obj.acl || flag === 'list') { + return true; + } + + if (options.user !== 'system.user.admin' && + options.groups && options.groups.indexOf('system.group.administrator') === -1) { + if (obj.acl.owner !== options.user) { + // Check if the user is in the group + if (options.groups.indexOf(obj.acl.ownerGroup) !== -1) { + // Check group rights + if (!(obj.acl.object & (flag << 4))) { + return false; + } + } else { + // everybody + if (!(obj.acl.object & flag)) { + return false; + } + } + } else { + // Check group rights + if (!(obj.acl.object & (flag << 8))) { + return false; + } + } + } + return true; + } + + function checkObjectRights(options, flag, callback) { + options = options || {}; + if (!options.user) { + // Before files converted, lets think: if no options it is admin + options = { + user: 'system.user.admin', + params: options, + group: 'system.group.administrator', + acl: { + object: { + read: true, + write: true, + 'delete': true, + create: true, + list: true + }, + file: { + read: true, + write: true, + 'delete': true, + create: true, + list: true + }, + /* state: { + read: true, + write: true, + 'delete': true, + create: true, + list: true + },*/ + users: { + read: true, + write: true, + create: true, + 'delete': true, + list: true + } + } + }; + } + + if (options.checked) { + return callback(null, options); + } + + if (!options.acl) { + that.getUserGroup(options.user, function (user, groups, acl) { + options.acl = acl || {}; + options.groups = groups; + options.group = groups ? groups[0] : null; + checkObjectRights(options, flag, callback); + }); + return; + } + + // if user or group objects + if (regUser.test(id) || regGroup.test(id)) { + // If user may write + if (flag === 2 && !options.acl.users.write) {// write + return callback('permissionError', options); + } + + // If user may read + if (flag === 4 && !options.acl.users.read) {// read + return callback('permissionError', options); + } + + // If user may delete + if (flag === 'delete' && !options.acl.users.delete) {// delete + return callback('permissionError', options); + } + + // If user may list + if (flag === 'list' && !options.acl.users.list) {// list + return callback('permissionError', options); + } + + // If user may create + if (flag === 'create' && !options.acl.users.create) {// create + return callback('permissionError', options); + } + + if (flag === 'delete') flag = 2; // write + } + + // If user may write + if (flag === 2 && !options.acl.object.write) {// write + return callback('permissionError', options); + } + + // If user may read + if (flag === 4 && !options.acl.object.read) {// read + return callback('permissionError', options); + } + + // If user may delete + if (flag === 'delete' && !options.acl.object.delete) {// delete + return callback('permissionError', options); + } + + // If user may list + if (flag === 'list' && !options.acl.object.list) {// list + return callback('permissionError', options); + } + + if (flag === 'delete') flag = 2; // write + + options.checked = true; + + return callback(null, options); + } + + function clone(obj) { + if (obj === null || obj === undefined || typeof obj !== 'object') + return obj; + + let temp = obj.constructor(); // changed + + for (let key in obj) { + if (obj.hasOwnProperty(key)) { + temp[key] = clone(obj[key]); + } + } + return temp; + } + + this.subscribe = (pattern, options, callback) => { + if (!options || !options.checked) { + checkObjectRights(options, 'list', (err, options) => { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return this.subscribe(pattern, options, callback); + } + }); + return; + } + + log.silly(settings.namespace + ' redis psubscribe ' + redisNamespace + pattern); + sub.psubscribe(redisNamespace + pattern, function (err) { + if (typeof callback === 'function') callback(err); + }); + }; + + this.unsubscribe = (pattern, options, callback) => { + if (!options || !options.checked) { + checkObjectRights(options, 'list', (err, options) => { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return this.unsubscribe(pattern, options, callback); + } + }); + return; + } + + log.silly(settings.namespace + ' redis punsubscribe ' + redisNamespace + pattern); + sub.punsubscribe(redisNamespace + pattern, function (err) { + if (typeof callback === 'function') callback(err); + }); + }; + + this.chownObject = function (pattern, options, callback) { + options = options || {}; + if (typeof options !== 'object') { + options = {owner: options}; + } + + // renaming + if (!options.ownerGroup && options.group) options.ownerGroup = options.group; + if (!options.owner && options.user) options.owner = options.user; + + if (!options.owner) { + log.error('user is not defined'); + if (typeof callback === 'function') callback('invalid parameter'); + return; + } + + if (!options.ownerGroup) { + // get user group + this.getUserGroup(options.owner, (user, groups /* , permissions */) => { + if (!groups || !groups[0]) { + if (typeof callback === 'function') callback('user "' + options.owner + '" belongs to no group'); + return; + } else { + options.ownerGroup = groups[0]; + } + this.chownObject(pattern, options, callback); + }); + return; + } + + if (!options.checked) { + checkObjectRights(options, 0x2/*write*/, (err, options) => { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + if (!options.acl.object || !options.acl.object.write) { + if (typeof callback === 'function') callback('permissionError'); + } else { + return this.chownObject(pattern, options, callback); + } + } + }); + } + + // TODO + }; + + this.chmodObject = function (pattern, options, callback) { + options = options || {}; + + if (typeof options !== 'object') { + options = {object: options}; + } + + if (options.mode && !options.object) options.object = options.mode; + + if (options.object === undefined) { + log.error('mode is not defined'); + if (typeof callback === 'function') callback('invalid parameter'); + return; + } else if (typeof options.mode === 'string') { + options.mode = parseInt(options.mode, 16); + } + + if (!options.checked) { + checkObjectRights(options, 0x2/*write*/, (err, options) => { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + if (!options.acl.file.write) { + if (typeof callback === 'function') callback('permissionError'); + } else { + return that.chmodObject(pattern, options, callback); + } + } + }); + } + + // TODO + }; + + this.getObject = function (id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!options || !options.checked) { + checkObjectRights(options, 0x4/*read*/, (err, options) => { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return this.getObject(id, options, callback); + } + }); + return; + } + + if (typeof callback === 'function') { + client.get(redisNamespace + id, function (err, obj) { + if (err) { + log.warn(settings.namespace + ' redis get ' + id + ', error - ' + err); + } else { + log.silly(settings.namespace + ' redis get ' + id + ' ok: ' + obj); + } + try { + obj = obj ? JSON.parse(obj) : null; + } catch (e) { + log.error(`Cannot parse ${id} - ${obj}: ${JSON.stringify(e)}`); + } + callback(err, obj ? JSON.parse(obj) : null); + }); + } + }; + + function pattern2RegEx(pattern) { + if (pattern !== '*') { + if (pattern[0] === '*' && pattern[pattern.length - 1] !== '*') pattern += '$'; + if (pattern[0] !== '*' && pattern[pattern.length - 1] === '*') pattern = '^' + pattern; + } + pattern = pattern.replace(/\./g, '\\.'); + pattern = pattern.replace(/\*/g, '.*'); + return pattern; + } + + this.getKeys = function (pattern, options, callback, dontModify) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!options || !options.checked) { + checkObjectRights(options, 'list', (err, options) => { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return this.getConfigKeys(pattern, options, callback, dontModify); + } + }); + return; + } + + let r = new RegExp(pattern2RegEx(pattern)); + client.keys(redisNamespace + pattern, (err, keys) => { + log.silly(settings.namespace + ' redis keys ' + obj.length + ' ' + pattern); + if (typeof callback === 'function') { + let result = []; + if (keys) { + keys.sort(); + if (options.user === 'system.user.admin' || options.group === 'system.group.administrator') { + setImmediate(function () { + callback(err, keys); + }); + } else { + let result = []; + // Check permissions + client.mget(keys, function (err, objs) { + for (let i = 0; i < keys.length; i++) { + if (r.test(keys[i]) && checkObject(objs[i], options, 0x4/*read*/)) { + if (!dontModify) { + result.push(keys[i].substring(redisNamespaceL)); + } else { + result.push(keys[i]); + } + } + } + setImmediate(function () { + callback(err, result); + }); + }) + } + } else { + setImmediate(function () { + callback(err, result); + }); + } + } + }); + }; + this.getConfigKeys = this.getKeys; + + this.getObjects = function (keys, options, callback, dontModify) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!options || !options.checked) { + checkObjectRights(options, 0x4/*read*/, (err, options) => { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return this.getObjects(keys, options, callback, dontModify); + } + }); + return; + } + + if (!keys) { + if (typeof callback === 'function') callback('no keys', null); + return; + } + if (!keys.length) { + if (typeof callback === 'function') callback(null, []); + return; + } + + let _keys; + if (!dontModify) { + _keys = []; + for (let i = 0; i < keys.length; i++) { + _keys[i] = redisNamespace + keys[i]; + } + } else { + _keys = keys; + } + client.mget(_keys, function (err, objs) { + let result = []; + if (err) { + log.warn(settings.namespace + ' redis mget ' + ((!objs) ? 0 : objs.length) + ' ' + _keys.length + ', err: ' + err); + } else { + log.silly(settings.namespace + ' redis mget ' + ((!objs) ? 0 : objs.length) + ' ' + _keys.length); + } + if (objs) { + if (options.user !== 'system.user.admin' && options.group !== 'system.group.administrator') { + for (let i = 0; i < objs.length; i++) { + if (checkObject(objs[i], options, 4 /*read*/)) { + result.push(objs[i]); + } else { + result.push({error: 'permissionError'}); + } + } + } else { + result = objs; + } + } + if (typeof callback === 'function') { + setImmediate(function () { + callback(null, result); + }); + } + }); + }; + + this.getObjectsByPattern = (pattern, options, callback) => { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (!options || !options.checked) { + checkObjectRights(options, 0x4/*read*/, (err, options) => { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return this.getObjectsByPattern(keys, options, callback); + } + }); + return; + } + client.keys(redisNamespace + pattern, (err, keys) => { + log.silly(settings.namespace + ' redis keys ' + obj.length + ' ' + pattern); + if (typeof callback === 'function') { + let result = []; + if (keys) { + keys.sort(); + // read objects + client.mget(keys, function (err, objs) { + let result = []; + if (err) { + log.warn(settings.namespace + ' redis mget ' + ((!objs) ? 0 : objs.length) + ' ' + keys.length + ', err: ' + err); + } else { + log.silly(settings.namespace + ' redis mget ' + ((!objs) ? 0 : objs.length) + ' ' + keys.length); + } + if (objs) { + if (options.user !== 'system.user.admin' && options.group !== 'system.group.administrator') { + const r = new RegExp(pattern2RegEx(pattern)); + for (let i = 0; i < objs.length; i++) { + if (!objs[i] || (r.test(objs[i]._id) && checkObject(objs[i], options, 4 /*read*/))) { + result.push(objs[i]); + } else { + result.push({error: 'permissionError'}); + } + } + } else { + result = objs; + } + } + if (typeof callback === 'function') { + setImmediate(function () { + callback(null, result); + }); + } + }); + } else { + setImmediate(function () { + callback(err, result); + }); + } + } + }); + }; + + /** + * set anew or update object + * + * This function writes the object into DB + * + * @alias setObject + * @memberof objectsInMemServer + * @param {string} id ID of the object + * @param {object} obj + * @param {object} options options for access control are optional + * @param {function} callback return function + */ + this.setObject = function (id, obj, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!options || !options.checked) { + checkObjectRights(options, 0x2/*write*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return this.setObject(id, obj, options, callback); + } + }.bind(this)); + return; + } + + if (!obj) { + log.error('setObject: Argument object is null'); + if (typeof callback === 'function') callback('obj is null'); + return; + } + + obj._id = id; + + // read object + client.get(redisNamespace + id, (err, oldObj) => { + if (err) { + log.warn(settings.namespace + ' redis get ' + id + ', error - ' + err); + } else { + log.silly(settings.namespace + ' redis get ' + id + ' ok: ' + oldObj); + } + try { + oldObj = oldObj ? JSON.parse(oldObj) : null; + } catch (e) { + log.error(`Cannot parse ${id} - ${oldObj}: ${JSON.stringify(e)}`); + } + + if (!checkObject(oldObj, options, 0x2/*write*/)) { + if (typeof callback === 'function') { + callback({error: 'permissionError'}); + } + return; + } + + if (id === 'system.config' && obj && obj.common && oldObj && oldObj.common && JSON.stringify(obj.common.defaultNewAcl) !== JSON.stringify(oldObj.common.defaultNewAcl)) { + oldObj = obj; + return setDefaultAcl(function () { + that.setObject(id, obj, options, callback); + }); + } + + if (!tools.checkNonEditable(oldObj, obj)) { + if (typeof callback === 'function') { + callback('Invalid password for update of vendor information'); + } + return; + } + + // do not delete common settings, like "history" or "mobile". It can be erased only with "null" + if (oldObj && oldObj.common) { + for (let i = 0; i < preserveSettings.length; i++) { + // remove settings if desired + if (obj.common && obj.common[preserveSettings[i]] === null) { + delete obj.common[preserveSettings[i]]; + continue; + } + + if (oldObj.common[preserveSettings[i]] !== undefined && (!obj.common || obj.common[preserveSettings[i]] === undefined)) { + if (!obj.common) obj.common = {}; + obj.common[preserveSettings[i]] = oldObj.common[preserveSettings[i]]; + } + } + } + + if (oldObj && oldObj.acl && !obj.acl) { + obj.acl = oldObj.acl; + } + + // add user default rights + if (defaultNewAcl && !obj.acl) { + obj.acl = Object.assign({}, defaultNewAcl); + delete obj.acl.file; + if (obj.type !== 'state') { + delete obj.acl.state; + } + if (options.owner) { + obj.acl.owner = options.owner; + + if (!options.ownerGroup) { + obj.acl.ownerGroup = null; + this.getUserGroup(options.owner, (user, groups /* , permissions */) => { + if (!groups || !groups[0]) { + options.ownerGroup = (defaultNewAcl && defaultNewAcl.ownerGroup) || 'system.group.administrator'; + } else { + options.ownerGroup = groups[0]; + } + this.setObject(id, obj, options, callback); + }); + return; + } + } + } + if (defaultNewAcl && obj.acl && !obj.acl.ownerGroup && options.ownerGroup) { + obj.acl.ownerGroup = options.ownerGroup; + } + const jsonObj = JSON.stringify(obj); + client.set(redisNamespace + id, jsonObj, function (err) { + log.silly(settings.namespace + ' redis set', id, obj); + client.publish(redisNamespace + id, jsonObj); + if (typeof callback === 'function') { + callback(err, {id: id}); + } + }) + }); + }; + + this.delObject = function (id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!options || !options.checked) { + checkObjectRights(options, 'delete', (err, options) => { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return this.delObject(id, options, callback); + } + }); + return; + } + // read object + client.get(redisNamespace + id, (err, oldObj) => { + if (err) { + log.warn(settings.namespace + ' redis get ' + id + ', error - ' + err); + } else { + log.silly(settings.namespace + ' redis get ' + id + ' ok: ' + oldObj); + } + try { + oldObj = oldObj ? JSON.parse(oldObj) : null; + } catch (e) { + log.error(`Cannot parse ${id} - ${oldObj}: ${JSON.stringify(e)}`); + } + + if (!checkObject(oldObj, options, 2/* write */)) { + if (typeof callback === 'function') { + callback({error: 'permissionError'}); + } + return; + } + client.del(redisNamespace + id, function (err) { + client.publish(redisNamespace + id, null); + if (typeof callback === 'function') callback(err); + }); + }); + }; + // this function is very ineffective. Because reads all objects and then process them + function _applyView(func, params, options, callback) { + let result = { + rows: [] + }; + + function _emit_(id, obj) { + result.rows.push({id: id, value: obj}); + } + params = params || {startkey: '', endkey: '\u9999'}; + client.keys(redisNamespace + '*', (err, keys) => { + keys.sort(); + client.mget(keys, (err, objs) => { + let f = eval('(' + func.map.replace(/emit/g, '_emit_') + ')'); + + for (let i = 0; i < keys.length; i++) { + const id = keys[i]; + if (params) { + if (params.startkey && id < params.startkey) continue; + if (params.endkey && id > params.endkey) continue; + } + if (objects[id]) { + try { + f(objects[id]); + } catch (e) { + console.log('Cannot execute map: ' + e.message); + + } + } + } + // Calculate max + if (func.reduce === '_stats') { + let max = null; + for (let i = 0; i < result.rows.length; i++) { + if (max === null || result.rows[i].value > max) { + max = result.rows[i].value; + } + } + if (max !== null) { + result.rows = [{id: '_stats', value: {max: max}}]; + } else { + result.rows = []; + } + } + + if (typeof callback === 'function') callback(null, result); + }); + }); + } + + this.getObjectView = function (design, search, params, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!options || !options.checked) { + checkObjectRights(options, 4/*read*/, (err, options) => { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return this.getObjectView(design, search, params, options, callback); + } + }); + return; + } + if (typeof callback === 'function') { + client.get(redisNamespace + '_design/' + design, (err, obj) => { + if (err) { + console.error('Cannot find view "' + design + '": ' + err); + } + if (obj && obj.views && obj.views[search]) { + this.getObjectList(params, options, (err, _result) => { + const func = obj.views[search]; + let f = eval('(' + func.map.replace(/emit/g, '_emit_') + ')'); + let result = {rows: []}; + + // required for getObjectView + function _emit_(id, obj) { + result.rows.push({id: id, value: obj}); + } + + for (let i = 0; i < _result.rows.length; i++) { + const id = _result.rows[i].id; + const obj = _result.rows[i].value; + if (obj) { + try { + f(obj); + } catch (e) { + console.log('Cannot execute map: ' + e.message); + } + } + } + // Calculate max + if (func.reduce === '_stats') { + let max = null; + for (let i = 0; i < result.rows.length; i++) { + if (max === null || result.rows[i].value > max) { + max = result.rows[i].value; + } + } + if (max !== null) { + result.rows = [{id: '_stats', value: {max: max}}]; + } else { + result.rows = []; + } + } + + callback(null, result); + }); + } else { + console.log('Cannot find view "' + design + '"'); + callback('Cannot find view "' + design + '"'); + } + }); + } + }; + + this.getObjectList = function (params, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!options || !options.checked) { + checkObjectRights(options, 4/*read*/, (err, options) => { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return this.getObjectList(params, options, callback); + } + }); + return; + } + + if (typeof callback === 'function') { + //params = {startkey, endkey, include_docs} + params = params || {startkey: '', endkey: '\u9999'}; + let pattern = (params.endkey.substring(0, params.startkey.length) === params.startkey) ? redisNamespace + params.startkey + '*' : redisNamespace + '*'; + client.keys(pattern, (err, keys) => { + let _keys = []; + for (let i = 0; i < keys.length; i++) { + if (params.startkey && keys[i] < params.startkey) continue; + if (params.endkey && keys[i] > params.endkey) continue; + if (!params.include_docs && keys[i][redisNamespaceL] === '_') continue; + _keys.push(keys[i]); + } + keys.sort(); + client.mget(_keys, function (err, objs) { + // return rows with id and doc + let result = { + rows: [] + }; + for (let r = 0; r < objs.length; r++) { + let obj = JSON.parse(objs[r]); + if (!checkObject(obj, options, 4/*read*/)) continue; + result.rows.push({id: keys[r], value: obj, doc: obj}); + } + callback(null, result); + }); + }); + } + }; + + this.getObjectListAsync = (params, options) => { + return new Promise((resolve, reject) => { + this.getObjectList(params, options, (err, arr) => { + if (err) { + reject(err); + } else { + resolve(arr); + } + }) + }); + }; + + // could be optimised, to read object only once. Now it will read 3 times + this.extendObject = function (id, obj, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!options || !options.checked) { + checkObjectRights(options, 2/*write*/, (err, options) => { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return this.extendObject(id, obj, options, callback); + } + }); + return; + } + + this.getObject(id, options, function (err, data) { + let newObj = extend(true, data || {}, obj); + that.setObject(id, newObj, options, function (err, res) { + if (typeof callback === 'function') callback(err, {id: id, value: res}, id); + }); + }); + }; + + this.setConfig = this.setObject; + + this.delConfig = this.delObject; + + this.getConfig = this.getObject; + + this.getConfigs = this.getObjects; + + this.findObject = function (idOrName, type, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + + if (!options || !options.checked) { + checkObjectRights(options, 'list', function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + return this.findObject(idOrName, type, options, callback); + } + }.bind(this)); + return; + } + + if (!objects) { + if (typeof callback === 'function') callback('Not implemented'); + return; + } + + // Assume it is ID + if (objects[idOrName] && (!type || (objects[idOrName].common && objects[idOrName].common.type === type))) { + if (typeof callback === 'function') callback(null, idOrName, objects[idOrName].common.name); + } else { + // Assume it is name + for (let id in objects) { + if (!checkObject(objects[id], options, 4/*read*/)) continue; + if (objects[id].common && + objects[id].common.name === idOrName && + (!type || (objects[id].common && objects[id].common.type === type))) { + if (typeof callback === 'function') callback(null, id, idOrName); + return; + } + } + if (typeof callback === 'function') callback(null, null, idOrName); + } + }; + + // can be called only from js-controller + this.addPreserveSettings = function (settings) { + if (typeof settings !== 'object') settings = [settings]; + + for (let s = 0; s < settings.length; s++) { + if (preserveSettings.indexOf(settings[s]) === -1) preserveSettings.push(settings[s]); + } + }; + + this.destroyDB = function (options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + options = options || {}; + + if (!options.checked) { + checkObjectRights(options, 0x2/*write*/, function (err, options) { + if (err) { + if (typeof callback === 'function') callback(err); + } else { + if (!options.acl.file.write) { + if (typeof callback === 'function') callback('permissionError'); + } else { + return that.destroyDB(options, callback); + } + } + }); + return; + } + + states.getConfigKeys('*', function (err, obj) { + for (let t = 0; t < obj.length; t++) { + states.delConfig(obj.substring(7)); + } + if (typeof callback === 'function') callback(); + }); + }; + + // Destructor of the class. Called by shutting down. + this.destroy = () => { + if (client) { + client.end(true); + client = null; + } + if (sub) { + sub.end(); + sub = null; + } + }; + + (function __construct() { + if (settings.connection.port === 0) { + // initiate a unix socket connection using the parameter 'host' + client = redis.createClient(settings.connection.host, settings.connection.options); + sub = redis.createClient(settings.connection.host, settings.connection.options); + } else { + client = redis.createClient(settings.connection.port, settings.connection.host, settings.connection.options); + sub = redis.createClient(settings.connection.port, settings.connection.host, settings.connection.options); + } + + if (typeof onChange === 'function') { + sub.on('pmessage', (pattern, channel, message) => { + log.debug(settings.namespace + ' redis pmessage ', pattern, channel, message); + try { + if (ioRegExp.test(channel)) { + const id = channel.substring(redisNamespaceL); + try { + const obj = message ? JSON.parse(message) : null; + onChange(id, obj); + } catch (e) { + log.error(`Cannot parse ${id} - ${message}: ${JSON.stringify(e)}`); + } + } else { + log.error(`Received unexpected pmessage: ${channel}`); + } + } catch (e) { + log.error(settings.namespace + ' pmessage ' + channel + ' ' + message + ' ' + e.message); + log.error(settings.namespace + ' ' + e.stack); + } + }); + } + + client.on('error', error => { + if (typeof settings.disconnected === 'function') { + settings.disconnected(error); + } else { + log.error(settings.namespace + ' ' + error.message); + log.error(settings.namespace + ' ' + error.stack); + } + }); + + sub.on('error', error => { + log.error(settings.namespace + ' No redis connection!'); + }); + + sub.on('connect', error => { + if (settings.connection.port === 0) { + log.info(settings.namespace + ' Objects connected to redis: ' + settings.connection.host); + } else { + log.info(settings.namespace + ' Objects connected to redis: ' + settings.connection.host + ':' + settings.connection.port); + } + }); + + client.on('connect', error => { + if (typeof settings.connected === 'function') settings.connected(); + }); + })(); + return this; +} + + +module.exports = ObjectsInRedis; \ No newline at end of file diff --git a/lib/password.js b/lib/password.js new file mode 100644 index 0000000..5110b95 --- /dev/null +++ b/lib/password.js @@ -0,0 +1,87 @@ +/** + * + * password hash and check + * + * 7'2014-2016 Bluefox + * 2014 hobbyquaker + * + * derived from https://github.com/florianheinemann/password-hash-and-salt/ (MIT License) + * + * The created hash is of the following format: $$$ + * + * Usage Example: + + var password = require('./lib/password.js'); + + password('test').hash(null, null, function (err, res) { + console.log(res); + + password('test').check(res, function (err, res) { + console.log('test: ' + res); + }); + + password('muh').check(res, function (err, res) { + console.log('muh: ' + res); + }); + + }); + + * + */ + +/* jshint -W097 */ +/* jshint strict: false */ +/* jslint node: true */ +'use strict'; + +const crypto = require('crypto'); +let version = null; + +const password = pw => { + return { + hash: (salt, iterations, callback) => { + + salt = salt || crypto.randomBytes(16).toString('hex'); + iterations = iterations || 10000; + + // version 0.10 has no 'sha256' and this option must be ignored + if (version === null) { + version = process.version.replace('v', ''); + version = !version.match(/^0\.10\./); + } + + if (version) { + crypto.pbkdf2(pw, salt, iterations, 256, 'sha256', (err, key) => { + if (err) return callback(err); + + callback(null, `pbkdf2$${iterations}$${key.toString('hex')}$${salt}`); + }); + } else { + crypto.pbkdf2(pw, salt, iterations, 64, (err, key) => { + if (err) return callback(err); + + callback(null, `pbkdf2$${iterations}$${key.toString('hex')}$${salt}`); + }); + + } + }, + check: function (hashedPassword, callback) { + if (!hashedPassword || !password) return callback(null, false); + let key = hashedPassword.split('$'); + if (key.length !== 4 || !key[2] || !key[3]) return callback('Hash not formatted correctly'); + if (key[0] !== 'pbkdf2') return callback('Unknown'); + + this.hash(key[3], parseInt(key[1], 10), function (error, newHash) { + if (error) return callback(error); + callback(null, newHash === hashedPassword); + }); + + }, + complexity: callback => { + // Todo: Check for password complexity + return true; + } + }; +}; + +module.exports = password; \ No newline at end of file diff --git a/lib/preinstall_check.js b/lib/preinstall_check.js new file mode 100644 index 0000000..3a4e655 --- /dev/null +++ b/lib/preinstall_check.js @@ -0,0 +1,159 @@ +'use strict'; + +// we cannot use semver here, because dependencies are not installed yet +// so the version checks get a bit messy + +const fs = require('fs'); +const path = require('path'); +const child_process = require('child_process'); +const os = require('os'); + +// where yunkong2 is installed +const rootDir = __dirname.substr(0, __dirname.lastIndexOf('node_modules')); + + +function checkNpmVersion() { + // Get npm version + try { + // remove local node_modules\.bin dir from path + // or we potentially get a wrong npm version + let newEnv = Object.assign({}, process.env); + newEnv.PATH = (newEnv.PATH || newEnv.Path || newEnv.path) + .split(path.delimiter) + .filter(dir => { + dir = dir.toLowerCase(); + return !(dir.indexOf('yunkong2') > -1 && dir.indexOf(path.join('node_modules', '.bin')) > -1); + + }) + .join(path.delimiter); + + let npmVersion = child_process.execSync('npm -v', {encoding: 'utf8', env: newEnv}); + npmVersion = npmVersion.trim(); + console.log('NPM version: ' + npmVersion); + + if (gte(npmVersion, '5.0.0') && lt(npmVersion, '5.7.1')) { + console.warn('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); + console.warn('WARNING:'); + console.warn('You are using an unsupported npm version!'); + console.warn('This can lead to problems when installing further packages'); + console.warn(); + console.warn('Please use "npm install -g npm@4" to downgrade npm to 4.x or '); + console.warn('use "npm install -g npm@latest" to install a supported version of npm 5!'); + console.warn('You need to make sure to repeat this step after installing an update to NodeJS and/or npm.'); + console.warn('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); + } + return npmVersion; + } catch (e) { + console.error('Could not check npm version: ' + e); + console.error('Assuming that correct version is installed.'); + process.exit(0); + } +} + +if (gte(checkNpmVersion(), '5.0.0')) { + // disables NPM's package-lock.json on NPM >= 5 because that creates heaps of problems + console.log('npm version >= 5: disabling package-lock'); + fs.writeFileSync(path.join(rootDir, '.npmrc'), 'package-lock=false' + os.EOL, 'utf8'); +} + +process.exit(0); + +// ====================================== +// all the functions to replace `semver`: + +/** + * @typedef {{major: number, minor: number, build: number}} Version + */ +/** + * Parses a version string + * @param {string} version The version string to parse + * @returns {Version | null} The parsed version + */ +function parseVersion(version) { + const versionRegExp = /^v?(\d+)\.(\d+)\.(\d+).*?/; + const parsed = versionRegExp.exec(version); + if (!parsed) return null; + + return { + major: +parsed[1], + minor: +parsed[2], + build: +parsed[3] + }; +} + +/** + * Checks if v1 > v2 + * @param {Version | string} v1 + * @param {Version | string} v2 + */ +function gt(v1, v2) { + if (typeof v1 === "string") v1 = parseVersion(v1); + if (typeof v2 === "string") v2 = parseVersion(v2); + + if (v1.major > v2.major) return true; + else if (v1.major < v2.major) return false; + + if (v1.minor > v2.minor) return true; + else if (v1.minor < v2.minor) return false; + + return (v1.build > v2.build); +} + +/** + * Checks if v1 < v2 + * @param {Version | string} v1 + * @param {Version | string} v2 + */ +function lt(v1, v2) { + if (typeof v1 === "string") v1 = parseVersion(v1); + if (typeof v2 === "string") v2 = parseVersion(v2); + + if (v1.major < v2.major) return true; + else if (v1.major > v2.major) return false; + + if (v1.minor < v2.minor) return true; + else if (v1.minor > v2.minor) return false; + + return (v1.build < v2.build); +} + +/** + * Checks if v1 == v2 + * @param {Version | string} v1 + * @param {Version | string} v2 + */ +function eq(v1, v2) { + if (typeof v1 === "string") v1 = parseVersion(v1); + if (typeof v2 === "string") v2 = parseVersion(v2); + + if (v1.major !== v2.major) return false; + if (v1.minor !== v2.minor) return false; + return v1.build === v2.build; +} + +/** + * Checks if v1 != v2 + * @param {Version | string} v1 + * @param {Version | string} v2 + */ +function ne(v1, v2) { + return !eq(v1, v2); +} + +/** + * Checks if v1 >= v2 + * @param {Version | string} v1 + * @param {Version | string} v2 + */ +function gte(v1, v2) { + return gt(v1, v2) || eq(v1, v2); +} + +/** + * Checks if v1 <= v2 + * @param {Version | string} v1 + * @param {Version | string} v2 + */ +function lte(v1, v2) { + return lt(v1, v2) || eq(v1, v2); +} \ No newline at end of file diff --git a/lib/renewal.conf.tpl b/lib/renewal.conf.tpl new file mode 100644 index 0000000..a4b838c --- /dev/null +++ b/lib/renewal.conf.tpl @@ -0,0 +1,68 @@ +#cert = :configDir/live/:hostname/cert.pem +cert = :cert_path +privkey = :privkey_path +chain = :chain_path +fullchain = :fullchain_path + +# Options and defaults used in the renewal process +[renewalparams] +apache_enmod = a2enmod +no_verify_ssl = False +ifaces = None +apache_dismod = a2dismod +register_unsafely_without_email = False +uir = None +installer = none +config_dir = :configDir +text_mode = True +# junk? +# https://github.com/letsencrypt/letsencrypt/issues/1955 +func = +prepare = False +work_dir = :work_dir +tos = :agree_tos +init = False +http01_port = :http_01_port +duplicate = False +# this is for the domain +key_path = :privkey_path +nginx = False +fullchain_path = :fullchain_path +email = :email +csr = None +agree_dev_preview = None +redirect = None +verbose_count = -3 +config_file = None +renew_by_default = True +hsts = False +authenticator = webroot +domains = :hostnames #comma,delimited,list +rsa_key_size = :rsa_key_size +# starts at 0 and increments at every renewal +checkpoints = -1 +manual_test_mode = False +apache = False +cert_path = :cert_path +webroot_path = :webroot_paths # comma,delimited,list +strict_permissions = False +apache_server_root = /etc/apache2 +# https://github.com/letsencrypt/letsencrypt/issues/1948 +account = :account_id +manual_public_ip_logging_ok = False +chain_path = :chain_path +standalone = False +manual = False +server = :acme_discovery_url +standalone_supported_challenges = "http-01,tls-sni-01" +webroot = True +apache_init_script = None +user_agent = None +apache_ctl = apache2ctl +apache_le_vhost_ext = -le-ssl.conf +debug = False +tls_sni_01_port = 443 +logs_dir = :logs_dir +configurator = None +[[webroot_map]] +# :hostname = :webroot_path diff --git a/lib/restart.js b/lib/restart.js new file mode 100644 index 0000000..22f8031 --- /dev/null +++ b/lib/restart.js @@ -0,0 +1,181 @@ +/** + * @fileOverview restart the controller + * @author bluefox + * @version 0.1 + */ +'use strict'; + +/** @module restart */ +var fs = require('fs'); +var tools = require(__dirname + '/tools.js'); + +var logFile; +try { + var dir = __dirname + '/../' + tools.getDefaultDataDir() + '../log'; + if (!fs.existsSync(dir)) fs.mkdirSync(dir); + logFile = dir + '/restart.log'; + console.log(require('path').normalize(logFile)); + if (fs.existsSync(logFile)) fs.unlinkSync(logFile); + fs.writeFileSync(logFile, 'started\n'); +} catch (err) { + console.error(err); +} + +function log(text) { + var t = (new Date()).toString() + text; + console.log(t); + fs.appendFileSync(logFile, t + '\n'); +} + +function checkRoot(callback) { + var data = ''; + + var child = require('child_process').spawn('whoami', []); + child.stdout.on('data', function (text) { + data += text.toString().replace('\n', ''); + }); + child.stderr.on('data', function (text) { + data += text.toString().replace('\n', ''); + }); + child.on('exit', function (exitCode) { + callback(data.trim() == 'root'); + }); +} + +function killPidsScript(callback) { + checkRoot(function (isRoot) { + var data = ''; + + try { + fs.chmodSync(__dirname + '/../killall.sh', '777'); + + if (isRoot) { + fs.writeFileSync(__dirname + '/../killall.sh', "sudo pgrep -f '^io.*' | sudo xargs kill -9\nsudo pgrep -f '^node-red*' | sudo xargs kill -9"); + } else { + fs.writeFileSync(__dirname + '/../killall.sh', "pgrep -f '^io.*' | xargs kill -9\pgrep -f '^node-red*' | xargs kill -9"); + } + + var child = require('child_process').spawn(__dirname + '/../killall.sh', []); + child.stdout.on('data', function (text) { + data += text.toString().replace('\n', ''); + }); + child.stderr.on('data', function (text) { + data += text.toString().replace('\n', ''); + }); + child.on('exit', function (exitCode) { + if (exitCode) log('Exit code for "killall.sh": ' + exitCode); + callback(exitCode, data); + }); + } catch (e) { + log('Cannot create "' + __dirname + '/../killall.sh"'); + callback(-1); + } + }); +} + +function killPid(pid, callback) { + var data = ''; + var child = require('child_process').spawn('kill', ['-KILL', pid]); + child.stdout.on('data', function (text) { + data += text.toString().replace('\n', ''); + }); + child.stderr.on('data', function (text) { + data += text.toString().replace('\n', ''); + }); + child.on('exit', function (exitCode) { + if (exitCode) log('Exit code for "kill -KILL ' + pid + '": ' + exitCode); + callback(exitCode, data); + }); +} + +function killPids(pids, callback) { + if (pids && pids.length) { + killPid(pids.pop(), function () { + killPids(pids, callback); + }); + } else { + callback(); + } +} + +if (require('os').platform().match(/^win/) && fs.existsSync(__dirname + '/../_service_' + tools.appName + '.bat')) { + log('Restarting service ' + tools.appName + '...'); + + var spawn = require('child_process').spawn; + var out; + var err; + var stat; + var fileName; + + if (fs.existsSync(__dirname + '/../../../log')) { + fileName = __dirname + '/../../../log/restart.log'; + } else { + fileName = __dirname + '/../log/restart.log'; + } + + stat = fs.statSync(fileName); + if (stat.size > 1024 * 1024) { + try { + fs.unlinkSync(fileName); + } catch (e) { + console.log('File is too big, but cannot delete restart.log: ' + e.toString()); + } + } + + out = fs.openSync(fileName, 'a'); + + err = out; + + log('Starting ' + __dirname + '/../_service_' + tools.appName + '.bat'); + + var child = spawn('cmd.exe', ['/c', __dirname + '/../_service_' + tools.appName + '.bat'], { + detached: true, + stdio: ['ignore', out, err] + }); + child.unref(); + process.exit(); +} else if (!fs.existsSync(__dirname + '/' + tools.appName + '.pid')) { + log(tools.appName + ' was started manually or was not running. Please restart it manually.'); +} else { + log('Restarting ' + tools.appName + '...'); + + var daemon = require('daemonize2').setup({ + main: '../controller.js', + name: tools.appName + ' controller', + pidfile: __dirname + '/' + tools.appName + '.pid', + stopTimeout: 5000 + }); + + log('Stopping daemon ' + tools.appName + '...'); + daemon.stop(function (err, pid) { + // force to stop all adapters + if (fs.existsSync(__dirname + '/../pids.txt')) { + try { + var pids = JSON.parse(fs.readFileSync(__dirname + '/../pids.txt').toString()); + killPids(pids, function () { + killPidsScript(function () { + log('Starting daemon ' + tools.appName + '...'); + daemon.start(function (err, pid) { + log('Daemon ' + tools.appName + ' started'); + process.exit(); + }); + }); + }); + } catch (e) { + log('Error by pids.txt: ' + e); + log('Starting daemon ' + tools.appName + '...'); + daemon.start(function (err, pid) { + log('Daemon ' + tools.appName + ' started'); + process.exit(); + }); + + } + } else { + log('Starting daemon ' + tools.appName + '...'); + daemon.start(function (err, pid) { + log('Daemon ' + tools.appName + ' started'); + process.exit(); + }); + } + }); +} diff --git a/lib/scripts/scripts.js b/lib/scripts/scripts.js new file mode 100644 index 0000000..6030db2 --- /dev/null +++ b/lib/scripts/scripts.js @@ -0,0 +1,112 @@ +var https = require('https'); +var fs = require('fs'); +var tools = require(__dirname + '/../tools.js'); + +function httpsGet(link, callback) { + https.get(link, function (res) { + var statusCode = res.statusCode; + + if (statusCode !== 200) { + // consume response data to free up memory + res.resume(); + callback(statusCode, null, link); + } + + res.setEncoding('utf8'); + var rawData = ''; + res.on('data', function (chunk) { + rawData += chunk; + }); + res.on('end', function () { + callback(null, rawData ? rawData.toString() : null, link); + }); + }).on('error', function (e) { + callback(e.message, null, link); + }); +} + +var stableURL = 'https://raw.githubusercontent.com/' + tools.appName + '/' + tools.appName + '.repositories/master/sources-dist-stable.json'; + +function updateVersion(name, callback, _sources) { + if (!_sources) { + httpsGet(stableURL, function (err, body) { + updateVersion(name, callback, JSON.parse(body)); + }); + } + var cmd = 'npm show ' + tools.appName + '.' + name + ' version'; + var exec = require('child_process').exec; + var result = ''; + var child = exec(cmd, function(error, stdout, stderr){ + result = stdout; + }); + child.stderr.pipe(process.stdout); + child.on('exit', function (code /* , signal */) { + if (code) { + console.error('host.' + tools.getHostName() + ' Cannot get version of ' + tools.appName + '.' + name + ': ' + code); + callback(code, _sources, name, null); + } else { + _sources[name].version = result; + callback(null, _sources, name, result); + } + }); +} + +function updateVersions(callback) { + httpsGet(stableURL, function (err, body) { + var sources = JSON.parse(body); + var count = 0; + for (var name in sources) { + if (!sources.hasOwnProperty(name)) continue; + if (!sources[name].version) { + count++; + updateVersion(name, function () { + if (!--count) callback(sources); + }, sources); + } + } + if (!count) callback(sources); + }); +} + +// get the sources-dist.json +if (process.argv.indexOf('--prepublish') !== -1) { + httpsGet(stableURL, function (err, body) { + if (err || !body) { + console.error('Cannot read sources file "' + stableURL + '": ' + err); + process.exit(2); + } else { + fs.writeFileSync(__dirname + '/../../conf/sources-dist.json', body); + process.exit(); + } + }); +} + +// update versions for all adapter, which do not have the version +if (process.argv.indexOf('--init') !== -1) { + updateVersions(function (sources) { + var file = process.argv.indexOf('--file'); + if (file !== -1 && process.argv[file + 1]) { + fs.writeFileSync(file, JSON.stringify(sources, null, 2)); + } else { + console.log(JSON.stringify(sources, null, 2)); + } + }); +} + +// update version for one adapter +if (process.argv.indexOf('--update') !== -1) { + var pos = process.argv.indexOf('--update'); + if (process.argv[pos + 1]) { + updateVersion(process.argv[pos + 1], function (sources) { + var file = process.argv.indexOf('--file'); + if (file !== -1 && process.argv[file + 1]) { + fs.writeFileSync(file, JSON.stringify(sources, null, 2)); + } else { + console.log(JSON.stringify(sources, null, 2)); + } + }); + } else { + console.warn('Pleas specify name of adapter to update: script.js --update admin'); + process.exit(1); + } +} \ No newline at end of file diff --git a/lib/session.js b/lib/session.js new file mode 100644 index 0000000..de82c9e --- /dev/null +++ b/lib/session.js @@ -0,0 +1,73 @@ +'use strict'; + +module.exports = function (session, defaultTtl) { + var Store = session.Store; + defaultTtl = defaultTtl || 3600; + + function AdapterStore(options) { + var that = this; + + this.adapter = options.adapter; + + options = options || {}; + Store.call(this, options); + + } + + // Object.getPrototypeOf(AdapterStore.prototype) ? + // AdapterStore.prototype.__proto__ = Store.prototype; + AdapterStore.prototype = Object.create(Store.prototype); + + /** + * Attempt to fetch session by the given `sid`. + * + * @param {String} sid + * @param {Function} fn + * @api public + */ + + AdapterStore.prototype.get = function (sid, fn) { + + this.adapter.getSession(sid, function (obj) { + if (obj) { + if (fn) return fn(null, obj); + } else { + if (fn) return fn(); + } + }); + }; + + /** + * Commit the given `sess` object associated with the given `sid`. + * + * @param {String} sid + * @param {Session} sess + * @param {Function} fn + * @api public + */ + AdapterStore.prototype.set = function (sid, ttl, sess, fn) { + if (typeof ttl === 'object') { + fn = sess; + sess = ttl; + ttl = defaultTtl; + } + + ttl = ttl || defaultTtl; + this.adapter.setSession(sid, ttl, sess, function () { + if (fn) fn.apply(this, arguments); + }); + }; + + /** + * Destroy the session associated with the given `sid`. + * + * @param {String} sid + * @api public + */ + + AdapterStore.prototype.destroy = function (sid, fn) { + this.adapter.destroySession(sid, fn); + }; + + return AdapterStore; +}; \ No newline at end of file diff --git a/lib/setup.js b/lib/setup.js new file mode 100644 index 0000000..17426cf --- /dev/null +++ b/lib/setup.js @@ -0,0 +1,3167 @@ +/** + * + * js-controller Controller start/stop and install script + * + * 7'2014-2018 bluefox + * 2014 hobbyquaker + * + */ + +/* jshint -W097 */ +/* jshint strict:false */ +/* jslint node: true */ +'use strict'; + +// TODO need info about progress of stopping + +var fs = require('fs'); +var tools = require(__dirname + '/tools'); + +require('events').EventEmitter.prototype._maxListeners = 100; +process.setMaxListeners(0); + +var yargs; + +function initYargs() { + yargs = require('yargs') + .usage('Commands:\n' + + tools.appName + ' setup [--objects ] [--states ] [custom]\n' + + tools.appName + ' start\n' + + tools.appName + ' stop\n' + + tools.appName + ' start \n' + + tools.appName + ' stop \n' + + tools.appName + ' start all\n' + + tools.appName + ' restart\n' + + tools.appName + ' restart \n' + + tools.appName + ' add [desiredNumber] [--enabled] [--host ] [--port ]\n' + + tools.appName + ' install \n' + + tools.appName + ' url []\n' + + tools.appName + ' del \n' + + tools.appName + ' del .\n' + + tools.appName + ' update [repository url] [--updatable/--u] [--installed/--i]\n' + + tools.appName + ' upgrade [repository url]\n' + + tools.appName + ' upgrade self [repository url]\n' + + tools.appName + ' upgrade [repository url]\n' + + tools.appName + ' upload \n' + + tools.appName + ' upload all\n' + + tools.appName + ' upload \n' + + tools.appName + ' object get \n' + + tools.appName + ' object del \n' + + tools.appName + ' object chmod [state-mode] \n' + + tools.appName + ' object chown \n' + + tools.appName + ' object list \n' + + tools.appName + ' state get \n' + + tools.appName + ' state getplain \n' + + tools.appName + ' state set [ack]\n' + + tools.appName + ' state del \n' + + tools.appName + ' message [.instanceid] []\n' + + tools.appName + ' list [filter]\n' + + tools.appName + ' chmod \n' + + tools.appName + ' chown \n' + + tools.appName + ' touch \n' + + tools.appName + ' rm \n' + + tools.appName + ' file read <' + tools.appName + '-path-to-read> []\n' + + tools.appName + ' file write <' + tools.appName + '-path-to-read> \n' + + tools.appName + ' user add [--ingroup group] [--password pass]\n' + + tools.appName + ' user del \n' + + tools.appName + ' user passwd [--password pass]\n' + + tools.appName + ' user enable \n' + + tools.appName + ' user disable \n' + + tools.appName + ' user get \n' + + tools.appName + ' user check [--password pass]\n' + + tools.appName + ' group add \n' + + tools.appName + ' group del \n' + + tools.appName + ' group list \n' + + tools.appName + ' group enable \n' + + tools.appName + ' group disable \n' + + tools.appName + ' group get \n' + + tools.appName + ' group adduser \n' + + tools.appName + ' group deluser \n' + + tools.appName + ' set . [--port port] [--ip address] [--ssl true|false]\n' + + tools.appName + ' license \n' + + tools.appName + ' clean\n' + + tools.appName + ' backup\n' + + tools.appName + ' restore \n' + + tools.appName + ' --timeout 5000\n' + + tools.appName + ' status\n' + + tools.appName + ' repo [name]\n' + + tools.appName + ' repo add \n' + + tools.appName + ' repo set \n' + + tools.appName + ' repo del \n' + + tools.appName + ' uuid\n' + + tools.appName + ' unsetup\n' + + tools.appName + ' multihost [--secure true|false]\n' + + tools.appName + ' multihost browse\n' + + tools.appName + ' multihost connect\n' + + tools.appName + ' version [adapter]\n' + + tools.appName + ' [adapter] -v\n') + //.default('objects', '127.0.0.1') + //.default('states', '127.0.0.1') + //.default('lang', 'en') + ; + return yargs; +} + +function showHelp(_yargs) { + if (_yargs) { + _yargs.showHelp(); + } else if (yargs) { + yargs.showHelp(); + } +} + +var Objects; // constructor +var objects; // instance +var States; // constructor +var states; // instance + +// params can have: +// pretty, force, password, ingroup, v, version, timeout, +// enabled, disabled, port, ssl, ip, updatable, host, enabled, port, +// objects, states +function processCommand(command, args, params, callback) { + if (typeof args === 'function') { + callback = args; + args = null; + } + if (typeof params === 'function') { + callback = params; + params = null; + } + if (!params) params = {}; + if (!args) args = []; + if (!callback) callback = processExit; + + switch (command) { + + case 'start': + case 'stop': + (function () { + // Start stop of adapter + if (args[0]) { + Objects = require(__dirname + '/objects'); + var adapter = args[0]; + // If user accidentally wrote tools.appName.adapter => remove adapter + var regExp = new RegExp('^' + tools.appName + '\\.', 'i'); + if (adapter && regExp.test(adapter)) { + adapter = adapter.substring(tools.appName.length + 1); + } + + dbConnect(params, function () { + if (adapter === 'all') { + objects.getObjectList({startkey: 'system.adapter.', endkey: 'system.adapter.\u9999'}, function (err, objs) { + var count = 0; + for (var i = 0; i < objs.rows.length; i++) { + if (objs.rows[i].value.type !== 'instance') continue; + var obj = objs.rows[i].value; + if (command === 'start') { + if (!obj.common.enabled) { + obj.common.enabled = true; + count++; + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = new Date().getTime(); + objects.setObject(obj._id, obj, function () { + console.log('Adapter "' + adapter + '" started.'); + if (!--count) callback(); + }); + } + } else { + if (obj.common.enabled) { + obj.common.enabled = false; + count++; + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = new Date().getTime(); + objects.setObject(obj._id, obj, function () { + console.log('Adapter "' + adapter + '" stopped.'); + if (!--count) callback(); + }); + } + } + } + if (!count) callback(); + }); + } else { + if (adapter.indexOf('.') === -1) { + objects.getObjectList({startkey: 'system.adapter.' + adapter + '.', endkey: 'system.adapter.' + adapter + '.\u9999'}, function (err, objs) { + var obj; + if (!err && objs) { + for (var i = 0; i < objs.rows.length; i++) { + if (objs.rows[i].value.type !== 'instance') continue; + if (obj) { + console.log('Please enter instance of adapter, e.g. "' + obj._id.replace('system.adapter.', '') + '"'); + callback(1); + } + obj = objs.rows[i].value; + } + } + if (!obj) { + console.log('Cannot find any instances of "' + adapter + '"'); + callback(1); + } else { + if (command === 'start') { + if (!obj.common.enabled) { + obj.common.enabled = true; + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = new Date().getTime(); + objects.setObject(obj._id, obj, function () { + console.log('Adapter "' + obj._id.replace('system.adapter.', '') + '" started.'); + callback(); + }); + } else { + callback(); + } + } else { + if (obj.common.enabled) { + obj.common.enabled = false; + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = new Date().getTime(); + objects.setObject(obj._id, obj, function () { + console.log('Adapter "' + obj._id.replace('system.adapter.', '') + '" stopped.'); + callback(); + }); + } else { + callback(); + } + } + } + }); + } else { + objects.getObject('system.adapter.' + adapter, function (err, obj) { + if (!err && obj) { + if (command === 'start') { + obj.common.enabled = true; + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = new Date().getTime(); + objects.setObject('system.adapter.' + adapter, obj, function () { + console.log('Adapter "' + adapter + '" started.'); + callback(); + }); + } else { + obj.common.enabled = false; + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = new Date().getTime(); + objects.setObject('system.adapter.' + adapter, obj, function () { + console.log('Adapter "' + adapter + '" stopped.'); + callback(); + }); + } + } else { + console.log('Adapter "' + adapter + '" does not exist.'); + callback(24); + } + }); + } + } + }); + } else { + var memoryLimitMB = 0; + try { + var config = require(tools.getConfigFileName()); + if (config && config.system && config.system.memoryLimitMB) { + memoryLimitMB = parseInt(config.system.memoryLimitMB, 10); + } + } catch (err) { + console.warn('Cannot read memoryLimitMB'); + console.warn('May be config file does not exist.\nPlease call "' + tools.appName + ' setup first" to initialize the settings.') + } + var startObj = { + main: '../controller.js', + name: tools.appName + ' controller', + pidfile: __dirname + '/' + tools.appName + '.pid', + cwd: '../', + stopTimeout: 6000 + }; + if (memoryLimitMB) startObj.args = '--max-old-space-size=' + memoryLimitMB; + + var daemon = require('daemonize2').setup(startObj); + daemon.on('error', function (error) { + console.log('Error: ' + error.message); + }); + daemon.on('stopped', function () { + // start KILLALL script if something still runs + if (command === 'stop' && !require('os').platform().match(/^win/)) { + var data = ''; + fs.chmodSync(__dirname + '/../killall.sh', '777'); + var child = require('child_process').spawn(__dirname + '/../killall.sh', []); + child.stdout.on('data', function (data) { + data += data.toString().replace('\n', ''); + }); + child.stderr.on('data', function (data) { + data += data.toString().replace('\n', ''); + }); + child.on('exit', function (exitCode) { + console.log('Exit code for "killall.sh": ' + exitCode); + callback(); + }); + } + }); + daemon[command](); + } + })(); + break; + + case 'status': + case 'isrun': + (function () { + dbConnect(params, function (objects, states, isOffline) { + if (isOffline) { + console.log(tools.appName + ' is not running'); + callback(100); + } else { + console.log(tools.appName + ' is running'); + callback(); + } + }); + })(); + break; + + case 'r': + case 'restart': + (function () { + if (args[0]) { + Objects = require(__dirname + '/objects'); + var adapter = args[0]; + // If user accidentally wrote tools.appName.adapter => remove adapter + var regExp = new RegExp('^' + tools.appName + '\\.', 'i'); + if (adapter && regExp.test(adapter)) { + adapter = adapter.substring(tools.appName.length + 1); + } + + dbConnect(params, function () { + + if (adapter.indexOf('.') === -1) { + objects.getObjectList({startkey: 'system.adapter.' + adapter + '.', endkey: 'system.adapter.' + adapter + '.\u9999'}, function (err, objs) { + var obj; + if (!err && objs) { + for (var i = 0; i < objs.rows.length; i++) { + if (objs.rows[i].value.type !== 'instance') continue; + if (obj) { + console.log('Please enter instance of adapter, e.g. "' + obj._id.replace('system.adapter.', '') + '"'); + callback(1); + } + obj = objs.rows[i].value; + } + } + if (!obj) { + console.log('Cannot find any instances of "' + adapter + '"'); + callback(1); + } else { + obj.common.enabled = true; + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = new Date().getTime(); + objects.setObject(obj._id, obj, function (err) { + console.log('Adapter "' + obj._id.replace('system.adapter.', '') + '" restarted.'); + callback(); + }); + } + }); + } else { + objects.getObject('system.adapter.' + adapter, function (err, obj) { + if (!err && obj) { + obj.common.enabled = true; + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = new Date().getTime(); + objects.setObject('system.adapter.' + adapter, obj, function () { + console.log('Adapter "' + adapter + '" restarted.'); + callback(); + }); + } else { + console.log('Adapter "' + adapter + '" does not exist.'); + callback(24); + } + }); + } + }); + } else { + var memoryLimitMB = 0; + try { + var config = require(tools.getConfigFileName()); + if (config && config.system && config.system.memoryLimitMB) { + memoryLimitMB = parseInt(config.system.memoryLimitMB, 10); + } + } catch (err) { + console.warn('Cannot read memoryLimitMB'); + } + var startObj = { + main: '../controller.js', + name: tools.appName + ' controller', + pidfile: __dirname + '/' + tools.appName + '.pid', + cwd: '../', + stopTimeout: 1000 + }; + if (memoryLimitMB) startObj.args = '--max-old-space-size=' + memoryLimitMB; + + var daemon = require('daemonize2').setup(startObj); + daemon.on('stopped', function () { + daemon.start(); + }).on('notrunning', function () { + daemon.start(); + }).on('error', function (error) { + console.log('Error: ' + error.message); + }); + daemon.stop(); + } + })(); + break; + + case '_restart': + restartController(function () { + callback(); + }); + break; + + case 'update': + (function () { + Objects = require(__dirname + '/objects'); + var repoUrl = args[0]; // Repo url or name + dbConnect(params, function () { + var Repo = require(__dirname + '/setup/setupRepo.js'); + var repo = new Repo({ + objects: objects + }); + + repo.showRepo(repoUrl, params, function () { + setTimeout(function () { + callback(); + }, 2000); + }); + }); + })(); + break; + + case 'setup': + (function () { + if (args[0] === 'custom') { + var readline = require('readline'); + + var config; + // read actual configuration + try { + if (fs.existsSync(tools.getConfigFileName())) { + config = JSON.parse(fs.readFileSync(tools.getConfigFileName(), 'utf8')); + } else { + config = require(__dirname + '/../conf/' + tools.appName + '-dist.json'); + } + } catch (e) { + config = require(__dirname + '/../conf/' + tools.appName + '-dist.json'); + } + + var rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + rl.question('Type of objects DB [(f)ile, (c)ouch, (r)edis], default [file]: ', function (otype) { + if (!otype) { + otype = 'file'; + } else { + otype = (otype || '').toLowerCase(); + + if (otype === 'r') otype = 'redis'; + if (otype === 'f') otype = 'file'; + if (otype === 'c') otype = 'couch'; + + if (otype !== 'file' && otype !== 'couch' && otype !== 'redis') { + console.log('Unknown objects type: ' + otype); + callback(23); + } + } + rl.question('Host / Unix Socket of objects DB(' + otype + '), default[127.0.0.1]: ', function (ohost) { + if (!ohost) { + ohost = '127.0.0.1'; + } else { + ohost = (ohost || '').toLowerCase(); + } + var op; + + if (otype === 'file') { + op = 9001; + } else if (otype === 'redis') { + op = 6379; + } else if (otype === 'couch') { + op = 5984; + } + + rl.question('Port of objects DB(' + otype + '), default[' + op + ']: ', function (oport) { + var ot; + if (!oport) { + if (otype === 'file') { + oport = 9001; + ot = 'file'; + } else if (otype === 'redis') { + ot = 'redis'; + oport = 6379; + } else if (otype === 'couch') { + ot = 'couch'; + oport = 5984; + } + } else { + oport = parseInt(oport, 10); + if (isNaN(oport)) { + console.log('Invalid objects port: ' + oport); + callback(23); + } + } + rl.question('Type of states DB [(f)file, (r)edis], default [' + ot + ']: ', function (stype) { + if (!stype) { + stype = ot; + } else { + stype = (stype || '').toLowerCase(); + + if (stype === 'r') stype = 'redis'; + if (stype === 'f') stype = 'file'; + + if (stype !== 'file' && stype !== 'redis') { + console.log('Unknown states type: ' + stype); + callback(23); + } + } + + rl.question('Host / Unix Socket of states DB (' + stype + '), default[' + ohost + ']: ', function (shost) { + if (!shost) { + shost = ohost; + } else { + shost = (shost || '').toLowerCase(); + } + var sp; + + if (stype === 'file') { + sp = 9000; + } else if (stype === 'redis') { + sp = 6379; + } + + rl.question('Port of states DB (' + stype + '), default[' + sp + ']: ', function (sport) { + if (!sport) { + if (stype === 'file') { + sport = 9000; + } else if (stype === 'redis') { + sport = 6379; + } + } else { + sport = parseInt(sport, 10); + if (isNaN(sport)) { + console.log('Invalid states port: ' + sport); + callback(23); + } + } + if ((stype === 'file' && (shost === 'localhost' || shost === '127.0.0.1')) || + (otype === 'file' && (ohost === 'localhost' || ohost === '127.0.0.1'))) { + rl.question('Data directory (file), default[../' + tools.getDefaultDataDir() + ']: ', function (dir) { + if (!dir) dir = tools.getDefaultDataDir(); + + rl.question('Host name of this machine [' + require('os').hostname() + ']: ', function (hname) { + if (!hname) { + hname = ''; + } else { + hname = (hname || ''); + if (hname.match(/\s/)) { + console.log('Invalid host name: ' + hname); + callback(23); + } + } + rl.close(); + console.log('creating conf/' + tools.appName + '.json'); + config.system = config.system || {}; + config.system.hostname = hname; + config.objects.host = ohost; + config.objects.type = otype; + config.objects.port = oport; + if (config.objects.type === 'file') config.objects.dataDir = dir; + config.states.host = shost; + config.states.type = stype; + config.states.port = sport; + if (config.states.type === 'file') config.states.dataDir = dir; + fs.writeFileSync(tools.getConfigFileName(), JSON.stringify(config, null, 2)); + }); + }); + } else { + rl.question('Host name of this machine [' + require('os').hostname() + ']: ', function (hname) { + if (!hname) { + hname = ''; + } else { + hname = (hname || ''); + if (hname.match(/\s/)) { + console.log('Invalid host name: ' + hname); + callback(23); + } + } + rl.close(); + console.log('creating conf/' + tools.appName + '.json'); + config.system = config.system || {}; + config.system.hostname = hname; + config.objects.host = ohost; + config.objects.type = otype; + config.objects.port = oport; + config.states.host = shost; + config.states.type = stype; + config.states.port = sport; + config.states.dataDir = undefined; + config.objects.dataDir = undefined; + fs.writeFileSync(tools.getConfigFileName(), JSON.stringify(config, null, 2)); + }); + } + }); + }); + }); + }); + }); + }); + } else { + var Setup = require(__dirname + '/setup/setupSetup.js'); + var setup = new Setup({ + dbConnect: dbConnect, + processExit: callback, + params: params + }); + var i = 0; + var isFirst; + var isRedis; + while (args[i] !== undefined) { + if (args[i] === 'first' || args[i] === '--first') { + isFirst = true; + } else if (args[i] === 'redis' || args[i] === '--redis') { + isRedis = true; + } + i++; + } + + setup.setup(function (isFirst, isRedis) { + if (isFirst) { + var Install = require(__dirname + '/setup/setupInstall.js'); + var install = new Install({ + objects: objects, + states: states, + installNpm: installNpm, + getRepository: getRepository, + processExit: callback, + params: params + }); + + //install admin adapter + install.createInstance('admin', {enabled: true, ignoreIfExists: true}, function () { + // check if discovery is installed too + try { + var path = require.resolve(tools.appName + '.discovery'); + if (path) { + install.createInstance('discovery', {enabled: true, ignoreIfExists: true}, function () { + callback(); + }); + } + } catch (e) { + // no discovery found + callback(); + } + }); + } else { + callback(); + } + }, isFirst, isRedis); + } + })(); + break; + + case 'url': + (function () { + Objects = require(__dirname + '/objects'); + + var url = args[0]; + var name = args[1]; + + if (url[0] === '"' && url[url.length - 1] === '"') { + url = url.substring(1, url.length - 1); + } + // try to fix URL + if (url.match(/^https:\/\/github\.com\//)) { + url = url.replace(/\.git$/, ''); + if (!url.match(/\.zip$/) && !url.match(/\.gz$/) && !url.match(/\/tarball\/[^\/]+$/)) { + url += '/tarball/master'; + } + } + console.log('install ' + url); + + dbConnect(params, function () { + var Install = require(__dirname + '/setup/setupInstall.js'); + var install = new Install({ + objects: objects, + states: states, + installNpm: installNpm, + getRepository: getRepository, + processExit: callback, + params: params + }); + + install.npmInstall(url, true, false, function (_url, installDir) { + var Upload = require(__dirname + '/setup/setupUpload.js'); + var upload = new Upload({ + states: states, + objects: objects, + processExit: callback + }); + + // Try to extract name from URL + if (!name) { + if (url.match(/\.tgz$|\.zip/)) { + var parts = url.split('/'); + var last = parts.pop(); + var mm = last.match(/\.([-_\w\d]+)\-[.\d]+/); + if (mm) { + name = mm[1]; + } + } else { + var reG = new RegExp(tools.appName + '\\.([-_\\w\\d]+)\\/'); + var m = reG.exec(url); + if (m) { + name = m[1]; + } else { + var reg = new RegExp(tools.appName.toLowerCase() + '\\.([-_\\w\\d]+)\\/'); + m = reg.exec(url); + if (m) name = m[1]; + } + } + } + + if (name) { + upload.uploadAdapter(name, true, true, function () { + upload.uploadAdapter(name, false, true, function () { + callback(); + }); + }); + } else { + // Try to find io-package.json with newest date + var dirs = fs.readdirSync(installDir); + var date = null; + var dir = null; + for (var i = 0; i < dirs.length; i++) { + if (fs.existsSync(installDir + '/' + dirs[i] + '/io-package.json')) { + var stat = fs.statSync(installDir + '/' + dirs[i] + '/io-package.json'); + if (!date || stat.mtime.getTime() > date.getTime()) { + dir = dirs[i]; + date = stat.mtime; + } + } + } + // if modify time is not older than one hour + if (dir && (new Date()).getTime() - date.getTime() < 3600000) { + name = dir.substring(tools.appName.length + 1); + upload.uploadAdapter(name, true, true, function () { + upload.uploadAdapter(name, false, true, function () { + callback(); + }); + }); + } else { + callback(); + } + } + }); + }); + })(); + break; + + case 'a': + case 'add': + case 'install': + case 'i': + (function () { + Objects = require(__dirname + '/objects'); + + let name = args[0]; + let instance = args[1]; + let repoUrl = args[2]; + + if (parseInt(instance, 10).toString() !== (instance || '').toString()) { + repoUrl = instance; + instance = null; + } + if (parseInt(repoUrl, 10).toString() === (repoUrl || '').toString()) { + const temp = instance; + instance = repoUrl; + repoUrl = temp; + } + if (parseInt(instance, 10).toString() === (instance || '').toString()) { + instance = parseInt(instance, 10); + params.instance = instance; + } + + // If user accidentally wrote tools.appName.adapter => remove adapter + const regExp = new RegExp('^' + tools.appName + '\\.', 'i'); + if (name && regExp.test(name)) { + name = name.substring(tools.appName.length + 1); + } + + let adapterDir = tools.getAdapterDir(name); + + dbConnect(params, function () { + const Install = require(__dirname + '/setup/setupInstall.js'); + let install = new Install({ + objects: objects, + states: states, + installNpm: installNpm, + getRepository: getRepository, + processExit: callback, + params: params + }); + + if (!fs.existsSync(adapterDir)) { + install.downloadPacket(repoUrl, name, null, function () { + if (command !== 'install' && command !== 'i') { + install.createInstance(name, params, function () { + callback(); + }); + } else { + const Upload = require(__dirname + '/setup/setupUpload.js'); + let upload = new Upload({ + states: states, + objects: objects, + processExit: callback + }); + + // create objects + install.uploadStaticObjects(name, function () { + upload.uploadAdapter(name, true, true, function () { + upload.uploadAdapter(name, false, true, function () { + callback(); + }); + }); + }); + } + }); + } else { + if (command !== 'install' && command !== 'i') { + install.createInstance(name, params, function () { + callback(); + }); + } else { + console.log('adapter "' + name + '" yet installed. Use "upgrade" to install newer version.'); + callback(51); + } + } + }); + })(); + break; + + case 'upload': + case 'u': + (function () { + Objects = require(__dirname + '/objects'); + var name = args[0]; + var subTree = args[1]; + if (name) { + dbConnect(params, function () { + var Upload = require(__dirname + '/setup/setupUpload.js'); + var upload = new Upload({ + states: states, + objects: objects, + processExit: callback + }); + + if (name === 'all') { + objects.getObjectList({startkey: 'system.adapter.', endkey: 'system.adapter.\u9999'}, function (err, objs) { + var adapters = []; + for (var i = 0; i < objs.rows.length; i++) { + if (objs.rows[i].value.type !== 'adapter') continue; + adapters.push(objs.rows[i].value.common.name); + } + + upload.uploadAdapterFull(adapters, function () { + callback(); + }); + }); + } else { + // if upload of file + if (name.indexOf('.') !== -1) { + if (!subTree) { + console.log('Please specify target name, like:\n ' + tools.appName + ' upload /file/picture.png /vis.0/main/img/picture.png'); + callback(1); + } + + upload.uploadFile(name, subTree, function (err, newName) { + if (!err) console.log('File "' + name + '" is successfully saved under ' + newName); + callback(err ? 40 : undefined); + }); + } else { + if (subTree) { + upload.uploadAdapter(name, false, true, subTree, function () { + callback(); + }); + } else { + upload.uploadAdapter(name, true, true, function () { + upload.upgradeAdapterObjects(name, function () { + upload.uploadAdapter(name, false, true, function () { + callback(); + }); + }); + }); + } + } + } + }); + } else { + console.log('No adapter name found!'); + showHelp(); + callback(1); + } + })(); + break; + + case 'delete': + case 'del': + (function () { + var adpr = args[0]; + var instance = args[1]; + + // If user accidentally wrote tools.appName.adapter => remove adapter + var regExp = new RegExp('^' + tools.appName + '\\.', 'i'); + if (adpr && regExp.test(adpr)) { + adpr = adpr.substring(tools.appName.length + 1); + } + if (!adpr) { + showHelp(); + callback(2); + } + + if (adpr && adpr.indexOf('.') !== -1) { + var parts = adpr.split('.'); + adpr = parts[0]; + instance = parts[1]; + } + + if (instance || instance === 0) { + dbConnect(params, function () { + var Install = require(__dirname + '/setup/setupInstall.js'); + var install = new Install({ + objects: objects, + states: states, + installNpm: installNpm, + getRepository: getRepository, + processExit: callback, + params: params + }); + + console.log('Delete adapter "' + adpr + '.' + instance + '"'); + install.deleteInstance(adpr, instance, function () { + callback(); + }); + }); + } else { + dbConnect(params, function () { + var Install = require(__dirname + '/setup/setupInstall.js'); + var install = new Install({ + objects: objects, + states: states, + installNpm: installNpm, + getRepository: getRepository, + processExit: callback, + params: params + }); + console.log('Delete adapter "' + adpr + '"'); + install.deleteAdapter(adpr, function (a, resultCode) { + callback(resultCode); + }); + }); + } + })(); + break; + + case 'unsetup': + (function () { + const rl = require('readline').createInterface({ + input: process.stdin, + output: process.stdout + }); + rl.question('UUID will be deleted. Are you sure? [y/N]: ', function (answer) { + rl.close(); + answer = answer.toLowerCase(); + if (answer === 'y' || answer === 'yes' || answer === 'ja' || answer === 'j') { + dbConnect(params, function () { + objects.delObject('system.meta.uuid', function (err) { + if (err) { + console.log('uuid cannot be deleted: ' + err); + } else { + console.log('system.meta.uuid deleted'); + } + objects.getObject('system.config', function (err, obj) { + if (obj.common.licenseConfirmed || obj.common.language || (obj.native && obj.native.secret)) { + obj.common.licenseConfirmed = false; + obj.common.language = ''; + if (obj.native) delete obj.native.secret; + + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = new Date().getTime(); + + objects.setObject('system.config', obj, function (err) { + if (err) { + console.log('not found: ' + err); + callback(3); + } else { + console.log('system.config reset'); + callback(); + } + }); + } else { + console.log('system.config is OK'); + callback(); + } + }); + }); + }); + } else { + console.log('Nothing deleted'); + callback(); + } + }); + }()); + break; + + case 'o': + case 'object': + (function () { + var cmd = args[0]; + var id = args[1]; + var pattern; + + if (cmd === 'chmod') { + var modeObject = args[1]; + var modeState = args[2]; + pattern = args[3]; + + if (!modeObject) { + console.log('No mode found. Example: "object chmod 644 system.*"'); + callback(1); + return; + } else { + //yargs has converted it to number + modeObject = parseInt(modeObject.toString(), 16); + + if (modeState) { + modeState = modeState.toString(); + if (modeState[0] < '0' || modeState > '7') { + pattern = modeState; + modeState = undefined + } else { + modeState = parseInt(modeState.toString(), 16); + } + } + } + if (!pattern) { + console.log('No pattern found. Example: "object chmod 644 system.*"'); + callback(1); + return; + } + dbConnect(params, function () { + objects.chmodObject(pattern, {user: 'system.user.admin', object: modeObject, state: modeState}, function (err, processed) { + if (err) { + console.error(err); + } else { + if (processed) { + var List = require(__dirname + '/setup/setupList.js'); + var list = new List({ + states: states, + objects: objects, + processExit: callback + }); + list.showObjectHeader(); + for (var i = 0; i < processed.length; i++) { + list.showObject(processed[i]); + } + } + } + setTimeout(function () { + callback(); + }, 1000); + }); + }); + } else + if (cmd === 'chown') { + var user = args[1]; + var group = args[2]; + pattern = args[3]; + + if (!pattern) { + pattern = group; + group = undefined; + } + + if (!user) { + console.log('No user found. Example: "object chown user system.*"'); + callback(1); + } else if (user.substring(12) !== 'system.user.') { + user = 'system.user.' + user; + } + if (group && group.substring(13) !== 'system.group.') { + group = 'system.group.' + group; + } + + if (!pattern) { + console.log('No file path found. Example: "object chown user system.*"'); + callback(1); + return; + } + dbConnect(params, function () { + objects.chownObject(pattern, {user: 'system.user.admin', owner: user, ownerGroup: group}, function (err, processed) { + if (err) { + console.error(err); + } else { + if (processed) { + var List = require(__dirname + '/setup/setupList.js'); + var list = new List({ + states: states, + objects: objects, + processExit: callback + }); + list.showObjectHeader(); + for (var i = 0; i < processed.length; i++) { + list.showObject(processed[i]); + } + } + } + setTimeout(function () { + callback(); + }, 1000); + }); + }); + } else + if (cmd === 'list' || cmd === 'l') { + pattern = args[1]; + + if (pattern) { + pattern = {startkey: pattern.replace('*', ''), endkey: pattern.replace('*', '\u9999')}; + } + + dbConnect(params, function () { + objects.getObjectList(pattern, {user: 'system.user.admin', sorted: true}, function (err, processed) { + if (err) { + console.error(err); + callback(33); + } + if (processed) { + var List = require(__dirname + '/setup/setupList.js'); + var list = new List({ + states: states, + objects: objects, + processExit: callback + }); + list.showObjectHeader(); + for (var id = 0; id < processed.rows.length; id++) { + list.showObject(processed.rows[id].value); + } + } + setTimeout(function () { + callback(); + }, 1000); + }); + }); + } else + if (id) { + Objects = require(__dirname + '/objects'); + + if (cmd === 'get') { + dbConnect(params, function () { + objects.getObject(id, function (err, res) { + if (err || !res) { + console.log('not found'); + callback(3); + } else { + if (params.pretty) { + console.log(JSON.stringify(res, null, 2)); + } else { + console.log(JSON.stringify(res)); + } + callback(); + } + }); + }); + } else + if (cmd === 'del' || cmd === 'delete') { + dbConnect(params, function () { + objects.delObject(id, function (err) { + if (err) { + console.log('not found: ' + err); + callback(3); + } else { + console.log(id + ' deleted'); + callback(); + } + }); + }); + } else { + console.log('Unknown command or empty: "' + cmd + '"'); + callback(3); + } + } + })(); + break; + + case 's': + case 'state': + (function () { + var cmd = args[0]; + var id = args[1]; + if (id) { + dbConnect(params, function () { + if (cmd === 'get') { + states.getState(id, function (err, obj) { + if (err || !obj) { + console.log('Error: ' + err); + } else { + if (params.pretty) { + console.log(JSON.stringify(obj, null, 2)); + } else { + console.log(JSON.stringify(obj)); + } + } + callback(); + }); + } + else if (cmd === 'getplain') { + states.getState(id, function (err, obj) { + if (err || !obj) { + console.log('Error: ' + err); + } else { + if (obj) { + console.log(obj.val); + console.log(obj.ack); + console.log(obj.from); + console.log(obj.ts); + console.log(obj.lc); + } else { + console.log(null); + } + } + callback(); + }); + } + else if (cmd === 'set') { + var val = args[2]; + var ack = args[3]; + if (val === undefined) { + console.log('Invalid format: No value found.'); + showHelp(); + callback(); + } else { + if (ack === undefined) { + console.log('Set "' + id + '" with value: ' + val); + states.setState(id, val, function () { + callback(); + }); + } else { + console.log('Set "' + id + '" with value: ' + val + ' and ack flag ' + ack); + states.setState(id, {val: val, ack: ack}, function () { + callback(); + }); + } + } + } + else if (cmd === 'del' || cmd === 'delete') { + states.delState(id, function (err) { + if (err) { + console.log('not found: ' + err); + callback(3); + } else { + console.log(id + ' deleted'); + callback(); + } + }); + } + else if (cmd === 'chmod' || cmd === 'chwon') { + console.log('Please use object command for that: "' + tools.appName + ' object ' + cmd + ' ' + (args[1] || '') + ' ' + (args[1] || '') + ' ' + (args[2] || '') + '"'); + callback(1); + } + else { + console.log('Invalid format: unknown state command'); + showHelp(); + callback(1); + } + }); + } else { + console.log('Invalid format: no id found'); + showHelp(); + callback(1); + } + })(); + break; + + case 'msg': + case 'message': + (function () { + var adapter = args[0]; + var instances = []; + if (adapter) { + if (adapter.indexOf('.') !== -1) { + instances.push('system.adapter.' + adapter); + } + + dbConnect(params, function () { + objects.getObjectView('system', 'instance', {startkey: 'system.adapter.' + adapter, endkey: 'system.adapter.' + adapter + '.\u9999'}, null, function (err, res) { + if (res && res.rows.length) { + if (instances.length === 0) { + for (var t = 0; t < res.rows.length; t++) { + instances.push(res.rows[t].id); + } + } + var cmd = args[1]; + var msg = args[2]; + if (!msg) { + msg = cmd; + cmd = 'send'; + } + if (msg && (typeof msg === 'string') && (msg[0] === '{') && (msg[msg.length - 1] === '}')) { + msg = JSON.parse(msg); + } + + if (!msg && msg !== 0) { + console.log('Invalid format: No message found.'); + showHelp(); + callback(); + } else { + var exitCount = instances.length; + for (var i = 0; i < instances.length; i++) { + console.log('Send command "' + cmd + '" to ' + instances[i] + ' with "' + msg + '"'); + states.pushMessage(instances[i], {command: cmd, message: msg, from: 'setup'}, function() { + exitCount--; + if (exitCount === 0) callback(); + }); + } + } + } else { + console.log('No one instance of adapter "' + adapter + '" found'); + callback(4); + } + }); + }); + } else { + console.log('Invalid format: no adapter found'); + showHelp(); + callback(1); + } + })(); + break; + + case 'upgrade': + (function () { + Objects = require(__dirname + '/objects'); + + + var adapter = args[0]; + var repoUrl = args[1]; + var regExp = new RegExp('^' + tools.appName + '\\.', 'i'); + + // If user accidentally wrote tools.appName.adapter => remove adapter + if (adapter && regExp.test(adapter)) { + adapter = adapter.substring(tools.appName.length + 1); + } + + if (adapter && !repoUrl && adapter.indexOf('/') !== -1) { + repoUrl = adapter; + adapter = null; + } + + dbConnect(params, function () { + var Upgrade = require(__dirname + '/setup/setupUpgrade.js'); + var upgrade = new Upgrade({ + objects: objects, + states: states, + installNpm: installNpm, + getRepository: getRepository, + params: params, + processExit: callback, + restartController: restartController + }); + + if (adapter) { + if (adapter === 'self') { + upgrade.upgradeController(repoUrl, params.force, function () { + callback(); + }); + } else { + upgrade.upgradeAdapter(repoUrl, adapter, params.force, function () { + callback(); + }); + } + } else { + getRepository(repoUrl, function (err, links) { + var result = []; + for (var name in links) { + if (links.hasOwnProperty(name)) { + result.push(name); + } + } + if (err) console.log(err); + if (links) { + result.sort(); + upgrade.upgradeAdapterHelper(links, result, 0, false, function () { + upgrade.upgradeController(links, params.force, function () { + callback(); + }); + }); + } else { + // No information + callback(26); + } + }); + } + }); + })(); + break; + + case 'clean': + (function () { + var yes = args[0]; + if (yes !== 'yes') { + console.log('Command "clean" clears all Objects and States. To execute it write "' + tools.appName + ' clean yes"'); + } else { + dbConnect(params, function (obj, stat, isNotRun) { + if (!isNotRun) { + console.error('Stop ' + tools.appName + ' first!'); + callback(1); + return; + } + cleanDatabase(true, function (count) { + console.log('Deleted ' + count + ' states'); + restartController(function () { + console.log('Restarting ' + tools.appName + '...'); + callback(); + }); + }); + }); + } + })(); + break; + + case 'restore': + (function () { + var Backup = require(__dirname + '/setup/setupBackup.js'); + + dbConnect(params, function (obj, stat, isNotRun) { + + if (!isNotRun) { + console.error('Stop ' + tools.appName + ' first!'); + callback(1); + return; + } + + var backup = new Backup({ + states: states, + objects: objects, + cleanDatabase: cleanDatabase, + restartController: restartController, + processExit: callback + }); + + backup.restoreBackup(args[0], function () { + console.log("System successfully restored!"); + callback(0); + }); + }); + })(); + break; + + case 'backup': + (function () { + var name = args[0]; + var Backup = require(__dirname + '/setup/setupBackup.js'); + + dbConnect(params, function () { + var backup = new Backup({ + states: states, + objects: objects, + cleanDatabase: cleanDatabase, + restartController: restartController, + processExit: callback + }); + + backup.createBackup(name, function (filePath) { + console.log('Backup created: ' + filePath); + callback(0); + }); + }); + })(); + break; + + case 'l': + case 'list': + (function () { + dbConnect(params, function () { + var List = require(__dirname + '/setup/setupList.js'); + var list = new List({ + states: states, + objects: objects, + processExit: callback + }); + list.list(args[0], args[1], params); + }); + })(); + break; + + case 'touch': + (function () { + var pattern = args[0]; + + if (!pattern) { + console.log('No file path found. Example: "touch /vis.0/main/*"'); + callback(1); + return; + } + dbConnect(params, function () { + // extract id + pattern = pattern.replace(/\\/g, '/'); + if (pattern[0] === '/') pattern = pattern.substring(1); + + if (pattern === '*') { + objects.getObjectList({startkey: 'system.adapter.', endkey: 'system.adapter.\u9999'}, function (err, arr) { + if (!err && arr && arr.rows) { + var files = []; + var count = 0; + for (var i = 0; i < arr.rows.length; i++) { + if (arr.rows[i].value.type !== 'adapter') continue; + count++; + objects.touch(arr.rows[i].value.common.name, '*', {user: 'system.user.admin'}, function (err, processed, _id) { + if (!err && processed) { + files.push({id: _id, processed: processed}); + } + if (!--count) { + var List = require(__dirname + '/setup/setupList.js'); + var list = new List({ + states: states, + objects: objects, + processExit: callback + }); + files.sort(function (a, b) { + return a.id.localeCompare(b.id); + }); + + for (var k = 0; k < files.length; k++) { + for (var t = 0; t < files[k].processed.length; t++) { + list.showFile(files[k].id, files[k].processed[t].path, files[k].processed[t]); + } + } + setTimeout(function () { + callback(); + }, 1000); + } + }); + } + if (!count) { + console.log('Nothing found'); + callback(); + } + } + }); + } else { + var parts = pattern.split('/'); + var id = parts.shift(); + var path = parts.join('/'); + + objects.touch(id, path, {user: 'system.user.admin'}, function (err, processed) { + if (err) { + console.error(err); + } else { + if (processed) { + var List = require(__dirname + '/setup/setupList.js'); + var list = new List({ + states: states, + objects: objects, + processExit: callback + }); + for (var i = 0; i < processed.length; i++) { + list.showFile(id, processed[i].path, processed[i]); + } + } + } + setTimeout(function () { + callback(); + }, 1000); + }); + + } + }); + })(); + break; + + case 'rm': + (function () { + var pattern = args[0]; + + if (!pattern) { + console.log('No file path found. Example: "touch /vis.0/main/*"'); + callback(1); + return; + } + dbConnect(params, function () { + // extract id + pattern = pattern.replace(/\\/g, '/'); + if (pattern[0] === '/') pattern = pattern.substring(1); + + if (pattern === '*') { + objects.getObjectList({startkey: 'system.adapter.', endkey: 'system.adapter.\u9999'}, function (err, arr) { + if (!err && arr && arr.rows) { + var files = []; + var count = 0; + for (var i = 0; i < arr.rows.length; i++) { + if (arr.rows[i].value.type !== 'adapter') continue; + count++; + objects.rm(arr.rows[i].value.common.name, '*', {user: 'system.user.admin'}, function (err, processed, _id) { + if (!err && processed) { + files.push({id: _id, processed: processed}); + } + if (!--count) { + var List = require(__dirname + '/setup/setupList.js'); + var list = new List({ + states: states, + objects: objects, + processExit: callback + }); + files.sort(function (a, b) { + return a.id.localeCompare(b.id); + }); + + list.showFileHeader(); + for (var k = 0; k < files.length; k++) { + for (var t = 0; t < files[k].processed.length; t++) { + list.showFile(files[k].id, files[k].processed[t].path, files[k].processed[t]); + } + } + setTimeout(function () { + callback(); + }, 1000); + } + }); + } + if (!count) { + console.log('Nothing found'); + callback(); + } + } + }); + } else { + var parts = pattern.split('/'); + var id = parts.shift(); + var path = parts.join('/'); + + objects.rm(id, path, {user: 'system.user.admin'}, function (err, processed) { + if (err) { + console.error(err); + } else { + if (processed) { + var List = require(__dirname + '/setup/setupList.js'); + var list = new List({ + states: states, + objects: objects, + processExit: callback + }); + list.showFileHeader(); + for (var i = 0; i < processed.length; i++) { + list.showFile(id, processed[i].path, processed[i]); + } + } + } + setTimeout(function () { + callback(); + }, 1000); + }); + + } + }); + })(); + break; + + case 'chmod': + (function () { + var mode = args[0]; + var pattern = args[1]; + + if (!mode) { + console.log('No mode found. Example: "chmod 777 /vis.0/main/*"'); + callback(1); + return; + } else { + //yargs has converted it to number + mode = parseInt(mode.toString(), 16); + } + + if (!pattern) { + console.log('No file path found. Example: "chmod 777 /vis.0/main/*"'); + callback(1); + return; + } + dbConnect(params, function () { + // extract id + pattern = pattern.replace(/\\/g, '/'); + if (pattern[0] === '/') pattern = pattern.substring(1); + + if (pattern === '*') { + objects.getObjectList({startkey: 'system.adapter.', endkey: 'system.adapter.\u9999'}, function (err, arr) { + if (!err && arr && arr.rows) { + var files = []; + var count = 0; + for (var i = 0; i < arr.rows.length; i++) { + if (arr.rows[i].value.type !== 'adapter') continue; + count++; + objects.chmodFile(arr.rows[i].value.common.name, '*', {user: 'system.user.admin', mode: mode}, function (err, processed, _id) { + if (!err && processed) { + files.push({id: _id, processed: processed}); + } + if (!--count) { + var List = require(__dirname + '/setup/setupList.js'); + var list = new List({ + states: states, + objects: objects, + processExit: callback + }); + files.sort(function (a, b) { + return a.id.localeCompare(b.id); + }); + + list.showFileHeader(); + for (var k = 0; k < files.length; k++) { + for (var t = 0; t < files[k].processed.length; t++) { + list.showFile(files[k].id, files[k].processed[t].path, files[k].processed[t]); + } + } + setTimeout(function () { + callback(); + }, 1000); + } + }); + } + if (!count) { + console.log('Nothing found'); + callback(); + } + } + }); + } else { + var parts = pattern.split('/'); + var id = parts.shift(); + var path = parts.join('/'); + + objects.chmodFile(id, path, {user: 'system.user.admin', mode: mode}, function (err, processed) { + if (err) { + console.error(err); + } else { + if (processed) { + var List = require(__dirname + '/setup/setupList.js'); + var list = new List({ + states: states, + objects: objects, + processExit: callback + }); + list.showFileHeader(); + for (var i = 0; i < processed.length; i++) { + list.showFile(id, processed[i].path, processed[i]); + } + } + } + setTimeout(function () { + callback(); + }, 1000); + }); + + } + }); + })(); + break; + + case 'chown': + (function () { + var user = args[0]; + var group = args[1]; + var pattern = args[2]; + + if (!pattern) { + pattern = group; + group = undefined; + } + + if (!user) { + console.log('No user found. Example: "chown user /vis.0/main/*"'); + callback(1); + } else if (user.substring(12) !== 'system.user.') { + user = 'system.user.' + user; + } + if (group && group.substring(13) !== 'system.group.') { + group = 'system.group.' + group; + } + + if (!pattern) { + console.log('No file path found. Example: "chown user /vis.0/main/*"'); + callback(1); + return; + } + dbConnect(params, function () { + // extract id + pattern = pattern.replace(/\\/g, '/'); + if (pattern[0] === '/') pattern = pattern.substring(1); + + if (pattern === '*') { + objects.getObjectList({startkey: 'system.adapter.', endkey: 'system.adapter.\u9999'}, function (err, arr) { + if (!err && arr && arr.rows) { + var files = []; + var count = 0; + for (var i = 0; i < arr.rows.length; i++) { + if (arr.rows[i].value.type !== 'adapter') continue; + count++; + objects.chownFile(arr.rows[i].value.common.name, '*', {user: 'system.user.admin', owner: user, ownerGroup: group}, function (err, processed, _id) { + if (!err && processed) { + files.push({id: _id, processed: processed}); + } + if (!--count) { + var List = require(__dirname + '/setup/setupList.js'); + var list = new List({ + states: states, + objects: objects, + processExit: callback + }); + files.sort(function (a, b) { + return a.id.localeCompare(b.id); + }); + + list.showFileHeader(); + for (var k = 0; k < files.length; k++) { + for (var t = 0; t < files[k].processed.length; t++) { + list.showFile(files[k].id, files[k].processed[t].path, files[k].processed[t]); + } + } + setTimeout(function () { + callback(); + }, 1000); + } + }); + } + if (!count) { + console.log('Nothing found'); + callback(); + } + } + }); + } else { + + var parts = pattern.split('/'); + var id = parts.shift(); + var path = parts.join('/'); + + objects.chownFile(id, path, {user: 'system.user.admin', owner: user, ownerGroup: group}, function (err, processed) { + if (err) { + console.error(err); + } else { + // call here list + if (processed) { + var List = require(__dirname + '/setup/setupList.js'); + var list = new List({ + states: states, + objects: objects, + processExit: callback + }); + list.showFileHeader(); + for (var i = 0; i < processed.length; i++) { + list.showFile(id, processed[i].path, processed[i]); + } + } + } + setTimeout(function () { + callback(); + }, 1000); + }); + } + }); + })(); + break; + + case 'user': + (function () { + var command = args[0] || ''; + var user = args[1] || ''; + + if (user && user.match(/^system\.user\./)) user = user.substring('system.user.'.length); + + dbConnect(params, function () { + var Users = require(__dirname + '/setup/setupUsers.js'); + var users = new Users({ + objects: objects, + processExit: callback + }); + var password = params.password; + var group = params.ingroup || 'system.group.administrator'; + + if (command === 'add') { + users.addUserPrompt(user, group, password, function (err) { + if (err) { + console.error(err); + callback(30); + } else { + console.log('User "' + user + '" created (Group: ' + group.replace('system.group.', '') + ')'); + callback(); + } + }); + } + else if (command === 'del' || command === 'delete') { + users.delUser(user, function (err) { + if (err) { + console.error(err); + callback(30); + } else { + console.log('User "' + user + '" deleted'); + callback(); + } + }); + } + else if (command === 'check') { + users.checkUserPassword(user, password, function (err) { + if (err) { + console.error(err); + callback(30); + } else { + console.log('Password for user "' + user + '" matched.'); + callback(); + } + }); + } + else if (command === 'set' || command === 'passwd') { + users.setUserPassword(user, password, function (err) { + if (err) { + console.error(err); + callback(30); + } else { + console.log('Password for "' + user + '" was successfully set.'); + callback(); + } + }); + } + else if (command === 'enable' || command === 'e') { + users.enableUser(user, true, function (err) { + if (err) { + console.error(err); + callback(30); + } else { + console.log('User "' + user + '" was successfully enabled.'); + callback(); + } + }); + } + else if (command === 'disable' || command === 'd') { + users.enableUser(user, false, function (err) { + if (err) { + console.error(err); + callback(30); + } else { + console.log('User "' + user + '" was successfully disabled.'); + callback(); + } + }); + } + else if (command === 'get') { + users.getUser(user, function (err, isEnabled) { + if (err) { + console.error(err); + callback(30); + } else { + console.log('User "' + user + '" is ' + (isEnabled ? 'enabled' : 'disabled')); + callback(); + } + }); + } + else { + console.warn('Unknown command "' + command + '". Available commands are: add, del, passwd, enable, disable, check, get'); + callback(1); + } + }); + })(); + break; + + case 'g': + case 'group': + (function () { + var command = args[0] || ''; + var group = args[1] || ''; + var user = args[2] || ''; + + if (group && group.match(/^system\.group\./)) group = group.substring('system.group.'.length); + if (user && user.match(/^system\.user\./)) user = user.substring('system.user.'.length); + if (!command) { + console.warn('Unknown command "' + command + '". Available commands are: add, del, passwd, enable, disable, list, get'); + return callback(1); + } + if (!group) { + console.warn('Please define group name: group ' + command + ' groupName'); + return callback(30); + } + dbConnect(params, function () { + var Users = require(__dirname + '/setup/setupUsers.js'); + var users = new Users({ + objects: objects, + processExit: callback + }); + + if (command === 'useradd' || command === 'adduser') { + if (!user) { + console.warn('Please define user name: group useradd groupName userName'); + callback(30); + } + users.addUserToGroup(user, group, function (err) { + if (err) { + console.error(err); + callback(30); + } else { + console.log('User "' + user + '" created'); + callback(); + } + }); + } + else if (command === 'userdel' || command === 'deluser') { + if (!user) { + console.warn('Please define user name: group userdel groupName userName'); + callback(30); + } + users.removeUserFromGroup(user, group, function (err) { + if (err) { + console.error(err); + callback(30); + } else { + console.log('User "' + user + '" created'); + callback(); + } + }); + } + else if (command === 'add') { + users.addGroup(group, function (err) { + if (err) { + console.error(err); + callback(30); + } else { + console.log('User "' + group + '" created'); + callback(); + } + }); + } + else if (command === 'del' || command === 'delete') { + users.delGroup(group, function (err) { + if (err) { + console.error(err); + callback(30); + } else { + console.log('User "' + group + '" deleted'); + callback(); + } + }); + } + else if (command === 'list' || command === 'l') { + users.getGroup(group, function (err, isEnabled, list) { + if (err) { + console.error(err); + callback(30); + } else { + console.log('Group "' + group + '" is ' + (isEnabled ? 'enabled' : 'disabled') + ' and has following members:'); + if (list) { + for (var i = 0; i < list.length; i++) { + console.log(list[i].substring('system.user.'.length)); + } + } + callback(); + } + }); + } + else if (command === 'enable' || command === 'e') { + users.enableGroup(group, true, function (err) { + if (err) { + console.error(err); + callback(30); + } else { + console.log('Group "' + group + '" was successfully enabled.'); + callback(); + } + }); + } + else if (command === 'disable' || command === 'd') { + users.enableGroup(group, false, function (err) { + if (err) { + console.error(err); + callback(30); + } else { + console.log('Group "' + group + '" was successfully disabled.'); + callback(); + } + }); + } + else if (command === 'get') { + users.getGroup(group, function (err, isEnabled, list) { + if (err) { + console.error(err); + callback(30); + } else { + console.log('Group "' + group + '" is ' + (isEnabled ? 'enabled' : 'disabled')); + callback(); + } + }); + } + else { + console.warn('Unknown command "' + command + '". Available commands are: add, del, passwd, enable, disable, list, get'); + callback(1); + } + }); + })(); + break; + + case 'adduser': + (function () { + var user = args[0]; + var group = params.ingroup || 'system.group.administrator'; + var password = params.password; + + dbConnect(params, function () { + var Users = require(__dirname + '/setup/setupUsers.js'); + var users = new Users({ + objects: objects, + processExit: callback + }); + users.addUserPrompt(user, group, password, function (err) { + if (err) { + console.error(err); + callback(30); + } else { + console.log('User "' + user + '" created (Group: ' + group.replace('system.group.', '') + ')'); + callback(); + } + }); + }); + })(); + break; + + case 'passwd': + (function () { + var user = args[0]; + var password = params.password; + dbConnect(params, function () { + var Users = require(__dirname + '/setup/setupUsers.js'); + var users = new Users({ + objects: objects, + processExit: callback + }); + users.setUserPassword(user, password, function (err) { + if (err) { + console.error(err); + callback(30); + } else { + console.log('Password for "' + user + '" was successfully set.'); + callback(); + } + }); + }); + })(); + break; + + case 'ud': + case 'udel': + case 'userdel': + case 'deluser': + (function () { + var user = args[0]; + + dbConnect(params, function () { + var Users = require(__dirname + '/setup/setupUsers.js'); + var users = new Users({ + objects: objects, + processExit: callback + }); + users.delUser(user, function (err) { + if (err) { + console.error(err); + callback(30); + } else { + console.log('User "' + user + '" deleted'); + callback(); + } + }); + }); + })(); + break; + + // Create package.json in /opt/' + tools.appName + ' + case 'package': + (function () { + var json = { + name: tools.appName, + engines: { + node: '>=0.8' + }, + optionalDependencies: { + }, + dependencies: {}, + author: 'bluefox ' + }; + json.dependencies[tools.appName + '.js-controller'] = '*'; + json.dependencies[tools.appName + '.admin'] = '*'; + + tools.getRepositoryFile(null, function (err, sources) { + if (sources) { + for (var s in sources) { + if (sources.hasOwnProperty(s)) { + if (sources[s].url) { + if (!json.dependencies[tools.appName + '.' + s]) { + json.optionalDependencies[tools.appName + '.' + s] = sources[s].url; + } + } else { + if (!json.dependencies[tools.appName + '.' + s]) { + json.optionalDependencies[tools.appName + '.' + s] = '*'; + } + } + } + } + } + + fs.writeFileSync(__dirname + '/../../../package.json', JSON.stringify(json, null, 2)); + callback(); + }); + })(); + break; + + case 'set': + (function () { + var instance = args[0]; + if (!instance) { + console.warn('please specify instance.'); + callback(1); + } + if (instance.indexOf('.') === -1) { + console.warn('please specify instance, like "' + instance + '.0"'); + callback(1); + } + dbConnect(params, function () { + objects.getObject('system.adapter.' + instance, function (err, obj) { + if (!err && obj) { + var changed = false; + for (var a = 0; a < process.argv.length; a++) { + if (process.argv[a].match(/^--/) && process.argv[a + 1] && !process.argv[a + 1].match(/^--/)) { + var attr = process.argv[a].substring(2); + var val = process.argv[a + 1]; + if (val === 'true') val = true; + if (val === 'false') val = false; + if (parseFloat(val).toString() === val) val = parseFloat(val); + if (attr.indexOf('.') !== -1) { + var parts = attr.split('.'); + if (!obj.native[parts[0]] || obj.native[parts[0]][parts[1]] === undefined) { + console.warn('Adapter "' + instance + '" has no setting "' + attr + '".'); + } else { + changed = true; + obj.native[parts[0]][parts[1]] = val; + console.log('New ' + attr + ' for "' + instance + '" is: ' + val); + } + } else { + if (obj.native[attr] === undefined) { + console.warn('Adapter "' + instance + '" has no setting "' + attr + '".'); + } else { + changed = true; + obj.native[attr] = val; + console.log('New ' + attr + ' for "' + instance + '" is: ' + val); + } + } + a++; + } + } + if (changed) { + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = new Date().getTime(); + objects.setObject('system.adapter.' + instance, obj, function () { + console.log('Instance settings for "' + instance + '" are changed.'); + callback(); + }); + } else { + console.log('No parameters set.'); + callback(); + } + } else { + console.error('Instance "' + instance + '" does not exist.'); + callback(24); + } + }); + }); + })(); + break; + + case 'host': + (function () { + var change = args[0]; + + var oldHostname; + var newHostname; + if (!change) { + console.warn('Please write "' + tools.appName + ' host this" to use this host ("' + tools.getHostName() + '") in ' + tools.appName + ' for all instances.'); + callback(1); + } + + var config = tools.getConfigFileName(); + var data = JSON.parse(fs.readFileSync(config, 'utf8')); + + if (change === 'set') { + oldHostname = tools.getHostName(); + newHostname = args[1]; + if (!newHostname) { + console.error('To change host name call: ' + tools.appName + ' host set newName'); + callback(34); + } + + data.system = data.system || {}; + data.system.hostname = newHostname; + fs.writeFileSync(config, JSON.stringify(data)); + } else if (change === 'remove') { + oldHostname = args[1]; + newHostname = tools.getHostName(); + if (!oldHostname) { + console.error('Host to remove is not defined. Usage: ' + tools.appName + ' host remove '); + callback(34); + } + } else { + oldHostname = (change !== 'self' && change !== 'this') ? change : null; + newHostname = require('os').hostname(); + } + + dbConnect(params, function (_objects, _states, isOffline) { + if (!isOffline) { + console.error('Cannot execute changes on running system. Stop ' + tools.appName + ' first.'); + callback(30); + } + + var count = 0; + // find first host + objects.getObjectList({startkey: 'system.host.', endkey: 'system.host.\u9999'}, function (err, objs) { + if (!oldHostname) { + var hostCount = 0; + for (var j = 0; j < objs.rows.length; j++) { + if (objs.rows[j].value.type === 'host') { + var hNameM = objs.rows[j].value._id.match(/^system\.host\.([^.]+)(\..*)?$/); + if (hNameM) { + oldHostname = hNameM[1]; + hostCount++; + } + } + } + if (hostCount > 1) { + console.warn('More than one host found. You must specifiy which hist must be renamed.'); + callback(30); + } + + } + if (change !== 'set' && change !== 'remove' && oldHostname && data.system && data.system.hostname === oldHostname) { + data.system = data.system || {}; + data.system.hostname = newHostname; + fs.writeFileSync(config, JSON.stringify(data)); + } + hostCount = 0; + for (var i = 0; i < objs.rows.length; i++) { + + if (objs.rows[i].value.type !== 'host' && objs.rows[i].value.type !== 'state') continue; + + var hNameM = objs.rows[i].value._id.match(/^system\.host\.([^.]+)(\..*)?$/); + var hName = null; + if (hNameM) { + hName = hNameM[1]; + } else { + continue; + } + + var obj = objs.rows[i].value; + + if (obj.type === 'host') { + if (hName === newHostname && change !== 'remove') { + hostCount++; + if (hostCount > 1) { + console.error('Host with actual hostname "' + hName + '" found. Cannot rename it.'); + continue; + } + } + } + + if (hName === oldHostname) { + if (obj.type === 'host') { + count++; + objects.delObject(obj._id, function (err) { + if (err) console.error('Cannot delete object: ' + err); + if (!--count) callback(); + }); + if (change !== 'remove') { + console.log('Rename host "' + obj._id + '" to system.host.' + newHostname); + obj._id = 'system.host.' + newHostname; + obj.common.name = obj._id; + obj.common.hostname = newHostname; + obj.common.address = []; + obj.common.cmd = ''; + obj.common.native = {process: {}, os: {}, hardware: {}}; + count++; + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = new Date().getTime(); + objects.setObject(obj._id, obj, function (err) { + if (err) console.error('Cannot set object: ' + err); + if (!--count) callback(); + }); + } else { + console.log('Remove host "' + obj._id + '"'); + + } + } else { + // state + count++; + objects.delObject(obj._id, function (err) { + if (err) console.error('Cannot delete object: ' + err); + if (!--count) callback(); + }); + if (change !== 'remove') { + console.log('Rename state "' + obj._id + '".'); + obj._id = 'system.host.' + newHostname + (hNameM[2] || ''); + count++; + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = new Date().getTime(); + objects.setObject(obj._id, obj, function (err) { + if (err) console.error('Cannot set object: ' + err); + if (!--count) callback(); + }); + } else { + console.log('Remove state "' + obj._id + '".'); + } + } + } + } + + objects.getObjectList({startkey: 'system.adapter.', endkey: 'system.adapter.\u9999'}, function (err, objs) { + for (var i = 0; i < objs.rows.length; i++) { + if (objs.rows[i].value.type !== 'instance') continue; + if (objs.rows[i].value.common.host === oldHostname) { + count++; + console.log('Instance host changed for "' + objs.rows[i].value._id + '" from "' + objs.rows[i].value.common.host + '" to "' + newHostname + '".'); + objs.rows[i].value.common.host = newHostname; + objs.rows[i].value.from = 'system.host.' + tools.getHostName() + '.cli'; + objs.rows[i].value.ts = new Date().getTime(); + objects.setObject(objs.rows[i].value._id, objs.rows[i].value, function (err) { + if (err) console.error('Cannot set object: ' + err); + if (!--count) callback(); + }); + } + } + if (!count) { + console.warn('No instances found for host "' + change + '".'); + callback(); + } + }); + }); + }); + + })(); + break; + + case 'visdebug': + (function () { + var widgetset = args[0]; + if (widgetset && widgetset.match('/^vis-/')) { + widgetset = widgetset.substring(4); + } + + var VisDebug = require(__dirname + '/setup/setupVisDebug.js'); + + dbConnect(params, function (_objects) { + var visDebug = new VisDebug({ + objects: _objects, + processExit: callback + }); + + visDebug.enableDebug(widgetset); + }); + })(); + break; + + case 'file': + case 'f': + (function () { + var cmd = args[0]; + if (cmd !== 'read' && cmd !== 'r' && cmd !== 'w' && cmd !== 'write') { + console.log('Invalid parameters: write "file read /vis.0/main/img/picture.png /opt/picture/image.png" to read the file'); + callback(1); + } + if (!args[1]) { + console.log('Invalid parameters: write "file read /vis.0/main/img/picture.png /opt/picture/image.png" to read the file'); + callback(1); + } + + dbConnect(params, function (_objects) { + if (cmd === 'read' || cmd ==='r') { + var toRead = args[1]; + var parts = toRead.replace(/\\/g, '/').split('/'); + + var path = (args[2] || process.cwd()).replace(/\\/g, '/').split('/'); + var file = path[path.length - 1]; + if (!file.match(/\.[a-zA-Z0-9]+$/)) { + path.push(parts[parts.length - 1]); + } + var adapt = parts.shift(); + if (!adapt) adapt = parts.shift(); + _objects.readFile(adapt, parts.join('/'), function (err, data) { + if (err) console.error(err); + if (data) { + fs.writeFileSync(path.join('/'), data); + console.log('File "' + toRead + '" stored as "' + path.join('/') + '"'); + } + callback(0); + }); + } else { + var toRead = args[1]; + var parts = toRead.replace(/\\/g, '/').split('/'); + + var path = args[2].replace(/\\/g, '/').split('/'); + + var file = path[parts.length - 1]; + if (!file) { + path.splice(path.length - 1, 1); + file = path[path.length - 1]; + } + if (!file.match(/\.[a-zA-Z0-9]+$/)) { + path.push(parts[parts.length - 1]); + } + var adapt = path.shift(); + if (!adapt) adapt = path.shift(); + var data = fs.readFileSync(toRead); + _objects.writeFile(adapt, path.join('/'), data, function (err) { + console.log('File "' + toRead + '" stored as "' + path.join('/') + '"'); + callback(0); + }); + } + }); + })(); + break; + + case 'id': + case 'uuid': + (function () { + dbConnect(params, function (objects) { + objects.getObject('system.meta.uuid', function (err, obj) { + if (err) { + console.error('Error: ' + err); + callback(101); + } + if (obj && obj.native) { + console.log(obj.native.uuid); + callback(); + } else { + console.error('Error: no UUID found'); + callback(101); + } + }); + }); + })(); + break; + + case 'v': + case 'version': + (function () { + var adapter = args[0]; + if (adapter) { + try { + iopckg = require(tools.appName + '.' + adapter + '/package.json'); + } catch (err) { + iopckg = {version: '"' + adapter + '" not found'}; + } + } else { + iopckg = require(__dirname + '/../package.json'); + } + console.log(iopckg.version); + + callback(); + })(); + break; + + case 'checklog': + (function () { + dbConnect(params, function (objects, states, isOffline) { + if (isOffline) { + console.log(tools.appName + ' is not running'); + callback(100); + } else { + console.log(tools.appName + ' is running'); + objects.getObjectList({startkey: 'system.host.', endkey: 'system.host.' + '\u9999'}, null, function (err, res) { + if (!err && res.rows.length) { + for (var i = 0; i < res.rows.length; i++) { + var parts = res.rows[i].id.split('.'); + // ignore system.host.name.alive and so on + if (parts.length === 3) { + states.pushMessage(res.rows[i].id, {command: 'checkLogging', message: null, from: 'console'}); + } + } + } + setTimeout(function () { + callback(); + }, 200); + }); + } + }); + })(); + break; + + case 'repo': + (function () { + Objects = require(__dirname + '/objects'); + var repoUrlOrCommand = args[0]; // Repo url or name or "add" / "del" / "set" / "show" / "addset" + var repoName = args[1]; // Repo url or name + var repoUrl = args[2]; // Repo url or name + if (repoUrlOrCommand !== 'add' && repoUrlOrCommand !== 'del' && repoUrlOrCommand !== 'set' && repoUrlOrCommand !== 'show' && repoUrlOrCommand !== 'addset') { + repoUrl = repoUrlOrCommand; + repoUrlOrCommand = 'show'; + } + + dbConnect(params, function () { + var Repo = require(__dirname + '/setup/setupRepo.js'); + var repo = new Repo({ + objects: objects + }); + + if (repoUrlOrCommand === 'show') { + repo.showRepoStatus(callback); + } else if (repoUrlOrCommand === 'add' || repoUrlOrCommand === 'del' || repoUrlOrCommand === 'set' || repoUrlOrCommand === 'addset') { + if (!repoName || !repoName.match(/[-_\w\d]+/)) { + console.error('Invalid repository name: "' + repoName + '"'); + callback(); + } else { + if (repoUrlOrCommand === 'add' || repoUrlOrCommand === 'addset') { + if (!repoUrl) { + console.warn('Please define repository URL or path: ' + tools.appName + ' add '); + callback(45); + } else { + repo.add(repoName, repoUrl, function (err) { + if (err) { + console.error(err); + callback(45); + } else { + if (repoUrlOrCommand === 'addset') { + repo.setActive(repoName, function (err) { + if (err) { + console.error(err); + callback(45); + } else { + console.log('Repository "' + repoName + '" set as active: "' + repoUrl + '"'); + repo.showRepoStatus(callback); + } + }); + } else { + console.log('Repository "' + repoName + '" added as "' + repoUrl + '"'); + repo.showRepoStatus(callback); + } + } + }); + + } + } else if (repoUrlOrCommand === 'set') { + repo.setActive(repoName, function (err) { + if (err) { + console.error(err); + callback(45); + } else { + console.log('Repository "' + repoName + '" set as active.'); + repo.showRepoStatus(callback); + } + }); + } else if (repoUrlOrCommand === 'del') { + repo.del(repoName, function (err) { + if (err) { + console.error(err); + callback(45); + } else { + console.log('Repository "' + repoName + '" deleted.'); + repo.showRepoStatus(callback); + } + }); + } else { + console.warn('Unknown repo command: ' + repoUrlOrCommand); + callback(105); + } + } + } + }); + })(); + break; + + case 'multihost': + case 'mh': + (() => { + var cmd = args[0]; + if (cmd !== 'c' && cmd !== 'connect' && cmd !== 's' && cmd !== 'status' && cmd !== 'b' && cmd !== 'browse' && cmd !== 'e' && cmd !== 'enable' && cmd !== 'd' && cmd !== 'disable') { + console.log('Invalid parameters. Following is possible: enable, browse, connect, status'); + callback(1); + } else { + dbConnect(params, function () { + var Multihost = require(__dirname + '/setup/setupMultihost.js'); + var mh = new Multihost({ + params: params, + processExit: callback, + objects: objects + }); + + if (cmd === 's' || cmd === 'status') { + mh.status(function () { + callback(30); + }); + } else + if (cmd === 'b' || cmd === 'browse') { + mh.browse(function (err, list) { + if (err) { + console.error(err); + callback(30); + } else { + mh.showHosts(list); + callback(); + } + }); + } else if (cmd === 'e' || cmd === 'enable') { + mh.enable(true, function (err) { + if (err) { + console.error(err); + callback(1); + } else { + states.pushMessage('system.host.' + tools.getHostName(), {command: 'updateMultihost', message: null, from: 'setup'}, function() { + callback(); + }); + } + }); + } else if (cmd === 'd' || cmd === 'disable') { + mh.enable(false, function (err) { + if (err) { + console.error(err); + callback(1); + } else { + states.pushMessage('system.host.' + tools.getHostName(), {command: 'updateMultihost', message: null, from: 'setup'}, function() { + callback(); + }); + } + }); + } else if (cmd === 'c' || cmd === 'connect') { + mh.connect(args[1], args[2], function (err) { + if (err) { + console.error(err) + } + callback(err ? 1 : 0); + }); + } + }); + } + })(); + break; + + case 'vendor': + (() => { + const password = args[0]; + const file = args[1]; + if (!password) { + console.warn(`Please specify the password to update the vendor information!\n${tools.appName.toLowerCase()} vendor `); + callback(1); + } if (!file) { + console.warn(`Please specify the path to the vendor file to update the vendor information!\n${tools.appName.toLowerCase()} vendor `); + callback(1); + } else { + dbConnect(params, function () { + const Vendor = require('./setup/setupVendor'); + const vendor = new Vendor({ + objects: objects + }); + vendor.checkVendor(file, password).then(() => { + console.log(`Synchronised vendor information.`); + callback(); + }).catch(err => { + console.error(`Cannot update vendor information: ${JSON.stringify(err)}`); + callback(1); + }); + }); + } + })(); + break; + + case 'license': + (() => { + const file = args[0]; + if (!file) { + console.warn(`Please specify the path to the license file or place license text directly!\n${tools.appName.toLowerCase()} license `); + callback(1); + } else { + dbConnect(params, function () { + const License = require('./setup/setupLicense'); + const license = new License({ + objects: objects + }); + license.setLicense(file).then(type => { + console.log(`License ${type} updated.`); + callback(); + }).catch(err => { + console.error(`Cannot update license: ${JSON.stringify(err)}`); + callback(1); + }); + }); + } + })(); + break; + + default: + if (params.v || params.version) { + var iopckg; + if (command) { + try { + iopckg = require(tools.appName + '.' + command + '/package.json'); + } catch (err) { + iopckg = {version: '"' + command + '" not found'}; + } + } else { + iopckg = require(__dirname + '/../package.json'); + } + console.log(iopckg.version); + } else { + showHelp(); + callback(1); + } + callback(); + break; + } +} + +// Save objects before exit +function processExit(exitCode) { + if (objects && objects.destroy) objects.destroy(); + if (states && states.destroy) states.destroy(); + process.exit(exitCode); +} + +function cleanDatabase(isDeleteDb, callback) { + var taskCnt = 0; + + if (isDeleteDb) { + objects.destroyDB(function () { + + // Clean up states + states.getKeys('*', function (err, obj) { + var delState = []; + var i; + if (obj) { + for (i = 0; i < obj.length; i++) { + delState.push(obj[i]); + } + } + taskCnt = 0; + for (i = 0; i < obj.length; i++) { + taskCnt++; + states.delState(delState[i], function () { + if (!(--taskCnt) && callback) callback(obj.length); + }); + } + }); + }); + } else { + // Clean only objects, not the views + objects.getObjectList({startkey: '\u0000', endkey: '\u9999'}, function (err, res) { + if (!err && res.rows.length) { + console.log('clean ' + res.rows.length + ' objects...'); + for (var i = 0; i < res.rows.length; i++) { + //console.log('Delete ' + res.rows[i].id); + objects.delObject(res.rows[i].id); + } + } + + // Clean up states + states.getKeys('*', function (err, obj) { + var delState = []; + var i; + if (obj) { + for (i = 0; i < obj.length; i++) { + delState.push(obj[i]); + } + } + taskCnt = 0; + console.log('clean ' + obj.length + ' states...'); + for (i = 0; i < obj.length; i++) { + taskCnt++; + states.delState(delState[i], function () { + if (!(--taskCnt) && callback) callback(obj.length); + }); + } + if (!taskCnt && callback) callback(obj.length); + }); + + }); + } +} + +function restartController(callback) { + var spawn = require('child_process').spawn; + + console.log('Starting node restart.js'); + + var child = spawn('node', [__dirname + '/restart.js'], { + detached: true, + stdio: ['ignore', 'ignore', 'ignore'] + }); + + child.unref(); + + if (callback) { + callback(); + } else { + processExit(); + } +} + +function installNpm(adapter, callback) { + var path = __dirname; + if (typeof adapter === 'function') { + callback = adapter; + adapter = undefined; + } + + if (adapter) { + path = tools.getAdapterDir(adapter); + } + + // iob_npm.done file was created if "npm i" yet called there + if (fs.existsSync(path + '/package.json') && !fs.existsSync(path + '/iob_npm.done')) { + tools.disablePackageLock(function (err) { + var cmd = 'npm install --production'; + console.log(cmd + ' (System call) in "' + path + '"'); + // Install node modules as system call + + // System call used for update of js-controller itself, + // because during installation npm packet will be deleted too, but some files must be loaded even during the install process. + var exec = require('child_process').exec; + var child = exec(cmd, { + cwd: path + }); + child.stderr.pipe(process.stdout); + child.on('exit', function (code, signal) { + if (code) { + console.log('Cannot install ' + tools.appName + '.' + adapter + ': ' + code); + (callback || processExit)(25); + } + // command succeeded + if (callback) callback(null, adapter); + }); + }); + } else { + if (callback) callback(null, adapter); + } +} + +function getRepository(repoUrl, params, callback) { + if (typeof params === 'function') { + callback = params; + params = {}; + } + params = params || {}; + + if (!repoUrl || typeof repoUrl !== 'object') { + if (!objects) { + dbConnect(params, function () { + getRepository(repoUrl, params, callback); + }); + } else { + // try to read repository + objects.getObject('system.config', function (err, systemConfig) { + objects.getObject('system.repositories', function (err, repos) { + // Check if repositories exists + if (!err && repos && repos.native && repos.native.repositories) { + var active = systemConfig.common.activeRepo; + + if (repos.native.repositories[active]) { + if (typeof repos.native.repositories[active] === 'string') { + repos.native.repositories[active] = { + link: repos.native.repositories[active], + json: null + }; + } + + // If repo is not yet loaded + if (!repos.native.repositories[active].json) { + console.log('Update repository "' + active + '" under "' + repos.native.repositories[active].link + '"'); + // Load it + tools.getRepositoryFile(repos.native.repositories[active].link, function (err, sources) { + repos.native.repositories[active].json = sources; + repos.from = 'system.host.' + tools.getHostName() + '.cli'; + repos.ts = new Date().getTime(); + // Store uploaded repo + objects.setObject('system.repositories', repos, function () { + callback(null, sources); + }); + }); + } else { + // We have already repo, give it back + callback(null, repos.native.repositories[active].json); + } + } else { + console.log('Requested repository "' + active + '" does not exit in config.'); + callback(25); + } + } else { + console.log('No repositories defined.'); + callback(25); + } + }); + }); + } + } else { + callback(null, repoUrl); + } +} + +function dbConnect(onlyCheck, params, callback) { + if (typeof onlyCheck === 'object') { + callback = params; + params = onlyCheck; + onlyCheck = false; + } + if (typeof onlyCheck === 'function') { + callback = onlyCheck; + onlyCheck = false; + } + if (typeof params === 'function') { + callback = params; + params = null; + } + params = params || {}; + if (objects && states) { + callback(objects, states); + return; + } + + var config = require(tools.getConfigFileName()); + if (!config.states) config.states = {type: 'file'}; + if (!config.objects) config.objects = {type: 'file'}; + + Objects = require(__dirname + '/objects'); + States = require(__dirname + '/states'); + + // Give to controller 2 seconds for connection + var isObjectConnected = false; + var isStatesConnected = false; + + // Detect timeout or try to open file itself + setTimeout(function () { + if (isObjectConnected && isStatesConnected) return; + + if (onlyCheck) { + if (typeof callback === 'function') callback(null, null, true); + return; + } + + + if (!isObjectConnected) { + if (config.objects.type === 'file') { + // Just open in memory DB itself + Objects = require(__dirname + '/objects/objectsInMemServer'); + objects = new Objects({ + connection: config.objects, + logger: { + silly: function (msg) { + }, + debug: function (msg) { + }, + info: function (msg) { + }, + warn: function (msg) { + console.log(msg); + }, + error: function (msg) { + console.log(msg); + } + }, + connected: function () { + isObjectConnected = true; + if (isStatesConnected && typeof callback === 'function') callback(objects, states, true); + } + }); + } else { + console.log('No connection to ' + config.objects.host + ':' + config.objects.port + '[' + config.objects.type + ']'); + processExit(22); + } + } + + if (!isStatesConnected) { + if (config.states.type === 'file') { + // Just open in memory DB itself + States = require(__dirname + '/states/statesInMemServer'); + states = new States({ + connection: config.states, + logger: { + silly: function (msg) { + }, + debug: function (msg) { + }, + info: function (msg) { + }, + warn: function (msg) { + console.log(msg); + }, + error: function (msg) { + console.log(msg); + } + }, + connected: function () { + isStatesConnected = true; + if (isObjectConnected && typeof callback === 'function') callback(objects, states, true); + } + }); + } else { + console.log('No connection to states ' + config.states.host + ':' + config.states.port + '[' + config.states.type + ']'); + processExit(22); + } + } + }, params.timeout || config.objects.connectTimeout || 2000); + + // try to connect as client + objects = new Objects({ + connection: config.objects, + logger: { + silly: function (msg) { }, + debug: function (msg) { }, + info: function (msg) { }, + warn: function (msg) { + console.log(msg); + }, + error: function (msg) { + console.log(msg); + } + }, + connected: function () { + if (isObjectConnected) return; + isObjectConnected = true; + + if (isStatesConnected && typeof callback === 'function') callback(objects, states); + } + }); + + states = new States({ + connection: config.states, + logger: { + silly: function (msg) { }, + debug: function (msg) { }, + info: function (msg) { }, + warn: function (msg) { + console.log(msg); + }, + error: function (msg) { + console.log(msg); + } + }, + connected: function () { + if (isStatesConnected) return; + isStatesConnected = true; + + if (isObjectConnected && typeof callback === 'function') callback(objects, states); + } + }); +} + +module.exports.processCommand = function (_objects, _states, command, args, params, callback) { + objects = _objects; + states = _states; + processCommand(command, args, params, callback); +}; + +module.exports.execute = function () { + // direct call + var _yargs = initYargs(); + var command = _yargs.argv._[0]; + + var args = []; + for (var a = 1; a < _yargs.argv._.length; a++) { + args.push(_yargs.argv._[a]); + } + + processCommand(command, args, _yargs.argv, processExit); +}; diff --git a/lib/setup/setupBackup.js b/lib/setup/setupBackup.js new file mode 100644 index 0000000..197452e --- /dev/null +++ b/lib/setup/setupBackup.js @@ -0,0 +1,602 @@ +'use strict'; + +function BackupRestore(options) { + var fs = require('fs'); + var tools = require(__dirname + '/../tools.js'); + var pathLib = require('path'); + var tmpDir = pathLib.normalize(__dirname + '/../../tmp'); + var bkpDir = pathLib.normalize(__dirname + '/../../backups'); + var hostname = tools.getHostName(); + + // allow use without new operator + if (!(this instanceof BackupRestore)) return new BackupRestore(options); + + options = options || {}; + + if (!options.states) throw 'Invalid arguments: states is missing'; + if (!options.objects) throw 'Invalid arguments: objects is missing'; + if (!options.processExit) throw 'Invalid arguments: processExit is missing'; + if (!options.cleanDatabase) throw 'Invalid arguments: cleanDatabase is missing'; + if (!options.restartController) throw 'Invalid arguments: restartController is missing'; + + var objects = options.objects; + var states = options.states; + var processExit = options.processExit; + var cleanDatabase = options.cleanDatabase; + var restartController = options.restartController; + var mime; + + var Upload = require(__dirname + '/setupUpload.js'); + var upload = new Upload(options); + + // --------------------------------------- BACKUP --------------------------------------------------- + function _copyFile(id, srcPath, destPath, callback) { + objects.readFile(id, srcPath, "", function (err, data) { + if (data) fs.writeFileSync(destPath, data); + callback(); + }); + } + + function copyDir(id, srcPath, destPath, callback) { + var count = 0; + if (!fs.existsSync(destPath)) fs.mkdirSync(destPath); + objects.readDir(id, srcPath, function (err, res) { + if (res) { + for (var t = 0; t < res.length; t++) { + if (res[t].isDir) { + count++; + copyDir(id, srcPath + '/' + res[t].file, destPath + '/' + res[t].file, function () { + count--; + if (!count) callback(); + }); + } else { + if (!fs.existsSync(destPath)) fs.mkdirSync(destPath); + count++; + _copyFile(id, srcPath + '/' + res[t].file, destPath + '/' + res[t].file, function () { + count--; + if (!count) callback(); + }); + } + } + } + if (!count) callback(); + }); + } + + function getBackupDir() { + var dataDir = tools.getDefaultDataDir(); + + // All pathes are returned always relative to /node_modules/appName.js-controller + if (dataDir) { + if (dataDir[0] === '.' && dataDir[1] === '.') { + dataDir = __dirname + '/../../' + dataDir; + } else if (dataDir[0] === '.' && dataDir[1] === '/') { + dataDir = __dirname + '/../../' + dataDir.substring(2); + } + } + dataDir = dataDir.replace(/\\/g, '/'); + if (dataDir[dataDir.length - 1] !== '/') dataDir += '/'; + + var parts = dataDir.split('/'); + parts.pop();// remove data or appName-data + parts.pop(); + + return parts.join('/') + '/backups/'; + } + + function copyFileSync(source, target) { + var targetFile = target; + + // if target is a directory a new file with the same name will be created + if (fs.existsSync(target)) { + if (fs.lstatSync(target).isDirectory()) { + targetFile = pathLib.join(target, pathLib.basename(source)); + } + } + + fs.writeFileSync(targetFile, fs.readFileSync(source)); + } + + function copyFolderRecursiveSync(source, target) { + var files = []; + + if (!fs.existsSync(target)) fs.mkdirSync(target); + + // check if folder needs to be created or integrated + var targetFolder = pathLib.join(target, pathLib.basename(source)); + if (!fs.existsSync(targetFolder)) fs.mkdirSync(targetFolder); + + // copy + if (fs.lstatSync(source).isDirectory() ) { + files = fs.readdirSync(source); + files.forEach(function (file) { + var curSource = pathLib.join(source, file); + if (fs.lstatSync(curSource).isDirectory()) { + copyFolderRecursiveSync(curSource, targetFolder); + } else { + copyFileSync(curSource, targetFolder); + } + }); + } + } + + this.createBackup = function (name, callback) { + var count = 0; + + if (!name) { + var d = new Date(); + name = d.getFullYear() + '_' + + ('0' + (d.getMonth() + 1)).slice(-2) + '_' + + ('0' + d.getDate()).slice(-2) + '-' + + ('0' + d.getHours()).slice(-2) + '_' + + ('0' + d.getMinutes()).slice(-2) + '_' + + ('0' + d.getSeconds()).slice(-2) + '_backup' + tools.appName; + } + + name = name.replace(/\\/g, '/'); + if (name.indexOf('/') === -1) { + var path = getBackupDir(); + + // create directory if not exists + if (!fs.existsSync(path)) { + fs.mkdirSync(path); + } + + if (name.indexOf('.tar.gz') === -1) { + name = path + name + '.tar.gz'; + } else { + name = path + name; + } + } + + objects.getObjectList({include_docs: true}, function (err, res) { + var result = {objects: null, states: {}, config: null}; + if (err) { + console.error('host.' + hostname + ' Cannot get objects: ' + err); + } else { + result.objects = res.rows; + } + + if (fs.existsSync(tools.getConfigFileName())) result.config = JSON.parse(fs.readFileSync(tools.getConfigFileName(), 'utf8')); + + states.getKeys('io.*', function (err, keys) { + /*for (var i = keys.length - 1; i >= 0; i--) { + if (keys[i].match(/^messagebox\./) || keys[i].match(/^log\./)) { + keys.splice(i, 1); + } + }*/ + + states.getStates(keys, function (err, obj) { + var hostname = tools.getHostName(); + var r = new RegExp('^system\\.host\\.' + hostname + '\\.(\\w+)$'); + + for (var i = 0; i < keys.length; i++) { + if (obj[i].from === 'system.host.' + hostname) { + obj[i].from = 'system.host.$$__hostname__$$'; + } + if (r.test(keys[i])) { + keys[i] = keys[i].replace(hostname, '$$__hostname__$$'); + } + result.states[keys[i]] = obj[i]; + } + + if (!fs.existsSync(bkpDir)) fs.mkdirSync(bkpDir); + if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir); + if (!fs.existsSync(tmpDir + '/backup')) fs.mkdirSync(tmpDir + '/backup'); + if (!fs.existsSync(tmpDir + '/backup/files')) fs.mkdirSync(tmpDir + '/backup/files'); + + // try to find user files + for (var j = 0; j < result.objects.length; j++) { + if (!result.objects[j].value || !result.objects[j].value._id) continue; + //if (result.objects[j].doc) delete result.objects[j].doc; + if (result.objects[j].value._id.match(/^system\.adapter\.([\w\d_-]+).(\d+)$/) && + result.objects[j].value.common.host === hostname) { + result.objects[j].value.common.host = '$$__hostname__$$'; + if (result.objects[j].doc) { + result.objects[j].doc.common.host = '$$__hostname__$$'; + } + } else + if (r.test(result.objects[j].value._id)) { + result.objects[j].value._id = result.objects[j].value._id.replace(hostname, '$$__hostname__$$'); + result.objects[j].id = result.objects[j].value._id; + if (result.objects[j].doc) { + result.objects[j].doc._id = result.objects[j].value._id; + } + } else if (result.objects[j].value._id === 'system.host.' + hostname) { + result.objects[j].value._id = 'system.host.$$__hostname__$$'; + result.objects[j].value.common.name = result.objects[j].value._id; + result.objects[j].value.common.hostname = '$$__hostname__$$'; + if (result.objects[j].value.native && result.objects[j].value.native.os) { + result.objects[j].value.native.os.hostname = '$$__hostname__$$'; + } + result.objects[j].id = result.objects[j].value._id; + if (result.objects[j].doc) { + result.objects[j].doc._id = result.objects[j].value._id; + result.objects[j].doc.common.name = result.objects[j].value._id; + result.objects[j].doc.common.hostname = '$$__hostname__$$'; + if (result.objects[j].doc.native && result.objects[j].value.native.os) { + result.objects[j].doc.native.os.hostname = '$$__hostname__$$'; + } + } + } + + // Read all files + if (result.objects[j].value.type === 'meta' && + result.objects[j].value.common && + result.objects[j].value.common.type === 'meta.user') { + count++; + copyDir(result.objects[j].id, '', tmpDir + '/backup/files/' + result.objects[j].id, function () { + count--; + if (!count) { + // todo: store letsencrypt files too => change it as letitbit will be better integrated + var configDir = tools.getConfigFileName().split('/'); + configDir.pop(); + configDir.push('letsencrypt'); + var letsEncrypt = configDir.join('/'); + if (fs.existsSync(letsEncrypt)) { + copyFolderRecursiveSync(letsEncrypt, tmpDir + '/backup'); + } + + var tar = require('tar'); + + var f = fs.createWriteStream(name); + f.on('finish', function () { + tools.rmdirRecursiveSync(tmpDir + '/backup'); + if (callback) callback(pathLib.normalize(name)); + }); + f.on('error', function (err) { + console.error('host.' + hostname + ' Cannot pack directory ' + pathLib.normalize(tmpDir + '/backup') + ': ' + err); + processExit(9); + }); + + try { + tar.create({gzip: true, cwd: tmpDir + '/'}, ['backup']).pipe(f); + } catch (err) { + console.error('host.' + hostname + ' Cannot pack directory ' + pathLib.normalize(tmpDir + '/backup') + ': ' + err); + processExit(9); + } + } + }); + } + } + + fs.writeFileSync(tmpDir + '/backup/backup.json', JSON.stringify(result, null, 2)); + + if (!count) { + var tar = require('tar'); + + var f = fs.createWriteStream(name); + f.on('finish', function () { + tools.rmdirRecursiveSync(tmpDir + '/backup'); + if (callback) callback(pathLib.normalize(name)); + }); + f.on('error', function (err) { + console.error('host.' + hostname + ' Cannot pack directory ' + pathLib.normalize(tmpDir + '/backup') + ': ' + err); + processExit(9); + }); + + try { + tar.create({gzip: true, cwd: tmpDir + '/'}, ['backup']).pipe(f); + } catch (err) { + console.error('host.' + hostname + ' Cannot pack directory ' + pathLib.normalize(tmpDir + '/backup') + ': ' + err); + processExit(9); + } + } + }); + }); + + }); + }; + + //--------------------------------------- RESTORE --------------------------------------------------- + function _setStateHelper(_index, statesList, stateObjects, callback) { + states.setRawState(statesList[_index], stateObjects[statesList[_index]], function () { + if ((_index % 200) === 0) console.log('host.' + hostname + ' Processed ' + _index + '/' + statesList.length + ' states'); + _index++; + if (_index < statesList.length) { + setImmediate(_setStateHelper, _index, statesList, stateObjects, callback); + } else { + if (callback) callback(); + } + }); + } + + function _setObjHelper(_index, _objects, callback) { + // Disable all adapters. + if (_objects[_index].id.match(/^system\.adapter\./) && !_objects[_index].id.match(/^system\.adapter\.admin/)) { + if (_objects[_index].doc.common && _objects[_index].doc.common.enabled) { + _objects[_index].doc.common.enabled = false; + } + } + if (_objects[_index].doc && _objects[_index].doc._rev) delete _objects[_index].doc._rev; + + objects.setObject(_objects[_index].id, _objects[_index].doc, function (err /* , obj */) { + if (err) { + console.warn('host.' + hostname + ' Cannot restore ' + _objects[_index].id + ': ' + err); + } + + if ((_index % 200) === 0) console.log('host.' + hostname + ' Processed ' + _index + '/' + _objects.length + ' objects'); + _index++; + if (_index < _objects.length) { + setImmediate(_setObjHelper, _index, _objects, callback); + } else { + if (callback) callback(); + } + }); + } + + function reloadAdapterObject(index, objectList, callback) { + if (objectList && index < objectList.length) { + objects.getObject(objectList[index]._id, function (err, obj) { + if (err || !obj) { + objects.setObject(objectList[index]._id, objectList[index], function () { + console.log('host.' + hostname + ' object ' + objectList[index]._id + ' created'); + index++; + setImmediate(reloadAdapterObject, index, objectList, callback); + }); + } else { + index++; + setImmediate(reloadAdapterObject, index, objectList, callback); + } + }); + } else { + if (callback) callback(); + } + } + + function reloadAdaptersObjects(callback, dirs, index) { + if (!dirs) { + dirs = []; + var _modules; + if (fs.existsSync(__dirname + '/../../node_modules')) { + _modules = fs.readdirSync(__dirname + '/../../node_modules'); + if (_modules) { + var regEx = new RegExp('^' + tools.appName + '\\.', 'i'); + for (var i = 0; i < _modules.length; i++) { + if (regEx.test(_modules[i]) && + dirs.indexOf(_modules[i].substring(tools.appName.length + 1)) === -1) { + dirs.push(_modules[i]); + } + } + } + } + // if installed as npm + if (fs.existsSync(__dirname + '/../../../../node_modules/' + tools.appName + '.js-controller')) { + _modules = fs.readdirSync(__dirname + '/../../..'); + var regEx_ = new RegExp('^' + tools.appName + '\\.', 'i'); + for (var j = 0; j < _modules.length; j++) { + // if starting from application name + '.' + if (regEx_.test(_modules[j]) && + // If not js-controller + (_modules[j].substring(tools.appName.length + 1) !== 'js-controller') && + dirs.indexOf(_modules[j].substring(tools.appName.length + 1)) === -1) dirs.push(_modules[j]); + } + } + if (dirs.length) { + reloadAdaptersObjects(callback, dirs, 0); + } else { + if (callback) callback(); + } + } else { + if (index < dirs.length) { + upload.uploadAdapter(dirs[index], false, true, function () { + upload.uploadAdapter(dirs[index], true, true, function () { + var pkg = null; + if (!dirs[index]) { + console.error('Wrong'); + } + var adapterDir = tools.getAdapterDir(dirs[index]); + if (fs.existsSync(adapterDir + '/io-package.json')) { + pkg = JSON.parse(fs.readFileSync(adapterDir + '/io-package.json', 'utf8')); + } + + if (pkg && pkg.objects && pkg.objects.length) { + console.log('host.' + hostname + ' Setup "' + dirs[index] + '" adapter'); + reloadAdapterObject(0, pkg.objects, function () { + index++; + setImmediate(reloadAdaptersObjects, callback, dirs, index); + }); + } else { + index++; + reloadAdaptersObjects(callback, dirs, index); + } + }); + }); + } else { + if (callback) callback(); + } + } + } + + function uploadUserFiles(root, path, callback) { + if (typeof path === 'function') { + callback = path; + path = ''; + } + + var called = false; + if (!fs.existsSync(root)) { + callback(); + return; + } + var files = fs.readdirSync(root + path); + var count = files.length; + for (var i = 0; i < files.length; i++) { + var stat = fs.statSync(root + path + '/' + files[i]); + if (stat.isDirectory()) { + called = true; + uploadUserFiles(root, path + '/' + files[i], function (err) { + if (err) console.error('Error: ' + err); + if (!--count) setImmediate(callback); + }); + } else { + var parts = path.split('/'); + var adapter = parts.splice(0, 2); + adapter = adapter[1]; + var _path = parts.join('/') + '/' + files[i]; + console.log('host.' + hostname + ' Upload user file "' + adapter + "/" + _path); + called = true; + objects.writeFile(adapter, _path, fs.readFileSync(root + path + '/' + files[i]), null, function (err) { + if (err) console.error('Error: ' + err); + if (!--count) setImmediate(callback); + }); + } + } + if (!called) callback(); + } + + function restoreAfterStop(restartOnFinish, callback) { + // Open file + var data = fs.readFileSync(tmpDir + '/backup/backup.json').toString(); + var hostname = tools.getHostName(); + data = data.replace(/\$\$__hostname__\$\$/g, hostname); + fs.writeFileSync(tmpDir + '/backup/backup_.json', data); + var restore; + try { + restore = JSON.parse(data); + } catch (e) { + console.error('Cannot parse "' + tmpDir + '/backup/backup_.json": ' + e); + if (callback) callback(31); + } + + // stop all adapters + console.log('host.' + hostname + ' Clear all objects and states...'); + cleanDatabase(false, function () { + console.log('host.' + hostname + ' done.'); + // upload all data into DB + // restore ioBorker.json + if (restore.config) fs.writeFileSync(tools.getConfigFileName(), JSON.stringify(restore.config, null, 2)); + + var sList = []; + for (var state in restore.states) { + if (restore.states.hasOwnProperty(state)) { + sList.push(state); + } + } + + _setStateHelper(0, sList, restore.states, function () { + console.log(sList.length + ' states restored.'); + _setObjHelper(0, restore.objects, function () { + console.log(restore.objects.length + ' objects restored.'); + // Required for upload adapter + mime = require('mime'); + // Load user files into DB + uploadUserFiles(tmpDir + '/backup/files', function () { + // reload objects of adapters + reloadAdaptersObjects(function () { + // Reload host objects + var pckgio = JSON.parse(fs.readFileSync(__dirname + '/../../io-package.json', 'utf8')); + reloadAdapterObject(0, pckgio ? pckgio.objects : null, function () { + if (restartOnFinish) { + restartController(callback); + } else { + if (callback) callback(); + } + }); + }); + + }); + }); + }); + }); + } + + this.listBackups = function () { + var dir = getBackupDir(); + var result = []; + if (fs.existsSync(dir)) { + var files = fs.readdirSync(dir); + for (var i = 0; i < files.length; i++) { + if (files[i].match(/\.tar\.gz$/i)) { + result.push(files[i]); + } + } + return result; + } else { + return result; + } + }; + + this.restoreBackup = function (name, callback) { + var backups; + if (!name && name !== 0) { + // List all available backups + console.log('Please specify one of the backup names:'); + backups = this.listBackups(); + backups.sort(function (a, b) { + return b > a; + }); + if (backups.length) { + for (var t = 0; t < backups.length; t++){ + console.log(backups[t] + ' or ' + backups[t].replace('_backup' + tools.appName + '.tar.gz', '') + ' or ' + t); + } + } else { + console.warn('No backups found'); + } + processExit(10); + } + + if (!options.cleanDatabase) throw "Invalid arguments: cleanDatabase is missing"; + if (!options.restartController) throw "Invalid arguments: restartController is missing"; + + if (parseInt(name, 10).toString() === name.toString()) { + backups = this.listBackups(); + backups.sort(function (a, b) { + return b > a; + }); + name = backups[parseInt(name, 10)]; + console.log('host.' + hostname + ' Using backup file ' + name); + } + + name = (name || '').toString().replace(/\\/g, '/'); + if (name.indexOf('/') === -1) { + name = getBackupDir() + name; + var regEx = new RegExp('_backup' + tools.appName, 'i'); + if (!regEx.test(name)) name += '_backup' + tools.appName; + if (!name.match(/\.tar\.gz$/i)) name += '.tar.gz'; + } + if (!fs.existsSync(name)) { + console.error('host.' + hostname + ' Cannot find ' + name); + processExit(11); + } + var tar = require('tar'); + if (fs.existsSync(tmpDir + '/backup/backup.json')) { + fs.unlinkSync(tmpDir + '/backup/backup.json'); + } + + tar.extract({ + file: name, + cwd: tmpDir + }, function (err) { + if (err) { + console.error('host.' + hostname + ' Cannot extract from file "' + name + '"'); + processExit(9); + } + if (!fs.existsSync(tmpDir + '/backup/backup.json')) { + console.error('host.' + hostname + ' Cannot find extracted file from file "' + tmpDir + '/backup/backup.json"'); + processExit(9); + } + // Stop controller + var daemon = require('daemonize2').setup({ + main: '../../controller.js', + name: tools.appName + ' controller', + pidfile: __dirname + '/../' + tools.appName + '.pid', + cwd: '../../', + stopTimeout: 1000 + }); + daemon.on('error', function (/* error */) { + restoreAfterStop(false, callback); + }); + daemon.on('stopped', function () { + restoreAfterStop(true, callback); + }); + daemon.on('notrunning', function () { + console.log('host.' + hostname + ' OK.'); + restoreAfterStop(false, callback); + }); + daemon.stop(); + }); + } +} + +module.exports = BackupRestore; diff --git a/lib/setup/setupInstall.js b/lib/setup/setupInstall.js new file mode 100644 index 0000000..e18de54 --- /dev/null +++ b/lib/setup/setupInstall.js @@ -0,0 +1,1397 @@ +//@ts-check + +'use strict'; + +function Install(options) { + const fs = require('fs'); + const tools = require('../tools.js'); + const extend = require('node.extend'); + const hostname = tools.getHostName(); + const path = require('path'); + const semver = require('semver'); + const child_process = require('child_process'); + // todo solve it somehow + const unsafePermAlways = [tools.appName.toLowerCase() + '.zwave']; + let JSZip; + + /** @type {Install} */ + let that = this; + + options = options || {}; + + if (!options.states) throw 'Invalid arguments: states is missing'; + if (!options.objects) throw 'Invalid arguments: objects is missing'; + if (!options.processExit) throw 'Invalid arguments: processExit is missing'; + if (!options.installNpm) throw 'Invalid arguments: installNpm is missing'; + if (!options.getRepository) throw 'Invalid arguments: getRepository is missing'; + + let objects = options.objects; + let states = options.states; + let processExit = options.processExit; + let installNpm = options.installNpm; + let getRepository = options.getRepository; + let params = options.params || {}; + let mime; + + // TODO: promisify States and Objects at some point + /** @type {(stateId: string) => Promise} */ + const delStateAsync = tools.promisify(states.delState, states); + /** @type {(objId: string) => Promise} */ + const delObjectAsync = tools.promisify(objects.delObject, objects); + /** @type {(id: string, name: string) => Promise} */ + const unlinkAsync = tools.promisify(objects.unlink, objects); + /** @type {(design: string, search: string, params: any, options?: any) => Promise<{rows: {id: string, value: any}[]}>} */ + const getObjectViewAsync = tools.promisify(objects.getObjectView, objects); + /** @type {(params: any | null) => Promise<{rows: {id: string, value: any}[]}>} */ + const getObjectListAsync = tools.promisify(objects.getObjectList, objects); + /** @type {(objId: string) => Promise} */ + const getObjectAsync = tools.promisify(objects.getObject, objects); + /** @type {(objId: string, newObj: any) => Promise} */ + const setObjectAsync = tools.promisify(objects.setObject, objects); + /** @type {(pattern: string) => Promise} */ + const getKeysAsync = tools.promisify(states.getKeys, states); + + + let installCount = 0; + + const Upload = require(__dirname + '/setupUpload.js'); + let upload = new Upload(options); + + function enableAdapters(adapters, isEnable, callback) { + let count = 0; + if (adapters) { + count = adapters.length; + const ts = new Date().getTime(); + for (let i = 0; i < adapters.length; i++) { + adapters[i].common.enabled = isEnable; + console.log('host.' + hostname + ' Adapter "' + adapters[i]._id + '" is ' + (isEnable ? 'started' : 'stopped.')); + adapters[i].from = 'system.host.' + tools.getHostName() + '.cli'; + adapters[i].ts = ts; + objects.setObject(adapters[i]._id, adapters[i], function () { + if (!--count) callback(); + }); + } + } + if (!count) callback(); + } + + function _writeOneFile(zip, targetName, fileName, callback) { + zip.files[fileName].async('nodebuffer').then(function (data) { + fs.writeFileSync(path.join(targetName, fileName), data); + callback(); + }, function (err) { + callback(err); + }); + } + + function extractFiles(fileName, targetName, callback) { + JSZip = JSZip || require('jszip'); + const zip = new JSZip(); + zip.loadAsync(fs.readFileSync(fileName)).then(function () { + let count = 0; + for (let fName in zip.files) { + if (!zip.files.hasOwnProperty(fName) || !fName || fName[fName.length - 1] === '/') continue; + count++; + _writeOneFile(zip, targetName, fName, function (err) { + if (!--count) callback(err); + }); + } + if (!count) callback(); + }); + } + + this.downloadPacket = function (repoUrl, packetName, options, stoppedList, callback) { + let url; + let name; + if (!options || typeof options !== 'object') { + options = {}; + } + + if (typeof stoppedList === 'function') { + callback = stoppedList; + stoppedList = null; + } + + if (!repoUrl || typeof repoUrl !== 'object') { + getRepository(repoUrl, params, function (err, sources) { + if (err) { + processExit(err); + return; + } + that.downloadPacket(sources, packetName, options, stoppedList, callback); + }); + return; + } + let version; + if (packetName.indexOf('@') !== -1) { + const parts = packetName.split('@'); + packetName = parts[0]; + version = parts[1]; + } else { + version = ''; + } + + let sources = repoUrl; + options.unsafePerm = sources[packetName] && sources[packetName].unsafePerm; + + // Check if flag stopBeforeUpdate is true + if (sources[packetName] && sources[packetName].stopBeforeUpdate && !stoppedList) { + objects.getObjectList({startkey: 'system.adapter.' + packetName + '.', endkey: 'system.adapter.' + packetName + '.\u9999'}, function (err, arr) { + stoppedList = []; + if (!err && arr) { + for (let id = 0; id < arr.rows.length; id++) { + if (arr.rows[id].value.common.enabled) { + stoppedList.push(arr.rows[id].value); + } + } + } + enableAdapters(stoppedList, false, function () { + that.downloadPacket(sources, packetName + '@' + version, options, stoppedList, callback); + }); + }); + return; + } + + // try to extract the information from local sources-dist.json + if (!sources[packetName]) { + try { + const sourcesDist = JSON.parse(fs.readFileSync(__dirname + '/../../conf/sources-dist.json', 'utf8')); + sources[packetName] = sourcesDist[packetName]; + } catch (e) { + + } + } + + if (sources[packetName]) { + url = sources[packetName].url; + + if (url && + packetName === 'js-controller' && + fs.existsSync(__dirname + '/../../../../node_modules/' + tools.appName + '.js-controller')) { + url = null; + } + + if (!url && packetName !== 'example') { + // Install node modules + that.npmInstallWithCheck(tools.appName.toLowerCase() + '.' + packetName + (version ? '@' + version : ''), options, false, function () { + // command succeeded + enableAdapters(stoppedList, true, function () { + if (callback) callback(packetName); + }); + }); + return; + } + if (url && url.match(/\/tarball\/master$/)) { + // Install node modules + that.npmInstallWithCheck(url, options, false, function () { + // command succeeded + enableAdapters(stoppedList, true, function () { + if (callback) callback(packetName); + }); + }); + return; + } + // Adapter + if (!url) { + console.warn('host.' + hostname + ' Adapter "' + packetName + '" can be updated only together with ' + tools.appName + '.js-controller'); + if (typeof callback === 'function') callback(packetName); + return; + } + name = packetName.replace(/[\/ $&*\\]/g, '_'); + } else { + url = packetName; + if (url.indexOf('http://') === -1 && url.indexOf('https://') === -1 && url.indexOf('file://') === -1) { + console.error('host.' + hostname + ' Unknown packetName ' + packetName); + processExit(5); + } + name = Math.floor(Math.random() * 0xFFFFFFE); + } + + const ncp = require('ncp').ncp; + ncp.limit = 16; + + console.log('host.' + hostname + ' download ' + url); + + tools.getFile(url, name + '.zip', function (tmpFile) { + tmpFile = path.normalize(tmpFile); + console.log('host.' + hostname + ' unzip ' + tmpFile); + + // Extract files into tmp/ + extractFiles(tmpFile, path.join(__dirname + '/../../tmp/', name), function (error) { + if (error) { + console.error(error); + processExit(12); + } + // Find out the first directory + const dirs = fs.readdirSync(__dirname + '/../../tmp/' + name); + if (dirs.length) { + const source = __dirname + '/../../tmp/' + name + ((dirs.length === 1) ? '/' + dirs[0] : ''); + // Copy files into adapter or controller + if (fs.existsSync(source + '/io-package.json')) { + let packetIo; + try { + packetIo = JSON.parse(fs.readFileSync(source + '/io-package.json', 'utf8')); + } catch (e) { + console.error('host.' + hostname + ' io-package.json has invalid format! Installation terminated.'); + if (typeof callback === 'function') callback(name, 'Invalid io-package.json!'); + processExit(6); + } + + let destination = __dirname + '/../..'; + if (!packetIo.common.controller) { + if (fs.existsSync(destination + '/../../node_modules')) { + destination += '/../' + tools.appName + '.' + packetIo.common.name; + } else { + destination += '/node_modules/' + tools.appName + '.' + packetIo.common.name; + } + } + + destination = path.normalize(destination); + + console.log('host.' + hostname + ' copying ' + source + ' to ' + destination + '(Version: ' + packetIo.common.version + ')'); + + ncp(source, destination, function (err) { + if (err) { + console.error('host.' + hostname + ' ncp error: ' + err); + processExit(7); + } + if (tmpFile.substring(0, (path.normalize(__dirname + '/../../tmp/')).length) === path.normalize(__dirname + '/../../tmp/')) { + console.log('host.' + hostname + ' delete ' + tmpFile); + fs.unlinkSync(tmpFile); + } + console.log('host.' + hostname + ' delete ' + path.normalize(__dirname + '/../../tmp/' + name)); + tools.rmdirRecursiveSync(__dirname + '/../../tmp/' + name); + + // Call npm install + if (typeof callback === 'function') { + enableAdapters(stoppedList, true, function () { + if (callback) callback(name, packetIo); + }); + } + + }); + } else { + console.error('host.' + hostname + ' io-package.json not found in ' + source + '/io-package.json. Invalid packet! Installation terminated.'); + if (typeof callback === 'function') callback(name, 'Invalid packet!'); + processExit(8); + } + } else { + console.error('host.' + hostname + ' Packet is empty! Installation terminated.'); + if (typeof callback === 'function') callback(name, 'Packet is empty'); + processExit(12); + } + }); + }); + }; + + this.npmInstallWithCheck = function (npmUrl, options, debug, callback) { + // Get npm version + try { + let npmVersion; + try { + npmVersion = child_process.execSync('npm -v', {encoding: 'utf8'}); + if (npmVersion) npmVersion = semver.valid(npmVersion.trim()); + console.log('NPM version: ' + npmVersion); + } catch (e) { + console.error('Error trying to check npm version: ' + e); + } + + if (!npmVersion) { + console.error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); + console.error('Aborting install because the npm version could not be checked!'); + console.error('Please check that npm is installed correctly.'); + console.error('Use "npm install -g npm@4" or "npm install -g npm@>=5.7.1" to install a supported version.'); + console.error('You need to make sure to repeat this step after installing an update to NodeJS and/or npm'); + console.error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); + processExit(25); + return; + } + + if (semver.gte(npmVersion, "5.0.0") && semver.lt(npmVersion, "5.7.1")) { + console.error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); + console.error('NPM 5 is only supported starting with version 5.7.1!'); + console.error('Please use "npm install -g npm@4" to downgrade npm to 4.x or '); + console.error('use "npm install -g npm@>=5.7.1" to install a supported version of npm 5!'); + console.error('You need to make sure to repeat this step after installing an update to NodeJS and/or npm'); + console.error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); + processExit(25); + return; + } + + this.npmInstall(npmUrl, options, debug, callback); + } catch (e) { + console.error('Could not check npm version: ' + e); + console.error('Assuming that correct version is installed.'); + } + }; + + this.npmInstall = function (npmUrl, options, debug, callback) { + if (typeof options !== 'object') { + options = {}; + } + + // Install node modules + /** @type {string|string[]} */ + let cwd = __dirname.replace(/\\/g, '/'); + if (fs.existsSync(__dirname + '/../../../../node_modules/' + tools.appName + '.js-controller')) { + // js-controller installed as npm + cwd = cwd.split('/'); + cwd.splice(cwd.length - 4, 4); + cwd = cwd.join('/'); + } else { + // remove lib + cwd = cwd.split('/'); + cwd.pop(); + cwd.pop(); + cwd = cwd.join('/'); + } + + // zwave for example requires always unsafe-perm option + for (let a = 0; a < unsafePermAlways.length; a++) { + if (npmUrl.indexOf(unsafePermAlways[a]) !== -1) { + options.unsafePerm = true; + break; + } + } + + tools.disablePackageLock(function (err) { + const cmd = 'npm install ' + npmUrl + (options.unsafePerm ? ' --unsafe-perm' : '') + ' --production --save --prefix "' + cwd + '"'; + + console.log(cmd + ' (System call)'); + // Install node modules as system call + + // System call used for update of js-controller itself, + // because during installation npm packet will be deleted too, but some files must be loaded even during the install process. + const exec = require('child_process').exec; + const child = exec(cmd); + child.stderr.pipe(process.stdout); + if (debug || params.debug) { + child.stdout.pipe(process.stdout); + } + child.on('exit', function (code /* , signal */) { + // code 1 is strange error that cannot be explained. Everything is installed but error :( + if (code && code !== 1) { + console.error('host.' + hostname + ' Cannot install ' + npmUrl + ': ' + code); + processExit(25); + } + // create file that indicates, that npm was called there + if (npmUrl.indexOf(':') === -1 && fs.existsSync(cwd + '/node_modules/' + npmUrl)) { + fs.writeFileSync(cwd + '/node_modules/' + npmUrl + '/iob_npm.done', ' '); + } + // command succeeded + if (callback) callback(npmUrl, cwd + '/node_modules'); + }); + }); + }; + + this.npmUninstall = function (packageName, options, debug, callback) { + if (typeof options !== 'object') { + options = {}; + } + + // TODO: fine nicer way to find the root directory + + // Install node modules + /** @type {string|string[]} */ + let cwd = __dirname.replace(/\\/g, '/'); + if (fs.existsSync(__dirname + '/../../../../node_modules/' + tools.appName + '.js-controller')) { + // js-controller installed as npm + cwd = cwd.split('/'); + cwd.splice(cwd.length - 4, 4); + cwd = cwd.join('/'); + } else { + // remove lib + cwd = cwd.split('/'); + cwd.pop(); + cwd.pop(); + cwd = cwd.join('/'); + } + + tools.disablePackageLock(function (err) { + let cmd = `npm uninstall ${packageName} --silent --save --prefix "${cwd}"`; + + console.log(cmd + ' (System call)'); + // Install node modules as system call + + // System call used for update of js-controller itself, + // because during installation npm packet will be deleted too, but some files must be loaded even during the install process. + const exec = require('child_process').exec; + const child = exec(cmd); + child.stderr.pipe(process.stdout); + if (debug || params.debug) { + child.stdout.pipe(process.stdout); + } + child.on('exit', function (code /* , signal */) { + // code 1 is strange error that cannot be explained. Everything is installed but error :( + if (code && code !== 1) { + if (typeof callback === "function") callback(`host.${hostname}: Cannot uninstall ${packageName}: ${code}`); + } + // command succeeded + if (callback) callback(); + }); + }); + }; + /** @type {(packageName: string, options: any, debug: boolean) => Promise} */ + this.npmUninstallAsync = tools.promisify(this.npmUninstall, this); + + this.uploadStaticObjects = function (adapter, adapterConf, callback) { + if (typeof adapterConf === 'function') { + callback = adapterConf; + adapterConf = null; + } + if (!adapterConf) { + const adapterDir = tools.getAdapterDir(adapter); + if (!fs.existsSync(adapterDir + '/io-package.json')) { + console.error('host.' + hostname + ' Adapter directory "' + adapterDir + '" does not exists'); + callback(17, adapter); + return; + } + try { + adapterConf = JSON.parse(fs.readFileSync(adapterDir + '/io-package.json').toString()); + } catch (e) { + console.error('host.' + hostname + ' error: reading io-package.json ' + e, adapter); + callback(17, adapter); + return; + } + } + + let objs; + if (adapterConf.objects && adapterConf.objects.length > 0) { + objs = adapterConf.objects; + } else { + objs = []; + } + function checkDependencies(deps, _options, callback) { + if (!deps || !deps.length) { + if (callback) callback(adapter); + return; + } + + let cnt = 0; + // Get all installed adapters + objects.getObjectView('system', 'instance', {}, null, function (err, objs) { + if (err) console.error(err); + if (objs && objs.rows && objs.rows.length) { + for (let i = 0; i < deps.length; i++) { + let dName; + let version = null; + let isFound = false; + + if (typeof deps[i] === 'object') { + for (let d in deps[i]) { + if (!deps[i].hasOwnProperty(d)) continue; + dName = d; + version = deps[i][d]; + break; + } + } else { + dName = deps[i]; + } + + if (dName === 'js-controller') { + // Check only version + if (version !== null) { + const iopkg_ = JSON.parse(fs.readFileSync(__dirname + '/../../package.json', 'utf8')); + if (!semver.satisfies(iopkg_.version, version)) { + console.error('host.' + hostname + ' Invalid version of "' + dName + '". Installed "' + iopkg_.version + '", required "' + version); + processExit(30); + } else { + isFound = true; + } + } else { + isFound = true; + } + } + + if (!isFound) { + for (let t = 0; t < objs.rows.length; t++) { + if (objs.rows[t] && objs.rows[t].value && objs.rows[t].value.common && objs.rows[t].value.common.name === dName) { + + if (version !== null) { + // var iopkg = JSON.parse(fs.readFileSync(__dirname + '/../../package.json')); + if (!semver.satisfies(objs.rows[t].value.common.version, version)) { + console.error('host.' + hostname + ' Invalid version of "' + dName + '". Installed "' + objs.rows[t].value.common.version + '", required "' + version); + processExit(30); + } else { + isFound = true; + } + } else { + isFound = true; + } + + break; + } + } + } + + if (!isFound) { + cnt++; + that.createInstance(dName, _options, function (name) { + upload.uploadAdapter(name, true, false, function () { + upload.uploadAdapter(name, false, false, function () { + cnt--; + if (!cnt && callback) callback(adapter); + }); + }); + }); + } + } + } + if (!cnt && callback) callback(adapter); + }); + } + + checkDependencies(adapterConf.common.dependencies, params, function () { + adapterConf.common.installedVersion = adapterConf.common.version; + + objs.push({ + _id: 'system.adapter.' + adapterConf.common.name, + type: 'adapter', + common: adapterConf.common, + native: adapterConf.native + }); + + function setObjects(_objs, _callback) { + if (!_objs || _objs.length === 0) { + _callback(null, adapter); + } else { + let obj = _objs.pop(); + + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = new Date().getTime(); + + objects.extendObject(obj._id, obj, function (err /* , res */) { + if (err) { + console.error('host.' + hostname + ' error setObject ' + obj._id + ' ' + err); + _callback(17, adapter); + } else { + console.log('host.' + hostname + ' object ' + obj._id + ' created'); + setImmediate(setObjects, _objs, _callback); + } + }); + } + } + + setObjects(objs, callback); + }); + }; + + function installAdapter(adapter, callback) { + const adapterDir = tools.getAdapterDir(adapter); + + console.log('host.' + hostname + ' install adapter ' + adapter); + + if (!fs.existsSync(adapterDir + '/io-package.json')) { + if (installCount === 2) { + console.error('host.' + hostname + ' Cannot install ' + adapter); + processExit(13); + return; + } + installCount++; + + that.downloadPacket(null, adapter, null, function () { + installAdapter(adapter, callback); + }); + return; + } + installCount = 0; + let adapterConf; + try { + adapterConf = JSON.parse(fs.readFileSync(adapterDir + '/io-package.json').toString()); + } catch (e) { + console.error('host.' + hostname + ' error: reading io-package.json ' + e); + processExit(14); + } + + // Check if the operation system is ok + if (adapterConf.common && adapterConf.common.os) { + if (typeof adapterConf.common.os === 'string' && adapterConf.common.os !== require('os').platform()) { + console.error('host.' + hostname + ' Adapter does not support current os. Required ' + adapterConf.common.os + '. Actual platform: ' + require('os').platform()); + processExit(15); + } else { + if (adapterConf.common.os.indexOf(require('os').platform()) === -1) { + console.error('host.' + hostname + ' Adapter does not support current os. Required one of ' + adapterConf.common.os.join(', ') + '. Actual platform: ' + require('os').platform()); + processExit(16); + } + } + } + + if (!fs.existsSync(adapterDir + '/node_modules')) { + // Install node modules + installNpm(adapter, function (err, _adapter) { + if (err) { + processExit(err); + } else { + upload.uploadAdapter(_adapter, true, true, function () { + upload.uploadAdapter(_adapter, false, true, function () { + callInstallOfAdapter(_adapter, adapterConf, function () { + that.uploadStaticObjects(adapter, function (err /* , _adapter */) { + if (err) { + processExit(err); + } else { + callback(adapter); + } + }); + }); + }); + }); + } + }); + } else { + upload.uploadAdapter(adapter, true, true, function () { + upload.uploadAdapter(adapter, false, true, function () { + callInstallOfAdapter(adapter, adapterConf, function () { + that.uploadStaticObjects(adapter, function (err /* , _adapter */) { + if (err) { + processExit(err); + } else { + callback(adapter); + } + }); + }); + }); + }); + } + } + + function callInstallOfAdapter(adapter, config, callback) { + const path_ = tools.getAdapterDir(adapter); + + if (config.common.install && fs.existsSync(path_ + '/io-package.json')) { + // Install node modules + const exec = require('child_process').exec; + let cmd = 'node '; + + let fileName = config.common.main || 'main.js'; + if (!fs.existsSync(path_ + '/' + fileName)) { + fileName = adapter + '.js'; + } + cmd += '"' + path + '/' + fileName + '" --install'; + console.log('host.' + hostname + ' command: ' + cmd); + const child = exec(cmd); + child.stderr.pipe(process.stdout); + child.on('exit', function () { + if (callback) callback(adapter); + }); + } else { + if (callback) callback(adapter); + } + } + + //options = enabled, host, port + this.createInstance = function (adapter, options, callback) { + const adapterDir = tools.getAdapterDir(adapter); + if (typeof options === 'function') { + callback = options; + options = null; + } + let ignoreIfExists = false; + if (!options) options = {}; + if (!options.host) options.host = tools.getHostName(); + if (options.enabled === 'true') options.enabled = true; + if (options.enabled === 'false') options.enabled = false; + if (options.ignoreIfExists !== undefined) { + ignoreIfExists = !!options.ignoreIfExists; + delete options.ignoreIfExists; + } + + if (!mime) mime = require('mime'); + + objects.getObject('system.adapter.' + adapter, function (err, doc) { + + // Adapter is not installed - install it now + if (err || !doc || !doc.common.installedVersion) { + installAdapter(adapter, function () { + that.createInstance(adapter, options, callback); + }); + return; + } + + // Check if some web pages should be uploaded + upload.uploadAdapter(adapter, true, false, function () { + upload.uploadAdapter(adapter, false, false, function () { + objects.getObjectView('system', 'instance', {startkey: 'system.adapter.' + adapter + '.', endkey: 'system.adapter.' + adapter + '.\u9999'}, null, function (err, res) { + let a; + if (err || !res) { + console.error('host.' + hostname + ' error: view instanceStats ' + err); + processExit(18); + return; + } + + // Count started instances + if (doc.common.singleton && res.rows.length) { + if (ignoreIfExists) { + callback && callback(); + return; + } + console.error('host.' + hostname + ' error: this adapter does not allow multiple instances'); + processExit(19); + return; + } + + // check singletonHost one on host + if (doc.common.singletonHost) { + for (a = 0; a < res.rows.length; a++) { + if (res.rows[a].value.common.host === hostname) { + if (ignoreIfExists) { + callback && callback(); + return; + } + console.error('host.' + hostname + ' error: this adapter does not allow multiple instances on one host'); + processExit(21); + return; + } + } + } + + let adapterConf; + let instance = null; + + if (options.instance !== undefined) { + instance = options.instance; + // find max instance + if (res.rows.find(obj => parseInt(obj.id.split('.').pop(), 10) === instance)) { + console.error('host.' + hostname + ' error: instance yet exists'); + processExit(26); + return; + } + } else { + // find max instance + for (a = 0; a < res.rows.length; a++) { + const iInstance = parseInt(res.rows[a].id.split('.').pop(), 10); + if (instance === null || iInstance > instance) { + instance = iInstance; + } + } + if (instance === null) { + instance = 0; + } else { + instance++; + } + } + + const instanceObj = doc; + doc = JSON.parse(JSON.stringify(doc)); + + instanceObj._id = 'system.adapter.' + adapter + '.' + instance; + instanceObj.type = 'instance'; + if (instanceObj._rev) delete instanceObj._rev; + instanceObj.common.enabled = (options.enabled === true || options.enabled === false) ? options.enabled : + ((instanceObj.common.enabled === true || instanceObj.common.enabled === false) ? instanceObj.common.enabled : false); + instanceObj.common.host = options.host; + + if (options.port) { + instanceObj.native = instanceObj.native || {}; + instanceObj.native.port = options.port; + } + + console.log('host.' + hostname + ' create instance ' + adapter); + + const _id = 'system.adapter.' + adapter + '.' + instance; + + let objs; + if (!instanceObj.common.onlyWWW && instanceObj.common.mode !== 'once') { + objs = [ + { + _id: _id + '.alive', + type: 'state', + common: { + name: adapter + '.' + instance + '.alive', + type: 'boolean', + read: true, + write: true, + role: 'indicator.state' + }, + native: {} + }, + { + _id: _id + '.connected', + type: 'state', + common: { + name: adapter + '.' + instance + '.connected', + type: 'boolean', + read: true, + write: false, + role: 'indicator.state' + }, + native: {} + }, + { + _id: _id + '.memHeapUsed', + type: 'state', + common: { + name: adapter + '.' + instance + '.memHeapUsed', + type: 'number', + read: true, + write: false, + role: 'indicator.state', + unit: 'MB' + }, + native: {} + }, + { + _id: _id + '.memHeapTotal', + type: 'state', + common: { + name: adapter + '.' + instance + '.memHeapTotal', + read: true, + write: false, + type: 'number', + role: 'indicator.state', + unit: 'MB' + }, + native: {} + }, + { + _id: _id + '.memRss', + type: 'state', + common: { + name: adapter + '.' + instance + '.memRss', + desc: 'Resident set size', + read: true, + write: false, + type: 'number', + role: 'indicator.state', + unit: 'MB' + }, + native: {} + }, + { + _id: _id + '.uptime', + type: 'state', + common: { + name: adapter + '.' + instance + '.uptime', + type: 'number', + read: true, + write: false, + role: 'indicator.state', + unit: 'seconds' + }, + native: {} + }, + { + _id: _id + '.inputCount', + type: 'state', + common: { + name: hostname + ' - inputs level', + desc: 'State\'s inputs in 15 seconds', + type: 'number', + read: true, + write: false, + role: 'state', + unit: 'events/15 seconds' + }, + native: {} + }, + { + _id: _id + '.outputCount', + type: 'state', + common: { + name: hostname + ' outputs level', + desc: 'State\'s outputs in 15 seconds', + type: 'number', + read: true, + write: false, + role: 'state', + unit: 'events/15 seconds' + }, + native: {} + } + ]; + } else { + objs = []; + } + + if (fs.existsSync(adapterDir + '/www')) { + objs.push({ + _id: 'system.adapter.' + adapter + '.upload', + type: 'state', + common: { + name: adapter + '.upload', + type: 'number', + read: true, + write: false, + role: 'indicator.state', + unit: '%', + def: 0, + desc: 'Upload process indicator' + }, + native: {} + }); + } + + if (instanceObj.common.wakeup) { + objs.push({ + _id: _id + '.wakeup', + type: 'state', + common: { + name: adapter + '.' + instance + '.wakeup', + read: true, + write: true, + type: 'boolean', + role: 'adapter.wakeup' + }, + native: {} + }); + } + + if (!adapterConf) { + try { + adapterConf = JSON.parse(fs.readFileSync(adapterDir + '/io-package.json').toString()); + } catch (e) { + console.error('host.' + hostname + ' error: reading io-package.json ' + e); + processExit(20); + } + } + if (!adapterConf.instanceObjects) adapterConf.instanceObjects = []; + if (!adapterConf.objects) adapterConf.objects = []; + + // Create only for this instance the predefined in io-package.json objects + // It is not necessary to write "system.adapter.name.N." in the object '_id' + for (let i = 0; i < adapterConf.instanceObjects.length; i++) { + adapterConf.instanceObjects[i]._id = adapter + '.' + instance + (adapterConf.instanceObjects[i]._id ? ('.' + adapterConf.instanceObjects[i]._id) : ''); + + if (adapterConf.instanceObjects[i].common) { + if (adapterConf.instanceObjects[i].common.name) { + adapterConf.instanceObjects[i].common.name = adapterConf.instanceObjects[i].common.name.replace('%INSTANCE%', instance); + } + if (adapterConf.instanceObjects[i].common.desc) { + adapterConf.instanceObjects[i].common.desc = adapterConf.instanceObjects[i].common.desc.replace('%INSTANCE%', instance); + } + } + + objs.push(adapterConf.instanceObjects[i]); + } + + /* these are already created on adapter install + if (adapterConf.objects && adapterConf.objects.length > 0) { + for (var j = 0, l = adapterConf.objects.length; j < l; j++) { + objs.push(adapterConf.objects[j]); + } + } + */ + + function setObjs() { + if (objs.length > 0) { + let obj = objs.pop(); + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = new Date().getTime(); + objects.setObject(obj._id, obj, function (err /*, res */) { + if (err) { + console.error('host.' + hostname + ' error: ' + err); + } else { + console.log('host.' + hostname + ' object ' + obj._id + ' created'); + } + setTimeout(setObjs, 25); + }); + } else { + instanceObj.from = 'system.host.' + tools.getHostName() + '.cli'; + instanceObj.ts = new Date().getTime(); + + objects.setObject(instanceObj._id, instanceObj, function (err /* , res */) { + if (err) { + console.error('host.' + hostname + ' error: ' + err); + } else { + console.log('host.' + hostname + ' object ' + instanceObj._id + ' created'); + } + + if (callback) { + callback(adapter); + } else { + processExit(0); + } + }); + } + } + + setObjs(); + }); + }); + }); + + }); + }; + + /** + * Enumerate all instances of an adapter + * @type {(knownObjIDs: string[], adapter: string, instance: string) => Promise} + */ + this.enumerateAdapterInstances = function enumerateInstances(knownObjIDs, adapter, instance) { + const startkey = instance ? + 'system.adapter.' + adapter + '.' + instance : + 'system.adapter.' + adapter; + const endkey = instance ? + 'system.adapter.' + adapter + '.' + instance : + 'system.adapter.' + adapter + '\u9999'; + + return getObjectViewAsync('system', 'instance', {startkey, endkey}, null).then(doc => { + if (doc.rows.length === 0) { + console.log('host.' + hostname + ' no instances of adapter ' + adapter + ' found'); + } else { + // add non-duplicates to the list + const newObjs = doc.rows + .filter(row => row && row.value && row.value._id) + .map(row => row.value._id) + .filter(id => knownObjIDs.indexOf(id) === -1) + ; + knownObjIDs.push.apply(knownObjIDs, newObjs); + if (newObjs.length > 0) { + console.log(`host.${hostname} Counted ${newObjs.length} instances of ${adapter}`); + } + } + }).catch(err => { + if (err !== 'Not exists') console.error('host.' + hostname + ' error: ' + err); + }) + }; + + /** + * Enumerate all meta objects of an adapter + * @type {(knownObjIDs: string[], adapter: string) => Promise} + */ + this.enumerateAdapterMeta = function enumerateMeta(knownObjIDs, adapter) { + return getObjectViewAsync('system', 'meta', {startkey: adapter + '.meta', endkey: adapter + '.meta\u9999'}).then(doc => { + if (doc.rows.length !== 0) { + // add non-duplicates to the list + const newObjs = doc.rows + .filter(row => row && row.value && row.value._id) + .map(row => row.value._id) + .filter(id => knownObjIDs.indexOf(id) === -1) + ; + knownObjIDs.push.apply(knownObjIDs, newObjs); + if (newObjs.length > 0) { + console.log(`host.${hostname} Counted ${newObjs.length} meta of ${adapter}`); + } + } + }).catch(err => { + if (err !== 'Not exists') console.error('host.' + hostname + ' error: ' + err); + }) + }; + + /** + * @type {(knownObjIDs: string[], adapter: string) => Promise} + * @returns 22 if the adapter could not be deleted, 0 otherwise + */ + this.enumerateAdapters = tools.poorMansAsync(function*(knownObjIDs, adapter) { + let resultCode = 0; + try { + const doc = yield getObjectViewAsync('system', 'adapter', {startkey: 'system.adapter.' + adapter, endkey: 'system.adapter.' + adapter + '\u9999'}) + if (doc.rows.length !== 0) { + // change nondeletable adapters + const nondeletable = doc.rows.filter(row => row.value.common.nondeletable); + if (nondeletable.length > 0) { + console.log('host.' + hostname + ' Adapter ' + adapter + ' cannot be deleted completely, because non-deletable.'); + resultCode = 22; + for (const row of nondeletable) { + const adapterConf = row.value; + try { + let oldObj = yield getObjectAsync(adapterConf._id); + if (oldObj) { + oldObj = extend(true, oldObj, {installedVersion: ''}); + } else { + oldObj = {installedVersion: ''}; + } + oldObj.from = 'system.host.' + tools.getHostName() + '.cli'; + oldObj.ts = new Date().getTime(); + yield setObjectAsync(adapterConf._id, oldObj); + } catch (e) { + console.error(e); + } + } + } + + // remember deletable adapters + const deletable = doc.rows.filter(row => !row.value.common.nondeletable); + if (deletable.length > 0) { + // add non-duplicates to the list + const newObjs = deletable + .map(row => row.value._id) + .filter(id => knownObjIDs.indexOf(id) === -1) + ; + knownObjIDs.push.apply(knownObjIDs, newObjs); + if (newObjs.length > 0) { + console.log(`host.${hostname} Counted ${newObjs.length} adapters for ${adapter}`); + } + } + } + } catch (e) { + if (e.message !== 'Not exists') console.error('host.' + hostname + ' error: ' + e); + } + return resultCode; + }); + + /** + * Enumerates the devices of an adapter (or instance) + * @param {string[]} knownObjIDs The already known object ids + * @param {string} adapter The adapter to enumerate the devices for + * @param {string} [instance] The instance to enumerate the devices for (optional) + */ + this.enumerateAdapterDevices = function enumerateAdapterDevices(knownObjIDs, adapter, instance) { + const adapterRegex = new RegExp(`^${adapter}${instance ? `\\.${instance}` : ''}`); + + return getObjectViewAsync('system', 'device', {}, null).then(doc => { + if (doc.rows.length !== 0) { + // add non-duplicates to the list + const newObjs = doc.rows + .filter(row => row && row.value && row.value._id) + .map(row => row.value._id) + .filter(id => adapterRegex.test(id)) + .filter(id => knownObjIDs.indexOf(id) === -1) + ; + knownObjIDs.push.apply(knownObjIDs, newObjs); + if (newObjs.length > 0) { + console.log(`host.${hostname} Counted ${newObjs.length} devices of ${adapter}${instance ? `.${instance}` : ''}`); + } + } + }).catch(err => { + if (err !== 'Not exists') console.error('host.' + hostname + ' error: ' + err); + }) + }; + + /** + * Enumerates the channels of an adapter (or instance) + * @param {string[]} knownObjIDs The already known object ids + * @param {string} adapter The adapter to enumerate the channels for + * @param {string} [instance] The instance to enumerate the channels for (optional) + */ + this.enumerateAdapterChannels = function enumerateAdapterChannels(knownObjIDs, adapter, instance) { + const adapterRegex = new RegExp(`^${adapter}${instance ? `\\.${instance}` : ''}`); + + return getObjectViewAsync('system', 'channel', {}, null).then(doc => { + if (doc.rows.length !== 0) { + // add non-duplicates to the list + const newObjs = doc.rows + .filter(row => row && row.value && row.value._id) + .map(row => row.value._id) + .filter(id => adapterRegex.test(id)) + .filter(id => knownObjIDs.indexOf(id) === -1) + ; + knownObjIDs.push.apply(knownObjIDs, newObjs); + if (newObjs.length > 0) { + console.log(`host.${hostname} Counted ${newObjs.length} channels of ${adapter}${instance ? `.${instance}` : ''}`); + } + } + }).catch(err => { + if (err !== 'Not exists') console.error('host.' + hostname + ' error: ' + err); + }) + }; + + /** + * Enumerates the states of an adapter (or instance) + * @param {string[]} knownObjIDs The already known object ids + * @param {string} adapter The adapter to enumerate the states for + * @param {string} [instance] The instance to enumerate the states for (optional) + */ + this.enumerateAdapterStateObjects = function enumerateAdapterStateObjects(knownObjIDs, adapter, instance) { + const adapterRegex = new RegExp(`^${adapter}${instance ? `\\.${instance}` : ''}`); + const sysAdapterRegex = new RegExp(`^system\\.adapter\\.${adapter}${instance ? `\\.${instance}` : ''}`); + + return getObjectViewAsync('system', 'state', {}, null).then(doc => { + if (doc.rows.length !== 0) { + // add non-duplicates to the list + const newObjs = doc.rows + .filter(row => row && row.value && row.value._id) + .map(row => row.value._id) + .filter(id => adapterRegex.test(id) || sysAdapterRegex.test(id)) + .filter(id => knownObjIDs.indexOf(id) === -1) + ; + knownObjIDs.push.apply(knownObjIDs, newObjs); + if (newObjs.length > 0) { + console.log(`host.${hostname} Counted ${newObjs.length} states of ${adapter}${instance ? `.${instance}` : ''}`); + } + } + }).catch(err => { + if (err !== 'Not exists') console.error('host.' + hostname + ' error: ' + err); + }) + }; + + // TODO: is enumerateAdapterDocs the correct name??? + /** + * Enumerates the docs of an adapter (or instance) + * @param {string[]} knownObjIDs The already known object ids + * @param {string} adapter The adapter to enumerate the states for + * @param {string} [instance] The instance to enumerate the states for (optional) + */ + this.enumerateAdapterDocs = function enumerateAdapterDocs(knownObjIDs, adapter, instance) { + const adapterRegex = new RegExp(`^${adapter}${instance ? `\\.${instance}` : ''}`); + const sysAdapterRegex = new RegExp(`^system\\.adapter\\.${adapter}${instance ? `\\.${instance}` : ''}`); + + return getObjectListAsync({include_docs: true}).then(doc => { + if (doc.rows.length !== 0) { + // add non-duplicates to the list + const newObjs = doc.rows + .filter(row => row && row.value && row.value._id) + .map(row => row.value._id) + .filter(id => adapterRegex.test(id) || sysAdapterRegex.test(id)) + .filter(id => knownObjIDs.indexOf(id) === -1) + ; + knownObjIDs.push.apply(knownObjIDs, newObjs); + if (newObjs.length > 0) { + console.log(`host.${hostname} Counted ${newObjs.length} objects of ${adapter}${instance ? `.${instance}` : ''}`); + } + } + }).catch(err => { + if (err !== 'Not exists') console.error('host.' + hostname + ' error: ' + err); + }); + }; + + /** + * Enumerate all state IDs of an adapter (or instance) + * @type {(knownStateIDs: string[], adapter: string, instance?: string) => Promise} + */ + this.enumerateAdapterStates = tools.poorMansAsync(function* (knownStateIDs, adapter, instance) { + for (const pattern of [ + `io.${adapter}.${instance ? instance + '.' : ''}*`, + `messagebox.${adapter}.${instance ? instance + '.' : ''}*`, + `log.${adapter}.${instance ? instance + '.' : ''}*`, + `${adapter}.${instance ? instance + '.' : ''}*`, + `system.adapter.${adapter}.${instance ? instance + '.' : ''}*` + ]) { + try { + const ids = yield getKeysAsync(pattern); + if (ids && ids.length) { + // add non-duplicates to the list + const newStates = ids + .filter(id => knownStateIDs.indexOf(id) === -1) + ; + knownStateIDs.push.apply(knownStateIDs, newStates); + if (newStates.length > 0) { + console.log(`host.${hostname} Counted ${newStates.length} states (${pattern}) from states`); + } + } + } catch (e) { + console.error(e); + } + } + }); + + /** + * delete WWW pages and objects + * @type {(adapter: string) => Promise} + */ + this.deleteWWW = tools.poorMansAsync(function* (adapter) { + for (const file of [ + adapter, adapter + '.admin' + ]) { + try { + yield unlinkAsync(file, ''); + } catch (e) { + if (e.message !== 'Not exists') console.error(`Cannot delete ${file} files folder: ${e}`); + } + } + + for (const objId of [ + adapter, adapter + '.admin' + ]) { + try { + const obj = yield delObjectAsync(objId); + if (obj) console.log(`host.${hostname} object ${objId} deleted`); + } catch (e) { + if (e.message !== 'Not exists') console.error('host.' + hostname + ' error: ' + e); + } + } + }); + + /** + * @type {(stateIDs: string[]) => Promise} + */ + this.deleteAdapterStates = tools.poorMansAsync(function*(stateIDs) { + if (stateIDs.length > 1000) { + console.log('host.' + hostname + ' Deleting ' + stateIDs.length + ' state(s). Be patient...'); + } else if (stateIDs.length) { + console.log('host.' + hostname + ' Deleting ' + stateIDs.length + ' state(s).'); + } + + while (stateIDs.length > 0) { + if (stateIDs.length % 200 === 0) { + // write progress report + console.log(`host.${hostname}: Only ${stateIDs.length} states left to be deleted.`); + } + // try to delete the current state + try { + yield delStateAsync(stateIDs.pop()); + } catch (e) { // yep that works! + if (e.message !== 'Not exists') console.error(e); + } + } + }); + + /** + * @type {(objIDs: string[]) => Promise} + */ + this.deleteAdapterObjects = tools.poorMansAsync(function*(objIDs) { + if (objIDs.length > 1000) { + console.log('host.' + hostname + ' Deleting ' + objIDs.length + ' object(s). Be patient...'); + } else if (objIDs.length) { + console.log('host.' + hostname + ' Deleting ' + objIDs.length + ' object(s).'); + } + + while (objIDs.length > 0) { + if (objIDs.length % 200 === 0) { + // write progress report + console.log(`host.${hostname}: Only ${objIDs.length} objects left to be deleted.`); + } + // try to delete the current state + try { + yield delObjectAsync(objIDs.pop()); + } catch (e) { + if (e.message !== 'Not exists') console.error('host.' + hostname + ' error: ' + e); + } + } + }); + + this.deleteAdapter = function (adapter, callback) { + const knownObjectIDs = []; + const knownStateIDs = []; + let resultCode = 0; + + const uninstallNpm = tools.poorMansAsync(function*() { + try { + // find the adapter's io-package.json + const adapterNpm = `${tools.appName}.${adapter}`; + const ioPack = require(`${adapterNpm}/io-package.json`); // yep, it's that easy + if (!ioPack.common || !ioPack.common.nondeletable) { + yield that.npmUninstallAsync(adapterNpm, null, false); + } + } catch (e) { + console.error(`Error deleting adapter ${adapter} from disk: ${e}`); + console.error(`You might have to delete it yourself!`); + } + }); + + that.enumerateAdapterInstances(knownObjectIDs, adapter) + .then(() => that.enumerateAdapterMeta(knownObjectIDs, adapter)) + .then(() => that.enumerateAdapters(knownObjectIDs, adapter).then(ret => resultCode = ret)) + .then(() => that.enumerateAdapterDevices(knownObjectIDs, adapter)) + .then(() => that.enumerateAdapterChannels(knownObjectIDs, adapter)) + .then(() => that.enumerateAdapterStateObjects(knownObjectIDs, adapter)) + .then(() => that.enumerateAdapterStates(knownStateIDs, adapter)) + .then(() => that.deleteWWW(adapter)) + .then(() => that.deleteAdapterObjects(knownObjectIDs)) + .then(() => that.deleteAdapterStates(knownStateIDs)) + .then(uninstallNpm) + .catch(err => console.error(`There was an error uninstalling ${adapter}: ${err}`)) + .then(() => callback(adapter, resultCode)) + ; + + }; + + this.deleteInstance = function (adapter, instance, callback) { + const knownObjectIDs = []; + const knownStateIDs = []; + + that.enumerateAdapterInstances(knownObjectIDs, adapter, instance) + .then(() => that.enumerateAdapterDevices(knownObjectIDs, adapter, instance)) + .then(() => that.enumerateAdapterChannels(knownObjectIDs, adapter, instance)) + .then(() => that.enumerateAdapterStateObjects(knownObjectIDs, adapter, instance)) + .then(() => that.enumerateAdapterStates(knownStateIDs, adapter, instance)) + .then(() => that.enumerateAdapterDocs(knownObjectIDs, adapter, instance)) + .then(() => that.deleteAdapterObjects(knownObjectIDs)) + .then(() => that.deleteAdapterStates(knownStateIDs)) + .then(() => callback(adapter, instance)) + ; + + // TODO delete meta objects - i think a recursive deletion of all child object would be less effort. + }; +} + +module.exports = Install; diff --git a/lib/setup/setupLicense.js b/lib/setup/setupLicense.js new file mode 100644 index 0000000..a419db0 --- /dev/null +++ b/lib/setup/setupLicense.js @@ -0,0 +1,76 @@ +'use strict'; +function License(options) { + const fs = require('fs'); + const jwt = require('jsonwebtoken'); + options = options || {}; + + let objects = options.objects; + // read info from '/etc/iob_vendor.json' and executes instructions stored there + this.setLicense = file => { + if (fs.existsSync(file)) { + try { + file = fs.readFileSync(file).toString('utf8'); + } catch (e) { + return Promise.reject(e); + } + } + // try to encode license + let license = jwt.decode(file); + if (!license) { + return Promise.reject('License cannot be decoded'); + } + if (!license.name) { + return Promise.reject('Name not found in the license'); + } + let adapter = license.name.split('.')[1]; + if (!adapter) { + return Promise.reject(`Invalid license name ${license.name}`); + } + // read all instances of this adapter + return objects.getObjectListAsync({ + startkey: 'system.adapter.' + adapter + '.', + endkey: 'system.adapter.' + adapter + '.\u9999' + }, {checked: true}).then(arr => { + let promises = []; + if (arr && arr.rows && arr.rows.length) { + for (let g = 0; g < arr.rows.length; g++) { + let obj = arr.rows[g].value; + if (obj && obj.type === 'instance') { + obj.native = obj.native || {}; + obj.native.license = file; + promises.push(objects.setObjectAsync(obj._id, obj).then(() => { + console.log(`Instance "${obj._id}" updated`); + }).catch(err => { + console.error(`Cannot update "${obj._id}": ${err}`); + })); + } + } + } + if (!promises.length) { + console.warn(`no instances of ${adapter} found`); + if (arr && arr.rows && arr.rows.length) { + for (let g = 0; g < arr.rows.length; g++) { + let obj = arr.rows[g].value; + if (obj && obj.type === 'adapter') { + obj.native = obj.native || {}; + obj.native.license = file; + promises.push(objects.setObjectAsync(obj._id, obj).then(() => { + console.log(`Adapter "${obj._id}" updated`); + }).catch(err => { + console.error(`Cannot update "${obj._id}": ${err}`); + })); + } + } + } + } + if (!promises.length) { + console.error(`no installations of ${adapter} found`); + } + return Promise.all(promises); + }); + }; + + return this; +} + +module.exports = License; \ No newline at end of file diff --git a/lib/setup/setupList.js b/lib/setup/setupList.js new file mode 100644 index 0000000..725bad4 --- /dev/null +++ b/lib/setup/setupList.js @@ -0,0 +1,627 @@ +'use strict'; +function List(options) { + var fs = require('fs'); + var tools = require(__dirname + '/../tools.js'); + + var that = this; + + options = options || {}; + + if (!options.states) throw 'Invalid arguments: states is missing'; + if (!options.objects) throw 'Invalid arguments: objects is missing'; + if (!options.processExit) throw 'Invalid arguments: processExit is missing'; + + var objects = options.objects; + var states = options.states; + var processExit = options.processExit; + + function perm2str(perm) { + var result = ''; + // user + result += (perm & 0x400) ? 'r' : '-'; + result += (perm & 0x200) ? 'w' : '-'; + result += (perm & 0x100) ? 'x' : '-'; + // group + result += (perm & 0x040) ? 'r' : '-'; + result += (perm & 0x020) ? 'w' : '-'; + result += (perm & 0x010) ? 'x' : '-'; + // any + result += (perm & 0x004) ? 'r' : '-'; + result += (perm & 0x002) ? 'w' : '-'; + result += (perm & 0x001) ? 'x' : '-'; + return result; + } + + this.showFileHeader = function () { + console.log(' Modified at |Permission| User | Group | Size | Name'); + console.log('----------------+----------+--------------+--------------+------+---------'); + }; + + this.showFile = function (adapter, path, file) { + //drwxr-xr-x 1 odroid odroid 43 Oct 3 2013 .xsessionrc + var text = ''; + var time; + if (file.modifiedAt) { + var ts = new Date(file.modifiedAt); + time = ts.toISOString(); + time = time.replace('T', ' '); + time = time.substring(0, 16) + ' '; + } else { + time = Array(18).join(' '); + } + text += time; + + if (file.acl){ + text += (file.isDir ? 'd' : '-') + perm2str(file.acl.permissions || 0); + var owner = file.acl.owner; + if (owner) { + owner = owner.substring(12); + if (owner.length < 15) owner = Array(15 - owner.length).join(' ') + owner; + } else { + owner = Array(15).join(' '); + } + text += ' ' + owner; + var group = file.acl.ownerGroup; + if (group) { + group = group.substring(13); + if (group.length < 15) group = Array(15 - group.length).join(' ') + group; + } else { + group = Array(15).join(' '); + } + + text += ' ' + group; + } else { + text += (file.isDir ? 'd' : '-') + '?????????' + Array(31).join(' '); + } + var size = (file.stats) ? file.stats.size.toString() : ''; + if (size.length < 7) size = Array(7 - size.length).join(' ') + size; + + text += ' ' + size + ' ' + adapter + ((!path || path[0] === '/') ? '' : '/') + path + '/' + file.file; + if (file.isDir) { + text += '/'; + console.log(text); + } else { + console.log(text); + } + }; + + this.showObjectHeader = function () { + console.log('ObjectAC | StateAC | User | Group | ID'); + console.log('---------+---------+--------------+--------------+--------------'); + }; + + this.showObject = function (obj) { + //drwxr-xr-x 1 odroid odroid 43 Oct 3 2013 .xsessionrc + var text = ''; + if (obj.acl){ + text += perm2str(obj.acl.object || 0) + ' ' + ((obj.type === 'state') ? perm2str(obj.acl.state || 0) : ' '); + var owner = obj.acl.owner; + if (owner) { + owner = owner.substring(12); + if (owner.length < 15) owner = Array(15 - owner.length).join(' ') + owner; + } else { + owner = Array(15).join(' '); + } + text += ' ' + owner; + var group = obj.acl.ownerGroup; + if (group) { + group = group.substring(13); + if (group.length < 15) group = Array(15 - group.length).join(' ') + group; + } else { + group = Array(15).join(' '); + } + text += ' ' + group; + } else { + text += '?????????' + ((obj.type === 'state') ? ' ?????????' : ' ') + Array(31).join(' '); + } + text += ' ' + obj._id; + console.log(text); + }; + + this.listDirectory = function (adapter, path, allFiles, callback) { + if (typeof path === 'function') { + callback = path; + path = ''; + allFiles = []; + } + if (typeof allFiles === 'function') { + callback = allFiles; + allFiles = []; + } + allFiles = allFiles || []; + + path = path || ''; + + objects.readDir(adapter, path, null, function (err, files) { + if (err && err.code === 'ENOTDIR') { + var pos = path.lastIndexOf('/'); + if (pos !== -1) { + var dir = path.substring(0, pos); + var fname = path.substring(pos + 1); + objects.readDir(adapter, dir, null, function (err, files) { + if (err) { + console.log('Cannot read "' + path + '": ' + err); + callback(allFiles); + } else { + for (var f = 0; f < files.length; f++) { + if (files[f].file === '.' || files[f].file === '..') continue; + if (files[f].file === fname) { + allFiles.push({adapter: adapter, path: path, file: files[f]}); + break; + } + } + callback(allFiles); + } + }); + } else { + callback(allFiles); + } + } else { + var count = 0; + for (var f = 0; f < files.length; f++) { + if (files[f].file === '.' || files[f].file === '..') continue; + allFiles.push({adapter: adapter, path: path, file: files[f]}); + if (files[f].isDir) { + count++; + that.listDirectory(adapter, path + '/' + files[f].file, allFiles, function () { + if (!--count && callback) { + callback(allFiles); + } + }); + } + } + if (!count && callback) callback(allFiles); + } + }); + }; + + function pattern2RegEx(pattern) { + if (pattern !== '*') { + if (pattern[0] === '*' && pattern[pattern.length - 1] !== '*') pattern += '$'; + if (pattern[0] !== '*' && pattern[pattern.length - 1] === '*') pattern = '^' + pattern; + } + pattern = pattern.replace(/\./g, '\\.'); + pattern = pattern.replace(/\*/g, '.*'); + return pattern; + } + + function sortFiles (a, b) { + var a1 = a.path + a.file.file; + if (a1[0] !== '/') a1 = '/' + a1; + var b1 = b.path + b.file.file; + if (b1[0] !== '/') b1 = '/' + b1; + return a1.localeCompare(b1); + } + + this.listAdaptersFiles = function (adapters, filter, callback) { + if (typeof filter === 'function') { + callback = filter; + filter = null; + } + if (filter) { + filter = filter.replace('*', ''); + if (filter[filter.length - 1] === '/') filter = filter.substring(0, filter.length - 1); + } + + + if (adapters && adapters.length) { + var adapter = adapters.pop(); + this.listDirectory(adapter, filter, function (files) { + files.sort(sortFiles); + that.showFileHeader(); + for (var k = 0; k < files.length; k++) { + if (filter && filter !== (files[k].path + '/'+ files[k].file.file).substring(0, filter.length)) continue; + + that.showFile(files[k].adapter, files[k].path, files[k].file); + } + + that.listDirectory(adapter + '.admin', filter, function (files) { + files.sort(sortFiles); + for (var k = 0; k < files.length; k++) { + if (filter && filter !== (files[k].path + '/'+ files[k].file.file).substring(0, filter.length)) continue; + + that.showFile(files[k].adapter, files[k].path, files[k].file); + } + + that.listAdaptersFiles(adapters, callback); + }); + }); + } else { + if (callback) callback (); + } + }; + + this.list = function (type, filter, flags) { + switch (type) { + case 'objects': + case 'o': + objects.getObjectList(null, function (err, objs) { + var reg = filter ? new RegExp(pattern2RegEx(filter)) : null; + for (var i = 0; i < objs.rows.length; i++) { + if (!reg || reg.test(objs.rows[i].value._id) || (objs.rows[i].value.common && reg.test(objs.rows[i].value.common.name))) { + if (objs.rows[i].value.type) { + var id = objs.rows[i].value._id; + var type = objs.rows[i].value.type; + if (id.length < 40) id += Array(40 - id.length).join(' '); + if (type.length < 10) type += Array(10 - type.length).join(' '); + + console.log(id + ': ' + type + ' - ' + (objs.rows[i].value.common.name || '')); + } else { + console.log(objs.rows[i].value._id); + } + } + } + setTimeout(function () { + processExit(); + }, 1000); + }); + break; + + case 'states': + case 's': + states.getKeys(filter || '*', function (err, keys) { + if (err) { + console.error(err); + processExit(23); + } + states.getStates(keys, function (err, states) { + if (err) { + console.error(err); + processExit(23); + } + for (var i = 0; i < states.length; i++) { + var id = keys[i]; + var from = states[i].from || ''; + var type = typeof states[i].val; + if (type.length < 10) type += Array(10 - type.length).join(' '); + if (id.length < 40) id += Array(40 - id.length).join(' '); + if (from.length < 30) from += Array(30 - from.length).join(' '); + console.log(id + ': from [' + from + '] (' + type + ') ' + (states[i].ack ? ' ack': 'not ack') + ' ' + JSON.stringify(states[i].val)); + } + setTimeout(function () { + processExit(); + }, 1000); + }); + }); + break; + + case 'adapters': + case 'a': + objects.getObjectList({startkey: 'system.adapter.', endkey: 'system.adapter.\u9999'}, function (err, objs) { + var reg = filter ? new RegExp(pattern2RegEx('system.adapter.' + filter)) : null; + for (var i = 0; i < objs.rows.length; i++) { + if (objs.rows[i].value.type !== 'adapter') continue; + if (!reg || reg.test(objs.rows[i].value._id) || (objs.rows[i].value.common && reg.test(objs.rows[i].value.common.name))) { + var id = objs.rows[i].value._id; + var name = objs.rows[i].value.common.name; + + if (id.length < 40) id += Array(40 - id.length).join(' '); + if (name.length < 12) name += Array(12 - name.length).join(' '); + + var text = id + ': ' + name + ' - ' + + objs.rows[i].value.common.version; + + console.log(text); + } + } + setTimeout(function () { + processExit(); + }, 1000); + }); + break; + + case 'instances': + case 'i': + objects.getObjectList({startkey: 'system.adapter.', endkey: 'system.adapter.\u9999'}, function (err, objs) { + var reg = filter ? new RegExp(pattern2RegEx('system.adapter.' + filter)) : null; + objs.rows.sort(function (a, b) { + if (a.id > b.id) return 1; + if (a.id < b.id) return -1; + return 0; + }); + for (var i = 0; i < objs.rows.length; i++) { + if (objs.rows[i].value.type !== 'instance') continue; + if (!reg || reg.test(objs.rows[i].value._id) || (objs.rows[i].value.common && reg.test(objs.rows[i].value.common.name))) { + if (flags.enabled && !objs.rows[i].value.common.enabled) continue; + if (flags.disabled && objs.rows[i].value.common.enabled) continue; + if (flags.port && objs.rows[i].value.native.port === undefined) continue; + if (flags.ssl && objs.rows[i].value.native.secure === undefined) continue; + if (flags.ip && objs.rows[i].value.native.bind === undefined) continue; + + var id = objs.rows[i].value._id; + var name = objs.rows[i].value.common.name; + + if (id.length < 40) id += Array(40 - id.length).join(' '); + if (name && name.length < 12) name += Array(12 - name.length).join(' '); + + var text = id + ': ' + (name || '') + ' - ' + + (objs.rows[i].value.common.enabled ? ' enabled' : 'disabled'); + + if (objs.rows[i].value.native && objs.rows[i].value.native.port) { + text += ', port: ' + objs.rows[i].value.native.port; + } + if (objs.rows[i].value.native && objs.rows[i].value.native.bind) { + text += ', bind: ' + objs.rows[i].value.native.bind; + } + if (objs.rows[i].value.native && objs.rows[i].value.native.secure) { + text += ' (SSL)'; + } + if (objs.rows[i].value.native && objs.rows[i].value.native.defaultUser) { + text += ', run as: ' + objs.rows[i].value.native.defaultUser; + } + console.log(text); + } + } + setTimeout(function () { + processExit(); + }, 1000); + }); + break; + + case 'users': + case 'u': + objects.getObjectList({startkey: 'system.user.', endkey: 'system.user.\u9999'}, function (err, objs) { + objects.getObjectList({startkey: 'system.group.', endkey: 'system.group.\u9999'}, function (err, groups) { + var reg = filter ? new RegExp(pattern2RegEx('system.user.' + filter)) : null; + console.log(' ID | Name | Active | Groups'); + console.log('---------------------------------------+-------------+----------+--------------'); + for (var i = 0; i < objs.rows.length; i++) { + if (objs.rows[i].value.type !== 'user') continue; + + if (!reg || reg.test(objs.rows[i].value._id) || (objs.rows[i].value.common && reg.test(objs.rows[i].value.common.name))) { + var id = objs.rows[i].value._id; + var name = objs.rows[i].value.common.name; + + if (id.length < 40) id += Array(40 - id.length).join(' '); + if (name.length < 12) name += Array(12 - name.length).join(' '); + + var text = id + '| ' + name + ' | ' + + (objs.rows[i].value.common.enabled ? ' enabled' : 'disabled') + ' |'; + var gs = []; + // find all groups + for (var g = 0; g < groups.rows.length; g++) { + if (groups.rows[g].value.common.members && groups.rows[g].value.common.members.indexOf(objs.rows[i].value._id) !== -1) { + gs.push(groups.rows[g].value._id.substring(13)); + } + } + console.log(text + ' ' + gs.join(', ')); + } + } + setTimeout(function () { + processExit(); + }, 1000); + }); + }); + break; + + case 'groups': + case 'g': + objects.getObjectList({startkey: 'system.group.', endkey: 'system.group.\u9999'}, function (err, objs) { + var reg = filter ? new RegExp(pattern2RegEx('system.group.' + filter)) : null; + console.log(''); + console.log(' system.group | object | state | file | user | others | users'); + console.log(' | l r w d | l r w d | l r w c d | w c d | |'); + console.log('--------------------+---------+---------+-----------+-------+------------------------+---------'); + for (var i = 0; i < objs.rows.length; i++) { + if (objs.rows[i].value.type !== 'group') continue; + if (!reg || reg.test(objs.rows[i].value._id) || (objs.rows[i].value.common && reg.test(objs.rows[i].value.common.name))) { + var id = objs.rows[i].value._id.substring(13); + //var name = objs.rows[i].value.common.name; + + if (id === 'administrator') { + objs.rows[i].value.common.acl = { + file: { + read: true, + write: true, + 'delete': true, + create: true, + list: true + }, + object: { + read: true, + write: true, + 'delete': true, + list: true + }, + state: { + read: true, + write: true, + 'delete': true, + create: true, + list: true + }, + user: { + write: true, + create: true, + 'delete': true + }, + other: { + execute: true, + http: true, + sendto: true + } + }; + } + + if (id.length < 20) id += Array(20 - id.length).join(' '); + var text = id; + text += ' | '; + if (objs.rows[i].value.common.acl && objs.rows[i].value.common.acl.object) { + text += (objs.rows[i].value.common.acl.object.list ? '+': '-') + ' '; + text += (objs.rows[i].value.common.acl.object.read ? '+': '-') + ' '; + text += (objs.rows[i].value.common.acl.object.write ? '+': '-') + ' '; + text += (objs.rows[i].value.common.acl.object.delete ? '+': '-') + ' '; + text += '|'; + } else { + text += ' |'; + } + if (objs.rows[i].value.common.acl && objs.rows[i].value.common.acl.state) { + text += ' '; + text +=(objs.rows[i].value.common.acl.state.list ? '+': '-') + ' '; + text +=(objs.rows[i].value.common.acl.state.read ? '+': '-') + ' '; + text +=(objs.rows[i].value.common.acl.state.write ? '+': '-') + ' '; + text +=(objs.rows[i].value.common.acl.state.delete ? '+': '-') + ' '; + text += '|'; + } else { + text += ' |'; + } + if (objs.rows[i].value.common.acl && objs.rows[i].value.common.acl.file) { + text += ' '; + text += (objs.rows[i].value.common.acl.file.list ? '+': '-') + ' '; + text += (objs.rows[i].value.common.acl.file.read ? '+': '-') + ' '; + text += (objs.rows[i].value.common.acl.file.write ? '+': '-') + ' '; + text += (objs.rows[i].value.common.acl.file.create ? '+': '-') + ' '; + text += (objs.rows[i].value.common.acl.file.delete ? '+': '-') + ' '; + text += '|'; + } else { + text += ' |'; + } + if (objs.rows[i].value.common.acl && objs.rows[i].value.common.acl.users) { + text += ' '; + text += (objs.rows[i].value.common.acl.users.write ? '+': '-') + ' '; + text += (objs.rows[i].value.common.acl.users.create ? '+': '-') + ' '; + text += (objs.rows[i].value.common.acl.users.delete ? '+': '-') + ' '; + text += '|'; + } else { + text += ' |'; + } + if (objs.rows[i].value.common.acl && objs.rows[i].value.common.acl.other) { + text += ' '; + var others = ''; + for (var r in objs.rows[i].value.common.acl.other) { + others += r + (objs.rows[i].value.common.acl.other[r] ? '+' : '-') + ' '; + } + if (others.length < 23) others += Array(23 - others.length).join(' '); + text += others + '|'; + } else { + text += Array(25).join(' ') + '|'; + } + + //if (name.length < 30) name += Array(30 - name.length).join(' '); + if ( objs.rows[i].value.common.members) { + for (var m = 0; m < objs.rows[i].value.common.members.length; m++) { + objs.rows[i].value.common.members[m] = objs.rows[i].value.common.members[m].substring(12); + } + text += ' ' + objs.rows[i].value.common.members.join(', '); + } + //text += '| (' + name + ')'; + console.log(text); + } + } + + console.log('--------------------+---------+---------+-----------+-------+------------------------+---------'); + console.log('Legend: (l)ist, (r)ead, (w)rite, (c)reate, (d)elete'); + setTimeout(function () { + processExit(); + }, 1000); + }); + break; + + case 'h': + case 'hosts': + objects.getObjectList({startkey: 'system.host.', endkey: 'system.host.\u9999'}, function (err, objs) { + states.getKeys('system.host.*', function (err, keys) { + states.getStates(keys, function (err, states) { + var reg = filter ? new RegExp(pattern2RegEx('system.host.' + filter)) : null; + + for (var i = 0; i < objs.rows.length; i++) { + if (objs.rows[i].value.type !== 'host') continue; + if (!reg || reg.test(objs.rows[i].value._id) || (objs.rows[i].value.common && reg.test(objs.rows[i].value.common.name))) { + var id = objs.rows[i].value._id.substring(12); + var name = objs.rows[i].value.common.name; + if (id.length < 20) id += Array(20 - id.length).join(' '); + var hostname = objs.rows[i].value.common.hostname; + if (hostname.length < 15) hostname += Array(15 - hostname.length).join(' '); + var version = objs.rows[i].value.common.installedVersion; + var alive = ''; + var uptime = ''; + for (var k = 0; k < keys.length; k++) { + if (keys[k] === objs.rows[i].value._id + '.alive') { + alive = states[k].val; + } + if (keys[k] === objs.rows[i].value._id + '.uptime') { + uptime = states[k].val; + } + } + alive = alive ? 'alive' : ' dead'; + //if (uptime.toString().length < 10) uptime = Array(10 - uptime.toString().length).join(' ') + uptime.toString(); + if (!uptime) uptime = '-'; + var text = id + ' ' + name + ' (version: ' + version + ', hostname: ' + hostname + ', ' + alive + ', uptime: ' + uptime + ')'; + // todo + console.log(text); + } + } + + setTimeout(function () { + processExit(); + }, 1000); + }); + }); + }); + break; + + case 'enums': + case 'e': + objects.getObjectList({startkey: 'enum.', endkey: 'enum.\u9999'}, function (err, objs) { + var reg = filter ? new RegExp(pattern2RegEx('enum.' + filter)) : null; + for (var i = 0; i < objs.rows.length; i++) { + if (objs.rows[i].value.type !== 'enum') continue; + if (!reg || reg.test(objs.rows[i].value._id) || (objs.rows[i].value.common && reg.test(objs.rows[i].value.common.name))) { + console.log('\n====================================================================================='); + var id = objs.rows[i].value._id.substring(5); + var name = objs.rows[i].value.common.name; + if (id.length < 20) id += Array(20 - id.length).join(' '); + console.log(id + '(' + name + ')'); + console.log('-------------------------------------------------------------------------------------'); + + if (objs.rows[i].value.common.members) { + console.log(objs.rows[i].value.common.members.join(', ')); + } + } + } + setTimeout(function () { + processExit(); + }, 1000); + }); + break; + + case 'files': + case 'f': + objects.getObjectList({startkey: 'system.adapter.', endkey: 'system.adapter.\u9999'}, function (err, objs) { + var adapter = filter || null; + var names = filter ? filter.split('/') : null; + if (names && !names[0]) names.splice(0, 1); + + var adapters = []; + for (var i = 0; i < objs.rows.length; i++) { + if (objs.rows[i].value.type === 'adapter') { + if (adapter && objs.rows[i].value.common.name !== names[0]) continue; + adapters.push(objs.rows[i].value.common.name); + } else if (objs.rows[i].value.type === 'instance') { + if (adapter && objs.rows[i].value._id.substring(15) !== names[0]) continue; + adapters.push(objs.rows[i].value._id.substring(15)); + } + } + if (names) names.shift(); + that.listAdaptersFiles(adapters, names ? names.join('/') : null, function () { + setTimeout(function () { + processExit(); + }, 1000); + }); + }); + break; + + default: + if (type) { + console.error('Unknown type: ' + type); + processExit(23); + } else { + console.log('Please specify type: objects, states, instances, adapters, users, groups, enums, files'); + processExit(); + } + break; + } + }; +} + +module.exports = List; diff --git a/lib/setup/setupMultihost.js b/lib/setup/setupMultihost.js new file mode 100644 index 0000000..04c4f3d --- /dev/null +++ b/lib/setup/setupMultihost.js @@ -0,0 +1,294 @@ +'use strict'; +function Multihost(options) { + var fs = require('fs'); + var tools = require(__dirname + '/../tools.js'); + var configName = tools.getConfigFileName(); + var that = this; + + options = options || {}; + + var params = options.params || {}; + var objects = options.objects; + + function getConfig() { + var config; + // read actual configuration + try { + if (fs.existsSync(configName)) { + config = JSON.parse(fs.readFileSync(configName, 'utf8')); + } else { + config = require(__dirname + '/../../conf/' + tools.appName + '-dist.json'); + } + } catch (e) { + config = require(__dirname + '/../../conf/' + tools.appName + '-dist.json'); + } + return config; + } + + function leftPad(text, len) { + text = text || ''; + if (text.length >= len) return len; + return new Array(len - text.length).join(' ') + text; + } + + this.showHosts = function (list) { + if (!list || !list.length) { + console.info('Nothing found'); + } else { + for (var i = 0; i < list.length; i++) { + console.log((i + 1) + ' | ' + leftPad(list[i].hostname, 20) + ' | ' + (list[i].slave ? 'slave' : ' host') + ' | ' + leftPad(list[i].ip, 20) + ' | ' + JSON.stringify(list[i].info)); + } + } + }; + + this.browse = function (callback) { + var MHClient = require(__dirname + '/../multihostClient'); + var mhClient = new MHClient(); + mhClient.browse(2000, params.debug, function (err, list) { + if (err) { + callback('Cannot browse: ' + err); + } else { + callback(null, list); + } + }); + }; + + function showMHState(config, changed, callback) { + if (config.multihostService.enabled) { + if (config.objects.type === 'file' && (config.objects.host === '127.0.0.1' || config.objects.host === 'localhost')) { + console.log('Server accept connections for objects on all IP addresses.'); + config.objects.host = '0.0.0.0'; + changed = true; + } else if (config.objects.type === 'redis') { + console.log('Please check the binding of redis service. By default it is only local: http://download.redis.io/redis-stable/redis.conf\nChange "bind 127.0.0.1" to "bind 0.0.0.0" or to others.') + } + if (config.states.type === 'file' && (config.states.host === '127.0.0.1' || config.states.host === 'localhost')) { + console.log('Server accept connections for states on all IP addresses.'); + config.states.host = '0.0.0.0'; + changed = true; + } else if (config.states.type === 'redis') { + console.log('Please check the binding of redis service. By default it is only local: http://download.redis.io/redis-stable/redis.conf\nChange "bind 127.0.0.1" to "bind 0.0.0.0" or to others.') + } + } + if (!changed) { + console.log('Nothing changed.'); + } else { + fs.writeFileSync(configName, JSON.stringify(config, null, 2)); + console.log('Please restart yunkong2: "yunkong2 restart"'); + } + console.log('\n'); + console.log('Miltihost: ' + (config.multihostService.enabled ? 'enabled' : 'disabled')); + console.log('Authentication: ' + (config.multihostService.secure ? 'enabled' : 'disabled')); + console.log('Objects: ' + config.objects.type + ' on ' + config.objects.host); + console.log('States: ' + config.states.type + ' on ' + config.states.host); + callback(); + } + + this.enable = function (isEnable, callback) { + var changed = false; + var config = getConfig(); + config.multihostService = config.multihostService || {enabled: false, secure: true}; + + if (isEnable && !config.multihostService.enabled) { + changed = true; + config.multihostService.enabled = true; + config.multihostService.password = ''; + console.log('Miltihost activated.') + } else if (!isEnable && config.multihostService.enabled) { + changed = true; + config.multihostService.enabled = false; + config.multihostService.password = ''; + console.log('Miltihost deactivated.') + } + if (params.secure === undefined) { + params.secure = true; + } + + if (isEnable && (config.multihostService.secure !== params.secure || (config.multihostService.secure && !config.multihostService.password))) { + changed = true; + config.multihostService.secure = params.secure; + console.log('Authentication ' + (params.secure ? 'activated' : 'deactivated') + '.'); + if (config.multihostService.secure) { + var prompt = require('prompt'); + prompt.message = ''; + prompt.delimiter = ''; + var schema = { + properties: { + password: { + description: 'Enter secret phrase for connection:', + pattern: /^[^'"]+$/, + message: 'No " are allowed', + hidden: true + }, + passwordRepeat: { + description: 'Repeat secret phrase for connection:', + pattern: /^[^'"]+$/, + message: 'No " are allowed', + hidden: true + } + } + }; + prompt.start(); + + prompt.get(schema, function (err, password) { + if (password && password.password) { + if (password.password !== password.passwordRepeat) { + callback('Secret phrases are not equal!'); + } else { + objects.getObject('system.config', function (err, obj) { + tools.encryptPhrase(obj.native.secret, password.password, function (encoded) { + config.multihostService.password = encoded; + showMHState(config, changed, callback); + }) + }); + } + } else { + callback('No secret phrase entered!'); + } + }); + } else { + showMHState(config, changed, callback); + } + } else { + showMHState(config, changed, callback); + } + }; + + this.status = function (callback) { + var config = getConfig(); + config.multihostService = config.multihostService || {enabled: false, secure: true}; + showMHState(config, false, callback); + }; + + function readPassword(callback) { + var readline = require('readline'); + + var rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + function hidden(query, callback) { + var stdin = process.openStdin(); + process.stdin.on('data', function (char) { + char = char + ''; + switch (char) { + case '\n': + case '\r': + case '\u0004': + stdin.pause(); + break; + + default: + process.stdout.write('\x1B[2K\x1B[200D' + query + new Array(rl.line.length + 1).join('*')); + break; + } + }); + + rl.question(query, function (value) { + rl.history = rl.history.slice(1); + callback(value); + }); + } + + hidden('Enter secret phrase for connection: ', function(password) { + callback(password); + }); + } + + function connect(mhClient, ip, pass, callback) { + mhClient.connect(ip, pass, function (err, oObjects, oStates, ipHost) { + if (err) { + callback('Cannot connect to "' + ip + '": ' + err); + } else if (oObjects && oStates) { + var config = getConfig(); + config.objects = oObjects; + config.states = oStates; + if (config.objects.host === '127.0.0.1' || config.objects.host === 'localhost' || config.states.host === '127.0.0.1' || config.states.host === 'localhost') { + callback('IP Address of the host is 127.0.0.1. It accepts no connections. Please change.'); + } else { + if (config.states.host === '0.0.0.0') { + config.states.host = ipHost; + } + if (config.objects.host === '0.0.0.0') { + config.objects.host = ipHost; + } + + fs.writeFileSync(configName, JSON.stringify(config, null, 2)); + console.log('Config ok. Please restart yunkong2: "yunkong2 restart"'); + callback(); + } + } else { + callback('No configuration received!'); + } + }); + } + + this.connect = function (number, pass, callback) { + if (typeof pass === 'function') { + callback = pass; + pass = null; + } + if (typeof number === 'function') { + callback = number; + number = null; + } + var MHClient = require(__dirname + '/../multihostClient'); + var mhClient = new MHClient(); + + mhClient.browse(2000, params.debug, function (err, list) { + if (err) { + callback('Cannot browse: ' + err); + } else { + that.showHosts(list); + + if (number !== null && number !== undefined && parseInt(number, 10) > 0) { + number = parseInt(number, 10); + if (list && number < list.length + 1) { + if (!pass) { + callback('No password defined: please use "multihost connect "'); + } else { + connect(mhClient, list[number - 1].ip, pass, callback); + } + } else { + callback('Invalid index: ' + number); + } + } else + if (list && list.length) { + var readline = require('readline'); + + var rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + rl.question('Please select host [1]: ', function (answer) { + if (answer === '' || answer === null || answer === undefined) { + answer = 1; + } + answer = parseInt(answer, 10) - 1; + if (!list[answer]) { + rl.close(); + callback('Invalid index: ' + answer); + } else { + if (list[answer].auth) { + readPassword(function (password) { + if (password) { + connect(mhClient, list[answer].ip, password, callback); + } else { + callback('No password entered!'); + } + }); + } else { + connect(mhClient, list[answer].ip, null, callback); + } + } + }); + } else { + callback(null, list); + } + } + }); + }; +} + +module.exports = Multihost; diff --git a/lib/setup/setupRepo.js b/lib/setup/setupRepo.js new file mode 100644 index 0000000..a94dbb5 --- /dev/null +++ b/lib/setup/setupRepo.js @@ -0,0 +1,279 @@ +'use strict'; +function Repo(options) { + const tools = require(__dirname + '/../tools.js'); + const extend = require('node.extend'); + let defaultSystemRepo = { + "common": { + "name": "System repositories", + "dontDelete": true + }, + "native": { + "repositories": { + "default": { + "link": "http://download.yunkong2.net/sources-dist.json", + "json": null + }, + "latest": { + "link": "http://download.yunkong2.net/sources-dist-latest.json", + "json": null + } + } + }, + "_id": "system.repositories", + "type": "config" + }; + + options = options || {}; + + if (!options.objects) throw 'Invalid arguments: objects is missing'; + + var objects = options.objects; + + function updateRepo(repoUrl, callback) { + var result = {}; + + if (!repoUrl || typeof repoUrl !== 'object') { + tools.getRepositoryFile(repoUrl, function (err, sources) { + updateRepo(sources, callback); + }); + return; + } + var sources = repoUrl; + var downloads = []; + + function download() { + if (downloads.length < 1) { + console.log('update done'); + if (callback) callback(result); + } else { + var name = downloads.pop(); + + if (sources[name].version) { + result[name] = sources[name]; + } else if (sources[name].meta) { + tools.getJson(sources[name].meta, function (ioPack) { + if (ioPack && ioPack.common) { + result[name] = extend(true, sources[name], ioPack.common); + } + setImmediate(download); + }); + return; + } else if (sources[name].url) { + console.log('Cannot get version of "' + name + '".'); + result[name] = sources[name]; + } else { + console.log('Cannot get any information of "' + name + '". Ignored.'); + } + setImmediate(download); + } + } + + // Read repository file, local or by url + for (var name in sources) { + if (!sources.hasOwnProperty(name)) continue; + downloads.push(name); + } + + download(/*sources*/); + } + + this.showRepo = function (repoUrl, flags, callback) { + if (typeof flags === 'function') { + callback = flags; + flags = {}; + } + + function showRepoResult(_name, sources) { + var installed = tools.getInstalledInfo(); + var updatable; + var keys = []; + for (var key in sources) { + if (!sources.hasOwnProperty(key)) continue; + keys.push(key); + } + keys.sort(); + + for (var i = 0; i < keys.length; i++) { + var name = keys[i]; + if (!sources.hasOwnProperty(name)) continue; + updatable = false; + var text = (sources[name].controller ? 'Controller ' : 'Adapter '); + text += '"' + name + '"' + ((name.length < 15) ? new Array(15 - name.length).join(' ') : ''); + + var tLen = 10; + if (name.length >= 15) tLen -= (name.length > tLen ? 1 : 0); + if (tLen < 0) tLen = 0; + + if (sources[name].version) { + text += ': ' + sources[name].version + ((sources[name].version.length < tLen) ? new Array(tLen - sources[name].version.length).join(' ') : ''); + } else { + text += new Array(tLen).join(' ') + } + if ((flags.installed || flags.i) && !installed[name]) { + continue; + } + + if (installed[name] && installed[name].version) { + text += ', installed ' + installed[name].version; + if (sources[name].version !== installed[name].version && + sources[name].version && + !tools.upToDate(sources[name].version, installed[name].version)) { + updatable = true; + text += ' [Updateable]'; + } + } + if ((flags.updatable || flags.u) && !updatable) { + continue; + } + console.log(text); + } + } + + // Get the repositories + objects.getObject('system.config', function (err, sysConfig) { + objects.getObject('system.repositories', function (err, obj) { + if (err || !obj) { + console.log('Error: Object "system.config" not found'); + } else { + if (!obj.native || !obj.native.repositories) { + console.log('Error: no repositories found in the "system.config'); + } else { + repoUrl = repoUrl || sysConfig.common.activeRepo; + + console.log('Used repository: ' + repoUrl); + + // If known repository + if (obj.native.repositories[repoUrl]) { + if (typeof obj.native.repositories[repoUrl] === 'string') { + obj.native.repositories[repoUrl] = { + link: obj.native.repositories[repoUrl], + json: null + }; + } + + updateRepo(obj.native.repositories[repoUrl].link, function (sources) { + obj.native.repositories[repoUrl].json = sources; + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = new Date().getTime(); + objects.setObject(obj._id, obj, function () { + showRepoResult(repoUrl, sources); + if (callback) callback(); + }); + }); + } else { + updateRepo(repoUrl, function (sources) { + showRepoResult(null, sources); + if (callback) callback(); + }); + } + } + } + }); + }); + }; + + this.showRepoStatus = function (callback) { + objects.getObject('system.repositories', function (err, obj) { + if (err || !obj) { + console.error('Cannot get list: ' + err); + callback(102); + } else { + if (obj.native.repositories) { + for (var r in obj.native.repositories) { + if (obj.native.repositories.hasOwnProperty(r)){ + console.log(r + (r.length < 12 ? new Array(12 - r.length).join(' ') : '') + ': ' + obj.native.repositories[r].link); + } + } + objects.getObject('system.config', function (err, obj) { + if (obj) { + console.log('\nActive repo: ' + obj.common.activeRepo); + } + callback(); + }); + } else { + console.error('Cannot get list: ' + err); + callback(102); + } + } + }); + }; + + this.add = function (repoName, repoUrl, callback) { + objects.getObject('system.repositories', function (err, obj) { + if (err) { + callback && callback(err); + } else + if (!obj) { + obj = defaultSystemRepo; + } + + if (obj.native.repositories[repoName]) { + callback && callback('Repository "' + repoName + '" yet exists: ' + obj.native.repositories[repoName].link); + } else { + obj.native.repositories[repoName] = { + link: repoUrl, + json: null + }; + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = new Date().getTime(); + objects.setObject('system.repositories', obj, callback); + } + }); + }; + + this.del = function (repoName, callback) { + objects.getObject('system.config', function (err, obj) { + if (err) { + callback && callback(err); + } else { + if (obj.common.activeRepo === repoName) { + callback && callback('Cannot delete active repository: ' + repoName); + } else { + objects.getObject('system.repositories', function (err, obj) { + if (err) { + callback && callback(err); + } else if (!obj) { + callback && callback(); + } else { + if (!obj.native.repositories[repoName]) { + callback && callback('Repository "' + repoName + '" not found.'); + } else { + delete obj.native.repositories[repoName]; + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = new Date().getTime(); + objects.setObject('system.repositories', obj, callback); + } + } + }); + } + } + }); + }; + + this.setActive = function (repoName, callback) { + objects.getObject('system.repositories', function (err, obj) { + if (err) { + callback && callback(err); + } else + if (!obj) { + obj = defaultSystemRepo; + } + if (!obj.native.repositories[repoName]) { + callback && callback('Repository "' + repoName + '" not found.'); + } else { + objects.getObject('system.config', function (err, obj) { + if (err) { + callback && callback(err); + } else { + obj.common.activeRepo = repoName; + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = new Date().getTime(); + objects.setObject('system.config', obj, callback); + } + }); + } + }) + }; +} + +module.exports = Repo; diff --git a/lib/setup/setupSetup.js b/lib/setup/setupSetup.js new file mode 100644 index 0000000..b3110b5 --- /dev/null +++ b/lib/setup/setupSetup.js @@ -0,0 +1,199 @@ +'use strict'; + +function Setup(options) { + var fs = require('fs'); + var tools = require(__dirname + '/../tools.js'); + + options = options || {}; + + var processExit = options.processExit; + var dbConnect = options.dbConnect; + var params = options.params; + var password; + var objects; + + function mkpathSync(rootpath, dirpath) { + // Remove filename + dirpath = dirpath.split('/'); + dirpath.pop(); + if (!dirpath.length) return; + + for (var i = 0; i < dirpath.length; i++) { + rootpath += dirpath[i] + '/'; + if (!fs.existsSync(rootpath)) { + if (dirpath[i] !== '..') { + fs.mkdirSync(rootpath); + } else { + throw 'Cannot create ' + rootpath + dirpath.join('/'); + } + } + } + } + + function setupReady(callback) { + if (!callback) { + console.log('database setup done. you can add adapters and start ' + tools.appName + ' now'); + processExit(0); + } else { + callback(); + } + } + + function dbSetup(iopkg, callback) { + if (iopkg.objects && iopkg.objects.length > 0) { + var obj = iopkg.objects.pop(); + objects.getObject(obj._id, function (err, _obj) { + if (err || !_obj) { + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = new Date().getTime(); + objects.setObject(obj._id, obj, function () { + console.log('object ' + obj._id + ' created'); + setTimeout(dbSetup, 25, iopkg, callback); + }); + } else { + console.log('object ' + obj._id + ' yet exists'); + setTimeout(dbSetup, 25, iopkg, callback); + } + }); + } else { + tools.createUuid(objects, function () { + // check if encrypt secret exists + objects.getObject('system.config', function (err, obj) { + if (obj && (!obj.native || !obj.native.secret)) { + require('crypto').randomBytes(24, function (ex, buf) { + obj.native = obj.native || {}; + obj.native.secret = buf.toString('hex'); + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = new Date().getTime(); + objects.setObject('system.config', obj, function () { + setupReady(callback); + }); + }); + } else { + setupReady(callback); + } + }); + }); + } + } + + function setupObjects(callback) { + dbConnect(params, function (_objects, _states) { + objects = _objects; + var iopkg = JSON.parse(fs.readFileSync(__dirname + '/../../io-package.json', 'utf8')); + dbSetup(iopkg, callback); + }); + } + + this.setup = function (callback, ignoreIfExist, useRedis) { + password = require(__dirname + '/../password'); + + var config; + var isCreated = false; + var platform = require('os').platform(); + var otherInstallDirs = []; + + // Delete files for other OS + if (platform.match(/^win/)) { + otherInstallDirs.push(__dirname + '/../../' + tools.appName); + otherInstallDirs.push(__dirname + '/../../killall.sh'); + otherInstallDirs.push(__dirname + '/../../reinstall.sh'); + } else { + otherInstallDirs.push(__dirname + '/../../_service_' + tools.appName + '.bat'); + otherInstallDirs.push(__dirname + '/../../' + tools.appName + '.bat'); + // copy scripts to root directory + if (fs.existsSync(__dirname + '/../../../../node_modules/')) { + try { + if (fs.existsSync(__dirname + '/../../killall.sh')) { + fs.writeFileSync(__dirname + '/../../../../killall.sh', fs.readFileSync(__dirname + '/../../killall.sh')); + } + if (fs.existsSync(__dirname + '/../../reinstall.sh')) { + fs.writeFileSync(__dirname + '/../../../../reinstall.sh', fs.readFileSync(__dirname + '/../../reinstall.sh')); + } + if (fs.existsSync(__dirname + '/../..' + tools.appName)) { + fs.writeFileSync(__dirname + '/../../../..' + tools.appName, fs.readFileSync(__dirname + '/../..' + tools.appName)); + } + } catch (e) { + console.warn('Cannot write file. Not critical: ' + e); + } + } + } + + for (var t = 0; t < otherInstallDirs.length; t++) { + if (fs.existsSync(otherInstallDirs[t])) { + var stat = fs.statSync(otherInstallDirs[t]); + if (stat.isDirectory()) { + var files = fs.readdirSync(otherInstallDirs[t]); + for (var f = 0; f < files.length; f++) { + fs.unlinkSync(otherInstallDirs[t] + '/' + files[f]); + } + fs.rmdirSync(otherInstallDirs[t]); + } else { + try { + fs.unlinkSync(otherInstallDirs[t]); + } + catch(e) { + console.warn('Cannot delete file. Not critical: ' + e); + } + } + } + } + + // Create log and tmp directory + if (!fs.existsSync(__dirname + '/../../tmp')) fs.mkdirSync(__dirname + '/../../tmp'); + + if (!fs.existsSync(tools.getConfigFileName())) { + isCreated = true; + if (fs.existsSync(__dirname + '/../../conf/' + tools.appName + '-dist.json')) { + config = require(__dirname + '/../../conf/' + tools.appName + '-dist.json'); + } else { + config = require(__dirname + '/../../conf/' + tools.appName.toLowerCase() + '-dist.json'); + } + console.log('creating conf/' + tools.appName + '.json'); + config.objects.host = params.objects || '127.0.0.1'; + config.states.host = params.states || '127.0.0.1'; + if (useRedis) { + config.states.type = 'redis'; + config.states.port = 6379; + } + + // this path is relative to yunkong2.js-controller + config.dataDir = tools.getDefaultDataDir(); + if (fs.existsSync(__dirname + '/../../../node_modules/' + tools.appName + '.js-controller')) { + mkpathSync(__dirname + '/../', config.dataDir); + } else { + mkpathSync(__dirname + '/../', '../' + config.dataDir); + } + + // Create default data dir + fs.writeFileSync(tools.getConfigFileName(), JSON.stringify(config, null, 2)); + + try { + // Create + if (__dirname.toLowerCase().replace(/\\/g, '/').indexOf('node_modules/' + tools.appName + '.js-controller') !== -1) { + var parts = config.dataDir.split('/'); + // Remove appName-data/ + parts.pop(); + parts.pop(); + var path = parts.join('/'); + + if (!fs.existsSync(__dirname + '/../../' + path + '/log')) fs.mkdirSync(__dirname + '/../../' + path + '/log'); + } else { + if (!fs.existsSync(__dirname + '/../../log')) fs.mkdirSync(__dirname + '/../../log'); + } + } catch (e) { + console.log('Non-critical error: ' + e.message); + } + } else if (ignoreIfExist) { + if (callback) callback(); + return; + } + setupObjects(function () { + if (callback) callback(isCreated); + }); + } + + +} + +module.exports = Setup; diff --git a/lib/setup/setupUpgrade.js b/lib/setup/setupUpgrade.js new file mode 100644 index 0000000..5fdddad --- /dev/null +++ b/lib/setup/setupUpgrade.js @@ -0,0 +1,360 @@ +'use strict'; + +function Upgrade(options) { + var fs = require('fs'); + var tools = require(__dirname + '/../tools.js'); + + var that = this; + + options = options || {}; + + if (!options.objects) throw 'Invalid arguments: objects is missing'; + if (!options.processExit) throw 'Invalid arguments: processExit is missing'; + if (!options.installNpm) throw 'Invalid arguments: installNpm is missing'; + if (!options.restartController) throw 'Invalid arguments: restartController is missing'; + if (!options.getRepository) throw 'Invalid arguments: getRepository is missing'; + + var objects = options.objects; + var processExit = options.processExit; + var installNpm = options.installNpm; + var restartController = options.restartController; + var getRepository = options.getRepository; + var params = options.params; + var semver; + + var Upload = require(__dirname + '/setupUpload.js'); + var upload = new Upload(options); + + var Install = require(__dirname + '/setupInstall.js'); + var install = new Install(options); + + this.upgradeAdapterHelper = function (repoUrl, list, i, forceDowngrade, callback) { + that.upgradeAdapter(repoUrl, list[i], forceDowngrade, function () { + i++; + while (repoUrl[list[i]] && repoUrl[list[i]].controller) { + i++; + } + + if (list[i]) { + setImmediate(function () { + that.upgradeAdapterHelper(repoUrl, list, i, forceDowngrade, callback); + }); + } else if (callback) { + callback(); + } + }); + }; + + function checkDependencies(dependencies) { + if (!dependencies) return ''; + // like [{"js-controller": ">=0.10.1"}] + var adapters; + if (dependencies instanceof Array) { + adapters = {}; + for (var a = 0; a < dependencies.length; a++) { + if (typeof dependencies[a] === 'string') continue; + for (var b in dependencies[a]) adapters[b] = dependencies[a][b]; + } + } else { + adapters = dependencies; + } + + for (var adapter in adapters) { + var adapterDir = tools.getAdapterDir(adapter); + var iopack; + if (!semver) semver = require('semver'); + if (adapter === 'js-controller') { + try { + iopack = JSON.parse(fs.readFileSync(__dirname + '/../../io-package.json', 'utf8')); + } catch (e) { + return 'Cannot find io-package.json in "' + __dirname + '/../../": ' + e; + } + if (!iopack || !iopack.common || !iopack.common.version) return 'No version of "js-controller"'; + if (!semver.satisfies(iopack.common.version, adapters[adapter])) return 'Invalid version of js-controler. Required ' + adapters[adapter]; + } else { + try { + iopack = JSON.parse(fs.readFileSync(adapterDir + '/io-package.json', 'utf8')); + } catch (e) { + return 'Cannot find io-package.json in "' + adapterDir + '": ' + e; + } + + if (!iopack || !iopack.common || !iopack.common.version) return 'No version of "' + adapter + '"'; + if (!semver.satisfies(iopack.common.version, adapters[adapter])) return 'Invalid version of "' + adapter + '"'; + } + } + return ''; + } + + this.upgradeAdapter = function (repoUrl, adapter, forceDowngrade, callback) { + if (!repoUrl || typeof repoUrl !== 'object') { + getRepository(repoUrl, params, function (err, sources) { + if (err) { + processExit(err); + } else { + that.upgradeAdapter(sources, adapter, forceDowngrade, callback); + } + }); + return; + } + + function finishUpgrade(name, iopack, callback) { + if (!iopack) { + var adapterDir = tools.getAdapterDir(name); + try { + iopack = JSON.parse(fs.readFileSync(adapterDir + '/io-package.json', 'utf8')); + } catch (e) { + console.error('Cannot find io-package.json in ' + adapterDir); + processExit(10); + } + } + + var count = 0; + installNpm(name, function (err, _name) { + if (err) { + processExit(err); + } else { + // Upload www and admin files of adapter into CouchDB + count++; + upload.uploadAdapter(name, false, true, function () { + // extend all adapter instance default configs with current config + // (introduce potentially new attributes while keeping current settings) + upload.upgradeAdapterObjects(name, iopack, function () { + count--; + if (!count) { + console.log('Adapter "' + name + '" updated'); + if (callback) callback(name); + } + }); + }); + count++; + upload.uploadAdapter(name, true, true, function () { + count--; + if (!count) { + console.log('Adapter "' + name + '" updated'); + if (callback) callback(name); + } + }); + } + }); + } + + var sources = repoUrl; + var version; + if (adapter.indexOf('@') !== -1) { + var parts = adapter.split('@'); + adapter = parts[0]; + version = parts[1]; + } else { + version = ''; + } + if (version) forceDowngrade = true; + + var adapterDir = tools.getAdapterDir(adapter); + + // Read actual description of installed adapter with version + if (!version && !fs.existsSync(adapterDir + '/io-package.json')) { + console.log('Adapter "' + adapter + '"' + ((adapter.length < 15) ? new Array(15 - adapter.length).join(' '): '') + ' is not installed.'); + if (callback) callback(); + return; + } + // Get the url of io-package.json or direct the version + if (!repoUrl[adapter]) { + console.log('Adapter "' + adapter + '" is not in the repository and cannot be updated.'); + if (callback) callback(); + return; + } + + var ioInstalled; + if (fs.existsSync(adapterDir + '/io-package.json')) { + ioInstalled = require(adapterDir + '/io-package.json'); + } + if (!ioInstalled) { + ioInstalled = {common: {version: '0.0.0'}}; + } + + // If version is included in repository + if (repoUrl[adapter].version) { + if (!forceDowngrade) { + var error = checkDependencies(repoUrl[adapter].dependencies); + if (error) { + console.error(error); + if (callback) callback(); + return; + } + } + + if (!forceDowngrade && (repoUrl[adapter].version === ioInstalled.common.version || + tools.upToDate(repoUrl[adapter].version, ioInstalled.common.version))) { + console.log('Adapter "' + adapter + '"' + ((adapter.length < 15) ? new Array(15 - adapter.length).join(' '): '') + ' is up to date.'); + if (callback) callback(); + } else { + console.log('Update ' + adapter + ' from @' + ioInstalled.common.version + ' to @' + (version || repoUrl[adapter].version)); + // Get the adapter from web site + install.downloadPacket(sources, adapter + '@' + (version || repoUrl[adapter].version), null, function (name, ioPack) { + finishUpgrade(name, ioPack, callback); + }); + } + } else if (repoUrl[adapter].meta) { + // Read repository from url or file + tools.getJson(repoUrl[adapter].meta, function (iopack) { + if (!iopack) { + console.error('Cannot parse file' + repoUrl[adapter].meta); + if (callback) callback(); + return; + } + + if (!forceDowngrade) { + var error = checkDependencies(iopack.common ? iopack.common.dependencies : null); + if (error) { + console.error(error); + if (callback) callback(); + return; + } + } + + if (!version && (iopack.common.version === ioInstalled.common.version || + (!forceDowngrade && tools.upToDate(iopack.common.version, ioInstalled.common.version)))) { + console.log('Adapter "' + adapter + '"' + ((adapter.length < 15) ? new Array(15 - adapter.length).join(' '): '') + ' is up to date.'); + if (callback) callback(); + } else { + // Get the adapter from web site + console.log('Update ' + adapter + ' from @' + ioInstalled.common.version + ' to @' + (version || iopack.common.version)); + install.downloadPacket(sources, adapter + '@' + (version || iopack.common.version), null, function (name, ioPack) { + finishUpgrade(name, ioPack, callback); + }); + } + }); + } else { + if (forceDowngrade) { + console.warn('Unable to get version for "' + adapter + '". Update anyway.'); + console.log('Update ' + adapter + ' from @' + ioInstalled.common.version + ' to @' + version); + // Get the adapter from web site + install.downloadPacket(sources, adapter + '@' + version, null, function (name, ioPack) { + finishUpgrade(name, ioPack, callback); + }); + } else { + console.error('Unable to get version for "' + adapter + '".'); + if (callback) callback(); + } + } + }; + + this.upgradeController = function (repoUrl, forceDowngrade, callback) { + if (!repoUrl || typeof repoUrl !== 'object') { + getRepository(repoUrl, params, function (err, sources) { + if (!sources) { + console.warn('Cannot get repository under "' + repoUrl + '"'); + if (callback) callback(err); + } else { + that.upgradeController(sources, forceDowngrade, callback); + } + }); + return; + } + + var hostname = tools.getHostName(); + var installed = JSON.parse(fs.readFileSync(__dirname + '/../../io-package.json', 'utf8')); + if (!installed || !installed.common || !installed.common.version) { + console.error('Host "' + hostname + '"' + ((hostname.length < 15) ? new Array(15 - hostname.length).join(' '): '') + ' is not installed.'); + if (callback) callback(); + return; + } + if (!repoUrl[installed.common.name]) { + // no info for controller + console.error('Cannot find this controller "' + installed.common.name + '" in repository.'); + + if (callback) callback(); + return; + } + + if (repoUrl[installed.common.name].version) { + if (!forceDowngrade && (repoUrl[installed.common.name].version === installed.common.version || + tools.upToDate(repoUrl[installed.common.name].version, installed.common.version))) { + console.log('Host "' + hostname + '"' + ((hostname.length < 15) ? new Array(15 - hostname.length).join(' '): '') + ' is up to date.'); + if (callback) { + callback(); + } + } else { + console.log('Update ' + installed.common.name + ' from @' + installed.common.version + ' to @' + repoUrl[installed.common.name].version); + // Get the controller from web site + install.downloadPacket(repoUrl, installed.common.name + '@' + repoUrl[installed.common.name].version, null, function (name) { + installNpm(function (err, _name) { + if (err) { + processExit(err); + } else { + setChmod(function () { + restartController(callback); + }); + } + }); + }); + } + } else { + tools.getJson(repoUrl[installed.common.name].meta, function (ioPack) { + if ((!ioPack || !ioPack.common) && !forceDowngrade) { + console.warn('Cannot read version. Write "' + tools.appName + ' upgrade self --force" to upgrade controller anyway.'); + if (callback) { + callback(); + } + return; + } + var version = ioPack && ioPack.common ? ioPack && ioPack.common.version : ''; + if (version) { + version = '@' + version; + } + + if ((ioPack && ioPack.common && ioPack.common.version === installed.common.version) || + (!forceDowngrade && ioPack && ioPack.common && tools.upToDate(ioPack.common.version, installed.common.version))) { + console.log('Host "' + hostname + '"' + ((hostname.length < 15) ? new Array(15 - hostname.length).join(' '): '') + ' is up to date.'); + if (callback) callback(); + } else { + var name = (ioPack && ioPack.common && ioPack.common.name) ? ioPack.common.name : installed.common.name; + console.log('Update ' + name + ' from @' + installed.common.version + ' to ' + version); + // Get the controller from web site + install.downloadPacket(repoUrl, name + version, null, function (name) { + installNpm(function (err, _name) { + if (err) { + processExit(err); + } else { + setChmod(function () { + restartController(callback); + }); + } + }); + }); + } + + }); + } + }; + + function setChmod(callback) { + var platform = require('os').platform(); + console.log('Host "' + tools.getHostName() + '" (' + platform + ') updated'); + // Call command chmod +x __dirname if under linux or darwin + if (platform === 'linux' || platform === 'darwin') { + var exec = require('child_process').exec; + var dir; + if (__dirname.toLowerCase().replace(/\\/g, '/').indexOf('node_modules/' + tools.appName + '.js-controller') !== -1) { + dir = require('path').normalize(__dirname + '/../../../..').replace(/\\/g, '/'); + } else { + dir = require('path').normalize(__dirname + '/../..').replace(/\\/g, '/'); + } + + var cmd = 'chmod -R 777 ' + dir; + console.log('Execute: ' + cmd); + var child = exec(cmd); + child.stderr.pipe(process.stdout); + child.on('exit', function () { + console.log('Chmod finished. Restart controller'); + if (callback) callback(); + }); + } else { + if (callback) callback(); + } + } + + +} + +module.exports = Upgrade; diff --git a/lib/setup/setupUpload.js b/lib/setup/setupUpload.js new file mode 100644 index 0000000..88cdb65 --- /dev/null +++ b/lib/setup/setupUpload.js @@ -0,0 +1,543 @@ +'use strict'; + +function Upload(options) { + const fs = require('fs'); + const tools = require(__dirname + '/../tools.js'); + + options = options || {}; + + if (!options.states) throw 'Invalid arguments: states is missing'; + if (!options.objects) throw 'Invalid arguments: objects is missing'; + + let states = options.states; + let objects = options.objects; + let mime; + + // get all instances of one adapter + function getInstances(adapter, callback) { + objects.getObjectList({startkey: 'system.adapter.' + adapter + '.', endkey: 'system.adapter.' + adapter + '.\u9999'}, (err, arr) => { + let instances = []; + if (!err && arr && arr.rows) { + for (let i = 0; i < arr.rows.length; i++) { + if (arr.rows[i].value.type !== 'instance') continue; + instances.push(arr.rows[i].value._id); + } + } + callback(instances); + }); + } + + // get all instances of all adapters in the list + function getAllInstances(adapters, callback) { + let instances = []; + let count = 0; + for (let k = 0; k < adapters.length; k++) { + if (!adapters[k]) continue; + if (adapters[k].indexOf('.') === -1) count++; + } + for (let i = 0; i < adapters.length; i++) { + if (!adapters[i]) continue; + if (adapters[i].indexOf('.') === -1) { + getInstances(adapters[i], inst => { + for (let j = 0; j < inst.length; j++) { + if (instances.indexOf(inst[j]) === -1) { + instances.push(inst[j]); + } + } + if (!--count && callback) { + callback(instances); + callback = null; + } + }); + } else { + if (instances.indexOf(adapters[i]) === -1) { + instances.push(adapters[i]); + } + } + } + if (!count && callback) { + callback(instances); + callback = null; + } + } + + // Check if some adapters must be restarted and restart them + function checkRestartOther(adapter, callback) { + let adapterDir = tools.getAdapterDir(adapter); + try { + let adapterConf = JSON.parse(fs.readFileSync(adapterDir + '/io-package.json').toString()); + if (adapterConf.common.restartAdapters) { + if (typeof adapterConf.common.restartAdapters !== 'object') adapterConf.common.restartAdapters = [adapterConf.common.restartAdapters]; + + if (adapterConf.common.restartAdapters.length && adapterConf.common.restartAdapters[0]) { + getAllInstances(adapterConf.common.restartAdapters, instances => { + if (!instances || !instances.length) { + if (callback) { + callback(); + callback = null; + } + } else { + let instancesCount = instances.length; + for (let r = 0; r < instances.length; r++) { + objects.getObject(instances[r], (err, obj) => { + // if instance is enabled + if (!err && obj && obj.common.enabled) { + + obj.common.enabled = false; // disable instance + + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = Date.now(); + + objects.setObject(obj._id, obj, err => { + if (!err) { + + obj.common.enabled = true; // enable instance + + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = Date.now(); + + objects.setObject(obj._id, obj, err => { + console.log('Adapter "' + obj._id + '" restarted.'); + if (!--instancesCount && callback) { + callback(); + callback = null; + } + }); + } else { + if (err) { + console.error('Cannot restart adapter "' + obj._id + '": ' + err); + } else { + console.warn('Adapter "' + obj._id + '" is disabled and cannot be restarted.'); + } + if (!--instancesCount && callback) { + callback(); + callback = null; + } + } + }); + } else if (!--instancesCount && callback) { + callback(); + callback = null; + } + }); + } + } + }); + } else if (callback) { + callback(); + callback = null; + } + } else if (callback) { + callback(); + callback = null; + } + } catch (e) { + console.error('Cannot parse ' + adapterDir + '/io-package.json:' + e); + if (callback) { + callback(); + callback = null; + } + } + } + + this.uploadAdapterFull = (adapters, callback) => { + if (!adapters || !adapters.length) { + if (callback) callback(); + return; + } + const adapter = adapters.pop(); + this.uploadAdapter(adapter, true, true, () => { + this.upgradeAdapterObjects(adapter, () => { + this.uploadAdapter(adapter, false, true, () => { + setImmediate(() => this.uploadAdapterFull(adapters, callback)); + }); + }); + }); + }; + + this.uploadFile = (source, target, callback) => { + const request = require('request'); + target = target.replace(/\\/g, '/'); + source = source.replace(/\\/g, '/'); + if (target[0] === '/') target = target.substring(1); + if (target[target.length - 1] === '/') { + let name = source.split('/').pop(); + name = name.split('?')[0]; + if (name.indexOf('.') === -1) name = 'index.html'; + target += name; + } + let parts = target.split('/'); + const adapter = parts[0]; + parts.splice(0, 1); + target = parts.join('/'); + + if (source.match(/^http:\/\/|^https:\/\//)) { + request(source, (error, response, body) => { + if (!error && response.statusCode === 200) { + objects.writeFile(adapter, target, body, err => { + if (err) console.error(err); + if (typeof callback === 'function') callback(err, adapter + '/' + target); + }); + } else { + console.error('Cannot get URL: ' + error || response.statusCode); + if (typeof callback === 'function') callback(error || response.statusCode, adapter + '/' + target); + } + }); + } else { + try { + objects.writeFile(adapter, target, fs.readFileSync(source), err => { + if (err) console.error(err); + if (typeof callback === 'function') callback(err, adapter + '/' + target); + }); + } catch (err) { + console.error('Cannot read file "' + source + '": ' + err); + if (typeof callback === 'function') callback(err, adapter + '/' + target); + } + } + }; + + // Upload www folder of adapter into ObjectsDB + this.uploadAdapter = (adapter, isAdmin, forceUpload, subTree, callback) => { + const id = adapter + (isAdmin ? '.admin' : ''); + const adapterDir = tools.getAdapterDir(adapter); + let dir = adapterDir + (isAdmin ? '/admin' : '/www'); + let files = []; + let rev; + + if (typeof subTree === 'function') { + callback = subTree; + subTree = null; + } + if (subTree) { + dir += '/' + subTree; + } + + if (!isAdmin) { + let cfg; + // check for common.wwwDontUpload (required for legacy adapter) + if (fs.existsSync(adapterDir + '/io-package.json')) { + cfg = require(adapterDir + '/io-package.json'); + } + if (cfg && cfg.common && cfg.common.wwwDontUpload) { + if (typeof callback === 'function') callback(adapter); + return; + } + } + // do not upload www dir of admin adapter + if (adapter === 'admin' && !isAdmin) { + // To DO remove after a while + console.log('This should never happens!'); + if (typeof callback === 'function') callback(adapter); + return; + } + + // Create "upload progress" object if not exists + if (!isAdmin) { + objects.getObject('system.adapter.' + adapter + '.upload', (err, obj) => { + if (err || !obj) { + objects.setObject('system.adapter.' + adapter + '.upload', + { + _id: 'system.adapter.' + adapter + '.upload', + type: 'state', + common: { + name: adapter + '.upload', + type: 'number', + role: 'indicator.state', + unit: '%', + def: 0, + desc: 'Upload process indicator' + }, + from: 'system.host.' + tools.getHostName() + '.cli', + ts: Date.now(), + native: {} + } + ); + } + }); + // Set indicator to 0 + states.setState('system.adapter.' + adapter + '.upload', 0, true); + } + + if (!mime) mime = require('mime'); + + function done(err, res) { + if (err) { + callback(); + } else { + console.log('got ' + dir); + files = res; + setTimeout(_adapter => { + maxFiles = files.length || 1; + upload(_adapter); + }, 25, adapter); + } + } + + let maxFiles = 0; + let lastProgressUpdate = (new Date()).getTime(); + + function upload(adapter) { + let file; + if (!files.length) { + if (!isAdmin) { + states.setState('system.adapter.' + adapter + '.upload', {val: 0, ack: true}, () => { + if (typeof callback === 'function') callback(adapter); + }); + } else { + if (typeof callback === 'function') callback(adapter); + } + } else { + file = files.pop(); + if (file === '.gitignore') { + upload(adapter); + return; + } + + const mimeType = mime.lookup(file); + let attName; + attName = file.split('/' + tools.appName + '.'); + if (attName.length === 1) { + // try to find anyway if adapter is not lower case + const pos = file.toLowerCase().indexOf(tools.appName.toLowerCase()); + if (pos !== -1) { + attName = ['', file.substring(tools.appName.length + 2)]; + } + } + + attName = attName.pop(); + attName = attName.split('/').slice(2).join('/'); + if (files.length > 100) { + if (!(files.length % 50)) { + console.log('upload [' + files.length + ']', id, file, attName, mimeType); + } + + } else if (files.length > 20) { + if (!(files.length % 10)) { + console.log('upload [' + files.length + ']', id, file, attName, mimeType); + } + } else { + console.log('upload [' + files.length + ']', id, file, attName, mimeType); + } + + // Update upload indicator + if (!isAdmin) { + const now = (new Date()).getTime(); + if (now - lastProgressUpdate > 1000) { + lastProgressUpdate = now; + states.setState('system.adapter.' + adapter + '.upload', {val: Math.round(1000 * (maxFiles - files.length) / maxFiles) / 10, ack: true}); + } + } + + fs.createReadStream(file).pipe( + objects.insert(id, attName, null, mimeType, { + rev: rev + }, (err, res) => { + if (err) { + console.log(err); + if (typeof callback === 'function') callback(adapter); + } + if (res) rev = res.rev; + setTimeout(upload, 50, adapter); + }) + ); + } + } + + function walk(dir, done) { + let results = []; + fs.readdir(dir, (err, list) => { + if (err) return done(err); + let i = 0; + (function next() { + let file = list[i++]; + if (!file) return done(null, results); + file = dir + '/' + file; + fs.stat(file, (err, stat) => { + if (stat && stat.isDirectory()) { + walk(file, (err, res) => { + results = results.concat(res); + next(); + }); + } else { + if (!file.match(/\.npmignore$/) && !file.match(/\.gitignore$/)) results.push(file); + next(); + } + }); + })(); + }); + } + + objects.getObject(id, (err, res) => { + if (err || !res) { + objects.setObject(id, { + type: 'meta', + common: { + name: id.split('.').pop(), + type: isAdmin ? 'admin' : 'www' + }, + from: 'system.host.' + tools.getHostName() + '.cli', + ts: Date.now(), + native: {} + }, (err, res) => { + if (res) rev = res.rev; + if (!isAdmin) { + checkRestartOther(adapter, () => walk(dir, done)); + } else { + walk(dir, done); + } + }); + } else { + if (!forceUpload) { + if (typeof callback === 'function') callback(adapter); + } else { + rev = res.rev; + if (!isAdmin) { + checkRestartOther(adapter, () => walk(dir, done)); + } else { + walk(dir, done); + } + } + } + }); + }; + + function extendNative(target, additional) { + for (let attr in additional) { + if (additional.hasOwnProperty(attr)) { + if (target[attr] === undefined) { + target[attr] = additional[attr]; + } else if (typeof additional[attr] === 'object' && !(additional[attr] instanceof Array)) { + try { + target[attr] = target[attr] || {}; + } catch (e) { + console.warn(`Cannot update attribute ${attr} of native`); + } + extendNative(target[attr], additional[attr]); + } + } + } + return target; + } + + function extendCommon(target, additional) { + for (let attr in additional) { + if (additional.hasOwnProperty(attr)) { + if (attr === 'title' || attr === 'schedule' || attr === 'mode' || attr === 'loglevel' || attr === 'enabled') { + if (target[attr] === undefined) { + target[attr] = additional[attr]; + } + } else if (typeof additional[attr] !== 'object' || (additional[attr] instanceof Array)) { + try { + target[attr] = additional[attr]; + } catch (e) { + console.warn(`Cannot update attribute ${attr} of common`); + } + } else { + target[attr] = target[attr] || {}; + if (typeof target[attr] !== 'object') { + target[attr] = {}; // here we clean the simple value with object + } + + extendCommon(target[attr], additional[attr]); + } + } + } + return target; + } + + this.upgradeAdapterObjects = (name, iopack, callback) => { + if (typeof iopack === 'function') { + callback = iopack; + iopack = null; + } + if (!iopack) { + const adapterDir = tools.getAdapterDir(name); + try { + iopack = JSON.parse(fs.readFileSync(adapterDir + '/io-package.json', 'utf8')); + } catch (e) { + console.error('Cannot find io-package.json in ' + adapterDir); + iopack = null; + } + } + + if (!iopack) { + callback(name); + } else { + objects.getObject('system.adapter.' + name, (err, obj) => { + if (err || !obj) { + console.error('system.adapter.' + name + ' does not exist'); + callback(name); + } else { + obj.common = iopack.common || {}; + obj.native = iopack.native || {}; + + obj.common.installedVersion = iopack.common.version; + + const hostname = tools.getHostName(); + + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = Date.now(); + + objects.setObject('system.adapter.' + name, obj, () => { + // Update all instances of this host + objects.getObjectView('system', 'instance', {startkey: 'system.adapter.' + name + '.', endkey: 'system.adapter.' + name + '.\u9999'}, null, function (err, res) { + let cntr = 0; + + if (res) { + for (let i = 0; i < res.rows.length; i++) { + if (res.rows[i].value.common.host === hostname) { + cntr++; + objects.getObject(res.rows[i].id, (err, _obj) => { + let newObject = JSON.parse(JSON.stringify(_obj)); + + // all common settings should be taken from new one + newObject.common = extendCommon(newObject.common, iopack.common); + newObject.native = extendNative(newObject.native, iopack.native); + + newObject.common.installedVersion = iopack.common.version; + newObject.common.version = iopack.common.version; + + // Compare objects to reduce restarts of instances + if (JSON.stringify(newObject) !== JSON.stringify(_obj)) { + console.log('Update "' + newObject._id + '"'); + + newObject.from = 'system.host.' + tools.getHostName() + '.cli'; + newObject.ts = Date.now(); + + objects.setObject(newObject._id, newObject, () => { + if (!--cntr && callback) callback(name); + }); + } else { + if (!--cntr && callback) callback(name); + } + }); + } + } + } + + // updates "_design/system" and co + if (iopack.objects && typeof iopack.objects === 'object') { + for (let _id in iopack.objects) { + if (!iopack.objects.hasOwnProperty(_id)) continue; + cntr++; + + iopack.objects[_id].from = 'system.host.' + tools.getHostName() + '.cli'; + iopack.objects[_id].ts = Date.now(); + + objects.setObject(iopack.objects[_id]._id, iopack.objects[_id], err => { + if (err) console.error('Cannot update object: ' + err); + if (!--cntr && callback) callback(name); + }); + } + } + + if (!cntr && callback) callback(name); + }); + }); + } + }); + } + }; +} + +module.exports = Upload; diff --git a/lib/setup/setupUsers.js b/lib/setup/setupUsers.js new file mode 100644 index 0000000..693dbfe --- /dev/null +++ b/lib/setup/setupUsers.js @@ -0,0 +1,522 @@ +'use strict'; + +function Users(options) { + var fs = require('fs'); + var tools = require(__dirname + '/../tools.js'); + + var that = this; + + options = options || {}; + + if (!options.objects) throw 'Invalid arguments: objects is missing'; + if (!options.processExit) throw 'Invalid arguments: processExit is missing'; + + var objects = options.objects; + var processExit = options.processExit; + + this.addUser = function (user, pw, callback) { + var _user = user.replace(/\s/g, '_'); + objects.getObject('system.user.' + _user, function (err, obj) { + if (obj) { + if (callback) callback('User yet exists'); + } else { + objects.setObject('system.user.' + _user, { + type: 'user', + common: { + name: user, + enabled: true + }, + from: 'system.host.' + tools.getHostName() + '.cli', + ts: new Date().getTime(), + native: {} + }, function (err) { + if (!err) { + that.setPassword(user, pw, callback); + } else { + if (typeof callback === 'function') callback(err); + } + }); + } + }); + }; + + this.isUser = function (user, callback) { + var _user = user.replace(/\s/g, '_'); + objects.getObject('system.user.' + _user, function (err, obj) { + if (callback) callback(null, !!obj); + }); + }; + + this.setPassword = function (user, pw, callback) { + var _user = user.replace(/\s/g, '_'); + + objects.getObject('system.user.' + _user, function (err, obj) { + if (err || !obj) { + if (typeof callback === 'function') callback('User does not exist'); + return; + } + var password = require(__dirname + '/../password'); + + password(pw).hash(null, null, function (err, res) { + if (err) { + if (typeof callback === 'function') callback(err); + return; + } + obj.common.password = res; + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = new Date().getTime(); + objects.setObject('system.user.' + _user, obj, function (err) { + if (typeof callback === 'function') callback(err); + }); + }); + }); + }; + + this.checkPassword = function (user, pw, callback) { + objects.getObject('system.user.' + user, function (err, obj) { + if (err || !obj) { + if (typeof callback === 'function') callback('User does not exist'); + return; + } + var password = require(__dirname + '/../password'); + + password(pw).check(obj.common.password, function (err, res) { + if (typeof callback === 'function') callback(err, res); + }); + }); + }; + + this.delUser = function (user, callback) { + if (!user) { + if (callback) callback('Please define user name, like: "userdel user"'); + return; + } + + var _user = user.replace(/\s/g, '_'); + + objects.getObject('system.user.' + _user, function (err, obj) { + if (err || !obj) { + if (callback) callback('User does not exist'); + } else { + if (obj.common.dontDelete) { + if (callback) callback('Cannot delete user, while is system user'); + } else { + objects.delObject('system.user.' + _user, function (err) { + // Remove this user from all groups + if (!err) { + objects.getObjectList({startkey: 'system.group.', endkey: 'system.group.\u9999'}, function (err, groups) { + var count = 0; + for (var i = 0; i < groups.rows.length; i++) { + if (groups.rows[i].value.type != 'group') continue; + // find all groups + if (groups.rows[i].value.common.members && groups.rows[i].value.common.members.indexOf('system.user.' + _user) != -1) { + var pos = groups.rows[i].value.common.members.indexOf('system.user.' + _user); + groups.rows[i].value.common.members.splice(pos, 1); + count++; + groups.rows[i].value.from = 'system.host.' + tools.getHostName() + '.cli'; + groups.rows[i].value.ts = new Date().getTime(); + objects.setObject(groups.rows[i].value._id, groups.rows[i].value, function (err) { + if (!(--count)) callback(err); + }); + } + } + if (!count) callback(); + }); + } else if (callback) { + callback(err); + } + }); + } + } + }); + }; + + this.addUserToGroup = function (user, group, callback) { + var _user = user.replace(/\s/g, '_'); + if (!group.match(/^system\.group\./)) group = 'system.group.' + group; + if (!_user.match(/^system\.user\./)) _user = 'system.user.' + _user; + + objects.getObject(_user, function (err, obj) { + if (err || !obj) { + if (typeof callback === 'function') callback('User does not exist'); + return; + } + objects.getObject(group, function (err, obj) { + if (err || !obj) { + if (typeof callback === 'function') callback('Group does not exist'); + return; + } + obj.common = obj.common || {}; + obj.common.members = obj.common.members || []; + + if (obj.common.members.indexOf(_user) == -1) { + obj.common.members.push(_user); + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = new Date().getTime(); + objects.setObject(group, obj, function (err) { + callback(err); + }); + } else { + callback(); + } + }); + }); + }; + + this.addUserPrompt = function (user, group, password, callback) { + if (!user) { + if (callback) callback('Please define user name, like: "adduser newUser"'); + return; + } + + // Check group + if (group.substring(0, 13) !== 'system.group.' ) group = 'system.group.' + group; + + objects.getObject(group, function (err, obj) { + if (!obj) { + if (callback) callback('Unknown group: ' + group); + return; + } + if (!password) { + var prompt = require('prompt'); + prompt.message = ''; + prompt.delimiter = ''; + var schema = { + properties: { + password: { + description: 'Enter your password:', + pattern: /^[^'"]+$/, + message: 'No " are allowed', + hidden: true + }, + repeatPassword: { + description: 'Repeat your password:', + pattern: /^[^'"]+$/, + message: 'No " are allowed', + hidden: true + } + } + }; + prompt.start(); + + prompt.get(schema, function (err, result) { + if (result) { + if (result.password !== result.repeatPassword) { + console.log('Passwords are not identical!'); + processExit(31); + } + //create user + that.addUser(user, result.password, function (err) { + if (err) { + if (callback) callback(err); + } else { + that.addUserToGroup(user, group, function (err) { + if (err) { + if (callback) callback(err); + } else { + if (callback) callback(); + } + }); + } + }); + } else { + if (callback) callback(err); + } + }); + } else { + that.addUser(user, password, function (err) { + if (err) { + if (callback) callback(err); + } else { + that.addUserToGroup(user, group, function (err) { + if (err) { + if (callback) callback(err); + } else { + if (callback) callback(); + } + }); + } + }); + } + }); + }; + + this.setUserPassword = function (user, password, callback) { + if (!user) { + if (callback) callback('Please define user name, like: "passwd username"'); + return; + } + + this.isUser(user, function (err, result) { + if (err) console.error('Cannot read user: ' + err); + if (!result) { + if (callback) callback('User "' + user + '" does not exist.'); + } else { + // Check group + if (!password) { + var prompt = require('prompt'); + prompt.message = ''; + prompt.delimiter = ''; + var schema = { + properties: { + password: { + description: 'Enter your password:', + pattern: /^[^'"]*$/, + message: 'No " are allowed', + hidden: true + }, + repeatPassword: { + description: 'Repeat your password:', + pattern: /^[^'"]*$/, + message: 'No " are allowed', + hidden: true + } + } + }; + prompt.start(); + + prompt.get(schema, function (err, result) { + if (result) { + if (result.password !== result.repeatPassword) { + if (callback) callback('Passwords are not identical!'); + return; + } + // set user password + that.setPassword(user, result.password, function (err) { + if (err) { + if (callback) callback(err); + } else { + if (callback) callback(); + } + }); + } else { + if (callback) callback('No password entered!'); + } + }); + } else { + that.setPassword(user, password, function (err) { + if (err) { + if (callback) callback(err); + } else { + if (callback) callback(); + } + }); + } + } + }); + }; + + this.enableUser = function (user, enable, callback) { + if (!user) { + if (callback) callback('Please define user name, like: "enable username"'); + return; + } + if (user && user.match(/^system\.user\./)) user = user.substring('system.user.'.length); + + if (user === 'admin' && !enable) { + if (callback) callback('User admin cannot be disabled'); + return; + } + + objects.getObject('system.user.' + user, function (err, obj) { + if (err) { + if (callback) callback('Cannot read user: ' + err); + } if (!obj) { + if (callback) callback('User "' + user + '" not found'); + } else { + obj.common.enabled = enable; + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = new Date().getTime(); + objects.setObject(obj._id, obj, function (err) { + if (typeof callback === 'function') callback(err); + }); + } + }); + }; + + this.checkUserPassword = function (user, password, callback) { + var prompt; + var schema; + if (!user && !password) { + prompt = require('prompt'); + prompt.message = ''; + prompt.delimiter = ''; + schema = { + properties: { + username: { + description: 'Enter username to check password:', + pattern: /^[^'"]+$/, + message: 'No " are allowed', + hidden: false + }, + password: { + description: 'Enter current password:', + pattern: /^[^'"]+$/, + message: 'No " are allowed', + hidden: true + } + } + }; + prompt.start(); + + prompt.get(schema, function (err, result) { + that.checkPassword(result.username, result.password, function (err, res) { + if (err || !res) { + if (callback) callback('Password for user "' + result.username + '" does not matched' + (err ? ': ' + err : '')); + } else { + if (callback) callback(null); + } + }); + }); + } else if (!password) { + prompt = require('prompt'); + prompt.message = ''; + prompt.delimiter = ''; + schema = { + properties: { + password: { + description: 'Enter current password:', + pattern: /^[^'"]+$/, + message: 'No " are allowed', + hidden: true + } + } + }; + prompt.start(); + + prompt.get(schema, function (err, result) { + that.checkPassword(user, result.password, function (err, res) { + if (err || !res) { + if (callback) callback('Password for user "' + user + '" does not matched' + (err ? ': ' + err : '')); + } else { + if (callback) callback(null); + } + }); + }); + } else{ + this.checkPassword(user, password, function (err, res) { + if (err || !res) { + if (callback) callback('Password for user "' + user + '" does not matched' + (err ? ': ' + err : '')); + } else { + if (callback) callback(null); + } + }); + } + }; + + this.getUser = function (user, callback) { + objects.getObject('system.user.' + user, function (err, obj) { + if (err) { + if (callback) callback('Cannot read user: ' + err); + } if (!obj) { + if (callback) callback('User "' + user + '" not found'); + } else { + if (callback) callback(null, obj.common.enabled); + } + }); + }; + + this.getGroup = function (group, callback) { + objects.getObject('system.group.' + group, function (err, obj) { + if (err) { + if (callback) callback('Cannot read group: ' + err); + } if (!obj) { + if (callback) callback('Group "' + group + '" not found'); + } else { + if (callback) callback(null, obj.common.enabled, obj.common.members); + } + }); + }; + + this.enableGroup = function (group, enable, callback) { + if (!group) { + if (callback) callback('Please define group name, like: "enable groupname"'); + return; + } + if (group && group.match(/^system\.group\./)) group = group.substring('system.group.'.length); + + if (group === 'administrator' && !enable) { + if (callback) callback('Group "administrator" cannot be disabled'); + return; + } + + objects.getObject('system.group.' + group, function (err, obj) { + if (err) { + if (callback) callback('Cannot read group: ' + err); + } if (!obj) { + if (callback) callback('Group "' + group + '" not found'); + } else { + obj.common.enabled = enable; + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = new Date().getTime(); + objects.setObject(obj._id, obj, function (err) { + if (typeof callback === 'function') callback(err); + }); + } + }); + }; + + this.addGroup = function (group, callback) { + var _group = group.replace(/\s/g, '_'); + objects.getObject('system.group.' + _group, function (err, obj) { + if (obj) { + if (callback) callback('Group yet exists'); + } else { + objects.setObject('system.group.' + _group, { + type: 'group', + common: { + name: group, + enabled: true, + members: [] + }, + from: 'system.host.' + tools.getHostName() + '.cli', + ts: new Date().getTime(), + native: {} + }, function (err) { + if (typeof callback === 'function') callback(err); + }); + } + }); + }; + + this.delGroup = function (group, callback) { + var _group = group.replace(/\s/g, '_'); + + if (group === 'administrator') { + if (typeof callback === 'function') callback('Group "administrator" cannot be deleted'); + } else { + objects.getObject('system.group.' + _group, function (err, obj) { + if (!obj) { + if (callback) callback('Group does not exists'); + } else { + objects.delObject('system.group.' + _group, function (err) { + if (typeof callback === 'function') callback(err); + }); + } + }); + } + }; + + this.removeUserFromGroup = function (user, group, callback) { + var _group = group.replace(/\s/g, '_'); + objects.getObject('system.group.' + _group, function (err, obj) { + if (!obj) { + if (callback) callback('Group does not exists'); + } else { + var pos = obj.common.members.indexOf('system.user.' + user); + if (pos === -1) { + if (typeof callback === 'function') callback('User not in group'); + } else { + obj.common.members.splice(pos, 1); + obj.from = 'system.host.' + tools.getHostName() + '.cli'; + obj.ts = new Date().getTime(); + objects.setObject(obj._id, obj, function (err) { + if (typeof callback === 'function') callback(err); + }); + } + } + }); + }; +} + +module.exports = Users; \ No newline at end of file diff --git a/lib/setup/setupVendor.js b/lib/setup/setupVendor.js new file mode 100644 index 0000000..bbfcd00 --- /dev/null +++ b/lib/setup/setupVendor.js @@ -0,0 +1,197 @@ +'use strict'; +function Vendor(options) { + const fs = require('fs'); + const tools = require(__dirname + '/../tools.js'); + options = options || {}; + + let objects = options.objects; + // read info from '/etc/iob_vendor.json' and executes instructions stored there + this.checkVendor = (file, password) => { + file = file || '/etc/iob-vendor.json'; + let data; + if (fs.existsSync(file)) { + try { + data = JSON.parse(fs.readFileSync(file).toString('utf8')); + } catch (e) { + return Promise.reject(`cannot read or parse "${file}": ${JSON.stringify(e)}`); + } + } else { + return Promise.reject(`"${file}" does not exist`); + } + + let promises = []; + if (data.uuid) { + const uuid = data.uuid; + data.uuid = null; + + // check UUID + promises.push(objects.getObjectAsync('system.meta.uuid').then(obj => { + if (obj && obj.native) { + if (obj.native.uuid !== uuid) { + obj.native.uuid = uuid; + + console.log(`Update "system.meta.uuid:native.uuid" = "${obj.native.uuid}"`); + + obj.nonEdit = obj.nonEdit || {}; + obj.nonEdit.password = password; + return objects.setObjectAsync('system.meta.uuid', obj).then(() => { + console.log('object system.meta.uuid updated: ' + uuid); + }).catch(err => { + console.error(`Cannot update system.meta.uuid: ${err}`); + }); + } + } else { + return objects.setObjectAsync('system.meta.uuid', { + type: 'meta', + common: { + name: 'uuid', + type: 'uuid' + }, + ts: new Date().getTime(), + from: 'system.host.' + tools.getHostName() + '.tools', + native: { + uuid: uuid + } + }).then(() => { + console.log('object system.meta.uuid created: ' + uuid); + }).catch(err => { + console.error(`Cannot create system.meta.uuid: ${err}`); + }); + } + })); + } + + if (data.model) { + const model = data.model; + data.model = null; + const hostname = tools.getHostName(); + promises.push(objects.getObjectAsync('system.host.' + hostname).then(obj => { + if (obj && obj.common) { + if ((model.name && model.name !== 'JS controller' && obj.common.title === 'JS controller') || + (model.icon && !obj.common.icon) || + (model.color && !obj.common.color)) { + obj.common.title = model.name; + obj.common.icon = model.icon; + obj.common.color = model.color; + + obj.nonEdit = obj.nonEdit || {}; + obj.nonEdit.password = password; + + console.log(`Update "system.host.${hostname}:common.title" = "${obj.common.title}"`); + console.log(`Update "system.host.${hostname}:common.icon" = "${!!obj.common.icon}"`); + console.log(`Update "system.host.${hostname}:common.color" = "${obj.common.color}"`); + + return objects.setObjectAsync(obj._id, obj).then(() => { + console.log(`object "system.host.${hostname}" updated`); + }).catch(err => { + console.error(`Cannot update "system.host.${hostname}": ${err}`); + }); + } + } + })); + } + + if (data.vendor) { + const vendor = JSON.parse(JSON.stringify(data.vendor)); + data._vendor = JSON.parse(JSON.stringify(vendor)); + data.vendor = null; + + // store vendor + promises.push(objects.getObjectAsync('system.config').then(obj => { + if (obj && obj.native) { + if (JSON.stringify(obj.native.vendor) !== JSON.stringify(vendor)) { + obj.native.vendor = vendor; + obj.nonEdit = obj.nonEdit || {}; + obj.nonEdit.password = password; + return objects.setObjectAsync(obj._id, obj).then(() => { + console.log('object system.config updated'); + }).catch(err => { + console.error(`Cannot update system.config: ${err}`); + }); + } + } + })); + } + + return Promise.all(promises).then(() => { + let _promises = []; + // update all existing objects according to vendor + if (data.objects) { + for (let id in data.objects) { + if (!data.objects.hasOwnProperty(id)) continue; + if (id.indexOf('*') === -1) { + ((_id, _obj) => { + _promises.push(objects.getObjectAsync(_id).then(obj => { + if (obj) { + obj.nonEdit = obj.nonEdit || {}; + const originalObj = JSON.stringify(obj); + _obj.nonEdit = _obj.nonEdit || {}; + _obj.nonEdit.passHash = obj.nonEdit.passHash; + // merge objects + tools.copyAttributes(_obj, obj); + + if (originalObj !== JSON.stringify(obj)) { + delete obj.nonEdit.passHash; + obj.nonEdit.password = password; + console.log(`Update "${obj._id}"`); + return objects.setObjectAsync(obj._id, obj).then(() => { + console.log(`object "${obj._id}" updated`); + }).catch(err => { + console.error(`Cannot update "${obj._id}": ${err}`); + }); + } + } else { + return objects.setObjectAsync(_id, _obj).then(() => { + console.log(`object "${obj._id}" updated`); + }).catch(err => { + console.error(`Cannot update "${obj._id}": ${err}`); + }); + } + })); + })(id, data.objects[id]); + } else { + id = id.replace('*', ''); + ((_id, _obj) => { + _promises.push(objects.getObjectListAsync({ + startkey: _id, + endkey: _id + '\u9999' + }, {checked: true}).then(arr => { + let tasks = []; + if (arr && arr.rows && arr.rows.length) { + for (let g = 0; g < arr.rows.length; g++) { + let obj = arr.rows[g].value; + if (obj) { + obj.nonEdit = obj.nonEdit || {}; + const originalObj = JSON.stringify(obj); + _obj.nonEdit = _obj.nonEdit || {}; + _obj.nonEdit.passHash = obj.nonEdit.passHash; + // merge objects + tools.copyAttributes(_obj, obj); + if (originalObj !== JSON.stringify(obj)) { + delete obj.nonEdit.passHash; + obj.nonEdit.password = password; + console.log(`Update "${obj._id}"`); + tasks.push(objects.setObjectAsync(obj._id, obj).then(() => { + console.log(`object "${obj._id}" updated`); + }).catch(err => { + console.error(`Cannot update "${obj._id}": ${err}`); + })); + } + } + } + } + return Promise.all(tasks); + })); + })(id, data.objects[id]); + } + } + } + + return Promise.all(_promises); + }); + }; + + return this; +} + +module.exports = Vendor; \ No newline at end of file diff --git a/lib/setup/setupVisDebug.js b/lib/setup/setupVisDebug.js new file mode 100644 index 0000000..e75c9e1 --- /dev/null +++ b/lib/setup/setupVisDebug.js @@ -0,0 +1,175 @@ +'use strict'; + +function VisDebug(options) { + var fs = require('fs'); + var tools = require(__dirname + '/../tools.js'); + var path = require('path'); + + // allow use without new operator + if (!(this instanceof VisDebug)) return new VisDebug(options); + + options = options || {}; + + if (!options.objects) throw 'Invalid arguments: objects is missing'; + if (!options.processExit) throw 'Invalid arguments: processExit is missing'; + + var objects = options.objects; + var processExit = options.processExit; + + // upload widget directory to vis directory + function uploadWidgets(dir, adapter, path, callback) { + var dirs = fs.readdirSync(dir); + var count = 0; + for (var d = 0; d < dirs.length; d++) { + var stat = fs.statSync(dir + '/' + dirs[d]); + count++; + if (stat.isDirectory()) { + uploadWidgets(dir + '/' + dirs[d], adapter, path + '/' + dirs[d], function () { + if (!--count && callback) callback(); + }); + } else { + console.log('Upload "' + dir + '/' + dirs[d] + '"'); + objects.writeFile(adapter, path + '/' + dirs[d], fs.readFileSync(dir + '/' + dirs[d]), function () { + if (!--count && callback) callback(); + }); + } + } + if (!count && callback) callback(); + } + + this.enableDebug = function (widgetset) { + + if (widgetset) { + // Try to find out the adapter directory out of a list of options + var adapterDir; + var adapterNames2Try = ['vis-' + widgetset, widgetset]; + if (adapterNames2Try[0] === adapterNames2Try[1]) adapterNames2Try.splice(1, 1); + for (var i = 0; i < adapterNames2Try.length; i++) { + try { + var adapterDir2Try = tools.getAdapterDir(adapterNames2Try[i]); + // Query the entry + var stats = fs.lstatSync(adapterDir2Try); + + // Is it a directory? + if (stats.isDirectory()) { + //found it! + adapterDir = adapterDir2Try; + break; + } + } catch (e) { + + } + } + + if (!adapterDir) throw 'Adapter not found. Tried: ' + adapterNames2Try.join(', '); + } + + + // copy index.html.original to index.html + // copy edit.html.original to edit.html + // correct appName.json + // correct config.js + var visDir = __dirname + '/../../node_modules/' + tools.appName + '.vis'; + if (!fs.existsSync(visDir)) { + visDir = __dirname + '/../../node_modules/' + tools.appName.toLowerCase() + '.vis'; + if (!fs.existsSync(visDir)) { + visDir = __dirname + '/../../../' + tools.appName + '.vis'; + if (!fs.existsSync(visDir)) { + visDir = __dirname + '/../../../' + tools.appName.toLowerCase() + '.vis'; + if (!fs.existsSync(visDir)) { + console.error('Cannot find ' + tools.appName + '.vis'); + processExit(40); + } + } + } + } + + console.log('Upload "' + path.normalize(visDir + '/www/index.html.original') + '"'); + var file = fs.readFileSync(visDir + '/www/index.html.original'); + objects.writeFile('vis', 'index.html', file); + + console.log('Upload "' + path.normalize(visDir + '/www/edit.html.original') + '"'); + file = fs.readFileSync(visDir + '/www/edit.html.original'); + objects.writeFile('vis', 'edit.html', file); + + console.log('Modify "' + path.normalize(visDir + '/www/cache.manifest') + '"'); + file = fs.readFileSync(visDir + '/www/cache.manifest', 'utf8'); + var n = file.match(/# dev build (\d+)/, '5'); + n = n[1]; + file = file.replace('# dev build '+ n, '# dev build ' + (parseInt(n, 10) + 1)); + objects.writeFile('vis', 'cache.manifest', file); + + file = fs.readFileSync(tools.getConfigFileName(), 'utf8'); + file = JSON.parse(file); + + var count = 0; + if (!file.objects.noFileCache) { + file.objects.noFileCache = true; + fs.writeFileSync(tools.getConfigFileName(), JSON.stringify(file, null, 2)); + count++; + objects.enableFileCache(false, function (err, actual) { + console.log('Disable cache'); + if (!--count) processExit(); + }); + } + + if (widgetset) { + count++; + objects.readFile('vis', 'js/config.js', null, function (err, data) { + data = data.replace(/[\r\n]/g, ''); + var json = JSON.parse(data.match(/"widgetSets":\s(.*)};/)[1]); + var found = false; + for (var f = 0; f < json.length; f++) { + if (json[f] === widgetset || json[f].name === widgetset) { + found = true; + break; + } + } + // if widget-set not found in config.js + if (!found) { + console.log('Modify config.js'); + var pckg = JSON.parse(fs.readFileSync(adapterDir + '/io-package.json').toString()); + if (pckg.native && pckg.native.dependencies && pckg.native.dependencies.length){ + json.push({ + name: widgetset, + depends: pckg.native.dependencies + }); + } else { + json.push(widgetset); + } + + data = data.replace(/"widgetSets":\s+.*};/, '"widgetSets": ' + JSON.stringify(json, null, 2) + '};'); + + objects.writeFile('vis', 'js/config.js', data, function () { + // upload all files into vis + console.log('Upload ' + adapterDir + '/widgets'); + uploadWidgets(adapterDir + '/widgets', 'vis', 'widgets', function () { + if (!--count) { + // timeoout to print all messages + setTimeout(function () { + processExit(); + }, 100); + } + }); + }); + } else { + // upload all files into vis + console.log('Upload "' + adapterDir + '/widgets' + '"'); + uploadWidgets(adapterDir + '/widgets', 'vis', 'widgets', function () { + if (!--count) { + // timeoout to print all messages + setTimeout(function () { + processExit(); + }, 100); + } + }); + } + + }); + } else { + if (!count) processExit(); + } + }; +} + +module.exports = VisDebug; diff --git a/lib/states.js b/lib/states.js new file mode 100644 index 0000000..e9dfb65 --- /dev/null +++ b/lib/states.js @@ -0,0 +1,13 @@ +'use strict'; + +var getConfigFileName = require(__dirname + '/tools').getConfigFileName; +var config = JSON.parse(require('fs').readFileSync(getConfigFileName(), 'utf8')); +if (!config.states) config.states = {type: 'file'}; + +if (config.states.type === 'file') { + module.exports = require(__dirname + '/states/statesInMemClient'); +} else if (config.states.type === 'redis') { + module.exports = require(__dirname + '/states/statesInRedis'); +} else { + throw 'Unknown objects type: ' + config.objects.type; +} \ No newline at end of file diff --git a/lib/states/statesInMemClient.js b/lib/states/statesInMemClient.js new file mode 100644 index 0000000..fdf5bb3 --- /dev/null +++ b/lib/states/statesInMemClient.js @@ -0,0 +1,368 @@ +/** + * States DB in memory - Client + * + * Copyright 2013-2018 bluefox + * + * MIT License + * + */ + +/** @module statesRedis */ + +/* jshint -W097 */ +/* jshint strict: false */ +/* jslint node: true */ +'use strict'; + +const io = require('socket.io-client'); + +function StatesInMemClient(settings) { + var client; + var subscribes = {}; + var connectionTimeout; + + var log = settings.logger; + if (!log) { + log = { + silly: function (msg) {/*console.log(msg);*/}, + debug: function (msg) {/*console.log(msg);*/}, + info: function (msg) {/*console.log(msg);*/}, + warn: function (msg) { + console.log(msg); + }, + error: function (msg) { + console.log(msg); + } + }; + } else if (!log.silly) { + log.silly = log.debug; + } + + var __construct = (function () { + if (!settings.connection.secure) { + client = io.connect('http://' + ((settings.connection.host && settings.connection.host !== '0.0.0.0') ? settings.connection.host : '127.0.0.1') + ':' + (settings.connection.port || 9000)); + } else { + client = io.connect('https://' + ((settings.connection.host && settings.connection.host !== '0.0.0.0') ? settings.connection.host : '127.0.0.1') + ':' + (settings.connection.port || 9000)); + } + + if (typeof settings.change === 'function') { + client.on('message', function (pattern, channel, message) { + log.silly(settings.namespace + ' inMem message ', pattern, channel, message); + try { + settings.change(channel, message); + } catch (e) { + log.error(settings.namespace + ' message ' + channel + ' ' + message + ' ' + e.message); + log.error(settings.namespace + ' ' + e.stack); + } + }); + } + client.on('disconnect', function (error) { + if (connectionTimeout) { + clearTimeout(connectionTimeout); + connectionTimeout = null; + } + if (typeof settings.disconnected === 'function') { + settings.disconnected(error); + } else { + log.silly(settings.namespace + ' ' + error); + } + }); + client.on('error', function (error) { + if (connectionTimeout) { + clearTimeout(connectionTimeout); + connectionTimeout = null; + } + if (typeof settings.disconnected === 'function') { + settings.disconnected(error); + } else { + log.error(settings.namespace + ' ' + error.message); + log.error(settings.namespace + ' ' + error.stack); + } + }); + client.on('connect', function (error) { + if (connectionTimeout) { + clearTimeout(connectionTimeout); + connectionTimeout = null; + } + if (typeof settings.connected === 'function') settings.connected(); + }); + client.on('reconnect', function (error) { + // Re-init subscribes + for (var t in subscribes) { + for (var i = 0; i < subscribes[t].length; i++) { + client.emit(t, subscribes[t][i]); + } + } + if (typeof settings.connected === 'function') settings.connected(); + }); + connectionTimeout = setTimeout(function () { + if (typeof settings.connectTimeout === 'function') settings.connectTimeout('Connection timeout'); + connectionTimeout = null; + }, 5000); + })(); + + /** + * @method setState + * @param id {String} the id of the value. '.' will be prepended + * @param state {any} + * + * + * an object containing the actual value and some metadata:
+ * setState(id, {'val': val, 'ts': ts, 'ack': ack, 'from': from, 'lc': lc}) + * + * if no object is given state is treated as val:
+ * setState(id, val) + * + *
  • val the actual value. Can be any JSON-stringifiable object. If undefined the + * value is kept unchanged.
  • + * + *
  • ack a boolean that can be used to mark a value as confirmed, used in bidirectional systems which + * acknowledge that a value has been successfully set. Will be set to false if undefined.
  • + * + *
  • ts a unix timestamp indicating the last write-operation on the state. Will be set by the + * setState method if undefined.
  • + * + *
  • lc a unix timestamp indicating the last change of the actual value. this should be undefined + * when calling setState, it will be set by the setValue method itself.
+ * + * @param callback {Function} will be called when redis confirmed reception of the command + * + * + */ + this.setState = function (id, state, callback) { + if (!client) return; + client.emit('setState', id, state, callback); + }; + + // Used for restore function (do not call it + this.setRawState = function (id, state, callback) { + if (!client) return; + client.emit('setRawState', id, state, callback); + }; + + /** + * @method getState + * + * @param {String} id + * @param callback + */ + this.getState = function (id, callback) { + if (!client) return; + client.emit('getState', id, callback); + }; + + this.getStates = function (keys, callback) { + if (!client) return; + client.emit('getStates', keys, function (err, res) { + if (callback) callback(err, res); + }); + }; + + this.delState = function (id, callback) { + if (!client) return; + client.emit('delState', id, callback); + }; + + this.getKeys = function (pattern, callback, dontModify) { + if (!client) return; + client.emit('getKeys', pattern, callback); + }; + + /** + * @method subscribe + * + * @param {string} pattern + * @param {function} callback + */ + this.subscribe = function (pattern, callback) { + subscribes.subscribe = subscribes.subscribe || []; + if (subscribes.subscribe.indexOf(pattern) === -1) subscribes.subscribe.push(pattern); + if (!client) return; + client.emit('subscribe', pattern, callback); + }; + + this.unsubscribe = function (pattern, callback) { + if (subscribes.subscribe) { + var pos = subscribes.subscribe.indexOf(pattern); + if (pos !== -1) subscribes.subscribe.splice(pos, 1); + } + if (!client) return; + client.emit('unsubscribe', pattern, callback); + }; + + // this.pushFifoExists = function (id, state, callback) { + // if (!client) return; + // client.emit('pushFifoExists', id, state, callback); + // }; + // + // this.pushFifo = function (id, state, callback) { + // if (!client) return; + // client.emit('pushFifo', id, state, callback); + // }; + // + // this.lenFifo = function (id, callback) { + // if (!client) return; + // client.emit('lenFifo', id, callback); + // }; + // + // this.getFifo = function (id, callback) { + // if (!client) return; + // client.emit('getFifo', id, callback); + // }; + // + // this.getFifoRange = function (id, start, end, callback) { + // if (!client) return; + // client.emit('getFifoRange', id, start, end, callback); + // }; + // + // this.trimFifo = function (id, minLength, maxLength, callback) { + // if (!client) return; + // client.emit('trimFifo', id, minLength, maxLength, callback); + // }; + + this.pushMessage = function (id, state, callback) { + if (!client) return; + client.emit('pushMessage', id, state, callback); + }; + + this.lenMessage = function (id, callback) { + if (!client) return; + client.emit('lenMessage', id, callback); + }; + + this.getMessage = function (id, callback) { + if (!client) return; + client.emit('getMessage', id, callback); + }; + + this.delMessage = function (id, messageId, callback) { + if (!client) return; + client.emit('delMessage', id, messageId, callback); + }; + + this.subscribeMessage = function (pattern, callback) { + subscribes.subscribeMessage = subscribes.subscribeMessage || []; + if (subscribes.subscribeMessage.indexOf(pattern) === -1) subscribes.subscribeMessage.push(pattern); + if (!client) return; + client.emit('subscribeMessage', pattern, callback); + }; + + this.unsubscribeMessage = function (pattern, callback) { + if (subscribes.subscribeMessage) { + var pos = subscribes.subscribeMessage.indexOf(pattern); + if (pos !== -1) subscribes.subscribeMessage.splice(pos, 1); + } + if (!client) return; + client.emit('unsubscribeMessage', pattern, callback); + }; + + this.pushLog = function (id, state, callback) { + if (!client) return; + client.emit('pushLog', id, state, callback); + }; + + this.lenLog = function (id, callback) { + if (!client) return; + client.emit('lenLog', id, callback); + }; + + this.getLog = function (id, callback) { + if (!client) return; + client.emit('getLog', id, callback); + }; + + this.delLog = function (id, logId, callback) { + if (!client) return; + client.emit('delLog', id, logId, callback); + }; + + this.subscribeLog = function (pattern, callback) { + subscribes.subscribeLog = subscribes.subscribeLog || []; + if (subscribes.subscribeLog.indexOf(pattern) === -1) subscribes.subscribeLog.push(pattern); + + if (!client) return; + client.emit('subscribeLog', pattern, callback); + }; + + this.unsubscribeLog = function (pattern, callback) { + if (subscribes.subscribeLog) { + var pos = subscribes.subscribeLog.indexOf(pattern); + if (pos !== -1) subscribes.subscribeLog.splice(pos, 1); + } + if (!client) return; + client.emit('unsubscribeLog', pattern, callback); + }; + + this.getSession = function (id, callback) { + if (!client) return; + client.emit('getSession', id, callback); + }; + + this.setSession = function (id, expire, obj, callback) { + if (!client) return; + client.emit('setSession', id, expire, obj, callback); + }; + + this.destroySession = function (id, callback) { + if (!client) return; + client.emit('destroySession', id, callback); + }; + + this.getConfig = function (id, callback) { + if (!client) return; + client.emit('getConfig', id, callback); + }; + + this.getConfigKeys = function (pattern, callback, dontModify) { + if (!client) return; + client.emit('getConfigKeys', pattern, callback); + }; + + this.getConfigs = function (keys, callback, dontModify) { + if (!client) return; + client.emit('getConfigs', keys, callback); + }; + + this.setConfig = function (id, obj, callback) { + if (!client) return; + client.emit('setConfig', id, obj, callback); + }; + + this.delConfig = function (id, callback) { + if (!client) return; + client.emit('delConfig', id, callback); + }; + + this.subscribeConfig = function (pattern, callback) { + subscribes.subscribeConfig = subscribes.subscribeConfig || []; + if (subscribes.subscribeConfig.indexOf(pattern) === -1) subscribes.subscribeConfig.push(pattern); + if (!client) return; + client.emit('subscribeConfig', pattern, callback); + }; + + this.unsubscribeConfig = function (pattern, callback) { + if (subscribes.subscribeConfig) { + var pos = subscribes.subscribeConfig.indexOf(pattern); + if (pos !== -1) subscribes.subscribeConfig.splice(pos, 1); + } + if (!client) return; + client.emit('unsubscribeConfig', pattern, callback); + }; + + this.setBinaryState = function (id, data, callback) { + if (!client) return; + client.emit('setBinaryState', id, data, callback); + }; + + this.getBinaryState = function (id, callback) { + if (!client) return; + client.emit('getBinaryState', id, callback); + }; + + this.delBinaryState = function (id, callback) { + if (!client) return; + client.emit('delBinaryState', id, callback); + }; +} + +module.exports = StatesInMemClient; diff --git a/lib/states/statesInMemServer.js b/lib/states/statesInMemServer.js new file mode 100644 index 0000000..159fa87 --- /dev/null +++ b/lib/states/statesInMemServer.js @@ -0,0 +1,1036 @@ +/** + * States DB in memory - Server + * + * Copyright 2013-2018 bluefox + * + * MIT License + * + */ + +/** @module statesInMemory */ + +/* jshint -W097 */ +/* jshint strict:false */ +/* jslint node: true */ +'use strict'; + +const socketio = require('socket.io'); +const fs = require('fs'); +const getDefaultDataDir = require(__dirname + '/../tools').getDefaultDataDir; + +// settings = { +// change: function (id, state) {}, +// connected: function (nameOfServer) {}, +// logger: { +// silly: function (msg) {}, +// debug: function (msg) {}, +// info: function (msg) {}, +// warn: function (msg) {}, +// error: function (msg) {} +// }, +// connection: { +// dataDir: 'relative path' +// }, +// auth: null, //unused +// secure: true/false, +// certificates: as required by createServer +// port: 9000, +// host: localhost +// }; +// + +function StatesInMemory(settings) { + if (!(this instanceof StatesInMemory)) return new StatesInMemory(settings); + settings = settings || {}; + + let change = settings.change; + + let states = {}; + let messagebox = {}; + let logs = {}; + let session = {}; + let globalMessageId = Math.round(Math.random() * 100000000); + let globalLogId = Math.round(Math.random() * 100000000); + + let expires = []; + let adapterSubs = []; + let lastExpire = null; + let expiresInterval = null; + let namespace = settings.namespace || ''; + let lastSave = null; + let zlib; + + settings.backup = settings.backup || { + disabled: false, // deactivates + files: 24, // minimum number of files + hours: 48, // hours + period: 120, // minutes + path: '' // use default path + }; + + // path is always relative to appName.js-controller + let dataDir = (settings.connection.dataDir || getDefaultDataDir()); + if (dataDir) { + if (dataDir[0] === '.' && dataDir[1] === '.') { + dataDir = __dirname + '/../../' + dataDir; + } else if (dataDir[0] === '.' && dataDir[1] === '/') { + dataDir = __dirname + '/../../' + dataDir.substring(2); + } + } + dataDir = dataDir.replace(/\\/g, '/'); + if (dataDir[dataDir.length - 1] !== '/') dataDir += '/'; + + let statesName = dataDir + 'states.json'; + let stateTimer = null; + let that = this; + + const backupDir = settings.backup.path || (dataDir + 'backup-objects/'); + + if (!settings.backup.disabled) { + zlib = zlib || require('zlib'); + // Interval in minutes => to milliseconds + settings.backup.period = settings.backup.period === undefined ? 120 : parseInt(settings.backup.period); + if (isNaN(settings.backup.period)) { + settings.backup.period = 120; + } + settings.backup.period *= 60000; + + settings.backup.files = settings.backup.files === undefined ? 24 : parseInt(settings.backup.files); + if (isNaN(settings.backup.files)) { + settings.backup.files = 24; + } + + settings.backup.hours = settings.backup.hours === undefined ? 48 : parseInt(settings.backup.hours); + if (isNaN(settings.backup.hours)) { + settings.backup.hours = 48; + } + // Create backup directory + if (!fs.existsSync(backupDir)) { + fs.mkdirSync(backupDir); + } + } + + let log = settings.logger; + if (!log) { + log = { + silly: function (msg) {/*console.log(msg);*/}, + debug: function (msg) {/*console.log(msg);*/}, + info: function (msg) {/*console.log(msg);*/}, + warn: function (msg) { + console.log(msg); + }, + error: function (msg) { + console.log(msg); + } + }; + } else if (!log.silly) { + log.silly = log.debug; + } + + let server = { + app: null, + server: null, + io: null, + settings: settings + }; + + (function __construct() { + settings.connection.maxQueue = settings.connection.maxQueue || 1000; + + // load values from file + if (fs.existsSync(statesName)) { + try { + states = JSON.parse(fs.readFileSync(statesName).toString()); + } catch (e) { + log.error(namespace + ' Cannot parse ' + statesName + ': ' + e); + if (fs.existsSync(statesName + '.bak')) { + try { + states = JSON.parse(fs.readFileSync(statesName + '.bak').toString()); + } catch (e) { + log.error(namespace + ' Cannot parse ' + statesName + '.bak: ' + e); + states = {}; + } + } else { + states = {}; + } + } + } else if (fs.existsSync(statesName + '.bak')) { + try { + states = JSON.parse(fs.readFileSync(statesName + '.bak').toString()); + } catch (e) { + log.error(namespace + ' Cannot parse ' + statesName + '.bak: ' + e); + states = {}; + } + } else { + states = {}; + } + + // Reset expires, that are still in DB + expireAll(); + + // Check if directory exists + statesName = statesName.replace(/\\/g, '/'); + /** @type {string|string[]} */ + let parts = statesName.split('/'); + parts.pop(); + parts = parts.join('/'); + if (!fs.existsSync(parts)) fs.mkdirSync(parts); + + _initWebServer(settings.connection, server); + + if (settings.connected) { + setImmediate(function () { + settings.connected('InMemoryDB'); + }); + } + })(); + + function expireAll() { + for (let i = expires.length - 1; i >= 0; i--) { + let e = expires[i]; + if (states[e]) { + states[e].ts = (new Date()).getTime(); + states[e].lc = (states[e].val) ? states[e].ts : states[e].lc; + states[e].val = null; + delete states[e].expire; + that.publishAll('state', e, states[e]); + } + } + // Set as expire all states that could expire + for (let t in states) { + if (!states.hasOwnProperty(t) || !states[t]) continue; + if (states[t].expire !== undefined) { + states[t].ts = (new Date()).getTime(); + states[t].lc = (states[t].val) ? states[t].ts : states[t].lc; + states[t].val = null; + delete states[t].expire; + } + } + expires = []; + } + + function expiresCheck() { + let now = (new Date()).getTime(); + if (lastExpire !== null) { + let diff = now - lastExpire; + let count = 0; + for (let i = expires.length - 1; i >= 0; i--) { + let e = expires[i]; + if (states[e] && states[e].expire !== undefined) { + states[e].expire -= diff; + + // if expired + if (states[e].expire < 0) { + // Set value to null + states[e].ts = (new Date()).getTime(); + states[e].lc = (states[e].val) ? states[e].ts : states[e].lc; + states[e].val = null; + expires.splice(i, 1); + delete states[e].expire; + that.publishAll('state', e, states[e]); + } else { + count++; + } + } else { + expires.splice(i, 1); + } + } + + for (let t in session) { + session[t]._expire -= diff; + if (session[t]._expire < 0) { + delete session[t]; + } else { + count++; + } + } + + if (!count && expiresInterval) { + clearInterval(expiresInterval); + expiresInterval = null; + } + } + lastExpire = now; + } + + function pattern2RegEx(pattern) { + if (pattern && pattern !== '*') { + if (pattern[0] === '*' && pattern[pattern.length - 1] !== '*') pattern += '$'; + if (pattern[0] !== '*' && pattern[pattern.length - 1] === '*') pattern = '^' + pattern; + } + pattern = (pattern || '').toString().replace(/\./g, '\\.'); + pattern = pattern.replace(/\*/g, '.*'); + return pattern; + } + + function subscribe(socket, type, pattern, cb) { + socket._subscribe = socket._subscribe || {}; + if (!socket._subscribe[type]) socket._subscribe[type] = []; + let s = socket._subscribe[type]; + for (let i = 0; i < s.length; i++) { + if (s[i].pattern === pattern) { + if (typeof cb === 'function') cb(); + return; + } + } + // + s.push({pattern: pattern, regex: new RegExp(pattern2RegEx(pattern))}); + if (typeof cb === 'function') cb(); + } + + function unsubscribe(socket, type, pattern, cb) { + if (!socket._subscribe || !socket._subscribe[type]) { + if (typeof cb === 'function') cb(); + return; + } + let s = socket._subscribe[type]; + for (let i = 0; i < s.length; i++) { + if (s[i].pattern === pattern) { + s.splice(i, 1); + if (typeof cb === 'function') cb(); + return; + } + } + if (typeof cb === 'function') cb(); + } + + function publish(socket, type, id, obj) { + if (!socket._subscribe || !socket._subscribe[type]) return; + let s = socket._subscribe[type]; + for (let i = 0; i < s.length; i++) { + if (s[i].regex.test(id)) { + socket.emit('message', s[i].pattern, id, obj); + return; + } + } + } + + function deleteOldBackupFiles() { + // delete files only if settings.backupNumber is not 0 + let files = fs.readdirSync(backupDir); + files.sort(); + const limit = Date.now() - settings.backup.hours * 3600000; + + for (let f = files.length - 1; f >= 0; f--) { + if (!files[f].match(/_states.json.gz$/)) { + files.splice(f, 1); + } + } + + while (files.length > settings.backup.files) { + let file = files.shift(); + // extract time + const ms = new Date(file.substring(0, 10) + ' ' + file.substring(11, 16).replace('-', ':') + ':00').getTime(); + if (limit > ms) { + try { + fs.unlink(backupDir + file); + } catch (e) { + log.error(`Cannot delete file "${backupDir + file}: ${JSON.stringify(e)}`); + } + } + } + } + + function getTimeStr(date) { + let dateObj = new Date(date); + + let text = dateObj.getFullYear().toString() + '-'; + let v = dateObj.getMonth() + 1; + if (v < 10) text += '0'; + text += v.toString() + '-'; + + v = dateObj.getDate(); + if (v < 10) text += '0'; + text += v.toString() + '_'; + + v = dateObj.getHours(); + if (v < 10) text += '0'; + text += v.toString() + '-'; + + v = dateObj.getMinutes(); + if (v < 10) text += '0'; + text += v.toString(); + + return text; + } + + function saveState() { + if (fs.existsSync(statesName)) { + let old = fs.readFileSync(statesName); + fs.writeFileSync(statesName + '.bak', old); + } + const actual = JSON.stringify(states); + try { + fs.writeFileSync(statesName, actual); + + if (!settings.backup.disabled) { + // save files for the last x hours + const now = Date.now(); + + // makes backups only if settings.backupInterval is not 0 + if (settings.backup.period && (!lastSave || now - lastSave > settings.backup.period)) { + lastSave = now; + let backFileName = backupDir + getTimeStr(now) + '_states.json.gz'; + + if (!fs.existsSync(backFileName)) { + zlib = zlib || require('zlib'); + let output = fs.createWriteStream(backFileName); + let compress = zlib.createGzip(); + /* The following line will pipe everything written into compress to the file stream */ + compress.pipe(output); + /* Since we're piped through the file stream, the following line will do: + 'Hello World!'->gzip compression->file which is the desired effect */ + compress.write(actual); + compress.end(); + + // analyse older files + deleteOldBackupFiles(); + } + } + } + } catch (e) { + log.error(namespace + ' Cannot save ' + statesName + ': ' + e); + } + if (stateTimer) { + clearTimeout(stateTimer); + stateTimer = null; + } + } + + function socketEvents(socket, user) { + /* + * states + */ + socket.on('getStates', function (keys, callback, dontModify) { + that.getStates.apply(that, arguments); + }); + socket.on('getState', function (id, callback) { + that.getState.apply(that, arguments); + }); + socket.on('setState', function (id, state, callback) { + that.setState.apply(that, arguments); + }); + socket.on('setRawState', function (id, state, callback) { + that.setRawState.apply(that, arguments); + }); + socket.on('delState', function (id, callback) { + that.delState.apply(that, arguments); + }); + socket.on('getKeys', function (pattern, callback, dontModify) { + that.getKeys.apply(that, arguments); + }); + socket.on('subscribe', function (pattern, callback) { + that.subscribe.apply(this, arguments); + }); + socket.on('unsubscribe', function (pattern, callback) { + that.unsubscribe.apply(this, arguments); + }); + // socket.on('pushFifoExists', function (id, state, callback) { + // that.pushFifoExists.apply(that, arguments); + // }); + // socket.on('pushFifo', function (id, state, callback) { + // that.pushFifo.apply(that, arguments); + // }); + // socket.on('lenFifo', function (id, callback) { + // that.lenFifo.apply(that, arguments); + // }); + // socket.on('getFifo', function (id, callback) { + // that.getFifo.apply(that, arguments); + // }); + // socket.on('getFifoRange', function (id, start, end, callback) { + // that.getFifoRange.apply(that, arguments); + // }); + // socket.on('trimFifo', function (id, minLength, maxLength, callback) { + // that.trimFifo.apply(that, arguments); + // }); + socket.on('pushMessage', function (id, state, callback) { + that.pushMessage.apply(that, arguments); + }); + socket.on('lenMessage', function (id, callback) { + that.lenMessage.apply(that, arguments); + }); + socket.on('getMessage', function (id, callback) { + that.getMessage.apply(that, arguments); + }); + socket.on('delMessage', function (id, messageId, callback) { + that.delMessage.apply(that, arguments); + }); + socket.on('subscribeMessage', function (id, callback) { + that.subscribeMessage.apply(this, arguments); + }); + socket.on('unsubscribeMessage', function (id, callback) { + that.unsubscribeMessage.apply(this, arguments); + }); + socket.on('pushLog', function (id, state, callback) { + that.pushLog.apply(that, arguments); + }); + socket.on('lenLog', function (id, callback) { + that.lenLog.apply(that, arguments); + }); + socket.on('getLog', function (id, callback) { + that.getLog.apply(that, arguments); + }); + socket.on('delLog', function (id, logId, callback) { + that.delLog.apply(that, arguments); + }); + socket.on('subscribeLog', function (id, callback) { + that.subscribeLog.apply(this, arguments); + }); + socket.on('unsubscribeLog', function (id, callback) { + that.unsubscribeLog.apply(this, arguments); + }); + socket.on('getSession', function (id, callback) { + that.getSession.apply(that, arguments); + }); + socket.on('setSession', function (id, expire, obj, callback) { + that.setSession.apply(that, arguments); + }); + socket.on('destroySession', function (id, callback) { + that.destroySession.apply(that, arguments); + }); + socket.on('getConfig', function (id, callback) { + that.getConfig.apply(that, arguments); + }); + socket.on('getConfigKeys', function (pattern, callback, dontModify) { + that.getConfigKeys.apply(that, arguments); + }); + socket.on('getConfigs', function (keys, callback, dontModify) { + that.getConfigs.apply(that, arguments); + }); + socket.on('setConfig', function (id, obj, callback) { + that.setConfig.apply(that, arguments); + }); + socket.on('delConfig', function (id, callback) { + that.delConfig.apply(that, arguments); + }); + socket.on('subscribeConfig', function (pattern, callback) { + that.subscribeConfig.apply(this, arguments); + }); + socket.on('unsubscribeConfig', function (pattern, callback) { + that.unsubscribeConfig.apply(this, arguments); + }); + socket.on('setBinaryState', function (id, data, callback) { + that.setBinaryState.apply(that, arguments); + }); + socket.on('getBinaryState', function (id, callback) { + that.getBinaryState.apply(that, arguments); + }); + socket.on('delBinaryState', function (id, callback) { + that.delBinaryState.apply(that, arguments); + }); + } + + this.publishAll = function (type, id, obj) { + let clients = server.io.sockets.connected; + + for (let i in clients) { + if (clients.hasOwnProperty(i)) { + publish(clients[i], type, id, obj); + } + } + + if (change && this._subscribe && this._subscribe[type]) { + for (let j = 0; j < this._subscribe[type].length; j++) { + if (this._subscribe[type][j].regex.test(id)) { + setImmediate(function () { + change(id, obj); + }); + break; + } + } + } + }; + + // Destructor of the class. Called by shutting down. + this.destroy = function () { + expireAll(); + if (stateTimer) saveState(); + + if (server.io) { + if (server.io.sockets && server.io.sockets.connected) { + for (let s in server.io.sockets.connected) { + if (server.io.sockets.connected.hasOwnProperty(s)) { + delete server.io.sockets.connected[s]; + } + } + } + try { + server.io.close(); + } catch (e) { + console.log(e.message); + } + } + }; + + this.getStates = function (keys, callback, dontModify) { + if (!keys) { + if (callback) callback('no keys', null); + return; + } + if (!keys.length) { + if (callback) callback(null, []); + return; + } + let result = []; + for (let i = 0; i < keys.length; i++) { + result.push(states[keys[i]]); + } + if (typeof callback === 'function') callback(null, result); + }; + + this.getState = function (id, callback) { + if (typeof callback === 'function') { + callback(null, states[id]); + } + }; + + /** + * @method setState + * @param id {String} the id of the value. + * @param state {any} + * + * + * an object containing the actual value and some metadata:
+ * setState(id, {'val': val, 'ts': ts, 'ack': ack, 'from': from, 'lc': lc}) + * + * if no object is given state is treated as val:
+ * setState(id, val) + * + *
  • val the actual value. Can be any JSON-stringifiable object. If undefined the + * value is kept unchanged.
  • + * + *
  • ack a boolean that can be used to mark a value as confirmed, used in bidirectional systems which + * acknowledge that a value has been successfully set. Will be set to false if undefined.
  • + * + *
  • ts a unix timestamp indicating the last write-operation on the state. Will be set by the + * setState method if undefined.
  • + * + *
  • lc a unix timestamp indicating the last change of the actual value. this should be undefined + * when calling setState, it will be set by the setValue method itself.
+ * + * @param callback {Function} will be called when redis confirmed reception of the command + */ + this.setState = function (id, state, callback) { + let that = this; + let obj = {}; + + if (typeof state !== 'object' || state === null || state === undefined) { + state = { + val: state + }; + } + + let oldObj = states[id]; + + if (!oldObj) { + oldObj = {}; + } + + if (state.val !== undefined) { + obj.val = state.val; + } else { + obj.val = oldObj.val; + } + + if (state.ack !== undefined) { + obj.ack = state.ack; + } else { + obj.ack = false; + } + + if (state.ts !== undefined) { + obj.ts = (state.ts < 946681200000) ? state.ts * 1000 : state.ts; // if less 2000.01.01 00:00:00 + } else { + obj.ts = (new Date()).getTime(); + } + + if (state.q !== undefined) { + obj.q = state.q; + } else { + obj.q = 0; + } + + // comment + if (state.c) { + obj.c = state.c.toString().substring(0, 512); + } + + if (state.ms !== undefined) { + obj.ms = state.ms; + } + + obj.from = state.from; + + let hasChanged; + + if (state.lc !== undefined) { + obj.lc = state.lc; + } else { + if (typeof obj.val === 'object') { + hasChanged = JSON.stringify(oldObj.val) !== JSON.stringify(obj.val); + } else { + hasChanged = oldObj.val !== obj.val; + } + if (!oldObj.lc || hasChanged) { + obj.lc = obj.ts; + } else { + obj.lc = oldObj.lc; + } + } + + // publish event in states + log.silly(namespace + ' memory publish ' + id + ' ' + JSON.stringify(obj)); + // If val === undefined, the state was just created and not filled with value + if (obj.val !== undefined) that.publishAll('state', id, obj); + + // set object in redis + if (state.expire) { + state.expire *= 1000; // make ms from seconds + + if (expires.indexOf(id) === -1) expires.push(id); + + if (!expiresInterval) { + lastExpire = (new Date()).getTime(); + expiresInterval = setInterval(expiresCheck, 5000); + } else { + if (lastExpire) state.expire -= ((new Date()).getTime() - lastExpire); + } + obj.expire = state.expire; + } + states[id] = obj; + if (typeof callback === 'function') callback(null, id); + + if (!stateTimer) stateTimer = setTimeout(saveState, 30000); + }; + + this.setRawState = function (id, state, callback) { + states[id] = state; + if (typeof callback === 'function') { + setImmediate(function () { + callback(null, id); + }); + } + }; + + this.delState = function (id, callback) { + if (states[id]) { + delete states[id]; + this.publishAll('state', id, null); + } + if (typeof callback === 'function') { + setImmediate(function () { + callback(null, id); + }); + } + }; + + this.getKeys = function (pattern, callback, dontModify) { + // special case because of simulation of redis + if (pattern.substring(0, 3) === 'io.') pattern = pattern.substring(3); + + let r = new RegExp(pattern2RegEx(pattern)); + let result = []; + for (let id in states) { + if (r.test(id)) result.push(id); + } + if (typeof callback === 'function') callback(null, result); + }; + + this.subscribe = function (pattern, cb) { + subscribe(this, 'state', pattern, cb); + }; + + this.unsubscribe = function (pattern, cb) { + unsubscribe(this, 'state', pattern, cb); + }; + + /** + * Register some instance as subscribable. + * If some instance says, that it is subscribable, the instance can read every time (and at start) + * all subscriptions to their states and will receive messages about changes of subscriptions + * + * @param instance name of instance + * @param cb callback which says if subscription added or yet exists + */ + this.registerAdapterSubs = function (instance, cb) { + let added = false; + if (adapterSubs.indexOf(instance) === -1) { + adapterSubs.push(instance); + adapterSubs.sort(); + added = true; + } + if (cb) cb(null, added); + }; + + /** + * Unregister instance as subscribable. + * + * @param instance name of instance + * @param cb callback which says if subscription removed or no + */ + this.unregisterAdapterSubs = function (instance, cb) { + let pos = adapterSubs.indexOf(instance); + if (pos !== -1) { + adapterSubs.splice(pos, 1); + } + if (cb) cb(null, pos !== -1); + }; + + this.pushMessage = function (id, state, callback) { + //messagebox[id] = messagebox[id] || []; + state._id = globalMessageId++; + if (globalMessageId >= 0xFFFFFFFF) globalMessageId = 0; + //messagebox[id].unshift(state); + that.publishAll('messagebox', 'messagebox.' + id, state); + if (typeof callback === 'function') callback(null, id); + }; + + this.lenMessage = function (id, callback) { + if (messagebox[id]) { + if (typeof callback === 'function') callback(null, messagebox[id].length); + } else { + if (typeof callback === 'function') callback('Not exists', null); + } + }; + + this.getMessage = function (id, callback) { + if (messagebox[id]) { + if (typeof callback === 'function') callback(null, messagebox[id].pop()); + } else { + if (typeof callback === 'function') callback('Not exists', null); + } + }; + + this.delMessage = function (id, messageId, callback) { + if (messagebox[id]) { + let found = false; + for (let i = messagebox[id].length - 1; i >= 0; i--) { + if (messagebox[id][i]._id === messageId) { + messagebox[id].splice(i, 1); + found = true; + break; + } + } + if (!found) { + console.log('WARNING: cannot find message with id = ' + messageId); + log.error(namespace + ' WARNING: cannot find message with id = ' + messageId); + if (typeof callback === 'function') callback('Not exists'); + } else { + if (typeof callback === 'function') callback(); + } + } else { + if (typeof callback === 'function') callback(); + } + }; + + this.clearAllMessages = function (callback) { + messagebox = {}; + if (typeof callback === 'function') callback(); + }; + + this.subscribeMessage = function (id, cb) { + subscribe(this, 'messagebox', 'messagebox.' + id, cb); + }; + + this.unsubscribeMessage = function (id, cb) { + unsubscribe(this, 'messagebox', 'messagebox.' + id, cb); + }; + + /** + * @method pushLog + * @param {String} id the id of the logger. + * @param {object} log log object, looks like + * pushLog(id, {message: msg, severity: info|debug|warn|error, from: that.namespace, ts: (new Date()).getTime()}) + * + *
  • message the actual value. Can be any JSON-stringifiable object. If undefined the + * value is kept unchanged.
  • + * + *
  • severity a boolean that can be used to mark a value as confirmed, used in bidirectional systems which + * acknowledge that a value has been successfully set. Will be set to false if undefined.
  • + * + *
  • from a unix timestamp indicating the last write-operation on the state. Will be set by the + * setState method if undefined.
  • + * + *
  • ts a unix timestamp indicating the last change of the actual value. this should be undefined + * when calling setState, it will be set by the setValue method itself.
+ * + * @param callback {Function} will be called when confirmed reception of the command + */ + this.pushLog = function (id, log, callback) { + // do not store messages. + //logs[id] = logs[id] || []; + log._id = globalLogId++; + if (globalLogId >= 0xFFFFFFFF) globalLogId = 0; + //logs[id].unshift(state); + //if (logs[id].length > settings.connection.maxQueue) { + // logs[id].splice(settings.connection.maxQueue - logs[id].length); + //} + that.publishAll('log', 'log.' + id, log); + if (typeof callback === 'function') callback(null, id); + }; + + this.lenLog = function (id, callback) { + if (logs[id]) { + if (typeof callback === 'function') callback(null, logs[id].length, id); + } else { + if (typeof callback === 'function') callback('Not exists', null, id); + } + }; + + this.getLog = function (id, callback) { + if (logs[id]) { + if (typeof callback === 'function') callback(null, logs[id].pop(), logs[id].length); + } else { + if (typeof callback === 'function') callback('Not exists', null, 0); + } + }; + + this.delLog = function (id, logId, callback) { + if (logs[id]) { + let found = false; + for (let i = logs[id].length - 1; i >= 0; i--) { + if (logs[id][i]._id === logId) { + logs[id].splice(i, 1); + found = true; + break; + } + } + if (!found) { + // Protection against too much lost IDs + if (logs[id].length > 100) { + console.log('WARNING: cannot find logs with id = ' + logId); + log.error(namespace + ' WARNING: cannot find logs with id = ' + logId); + logs[id].splice(100, logs[id].length - 100); + } + if (typeof callback === 'function') callback('Not exists'); + } else { + if (typeof callback === 'function') callback(); + } + } else if (typeof callback === 'function') { + callback('Not exists'); + } + }; + + this.clearAllLogs = function (callback) { + logs = {}; + if (typeof callback === 'function') callback(); + }; + + this.subscribeLog = function (id, cb) { + subscribe(this, 'log', 'log.' + id, cb); + }; + + this.unsubscribeLog = function (id, cb) { + unsubscribe(this, 'log', 'log.' + id, cb); + }; + + this.getSession = function (id, callback) { + if (typeof callback === 'function') callback(session[id]); + }; + + this.setSession = function (id, expire, obj, callback) { + session[id] = obj || {}; + session[id]._expire = expire * 1000; + if (!expiresInterval) { + lastExpire = (new Date()).getTime(); + expiresInterval = setInterval(expiresCheck, 5000); + } else { + if (lastExpire) session[id]._expire -= ((new Date()).getTime() - lastExpire); + } + + if (typeof callback === 'function') callback(); + }; + + this.destroySession = function (id, callback) { + if (session[id]) { + delete session[id]; + } + if (typeof callback === 'function') callback(); + }; + + this.setBinaryState = function (id, data, callback) { + states[id] = data; + if (typeof callback === 'function') callback(null, id); + if (!stateTimer) { + stateTimer = setTimeout(saveState, 30000); + } + }; + + this.getBinaryState = function (id, callback) { + if (states[id]) { + if (callback) callback(null, states[id]); + } else { + if (callback) callback('not exists'); + } + }; + + this.delBinaryState = function (id, callback) { + if (states[id]) { + delete states[id]; + } + if (typeof callback === 'function') callback(null, id); + }; + + function initSocket(socket) { + if (settings.auth) { + let user = null; + socketEvents(socket, user); + } else { + socketEvents(socket); + } + } + + function _initWebServer(settings, server) { + try { + if (settings.secure) { + if (!settings.certificates) return; + server.server = require('https').createServer(settings.certificates, function (req, res) { + res.writeHead(501); + res.end('Not Implemented'); + }); + } else { + server.server = require('http').createServer(function (req, res) { + res.writeHead(501); + res.end('Not Implemented'); + }); + } + server.server.listen(settings.port || 9000, (settings.host && settings.host !== 'localhost') ? settings.host : ((settings.host === 'localhost') ? '127.0.0.1' : undefined)); + } catch (e) { + log.error(namespace + ' Cannot start inMem-objects on port ' + (settings.port || 9000) + ': ' + e.message); + console.log('Cannot start inMem-objects on port ' + (settings.port || 9000) + ': ' + e.message); + process.exit(24); + } + + server.io = socketio.listen(server.server); + + if (settings.auth) { + + server.io.use(function (socket, next) { + if (!socket.request._query.user || !socket.request._query.pass) { + console.log("No password or username!"); + next(new Error('Authentication error')); + } else { + // TODO + console.log('Not implemented'); + next(new Error('Authentication error/Not implemented')); + /* + adapter.checkPassword(socket.request._query.user, socket.request._query.pass, function (res) { + if (res) { + console.log("Logged in: " + socket.request._query.user + ', ' + socket.request._query.pass); + return next(); + } else { + console.log("Invalid password or user name: " + socket.request._query.user + ', ' + socket.request._query.pass); + next(new Error('Invalid password or user name')); + } + });*/ + } + }); + } + server.io.set('origins', '*:*'); + server.io.on('connection', initSocket); + + log.info(namespace + ' ' + (settings.secure ? 'Secure ' : '') + ' inMem-states listening on port ' + (settings.port || 9000)); + } +} + +module.exports = StatesInMemory; diff --git a/lib/states/statesInRedis.js b/lib/states/statesInRedis.js new file mode 100644 index 0000000..323ee1b --- /dev/null +++ b/lib/states/statesInRedis.js @@ -0,0 +1,636 @@ +/** + * States DB in redis - Client + * + * Copyright 2013-2018 bluefox + * Copyright 2013-2014 hobbyquaker + * + * MIT License + * + */ +/** @module statesRedis */ + +/* jshint -W097 */ +/* jshint strict: false */ +/* jslint node: true */ +'use strict'; + +const redis = require('redis'); + +function StateRedis(settings) { + + const redisNamespace = (settings.redisNamespace || 'io') + '.'; + const namespaceMsg = (settings.namespaceMsg || 'messagebox') + '.'; + const namespaceLog = (settings.namespaceLog || 'log') + '.'; + const namespaceSession = (settings.namespaceSession || 'session') + '.'; + // const namespaceConfig = (settings.namespaceConfig || 'config') + '.'; + const onChange = settings.change; // on change handler + let globalMessageId = Math.round(Math.random() * 100000000); + let globalLogId = Math.round(Math.random() * 100000000); + settings.namespace = settings.namespace || settings.hostname || ''; + + let client; + let clientBin; + let sub; + const ioRegExp = new RegExp('^' + redisNamespace); + + let log = settings.logger; + if (!log) { + log = { + silly: function (msg) {/* console.log(msg); */}, + debug: function (msg) {/* console.log(msg); */}, + info: function (msg) {/* console.log(msg); */}, + warn: function (msg) { + console.log(msg); + }, + error: function (msg) { + console.log(msg); + } + }; + } else if (!log.silly) { + log.silly = log.debug; + } + + // limit max number of log entries in the list + settings.connection.maxQueue = settings.connection.maxQueue || 1000; + + if (settings.connection.options) { + if (settings.connection.options.retry_max_delay) { + const retry_max_delay = settings.connection.options.retry_max_delay; + // convert redis 0.1 options to redis 3.0 + settings.connection.options.retry_strategy = function (options) { + // A function that receives an options object as parameter including the retry attempt, + // the total_retry_time indicating how much time passed since the last time connected, + // the error why the connection was lost and the number of times_connected in total. + // If you return a number from this function, the retry will happen exactly after that + // time in milliseconds. If you return a non-number, no further retry will happen and + // all offline commands are flushed with errors. Return an error to return that + // specific error to all offline commands. + + return retry_max_delay; + /*if (options.error.code === 'ECONNREFUSED') { + // End reconnecting on a specific error and flush all commands with a individual error + return new Error('The server refused the connection'); + } + if (options.total_retry_time > 1000 * 60 * 60) { + // End reconnecting after a specific timeout and flush all commands with a individual error + return new Error('Retry time exhausted'); + } + if (options.times_connected > 10) { + // End reconnecting with built in error + return undefined; + } + // reconnect after + return Math.max(options.attempt * 100, 3000);*/ + }; + delete settings.connection.options.retry_max_delay; + } + } + + /** + * @method setState + * @param id {String} the id of the value. '.' will be prepended + * @param state {any} + * + * + * an object containing the actual value and some metadata:
+ * setState(id, {'val': val, 'ts': ts, 'ack': ack, 'from': from, 'lc': lc}) + * + * if no object is given state is treated as val:
+ * setState(id, val) + * + *
  • val the actual value. Can be any JSON-stringifiable object. If undefined the + * value is kept unchanged.
  • + * + *
  • ack a boolean that can be used to mark a value as confirmed, used in bidirectional systems which + * acknowledge that a value has been successfully set. Will be set to false if undefined.
  • + * + *
  • ts a unix timestamp indicating the last write-operation on the state. Will be set by the + * setState method if undefined.
  • + * + *
  • lc a unix timestamp indicating the last change of the actual value. this should be undefined + * when calling setState, it will be set by the setValue method itself.
+ * + * @param callback {Function} will be called when redis confirmed reception of the command + */ + this.setState = function (id, state, callback) { + let expire; + if (state.expire) { + expire = state.expire; + delete state.expire; + } + //var that = this; + let obj = {}; + + if (typeof state !== 'object') { + state = { + val: state + }; + } + + client.get(redisNamespace + id, function (err, oldObj) { + // TODO Error Handling + if (err) log.warn(settings.namespace + ' ' + err); + + if (!oldObj) { + oldObj = {}; + } else { + try { + oldObj = JSON.parse(oldObj); + } catch (e) { + oldObj = {}; + } + + } + + if (state.val !== undefined) { + obj.val = state.val; + } else { + obj.val = oldObj.val; + } + + if (state.ack !== undefined) { + obj.ack = state.ack; + } else { + obj.ack = false; + } + + if (state.ts !== undefined) { + obj.ts = (state.ts < 946681200000) ? state.ts * 1000 : state.ts; // if less 2000.01.01 00:00:00 + } else { + obj.ts = (new Date()).getTime(); + } + + if (state.q !== undefined) { + obj.q = state.q; + } else { + obj.q = 0; + } + + obj.from = state.from; + + let hasChanged; + + if (state.lc !== undefined) { + obj.lc = state.lc; + } else { + if (typeof obj.val === 'object') { + hasChanged = JSON.stringify(oldObj.val) !== JSON.stringify(obj.val); + } else { + hasChanged = oldObj.val !== obj.val; + } + if (!oldObj.lc || hasChanged) { + obj.lc = obj.ts; + } else { + obj.lc = oldObj.lc; + } + } + + // publish event in redis + log.silly(settings.namespace + ' redis publish ' + redisNamespace + id + ' ' + JSON.stringify(obj)); + client.publish(redisNamespace + id, JSON.stringify(obj)); + + + // set object in redis + if (expire) { + //console.log('setex',redisNamespace + id, expire, JSON.stringify(obj)); + client.setex(redisNamespace + id, expire, JSON.stringify(obj), function () { + if (typeof callback === 'function') { + callback(); + } + }); + } else { + //console.log('set',redisNamespace + id, JSON.stringify(obj)); + client.set(redisNamespace + id, JSON.stringify(obj), function () { + if (typeof callback === 'function') { + callback(); + } + }); + } + }); + }; + + // Used for restore function (do not call it) + this.setRawState = function (id, state, callback) { + //console.log('set',redisNamespace + id, JSON.stringify(obj)); + client.set(redisNamespace + id, state, function () { + if (typeof callback === 'function') { + callback(); + } + }); + }; + + /** + * @method getState + * + * @param {String} id + * @param callback + */ + this.getState = function (id, callback) { + client.get(redisNamespace + id, function (err, obj) { + if (err) { + log.warn(settings.namespace + ' redis get ' + id + ', error - ' + err); + } else { + log.silly(settings.namespace + ' redis get ' + id + ' ok: ' + obj); + } + if (typeof callback === 'function') { + callback(err, obj ? JSON.parse(obj) : null); + } + }); + }; + + this.getStates = function (keys, callback, dontModify) { + if (!keys) { + if (callback) callback('no keys', null); + return; + } + if (!keys.length) { + if (callback) callback(null, []); + return; + } + let _keys; + if (!dontModify) { + _keys = []; + for (let i = 0; i < keys.length; i++) { + _keys[i] = redisNamespace + keys[i]; + } + } else { + _keys = keys; + } + client.mget(_keys, function (err, obj) { + if (err) { + log.warn(settings.namespace + ' redis mget ' + ((!obj) ? 0 : obj.length) + ' ' + _keys.length + ', err: ' + err); + } else { + log.silly(settings.namespace + ' redis mget ' + ((!obj) ? 0 : obj.length) + ' ' + _keys.length); + } + if (typeof callback === 'function') callback(err, obj); + }); + }; + + // Destructor of the class. Called by shutting down. + this.destroy = function () { + if (client) { + client.end(true); + client = null; + } + if (sub) { + sub.end(); + sub = null; + } + }; + + this.delState = function (id, callback) { + client.del(redisNamespace + id, function (err) { + if (err) { + log.warn(settings.namespace + ' redis del ' + id + ', error - ' + err); + } else { + client.publish(redisNamespace + id, 'null'); + log.silly(settings.namespace + ' redis del ' + id + ', ok'); + } + if (typeof callback === 'function') callback(err); + }); + }; + + this.getKeys = function (pattern, callback, dontModify) { + client.keys(redisNamespace + pattern, function (err, obj) { + log.silly(settings.namespace + ' redis keys ' + obj.length + ' ' + pattern); + if (typeof callback === 'function') { + if (obj && !dontModify) { + const len = redisNamespace.length; + for (let i = 0; i < obj.length; i++) { + obj[i] = obj[i].substring(len); + } + } + callback(err, obj); + } + }); + }; + /** + * @method subscribe + * + * @param pattern + * @param {function} callback callback function (optional) + */ + this.subscribe = function (pattern, callback) { + log.silly(settings.namespace + ' redis psubscribe ' + redisNamespace + pattern); + sub.psubscribe(redisNamespace + pattern, function (err) { + if (typeof callback === 'function') callback(err); + }); + }; + + this.unsubscribe = function (pattern, callback) { + log.silly(settings.namespace + ' redis punsubscribe ' + redisNamespace + pattern); + sub.punsubscribe(redisNamespace + pattern, function (err) { + if (typeof callback === 'function') callback(err); + }); + }; + + this.pushMessage = function (id, state, callback) { + state._id = globalMessageId++; + if (globalMessageId >= 0xFFFFFFFF) globalMessageId = 0; + client.publish(namespaceMsg + id, JSON.stringify(state)); + if (typeof callback === 'function') callback(null, id); + }; + + // todo: delete it + this.lenMessage = function (id, callback) { + if (typeof callback === 'function') callback(null, 0, id); + }; + + // todo: delete it + this.getMessage = function (id, callback) { + if (typeof callback === 'function') callback(null, null, id); + }; + + // todo: delete it + this.delMessage = function (id, messageId, callback) { + if (typeof callback === 'function') callback(null, id); + }; + + // todo: delete it + this.clearAllMessages = function (callback) { + client.keys(namespaceLog + '*', function (err, obj) { + if (obj) { + for (let i = 0; i < obj.length; i++) { + log.silly('redis clear message for ' + obj[i]); + client.del(obj[i]); + } + } + + if (typeof callback === 'function') callback(err); + }); + }; + + this.subscribeMessage = function (id, callback) { + if (id && id[0] === '.') id = id.substring(1); + log.silly('redis subscribeMessage ' + namespaceMsg + id); + sub.psubscribe(namespaceMsg + id, function (err) { + if (typeof callback === 'function') callback(err); + }); + }; + + this.unsubscribeMessage = function (id, callback) { + if (id && id[0] === '.') id = id.substring(1); + log.silly('redis unsubscribeMessage ' + namespaceMsg + id); + sub.punsubscribe(namespaceMsg + id, function (err) { + if (typeof callback === 'function') callback(err); + }); + }; + + this.pushLog = function (id, log, callback) { + log._id = globalLogId++; + if (globalLogId >= 0xFFFFFFFF) globalLogId = 0; + client.publish(namespaceLog + id, JSON.stringify(log)); + if (typeof callback === 'function') callback(null, id); + }; + + // todo: delete it + this.lenLog = function (id, callback) { + if (typeof callback === 'function') callback('Not exists', 0, id); + // client.llen(namespaceLog + id, function (err, obj) { + // if (typeof callback === 'function') callback(err, obj, id); + // }); + }; + + // todo: delete it + this.getLog = function (id, callback) { + if (typeof callback === 'function') { + callback('Not exists', null, 0); + } + }; + + // todo: delete it + this.delLog = function (id, logId, callback) { + if (typeof callback === 'function') { + callback('Not exists'); + } + }; + + // todo: delete it + this.clearAllLogs = function (callback) { + client.keys(namespaceLog + '*', function (err, obj) { + if (obj) { + for (let i = 0; i < obj.length; i++) { + log.silly(settings.namespace + ' redis clear log for ' + obj[i]); + client.del(obj[i]); + } + } + + if (typeof callback === 'function') { + callback(err); + } + }); + }; + + this.subscribeLog = function (id, callback) { + log.silly(settings.namespace + ' redis subscribeMessage ' + namespaceLog + id); + sub.psubscribe(namespaceLog + id, function (err) { + if (typeof callback === 'function') callback(err); + }); + }; + + this.unsubscribeLog = function (id, callback) { + log.silly(settings.namespace + ' redis unsubscribeMessage ' + namespaceLog + id); + sub.punsubscribe(namespaceLog + id, function (err) { + if (typeof callback === 'function') callback(err); + }); + }; + + this.getSession = function (id, callback) { + client.get(namespaceSession + id, function (err, obj) { + log.silly(settings.namespace + ' redis get ' + id + ' ' + obj); + if (typeof callback === 'function') callback(obj ? JSON.parse(obj) : null); + }); + }; + + this.setSession = function (id, expire, obj, callback) { + client.setex(namespaceSession + id, expire, JSON.stringify(obj), function () { + log.silly(settings.namespace + ' redis setex', id, expire, obj); + if (typeof callback === 'function') callback(); + }); + }; + + this.destroySession = function (id, callback) { + id = namespaceSession + id; + log.silly(settings.namespace + ' redis del ' + id); + client.del(id, function () { + if (typeof callback === 'function') callback(); + }); + }; + + /* this.getConfig = function (id, callback) { + id = namespaceConfig + id; + client.get(id, function (err, obj) { + log.silly(settings.namespace + ' redis get ' + id + ' ' + obj); + if (typeof callback === 'function') callback(err, obj ? JSON.parse(obj) : null); + }); + }; + + this.getConfigKeys = function (pattern, callback, dontModify) { + client.keys(namespaceConfig + pattern, function (err, obj) { + log.silly(settings.namespace + ' redis config keys ' + obj.length + ' ' + pattern); + if (typeof callback === 'function') { + if (obj && !dontModify) { + var len = redisNamespace.length; + for (var i = 0; i < obj.length; i++) { + obj[i] = obj[i].substring(len); + } + } + callback(err, obj); + } + }); + }; + + this.getConfigs = function (keys, callback, dontModify) { + if (!keys) { + if (callback) callback('no keys', null); + return; + } + if (!keys.length) { + if (callback) callback(null, []); + return; + } + var _keys; + if (!dontModify) { + _keys = []; + for (var i = 0; i < keys.length; i++) { + _keys[i] = namespaceConfig + keys[i]; + } + } else { + _keys = keys; + } + + client.mget(_keys, function (err, obj) { + if (err) { + log.warn(settings.namespace + ' redis mget ' + ((!obj) ? 0 : obj.length) + ' ' + _keys.length + ', err: ' + err); + } else { + log.silly(settings.namespace + ' redis mget ' + ((!obj) ? 0 : obj.length) + ' ' + _keys.length); + } + if (typeof callback === 'function') callback(err, obj); + }); + }; + + this.setConfig = function (id, obj, callback) { + id = namespaceConfig + id; + client.set(id, JSON.stringify(obj), function (err) { + log.silly(settings.namespace + ' redis set', id, obj); + client.publish(id, JSON.stringify(obj)); + if (typeof callback === 'function') callback(err, {id: id}); + }); + }; + + this.delConfig = function (id, callback) { + id = namespaceConfig + id; + log.silly(settings.namespace + ' redis del ' + id); + client.del(id, function (err) { + client.publish(id, null); + if (typeof callback === 'function') callback(err); + }); + }; + + this.subscribeConfig = function (id, callback) { + log.silly(settings.namespace + ' redis subscribeConfig ' + namespaceConfig + id); + sub.psubscribe(namespaceConfig + id, function (err) { + if (typeof callback === 'function') callback(err); + }); + }; + + this.unsubscribeConfig = function (id, callback) { + log.silly(settings.namespace + ' redis unsubscribeConfig ' + namespaceConfig + id); + sub.punsubscribe(namespaceConfig + id, function (err) { + if (typeof callback === 'function') callback(err); + }); + };*/ + + function _createBinaryClient() { + if (!clientBin) { + settings.connection.options = settings.connection.options || {}; + let opt = JSON.parse(JSON.stringify(settings.connection.options)); + opt.return_buffers = true; + if (settings.connection.port === 0) { + // initiate a unix socket connection using the parameter 'host' + clientBin = redis.createClient(settings.connection.host, opt); + } else { + clientBin = redis.createClient(settings.connection.port, settings.connection.host, opt); + } + } + } + + this.setBinaryState = function (id, data, callback) { + if (!clientBin) _createBinaryClient (); + clientBin.set(id, data, callback); + }; + + this.getBinaryState = function (id, callback) { + if (!clientBin) _createBinaryClient (); + clientBin.get(id, function (err, data) { + if (!err && data) { + if (callback) callback(err, new Buffer(data, 'binary')); + } else { + if (callback) callback(err); + + } + }); + }; + + this.delBinaryState = function (id, callback) { + if (!clientBin) _createBinaryClient (); + clientBin.del(id, function () { + if (typeof callback === 'function') callback(); + }); + }; + (function __construct() { + if (settings.connection.port === 0) { + // initiate a unix socket connection using the parameter 'host' + client = redis.createClient(settings.connection.host, settings.connection.options); + sub = redis.createClient(settings.connection.host, settings.connection.options); + } else { + client = redis.createClient(settings.connection.port, settings.connection.host, settings.connection.options); + sub = redis.createClient(settings.connection.port, settings.connection.host, settings.connection.options); + } + + if (typeof onChange === 'function') { + sub.on('pmessage', function (pattern, channel, message) { + log.debug(settings.namespace + ' redis pmessage ', pattern, channel, message); + try { + if (ioRegExp.test(channel)) { + onChange(channel.slice(redisNamespace.length), message ? JSON.parse(message) : null); + } else { + onChange(channel, message ? JSON.parse(message) : null); + } + } catch (e) { + log.error(settings.namespace + ' pmessage ' + channel + ' ' + message + ' ' + e.message); + log.error(settings.namespace + ' ' + e.stack); + } + }); + } + + client.on('error', error => { + if (typeof settings.disconnected === 'function') { + settings.disconnected(error); + } else { + log.error(settings.namespace + ' ' + error.message); + log.error(settings.namespace + ' ' + error.stack); + } + }); + + sub.on('error', error => { + log.error(settings.namespace + ' No redis connection!'); + }); + + sub.on('connect', error => { + if (settings.connection.port === 0) { + log.info(settings.namespace + ' States connected to redis: ' + settings.connection.host); + } else { + log.info(settings.namespace + ' States connected to redis: ' + settings.connection.host + ':' + settings.connection.port); + } + }); + + client.on('connect', error => { + if (typeof settings.connected === 'function') settings.connected(); + }); + })(); + + return this; +} + +module.exports = StateRedis; diff --git a/lib/tools.js b/lib/tools.js new file mode 100644 index 0000000..9f9f69c --- /dev/null +++ b/lib/tools.js @@ -0,0 +1,1203 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const semver = require('semver'); +require('events').EventEmitter.prototype._maxListeners = 100; +let request; +let extend; +let password; +let npmVersion; +let crypto; + +/** + * recursively copy values from old object to new one + * + * @alias copyAttributes + * @memberof tools + * @param {object} oldObj source object + * @param {object} newObj destination object + * @param {object} originalObj optional object for read __no_change__ values + * @param {boolean} isNonEdit optional indicator if copy is in nonEdit part + * + */ +function copyAttributes(oldObj, newObj, originalObj, isNonEdit) { + for (let attr in oldObj) { + if (oldObj.hasOwnProperty(attr)) { + if (typeof oldObj[attr] !== 'object' || oldObj[attr] instanceof Array) { + if (oldObj[attr] === '__no_change__' && originalObj && !isNonEdit) { + newObj[attr] = JSON.parse(JSON.stringify(originalObj[attr])); + } else + if (oldObj[attr] === '__delete__' && !isNonEdit) { + if (newObj[attr] !== undefined) { + delete newObj[attr]; + } + } else { + newObj[attr] = oldObj[attr]; + } + } else { + newObj[attr] = newObj[attr] || {}; + copyAttributes(oldObj[attr], newObj[attr], originalObj && originalObj[attr], isNonEdit || attr === 'nonEdit'); + } + } + } +} + +/** + * Checks the flag nonEdit and restores non-changeable values if required + * + * @alias checkNonEditable + * @memberof tools + * @param {object} oldObject source object + * @param {object} newObject destination object + * + */ +function checkNonEditable(oldObject, newObject) { + if (!oldObject) return true; + if (!oldObject.nonEdit && !newObject.nonEdit) return true; + + // if nonEdit is protected with password + if (oldObject.nonEdit && oldObject.nonEdit.passHash) { + // If new Object wants to update the nonEdit information + if (newObject.nonEdit && newObject.nonEdit.password) { + crypto = crypto || require('crypto'); + const hash = crypto.createHash('sha256').update(newObject.nonEdit.password).digest('base64'); + if (oldObject.nonEdit.passHash !== hash) { + delete newObject.nonEdit; + return false; + } else { + oldObject.nonEdit = JSON.parse(JSON.stringify(newObject.nonEdit)); + delete oldObject.nonEdit.password; + delete newObject.nonEdit.password; + oldObject.nonEdit.passHash = hash; + newObject.nonEdit.passHash = hash; + } + copyAttributes(newObject.nonEdit, newObject, newObject); + return true; + } else if (newObject.nonEdit !== undefined) { + delete newObject.nonEdit; + } + } else if (newObject.nonEdit) { + oldObject.nonEdit = JSON.parse(JSON.stringify(newObject.nonEdit)); + if (newObject.nonEdit.password) { + crypto = crypto || require('crypto'); + const hash = crypto.createHash('sha256').update(newObject.nonEdit.password).digest('base64'); + delete oldObject.nonEdit.password; + delete newObject.nonEdit.password; + oldObject.nonEdit.passHash = hash; + newObject.nonEdit.passHash = hash; + } + } + + // restore settings + copyAttributes(oldObject.nonEdit, newObject, oldObject); + + if (newObject.passHash) delete newObject.passHash; + if (newObject.nonEdit && newObject.nonEdit.password) delete newObject.nonEdit.password; + return true; +} + +// Compare versions +function upToDate(online, installed) { + online = online.split('.'); + installed = installed.split('.'); + online[0] = parseInt(online[0], 10); + installed[0] = parseInt(installed[0], 10); + if (online[0] > installed[0]) { + return false; + } else if (online[0] === installed[0]) { + online[1] = parseInt(online[1], 10); + installed[1] = parseInt(installed[1], 10); + + if (online[1] > installed[1]) { + return false; + } else if (online[1] === installed[1]) { + online[2] = parseInt(online[2], 10); + installed[2] = parseInt(installed[2], 10); + return installed[2] >= online[2]; + } else { + return true; + } + } else { + return true; + } +} + +function encryptPhrase(password, phrase, callback) { + // encrypt secret + crypto = crypto || require('crypto'); + const cipher = crypto.createCipher('aes192', password); + + let encrypted = ''; + cipher.on('readable', function () { + const data = cipher.read(); + if (data) { + encrypted += data.toString('hex'); + } + }); + cipher.on('end', function () { + callback(encrypted); + }); + + cipher.write(phrase); + cipher.end(); +} + +function decryptPhrase(password, data, callback) { + crypto = crypto || require('crypto'); + const decipher = crypto.createDecipher('aes192', password); + + try { + let decrypted = ''; + decipher.on('readable', function () { + let data = decipher.read(); + if (data) { + decrypted += data.toString('utf8'); + } + }); + decipher.on('error', function (error) { + console.error('Cannot decode secret: ' + error); + callback(null); + }); + decipher.on('end', function () { + callback(decrypted); + }); + + decipher.write(data, 'hex'); + decipher.end(); + } catch (e) { + console.error('Cannot decode secret: ' + e); + callback(null); + } +} + +function getAppName() { + const parts = __dirname.replace(/\\/g, '/').split('/'); + return parts[parts.length - 2].split('.')[0]; +} + +function rmdirRecursiveSync(path) { + if (fs.existsSync(path)) { + fs.readdirSync(path).forEach(function (file/*, index*/) { + const curPath = path + '/' + file; + if (fs.statSync(curPath).isDirectory()) { + // recurse + rmdirRecursiveSync(curPath); + } else { + // delete file + fs.unlinkSync(curPath); + } + }); + // delete (hopefully) empty folder + try { + fs.rmdirSync(path); + } catch (e) { + console.log('Cannot delete directory ' + path + ': ' + e.toString()); + } + } +} + +function findIPs() { + const ifaces = require('os').networkInterfaces(); + let ipArr = []; + for (let dev in ifaces) { + if (!ifaces.hasOwnProperty(dev)) continue; + /*jshint loopfunc:true */ + ifaces[dev].forEach(function (details) { + //noinspection JSUnresolvedVariable + if (!details.internal) ipArr.push(details.address); + }); + } + return ipArr; +} + +function findPath(path, url) { + if (!url) return ''; + if (url.substring(0, 'http://'.length) === 'http://' || + url.substring(0, 'https://'.length) === 'https://') { + return url; + } else { + if (path.substring(0, 'http://'.length) === 'http://' || + path.substring(0, 'https://'.length) === 'https://') { + return (path + url).replace(/\/\//g, '/').replace('http:/', 'http://').replace('https:/', 'https://'); + } else { + if (url && url[0] === '/') { + return __dirname + '/..' + url; + } else { + return __dirname + '/../' + path + url; + } + } + } +} + +function getMac(callback) { + const macRegex = /(?:[a-z0-9]{2}[:\-]){5}[a-z0-9]{2}/ig; + const zeroRegex = /(?:[0]{2}[:\-]){5}[0]{2}/; + const command = (process.platform.indexOf('win') === 0) ? 'getmac' : 'ifconfig || ip link'; + + require('child_process').exec(command, function(err, stdout, stderr) { + if (err) { + callback(err); + } else { + let macAddress; + let match; + let result = null; + + while (match = macRegex.exec(stdout)) { + macAddress = match[0]; + if (!zeroRegex.test(macAddress) && !result) result = macAddress; + } + + if (result === null) { + callback(new Error('could not determine the mac address from:\n' + stdout)); + } else { + callback(null, result.replace(/-/g, ':').toLowerCase()); + } + } + }); +} + +// Build unique uuid based on MAC address if possible +function uuid(givenMac, callback) { + if (typeof givenMac === 'function') { + callback = givenMac; + givenMac = ''; + } + + let mac = (givenMac !== null) ? (givenMac || '') : null; + let u; + + if (mac === '') { + const ifaces = require('os').networkInterfaces(); + + // Find first not empty MAC + for (let n in ifaces) { + if (!ifaces.hasOwnProperty(n)) continue; + for (let c = 0; c < ifaces[n].length; c++) { + if (ifaces[n][c].mac && ifaces[n][c].mac !== '00:00:00:00:00:00') { + mac = ifaces[n][c].mac; + break; + } + } + if (mac) break; + } + } + + if (mac === '') { + getMac(function(err, mac) { + uuid(mac || null, callback); + }); + return; + } + + if (mac) { + const md5sum = require('crypto').createHash('md5'); + md5sum.update(mac); + mac = md5sum.digest('hex'); + u = mac.substring(0, 8) + '-' + mac.substring(8, 12) + '-' + mac.substring(12, 16) + '-' + mac.substring(16, 20) + '-' + mac.substring(20); + } else { + // Returns a RFC4122 compliant v4 UUID https://gist.github.com/LeverOne/1308368 (DO WTF YOU WANT TO PUBLIC LICENSE) + let a; + let b; + b = a = ''; + while (a++ < 36) { + b += ((a * 51) & 52) ? (a ^ 15 ? 8 ^ Math.random() * (a ^ 20 ? 16 : 4) : 4).toString(16) : '-'; + } + u = b; + } + + // todo => delete it after the vendor works 2018.03.28 BF + if (fs.existsSync('/opt/io-box/config/setup.cfg')) { + u = 'CM' + u; + } + + callback(u); +} + +function createUuid(_objects, callback) { + let tasks = 2; + let _uuid; + _objects.getObject('system.user.admin', function (err, obj) { + if (err || !obj) { + password = password || require(__dirname + '/password'); + + // Default Password for user 'admin' is application name in lower case + password(getAppName()).hash(null, null, function (err, res) { + if (err) console.error(err); + // Create user here and not in io-package.js because of hash password + let tasks = 0; + + tasks++; + _objects.setObject('system.user.admin', { + type: 'user', + common: { + name: 'admin', + password: res, + dontDelete: true, + enabled: true + }, + ts: new Date().getTime(), + from: 'system.host.' + getHostName() + '.tools', + native: {} + }, function () { + console.log('object system.user.admin created'); + if (!--tasks && callback) callback(_uuid); + }); + }); + } else { + if (!--tasks && callback) callback(_uuid); + } + }); + + _objects.getObject('system.meta.uuid', function (err, obj) { + if (!err && obj && obj.native && obj.native.uuid) { + if (!--tasks && callback) callback(); + } else { + uuid(function (res) { + _uuid = res; + _objects.setObject('system.meta.uuid', { + type: 'meta', + common: { + name: 'uuid', + type: 'uuid' + }, + ts: new Date().getTime(), + from: 'system.host.' + getHostName() + '.tools', + native: { + uuid: res + } + }, function () { + console.log('object system.meta.uuid created: ' + res); + if (!--tasks && callback) callback(_uuid); + }); + }); + } + }); +} + +// Download file to tmp or return file name directly +function getFile(urlOrPath, fileName, callback) { + if (!request) request = require('request'); + + // If object was read + if (urlOrPath.substring(0, 'http://'.length) === 'http://' || + urlOrPath.substring(0, 'https://'.length) === 'https://') { + const tmpFile = __dirname + '/../tmp/' + (fileName || Math.floor(Math.random() * 0xFFFFFFE) + '.zip'); + request(urlOrPath).on('error', function (error) { + console.log('Cannot download "' + tmpFile + '": ' + error); + if (callback) callback(tmpFile); + }).pipe(fs.createWriteStream(tmpFile)).on('close', function () { + console.log('downloaded ' + tmpFile); + if (callback) callback(tmpFile); + }); + } else { + if (fs.existsSync(urlOrPath)) { + if (callback) callback(urlOrPath); + } else + if (fs.existsSync(__dirname + '/../' + urlOrPath)) { + if (callback) callback(__dirname + '/../' + urlOrPath); + } else if (fs.existsSync(__dirname + '/../tmp/' + urlOrPath)) { + if (callback) callback(__dirname + '/../tmp/' + urlOrPath); + } else { + console.log('File not found: ' + urlOrPath); + process.exit(1); + } + } +} + +// Return content of the json file. Download it or read directly +function getJson(urlOrPath, callback) { + if (!request) request = require('request'); + let sources = {}; + // If object was read + if (urlOrPath && typeof urlOrPath === 'object') { + if (callback) callback(urlOrPath); + } else + if (!urlOrPath) { + console.log('Empty url!'); + if (callback) callback(null); + } else { + if (urlOrPath.substring(0, 'http://'.length) === 'http://' || + urlOrPath.substring(0, 'https://'.length) === 'https://') { + request({url: urlOrPath, timeout: 10000}, function (error, response, body) { + if (error || !body || response.statusCode !== 200) { + console.log('Cannot download json from ' + urlOrPath + '. Error: ' + (error || body)); + if (callback) callback(null, urlOrPath); + return; + } + try { + sources = JSON.parse(body); + } catch (e) { + console.log('Json file is invalid on ' + urlOrPath); + if (callback) callback(null, urlOrPath); + return; + } + + if (callback) callback(sources, urlOrPath); + }).on('error', function (error) { + //console.log('Cannot download json from ' + urlOrPath + '. Error: ' + error); + //if (callback) callback(null, urlOrPath); + }); + } else { + if (fs.existsSync(urlOrPath)) { + try { + sources = JSON.parse(fs.readFileSync(urlOrPath, 'utf8')); + } catch (e) { + console.log('Cannot parse json file from ' + urlOrPath + '. Error: ' + e); + if (callback) callback(null, urlOrPath); + return; + } + if (callback) callback(sources, urlOrPath); + } else + if (fs.existsSync(__dirname + '/../' + urlOrPath)) { + try { + sources = JSON.parse(fs.readFileSync(__dirname + '/../' + urlOrPath, 'utf8')); + }catch (e) { + console.log('Cannot parse json file from ' + __dirname + '/../' + urlOrPath + '. Error: ' + e); + if (callback) callback(null, urlOrPath); + return; + } + if (callback) callback(sources, urlOrPath); + } else if (fs.existsSync(__dirname + '/../tmp/' + urlOrPath)) { + try { + sources = JSON.parse(fs.readFileSync(__dirname + '/../tmp/' + urlOrPath, 'utf8')); + } catch (e) { + console.log('Cannot parse json file from ' + __dirname + '/../tmp/' + urlOrPath + '. Error: ' + e); + if (callback) callback(null, urlOrPath); + return; + } + if (callback) callback(sources, urlOrPath); + } else { + //if (urlOrPath.indexOf('/example/') === -1) console.log('Json file not found: ' + urlOrPath); + if (callback) callback(null, urlOrPath); + } + } + } +} + +function scanDirectory(dirName, list, regExp) { + if (fs.existsSync(dirName)) { + const dirs = fs.readdirSync(dirName); + for (let i = 0; i < dirs.length; i++) { + try { + const fullPath = path.join(dirName, dirs[i]); + const fileIoName = path.join(fullPath, 'io-package.json'); + const fileName = path.join(fullPath, 'package.json'); + if (regExp.test(dirs[i]) && fs.existsSync(fileIoName)) { + const ioPackage = JSON.parse(fs.readFileSync(fileIoName, 'utf8')); + const package_ = fs.existsSync(fileName) ? JSON.parse(fs.readFileSync(fileName, 'utf8')) : {}; + + //noinspection JSUnresolvedVariable + list[ioPackage.common.name] = { + controller: ioPackage.common.controller || false, + version: ioPackage.common.version, + icon: ioPackage.common.extIcon || (ioPackage.common.icon ? '/adapter/' + dirs[i] + '/' + ioPackage.common.icon : ''), + title: ioPackage.common.title, + desc: ioPackage.common.desc, + platform: ioPackage.common.platform, + keywords: ioPackage.common.keywords, + readme: ioPackage.common.readme, + type: ioPackage.common.type, + license: ioPackage.common.license ? ioPackage.common.license : ((package_.licenses && package_.licenses.length) ? package_.licenses[0].type : ''), + licenseUrl: (package_.licenses && package_.licenses.length) ? package_.licenses[0].url : '' + }; + } + } catch (e) { + console.log('Cannot read or parse ' + __dirname + '/../node_modules/' + dirs[i] + '/io-package.json: ' + e.toString()); + } + } + } +} +// Get list of all installed adapters and controller version on this host +function getInstalledInfo(hostRunningVersion) { + let result = {}; + const path = __dirname + '/../'; + // Get info about host + const ioPackage = JSON.parse(fs.readFileSync(path + 'io-package.json', 'utf8')); + const package_ = fs.existsSync(path + 'package.json') ? JSON.parse(fs.readFileSync(path + 'package.json', 'utf8')) : {}; + const regExp = new RegExp('^' + module.exports.appName + '\\.', 'i'); + + //noinspection JSUnresolvedVariable + result[ioPackage.common.name] = { + controller: true, + version: ioPackage.common.version, + icon: ioPackage.common.extIcon || ioPackage.common.icon, + title: ioPackage.common.title, + desc: ioPackage.common.desc, + platform: ioPackage.common.platform, + keywords: ioPackage.common.keywords, + readme: ioPackage.common.readme, + runningVersion: hostRunningVersion, + license: ioPackage.common.license ? ioPackage.common.license : ((package_.licenses && package_.licenses.length) ? package_.licenses[0].type : ''), + licenseUrl: (package_.licenses && package_.licenses.length) ? package_.licenses[0].url : '' + }; + scanDirectory(__dirname + '/../node_modules', result, regExp); + scanDirectory(__dirname + '/../../node_modules', result, regExp); + + if (fs.existsSync(__dirname + '/../../../node_modules/' + module.exports.appName.toLowerCase() + '.js-controller') || + fs.existsSync(__dirname + '/../../../node_modules/' + module.exports.appName + '.js-controller')) { + scanDirectory(__dirname + '/../..', result, regExp); + } + return result; +} + +/** + * Reads an adapter's npm version + * @param {string | null} adapter The adapter to read the npm version from. Null for the root yunkong2 packet + * @param {(err: Error | null, version: string) => void} [callback] + */ +function getNpmVersion(adapter, callback) { + adapter = adapter ? module.exports.appName + '.' + adapter : module.exports.appName; + adapter = adapter.toLowerCase(); + + const cliCommand = `npm view ${adapter}@latest version`; + + const exec = require('child_process').exec; + exec(cliCommand, {timeout: 2000}, (error, stdout, stderr) => { + let version; + if (error) { + // command failed + if (typeof callback === 'function') { + callback(error); + return; + } + } else if (stdout) { + version = semver.valid(stdout.trim()); + } + if (typeof callback === 'function') callback(null, version); + }); +} + +function getIoPack(sources, name, callback) { + getJson(sources[name].meta, function (ioPack) { + const packUrl = sources[name].meta.replace('io-package.json', 'package.json'); + if (!ioPack) { + if (sources._helper) sources._helper.failCounter.push(name); + if (callback) callback(sources, name); + } else { + setImmediate(function () { + getJson(packUrl, function (pack) { + const version = sources[name].version; + const type = sources[name].type; + // If installed from git or something else + // js-controller is exception, because can be installed from npm and from git + if (sources[name].url && name !== 'js-controller') { + if (ioPack && ioPack.common) { + sources[name] = extend(true, sources[name], ioPack.common); + + // overwrite type of adapter from repository + if (type) { + sources[name].type = type; + } + if (pack && pack.licenses && pack.licenses.length) { + if (!sources[name].license) sources[name].license = pack.licenses[0].type; + if (!sources[name].licenseUrl) sources[name].licenseUrl = pack.licenses[0].url; + } + } + + if (callback) callback(sources, name); + } else { + if (ioPack && ioPack.common) { + sources[name] = extend(true, sources[name], ioPack.common); + if (pack && pack.licenses && pack.licenses.length) { + if (!sources[name].license) sources[name].license = pack.licenses[0].type; + if (!sources[name].licenseUrl) sources[name].licenseUrl = pack.licenses[0].url; + } + } + + // overwrite type of adapter from repository + if (type) { + sources[name].type = type; + } + + if (version) { + sources[name].version = version; + if (callback) callback(sources, name); + } else { + if (sources[name].meta.substring(0, 'http://'.length) === 'http://' || + sources[name].meta.substring(0, 'https://'.length) === 'https://') { + //installed from npm + getNpmVersion(name, function (err, version) { + if (version) { + sources[name].version = version; + } else { + sources[name].version = 'npm error'; + } + if (callback) callback(sources, name); + }); + } else { + if (callback) callback(sources, name); + } + } + } + }); + }); + } + }); +} + +function _getRepositoryFile(sources, path, callback) { + if (!sources._helper) { + let count = 0; + for (let _name in sources) { + if (!sources.hasOwnProperty(_name)) continue; + count++; + } + sources._helper = {failCounter: []}; + + sources._helper.timeout = setTimeout(function () { + if (sources._helper) { + delete sources._helper; + for (let __name in sources) { + if (!sources.hasOwnProperty(__name)) continue; + if (sources[__name].processed !== undefined) delete sources[__name].processed; + } + if (callback) callback('Timeout by read all package.json (' + count + ') seconds', sources); + callback = null; + } + }, count * 1000); + } + + for (let name in sources) { + if (!sources.hasOwnProperty(name)) continue; + if (sources[name].processed || name === '_helper') continue; + + sources[name].processed = true; + if (sources[name].url) sources[name].url = findPath(path, sources[name].url); + if (sources[name].meta) sources[name].meta = findPath(path, sources[name].meta); + if (sources[name].icon) sources[name].icon = findPath(path, sources[name].icon); + + if (!sources[name].name && sources[name].meta) { + getIoPack(sources, name, function (ignore/*, name*/) { + if (sources._helper) { + if (sources._helper.failCounter.length > 10) { + clearTimeout(sources._helper.timeout); + delete sources._helper; + for (let _name in sources) { + if (!sources.hasOwnProperty(_name)) continue; + if (sources[_name].processed !== undefined) delete sources[_name].processed; + } + if (callback) callback('Looks like there is no internet.', sources); + callback = null; + } else { + // process next + setImmediate(function () { + _getRepositoryFile(sources, path, callback); + }); + } + } + }); + return; + } + } + // all packages are processed + if (sources._helper) { + let err; + if (sources._helper.failCounter.length) { + err = 'Following packages cannot be read: ' + sources._helper.failCounter.join(', '); + } + clearTimeout(sources._helper.timeout); + delete sources._helper; + for (let __name in sources) { + if (!sources.hasOwnProperty(__name)) continue; + if (sources[__name].processed !== undefined) delete sources[__name].processed; + } + if (callback) callback(err, sources); + callback = null; + } +} + +// Get list of all adapters and controller in some repository file or in /conf/source-dist.json +function getRepositoryFile(urlOrPath, additionalInfo, callback) { + let sources = {}; + let path = ''; + + if (typeof additionalInfo === 'function') { + callback = additionalInfo; + additionalInfo = {}; + } + if (!additionalInfo) additionalInfo = {}; + + if (!extend) extend = require('node.extend'); + + if (urlOrPath) { + let parts = urlOrPath.split('/'); + path = parts.splice(0, parts.length - 1).join('/') + '/'; + } + + // If object was read + if (urlOrPath && typeof urlOrPath === 'object') { + if (typeof callback === 'function') callback(null, urlOrPath); + } else + if (!urlOrPath) { + try { + sources = JSON.parse(fs.readFileSync(getDefaultDataDir() + 'sources.json', 'utf8')); + } catch (e) { + sources = {}; + } + try { + const sourcesDist = JSON.parse(fs.readFileSync(__dirname + '/../conf/sources-dist.json', 'utf8')); + sources = extend(true, sourcesDist, sources); + } catch (e) { + + } + + for (let s in sources) { + if (sources.hasOwnProperty(s) && additionalInfo[s] && additionalInfo[s].published) { + sources[s].published = additionalInfo[s].published + } + } + + _getRepositoryFile(sources, path, function (err) { + if (err) console.error('[' + new Date() + '] ' + err); + if (typeof callback === 'function') callback(err, sources); + }); + } else { + getJson(urlOrPath, function (sources) { + if (sources) { + for (let s in sources) { + if (sources.hasOwnProperty(s) && additionalInfo[s] && additionalInfo[s].published) { + sources[s].published = additionalInfo[s].published + } + } + setImmediate(function () { + _getRepositoryFile(sources, path, function (err) { + if (err) console.error('[' + new Date() + '] ' + err); + if (typeof callback === 'function') callback(err, sources); + }); + }); + } else { + if (typeof callback === 'function') callback('Cannot read "' + urlOrPath + '"', {}); + } + }); + } +} + +function sendDiagInfo(obj, callback) { + if (!request) request = require('request'); + console.log('Send diag info: ' + JSON.stringify(obj)); + request.post({ + url: 'http://download.' + module.exports.appName + '.net/diag.php', + method: 'POST', + headers: {'content-type': 'application/x-www-form-urlencoded'}, + body: 'data=' + JSON.stringify(obj), + timeout: 2000 + }, function (err, response, body) { + /*if (err || !body || response.statusCode !== 200) { + + }*/ + if (typeof callback === 'function') callback(); + }).on('error', function (error) { + console.log('Cannot send diag info: ' + error.message); + if (typeof callback === 'function') callback(error); + }); +} + +function getAdapterDir(adapter) { + const appName = module.exports.appName; + + if (adapter.substring(0, appName.length + 1) === appName + '.') { + adapter = adapter.substring(appName.length + 1); + } + + let possibilities = [ + appName.toLowerCase() + '.' + adapter + '/package.json', + appName + '.' + adapter + '/package.json' + ]; + + /** @type {string} */ + let controllerPath; + for (let i = 0; i < possibilities.length; i++) { + // special case to not read adapters from js-controller/node_module/adapter adn check first in parent directory + if (fs.existsSync(__dirname + '/../../' + possibilities[i])) { + controllerPath = __dirname + '/../../' + possibilities[i]; + } else { + try { + controllerPath = require.resolve(possibilities[i]); + break; + } catch (e) { /* not found */ } + } + } + + if (!controllerPath) { + return null; // inactive + } else { + let parts = path.normalize(controllerPath).split(/[\\\/]/g); + parts.pop(); + return parts.join('/'); + } +} + +function getHostName() { + try { + const configName = getConfigFileName(); + const config = JSON.parse(fs.readFileSync(configName, 'utf8')); + return config.system ? config.system.hostname || require('os').hostname() : require('os').hostname(); + } catch (err) { + return require('os').hostname(); + } +} + +/** + * Read version of systen npm + * + * @alias getSystemNpmVersion + * @memberof Tools + * @param {function} callback return result + *

+ *            function (err, version) {
+ *              adapter.log.debug('NPM version is: ' + version);
+ *            }
+ *        
+ */ +function getSystemNpmVersion(callback) { + const exec = require('child_process').exec; + + // remove local node_modules\.bin dir from path + // or we potentially get a wrong npm version + let newEnv = Object.assign({}, process.env); + newEnv.PATH = (newEnv.PATH || newEnv.Path || newEnv.path) + .split(path.delimiter) + .filter(dir => { + dir = dir.toLowerCase(); + return !(dir.indexOf('yunkong2') > -1 && dir.indexOf(path.join('node_modules', '.bin')) > -1); + + }) + .join(path.delimiter); + + exec('npm -v', {encoding: 'utf8', env: newEnv}, function (error, stdout) {//, stderr) { + if (stdout) stdout = semver.valid(stdout.trim()); + if (callback) callback(error, stdout); + }); +} + +/** + * Collects information about host and available adapters + * + * Following info will be collected: + * - available adapters + * - node.js --version + * - npm --version + * + * @alias getHostInfo + * @memberof Tools + * @param {object} objects + * @param {function} callback return result + *

+ *            function (err, result) {
+ *              adapter.log.debug('Info about host: ' + JSON.stringify(result, null, 2);
+ *            }
+ *        
+ */ +function getHostInfo(objects, callback) { + const os = require('os'); + const cpus = os.cpus(); + let data = { + 'Platform': os.platform(), + 'Architecture': os.arch(), + 'CPUs': cpus.length, + 'Speed': cpus[0].speed, + 'Model': cpus[0].model, + 'RAM': os.totalmem(), + 'System uptime': Math.round(os.uptime()), + 'Node.js': process.version + }; + let task = 0; + task++; + objects.getObject('system.config', function (err, systemConfig) { + objects.getObject('system.repositories', function (err, repos) { + // Check if repositories exists + if (!err && repos && repos.native && repos.native.repositories) { + const repo = repos.native.repositories[systemConfig.common.activeRepo]; + if (repo && repo.json) { + let count = 0; + for (let a in repo.json) { + count++; + } + data['adapters count'] = count; + } + } + if (!--task) { + callback(err, data); + } + }); + }); + + if (!npmVersion) { + task++; + getSystemNpmVersion(function (err, version) { + data['NPM'] = 'v' + version; + npmVersion = version; + if (!--task) { + callback(err, data); + } + + }); + } else { + data['NPM'] = npmVersion; + if (!task) { + callback(null, data); + } + } +} + +// All pathes are returned always relative to /node_modules/' + module.exports.appName + '.js-controller +// the result has always "/" as last symbol +function getDefaultDataDir() { + //var dataDir = __dirname.replace(/\\/g, '/'); + //dataDir = dataDir.split('/'); + + // if debugging with npm5 + if (fs.existsSync(__dirname + '/../../node_modules/' + module.exports.appName + '.js-controller')) { + return '../' + module.exports.appName + '-data/'; + } else // If installed with npm + if (fs.existsSync(__dirname + '/../../../node_modules/' + module.exports.appName + '.js-controller')) { + return '../../' + module.exports.appName + '-data/'; + } else + { + //dataDir.splice(dataDir.length - 1, 1); + //dataDir = dataDir.join('/'); + return './data/'; + } +} + +function getConfigFileName() { + /** @type {string|string[]} */ + let configDir = __dirname.replace(/\\/g, '/'); + configDir = configDir.split('/'); + + // If installed with npm + if (fs.existsSync(__dirname + '/../../../node_modules/' + module.exports.appName.toLowerCase() + '.js-controller') || + fs.existsSync(__dirname + '/../../../node_modules/' + module.exports.appName + '.js-controller')) { + // remove /node_modules/' + module.exports.appName + '.js-controller/lib + configDir.splice(configDir.length - 3, 3); + configDir = configDir.join('/'); + return configDir + '/' + module.exports.appName + '-data/' + module.exports.appName + '.json'; + } else + // if debugging with npm5 + if (fs.existsSync(__dirname + '/../../node_modules/' + module.exports.appName.toLowerCase() + '.js-controller') || + fs.existsSync(__dirname + '/../../node_modules/' + module.exports.appName + '.js-controller')) { + // remove /node_modules/' + module.exports.appName + '.js-controller/lib + configDir.splice(configDir.length - 2, 2); + configDir = configDir.join('/'); + return configDir + '/' + module.exports.appName + '-data/' + module.exports.appName + '.json'; + } else { + // Remove /lib + configDir.splice(configDir.length - 1, 1); + configDir = configDir.join('/'); + if (fs.existsSync(__dirname + '/../conf/' + module.exports.appName + '.json')) { + return configDir + '/conf/' + module.exports.appName + '.json'; + } else { + return configDir + '/data/' + module.exports.appName + '.json'; + } + } +} + +/** + * Puts all values from an `arguments` object into an array, starting at the given index + * @param {IArguments} argsObj An `arguments` object as passed to a function + * @param {number} [startIndex=0] The optional index to start taking the arguments from + */ +function sliceArgs(argsObj, startIndex) { + if (startIndex == null) startIndex = 0; + const ret = []; + for (let i = startIndex; i < argsObj.length; i++) { + ret.push(argsObj[i]); + } + return ret; +} + +/** + * Promisifies a function which returns an error as the first argument in its callback + * @param {Function} fn The function to promisify + * @param {any} [context=this] (optional) The context (value of `this` to bind the function to) + * @param {string[]} [returnArgNames] (optional) If the callback contains multiple arguments, + * you can combine them into one object by passing the names as an array. + * Otherwise the Promise will resolve with an array + * @returns {(...args: any[]) => Promise} + */ +function promisify(fn, context, returnArgNames) { + return function () { + const args = sliceArgs(arguments); + context = context || this; + return new Promise(function (resolve, reject) { + fn.apply(context, args.concat([ + function (error, result) { + if (error) { + return reject(error); + } else { + // decide on how we want to return the callback arguments + switch (arguments.length) { + case 1: // only an error was given + return resolve(); // Promise + case 2: // a single value (result) was returned + return resolve(result); + default: // multiple values should be returned + /** @type {{} | any[]} */ + let ret; + const extraArgs = sliceArgs(arguments, 1); + if (returnArgNames && returnArgNames.length === extraArgs.length) { + // we can build an object + ret = {}; + for (let i = 0; i < returnArgNames.length; i++) { + ret[returnArgNames[i]] = extraArgs[i]; + } + } else { + // we return the raw array + ret = extraArgs; + } + return resolve(ret); + } + } + } + ])); + }); + }; +} + +/** + * Promisifies a function which does not provide an error as the first argument in its callback + * @param {Function} fn The function to promisify + * @param {any} context (optional) The context (value of `this` to bind the function to) + * @param {string[]} returnArgNames (optional) If the callback contains multiple arguments, + * you can combine them into one object by passing the names as an array. + * Otherwise the Promise will resolve with an array + * @returns {(...args: any[]) => Promise} + */ +function promisifyNoError(fn, context, returnArgNames) { + return function () { + const args = sliceArgs(arguments); + context = context || this; + return new Promise(function (resolve, reject) { + fn.apply(context, args.concat([ + function (result) { + // decide on how we want to return the callback arguments + switch (arguments.length) { + case 0: // no arguments were given + return resolve(); // Promise + case 1: // a single value (result) was returned + return resolve(result); + default: // multiple values should be returned + /** @type {{} | any[]} */ + let ret; + const extraArgs = sliceArgs(arguments, 0); + if (returnArgNames && returnArgNames.length === extraArgs.length) { + // we can build an object + ret = {}; + for (let i = 0; i < returnArgNames.length; i++) { + ret[returnArgNames[i]] = extraArgs[i]; + } + } else { + // we return the raw array + ret = extraArgs; + } + return resolve(ret); + } + } + ])); + }); + }; +} + +/** + * Creates and executes an array of promises in sequence + * @param {((...args: any[]) => Promise)[]} promiseFactories An array of promise-returning functions + */ +function promiseSequence(promiseFactories) { + return promiseFactories.reduce((promise, factory) => { + return promise.then(result => factory().then(Array.prototype.concat.bind(result))) + }, Promise.resolve([])); +} + +/** + * Poor man's async/await using generator functions. Turns a generator function into a promise returning function. + * yield equals await. + * @template TReturn + * @param {(...args: any[]) => IterableIterator} makeGenerator A generator function to sequentially execute. + * @returns {(...args: any[]) => Promise} + */ +function poorMansAsync(makeGenerator) { + return function () { + let generator = makeGenerator.apply(this, arguments); + + function handle(result) { + // result => { done: [Boolean], value: [Object] } + if (result.done) return Promise.resolve(result.value); + + return Promise.resolve(result.value).then(function (res) { + return handle(generator.next(res)); + }, function (err) { + if (typeof err === 'string') err = new Error(err); + return handle(generator.throw(err)); + }); + } + + try { + return handle(generator.next()); + } catch (ex) { + return Promise.reject(ex); + } + } +} +// // Example usage: +// function *test(a, b, c) { +// yield somethingAsyncThatReturnsAPromise(a); +// // write progress report +// yield somethingElseThatReturnsAPromise(b); +// // white progress report +// yield c; +// } +// var testAsync = gen2Async(test); +// testAsync(1,2,3).then(() => /* we're done */ ); + +let packageLockDisabled = false; +/** + * Ensures that package-lock.json gets ignored. Fixes installation issues on npm5 + * @param {(err?) => void} callback The callback to invoke after the command has finished + */ +function disablePackageLock(callback) { + if (packageLockDisabled) return callback(); + + const npmrcPath = path.join(__dirname, '../../..', '.npmrc'); + getSystemNpmVersion(function (err, version) { + packageLockDisabled = true; + if (version && semver.gte(version, '5.0.0')) { + // we need to disable the package lock + if (!fs.existsSync(npmrcPath)) { + // create the file + fs.writeFile(npmrcPath, 'package-lock=false\n', {encoding: 'utf8'}, callback); + return; + } + } + callback(); + }); +} + +module.exports = { + appName: getAppName(), + createUuid, + decryptPhrase, + disablePackageLock, + encryptPhrase, + findIPs, + poorMansAsync, + getAdapterDir, + getConfigFileName, + getDefaultDataDir, + getFile, + getHostInfo, + getHostName, + getInstalledInfo, + getIoPack, + getJson, + getRepositoryFile, + getSystemNpmVersion, + promisify, + promisifyNoError, + promiseSequence, + rmdirRecursiveSync, + sendDiagInfo, + upToDate, + checkNonEditable, + copyAttributes +}; diff --git a/lib/uploadFiles.js b/lib/uploadFiles.js new file mode 100644 index 0000000..538d0f6 --- /dev/null +++ b/lib/uploadFiles.js @@ -0,0 +1,144 @@ +/** + * + * upload.js + * + * bulk upload a folder as attachment into one CouchDB object + * + * 8'2014 hobbyquaker + * + */ +'use strict'; + +var yargs = require('yargs') + .alias('d', 'dir') + .alias('o', 'object') + .alias('p', 'prefix') + .demand(['object']) + .usage('$0 --object object._id [--dir directory] [--prefix prefix]\n' + + 'Example: \n' + + ' Upload the content of the folder "images" into the virtual folder "img" of the virtual filesystem "fs.www"\n' + + ' $0 -o fs.www -d images -p img') + ; + +var fs = require('fs'); +var mime = require('mime'); +var Objects = require(__dirname + '/objects.js'); +var tools = require('./tools'); + +var files = []; +var rev; +var dir; +var prefix; + +var db = new Objects({ + logger: { + silly: function (msg) { }, + debug: function (msg) { }, + info: function (msg) { }, + warn: function (msg) { + console.log(msg); + }, + error: function (msg) { + console.log(msg); + } + }, + connected: function (type) { + + var id = yargs.argv.object; + + + dir = yargs.argv.dir || './'; + prefix = yargs.argv.prefix || ''; + db.getObject(yargs.argv.object, function (err, res) { + if (err || !res) { + db.setObject(yargs.argv.object, { + type: 'fs', + parent: 'fs', + common: { + name: yargs.argv.object.split('.').pop() + }, + ts: new Date().getTime(), + from: 'system.host.' + tools.getHostName() + '.cli', + native: {} + }, function (err, res) { + rev = res.rev; + main(); + }); + + } else { + rev = res._rev; + main(); + } + + + }); + + } +}); + +function main() { + walk(dir, function (err, res) { + if (err) { + console.log(err); + process.exit(1); + } + files = res; + upload(); + }); +} + +function upload() { + var file; + if (!files.length) { + console.log('done. ' + yargs.argv.object + '._rev=' + rev); + process.exit(0); + } else { + file = files.pop(); + var mimeType = mime.lookup(file); + var attName = prefix + (file.split('/').slice(1).join('/')); + console.log('upload', file, attName, mimeType); + + + fs.createReadStream(file).pipe( + db.insert(yargs.argv.object, attName, null, mimeType, { + rev: rev + }, function (err, res) { + if (err) { + console.log(err); + process.exit(1); + } + rev = res.rev; + setTimeout(function () { + upload(); + }, 50); + }) + ); + } + +} + +function walk(dir, done) { + var results = []; + fs.readdir(dir, function (err, list) { + if (err) return done(err); + var i = 0; + (function next() { + var file = list[i++]; + if (!file) return done(null, results); + file = dir + '/' + file; + fs.stat(file, function (err, stat) { + if (stat && stat.isDirectory()) { + walk(file, function (err, res) { + results = results.concat(res); + next(); + }); + } else { + results.push(file); + next(); + } + }); + })(); + }); +} + + diff --git a/lib/vis/states.js b/lib/vis/states.js new file mode 100644 index 0000000..c8057ce --- /dev/null +++ b/lib/vis/states.js @@ -0,0 +1,53 @@ +var getUsedObjectIDs = require(__dirname + '/../www/js/visUtils').getUsedObjectIDs; + +function calcProject(objects, projects, instance, result, callback) { + if (!projects || !projects.length) { + callback(null, result || []); + return; + } + result = result || []; + var project = projects.shift(); + if (!project || !project.isDir) { + setImmediate(calcProject, objects, projects, instance, result, callback); + return; + } + + // calculate datapoints in one project + objects.readFile('vis.' + instance, '/' + project.file + '/vis-views.json', function (err, data) { + var json; + try { + json = JSON.parse(data); + } catch (e) { + console.error('Cannot parse "/' + project.file + '/vis-views.json'); + setImmediate(calcProject, objects, projects, instance, result, callback); + return; + } + var dps = getUsedObjectIDs(json, false); + if (dps && dps.IDs) { + result.push({id: 'vis.' + instance + '.datapoints.' + project.file.replace(/[.\\s]/g, '_'), val: dps.IDs.length}); + } + setImmediate(calcProject, objects, projects, instance, result, callback); + }); +} + +function calcProjects(objects, states, instance, config, callback) { + objects.readDir('vis.' + instance, '/', function (err, projects) { + if (err || !projects || !projects.length) { + callback && callback(err || null, [{id: 'vis.' + instance + '.datapoints.total', val: 0}]); + } else { + calcProject(objects, projects, instance, [], function (err, result) { + if (result && result.length) { + var total = 0; + for (var r = 0; r < result.length; r++) { + total += result[r].val; + } + result.push({id: 'vis.' + instance + '.datapoints.total', val: total}); + } + + callback && callback(err, result); + }); + } + }); +} + +module.exports = calcProjects; \ No newline at end of file diff --git a/lib/www/js/visUtils.js b/lib/www/js/visUtils.js new file mode 100644 index 0000000..9f4c74e --- /dev/null +++ b/lib/www/js/visUtils.js @@ -0,0 +1,537 @@ +function getWidgetGroup(views, view, widget) { + var widgets = views[view].widgets; + var members; + for (var w in widgets) { + if (!widgets.hasOwnProperty(w)) continue; + members = views[view].widgets[w].data.members; + if (members && members.indexOf(widget) !== -1) { + return w; + } + } + return null; +} + +function extractBinding(format) { + var oid = format.match(/{(.+?)}/g); + var result = null; + if (oid) { + if (oid.length > 50) { + console.warn('Too many bindings in one widget: ' + oid.length + '[max = 50]'); + } + for (var p = 0; p < oid.length && p < 50; p++) { + var _oid = oid[p].substring(1, oid[p].length - 1); + if (_oid[0] === '{') continue; + // If first symbol '"' => it is JSON + if (_oid && _oid[0] === '"') continue; + var parts = _oid.split(';'); + result = result || []; + var systemOid = parts[0].trim(); + var visOid = systemOid; + + var test1 = visOid.substring(visOid.length - 4); + var test2 = visOid.substring(visOid.length - 3); + + if (visOid && test1 !== '.val' && test2 !== '.ts' && test2 !== '.lc' && test1 !== '.ack') { + visOid = visOid + '.val'; + } + + var isSeconds = (test2 === '.ts' || test2 === '.lc'); + + test1 = systemOid.substring(systemOid.length - 4); + test2 = systemOid.substring(systemOid.length - 3); + + if (test1 === '.val' || test1 === '.ack') { + systemOid = systemOid.substring(0, systemOid.length - 4); + } else if (test2 === '.lc' || test2 === '.ts') { + systemOid = systemOid.substring(0, systemOid.length - 3); + } + var operations = null; + var isEval = visOid.match(/[\d\w_.]+:[-\d\w_.]+/) || (!visOid.length && parts.length > 0);//(visOid.indexOf(':') !== -1) && (visOid.indexOf('::') === -1); + + if (isEval) { + var xx = visOid.split(':', 2); + var yy = systemOid.split(':', 2); + visOid = xx[1]; + systemOid = yy[1]; + operations = operations || []; + operations.push({ + op: 'eval', + arg: [{ + name: xx[0], + visOid: visOid, + systemOid: systemOid + }] + }); + } + + for (var u = 1; u < parts.length; u++) { + // eval construction + if (isEval) { + if (parts[u].trim().match(/^[\d\w_.]+:[-.\d\w_]+$/)) {//parts[u].indexOf(':') !== -1 && parts[u].indexOf('::') === -1) { + var _systemOid = parts[u].trim(); + var _visOid = _systemOid; + + test1 = _visOid.substring(_visOid.length - 4); + test2 = _visOid.substring(_visOid.length - 3); + + if (test1 !== '.val' && test2 !== '.ts' && test2 !== '.lc' && test1 !== '.ack') { + _visOid = _visOid + '.val'; + } + + test1 = systemOid.substring(_systemOid.length - 4); + test2 = systemOid.substring(_systemOid.length - 3); + + if (test1 === '.val' || test1 === '.ack') { + _systemOid = _systemOid.substring(0, _systemOid.length - 4); + } else if (test2 === '.lc' || test2 === '.ts') { + _systemOid = _systemOid.substring(0, _systemOid.length - 3); + } + var x1 = _visOid.split(':', 2); + var y1 = _systemOid.split(':', 2); + + operations[0].arg.push({ + name: x1[0], + visOid: x1[1], + systemOid: y1[1] + }); + } else { + parts[u] = parts[u].replace(/::/g, ':'); + if (operations[0].formula) { + var n = JSON.parse(JSON.stringify(operations[0])); + n.formula = parts[u]; + operations.push(n); + } else { + operations[0].formula = parts[u]; + } + } + } else { + var parse = parts[u].match(/([\w\s\/+*-]+)(\(.+\))?/); + if (parse && parse[1]) { + parse[1] = parse[1].trim(); + // operators requires parameter + if (parse[1] === '*' || + parse[1] === '+' || + parse[1] === '-' || + parse[1] === '/' || + parse[1] === '%' || + parse[1] === 'min' || + parse[1] === 'max') { + if (parse[2] === undefined) { + console.log('Invalid format of format string: ' + format); + parse[2] = null; + } else { + parse[2] = (parse[2] || '').trim().replace(',', '.'); + parse[2] = parse[2].substring(1, parse[2].length - 1); + parse[2] = parseFloat(parse[2].trim()); + + if (parse[2].toString() === 'NaN') { + console.log('Invalid format of format string: ' + format); + parse[2] = null; + } else { + operations = operations || []; + operations.push({op: parse[1], arg: parse[2]}); + } + } + } else + // date formatting + if (parse[1] === 'date') { + operations = operations || []; + parse[2] = (parse[2] || '').trim(); + parse[2] = parse[2].substring(1, parse[2].length - 1); + operations.push({op: parse[1], arg: parse[2]}); + } else + // returns array[value]. e.g.: {id.ack;array(ack is false,ack is true)} + if (parse[1] === 'array') { + operations = operations || []; + param = (parse[2] || '').trim(); + param = param.substring(1, param.length - 1); + param = param.split(','); + if (Array.isArray(param)) { + operations.push ({op: parse[1], arg: param}); //xxx + } + } else + // value formatting + if (parse[1] === 'value') { + operations = operations || []; + var param = (parse[2] === undefined) ? '(2)' : (parse[2] || ''); + param = param.trim(); + param = param.substring(1, param.length - 1); + operations.push({op: parse[1], arg: param}); + } else + // operators have optional parameter + if (parse[1] === 'pow' || parse[1] === 'round' || parse[1] === 'random') { + if (parse[2] === undefined) { + operations = operations || []; + operations.push({op: parse[1]}); + } else { + parse[2] = (parse[2] || '').trim().replace(',', '.'); + parse[2] = parse[2].substring(1, parse[2].length - 1); + parse[2] = parseFloat(parse[2].trim()); + + if (parse[2].toString() === 'NaN') { + console.log('Invalid format of format string: ' + format); + parse[2] = null; + } else { + operations = operations || []; + operations.push({op: parse[1], arg: parse[2]}); + } + } + } else + // operators without parameter + { + operations = operations || []; + operations.push({op: parse[1]}); + } + } else { + console.log('Invalid format ' + format); + } + } + } + + result.push({ + visOid: visOid, + systemOid: systemOid, + token: oid[p], + operations: operations ? operations : undefined, + format: format, + isSeconds: isSeconds + }); + } + } + return result; +} + +function getUsedObjectIDs(views, isByViews) { + if (!views) { + console.log('Check why views are not yet loaded!'); + return null; + } + + var _views = isByViews ? {} : null; + var IDs = []; + var visibility = {}; + var bindings = {}; + var lastChanges = {}; + var signals = {}; + + var view; + var id; + var sidd; + for (view in views) { + if (!views.hasOwnProperty(view)) continue; + + if (view === '___settings') continue; + + if (_views) _views[view] = []; + + for (id in views[view].widgets) { + if (!views[view].widgets.hasOwnProperty(id)) continue; + // Check all attributes + var data = views[view].widgets[id].data; + var style = views[view].widgets[id].style; + + // fix error in naming + if (views[view].widgets[id].groupped) { + views[view].widgets[id].grouped = true; + delete views[view].widgets[id].groupped; + } + + // rename hqWidgets => hqwidgets + if (views[view].widgets[id].widgetSet === 'hqWidgets') { + views[view].widgets[id].widgetSet = 'hqwidgets'; + } + + // rename RGraph => rgraph + if (views[view].widgets[id].widgetSet === 'RGraph') { + views[view].widgets[id].widgetSet = 'rgraph'; + } + + // rename timeAndWeather => timeandweather + if (views[view].widgets[id].widgetSet === 'timeAndWeather') { + views[view].widgets[id].widgetSet = 'timeandweather'; + } + + // convert "Show on Value" to HTML + if (views[view].widgets[id].tpl === 'tplShowValue') { + views[view].widgets[id].tpl = 'tplHtml'; + views[view].widgets[id].data['visibility-oid'] = views[view].widgets[id].data.oid; + views[view].widgets[id].data['visibility-val'] = views[view].widgets[id].data.value; + delete views[view].widgets[id].data.oid; + delete views[view].widgets[id].data.value; + } + + // convert "Hide on >0/True" to HTML + if (views[view].widgets[id].tpl === 'tplHideTrue') { + views[view].widgets[id].tpl = 'tplHtml'; + views[view].widgets[id].data['visibility-cond'] = '!='; + views[view].widgets[id].data['visibility-oid'] = views[view].widgets[id].data.oid; + views[view].widgets[id].data['visibility-val'] = true; + delete views[view].widgets[id].data.oid; + } + + // convert "Hide on 0/False" to HTML + if (views[view].widgets[id].tpl === 'tplHide') { + views[view].widgets[id].tpl = 'tplHtml'; + views[view].widgets[id].data['visibility-cond'] = '!='; + views[view].widgets[id].data['visibility-oid'] = views[view].widgets[id].data.oid; + views[view].widgets[id].data['visibility-val'] = false; + delete views[view].widgets[id].data.oid; + } + + // convert "Door/Window sensor" to HTML + if (views[view].widgets[id].tpl === 'tplHmWindow') { + views[view].widgets[id].tpl = 'tplValueBool'; + views[view].widgets[id].data.html_false = views[view].widgets[id].data.html_closed; + views[view].widgets[id].data.html_true = views[view].widgets[id].data.html_open; + delete views[view].widgets[id].data.html_closed; + delete views[view].widgets[id].data.html_open; + } + + // convert "Door/Window sensor" to HTML + if (views[view].widgets[id].tpl === 'tplHmWindowRotary') { + views[view].widgets[id].tpl = 'tplValueListHtml8'; + views[view].widgets[id].data.count = 2; + views[view].widgets[id].data.value0 = views[view].widgets[id].data.html_closed; + views[view].widgets[id].data.value1 = views[view].widgets[id].data.html_open; + views[view].widgets[id].data.value2 = views[view].widgets[id].data.html_tilt; + delete views[view].widgets[id].data.html_closed; + delete views[view].widgets[id].data.html_open; + delete views[view].widgets[id].data.html_tilt; + } + + // convert "tplBulbOnOff" to tplBulbOnOffCtrl + if (views[view].widgets[id].tpl === 'tplBulbOnOff') { + views[view].widgets[id].tpl = 'tplBulbOnOffCtrl'; + views[view].widgets[id].data.readOnly = true; + } + + // convert "tplValueFloatBarVertical" to tplValueFloatBar + if (views[view].widgets[id].tpl === 'tplValueFloatBarVertical') { + views[view].widgets[id].tpl = 'tplValueFloatBar'; + views[view].widgets[id].data.orientation = 'vertical'; + } + + for (var attr in data) { + if (!data.hasOwnProperty(attr) || !attr) continue; + /* TODO DO do not forget remove it after a while. Required for import from DashUI */ + if (attr === 'state_id') { + data.state_oid = data[attr]; + delete data[attr]; + attr = 'state_oid'; + } else + if (attr === 'number_id') { + data.number_oid = data[attr]; + delete data[attr]; + attr = 'number_oid'; + } else + if (attr === 'toggle_id') { + data.toggle_oid = data[attr]; + delete data[attr]; + attr = 'toggle_oid'; + } else + if (attr === 'set_id') { + data.set_oid = data[attr]; + delete data[attr]; + attr = 'set_oid'; + } else + if (attr === 'temp_id') { + data.temp_oid = data[attr]; + delete data[attr]; + attr = 'temp_oid'; + } else + if (attr === 'drive_id') { + data.drive_oid = data[attr]; + delete data[attr]; + attr = 'drive_oid'; + } else + if (attr === 'content_id') { + data.content_oid = data[attr]; + delete data[attr]; + attr = 'content_oid'; + } else + if (attr === 'dialog_id') { + data.dialog_oid = data[attr]; + delete data[attr]; + attr = 'dialog_oid'; + } else + if (attr === 'max_value_id') { + data.max_value_oid = data[attr]; + delete data[attr]; + attr = 'max_value_oid'; + } else + if (attr === 'dialog_id') { + data.dialog_oid = data[attr]; + delete data[attr]; + attr = 'dialog_oid'; + } else + if (attr === 'weoid') { + data.woeid = data[attr]; + delete data[attr]; + attr = 'woeid'; + } + + if (typeof data[attr] === 'string') { + var m; + var oids = extractBinding(data[attr]); + if (oids) { + for (var t = 0; t < oids.length; t++) { + var ssid = oids[t].systemOid; + if (ssid) { + if (IDs.indexOf(ssid) === -1) IDs.push(ssid); + if (_views && _views[view].indexOf(ssid) === -1) _views[view].push(ssid); + if (!bindings[ssid]) bindings[ssid] = []; + oids[t].type = 'data'; + oids[t].attr = attr; + oids[t].view = view; + oids[t].widget = id; + + bindings[ssid].push(oids[t]); + } + + if (oids[t].operations && oids[t].operations[0].arg instanceof Array) { + for (var ww = 0; ww < oids[t].operations[0].arg.length; ww++) { + ssid = oids[t].operations[0].arg[ww].systemOid; + if (!ssid) continue; + if (IDs.indexOf(ssid) === -1) IDs.push(ssid); + if (_views && _views[view].indexOf(ssid) === -1) _views[view].push(ssid); + if (!bindings[ssid]) bindings[ssid] = []; + bindings[ssid].push(oids[t]); + } + } + } + } else + if (attr !== 'oidTrueValue' && attr !== 'oidFalseValue' && ((attr.match(/oid\d{0,2}$/) || attr.match(/^oid/) || attr.match(/^signals-oid-/) || attr === 'lc-oid') && data[attr])) { + if (data[attr] && data[attr] !== 'nothing_selected') { + if (IDs.indexOf(data[attr]) === -1) IDs.push(data[attr]); + if (_views && _views[view].indexOf(data[attr]) === -1) _views[view].push(data[attr]); + } + + // Visibility binding + if (attr === 'visibility-oid' && data['visibility-oid']) { + var vid = data['visibility-oid']; + if (vid.match(/^groupAttr(\d+)$/)) { + var vgroup = getWidgetGroup(views, view, id); + if (vgroup) vid = views[view].widgets[vgroup].data[vid]; + } + + if (!visibility[vid]) visibility[vid] = []; + visibility[vid].push({view: view, widget: id}); + } + + // Signal binding + if (attr.match(/^signals-oid-/) && data[attr]) { + var sid = data[attr]; + if (sid.match(/^groupAttr(\d+)$/)) { + var group = getWidgetGroup(views, view, id); + if (group) sid = views[view].widgets[group].data[sid]; + } + + if (!signals[sid]) signals[sid] = []; + signals[sid].push({ + view: view, + widget: id, + index: parseInt(attr.substring('signals-oid-'.length), 10) + }); + } + if (attr === 'lc-oid') { + var lcsid = data[attr]; + if (lcsid.match(/^groupAttr(\d+)$/)) { + var ggroup = getWidgetGroup(views, view, id); + if (ggroup) lcsid = views[view].widgets[ggroup].data[lcsid]; + } + + if (!lastChanges[lcsid]) lastChanges[lcsid] = []; + lastChanges[lcsid].push({ + view: view, + widget: id + }); + } + } else + if ((m = attr.match(/^attrType(\d+)$/)) && data[attr] === 'id') { + var _id = 'groupAttr' + m[1]; + if (data[_id]) { + if (IDs.indexOf(data[_id]) === -1) IDs.push(data[_id]); + if (_views && _views[view].indexOf(data[_id]) === -1) _views[view].push(data[_id]); + } + } + } + } + + // build bindings for styles + if (style) { + for (var cssAttr in style) { + if (!style.hasOwnProperty(cssAttr) || !cssAttr) continue; + if (typeof style[cssAttr] === 'string') { + var objIDs = extractBinding(style[cssAttr]); + if (objIDs) { + for (var tt = 0; tt < objIDs.length; tt++) { + sidd = objIDs[tt].systemOid; + if (sidd) { + if (IDs.indexOf(sidd) === -1) IDs.push(sidd); + if (_views && _views[view].indexOf(sidd) === -1) _views[view].push(sidd); + if (!bindings[sidd]) bindings[sidd] = []; + + objIDs[tt].type = 'style'; + objIDs[tt].attr = cssAttr; + objIDs[tt].view = view; + objIDs[tt].widget = id; + + bindings[sidd].push(objIDs[tt]); + } + + if (objIDs[tt].operations && objIDs[tt].operations[0].arg instanceof Array) { + for (var w = 0; w < objIDs[tt].operations[0].arg.length; w++) { + sidd = objIDs[tt].operations[0].arg[w].systemOid; + if (!sidd) continue; + if (IDs.indexOf(sidd) === -1) IDs.push(sidd); + if (_views && _views[view].indexOf(sidd) === -1) _views[view].push(sidd); + if (!bindings[sidd]) bindings[sidd] = []; + bindings[sidd].push(objIDs[tt]); + } + } + } + } + } + } + } + } + } + + if (_views) { + var changed; + do { + changed = false; + // Check containers + for (view in views) { + if (!views.hasOwnProperty(view)) continue; + + if (view === '___settings') continue; + + for (id in views[view].widgets) { + if (!views[view].widgets.hasOwnProperty(id)) continue; + + // Add all OIDs from this view to parent + if (views[view].widgets[id].tpl === 'tplContainerView' && views[view].widgets[id].data.contains_view) { + var ids = _views[views[view].widgets[id].data.contains_view]; + if (ids) { + for (var a = 0; a < ids.length; a++) { + if (ids[a] && _views[view].indexOf(ids[a]) === -1) { + _views[view].push(ids[a]); + changed = true; + } + } + } else { + console.warn('View does not exist: "' + views[view].widgets[id].data.contains_view + '"'); + } + } + } + } + } while (changed); + } + + return {IDs: IDs, byViews: _views, visibility: visibility, bindings: bindings, lastChanges: lastChanges, signals: signals}; +} + +if (module && module.parent) { + module.exports.getUsedObjectIDs = getUsedObjectIDs; +} \ No newline at end of file diff --git a/lib/zipFiles.js b/lib/zipFiles.js new file mode 100644 index 0000000..a73dd94 --- /dev/null +++ b/lib/zipFiles.js @@ -0,0 +1,366 @@ +'use strict'; + +var JSZip; +var tools = require(__dirname + '/tools'); + +function _getAllFilesInDir(objects, id, name, options, callback, result) { + result = result || []; + objects.readDir(id, name, options, function (err, files) { + var count = 0; + var errors = []; + if (files) { + for (var f = 0; f < files.length; f++) { + if (files[f].isDir) { + count++; + _getAllFilesInDir(objects, id, name + '/' + files[f].file, options, function (err, _result) { + if (err) errors.push(err); + if (!--count) callback(errors.length ? errors : null, _result); + }, result); + } else { + result.push(name + '/' + files[f].file); + } + } + } + if (!count) callback(null, result); + }); +} + +function _addFile(objects, id, name, options, zip, files, callback) { + objects.readFile(id, name, options, function (err, data, mime) { + if (!zip) { + console.log(err); + callback('Cannot read file "' + name + '": ' + err, files); + } else { + // if handler installed + if (options.stringify) { + try { + data = options.stringify(name, data, options ? options.settings : null, files); + } catch (error) { + console.error('Cannot stringify file "' + name + '": ' + error); + if (!err) err = 'Cannot stringify file "' + name + '": ' + error; + } + } + var parts = name.split('/'); + if (parts.length > 1) { + parts.shift(); + name = parts.join('/'); + } + + zip.file(name, data); + setImmediate(function () { + callback(err, files); + }); + } + }); +} + +// pack all files as zip and send it back +function readDirAsZip(objects, id, name, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (name[0] === '/') name = name.substring(1); + options = options || {}; + var adapter = id; + if (adapter.indexOf('.') !== -1) { + adapter = id.split('.')[0]; + } + + // try to load processor of adapter + try { + options.stringify = require(tools.appName + '.' + adapter + '/lib/convert.js').stringify; + } catch (e) { + + } + + _getAllFilesInDir(objects, id, name, options, function (err, files) { + var count = 0; + if (files) { + JSZip = JSZip || require('jszip'); + var zip = new JSZip(); + var additional = []; + for (var f = 0; f < files.length; f++) { + count++; + _addFile(objects, id, files[f], options, zip, additional, function (err, additional) { + if (!--count) { + if (additional && additional.length) { + for (var ff = 0; ff < additional.length; ff++) { + if (!additional[ff] || typeof additional[ff] !== 'string') continue; + count++; + + if (additional[ff][0] === '/') additional[ff] = additional[ff].substring(1); + var parts = additional[ff].split('/'); + var adapter = parts.shift(); + + _addFile(objects, adapter, parts.join('/'), options, zip, null, function (err) { + if (!--count) { + zip.generateAsync({type: 'base64'}) + .then(function (base64) { + callback(err, base64); + }, function (err) { + callback(err); + }); + zip = null; + } + }); + } + if (!count) { + zip.generateAsync({type: 'base64'}) + .then(function (base64) { + callback(err, base64); + }, function (err) { + callback(err); + }); + zip = null; + } + } else { + zip.generateAsync({type: 'base64'}) + .then(function (base64) { + callback(err, base64); + }, function (err) { + callback(err); + }); + zip = null; + } + } + }); + } + } + if (!count) { + callback(err, null); + } + }); +} + +function _checkDir(objects, id, root, parts, options, callback) { + if (!parts || !parts.length) { + callback(); + return; + } + root += '/' + parts.shift(); + objects.readDir(id, root, options, function (err, files) { + if (err === 'Not exists') { + objects.mkdir(id, root, options, function (err) { + _checkDir(objects, id, root, parts, options, callback); + }); + } else { + _checkDir(objects, id, root, parts, options, callback); + } + }); +} + +function _writeOneFile(objects, zip, id, name, filename, options, callback) { + zip.files[filename].async('nodebuffer').then(function (data) { + var _err; + if (options.parse) { + try { + data = options.parse(name, filename, data, options ? options.settings : null); + } catch (e) { + _err = e; + } + } + var fName = name + filename; + var parts = fName.split('/'); + parts.pop(); + _checkDir(objects, id, '', parts, options, function () { + objects.writeFile(id, name + filename, data, options, function (err) { + callback(_err || err); + }); + }); + }, function (err) { + callback(err); + }); +} + +function writeDirAsZip(objects, id, name, data, options, callback) { + JSZip = JSZip || require('jszip'); + var zip = new JSZip(); + + options = options || {}; + + var adapter = id; + if (adapter.indexOf('.') !== -1) { + adapter = id.split('.')[0]; + } + + // try to load processor of adapter + try { + options.parse = require(tools.appName + '.' + adapter + '/lib/convert.js').parse; + } catch (e) { + + } + + try { + zip.loadAsync(data).then(function () { + var count = 0; + var error = []; + if (name[name.length - 1] !== '/') name += '/'; + for (var filename in zip.files) { + if (!filename || filename[filename.length - 1] === '/') continue; + count++; + try { + _writeOneFile(objects, zip, id, name, filename, options, function (err) { + if (err) { + error.push('Cannot write file "' + filename + '":' + err.toString()); + } + if (!--count && callback) { + callback(error.length ? error.join(', ') : null); + callback = null; + } + }); + } catch (error) { + if (callback) { + callback(error.toString()); + callback = null; + } + } + } + }, function (error) { + if (callback) { + callback(error.toString()); + callback = null; + } + }); + } catch (error) { + if (callback) { + callback(error.toString()); + callback = null; + } + } +} + +// pack all files as zip and send it back +function readObjectsAsZip(objects, rootId, adapter, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + options = options || {}; + + if (adapter) { + // try to load processor of adapter + try { + options.stringify = require(tools.appName + '.' + adapter + '/lib/convert.js').stringify; + } catch (e) { + + } + } + + objects.getConfigKeys(rootId + '.*', options, function (err, keys) { + objects.getObjects(keys, options, function(err, objs) { + JSZip = JSZip || require('jszip'); + var zip = new JSZip(); + for (var f = 0; f < objs.length; f++) { + var data = {id: keys[f], data: objs[f]}; + + if (options.stringify) { + try { + data = options.stringify(data, options ? options.settings : null); + } catch (e) { + data.id = keys[f].replace(/\./g, '/').substring(rootId.length + 1) + '.json'; + } + } else { + data.id = keys[f].replace(/\./g, '/').substring(rootId.length + 1) + '.json'; + } + if (typeof data.data === 'object') data.data = JSON.stringify(data.data, null, 2); + + zip.file(data.id, data.data); + } + zip.generateAsync({type: 'base64'}) + .then(function (base64) { + callback(err, base64); + }, function (err) { + callback(err); + }); + }); + }); +} + +function _writeOneObject(objects, zip, rootId, filename, options, callback) { + zip.files[filename].async('nodebuffer').then(function (data) { + data = {data: data.toString(), id: filename}; + if (options.parse){ + try { + data = options.parse(data, options ? options.settings : null); + } catch (e) { + callback('Cannot custom parse "' + data.id + '": ' + e); + return; + } + } else { + data.id = (rootId ? (rootId + '.') : '') + data.id.replace(/\//g, '.').replace(/\.json$/, ''); + } + if (typeof data.data !== 'object') { + try { + data.data = JSON.parse(data.data); + } catch (e) { + callback('Cannot parse "' + data.id + '": ' + e); + return; + } + } + if (data && data.id && data.data) { + options.ts = new Date().getTime(); + options.from = 'system.host.' + tools.getHostName() + '.cli'; + objects.setObject(data.id, data.data, options, function (err) { + callback(err); + }); + } else { + if (data && data.error) { + callback(data.error); + } else { + callback(); + } + } + }, function (err) { + callback('Cannot parse unzip: ' + err); + }); +} + +function writeObjectsAsZip(objects, rootId, adapter, data, options, callback) { + JSZip = JSZip || require('jszip'); + + options = options || {}; + + if (adapter) { + // try to load processor of adapter + try { + options.parse = require(tools.appName + '.' + adapter + '/lib/convert.js').parse; + } catch (e) { + + } + } + + var zip = new JSZip(); + try { + zip.loadAsync(data).then(function () { + var count = 0; + var error = []; + for (var filename in zip.files) { + if (filename[filename.length - 1] === '/') continue; + count++; + _writeOneObject(objects, zip, rootId, filename, options, function (err) { + if (err) error.push(err.toString()); + if (!--count && callback) { + callback(error.length ? error.join(', ') : null); + callback = null; + } + }); + } + }, function (error) { + if (callback) { + callback(error.toString()); + callback = null; + } + }); + } catch (error) { + if (callback) { + callback(error.toString()); + callback = null; + } + } +} + +module.exports.readDirAsZip = readDirAsZip; +module.exports.writeDirAsZip = writeDirAsZip; +module.exports.readObjectsAsZip = readObjectsAsZip; +module.exports.writeObjectsAsZip = writeObjectsAsZip; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..94b3369 --- /dev/null +++ b/package.json @@ -0,0 +1,101 @@ +{ + "name": "yunkong2.js-controller", + "version": "1.4.2", + "engines": { + "node": ">=4" + }, + "optionalDependencies": { + "redis": "^2.8.0", + "greenlock": "^2.1.19", + "le-challenge-fs": "^2.0.8", + "le-sni-auto": "^2.1.1", + "winston-syslog": "^1.2.6" + }, + "bin": { + "yunkong2": "./yunkong2.js" + }, + "dependencies": { + "bluebird": "^3.5.1", + "daemonize2": "^0.4.2", + "yunkong2.admin": ">=2.0.9", + "jszip": "^3.1.5", + "mime": "^1.4.0", + "mkdirp": "^0.5.1", + "ncp": "^2.0.0", + "node-schedule": "^1.3.0", + "node.extend": "^2.0.0", + "prompt": "^1.0.0", + "pyconf": "^1.1.2", + "request": "^2.85.0", + "jsonwebtoken": "^8.2.1", + "safe-replace": "^1.0.2", + "semver": "^5.5.0", + "socket.io": "~2.1.0", + "socket.io-client": "~2.1.0", + "tar": "^4.4.1", + "winston": "^2.4.1", + "winston-daily-rotate-file": "^1.7.2", + "yargs": "^11.0.0" + }, + "devDependencies": { + "@types/node": "^4.2.23", + "chai": "^4.1.2", + "chai-as-promised": "^7.1.1", + "grunt": "^1.0.1", + "grunt-contrib-jshint": "^1.1.0", + "grunt-http": "^2.2.0", + "grunt-jscs": "^3.0.1", + "grunt-jsdoc": "^2.1.0", + "grunt-replace": "^1.0.1", + "istanbul": "^0.4.5", + "mocha": "^5.0.1" + }, + "homepage": "http://www.yunkong2.com", + "description": "...domesticate the Internet of Things", + "keywords": [ + "yunkong2", + "Smarthome", + "Home Automation", + "Smart Metering", + "Homematic", + "Hue", + "KNX", + "Z-Wave", + "ZigBee", + "Bidcos", + "TV", + "Sonos", + "AV Receiver" + ], + "bugs": { + "url": "https://github.com/yunkong2/yunkong2.js-controller/issues" + }, + "licenses": [ + { + "type": "MIT", + "url": "https://github.com/yunkong2/yunkong2.js-controller/blob/master/LICENSE" + } + ], + "author": "bluefox ", + "contributors": [ + "bluefox ", + "hobbyquaker " + ], + "repository": { + "type": "git", + "url": "https://github.com/yunkong2/yunkong2.js-controller" + }, + "scripts": { + "preinstall": "node lib/preinstall_check.js", + "install": "node yunkong2.js setup first", + "start": "node yunkong2.js start", + "stop": "node yunkong2.js stop", + "restart": "node yunkong2.js restart", + "prepublish": "node lib/scripts/scripts.js --prepublish", + "test": "node node_modules/mocha/bin/mocha test --exit", + "test-redis-socket": "node node_modules/mocha/bin/mocha test/redis-socket/ --exit", + "coverage": "node node_modules/istanbul/lib/cli.js cover --config istanbul.yml node_modules/mocha/bin/_mocha ./test -- --ui bdd -R spec" + }, + "main": "controller.js", + "license": "MIT" +} diff --git a/reinstall.sh b/reinstall.sh new file mode 100644 index 0000000..3c7eae7 --- /dev/null +++ b/reinstall.sh @@ -0,0 +1,33 @@ +#!/bin/bash +yunkong2 stop +BASE=$(pwd) + +if [ ! -f "$BASE/yunkong2" ] +then + echo "Script needs to be started in the yunkong2 base directory (normally /opt/yunkong2 on linux)" + exit +fi + +if [ -d ./node_modules ] +then + ls -1 ./node_modules | grep yunkong2. > reinstall.list.txt + chmod -R 777 * + cd node_modules + rm -R * + pwd + + while read IN + do + npm install $IN --production --save --unsafe-perm --prefix $BASE + if [ $? -eq 0 ] + then + echo "DONE $IN" + else + echo "FAIL $IN" + fi + + done < "$BASE/reinstall.list.txt" + chmod -R 777 * + rm "$BASE/reinstall.list.txt" + yunkong2 upload all +fi diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..82c7f18 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,58 @@ +{ + "compilerOptions": { + /* Basic Options */ + "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + "lib": ["es6"], /* Specify library files to be included in the compilation: */ + "allowJs": true, /* Allow javascript files to be compiled. */ + "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "removeComments": true, /* Do not emit comments to output. */ + "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": false, /* Enable all strict type-checking options. */ + "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + + /* Source Map Options */ + // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + }, + "include": [ + "yunkong2.js", + "lib/**/*.js" + ] + } \ No newline at end of file diff --git a/yunkong2.js b/yunkong2.js new file mode 100644 index 0000000..2ffda74 --- /dev/null +++ b/yunkong2.js @@ -0,0 +1 @@ +require(__dirname + '/lib/setup.js').execute(); \ No newline at end of file