├── .gitattributes ├── .gitignore ├── .jshintrc ├── Gruntfile.js ├── License.txt ├── README.md ├── debug-node-test.bat ├── nd-node.js ├── package.json ├── results.txt ├── src ├── backend-instance.js ├── core-modules │ ├── config │ │ └── config.js │ ├── fs │ │ └── fs.js │ ├── job │ │ ├── core-jobs │ │ │ ├── copy.js │ │ │ ├── lib │ │ │ │ └── ndjs_ncp.js │ │ │ ├── multi.js │ │ │ └── sfx.js │ │ └── job.js │ ├── notifications │ │ └── notifications.js │ ├── pages │ │ └── pages.js │ ├── state │ │ └── state.js │ ├── ui-actions │ │ └── ui-actions.js │ └── utils │ │ └── utils.js ├── logger.js └── module-loader.js └── test ├── .jshintrc ├── back-instance-mocks ├── mock-module1.js ├── mock-module2.js └── mock-module3.js ├── backend-instance.spec.js ├── core-modules ├── config │ └── config.spec.js ├── job │ ├── copymock-ndfile.js │ └── job.spec.js ├── notifications │ └── notifications.spec.js ├── pages │ └── pages.spec.js ├── state │ └── state.spec.js ├── ui-actions │ └── ui-actions.spec.js └── utils │ ├── test.xml │ └── utils.spec.js ├── mock-module.js ├── mock-ndfile.js └── module-loader.spec.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear on external disk 35 | .Spotlight-V100 36 | .Trashes 37 | 38 | # Directories potentially created on remote AFP share 39 | .AppleDB 40 | .AppleDesktop 41 | Network Trash Folder 42 | Temporary Items 43 | .apdisk 44 | 45 | node_modules/ 46 | results.txt 47 | .tmp/ 48 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "node": true, 4 | 5 | "curly": true, 6 | "eqeqeq": true, 7 | "es3": true, 8 | "newcap": false, 9 | "noarg": true, 10 | "nonew": true, 11 | "quotmark": "single", 12 | "strict": true, 13 | "trailing": true, 14 | "undef": true, 15 | "unused": true, 16 | "-W024":true, 17 | "globals": { 18 | "angular": true 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(grunt) { 3 | 'use strict'; 4 | // Add the grunt-mocha-test tasks. 5 | grunt.loadNpmTasks('grunt-mocha-test'); 6 | 7 | grunt.initConfig({ 8 | // Configure a mochaTest task 9 | mochaTest: { 10 | test: { 11 | options: { 12 | reporter: 'spec', 13 | captureFile: 'results.txt', // Optionally capture the reporter output to a file 14 | quiet: false, // Optionally suppress output to standard out (defaults to false) 15 | clearRequireCache: false // Optionally clear the require cache before running tests (defaults to false) 16 | }, 17 | src: ['test/**/*spec.js'] 18 | } 19 | }, 20 | nddox: 21 | { 22 | files: { 23 | src: ['src/**/*.js'], 24 | dest: 'docs/docs.html' 25 | } 26 | } 27 | }); 28 | 29 | 30 | 31 | 32 | 33 | grunt.registerTask('default', 'mochaTest'); 34 | 35 | }; 36 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 asaf amrami 3 | 4 | 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: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | 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. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Stories in Ready](https://badge.waffle.io/asafamr/nd-node.png?label=ready&title=Ready)](https://waffle.io/asafamr/nd-node) 2 | # ND-Node 3 | 4 | NDJS is a free framework for creating installers 5 | 6 | Check out our [getting started guide](http://ndjs.io/started.html) 7 | 8 | 9 | 10 |

11 | 12 | [NDJS website](http://ndjs.io) 13 | 14 | [NDJS API](http://ndjs.io/api.html) 15 | 16 | # License 17 | 18 | MIT 19 | -------------------------------------------------------------------------------- /debug-node-test.bat: -------------------------------------------------------------------------------- 1 | SET BLUEBIRD_LONG_STACK_TRACES =1; 2 | node-debug --save-live-edit true %APPDATA%\npm\node_modules\grunt-cli\bin\grunt --enable-stack-trace --debug --verobse mochaTest --grep pending --timeout 1000000 3 | -------------------------------------------------------------------------------- /nd-node.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 'use strict'; 3 | 4 | var backendInstance = require('./src/backend-instance'); 5 | 6 | 7 | 8 | var ndNode = {}; 9 | module.exports=ndNode; 10 | ndNode.create=create; 11 | 12 | 13 | 14 | function create(ndConfigPath) 15 | { 16 | return backendInstance.create(ndConfigPath); 17 | } 18 | 19 | 20 | })(); 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nd-node", 3 | "description": "NDJS node back end", 4 | "author": { 5 | "name": "Asaf Amrami", 6 | "email": "contact@ndjs.io" 7 | }, 8 | "main": "nd-node.js", 9 | "repository": { 10 | "type": "git", 11 | "url": "http://github.com/asafamr/nd-node.git" 12 | }, 13 | "dependencies": { 14 | "bluebird": "^3.0.5", 15 | "fs-extra": "^0.26.2", 16 | "graceful-fs": "^4.1.2", 17 | "lodash": "^3.10.1", 18 | "toposort": "^0.2.12", 19 | "windows-shortcuts": "^0.1.1", 20 | "xmldom": "^0.1.19", 21 | "xpath": "0.0.9" 22 | }, 23 | "devDependencies": { 24 | "chai": "^3.4.1", 25 | "chai-as-promised": "^5.1.0", 26 | "dox": "^0.8.0", 27 | "grunt": "^0.4.5", 28 | "grunt-mocha-test": "^0.12.7", 29 | "html": "0.0.10", 30 | "lodash": "^3.10.1", 31 | "lodash.template": "^3.6.2", 32 | "mocha": "^2.3.3", 33 | "ncp": "^2.0.0" 34 | }, 35 | "optionalDependencies": {}, 36 | "license": "AGPL-1.0", 37 | "version": "0.2.1" 38 | } 39 | -------------------------------------------------------------------------------- /results.txt: -------------------------------------------------------------------------------- 1 | 2 |  3 |  backend instance 4 |  modules load dependencies 5 | registering a custom module mock1 6 | registering a custom module mock2 7 | registering a custom module mock3 8 | Creating an instace of mock3 9 | Creating an instace of $backend 10 | Creating an instace of $uiActions 11 | Creating an instace of $logger 12 | Creating an instace of mock1 13 | Creating an instace of $state 14 | Creating an instace of $notifications 15 | Creating an instace of $fs 16 | Creating an instace of mock2 17 | Creating an instace of $config 18 | Creating an instace of $job 19 | Creating an instace of $pages 20 | finished loading modules 21 |   √ load according to order 22 | registering a custom module AnonymousModule0 23 | registering a custom module mock2b 24 | registering a custom module mock3b 25 | Creating an instace of mock2b 26 | Creating an instace of mock3b 27 | Creating an instace of $backend 28 | Creating an instace of $uiActions 29 | Creating an instace of $logger 30 | Creating an instace of AnonymousModule0 31 | Creating an instace of $state 32 | Creating an instace of $notifications 33 | Creating an instace of $fs 34 | Creating an instace of $config 35 | Creating an instace of $job 36 | Creating an instace of $pages 37 | finished loading modules 38 |   √ should get dependencies 39 | registering a custom module mock1 40 | registering a custom module mock2 41 | registering a custom module mock3 42 |   √ should fail on cyclic depend 43 | 44 | 45 |   3 passing (62ms) 46 | 47 | -------------------------------------------------------------------------------- /src/backend-instance.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var backendInstance={}; 4 | module.exports=backendInstance; 5 | backendInstance.create=create;//function create(ndConfigPath,logger) 6 | 7 | var logger= require('./logger'); 8 | var path= require('path'); 9 | var _= require('lodash'); 10 | var BBPromise= require('bluebird'); 11 | var fs= require('fs'); 12 | var moduleLoader= require('./module-loader'); 13 | 14 | var coreModules= 15 | [ 16 | 'config', 17 | 'ui-actions', 18 | 'notifications', 19 | 'state', 20 | 'job', 21 | 'pages', 22 | 'fs', 23 | 'utils' 24 | /*'job-manager', 25 | 'nd-fs', 26 | '', 27 | '*/ 28 | ]; 29 | 30 | 31 | function create(ndConfigPath) 32 | { 33 | var loadedModules={}; 34 | var newBackendInstance={}; 35 | newBackendInstance.startLoad=startLoad; 36 | newBackendInstance.registerModule=registerModule; 37 | newBackendInstance.registerModulesDir=registerModulesDir; 38 | newBackendInstance.getConfigPath=function(){return ndConfigPath;}; 39 | newBackendInstance.hasFinishedLoading=function(){return finishedLoading;}; 40 | newBackendInstance.getModule=function(name){return loadedModules[name];}; 41 | 42 | 43 | //private params 44 | var moduleLoaders=[]; 45 | var startedLoading=false; 46 | var finishedLoading=false; 47 | 48 | activate(); 49 | return newBackendInstance; 50 | 51 | function activate() 52 | { 53 | registerModulesDir(__dirname+'/core-modules/job/core-jobs'); 54 | } 55 | /*** 56 | startLoad - sort modules according to dependencies, loads them and wait for their actions registration 57 | ***/ 58 | function startLoad() 59 | { 60 | if(startedLoading) 61 | { 62 | logger.debug('ND-node startLoad called more than once'); 63 | return; 64 | } 65 | startedLoading=true; 66 | 67 | coreModules.forEach(function (moduleName){ 68 | moduleLoaders.push(moduleLoader.getLoader(getCoreModulePath(moduleName))); 69 | }); 70 | moduleLoaders.push(moduleLoader.getLoader({instance:logger,moduleName:'$logger'})); 71 | moduleLoaders.push(moduleLoader.getLoader({instance:newBackendInstance,moduleName:'$backend'})); 72 | 73 | //var modules=getModulesAndDependencies(modulePaths); 74 | moduleLoaders=moduleLoader.getSortedByDepends(moduleLoaders); 75 | 76 | var modulePromises={}; 77 | 78 | //we will augment each module in modules with .instance - a member containg a promise returning its instance 79 | moduleLoaders.forEach( 80 | function(moduleLoader) 81 | { 82 | //we init each module instance promise then create it when all dependcies are ready 83 | modulePromises[moduleLoader.name]=new BBPromise(function (resolve){ 84 | var args= 85 | _.map(moduleLoader.dependencies, 86 | function(name){ return modulePromises[name];} 87 | );//concat all dependencies 88 | 89 | resolve(BBPromise.all(args).then(//all dependencies have ben created - create this module now 90 | function(argValues) 91 | { 92 | logger.debug('Creating an instance of '+moduleLoader.name); 93 | var factoryRet=moduleLoader.factory.apply(newBackendInstance,argValues); 94 | if(!factoryRet) 95 | { 96 | loadedModules[moduleLoader.name]={}; 97 | return BBPromise.resolve({}); 98 | } 99 | return BBPromise.resolve(factoryRet).then(function(instResolved) 100 | { 101 | loadedModules[moduleLoader.name]=instResolved; 102 | return instResolved; 103 | }); 104 | 105 | } 106 | )); 107 | }); 108 | } 109 | ); 110 | 111 | //wait for all loading to complete and update status 112 | return BBPromise.all(_.values(modulePromises)).then( 113 | function() 114 | { 115 | logger.debug('finished loading modules'); 116 | finishedLoading=true; 117 | } 118 | ); 119 | 120 | } 121 | 122 | 123 | function registerModule(ndjsModule) 124 | { 125 | var loader= moduleLoader.getLoader(ndjsModule); 126 | logger.debug('registering a custom module ' +loader.name); 127 | moduleLoaders.push(loader); 128 | } 129 | function registerModulesDir(dirPath) 130 | { 131 | var files=fs.readdirSync(dirPath); 132 | files.forEach(function(filename) 133 | { 134 | if(filename.indexOf('.js')!==-1) 135 | { 136 | registerModule(path.resolve(dirPath,filename)); 137 | } 138 | }); 139 | } 140 | 141 | 142 | } 143 | 144 | 145 | 146 | 147 | function getCoreModulePath(moduleName) 148 | { 149 | return path.normalize(__dirname+'/core-modules/'+moduleName+'/'+moduleName); 150 | } 151 | -------------------------------------------------------------------------------- /src/core-modules/config/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | @name config module 3 | @description gets configurations from ndjs file according to the current installer stage. parses templates through the state module 4 | **/ 5 | 'use strict'; 6 | 7 | module.exports=createModule; 8 | createModule.moduleName='$config'; 9 | createModule.$inject=['$backend','$state']; 10 | 11 | var _ =require('lodash'); 12 | var path =require('path'); 13 | 14 | 15 | function createModule($backend,$state) 16 | { 17 | var installerStage='install'; 18 | var config={}; 19 | // loads the config object above 20 | require($backend.getConfigPath())(getLoadingInterface()); 21 | 22 | var configModule={}; 23 | configModule.getConfig=getConfig; 24 | configModule.getOutgoingDir=getOutgoingDir; 25 | 26 | configModule.getInstallerStage=getInstallerStage; 27 | return configModule; 28 | 29 | /** 30 | * @name getInstallerStage 31 | * @return current installer stage ('install', 'uninstall' etc...) 32 | **/ 33 | function getInstallerStage() 34 | { 35 | return installerStage; 36 | } 37 | function cloneDeepAndParse(val) 38 | { 39 | if(_.isPlainObject(val)) 40 | { 41 | return _.mapValues(val,cloneDeepAndParse); 42 | } 43 | else if(_.isArray(val)) 44 | { 45 | return _.map(val,cloneDeepAndParse); 46 | } 47 | else if (_.isString(val)) { 48 | return parseStateStrings(val); 49 | } 50 | return val; 51 | } 52 | function parseStateStrings(val) 53 | { 54 | var parsed=$state.parseTemplate(val); 55 | if(parsed!==val) 56 | { 57 | return parseStateStrings(parsed); 58 | } 59 | return val; 60 | } 61 | /** 62 | * @name getConfig 63 | * @param path {String} property path 64 | * @return parsed value of the property if found 65 | * @example getConfig('pages[2]') will return the 3rd page in the current install stage 66 | **/ 67 | function getConfig(path) 68 | { 69 | return cloneDeepAndParse(_.get(config[installerStage],path)); 70 | } 71 | /** 72 | * @name getOutgoingDir 73 | * @return outgoing dir (should not be used on production) 74 | **/ 75 | function getOutgoingDir() 76 | { 77 | return path.resolve(config.options.outgoing); 78 | } 79 | 80 | function getLoadingInterface() 81 | { 82 | return { 83 | initConfig:function(v){config=v;} 84 | }; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/core-modules/fs/fs.js: -------------------------------------------------------------------------------- 1 | /** 2 | @name fs module 3 | @description file system operations 4 | **/ 5 | 'use strict'; 6 | 7 | var fs=require('fs-extra'); 8 | var BBPromise=require('bluebird'); 9 | 10 | module.exports=createModule; 11 | createModule.moduleName='$fs'; 12 | createModule.$inject=['$uiActions','$logger']; 13 | 14 | 15 | function createModule($uiActions,$logger) 16 | { 17 | 18 | var fsModule={}; 19 | fsModule.getWorkingDir=getWorkingDir; 20 | fsModule.isDirValid=isDirValid; 21 | fsModule.isDirEmpty=isDirEmpty; 22 | activate(); 23 | return fsModule; 24 | 25 | function activate() 26 | { 27 | registerUiActions(); 28 | } 29 | 30 | function registerUiActions() 31 | { 32 | $uiActions.registerAction('fs_getWorkingDir',[],getWorkingDir); 33 | $uiActions.registerAction('fs_isDirValid',['dir'],isDirValid); 34 | $uiActions.registerAction('fs_isDirEmpty',['dir'],isDirEmpty); 35 | } 36 | /** 37 | * @name getWorkingDir 38 | * @description registered as uiAction fs_getWorkingDir 39 | * @return current working dir 40 | **/ 41 | function getWorkingDir() 42 | { 43 | return process.cwd(); 44 | } 45 | 46 | function isDirValid(dir) 47 | { 48 | return BBPromise.promisify(fs.stat)(dir).then(function(res) 49 | { 50 | if(res.isFile()) 51 | { 52 | return false; 53 | } 54 | }).catch(function(err) 55 | { 56 | $logger.debug('isDirValid returned '+err); 57 | return false; 58 | }); 59 | } 60 | function isDirEmpty(dir) 61 | { 62 | return BBPromise.promisify(fs.readdir)(dir).then(function(res) 63 | { 64 | if(res.length>0) 65 | { 66 | return false; 67 | } 68 | return true; 69 | }); 70 | } 71 | 72 | 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/core-modules/job/core-jobs/copy.js: -------------------------------------------------------------------------------- 1 | /** 2 | @name copy corejob 3 | @description copy files/directories job 4 | @param subjobs {Array} 5 | @example {'type':'copy','settings':{'files':[ {'from':'<%=user.config.installDir%>/tomcat','to':'<%=user.config.installDir%>/tomcat2','size':109}]}} 6 | **/ 7 | 'use strict'; 8 | 9 | var BBPromise =require('bluebird'); 10 | var fs=require('fs-extra'); 11 | var ourNcp=require(__dirname+'/lib/ndjs_ncp'); 12 | var path=require('path'); 13 | var _=require('lodash'); 14 | var copyPromisified=BBPromise.promisify(ourNcp); 15 | 16 | module.exports=createModule; 17 | createModule.moduleName='$job_copy'; 18 | createModule.$inject=['$job','$logger']; 19 | 20 | function createModule($job,$logger) 21 | { 22 | $job.registerJobType(startJob,'copy'); 23 | 24 | function startJob(settings,callbackProgress,callbackCancelCalled,callbackPend,callbackAbort) 25 | { 26 | var totalSize=_.sum(settings.files,'size'); 27 | var extractedSoFar=0; 28 | var baseOutgiong=process.cwd(); 29 | return BBPromise.each(settings.files,function(fromTo) 30 | { 31 | var aborted=false; 32 | var pathFrom=path.resolve(baseOutgiong,fromTo.from); 33 | var ncpPendingCallback=function(params) 34 | { 35 | var message=''+params.error; 36 | var detail=''; 37 | if(params.error.code) 38 | { 39 | message=params.error.code; 40 | } 41 | if(params.error.path) 42 | { 43 | detail=params.error.path; 44 | } 45 | callbackPend(message,detail,{ 46 | retry:params.retry, 47 | ignore:params.ignore, 48 | abort:function(){ 49 | if(!aborted){callbackAbort();} 50 | aborted=true; 51 | params.abort();} 52 | }); 53 | }; 54 | return copyPromisified(pathFrom,fromTo.to,{transform:transformFunc,pendingCallback:ncpPendingCallback}).catch( 55 | function(err) 56 | { 57 | if(aborted) 58 | { 59 | return BBPromise.reject('ABORTED'); 60 | } 61 | else 62 | { 63 | throw err; 64 | } 65 | } 66 | ); 67 | }); 68 | 69 | function transformFunc(read, write,fileName) 70 | { 71 | fs.stat(fileName.name,function(err,stats) 72 | { 73 | if(err) 74 | { 75 | $logger.debug(err); 76 | } 77 | else { 78 | if(totalSize!==0) 79 | { 80 | extractedSoFar+=parseFloat(stats.size)/(1024*1024); 81 | var prog =Math.max(0,Math.min(1,extractedSoFar/totalSize)); 82 | callbackProgress(prog); 83 | } 84 | } 85 | }); 86 | read.pipe(write); 87 | } 88 | 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/core-modules/job/core-jobs/lib/ndjs_ncp.js: -------------------------------------------------------------------------------- 1 | // imported from fs-extra - added retry,abort,ignore callback and semicolons 2 | // imported from ncp (this is temporary, will rewrite) 3 | 4 | var fs = require('graceful-fs'); 5 | var path = require('path'); 6 | var utimes = require(path.dirname(require.resolve('fs-extra'))+'../../lib/util/utimes'); 7 | 8 | function ncp (source, dest, options, callback) { 9 | 'use strict'; 10 | if (!callback) { 11 | callback = options; 12 | options = {}; 13 | } 14 | var aborted=false; 15 | 16 | var basePath = process.cwd(); 17 | var currentPath = path.resolve(basePath, source); 18 | var targetPath = path.resolve(basePath, dest); 19 | 20 | var filter = options.filter; 21 | var transform = options.transform; 22 | var clobber = options.clobber !== false; 23 | var dereference = options.dereference; 24 | var preserveTimestamps = options.preserveTimestamps === true; 25 | 26 | var pendingCallback=options.pendingCallback; 27 | if(!pendingCallback) 28 | { 29 | throw new Error('No pendingCallback'); 30 | } 31 | var errs = null; 32 | 33 | var started = 0; 34 | var finished = 0; 35 | var running = 0; 36 | // this is pretty useless now that we're using graceful-fs 37 | // consider removing - said by fs-extra 38 | var limit = 1;//options.limit || 512; 39 | 40 | startCopy(currentPath); 41 | 42 | function startCopy (source) { 43 | started++; 44 | if (filter) { 45 | if (filter instanceof RegExp) { 46 | if (!filter.test(source)) { 47 | return doneOne(true); 48 | } 49 | } else if (typeof filter === 'function') { 50 | if (!filter(source)) { 51 | return doneOne(true); 52 | } 53 | } 54 | } 55 | return getStats(source); 56 | } 57 | 58 | function getStats (source) { 59 | var stat = dereference && fs.stat || fs.lstat; 60 | if (running >= limit) { 61 | return setTimeout(function () { 62 | if(!aborted){getStats(source);} 63 | },10); 64 | } 65 | running++; 66 | stat(source, function (err, stats) { 67 | if (err) {return onError(err, 68 | function(){return getStats(source);});} 69 | 70 | // We need to get the mode from the stats object and preserve it. 71 | var item = { 72 | name: source, 73 | mode: stats.mode, 74 | mtime: stats.mtime, // modified time 75 | atime: stats.atime, // access time 76 | stats: stats // temporary 77 | }; 78 | 79 | if (stats.isDirectory()) { 80 | return onDir(item); 81 | } else if (stats.isFile()) { 82 | return onFile(item); 83 | } else if (stats.isSymbolicLink()) { 84 | // Symlinks don't really need to know about the mode. 85 | return onLink(source); 86 | } 87 | }); 88 | } 89 | 90 | function onFile (file) { 91 | var target = file.name.replace(currentPath, targetPath); 92 | isWritable(target, function (writable) { 93 | if (writable) { 94 | copyFile(file, target); 95 | } else { 96 | if (clobber) { 97 | rmFile(target, function () { 98 | copyFile(file, target); 99 | }); 100 | } else { 101 | doneOne(); 102 | } 103 | } 104 | }); 105 | } 106 | 107 | function copyFile (file, target) { 108 | var readStream = fs.createReadStream(file.name); 109 | var writeStream = fs.createWriteStream(target, { mode: file.mode }); 110 | 111 | var errFunc=function(err) 112 | { 113 | return onError(err,function(){return copyFile(file, target);}); 114 | }; 115 | readStream.on('error', errFunc); 116 | writeStream.on('error', errFunc); 117 | 118 | if (transform) { 119 | transform(readStream, writeStream, file); 120 | } else { 121 | writeStream.on('open', function () { 122 | readStream.pipe(writeStream); 123 | }); 124 | } 125 | 126 | writeStream.once('finish', function () { 127 | fs.chmod(target, file.mode, function (err) { 128 | if (err) {return errFunc(err);} 129 | if (preserveTimestamps) { 130 | utimes.utimesMillis(target, file.atime, file.mtime, function (err) { 131 | if (err) {return errFunc(err);} 132 | return doneOne(); 133 | }); 134 | } else { 135 | doneOne(); 136 | } 137 | }); 138 | }); 139 | } 140 | 141 | function rmFile (file, done) { 142 | var errFunc=function(err) 143 | { 144 | return onError(err,function(){return rmFile (file, done);}); 145 | }; 146 | fs.unlink(file, function (err) { 147 | if (err) { 148 | return errFunc(err); 149 | } 150 | 151 | return done(); 152 | }); 153 | } 154 | 155 | function onDir (dir) { 156 | var target = dir.name.replace(currentPath, targetPath); 157 | isWritable(target, function (writable) { 158 | if (writable) { 159 | return mkDir(dir, target); 160 | } 161 | copyDir(dir.name); 162 | }); 163 | } 164 | 165 | function mkDir (dir, target) { 166 | var errFunc=function(err) 167 | { 168 | return onError(err,function(){return mkDir (dir, target);}); 169 | }; 170 | fs.mkdir(target, dir.mode, function (err) { 171 | if (err) {return errFunc(err);} 172 | // despite setting mode in fs.mkdir, doesn't seem to work 173 | // so we set it here. 174 | fs.chmod(target, dir.mode, function (err) { 175 | if (err){ return errFunc(err);} 176 | copyDir(dir.name); 177 | }); 178 | }); 179 | } 180 | 181 | function copyDir (dir) { 182 | var errFunc=function(err) 183 | { 184 | return onError(err,function(){return copyDir (dir);}); 185 | }; 186 | fs.readdir(dir, function (err, items) { 187 | if (err) {return errFunc(err);} 188 | items.forEach(function (item) { 189 | startCopy(path.join(dir, item)); 190 | }); 191 | return doneOne(); 192 | }); 193 | } 194 | 195 | function onLink (link) { 196 | var errFunc=function(err) 197 | { 198 | return onError(err,function(){return onLink (link);}); 199 | }; 200 | var target = link.replace(currentPath, targetPath); 201 | fs.readlink(link, function (err, resolvedPath) { 202 | if (err) {return errFunc(err);} 203 | checkLink(resolvedPath, target); 204 | }); 205 | } 206 | 207 | function checkLink (resolvedPath, target) { 208 | var errFunc=function(err) 209 | { 210 | return onError(err,function(){return checkLink (resolvedPath, target) ;}); 211 | }; 212 | if (dereference) { 213 | resolvedPath = path.resolve(basePath, resolvedPath); 214 | } 215 | isWritable(target, function (writable) { 216 | if (writable) { 217 | return makeLink(resolvedPath, target); 218 | } 219 | fs.readlink(target, function (err, targetDest) { 220 | if (err) {return errFunc(err);} 221 | 222 | if (dereference) { 223 | targetDest = path.resolve(basePath, targetDest); 224 | } 225 | if (targetDest === resolvedPath) { 226 | return doneOne(); 227 | } 228 | return rmFile(target, function () { 229 | makeLink(resolvedPath, target); 230 | }); 231 | }); 232 | }); 233 | } 234 | 235 | function makeLink (linkPath, target) { 236 | var errFunc=function(err) 237 | { 238 | return onError(err,function(){return makeLink (linkPath, target) ;}); 239 | }; 240 | fs.symlink(linkPath, target, function (err) { 241 | if (err){ return errFunc(err);} 242 | return doneOne(); 243 | }); 244 | } 245 | 246 | function isWritable (path, done) { 247 | fs.lstat(path, function (err) { 248 | if (err) { 249 | if (err.code === 'ENOENT') {return done(true);} 250 | return done(false); 251 | } 252 | return done(false); 253 | }); 254 | } 255 | function onError(err,retryCallback) 256 | { 257 | pendingCallback( 258 | { 259 | error:err, 260 | ignore:function() 261 | { 262 | doneOne(); 263 | }, 264 | abort:function(){ 265 | callback(err); 266 | }, 267 | retry:function() 268 | { 269 | retryCallback(); 270 | } 271 | }); 272 | 273 | } 274 | function _onError (err) { 275 | if (options.stopOnError) { 276 | return callback(err); 277 | } else if (!errs && options.errs) { 278 | errs = fs.createWriteStream(options.errs); 279 | } else if (!errs) { 280 | errs = []; 281 | } 282 | if (typeof errs.write === 'undefined') { 283 | errs.push(err); 284 | } else { 285 | errs.write(err.stack + '\n\n'); 286 | } 287 | return doneOne(); 288 | } 289 | 290 | function doneOne (skipped) { 291 | if (!skipped){ running--;} 292 | finished++; 293 | if ((started === finished) && (running === 0)) { 294 | if (callback !== undefined) { 295 | return errs && callback(errs) || callback(null); 296 | } 297 | } 298 | } 299 | } 300 | 301 | module.exports = ncp; 302 | -------------------------------------------------------------------------------- /src/core-modules/job/core-jobs/multi.js: -------------------------------------------------------------------------------- 1 | /** 2 | @name multi corejob 3 | @description run sub jobs one after the other 4 | @param subjobs {Array} 5 | @example {type:"multi",settings:{subJobs:[{another-job1},{another-job2}]} 6 | **/ 7 | 'use strict'; 8 | 9 | module.exports=createModule; 10 | createModule.moduleName='$job_multi'; 11 | createModule.$inject=['$job','$logger']; 12 | 13 | var BBPromise =require('bluebird'); 14 | 15 | function createModule($job,$logger) 16 | { 17 | $job.registerJobType(startJob,'multi'); 18 | 19 | function startJob(settings,callbackProgress,callbackCancelCalled,callbackPend,callbackAbort) 20 | { 21 | if(!settings.subJobs || !Array.isArray(settings.subJobs) ) 22 | { 23 | $logger.error('multi job settings.subJobs is not an array'); 24 | throw new Error('multi job settings.subJobs is not an array'); 25 | } 26 | return BBPromise.each(settings.subJobs,function(subJob,index,length){ 27 | if(callbackCancelCalled()) 28 | { 29 | throw new Error('NDJS_ABORT'); 30 | } 31 | var thisJobProgressCallback=function(subProgress) 32 | { 33 | callbackProgress(index * 1.0/length +subProgress/length); 34 | }; 35 | thisJobProgressCallback(0); 36 | return $job.startJob(subJob.type,subJob.settings,thisJobProgressCallback,callbackCancelCalled,callbackPend,callbackAbort); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/core-modules/job/core-jobs/sfx.js: -------------------------------------------------------------------------------- 1 | /** 2 | @name sfx corejob 3 | @description extract files compressed into the installer file (mocks by copy during debug) thorw retry error when insufficient premmissions 4 | @param files {Array} array of objects with properties: from (source of extract), to (destenation) and optionally size:MB to extract for progress estimation 5 | @example 6 | settings example {'type':'extract','settings':{'files':[ {'from':'tomcat','to':'<%=user.config.installDir%>','size':109}, 7 | {'from':'jre','to':'<%=user.config.installDir%>/jre','size':93}, 8 | {'from':'webapp','to':'<%=user.config.installDir%>/webapps/ROOT','size':0}]}} 9 | **/ 10 | 'use strict'; 11 | 12 | 13 | var BBPromise =require('bluebird'); 14 | var childProcess=require('child_process'); 15 | var _=require('lodash'); 16 | var path=require('path'); 17 | 18 | module.exports=createModule; 19 | createModule.moduleName='$job_sfx'; 20 | createModule.$inject=['$job','$config','$logger']; 21 | 22 | function createModule($job,$config,$logger) 23 | { 24 | $job.registerJobType(startJob,'sfx'); 25 | 26 | 27 | 28 | function startJob(settings,callbackProgress,callbackCancelCalled,callbackPend,callbackAbort) 29 | { 30 | var baseOutgiong=$config.getOutgoingDir(); 31 | if(!process.env.hasOwnProperty('ND_SFX_EXE')) 32 | { 33 | //we are running in nd_debug and files need copying from outgoing, not sfx 34 | var newSettings=_.clone(settings,true); 35 | newSettings.files=_.map(newSettings.files,function(file) 36 | { 37 | var ret= { 38 | from:path.resolve(baseOutgiong,file.from), 39 | to:path.resolve(baseOutgiong,file.to) 40 | }; 41 | if(file.size) 42 | { 43 | ret.size=file.size; 44 | } 45 | return ret; 46 | }); 47 | return $job.startJob('copy',newSettings,callbackProgress,callbackCancelCalled,callbackPend,callbackAbort); 48 | } 49 | else 50 | { 51 | return selfExtract(); 52 | } 53 | function selfExtract() 54 | { 55 | var extractedByPrevious=0;//accumulate all extractions done before current task in MB 56 | var totalSize=_.sum(settings.files,'size'); 57 | 58 | return BBPromise.each(settings.files,function(fromTo) 59 | { 60 | return new BBPromise(function(resolve,reject){ 61 | if(callbackCancelCalled()) 62 | { 63 | callbackAbort(); 64 | reject('NDJS_ABORT'); 65 | } 66 | var pathFrom=fromTo.from; 67 | var pathTo=fromTo.to; 68 | var args=['--block2','--block2Target',pathTo]; 69 | args.push('--block2Path'); 70 | args.push(pathFrom); 71 | var status={currentFile:'',writeError:false,extractedByThisJob:0}; 72 | $logger.debug('extracting '+pathFrom +' to '+pathTo); 73 | 74 | var processOutputBuffer=''; 75 | var extractProcess=childProcess.spawn(process.env.ND_SFX_EXE,args); 76 | 77 | extractProcess.stdout.on('data', function(data) { 78 | processOutputBuffer+=data; 79 | var lines=processOutputBuffer.split(/\r?\n/); 80 | if(data[data.length-1] !== '\n') 81 | { 82 | //if the final line has not finished printing save it for later parseing 83 | processOutputBuffer=lines.pop(); 84 | } 85 | else 86 | { 87 | processOutputBuffer=''; 88 | } 89 | processSfxOutputLogLines(lines,status); 90 | if(status.writeError) 91 | { 92 | callbackPend('EPERM',status.currentFile,{ 93 | retry:function(){extractProcess.stdin.write('r\n');}, 94 | ignore:function(){extractProcess.stdin.write('i\n');}, 95 | abort:function(){callbackAbort();reject();} 96 | }); 97 | } 98 | }); 99 | extractProcess.on('close',function(code){ 100 | if(!code){code=-1;} 101 | extractedByPrevious+=status.extractedByThisJob; 102 | resolve(code); 103 | }); 104 | }); 105 | }); 106 | function processSfxOutputLogLines(lines,status) 107 | { 108 | for(var i=0;i0) 117 | { 118 | status.extractedByThisJob=parseFloat(byteNums[0])/(1024*1024); 119 | var extractedTotal=status.extractedByThisJob+extractedByPrevious; 120 | var prog =Math.max(0,Math.min(1,extractedTotal/totalSize)); 121 | callbackProgress(prog); 122 | } 123 | } 124 | else if(line.lastIndexOf('Write error', 0) === 0) 125 | { 126 | status.writeError=true; 127 | } 128 | else if(line.lastIndexOf('Write finish error', 0) === 0) 129 | { 130 | $logger.warn('Write finish error:'+line); 131 | } 132 | else if(line.lastIndexOf('Write finish fatal error', 0) === 0) 133 | { 134 | $logger.error('extractor fatal error:'+line); 135 | } 136 | else if(line.lastIndexOf('Writing file', 0) === 0) 137 | { 138 | status.currentFile=line.substring(14); 139 | status.writeError=false; 140 | } 141 | else if(line.lastIndexOf('Written file', 0) === 0) 142 | { 143 | status.writeError=false; 144 | } 145 | else if(line.lastIndexOf('Total size:', 0) === 0) 146 | { 147 | $logger.debug('total size '+parseInt(line.substr('Total size: '.length),10)); 148 | } 149 | $logger.debug('sfx: '+line); 150 | } 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/core-modules/job/job.js: -------------------------------------------------------------------------------- 1 | /** 2 | @name job module 3 | @description job queue operations 4 | **/ 5 | 'use strict'; 6 | 7 | module.exports=createModule; 8 | createModule.moduleName='$job'; 9 | createModule.$inject=['$uiActions','$config','$logger','$backend','$notifications']; 10 | 11 | 12 | function createModule($uiActions,$config,$logger,$backend,$notifications) 13 | { 14 | var jobTypes={}; 15 | var jobQueue={}; 16 | var canceledJobs={}; 17 | 18 | var pendingCounter=0; 19 | var pendingQueue={}; 20 | 21 | var jobModule={}; 22 | 23 | jobModule.getAllJobs=getAllJobs; 24 | jobModule.registerJobType=registerJobType; 25 | jobModule.getRegisteredJobTypes=getRegisteredJobTypes; 26 | jobModule.startJob=startJob; 27 | jobModule.startNamedJob=startNamedJob; 28 | jobModule.cancelNamedJob=cancelNamedJob; 29 | jobModule.releasePendingJob=releasePendingJob; 30 | 31 | activate(); 32 | return jobModule; 33 | 34 | function activate() 35 | { 36 | registerUiActions(); 37 | } 38 | function getJobsFromConfig() 39 | { 40 | return $config.getConfig('jobs'); 41 | } 42 | 43 | function registerUiActions() 44 | { 45 | $uiActions.registerAction('job_getAllJobs',[],getAllJobs); 46 | $uiActions.registerAction('job_startNamedJob',['name','force'],startNamedJob); 47 | $uiActions.registerAction('job_cancelNamedJob',['name'],cancelNamedJob); 48 | $uiActions.registerAction('job_releasePendingJob',['id','answer'],releasePendingJob); 49 | } 50 | /** 51 | * @name getAllJobs 52 | * @description registered as UI action 53 | * @return return all jobs names from the current install stage in ndfile 54 | **/ 55 | function getAllJobs() 56 | { 57 | return Object.keys(getJobsFromConfig()); 58 | } 59 | /** 60 | * @name getRegisteredJobTypes 61 | * @return all registered job types and creation callbacks 62 | **/ 63 | function getRegisteredJobTypes() 64 | { 65 | return jobTypes; 66 | } 67 | /** 68 | * @name getAllJobs 69 | * @param path {String} property path 70 | * @return parsed value of the property if found 71 | * @example getConfig('pages[2]') will return the 3rd page in the current install stage 72 | **/ 73 | function registerJobType(factory,setName) 74 | { 75 | var jobTypeName=setName; 76 | if(!jobTypeName && factory.jobType) 77 | { 78 | jobTypeName=factory.jobType; 79 | } 80 | if(!jobTypeName) 81 | { 82 | throw new Error('Job type does not have a name (.jobType property)'); 83 | } 84 | if(jobTypes.hasOwnProperty(jobTypeName)) 85 | { 86 | throw new Error('Job type '+name + ' alrady registered'); 87 | } 88 | jobTypes[jobTypeName]=factory; 89 | } 90 | /** 91 | * @name startJob 92 | * @param jobType {String} type of job to strart 93 | * @param settings {Object} settings of job instance 94 | * @param callbackProgress {Function} callback to report progress - should pass a number between 0(no progress) and 1 (job done) 95 | * @param callbackCancelCalled {Function} callback to be called for checking if job cancel was asked - should return true if should cancel 96 | * @param callbackPend {Function} callback to be called when job is waiting for user decisions. check copy core job for refernce 97 | * @return promise of the new job or throws error 98 | * @example startJob('extract',{'files':[{'from':'a','to':'b','size':109}]}) will start extracting from a to b 99 | **/ 100 | function startJob(jobType,settings,callbackProgress,callbackCancelCalled,callbackPend,callbackAbort) 101 | { 102 | if(!jobTypes.hasOwnProperty(jobType)) 103 | { 104 | throw new Error('Job type '+jobType + ' unknown'); 105 | } 106 | var jobPromise= jobTypes[jobType](settings,callbackProgress,callbackCancelCalled,callbackPend,callbackAbort); 107 | if(!jobPromise.then) 108 | { 109 | throw new Error('Job type '+jobType + '.create returned a non promise'); 110 | } 111 | return jobPromise; 112 | } 113 | 114 | /** 115 | * @name cancelNamedJob 116 | * @description registered UI action - signal to cancel a job - does not force anything 117 | * @param name {String} job name 118 | * @example cancelNamedJob('main'); 119 | **/ 120 | function cancelNamedJob(name) 121 | { 122 | canceledJobs[name]=true; 123 | } 124 | /** 125 | * @name releasePendingJob 126 | * @description registered UI action - release a job pending for user decisions 127 | * @param id {Number} id got from job_pending notification 128 | * @param answer {String} action to take - also in job_pending notification 129 | * @param options {Object} additional argument to send 130 | * @example releasePendingJob(2,'ignore',{}); 131 | **/ 132 | function releasePendingJob(id,answer,options) 133 | { 134 | pendingQueue[id][answer](options); 135 | delete pendingQueue[id]; 136 | } 137 | /** 138 | * @name startNamedJob 139 | * @description registered UI action 140 | * @param name {String} name of job in ndfile.js 141 | * @param force {Boolean} start even if the job was started 142 | * @return parsed value of the property if found 143 | * @example startNamedJob(main') will start job 'main' of the current install stage 144 | **/ 145 | function startNamedJob(name,force) 146 | { 147 | if(typeof force === 'undefined'){force=false;} 148 | var jobInConfig=getJobsFromConfig()[name]; 149 | if(!jobInConfig) 150 | { 151 | $logger.error('Job '+name + ' was not found in configuration'); 152 | throw new Error('Job '+name + ' was not found in configuration'); 153 | } 154 | var type=jobInConfig.type; 155 | if(jobQueue.hasOwnProperty(name) && !force) 156 | { 157 | if(jobQueue[name].isFulfilled())//it is a promise 158 | { 159 | $logger.debug('Job start requestd for '+name + ' but it has already finished'); 160 | $notifications.pushNotification('job_status',{jobName:name,progress:1}); 161 | return; 162 | } 163 | else { 164 | throw new Error('Job '+name + ' already started'); 165 | } 166 | } 167 | if(canceledJobs.hasOwnProperty(name)) 168 | { 169 | delete canceledJobs[name]; 170 | } 171 | 172 | var callbackProgress=function(progress) 173 | { 174 | $notifications.pushNotification('job_status',{jobName:name,progress:progress}); 175 | }; 176 | var callbackCancelCalled=function() 177 | { 178 | return canceledJobs.hasOwnProperty(name); 179 | }; 180 | var jobWasAborted=false; 181 | var callbackAbort=function() 182 | { 183 | $notifications.pushNotification('job_aborted',{jobName:name}); 184 | jobWasAborted=true; 185 | return; 186 | }; 187 | var callbackPend=function(message,detailed,options) 188 | { 189 | pendingCounter++; 190 | var id='Pending'+pendingCounter; 191 | pendingQueue[id]=options; 192 | $notifications.pushNotification('job_pending',{jobName:name,message:message,detailed:detailed,pendingId:id,options:Object.keys(options)}); 193 | }; 194 | var job=startJob(type,jobInConfig.settings,callbackProgress,callbackCancelCalled,callbackPend,callbackAbort); 195 | job.then(function(){ 196 | if(!jobWasAborted){ 197 | callbackProgress(1);} 198 | //delete jobQueue[name];//should we delete? TODO:rethink 199 | }).catch(function(err) 200 | { 201 | if(!jobWasAborted)// if a job return this token its not marked as failed but as aborted 202 | { 203 | $logger.error('job failed: '+name+ ' '+err + (err.stack || '')); 204 | $notifications.pushNotification('job_failed',{jobName:name,details:''+err}); 205 | } 206 | //delete jobQueue[name];//should we delete? TODO:rethink 207 | }); 208 | jobQueue[name]=job; 209 | 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/core-modules/notifications/notifications.js: -------------------------------------------------------------------------------- 1 | /** 2 | @name notifications module 3 | @description push notifications to UI 4 | **/ 5 | 'use strict'; 6 | 7 | //var _=require('lodash'); 8 | var BBPromise=require('bluebird'); 9 | 10 | module.exports=createModule; 11 | createModule.moduleName='$notifications'; 12 | createModule.$inject=['$uiActions']; 13 | 14 | function createModule($uiActions) 15 | { 16 | var notificationsQueue=[]; 17 | var notificationsListeners=[]; 18 | 19 | var notificationsModule={}; 20 | notificationsModule.getNotificationsFromIdx=getNotificationsFromIdx; 21 | notificationsModule.pushNotification=pushNotification; 22 | notificationsModule.listenToNotifications=listenToNotifications; 23 | registerUiActions(); 24 | return notificationsModule; 25 | 26 | function registerUiActions() 27 | { 28 | $uiActions.registerAction('notifications_getFromIdx',['idx'],getNotificationsFromIdx); 29 | } 30 | /** 31 | * @name getNotificationsFromIdx 32 | * @description registered UI action 33 | * @param idx {Number} 34 | * @return get all notfications starting from this idx 35 | **/ 36 | function getNotificationsFromIdx(idx) 37 | { 38 | return new BBPromise(function(resolve,reject) 39 | { 40 | void reject; 41 | var fromIdx=Math.max(notificationsQueue.length,idx); 42 | resolve(notificationsQueue.splice(0,fromIdx)); 43 | }); 44 | } 45 | /** 46 | * @name pushNotification 47 | * @description push notfications to UI and custom listeners 48 | * @param name {String} name of event 49 | * @param value {Object} event data 50 | * @example pushNotification('downloadComplete',{package:abc.zip}) 51 | **/ 52 | function pushNotification(name,value) 53 | { 54 | var notification={name:name,value:value,time: Date.now()}; 55 | notificationsQueue.push(notification); 56 | notificationsListeners.forEach(function(listener) 57 | { 58 | listener(notification); 59 | }); 60 | } 61 | 62 | /** 63 | * @name listenToNotifications 64 | * @description push notfications to a custom listener for easier server side global notifications 65 | * @param listener {Function} will be called with notification full data {name,value,time} 66 | * @example listenToNotifications(myCallback) 67 | **/ 68 | function listenToNotifications(listener) 69 | { 70 | notificationsListeners.push(listener); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/core-modules/pages/pages.js: -------------------------------------------------------------------------------- 1 | /** 2 | @name pages module 3 | @description pages handling 4 | **/ 5 | 'use strict'; 6 | 7 | 8 | module.exports=createModule; 9 | createModule.moduleName='$pages'; 10 | createModule.$inject=['$uiActions','$config']; 11 | 12 | function createModule($uiActions,$config) 13 | { 14 | var pagesModule={}; 15 | pagesModule.getPages=getPages; 16 | registerUiActions(); 17 | return pagesModule; 18 | 19 | function registerUiActions() 20 | { 21 | $uiActions.registerAction('pages_getPages',[],getPages); 22 | } 23 | 24 | 25 | /** 26 | * @name getPages 27 | * @return all pages an their current data for current installation stage 28 | **/ 29 | function getPages() 30 | { 31 | var pages=$config.getConfig('pages'); 32 | if(pages===undefined) 33 | { 34 | throw new Error('no pages found'); 35 | } 36 | return pages;//_.pluck(pages,'name'); 37 | } 38 | 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/core-modules/state/state.js: -------------------------------------------------------------------------------- 1 | /** 2 | @name state module 3 | @description installer stage - holds settings set from the UI 4 | **/ 5 | 'use strict'; 6 | 7 | module.exports=createModule; 8 | createModule.moduleName='$state'; 9 | createModule.$inject=['$uiActions','$logger']; 10 | 11 | var _ =require('lodash'); 12 | 13 | function createModule($uiActions,$logger) 14 | { 15 | var state={user:{}}; 16 | var stateModuleInstance={}; 17 | stateModuleInstance.getSettings=getSettings; 18 | stateModuleInstance.setSettings=setSettings; 19 | stateModuleInstance.parseTemplate=parseTemplate; 20 | registerUiActions(); 21 | return stateModuleInstance; 22 | function registerUiActions() 23 | { 24 | $uiActions.registerAction('state_setUserSettings',['key','value'], 25 | function(key,val) 26 | { 27 | setSettings('user.'+key,val); 28 | }); 29 | 30 | $uiActions.registerAction('state_getUserSettings',['key'], 31 | function(key) 32 | { 33 | var missingToken={}; 34 | var got= getSettings('user.'+key,missingToken); 35 | if(got===missingToken) 36 | { 37 | return {status:'not_found',value:{}}; 38 | } 39 | else 40 | { 41 | return {status:'found',value:got}; 42 | } 43 | }); 44 | } 45 | /** 46 | * @name getSettings 47 | * @param settingsPath {String} property path of the settings 48 | * @param defaultValue {Object} defualt value to be returned of property not found 49 | * @return current value of .settingsPath or defaultValue if not found 50 | **/ 51 | function getSettings(settingsPath,defaultValue) 52 | { 53 | var got= _.get(state,settingsPath,defaultValue); 54 | if(!got && !defaultValue) 55 | { 56 | $logger.warn('requested settings '+settingsPath +' missing and no default value supplied'); 57 | } 58 | return got; 59 | } 60 | /** 61 | * @name setSettings 62 | * @description setUserSettings ui actions calls this function with .user prefix. this functions set current state 63 | * @param settingsPath {String} name of event 64 | * @param value {Object} 65 | **/ 66 | function setSettings(settingsPath,value) 67 | { 68 | _.set(state,settingsPath,value); 69 | } 70 | /** 71 | * @name parseTemplate 72 | * @param str {String} name of event 73 | * @return lodash template parsed value of str according to current state 74 | **/ 75 | function parseTemplate(str) 76 | { 77 | return _.template(str)(state); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/core-modules/ui-actions/ui-actions.js: -------------------------------------------------------------------------------- 1 | /** 2 | @name state module 3 | @description installer stage - holds settings set from the UI 4 | **/ 5 | 'use strict'; 6 | 7 | var _=require('lodash'); 8 | var BBPromise=require('bluebird'); 9 | 10 | module.exports=createModule; 11 | createModule.moduleName='$uiActions'; 12 | createModule.$inject=[]; 13 | 14 | function createModule() 15 | { 16 | var uiActionsModule={}; 17 | uiActionsModule.registerAction=registerAction; 18 | uiActionsModule.getRegisteredActions=getRegisteredActions; 19 | uiActionsModule.callAction=callAction; 20 | var uiActions={}; 21 | return uiActionsModule; 22 | 23 | /** 24 | * @name getRegisteredActions 25 | * @description get currently registered UI actions 26 | * @return mapping action-name -> array of param-names 27 | **/ 28 | function getRegisteredActions() 29 | { 30 | return _.cloneDeep(_.mapValues(uiActions,function(allData){return allData.paramNames;})); 31 | } 32 | /** 33 | * @name registerAction 34 | * @description register a new UI action 35 | * @param name {String} name of event 36 | * @param paramNames {Array} array of paramerter names 37 | * @param action {Function} callback of action( could be syncronous or retrun a pormise) 38 | * @example rregisterAction('notifications_getNotificationsFromIdx',['idx'],getNotificationsFromIdx) 39 | **/ 40 | function registerAction(name,paramNames,action) 41 | { 42 | uiActions[name]={name:name,paramNames:paramNames,action:action}; 43 | } 44 | 45 | /** 46 | * @name pushNotification 47 | * @description push notfications to UI 48 | * @param name {String} name of event 49 | * @param value {Object} sevent data 50 | * @example pushNotification('downloadComplete',{package:abc.zip}) 51 | **/ 52 | function callAction(name,paramArray) 53 | { 54 | return new BBPromise(function(resolve,reject) 55 | { 56 | /*if(name!=='getNotificationsFromIdx')//this just flood the log 57 | { 58 | $logger.debug('callUiAction '+name +', '+ JSON.stringify(paramArray)); 59 | }*/ 60 | /*if(!Array.isArray(paramArray)) 61 | { 62 | paramArray = _.values(paramArray); 63 | }*/ 64 | if(!uiActions.hasOwnProperty(name)) 65 | { 66 | reject('No Action named '+name); 67 | return; 68 | } 69 | if(paramArray.length!==uiActions[name].paramNames.length) 70 | { 71 | reject('Wrong number of arguments passed to '+name); 72 | return; 73 | } 74 | resolve(uiActions[name].action.apply(null,paramArray)); 75 | }).then(function(result) 76 | { 77 | if(result!==false && !result) 78 | { 79 | return {};//so we always return a valid json 80 | } 81 | return result; 82 | }); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/core-modules/utils/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs=require('fs'); 4 | var BBPromise=require('bluebird'); 5 | var _=require('lodash'); 6 | 7 | module.exports=createModule; 8 | createModule.moduleName='$utils'; 9 | createModule.$inject=[]; 10 | 11 | function createModule(backendInstance) 12 | { 13 | void backendInstance; 14 | var utilsModule={}; 15 | utilsModule.getFileContentSync=getFileContentSync; 16 | utilsModule.setXmlFileProps=setXmlFileProps; 17 | utilsModule.isAlphanumeric=isAlphanumeric; 18 | utilsModule.runShellCmdAndDetach=runShellCmdAndDetach; 19 | return utilsModule; 20 | 21 | function runShellCmdAndDetach(cmd,argArray,cwd) 22 | { 23 | return new BBPromise(function(resolve) 24 | { 25 | var spawn = require('child_process').spawn; 26 | //var out = fs.openSync('./out.log', 'a'), 27 | //var err = fs.openSync('./out.log', 'a'); 28 | var options={detached:true,stdio: [ 'ignore', 'ignore', 'ignore' ]}; 29 | if(cwd) 30 | { 31 | options.cwd=cwd; 32 | } 33 | 34 | 35 | spawn(cmd,argArray, options).unref(); 36 | setTimeout(resolve,100);//takes some time to detach 37 | }); 38 | } 39 | 40 | function setXmlFileProps(pathToFile,propsMappings) 41 | { 42 | return BBPromise.promisify(fs.readFile)(pathToFile,'utf8').then( 43 | function(fileContent) 44 | { 45 | var xpath = require('xpath'); 46 | var dom = require('xmldom').DOMParser; 47 | var doc = new dom().parseFromString(fileContent); 48 | _.forEach(propsMappings,function(val,key) 49 | { 50 | var nodes = xpath.select(key, doc); 51 | _.forEach(nodes,function(node) 52 | { 53 | if(node.nodeType===2)//atribbutes 54 | { 55 | node.value=''+val; 56 | } 57 | else if(node.nodeType===3)//text 58 | { 59 | node.data=''+val; 60 | } 61 | else 62 | { 63 | node.value=''+val; 64 | } 65 | }); 66 | }); 67 | return doc.toString(); 68 | 69 | }).then(function(newFileContent) 70 | { 71 | return BBPromise.promisify(fs.writeFile)(pathToFile,newFileContent); 72 | }); 73 | } 74 | 75 | function getFileContentSync(path) 76 | { 77 | return fs.readFileSync(path).toString(); 78 | } 79 | 80 | function isAlphanumeric(str) 81 | { 82 | return str && /^[0-9a-zA-Z]+$/.test(str); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | var logger={};//require('winston'); 4 | logger.level = 'debug'; 5 | logger.debug=logger.error=logger.info=logger.warn=function(msg){console.log(msg);}; 6 | module.exports=logger; 7 | -------------------------------------------------------------------------------- /src/module-loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var moduleLoader={}; 4 | moduleLoader.getLoader=getLoader; 5 | moduleLoader.getSortedByDepends=getSortedByDepends; 6 | module.exports=moduleLoader; 7 | 8 | var _=require('lodash'); 9 | var toposort= require('toposort'); 10 | var anonCounter=0; 11 | function getAnonymousName() 12 | { 13 | var newName='AnonymousModule'+anonCounter; 14 | anonCounter++; 15 | return newName; 16 | } 17 | function getLoaderFromFullModule(obj) 18 | { 19 | var dependencies=[]; 20 | var name=obj.moduleName; 21 | if(!name) 22 | { 23 | name = getAnonymousName(); 24 | } 25 | if(obj.$inject) 26 | { 27 | dependencies=obj.$inject; 28 | } 29 | return {factory:obj,name:name,dependencies:dependencies}; 30 | } 31 | function getLoader(ndjsModule) 32 | { 33 | //we support 3 kinds of inputs: path to factory module(implements createModule,getName) 34 | // factory object(implements createModule,getName) 35 | // singleton instance(implements getName,instance) 36 | 37 | if(_.isString(ndjsModule))//path to module 38 | { 39 | var requiredModule; 40 | try { 41 | requiredModule=require(ndjsModule); 42 | } catch (e) { 43 | throw new Error('Error while trying to load '+ndjsModule +': '+e); 44 | } 45 | if(typeof requiredModule !== 'function') 46 | { 47 | throw new Error('Module '+ndjsModule +' does not return a factory function'); 48 | } 49 | return getLoaderFromFullModule(requiredModule); 50 | 51 | } 52 | else if(typeof ndjsModule === 'function'){ 53 | return getLoaderFromFullModule(ndjsModule); 54 | } 55 | else if(ndjsModule.moduleName && ndjsModule.instance) 56 | {//if this is a singleton instance it needs to have a name to be injected 57 | var factory=function(){return ndjsModule.instance;}; 58 | return {factory:factory,name:ndjsModule.moduleName,dependencies:[]}; 59 | } 60 | else { 61 | throw new Error('Module format invalid'); 62 | } 63 | } 64 | 65 | /** 66 | @param moduleLoaders array of moduleLoader s 67 | @return array of module loaders topologically sorted by dependencies(dependencies comes before dependents) 68 | **/ 69 | function getSortedByDepends(moduleLoaders) 70 | { 71 | var graphEdges=[]; 72 | var modulesByName=_.indexBy(moduleLoaders,'name'); 73 | //add graphEdges each is like [depnds,dependency] 74 | _.forEach(moduleLoaders,function(moduleLoader) 75 | { 76 | if(moduleLoader.dependencies===[]) 77 | { 78 | graphEdges.push(['{}',moduleLoader.name]); 79 | } 80 | else { 81 | moduleLoader.dependencies.forEach( 82 | function(dependency) 83 | { 84 | if(!modulesByName.hasOwnProperty(dependency)) 85 | { 86 | throw new Error('Module '+dependency + ' not found and is required by '+moduleLoader.name); 87 | } 88 | graphEdges.push([dependency,moduleLoader.name]); 89 | } 90 | ); 91 | } 92 | }); 93 | 94 | var modulesLoadOrder=[]; 95 | try { 96 | modulesLoadOrder=_.filter(toposort(graphEdges),function(x){return x!=='{}';});//remove {} (the base of the graph) 97 | } catch (e) { 98 | throw new Error('Modules dependency order error: ' +e); 99 | } 100 | 101 | return _.map(modulesLoadOrder,function(moduleName){return modulesByName[moduleName];}); 102 | } 103 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "browser": true, 3 | "node": true, 4 | 5 | "curly": true, 6 | "eqeqeq": true, 7 | "es3": true, 8 | "newcap": false, 9 | "noarg": true, 10 | "nonew": true, 11 | "quotmark": "single", 12 | "strict": true, 13 | "trailing": true, 14 | "undef": true, 15 | "unused": true, 16 | 17 | "globals": { 18 | "describe": true, 19 | "it":true, 20 | "should":true, 21 | "expect":true, 22 | "beforeEach":true, 23 | "before":true, 24 | "after":true, 25 | "JSON":true, 26 | "__dirname":true 27 | 28 | 29 | 30 | 31 | 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/back-instance-mocks/mock-module1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | createModule.moduleName='mock1'; 3 | createModule.$inject=[]; 4 | module.exports=createModule; 5 | createModule.func=function(){};//overrriden in tests 6 | function createModule(){createModule.args=arguments;createModule.func();return {};} 7 | -------------------------------------------------------------------------------- /test/back-instance-mocks/mock-module2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | createModule.moduleName='mock2'; 3 | createModule.$inject=[]; 4 | module.exports=createModule; 5 | createModule.func=function(){};//overrriden in tests 6 | function createModule(){createModule.args=arguments;createModule.func();return {};} 7 | -------------------------------------------------------------------------------- /test/back-instance-mocks/mock-module3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | createModule.moduleName='mock3'; 3 | createModule.$inject=[]; 4 | module.exports=createModule; 5 | createModule.func=function(){};//overrriden in tests 6 | function createModule(){createModule.args=arguments;createModule.func();return {};} 7 | -------------------------------------------------------------------------------- /test/backend-instance.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var chai= require('chai'); 5 | var expect = chai.expect; 6 | var path= require('path'); 7 | 8 | var backendInstance= require('../src/backend-instance'); 9 | 10 | 11 | var mockDir=path.normalize(__dirname +'/back-instance-mocks'); 12 | var modulesPath=[ 13 | path.normalize(mockDir+'/mock-module1.js'), 14 | path.normalize(mockDir+'/mock-module2.js'), 15 | path.normalize(mockDir+'/mock-module3.js') 16 | ]; 17 | function clearCache() 18 | { 19 | delete require.cache[modulesPath[0]]; 20 | delete require.cache[modulesPath[1]]; 21 | delete require.cache[modulesPath[2]]; 22 | } 23 | 24 | describe('backend instance', function(){ 25 | 26 | describe('modules load dependencies', function(){ 27 | 28 | beforeEach(clearCache); 29 | 30 | it('load according to order', function(){ 31 | var loadOrder=[]; 32 | var mock1=require(modulesPath[0]); 33 | var mock2=require(modulesPath[1]); 34 | var mock3=require(modulesPath[2]); 35 | mock1.func=function(){loadOrder.push(1);return {};}; 36 | mock2.func=function(){loadOrder.push(2);return {};}; 37 | mock3.func=function(){loadOrder.push(3);return {};}; 38 | mock2.$inject=[mock1.moduleName]; 39 | mock1.$inject=[mock3.moduleName]; 40 | 41 | var inst=backendInstance.create(path.normalize(__dirname+'/mock-ndfile.js')); 42 | inst.registerModule(mockDir+'/mock-module1'); 43 | inst.registerModule(mockDir+'/mock-module2'); 44 | inst.registerModule(mockDir+'/mock-module3'); 45 | return inst.startLoad().then(function(){ 46 | 47 | expect(loadOrder).to.deep.equal([3,1,2]); 48 | 49 | }); 50 | 51 | 52 | }); 53 | 54 | it('should get dependencies', function(){ 55 | 56 | //as ugly and cryptic as it gets... todo: clean 57 | 58 | var somespecialobject1={'im':'special1'}; 59 | var somespecialobject2={'im':'special2'}; 60 | var somespecialobject3={'im':'special3'}; 61 | 62 | 63 | var mock1Args; 64 | var mock1b=function(){ 65 | mock1Args=arguments; 66 | return somespecialobject1;}; 67 | var mock2b=function(){return somespecialobject2;}; 68 | mock2b.moduleName='mock2b'; 69 | var mock3b=function(){return somespecialobject3;}; 70 | mock3b.moduleName='mock3b'; 71 | mock1b.$inject=[mock2b.moduleName,mock3b.moduleName]; 72 | 73 | var inst=backendInstance.create(path.normalize(__dirname+'/mock-ndfile.js')); 74 | inst.registerModule(mock1b); 75 | inst.registerModule(mock2b); 76 | inst.registerModule(mock3b); 77 | return inst.startLoad().then(function(){ 78 | 79 | expect(mock1Args[0]).to.be.equal(somespecialobject2); 80 | expect(mock1Args[1]).to.be.equal(somespecialobject3); 81 | 82 | }); 83 | }); 84 | 85 | 86 | 87 | it('should fail on cyclic depend', function(){ 88 | var mock1=require(modulesPath[0]); 89 | var mock2=require(modulesPath[1]); 90 | var mock3=require(modulesPath[2]); 91 | 92 | 93 | 94 | mock1.createModule=function(){return 1;}; 95 | mock2.createModule=function(){return 2;}; 96 | mock3.createModule=function(){return 3;}; 97 | mock1.$inject=[mock2.moduleName]; 98 | mock2.$inject=[mock3.moduleName]; 99 | mock3.$inject=[mock1.moduleName]; 100 | 101 | var inst=backendInstance.create(path.normalize(__dirname+'/mock-ndfile.js')); 102 | inst.registerModule(modulesPath[0]); 103 | inst.registerModule(modulesPath[1]); 104 | inst.registerModule(modulesPath[2]); 105 | expect(inst.startLoad).to.throw(Error);//jshint ignore:line 106 | 107 | }); 108 | }); 109 | 110 | 111 | 112 | }); 113 | -------------------------------------------------------------------------------- /test/core-modules/config/config.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //var BBPromise= require('bluebird'); 4 | //var path= require('path'); 5 | 6 | var chai= require('chai'); 7 | var chaiAsPromised = require('chai-as-promised'); 8 | chai.use(chaiAsPromised); 9 | var expect = chai.expect; 10 | 11 | var moduleReq= require(__dirname+'/../../../src/core-modules/config/config'); 12 | 13 | 14 | 15 | describe('config module', function(){ 16 | it('name should be $config', function(){ 17 | expect(moduleReq.moduleName).to.equal('$config'); 18 | }); 19 | 20 | 21 | }); 22 | -------------------------------------------------------------------------------- /test/core-modules/job/copymock-ndfile.js: -------------------------------------------------------------------------------- 1 | module.exports=function(ndjs) 2 | { 3 | 'use strict'; 4 | ndjs.initConfig( 5 | { 6 | 'options': 7 | { 8 | 'frontend':'front', 9 | 'backend':'back', 10 | 'outgoing':'outgoing' 11 | 12 | }, 13 | 'install': 14 | { 15 | 'pages': 16 | [ 17 | {'name':'welcome','type':'custom'} 18 | ], 19 | 'jobs': 20 | { 21 | main:{'type':'multi','settings':{'subJobs':[ 22 | {'type':'copy', 23 | 'settings':{'files':[ 24 | {'from':'<%=user.source%>/a','to':'<%=user.target%>/target-a'}, 25 | {'from':'<%=user.source%>/b','to':'<%=user.target%>/target-b'} 26 | ]}} 27 | ]}} 28 | } 29 | } 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /test/core-modules/job/job.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var BBPromise= require('bluebird'); 4 | var path= require('path'); 5 | 6 | var chai= require('chai'); 7 | var fs= require('fs-extra'); 8 | //var chaiAsPromised = require('chai-as-promised'); 9 | //chai.use(chaiAsPromised); 10 | var expect = chai.expect; 11 | 12 | //var uiActions= require(__dirname+'/../../../src/core-modules/job/job'); 13 | var backendInstance= require('../../../src/backend-instance'); 14 | 15 | 16 | var $job; 17 | var $notifications; 18 | var tempPath=path.normalize(__dirname+'../../../../.tmp'); 19 | function initMock(done) 20 | { 21 | var backInst=backendInstance.create(path.normalize(__dirname+'/copymock-ndfile.js')); 22 | backInst.startLoad().then(function(){ 23 | $job=backInst.getModule('$job'); 24 | $notifications=backInst.getModule('$notifications'); 25 | backInst.getModule('$state').setSettings('user.source',tempPath+'/source'); 26 | backInst.getModule('$state').setSettings('user.target',tempPath+'/target'); 27 | done(); 28 | }); 29 | 30 | } 31 | 32 | function deleteTempDirs(done) 33 | { 34 | console.log('starting deleteTempDirs'); 35 | BBPromise.promisify(fs.emptyDir)(tempPath) 36 | .then(function(){console.log('finished deleteTempDirs');}).then(done); 37 | } 38 | //create a source dir and target dir with some files in the source so we could run 39 | //the mock self extract job on in(it should copy them to the target) 40 | function createTempDirsWithSources(done) 41 | { 42 | console.log('starting createTempDirsWithSources'); 43 | BBPromise.promisify(fs.ensureDir)(tempPath).then( 44 | function() 45 | { 46 | return BBPromise.promisify(fs.ensureDir)(path.join(tempPath,'target')); 47 | } 48 | ).then( 49 | function() 50 | { 51 | return BBPromise.promisify(fs.ensureDir)(path.join(tempPath,'source','a')); 52 | } 53 | ).then( 54 | function() 55 | { 56 | return BBPromise.promisify(fs.ensureDir)(path.join(tempPath,'source','b')); 57 | } 58 | ).then( 59 | function() 60 | { 61 | return BBPromise.promisify(fs.writeFile)(path.join(tempPath,'source','a','file1.txt'),'file a1'); 62 | } 63 | ).then( 64 | function() 65 | { 66 | return BBPromise.promisify(fs.writeFile)(path.join(tempPath,'source','a','file2.txt'),'file a2'); 67 | } 68 | ).then( 69 | function() 70 | { 71 | return BBPromise.promisify(fs.writeFile)(path.join(tempPath,'source','a','file2.txt'),'file a2'); 72 | } 73 | ).then( 74 | function() 75 | { 76 | return BBPromise.promisify(fs.writeFile)(path.join(tempPath,'source','b','file1.txt'),'file b1'); 77 | } 78 | ) 79 | .then(function(){console.log('finished createTempDirsWithSources');}).then(done); 80 | 81 | } 82 | describe('job module', function(){ 83 | beforeEach(initMock); 84 | describe('multi job',function() 85 | { 86 | 87 | it('should be able to manually run', function(){ 88 | var testStr=''; 89 | var emptyFunc=function(){}; 90 | var customJobCreate=function(settings) 91 | { 92 | return new BBPromise(function(resolve) 93 | { 94 | void resolve; 95 | testStr+=settings.str; 96 | resolve('ok'); 97 | }); 98 | }; 99 | customJobCreate.jobType='custom'; 100 | $job.registerJobType(customJobCreate); 101 | $job.startJob('multi',{'subJobs':[ 102 | {'type':'custom','settings':{'str':'a'}}, 103 | {'type':'custom','settings':{'str':'b'}}, 104 | {'type':'custom','settings':{'str':'c'}} 105 | ]},emptyFunc,emptyFunc,emptyFunc).then(function() 106 | { 107 | expect(testStr).to.equal('abc'); 108 | }).error(function(){expect.fail('multi job did not run correctly');}); 109 | }); 110 | 111 | 112 | 113 | }); 114 | 115 | before(deleteTempDirs); 116 | before(createTempDirsWithSources); 117 | describe('sfx job',function() 118 | { 119 | 120 | it('on debug-mock should copy instead', function(){ 121 | var emptyFunc=function(){}; 122 | return $job.startJob('sfx',{'files':[ 123 | {'from':path.join(tempPath,'source','a'),'to':path.join(tempPath,'target','target-a')}, 124 | {'from':path.join(tempPath,'source','b'),'to':path.join(tempPath,'target','target-b')} 125 | ]},emptyFunc,emptyFunc,emptyFunc).then(function() 126 | { 127 | return BBPromise.promisify(fs.readFile)(path.join(tempPath,'target','target-a','file2.txt'),'utf8'); 128 | }).then(function(fileA2) 129 | { 130 | expect(fileA2).to.equal('file a2'); 131 | }).then(function() 132 | { 133 | return BBPromise.promisify(fs.readFile)(path.join(tempPath,'target','target-b','file1.txt'),'utf8'); 134 | }).then(function(fileA2) 135 | { 136 | expect(fileA2).to.equal('file b1'); 137 | }); 138 | }); 139 | }); 140 | 141 | 142 | before(deleteTempDirs); 143 | before(createTempDirsWithSources); 144 | describe('job pending and retrying',function() 145 | { 146 | it('should stop copy and wait for answer when no permission', function(){ 147 | 148 | var hasStopped=false; 149 | var hasRetriedFailed=false; 150 | var hasRetried2=false; 151 | 152 | var pendingId; 153 | var lockFilePath=path.join(tempPath,'target','target-a','file2.txt'); 154 | fs.ensureFileSync(lockFilePath); 155 | var openedFile = fs.openSync(lockFilePath,'w'); 156 | 157 | return new BBPromise(function (resolve) 158 | { 159 | var processNotif=function(notif) 160 | { 161 | if(notif.name==='job_pending') 162 | { 163 | pendingId=notif.value.pendingId; 164 | if(hasRetriedFailed) 165 | { 166 | hasRetried2=true; 167 | } 168 | else if(hasStopped) 169 | { 170 | hasRetriedFailed=true; 171 | fs.closeSync(openedFile); 172 | $job.releasePendingJob(pendingId,'retry'); 173 | } 174 | else 175 | { 176 | hasStopped=true; 177 | $job.releasePendingJob(pendingId,'retry'); 178 | } 179 | } 180 | else if(notif.name==='job_status') 181 | { 182 | if(notif.value.progress===1 && hasRetriedFailed && hasStopped) 183 | { 184 | expect(hasRetriedFailed && hasStopped && !hasRetried2).to.equal(true); 185 | resolve('ok'); 186 | } 187 | } 188 | }; 189 | $notifications.listenToNotifications(processNotif); 190 | $job.startNamedJob('main'); 191 | }); 192 | });}); 193 | 194 | 195 | before(deleteTempDirs); 196 | before(createTempDirsWithSources); 197 | describe('job pending and retrying',function() 198 | { 199 | it('should stop copy and wait for answer when no permission', function(){ 200 | 201 | var hasStopped=false; 202 | var hasRetriedFailed=false; 203 | var hasRetried2=false; 204 | 205 | var pendingId; 206 | var lockFilePath=path.join(tempPath,'target','target-a','file2.txt'); 207 | fs.ensureFileSync(lockFilePath); 208 | var openedFile = fs.openSync(lockFilePath,'w'); 209 | 210 | return new BBPromise(function (resolve) 211 | { 212 | var processNotif=function(notif) 213 | { 214 | if(notif.name==='job_pending') 215 | { 216 | pendingId=notif.value.pendingId; 217 | if(hasRetriedFailed) 218 | { 219 | hasRetried2=true; 220 | } 221 | else if(hasStopped) 222 | { 223 | hasRetriedFailed=true; 224 | $job.releasePendingJob(pendingId,'ignore'); 225 | } 226 | else 227 | { 228 | hasStopped=true; 229 | $job.releasePendingJob(pendingId,'retry'); 230 | } 231 | } 232 | else if(notif.name==='job_status') 233 | { 234 | if(notif.value.progress===1 && hasRetriedFailed && hasStopped) 235 | { 236 | expect(hasRetriedFailed && hasStopped && !hasRetried2).to.equal(true); 237 | resolve('ok'); 238 | fs.closeSync(openedFile); 239 | } 240 | } 241 | }; 242 | $notifications.listenToNotifications(processNotif); 243 | $job.startNamedJob('main'); 244 | }); 245 | }); 246 | }); 247 | 248 | before(deleteTempDirs); 249 | before(createTempDirsWithSources); 250 | describe('job pending and aborting',function() 251 | { 252 | it('should stop copy and wait for answer when no permission', function(){ 253 | 254 | var hasStopped=false; 255 | var hasRetriedFailed=false; 256 | var hasRetried2=false; 257 | 258 | var pendingId; 259 | var lockFilePath=path.join(tempPath,'target','target-a','file2.txt'); 260 | fs.ensureFileSync(lockFilePath); 261 | var openedFile = fs.openSync(lockFilePath,'w'); 262 | 263 | return new BBPromise(function (resolve) 264 | { 265 | var processNotif=function(notif) 266 | { 267 | if(notif.name==='job_pending') 268 | { 269 | pendingId=notif.value.pendingId; 270 | if(hasRetriedFailed) 271 | { 272 | hasRetried2=true; 273 | } 274 | else if(hasStopped) 275 | { 276 | hasRetriedFailed=true; 277 | $job.releasePendingJob(pendingId,'abort'); 278 | } 279 | else 280 | { 281 | hasStopped=true; 282 | $job.releasePendingJob(pendingId,'retry'); 283 | } 284 | } 285 | else if(notif.name==='job_aborted') 286 | { 287 | if(hasRetriedFailed && hasStopped) 288 | { 289 | expect(hasRetriedFailed && hasStopped && !hasRetried2).to.equal(true); 290 | resolve('ok'); 291 | fs.closeSync(openedFile); 292 | } 293 | } 294 | }; 295 | $notifications.listenToNotifications(processNotif); 296 | $job.startNamedJob('main'); 297 | }); 298 | }); 299 | }); 300 | 301 | 302 | 303 | }); 304 | -------------------------------------------------------------------------------- /test/core-modules/notifications/notifications.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var BBPromise= require('bluebird'); 4 | 5 | var chai= require('chai'); 6 | var chaiAsPromised = require('chai-as-promised'); 7 | chai.use(chaiAsPromised); 8 | var expect = chai.expect; 9 | 10 | var uiActions= require(__dirname+'/../../../src/core-modules/notifications/notifications'); 11 | 12 | 13 | 14 | describe('notifications module', function(){ 15 | it('name should be $notifications', function(){ 16 | expect(uiActions.moduleName).to.equal('$notifications'); 17 | }); 18 | 19 | it('should register the get notificationsFromIdx', function(){ 20 | var run=null;//TODO:change to a sinon spy 21 | var uiactions={registerAction: 22 | function(name,argNames,callback) 23 | { 24 | void callback; 25 | expect(name).to.equal('notifications_getFromIdx'); 26 | expect(argNames).to.deep.equal(['idx']); 27 | run='all ok'; 28 | }}; 29 | var promise=BBPromise.resolve(uiActions(uiactions)).then( 30 | function() 31 | { 32 | return run; 33 | } 34 | ); 35 | 36 | return expect(promise).to.eventually.equal('all ok'); 37 | }); 38 | 39 | }); 40 | -------------------------------------------------------------------------------- /test/core-modules/pages/pages.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path= require('path'); 4 | 5 | var chai= require('chai'); 6 | var chaiAsPromised = require('chai-as-promised'); 7 | chai.use(chaiAsPromised); 8 | var expect = chai.expect; 9 | 10 | var backendInstance= require('../../../src/backend-instance'); 11 | 12 | var $pages; 13 | function initMock(done) 14 | { 15 | var backInst=backendInstance.create(path.normalize(__dirname+'../../../mock-ndfile.js')); 16 | backInst.startLoad().then(function(){ 17 | $pages=backInst.getModule('$pages'); 18 | }).then(done); 19 | 20 | } 21 | describe('pages module', function(){ 22 | 23 | before(initMock); 24 | it('should get all pages', function(){// break to several tests 25 | 26 | expect($pages.getPages()).to.deep.equal([ 27 | {'name':'welcome','type':'custom'}, 28 | {'name':'eula','type':'eula'}, 29 | {'name':'dir','type':'directory','settings':{'dir':'installDir'}}, 30 | {'name':'extract','type':'extract','settings':{'job':'main'}}, 31 | {'name':'conclusion','type':'custom'} 32 | ]); 33 | 34 | 35 | 36 | }); 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /test/core-modules/state/state.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var BBPromise= require('bluebird'); 4 | 5 | var chai= require('chai'); 6 | var chaiAsPromised = require('chai-as-promised'); 7 | chai.use(chaiAsPromised); 8 | var expect = chai.expect; 9 | 10 | var stateModule= require(__dirname+'/../../../src/core-modules/state/state'); 11 | 12 | 13 | 14 | describe('state module', function(){ 15 | it('name should be $notifications', function(){ 16 | expect(stateModule.moduleName).to.equal('$state'); 17 | }); 18 | 19 | it('should get and set nested properties with defualt values', function(){ 20 | var mockUiActions={registerAction:function(){}}; 21 | 22 | return BBPromise.resolve(stateModule(mockUiActions)).then( 23 | function(instance) 24 | { 25 | expect(instance.getSettings('user.abc.def','default val')).to.equal('default val'); 26 | instance.setSettings('user.abc.def','new val'); 27 | expect(instance.getSettings('user.abc.def','default val')).to.equal('new val'); 28 | expect(instance.getSettings('user',undefined)).to.deep.equal({abc:{def:'new val'}}); 29 | } 30 | ); 31 | 32 | }); 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /test/core-modules/ui-actions/ui-actions.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | var chai= require('chai'); 5 | var chaiAsPromised = require('chai-as-promised'); 6 | chai.use(chaiAsPromised); 7 | var expect = chai.expect; 8 | 9 | var uiActions= require(__dirname+'/../../../src/core-modules/ui-actions/ui-actions'); 10 | 11 | 12 | 13 | describe('ui actions module', function(){ 14 | it('name should be $uiActions', function(){ 15 | expect(uiActions.moduleName).to.equal('$uiActions'); 16 | }); 17 | 18 | it('actions number of arguments', function(){ 19 | var instance=uiActions(); 20 | instance.registerAction('someAction',['arg1'],function(){}); 21 | return expect(instance.callAction('someAction',[])).to.eventually.be.rejected; 22 | }); 23 | it('actions run with arguments and return promise', function(){ 24 | var run=false; 25 | var instance=uiActions(); 26 | var aAction=function(anArg,anotherArg) 27 | { 28 | if(anArg==='passedValue' && anotherArg==='passedValue2') 29 | { 30 | run='alright'; 31 | } 32 | }; 33 | instance.registerAction('someAction',['arg1','arg2'],aAction); 34 | return expect(instance.callAction('someAction',['passedValue','passedValue2']).then( 35 | function(){return run;} 36 | )).to.eventually.equal('alright'); 37 | 38 | }); 39 | 40 | }); 41 | -------------------------------------------------------------------------------- /test/core-modules/utils/test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | rand6832 6 | -------------------------------------------------------------------------------- /test/core-modules/utils/utils.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var BBPromise= require('bluebird'); 4 | var path= require('path'); 5 | var _= require('lodash'); 6 | 7 | var chai= require('chai'); 8 | var chaiAsPromised = require('chai-as-promised'); 9 | chai.use(chaiAsPromised); 10 | var expect = chai.expect; 11 | 12 | var utilsModule= require(__dirname+'/../../../src/core-modules/utils/utils'); 13 | var testXmlPath=path.normalize(__dirname+'/test.xml'); 14 | 15 | 16 | describe('utils module', function(){ 17 | it('name should be $utils', function(){ 18 | expect(utilsModule.moduleName).to.equal('$utils'); 19 | }); 20 | 21 | it('xml set props', function(){// a bit slow for and disk bound for a unit test 22 | var rand1='rand'+_.random(0, 10000); 23 | var rand2='rand'+_.random(0, 10000); 24 | return BBPromise.resolve(utilsModule()).then( 25 | function(instance) 26 | { 27 | return instance.setXmlFileProps(testXmlPath,{ 28 | '/somexml//change-my-prop/@prop':rand1, 29 | '//change-my-inner-text/text()':rand2 30 | }).then( 31 | function() 32 | { 33 | var newContent=instance.getFileContentSync(testXmlPath); 34 | expect(newContent.match(/prop=['"](.+)['"]/)[1]).to.equal(rand1); 35 | expect(newContent.match(/\s*(rand[0-9]+)\s*